All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m49s
Three-layer architecture:
spec/ — Core language (19 files): evaluator, parser, primitives,
CEK machine, types, continuations. Host-independent.
web/ — Web framework (20 files): signals, adapters, engine,
orchestration, boot, router, CSSX. Built on core spec.
sx/ — Application (sx-docs website). Built on web framework.
Split boundary.sx into boundary-core.sx (type-of, make-env, identical?)
and boundary-web.sx (IO primitives, signals, spreads, page helpers).
Bootstrappers search spec/ → web/ → shared/sx/ref/ for .sx files.
Original files remain in shared/sx/ref/ as fallback during transition.
All 63 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
445 lines
18 KiB
Plaintext
445 lines
18 KiB
Plaintext
;; ==========================================================================
|
|
;; special-forms.sx — Specification of all SX special forms
|
|
;;
|
|
;; Special forms are syntactic constructs whose arguments are NOT evaluated
|
|
;; before dispatch. Each form has its own evaluation rules — unlike primitives,
|
|
;; which receive pre-evaluated values.
|
|
;;
|
|
;; This file is a SPECIFICATION, not executable code. Bootstrap compilers
|
|
;; consume these declarations but implement special forms natively.
|
|
;;
|
|
;; Format:
|
|
;; (define-special-form "name"
|
|
;; :syntax (name arg1 arg2 ...)
|
|
;; :doc "description"
|
|
;; :tail-position "which subexpressions are in tail position"
|
|
;; :example "(name ...)")
|
|
;;
|
|
;; ==========================================================================
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Control flow
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define-special-form "if"
|
|
:syntax (if condition then-expr else-expr)
|
|
:doc "If condition is truthy, evaluate then-expr; otherwise evaluate else-expr.
|
|
Both branches are in tail position. The else branch is optional and
|
|
defaults to nil."
|
|
:tail-position "then-expr, else-expr"
|
|
:example "(if (> x 10) \"big\" \"small\")")
|
|
|
|
(define-special-form "when"
|
|
:syntax (when condition body ...)
|
|
:doc "If condition is truthy, evaluate all body expressions sequentially.
|
|
Returns the value of the last body expression, or nil if condition
|
|
is falsy. Only the last body expression is in tail position."
|
|
:tail-position "last body expression"
|
|
:example "(when (logged-in? user)
|
|
(render-dashboard user))")
|
|
|
|
(define-special-form "cond"
|
|
:syntax (cond test1 result1 test2 result2 ... :else default)
|
|
:doc "Multi-way conditional. Tests are evaluated in order; the result
|
|
paired with the first truthy test is returned. The :else keyword
|
|
(or the symbol else) matches unconditionally. Supports both
|
|
Clojure-style flat pairs and Scheme-style nested pairs:
|
|
Clojure: (cond test1 result1 test2 result2 :else default)
|
|
Scheme: (cond (test1 result1) (test2 result2) (else default))"
|
|
:tail-position "all result expressions"
|
|
:example "(cond
|
|
(= status \"active\") (render-active item)
|
|
(= status \"draft\") (render-draft item)
|
|
:else (render-unknown item))")
|
|
|
|
(define-special-form "case"
|
|
:syntax (case expr val1 result1 val2 result2 ... :else default)
|
|
:doc "Match expr against values using equality. Like cond but tests
|
|
a single expression against multiple values. The :else keyword
|
|
matches if no values match."
|
|
:tail-position "all result expressions"
|
|
:example "(case (get request \"method\")
|
|
\"GET\" (handle-get request)
|
|
\"POST\" (handle-post request)
|
|
:else (method-not-allowed))")
|
|
|
|
(define-special-form "and"
|
|
:syntax (and expr ...)
|
|
:doc "Short-circuit logical AND. Evaluates expressions left to right.
|
|
Returns the first falsy value, or the last value if all are truthy.
|
|
Returns true if given no arguments."
|
|
:tail-position "last expression"
|
|
:example "(and (valid? input) (authorized? user) (process input))")
|
|
|
|
(define-special-form "or"
|
|
:syntax (or expr ...)
|
|
:doc "Short-circuit logical OR. Evaluates expressions left to right.
|
|
Returns the first truthy value, or the last value if all are falsy.
|
|
Returns false if given no arguments."
|
|
:tail-position "last expression"
|
|
:example "(or (get cache key) (fetch-from-db key) \"default\")")
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Binding
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define-special-form "let"
|
|
:syntax (let bindings body ...)
|
|
:doc "Create local bindings and evaluate body in the extended environment.
|
|
Bindings can be Scheme-style ((name val) ...) or Clojure-style
|
|
(name val name val ...). Each binding can see previous bindings.
|
|
Only the last body expression is in tail position.
|
|
|
|
Named let: (let name ((x init) ...) body) creates a loop. The name
|
|
is bound to a function that takes the same params and recurses with
|
|
tail-call optimization."
|
|
:tail-position "last body expression; recursive call in named let"
|
|
:example ";; Basic let
|
|
(let ((x 10) (y 20))
|
|
(+ x y))
|
|
|
|
;; Clojure-style
|
|
(let (x 10 y 20)
|
|
(+ x y))
|
|
|
|
;; Named let (loop)
|
|
(let loop ((i 0) (acc 0))
|
|
(if (= i 100)
|
|
acc
|
|
(loop (+ i 1) (+ acc i))))")
|
|
|
|
(define-special-form "let*"
|
|
:syntax (let* bindings body ...)
|
|
:doc "Alias for let. In SX, let is already sequential (each binding
|
|
sees previous ones), so let* is identical to let."
|
|
:tail-position "last body expression"
|
|
:example "(let* ((x 10) (y (* x 2)))
|
|
(+ x y)) ;; → 30")
|
|
|
|
(define-special-form "letrec"
|
|
:syntax (letrec bindings body ...)
|
|
:doc "Mutually recursive local bindings. All names are bound to nil first,
|
|
then all values are evaluated (so they can reference each other),
|
|
then lambda closures are patched to include the final bindings.
|
|
Used for defining mutually recursive local functions."
|
|
:tail-position "last body expression"
|
|
:example "(letrec ((even? (fn (n) (if (= n 0) true (odd? (- n 1)))))
|
|
(odd? (fn (n) (if (= n 0) false (even? (- n 1))))))
|
|
(even? 10)) ;; → true")
|
|
|
|
(define-special-form "define"
|
|
:syntax (define name value)
|
|
:doc "Bind name to value in the current environment. If value is a lambda
|
|
and has no name, the lambda's name is set to the symbol name.
|
|
Returns the value."
|
|
:tail-position "none (value is eagerly evaluated)"
|
|
:example "(define greeting \"hello\")
|
|
(define double (fn (x) (* x 2)))")
|
|
|
|
(define-special-form "set!"
|
|
:syntax (set! name value)
|
|
:doc "Mutate an existing binding. The name must already be bound in the
|
|
current environment. Returns the new value."
|
|
:tail-position "none (value is eagerly evaluated)"
|
|
:example "(let (count 0)
|
|
(set! count (+ count 1)))")
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Functions and components
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define-special-form "lambda"
|
|
:syntax (lambda params body)
|
|
:doc "Create a function. Params is a list of parameter names. Body is
|
|
a single expression (the return value). The lambda captures the
|
|
current environment as its closure."
|
|
:tail-position "body"
|
|
:example "(lambda (x y) (+ x y))")
|
|
|
|
(define-special-form "fn"
|
|
:syntax (fn params body)
|
|
:doc "Alias for lambda."
|
|
:tail-position "body"
|
|
:example "(fn (x) (* x x))")
|
|
|
|
(define-special-form "defcomp"
|
|
:syntax (defcomp ~name (&key param1 param2 &rest children) body)
|
|
:doc "Define a component. Components are called with keyword arguments
|
|
and optional positional children. The &key marker introduces
|
|
keyword parameters. The &rest (or &children) marker captures
|
|
remaining positional arguments as a list.
|
|
|
|
Component names conventionally start with ~ to distinguish them
|
|
from HTML elements. Components are evaluated with a merged
|
|
environment: closure + caller-env + bound-params."
|
|
:tail-position "body"
|
|
:example "(defcomp ~card (&key title subtitle &rest children)
|
|
(div :class \"card\"
|
|
(h2 title)
|
|
(when subtitle (p subtitle))
|
|
children))")
|
|
|
|
(define-special-form "defisland"
|
|
:syntax (defisland ~name (&key param1 param2 &rest children) body)
|
|
:doc "Define a reactive island. Islands have the same calling convention
|
|
as components (defcomp) but create a reactive boundary. Inside an
|
|
island, signals are tracked — deref subscribes DOM nodes to signals,
|
|
and signal changes update only the affected nodes.
|
|
|
|
On the server, islands render as static HTML wrapped in a
|
|
data-sx-island container with serialized initial state. On the
|
|
client, islands hydrate into reactive contexts."
|
|
:tail-position "body"
|
|
:example "(defisland ~counter (&key initial)
|
|
(let ((count (signal (or initial 0))))
|
|
(div :class \"counter\"
|
|
(span (deref count))
|
|
(button :on-click (fn (e) (swap! count inc)) \"+\"))))")
|
|
|
|
(define-special-form "defmacro"
|
|
:syntax (defmacro name (params ...) body)
|
|
:doc "Define a macro. Macros receive their arguments unevaluated (as raw
|
|
AST) and return a new expression that is then evaluated. The
|
|
returned expression replaces the macro call. Use quasiquote for
|
|
template construction."
|
|
:tail-position "none (expansion is evaluated separately)"
|
|
:example "(defmacro unless (condition &rest body)
|
|
`(when (not ~condition) ~@body))")
|
|
|
|
(define-special-form "deftype"
|
|
:syntax (deftype name body)
|
|
:doc "Define a named type. The name can be a simple symbol for type aliases
|
|
and records, or a list (name param ...) for parameterized types.
|
|
Body is a type expression: a symbol (alias), (union t1 t2 ...) for
|
|
union types, or {:field1 type1 :field2 type2} for record types.
|
|
Type definitions are metadata for the type checker with no runtime cost."
|
|
:tail-position "none"
|
|
:example "(deftype price number)
|
|
(deftype card-props {:title string :price number})
|
|
(deftype (maybe a) (union a nil))")
|
|
|
|
(define-special-form "defeffect"
|
|
:syntax (defeffect name)
|
|
:doc "Declare a named effect. Effects annotate functions and components
|
|
to track side effects. A pure function (:effects [pure]) cannot
|
|
call IO functions. Unannotated functions are assumed to have all
|
|
effects. Effect checking is gradual — annotations opt in."
|
|
:tail-position "none"
|
|
:example "(defeffect io)
|
|
(defeffect async)
|
|
(define add :effects [pure] (fn (a b) (+ a b)))")
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Sequencing and threading
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define-special-form "begin"
|
|
:syntax (begin expr ...)
|
|
:doc "Evaluate expressions sequentially. Returns the value of the last
|
|
expression. Used when multiple side-effecting expressions need
|
|
to be grouped."
|
|
:tail-position "last expression"
|
|
:example "(begin
|
|
(log \"starting\")
|
|
(process data)
|
|
(log \"done\"))")
|
|
|
|
(define-special-form "do"
|
|
:syntax (do expr ...)
|
|
:doc "Alias for begin."
|
|
:tail-position "last expression"
|
|
:example "(do (set! x 1) (set! y 2) (+ x y))")
|
|
|
|
(define-special-form "->"
|
|
:syntax (-> value form1 form2 ...)
|
|
:doc "Thread-first macro. Threads value through a series of function calls,
|
|
inserting it as the first argument of each form. Nested lists are
|
|
treated as function calls; bare symbols become unary calls."
|
|
:tail-position "last form"
|
|
:example "(-> user
|
|
(get \"name\")
|
|
upper
|
|
(str \" says hello\"))
|
|
;; Expands to: (str (upper (get user \"name\")) \" says hello\")")
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Quoting
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define-special-form "quote"
|
|
:syntax (quote expr)
|
|
:doc "Return expr as data, without evaluating it. Symbols remain symbols,
|
|
lists remain lists. The reader shorthand is the ' prefix."
|
|
:tail-position "none (not evaluated)"
|
|
:example "'(+ 1 2) ;; → the list (+ 1 2), not the number 3")
|
|
|
|
(define-special-form "quasiquote"
|
|
:syntax (quasiquote expr)
|
|
:doc "Template construction. Like quote, but allows unquoting with ~ and
|
|
splicing with ~@. The reader shorthand is the ` prefix.
|
|
`(a ~b ~@c)
|
|
Quotes everything except: ~expr evaluates expr and inserts the
|
|
result; ~@expr evaluates to a list and splices its elements."
|
|
:tail-position "none (template is constructed, not evaluated)"
|
|
:example "`(div :class \"card\" ~title ~@children)")
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Continuations
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define-special-form "reset"
|
|
:syntax (reset body)
|
|
:doc "Establish a continuation delimiter. Evaluates body normally unless
|
|
a shift is encountered, in which case the continuation (the rest
|
|
of the computation up to this reset) is captured and passed to
|
|
the shift's body. Without shift, reset is a no-op wrapper."
|
|
:tail-position "body"
|
|
:example "(reset (+ 1 (shift k (k 10)))) ;; → 11")
|
|
|
|
(define-special-form "shift"
|
|
:syntax (shift k body)
|
|
:doc "Capture the continuation to the nearest reset as k, then evaluate
|
|
body with k bound. If k is never called, the value of body is
|
|
returned from the reset (abort). If k is called with a value,
|
|
the reset body is re-evaluated with shift returning that value.
|
|
k can be called multiple times."
|
|
:tail-position "body"
|
|
:example ";; Abort: shift body becomes the reset result
|
|
(reset (+ 1 (shift k 42))) ;; → 42
|
|
|
|
;; Resume: k re-enters the computation
|
|
(reset (+ 1 (shift k (k 10)))) ;; → 11
|
|
|
|
;; Multiple invocations
|
|
(reset (* 2 (shift k (+ (k 1) (k 10))))) ;; → 24")
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Guards
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define-special-form "dynamic-wind"
|
|
:syntax (dynamic-wind before-thunk body-thunk after-thunk)
|
|
:doc "Entry/exit guards. All three arguments are zero-argument functions
|
|
(thunks). before-thunk is called on entry, body-thunk is called
|
|
for the result, and after-thunk is always called on exit (even on
|
|
error). The wind stack is maintained so that when continuations
|
|
jump across dynamic-wind boundaries, the correct before/after
|
|
thunks fire."
|
|
:tail-position "none (all thunks are eagerly called)"
|
|
:example "(dynamic-wind
|
|
(fn () (log \"entering\"))
|
|
(fn () (do-work))
|
|
(fn () (log \"exiting\")))")
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Higher-order forms
|
|
;;
|
|
;; These are syntactic forms (not primitives) because the evaluator
|
|
;; handles them directly for performance — avoiding the overhead of
|
|
;; constructing argument lists and doing generic dispatch. They could
|
|
;; be implemented as primitives but are special-cased in eval-list.
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define-special-form "map"
|
|
:syntax (map fn coll)
|
|
:doc "Apply fn to each element of coll, returning a list of results."
|
|
:tail-position "none"
|
|
:example "(map (fn (x) (* x x)) (list 1 2 3 4)) ;; → (1 4 9 16)")
|
|
|
|
(define-special-form "map-indexed"
|
|
:syntax (map-indexed fn coll)
|
|
:doc "Like map, but fn receives two arguments: (index element)."
|
|
:tail-position "none"
|
|
:example "(map-indexed (fn (i x) (str i \": \" x)) (list \"a\" \"b\" \"c\"))")
|
|
|
|
(define-special-form "filter"
|
|
:syntax (filter fn coll)
|
|
:doc "Return elements of coll for which fn returns truthy."
|
|
:tail-position "none"
|
|
:example "(filter (fn (x) (> x 3)) (list 1 5 2 8 3)) ;; → (5 8)")
|
|
|
|
(define-special-form "reduce"
|
|
:syntax (reduce fn init coll)
|
|
:doc "Reduce coll to a single value. fn receives (accumulator element)
|
|
and returns the new accumulator. init is the initial value."
|
|
:tail-position "none"
|
|
:example "(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3 4)) ;; → 10")
|
|
|
|
(define-special-form "some"
|
|
:syntax (some fn coll)
|
|
:doc "Return the first truthy result of applying fn to elements of coll,
|
|
or nil if none match. Short-circuits on first truthy result."
|
|
:tail-position "none"
|
|
:example "(some (fn (x) (> x 3)) (list 1 2 5 3)) ;; → true")
|
|
|
|
(define-special-form "every?"
|
|
:syntax (every? fn coll)
|
|
:doc "Return true if fn returns truthy for every element of coll.
|
|
Short-circuits on first falsy result."
|
|
:tail-position "none"
|
|
:example "(every? (fn (x) (> x 0)) (list 1 2 3)) ;; → true")
|
|
|
|
(define-special-form "for-each"
|
|
:syntax (for-each fn coll)
|
|
:doc "Apply fn to each element of coll for side effects. Returns nil."
|
|
:tail-position "none"
|
|
:example "(for-each (fn (x) (log x)) (list 1 2 3))")
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Definition forms (domain-specific)
|
|
;;
|
|
;; These define named entities in the environment. They are special forms
|
|
;; because their arguments have domain-specific structure that the
|
|
;; evaluator parses directly.
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define-special-form "defstyle"
|
|
:syntax (defstyle name expr)
|
|
:doc "Define a named style value. Evaluates expr and binds the result
|
|
to name in the environment. The value is typically a class string
|
|
or a function that returns class strings."
|
|
:tail-position "none"
|
|
:example "(defstyle card-style \"rounded-lg shadow-md p-4 bg-white\")")
|
|
|
|
(define-special-form "defhandler"
|
|
:syntax (defhandler name (&key params ...) body)
|
|
:doc "Define an event handler function. Used by the SxEngine for
|
|
client-side event handling."
|
|
:tail-position "body"
|
|
:example "(defhandler toggle-menu (&key target)
|
|
(toggle-class target \"hidden\"))")
|
|
|
|
(define-special-form "defpage"
|
|
:syntax (defpage name &key route method content ...)
|
|
:doc "Define a page route. Declares the URL pattern, HTTP method, and
|
|
content component for server-side page routing."
|
|
:tail-position "none"
|
|
:example "(defpage dashboard-page
|
|
:route \"/dashboard\"
|
|
:content (~dashboard-content))")
|
|
|
|
(define-special-form "defquery"
|
|
:syntax (defquery name (&key params ...) body)
|
|
:doc "Define a named query for data fetching. Used by the resolver
|
|
system to declare data dependencies."
|
|
:tail-position "body"
|
|
:example "(defquery user-profile (&key user-id)
|
|
(fetch-user user-id))")
|
|
|
|
(define-special-form "defaction"
|
|
:syntax (defaction name (&key params ...) body)
|
|
:doc "Define a named action for mutations. Like defquery but for
|
|
write operations."
|
|
:tail-position "body"
|
|
:example "(defaction update-profile (&key user-id name email)
|
|
(save-user user-id name email))")
|