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) - "" tag ">")))))) - - -;; -------------------------------------------------------------------------- -;; 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