Restructure SX ref spec into core + selectable adapters

Split monolithic render.sx into core (tag registries, shared utils) plus
four adapter .sx files: adapter-html (server HTML strings), adapter-sx
(SX wire format), adapter-dom (browser DOM nodes), and engine (SxEngine
triggers, morphing, swaps). All adapters written in s-expressions with
platform interface declarations for JS bridge functions.

Bootstrap compiler now accepts --adapters flag to emit targeted builds:
  -a html        → server-only (1108 lines)
  -a dom,engine  → browser-only (1634 lines)
  -a html,sx     → server with SX wire (1169 lines)
  (default)      → all adapters (1800 lines)

Fixes: keyword arg i-counter desync in reduce across all adapters,
render-aware special forms (let/if/when/cond/map) in HTML adapter,
component children double-escaping, ~prefixed macro dispatch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 11:49:44 +00:00
parent a9526c4fa1
commit daeecab310
8 changed files with 3083 additions and 384 deletions

View File

@@ -1,18 +1,17 @@
;; ==========================================================================
;; render.sx — Reference rendering specification
;; render.sx — Core rendering specification
;;
;; Defines how evaluated SX expressions become output (DOM nodes, HTML
;; strings, or SX wire format). Each target provides a renderer adapter
;; that implements the platform-specific output operations.
;; Shared registries and utilities used by all rendering adapters.
;; This file defines WHAT is renderable (tag registries, attribute rules)
;; and HOW arguments are parsed — but not the output format.
;;
;; Three rendering modes (matching the Python/JS implementations):
;; Adapters:
;; adapter-html.sx — HTML string output (server)
;; adapter-sx.sx — SX wire format output (server → client)
;; adapter-dom.sx — Live DOM node output (browser)
;;
;; 1. render-to-dom — produces DOM nodes (browser only)
;; 2. render-to-html — produces HTML string (server)
;; 3. render-to-sx — produces SX wire format (server → client)
;;
;; This file specifies the LOGIC of rendering. Platform-specific
;; operations are declared as interfaces at the bottom.
;; Each adapter imports these shared definitions and provides its own
;; render entry point (render-to-html, render-to-sx, render-to-dom).
;; ==========================================================================
@@ -69,104 +68,13 @@
;; --------------------------------------------------------------------------
;; render-to-html — server-side HTML rendering
;; Shared utilities
;; --------------------------------------------------------------------------
(define render-to-html
(fn (expr env)
(case (type-of expr)
;; Literals — render directly
"nil" ""
"string" (escape-html expr)
"number" (str expr)
"boolean" (if expr "true" "false")
;; List — dispatch to render-list which handles HTML tags, special forms, etc.
"list" (if (empty? expr) "" (render-list-to-html expr env))
;; Symbol — evaluate then render
"symbol" (render-value-to-html (trampoline (eval-expr expr env)) env)
;; Keyword — render as text
"keyword" (escape-html (keyword-name expr))
;; Everything else — evaluate first
:else (render-value-to-html (trampoline (eval-expr expr env)) env))))
(define render-value-to-html
(fn (val env)
(case (type-of val)
"nil" ""
"string" (escape-html val)
"number" (str val)
"boolean" (if val "true" "false")
"list" (render-list-to-html val env)
"raw-html" (raw-html-content val)
"style-value" (style-value-class val)
:else (escape-html (str val)))))
(define render-list-to-html
(fn (expr env)
(if (empty? expr)
""
(let ((head (first expr)))
(if (not (= (type-of head) "symbol"))
;; Data list — render each item
(join "" (map (fn (x) (render-value-to-html x env)) expr))
(let ((name (symbol-name head))
(args (rest expr)))
(cond
;; Fragment
(= name "<>")
(join "" (map (fn (x) (render-to-html x env)) args))
;; Raw HTML passthrough
(= name "raw!")
(join "" (map (fn (x) (str (trampoline (eval-expr x env)))) args))
;; HTML tag
(contains? HTML_TAGS name)
(render-html-element name args env)
;; Component call (~name)
(starts-with? name "~")
(let ((comp (env-get env name)))
(if (component? comp)
(render-to-html
(trampoline (call-component comp args env))
env)
(error (str "Unknown component: " name))))
;; Definitions — evaluate for side effects, render nothing
(or (= name "define") (= name "defcomp") (= name "defmacro")
(= name "defstyle") (= name "defkeyframes") (= name "defhandler"))
(do (trampoline (eval-expr expr env)) "")
;; Macro expansion
(and (env-has? env name) (macro? (env-get env name)))
(render-to-html
(trampoline
(eval-expr
(expand-macro (env-get env name) args env)
env))
env)
;; Special form / function call — evaluate then render result
:else
(render-value-to-html
(trampoline (eval-expr expr env))
env))))))))
(define render-html-element
(fn (tag args env)
(let ((parsed (parse-element-args args env))
(attrs (first parsed))
(children (nth parsed 1))
(is-void (contains? VOID_ELEMENTS tag)))
(str "<" tag
(render-attrs attrs)
(if is-void
" />"
(str ">"
(join "" (map (fn (c) (render-to-html c env)) children))
"</" tag ">"))))))
(define definition-form?
(fn (name)
(or (= name "define") (= name "defcomp") (= name "defmacro")
(= name "defstyle") (= name "defkeyframes") (= name "defhandler"))))
(define parse-element-args
@@ -178,7 +86,7 @@
(fn (state arg)
(let ((skip (get state "skip")))
(if skip
(assoc state "skip" false)
(assoc state "skip" false "i" (inc (get state "i")))
(if (and (= (type-of arg) "keyword")
(< (inc (get state "i")) (len args)))
(let ((val (trampoline (eval-expr (nth args (inc (get state "i"))) env))))
@@ -194,6 +102,8 @@
(define render-attrs
(fn (attrs)
;; Render an attrs dict to an HTML attribute string.
;; Used by adapter-html.sx and adapter-sx.sx.
(join ""
(map
(fn (key)
@@ -215,141 +125,14 @@
;; --------------------------------------------------------------------------
;; render-to-sx — server-side SX wire format (for client rendering)
;; --------------------------------------------------------------------------
;; This mode serializes the expression as SX source text.
;; Component calls are NOT expanded — they're sent to the client.
;; HTML tags are serialized as-is. Special forms are evaluated.
(define render-to-sx
(fn (expr env)
(let ((result (aser expr env)))
(serialize result))))
(define aser
(fn (expr env)
;; Evaluate for SX wire format — serialize rendering forms,
;; evaluate control flow and function calls.
(case (type-of expr)
"number" expr
"string" expr
"boolean" expr
"nil" nil
"symbol"
(let ((name (symbol-name expr)))
(cond
(env-has? env name) (env-get env name)
(primitive? name) (get-primitive name)
(= name "true") true
(= name "false") false
(= name "nil") nil
:else (error (str "Undefined symbol: " name))))
"keyword" (keyword-name expr)
"list"
(if (empty? expr)
(list)
(aser-list expr env))
:else expr)))
(define aser-list
(fn (expr env)
(let ((head (first expr))
(args (rest expr)))
(if (not (= (type-of head) "symbol"))
(map (fn (x) (aser x env)) expr)
(let ((name (symbol-name head)))
(cond
;; Fragment — serialize children
(= name "<>")
(aser-fragment args env)
;; Component call — serialize WITHOUT expanding
(starts-with? name "~")
(aser-call name args env)
;; HTML tag — serialize
(contains? HTML_TAGS name)
(aser-call name args env)
;; Special/HO forms — evaluate (produces data)
(or (special-form? name) (ho-form? name))
(aser-special name expr env)
;; Macro — expand then aser
(and (env-has? env name) (macro? (env-get env name)))
(aser (expand-macro (env-get env name) args env) env)
;; Function call — evaluate fully
:else
(let ((f (trampoline (eval-expr head env)))
(evaled-args (map (fn (a) (trampoline (eval-expr a env))) args)))
(cond
(and (callable? f) (not (lambda? f)) (not (component? f)))
(apply f evaled-args)
(lambda? f)
(trampoline (call-lambda f evaled-args env))
(component? f)
(aser-call (str "~" (component-name f)) args env)
:else (error (str "Not callable: " (inspect f)))))))))))
(define aser-fragment
(fn (children env)
;; Serialize (<> child1 child2 ...) to sx source string
(let ((parts (filter
(fn (x) (not (nil? x)))
(map (fn (c) (aser c env)) children))))
(if (empty? parts)
""
(str "(<> " (join " " (map serialize parts)) ")")))))
(define aser-call
(fn (name args env)
;; Serialize (name :key val child ...) — evaluate args but keep as sx
(let ((parts (list name)))
(reduce
(fn (state arg)
(let ((skip (get state "skip")))
(if skip
(assoc state "skip" false)
(if (and (= (type-of arg) "keyword")
(< (inc (get state "i")) (len args)))
(let ((val (aser (nth args (inc (get state "i"))) env)))
(when (not (nil? val))
(append! parts (str ":" (keyword-name arg)))
(append! parts (serialize val)))
(assoc state "skip" true "i" (inc (get state "i"))))
(let ((val (aser arg env)))
(when (not (nil? val))
(append! parts (serialize val)))
(assoc state "i" (inc (get state "i"))))))))
(dict "i" 0 "skip" false)
args)
(str "(" (join " " parts) ")"))))
;; --------------------------------------------------------------------------
;; Platform rendering interface
;; Platform interface (shared across adapters)
;; --------------------------------------------------------------------------
;;
;; HTML rendering (server targets):
;; HTML/attribute escaping (used by HTML and SX wire adapters):
;; (escape-html s) → HTML-escaped string
;; (escape-attr s) → attribute-value-escaped string
;; (raw-html-content r) → unwrap RawHTML marker to string
;;
;; DOM rendering (browser target):
;; (create-element tag) → DOM Element
;; (create-text-node s) → DOM Text
;; (create-fragment) → DOM DocumentFragment
;; (set-attribute el k v) → void
;; (append-child parent c) → void
;;
;; StyleValue:
;; (style-value? x) → boolean (is x a StyleValue?)
;; (style-value-class sv) → string (CSS class name)
@@ -357,7 +140,7 @@
;; Serialization:
;; (serialize val) → SX source string representation of val
;;
;; Form classification:
;; Form classification (used by SX wire adapter):
;; (special-form? name) → boolean
;; (ho-form? name) → boolean
;; (aser-special name expr env) → evaluate special/HO form through aser