Phase 7a: affinity annotations + fix parser escape sequences

Add :affinity :client/:server/:auto annotations to defcomp, with
render-target function combining affinity + IO analysis. Includes
spec (eval.sx, deps.sx), tests, Python evaluator, and demo page.

Fix critical bug: Python SX parser _ESCAPE_MAP was missing \r and \0,
causing bootstrapped JS parser to treat 'r' as whitespace — breaking
all client-side SX parsing. Also add \0 to JS string emitter and
fix serializer round-tripping for \r and \0.

Reserved word escaping: bootstrappers now auto-append _ to identifiers
colliding with JS/Python reserved words (e.g. default → default_,
final → final_), so the spec never needs to avoid host language keywords.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 23:53:33 +00:00
parent 81d8e55fb0
commit a70ff2b153
19 changed files with 540 additions and 224 deletions

View File

@@ -314,19 +314,47 @@
(empty? (transitive-io-refs name env io-names))))
;; --------------------------------------------------------------------------
;; 5. Render target — boundary decision per component
;; --------------------------------------------------------------------------
;; Combines IO analysis with affinity annotations to decide where a
;; component should render:
;;
;; :affinity :server → always "server" (auth-sensitive, secrets)
;; :affinity :client → "client" even if IO-dependent (IO proxy)
;; :affinity :auto → "server" if IO-dependent, "client" if pure
;;
;; Returns: "server" | "client"
(define render-target
(fn (name env io-names)
(let ((key (if (starts-with? name "~") name (str "~" name))))
(let ((val (env-get env key)))
(if (not (= (type-of val) "component"))
"server"
(let ((affinity (component-affinity val)))
(cond
(= affinity "server") "server"
(= affinity "client") "client"
;; auto: decide from IO analysis
(not (component-pure? name env io-names)) "server"
:else "client")))))))
;; --------------------------------------------------------------------------
;; Host obligation: selective expansion in async partial evaluation
;; --------------------------------------------------------------------------
;; The spec classifies components as pure or IO-dependent. Each host's
;; async partial evaluator (the server-side rendering path that bridges
;; sync evaluation with async IO) must use this classification:
;; The spec classifies components as pure or IO-dependent and provides
;; per-component render-target decisions. Each host's async partial
;; evaluator (the server-side rendering path that bridges sync evaluation
;; with async IO) must use this classification:
;;
;; IO-dependent component → expand server-side (IO must resolve)
;; Pure component → serialize for client (can render anywhere)
;; render-target "server" → expand server-side (IO must resolve)
;; render-target "client" → serialize for client (can render anywhere)
;; Layout slot context → expand all (server needs full HTML)
;;
;; The spec provides the data (component-io-refs, component-pure?).
;; The host provides the async runtime that acts on it.
;; The spec provides: component-io-refs, component-pure?, render-target,
;; component-affinity. The host provides the async runtime that acts on it.
;; This is not SX semantics — it is host infrastructure. Every host
;; with a server-side async evaluator implements the same rule.
;; --------------------------------------------------------------------------
@@ -349,6 +377,7 @@
;; (component-css-classes c)→ pre-scanned CSS class list
;; (component-io-refs c) → cached IO ref list (may be empty)
;; (component-set-io-refs! c r)→ cache IO refs on component
;; (component-affinity c) → "auto" | "client" | "server"
;; (macro-body m) → AST body of macro
;; (env-components env) → list of component names in env
;; (regex-find-all pat src) → list of capture group matches