Meta-circular evaluator: the SX language specifying its own semantics. A thin bootstrap compiler per target (JS, Python, Rust) reads these .sx files and emits a native evaluator. Files: - eval.sx: Core evaluator — type dispatch, special forms, TCO trampoline, lambda/component/macro invocation, higher-order forms - primitives.sx: Declarative specification of ~80 built-in pure functions - render.sx: Three rendering modes (DOM, HTML string, SX wire format) - parser.sx: Tokenizer, parser, and serializer specification Platform-specific concerns (DOM ops, async I/O, HTML emission) are declared as interfaces that each target implements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
334 lines
12 KiB
Plaintext
334 lines
12 KiB
Plaintext
;; ==========================================================================
|
|
;; render.sx — Reference 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.
|
|
;;
|
|
;; Three rendering modes (matching the Python/JS implementations):
|
|
;;
|
|
;; 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.
|
|
;; ==========================================================================
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; HTML tag registry
|
|
;; --------------------------------------------------------------------------
|
|
;; Tags known to the renderer. Unknown names are treated as function calls.
|
|
;; Void elements self-close (no children). Boolean attrs emit name only.
|
|
|
|
(define HTML_TAGS
|
|
(list
|
|
;; Document
|
|
"html" "head" "body" "title" "meta" "link" "script" "style" "noscript"
|
|
;; Sections
|
|
"header" "nav" "main" "section" "article" "aside" "footer"
|
|
"h1" "h2" "h3" "h4" "h5" "h6" "hgroup"
|
|
;; Block
|
|
"div" "p" "blockquote" "pre" "figure" "figcaption" "address" "details" "summary"
|
|
;; Inline
|
|
"a" "span" "em" "strong" "small" "b" "i" "u" "s" "mark" "sub" "sup"
|
|
"abbr" "cite" "code" "time" "br" "wbr" "hr"
|
|
;; Lists
|
|
"ul" "ol" "li" "dl" "dt" "dd"
|
|
;; Tables
|
|
"table" "thead" "tbody" "tfoot" "tr" "th" "td" "caption" "colgroup" "col"
|
|
;; Forms
|
|
"form" "input" "textarea" "select" "option" "optgroup" "button" "label"
|
|
"fieldset" "legend" "output" "datalist"
|
|
;; Media
|
|
"img" "video" "audio" "source" "picture" "canvas" "iframe"
|
|
;; SVG
|
|
"svg" "path" "circle" "rect" "line" "polyline" "polygon" "text"
|
|
"g" "defs" "use" "clipPath" "mask" "pattern" "linearGradient"
|
|
"radialGradient" "stop" "filter" "feGaussianBlur" "feOffset"
|
|
"feBlend" "feColorMatrix" "feComposite" "feMerge" "feMergeNode"
|
|
"animate" "animateTransform" "foreignObject"
|
|
;; Other
|
|
"template" "slot" "dialog" "menu"))
|
|
|
|
(define VOID_ELEMENTS
|
|
(list "area" "base" "br" "col" "embed" "hr" "img" "input"
|
|
"link" "meta" "param" "source" "track" "wbr"))
|
|
|
|
(define BOOLEAN_ATTRS
|
|
(list "disabled" "checked" "selected" "readonly" "required" "hidden"
|
|
"autofocus" "autoplay" "controls" "loop" "muted" "defer" "async"
|
|
"novalidate" "formnovalidate" "multiple" "open" "allowfullscreen"))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; render-to-html — server-side HTML rendering
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define render-to-html
|
|
(fn (expr env)
|
|
(let ((result (trampoline (eval-expr expr env))))
|
|
(render-value-to-html result 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)
|
|
: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))))
|
|
|
|
;; 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 parse-element-args
|
|
(fn (args env)
|
|
;; Parse (:key val :key2 val2 child1 child2) into (attrs-dict children-list)
|
|
(let ((attrs (dict))
|
|
(children (list)))
|
|
(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 (trampoline (eval-expr (nth args (inc (get state "i"))) env))))
|
|
(dict-set! attrs (keyword-name arg) val)
|
|
(assoc state "skip" true "i" (inc (get state "i"))))
|
|
(do
|
|
(append! children arg)
|
|
(assoc state "i" (inc (get state "i"))))))))
|
|
(dict "i" 0 "skip" false)
|
|
args)
|
|
(list attrs children))))
|
|
|
|
|
|
(define render-attrs
|
|
(fn (attrs)
|
|
(join ""
|
|
(map
|
|
(fn (key)
|
|
(let ((val (dict-get attrs key)))
|
|
(cond
|
|
;; Boolean attrs
|
|
(and (contains? BOOLEAN_ATTRS key) val)
|
|
(str " " key)
|
|
(and (contains? BOOLEAN_ATTRS key) (not val))
|
|
""
|
|
;; Nil values — skip
|
|
(nil? val) ""
|
|
;; Normal attr
|
|
:else (str " " key "=\"" (escape-attr (str val)) "\""))))
|
|
(keys attrs)))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 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
|
|
;; --------------------------------------------------------------------------
|
|
;;
|
|
;; HTML rendering (server targets):
|
|
;; (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
|
|
;;
|
|
;; Serialization:
|
|
;; (serialize val) → SX source string representation of val
|
|
;;
|
|
;; Form classification:
|
|
;; (special-form? name) → boolean
|
|
;; (ho-form? name) → boolean
|
|
;; (aser-special name expr env) → evaluate special/HO form through aser
|
|
;; --------------------------------------------------------------------------
|