Phase 7b: page render plans — per-page boundary optimizer

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>
This commit is contained in:
2026-03-08 00:02:53 +00:00
parent a70ff2b153
commit 2da80c69ed
14 changed files with 214 additions and 5 deletions

View File

@@ -549,6 +549,7 @@ class JSEmitter:
"compute-all-io-refs": "computeAllIoRefs",
"component-pure?": "componentPure_p",
"render-target": "renderTarget",
"page-render-plan": "pageRenderPlan",
# router.sx
"split-path-segments": "splitPathSegments",
"make-route-segment": "makeRouteSegment",

View File

@@ -278,6 +278,7 @@ class PyEmitter:
"compute-all-io-refs": "compute_all_io_refs",
"component-pure?": "component_pure_p",
"render-target": "render_target",
"page-render-plan": "page_render_plan",
# router.sx
"split-path-segments": "split_path_segments",
"make-route-segment": "make_route_segment",

View File

@@ -341,6 +341,50 @@
: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
;; --------------------------------------------------------------------------

View File

@@ -1301,6 +1301,9 @@ component_pure_p = lambda name, env, io_names: empty_p(transitive_io_refs(name,
# render-target
render_target = lambda name, env, io_names: (lambda key: (lambda val: ('server' if sx_truthy((not sx_truthy((type_of(val) == 'component')))) else (lambda affinity: ('server' if sx_truthy((affinity == 'server')) else ('client' if sx_truthy((affinity == 'client')) else ('server' if sx_truthy((not sx_truthy(component_pure_p(name, env, io_names)))) else 'client'))))(component_affinity(val))))(env_get(env, key)))((name if sx_truthy(starts_with_p(name, '~')) else sx_str('~', name)))
# page-render-plan
page_render_plan = lambda page_source, env, io_names: (lambda needed: (lambda comp_targets: (lambda server_list: (lambda client_list: (lambda io_deps: _sx_begin(for_each(lambda name: (lambda target: _sx_begin(_sx_dict_set(comp_targets, name, target), (_sx_begin(_sx_append(server_list, name), for_each(lambda io_ref: (_sx_append(io_deps, io_ref) if sx_truthy((not sx_truthy(contains_p(io_deps, io_ref)))) else NIL), transitive_io_refs(name, env, io_names))) if sx_truthy((target == 'server')) else _sx_append(client_list, name))))(render_target(name, env, io_names)), needed), {'components': comp_targets, 'server': server_list, 'client': client_list, 'io-deps': io_deps}))([]))([]))([]))({}))(components_needed(page_source, env))
# =========================================================================
# Fixups -- wire up render adapter dispatch

View File

@@ -263,3 +263,43 @@
(deftest "unknown name targets server"
(assert-equal "server" (render-target "~nonexistent" (test-env) (list "fetch-data")))))
;; --------------------------------------------------------------------------
;; 7. page-render-plan — per-page boundary plan
;; --------------------------------------------------------------------------
;; A page component that uses both pure and IO components
(defcomp ~plan-page (&key data)
(div
(~dep-auto-pure :x "hello")
(~dep-auto-io :x data)
(~dep-force-client :x "interactive")))
(defsuite "page-render-plan"
(deftest "plan classifies components correctly"
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
;; ~plan-page has transitive IO deps (via ~dep-auto-io) so targets server
(assert-equal "server" (dict-get (get plan :components) "~plan-page"))
(assert-equal "client" (dict-get (get plan :components) "~dep-auto-pure"))
(assert-equal "server" (dict-get (get plan :components) "~dep-auto-io"))
(assert-equal "client" (dict-get (get plan :components) "~dep-force-client"))))
(deftest "plan server list contains IO components"
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
(assert-true (contains? (get plan :server) "~dep-auto-io"))))
(deftest "plan client list contains pure components"
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
(assert-true (contains? (get plan :client) "~dep-auto-pure"))
(assert-true (contains? (get plan :client) "~dep-force-client"))))
(deftest "plan collects IO deps from server components"
(let ((plan (page-render-plan "(~plan-page :data d)" (test-env) (list "fetch-data"))))
(assert-true (contains? (get plan :io-deps) "fetch-data"))))
(deftest "pure-only page has empty server list"
(let ((plan (page-render-plan "(~dep-auto-pure :x 1)" (test-env) (list "fetch-data"))))
(assert-equal 0 (len (get plan :server)))
(assert-true (> (len (get plan :client)) 0)))))