diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 48da949..7b969d9 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-11T22:53:48Z"; + var SX_VERSION = "2026-03-11T23:22:03Z"; 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 index 09803f3..0922111 100644 --- a/shared/sx/ref/adapter-async.sx +++ b/shared/sx/ref/adapter-async.sx @@ -40,7 +40,7 @@ ;; Async HTML renderer ;; -------------------------------------------------------------------------- -(define-async async-render +(define-async async-render :effects [render io] (fn (expr (env :as dict) ctx) (case (type-of expr) "nil" "" @@ -56,7 +56,7 @@ :else (escape-html (str expr))))) -(define-async async-render-list +(define-async async-render-list :effects [render io] (fn (expr (env :as dict) ctx) (let ((head (first expr))) (if (not (= (type-of head) "symbol")) @@ -138,7 +138,7 @@ ;; async-render-raw — handle (raw! ...) in async context ;; -------------------------------------------------------------------------- -(define-async async-render-raw +(define-async async-render-raw :effects [render io] (fn ((args :as list) (env :as dict) ctx) (let ((parts (list))) (for-each @@ -157,7 +157,7 @@ ;; async-render-element — render an HTML element with async arg evaluation ;; -------------------------------------------------------------------------- -(define-async async-render-element +(define-async async-render-element :effects [render io] (fn ((tag :as string) (args :as list) (env :as dict) ctx) (let ((attrs (dict)) (children (list))) @@ -185,7 +185,7 @@ ;; 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 +(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)) @@ -210,7 +210,7 @@ ;; async-render-component — expand and render a component asynchronously ;; -------------------------------------------------------------------------- -(define-async async-render-component +(define-async async-render-component :effects [render io] (fn ((comp :as component) (args :as list) (env :as dict) ctx) (let ((kwargs (dict)) (children (list))) @@ -232,7 +232,7 @@ ;; async-render-island — SSR render of reactive island with hydration markers ;; -------------------------------------------------------------------------- -(define-async async-render-island +(define-async async-render-island :effects [render io] (fn ((island :as island) (args :as list) (env :as dict) ctx) (let ((kwargs (dict)) (children (list))) @@ -261,7 +261,7 @@ ;; async-render-lambda — render lambda body in HTML context ;; -------------------------------------------------------------------------- -(define-async async-render-lambda +(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 @@ -274,7 +274,7 @@ ;; async-parse-kw-args — parse keyword args and children with async eval ;; -------------------------------------------------------------------------- -(define-async async-parse-kw-args +(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)) @@ -300,7 +300,7 @@ ;; -------------------------------------------------------------------------- ;; Bootstrapper emits this as: [await async_render(x, env, ctx) for x in exprs] -(define-async async-map-render +(define-async async-map-render :effects [render io] (fn ((exprs :as list) (env :as dict) ctx) (let ((results (list))) (for-each @@ -319,7 +319,7 @@ "deftype" "defeffect" "map" "map-indexed" "filter" "for-each")) -(define async-render-form? +(define async-render-form? :effects [] (fn ((name :as string)) (contains? ASYNC_RENDER_FORMS name))) @@ -331,7 +331,7 @@ ;; 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 +(define-async dispatch-async-render-form :effects [render io] (fn ((name :as string) expr (env :as dict) ctx) (cond ;; if @@ -407,7 +407,7 @@ ;; async-render-cond-scheme — scheme-style cond for render mode ;; -------------------------------------------------------------------------- -(define-async async-render-cond-scheme +(define-async async-render-cond-scheme :effects [render io] (fn ((clauses :as list) (env :as dict) ctx) (if (empty? clauses) "" @@ -429,7 +429,7 @@ ;; async-render-cond-clojure — clojure-style cond for render mode ;; -------------------------------------------------------------------------- -(define-async async-render-cond-clojure +(define-async async-render-cond-clojure :effects [render io] (fn ((clauses :as list) (env :as dict) ctx) (if (< (len clauses) 2) "" @@ -449,7 +449,7 @@ ;; async-process-bindings — evaluate let-bindings asynchronously ;; -------------------------------------------------------------------------- -(define-async async-process-bindings +(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. @@ -470,7 +470,7 @@ local))) -(define-async async-process-bindings-flat +(define-async async-process-bindings-flat :effects [render io] (fn ((bindings :as list) (local :as dict) ctx) (let ((skip false) (i 0)) @@ -495,7 +495,7 @@ ;; async-map-fn-render — map a lambda/callable over collection for render ;; -------------------------------------------------------------------------- -(define-async async-map-fn-render +(define-async async-map-fn-render :effects [render io] (fn (f (coll :as list) (env :as dict) ctx) (let ((results (list))) (for-each @@ -512,7 +512,7 @@ ;; async-map-indexed-fn-render — map-indexed variant for render ;; -------------------------------------------------------------------------- -(define-async async-map-indexed-fn-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)) @@ -531,7 +531,7 @@ ;; async-invoke — call a native callable, await if coroutine ;; -------------------------------------------------------------------------- -(define-async async-invoke +(define-async async-invoke :effects [io] (fn (f &rest args) (let ((r (apply f args))) (if (async-coroutine? r) @@ -543,7 +543,7 @@ ;; Async SX wire format (aser) ;; ========================================================================== -(define-async async-aser +(define-async async-aser :effects [render io] (fn (expr (env :as dict) ctx) (case (type-of expr) "number" expr @@ -573,7 +573,7 @@ :else expr))) -(define-async async-aser-dict +(define-async async-aser-dict :effects [render io] (fn ((expr :as dict) (env :as dict) ctx) (let ((result (dict))) (for-each @@ -587,7 +587,7 @@ ;; async-aser-list — dispatch on list head for aser mode ;; -------------------------------------------------------------------------- -(define-async async-aser-list +(define-async async-aser-list :effects [render io] (fn (expr (env :as dict) ctx) (let ((head (first expr)) (args (rest expr))) @@ -666,7 +666,7 @@ ;; async-aser-eval-call — evaluate a function call fully in aser mode ;; -------------------------------------------------------------------------- -(define-async async-aser-eval-call +(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))) @@ -694,7 +694,7 @@ ;; async-eval-args — evaluate a list of args asynchronously ;; -------------------------------------------------------------------------- -(define-async async-eval-args +(define-async async-eval-args :effects [io] (fn ((args :as list) (env :as dict) ctx) (let ((results (list))) (for-each @@ -707,7 +707,7 @@ ;; async-aser-map-list — aser each element of a list ;; -------------------------------------------------------------------------- -(define-async async-aser-map-list +(define-async async-aser-map-list :effects [render io] (fn ((exprs :as list) (env :as dict) ctx) (let ((results (list))) (for-each @@ -720,7 +720,7 @@ ;; async-aser-fragment — serialize (<> child1 child2 ...) in aser mode ;; -------------------------------------------------------------------------- -(define-async async-aser-fragment +(define-async async-aser-fragment :effects [render io] (fn ((children :as list) (env :as dict) ctx) (let ((parts (list))) (for-each @@ -744,7 +744,7 @@ ;; async-aser-component — expand component server-side in aser mode ;; -------------------------------------------------------------------------- -(define-async async-aser-component +(define-async async-aser-component :effects [render io] (fn ((comp :as component) (args :as list) (env :as dict) ctx) (let ((kwargs (dict)) (children (list))) @@ -776,7 +776,7 @@ ;; async-parse-aser-kw-args — parse keyword args for aser mode ;; -------------------------------------------------------------------------- -(define-async async-parse-aser-kw-args +(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)) @@ -801,7 +801,7 @@ ;; async-aser-call — serialize an SX call (tag or component) in aser mode ;; -------------------------------------------------------------------------- -(define-async async-aser-call +(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) @@ -860,7 +860,7 @@ (define ASYNC_ASER_HO_NAMES (list "map" "map-indexed" "filter" "for-each")) -(define async-aser-form? +(define async-aser-form? :effects [] (fn ((name :as string)) (or (contains? ASYNC_ASER_FORM_NAMES name) (contains? ASYNC_ASER_HO_NAMES name)))) @@ -872,7 +872,7 @@ ;; ;; Uses cond-scheme? from eval.sx (the FIXED version with every? check). -(define-async dispatch-async-aser-form +(define-async dispatch-async-aser-form :effects [render io] (fn ((name :as string) expr (env :as dict) ctx) (let ((args (rest expr))) (cond @@ -1002,7 +1002,7 @@ ;; async-aser-cond-scheme — scheme-style cond for aser mode ;; -------------------------------------------------------------------------- -(define-async async-aser-cond-scheme +(define-async async-aser-cond-scheme :effects [render io] (fn ((clauses :as list) (env :as dict) ctx) (if (empty? clauses) nil @@ -1024,7 +1024,7 @@ ;; async-aser-cond-clojure — clojure-style cond for aser mode ;; -------------------------------------------------------------------------- -(define-async async-aser-cond-clojure +(define-async async-aser-cond-clojure :effects [render io] (fn ((clauses :as list) (env :as dict) ctx) (if (< (len clauses) 2) nil @@ -1044,7 +1044,7 @@ ;; async-aser-case-loop — case dispatch for aser mode ;; -------------------------------------------------------------------------- -(define-async async-aser-case-loop +(define-async async-aser-case-loop :effects [render io] (fn (match-val (clauses :as list) (env :as dict) ctx) (if (< (len clauses) 2) nil @@ -1064,7 +1064,7 @@ ;; async-aser-thread-first — -> form in aser mode ;; -------------------------------------------------------------------------- -(define-async async-aser-thread-first +(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 @@ -1084,7 +1084,7 @@ ;; async-invoke-or-lambda — invoke a callable or lambda with args ;; -------------------------------------------------------------------------- -(define-async async-invoke-or-lambda +(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))) @@ -1106,7 +1106,7 @@ ;; Async aser HO forms (map, map-indexed, for-each) ;; -------------------------------------------------------------------------- -(define-async async-aser-ho-map +(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)) @@ -1122,7 +1122,7 @@ results))) -(define-async async-aser-ho-map-indexed +(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)) @@ -1141,7 +1141,7 @@ results))) -(define-async async-aser-ho-for-each +(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)) @@ -1172,7 +1172,7 @@ ;; (sx-expr? x) — check if SxExpr ;; (set-expand-components!) — enable component expansion context var -(define-async async-eval-slot-inner +(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. @@ -1198,7 +1198,7 @@ (make-sx-expr (serialize result)))))))) -(define-async async-maybe-expand-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 diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx index 5ff0b53..8c00e92 100644 --- a/shared/sx/ref/adapter-dom.sx +++ b/shared/sx/ref/adapter-dom.sx @@ -18,7 +18,7 @@ ;; render-to-dom — main entry point ;; -------------------------------------------------------------------------- -(define render-to-dom +(define render-to-dom :effects [render] (fn (expr (env :as dict) (ns :as string)) (set-render-active! true) (case (type-of expr) @@ -66,7 +66,7 @@ ;; render-dom-list — dispatch on list head ;; -------------------------------------------------------------------------- -(define render-dom-list +(define render-dom-list :effects [render] (fn (expr (env :as dict) (ns :as string)) (let ((head (first expr))) (cond @@ -165,7 +165,7 @@ ;; render-dom-element — create a DOM element with attrs and children ;; -------------------------------------------------------------------------- -(define render-dom-element +(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 @@ -236,7 +236,7 @@ ;; render-dom-component — expand and render a component ;; -------------------------------------------------------------------------- -(define render-dom-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)) @@ -283,7 +283,7 @@ ;; render-dom-fragment — render children into a DocumentFragment ;; -------------------------------------------------------------------------- -(define render-dom-fragment +(define render-dom-fragment :effects [render] (fn ((args :as list) (env :as dict) (ns :as string)) (let ((frag (create-fragment))) (for-each @@ -296,7 +296,7 @@ ;; render-dom-raw — insert unescaped content ;; -------------------------------------------------------------------------- -(define render-dom-raw +(define render-dom-raw :effects [render] (fn ((args :as list) (env :as dict)) (let ((frag (create-fragment))) (for-each @@ -317,7 +317,7 @@ ;; render-dom-unknown-component — visible warning element ;; -------------------------------------------------------------------------- -(define render-dom-unknown-component +(define render-dom-unknown-component :effects [render] (fn ((name :as string)) (error (str "Unknown component: " name)))) @@ -334,11 +334,11 @@ "map" "map-indexed" "filter" "for-each" "portal" "error-boundary")) -(define render-dom-form? +(define render-dom-form? :effects [] (fn ((name :as string)) (contains? RENDER_DOM_FORMS name))) -(define dispatch-render-form +(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) @@ -580,7 +580,7 @@ ;; render-lambda-dom — render a lambda body in DOM context ;; -------------------------------------------------------------------------- -(define render-lambda-dom +(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))) @@ -604,7 +604,7 @@ ;; - Attribute bindings: (deref sig) in attr → reactive attribute ;; - Conditional fragments: (when (deref sig) ...) → reactive show/hide -(define render-dom-island +(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)) @@ -678,7 +678,7 @@ ;; ;; Supports :tag keyword to change wrapper element (default "div"). -(define render-dom-lake +(define render-dom-lake :effects [render] (fn ((args :as list) (env :as dict) (ns :as string)) (let ((lake-id nil) (lake-tag "div") @@ -722,7 +722,7 @@ ;; Renders as