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:
731
shared/sx/ref/eval.sx
Normal file
731
shared/sx/ref/eval.sx
Normal 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
319
shared/sx/ref/parser.sx
Normal 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
428
shared/sx/ref/primitives.sx
Normal 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
333
shared/sx/ref/render.sx
Normal 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
|
||||
;; --------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user