diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index e6615bd..1b914d8 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-15T02:24:46Z"; + var SX_VERSION = "2026-03-15T02:29:18Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } diff --git a/shared/sx/ref/adapter-async.sx b/shared/sx/ref/adapter-async.sx deleted file mode 100644 index 0a87d5f..0000000 --- a/shared/sx/ref/adapter-async.sx +++ /dev/null @@ -1,1375 +0,0 @@ -;; ========================================================================== -;; adapter-async.sx — Async rendering and serialization adapter -;; -;; Async versions of adapter-html.sx (render) and adapter-sx.sx (aser) -;; for use with I/O-capable server environments (Python async, JS promises). -;; -;; Structurally identical to the sync adapters but uses async primitives: -;; async-eval — evaluate with I/O interception (platform primitive) -;; async-render — defined here, async HTML rendering -;; async-aser — defined here, async SX wire format -;; -;; All functions in this file are emitted as async by the bootstrapper. -;; Calls to other async functions receive await automatically. -;; -;; Depends on: -;; eval.sx — cond-scheme?, eval-cond-scheme, eval-cond-clojure, -;; expand-macro, env-merge, lambda?, component?, island?, -;; macro?, lambda-closure, lambda-params, lambda-body -;; render.sx — HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, -;; render-attrs, definition-form?, process-bindings, eval-cond -;; -;; Platform primitives (provided by host): -;; (async-eval expr env ctx) — evaluate with I/O interception -;; (io-primitive? name) — check if name is I/O primitive -;; (execute-io name args kw ctx) — execute an I/O primitive -;; (expand-components?) — context var: expand components in aser? -;; (svg-context?) — context var: in SVG rendering context? -;; (svg-context-set! val) — set SVG context -;; (svg-context-reset! token) — reset SVG context -;; (css-class-collect! val) — collect CSS classes for bundling -;; (is-raw-html? x) — check if value is raw HTML marker -;; (raw-html-content x) — extract HTML string from marker -;; (make-raw-html s) — wrap string as raw HTML -;; (async-coroutine? x) — check if value is a coroutine -;; (async-await! x) — await a coroutine value -;; ========================================================================== - - -;; -------------------------------------------------------------------------- -;; Async HTML renderer -;; -------------------------------------------------------------------------- - -(define-async async-render :effects [render io] - (fn (expr (env :as dict) ctx) - (case (type-of expr) - "nil" "" - "boolean" "" - "string" (escape-html expr) - "number" (escape-html (str expr)) - "raw-html" (raw-html-content expr) - "spread" (do (emit! "element-attrs" (spread-attrs expr)) "") - "symbol" (let ((val (async-eval expr env ctx))) - (async-render val env ctx)) - "keyword" (escape-html (keyword-name expr)) - "list" (if (empty? expr) "" (async-render-list expr env ctx)) - "dict" "" - :else (escape-html (str expr))))) - - -(define-async async-render-list :effects [render io] - (fn (expr (env :as dict) ctx) - (let ((head (first expr))) - (if (not (= (type-of head) "symbol")) - ;; Non-symbol head — data list, render each item - (if (or (lambda? head) (= (type-of head) "list")) - ;; Lambda/list call — eval then render - (async-render (async-eval expr env ctx) env ctx) - ;; Data list - (join "" (async-map-render expr env ctx))) - - ;; Symbol head — dispatch - (let ((name (symbol-name head)) - (args (rest expr))) - (cond - ;; I/O primitive - (io-primitive? name) - (async-render (async-eval expr env ctx) env ctx) - - ;; raw! - (= name "raw!") - (async-render-raw args env ctx) - - ;; Fragment - (= name "<>") - (join "" (async-map-render args env ctx)) - - ;; html: prefix - (starts-with? name "html:") - (async-render-element (slice name 5) args env ctx) - - ;; Render-aware special form (but check HTML tag + keyword first) - (async-render-form? name) - (if (and (contains? HTML_TAGS name) - (or (and (> (len expr) 1) (= (type-of (nth expr 1)) "keyword")) - (svg-context?))) - (async-render-element name args env ctx) - (dispatch-async-render-form name expr env ctx)) - - ;; Macro - (and (env-has? env name) (macro? (env-get env name))) - (async-render - (trampoline (expand-macro (env-get env name) args env)) - env ctx) - - ;; HTML tag - (contains? HTML_TAGS name) - (async-render-element name args env ctx) - - ;; Island (~name) - (and (starts-with? name "~") - (env-has? env name) - (island? (env-get env name))) - (async-render-island (env-get env name) args env ctx) - - ;; Component (~name) - (starts-with? name "~") - (let ((val (if (env-has? env name) (env-get env name) nil))) - (cond - (component? val) (async-render-component val args env ctx) - (macro? val) (async-render (trampoline (expand-macro val args env)) env ctx) - :else (async-render (async-eval expr env ctx) env ctx))) - - ;; Custom element (has - and keyword arg) - (and (> (index-of name "-") 0) - (> (len expr) 1) - (= (type-of (nth expr 1)) "keyword")) - (async-render-element name args env ctx) - - ;; SVG context - (svg-context?) - (async-render-element name args env ctx) - - ;; Fallback — eval then render - :else - (async-render (async-eval expr env ctx) env ctx))))))) - - -;; -------------------------------------------------------------------------- -;; async-render-raw — handle (raw! ...) in async context -;; -------------------------------------------------------------------------- - -(define-async async-render-raw :effects [render io] - (fn ((args :as list) (env :as dict) ctx) - (let ((parts (list))) - (for-each - (fn (arg) - (let ((val (async-eval arg env ctx))) - (cond - (is-raw-html? val) (append! parts (raw-html-content val)) - (= (type-of val) "string") (append! parts val) - (and (not (nil? val)) (not (= val false))) - (append! parts (str val))))) - args) - (join "" parts)))) - - -;; -------------------------------------------------------------------------- -;; async-render-element — render an HTML element with async arg evaluation -;; -------------------------------------------------------------------------- - -(define-async async-render-element :effects [render io] - (fn ((tag :as string) (args :as list) (env :as dict) ctx) - (let ((attrs (dict)) - (children (list))) - ;; Parse keyword attrs and children - (async-parse-element-args args attrs children env ctx) - ;; Collect CSS classes - (let ((class-val (dict-get attrs "class"))) - (when (and (not (nil? class-val)) (not (= class-val false))) - (css-class-collect! (str class-val)))) - (if (contains? VOID_ELEMENTS tag) - (str "<" tag (render-attrs attrs) ">") - ;; Provide scope for spread emit! - (let ((token (if (or (= tag "svg") (= tag "math")) - (svg-context-set! true) - nil)) - (content-parts (list))) - (scope-push! "element-attrs" nil) - (for-each - (fn (c) (append! content-parts (async-render c env ctx))) - children) - (for-each - (fn (spread-dict) (merge-spread-attrs attrs spread-dict)) - (emitted "element-attrs")) - (scope-pop! "element-attrs") - (when token (svg-context-reset! token)) - (str "<" tag (render-attrs attrs) ">" - (join "" content-parts) - "")))))) - - -;; -------------------------------------------------------------------------- -;; async-parse-element-args — parse :key val pairs + children, async eval -;; -------------------------------------------------------------------------- -;; Uses for-each + mutable state instead of reduce, because the bootstrapper -;; compiles inline for-each lambdas as for loops (which can contain await). - -(define-async async-parse-element-args :effects [render io] - (fn ((args :as list) (attrs :as dict) (children :as list) (env :as dict) ctx) - (let ((skip false) - (i 0)) - (for-each - (fn (arg) - (if skip - (do (set! skip false) - (set! i (inc i))) - (if (and (= (type-of arg) "keyword") - (< (inc i) (len args))) - (let ((val (async-eval (nth args (inc i)) env ctx))) - (dict-set! attrs (keyword-name arg) val) - (set! skip true) - (set! i (inc i))) - (do - (append! children arg) - (set! i (inc i)))))) - args)))) - - -;; -------------------------------------------------------------------------- -;; async-render-component — expand and render a component asynchronously -;; -------------------------------------------------------------------------- - -(define-async async-render-component :effects [render io] - (fn ((comp :as component) (args :as list) (env :as dict) ctx) - (let ((kwargs (dict)) - (children (list))) - ;; Parse keyword args and children - (async-parse-kw-args args kwargs children env ctx) - ;; Build env: closure + caller env + params - (let ((local (env-merge (component-closure comp) env))) - (for-each - (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) - (component-params comp)) - ;; Pre-render children to raw HTML - (when (component-has-children? comp) - (let ((parts (list))) - (for-each - (fn (c) (append! parts (async-render c env ctx))) - children) - (env-set! local "children" - (make-raw-html (join "" parts))))) - (async-render (component-body comp) local ctx))))) - - -;; -------------------------------------------------------------------------- -;; async-render-island — SSR render of reactive island with hydration markers -;; -------------------------------------------------------------------------- - -(define-async async-render-island :effects [render io] - (fn ((island :as island) (args :as list) (env :as dict) ctx) - (let ((kwargs (dict)) - (children (list))) - (async-parse-kw-args args kwargs children env ctx) - (let ((local (env-merge (component-closure island) env)) - (island-name (component-name island))) - (for-each - (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) - (component-params island)) - ;; Pre-render children - (when (component-has-children? island) - (let ((parts (list))) - (for-each - (fn (c) (append! parts (async-render c env ctx))) - children) - (env-set! local "children" - (make-raw-html (join "" parts))))) - (let ((body-html (async-render (component-body island) local ctx)) - (state-json (serialize-island-state kwargs))) - (str "" - body-html - "")))))) - - -;; -------------------------------------------------------------------------- -;; async-render-lambda — render lambda body in HTML context -;; -------------------------------------------------------------------------- - -(define-async async-render-lambda :effects [render io] - (fn ((f :as lambda) (args :as list) (env :as dict) ctx) - (let ((local (env-merge (lambda-closure f) env))) - (for-each-indexed - (fn (i p) (env-set! local p (nth args i))) - (lambda-params f)) - (async-render (lambda-body f) local ctx)))) - - -;; -------------------------------------------------------------------------- -;; async-parse-kw-args — parse keyword args and children with async eval -;; -------------------------------------------------------------------------- - -(define-async async-parse-kw-args :effects [render io] - (fn ((args :as list) (kwargs :as dict) (children :as list) (env :as dict) ctx) - (let ((skip false) - (i 0)) - (for-each - (fn (arg) - (if skip - (do (set! skip false) - (set! i (inc i))) - (if (and (= (type-of arg) "keyword") - (< (inc i) (len args))) - (let ((val (async-eval (nth args (inc i)) env ctx))) - (dict-set! kwargs (keyword-name arg) val) - (set! skip true) - (set! i (inc i))) - (do - (append! children arg) - (set! i (inc i)))))) - args)))) - - -;; -------------------------------------------------------------------------- -;; async-map-render — map async-render over a list, return list of strings -;; -------------------------------------------------------------------------- -;; Bootstrapper emits this as: [await async_render(x, env, ctx) for x in exprs] - -(define-async async-map-render :effects [render io] - (fn ((exprs :as list) (env :as dict) ctx) - (let ((results (list))) - (for-each - (fn (x) (append! results (async-render x env ctx))) - exprs) - results))) - - -;; -------------------------------------------------------------------------- -;; Render-aware form classification -;; -------------------------------------------------------------------------- - -(define ASYNC_RENDER_FORMS - (list "if" "when" "cond" "case" "let" "let*" "begin" "do" - "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" - "deftype" "defeffect" - "map" "map-indexed" "filter" "for-each" "scope" "provide")) - -(define async-render-form? :effects [] - (fn ((name :as string)) - (contains? ASYNC_RENDER_FORMS name))) - - -;; -------------------------------------------------------------------------- -;; dispatch-async-render-form — async special form rendering for HTML output -;; -------------------------------------------------------------------------- -;; -;; Uses cond-scheme? from eval.sx (the FIXED version with every? check) -;; and eval-cond from render.sx for correct scheme/clojure classification. - -(define-async dispatch-async-render-form :effects [render io] - (fn ((name :as string) expr (env :as dict) ctx) - (cond - ;; if - (= name "if") - (let ((cond-val (async-eval (nth expr 1) env ctx))) - (if cond-val - (async-render (nth expr 2) env ctx) - (if (> (len expr) 3) - (async-render (nth expr 3) env ctx) - ""))) - - ;; when — single body: pass through. Multi: join strings. - (= name "when") - (if (not (async-eval (nth expr 1) env ctx)) - "" - (if (= (len expr) 3) - (async-render (nth expr 2) env ctx) - (join "" (async-map-render (slice expr 2) env ctx)))) - - ;; cond — uses cond-scheme? (every? check) from eval.sx - (= name "cond") - (let ((clauses (rest expr))) - (if (cond-scheme? clauses) - (async-render-cond-scheme clauses env ctx) - (async-render-cond-clojure clauses env ctx))) - - ;; case - (= name "case") - (async-render (async-eval expr env ctx) env ctx) - - ;; let / let* — single body: pass through. Multi: join strings. - (or (= name "let") (= name "let*")) - (let ((local (async-process-bindings (nth expr 1) env ctx))) - (if (= (len expr) 3) - (async-render (nth expr 2) local ctx) - (join "" (async-map-render (slice expr 2) local ctx)))) - - ;; begin / do — single body: pass through. Multi: join strings. - (or (= name "begin") (= name "do")) - (if (= (len expr) 2) - (async-render (nth expr 1) env ctx) - (join "" (async-map-render (rest expr) env ctx))) - - ;; Definition forms - (definition-form? name) - (do (async-eval expr env ctx) "") - - ;; map - (= name "map") - (let ((f (async-eval (nth expr 1) env ctx)) - (coll (async-eval (nth expr 2) env ctx))) - (join "" (async-map-fn-render f coll env ctx))) - - ;; map-indexed - (= name "map-indexed") - (let ((f (async-eval (nth expr 1) env ctx)) - (coll (async-eval (nth expr 2) env ctx))) - (join "" (async-map-indexed-fn-render f coll env ctx))) - - ;; filter — eval fully then render - (= name "filter") - (async-render (async-eval expr env ctx) env ctx) - - ;; for-each (render variant) - (= name "for-each") - (let ((f (async-eval (nth expr 1) env ctx)) - (coll (async-eval (nth expr 2) env ctx))) - (join "" (async-map-fn-render f coll env ctx))) - - ;; scope — unified render-time dynamic scope - (= name "scope") - (let ((scope-name (async-eval (nth expr 1) env ctx)) - (rest-args (slice expr 2)) - (scope-val nil) - (body-exprs nil)) - ;; Check for :value keyword - (if (and (>= (len rest-args) 2) - (= (type-of (first rest-args)) "keyword") - (= (keyword-name (first rest-args)) "value")) - (do (set! scope-val (async-eval (nth rest-args 1) env ctx)) - (set! body-exprs (slice rest-args 2))) - (set! body-exprs rest-args)) - (scope-push! scope-name scope-val) - (let ((result (if (= (len body-exprs) 1) - (async-render (first body-exprs) env ctx) - (join "" (async-map-render body-exprs env ctx))))) - (scope-pop! scope-name) - result)) - - ;; provide — sugar for scope with value - (= name "provide") - (let ((prov-name (async-eval (nth expr 1) env ctx)) - (prov-val (async-eval (nth expr 2) env ctx)) - (body-start 3) - (body-count (- (len expr) 3))) - (scope-push! prov-name prov-val) - (let ((result (if (= body-count 1) - (async-render (nth expr body-start) env ctx) - (join "" (async-map-render (slice expr body-start) env ctx))))) - (scope-pop! prov-name) - result)) - - ;; Fallback - :else - (async-render (async-eval expr env ctx) env ctx)))) - - -;; -------------------------------------------------------------------------- -;; async-render-cond-scheme — scheme-style cond for render mode -;; -------------------------------------------------------------------------- - -(define-async async-render-cond-scheme :effects [render io] - (fn ((clauses :as list) (env :as dict) ctx) - (if (empty? clauses) - "" - (let ((clause (first clauses)) - (test (first clause)) - (body (nth clause 1))) - (if (or (and (= (type-of test) "symbol") - (or (= (symbol-name test) "else") - (= (symbol-name test) ":else"))) - (and (= (type-of test) "keyword") - (= (keyword-name test) "else"))) - (async-render body env ctx) - (if (async-eval test env ctx) - (async-render body env ctx) - (async-render-cond-scheme (rest clauses) env ctx))))))) - - -;; -------------------------------------------------------------------------- -;; async-render-cond-clojure — clojure-style cond for render mode -;; -------------------------------------------------------------------------- - -(define-async async-render-cond-clojure :effects [render io] - (fn ((clauses :as list) (env :as dict) ctx) - (if (< (len clauses) 2) - "" - (let ((test (first clauses)) - (body (nth clauses 1))) - (if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else")) - (and (= (type-of test) "symbol") - (or (= (symbol-name test) "else") - (= (symbol-name test) ":else")))) - (async-render body env ctx) - (if (async-eval test env ctx) - (async-render body env ctx) - (async-render-cond-clojure (slice clauses 2) env ctx))))))) - - -;; -------------------------------------------------------------------------- -;; async-process-bindings — evaluate let-bindings asynchronously -;; -------------------------------------------------------------------------- - -(define-async async-process-bindings :effects [render io] - (fn (bindings (env :as dict) ctx) - ;; env-extend (not merge) — Env is not a dict subclass, so merge() - ;; returns an empty dict, losing all parent scope bindings. - (let ((local (env-extend env))) - (if (and (= (type-of bindings) "list") (not (empty? bindings))) - (if (= (type-of (first bindings)) "list") - ;; Scheme-style: ((name val) ...) - (for-each - (fn (pair) - (when (and (= (type-of pair) "list") (>= (len pair) 2)) - (let ((name (if (= (type-of (first pair)) "symbol") - (symbol-name (first pair)) - (str (first pair))))) - (env-set! local name (async-eval (nth pair 1) local ctx))))) - bindings) - ;; Clojure-style: (name val name val ...) - (async-process-bindings-flat bindings local ctx))) - local))) - - -(define-async async-process-bindings-flat :effects [render io] - (fn ((bindings :as list) (local :as dict) ctx) - (let ((skip false) - (i 0)) - (for-each - (fn (item) - (if skip - (do (set! skip false) - (set! i (inc i))) - (do - (let ((name (if (= (type-of item) "symbol") - (symbol-name item) - (str item)))) - (when (< (inc i) (len bindings)) - (env-set! local name - (async-eval (nth bindings (inc i)) local ctx)))) - (set! skip true) - (set! i (inc i))))) - bindings)))) - - -;; -------------------------------------------------------------------------- -;; async-map-fn-render — map a lambda/callable over collection for render -;; -------------------------------------------------------------------------- - -(define-async async-map-fn-render :effects [render io] - (fn (f (coll :as list) (env :as dict) ctx) - (let ((results (list))) - (for-each - (fn (item) - (if (lambda? f) - (append! results (async-render-lambda f (list item) env ctx)) - (let ((r (async-invoke f item))) - (append! results (async-render r env ctx))))) - coll) - results))) - - -;; -------------------------------------------------------------------------- -;; async-map-indexed-fn-render — map-indexed variant for render -;; -------------------------------------------------------------------------- - -(define-async async-map-indexed-fn-render :effects [render io] - (fn (f (coll :as list) (env :as dict) ctx) - (let ((results (list)) - (i 0)) - (for-each - (fn (item) - (if (lambda? f) - (append! results (async-render-lambda f (list i item) env ctx)) - (let ((r (async-invoke f i item))) - (append! results (async-render r env ctx)))) - (set! i (inc i))) - coll) - results))) - - -;; -------------------------------------------------------------------------- -;; async-invoke — call a native callable, await if coroutine -;; -------------------------------------------------------------------------- - -(define-async async-invoke :effects [io] - (fn (f &rest args) - (let ((r (apply f args))) - (if (async-coroutine? r) - (async-await! r) - r)))) - - -;; ========================================================================== -;; Async SX wire format (aser) -;; ========================================================================== - -(define-async async-aser :effects [render io] - (fn (expr (env :as dict) ctx) - (let ((t (type-of expr)) - (result nil)) - (cond - (= t "number") (set! result expr) - (= t "string") (set! result expr) - (= t "boolean") (set! result expr) - (= t "nil") (set! result nil) - (= t "symbol") - (let ((name (symbol-name expr))) - (set! result - (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))))) - (= t "keyword") (set! result (keyword-name expr)) - (= t "dict") (set! result (async-aser-dict expr env ctx)) - ;; Spread — emit attrs to nearest element provider - (= t "spread") (do (emit! "element-attrs" (spread-attrs expr)) - (set! result nil)) - (= t "list") (set! result (if (empty? expr) (list) (async-aser-list expr env ctx))) - :else (set! result expr)) - ;; Catch spread values from function calls and symbol lookups - (if (spread? result) - (do (emit! "element-attrs" (spread-attrs result)) nil) - result)))) - - -(define-async async-aser-dict :effects [render io] - (fn ((expr :as dict) (env :as dict) ctx) - (let ((result (dict))) - (for-each - (fn (key) - (dict-set! result key (async-aser (dict-get expr key) env ctx))) - (keys expr)) - result))) - - -;; -------------------------------------------------------------------------- -;; async-aser-list — dispatch on list head for aser mode -;; -------------------------------------------------------------------------- - -(define-async async-aser-list :effects [render io] - (fn (expr (env :as dict) ctx) - (let ((head (first expr)) - (args (rest expr))) - (if (not (= (type-of head) "symbol")) - ;; Non-symbol head - (if (or (lambda? head) (= (type-of head) "list")) - ;; Function/list call — eval fully - (async-aser-eval-call head args env ctx) - ;; Data list — aser each - (async-aser-map-list expr env ctx)) - - ;; Symbol head — dispatch - (let ((name (symbol-name head))) - (cond - ;; I/O primitive - (io-primitive? name) - (async-eval expr env ctx) - - ;; Fragment - (= name "<>") - (async-aser-fragment args env ctx) - - ;; raw! - (= name "raw!") - (async-aser-call "raw!" args env ctx) - - ;; html: prefix - (starts-with? name "html:") - (async-aser-call (slice name 5) args env ctx) - - ;; Component call (~name) - (starts-with? name "~") - (let ((val (if (env-has? env name) (env-get env name) nil))) - (cond - (macro? val) - (async-aser (trampoline (expand-macro val args env)) env ctx) - (and (component? val) - (or (expand-components?) - (= (component-affinity val) "server"))) - (async-aser-component val args env ctx) - :else - (async-aser-call name args env ctx))) - - ;; Special/HO forms - (or (async-aser-form? name)) - (if (and (contains? HTML_TAGS name) - (or (and (> (len expr) 1) (= (type-of (nth expr 1)) "keyword")) - (svg-context?))) - (async-aser-call name args env ctx) - (dispatch-async-aser-form name expr env ctx)) - - ;; HTML tag - (contains? HTML_TAGS name) - (async-aser-call name args env ctx) - - ;; Macro - (and (env-has? env name) (macro? (env-get env name))) - (async-aser (trampoline (expand-macro (env-get env name) args env)) env ctx) - - ;; Custom element - (and (> (index-of name "-") 0) - (> (len expr) 1) - (= (type-of (nth expr 1)) "keyword")) - (async-aser-call name args env ctx) - - ;; SVG context - (svg-context?) - (async-aser-call name args env ctx) - - ;; Fallback — function/lambda call - :else - (async-aser-eval-call head args env ctx))))))) - - -;; -------------------------------------------------------------------------- -;; async-aser-eval-call — evaluate a function call fully in aser mode -;; -------------------------------------------------------------------------- - -(define-async async-aser-eval-call :effects [render io] - (fn (head (args :as list) (env :as dict) ctx) - (let ((f (async-eval head env ctx)) - (evaled-args (async-eval-args args env ctx))) - (cond - (and (callable? f) (not (lambda? f)) (not (component? f))) - ;; apply directly — async-invoke takes &rest so passing a list - ;; would wrap it in another list - (let ((r (apply f evaled-args))) - (if (async-coroutine? r) (async-await! r) r)) - (lambda? f) - (let ((local (env-merge (lambda-closure f) env))) - (for-each-indexed - (fn (i p) (env-set! local p (nth evaled-args i))) - (lambda-params f)) - (async-aser (lambda-body f) local ctx)) - (component? f) - (async-aser-call (str "~" (component-name f)) args env ctx) - (island? f) - (async-aser-call (str "~" (component-name f)) args env ctx) - :else - (error (str "Not callable: " (inspect f))))))) - - -;; -------------------------------------------------------------------------- -;; async-eval-args — evaluate a list of args asynchronously -;; -------------------------------------------------------------------------- - -(define-async async-eval-args :effects [io] - (fn ((args :as list) (env :as dict) ctx) - (let ((results (list))) - (for-each - (fn (a) (append! results (async-eval a env ctx))) - args) - results))) - - -;; -------------------------------------------------------------------------- -;; async-aser-map-list — aser each element of a list -;; -------------------------------------------------------------------------- - -(define-async async-aser-map-list :effects [render io] - (fn ((exprs :as list) (env :as dict) ctx) - (let ((results (list))) - (for-each - (fn (x) (append! results (async-aser x env ctx))) - exprs) - results))) - - -;; -------------------------------------------------------------------------- -;; async-aser-fragment — serialize (<> child1 child2 ...) in aser mode -;; -------------------------------------------------------------------------- - -(define-async async-aser-fragment :effects [render io] - (fn ((children :as list) (env :as dict) ctx) - (let ((parts (list))) - (for-each - (fn (c) - (let ((result (async-aser c env ctx))) - (if (= (type-of result) "list") - (for-each - (fn (item) - (when (not (nil? item)) - (append! parts (serialize item)))) - result) - (when (not (nil? result)) - (append! parts (serialize result)))))) - children) - (if (empty? parts) - (make-sx-expr "") - (make-sx-expr (str "(<> " (join " " parts) ")")))))) - - -;; -------------------------------------------------------------------------- -;; async-aser-component — expand component server-side in aser mode -;; -------------------------------------------------------------------------- - -(define-async async-aser-component :effects [render io] - (fn ((comp :as component) (args :as list) (env :as dict) ctx) - (let ((kwargs (dict)) - (children (list))) - (async-parse-aser-kw-args args kwargs children env ctx) - (let ((local (env-merge (component-closure comp) env))) - (for-each - (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) - (component-params comp)) - (when (component-has-children? comp) - (let ((child-parts (list))) - (for-each - (fn (c) - (let ((result (async-aser c env ctx))) - (if (list? result) - (for-each - (fn (item) - (when (not (nil? item)) - (append! child-parts (serialize item)))) - result) - (when (not (nil? result)) - (append! child-parts (serialize result)))))) - children) - (env-set! local "children" - (make-sx-expr (str "(<> " (join " " child-parts) ")"))))) - (async-aser (component-body comp) local ctx))))) - - -;; -------------------------------------------------------------------------- -;; async-parse-aser-kw-args — parse keyword args for aser mode -;; -------------------------------------------------------------------------- - -(define-async async-parse-aser-kw-args :effects [render io] - (fn ((args :as list) (kwargs :as dict) (children :as list) (env :as dict) ctx) - (let ((skip false) - (i 0)) - (for-each - (fn (arg) - (if skip - (do (set! skip false) - (set! i (inc i))) - (if (and (= (type-of arg) "keyword") - (< (inc i) (len args))) - (let ((val (async-aser (nth args (inc i)) env ctx))) - (dict-set! kwargs (keyword-name arg) val) - (set! skip true) - (set! i (inc i))) - (do - (append! children arg) - (set! i (inc i)))))) - args)))) - - -;; -------------------------------------------------------------------------- -;; async-aser-call — serialize an SX call (tag or component) in aser mode -;; -------------------------------------------------------------------------- - -(define-async async-aser-call :effects [render io] - (fn ((name :as string) (args :as list) (env :as dict) ctx) - (let ((token (if (or (= name "svg") (= name "math")) - (svg-context-set! true) - nil)) - (attr-parts (list)) - (child-parts (list)) - (skip false) - (i 0)) - ;; Provide scope for spread emit! - (scope-push! "element-attrs" nil) - (for-each - (fn (arg) - (if skip - (do (set! skip false) - (set! i (inc i))) - (if (and (= (type-of arg) "keyword") - (< (inc i) (len args))) - (let ((val (async-aser (nth args (inc i)) env ctx))) - (when (not (nil? val)) - (append! attr-parts (str ":" (keyword-name arg))) - (if (= (type-of val) "list") - (let ((live (filter (fn (v) (not (nil? v))) val))) - (if (empty? live) - (append! attr-parts "nil") - (let ((items (map serialize live))) - (if (some (fn (v) (sx-expr? v)) live) - (append! attr-parts (str "(<> " (join " " items) ")")) - (append! attr-parts (str "(list " (join " " items) ")")))))) - (append! attr-parts (serialize val)))) - (set! skip true) - (set! i (inc i))) - (let ((result (async-aser arg env ctx))) - (when (not (nil? result)) - (if (= (type-of result) "list") - (for-each - (fn (item) - (when (not (nil? item)) - (append! child-parts (serialize item)))) - result) - (append! child-parts (serialize result)))) - (set! i (inc i)))))) - args) - ;; Collect emitted spread attrs — 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")) - (scope-pop! "element-attrs") - (when token (svg-context-reset! token)) - (let ((parts (concat (list name) attr-parts child-parts))) - (make-sx-expr (str "(" (join " " parts) ")")))))) - - -;; -------------------------------------------------------------------------- -;; Aser form classification -;; -------------------------------------------------------------------------- - -(define ASYNC_ASER_FORM_NAMES - (list "if" "when" "cond" "case" "and" "or" - "let" "let*" "lambda" "fn" - "define" "defcomp" "defmacro" "defstyle" - "defhandler" "defpage" "defquery" "defaction" - "begin" "do" "quote" "->" "set!" "defisland" - "deftype" "defeffect" "scope" "provide")) - -(define ASYNC_ASER_HO_NAMES - (list "map" "map-indexed" "filter" "for-each")) - -(define async-aser-form? :effects [] - (fn ((name :as string)) - (or (contains? ASYNC_ASER_FORM_NAMES name) - (contains? ASYNC_ASER_HO_NAMES name)))) - - -;; -------------------------------------------------------------------------- -;; dispatch-async-aser-form — evaluate special/HO forms in aser mode -;; -------------------------------------------------------------------------- -;; -;; Uses cond-scheme? from eval.sx (the FIXED version with every? check). - -(define-async dispatch-async-aser-form :effects [render io] - (fn ((name :as string) expr (env :as dict) ctx) - (let ((args (rest expr))) - (cond - ;; if - (= name "if") - (let ((cond-val (async-eval (first args) env ctx))) - (if cond-val - (async-aser (nth args 1) env ctx) - (if (> (len args) 2) - (async-aser (nth args 2) env ctx) - nil))) - - ;; when - (= name "when") - (if (not (async-eval (first args) env ctx)) - nil - (let ((result nil)) - (for-each - (fn (body) (set! result (async-aser body env ctx))) - (rest args)) - result)) - - ;; cond — uses cond-scheme? (every? check) - (= name "cond") - (if (cond-scheme? args) - (async-aser-cond-scheme args env ctx) - (async-aser-cond-clojure args env ctx)) - - ;; case - (= name "case") - (let ((match-val (async-eval (first args) env ctx))) - (async-aser-case-loop match-val (rest args) env ctx)) - - ;; let / let* - (or (= name "let") (= name "let*")) - (let ((local (async-process-bindings (first args) env ctx)) - (result nil)) - (for-each - (fn (body) (set! result (async-aser body local ctx))) - (rest args)) - result) - - ;; begin / do - (or (= name "begin") (= name "do")) - (let ((result nil)) - (for-each - (fn (body) (set! result (async-aser body env ctx))) - args) - result) - - ;; and — short-circuit via flag to avoid 'some' with async lambda - (= name "and") - (let ((result true) - (stop false)) - (for-each (fn (arg) - (when (not stop) - (set! result (async-eval arg env ctx)) - (when (not result) - (set! stop true)))) - args) - result) - - ;; or — short-circuit via flag to avoid 'some' with async lambda - (= name "or") - (let ((result false) - (stop false)) - (for-each (fn (arg) - (when (not stop) - (set! result (async-eval arg env ctx)) - (when result - (set! stop true)))) - args) - result) - - ;; lambda / fn - (or (= name "lambda") (= name "fn")) - (sf-lambda args env) - - ;; quote - (= name "quote") - (if (empty? args) nil (first args)) - - ;; -> thread-first - (= name "->") - (async-aser-thread-first args env ctx) - - ;; set! - (= name "set!") - (let ((value (async-eval (nth args 1) env ctx))) - (env-set! env (symbol-name (first args)) value) - value) - - ;; map - (= name "map") - (async-aser-ho-map args env ctx) - - ;; map-indexed - (= name "map-indexed") - (async-aser-ho-map-indexed args env ctx) - - ;; filter - (= name "filter") - (async-eval expr env ctx) - - ;; for-each - (= name "for-each") - (async-aser-ho-for-each args env ctx) - - ;; defisland — evaluate AND serialize - (= name "defisland") - (do (async-eval expr env ctx) - (serialize expr)) - - ;; Definition forms — evaluate for side effects - (or (= name "define") (= name "defcomp") (= name "defmacro") - (= name "defstyle") (= name "defhandler") (= name "defpage") - (= name "defquery") (= name "defaction") - (= name "deftype") (= name "defeffect")) - (do (async-eval expr env ctx) nil) - - ;; scope — unified render-time dynamic scope - (= name "scope") - (let ((scope-name (async-eval (first args) env ctx)) - (rest-args (rest args)) - (scope-val nil) - (body-args nil)) - ;; Check for :value keyword - (if (and (>= (len rest-args) 2) - (= (type-of (first rest-args)) "keyword") - (= (keyword-name (first rest-args)) "value")) - (do (set! scope-val (async-eval (nth rest-args 1) env ctx)) - (set! body-args (slice rest-args 2))) - (set! body-args rest-args)) - (scope-push! scope-name scope-val) - (let ((result nil)) - (for-each (fn (body) (set! result (async-aser body env ctx))) - body-args) - (scope-pop! scope-name) - result)) - - ;; provide — sugar for scope with value - (= name "provide") - (let ((prov-name (async-eval (first args) env ctx)) - (prov-val (async-eval (nth args 1) env ctx)) - (result nil)) - (scope-push! prov-name prov-val) - (for-each (fn (body) (set! result (async-aser body env ctx))) - (slice args 2)) - (scope-pop! prov-name) - result) - - ;; Fallback - :else - (async-eval expr env ctx))))) - - -;; -------------------------------------------------------------------------- -;; async-aser-cond-scheme — scheme-style cond for aser mode -;; -------------------------------------------------------------------------- - -(define-async async-aser-cond-scheme :effects [render io] - (fn ((clauses :as list) (env :as dict) ctx) - (if (empty? clauses) - nil - (let ((clause (first clauses)) - (test (first clause)) - (body (nth clause 1))) - (if (or (and (= (type-of test) "symbol") - (or (= (symbol-name test) "else") - (= (symbol-name test) ":else"))) - (and (= (type-of test) "keyword") - (= (keyword-name test) "else"))) - (async-aser body env ctx) - (if (async-eval test env ctx) - (async-aser body env ctx) - (async-aser-cond-scheme (rest clauses) env ctx))))))) - - -;; -------------------------------------------------------------------------- -;; async-aser-cond-clojure — clojure-style cond for aser mode -;; -------------------------------------------------------------------------- - -(define-async async-aser-cond-clojure :effects [render io] - (fn ((clauses :as list) (env :as dict) ctx) - (if (< (len clauses) 2) - nil - (let ((test (first clauses)) - (body (nth clauses 1))) - (if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else")) - (and (= (type-of test) "symbol") - (or (= (symbol-name test) "else") - (= (symbol-name test) ":else")))) - (async-aser body env ctx) - (if (async-eval test env ctx) - (async-aser body env ctx) - (async-aser-cond-clojure (slice clauses 2) env ctx))))))) - - -;; -------------------------------------------------------------------------- -;; async-aser-case-loop — case dispatch for aser mode -;; -------------------------------------------------------------------------- - -(define-async async-aser-case-loop :effects [render io] - (fn (match-val (clauses :as list) (env :as dict) ctx) - (if (< (len clauses) 2) - nil - (let ((test (first clauses)) - (body (nth clauses 1))) - (if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else")) - (and (= (type-of test) "symbol") - (or (= (symbol-name test) ":else") - (= (symbol-name test) "else")))) - (async-aser body env ctx) - (if (= match-val (async-eval test env ctx)) - (async-aser body env ctx) - (async-aser-case-loop match-val (slice clauses 2) env ctx))))))) - - -;; -------------------------------------------------------------------------- -;; async-aser-thread-first — -> form in aser mode -;; -------------------------------------------------------------------------- - -(define-async async-aser-thread-first :effects [render io] - (fn ((args :as list) (env :as dict) ctx) - (let ((result (async-eval (first args) env ctx))) - (for-each - (fn (form) - (if (= (type-of form) "list") - (let ((f (async-eval (first form) env ctx)) - (fn-args (cons result - (async-eval-args (rest form) env ctx)))) - (set! result (async-invoke-or-lambda f fn-args env ctx))) - (let ((f (async-eval form env ctx))) - (set! result (async-invoke-or-lambda f (list result) env ctx))))) - (rest args)) - result))) - - -;; -------------------------------------------------------------------------- -;; async-invoke-or-lambda — invoke a callable or lambda with args -;; -------------------------------------------------------------------------- - -(define-async async-invoke-or-lambda :effects [render io] - (fn (f (args :as list) (env :as dict) ctx) - (cond - (and (callable? f) (not (lambda? f)) (not (component? f))) - (let ((r (apply f args))) - (if (async-coroutine? r) - (async-await! r) - r)) - (lambda? f) - (let ((local (env-merge (lambda-closure f) env))) - (for-each-indexed - (fn (i p) (env-set! local p (nth args i))) - (lambda-params f)) - (async-eval (lambda-body f) local ctx)) - :else - (error (str "-> form not callable: " (inspect f)))))) - - -;; -------------------------------------------------------------------------- -;; Async aser HO forms (map, map-indexed, for-each) -;; -------------------------------------------------------------------------- - -(define-async async-aser-ho-map :effects [render io] - (fn ((args :as list) (env :as dict) ctx) - (let ((f (async-eval (first args) env ctx)) - (coll (async-eval (nth args 1) env ctx)) - (results (list))) - (for-each - (fn (item) - (if (lambda? f) - (let ((local (env-merge (lambda-closure f) env))) - (env-set! local (first (lambda-params f)) item) - (append! results (async-aser (lambda-body f) local ctx))) - (append! results (async-invoke f item)))) - coll) - results))) - - -(define-async async-aser-ho-map-indexed :effects [render io] - (fn ((args :as list) (env :as dict) ctx) - (let ((f (async-eval (first args) env ctx)) - (coll (async-eval (nth args 1) env ctx)) - (results (list)) - (i 0)) - (for-each - (fn (item) - (if (lambda? f) - (let ((local (env-merge (lambda-closure f) env))) - (env-set! local (first (lambda-params f)) i) - (env-set! local (nth (lambda-params f) 1) item) - (append! results (async-aser (lambda-body f) local ctx))) - (append! results (async-invoke f i item))) - (set! i (inc i))) - coll) - results))) - - -(define-async async-aser-ho-for-each :effects [render io] - (fn ((args :as list) (env :as dict) ctx) - (let ((f (async-eval (first args) env ctx)) - (coll (async-eval (nth args 1) env ctx)) - (results (list))) - (for-each - (fn (item) - (if (lambda? f) - (let ((local (env-merge (lambda-closure f) env))) - (env-set! local (first (lambda-params f)) item) - (append! results (async-aser (lambda-body f) local ctx))) - (append! results (async-invoke f item)))) - coll) - results))) - - -;; -------------------------------------------------------------------------- -;; async-eval-slot-inner — server-side slot expansion for aser mode -;; -------------------------------------------------------------------------- -;; -;; Coordinates component expansion for server-rendered pages: -;; 1. If expression is a direct component call (~name ...), expand it -;; 2. Otherwise aser the expression, then check if result is a (~...) -;; call that should be re-expanded -;; -;; Platform primitives required: -;; (sx-parse src) — parse SX source string -;; (make-sx-expr s) — wrap as SxExpr -;; (sx-expr? x) — check if SxExpr -;; (set-expand-components!) — enable component expansion context var - -(define-async async-eval-slot-inner :effects [render io] - (fn (expr (env :as dict) ctx) - ;; NOTE: Uses statement-form let + set! to avoid expression-context - ;; let (IIFE lambdas) which can't contain await in Python. - (let ((result nil)) - (if (and (list? expr) (not (empty? expr))) - (let ((head (first expr))) - (if (and (= (type-of head) "symbol") - (starts-with? (symbol-name head) "~")) - (let ((name (symbol-name head)) - (val (if (env-has? env name) (env-get env name) nil))) - (if (component? val) - (set! result (async-aser-component val (rest expr) env ctx)) - (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx)))) - (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx)))) - (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx))) - ;; Normalize result to SxExpr - (if (sx-expr? result) - result - (if (nil? result) - (make-sx-expr "") - (if (string? result) - (make-sx-expr result) - (make-sx-expr (serialize result)))))))) - - -(define-async async-maybe-expand-result :effects [render io] - (fn (result (env :as dict) ctx) - ;; If the aser result is a component call string like "(~foo ...)", - ;; re-parse and expand it. This handles indirect component references - ;; (e.g. a let binding that evaluates to a component call). - (let ((raw (if (sx-expr? result) - (trim (str result)) - (if (string? result) - (trim result) - nil)))) - (if (and raw (starts-with? raw "(~")) - (let ((parsed (sx-parse raw))) - (if (and parsed (not (empty? parsed))) - (async-eval-slot-inner (first parsed) env ctx) - result)) - result)))) - - -;; -------------------------------------------------------------------------- -;; Platform interface — async adapter -;; -------------------------------------------------------------------------- -;; -;; Async evaluation (provided by platform): -;; (async-eval expr env ctx) — evaluate with I/O interception -;; (execute-io name args kw ctx) — execute I/O primitive -;; (io-primitive? name) — check if name is I/O primitive -;; -;; From eval.sx: -;; cond-scheme?, eval-cond-scheme, eval-cond-clojure -;; eval-expr, trampoline, expand-macro, sf-lambda -;; env-has?, env-get, env-set!, env-merge -;; lambda?, component?, island?, macro?, callable? -;; lambda-closure, lambda-params, lambda-body -;; component-params, component-body, component-closure, -;; component-has-children?, component-name -;; inspect -;; -;; From render.sx: -;; HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS -;; render-attrs, definition-form?, cond-scheme? -;; escape-html, escape-attr, raw-html-content -;; -;; From adapter-html.sx: -;; serialize-island-state -;; -;; Context management (platform): -;; (expand-components?) — check if component expansion is enabled -;; (svg-context?) — check if in SVG context -;; (svg-context-set! val) — set SVG context (returns reset token) -;; (svg-context-reset! token) — reset SVG context -;; (css-class-collect! val) — collect CSS classes -;; -;; Spread + collect (from render.sx): -;; (spread? x) — check if spread value -;; (spread-attrs s) — extract attrs dict from spread -;; (merge-spread-attrs tgt src) — merge spread attrs onto target -;; (collect! bucket value) — add to render-time accumulator -;; (collected bucket) — read render-time accumulator -;; (clear-collected! bucket) — clear accumulator -;; -;; Raw HTML: -;; (is-raw-html? x) — check if raw HTML marker -;; (make-raw-html s) — wrap string as raw HTML -;; (raw-html-content x) — unwrap raw HTML -;; -;; SxExpr: -;; (make-sx-expr s) — wrap as SxExpr (wire format string) -;; (sx-expr? x) — check if SxExpr -;; -;; Async primitives: -;; (async-coroutine? x) — check if value is a coroutine -;; (async-await! x) — await a coroutine -;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx deleted file mode 100644 index 7feffd0..0000000 --- a/shared/sx/ref/adapter-dom.sx +++ /dev/null @@ -1,1314 +0,0 @@ -;; ========================================================================== -;; adapter-dom.sx — DOM rendering adapter -;; -;; Renders SX expressions to live DOM nodes. Browser-only. -;; Mirrors the render-to-html adapter but produces Element/Text/Fragment -;; nodes instead of HTML strings. -;; -;; Depends on: -;; render.sx — HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, definition-form? -;; eval.sx — eval-expr, trampoline, call-component, expand-macro -;; ========================================================================== - -(define SVG_NS "http://www.w3.org/2000/svg") -(define MATH_NS "http://www.w3.org/1998/Math/MathML") - - -;; -------------------------------------------------------------------------- -;; render-to-dom — main entry point -;; -------------------------------------------------------------------------- - -(define render-to-dom :effects [render] - (fn (expr (env :as dict) (ns :as string)) - (set-render-active! true) - (case (type-of expr) - ;; nil / boolean false / boolean true → empty fragment - "nil" (create-fragment) - "boolean" (create-fragment) - - ;; Pre-rendered raw HTML → parse into fragment - "raw-html" (dom-parse-html (raw-html-content expr)) - - ;; String → text node - "string" (create-text-node expr) - - ;; Number → text node - "number" (create-text-node (str expr)) - - ;; Symbol → evaluate then render - "symbol" (render-to-dom (trampoline (eval-expr expr env)) env ns) - - ;; Keyword → text - "keyword" (create-text-node (keyword-name expr)) - - ;; Pre-rendered DOM node → pass through - "dom-node" expr - - ;; Spread → emit attrs to nearest element provider, pass through for reactive-spread - "spread" (do (emit! "element-attrs" (spread-attrs expr)) expr) - - ;; Dict → empty - "dict" (create-fragment) - - ;; List → dispatch - "list" - (if (empty? expr) - (create-fragment) - (render-dom-list expr env ns)) - - ;; Signal → reactive text in island scope, deref outside - :else - (if (signal? expr) - (if (context "sx-island-scope" nil) - (reactive-text expr) - (create-text-node (str (deref expr)))) - (create-text-node (str expr)))))) - - -;; -------------------------------------------------------------------------- -;; render-dom-list — dispatch on list head -;; -------------------------------------------------------------------------- - -(define render-dom-list :effects [render] - (fn (expr (env :as dict) (ns :as string)) - (let ((head (first expr))) - (cond - ;; Symbol head — dispatch on name - (= (type-of head) "symbol") - (let ((name (symbol-name head)) - (args (rest expr))) - (cond - ;; raw! → insert unescaped HTML - (= name "raw!") - (render-dom-raw args env) - - ;; <> → fragment - (= name "<>") - (render-dom-fragment args env ns) - - ;; lake — server-morphable slot within an island - (= name "lake") - (render-dom-lake args env ns) - - ;; marsh — reactive server-morphable slot within an island - (= name "marsh") - (render-dom-marsh args env ns) - - ;; html: prefix → force element rendering - (starts-with? name "html:") - (render-dom-element (slice name 5) args env ns) - - ;; Render-aware special forms - (render-dom-form? name) - (if (and (contains? HTML_TAGS name) - (or (and (> (len args) 0) - (= (type-of (first args)) "keyword")) - ns)) - ;; Ambiguous: tag name that's also a form — treat as tag - ;; when keyword arg or namespace present - (render-dom-element name args env ns) - (dispatch-render-form name expr env ns)) - - ;; Macro expansion - (and (env-has? env name) (macro? (env-get env name))) - (render-to-dom - (expand-macro (env-get env name) args env) - env ns) - - ;; HTML tag - (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))) - (if (component? comp) - (render-dom-component comp args env ns) - (render-dom-unknown-component name))) - - ;; Custom element (hyphenated with keyword attrs) - (and (> (index-of name "-") 0) - (> (len args) 0) - (= (type-of (first args)) "keyword")) - (render-dom-element name args env ns) - - ;; Inside SVG/MathML namespace — treat as element - ns - (render-dom-element name args env ns) - - ;; deref in island scope → reactive text node - (and (= name "deref") (context "sx-island-scope" nil)) - (let ((sig-or-val (trampoline (eval-expr (first args) env)))) - (if (signal? sig-or-val) - (reactive-text sig-or-val) - (create-text-node (str (deref sig-or-val))))) - - ;; Fallback — evaluate then render - :else - (render-to-dom (trampoline (eval-expr expr env)) env ns))) - - ;; Lambda or list head → evaluate - (or (lambda? head) (= (type-of head) "list")) - (render-to-dom (trampoline (eval-expr expr env)) env ns) - - ;; Data list - :else - (let ((frag (create-fragment))) - (for-each (fn (x) - (let ((result (render-to-dom x env ns))) - (when (not (spread? result)) - (dom-append frag result)))) - expr) - frag))))) - - -;; -------------------------------------------------------------------------- -;; render-dom-element — create a DOM element with attrs and children -;; -------------------------------------------------------------------------- - -(define render-dom-element :effects [render] - (fn ((tag :as string) (args :as list) (env :as dict) (ns :as string)) - ;; Detect namespace from tag - (let ((new-ns (cond (= tag "svg") SVG_NS - (= tag "math") MATH_NS - :else ns)) - (el (dom-create-element tag new-ns))) - - ;; Provide scope for spread emit! — deeply nested spreads emit here - (scope-push! "element-attrs" nil) - - ;; Process args: keywords → attrs, others → children - (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))) - ;; Keyword arg → attribute - (let ((attr-name (keyword-name arg)) - (attr-expr (nth args (inc (get state "i"))))) - (cond - ;; Event handler: evaluate eagerly, bind listener - (starts-with? attr-name "on-") - (let ((attr-val (trampoline (eval-expr attr-expr env)))) - (when (callable? attr-val) - (dom-listen el (slice attr-name 3) attr-val))) - ;; Two-way input binding: :bind signal - (= attr-name "bind") - (let ((attr-val (trampoline (eval-expr attr-expr env)))) - (when (signal? attr-val) (bind-input el attr-val))) - ;; ref: set ref.current to this element - (= attr-name "ref") - (let ((attr-val (trampoline (eval-expr attr-expr env)))) - (dict-set! attr-val "current" el)) - ;; key: reconciliation hint, evaluate eagerly (not reactive) - (= attr-name "key") - (let ((attr-val (trampoline (eval-expr attr-expr env)))) - (dom-set-attr el "key" (str attr-val))) - ;; Inside island scope: reactive attribute binding. - ;; The effect tracks signal deps automatically — if none - ;; are deref'd, it fires once and never again (safe). - (context "sx-island-scope" nil) - (reactive-attr el attr-name - (fn () (trampoline (eval-expr attr-expr env)))) - ;; Static attribute (outside islands) - :else - (let ((attr-val (trampoline (eval-expr attr-expr env)))) - (cond - (or (nil? attr-val) (= attr-val false)) nil - (contains? BOOLEAN_ATTRS attr-name) - (when attr-val (dom-set-attr el attr-name "")) - (= attr-val true) - (dom-set-attr el attr-name "") - :else - (dom-set-attr el attr-name (str attr-val))))) - (assoc state "skip" true "i" (inc (get state "i")))) - - ;; Positional arg → child (or spread → merge attrs onto element) - (do - (when (not (contains? VOID_ELEMENTS tag)) - (let ((child (render-to-dom arg env new-ns))) - (cond - ;; Reactive spread: track signal deps, update attrs on change - (and (spread? child) (context "sx-island-scope" nil)) - (reactive-spread el (fn () (render-to-dom arg env new-ns))) - ;; Static spread: already emitted via provide, skip - (spread? child) nil - ;; Normal child: append to element - :else - (dom-append el child)))) - (assoc state "i" (inc (get state "i")))))))) - (dict "i" 0 "skip" false) - args) - - ;; Collect emitted spread attrs and merge onto DOM element - (for-each - (fn (spread-dict) - (for-each - (fn ((key :as string)) - (let ((val (dict-get spread-dict key))) - (if (= key "class") - (let ((existing (dom-get-attr el "class"))) - (dom-set-attr el "class" - (if (and existing (not (= existing ""))) - (str existing " " val) - val))) - (if (= key "style") - (let ((existing (dom-get-attr el "style"))) - (dom-set-attr el "style" - (if (and existing (not (= existing ""))) - (str existing ";" val) - val))) - (dom-set-attr el key (str val)))))) - (keys spread-dict))) - (emitted "element-attrs")) - (scope-pop! "element-attrs") - - el))) - - -;; -------------------------------------------------------------------------- -;; render-dom-component — expand and render a component -;; -------------------------------------------------------------------------- - -(define render-dom-component :effects [render] - (fn ((comp :as component) (args :as list) (env :as dict) (ns :as string)) - ;; Parse kwargs and children, bind into component env, render body. - (let ((kwargs (dict)) - (children (list))) - ;; Separate keyword args from positional children - (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))) - ;; Keyword arg — evaluate in caller's env - (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 component env: closure + caller env + params - (let ((local (env-merge (component-closure comp) env))) - ;; Bind params from kwargs - (for-each - (fn (p) - (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) - (component-params comp)) - - ;; If component accepts children, pre-render them to a fragment - ;; Spread values are filtered out (no parent element to merge onto) - (when (component-has-children? comp) - (let ((child-frag (create-fragment))) - (for-each - (fn (c) - (let ((result (render-to-dom c env ns))) - (when (not (spread? result)) - (dom-append child-frag result)))) - children) - (env-set! local "children" child-frag))) - - (render-to-dom (component-body comp) local ns))))) - - -;; -------------------------------------------------------------------------- -;; render-dom-fragment — render children into a DocumentFragment -;; -------------------------------------------------------------------------- - -(define render-dom-fragment :effects [render] - (fn ((args :as list) (env :as dict) (ns :as string)) - (let ((frag (create-fragment))) - (for-each - (fn (x) - (let ((result (render-to-dom x env ns))) - (when (not (spread? result)) - (dom-append frag result)))) - args) - frag))) - - -;; -------------------------------------------------------------------------- -;; render-dom-raw — insert unescaped content -;; -------------------------------------------------------------------------- - -(define render-dom-raw :effects [render] - (fn ((args :as list) (env :as dict)) - (let ((frag (create-fragment))) - (for-each - (fn (arg) - (let ((val (trampoline (eval-expr arg env)))) - (cond - (= (type-of val) "string") - (dom-append frag (dom-parse-html val)) - (= (type-of val) "dom-node") - (dom-append frag (dom-clone val)) - (not (nil? val)) - (dom-append frag (create-text-node (str val)))))) - args) - frag))) - - -;; -------------------------------------------------------------------------- -;; render-dom-unknown-component — visible warning element -;; -------------------------------------------------------------------------- - -(define render-dom-unknown-component :effects [render] - (fn ((name :as string)) - (error (str "Unknown component: " name)))) - - -;; -------------------------------------------------------------------------- -;; Render-aware special forms for DOM output -;; -------------------------------------------------------------------------- -;; These forms need special handling in DOM rendering because they -;; produce DOM nodes rather than evaluated values. - -(define RENDER_DOM_FORMS - (list "if" "when" "cond" "case" "let" "let*" "begin" "do" - "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" - "map" "map-indexed" "filter" "for-each" "portal" - "error-boundary" "scope" "provide")) - -(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 - ;; if — reactive inside islands (re-renders when signal deps change) - (= name "if") - (if (context "sx-island-scope" nil) - (let ((marker (create-comment "r-if")) - (current-nodes (list)) - (initial-result nil)) - ;; Effect runs synchronously on first call, tracking signal deps. - ;; On first run, store result in initial-result (marker has no parent yet). - ;; On subsequent runs, swap DOM nodes after marker. - (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) - ;; Marker is in DOM — swap nodes - (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)) - ;; Marker not yet in DOM (first run) — just save result - (set! initial-result result))))) - ;; Spread pass-through: spreads aren't DOM nodes, can't live - ;; in fragments. Return directly so parent element merges attrs. - (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))) - ;; Static if - (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))))) - - ;; when — reactive inside islands - (= name "when") - (if (context "sx-island-scope" nil) - (let ((marker (create-comment "r-when")) - (current-nodes (list)) - (initial-result nil)) - (effect (fn () - (if (dom-parent marker) - ;; In DOM — swap nodes - (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)))) - ;; First run — save result for fragment - (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)))))) - ;; Spread pass-through - (if (spread? initial-result) - initial-result - (let ((frag (create-fragment))) - (dom-append frag marker) - (when initial-result (dom-append frag initial-result)) - frag))) - ;; Static when - (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))) - - ;; cond — reactive inside islands - (= name "cond") - (if (context "sx-island-scope" nil) - (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) - ;; In DOM — swap nodes - (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)))) - ;; First run — save result for fragment - (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))))))) - ;; Spread pass-through - (if (spread? initial-result) - initial-result - (let ((frag (create-fragment))) - (dom-append frag marker) - (when initial-result (dom-append frag initial-result)) - frag))) - ;; Static cond - (let ((branch (eval-cond (rest expr) env))) - (if branch - (render-to-dom branch env ns) - (create-fragment)))) - - ;; case - (= name "case") - (render-to-dom (trampoline (eval-expr expr env)) env ns) - - ;; let / let* — single body: pass through (spread propagates). Multi: fragment. - (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))) - - ;; begin / do — single body: pass through. Multi: fragment. - (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 forms — eval for side effects - (definition-form? name) - (do (trampoline (eval-expr expr env)) (create-fragment)) - - ;; map — reactive-list when mapping over a signal inside an island - (= name "map") - (let ((coll-expr (nth expr 2))) - (if (and (context "sx-island-scope" nil) - (= (type-of coll-expr) "list") - (> (len coll-expr) 1) - (= (type-of (first coll-expr)) "symbol") - (= (symbol-name (first coll-expr)) "deref")) - ;; Reactive path: pass signal to reactive-list - (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) - ;; deref on non-signal: fall through to static - (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))) - ;; Static path: no island scope or no deref - (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))) - - ;; map-indexed - (= 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) - - ;; filter — evaluate fully then render - (= name "filter") - (render-to-dom (trampoline (eval-expr expr env)) env ns) - - ;; portal — render children into a remote target element - (= name "portal") - (render-dom-portal (rest expr) env ns) - - ;; error-boundary — catch errors, render fallback - (= name "error-boundary") - (render-dom-error-boundary (rest expr) env ns) - - ;; for-each (render variant) - (= 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) - - ;; scope — unified render-time dynamic scope - (= 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))) - ;; Check for :value keyword - (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) - - ;; provide — sugar for scope with value - (= 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) - - ;; Fallback - :else - (render-to-dom (trampoline (eval-expr expr env)) env ns)))) - - -;; -------------------------------------------------------------------------- -;; render-lambda-dom — render a lambda body in DOM context -;; -------------------------------------------------------------------------- - -(define render-lambda-dom :effects [render] - (fn ((f :as lambda) (args :as list) (env :as dict) (ns :as string)) - ;; Bind lambda params and render body as DOM - (let ((local (env-merge (lambda-closure f) env))) - (for-each-indexed - (fn (i p) - (env-set! local p (nth args i))) - (lambda-params f)) - (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 :effects [render mutation] - (fn ((island :as island) (args :as list) (env :as dict) (ns :as string)) - ;; 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 "span" nil)) - (disposers (list))) - - ;; Mark as island + already hydrated (so boot.sx skips it) - (dom-set-attr container "data-sx-island" island-name) - (mark-processed! container "island-hydrated") - - ;; 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)))))) - - -;; -------------------------------------------------------------------------- -;; render-dom-lake — server-morphable slot within an island -;; -------------------------------------------------------------------------- -;; -;; (lake :id "name" children...) -;; -;; Renders as
children
. -;; 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 :effects [render] - (fn ((args :as list) (env :as dict) (ns :as string)) - (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)))) - - -;; -------------------------------------------------------------------------- -;; render-dom-marsh — reactive server-morphable slot within an island -;; -------------------------------------------------------------------------- -;; -;; (marsh :id "name" :tag "div" :transform fn children...) -;; -;; Like a lake but reactive: during morph, new content is parsed as SX and -;; re-evaluated in the island's signal scope. The :transform function (if -;; present) reshapes server content before evaluation. -;; -;; Renders as
children
. -;; Stores the island env and transform on the element for morph retrieval. - -(define render-dom-marsh :effects [render] - (fn ((args :as list) (env :as dict) (ns :as string)) - (let ((marsh-id nil) - (marsh-tag "div") - (marsh-transform nil) - (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! marsh-id kval) - (= kname "tag") (set! marsh-tag kval) - (= kname "transform") (set! marsh-transform 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 marsh-tag nil))) - (dom-set-attr el "data-sx-marsh" (or marsh-id "")) - ;; Store transform function and island env for morph retrieval - (when marsh-transform - (dom-set-data el "sx-marsh-transform" marsh-transform)) - (dom-set-data el "sx-marsh-env" env) - (for-each - (fn (c) (dom-append el (render-to-dom c env ns))) - children) - el)))) - - -;; -------------------------------------------------------------------------- -;; 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 :effects [render mutation] - (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. -;; 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 :effects [render mutation] - (fn (el (attr-name :as string) (compute-fn :as lambda)) - ;; 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 ((raw (compute-fn))) - ;; If compute-fn returned a signal (e.g. from computed), deref it - ;; to get the actual value and track the dependency - (let ((val (if (signal? raw) (deref raw) raw))) - (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-spread — reactively bind spread attrs to parent element. -;; Used when a child of an element produces a spread inside an island. -;; Tracks signal deps in the spread expression. When signals change: -;; old classes are removed, new ones applied. Non-class attrs (data-tw etc.) -;; are overwritten. Flushes newly collected CSS rules to live stylesheet. -;; -;; Multiple reactive spreads on the same element are safe — each tracks -;; its own class contribution and only removes/adds its own tokens. -(define reactive-spread :effects [render mutation] - (fn (el (render-fn :as lambda)) - (let ((prev-classes (list)) - (prev-extra-keys (list))) - ;; Mark for morph protection - (let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") ""))) - (dom-set-attr el "data-sx-reactive-attrs" - (if (empty? existing) "_spread" (str existing ",_spread")))) - (effect (fn () - ;; 1. Remove previously applied classes from element's class list - (when (not (empty? prev-classes)) - (let ((current (or (dom-get-attr el "class") "")) - (tokens (filter (fn (c) (not (= c ""))) (split current " "))) - (kept (filter (fn (c) - (not (some (fn (pc) (= pc c)) prev-classes))) - tokens))) - (if (empty? kept) - (dom-remove-attr el "class") - (dom-set-attr el "class" (join " " kept))))) - ;; 2. Remove previously applied extra attrs - (for-each (fn (k) (dom-remove-attr el k)) prev-extra-keys) - ;; 3. Re-evaluate the spread expression (tracks signal deps) - (let ((result (render-fn))) - (if (spread? result) - (let ((attrs (spread-attrs result)) - (cls-str (or (dict-get attrs "class") "")) - (new-classes (filter (fn (c) (not (= c ""))) - (split cls-str " "))) - (extra-keys (filter (fn (k) (not (= k "class"))) - (keys attrs)))) - (set! prev-classes new-classes) - (set! prev-extra-keys extra-keys) - ;; Append new classes to element - (when (not (empty? new-classes)) - (let ((current (or (dom-get-attr el "class") ""))) - (dom-set-attr el "class" - (if (and current (not (= current ""))) - (str current " " cls-str) - cls-str)))) - ;; Set extra attrs (data-tw, etc.) — simple overwrite - (for-each (fn (k) - (dom-set-attr el k (str (dict-get attrs k)))) - extra-keys) - ;; Flush any newly collected CSS rules to live stylesheet - (run-post-render-hooks)) - ;; No longer a spread — clear tracked state - (do - (set! prev-classes (list)) - (set! prev-extra-keys (list)))))))))) - -;; 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 :effects [render mutation] - (fn ((test-fn :as lambda) (render-fn :as lambda) (env :as dict) (ns :as string)) - (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. -;; -;; Keyed reconciliation: if rendered elements have a "key" attribute, -;; existing DOM nodes are reused across updates. Only additions, removals, -;; and reorderings touch the DOM. Without keys, falls back to clear+rerender. - -(define render-list-item :effects [render] - (fn ((map-fn :as lambda) item (env :as dict) (ns :as string)) - (if (lambda? map-fn) - (render-lambda-dom map-fn (list item) env ns) - (render-to-dom (apply map-fn (list item)) env ns)))) - -(define extract-key :effects [render] - (fn (node (index :as number)) - ;; Extract key from rendered node: :key attr, data-key, or index fallback - (let ((k (dom-get-attr node "key"))) - (if k - (do (dom-remove-attr node "key") k) - (let ((dk (dom-get-data node "key"))) - (if dk (str dk) (str "__idx_" index))))))) - -(define reactive-list :effects [render mutation] - (fn ((map-fn :as lambda) (items-sig :as signal) (env :as dict) (ns :as string)) - (let ((container (create-fragment)) - (marker (create-comment "island-list")) - (key-map (dict)) - (key-order (list))) - (dom-append container marker) - (effect (fn () - (let ((items (deref items-sig))) - (if (dom-parent marker) - ;; Marker in DOM: reconcile - (let ((new-map (dict)) - (new-keys (list)) - (has-keys false)) - - ;; Render or reuse each item - (for-each-indexed - (fn (idx item) - (let ((rendered (render-list-item map-fn item env ns)) - (key (extract-key rendered idx))) - (when (and (not has-keys) - (not (starts-with? key "__idx_"))) - (set! has-keys true)) - ;; Reuse existing node if key matches, else use new - (if (dict-has? key-map key) - (dict-set! new-map key (dict-get key-map key)) - (dict-set! new-map key rendered)) - (append! new-keys key))) - items) - - (if (not has-keys) - ;; No keys: simple clear and re-render (original strategy) - (do - (dom-remove-children-after marker) - (let ((frag (create-fragment))) - (for-each - (fn (k) (dom-append frag (dict-get new-map k))) - new-keys) - (dom-insert-after marker frag))) - - ;; Keyed reconciliation - (do - ;; Remove stale nodes - (for-each - (fn (old-key) - (when (not (dict-has? new-map old-key)) - (dom-remove (dict-get key-map old-key)))) - key-order) - - ;; Reorder/insert to match new key order - (let ((cursor marker)) - (for-each - (fn (k) - (let ((node (dict-get new-map k)) - (next (dom-next-sibling cursor))) - (when (not (identical? node next)) - (dom-insert-after cursor node)) - (set! cursor node))) - new-keys)))) - - ;; Update state for next render - (set! key-map new-map) - (set! key-order new-keys)) - - ;; First run (marker not in DOM yet): render initial items into container - (for-each-indexed - (fn (idx item) - (let ((rendered (render-list-item map-fn item env ns)) - (key (extract-key rendered idx))) - (dict-set! key-map key rendered) - (append! key-order key) - (dom-append container rendered))) - items))))) - container))) - - -;; -------------------------------------------------------------------------- -;; bind-input — two-way signal binding for form elements -;; -------------------------------------------------------------------------- -;; -;; (bind-input el sig) creates a bidirectional link: -;; Signal → element: effect updates el.value (or el.checked) when sig changes -;; Element → signal: input/change listener updates sig when user types -;; -;; Handles: input[text/number/email/...], textarea, select, checkbox, radio - -(define bind-input :effects [render mutation] - (fn (el (sig :as signal)) - (let ((input-type (lower (or (dom-get-attr el "type") ""))) - (is-checkbox (or (= input-type "checkbox") - (= input-type "radio")))) - ;; Set initial value from signal - (if is-checkbox - (dom-set-prop el "checked" (deref sig)) - (dom-set-prop el "value" (str (deref sig)))) - ;; Signal → element (reactive effect) - (effect (fn () - (if is-checkbox - (dom-set-prop el "checked" (deref sig)) - (let ((v (str (deref sig)))) - (when (!= (dom-get-prop el "value") v) - (dom-set-prop el "value" v)))))) - ;; Element → signal (event listener) - (dom-listen el (if is-checkbox "change" "input") - (fn (e) - (if is-checkbox - (reset! sig (dom-get-prop el "checked")) - (reset! sig (dom-get-prop el "value")))))))) - - -;; -------------------------------------------------------------------------- -;; CEK-based reactive rendering (opt-in, deref-as-shift) -;; -------------------------------------------------------------------------- -;; -;; When enabled, (deref sig) inside a reactive-reset boundary performs -;; continuation capture: "the rest of this expression" becomes the subscriber. -;; No explicit effect() wrapping needed for text/attr bindings. - -(define *use-cek-reactive* true) -(define enable-cek-reactive! (fn () (set! *use-cek-reactive* true))) - -;; cek-reactive-text — create a text node bound via continuation capture -(define cek-reactive-text :effects [render mutation] - (fn (expr env) - (let ((node (create-text-node "")) - (update-fn (fn (val) - (dom-set-text-content node (str val))))) - (let ((initial (cek-run - (make-cek-state expr env - (list (make-reactive-reset-frame env update-fn true)))))) - (dom-set-text-content node (str initial)) - node)))) - -;; cek-reactive-attr — bind an attribute via continuation capture -(define cek-reactive-attr :effects [render mutation] - (fn (el attr-name expr env) - (let ((update-fn (fn (val) - (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)))))) - ;; Mark for morph protection - (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)) - ;; Initial render via CEK with ReactiveResetFrame - (let ((initial (cek-run - (make-cek-state expr env - (list (make-reactive-reset-frame env update-fn true)))))) - (cek-call update-fn (list initial)))))) - - -;; -------------------------------------------------------------------------- -;; render-dom-portal — render children into a remote target element -;; -------------------------------------------------------------------------- -;; -;; (portal "#modal-root" (div "content")) -;; -;; Renders children into the DOM node matched by the selector, rather than -;; into the current position. Returns a comment marker at the original -;; position. Registers a disposer to clean up portal content on island -;; teardown. - -(define render-dom-portal :effects [render] - (fn ((args :as list) (env :as dict) (ns :as string)) - (let ((selector (trampoline (eval-expr (first args) env))) - (target (or (dom-query selector) - (dom-ensure-element selector)))) - (if (not target) - (create-comment (str "portal: " selector " (not found)")) - (let ((marker (create-comment (str "portal: " selector))) - (frag (create-fragment))) - ;; Render children into the fragment - (for-each - (fn (child) (dom-append frag (render-to-dom child env ns))) - (rest args)) - ;; Track portal nodes for disposal - (let ((portal-nodes (dom-child-nodes frag))) - ;; Append into remote target - (dom-append target frag) - ;; Register disposer: remove portal content on island teardown - (register-in-scope - (fn () - (for-each (fn (n) (dom-remove n)) portal-nodes)))) - ;; Return marker at original position - marker))))) - - -;; -------------------------------------------------------------------------- -;; render-dom-error-boundary — catch errors, render fallback UI -;; -------------------------------------------------------------------------- -;; -;; (error-boundary fallback-fn body...) -;; -;; Renders body children inside a try/catch. If any child throws during -;; rendering, the fallback function is called with the error object, and -;; its result is rendered instead. Effects within the boundary are disposed -;; on error. -;; -;; The fallback function receives the error and a retry thunk: -;; (fn (err retry) ...) -;; Calling (retry) re-renders the body, replacing the fallback. - -(define render-dom-error-boundary :effects [render] - (fn ((args :as list) (env :as dict) (ns :as string)) - (let ((fallback-expr (first args)) - (body-exprs (rest args)) - (container (dom-create-element "div" nil)) - ;; retry-version: bump this signal to force re-render after fallback - (retry-version (signal 0))) - (dom-set-attr container "data-sx-boundary" "true") - - ;; The entire body is rendered inside ONE effect + try-catch. - ;; Body renders WITHOUT island scope so that if/when/cond use static - ;; paths — their signal reads become direct deref calls tracked by THIS - ;; effect. Errors from signal changes throw synchronously within try-catch. - ;; The error boundary's own effect handles all reactivity for its subtree. - (effect (fn () - ;; Touch retry-version so the effect re-runs when retry is called - (deref retry-version) - - ;; Clear container - (dom-set-prop container "innerHTML" "") - - ;; Push nil island scope to suppress reactive rendering in body. - ;; Pop in both success and error paths. - (scope-push! "sx-island-scope" nil) - (try-catch - (fn () - ;; Body renders statically — signal reads tracked by THIS effect, - ;; throws propagate to our try-catch. - (let ((frag (create-fragment))) - (for-each - (fn (child) - (dom-append frag (render-to-dom child env ns))) - body-exprs) - (dom-append container frag)) - (scope-pop! "sx-island-scope")) - (fn (err) - ;; Pop scope first, then render fallback - (scope-pop! "sx-island-scope") - (let ((fallback-fn (trampoline (eval-expr fallback-expr env))) - (retry-fn (fn () (swap! retry-version (fn (n) (+ n 1)))))) - (let ((fallback-dom - (if (lambda? fallback-fn) - (render-lambda-dom fallback-fn (list err retry-fn) env ns) - (render-to-dom (apply fallback-fn (list err retry-fn)) env ns)))) - (dom-append container fallback-dom))))))) - - container))) - - -;; -------------------------------------------------------------------------- -;; Platform interface — DOM adapter -;; -------------------------------------------------------------------------- -;; -;; Element creation: -;; (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) -;; (dom-get-data el key) → any (retrieve data stored on element) -;; -;; Property access (for input binding): -;; (dom-set-prop el name val) → void (set JS property: el[name] = val) -;; (dom-get-prop el name) → any (read JS property: el[name]) -;; -;; Query (for portals): -;; (dom-query selector) → Element or nil (document.querySelector) -;; -;; Event handling: -;; (dom-listen el name handler) → remove-fn (addEventListener, returns remover) -;; (dom-dispatch el name detail)→ boolean (dispatch CustomEvent, bubbles: true) -;; -;; Content parsing: -;; (dom-parse-html s) → DocumentFragment from HTML string -;; (dom-clone node) → deep clone of a DOM node -;; -;; Type checking: -;; DOM nodes have type-of → "dom-node" -;; -;; From render.sx: -;; HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, definition-form? -;; -;; From eval.sx: -;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond -;; env-has?, env-get, env-set!, env-merge -;; 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, register-in-scope -;; -;; Pure primitives used: -;; keys, get, str -;; -;; Iteration: -;; (for-each-indexed fn coll) → call fn(index, item) for each element -;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/adapter-html.sx b/shared/sx/ref/adapter-html.sx deleted file mode 100644 index 843f2a6..0000000 --- a/shared/sx/ref/adapter-html.sx +++ /dev/null @@ -1,545 +0,0 @@ -;; ========================================================================== -;; adapter-html.sx — HTML string rendering adapter -;; -;; Renders evaluated SX expressions to HTML strings. Used server-side. -;; -;; Depends on: -;; render.sx — HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, -;; 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?, island?, macro?, -;; lambda-closure, lambda-params, lambda-body -;; ========================================================================== - - -(define render-to-html :effects [render] - (fn (expr (env :as dict)) - (set-render-active! true) - (case (type-of expr) - ;; Literals — render directly - "nil" "" - "string" (escape-html expr) - "number" (str expr) - "boolean" (if expr "true" "false") - ;; List — dispatch to render-list which handles HTML tags, special forms, etc. - "list" (if (empty? expr) "" (render-list-to-html expr env)) - ;; Symbol — evaluate then render - "symbol" (render-value-to-html (trampoline (eval-expr expr env)) env) - ;; Keyword — render as text - "keyword" (escape-html (keyword-name expr)) - ;; Raw HTML passthrough - "raw-html" (raw-html-content expr) - ;; Spread — emit attrs to nearest element provider - "spread" (do (emit! "element-attrs" (spread-attrs expr)) "") - ;; Everything else — evaluate first - :else (render-value-to-html (trampoline (eval-expr expr env)) env)))) - -(define render-value-to-html :effects [render] - (fn (val (env :as dict)) - (case (type-of val) - "nil" "" - "string" (escape-html val) - "number" (str val) - "boolean" (if val "true" "false") - "list" (render-list-to-html val env) - "raw-html" (raw-html-content val) - "spread" (do (emit! "element-attrs" (spread-attrs val)) "") - :else (escape-html (str val))))) - - -;; -------------------------------------------------------------------------- -;; Render-aware form classification -;; -------------------------------------------------------------------------- - -(define RENDER_HTML_FORMS - (list "if" "when" "cond" "case" "let" "let*" "begin" "do" - "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" - "deftype" "defeffect" - "map" "map-indexed" "filter" "for-each" "scope" "provide")) - -(define render-html-form? :effects [] - (fn ((name :as string)) - (contains? RENDER_HTML_FORMS name))) - - -;; -------------------------------------------------------------------------- -;; render-list-to-html — dispatch on list head -;; -------------------------------------------------------------------------- - -(define render-list-to-html :effects [render] - (fn ((expr :as list) (env :as dict)) - (if (empty? expr) - "" - (let ((head (first expr))) - (if (not (= (type-of head) "symbol")) - ;; Data list — render each item - (join "" (map (fn (x) (render-value-to-html x env)) expr)) - (let ((name (symbol-name head)) - (args (rest expr))) - (cond - ;; Fragment - (= name "<>") - (join "" (map (fn (x) (render-to-html x env)) args)) - - ;; Raw HTML passthrough - (= name "raw!") - (join "" (map (fn (x) (str (trampoline (eval-expr x env)))) args)) - - ;; Lake — server-morphable slot within an island - (= name "lake") - (render-html-lake args env) - - ;; Marsh — reactive server-morphable slot within an island - (= name "marsh") - (render-html-marsh args env) - - ;; HTML tag - (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))) - (cond - (component? val) - (render-html-component val args env) - (macro? val) - (render-to-html - (expand-macro val args env) - env) - :else - (error (str "Unknown component: " name)))) - - ;; Render-aware special forms - (render-html-form? name) - (dispatch-html-form name expr env) - - ;; Macro expansion - (and (env-has? env name) (macro? (env-get env name))) - (render-to-html - (expand-macro (env-get env name) args env) - env) - - ;; Fallback — evaluate then render result - :else - (render-value-to-html - (trampoline (eval-expr expr env)) - env)))))))) - - -;; -------------------------------------------------------------------------- -;; dispatch-html-form — render-aware special form handling for HTML output -;; -------------------------------------------------------------------------- - -(define dispatch-html-form :effects [render] - (fn ((name :as string) (expr :as list) (env :as dict)) - (cond - ;; if - (= name "if") - (let ((cond-val (trampoline (eval-expr (nth expr 1) env)))) - (if cond-val - (render-to-html (nth expr 2) env) - (if (> (len expr) 3) - (render-to-html (nth expr 3) env) - ""))) - - ;; when — single body: pass through. Multi: join strings. - (= name "when") - (if (not (trampoline (eval-expr (nth expr 1) env))) - "" - (if (= (len expr) 3) - (render-to-html (nth expr 2) env) - (join "" (map (fn (i) (render-to-html (nth expr i) env)) - (range 2 (len expr)))))) - - ;; cond - (= name "cond") - (let ((branch (eval-cond (rest expr) env))) - (if branch - (render-to-html branch env) - "")) - - ;; case - (= name "case") - (render-to-html (trampoline (eval-expr expr env)) env) - - ;; let / let* — single body: pass through. Multi: join strings. - (or (= name "let") (= name "let*")) - (let ((local (process-bindings (nth expr 1) env))) - (if (= (len expr) 3) - (render-to-html (nth expr 2) local) - (join "" (map (fn (i) (render-to-html (nth expr i) local)) - (range 2 (len expr)))))) - - ;; begin / do — single body: pass through. Multi: join strings. - (or (= name "begin") (= name "do")) - (if (= (len expr) 2) - (render-to-html (nth expr 1) env) - (join "" (map (fn (i) (render-to-html (nth expr i) env)) - (range 1 (len expr))))) - - ;; Definition forms — eval for side effects - (definition-form? name) - (do (trampoline (eval-expr expr env)) "") - - ;; map - (= name "map") - (let ((f (trampoline (eval-expr (nth expr 1) env))) - (coll (trampoline (eval-expr (nth expr 2) env)))) - (join "" - (map - (fn (item) - (if (lambda? f) - (render-lambda-html f (list item) env) - (render-to-html (apply f (list item)) env))) - coll))) - - ;; map-indexed - (= name "map-indexed") - (let ((f (trampoline (eval-expr (nth expr 1) env))) - (coll (trampoline (eval-expr (nth expr 2) env)))) - (join "" - (map-indexed - (fn (i item) - (if (lambda? f) - (render-lambda-html f (list i item) env) - (render-to-html (apply f (list i item)) env))) - coll))) - - ;; filter — evaluate fully then render - (= name "filter") - (render-to-html (trampoline (eval-expr expr env)) env) - - ;; for-each (render variant) - (= name "for-each") - (let ((f (trampoline (eval-expr (nth expr 1) env))) - (coll (trampoline (eval-expr (nth expr 2) env)))) - (join "" - (map - (fn (item) - (if (lambda? f) - (render-lambda-html f (list item) env) - (render-to-html (apply f (list item)) env))) - coll))) - - ;; scope — unified render-time dynamic scope - (= name "scope") - (let ((scope-name (trampoline (eval-expr (nth expr 1) env))) - (rest-args (slice expr 2)) - (scope-val nil) - (body-exprs nil)) - ;; Check for :value keyword - (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) - (let ((result (if (= (len body-exprs) 1) - (render-to-html (first body-exprs) env) - (join "" (map (fn (e) (render-to-html e env)) body-exprs))))) - (scope-pop! scope-name) - result)) - - ;; provide — sugar for scope with value - (= name "provide") - (let ((prov-name (trampoline (eval-expr (nth expr 1) env))) - (prov-val (trampoline (eval-expr (nth expr 2) env))) - (body-start 3) - (body-count (- (len expr) 3))) - (scope-push! prov-name prov-val) - (let ((result (if (= body-count 1) - (render-to-html (nth expr body-start) env) - (join "" (map (fn (i) (render-to-html (nth expr i) env)) - (range body-start (+ body-start body-count))))))) - (scope-pop! prov-name) - result)) - - ;; Fallback - :else - (render-value-to-html (trampoline (eval-expr expr env)) env)))) - - -;; -------------------------------------------------------------------------- -;; render-lambda-html — render a lambda body in HTML context -;; -------------------------------------------------------------------------- - -(define render-lambda-html :effects [render] - (fn ((f :as lambda) (args :as list) (env :as dict)) - (let ((local (env-merge (lambda-closure f) env))) - (for-each-indexed - (fn (i p) - (env-set! local p (nth args i))) - (lambda-params f)) - (render-to-html (lambda-body f) local)))) - - -;; -------------------------------------------------------------------------- -;; render-html-component — expand and render a component -;; -------------------------------------------------------------------------- - -(define render-html-component :effects [render] - (fn ((comp :as component) (args :as list) (env :as dict)) - ;; Expand component and render body through HTML adapter. - ;; Component body contains rendering forms (HTML tags) that only the - ;; adapter understands, so expansion must happen here, not in eval-expr. - (let ((kwargs (dict)) - (children (list))) - ;; Separate keyword args from positional children - (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 component env: closure + caller env + params - (let ((local (env-merge (component-closure comp) env))) - ;; Bind params from kwargs - (for-each - (fn (p) - (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) - (component-params comp)) - ;; If component accepts children, pre-render them to raw HTML - (when (component-has-children? comp) - (env-set! local "children" - (make-raw-html (join "" (map (fn (c) (render-to-html c env)) children))))) - (render-to-html (component-body comp) local))))) - - -(define render-html-element :effects [render] - (fn ((tag :as string) (args :as list) (env :as dict)) - (let ((parsed (parse-element-args args env)) - (attrs (first parsed)) - (children (nth parsed 1)) - (is-void (contains? VOID_ELEMENTS tag))) - (if is-void - (str "<" tag (render-attrs attrs) " />") - ;; Provide scope for spread emit! - (do - (scope-push! "element-attrs" nil) - (let ((content (join "" (map (fn (c) (render-to-html c env)) children)))) - (for-each - (fn (spread-dict) (merge-spread-attrs attrs spread-dict)) - (emitted "element-attrs")) - (scope-pop! "element-attrs") - (str "<" tag (render-attrs attrs) ">" - content - ""))))))) - - -;; -------------------------------------------------------------------------- -;; render-html-lake — SSR rendering of a server-morphable slot -;; -------------------------------------------------------------------------- -;; -;; (lake :id "name" children...) →
children
-;; -;; Lakes are server territory inside islands. The morph can update lake -;; content while preserving surrounding reactive DOM. - -(define render-html-lake :effects [render] - (fn ((args :as list) (env :as dict)) - (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) - ;; Provide scope for spread emit! - (let ((lake-attrs (dict "data-sx-lake" (or lake-id "")))) - (scope-push! "element-attrs" nil) - (let ((content (join "" (map (fn (c) (render-to-html c env)) children)))) - (for-each - (fn (spread-dict) (merge-spread-attrs lake-attrs spread-dict)) - (emitted "element-attrs")) - (scope-pop! "element-attrs") - (str "<" lake-tag (render-attrs lake-attrs) ">" - content - "")))))) - - -;; -------------------------------------------------------------------------- -;; render-html-marsh — SSR rendering of a reactive server-morphable slot -;; -------------------------------------------------------------------------- -;; -;; (marsh :id "name" :tag "div" :transform fn children...) -;; →
children
-;; -;; Like a lake but reactive: during morph, new content is parsed as SX and -;; re-evaluated in the island's signal scope. Server renders children normally; -;; the :transform is a client-only concern. - -(define render-html-marsh :effects [render] - (fn ((args :as list) (env :as dict)) - (let ((marsh-id nil) - (marsh-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! marsh-id kval) - (= kname "tag") (set! marsh-tag kval) - (= kname "transform") nil) - (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) - ;; Provide scope for spread emit! - (let ((marsh-attrs (dict "data-sx-marsh" (or marsh-id "")))) - (scope-push! "element-attrs" nil) - (let ((content (join "" (map (fn (c) (render-to-html c env)) children)))) - (for-each - (fn (spread-dict) (merge-spread-attrs marsh-attrs spread-dict)) - (emitted "element-attrs")) - (scope-pop! "element-attrs") - (str "<" marsh-tag (render-attrs marsh-attrs) ">" - content - "")))))) - - -;; -------------------------------------------------------------------------- -;; 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 :effects [render] - (fn ((island :as island) (args :as list) (env :as dict)) - ;; 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-sx (serialize-island-state kwargs))) - ;; Wrap in container with hydration attributes - (str "" - body-html - "")))))) - - -;; -------------------------------------------------------------------------- -;; serialize-island-state — serialize kwargs to SX for hydration -;; -------------------------------------------------------------------------- -;; -;; Uses the SX serializer (not JSON) so the client can parse with sx-parse. -;; Handles all SX types natively: numbers, strings, booleans, nil, lists, dicts. - -(define serialize-island-state :effects [] - (fn ((kwargs :as dict)) - (if (empty-dict? kwargs) - nil - (sx-serialize kwargs)))) - - -;; -------------------------------------------------------------------------- -;; Platform interface — HTML adapter -;; -------------------------------------------------------------------------- -;; -;; Inherited from render.sx: -;; escape-html, escape-attr, raw-html-content -;; -;; From eval.sx: -;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond -;; env-has?, env-get, env-set!, env-merge -;; lambda?, component?, island?, macro? -;; lambda-closure, lambda-params, lambda-body -;; component-params, component-body, component-closure, -;; component-has-children?, component-name -;; -;; Raw HTML construction: -;; (make-raw-html s) → wrap string as raw HTML (not double-escaped) -;; -;; Island state serialization: -;; (sx-serialize val) → SX source string (from parser.sx) -;; (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 -;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/adapter-sx.sx b/shared/sx/ref/adapter-sx.sx deleted file mode 100644 index 9c6493c..0000000 --- a/shared/sx/ref/adapter-sx.sx +++ /dev/null @@ -1,407 +0,0 @@ -;; ========================================================================== -;; adapter-sx.sx — SX wire format rendering adapter -;; -;; Serializes SX expressions for client-side rendering. -;; Component calls are NOT expanded — they're sent to the client as-is. -;; HTML tags are serialized as SX source text. Special forms are evaluated. -;; -;; Depends on: -;; render.sx — HTML_TAGS -;; eval.sx — eval-expr, trampoline, call-lambda, expand-macro -;; ========================================================================== - - -(define render-to-sx :effects [render] - (fn (expr (env :as dict)) - (let ((result (aser expr env))) - ;; aser-call already returns serialized SX strings; - ;; only serialize non-string values - (if (= (type-of result) "string") - result - (serialize result))))) - -(define aser :effects [render] - (fn ((expr :as any) (env :as dict)) - ;; Evaluate for SX wire format — serialize rendering forms, - ;; evaluate control flow and function calls. - (set-render-active! true) - (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)))) - - "keyword" (keyword-name expr) - - "list" - (if (empty? expr) - (list) - (aser-list expr env)) - - ;; Spread — emit attrs to nearest element provider - "spread" (do (emit! "element-attrs" (spread-attrs expr)) nil) - - :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] - (fn ((expr :as list) (env :as dict)) - (let ((head (first expr)) - (args (rest expr))) - (if (not (= (type-of head) "symbol")) - (map (fn (x) (aser x env)) expr) - (let ((name (symbol-name head))) - (cond - ;; Fragment — serialize children - (= name "<>") - (aser-fragment args env) - - ;; Component call — serialize WITHOUT expanding - (starts-with? name "~") - (aser-call name args env) - - ;; Lake — serialize (server-morphable slot) - (= name "lake") - (aser-call name args env) - - ;; Marsh — serialize (reactive server-morphable slot) - (= name "marsh") - (aser-call name args env) - - ;; HTML tag — serialize - (contains? HTML_TAGS name) - (aser-call name args env) - - ;; Special/HO forms — evaluate (produces data) - (or (special-form? name) (ho-form? name)) - (aser-special name expr env) - - ;; Macro — expand then aser - (and (env-has? env name) (macro? (env-get env name))) - (aser (expand-macro (env-get env name) args env) env) - - ;; Function call — evaluate fully - :else - (let ((f (trampoline (eval-expr head env))) - (evaled-args (map (fn (a) (trampoline (eval-expr a env))) args))) - (cond - (and (callable? f) (not (lambda? f)) (not (component? f)) (not (island? f))) - (apply f evaled-args) - (lambda? f) - (trampoline (call-lambda f evaled-args env)) - (component? f) - (aser-call (str "~" (component-name f)) args env) - (island? f) - (aser-call (str "~" (component-name f)) args env) - :else (error (str "Not callable: " (inspect f))))))))))) - - -(define aser-fragment :effects [render] - (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 - (let ((parts (list))) - (for-each - (fn (c) - (let ((result (aser c env))) - (if (= (type-of result) "list") - (for-each - (fn (item) - (when (not (nil? item)) - (append! parts (serialize item)))) - result) - (when (not (nil? result)) - (append! parts (serialize result)))))) - children) - (if (empty? parts) - "" - (str "(<> " (join " " parts) ")"))))) - - -(define aser-call :effects [render] - (fn ((name :as string) (args :as list) (env :as dict)) - ;; 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. - ;; 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! - (scope-push! "element-attrs" nil) - (for-each - (fn (arg) - (if skip - (do (set! skip false) - (set! i (inc i))) - (if (and (= (type-of arg) "keyword") - (< (inc i) (len args))) - (let ((val (aser (nth args (inc i)) env))) - (when (not (nil? 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 (= (type-of val) "list") - (for-each - (fn (item) - (when (not (nil? item)) - (append! child-parts (serialize item)))) - val) - (append! child-parts (serialize val)))) - (set! i (inc i)))))) - args) - ;; 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")) - (scope-pop! "element-attrs") - (let ((parts (concat (list name) attr-parts child-parts))) - (str "(" (join " " parts) ")"))))) - - -;; -------------------------------------------------------------------------- -;; Form classification -;; -------------------------------------------------------------------------- - -(define SPECIAL_FORM_NAMES - (list "if" "when" "cond" "case" "and" "or" - "let" "let*" "lambda" "fn" - "define" "defcomp" "defmacro" "defstyle" - "defhandler" "defpage" "defquery" "defaction" "defrelation" - "begin" "do" "quote" "quasiquote" - "->" "set!" "letrec" "dynamic-wind" "defisland" - "deftype" "defeffect" "scope" "provide")) - -(define HO_FORM_NAMES - (list "map" "map-indexed" "filter" "reduce" - "some" "every?" "for-each")) - -(define special-form? :effects [] - (fn ((name :as string)) - (contains? SPECIAL_FORM_NAMES name))) - -(define ho-form? :effects [] - (fn ((name :as string)) - (contains? HO_FORM_NAMES name))) - - -;; -------------------------------------------------------------------------- -;; aser-special — evaluate special/HO forms in aser mode -;; -------------------------------------------------------------------------- -;; -;; Control flow forms evaluate conditions normally but render branches -;; through aser (serializing tags/components instead of rendering HTML). -;; Definition forms evaluate for side effects and return nil. - -(define aser-special :effects [render] - (fn ((name :as string) (expr :as list) (env :as dict)) - (let ((args (rest expr))) - (cond - ;; if — evaluate condition, aser chosen branch - (= name "if") - (if (trampoline (eval-expr (first args) env)) - (aser (nth args 1) env) - (if (> (len args) 2) - (aser (nth args 2) env) - nil)) - - ;; when — evaluate condition, aser body if true - (= name "when") - (if (not (trampoline (eval-expr (first args) env))) - nil - (let ((result nil)) - (for-each (fn (body) (set! result (aser body env))) - (rest args)) - result)) - - ;; cond — evaluate conditions, aser matching branch - (= name "cond") - (let ((branch (eval-cond args env))) - (if branch (aser branch env) nil)) - - ;; case — evaluate match value, check each pair - (= name "case") - (let ((match-val (trampoline (eval-expr (first args) env))) - (clauses (rest args))) - (eval-case-aser match-val clauses env)) - - ;; let / let* - (or (= name "let") (= name "let*")) - (let ((local (process-bindings (first args) env)) - (result nil)) - (for-each (fn (body) (set! result (aser body local))) - (rest args)) - result) - - ;; begin / do - (or (= name "begin") (= name "do")) - (let ((result nil)) - (for-each (fn (body) (set! result (aser body env))) args) - result) - - ;; and — short-circuit - (= name "and") - (let ((result true)) - (some (fn (arg) - (set! result (trampoline (eval-expr arg env))) - (not result)) - args) - result) - - ;; or — short-circuit - (= name "or") - (let ((result false)) - (some (fn (arg) - (set! result (trampoline (eval-expr arg env))) - result) - args) - result) - - ;; map — evaluate function and collection, map through aser - (= name "map") - (let ((f (trampoline (eval-expr (first args) env))) - (coll (trampoline (eval-expr (nth args 1) env)))) - (map (fn (item) - (if (lambda? f) - (let ((local (env-merge (lambda-closure f) env))) - (env-set! local (first (lambda-params f)) item) - (aser (lambda-body f) local)) - (cek-call f (list item)))) - coll)) - - ;; map-indexed - (= name "map-indexed") - (let ((f (trampoline (eval-expr (first args) env))) - (coll (trampoline (eval-expr (nth args 1) env)))) - (map-indexed (fn (i item) - (if (lambda? f) - (let ((local (env-merge (lambda-closure f) env))) - (env-set! local (first (lambda-params f)) i) - (env-set! local (nth (lambda-params f) 1) item) - (aser (lambda-body f) local)) - (cek-call f (list i item)))) - coll)) - - ;; for-each — evaluate for side effects, aser each body - (= name "for-each") - (let ((f (trampoline (eval-expr (first args) env))) - (coll (trampoline (eval-expr (nth args 1) env))) - (results (list))) - (for-each (fn (item) - (if (lambda? f) - (let ((local (env-merge (lambda-closure f) env))) - (env-set! local (first (lambda-params f)) item) - (append! results (aser (lambda-body f) local))) - (cek-call f (list item)))) - coll) - (if (empty? results) nil results)) - - ;; defisland — evaluate AND serialize (client needs the definition) - (= name "defisland") - (do (trampoline (eval-expr expr env)) - (serialize expr)) - - ;; Definition forms — evaluate for side effects - (or (= name "define") (= name "defcomp") (= name "defmacro") - (= name "defstyle") (= name "defhandler") (= name "defpage") - (= name "defquery") (= name "defaction") (= name "defrelation") - (= name "deftype") (= name "defeffect")) - (do (trampoline (eval-expr expr env)) nil) - - ;; scope — unified render-time dynamic scope - (= name "scope") - (let ((scope-name (trampoline (eval-expr (first args) env))) - (rest-args (rest args)) - (scope-val nil) - (body-args nil)) - ;; Check for :value keyword - (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-args (slice rest-args 2))) - (set! body-args rest-args)) - (scope-push! scope-name scope-val) - (let ((result nil)) - (for-each (fn (body) (set! result (aser body env))) - body-args) - (scope-pop! scope-name) - result)) - - ;; provide — sugar for scope with value - (= name "provide") - (let ((prov-name (trampoline (eval-expr (first args) env))) - (prov-val (trampoline (eval-expr (nth args 1) env))) - (result nil)) - (scope-push! prov-name prov-val) - (for-each (fn (body) (set! result (aser body env))) - (slice args 2)) - (scope-pop! prov-name) - result) - - ;; Everything else — evaluate normally - :else - (trampoline (eval-expr expr env)))))) - - -;; Helper: case dispatch for aser mode -(define eval-case-aser :effects [render] - (fn (match-val (clauses :as list) (env :as dict)) - (if (< (len clauses) 2) - nil - (let ((test (first clauses)) - (body (nth clauses 1))) - (if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else")) - (and (= (type-of test) "symbol") - (or (= (symbol-name test) ":else") - (= (symbol-name test) "else")))) - (aser body env) - (if (= match-val (trampoline (eval-expr test env))) - (aser body env) - (eval-case-aser match-val (slice clauses 2) env))))))) - - -;; -------------------------------------------------------------------------- -;; Platform interface — SX wire adapter -;; -------------------------------------------------------------------------- -;; -;; From eval.sx: -;; eval-expr, trampoline, call-lambda, expand-macro -;; env-has?, env-get, env-set!, env-merge, callable?, lambda?, component?, -;; macro?, island?, primitive?, get-primitive, component-name -;; lambda-closure, lambda-params, lambda-body -;; -;; From render.sx: -;; HTML_TAGS, eval-cond, process-bindings -;; -;; From parser.sx: -;; serialize (= sx-serialize) -;; -;; From signals.sx (optional): -;; invoke -;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/boot.sx b/shared/sx/ref/boot.sx deleted file mode 100644 index 72d7c00..0000000 --- a/shared/sx/ref/boot.sx +++ /dev/null @@ -1,552 +0,0 @@ -;; ========================================================================== -;; boot.sx — Browser boot, mount, hydrate, script processing -;; -;; Handles the browser startup lifecycle: -;; 1. CSS tracking init -;; 2. Component script processing (from