Phase 2: IO detection & selective expansion in deps.sx

Extend the spec with IO scanning functions (scan-io-refs, transitive-io-refs,
compute-all-io-refs, component-pure?) that detect IO primitive references in
component ASTs. Components are classified as pure (no IO deps, safe for client
rendering) or IO-dependent (must expand server-side).

The partial evaluator (_aser) now uses per-component IO metadata instead of
the global _expand_components toggle: IO-dependent components expand server-
side, pure components serialize for client. Layout slot context still expands
all components for backwards compat.

Spec: 5 new functions + 2 platform interface additions in deps.sx
Host: io_refs field + is_pure property on Component, compute_all_io_refs()
Bootstrap: both sx_ref.py and sx-ref.js updated with IO functions
Bundle analyzer: shows pure/IO classification per page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 13:19:17 +00:00
parent 652e7f81c8
commit 0ba7ebe349
13 changed files with 409 additions and 46 deletions

View File

@@ -414,7 +414,8 @@
:selected (or (find-current isomorphism-nav-items slug) ""))
:content (case slug
"bundle-analyzer" (~bundle-analyzer-content
:pages pages :total-components total-components :total-macros total-macros)
:pages pages :total-components total-components :total-macros total-macros
:pure-count pure-count :io-count io-count)
:else (~plan-isomorphic-content)))
(defpage bundle-analyzer
@@ -428,7 +429,8 @@
:selected "Bundle Analyzer")
:data (bundle-analyzer-data)
:content (~bundle-analyzer-content
:pages pages :total-components total-components :total-macros total-macros))
:pages pages :total-components total-components :total-macros total-macros
:pure-count pure-count :io-count io-count))
;; ---------------------------------------------------------------------------
;; Plans section

View File

@@ -277,6 +277,8 @@ def _bundle_analyzer_data() -> dict:
env = get_component_env()
total_components = sum(1 for v in env.values() if isinstance(v, Component))
total_macros = sum(1 for v in env.values() if isinstance(v, Macro))
pure_count = sum(1 for v in env.values() if isinstance(v, Component) and v.is_pure)
io_count = total_components - pure_count
pages_data = []
for name, page_def in sorted(get_all_pages("sx").items()):
@@ -286,6 +288,20 @@ def _bundle_analyzer_data() -> dict:
n = len(needed)
pct = round(n / total_components * 100) if total_components else 0
savings = 100 - pct
# IO classification for components in this page
pure_in_page = 0
io_in_page = 0
page_io_refs: set[str] = set()
for comp_name in needed:
val = env.get(comp_name)
if isinstance(val, Component):
if val.is_pure:
pure_in_page += 1
else:
io_in_page += 1
page_io_refs.update(val.io_refs)
pages_data.append({
"name": name,
"path": page_def.path,
@@ -293,6 +309,9 @@ def _bundle_analyzer_data() -> dict:
"needed": n,
"pct": pct,
"savings": savings,
"io-refs": len(page_io_refs),
"pure-in-page": pure_in_page,
"io-in-page": io_in_page,
})
pages_data.sort(key=lambda p: p["needed"], reverse=True)
@@ -301,6 +320,8 @@ def _bundle_analyzer_data() -> dict:
"pages": pages_data,
"total-components": total_components,
"total-macros": total_macros,
"pure-count": pure_count,
"io-count": io_count,
}