;; ========================================================================== ;; deps.sx — Component dependency analysis specification ;; ;; Pure functions for analyzing component dependency graphs. ;; Used by the bundling system to compute per-page component bundles ;; instead of sending every definition to every page. ;; ;; All functions are pure — no IO, no platform-specific operations. ;; Each host bootstraps this to native code alongside eval.sx/render.sx. ;; ;; From eval.sx platform (already provided by every host): ;; (type-of x) → type string ;; (symbol-name s) → string name of symbol ;; (component-body c) → unevaluated AST of component body ;; (component-name c) → string name (without ~) ;; (macro-body m) → macro body AST ;; (env-get env k) → value or nil ;; ;; New platform functions for deps (each host implements): ;; (component-deps c) → cached deps list (may be empty) ;; (component-set-deps! c d)→ cache deps on component ;; (component-css-classes c)→ pre-scanned CSS class list ;; (regex-find-all pat src) → list of capture group 1 matches ;; (scan-css-classes src) → list of CSS class strings from source ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; 1. AST scanning — collect ~component references from an AST node ;; -------------------------------------------------------------------------- ;; Walks all branches of control flow (if/when/cond/case) to find ;; every component that *could* be rendered. (define scan-refs :effects [] (fn (node) (let ((refs (list))) (scan-refs-walk node refs) refs))) (define scan-refs-walk :effects [] (fn (node (refs :as list)) (cond ;; Symbol starting with ~ → component reference (= (type-of node) "symbol") (let ((name (symbol-name node))) (when (starts-with? name "~") (when (not (contains? refs name)) (append! refs name)))) ;; List → recurse into all elements (covers all control flow branches) (= (type-of node) "list") (for-each (fn (item) (scan-refs-walk item refs)) node) ;; Dict → recurse into values (= (type-of node) "dict") (for-each (fn (key) (scan-refs-walk (dict-get node key) refs)) (keys node)) ;; Literals (number, string, boolean, nil, keyword) → no refs :else nil))) ;; -------------------------------------------------------------------------- ;; 2. Transitive dependency closure ;; -------------------------------------------------------------------------- ;; Given a component name and an environment, compute all components ;; that it can transitively render. Handles cycles via seen-set. (define transitive-deps-walk :effects [] (fn ((n :as string) (seen :as list) (env :as dict)) (when (not (contains? seen n)) (append! seen n) (let ((val (env-get env n))) (cond (or (= (type-of val) "component") (= (type-of val) "island")) (for-each (fn ((ref :as string)) (transitive-deps-walk ref seen env)) (scan-refs (component-body val))) (= (type-of val) "macro") (for-each (fn ((ref :as string)) (transitive-deps-walk ref seen env)) (scan-refs (macro-body val))) :else nil))))) (define transitive-deps :effects [] (fn ((name :as string) (env :as dict)) (let ((seen (list)) (key (if (starts-with? name "~") name (str "~" name)))) (transitive-deps-walk key seen env) (filter (fn ((x :as string)) (not (= x key))) seen)))) ;; -------------------------------------------------------------------------- ;; 3. Compute deps for all components in an environment ;; -------------------------------------------------------------------------- ;; Iterates env, calls transitive-deps for each component, and ;; stores the result via the platform's component-set-deps! function. ;; ;; Platform interface: ;; (env-components env) → list of component names in env ;; (component-set-deps! comp deps) → store deps on component (define compute-all-deps :effects [mutation] (fn ((env :as dict)) (for-each (fn ((name :as string)) (let ((val (env-get env name))) (when (or (= (type-of val) "component") (= (type-of val) "island")) (component-set-deps! val (transitive-deps name env))))) (env-components env)))) ;; -------------------------------------------------------------------------- ;; 4. Scan serialized SX source for component references ;; -------------------------------------------------------------------------- ;; Regex-based extraction of (~name patterns from SX wire format. ;; Returns list of names WITH ~ prefix. ;; ;; Platform interface: ;; (regex-find-all pattern source) → list of matched group strings (define scan-components-from-source :effects [] (fn ((source :as string)) (let ((matches (regex-find-all "\\(~([a-zA-Z_][a-zA-Z0-9_\\-:/]*)" source))) (map (fn ((m :as string)) (str "~" m)) matches)))) ;; -------------------------------------------------------------------------- ;; 5. Components needed for a page ;; -------------------------------------------------------------------------- ;; Scans page source for direct component references, then computes ;; the transitive closure. Returns list of ~names. (define components-needed :effects [] (fn ((page-source :as string) (env :as dict)) (let ((direct (scan-components-from-source page-source)) (all-needed (list))) ;; Add each direct ref + its transitive deps (for-each (fn ((name :as string)) (when (not (contains? all-needed name)) (append! all-needed name)) (let ((val (env-get env name))) (let ((deps (if (and (= (type-of val) "component") (not (empty? (component-deps val)))) (component-deps val) (transitive-deps name env)))) (for-each (fn ((dep :as string)) (when (not (contains? all-needed dep)) (append! all-needed dep))) deps)))) direct) all-needed))) ;; -------------------------------------------------------------------------- ;; 6. Build per-page component bundle ;; -------------------------------------------------------------------------- ;; Given page source and env, returns list of component names needed. ;; The host uses this list to serialize only the needed definitions ;; and compute a page-specific hash. ;; ;; This replaces the "send everything" approach with per-page bundles. (define page-component-bundle :effects [] (fn ((page-source :as string) (env :as dict)) (components-needed page-source env))) ;; -------------------------------------------------------------------------- ;; 7. CSS classes for a page ;; -------------------------------------------------------------------------- ;; Returns the union of CSS classes from components this page uses, ;; plus classes from the page source itself. ;; ;; Platform interface: ;; (component-css-classes c) → set/list of class strings ;; (scan-css-classes source) → set/list of class strings from source (define page-css-classes :effects [] (fn ((page-source :as string) (env :as dict)) (let ((needed (components-needed page-source env)) (classes (list))) ;; Collect classes from needed components (for-each (fn ((name :as string)) (let ((val (env-get env name))) (when (= (type-of val) "component") (for-each (fn ((cls :as string)) (when (not (contains? classes cls)) (append! classes cls))) (component-css-classes val))))) needed) ;; Add classes from page source (for-each (fn ((cls :as string)) (when (not (contains? classes cls)) (append! classes cls))) (scan-css-classes page-source)) classes))) ;; -------------------------------------------------------------------------- ;; 8. IO detection — scan component ASTs for IO primitive references ;; -------------------------------------------------------------------------- ;; Extends the dependency walker to detect references to IO primitives. ;; IO names are provided by the host (from boundary.sx declarations). ;; A component is "pure" if it (transitively) references no IO primitives. ;; ;; Platform interface additions: ;; (component-io-refs c) → cached IO ref list (may be empty) ;; (component-set-io-refs! c r) → cache IO refs on component (define scan-io-refs-walk :effects [] (fn (node (io-names :as list) (refs :as list)) (cond ;; Symbol → check if name is in the IO set (= (type-of node) "symbol") (let ((name (symbol-name node))) (when (contains? io-names name) (when (not (contains? refs name)) (append! refs name)))) ;; List → recurse into all elements (= (type-of node) "list") (for-each (fn (item) (scan-io-refs-walk item io-names refs)) node) ;; Dict → recurse into values (= (type-of node) "dict") (for-each (fn (key) (scan-io-refs-walk (dict-get node key) io-names refs)) (keys node)) ;; Literals → no IO refs :else nil))) (define scan-io-refs :effects [] (fn (node (io-names :as list)) (let ((refs (list))) (scan-io-refs-walk node io-names refs) refs))) ;; -------------------------------------------------------------------------- ;; 9. Transitive IO refs — follow component deps and union IO refs ;; -------------------------------------------------------------------------- (define transitive-io-refs-walk :effects [] (fn ((n :as string) (seen :as list) (all-refs :as list) (env :as dict) (io-names :as list)) (when (not (contains? seen n)) (append! seen n) (let ((val (env-get env n))) (cond (= (type-of val) "component") (do ;; Scan this component's body for IO refs (for-each (fn ((ref :as string)) (when (not (contains? all-refs ref)) (append! all-refs ref))) (scan-io-refs (component-body val) io-names)) ;; Recurse into component deps (for-each (fn ((dep :as string)) (transitive-io-refs-walk dep seen all-refs env io-names)) (scan-refs (component-body val)))) (= (type-of val) "macro") (do (for-each (fn ((ref :as string)) (when (not (contains? all-refs ref)) (append! all-refs ref))) (scan-io-refs (macro-body val) io-names)) (for-each (fn ((dep :as string)) (transitive-io-refs-walk dep seen all-refs env io-names)) (scan-refs (macro-body val)))) :else nil))))) (define transitive-io-refs :effects [] (fn ((name :as string) (env :as dict) (io-names :as list)) (let ((all-refs (list)) (seen (list)) (key (if (starts-with? name "~") name (str "~" name)))) (transitive-io-refs-walk key seen all-refs env io-names) all-refs))) ;; -------------------------------------------------------------------------- ;; 10. Compute IO refs for all components in an environment ;; -------------------------------------------------------------------------- (define compute-all-io-refs :effects [mutation] (fn ((env :as dict) (io-names :as list)) (for-each (fn ((name :as string)) (let ((val (env-get env name))) (when (= (type-of val) "component") (component-set-io-refs! val (transitive-io-refs name env io-names))))) (env-components env)))) (define component-io-refs-cached :effects [] (fn ((name :as string) (env :as dict) (io-names :as list)) (let ((key (if (starts-with? name "~") name (str "~" name)))) (let ((val (env-get env key))) (if (and (= (type-of val) "component") (not (nil? (component-io-refs val))) (not (empty? (component-io-refs val)))) (component-io-refs val) ;; Fallback: not yet cached (shouldn't happen after compute-all-io-refs) (transitive-io-refs name env io-names)))))) (define component-pure? :effects [] (fn ((name :as string) (env :as dict) (io-names :as list)) (let ((key (if (starts-with? name "~") name (str "~" name)))) (let ((val (env-get env key))) (if (and (= (type-of val) "component") (not (nil? (component-io-refs val)))) ;; Use cached io-refs (empty list = pure) (empty? (component-io-refs val)) ;; Fallback (empty? (transitive-io-refs name env io-names))))))) ;; -------------------------------------------------------------------------- ;; 5. Render target — boundary decision per component ;; -------------------------------------------------------------------------- ;; Combines IO analysis with affinity annotations to decide where a ;; component should render: ;; ;; :affinity :server → always "server" (auth-sensitive, secrets) ;; :affinity :client → "client" even if IO-dependent (IO proxy) ;; :affinity :auto → "server" if IO-dependent, "client" if pure ;; ;; Returns: "server" | "client" (define render-target :effects [] (fn ((name :as string) (env :as dict) (io-names :as list)) (let ((key (if (starts-with? name "~") name (str "~" name)))) (let ((val (env-get env key))) (if (not (= (type-of val) "component")) "server" (let ((affinity (component-affinity val))) (cond (= affinity "server") "server" (= affinity "client") "client" ;; auto: decide from IO analysis (not (component-pure? name env io-names)) "server" :else "client"))))))) ;; -------------------------------------------------------------------------- ;; 6. Page render plan — pre-computed boundary decisions for a page ;; -------------------------------------------------------------------------- ;; Given page source + env + IO names, returns a render plan dict: ;; ;; {:components {~name "server"|"client" ...} ;; :server (list of ~names that render server-side) ;; :client (list of ~names that render client-side) ;; :io-deps (list of IO primitives needed by server components)} ;; ;; This is computed once at page registration and cached on the page def. ;; The async evaluator and client router both use it to make decisions ;; without recomputing at every request. (define page-render-plan :effects [] (fn ((page-source :as string) (env :as dict) (io-names :as list)) (let ((needed (components-needed page-source env)) (comp-targets (dict)) (server-list (list)) (client-list (list)) (io-deps (list))) (for-each (fn ((name :as string)) (let ((target (render-target name env io-names))) (dict-set! comp-targets name target) (if (= target "server") (do (append! server-list name) ;; Collect IO deps from server components (use cache) (for-each (fn ((io-ref :as string)) (when (not (contains? io-deps io-ref)) (append! io-deps io-ref))) (component-io-refs-cached name env io-names))) (append! client-list name)))) needed) {:components comp-targets :server server-list :client client-list :io-deps io-deps}))) ;; -------------------------------------------------------------------------- ;; Host obligation: selective expansion in async partial evaluation ;; -------------------------------------------------------------------------- ;; The spec classifies components as pure or IO-dependent and provides ;; per-component render-target decisions. Each host's async partial ;; evaluator (the server-side rendering path that bridges sync evaluation ;; with async IO) must use this classification: ;; ;; render-target "server" → expand server-side (IO must resolve) ;; render-target "client" → serialize for client (can render anywhere) ;; Layout slot context → expand all (server needs full HTML) ;; ;; The spec provides: component-io-refs, component-pure?, render-target, ;; component-affinity. The host provides the async runtime that acts on it. ;; This is not SX semantics — it is host infrastructure. Every host ;; with a server-side async evaluator implements the same rule. ;; -------------------------------------------------------------------------- ;; -------------------------------------------------------------------------- ;; Platform interface summary ;; -------------------------------------------------------------------------- ;; ;; From eval.sx (already provided): ;; (type-of x) → type string ;; (symbol-name s) → string name of symbol ;; (env-get env k) → value or nil ;; ;; New for deps.sx (each host implements): ;; (component-body c) → AST body of component ;; (component-name c) → name string ;; (component-deps c) → cached deps list (may be empty) ;; (component-set-deps! c d)→ cache deps on component ;; (component-css-classes c)→ pre-scanned CSS class list ;; (component-io-refs c) → cached IO ref list (may be empty) ;; (component-set-io-refs! c r)→ cache IO refs on component ;; (component-affinity c) → "auto" | "client" | "server" ;; (macro-body m) → AST body of macro ;; (regex-find-all pat src) → list of capture group matches ;; (scan-css-classes src) → list of CSS class strings from source ;; -------------------------------------------------------------------------- ;; -------------------------------------------------------------------------- ;; env-components — list component/macro names in an environment ;; -------------------------------------------------------------------------- ;; Moved from platform to spec: pure logic using type predicates. (define env-components :effects [] (fn ((env :as dict)) (filter (fn ((k :as string)) (let ((v (env-get env k))) (or (component? v) (macro? v)))) (keys env))))