diff --git a/shared/sx/ref/deps.sx b/shared/sx/ref/deps.sx new file mode 100644 index 0000000..ca174e9 --- /dev/null +++ b/shared/sx/ref/deps.sx @@ -0,0 +1,226 @@ +;; ========================================================================== +;; 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. +;; +;; Platform interface (provided by 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 ~) +;; +;; Already available from eval.sx platform: +;; (type-of x), (symbol-name s) +;; +;; New platform functions for deps: +;; (component-body c) → component body AST +;; (component-name c) → component name string +;; (macro-body m) → macro body AST +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; 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 + (fn (node) + (let ((refs (list))) + (scan-refs-walk node refs) + refs))) + + +(define scan-refs-walk + (fn (node refs) + (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 + (fn (name env) + (let ((seen (list)) + (key (if (starts-with? name "~") name (str "~" name)))) + + (define walk + (fn (n) + (when (not (contains? seen n)) + (append! seen n) + (let ((val (env-get-or env n nil))) + (cond + (= (type-of val) "component") + (for-each walk (scan-refs (component-body val))) + (= (type-of val) "macro") + (for-each walk (scan-refs (macro-body val))) + :else nil))))) + + (walk key) + (filter (fn (x) (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 + (fn (env) + (for-each + (fn (name) + (let ((val (env-get-or env name nil))) + (when (= (type-of val) "component") + (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 + (fn (source) + (let ((matches (regex-find-all "\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)" source))) + (map (fn (m) (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 + (fn (page-source env) + (let ((direct (scan-components-from-source page-source)) + (all-needed (list))) + + ;; Add each direct ref + its transitive deps + (for-each + (fn (name) + (when (not (contains? all-needed name)) + (append! all-needed name)) + (let ((val (env-get-or env name nil))) + (let ((deps (if (and (= (type-of val) "component") + (not (empty? (component-deps val)))) + (component-deps val) + (transitive-deps name env)))) + (for-each + (fn (dep) + (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 + (fn (page-source env) + (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 + (fn (page-source env) + (let ((needed (components-needed page-source env)) + (classes (list))) + + ;; Collect classes from needed components + (for-each + (fn (name) + (let ((val (env-get-or env name nil))) + (when (= (type-of val) "component") + (for-each + (fn (cls) + (when (not (contains? classes cls)) + (append! classes cls))) + (component-css-classes val))))) + needed) + + ;; Add classes from page source + (for-each + (fn (cls) + (when (not (contains? classes cls)) + (append! classes cls))) + (scan-css-classes page-source)) + + classes))) + + +;; -------------------------------------------------------------------------- +;; Platform interface summary +;; -------------------------------------------------------------------------- +;; +;; From eval.sx (already provided): +;; (type-of x) → type string +;; (symbol-name s) → string name of symbol +;; (env-get-or env k d) → value or default +;; +;; 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 +;; (macro-body m) → AST body of macro +;; (env-components env) → list of component names in env +;; (regex-find-all pat src) → list of capture group matches +;; (scan-css-classes src) → list of CSS class strings from source +;; --------------------------------------------------------------------------