Add page-render-plan to deps.sx: given page source + env + IO names, computes a dict mapping each needed component to "server" or "client", with server/client lists and IO dep collection. 5 new spec tests. Integration: - PageDef.render_plan field caches the plan at registration - compute_page_render_plans() called from auto_mount_pages() - Client page registry includes :render-plan per page - Affinity demo page shows per-page render plans Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
430 lines
16 KiB
Plaintext
430 lines
16 KiB
Plaintext
;; ==========================================================================
|
|
;; 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
|
|
;; (env-components env) → list of component/macro names in env
|
|
;; (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
|
|
(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-walk
|
|
(fn (n seen env)
|
|
(when (not (contains? seen n))
|
|
(append! seen n)
|
|
(let ((val (env-get env n)))
|
|
(cond
|
|
(= (type-of val) "component")
|
|
(for-each (fn (ref) (transitive-deps-walk ref seen env))
|
|
(scan-refs (component-body val)))
|
|
(= (type-of val) "macro")
|
|
(for-each (fn (ref) (transitive-deps-walk ref seen env))
|
|
(scan-refs (macro-body val)))
|
|
:else nil)))))
|
|
|
|
|
|
(define transitive-deps
|
|
(fn (name env)
|
|
(let ((seen (list))
|
|
(key (if (starts-with? name "~") name (str "~" name))))
|
|
(transitive-deps-walk key seen env)
|
|
(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 env name)))
|
|
(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 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)
|
|
(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 env name)))
|
|
(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)))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; 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
|
|
(fn (node io-names refs)
|
|
(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
|
|
(fn (node io-names)
|
|
(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
|
|
(fn (n seen all-refs env io-names)
|
|
(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)
|
|
(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) (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)
|
|
(when (not (contains? all-refs ref))
|
|
(append! all-refs ref)))
|
|
(scan-io-refs (macro-body val) io-names))
|
|
(for-each
|
|
(fn (dep) (transitive-io-refs-walk dep seen all-refs env io-names))
|
|
(scan-refs (macro-body val))))
|
|
|
|
:else nil)))))
|
|
|
|
|
|
(define transitive-io-refs
|
|
(fn (name env io-names)
|
|
(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
|
|
(fn (env io-names)
|
|
(for-each
|
|
(fn (name)
|
|
(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-pure?
|
|
(fn (name env io-names)
|
|
(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
|
|
(fn (name env io-names)
|
|
(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
|
|
(fn (page-source env io-names)
|
|
(let ((needed (components-needed page-source env))
|
|
(comp-targets (dict))
|
|
(server-list (list))
|
|
(client-list (list))
|
|
(io-deps (list)))
|
|
|
|
(for-each
|
|
(fn (name)
|
|
(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
|
|
(for-each
|
|
(fn (io-ref)
|
|
(when (not (contains? io-deps io-ref))
|
|
(append! io-deps io-ref)))
|
|
(transitive-io-refs 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
|
|
;; (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
|
|
;; --------------------------------------------------------------------------
|