Add reference SX evaluator written in s-expressions

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>
This commit is contained in:
2026-03-05 09:31:40 +00:00
parent 0c9dbd6657
commit 235428628a
4 changed files with 1811 additions and 0 deletions

731
shared/sx/ref/eval.sx Normal file
View File

@@ -0,0 +1,731 @@
;; ==========================================================================
;; eval.sx — Reference SX evaluator written in SX
;;
;; This is the canonical specification of SX evaluation semantics.
;; A thin bootstrap compiler per target reads this file and emits
;; a native evaluator (JavaScript, Python, Rust, etc.).
;;
;; The evaluator is written in a restricted subset of SX:
;; - defcomp, define, defmacro, lambda/fn
;; - if, when, cond, case, let, do, and, or
;; - map, filter, reduce, some, every?
;; - Primitives: list ops, string ops, arithmetic, predicates
;; - quote, quasiquote/unquote/splice-unquote
;; - Pattern matching via (case (type-of expr) ...)
;;
;; Platform-specific concerns (DOM rendering, async I/O, HTML emission)
;; are declared as interfaces — each target provides its own adapter.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; 1. Types
;; --------------------------------------------------------------------------
;;
;; The evaluator operates on these value types:
;;
;; number — integer or float
;; string — double-quoted text
;; boolean — true / false
;; nil — singleton null
;; symbol — unquoted identifier (e.g. div, ~card, map)
;; keyword — colon-prefixed key (e.g. :class, :id)
;; list — ordered sequence (also used as code)
;; dict — string-keyed hash map
;; lambda — closure: {params, body, closure-env, name?}
;; macro — AST transformer: {params, rest-param, body, closure-env}
;; component — UI component: {name, params, has-children, body, closure-env}
;; thunk — deferred eval for TCO: {expr, env}
;;
;; Each target must provide:
;; (type-of x) → one of the strings above
;; (make-lambda ...) → platform Lambda value
;; (make-component ..) → platform Component value
;; (make-macro ...) → platform Macro value
;; (make-thunk ...) → platform Thunk value
;;
;; These are declared in platform.sx and implemented per target.
;; --------------------------------------------------------------------------
;; --------------------------------------------------------------------------
;; 2. Trampoline — tail-call optimization
;; --------------------------------------------------------------------------
(define trampoline
(fn (val)
;; Iteratively resolve thunks until we get an actual value.
;; Each target implements thunk? and thunk-expr/thunk-env.
(let ((result val))
(do
;; Loop while result is a thunk
;; Note: this is pseudo-iteration — bootstrap compilers convert
;; this tail-recursive form to a while loop.
(if (thunk? result)
(trampoline (eval-expr (thunk-expr result) (thunk-env result)))
result)))))
;; --------------------------------------------------------------------------
;; 3. Core evaluator
;; --------------------------------------------------------------------------
(define eval-expr
(fn (expr env)
(case (type-of expr)
;; --- literals pass through ---
"number" expr
"string" expr
"boolean" expr
"nil" nil
;; --- symbol lookup ---
"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 → its string name ---
"keyword" (keyword-name expr)
;; --- dict literal ---
"dict"
(map-dict (fn (k v) (list k (trampoline (eval-expr v env)))) expr)
;; --- list = call or special form ---
"list"
(if (empty? expr)
(list)
(eval-list expr env))
;; --- anything else passes through ---
:else expr)))
;; --------------------------------------------------------------------------
;; 4. List evaluation — dispatch on head
;; --------------------------------------------------------------------------
(define eval-list
(fn (expr env)
(let ((head (first expr))
(args (rest expr)))
;; If head isn't a symbol, lambda, or list → treat as data list
(if (not (or (= (type-of head) "symbol")
(= (type-of head) "lambda")
(= (type-of head) "list")))
(map (fn (x) (trampoline (eval-expr x env))) expr)
;; Head is a symbol — check special forms, then function call
(if (= (type-of head) "symbol")
(let ((name (symbol-name head)))
(cond
;; Special forms
(= name "if") (sf-if args env)
(= name "when") (sf-when args env)
(= name "cond") (sf-cond args env)
(= name "case") (sf-case args env)
(= name "and") (sf-and args env)
(= name "or") (sf-or args env)
(= name "let") (sf-let args env)
(= name "let*") (sf-let args env)
(= name "lambda") (sf-lambda args env)
(= name "fn") (sf-lambda args env)
(= name "define") (sf-define args env)
(= name "defcomp") (sf-defcomp args env)
(= name "defmacro") (sf-defmacro args env)
(= name "begin") (sf-begin args env)
(= name "do") (sf-begin args env)
(= name "quote") (sf-quote args env)
(= name "quasiquote") (sf-quasiquote args env)
(= name "->") (sf-thread-first args env)
(= name "set!") (sf-set! args env)
;; Higher-order forms
(= name "map") (ho-map args env)
(= name "map-indexed") (ho-map-indexed args env)
(= name "filter") (ho-filter args env)
(= name "reduce") (ho-reduce args env)
(= name "some") (ho-some args env)
(= name "every?") (ho-every args env)
(= name "for-each") (ho-for-each args env)
;; Macro expansion
(and (env-has? env name) (macro? (env-get env name)))
(let ((mac (env-get env name)))
(make-thunk (expand-macro mac args env) env))
;; Fall through to function call
:else (eval-call head args env)))
;; Head is lambda or list — evaluate as function call
(eval-call head args env))))))
;; --------------------------------------------------------------------------
;; 5. Function / lambda / component call
;; --------------------------------------------------------------------------
(define eval-call
(fn (head args env)
(let ((f (trampoline (eval-expr head env)))
(evaluated-args (map (fn (a) (trampoline (eval-expr a env))) args)))
(cond
;; Native callable (primitive function)
(and (callable? f) (not (lambda? f)) (not (component? f)))
(apply f evaluated-args)
;; Lambda
(lambda? f)
(call-lambda f evaluated-args env)
;; Component
(component? f)
(call-component f args env)
:else (error (str "Not callable: " (inspect f)))))))
(define call-lambda
(fn (f args caller-env)
(let ((params (lambda-params f))
(local (env-merge (lambda-closure f) caller-env)))
(if (!= (len args) (len params))
(error (str (or (lambda-name f) "lambda")
" expects " (len params) " args, got " (len args)))
(do
;; Bind params
(for-each
(fn (pair) (env-set! local (first pair) (nth pair 1)))
(zip params args))
;; Return thunk for TCO
(make-thunk (lambda-body f) local))))))
(define call-component
(fn (comp raw-args env)
;; Parse keyword args and children from unevaluated arg list
(let ((parsed (parse-keyword-args raw-args env))
(kwargs (first parsed))
(children (nth parsed 1))
(local (env-merge (component-closure comp) env)))
;; Bind keyword params
(for-each
(fn (p) (env-set! local p (or (dict-get kwargs p) nil)))
(component-params comp))
;; Bind children if component accepts them
(when (component-has-children? comp)
(env-set! local "children" children))
;; Return thunk — body evaluated in local env
(make-thunk (component-body comp) local))))
(define parse-keyword-args
(fn (raw-args env)
;; Walk args: keyword + next-val → kwargs dict, else → children list
(let ((kwargs (dict))
(children (list))
(i 0))
;; Iterative parse — bootstrap converts to while loop
(reduce
(fn (state arg)
(let ((idx (get state "i"))
(skip (get state "skip")))
(if skip
;; This arg was consumed as a keyword value
(assoc state "skip" false "i" (inc idx))
(if (and (= (type-of arg) "keyword")
(< (inc idx) (len raw-args)))
;; Keyword: evaluate next arg and store
(do
(dict-set! kwargs (keyword-name arg)
(trampoline (eval-expr (nth raw-args (inc idx)) env)))
(assoc state "skip" true "i" (inc idx)))
;; Positional: evaluate and add to children
(do
(append! children (trampoline (eval-expr arg env)))
(assoc state "i" (inc idx)))))))
(dict "i" 0 "skip" false)
raw-args)
(list kwargs children))))
;; --------------------------------------------------------------------------
;; 6. Special forms
;; --------------------------------------------------------------------------
(define sf-if
(fn (args env)
(let ((condition (trampoline (eval-expr (first args) env))))
(if (and condition (not (nil? condition)))
(make-thunk (nth args 1) env)
(if (> (len args) 2)
(make-thunk (nth args 2) env)
nil)))))
(define sf-when
(fn (args env)
(let ((condition (trampoline (eval-expr (first args) env))))
(if (and condition (not (nil? condition)))
(do
;; Evaluate all but last for side effects
(for-each
(fn (e) (trampoline (eval-expr e env)))
(slice args 1 (dec (len args))))
;; Last is tail position
(make-thunk (last args) env))
nil))))
(define sf-cond
(fn (args env)
;; Detect scheme-style: first arg is a 2-element list
(if (and (= (type-of (first args)) "list")
(= (len (first args)) 2))
;; Scheme-style: ((test body) ...)
(sf-cond-scheme args env)
;; Clojure-style: test body test body ...
(sf-cond-clojure args env))))
(define sf-cond-scheme
(fn (clauses env)
(if (empty? clauses)
nil
(let ((clause (first clauses))
(test (first clause))
(body (nth clause 1)))
(if (or (and (= (type-of test) "symbol")
(or (= (symbol-name test) "else")
(= (symbol-name test) ":else")))
(and (= (type-of test) "keyword")
(= (keyword-name test) "else")))
(make-thunk body env)
(if (trampoline (eval-expr test env))
(make-thunk body env)
(sf-cond-scheme (rest clauses) env)))))))
(define sf-cond-clojure
(fn (clauses env)
(if (< (len clauses) 2)
nil
(let ((test (first clauses))
(body (nth clauses 1)))
(if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else"))
(and (= (type-of test) "symbol")
(or (= (symbol-name test) "else")
(= (symbol-name test) ":else"))))
(make-thunk body env)
(if (trampoline (eval-expr test env))
(make-thunk body env)
(sf-cond-clojure (slice clauses 2) env)))))))
(define sf-case
(fn (args env)
(let ((match-val (trampoline (eval-expr (first args) env)))
(clauses (rest args)))
(sf-case-loop match-val clauses env))))
(define sf-case-loop
(fn (match-val clauses env)
(if (< (len clauses) 2)
nil
(let ((test (first clauses))
(body (nth clauses 1)))
(if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else"))
(and (= (type-of test) "symbol")
(or (= (symbol-name test) "else")
(= (symbol-name test) ":else"))))
(make-thunk body env)
(if (= match-val (trampoline (eval-expr test env)))
(make-thunk body env)
(sf-case-loop match-val (slice clauses 2) env)))))))
(define sf-and
(fn (args env)
(if (empty? args)
true
(let ((val (trampoline (eval-expr (first args) env))))
(if (not val)
val
(if (= (len args) 1)
val
(sf-and (rest args) env)))))))
(define sf-or
(fn (args env)
(if (empty? args)
false
(let ((val (trampoline (eval-expr (first args) env))))
(if val
val
(sf-or (rest args) env))))))
(define sf-let
(fn (args env)
(let ((bindings (first args))
(body (rest args))
(local (env-extend env)))
;; Parse bindings — support both ((name val) ...) and (name val name val ...)
(if (and (= (type-of (first bindings)) "list")
(= (len (first bindings)) 2))
;; Scheme-style
(for-each
(fn (binding)
(let ((vname (if (= (type-of (first binding)) "symbol")
(symbol-name (first binding))
(first binding))))
(env-set! local vname (trampoline (eval-expr (nth binding 1) local)))))
bindings)
;; Clojure-style
(let ((i 0))
(reduce
(fn (acc pair-idx)
(let ((vname (if (= (type-of (nth bindings (* pair-idx 2))) "symbol")
(symbol-name (nth bindings (* pair-idx 2)))
(nth bindings (* pair-idx 2))))
(val-expr (nth bindings (inc (* pair-idx 2)))))
(env-set! local vname (trampoline (eval-expr val-expr local)))))
nil
(range 0 (/ (len bindings) 2)))))
;; Evaluate body — last expression in tail position
(for-each
(fn (e) (trampoline (eval-expr e local)))
(slice body 0 (dec (len body))))
(make-thunk (last body) local))))
(define sf-lambda
(fn (args env)
(let ((params-expr (first args))
(body (nth args 1))
(param-names (map (fn (p)
(if (= (type-of p) "symbol")
(symbol-name p)
p))
params-expr)))
(make-lambda param-names body env))))
(define sf-define
(fn (args env)
(let ((name-sym (first args))
(value (trampoline (eval-expr (nth args 1) env))))
(when (and (lambda? value) (nil? (lambda-name value)))
(set-lambda-name! value (symbol-name name-sym)))
(env-set! env (symbol-name name-sym) value)
value)))
(define sf-defcomp
(fn (args env)
(let ((name-sym (first args))
(params-raw (nth args 1))
(body (nth args 2))
(comp-name (strip-prefix (symbol-name name-sym) "~"))
(parsed (parse-comp-params params-raw))
(params (first parsed))
(has-children (nth parsed 1)))
(let ((comp (make-component comp-name params has-children body env)))
(env-set! env (symbol-name name-sym) comp)
comp))))
(define parse-comp-params
(fn (params-expr)
;; Parse (&key param1 param2 &rest children) → (params has-children)
(let ((params (list))
(has-children false)
(in-key false))
(for-each
(fn (p)
(when (= (type-of p) "symbol")
(let ((name (symbol-name p)))
(cond
(= name "&key") (set! in-key true)
(= name "&rest") (set! has-children true)
(and in-key (not has-children))
(append! params name)
:else
(append! params name)))))
params-expr)
(list params has-children))))
(define sf-defmacro
(fn (args env)
(let ((name-sym (first args))
(params-raw (nth args 1))
(body (nth args 2))
(parsed (parse-macro-params params-raw))
(params (first parsed))
(rest-param (nth parsed 1)))
(let ((mac (make-macro params rest-param body env (symbol-name name-sym))))
(env-set! env (symbol-name name-sym) mac)
mac))))
(define parse-macro-params
(fn (params-expr)
;; Parse (a b &rest rest) → ((a b) rest)
(let ((params (list))
(rest-param nil))
(reduce
(fn (state p)
(if (and (= (type-of p) "symbol") (= (symbol-name p) "&rest"))
(assoc state "in-rest" true)
(if (get state "in-rest")
(do (set! rest-param (if (= (type-of p) "symbol")
(symbol-name p) p))
state)
(do (append! params (if (= (type-of p) "symbol")
(symbol-name p) p))
state))))
(dict "in-rest" false)
params-expr)
(list params rest-param))))
(define sf-begin
(fn (args env)
(if (empty? args)
nil
(do
(for-each
(fn (e) (trampoline (eval-expr e env)))
(slice args 0 (dec (len args))))
(make-thunk (last args) env)))))
(define sf-quote
(fn (args env)
(if (empty? args) nil (first args))))
(define sf-quasiquote
(fn (args env)
(qq-expand (first args) env)))
(define qq-expand
(fn (template env)
(if (not (= (type-of template) "list"))
template
(if (empty? template)
(list)
(let ((head (first template)))
(if (and (= (type-of head) "symbol") (= (symbol-name head) "unquote"))
(trampoline (eval-expr (nth template 1) env))
;; Walk children, handling splice-unquote
(reduce
(fn (result item)
(if (and (= (type-of item) "list")
(= (len item) 2)
(= (type-of (first item)) "symbol")
(= (symbol-name (first item)) "splice-unquote"))
(let ((spliced (trampoline (eval-expr (nth item 1) env))))
(if (= (type-of spliced) "list")
(concat result spliced)
(if (nil? spliced) result (append result spliced))))
(append result (qq-expand item env))))
(list)
template)))))))
(define sf-thread-first
(fn (args env)
(let ((val (trampoline (eval-expr (first args) env))))
(reduce
(fn (result form)
(if (= (type-of form) "list")
(let ((f (trampoline (eval-expr (first form) env)))
(rest-args (map (fn (a) (trampoline (eval-expr a env)))
(rest form)))
(all-args (cons result rest-args)))
(cond
(and (callable? f) (not (lambda? f)))
(apply f all-args)
(lambda? f)
(trampoline (call-lambda f all-args env))
:else (error (str "-> form not callable: " (inspect f)))))
(let ((f (trampoline (eval-expr form env))))
(cond
(and (callable? f) (not (lambda? f)))
(f result)
(lambda? f)
(trampoline (call-lambda f (list result) env))
:else (error (str "-> form not callable: " (inspect f)))))))
val
(rest args)))))
(define sf-set!
(fn (args env)
(let ((name (symbol-name (first args)))
(value (trampoline (eval-expr (nth args 1) env))))
(env-set! env name value)
value)))
;; --------------------------------------------------------------------------
;; 6b. Macro expansion
;; --------------------------------------------------------------------------
(define expand-macro
(fn (mac raw-args env)
(let ((local (env-merge (macro-closure mac) env)))
;; Bind positional params (unevaluated)
(for-each
(fn (pair)
(env-set! local (first pair)
(if (< (nth pair 1) (len raw-args))
(nth raw-args (nth pair 1))
nil)))
(map-indexed (fn (i p) (list p i)) (macro-params mac)))
;; Bind &rest param
(when (macro-rest-param mac)
(env-set! local (macro-rest-param mac)
(slice raw-args (len (macro-params mac)))))
;; Evaluate body → new AST
(trampoline (eval-expr (macro-body mac) local)))))
;; --------------------------------------------------------------------------
;; 7. Higher-order forms
;; --------------------------------------------------------------------------
(define ho-map
(fn (args env)
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(map (fn (item) (trampoline (call-lambda f (list item) env))) coll))))
(define ho-map-indexed
(fn (args env)
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(map-indexed
(fn (i item) (trampoline (call-lambda f (list i item) env)))
coll))))
(define ho-filter
(fn (args env)
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(filter
(fn (item) (trampoline (call-lambda f (list item) env)))
coll))))
(define ho-reduce
(fn (args env)
(let ((f (trampoline (eval-expr (first args) env)))
(init (trampoline (eval-expr (nth args 1) env)))
(coll (trampoline (eval-expr (nth args 2) env))))
(reduce
(fn (acc item) (trampoline (call-lambda f (list acc item) env)))
init
coll))))
(define ho-some
(fn (args env)
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(some
(fn (item) (trampoline (call-lambda f (list item) env)))
coll))))
(define ho-every
(fn (args env)
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(every?
(fn (item) (trampoline (call-lambda f (list item) env)))
coll))))
;; --------------------------------------------------------------------------
;; 8. Primitives — pure functions available in all targets
;; --------------------------------------------------------------------------
;; These are the ~80 built-in functions. Each target implements them
;; natively but they MUST have identical semantics. This section serves
;; as the specification — bootstrap compilers use it for reference.
;;
;; Primitives are NOT defined here as SX lambdas (that would be circular).
;; Instead, this is a declarative registry that bootstrap compilers read.
;; --------------------------------------------------------------------------
;; See primitives.sx for the full specification.
;; --------------------------------------------------------------------------
;; 9. Platform interface — must be provided by each target
;; --------------------------------------------------------------------------
;;
;; Type inspection:
;; (type-of x) → "number" | "string" | "boolean" | "nil"
;; | "symbol" | "keyword" | "list" | "dict"
;; | "lambda" | "component" | "macro" | "thunk"
;; (symbol-name sym) → string
;; (keyword-name kw) → string
;;
;; Constructors:
;; (make-lambda params body env) → Lambda
;; (make-component name params has-children body env) → Component
;; (make-macro params rest-param body env name) → Macro
;; (make-thunk expr env) → Thunk
;;
;; Accessors:
;; (lambda-params f) → list of strings
;; (lambda-body f) → expr
;; (lambda-closure f) → env
;; (lambda-name f) → string or nil
;; (set-lambda-name! f n) → void
;; (component-params c) → list of strings
;; (component-body c) → expr
;; (component-closure c) → env
;; (component-has-children? c) → boolean
;; (macro-params m) → list of strings
;; (macro-rest-param m) → string or nil
;; (macro-body m) → expr
;; (macro-closure m) → env
;; (thunk? x) → boolean
;; (thunk-expr t) → expr
;; (thunk-env t) → env
;;
;; Predicates:
;; (callable? x) → boolean (native function or lambda)
;; (lambda? x) → boolean
;; (component? x) → boolean
;; (macro? x) → boolean
;; (primitive? name) → boolean (is name a registered primitive?)
;; (get-primitive name) → function
;;
;; Environment:
;; (env-has? env name) → boolean
;; (env-get env name) → value
;; (env-set! env name val) → void (mutating)
;; (env-extend env) → new env inheriting from env
;; (env-merge base overlay) → new env with overlay on top
;;
;; Mutation helpers (for parse-keyword-args):
;; (dict-set! d key val) → void
;; (dict-get d key) → value or nil
;; (append! lst val) → void (mutating append)
;;
;; Error:
;; (error msg) → raise/throw with message
;; (inspect x) → string representation for debugging
;;
;; Utility:
;; (strip-prefix s prefix) → string with prefix removed (or s unchanged)
;; (apply f args) → call f with args list
;; (zip lists...) → list of tuples
;; --------------------------------------------------------------------------

