diff --git a/shared/sx/ref/eval.sx b/shared/sx/ref/eval.sx new file mode 100644 index 0000000..018a621 --- /dev/null +++ b/shared/sx/ref/eval.sx @@ -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 +;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/parser.sx b/shared/sx/ref/parser.sx new file mode 100644 index 0000000..3ed6d17 --- /dev/null +++ b/shared/sx/ref/parser.sx @@ -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 +;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/primitives.sx b/shared/sx/ref/primitives.sx new file mode 100644 index 0000000..05a9a9e --- /dev/null +++ b/shared/sx/ref/primitives.sx @@ -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.") diff --git a/shared/sx/ref/render.sx b/shared/sx/ref/render.sx new file mode 100644 index 0000000..0e118bd --- /dev/null +++ b/shared/sx/ref/render.sx @@ -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)) + "")))))) + + +(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 +;; --------------------------------------------------------------------------