From 2f42e8826caff4030c23abafb4b730bb7d3bfe85 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 11 Mar 2026 23:22:34 +0000 Subject: [PATCH] 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 --- shared/static/scripts/sx-browser.js | 2 +- shared/sx/ref/adapter-async.sx | 82 +++++++++++----------- shared/sx/ref/adapter-dom.sx | 44 ++++++------ shared/sx/ref/adapter-html.sx | 24 +++---- shared/sx/ref/adapter-sx.sx | 18 ++--- shared/sx/ref/boot.sx | 28 ++++---- shared/sx/ref/bootstrap_py.py | 14 +++- shared/sx/ref/deps.sx | 38 +++++------ shared/sx/ref/engine.sx | 60 ++++++++-------- shared/sx/ref/js.sx | 7 +- shared/sx/ref/orchestration.sx | 102 ++++++++++++++-------------- shared/sx/ref/parser.sx | 42 ++++++------ shared/sx/ref/render.sx | 16 ++--- shared/sx/ref/router.sx | 12 ++-- shared/sx/ref/signals.sx | 42 ++++++------ shared/sx/ref/sx_ref.py | 2 +- 16 files changed, 274 insertions(+), 259 deletions(-) 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
children
. ;; 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)) diff --git a/shared/sx/ref/adapter-html.sx b/shared/sx/ref/adapter-html.sx index fa44e2d..368dd89 100644 --- a/shared/sx/ref/adapter-html.sx +++ b/shared/sx/ref/adapter-html.sx @@ -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 diff --git a/shared/sx/ref/adapter-sx.sx b/shared/sx/ref/adapter-sx.sx index a97f5f6..f55da20 100644 --- a/shared/sx/ref/adapter-sx.sx +++ b/shared/sx/ref/adapter-sx.sx @@ -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 diff --git a/shared/sx/ref/boot.sx b/shared/sx/ref/boot.sx index 494bbf4..aca976a 100644 --- a/shared/sx/ref/boot.sx +++ b/shared/sx/ref/boot.sx @@ -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