319
shared/sx/ref/parser.sx Normal file
View File

@@ -0,0 +1,319 @@
;; ==========================================================================
;; parser.sx — Reference SX parser specification
;;
;; Defines how SX source text is tokenized and parsed into AST.
;; The parser is intentionally simple — s-expressions need minimal parsing.
;;
;; Grammar:
;; program → expr*
;; expr → atom | list | quote-sugar
;; list → '(' expr* ')'
;; atom → string | number | keyword | symbol | boolean | nil
;; string → '"' (char | escape)* '"'
;; number → '-'? digit+ ('.' digit+)? ([eE] [+-]? digit+)?
;; keyword → ':' ident
;; symbol → ident
;; boolean → 'true' | 'false'
;; nil → 'nil'
;; ident → [a-zA-Z_~*+\-><=/!?&] [a-zA-Z0-9_~*+\-><=/!?.:&]*
;; comment → ';' to end of line (discarded)
;;
;; Quote sugar (optional — not used in current SX):
;; '(expr) → (quote expr)
;; `(expr) → (quasiquote expr)
;; ~(expr) → (unquote expr)
;; ~@(expr) → (splice-unquote expr)
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Tokenizer
;; --------------------------------------------------------------------------
;; Produces a flat stream of tokens from source text.
;; Each token is a (type value line col) tuple.
(define tokenize
(fn (source)
(let ((pos 0)
(line 1)
(col 1)
(tokens (list))
(len-src (len source)))
;; Main loop — bootstrap compilers convert to while
(define scan-next
(fn ()
(when (< pos len-src)
(let ((ch (nth source pos)))
(cond
;; Whitespace — skip
(whitespace? ch)
(do (advance-pos!) (scan-next))
;; Comment — skip to end of line
(= ch ";")
(do (skip-to-eol!) (scan-next))
;; String
(= ch "\"")
(do (append! tokens (scan-string)) (scan-next))
;; Open paren
(= ch "(")
(do (append! tokens (list "lparen" "(" line col))
(advance-pos!)
(scan-next))
;; Close paren
(= ch ")")
(do (append! tokens (list "rparen" ")" line col))
(advance-pos!)
(scan-next))
;; Open bracket (list sugar)
(= ch "[")
(do (append! tokens (list "lbracket" "[" line col))
(advance-pos!)
(scan-next))
;; Close bracket
(= ch "]")
(do (append! tokens (list "rbracket" "]" line col))
(advance-pos!)
(scan-next))
;; Keyword
(= ch ":")
(do (append! tokens (scan-keyword)) (scan-next))
;; Number (or negative number)
(or (digit? ch)
(and (= ch "-") (< (inc pos) len-src)
(digit? (nth source (inc pos)))))
(do (append! tokens (scan-number)) (scan-next))
;; Symbol
(ident-start? ch)
(do (append! tokens (scan-symbol)) (scan-next))
;; Unknown — skip
:else
(do (advance-pos!) (scan-next)))))))
(scan-next)
tokens)))
;; --------------------------------------------------------------------------
;; Token scanners (pseudo-code — each target implements natively)
;; --------------------------------------------------------------------------
(define scan-string
(fn ()
;; Scan from opening " to closing ", handling escape sequences.
;; Returns ("string" value line col).
;; Escape sequences: \" \\ \n \t \r
(let ((start-line line)
(start-col col)
(result ""))
(advance-pos!) ;; skip opening "
(define scan-str-loop
(fn ()
(if (>= pos (len source))
(error "Unterminated string")
(let ((ch (nth source pos)))
(cond
(= ch "\"")
(do (advance-pos!) nil) ;; done
(= ch "\\")
(do (advance-pos!)
(let ((esc (nth source pos)))
(set! result (str result
(case esc
"n" "\n"
"t" "\t"
"r" "\r"
:else esc)))
(advance-pos!)
(scan-str-loop)))
:else
(do (set! result (str result ch))
(advance-pos!)
(scan-str-loop)))))))
(scan-str-loop)
(list "string" result start-line start-col))))
(define scan-keyword
(fn ()
;; Scan :identifier
(let ((start-line line) (start-col col))
(advance-pos!) ;; skip :
(let ((name (scan-ident-chars)))
(list "keyword" name start-line start-col)))))
(define scan-number
(fn ()
;; Scan integer or float literal
(let ((start-line line) (start-col col) (buf ""))
(when (= (nth source pos) "-")
(set! buf "-")
(advance-pos!))
;; Integer part
(define scan-digits
(fn ()
(when (and (< pos (len source)) (digit? (nth source pos)))
(set! buf (str buf (nth source pos)))
(advance-pos!)
(scan-digits))))
(scan-digits)
;; Decimal part
(when (and (< pos (len source)) (= (nth source pos) "."))
(set! buf (str buf "."))
(advance-pos!)
(scan-digits))
;; Exponent
(when (and (< pos (len source))
(or (= (nth source pos) "e") (= (nth source pos) "E")))
(set! buf (str buf (nth source pos)))
(advance-pos!)
(when (and (< pos (len source))
(or (= (nth source pos) "+") (= (nth source pos) "-")))
(set! buf (str buf (nth source pos)))
(advance-pos!))
(scan-digits))
(list "number" (parse-number buf) start-line start-col))))
(define scan-symbol
(fn ()
;; Scan identifier, check for true/false/nil
(let ((start-line line)
(start-col col)
(name (scan-ident-chars)))
(cond
(= name "true") (list "boolean" true start-line start-col)
(= name "false") (list "boolean" false start-line start-col)
(= name "nil") (list "nil" nil start-line start-col)
:else (list "symbol" name start-line start-col)))))
;; --------------------------------------------------------------------------
;; Parser — tokens → AST
;; --------------------------------------------------------------------------
(define parse
(fn (tokens)
;; Parse all top-level expressions from token stream.
(let ((pos 0)
(exprs (list)))
(define parse-loop
(fn ()
(when (< pos (len tokens))
(let ((result (parse-expr tokens)))
(append! exprs result)
(parse-loop)))))
(parse-loop)
exprs)))
(define parse-expr
(fn (tokens)
;; Parse a single expression.
(let ((tok (nth tokens pos)))
(case (first tok) ;; token type
"lparen"
(do (set! pos (inc pos))
(parse-list tokens "rparen"))
"lbracket"
(do (set! pos (inc pos))
(parse-list tokens "rbracket"))
"string" (do (set! pos (inc pos)) (nth tok 1))
"number" (do (set! pos (inc pos)) (nth tok 1))
"boolean" (do (set! pos (inc pos)) (nth tok 1))
"nil" (do (set! pos (inc pos)) nil)
"keyword"
(do (set! pos (inc pos))
(make-keyword (nth tok 1)))
"symbol"
(do (set! pos (inc pos))
(make-symbol (nth tok 1)))
:else (error (str "Unexpected token: " (inspect tok)))))))
(define parse-list
(fn (tokens close-type)
;; Parse expressions until close-type token.
(let ((items (list)))
(define parse-list-loop
(fn ()
(if (>= pos (len tokens))
(error "Unterminated list")
(if (= (first (nth tokens pos)) close-type)
(do (set! pos (inc pos)) nil) ;; done
(do (append! items (parse-expr tokens))
(parse-list-loop))))))
(parse-list-loop)
items)))
;; --------------------------------------------------------------------------
;; Serializer — AST → SX source text
;; --------------------------------------------------------------------------
(define serialize
(fn (val)
(case (type-of val)
"nil" "nil"
"boolean" (if val "true" "false")
"number" (str val)
"string" (str "\"" (escape-string val) "\"")
"symbol" (symbol-name val)
"keyword" (str ":" (keyword-name val))
"list" (str "(" (join " " (map serialize val)) ")")
"dict" (serialize-dict val)
"sx-expr" (sx-expr-source val)
:else (str val))))
(define serialize-dict
(fn (d)
(str "(dict "
(join " "
(reduce
(fn (acc key)
(concat acc (list (str ":" key) (serialize (dict-get d key)))))
(list)
(keys d)))
")")))
;; --------------------------------------------------------------------------
;; Platform parser interface
;; --------------------------------------------------------------------------
;;
;; Character classification:
;; (whitespace? ch) → boolean
;; (digit? ch) → boolean
;; (ident-start? ch) → boolean (letter, _, ~, *, +, -, etc.)
;; (ident-char? ch) → boolean (ident-start + digits, ., :)
;;
;; Constructors:
;; (make-symbol name) → Symbol value
;; (make-keyword name) → Keyword value
;; (parse-number s) → number (int or float from string)
;;
;; String utilities:
;; (escape-string s) → string with " and \ escaped
;; (sx-expr-source e) → unwrap SxExpr to its source string
;;
;; Cursor state (mutable — each target manages its own way):
;; pos, line, col — current position in source
;; (advance-pos!) → increment pos, update line/col
;; (skip-to-eol!) → advance past end of line
;; (scan-ident-chars) → consume and return identifier string
;; --------------------------------------------------------------------------

428
shared/sx/ref/primitives.sx Normal file
View File

@@ -0,0 +1,428 @@
;; ==========================================================================
;; primitives.sx — Specification of all SX built-in pure functions
;;
;; Each entry declares: name, parameter signature, and semantics.
;; Bootstrap compilers implement these natively per target.
;;
;; This file is a SPECIFICATION, not executable code. The define-primitive
;; form is a declarative macro that bootstrap compilers consume to generate
;; native primitive registrations.
;;
;; Format:
;; (define-primitive "name"
;; :params (param1 param2 &rest rest)
;; :returns "type"
;; :doc "description"
;; :body (reference-implementation ...))
;;
;; The :body is optional — when provided, it gives a reference
;; implementation in SX that bootstrap compilers MAY use for testing
;; or as a fallback. Most targets will implement natively for performance.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Arithmetic
;; --------------------------------------------------------------------------
(define-primitive "+"
:params (&rest args)
:returns "number"
:doc "Sum all arguments."
:body (reduce (fn (a b) (native-add a b)) 0 args))
(define-primitive "-"
:params (a &rest b)
:returns "number"
:doc "Subtract. Unary: negate. Binary: a - b."
:body (if (empty? b) (native-neg a) (native-sub a (first b))))
(define-primitive "*"
:params (&rest args)
:returns "number"
:doc "Multiply all arguments."
:body (reduce (fn (a b) (native-mul a b)) 1 args))
(define-primitive "/"
:params (a b)
:returns "number"
:doc "Divide a by b."
:body (native-div a b))
(define-primitive "mod"
:params (a b)
:returns "number"
:doc "Modulo a % b."
:body (native-mod a b))
(define-primitive "sqrt"
:params (x)
:returns "number"
:doc "Square root.")
(define-primitive "pow"
:params (x n)
:returns "number"
:doc "x raised to power n.")
(define-primitive "abs"
:params (x)
:returns "number"
:doc "Absolute value.")
(define-primitive "floor"
:params (x)
:returns "number"
:doc "Floor to integer.")
(define-primitive "ceil"
:params (x)
:returns "number"
:doc "Ceiling to integer.")
(define-primitive "round"
:params (x &rest ndigits)
:returns "number"
:doc "Round to ndigits decimal places (default 0).")
(define-primitive "min"
:params (&rest args)
:returns "number"
:doc "Minimum. Single list arg or variadic.")
(define-primitive "max"
:params (&rest args)
:returns "number"
:doc "Maximum. Single list arg or variadic.")
(define-primitive "clamp"
:params (x lo hi)
:returns "number"
:doc "Clamp x to range [lo, hi]."
:body (max lo (min hi x)))
(define-primitive "inc"
:params (n)
:returns "number"
:doc "Increment by 1."
:body (+ n 1))
(define-primitive "dec"
:params (n)
:returns "number"
:doc "Decrement by 1."
:body (- n 1))
;; --------------------------------------------------------------------------
;; Comparison
;; --------------------------------------------------------------------------
(define-primitive "="
:params (a b)
:returns "boolean"
:doc "Equality (value equality, not identity).")
(define-primitive "!="
:params (a b)
:returns "boolean"
:doc "Inequality."
:body (not (= a b)))
(define-primitive "<"
:params (a b)
:returns "boolean"
:doc "Less than.")
(define-primitive ">"
:params (a b)
:returns "boolean"
:doc "Greater than.")
(define-primitive "<="
:params (a b)
:returns "boolean"
:doc "Less than or equal.")
(define-primitive ">="
:params (a b)
:returns "boolean"
:doc "Greater than or equal.")
;; --------------------------------------------------------------------------
;; Predicates
;; --------------------------------------------------------------------------
(define-primitive "odd?"
:params (n)
:returns "boolean"
:doc "True if n is odd."
:body (= (mod n 2) 1))
(define-primitive "even?"
:params (n)
:returns "boolean"
:doc "True if n is even."
:body (= (mod n 2) 0))
(define-primitive "zero?"
:params (n)
:returns "boolean"
:doc "True if n is zero."
:body (= n 0))
(define-primitive "nil?"
:params (x)
:returns "boolean"
:doc "True if x is nil/null/None.")
(define-primitive "number?"
:params (x)
:returns "boolean"
:doc "True if x is a number (int or float).")
(define-primitive "string?"
:params (x)
:returns "boolean"
:doc "True if x is a string.")
(define-primitive "list?"
:params (x)
:returns "boolean"
:doc "True if x is a list/array.")
(define-primitive "dict?"
:params (x)
:returns "boolean"
:doc "True if x is a dict/map.")
(define-primitive "empty?"
:params (coll)
:returns "boolean"
:doc "True if coll is nil or has length 0.")
(define-primitive "contains?"
:params (coll key)
:returns "boolean"
:doc "True if coll contains key. Strings: substring check. Dicts: key check. Lists: membership.")
;; --------------------------------------------------------------------------
;; Logic
;; --------------------------------------------------------------------------
(define-primitive "not"
:params (x)
:returns "boolean"
:doc "Logical negation. Note: and/or are special forms (short-circuit).")
;; --------------------------------------------------------------------------
;; Strings
;; --------------------------------------------------------------------------
(define-primitive "str"
:params (&rest args)
:returns "string"
:doc "Concatenate all args as strings. nil → empty string, bool → true/false.")
(define-primitive "concat"
:params (&rest colls)
:returns "list"
:doc "Concatenate multiple lists into one. Skips nil values.")
(define-primitive "upper"
:params (s)
:returns "string"
:doc "Uppercase string.")
(define-primitive "lower"
:params (s)
:returns "string"
:doc "Lowercase string.")
(define-primitive "trim"
:params (s)
:returns "string"
:doc "Strip leading/trailing whitespace.")
(define-primitive "split"
:params (s &rest sep)
:returns "list"
:doc "Split string by separator (default space).")
(define-primitive "join"
:params (sep coll)
:returns "string"
:doc "Join collection items with separator string.")
(define-primitive "replace"
:params (s old new)
:returns "string"
:doc "Replace all occurrences of old with new in s.")
(define-primitive "slice"
:params (coll start &rest end)
:returns "any"
:doc "Slice a string or list from start to end (exclusive). End is optional.")
(define-primitive "starts-with?"
:params (s prefix)
:returns "boolean"
:doc "True if string s starts with prefix.")
(define-primitive "ends-with?"
:params (s suffix)
:returns "boolean"
:doc "True if string s ends with suffix.")
;; --------------------------------------------------------------------------
;; Collections — construction
;; --------------------------------------------------------------------------
(define-primitive "list"
:params (&rest args)
:returns "list"
:doc "Create a list from arguments.")
(define-primitive "dict"
:params (&rest pairs)
:returns "dict"
:doc "Create a dict from key/value pairs: (dict :a 1 :b 2).")
(define-primitive "range"
:params (start end &rest step)
:returns "list"
:doc "Integer range [start, end) with optional step.")
;; --------------------------------------------------------------------------
;; Collections — access
;; --------------------------------------------------------------------------
(define-primitive "get"
:params (coll key &rest default)
:returns "any"
:doc "Get value from dict by key, or list by index. Optional default.")
(define-primitive "len"
:params (coll)
:returns "number"
:doc "Length of string, list, or dict.")
(define-primitive "first"
:params (coll)
:returns "any"
:doc "First element, or nil if empty.")
(define-primitive "last"
:params (coll)
:returns "any"
:doc "Last element, or nil if empty.")
(define-primitive "rest"
:params (coll)
:returns "list"
:doc "All elements except the first.")
(define-primitive "nth"
:params (coll n)
:returns "any"
:doc "Element at index n, or nil if out of bounds.")
(define-primitive "cons"
:params (x coll)
:returns "list"
:doc "Prepend x to coll.")
(define-primitive "append"
:params (coll x)
:returns "list"
:doc "Append x to end of coll (returns new list).")
(define-primitive "chunk-every"
:params (coll n)
:returns "list"
:doc "Split coll into sub-lists of size n.")
(define-primitive "zip-pairs"
:params (coll)
:returns "list"
:doc "Consecutive pairs: (1 2 3 4) → ((1 2) (2 3) (3 4)).")
;; --------------------------------------------------------------------------
;; Collections — dict operations
;; --------------------------------------------------------------------------
(define-primitive "keys"
:params (d)
:returns "list"
:doc "List of dict keys.")
(define-primitive "vals"
:params (d)
:returns "list"
:doc "List of dict values.")
(define-primitive "merge"
:params (&rest dicts)
:returns "dict"
:doc "Merge dicts left to right. Later keys win. Skips nil.")
(define-primitive "assoc"
:params (d &rest pairs)
:returns "dict"
:doc "Return new dict with key/value pairs added/overwritten.")
(define-primitive "dissoc"
:params (d &rest keys)
:returns "dict"
:doc "Return new dict with keys removed.")
(define-primitive "into"
:params (target coll)
:returns "any"
:doc "Pour coll into target. List target: convert to list. Dict target: convert pairs to dict.")
;; --------------------------------------------------------------------------
;; Format helpers
;; --------------------------------------------------------------------------
(define-primitive "format-date"
:params (date-str fmt)
:returns "string"
:doc "Parse ISO date string and format with strftime-style format.")
(define-primitive "format-decimal"
:params (val &rest places)
:returns "string"
:doc "Format number with fixed decimal places (default 2).")
(define-primitive "parse-int"
:params (val &rest default)
:returns "number"
:doc "Parse string to integer with optional default on failure.")
;; --------------------------------------------------------------------------
;; Text helpers
;; --------------------------------------------------------------------------
(define-primitive "pluralize"
:params (count &rest forms)
:returns "string"
:doc "Pluralize: (pluralize 1) → \"\", (pluralize 2) → \"s\". Or (pluralize n \"item\" \"items\").")
(define-primitive "escape"
:params (s)
:returns "string"
:doc "HTML-escape a string (&, <, >, \", ').")
(define-primitive "strip-tags"
:params (s)
:returns "string"
:doc "Remove HTML tags from string.")

333
shared/sx/ref/render.sx Normal file
View File

@@ -0,0 +1,333 @@
;; ==========================================================================
;; 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
;; --------------------------------------------------------------------------