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

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