Add :effects annotations to all spec files and update bootstrappers

Bootstrappers (bootstrap_py.py, js.sx) now skip :effects keyword in
define forms, enabling effect annotations throughout the spec without
changing generated output.

Annotated 180+ functions across 14 spec files:
- signals.sx: signal/deref [] pure, reset!/swap!/effect/batch [mutation]
- engine.sx: parse-* [] pure, morph-*/swap-* [mutation io]
- orchestration.sx: all [mutation io] (browser event binding)
- adapter-html.sx: render-* [render]
- adapter-dom.sx: render-* [render], reactive-* [render mutation]
- adapter-sx.sx: aser-* [render]
- adapter-async.sx: async-render-*/async-aser-* [render io]
- parser.sx: all [] pure
- render.sx: predicates [] pure, process-bindings [mutation]
- boot.sx: all [mutation io] (browser init)
- deps.sx: scan-*/transitive-* [] pure, compute-all-* [mutation]
- router.sx: all [] pure (URL matching)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 23:22:34 +00:00
parent 0f9b449315
commit 2f42e8826c
16 changed files with 274 additions and 259 deletions

View File

@@ -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); }

View File

@@ -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

View File

@@ -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 <div data-sx-marsh="name">children</div>.
;; Stores the island env and transform on the element for morph retrieval.
(define render-dom-marsh
(define render-dom-marsh :effects [render]
(fn ((args :as list) (env :as dict) (ns :as string))
(let ((marsh-id nil)
(marsh-tag "div")
@@ -769,7 +769,7 @@
;; 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
(define reactive-text :effects [render mutation]
(fn (sig)
(let ((node (create-text-node (str (deref sig)))))
(effect (fn ()
@@ -780,7 +780,7 @@
;; 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
(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") ""))
@@ -801,7 +801,7 @@
;; 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
(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)))
@@ -823,13 +823,13 @@
;; 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
(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
(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")))
@@ -838,7 +838,7 @@
(let ((dk (dom-get-data node "key")))
(if dk (str dk) (str "__idx_" index)))))))
(define reactive-list
(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"))
@@ -924,7 +924,7 @@
;;
;; Handles: input[text/number/email/...], textarea, select, checkbox, radio
(define bind-input
(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")
@@ -959,7 +959,7 @@
;; position. Registers a disposer to clean up portal content on island
;; teardown.
(define render-dom-portal
(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)
@@ -999,7 +999,7 @@
;; (fn (err retry) ...)
;; Calling (retry) re-renders the body, replacing the fallback.
(define render-dom-error-boundary
(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))

View File

@@ -13,7 +13,7 @@
;; ==========================================================================
(define render-to-html
(define render-to-html :effects [render]
(fn (expr (env :as dict))
(set-render-active! true)
(case (type-of expr)
@@ -33,7 +33,7 @@
;; Everything else — evaluate first
:else (render-value-to-html (trampoline (eval-expr expr env)) env))))
(define render-value-to-html
(define render-value-to-html :effects [render]
(fn (val (env :as dict))
(case (type-of val)
"nil" ""
@@ -55,7 +55,7 @@
"deftype" "defeffect"
"map" "map-indexed" "filter" "for-each"))
(define render-html-form?
(define render-html-form? :effects []
(fn ((name :as string))
(contains? RENDER_HTML_FORMS name)))
@@ -64,7 +64,7 @@
;; render-list-to-html — dispatch on list head
;; --------------------------------------------------------------------------
(define render-list-to-html
(define render-list-to-html :effects [render]
(fn ((expr :as list) (env :as dict))
(if (empty? expr)
""
@@ -135,7 +135,7 @@
;; dispatch-html-form — render-aware special form handling for HTML output
;; --------------------------------------------------------------------------
(define dispatch-html-form
(define dispatch-html-form :effects [render]
(fn ((name :as string) (expr :as list) (env :as dict))
(cond
;; if
@@ -235,7 +235,7 @@
;; render-lambda-html — render a lambda body in HTML context
;; --------------------------------------------------------------------------
(define render-lambda-html
(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
@@ -249,7 +249,7 @@
;; render-html-component — expand and render a component
;; --------------------------------------------------------------------------
(define render-html-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
@@ -288,7 +288,7 @@
(render-to-html (component-body comp) local)))))
(define render-html-element
(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))
@@ -312,7 +312,7 @@
;; Lakes are server territory inside islands. The morph can update lake
;; content while preserving surrounding reactive DOM.
(define render-html-lake
(define render-html-lake :effects [render]
(fn ((args :as list) (env :as dict))
(let ((lake-id nil)
(lake-tag "div")
@@ -351,7 +351,7 @@
;; re-evaluated in the island's signal scope. Server renders children normally;
;; the :transform is a client-only concern.
(define render-html-marsh
(define render-html-marsh :effects [render]
(fn ((args :as list) (env :as dict))
(let ((marsh-id nil)
(marsh-tag "div")
@@ -394,7 +394,7 @@
;; (reset! s v) → no-op
;; (swap! s f) → no-op
(define render-html-island
(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))
@@ -452,7 +452,7 @@
;; 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
(define serialize-island-state :effects []
(fn ((kwargs :as dict))
(if (empty-dict? kwargs)
nil

View File

@@ -11,7 +11,7 @@
;; ==========================================================================
(define render-to-sx
(define render-to-sx :effects [render]
(fn (expr (env :as dict))
(let ((result (aser expr env)))
;; aser-call already returns serialized SX strings;
@@ -20,7 +20,7 @@
result
(serialize result)))))
(define aser
(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.
@@ -51,7 +51,7 @@
:else expr)))
(define aser-list
(define aser-list :effects [render]
(fn ((expr :as list) (env :as dict))
(let ((head (first expr))
(args (rest expr)))
@@ -103,7 +103,7 @@
:else (error (str "Not callable: " (inspect f)))))))))))
(define aser-fragment
(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
@@ -125,7 +125,7 @@
(str "(<> " (join " " parts) ")")))))
(define aser-call
(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
@@ -177,11 +177,11 @@
(list "map" "map-indexed" "filter" "reduce"
"some" "every?" "for-each"))
(define special-form?
(define special-form? :effects []
(fn ((name :as string))
(contains? SPECIAL_FORM_NAMES name)))
(define ho-form?
(define ho-form? :effects []
(fn ((name :as string))
(contains? HO_FORM_NAMES name)))
@@ -194,7 +194,7 @@
;; through aser (serializing tags/components instead of rendering HTML).
;; Definition forms evaluate for side effects and return nil.
(define aser-special
(define aser-special :effects [render]
(fn ((name :as string) (expr :as list) (env :as dict))
(let ((args (rest expr)))
(cond
@@ -315,7 +315,7 @@
;; Helper: case dispatch for aser mode
(define eval-case-aser
(define eval-case-aser :effects [render]
(fn (match-val (clauses :as list) (env :as dict))
(if (< (len clauses) 2)
nil

View File

@@ -26,7 +26,7 @@
(define HEAD_HOIST_SELECTOR
"meta, title, link[rel='canonical'], script[type='application/ld+json']")
(define hoist-head-elements-full
(define hoist-head-elements-full :effects [mutation io]
(fn (root)
(let ((els (dom-query-all root HEAD_HOIST_SELECTOR)))
(for-each
@@ -71,7 +71,7 @@
;; Mount — render SX source into a DOM element
;; --------------------------------------------------------------------------
(define sx-mount
(define sx-mount :effects [mutation io]
(fn (target (source :as string) (extra-env :as dict))
;; Render SX source string into target element.
;; target: Element or CSS selector string
@@ -100,7 +100,7 @@
;; Finds the suspense wrapper by data-suspense attribute, renders the
;; new SX content, and replaces the wrapper's children.
(define resolve-suspense
(define resolve-suspense :effects [mutation io]
(fn ((id :as string) (sx :as string))
;; Process any new <script type="text/sx"> tags that arrived via
;; streaming (e.g. extra component defs) before resolving.
@@ -127,7 +127,7 @@
;; Hydrate — render all [data-sx] elements
;; --------------------------------------------------------------------------
(define sx-hydrate-elements
(define sx-hydrate-elements :effects [mutation io]
(fn (root)
;; Find all [data-sx] elements within root and render them.
(let ((els (dom-query-all (or root (dom-body)) "[data-sx]")))
@@ -143,7 +143,7 @@
;; Update — re-render a [data-sx] element with new env data
;; --------------------------------------------------------------------------
(define sx-update-element
(define sx-update-element :effects [mutation io]
(fn (el new-env)
;; Re-render a [data-sx] element.
;; Reads source from data-sx attr, base env from data-sx-env attr.
@@ -165,7 +165,7 @@
;; Render component — build synthetic call from kwargs dict
;; --------------------------------------------------------------------------
(define sx-render-component
(define sx-render-component :effects [mutation io]
(fn ((name :as string) (kwargs :as dict) (extra-env :as dict))
;; Render a named component with keyword args.
;; name: component name (with or without ~ prefix)
@@ -190,7 +190,7 @@
;; Script processing — <script type="text/sx">
;; --------------------------------------------------------------------------
(define process-sx-scripts
(define process-sx-scripts :effects [mutation io]
(fn (root)
;; Process all <script type="text/sx"> tags.
;; - data-components + data-hash → localStorage cache
@@ -235,7 +235,7 @@
;; Component script with caching
;; --------------------------------------------------------------------------
(define process-component-script
(define process-component-script :effects [mutation io]
(fn (script (text :as string))
;; Handle <script type="text/sx" data-components data-hash="...">
(let ((hash (dom-get-attr script "data-hash")))
@@ -288,7 +288,7 @@
(define _page-routes (list))
(define process-page-scripts
(define process-page-scripts :effects [mutation io]
(fn ()
;; Process <script type="text/sx-pages"> tags.
;; Parses SX page registry and builds route entries with parsed patterns.
@@ -331,7 +331,7 @@
;; 5. Morph existing DOM to preserve structure, focus, scroll
;; 6. Store disposers on the element for cleanup
(define sx-hydrate-islands
(define sx-hydrate-islands :effects [mutation io]
(fn (root)
(let ((els (dom-query-all (or root (dom-body)) "[data-sx-island]")))
(for-each
@@ -341,7 +341,7 @@
(hydrate-island el)))
els))))
(define hydrate-island
(define hydrate-island :effects [mutation io]
(fn (el)
(let ((name (dom-get-attr el "data-sx-island"))
(state-sx (or (dom-get-attr el "data-sx-state") "{}")))
@@ -388,7 +388,7 @@
;; Island disposal — clean up when island removed from DOM
;; --------------------------------------------------------------------------
(define dispose-island
(define dispose-island :effects [mutation io]
(fn (el)
(let ((disposers (dom-get-data el "sx-disposers")))
(when disposers
@@ -398,7 +398,7 @@
disposers)
(dom-set-data el "sx-disposers" nil)))))
(define dispose-islands-in
(define dispose-islands-in :effects [mutation io]
(fn (root)
;; Dispose islands within root, but SKIP hydrated islands —
;; they may be preserved across morphs. Only dispose islands
@@ -419,7 +419,7 @@
;; Full boot sequence
;; --------------------------------------------------------------------------
(define boot-init
(define boot-init :effects [mutation io]
(fn ()
;; Full browser initialization:
;; 1. CSS tracking

View File

@@ -664,7 +664,12 @@ class PyEmitter:
def _emit_define(self, expr, indent: int = 0) -> str:
pad = " " * indent
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
val_expr = expr[2]
# Handle (define name :effects [...] value) — skip :effects annotation
if (len(expr) >= 5 and isinstance(expr[2], Keyword)
and expr[2].name == "effects"):
val_expr = expr[4]
else:
val_expr = expr[2]
# Always emit fn-bodied defines as def statements for flat control flow
if (isinstance(val_expr, list) and val_expr and
isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")):
@@ -675,7 +680,12 @@ class PyEmitter:
def _emit_define_async(self, expr, indent: int = 0) -> str:
"""Emit a define-async form as an async def statement."""
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
val_expr = expr[2]
# Handle (define-async name :effects [...] value) — skip :effects annotation
if (len(expr) >= 5 and isinstance(expr[2], Keyword)
and expr[2].name == "effects"):
val_expr = expr[4]
else:
val_expr = expr[2]
if (isinstance(val_expr, list) and val_expr and
isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")):
return self._emit_define_as_def(name, val_expr, indent, is_async=True)

View File

@@ -31,14 +31,14 @@
;; Walks all branches of control flow (if/when/cond/case) to find
;; every component that *could* be rendered.
(define scan-refs
(define scan-refs :effects []
(fn (node)
(let ((refs (list)))
(scan-refs-walk node refs)
refs)))
(define scan-refs-walk
(define scan-refs-walk :effects []
(fn (node (refs :as list))
(cond
;; Symbol starting with ~ → component reference
@@ -67,7 +67,7 @@
;; Given a component name and an environment, compute all components
;; that it can transitively render. Handles cycles via seen-set.
(define transitive-deps-walk
(define transitive-deps-walk :effects []
(fn ((n :as string) (seen :as list) (env :as dict))
(when (not (contains? seen n))
(append! seen n)
@@ -82,7 +82,7 @@
:else nil)))))
(define transitive-deps
(define transitive-deps :effects []
(fn ((name :as string) (env :as dict))
(let ((seen (list))
(key (if (starts-with? name "~") name (str "~" name))))
@@ -100,7 +100,7 @@
;; (env-components env) → list of component names in env
;; (component-set-deps! comp deps) → store deps on component
(define compute-all-deps
(define compute-all-deps :effects [mutation]
(fn ((env :as dict))
(for-each
(fn ((name :as string))
@@ -119,7 +119,7 @@
;; Platform interface:
;; (regex-find-all pattern source) → list of matched group strings
(define scan-components-from-source
(define scan-components-from-source :effects []
(fn ((source :as string))
(let ((matches (regex-find-all "\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)" source)))
(map (fn ((m :as string)) (str "~" m)) matches))))
@@ -131,7 +131,7 @@
;; Scans page source for direct component references, then computes
;; the transitive closure. Returns list of ~names.
(define components-needed
(define components-needed :effects []
(fn ((page-source :as string) (env :as dict))
(let ((direct (scan-components-from-source page-source))
(all-needed (list)))
@@ -165,7 +165,7 @@
;;
;; This replaces the "send everything" approach with per-page bundles.
(define page-component-bundle
(define page-component-bundle :effects []
(fn ((page-source :as string) (env :as dict))
(components-needed page-source env)))
@@ -180,7 +180,7 @@
;; (component-css-classes c) → set/list of class strings
;; (scan-css-classes source) → set/list of class strings from source
(define page-css-classes
(define page-css-classes :effects []
(fn ((page-source :as string) (env :as dict))
(let ((needed (components-needed page-source env))
(classes (list)))
@@ -218,7 +218,7 @@
;; (component-io-refs c) → cached IO ref list (may be empty)
;; (component-set-io-refs! c r) → cache IO refs on component
(define scan-io-refs-walk
(define scan-io-refs-walk :effects []
(fn (node (io-names :as list) (refs :as list))
(cond
;; Symbol → check if name is in the IO set
@@ -241,7 +241,7 @@
:else nil)))
(define scan-io-refs
(define scan-io-refs :effects []
(fn (node (io-names :as list))
(let ((refs (list)))
(scan-io-refs-walk node io-names refs)
@@ -252,7 +252,7 @@
;; 9. Transitive IO refs — follow component deps and union IO refs
;; --------------------------------------------------------------------------
(define transitive-io-refs-walk
(define transitive-io-refs-walk :effects []
(fn ((n :as string) (seen :as list) (all-refs :as list) (env :as dict) (io-names :as list))
(when (not (contains? seen n))
(append! seen n)
@@ -285,7 +285,7 @@
:else nil)))))
(define transitive-io-refs
(define transitive-io-refs :effects []
(fn ((name :as string) (env :as dict) (io-names :as list))
(let ((all-refs (list))
(seen (list))
@@ -298,7 +298,7 @@
;; 10. Compute IO refs for all components in an environment
;; --------------------------------------------------------------------------
(define compute-all-io-refs
(define compute-all-io-refs :effects [mutation]
(fn ((env :as dict) (io-names :as list))
(for-each
(fn ((name :as string))
@@ -308,7 +308,7 @@
(env-components env))))
(define component-io-refs-cached
(define component-io-refs-cached :effects []
(fn ((name :as string) (env :as dict) (io-names :as list))
(let ((key (if (starts-with? name "~") name (str "~" name))))
(let ((val (env-get env key)))
@@ -319,7 +319,7 @@
;; Fallback: not yet cached (shouldn't happen after compute-all-io-refs)
(transitive-io-refs name env io-names))))))
(define component-pure?
(define component-pure? :effects []
(fn ((name :as string) (env :as dict) (io-names :as list))
(let ((key (if (starts-with? name "~") name (str "~" name))))
(let ((val (env-get env key)))
@@ -343,7 +343,7 @@
;;
;; Returns: "server" | "client"
(define render-target
(define render-target :effects []
(fn ((name :as string) (env :as dict) (io-names :as list))
(let ((key (if (starts-with? name "~") name (str "~" name))))
(let ((val (env-get env key)))
@@ -372,7 +372,7 @@
;; The async evaluator and client router both use it to make decisions
;; without recomputing at every request.
(define page-render-plan
(define page-render-plan :effects []
(fn ((page-source :as string) (env :as dict) (io-names :as list))
(let ((needed (components-needed page-source env))
(comp-targets (dict))
@@ -450,7 +450,7 @@
;; --------------------------------------------------------------------------
;; Moved from platform to spec: pure logic using type predicates.
(define env-components
(define env-components :effects []
(fn ((env :as dict))
(filter
(fn ((k :as string))

View File

@@ -31,7 +31,7 @@
;; Parses the sx-trigger attribute value into a list of trigger descriptors.
;; Each descriptor is a dict with "event" and "modifiers" keys.
(define parse-time
(define parse-time :effects []
(fn ((s :as string))
;; Parse time string: "2s" → 2000, "500ms" → 500
;; Uses nested if (not cond) because cond misclassifies 2-element
@@ -42,7 +42,7 @@
(parse-int s 0))))))
(define parse-trigger-spec
(define parse-trigger-spec :effects []
(fn ((spec :as string))
;; Parse "click delay:500ms once,change" → list of trigger descriptors
(if (nil? spec)
@@ -80,7 +80,7 @@
raw-parts))))))
(define default-trigger
(define default-trigger :effects []
(fn ((tag-name :as string))
;; Default trigger for element type
(cond
@@ -98,7 +98,7 @@
;; Verb extraction
;; --------------------------------------------------------------------------
(define get-verb-info
(define get-verb-info :effects [io]
(fn (el)
;; Check element for sx-get, sx-post, etc. Returns (dict "method" "url") or nil.
(some
@@ -114,7 +114,7 @@
;; Request header building
;; --------------------------------------------------------------------------
(define build-request-headers
(define build-request-headers :effects [io]
(fn (el (loaded-components :as list) (css-hash :as string))
;; Build the SX request headers dict
(let ((headers (dict
@@ -150,7 +150,7 @@
;; Response header processing
;; --------------------------------------------------------------------------
(define process-response-headers
(define process-response-headers :effects []
(fn ((get-header :as lambda))
;; Extract all SX response header directives into a dict.
;; get-header is (fn (name) → string or nil).
@@ -174,7 +174,7 @@
;; Swap specification parsing
;; --------------------------------------------------------------------------
(define parse-swap-spec
(define parse-swap-spec :effects []
(fn ((raw-swap :as string) (global-transitions? :as boolean))
;; Parse "innerHTML transition:true" → dict with style + transition flag
(let ((parts (split (or raw-swap DEFAULT_SWAP) " "))
@@ -193,7 +193,7 @@
;; Retry logic
;; --------------------------------------------------------------------------
(define parse-retry-spec
(define parse-retry-spec :effects []
(fn ((retry-attr :as string))
;; Parse "exponential:1000:30000" → spec dict or nil
(if (nil? retry-attr)
@@ -205,7 +205,7 @@
"cap-ms" (parse-int (nth parts 2) 30000))))))
(define next-retry-ms
(define next-retry-ms :effects []
(fn ((current-ms :as number) (cap-ms :as number))
;; Exponential backoff: double current, cap at max
(min (* current-ms 2) cap-ms)))
@@ -215,7 +215,7 @@
;; Form parameter filtering
;; --------------------------------------------------------------------------
(define filter-params
(define filter-params :effects []
(fn ((params-spec :as string) (all-params :as list))
;; Filter form parameters by sx-params spec.
;; all-params is a list of (key value) pairs.
@@ -239,7 +239,7 @@
;; Target resolution
;; --------------------------------------------------------------------------
(define resolve-target
(define resolve-target :effects [io]
(fn (el)
;; Resolve the swap target for an element
(let ((sel (dom-get-attr el "sx-target")))
@@ -253,7 +253,7 @@
;; Optimistic updates
;; --------------------------------------------------------------------------
(define apply-optimistic
(define apply-optimistic :effects [mutation io]
(fn (el)
;; Apply optimistic update preview. Returns state for reverting, or nil.
(let ((directive (dom-get-attr el "sx-optimistic")))
@@ -278,7 +278,7 @@
state)))))
(define revert-optimistic
(define revert-optimistic :effects [mutation io]
(fn ((state :as dict))
;; Revert an optimistic update
(when state
@@ -299,7 +299,7 @@
;; Out-of-band swap identification
;; --------------------------------------------------------------------------
(define find-oob-swaps
(define find-oob-swaps :effects [mutation io]
(fn (container)
;; Find elements marked for out-of-band swapping.
;; Returns list of (dict "element" el "swap-type" type "target-id" id).
@@ -329,7 +329,7 @@
;; preserving event listeners, focus, scroll position, and form state
;; on keyed (id) elements.
(define morph-node
(define morph-node :effects [mutation io]
(fn (old-node new-node)
;; Morph old-node to match new-node, preserving listeners/state.
(cond
@@ -371,7 +371,7 @@
(morph-children old-node new-node))))))
(define sync-attrs
(define sync-attrs :effects [mutation io]
(fn (old-el new-el)
;; Sync attributes from new to old, but skip reactively managed attrs.
;; data-sx-reactive-attrs="style,class" means those attrs are owned by
@@ -398,7 +398,7 @@
(dom-attr-list old-el)))))
(define morph-children
(define morph-children :effects [mutation io]
(fn (old-parent new-parent)
;; Reconcile children of old-parent to match new-parent.
;; Keyed elements (with id) are matched and moved in-place.
@@ -472,7 +472,7 @@
;; - Lakes = server substance (content, morphed)
;; - The morph = Aufhebung (cancellation/preservation/elevation of both)
(define morph-island-children
(define morph-island-children :effects [mutation io]
(fn (old-island new-island)
;; Find all lake and marsh slots in both old and new islands
(let ((old-lakes (dom-query-all old-island "[data-sx-lake]"))
@@ -522,7 +522,7 @@
;; as SX and rendered in the island's signal context. If the marsh has a
;; :transform function, it reshapes the content before evaluation.
(define morph-marsh
(define morph-marsh :effects [mutation io]
(fn (old-marsh new-marsh island-el)
(let ((transform (dom-get-data old-marsh "sx-marsh-transform"))
(env (dom-get-data old-marsh "sx-marsh-env"))
@@ -555,7 +555,7 @@
;;
;; Values are JSON-parsed: "7" → 7, "\"hello\"" → "hello", "true" → true.
(define process-signal-updates
(define process-signal-updates :effects [mutation io]
(fn (root)
(let ((signal-els (dom-query-all root "[data-sx-signal]")))
(for-each
@@ -576,7 +576,7 @@
;; Swap dispatch
;; --------------------------------------------------------------------------
(define swap-dom-nodes
(define swap-dom-nodes :effects [mutation io]
(fn (target new-nodes (strategy :as string))
;; Execute a swap strategy on live DOM nodes.
;; new-nodes is typically a DocumentFragment or Element.
@@ -630,7 +630,7 @@
(morph-children target wrapper))))))
(define insert-remaining-siblings
(define insert-remaining-siblings :effects [mutation io]
(fn (parent ref-node sib)
;; Insert sibling chain after ref-node
(when sib
@@ -643,7 +643,7 @@
;; String-based swap (fallback for HTML responses)
;; --------------------------------------------------------------------------
(define swap-html-string
(define swap-html-string :effects [mutation io]
(fn (target (html :as string) (strategy :as string))
;; Execute a swap strategy using an HTML string (DOMParser pipeline).
(case strategy
@@ -674,7 +674,7 @@
;; History management
;; --------------------------------------------------------------------------
(define handle-history
(define handle-history :effects [io]
(fn (el (url :as string) (resp-headers :as dict))
;; Process history push/replace based on element attrs and response headers
(let ((push-url (dom-get-attr el "sx-push-url"))
@@ -700,7 +700,7 @@
(define PRELOAD_TTL 30000) ;; 30 seconds
(define preload-cache-get
(define preload-cache-get :effects [mutation]
(fn ((cache :as dict) (url :as string))
;; Get and consume a cached preload response.
;; Returns (dict "text" ... "content-type" ...) or nil.
@@ -712,7 +712,7 @@
(do (dict-delete! cache url) entry))))))
(define preload-cache-set
(define preload-cache-set :effects [mutation]
(fn ((cache :as dict) (url :as string) (text :as string) (content-type :as string))
;; Store a preloaded response
(dict-set! cache url
@@ -725,7 +725,7 @@
;; Maps trigger event names to binding strategies.
;; This is the logic; actual browser event binding is platform interface.
(define classify-trigger
(define classify-trigger :effects []
(fn ((trigger :as dict))
;; Classify a parsed trigger descriptor for binding.
;; Returns one of: "poll", "intersect", "load", "revealed", "event"
@@ -742,7 +742,7 @@
;; Boost logic
;; --------------------------------------------------------------------------
(define should-boost-link?
(define should-boost-link? :effects [io]
(fn (link)
;; Whether a link inside an sx-boost container should be boosted
(let ((href (dom-get-attr link "href")))
@@ -756,7 +756,7 @@
(not (dom-has-attr? link "sx-disable"))))))
(define should-boost-form?
(define should-boost-form? :effects [io]
(fn (form)
;; Whether a form inside an sx-boost container should be boosted
(and (not (dom-has-attr? form "sx-get"))
@@ -768,7 +768,7 @@
;; SSE event classification
;; --------------------------------------------------------------------------
(define parse-sse-swap
(define parse-sse-swap :effects [io]
(fn (el)
;; Parse sx-sse-swap attribute
;; Returns event name to listen for (default "message")

View File

@@ -1318,10 +1318,15 @@
(define js-emit-define
(fn (expr)
;; Handle (define name :effects [...] value) — skip :effects annotation
(let ((name (if (= (type-of (nth expr 1)) "symbol")
(symbol-name (nth expr 1))
(str (nth expr 1))))
(val-expr (nth expr 2)))
(val-expr (if (and (>= (len expr) 5)
(= (type-of (nth expr 2)) "keyword")
(= (keyword-name (nth expr 2)) "effects"))
(nth expr 4)
(nth expr 2))))
(if (nil? val-expr)
(str "var " (js-mangle name) " = NIL;")
;; Detect zero-arg self-tail-recursive functions → while loops

View File

@@ -33,7 +33,7 @@
;; Event dispatch helpers
;; --------------------------------------------------------------------------
(define dispatch-trigger-events
(define dispatch-trigger-events :effects [mutation io]
(fn (el (header-val :as string))
;; Dispatch events from SX-Trigger / SX-Trigger-After-Swap headers.
;; Value can be JSON object (name → detail) or comma-separated names.
@@ -58,7 +58,7 @@
;; CSS tracking
;; --------------------------------------------------------------------------
(define init-css-tracking
(define init-css-tracking :effects [mutation io]
(fn ()
;; Read initial CSS hash from meta tag
(let ((meta (dom-query "meta[name=\"sx-css-classes\"]")))
@@ -72,7 +72,7 @@
;; Request execution
;; --------------------------------------------------------------------------
(define execute-request
(define execute-request :effects [mutation io]
(fn (el (verbInfo :as dict) (extraParams :as dict))
;; Gate checks then delegate to do-fetch.
;; verbInfo: dict with "method" and "url" (or nil to read from element).
@@ -105,7 +105,7 @@
extraParams))))))))))))
(define do-fetch
(define do-fetch :effects [mutation io]
(fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict))
;; Execute the actual fetch. Manages abort, headers, body, loading state.
(let ((sync (dom-get-attr el "sx-sync")))
@@ -201,7 +201,7 @@
(dict "error" err))))))))))))
(define handle-fetch-success
(define handle-fetch-success :effects [mutation io]
(fn (el (url :as string) (verb :as string) (extraParams :as dict) get-header (text :as string))
;; Route a successful response through the appropriate handler.
(let ((resp-headers (process-response-headers get-header)))
@@ -269,7 +269,7 @@
(dict "target" target-el "swap" swap-style)))))))
(define handle-sx-response
(define handle-sx-response :effects [mutation io]
(fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean))
;; Handle SX-format response: strip components, extract CSS, render, swap.
(let ((cleaned (strip-component-scripts text)))
@@ -300,7 +300,7 @@
(post-swap target)))))))))))
(define handle-html-response
(define handle-html-response :effects [mutation io]
(fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean))
;; Handle HTML-format response: parse, OOB, select, swap.
(let ((doc (dom-parse-html-document text)))
@@ -337,7 +337,7 @@
;; Retry
;; --------------------------------------------------------------------------
(define handle-retry
(define handle-retry :effects [mutation io]
(fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict))
;; Handle retry on failure if sx-retry is configured
(let ((retry-attr (dom-get-attr el "sx-retry"))
@@ -357,7 +357,7 @@
;; Trigger binding
;; --------------------------------------------------------------------------
(define bind-triggers
(define bind-triggers :effects [mutation io]
(fn (el (verbInfo :as dict))
;; Bind triggers from sx-trigger attribute (or defaults)
(let ((triggers (or (parse-trigger-spec (dom-get-attr el "sx-trigger"))
@@ -392,7 +392,7 @@
triggers))))
(define bind-event
(define bind-event :effects [mutation io]
(fn (el (event-name :as string) (mods :as dict) (verbInfo :as dict))
;; Bind a standard DOM event trigger.
;; Handles delay, once, changed, optimistic, preventDefault.
@@ -453,7 +453,7 @@
;; Post-swap lifecycle
;; --------------------------------------------------------------------------
(define post-swap
(define post-swap :effects [mutation io]
(fn (root)
;; Run lifecycle after swap: activate scripts, process SX, hydrate, process
(activate-scripts root)
@@ -474,7 +474,7 @@
;;
;; Example: (button :sx-get "/search" :sx-on-settle "(reset! (use-store \"count\") 0)")
(define process-settle-hooks
(define process-settle-hooks :effects [mutation io]
(fn (el)
(let ((settle-expr (dom-get-attr el "sx-on-settle")))
(when (and settle-expr (not (empty? settle-expr)))
@@ -484,7 +484,7 @@
exprs))))))
(define activate-scripts
(define activate-scripts :effects [mutation io]
(fn (root)
;; Re-activate scripts in swapped content.
;; Scripts inserted via innerHTML are inert — clone to make them execute.
@@ -505,7 +505,7 @@
;; OOB swap processing
;; --------------------------------------------------------------------------
(define process-oob-swaps
(define process-oob-swaps :effects [mutation io]
(fn (container (swap-fn :as lambda))
;; Find and process out-of-band swaps in container.
;; swap-fn is (fn (target oob-element swap-type) ...).
@@ -529,7 +529,7 @@
;; Head element hoisting
;; --------------------------------------------------------------------------
(define hoist-head-elements
(define hoist-head-elements :effects [mutation io]
(fn (container)
;; Move style[data-sx-css] and link[rel=stylesheet] to <head>
;; so they take effect globally.
@@ -551,7 +551,7 @@
;; Boost processing
;; --------------------------------------------------------------------------
(define process-boosted
(define process-boosted :effects [mutation io]
(fn (root)
;; Find [sx-boost] containers and boost their descendants
(for-each
@@ -560,7 +560,7 @@
(dom-query-all (or root (dom-body)) "[sx-boost]"))))
(define boost-descendants
(define boost-descendants :effects [mutation io]
(fn (container)
;; Boost links and forms within a container.
;; The sx-boost attribute value is the default target selector
@@ -609,7 +609,7 @@
(define _page-data-cache (dict))
(define _page-data-cache-ttl 30000) ;; 30 seconds in ms
(define page-data-cache-key
(define page-data-cache-key :effects []
(fn ((page-name :as string) (params :as dict))
;; Build a cache key from page name + params.
;; Params are from route matching so order is deterministic.
@@ -623,7 +623,7 @@
(keys params))
(str base ":" (join "&" parts)))))))
(define page-data-cache-get
(define page-data-cache-get :effects [mutation io]
(fn ((cache-key :as string))
;; Return cached data if fresh, else nil.
(let ((entry (get _page-data-cache cache-key)))
@@ -635,7 +635,7 @@
nil)
(get entry "data"))))))
(define page-data-cache-set
(define page-data-cache-set :effects [mutation io]
(fn ((cache-key :as string) data)
;; Store data with current timestamp.
(dict-set! _page-data-cache cache-key
@@ -646,7 +646,7 @@
;; Client-side routing — cache management
;; --------------------------------------------------------------------------
(define invalidate-page-cache
(define invalidate-page-cache :effects [mutation io]
(fn ((page-name :as string))
;; Clear cached data for a page. Removes all cache entries whose key
;; matches page-name (exact) or starts with "page-name:" (with params).
@@ -659,14 +659,14 @@
(sw-post-message {"type" "invalidate" "page" page-name})
(log-info (str "sx:cache invalidate " page-name))))
(define invalidate-all-page-cache
(define invalidate-all-page-cache :effects [mutation io]
(fn ()
;; Clear all cached page data and notify service worker.
(set! _page-data-cache (dict))
(sw-post-message {"type" "invalidate" "page" "*"})
(log-info "sx:cache invalidate *")))
(define update-page-cache
(define update-page-cache :effects [mutation io]
(fn ((page-name :as string) data)
;; Replace cached data for a page with server-provided data.
;; Uses a bare page-name key (no params) — the server knows the
@@ -675,7 +675,7 @@
(page-data-cache-set cache-key data)
(log-info (str "sx:cache update " page-name)))))
(define process-cache-directives
(define process-cache-directives :effects [mutation io]
(fn (el (resp-headers :as dict) (response-text :as string))
;; Process cache invalidation and update directives from both
;; element attributes and response headers.
@@ -721,7 +721,7 @@
(define _optimistic-snapshots (dict))
(define optimistic-cache-update
(define optimistic-cache-update :effects [mutation]
(fn ((cache-key :as string) (mutator :as lambda))
;; Apply predicted mutation to cached data. Saves snapshot for rollback.
;; Returns predicted data or nil if no cached data exists.
@@ -734,7 +734,7 @@
(page-data-cache-set cache-key predicted)
predicted)))))
(define optimistic-cache-revert
(define optimistic-cache-revert :effects [mutation]
(fn ((cache-key :as string))
;; Revert to pre-mutation snapshot. Returns restored data or nil.
(let ((snapshot (get _optimistic-snapshots cache-key)))
@@ -743,12 +743,12 @@
(dict-delete! _optimistic-snapshots cache-key)
snapshot))))
(define optimistic-cache-confirm
(define optimistic-cache-confirm :effects [mutation]
(fn ((cache-key :as string))
;; Server accepted — discard the rollback snapshot.
(dict-delete! _optimistic-snapshots cache-key)))
(define submit-mutation
(define submit-mutation :effects [mutation io]
(fn ((page-name :as string) (params :as dict) (action-name :as string) payload (mutator-fn :as lambda) (on-complete :as lambda))
;; Optimistic mutation: predict locally, send to server, confirm or revert.
;; on-complete is called with "confirmed" or "reverted" status.
@@ -787,14 +787,14 @@
(define _is-online true)
(define _offline-queue (list))
(define offline-is-online?
(define offline-is-online? :effects [io]
(fn () _is-online))
(define offline-set-online!
(define offline-set-online! :effects [mutation]
(fn ((val :as boolean))
(set! _is-online val)))
(define offline-queue-mutation
(define offline-queue-mutation :effects [mutation io]
(fn ((action-name :as string) payload (page-name :as string) (params :as dict) (mutator-fn :as lambda))
;; Queue a mutation for later sync. Apply optimistic update locally.
(let ((cache-key (page-data-cache-key page-name params))
@@ -813,7 +813,7 @@
(log-info (str "sx:offline queued " action-name " (" (len _offline-queue) " pending)"))
entry)))
(define offline-sync
(define offline-sync :effects [mutation io]
(fn ()
;; Replay all pending mutations. Called on reconnect.
(let ((pending (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue)))
@@ -830,11 +830,11 @@
(log-warn (str "sx:offline sync failed " (get entry "action") ": " error)))))
pending)))))
(define offline-pending-count
(define offline-pending-count :effects [io]
(fn ()
(len (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue))))
(define offline-aware-mutation
(define offline-aware-mutation :effects [mutation io]
(fn ((page-name :as string) (params :as dict) (action-name :as string) payload (mutator-fn :as lambda) (on-complete :as lambda))
;; Top-level mutation function. Routes to submit-mutation when online,
;; offline-queue-mutation when offline.
@@ -849,7 +849,7 @@
;; Client-side routing
;; --------------------------------------------------------------------------
(define current-page-layout
(define current-page-layout :effects [io]
(fn ()
;; Find the layout name of the currently displayed page by matching
;; the browser URL against the page route table.
@@ -859,7 +859,7 @@
(or (get match "layout") "")))))
(define swap-rendered-content
(define swap-rendered-content :effects [mutation io]
(fn (target rendered (pathname :as string))
;; Swap rendered DOM content into target and run post-processing.
;; Shared by pure and data page client routes.
@@ -875,7 +875,7 @@
(log-info (str "sx:route client " pathname)))))
(define resolve-route-target
(define resolve-route-target :effects [io]
(fn ((target-sel :as string))
;; Resolve a target selector to a DOM element, or nil.
(if (and target-sel (not (= target-sel "true")))
@@ -883,7 +883,7 @@
nil)))
(define deps-satisfied?
(define deps-satisfied? :effects [io]
(fn ((match :as dict))
;; Check if all component deps for a page are loaded client-side.
(let ((deps (get match "deps"))
@@ -893,7 +893,7 @@
(every? (fn ((dep :as string)) (contains? loaded dep)) deps)))))
(define try-client-route
(define try-client-route :effects [mutation io]
(fn ((pathname :as string) (target-sel :as string))
;; Try to render a page client-side. Returns true if successful, false otherwise.
;; target-sel is the CSS selector for the swap target (from sx-boost value).
@@ -1011,7 +1011,7 @@
true))))))))))))))))))
(define bind-client-route-link
(define bind-client-route-link :effects [mutation io]
(fn (link (href :as string))
;; Bind a boost link with client-side routing. If the route can be
;; rendered client-side (pure page, no :data), do so. Otherwise
@@ -1026,7 +1026,7 @@
;; SSE processing
;; --------------------------------------------------------------------------
(define process-sse
(define process-sse :effects [mutation io]
(fn (root)
;; Find and bind SSE elements
(for-each
@@ -1037,7 +1037,7 @@
(dom-query-all (or root (dom-body)) "[sx-sse]"))))
(define bind-sse
(define bind-sse :effects [mutation io]
(fn (el)
;; Connect to SSE endpoint and bind swap handler
(let ((url (dom-get-attr el "sx-sse")))
@@ -1049,7 +1049,7 @@
(bind-sse-swap el data))))))))
(define bind-sse-swap
(define bind-sse-swap :effects [mutation io]
(fn (el (data :as string))
;; Handle an SSE event: swap data into element
(let ((target (resolve-target el))
@@ -1081,7 +1081,7 @@
;; Inline event handlers
;; --------------------------------------------------------------------------
(define bind-inline-handlers
(define bind-inline-handlers :effects [mutation io]
(fn (root)
;; Find elements with sx-on:* attributes and bind SX event handlers.
;; Handler bodies are SX expressions evaluated with `event` and `this`
@@ -1115,7 +1115,7 @@
;; Preload
;; --------------------------------------------------------------------------
(define bind-preload-for
(define bind-preload-for :effects [mutation io]
(fn (el)
;; Bind preload event listeners based on sx-preload attribute
(let ((preload-attr (dom-get-attr el "sx-preload")))
@@ -1134,7 +1134,7 @@
(loaded-component-names) _css-hash)))))))))))
(define do-preload
(define do-preload :effects [mutation io]
(fn ((url :as string) (headers :as dict))
;; Execute a preload fetch into the cache
(when (nil? (preload-cache-get _preload-cache url))
@@ -1148,7 +1148,7 @@
(define VERB_SELECTOR
(str "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]"))
(define process-elements
(define process-elements :effects [mutation io]
(fn (root)
;; Find all elements with sx-* verb attributes and process them.
(let ((els (dom-query-all (or root (dom-body)) VERB_SELECTOR)))
@@ -1165,7 +1165,7 @@
(process-emit-elements root)))
(define process-one
(define process-one :effects [mutation io]
(fn (el)
;; Process a single element with an sx-* verb attribute
(let ((verb-info (get-verb-info el)))
@@ -1193,7 +1193,7 @@
;; On click → dispatches CustomEvent "cart:add" with detail {id:42, name:"Widget"}
;; The event bubbles up to the island container where bridge-event catches it.
(define process-emit-elements
(define process-emit-elements :effects [mutation io]
(fn (root)
(let ((els (dom-query-all (or root (dom-body)) "[data-sx-emit]")))
(for-each
@@ -1214,7 +1214,7 @@
;; History: popstate handler
;; --------------------------------------------------------------------------
(define handle-popstate
(define handle-popstate :effects [mutation io]
(fn ((scrollY :as number))
;; Handle browser back/forward navigation.
;; Derive target from [sx-boost] container or fall back to #main-panel.
@@ -1241,7 +1241,7 @@
;; Initialization
;; --------------------------------------------------------------------------
(define engine-init
(define engine-init :effects [mutation io]
(fn ()
;; Initialize: CSS tracking, scripts, hydrate, process.
(do

View File

@@ -49,20 +49,20 @@
;; --------------------------------------------------------------------------
;; Returns a list of top-level AST expressions.
(define sx-parse
(define sx-parse :effects []
(fn ((source :as string))
(let ((pos 0)
(len-src (len source)))
;; -- Cursor helpers (closure over pos, source, len-src) --
(define skip-comment
(define skip-comment :effects []
(fn ()
(when (and (< pos len-src) (not (= (nth source pos) "\n")))
(set! pos (inc pos))
(skip-comment))))
(define skip-ws
(define skip-ws :effects []
(fn ()
(when (< pos len-src)
(let ((ch (nth source pos)))
@@ -80,11 +80,11 @@
;; -- Atom readers --
(define read-string
(define read-string :effects []
(fn ()
(set! pos (inc pos)) ;; skip opening "
(let ((buf ""))
(define read-str-loop
(define read-str-loop :effects []
(fn ()
(if (>= pos len-src)
(error "Unterminated string")
@@ -110,10 +110,10 @@
(read-str-loop)
buf)))
(define read-ident
(define read-ident :effects []
(fn ()
(let ((start pos))
(define read-ident-loop
(define read-ident-loop :effects []
(fn ()
(when (and (< pos len-src)
(ident-char? (nth source pos)))
@@ -122,19 +122,19 @@
(read-ident-loop)
(slice source start pos))))
(define read-keyword
(define read-keyword :effects []
(fn ()
(set! pos (inc pos)) ;; skip :
(make-keyword (read-ident))))
(define read-number
(define read-number :effects []
(fn ()
(let ((start pos))
;; Optional leading minus
(when (and (< pos len-src) (= (nth source pos) "-"))
(set! pos (inc pos)))
;; Integer digits
(define read-digits
(define read-digits :effects []
(fn ()
(when (and (< pos len-src)
(let ((c (nth source pos)))
@@ -158,7 +158,7 @@
(read-digits))
(parse-number (slice source start pos)))))
(define read-symbol
(define read-symbol :effects []
(fn ()
(let ((name (read-ident)))
(cond
@@ -169,10 +169,10 @@
;; -- Composite readers --
(define read-list
(define read-list :effects []
(fn ((close-ch :as string))
(let ((items (list)))
(define read-list-loop
(define read-list-loop :effects []
(fn ()
(skip-ws)
(if (>= pos len-src)
@@ -184,10 +184,10 @@
(read-list-loop)
items)))
(define read-map
(define read-map :effects []
(fn ()
(let ((result (dict)))
(define read-map-loop
(define read-map-loop :effects []
(fn ()
(skip-ws)
(if (>= pos len-src)
@@ -206,10 +206,10 @@
;; -- Raw string reader (for #|...|) --
(define read-raw-string
(define read-raw-string :effects []
(fn ()
(let ((buf ""))
(define raw-loop
(define raw-loop :effects []
(fn ()
(if (>= pos len-src)
(error "Unterminated raw string")
@@ -224,7 +224,7 @@
;; -- Main expression reader --
(define read-expr
(define read-expr :effects []
(fn ()
(skip-ws)
(if (>= pos len-src)
@@ -322,7 +322,7 @@
;; -- Entry point: parse all top-level expressions --
(let ((exprs (list)))
(define parse-loop
(define parse-loop :effects []
(fn ()
(skip-ws)
(when (< pos len-src)
@@ -336,7 +336,7 @@
;; Serializer — AST → SX source text
;; --------------------------------------------------------------------------
(define sx-serialize
(define sx-serialize :effects []
(fn (val)
(case (type-of val)
"nil" "nil"
@@ -351,7 +351,7 @@
:else (str val))))
(define sx-serialize-dict
(define sx-serialize-dict :effects []
(fn ((d :as dict))
(str "{"
(join " "

View File

@@ -71,14 +71,14 @@
;; Shared utilities
;; --------------------------------------------------------------------------
(define definition-form?
(define definition-form? :effects []
(fn ((name :as string))
(or (= name "define") (= name "defcomp") (= name "defisland")
(= name "defmacro") (= name "defstyle") (= name "defhandler")
(= name "deftype") (= name "defeffect"))))
(define parse-element-args
(define parse-element-args :effects [render]
(fn ((args :as list) (env :as dict))
;; Parse (:key val :key2 val2 child1 child2) into (attrs-dict children-list)
(let ((attrs (dict))
@@ -101,7 +101,7 @@
(list attrs children))))
(define render-attrs
(define render-attrs :effects []
(fn ((attrs :as dict))
;; Render an attrs dict to an HTML attribute string.
;; Used by adapter-html.sx and adapter-sx.sx.
@@ -133,13 +133,13 @@
;; eval-cond: find matching cond branch, return unevaluated body expr.
;; Handles both scheme-style ((test body) ...) and clojure-style
;; (test body test body ...).
(define eval-cond
(define eval-cond :effects []
(fn ((clauses :as list) (env :as dict))
(if (cond-scheme? clauses)
(eval-cond-scheme clauses env)
(eval-cond-clojure clauses env))))
(define eval-cond-scheme
(define eval-cond-scheme :effects []
(fn ((clauses :as list) (env :as dict))
(if (empty? clauses)
nil
@@ -156,7 +156,7 @@
body
(eval-cond-scheme (rest clauses) env)))))))
(define eval-cond-clojure
(define eval-cond-clojure :effects []
(fn ((clauses :as list) (env :as dict))
(if (< (len clauses) 2)
nil
@@ -173,7 +173,7 @@
;; process-bindings: evaluate let-binding pairs, return extended env.
;; bindings = ((name1 expr1) (name2 expr2) ...)
(define process-bindings
(define process-bindings :effects [mutation]
(fn ((bindings :as list) (env :as dict))
;; env-extend (not merge) — Env is not a dict subclass, so merge()
;; returns an empty dict, losing all parent scope bindings.
@@ -195,7 +195,7 @@
;; Used by eval-list to dispatch rendering forms to the active adapter
;; (HTML, SX wire, or DOM) rather than evaluating them as function calls.
(define is-render-expr?
(define is-render-expr? :effects []
(fn (expr)
(if (or (not (= (type-of expr) "list")) (empty? expr))
false

View File

@@ -17,7 +17,7 @@
;; "/" → ()
;; "/docs/" → ("docs")
(define split-path-segments
(define split-path-segments :effects []
(fn ((path :as string))
(let ((trimmed (if (starts-with? path "/") (slice path 1) path)))
(let ((trimmed2 (if (and (not (empty? trimmed))
@@ -35,7 +35,7 @@
;; "/docs/<slug>" → ({"type" "literal" "value" "docs"}
;; {"type" "param" "value" "slug"})
(define make-route-segment
(define make-route-segment :effects []
(fn ((seg :as string))
(if (and (starts-with? seg "<") (ends-with? seg ">"))
(let ((param-name (slice seg 1 (- (len seg) 1))))
@@ -48,7 +48,7 @@
(dict-set! d "value" seg)
d))))
(define parse-route-pattern
(define parse-route-pattern :effects []
(fn ((pattern :as string))
(let ((segments (split-path-segments pattern)))
(map make-route-segment segments))))
@@ -59,7 +59,7 @@
;; --------------------------------------------------------------------------
;; Returns params dict if match, nil if no match.
(define match-route-segments
(define match-route-segments :effects []
(fn ((path-segs :as list) (parsed-segs :as list))
(if (not (= (len path-segs) (len parsed-segs)))
nil
@@ -87,7 +87,7 @@
;; --------------------------------------------------------------------------
;; Returns params dict (may be empty for exact matches) or nil.
(define match-route
(define match-route :effects []
(fn ((path :as string) (pattern :as string))
(let ((path-segs (split-path-segments path))
(parsed-segs (parse-route-pattern pattern)))
@@ -100,7 +100,7 @@
;; Each entry: {"pattern" "/docs/<slug>" "parsed" [...] "name" "docs-page" ...}
;; Returns matching entry with "params" added, or nil.
(define find-matching-route
(define find-matching-route :effects []
(fn ((path :as string) (routes :as list))
(let ((path-segs (split-path-segments path))
(result nil))

View File

@@ -41,7 +41,7 @@
;; 1. signal — create a reactive container
;; --------------------------------------------------------------------------
(define signal
(define signal :effects []
(fn ((initial-value :as any))
(make-signal initial-value)))
@@ -54,7 +54,7 @@
;; signal as a dependency. Outside reactive context, deref just returns
;; the current value — no subscription, no overhead.
(define deref
(define deref :effects []
(fn ((s :as any))
(if (not (signal? s))
s ;; non-signal values pass through
@@ -71,7 +71,7 @@
;; 3. reset! — write a new value, notify subscribers
;; --------------------------------------------------------------------------
(define reset!
(define reset! :effects [mutation]
(fn ((s :as signal) value)
(when (signal? s)
(let ((old (signal-value s)))
@@ -84,7 +84,7 @@
;; 4. swap! — update signal via function
;; --------------------------------------------------------------------------
(define swap!
(define swap! :effects [mutation]
(fn ((s :as signal) (f :as lambda) &rest args)
(when (signal? s)
(let ((old (signal-value s))
@@ -102,7 +102,7 @@
;; of its dependencies change. The dependency set is discovered automatically
;; by tracking deref calls during evaluation.
(define computed
(define computed :effects [mutation]
(fn ((compute-fn :as lambda))
(let ((s (make-signal nil))
(deps (list))
@@ -145,7 +145,7 @@
;; Like computed, but doesn't produce a signal value. Returns a dispose
;; function that tears down the effect.
(define effect
(define effect :effects [mutation]
(fn ((effect-fn :as lambda))
(let ((deps (list))
(disposed false)
@@ -201,7 +201,7 @@
(define *batch-depth* 0)
(define *batch-queue* (list))
(define batch
(define batch :effects [mutation]
(fn ((thunk :as lambda))
(set! *batch-depth* (+ *batch-depth* 1))
(invoke thunk)
@@ -231,14 +231,14 @@
;;
;; If inside a batch, queues the signal. Otherwise, notifies immediately.
(define notify-subscribers
(define notify-subscribers :effects [mutation]
(fn ((s :as signal))
(if (> *batch-depth* 0)
(when (not (contains? *batch-queue* s))
(append! *batch-queue* s))
(flush-subscribers s))))
(define flush-subscribers
(define flush-subscribers :effects [mutation]
(fn ((s :as signal))
(for-each
(fn ((sub :as lambda)) (sub))
@@ -268,7 +268,7 @@
;; For computed signals, unsubscribe from all dependencies.
;; For effects, the dispose function is returned by effect itself.
(define dispose-computed
(define dispose-computed :effects [mutation]
(fn ((s :as signal))
(when (signal? s)
(for-each
@@ -287,7 +287,7 @@
(define *island-scope* nil)
(define with-island-scope
(define with-island-scope :effects [mutation]
(fn ((scope-fn :as lambda) (body-fn :as lambda))
(let ((prev *island-scope*))
(set! *island-scope* scope-fn)
@@ -299,7 +299,7 @@
;; The platform's make-signal should call (register-in-scope s) if
;; *island-scope* is non-nil.
(define register-in-scope
(define register-in-scope :effects [mutation]
(fn ((disposable :as lambda))
(when *island-scope*
(*island-scope* disposable))))
@@ -322,7 +322,7 @@
;; (dom-set-data el key val) → void — store JS value on element
;; (dom-get-data el key) → any — retrieve stored value
(define with-marsh-scope
(define with-marsh-scope :effects [mutation io]
(fn (marsh-el (body-fn :as lambda))
;; Execute body-fn collecting all disposables into a marsh-local list.
;; Nested under the current island scope — if the island is disposed,
@@ -335,7 +335,7 @@
;; Store disposers on the marsh element for later cleanup
(dom-set-data marsh-el "sx-marsh-disposers" disposers))))
(define dispose-marsh-scope
(define dispose-marsh-scope :effects [mutation io]
(fn (marsh-el)
;; Dispose all effects/computeds registered in this marsh's scope.
;; Parent island scope and sibling marshes are unaffected.
@@ -358,7 +358,7 @@
(define *store-registry* (dict))
(define def-store
(define def-store :effects [mutation]
(fn ((name :as string) (init-fn :as lambda))
(let ((registry *store-registry*))
;; Only create the store once — subsequent calls return existing
@@ -366,14 +366,14 @@
(set! *store-registry* (assoc registry name (invoke init-fn))))
(get *store-registry* name))))
(define use-store
(define use-store :effects []
(fn ((name :as string))
(if (has-key? *store-registry* name)
(get *store-registry* name)
(error (str "Store not found: " name
". Call (def-store ...) before (use-store ...).")))))
(define clear-stores
(define clear-stores :effects [mutation]
(fn ()
(set! *store-registry* (dict))))
@@ -401,11 +401,11 @@
;;
;; These are platform primitives because they require browser DOM APIs.
(define emit-event
(define emit-event :effects [io]
(fn (el (event-name :as string) detail)
(dom-dispatch el event-name detail)))
(define on-event
(define on-event :effects [io]
(fn (el (event-name :as string) (handler :as lambda))
(dom-listen el event-name handler)))
@@ -415,7 +415,7 @@
;; When the effect is disposed (island teardown), the listener is
;; removed automatically via the cleanup return.
(define bridge-event
(define bridge-event :effects [mutation io]
(fn (el (event-name :as string) (target-signal :as signal) transform-fn)
(effect (fn ()
(let ((remove (dom-listen el event-name
@@ -449,7 +449,7 @@
;; Platform interface required:
;; (promise-then promise on-resolve on-reject) → void
(define resource
(define resource :effects [mutation io]
(fn ((fetch-fn :as lambda))
(let ((state (signal (dict "loading" true "data" nil "error" nil))))
;; Kick off the async operation

View File

@@ -3937,4 +3937,4 @@ def render(expr, env=None):
def make_env(**kwargs):
"""Create an environment with initial bindings."""
return _Env(dict(kwargs))
return _Env(dict(kwargs))