;; ========================================================================== ;; evaluator.sx — The SX evaluator specification ;; ;; This is the canonical, single-file specification of SX evaluation. ;; All evaluation goes through the CEK machine (explicit control, ;; environment, and continuation). There is no tree-walk interpreter. ;; ;; Structure: ;; Part 1: CEK frames — state and continuation frame constructors ;; Part 2: Evaluation utilities — lambda/component call, keyword arg ;; parsing, macro expansion, quasiquote, definition forms ;; Part 3: CEK machine — step function, frame dispatch, call dispatch ;; ;; The evaluator is written in a restricted subset of SX that bootstrap ;; compilers (JS, Python) can transpile to native code. ;; ;; Platform interface (must be provided by each host): ;; See Part 2 section headers for type constructors, env operations, ;; and rendering primitives. ;; ========================================================================== ;; ************************************************************************** ;; Part 1: CEK Frames — state, continuation, and frame constructors ;; ************************************************************************** ;; -------------------------------------------------------------------------- ;; 1. CEK State constructors ;; -------------------------------------------------------------------------- (define make-cek-state (fn (control env kont) {:control control :env env :kont kont :phase "eval" :value nil})) (define make-cek-value (fn (value env kont) {:control nil :env env :kont kont :phase "continue" :value value})) (define cek-terminal? (fn (state) (and (= (get state "phase") "continue") (empty? (get state "kont"))))) (define cek-control (fn (s) (get s "control"))) (define cek-env (fn (s) (get s "env"))) (define cek-kont (fn (s) (get s "kont"))) (define cek-phase (fn (s) (get s "phase"))) (define cek-value (fn (s) (get s "value"))) ;; -------------------------------------------------------------------------- ;; 2. Frame constructors ;; -------------------------------------------------------------------------- ;; Each frame type is a dict with a "type" key and frame-specific data. ;; IfFrame: waiting for condition value ;; After condition evaluates, choose then or else branch (define make-if-frame (fn (then-expr else-expr env) {:type "if" :then then-expr :else else-expr :env env})) ;; WhenFrame: waiting for condition value ;; If truthy, evaluate body exprs sequentially (define make-when-frame (fn (body-exprs env) {:type "when" :body body-exprs :env env})) ;; BeginFrame: sequential evaluation ;; Remaining expressions to evaluate after current one (define make-begin-frame (fn (remaining env) {:type "begin" :remaining remaining :env env})) ;; LetFrame: binding evaluation in progress ;; name = current binding name, remaining = remaining (name val) pairs ;; body = body expressions to evaluate after all bindings (define make-let-frame (fn (name remaining body local) {:type "let" :name name :remaining remaining :body body :env local})) ;; DefineFrame: waiting for value to bind (define make-define-frame (fn (name env has-effects effect-list) {:type "define" :name name :env env :has-effects has-effects :effect-list effect-list})) ;; SetFrame: waiting for value to assign (define make-set-frame (fn (name env) {:type "set" :name name :env env})) ;; ArgFrame: evaluating function arguments ;; f = function value (already evaluated), evaled = already evaluated args ;; remaining = remaining arg expressions (define make-arg-frame (fn (f evaled remaining env raw-args head-name) {:type "arg" :f f :evaled evaled :remaining remaining :env env :raw-args raw-args :head-name (or head-name nil)})) ;; CallFrame: about to call with fully evaluated args (define make-call-frame (fn (f args env) {:type "call" :f f :args args :env env})) ;; CondFrame: evaluating cond clauses (define make-cond-frame (fn (remaining env scheme?) {:type "cond" :remaining remaining :env env :scheme scheme?})) ;; CaseFrame: evaluating case clauses (define make-case-frame (fn (match-val remaining env) {:type "case" :match-val match-val :remaining remaining :env env})) ;; ThreadFirstFrame: pipe threading (define make-thread-frame (fn (remaining env) {:type "thread" :remaining remaining :env env})) ;; MapFrame: higher-order map/map-indexed in progress (define make-map-frame (fn (f remaining results env) {:type "map" :f f :remaining remaining :results results :env env :indexed false})) (define make-map-indexed-frame (fn (f remaining results env) {:type "map" :f f :remaining remaining :results results :env env :indexed true})) ;; FilterFrame: higher-order filter in progress (define make-filter-frame (fn (f remaining results current-item env) {:type "filter" :f f :remaining remaining :results results :current-item current-item :env env})) ;; ReduceFrame: higher-order reduce in progress (define make-reduce-frame (fn (f remaining env) {:type "reduce" :f f :remaining remaining :env env})) ;; ForEachFrame: higher-order for-each in progress (define make-for-each-frame (fn (f remaining env) {:type "for-each" :f f :remaining remaining :env env})) ;; SomeFrame: higher-order some (short-circuit on first truthy) (define make-some-frame (fn (f remaining env) {:type "some" :f f :remaining remaining :env env})) ;; EveryFrame: higher-order every? (short-circuit on first falsy) (define make-every-frame (fn (f remaining env) {:type "every" :f f :remaining remaining :env env})) ;; ScopeFrame: remaining body expressions for scope special form (define make-scope-frame (fn (name remaining env) {:type "scope" :name name :remaining remaining :env env})) ;; ProvideFrame: dynamic variable binding (context reads this from kont) (define make-provide-frame (fn (name value remaining env) {:type "provide" :name name :value value :remaining remaining :env env})) ;; ScopeAccFrame: accumulator scope (emit! appends, emitted reads) (define make-scope-acc-frame (fn (name value remaining env) {:type "scope-acc" :name name :value (or value nil) :emitted (list) :remaining remaining :env env})) ;; ResetFrame: delimiter for shift/reset continuations (define make-reset-frame (fn (env) {:type "reset" :env env})) ;; DictFrame: evaluating dict values (define make-dict-frame (fn (remaining results env) {:type "dict" :remaining remaining :results results :env env})) ;; AndFrame: short-circuit and (define make-and-frame (fn (remaining env) {:type "and" :remaining remaining :env env})) ;; OrFrame: short-circuit or (define make-or-frame (fn (remaining env) {:type "or" :remaining remaining :env env})) ;; QuasiquoteFrame (not a real frame — QQ is handled specially) ;; DynamicWindFrame: phases of dynamic-wind (define make-dynamic-wind-frame (fn (phase body-thunk after-thunk env) {:type "dynamic-wind" :phase phase :body-thunk body-thunk :after-thunk after-thunk :env env})) ;; ReactiveResetFrame: delimiter for reactive deref-as-shift ;; Carries an update-fn that gets called with new values on re-render. (define make-reactive-reset-frame (fn (env update-fn first-render?) {:type "reactive-reset" :env env :update-fn update-fn :first-render first-render?})) ;; DerefFrame: awaiting evaluation of deref's argument (define make-deref-frame (fn (env) {:type "deref" :env env})) ;; HoSetupFrame: staged evaluation of higher-order form arguments ;; ho-type is "map", "filter", "reduce", etc. ;; Evaluates args one at a time, then dispatches to the iteration frame. (define make-ho-setup-frame (fn (ho-type remaining-args evaled-args env) {:type "ho-setup" :ho-type ho-type :remaining remaining-args :evaled evaled-args :env env})) ;; -------------------------------------------------------------------------- ;; 3. Frame accessors ;; -------------------------------------------------------------------------- (define frame-type (fn (f) (get f "type"))) ;; -------------------------------------------------------------------------- ;; 4. Continuation operations ;; -------------------------------------------------------------------------- (define kont-push (fn (frame kont) (cons frame kont))) (define kont-top (fn (kont) (first kont))) (define kont-pop (fn (kont) (rest kont))) (define kont-empty? (fn (kont) (empty? kont))) ;; -------------------------------------------------------------------------- ;; 5. CEK shift/reset support ;; -------------------------------------------------------------------------- ;; shift captures all frames up to the nearest ResetFrame. ;; reset pushes a ResetFrame. (define kont-capture-to-reset (fn (kont) ;; Returns (captured-frames remaining-kont). ;; captured-frames: frames from top up to (not including) ResetFrame. ;; remaining-kont: frames after ResetFrame. ;; Stops at either "reset" or "reactive-reset" frames. (define scan (fn (k captured) (if (empty? k) (error "shift without enclosing reset") (let ((frame (first k))) (if (or (= (frame-type frame) "reset") (= (frame-type frame) "reactive-reset")) (list captured (rest k)) (scan (rest k) (append captured (list frame)))))))) (scan kont (list)))) ;; Walk kont for nearest ProvideFrame with matching name (define kont-find-provide (fn (kont name) (if (empty? kont) nil (let ((frame (first kont))) (if (and (= (frame-type frame) "provide") (= (get frame "name") name)) frame (kont-find-provide (rest kont) name)))))) ;; Walk kont for nearest ScopeAccFrame with matching name (define kont-find-scope-acc (fn (kont name) (if (empty? kont) nil (let ((frame (first kont))) (if (and (= (frame-type frame) "scope-acc") (= (get frame "name") name)) frame (kont-find-scope-acc (rest kont) name)))))) ;; Check if a ReactiveResetFrame exists anywhere in the continuation (define has-reactive-reset-frame? (fn (kont) (if (empty? kont) false (if (= (frame-type (first kont)) "reactive-reset") true (has-reactive-reset-frame? (rest kont)))))) ;; Capture frames up to nearest ReactiveResetFrame. ;; Returns (captured-frames, reset-frame, remaining-kont). (define kont-capture-to-reactive-reset (fn (kont) (define scan (fn (k captured) (if (empty? k) (error "reactive deref without enclosing reactive-reset") (let ((frame (first k))) (if (= (frame-type frame) "reactive-reset") (list captured frame (rest k)) (scan (rest k) (append captured (list frame)))))))) (scan kont (list)))) ;; -------------------------------------------------------------------------- ;; Extension points — custom special forms and render dispatch ;; -------------------------------------------------------------------------- ;; ;; Extensions (web forms, type system, etc.) register handlers here. ;; The evaluator calls these from step-eval-list after core forms. (define *custom-special-forms* (dict)) (define register-special-form! (fn ((name :as string) handler) (dict-set! *custom-special-forms* name handler))) ;; Render dispatch — installed by web adapters, nil when no renderer active. ;; *render-check*: (expr env) → boolean — should this expression be rendered? ;; *render-fn*: (expr env) → value — render and return result (define *render-check* nil) (define *render-fn* nil) ;; ************************************************************************** ;; Part 2: Evaluation Utilities ;; ************************************************************************** ;; -------------------------------------------------------------------------- ;; 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} ;; island — reactive component: like component but with island flag ;; 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-island ...) → platform Island value (component + island flag) ;; (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 :as any)) ;; 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))))) ;; -------------------------------------------------------------------------- ;; 2b. Strict mode — runtime type checking for primitive calls ;; -------------------------------------------------------------------------- ;; ;; When *strict* is true, primitive calls check arg types before dispatch. ;; The primitive param type registry maps name → {positional [[name type]...], ;; rest-type type-or-nil}. Stored in *prim-param-types* in the env. ;; ;; Strict mode is off by default. Hosts can enable it at startup via: ;; (set-strict! true) ;; (set-prim-param-types! types-dict) (define *strict* false) (define set-strict! (fn (val) (set! *strict* val))) (define *prim-param-types* nil) (define set-prim-param-types! (fn (types) (set! *prim-param-types* types))) (define value-matches-type? (fn (val expected-type) ;; Check if a runtime value matches a declared type string. (cond (= expected-type "any") true (= expected-type "number") (number? val) (= expected-type "string") (string? val) (= expected-type "boolean") (boolean? val) (= expected-type "nil") (nil? val) (= expected-type "list") (list? val) (= expected-type "dict") (dict? val) (= expected-type "lambda") (lambda? val) (= expected-type "symbol") (= (type-of val) "symbol") (= expected-type "keyword") (= (type-of val) "keyword") ;; Nullable: "string?" means string or nil (and (string? expected-type) (ends-with? expected-type "?")) (or (nil? val) (value-matches-type? val (slice expected-type 0 (- (string-length expected-type) 1)))) :else true))) (define strict-check-args (fn (name args) ;; Check args against *prim-param-types* if strict mode is on. ;; Throws on type violation. No-op if *strict* is false or types not registered. (when (and *strict* *prim-param-types*) (let ((spec (get *prim-param-types* name))) (when spec (let ((positional (get spec "positional")) (rest-type (get spec "rest-type"))) ;; Check positional params (when positional (for-each (fn (pair) (let ((idx (first pair)) (param (nth pair 1)) (p-name (first param)) (p-type (nth param 1))) (when (< idx (len args)) (let ((val (nth args idx))) (when (not (value-matches-type? val p-type)) (error (str "Type error: " name " expected " p-type " for param " p-name ", got " (type-of val) " (" (str val) ")"))))))) (map-indexed (fn (i p) (list i p)) positional))) ;; Check rest args (when (and rest-type (> (len args) (len (or positional (list))))) (for-each (fn (pair) (let ((idx (first pair)) (val (nth pair 1))) (when (not (value-matches-type? val rest-type)) (error (str "Type error: " name " expected " rest-type " for rest arg " idx ", got " (type-of val) " (" (str val) ")"))))) (map-indexed (fn (i v) (list i v)) (slice args (len (or positional (list))))))))))))) ;; -------------------------------------------------------------------------- ;; 3. Core evaluator — stub (overridden by CEK in fixups) ;; -------------------------------------------------------------------------- ;; ;; eval-expr and trampoline are defined as stubs here so the transpiler ;; creates the variable declarations. The CEK fixups override them with: ;; eval-expr = (expr, env) → cek-run(make-cek-state(expr, env, [])) ;; trampoline = (val) → if thunk? then eval-expr(thunk-expr, thunk-env) else val ;; All evaluation goes through the CEK machine. ;; eval-expr: forward declaration — redefined at end of file after cek-run exists. ;; This stub is needed so functions between here and Part 3 can reference eval-expr. (define eval-expr (fn (expr (env :as dict)) nil)) ;; -------------------------------------------------------------------------- ;; 5. Function / lambda / component call ;; -------------------------------------------------------------------------- (define call-lambda (fn ((f :as lambda) (args :as list) (caller-env :as dict)) (let ((params (lambda-params f)) (local (env-merge (lambda-closure f) caller-env))) ;; Too many args is an error; too few pads with nil (if (> (len args) (len params)) (error (str (or (lambda-name f) "lambda") " expects " (len params) " args, got " (len args))) (do ;; Bind params — provided args first, then nil for missing (for-each (fn (pair) (env-bind! local (first pair) (nth pair 1))) (zip params args)) (for-each (fn (p) (env-bind! local p nil)) (slice params (len args))) ;; Return thunk for TCO (make-thunk (lambda-body f) local)))))) (define call-component (fn ((comp :as component) (raw-args :as list) (env :as dict)) ;; 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-bind! local p (or (dict-get kwargs p) nil))) (component-params comp)) ;; Bind children if component accepts them (when (component-has-children? comp) (env-bind! local "children" children)) ;; Return thunk — body evaluated in local env (make-thunk (component-body comp) local)))) (define parse-keyword-args (fn ((raw-args :as list) (env :as dict)) ;; 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 ;; -------------------------------------------------------------------------- ;; — all superseded by CEK step handlers in cek.sx ;; cond-scheme? — still needed by CEK's step-sf-cond (define cond-scheme? (fn ((clauses :as list)) (every? (fn (c) (and (= (type-of c) "list") (= (len c) 2))) clauses))) ;; is-else-clause? — check if a cond/case test is an else marker (define is-else-clause? (fn (test) (or (and (= (type-of test) "keyword") (= (keyword-name test) "else")) (and (= (type-of test) "symbol") (or (= (symbol-name test) "else") (= (symbol-name test) ":else")))))) ;; Named let: (let name ((x 0) (y 1)) body...) ;; Desugars to a self-recursive lambda called with initial values. ;; The loop name is bound in the body so recursive calls produce TCO thunks. (define sf-named-let (fn ((args :as list) (env :as dict)) (let ((loop-name (symbol-name (first args))) (bindings (nth args 1)) (body (slice args 2)) (params (list)) (inits (list))) ;; Extract param names and init expressions (if (and (= (type-of (first bindings)) "list") (= (len (first bindings)) 2)) ;; Scheme-style: ((x 0) (y 1)) (for-each (fn (binding) (append! params (if (= (type-of (first binding)) "symbol") (symbol-name (first binding)) (first binding))) (append! inits (nth binding 1))) bindings) ;; Clojure-style: (x 0 y 1) (reduce (fn (acc pair-idx) (do (append! params (if (= (type-of (nth bindings (* pair-idx 2))) "symbol") (symbol-name (nth bindings (* pair-idx 2))) (nth bindings (* pair-idx 2)))) (append! inits (nth bindings (inc (* pair-idx 2)))))) nil (range 0 (/ (len bindings) 2)))) ;; Build loop body (wrap in begin if multiple exprs) (let ((loop-body (if (= (len body) 1) (first body) (cons (make-symbol "begin") body))) (loop-fn (make-lambda params loop-body env))) ;; Self-reference: loop can call itself by name (set-lambda-name! loop-fn loop-name) (env-bind! (lambda-closure loop-fn) loop-name loop-fn) ;; Evaluate initial values in enclosing env, then call (let ((init-vals (map (fn (e) (trampoline (eval-expr e env))) inits))) (call-lambda loop-fn init-vals env)))))) (define sf-lambda (fn ((args :as list) (env :as dict)) (let ((params-expr (first args)) (body-exprs (rest args)) (body (if (= (len body-exprs) 1) (first body-exprs) (cons (make-symbol "begin") body-exprs))) (param-names (map (fn (p) (cond (= (type-of p) "symbol") (symbol-name p) ;; Annotated param: (name :as type) → extract name (and (= (type-of p) "list") (= (len p) 3) (= (type-of (nth p 1)) "keyword") (= (keyword-name (nth p 1)) "as")) (symbol-name (first p)) :else p)) params-expr))) (make-lambda param-names body env)))) (define sf-defcomp (fn ((args :as list) (env :as dict)) ;; (defcomp ~name (params) [:affinity :client|:server] body) ;; Body is always the last element. Optional keyword annotations ;; may appear between the params list and the body. (let ((name-sym (first args)) (params-raw (nth args 1)) (body (last args)) (comp-name (strip-prefix (symbol-name name-sym) "~")) (parsed (parse-comp-params params-raw)) (params (first parsed)) (has-children (nth parsed 1)) (param-types (nth parsed 2)) (affinity (defcomp-kwarg args "affinity" "auto"))) (let ((comp (make-component comp-name params has-children body env affinity)) (effects (defcomp-kwarg args "effects" nil))) ;; Store type annotations if any were declared (when (and (not (nil? param-types)) (not (empty? (keys param-types)))) (component-set-param-types! comp param-types)) ;; Store effect annotation if declared (when (not (nil? effects)) (let ((effect-list (if (= (type-of effects) "list") (map (fn (e) (if (= (type-of e) "symbol") (symbol-name e) (str e))) effects) (list (str effects)))) (effect-anns (if (env-has? env "*effect-annotations*") (env-get env "*effect-annotations*") (dict)))) (dict-set! effect-anns (symbol-name name-sym) effect-list) (env-bind! env "*effect-annotations*" effect-anns))) (env-bind! env (symbol-name name-sym) comp) comp)))) (define defcomp-kwarg (fn ((args :as list) (key :as string) default) ;; Search for :key value between params (index 2) and body (last). (let ((end (- (len args) 1)) (result default)) (for-each (fn (i) (when (and (= (type-of (nth args i)) "keyword") (= (keyword-name (nth args i)) key) (< (+ i 1) end)) (let ((val (nth args (+ i 1)))) (set! result (if (= (type-of val) "keyword") (keyword-name val) val))))) (range 2 end 1)) result))) (define parse-comp-params (fn ((params-expr :as list)) ;; Parse (&key param1 param2 &children) → (params has-children param-types) ;; Also accepts &rest as synonym for &children. ;; Supports typed params: (name :as type) — a 3-element list where ;; the second element is the keyword :as. Unannotated params get no ;; type entry. param-types is a dict {name → type-expr} or empty dict. (let ((params (list)) (param-types (dict)) (has-children false) (in-key false)) (for-each (fn (p) (if (and (= (type-of p) "list") (= (len p) 3) (= (type-of (first p)) "symbol") (= (type-of (nth p 1)) "keyword") (= (keyword-name (nth p 1)) "as")) ;; Typed param: (name :as type) (let ((name (symbol-name (first p))) (ptype (nth p 2))) ;; Convert type to string if it's a symbol (let ((type-val (if (= (type-of ptype) "symbol") (symbol-name ptype) ptype))) (when (not has-children) (append! params name) (dict-set! param-types name type-val)))) ;; Untyped param or marker (when (= (type-of p) "symbol") (let ((name (symbol-name p))) (cond (= name "&key") (set! in-key true) (= name "&rest") (set! has-children true) (= name "&children") (set! has-children true) has-children nil ;; skip params after &children/&rest in-key (append! params name) :else (append! params name)))))) params-expr) (list params has-children param-types)))) (define sf-defisland (fn ((args :as list) (env :as dict)) ;; (defisland ~name (params) body) ;; Like defcomp but creates an island (reactive component). ;; Islands have the same calling convention as components but ;; render with a reactive context on the client. (let ((name-sym (first args)) (params-raw (nth args 1)) (body (last args)) (comp-name (strip-prefix (symbol-name name-sym) "~")) (parsed (parse-comp-params params-raw)) (params (first parsed)) (has-children (nth parsed 1))) (let ((island (make-island comp-name params has-children body env))) (env-bind! env (symbol-name name-sym) island) island)))) (define sf-defmacro (fn ((args :as list) (env :as dict)) (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-bind! env (symbol-name name-sym) mac) mac)))) (define parse-macro-params (fn ((params-expr :as list)) ;; 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 qq-expand (fn (template (env :as dict)) (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 (concat result (list spliced))))) (concat result (list (qq-expand item env))))) (list) template))))))) ;; -------------------------------------------------------------------------- ;; 6c. letrec — mutually recursive local bindings ;; -------------------------------------------------------------------------- ;; ;; (letrec ((even? (fn (n) (if (= n 0) true (odd? (- n 1))))) ;; (odd? (fn (n) (if (= n 0) false (even? (- n 1)))))) ;; (even? 10)) ;; ;; All bindings are first set to nil in the local env, then all values ;; are evaluated (so they can see each other's names), then lambda ;; closures are patched to include the final bindings. ;; -------------------------------------------------------------------------- (define sf-letrec (fn ((args :as list) (env :as dict)) (let ((bindings (first args)) (body (rest args)) (local (env-extend env)) (names (list)) (val-exprs (list))) ;; First pass: bind all names to nil (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)))) (append! names vname) (append! val-exprs (nth binding 1)) (env-bind! local vname nil))) bindings) ;; Clojure-style (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))))) (append! names vname) (append! val-exprs val-expr) (env-bind! local vname nil))) nil (range 0 (/ (len bindings) 2)))) ;; Second pass: evaluate values (they can see each other's names) (let ((values (map (fn (e) (trampoline (eval-expr e local))) val-exprs))) ;; Bind final values (for-each (fn (pair) (env-bind! local (first pair) (nth pair 1))) (zip names values)) ;; Patch lambda closures so they see the final bindings (for-each (fn (val) (when (lambda? val) (for-each (fn (n) (env-bind! (lambda-closure val) n (env-get local n))) names))) values)) ;; Evaluate body (for-each (fn (e) (trampoline (eval-expr e local))) (slice body 0 (dec (len body)))) (make-thunk (last body) local)))) ;; -------------------------------------------------------------------------- ;; 6d. dynamic-wind — entry/exit guards ;; -------------------------------------------------------------------------- ;; ;; (dynamic-wind before-thunk body-thunk after-thunk) ;; ;; All three are zero-argument functions (thunks): ;; 1. Call before-thunk ;; 2. Call body-thunk, capture result ;; 3. Call after-thunk (always, even on error) ;; 4. Return body result ;; ;; The wind stack is maintained so that when continuations jump across ;; dynamic-wind boundaries, the correct before/after thunks fire. ;; Without active continuations, this is equivalent to try/finally. ;; ;; Platform requirements: ;; (push-wind! before after) — push wind record onto stack ;; (pop-wind!) — pop wind record from stack ;; (call-thunk f env) — call a zero-arg function ;; -------------------------------------------------------------------------- (define sf-dynamic-wind (fn ((args :as list) (env :as dict)) (let ((before (trampoline (eval-expr (first args) env))) (body (trampoline (eval-expr (nth args 1) env))) (after (trampoline (eval-expr (nth args 2) env)))) ;; Delegate to platform — needs try/finally for error safety (dynamic-wind-call before body after env)))) ;; -------------------------------------------------------------------------- ;; 6a2. scope — unified render-time dynamic scope primitive ;; -------------------------------------------------------------------------- ;; ;; (scope name body...) or (scope name :value v body...) ;; Push a named scope with optional value and empty accumulator, ;; evaluate body, pop scope. Returns last body result. ;; ;; `provide` is sugar: (provide name value body...) = (scope name :value value body...) (define sf-scope (fn ((args :as list) (env :as dict)) (let ((name (trampoline (eval-expr (first args) env))) (rest (slice args 1)) (val nil) (body-exprs nil)) ;; Check for :value keyword (if (and (>= (len rest) 2) (= (type-of (first rest)) "keyword") (= (keyword-name (first rest)) "value")) (do (set! val (trampoline (eval-expr (nth rest 1) env))) (set! body-exprs (slice rest 2))) (set! body-exprs rest)) (scope-push! name val) (let ((result nil)) (for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs) (scope-pop! name) result)))) ;; provide — sugar for scope with a value ;; (provide name value body...) → (scope name :value value body...) (define sf-provide (fn ((args :as list) (env :as dict)) (let ((name (trampoline (eval-expr (first args) env))) (val (trampoline (eval-expr (nth args 1) env))) (body-exprs (slice args 2)) (result nil)) (scope-push! name val) (for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs) (scope-pop! name) result))) ;; -------------------------------------------------------------------------- ;; 6b. Macro expansion ;; -------------------------------------------------------------------------- (define expand-macro (fn ((mac :as macro) (raw-args :as list) (env :as dict)) (let ((local (env-merge (macro-closure mac) env))) ;; Bind positional params (unevaluated) (for-each (fn (pair) (env-bind! 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-bind! local (macro-rest-param mac) (slice raw-args (len (macro-params mac))))) ;; Evaluate body → new AST (trampoline (eval-expr (macro-body mac) local))))) ;; -------------------------------------------------------------------------- ;; 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" ;; | "spread" ;; (symbol-name sym) → string ;; (keyword-name kw) → string ;; ;; Constructors: ;; (make-lambda params body env) → Lambda ;; (make-component name params has-children body env affinity) → 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 ;; (component-affinity c) → "auto" | "client" | "server" ;; ;; (make-island name params has-children body env) → Island ;; (island? x) → boolean ;; ;; Islands reuse component accessors: component-params, component-body, etc. ;; ;; (make-spread attrs) → Spread (attrs dict injected onto parent element) ;; (spread? x) → boolean ;; (spread-attrs s) → dict ;; ;; (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 ;; (island? 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-bind! env name val) → void (create binding on THIS env, no chain walk) ;; (env-set! env name val) → void (mutate existing binding, walks scope chain) ;; (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 ;; ;; ;; Dynamic wind (for dynamic-wind): ;; (push-wind! before after) → void (push wind record onto stack) ;; (pop-wind!) → void (pop wind record from stack) ;; (call-thunk f env) → value (call a zero-arg function) ;; ;; Extension hooks (set by web adapters, type system, etc.): ;; *custom-special-forms* — dict of name → handler fn ;; register-special-form! — (name handler) → registers custom form ;; *render-check* — nil or (expr env) → boolean ;; *render-fn* — nil or (expr env) → value ;; -------------------------------------------------------------------------- ;; ************************************************************************** ;; Part 3: CEK Machine — the sole evaluator ;; ************************************************************************** ;; -------------------------------------------------------------------------- ;; 1. Run loop — drive the CEK machine to completion ;; -------------------------------------------------------------------------- (define cek-run (fn (state) ;; Drive the CEK machine until terminal state. ;; Returns the final value. (if (cek-terminal? state) (cek-value state) (cek-run (cek-step state))))) ;; -------------------------------------------------------------------------- ;; 2. Step function — single CEK step ;; -------------------------------------------------------------------------- (define cek-step (fn (state) (if (= (cek-phase state) "eval") (step-eval state) (step-continue state)))) ;; -------------------------------------------------------------------------- ;; 3. step-eval — Control is an expression, dispatch on type ;; -------------------------------------------------------------------------- (define step-eval (fn (state) (let ((expr (cek-control state)) (env (cek-env state)) (kont (cek-kont state))) (case (type-of expr) ;; --- Literals: immediate value --- "number" (make-cek-value expr env kont) "string" (make-cek-value expr env kont) "boolean" (make-cek-value expr env kont) "nil" (make-cek-value nil env kont) ;; --- Symbol lookup --- "symbol" (let ((name (symbol-name expr))) (let ((val (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))))) ;; Warn when a ~component symbol resolves to nil (likely missing) (when (and (nil? val) (starts-with? name "~")) (debug-log "Component not found:" name)) (make-cek-value val env kont))) ;; --- Keyword → string --- "keyword" (make-cek-value (keyword-name expr) env kont) ;; --- Dict literal: evaluate values --- "dict" (let ((ks (keys expr))) (if (empty? ks) (make-cek-value (dict) env kont) ;; Build entry pairs from dict, evaluate first value (let ((first-key (first ks)) (remaining-entries (list))) (for-each (fn (k) (append! remaining-entries (list k (get expr k)))) (rest ks)) (make-cek-state (get expr first-key) env (kont-push (make-dict-frame remaining-entries (list (list first-key)) ;; results: list of (key) waiting for val env) kont))))) ;; --- List = call or special form --- "list" (if (empty? expr) (make-cek-value (list) env kont) (step-eval-list expr env kont)) ;; --- Anything else passes through --- :else (make-cek-value expr env kont))))) ;; -------------------------------------------------------------------------- ;; 4. step-eval-list — Dispatch on list head ;; -------------------------------------------------------------------------- (define step-eval-list (fn (expr env kont) (let ((head (first expr)) (args (rest expr))) ;; If head isn't symbol/lambda/list → treat as data list (if (not (or (= (type-of head) "symbol") (= (type-of head) "lambda") (= (type-of head) "list"))) ;; Evaluate as data list — evaluate each element (if (empty? expr) (make-cek-value (list) env kont) (make-cek-state (first expr) env (kont-push (make-map-frame nil (rest expr) (list) env) kont))) ;; Head is symbol — check special forms (if (= (type-of head) "symbol") (let ((name (symbol-name head))) (cond ;; --- Special forms → push appropriate frame --- (= name "if") (step-sf-if args env kont) (= name "when") (step-sf-when args env kont) (= name "cond") (step-sf-cond args env kont) (= name "case") (step-sf-case args env kont) (= name "and") (step-sf-and args env kont) (= name "or") (step-sf-or args env kont) (= name "let") (step-sf-let args env kont) (= name "let*") (step-sf-let args env kont) (= name "lambda") (step-sf-lambda args env kont) (= name "fn") (step-sf-lambda args env kont) (= name "define") (step-sf-define args env kont) (= name "defcomp") (make-cek-value (sf-defcomp args env) env kont) (= name "defisland") (make-cek-value (sf-defisland args env) env kont) (= name "defmacro") (make-cek-value (sf-defmacro args env) env kont) (= name "begin") (step-sf-begin args env kont) (= name "do") (step-sf-begin args env kont) (= name "quote") (make-cek-value (if (empty? args) nil (first args)) env kont) (= name "quasiquote") (make-cek-value (qq-expand (first args) env) env kont) (= name "->") (step-sf-thread-first args env kont) (= name "set!") (step-sf-set! args env kont) (= name "letrec") (make-cek-value (sf-letrec args env) env kont) ;; Continuations — native in CEK (= name "reset") (step-sf-reset args env kont) (= name "shift") (step-sf-shift args env kont) ;; Reactive deref-as-shift (= name "deref") (step-sf-deref args env kont) ;; Scoped effects — frame-based dynamic scope (= name "scope") (step-sf-scope args env kont) (= name "provide") (step-sf-provide args env kont) (= name "context") (step-sf-context args env kont) (= name "emit!") (step-sf-emit args env kont) (= name "emitted") (step-sf-emitted args env kont) ;; Dynamic wind (= name "dynamic-wind") (make-cek-value (sf-dynamic-wind args env) env kont) ;; Higher-order forms (= name "map") (step-ho-map args env kont) (= name "map-indexed") (step-ho-map-indexed args env kont) (= name "filter") (step-ho-filter args env kont) (= name "reduce") (step-ho-reduce args env kont) (= name "some") (step-ho-some args env kont) (= name "every?") (step-ho-every args env kont) (= name "for-each") (step-ho-for-each args env kont) ;; Custom special forms (registered by extensions) (has-key? *custom-special-forms* name) (make-cek-value ((get *custom-special-forms* name) args env) env kont) ;; Macro expansion (and (env-has? env name) (macro? (env-get env name))) (let ((mac (env-get env name))) (make-cek-state (expand-macro mac args env) env kont)) ;; Render dispatch (installed by web adapters) (and *render-check* (*render-check* expr env)) (make-cek-value (*render-fn* expr env) env kont) ;; Fall through to function call :else (step-eval-call head args env kont))) ;; Head is lambda or list — function call (step-eval-call head args env kont)))))) ;; -------------------------------------------------------------------------- ;; 5. Special form step handlers ;; -------------------------------------------------------------------------- ;; if: evaluate condition, push IfFrame (define step-sf-if (fn (args env kont) (make-cek-state (first args) env (kont-push (make-if-frame (nth args 1) (if (> (len args) 2) (nth args 2) nil) env) kont)))) ;; when: evaluate condition, push WhenFrame (define step-sf-when (fn (args env kont) (make-cek-state (first args) env (kont-push (make-when-frame (rest args) env) kont)))) ;; begin/do: evaluate first expr, push BeginFrame for rest (define step-sf-begin (fn (args env kont) (if (empty? args) (make-cek-value nil env kont) (if (= (len args) 1) (make-cek-state (first args) env kont) (make-cek-state (first args) env (kont-push (make-begin-frame (rest args) env) kont)))))) ;; let: start evaluating bindings (define step-sf-let (fn (args env kont) ;; Detect named let (if (= (type-of (first args)) "symbol") ;; Named let — delegate to existing handler (complex desugaring) (make-cek-value (sf-named-let args env) env kont) (let ((bindings (first args)) (body (rest args)) (local (env-extend env))) ;; Parse first binding (if (empty? bindings) ;; No bindings — evaluate body (step-sf-begin body local kont) ;; Start evaluating first binding value (let ((first-binding (if (and (= (type-of (first bindings)) "list") (= (len (first bindings)) 2)) ;; Scheme-style: ((name val) ...) (first bindings) ;; Clojure-style: (name val ...) → synthesize pair (list (first bindings) (nth bindings 1)))) (rest-bindings (if (and (= (type-of (first bindings)) "list") (= (len (first bindings)) 2)) (rest bindings) ;; Clojure-style: skip 2 elements (let ((pairs (list))) (reduce (fn (acc i) (append! pairs (list (nth bindings (* i 2)) (nth bindings (inc (* i 2)))))) nil (range 1 (/ (len bindings) 2))) pairs)))) (let ((vname (if (= (type-of (first first-binding)) "symbol") (symbol-name (first first-binding)) (first first-binding)))) (make-cek-state (nth first-binding 1) local (kont-push (make-let-frame vname rest-bindings body local) kont))))))))) ;; define: evaluate value expression (define step-sf-define (fn (args env kont) (let ((name-sym (first args)) (has-effects (and (>= (len args) 4) (= (type-of (nth args 1)) "keyword") (= (keyword-name (nth args 1)) "effects"))) (val-idx (if (and (>= (len args) 4) (= (type-of (nth args 1)) "keyword") (= (keyword-name (nth args 1)) "effects")) 3 1)) (effect-list (if (and (>= (len args) 4) (= (type-of (nth args 1)) "keyword") (= (keyword-name (nth args 1)) "effects")) (nth args 2) nil))) (make-cek-state (nth args val-idx) env (kont-push (make-define-frame (symbol-name name-sym) env has-effects effect-list) kont))))) ;; set!: evaluate value (define step-sf-set! (fn (args env kont) (make-cek-state (nth args 1) env (kont-push (make-set-frame (symbol-name (first args)) env) kont)))) ;; and: evaluate first, push AndFrame (define step-sf-and (fn (args env kont) (if (empty? args) (make-cek-value true env kont) (make-cek-state (first args) env (kont-push (make-and-frame (rest args) env) kont))))) ;; or: evaluate first, push OrFrame (define step-sf-or (fn (args env kont) (if (empty? args) (make-cek-value false env kont) (make-cek-state (first args) env (kont-push (make-or-frame (rest args) env) kont))))) ;; cond: evaluate first test, push CondFrame (define step-sf-cond (fn (args env kont) (let ((scheme? (cond-scheme? args))) (if scheme? ;; Scheme-style: ((test body) ...) (if (empty? args) (make-cek-value nil env kont) (let ((clause (first args)) (test (first clause))) ;; Check for :else / else (if (is-else-clause? test) (make-cek-state (nth clause 1) env kont) (make-cek-state test env (kont-push (make-cond-frame args env true) kont))))) ;; Clojure-style: test body test body ... (if (< (len args) 2) (make-cek-value nil env kont) (let ((test (first args))) (if (is-else-clause? test) (make-cek-state (nth args 1) env kont) (make-cek-state test env (kont-push (make-cond-frame args env false) kont))))))))) ;; case: evaluate match value (define step-sf-case (fn (args env kont) (make-cek-state (first args) env (kont-push (make-case-frame nil (rest args) env) kont)))) ;; thread-first: evaluate initial value (define step-sf-thread-first (fn (args env kont) (make-cek-state (first args) env (kont-push (make-thread-frame (rest args) env) kont)))) ;; lambda/fn: immediate — create lambda value (define step-sf-lambda (fn (args env kont) (make-cek-value (sf-lambda args env) env kont))) ;; scope: evaluate name, then push ScopeFrame ;; scope/provide/context/emit!/emitted — ALL use hashtable stacks. ;; One world: the aser and CEK share the same scope mechanism. ;; No continuation frame walking — scope-push!/pop!/peek are the primitives. ;; scope: push scope, evaluate body, pop scope. ;; (scope name body...) or (scope name :value v body...) (define step-sf-scope (fn (args env kont) (let ((name (trampoline (eval-expr (first args) env))) (rest-args (slice args 1)) (val nil) (body nil)) (if (and (>= (len rest-args) 2) (= (type-of (first rest-args)) "keyword") (= (keyword-name (first rest-args)) "value")) (do (set! val (trampoline (eval-expr (nth rest-args 1) env))) (set! body (slice rest-args 2))) (set! body rest-args)) (scope-push! name val) (let ((result nil)) (for-each (fn (expr) (set! result (trampoline (eval-expr expr env)))) body) (scope-pop! name) (make-cek-value result env kont))))) ;; provide: sugar for scope with value. (define step-sf-provide (fn (args env kont) (let ((name (trampoline (eval-expr (first args) env))) (val (trampoline (eval-expr (nth args 1) env))) (body (slice args 2))) (scope-push! name val) (let ((result nil)) (for-each (fn (expr) (set! result (trampoline (eval-expr expr env)))) body) (scope-pop! name) (make-cek-value result env kont))))) ;; context: read from scope stack. (define step-sf-context (fn (args env kont) (let ((name (trampoline (eval-expr (first args) env))) (default-val (if (>= (len args) 2) (trampoline (eval-expr (nth args 1) env)) nil)) (val (scope-peek name))) (make-cek-value (if (nil? val) default-val val) env kont)))) ;; emit!: append to scope accumulator. (define step-sf-emit (fn (args env kont) (let ((name (trampoline (eval-expr (first args) env))) (val (trampoline (eval-expr (nth args 1) env)))) (scope-emit! name val) (make-cek-value nil env kont)))) ;; emitted: read accumulated scope values. (define step-sf-emitted (fn (args env kont) (let ((name (trampoline (eval-expr (first args) env))) (val (scope-peek name))) (make-cek-value (if (nil? val) (list) val) env kont)))) ;; reset: push ResetFrame, evaluate body (define step-sf-reset (fn (args env kont) (make-cek-state (first args) env (kont-push (make-reset-frame env) kont)))) ;; shift: capture frames to nearest reset (define step-sf-shift (fn (args env kont) (let ((k-name (symbol-name (first args))) (body (nth args 1)) (captured-result (kont-capture-to-reset kont)) (captured (first captured-result)) (rest-kont (nth captured-result 1))) ;; Store captured frames as a dict on the continuation value. ;; When the continuation is invoked, continue-with-call detects ;; the cek-frames key and restores them. (let ((k (make-cek-continuation captured rest-kont))) ;; Evaluate shift body with k bound, continuation goes to rest-kont (let ((shift-env (env-extend env))) (env-bind! shift-env k-name k) (make-cek-state body shift-env rest-kont)))))) ;; deref: evaluate argument, push DerefFrame (define step-sf-deref (fn (args env kont) (make-cek-state (first args) env (kont-push (make-deref-frame env) kont)))) ;; cek-call — call a function via CEK (replaces invoke) ;; cek-call — unified function dispatch ;; Both lambdas and native callables go through continue-with-call ;; so they interact identically with the continuation stack. ;; This is critical: replacing a native callable with an SX lambda ;; (e.g. stdlib.sx) must not change shift/reset behavior. (define cek-call (fn (f args) (let ((a (if (nil? args) (list) args))) (cond (nil? f) nil (or (lambda? f) (callable? f)) (cek-run (continue-with-call f a (make-env) a (list))) :else nil)))) ;; reactive-shift-deref: the heart of deref-as-shift ;; When deref encounters a signal inside a reactive-reset boundary, ;; capture the continuation up to the reactive-reset as the subscriber. (define reactive-shift-deref (fn (sig env kont) (let ((scan-result (kont-capture-to-reactive-reset kont)) (captured-frames (first scan-result)) (reset-frame (nth scan-result 1)) (remaining-kont (nth scan-result 2)) (update-fn (get reset-frame "update-fn"))) ;; Sub-scope for nested subscriber cleanup on re-invocation (let ((sub-disposers (list))) (let ((subscriber (fn () ;; Dispose previous nested subscribers (for-each (fn (d) (cek-call d nil)) sub-disposers) (set! sub-disposers (list)) ;; Re-invoke: push fresh ReactiveResetFrame (first-render=false) (let ((new-reset (make-reactive-reset-frame env update-fn false)) (new-kont (concat captured-frames (list new-reset) remaining-kont))) (with-island-scope (fn (d) (append! sub-disposers d)) (fn () (cek-run (make-cek-value (signal-value sig) env new-kont)))))))) ;; Register subscriber (signal-add-sub! sig subscriber) ;; Register cleanup with island scope (register-in-scope (fn () (signal-remove-sub! sig subscriber) (for-each (fn (d) (cek-call d nil)) sub-disposers))) ;; Initial render: value flows through captured frames + reset (first-render=true) ;; so the full expression completes normally (let ((initial-kont (concat captured-frames (list reset-frame) remaining-kont))) (make-cek-value (signal-value sig) env initial-kont))))))) ;; -------------------------------------------------------------------------- ;; 6. Function call step handler ;; -------------------------------------------------------------------------- (define step-eval-call (fn (head args env kont) ;; First evaluate the head, then evaluate args left-to-right ;; Preserve head name for strict mode type checking (let ((hname (if (= (type-of head) "symbol") (symbol-name head) nil))) (make-cek-state head env (kont-push (make-arg-frame nil (list) args env args hname) kont))))) ;; -------------------------------------------------------------------------- ;; 7. Higher-order form step handlers ;; -------------------------------------------------------------------------- ;; CEK-native higher-order forms — each callback invocation goes through ;; continue-with-call so deref-as-shift works inside callbacks. ;; Function and collection args are evaluated via tree-walk (simple exprs), ;; then the loop is driven by CEK frames. ;; HO step handlers — push HoSetupFrame to evaluate args via CEK ;; (no nested eval-expr calls). When all args are evaluated, the ;; HoSetupFrame dispatch in step-continue sets up the iteration frame. ;; ho-form-name? — is this symbol name a higher-order special form? (define ho-form-name? (fn (name) (or (= name "map") (= name "map-indexed") (= name "filter") (= name "reduce") (= name "some") (= name "every?") (= name "for-each")))) ;; ho-fn? — is this value usable as a HO callback? (define ho-fn? (fn (v) (or (callable? v) (lambda? v)))) ;; ho-swap-args: normalise data-first arg order ;; 2-arg forms: (coll fn) → (fn coll) ;; 3-arg reduce: (coll fn init) → (fn init coll) (define ho-swap-args (fn (ho-type evaled) (if (= ho-type "reduce") (let ((a (first evaled)) (b (nth evaled 1))) (if (and (not (ho-fn? a)) (ho-fn? b)) (list b (nth evaled 2) a) evaled)) (let ((a (first evaled)) (b (nth evaled 1))) (if (and (not (ho-fn? a)) (ho-fn? b)) (list b a) evaled))))) ;; ho-setup-dispatch: all HO args evaluated, set up iteration (define ho-setup-dispatch (fn (ho-type evaled env kont) (let ((ordered (ho-swap-args ho-type evaled))) (let ((f (first ordered))) (cond (= ho-type "map") (let ((coll (nth ordered 1))) (if (empty? coll) (make-cek-value (list) env kont) (continue-with-call f (list (first coll)) env (list) (kont-push (make-map-frame f (rest coll) (list) env) kont)))) (= ho-type "map-indexed") (let ((coll (nth ordered 1))) (if (empty? coll) (make-cek-value (list) env kont) (continue-with-call f (list 0 (first coll)) env (list) (kont-push (make-map-indexed-frame f (rest coll) (list) env) kont)))) (= ho-type "filter") (let ((coll (nth ordered 1))) (if (empty? coll) (make-cek-value (list) env kont) (continue-with-call f (list (first coll)) env (list) (kont-push (make-filter-frame f (rest coll) (list) (first coll) env) kont)))) (= ho-type "reduce") (let ((init (nth ordered 1)) (coll (nth ordered 2))) (if (empty? coll) (make-cek-value init env kont) (continue-with-call f (list init (first coll)) env (list) (kont-push (make-reduce-frame f (rest coll) env) kont)))) (= ho-type "some") (let ((coll (nth ordered 1))) (if (empty? coll) (make-cek-value false env kont) (continue-with-call f (list (first coll)) env (list) (kont-push (make-some-frame f (rest coll) env) kont)))) (= ho-type "every") (let ((coll (nth ordered 1))) (if (empty? coll) (make-cek-value true env kont) (continue-with-call f (list (first coll)) env (list) (kont-push (make-every-frame f (rest coll) env) kont)))) (= ho-type "for-each") (let ((coll (nth ordered 1))) (if (empty? coll) (make-cek-value nil env kont) (continue-with-call f (list (first coll)) env (list) (kont-push (make-for-each-frame f (rest coll) env) kont)))) :else (error (str "Unknown HO type: " ho-type))))))) (define step-ho-map (fn (args env kont) (make-cek-state (first args) env (kont-push (make-ho-setup-frame "map" (rest args) (list) env) kont)))) (define step-ho-map-indexed (fn (args env kont) (make-cek-state (first args) env (kont-push (make-ho-setup-frame "map-indexed" (rest args) (list) env) kont)))) (define step-ho-filter (fn (args env kont) (make-cek-state (first args) env (kont-push (make-ho-setup-frame "filter" (rest args) (list) env) kont)))) (define step-ho-reduce (fn (args env kont) (make-cek-state (first args) env (kont-push (make-ho-setup-frame "reduce" (rest args) (list) env) kont)))) (define step-ho-some (fn (args env kont) (make-cek-state (first args) env (kont-push (make-ho-setup-frame "some" (rest args) (list) env) kont)))) (define step-ho-every (fn (args env kont) (make-cek-state (first args) env (kont-push (make-ho-setup-frame "every" (rest args) (list) env) kont)))) (define step-ho-for-each (fn (args env kont) (make-cek-state (first args) env (kont-push (make-ho-setup-frame "for-each" (rest args) (list) env) kont)))) ;; -------------------------------------------------------------------------- ;; 8. step-continue — Value produced, dispatch on top frame ;; -------------------------------------------------------------------------- (define step-continue (fn (state) (let ((value (cek-value state)) (env (cek-env state)) (kont (cek-kont state))) (if (kont-empty? kont) state ;; Terminal — return as-is (let ((frame (kont-top kont)) (rest-k (kont-pop kont)) (ft (frame-type frame))) (cond ;; --- IfFrame: condition evaluated --- (= ft "if") (if (and value (not (nil? value))) (make-cek-state (get frame "then") (get frame "env") rest-k) (if (nil? (get frame "else")) (make-cek-value nil env rest-k) (make-cek-state (get frame "else") (get frame "env") rest-k))) ;; --- WhenFrame: condition evaluated --- (= ft "when") (if (and value (not (nil? value))) (let ((body (get frame "body")) (fenv (get frame "env"))) (if (empty? body) (make-cek-value nil fenv rest-k) (if (= (len body) 1) (make-cek-state (first body) fenv rest-k) (make-cek-state (first body) fenv (kont-push (make-begin-frame (rest body) fenv) rest-k))))) (make-cek-value nil env rest-k)) ;; --- BeginFrame: expression evaluated, continue with next --- (= ft "begin") (let ((remaining (get frame "remaining")) (fenv (get frame "env"))) (if (empty? remaining) (make-cek-value value fenv rest-k) (if (= (len remaining) 1) (make-cek-state (first remaining) fenv rest-k) (make-cek-state (first remaining) fenv (kont-push (make-begin-frame (rest remaining) fenv) rest-k))))) ;; --- LetFrame: binding value evaluated --- (= ft "let") (let ((name (get frame "name")) (remaining (get frame "remaining")) (body (get frame "body")) (local (get frame "env"))) ;; Bind the value (env-bind! local name value) ;; More bindings? (if (empty? remaining) ;; All bindings done — evaluate body (step-sf-begin body local rest-k) ;; Next binding (let ((next-binding (first remaining)) (vname (if (= (type-of (first next-binding)) "symbol") (symbol-name (first next-binding)) (first next-binding)))) (make-cek-state (nth next-binding 1) local (kont-push (make-let-frame vname (rest remaining) body local) rest-k))))) ;; --- DefineFrame: value evaluated --- (= ft "define") (let ((name (get frame "name")) (fenv (get frame "env")) (has-effects (get frame "has-effects")) (effect-list (get frame "effect-list"))) (when (and (lambda? value) (nil? (lambda-name value))) (set-lambda-name! value name)) (env-bind! fenv name value) ;; Effect annotation (when has-effects (let ((effect-names (if (= (type-of effect-list) "list") (map (fn (e) (if (= (type-of e) "symbol") (symbol-name e) (str e))) effect-list) (list (str effect-list)))) (effect-anns (if (env-has? fenv "*effect-annotations*") (env-get fenv "*effect-annotations*") (dict)))) (dict-set! effect-anns name effect-names) (env-bind! fenv "*effect-annotations*" effect-anns))) (make-cek-value value fenv rest-k)) ;; --- SetFrame: value evaluated --- (= ft "set") (let ((name (get frame "name")) (fenv (get frame "env"))) (env-set! fenv name value) (make-cek-value value env rest-k)) ;; --- AndFrame: value evaluated --- (= ft "and") (if (not value) (make-cek-value value env rest-k) (let ((remaining (get frame "remaining"))) (if (empty? remaining) (make-cek-value value env rest-k) (make-cek-state (first remaining) (get frame "env") (if (= (len remaining) 1) rest-k (kont-push (make-and-frame (rest remaining) (get frame "env")) rest-k)))))) ;; --- OrFrame: value evaluated --- (= ft "or") (if value (make-cek-value value env rest-k) (let ((remaining (get frame "remaining"))) (if (empty? remaining) (make-cek-value false env rest-k) (make-cek-state (first remaining) (get frame "env") (if (= (len remaining) 1) rest-k (kont-push (make-or-frame (rest remaining) (get frame "env")) rest-k)))))) ;; --- CondFrame: test evaluated --- (= ft "cond") (let ((remaining (get frame "remaining")) (fenv (get frame "env")) (scheme? (get frame "scheme"))) (if scheme? ;; Scheme-style: test truthy → evaluate body (if value (make-cek-state (nth (first remaining) 1) fenv rest-k) ;; Next clause (let ((next-clauses (rest remaining))) (if (empty? next-clauses) (make-cek-value nil fenv rest-k) (let ((next-clause (first next-clauses)) (next-test (first next-clause))) (if (is-else-clause? next-test) (make-cek-state (nth next-clause 1) fenv rest-k) (make-cek-state next-test fenv (kont-push (make-cond-frame next-clauses fenv true) rest-k))))))) ;; Clojure-style (if value (make-cek-state (nth remaining 1) fenv rest-k) (let ((next (slice remaining 2))) (if (< (len next) 2) (make-cek-value nil fenv rest-k) (let ((next-test (first next))) (if (is-else-clause? next-test) (make-cek-state (nth next 1) fenv rest-k) (make-cek-state next-test fenv (kont-push (make-cond-frame next fenv false) rest-k))))))))) ;; --- CaseFrame --- (= ft "case") (let ((match-val (get frame "match-val")) (remaining (get frame "remaining")) (fenv (get frame "env"))) (if (nil? match-val) ;; First step: match-val just evaluated (sf-case-step-loop value remaining fenv rest-k) ;; Subsequent: test clause evaluated (sf-case-step-loop match-val remaining fenv rest-k))) ;; --- ThreadFirstFrame --- (= ft "thread") (let ((remaining (get frame "remaining")) (fenv (get frame "env"))) (if (empty? remaining) (make-cek-value value fenv rest-k) ;; Apply next form to value (let ((form (first remaining)) (rest-forms (rest remaining)) (new-kont (if (empty? (rest remaining)) rest-k (kont-push (make-thread-frame (rest remaining) fenv) rest-k)))) ;; Check if form is a HO call like (map fn) (if (and (= (type-of form) "list") (not (empty? form)) (= (type-of (first form)) "symbol") (ho-form-name? (symbol-name (first form)))) ;; HO form — splice value as quoted arg, dispatch via CEK (make-cek-state (cons (first form) (cons (list 'quote value) (rest form))) fenv new-kont) ;; Normal: tree-walk eval + apply (let ((result (if (= (type-of form) "list") (let ((f (trampoline (eval-expr (first form) fenv))) (rargs (map (fn (a) (trampoline (eval-expr a fenv))) (rest form))) (all-args (cons value rargs))) (cond (and (callable? f) (not (lambda? f))) (apply f all-args) (lambda? f) (trampoline (call-lambda f all-args fenv)) :else (error (str "-> form not callable: " (inspect f))))) (let ((f (trampoline (eval-expr form fenv)))) (cond (and (callable? f) (not (lambda? f))) (f value) (lambda? f) (trampoline (call-lambda f (list value) fenv)) :else (error (str "-> form not callable: " (inspect f)))))))) (if (empty? rest-forms) (make-cek-value result fenv rest-k) (make-cek-value result fenv (kont-push (make-thread-frame rest-forms fenv) rest-k)))))))) ;; --- ArgFrame: head or arg evaluated --- (= ft "arg") (let ((f (get frame "f")) (evaled (get frame "evaled")) (remaining (get frame "remaining")) (fenv (get frame "env")) (raw-args (get frame "raw-args")) (hname (get frame "head-name"))) (if (nil? f) ;; Head just evaluated — value is the function (do ;; Strict mode: check arg types for named primitives (when (and *strict* hname) (strict-check-args hname (list))) (if (empty? remaining) ;; No args — call immediately (continue-with-call value (list) fenv raw-args rest-k) ;; Start evaluating args (make-cek-state (first remaining) fenv (kont-push (make-arg-frame value (list) (rest remaining) fenv raw-args hname) rest-k)))) ;; An arg was evaluated — accumulate (let ((new-evaled (append evaled (list value)))) (if (empty? remaining) ;; All args evaluated — strict check then call (do (when (and *strict* hname) (strict-check-args hname new-evaled)) (continue-with-call f new-evaled fenv raw-args rest-k)) ;; Next arg (make-cek-state (first remaining) fenv (kont-push (make-arg-frame f new-evaled (rest remaining) fenv raw-args hname) rest-k)))))) ;; --- DictFrame: value evaluated --- (= ft "dict") (let ((remaining (get frame "remaining")) (results (get frame "results")) (fenv (get frame "env"))) ;; Last result entry is (key) — append value to make (key val) (let ((last-result (last results)) (completed (append (slice results 0 (dec (len results))) (list (list (first last-result) value))))) (if (empty? remaining) ;; All done — build dict (let ((d (dict))) (for-each (fn (pair) (dict-set! d (first pair) (nth pair 1))) completed) (make-cek-value d fenv rest-k)) ;; Next entry (let ((next-entry (first remaining))) (make-cek-state (nth next-entry 1) fenv (kont-push (make-dict-frame (rest remaining) (append completed (list (list (first next-entry)))) fenv) rest-k)))))) ;; --- HoSetupFrame: evaluating HO form arguments --- (= ft "ho-setup") (let ((ho-type (get frame "ho-type")) (remaining (get frame "remaining")) (evaled (append (get frame "evaled") (list value))) (fenv (get frame "env"))) (if (empty? remaining) ;; All args evaluated — dispatch to iteration (ho-setup-dispatch ho-type evaled fenv rest-k) ;; More args to evaluate (make-cek-state (first remaining) fenv (kont-push (make-ho-setup-frame ho-type (rest remaining) evaled fenv) rest-k)))) ;; --- ResetFrame: body evaluated normally (no shift) --- (= ft "reset") (make-cek-value value env rest-k) ;; --- DerefFrame: deref argument evaluated --- (= ft "deref") (let ((val value) (fenv (get frame "env"))) (if (not (signal? val)) ;; Not a signal: pass through (make-cek-value val fenv rest-k) ;; Signal: check for ReactiveResetFrame (if (has-reactive-reset-frame? rest-k) ;; Perform reactive shift (reactive-shift-deref val fenv rest-k) ;; No reactive-reset: normal deref (scope-based tracking) (do (let ((ctx (context "sx-reactive" nil))) (when ctx (let ((dep-list (get ctx "deps")) (notify-fn (get ctx "notify"))) (when (not (contains? dep-list val)) (append! dep-list val) (signal-add-sub! val notify-fn))))) (make-cek-value (signal-value val) fenv rest-k))))) ;; --- ReactiveResetFrame: expression completed --- (= ft "reactive-reset") (let ((update-fn (get frame "update-fn")) (first? (get frame "first-render"))) ;; On re-render (not first), call update-fn with new value (when (and update-fn (not first?)) (cek-call update-fn (list value))) (make-cek-value value env rest-k)) ;; --- ScopeFrame: body result --- (= ft "scope") (let ((name (get frame "name")) (remaining (get frame "remaining")) (fenv (get frame "env"))) (if (empty? remaining) (do (scope-pop! name) (make-cek-value value fenv rest-k)) (make-cek-state (first remaining) fenv (kont-push (make-scope-frame name (rest remaining) fenv) rest-k)))) ;; --- ProvideFrame: body expression evaluated --- (= ft "provide") (let ((remaining (get frame "remaining")) (fenv (get frame "env"))) (if (empty? remaining) ;; Body done — return value, frame consumed (make-cek-value value fenv rest-k) ;; More body expressions — keep frame on kont (make-cek-state (first remaining) fenv (kont-push (make-provide-frame (get frame "name") (get frame "value") (rest remaining) fenv) rest-k)))) ;; --- ScopeAccFrame: body expression evaluated --- (= ft "scope-acc") (let ((remaining (get frame "remaining")) (fenv (get frame "env"))) (if (empty? remaining) ;; Body done — return value, frame consumed (make-cek-value value fenv rest-k) ;; More body expressions — carry emitted list forward (make-cek-state (first remaining) fenv (kont-push (let ((new-frame (make-scope-acc-frame (get frame "name") (get frame "value") (rest remaining) fenv))) ;; Preserve accumulated emitted from current frame (dict-set! new-frame "emitted" (get frame "emitted")) new-frame) rest-k)))) ;; --- MapFrame: callback result for map/map-indexed --- (= ft "map") (let ((f (get frame "f")) (remaining (get frame "remaining")) (results (get frame "results")) (indexed (get frame "indexed")) (fenv (get frame "env"))) (let ((new-results (append results (list value)))) (if (empty? remaining) (make-cek-value new-results fenv rest-k) (let ((call-args (if indexed (list (len new-results) (first remaining)) (list (first remaining)))) (next-frame (if indexed (make-map-indexed-frame f (rest remaining) new-results fenv) (make-map-frame f (rest remaining) new-results fenv)))) (continue-with-call f call-args fenv (list) (kont-push next-frame rest-k)))))) ;; --- FilterFrame: predicate result --- (= ft "filter") (let ((f (get frame "f")) (remaining (get frame "remaining")) (results (get frame "results")) (current-item (get frame "current-item")) (fenv (get frame "env"))) (let ((new-results (if value (append results (list current-item)) results))) (if (empty? remaining) (make-cek-value new-results fenv rest-k) (continue-with-call f (list (first remaining)) fenv (list) (kont-push (make-filter-frame f (rest remaining) new-results (first remaining) fenv) rest-k))))) ;; --- ReduceFrame: accumulator step --- (= ft "reduce") (let ((f (get frame "f")) (remaining (get frame "remaining")) (fenv (get frame "env"))) (if (empty? remaining) (make-cek-value value fenv rest-k) (continue-with-call f (list value (first remaining)) fenv (list) (kont-push (make-reduce-frame f (rest remaining) fenv) rest-k)))) ;; --- ForEachFrame: side effect, discard result --- (= ft "for-each") (let ((f (get frame "f")) (remaining (get frame "remaining")) (fenv (get frame "env"))) (if (empty? remaining) (make-cek-value nil fenv rest-k) (continue-with-call f (list (first remaining)) fenv (list) (kont-push (make-for-each-frame f (rest remaining) fenv) rest-k)))) ;; --- SomeFrame: short-circuit on first truthy --- (= ft "some") (let ((f (get frame "f")) (remaining (get frame "remaining")) (fenv (get frame "env"))) (if value (make-cek-value value fenv rest-k) (if (empty? remaining) (make-cek-value false fenv rest-k) (continue-with-call f (list (first remaining)) fenv (list) (kont-push (make-some-frame f (rest remaining) fenv) rest-k))))) ;; --- EveryFrame: short-circuit on first falsy --- (= ft "every") (let ((f (get frame "f")) (remaining (get frame "remaining")) (fenv (get frame "env"))) (if (not value) (make-cek-value false fenv rest-k) (if (empty? remaining) (make-cek-value true fenv rest-k) (continue-with-call f (list (first remaining)) fenv (list) (kont-push (make-every-frame f (rest remaining) fenv) rest-k))))) :else (error (str "Unknown frame type: " ft)))))))) ;; -------------------------------------------------------------------------- ;; 9. Helper: continue with function call ;; -------------------------------------------------------------------------- (define continue-with-call (fn (f args env raw-args kont) (cond ;; Continuation — run captured delimited continuation, return result to caller. ;; Multi-shot: each invocation runs captured frames to completion via nested ;; cek-run, then returns the result to the caller's kont. (continuation? f) (let ((arg (if (empty? args) nil (first args))) (cont-data (continuation-data f))) (let ((captured (get cont-data "captured"))) ;; Run ONLY the captured frames (delimited by reset). ;; Empty kont after captured = the continuation terminates and returns. (let ((result (cek-run (make-cek-value arg env captured)))) (make-cek-value result env kont)))) ;; Native callable (and (callable? f) (not (lambda? f)) (not (component? f)) (not (island? f))) (make-cek-value (apply f args) env kont) ;; Lambda — bind params, evaluate body (lambda? f) (let ((params (lambda-params f)) (local (env-merge (lambda-closure f) env))) (if (> (len args) (len params)) (error (str (or (lambda-name f) "lambda") " expects " (len params) " args, got " (len args))) (do (for-each (fn (pair) (env-bind! local (first pair) (nth pair 1))) (zip params args)) (for-each (fn (p) (env-bind! local p nil)) (slice params (len args))) (make-cek-state (lambda-body f) local kont)))) ;; Component — parse kwargs, bind, evaluate body (or (component? f) (island? f)) (let ((parsed (parse-keyword-args raw-args env)) (kwargs (first parsed)) (children (nth parsed 1)) (local (env-merge (component-closure f) env))) (for-each (fn (p) (env-bind! local p (or (dict-get kwargs p) nil))) (component-params f)) (when (component-has-children? f) (env-bind! local "children" children)) (make-cek-state (component-body f) local kont)) :else (error (str "Not callable: " (inspect f)))))) ;; -------------------------------------------------------------------------- ;; 10. Case step loop helper ;; -------------------------------------------------------------------------- (define sf-case-step-loop (fn (match-val clauses env kont) (if (< (len clauses) 2) (make-cek-value nil env kont) (let ((test (first clauses)) (body (nth clauses 1))) (if (is-else-clause? test) (make-cek-state body env kont) ;; Evaluate test expression (let ((test-val (trampoline (eval-expr test env)))) (if (= match-val test-val) (make-cek-state body env kont) (sf-case-step-loop match-val (slice clauses 2) env kont)))))))) ;; -------------------------------------------------------------------------- ;; 11. Compatibility wrapper — eval-expr-cek ;; -------------------------------------------------------------------------- ;; ;; Drop-in replacement for eval-expr. Creates a CEK state and runs. ;; All downstream code (adapters, services) works unchanged. (define eval-expr-cek (fn (expr env) (cek-run (make-cek-state expr env (list))))) (define trampoline-cek (fn (val) ;; In CEK mode, thunks are not produced — values are immediate. ;; But for compatibility, resolve any remaining thunks. (if (thunk? val) (eval-expr-cek (thunk-expr val) (thunk-env val)) val))) ;; ************************************************************************** ;; eval-expr / trampoline — canonical definitions (after cek-run is defined) ;; ************************************************************************** ;; ;; These override the forward declarations from Part 2. All evaluation ;; goes through the CEK machine. The CEK fixups in the host platform ;; may further override these (e.g., to make cek-run iterative). (define eval-expr (fn (expr (env :as dict)) (cek-run (make-cek-state expr env (list))))) (define trampoline (fn (val) (if (thunk? val) (eval-expr (thunk-expr val) (thunk-env val)) val)))