8 Commits

Author SHA1 Message Date
631394989c Add not-prose to all code blocks to enforce stone-100 background
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m32s
Tailwind's prose class applies dark backgrounds to pre/code elements,
overriding the intended bg-stone-100. Adding not-prose to every code
container div across docs, specs, and examples pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:35:49 +00:00
a0e39f0014 Fix bundle analyzer source display: override prose styling + add syntax highlighting
- Add not-prose class to escape Tailwind typography dark pre/code backgrounds
- Use (highlight source "lisp") for syntax-highlighted component source
- Add missing bg-blue-500 bg-amber-500 to @css annotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:30:18 +00:00
55adbf6463 Fix bundle analyzer source readability: white bg, darker text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:03:52 +00:00
fbfd203746 Bundle analyzer: drill-down component tree with SX source viewer
Click a page row to expand its component bundle tree. Each component
shows pure/IO badge, IO refs, dep count. Click a component to expand
its full defcomp SX source. Uses <details>/<summary> for zero-JS
expand/collapse.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:59:55 +00:00
65ed8a8941 Replace tagline with the sx identity cycle
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:43:39 +00:00
54814b4258 Update deps spec description and isomorphism roadmap for Phase 2
- deps.sx spec description now covers both Phase 1 (bundling) and Phase 2
  (IO detection, pure/IO classification, host obligation for selective
  expansion)
- Isomorphism roadmap context updated: boundary slides automatically
  based on IO detection, not future tense
- Current State section adds dependency analysis and IO detection bullets
- Phase 1 spec module note updated: 14 functions, 8 platform declarations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:38:21 +00:00
3482cbdaa6 Document host obligation for selective expansion in deps.sx
The spec classifies components as pure vs IO-dependent. Each host's
async partial evaluator must act on this: expand IO-dependent server-
side, serialize pure for client. This is host infrastructure, not SX
semantics — documented as a contract in the spec.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:34:09 +00:00
0ba7ebe349 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>
2026-03-06 13:19:17 +00:00
19 changed files with 511 additions and 74 deletions

View File

@@ -609,6 +609,14 @@
return result;
}
function componentIoRefs(c) {
return c.ioRefs ? c.ioRefs.slice() : [];
}
function componentSetIoRefs(c, refs) {
c.ioRefs = refs;
}
// =========================================================================
// Platform interface — Parser
@@ -2456,6 +2464,43 @@ callExpr.push(dictGet(kwargs, k)); } }
return classes;
})(); };
// scan-io-refs-walk
var scanIoRefsWalk = function(node, ioNames, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() {
var name = symbolName(node);
return (isSxTruthy(contains(ioNames, name)) ? (isSxTruthy(!contains(refs, name)) ? append_b(refs, name) : NIL) : NIL);
})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanIoRefsWalk(item, ioNames, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanIoRefsWalk(dictGet(node, key), ioNames, refs); }, keys(node)) : NIL))); };
// scan-io-refs
var scanIoRefs = function(node, ioNames) { return (function() {
var refs = [];
scanIoRefsWalk(node, ioNames, refs);
return refs;
})(); };
// transitive-io-refs-walk
var transitiveIoRefsWalk = function(n, seen, allRefs, env, ioNames) { return (isSxTruthy(!contains(seen, n)) ? (append_b(seen, n), (function() {
var val = envGet(env, n);
return (isSxTruthy((typeOf(val) == "component")) ? (forEach(function(ref) { return (isSxTruthy(!contains(allRefs, ref)) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(componentBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(componentBody(val)))) : (isSxTruthy((typeOf(val) == "macro")) ? (forEach(function(ref) { return (isSxTruthy(!contains(allRefs, ref)) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(macroBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(macroBody(val)))) : NIL));
})()) : NIL); };
// transitive-io-refs
var transitiveIoRefs = function(name, env, ioNames) { return (function() {
var allRefs = [];
var seen = [];
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
transitiveIoRefsWalk(key, seen, allRefs, env, ioNames);
return allRefs;
})(); };
// compute-all-io-refs
var computeAllIoRefs = function(env, ioNames) { return forEach(function(name) { return (function() {
var val = envGet(env, name);
return (isSxTruthy((typeOf(val) == "component")) ? componentSetIoRefs(val, transitiveIoRefs(name, env, ioNames)) : NIL);
})(); }, envComponents(env)); };
// component-pure?
var componentPure_p = function(name, env, ioNames) { return isEmpty(transitiveIoRefs(name, env, ioNames)); };
// =========================================================================
// Platform interface — DOM adapter (browser-only)
@@ -3626,6 +3671,10 @@ callExpr.push(dictGet(kwargs, k)); } }
componentsNeeded: componentsNeeded,
pageComponentBundle: pageComponentBundle,
pageCssClasses: pageCssClasses,
scanIoRefs: scanIoRefs,
transitiveIoRefs: transitiveIoRefs,
computeAllIoRefs: computeAllIoRefs,
componentPure_p: componentPure_p,
_version: "ref-2.0 (boot+cssx+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)"
};
@@ -3649,4 +3698,4 @@ callExpr.push(dictGet(kwargs, k)); } }
if (typeof module !== "undefined" && module.exports) module.exports = Sx;
else global.Sx = Sx;
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);

View File

@@ -1332,8 +1332,9 @@ async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
if isinstance(val, Macro):
expanded = _expand_macro(val, expr[1:], env)
return await _aser(expanded, env, ctx)
if isinstance(val, Component) and _expand_components.get():
return await _aser_component(val, expr[1:], env, ctx)
if isinstance(val, Component):
if _expand_components.get() or not val.is_pure:
return await _aser_component(val, expr[1:], env, ctx)
return await _aser_call(name, expr[1:], env, ctx)
# Serialize-mode special/HO forms (checked BEFORE HTML_TAGS

View File

@@ -68,6 +68,62 @@ def _compute_all_deps_fallback(env: dict[str, Any]) -> None:
val.deps = _transitive_deps_fallback(key, env)
def _scan_io_refs_fallback(node: Any, io_names: set[str]) -> set[str]:
"""Scan an AST node for references to IO primitive names."""
refs: set[str] = set()
_walk_io(node, io_names, refs)
return refs
def _walk_io(node: Any, io_names: set[str], refs: set[str]) -> None:
if isinstance(node, Symbol):
if node.name in io_names:
refs.add(node.name)
return
if isinstance(node, list):
for item in node:
_walk_io(item, io_names, refs)
return
if isinstance(node, dict):
for v in node.values():
_walk_io(v, io_names, refs)
return
def _transitive_io_refs_fallback(
name: str, env: dict[str, Any], io_names: set[str]
) -> set[str]:
"""Compute transitive IO primitive references for a component."""
all_refs: set[str] = set()
seen: set[str] = set()
def walk(n: str) -> None:
if n in seen:
return
seen.add(n)
val = env.get(n)
if isinstance(val, Component):
all_refs.update(_scan_io_refs_fallback(val.body, io_names))
for dep in _scan_ast(val.body):
walk(dep)
elif isinstance(val, Macro):
all_refs.update(_scan_io_refs_fallback(val.body, io_names))
for dep in _scan_ast(val.body):
walk(dep)
key = name if name.startswith("~") else f"~{name}"
walk(key)
return all_refs
def _compute_all_io_refs_fallback(
env: dict[str, Any], io_names: set[str]
) -> None:
for key, val in env.items():
if isinstance(val, Component):
val.io_refs = _transitive_io_refs_fallback(key, env, io_names)
def _scan_components_from_sx_fallback(source: str) -> set[str]:
import re
return {f"~{m}" for m in re.findall(r'\(~([a-zA-Z_][a-zA-Z0-9_\-]*)', source)}
@@ -131,3 +187,27 @@ def components_needed(page_sx: str, env: dict[str, Any]) -> set[str]:
from .ref.sx_ref import components_needed as _ref_cn
return set(_ref_cn(page_sx, env))
return _components_needed_fallback(page_sx, env)
def compute_all_io_refs(env: dict[str, Any], io_names: set[str]) -> None:
"""Compute and cache transitive IO refs for all Component entries in *env*."""
if _use_ref():
from .ref.sx_ref import compute_all_io_refs as _ref_cio
_ref_cio(env, list(io_names))
return
_compute_all_io_refs_fallback(env, io_names)
def get_all_io_names() -> set[str]:
"""Build the complete set of IO primitive names from all boundary tiers.
Includes: core IO (primitives_io.py handlers), plus all page helper names
from every service boundary.
"""
from .primitives_io import IO_PRIMITIVES
from .boundary import declared_helpers
names = set(IO_PRIMITIVES)
for _svc, helper_names in declared_helpers().items():
names.update(helper_names)
return names

View File

@@ -204,8 +204,9 @@ def register_components(sx_source: str) -> None:
val.css_classes = set(all_classes)
# Recompute transitive deps for all components (cheap — just AST walking)
from .deps import compute_all_deps
from .deps import compute_all_deps, compute_all_io_refs, get_all_io_names
compute_all_deps(_COMPONENT_ENV)
compute_all_io_refs(_COMPONENT_ENV, get_all_io_names())
_compute_component_hash()

View File

@@ -502,9 +502,17 @@ class JSEmitter:
"component-deps": "componentDeps",
"component-set-deps!": "componentSetDeps",
"component-css-classes": "componentCssClasses",
"component-io-refs": "componentIoRefs",
"component-set-io-refs!": "componentSetIoRefs",
"env-components": "envComponents",
"regex-find-all": "regexFindAll",
"scan-css-classes": "scanCssClasses",
# deps.sx IO detection
"scan-io-refs": "scanIoRefs",
"scan-io-refs-walk": "scanIoRefsWalk",
"transitive-io-refs": "transitiveIoRefs",
"compute-all-io-refs": "computeAllIoRefs",
"component-pure?": "componentPure_p",
}
if name in RENAMES:
return RENAMES[name]
@@ -1904,6 +1912,14 @@ PLATFORM_DEPS_JS = '''
}
return result;
}
function componentIoRefs(c) {
return c.ioRefs ? c.ioRefs.slice() : [];
}
function componentSetIoRefs(c, refs) {
c.ioRefs = refs;
}
'''
PLATFORM_PARSER_JS = r"""
@@ -3081,6 +3097,10 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has
api_lines.append(' componentsNeeded: componentsNeeded,')
api_lines.append(' pageComponentBundle: pageComponentBundle,')
api_lines.append(' pageCssClasses: pageCssClasses,')
api_lines.append(' scanIoRefs: scanIoRefs,')
api_lines.append(' transitiveIoRefs: transitiveIoRefs,')
api_lines.append(' computeAllIoRefs: computeAllIoRefs,')
api_lines.append(' componentPure_p: componentPure_p,')
api_lines.append(f' _version: "{version}"')
api_lines.append(' };')

View File

@@ -247,9 +247,17 @@ class PyEmitter:
"component-deps": "component_deps",
"component-set-deps!": "component_set_deps",
"component-css-classes": "component_css_classes",
"component-io-refs": "component_io_refs",
"component-set-io-refs!": "component_set_io_refs",
"env-components": "env_components",
"regex-find-all": "regex_find_all",
"scan-css-classes": "scan_css_classes",
# deps.sx IO detection
"scan-io-refs": "scan_io_refs",
"scan-io-refs-walk": "scan_io_refs_walk",
"transitive-io-refs": "transitive_io_refs",
"compute-all-io-refs": "compute_all_io_refs",
"component-pure?": "component_pure_p",
}
if name in RENAMES:
return RENAMES[name]
@@ -1982,6 +1990,14 @@ PLATFORM_DEPS_PY = (
' for m in _re.finditer(r\';;\\s*@css\\s+(.+)\', source):\n'
' classes.update(m.group(1).split())\n'
' return list(classes)\n'
'\n'
'def component_io_refs(c):\n'
' """Return cached IO refs list for a component (may be empty)."""\n'
' return list(c.io_refs) if hasattr(c, "io_refs") and c.io_refs else []\n'
'\n'
'def component_set_io_refs(c, refs):\n'
' """Cache IO refs on a component."""\n'
' c.io_refs = set(refs) if not isinstance(refs, set) else refs\n'
)

View File

@@ -208,6 +208,130 @@
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))))
;; --------------------------------------------------------------------------
;; Host obligation: selective expansion in async partial evaluation
;; --------------------------------------------------------------------------
;; The spec classifies components as pure or IO-dependent. Each host's
;; async partial evaluator (the server-side rendering path that bridges
;; sync evaluation with async IO) must use this classification:
;;
;; IO-dependent component → expand server-side (IO must resolve)
;; Pure component → serialize for client (can render anywhere)
;; Layout slot context → expand all (server needs full HTML)
;;
;; The spec provides the data (component-io-refs, component-pure?).
;; 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
;; --------------------------------------------------------------------------
@@ -223,6 +347,8 @@
;; (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
;; (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

View File

@@ -917,6 +917,14 @@ def scan_css_classes(source):
classes.update(m.group(1).split())
return list(classes)
def component_io_refs(c):
"""Return cached IO refs list for a component (may be empty)."""
return list(c.io_refs) if hasattr(c, "io_refs") and c.io_refs else []
def component_set_io_refs(c, refs):
"""Cache IO refs on a component."""
c.io_refs = set(refs) if not isinstance(refs, set) else refs
# === Transpiled from eval ===
@@ -1210,6 +1218,24 @@ page_component_bundle = lambda page_source, env: components_needed(page_source,
# page-css-classes
page_css_classes = lambda page_source, env: (lambda needed: (lambda classes: _sx_begin(for_each(lambda name: (lambda val: (for_each(lambda cls: (_sx_append(classes, cls) if sx_truthy((not sx_truthy(contains_p(classes, cls)))) else NIL), component_css_classes(val)) if sx_truthy((type_of(val) == 'component')) else NIL))(env_get(env, name)), needed), for_each(lambda cls: (_sx_append(classes, cls) if sx_truthy((not sx_truthy(contains_p(classes, cls)))) else NIL), scan_css_classes(page_source)), classes))([]))(components_needed(page_source, env))
# scan-io-refs-walk
scan_io_refs_walk = lambda node, io_names, refs: ((lambda name: ((_sx_append(refs, name) if sx_truthy((not sx_truthy(contains_p(refs, name)))) else NIL) if sx_truthy(contains_p(io_names, name)) else NIL))(symbol_name(node)) if sx_truthy((type_of(node) == 'symbol')) else (for_each(lambda item: scan_io_refs_walk(item, io_names, refs), node) if sx_truthy((type_of(node) == 'list')) else (for_each(lambda key: scan_io_refs_walk(dict_get(node, key), io_names, refs), keys(node)) if sx_truthy((type_of(node) == 'dict')) else NIL)))
# scan-io-refs
scan_io_refs = lambda node, io_names: (lambda refs: _sx_begin(scan_io_refs_walk(node, io_names, refs), refs))([])
# transitive-io-refs-walk
transitive_io_refs_walk = lambda n, seen, all_refs, env, io_names: (_sx_begin(_sx_append(seen, n), (lambda val: (_sx_begin(for_each(lambda ref: (_sx_append(all_refs, ref) if sx_truthy((not sx_truthy(contains_p(all_refs, ref)))) else NIL), scan_io_refs(component_body(val), io_names)), for_each(lambda dep: transitive_io_refs_walk(dep, seen, all_refs, env, io_names), scan_refs(component_body(val)))) if sx_truthy((type_of(val) == 'component')) else (_sx_begin(for_each(lambda ref: (_sx_append(all_refs, ref) if sx_truthy((not sx_truthy(contains_p(all_refs, ref)))) else NIL), scan_io_refs(macro_body(val), io_names)), for_each(lambda dep: transitive_io_refs_walk(dep, seen, all_refs, env, io_names), scan_refs(macro_body(val)))) if sx_truthy((type_of(val) == 'macro')) else NIL)))(env_get(env, n))) if sx_truthy((not sx_truthy(contains_p(seen, n)))) else NIL)
# transitive-io-refs
transitive_io_refs = lambda name, env, io_names: (lambda all_refs: (lambda seen: (lambda key: _sx_begin(transitive_io_refs_walk(key, seen, all_refs, env, io_names), all_refs))((name if sx_truthy(starts_with_p(name, '~')) else sx_str('~', name))))([]))([])
# compute-all-io-refs
compute_all_io_refs = lambda env, io_names: for_each(lambda name: (lambda val: (component_set_io_refs(val, transitive_io_refs(name, env, io_names)) if sx_truthy((type_of(val) == 'component')) else NIL))(env_get(env, name)), env_components(env))
# component-pure?
component_pure_p = lambda name, env, io_names: empty_p(transitive_io_refs(name, env, io_names))
# =========================================================================
# Fixups -- wire up render adapter dispatch

View File

@@ -168,6 +168,12 @@ class Component:
closure: dict[str, Any] = field(default_factory=dict)
css_classes: set[str] = field(default_factory=set) # pre-scanned :class values
deps: set[str] = field(default_factory=set) # transitive component deps (~names)
io_refs: set[str] = field(default_factory=set) # transitive IO primitive refs
@property
def is_pure(self) -> bool:
"""True if this component has no transitive IO dependencies."""
return not self.io_refs
def __repr__(self):
return f"<Component ~{self.name}({', '.join(self.params)})>"

View File

@@ -1,25 +1,30 @@
;; Bundle analyzer — live demonstration of Phase 1 component dependency analysis.
;; Bundle analyzer — live demonstration of dependency analysis + IO detection.
;; Shows per-page component bundles vs total, visualizing payload savings.
;; @css bg-green-100 text-green-800 bg-violet-600 bg-stone-200 text-violet-600 text-stone-600 text-green-600 rounded-full h-2.5 grid-cols-3
;; Drill down into each bundle to see component tree; expand to see SX source.
;; @css bg-green-100 text-green-800 bg-violet-600 bg-stone-200 text-violet-600 text-stone-600 text-green-600 rounded-full h-2.5 grid-cols-3 bg-blue-100 text-blue-800 bg-amber-100 text-amber-800 grid-cols-4 marker:text-stone-400 bg-blue-50 bg-amber-50 text-blue-700 text-amber-700 border-blue-200 border-amber-200 bg-blue-500 bg-amber-500
(defcomp ~bundle-analyzer-content (&key pages total-components total-macros)
(defcomp ~bundle-analyzer-content (&key pages total-components total-macros
pure-count io-count)
(~doc-page :title "Page Bundle Analyzer"
(p :class "text-stone-600 mb-6"
"Live analysis of component dependency graphs across all pages in this app. "
"Live analysis of component dependency graphs and IO classification across all pages. "
"Each bar shows how many of the "
(strong (str total-components))
" total components a page actually needs, computed by the "
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
" transitive closure algorithm.")
" transitive closure algorithm. "
"Click a page to see its component tree; expand a component to see its SX source.")
(div :class "mb-8 grid grid-cols-3 gap-4"
(div :class "mb-8 grid grid-cols-4 gap-4"
(~analyzer-stat :label "Total Components" :value (str total-components)
:cls "text-violet-600")
(~analyzer-stat :label "Total Macros" :value (str total-macros)
:cls "text-stone-600")
(~analyzer-stat :label "Pages Analyzed" :value (str (len pages))
:cls "text-green-600"))
(~analyzer-stat :label "Pure Components" :value (str pure-count)
:cls "text-blue-600")
(~analyzer-stat :label "IO-Dependent" :value (str io-count)
:cls "text-amber-600"))
(~doc-section :title "Per-Page Bundles" :id "bundles"
(div :class "space-y-3"
@@ -31,7 +36,11 @@
:direct (get page "direct")
:total total-components
:pct (get page "pct")
:savings (get page "savings")))
:savings (get page "savings")
:io-refs (get page "io-refs")
:pure-in-page (get page "pure-in-page")
:io-in-page (get page "io-in-page")
:components (get page "components")))
pages)))
(~doc-section :title "How It Works" :id "how"
@@ -39,7 +48,8 @@
(li (strong "Scan: ") "Regex finds all " (code "(~name") " patterns in the page's content expression.")
(li (strong "Resolve: ") "Each referenced component's body AST is walked to find transitive " (code "~") " references.")
(li (strong "Closure: ") "The full set is the union of direct + transitive deps, following chains through the component graph.")
(li (strong "Bundle: ") "Only these component definitions are serialized into the page payload. Everything else is omitted."))
(li (strong "Bundle: ") "Only these component definitions are serialized into the page payload. Everything else is omitted.")
(li (strong "IO detect: ") "Each component body is scanned for references to IO primitives (frag, query, service, etc.). Components with zero transitive IO refs are pure — safe for client rendering."))
(p :class "mt-4 text-stone-600"
"The analysis handles circular references (via seen-set), "
"walks all branches of control flow (if/when/cond/case), "
@@ -50,18 +60,61 @@
(div :class (str "text-3xl font-bold " cls) value)
(div :class "text-sm text-stone-500 mt-1" label)))
(defcomp ~analyzer-row (&key name path needed direct total pct savings)
(div :class "rounded border border-stone-200 p-4"
(div :class "flex items-center justify-between mb-2"
(div
(span :class "font-mono font-semibold text-stone-800" name)
(span :class "text-stone-400 text-sm ml-2" path))
(div :class "text-right"
(span :class "font-mono text-sm"
(span :class "text-violet-700 font-bold" (str needed))
(span :class "text-stone-400" (str " / " total)))
(span :class "ml-2 inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800"
(str savings "% saved"))))
(div :class "w-full bg-stone-200 rounded-full h-2.5"
(div :class "bg-violet-600 h-2.5 rounded-full transition-all"
:style (str "width: " pct "%")))))
(defcomp ~analyzer-row (&key name path needed direct total pct savings
io-refs pure-in-page io-in-page components)
(details :class "rounded border border-stone-200"
(summary :class "p-4 cursor-pointer hover:bg-stone-50 transition-colors"
(div :class "flex items-center justify-between mb-2"
(div
(span :class "font-mono font-semibold text-stone-800" name)
(span :class "text-stone-400 text-sm ml-2" path))
(div :class "flex items-center gap-2"
(span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800"
(str pure-in-page " pure"))
(span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800"
(str io-in-page " IO"))
(div :class "text-right"
(span :class "font-mono text-sm"
(span :class "text-violet-700 font-bold" (str needed))
(span :class "text-stone-400" (str " / " total)))
(span :class "ml-2 inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800"
(str savings "% saved")))))
(div :class "w-full bg-stone-200 rounded-full h-2.5"
(div :class "bg-violet-600 h-2.5 rounded-full transition-all"
:style (str "width: " pct "%"))))
;; Component tree (shown when expanded)
(div :class "border-t border-stone-200 p-4 bg-stone-50"
(div :class "text-xs font-medium text-stone-500 uppercase tracking-wide mb-3"
(str needed " components in bundle"))
(div :class "space-y-1"
(map (fn (comp)
(~analyzer-component
:comp-name (get comp "name")
:is-pure (get comp "is-pure")
:io-refs (get comp "io-refs")
:deps (get comp "deps")
:source (get comp "source")))
components)))))
(defcomp ~analyzer-component (&key comp-name is-pure io-refs deps source)
(details :class (str "rounded border "
(if is-pure "border-blue-200 bg-blue-50" "border-amber-200 bg-amber-50"))
(summary :class "px-3 py-2 cursor-pointer hover:opacity-80 transition-opacity"
(div :class "flex items-center justify-between"
(div :class "flex items-center gap-2"
(span :class (str "inline-block w-2 h-2 rounded-full "
(if is-pure "bg-blue-500" "bg-amber-500")))
(span :class "font-mono text-sm font-medium text-stone-800" comp-name))
(div :class "flex items-center gap-2"
(when (not (empty? io-refs))
(span :class "text-xs text-amber-700"
(str "IO: " (join ", " io-refs))))
(when (not (empty? deps))
(span :class "text-xs text-stone-500"
(str (len deps) " deps"))))))
;; SX source (shown when component expanded)
(div :class "not-prose border-t border-stone-200 p-3 bg-stone-100 rounded-b"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap overflow-x-auto"
(code (highlight source "lisp"))))))

View File

@@ -8,7 +8,7 @@
(defcomp ~doc-oob-code (&key target-id text)
(div :id target-id :sx-swap-oob "innerHTML"
(div :class "bg-stone-100 rounded p-4 mt-3"
(div :class "not-prose bg-stone-100 rounded p-4 mt-3"
(pre :class "text-sm whitespace-pre-wrap break-words"
(code text)))))
@@ -146,13 +146,13 @@
forms))))
(defcomp ~doc-special-form-card (&key name syntax doc tail-position example)
(div :class "border border-stone-200 rounded-lg p-4 space-y-3"
(div :class "not-prose border border-stone-200 rounded-lg p-4 space-y-3"
(div :class "flex items-baseline gap-3"
(code :class "text-lg font-bold text-violet-700" name)
(when (not (= tail-position "none"))
(span :class "text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700" "TCO")))
(when (not (= syntax ""))
(pre :class "bg-stone-50 rounded px-3 py-2 text-sm font-mono text-stone-700 overflow-x-auto"
(pre :class "bg-stone-100 rounded px-3 py-2 text-sm font-mono text-stone-700 overflow-x-auto"
syntax))
(p :class "text-stone-600 text-sm whitespace-pre-line" doc)
(when (not (= tail-position ""))

View File

@@ -174,8 +174,8 @@
(define module-spec-items (list
(dict :slug "deps" :filename "deps.sx" :title "Deps"
:desc "Component dependency analysis — per-page bundling, transitive closure, CSS scoping."
:prose "The deps module analyzes component dependency graphs to enable per-page bundling. Instead of sending every component definition to every page, deps.sx walks component AST bodies to find transitive ~component references, then computes the minimal set needed. It also collects per-page CSS classes from only the used components. All functions are pure — each host bootstraps them to native code via --spec-modules deps. Platform functions (component-deps, component-set-deps!, component-css-classes, env-components, regex-find-all, scan-css-classes) are implemented natively per target.")))
:desc "Component dependency analysis and IO detection — per-page bundling, transitive closure, CSS scoping, pure/IO classification."
:prose "The deps module analyzes component dependency graphs and classifies components as pure or IO-dependent. Phase 1 (bundling): walks component AST bodies to find transitive ~component references, computes the minimal set needed per page, and collects per-page CSS classes from only the used components. Phase 2 (IO detection): scans component ASTs for references to IO primitive names (from boundary.sx declarations — frag, query, service, current-user, highlight, etc.), computes transitive IO refs through the component graph, and caches the result on each component. Components with no transitive IO refs are pure — they can render anywhere without server data. IO-dependent components must expand server-side. The spec provides the classification; each host's async partial evaluator acts on it (expand IO-dependent server-side, serialize pure for client). All functions are pure — each host bootstraps them to native code via --spec-modules deps. Platform functions (component-deps, component-set-deps!, component-css-classes, component-io-refs, component-set-io-refs!, env-components, regex-find-all, scan-css-classes) are implemented natively per target.")))
(define all-spec-items (concat core-spec-items (concat adapter-spec-items (concat browser-spec-items (concat extension-spec-items module-spec-items)))))

View File

@@ -603,7 +603,7 @@
(~doc-section :title "Context" :id "context"
(p "SX has a working server-client pipeline: server evaluates pages with IO (DB, fragments), serializes as SX wire format, client parses and renders to DOM. The language and primitives are already isomorphic " (em "— same spec, same semantics, both sides.") " What's missing is the " (strong "plumbing") " that makes the boundary between server and client a sliding window rather than a fixed wall.")
(p "The key insight: " (strong "s-expressions can partially unfold on the server after IO, then finish unfolding on the client.") " The system should be clever enough to know which downstream components have data fetches, resolve those server-side, and send the rest as pure SX for client rendering. Eventually, the client can also do IO (mapping server DB queries to REST calls), handle routing (SPA), and even work offline with cached data."))
(p "The key insight: " (strong "s-expressions can partially unfold on the server after IO, then finish unfolding on the client.") " The system knows which components have data fetches (via IO detection in " (a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx") "), resolves those server-side, and sends the rest as pure SX for client rendering. The boundary slides automatically based on what each component actually needs."))
(~doc-section :title "Current State" :id "current-state"
(ul :class "space-y-2 text-stone-700 list-disc pl-5"
@@ -613,7 +613,9 @@
(li (strong "Wire format: ") "Server _aser → SX source → client parses → renders to DOM. Boundary is clean.")
(li (strong "Component caching: ") "Hash-based localStorage for component definitions and style dictionaries.")
(li (strong "CSS on-demand: ") "CSSX resolves keywords to CSS rules, injects only used rules.")
(li (strong "Boundary enforcement: ") "boundary.sx + SX_BOUNDARY_STRICT=1 validates all primitives/IO/helpers at registration.")))
(li (strong "Boundary enforcement: ") "boundary.sx + SX_BOUNDARY_STRICT=1 validates all primitives/IO/helpers at registration.")
(li (strong "Dependency analysis: ") "deps.sx computes per-page component bundles — only definitions a page actually uses are sent.")
(li (strong "IO detection: ") "deps.sx classifies every component as pure or IO-dependent by scanning for boundary primitive references transitively. The spec provides the classification; each host's async evaluator acts on it — expanding IO-dependent components server-side, serializing pure ones for client rendering.")))
;; -----------------------------------------------------------------------
;; Phase 1
@@ -667,9 +669,9 @@
(li (code "regex-find-all") " / " (code "scan-css-classes") " — host-native regex and CSS scanning")))))
(~doc-subsection :title "Spec module"
(p "deps.sx is loaded as a " (strong "spec module") " — an optional extension to the core spec. The bootstrapper flag " (code "--spec-modules deps") " includes it in the generated output alongside the core evaluator, parser, and renderer. The same mechanism can carry future modules (e.g., io-detection for Phase 2) without changing the bootstrapper architecture.")
(p "deps.sx is loaded as a " (strong "spec module") " — an optional extension to the core spec. The bootstrapper flag " (code "--spec-modules deps") " includes it in the generated output alongside the core evaluator, parser, and renderer. Phase 2 IO detection was added to the same module — same bootstrapping mechanism, no architecture changes needed.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "shared/sx/ref/deps.sx — canonical spec (9 functions, 6 platform declarations)")
(li "shared/sx/ref/deps.sx — canonical spec (14 functions, 8 platform declarations)")
(li "Bootstrapped to all host targets via --spec-modules deps")))
(~doc-subsection :title "Verification"
@@ -683,43 +685,61 @@
;; Phase 2
;; -----------------------------------------------------------------------
(~doc-section :title "Phase 2: Smart Server/Client Boundary" :id "phase-2"
(~doc-section :title "Phase 2: Smart Server/Client Boundary — IO Detection" :id "phase-2"
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
(p :class "text-violet-900 font-medium" "What it enables")
(p :class "text-violet-800" "Formalized partial evaluation model. Server evaluates IO, serializes pure subtrees. The system automatically knows \"this component needs server data\" vs \"this component is pure and can render anywhere.\""))
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
(div :class "flex items-center gap-2 mb-2"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
(a :href "/specs/deps" :class "text-green-700 underline text-sm font-medium" "View canonical spec: deps.sx")
(a :href "/isomorphism/bundle-analyzer" :class "text-green-700 underline text-sm font-medium" "Live bundle analyzer with IO"))
(p :class "text-green-900 font-medium" "What it enables")
(p :class "text-green-800" "Automatic IO detection and selective expansion. Server expands IO-dependent components, serializes pure ones for client. Per-component intelligence replaces global toggle."))
(~doc-subsection :title "Current Mechanism"
(p "_aser in async_eval.py already does partial evaluation — IO primitives are awaited and substituted, HTML tags and component calls serialize as SX. The _expand_components context var controls expansion. But this is a global toggle, not per-component."))
(~doc-subsection :title "Approach"
(~doc-subsection :title "IO Detection in the Spec"
(p "Five new functions in "
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
" extend the Phase 1 walker to detect IO primitive references:")
(div :class "space-y-4"
(div
(h4 :class "font-semibold text-stone-700" "1. Automatic IO detection")
(p "Extend Phase 1 AST walker to check for references to IO_PRIMITIVES names (frag, query, service, current-user, etc.).")
(~doc-code :code (highlight "def has_io_deps(name: str, env: dict) -> bool:\n \"\"\"True if component transitively references any IO primitive.\"\"\"\n ..." "python")))
(h4 :class "font-semibold text-stone-700" "1. IO scanning")
(p (code "scan-io-refs") " walks an AST node, collecting symbol names that match an IO name set. The IO set is provided by the host from boundary declarations (all three tiers: core IO, deployment IO, page helpers).")
(~doc-code :code (highlight "(define scan-io-refs\n (fn (node io-names)\n (let ((refs (list)))\n (scan-io-refs-walk node io-names refs)\n refs)))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "2. Component metadata")
(~doc-code :code (highlight "ComponentMeta:\n deps: set[str] # transitive component deps (Phase 1)\n io_refs: set[str] # IO primitive names referenced\n is_pure: bool # True if io_refs empty (transitively)" "python")))
(h4 :class "font-semibold text-stone-700" "2. Transitive IO closure")
(p (code "transitive-io-refs") " follows component deps recursively, unioning IO refs from all reachable components and macros. Cycle-safe via seen-set.")
(~doc-code :code (highlight "(define transitive-io-refs\n (fn (name env io-names)\n ;; Walk deps, scan each body for IO refs,\n ;; union all refs transitively.\n ...))" "lisp")))
(div
(h4 :class "font-semibold text-stone-700" "3. Selective expansion")
(p "Refine _aser: instead of checking a global _expand_components flag, check the component's is_pure metadata:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "IO-dependent → expand server-side (IO must resolve)")
(li "Pure → serialize for client (let client render)")
(li "Explicit override: :server true on defcomp forces server expansion")))
(h4 :class "font-semibold text-stone-700" "3. Batch computation")
(p (code "compute-all-io-refs") " iterates the env, computes transitive IO refs for each component, and caches the result via " (code "component-set-io-refs!") ". Called after " (code "compute-all-deps") " at component registration time."))
(div
(h4 :class "font-semibold text-stone-700" "4. Data manifest for pages")
(p "PageDef produces a declaration of what IO the page needs, enabling Phase 3 (client can prefetch data) and Phase 5 (streaming)."))))
(h4 :class "font-semibold text-stone-700" "4. Component metadata")
(p "Each component now carries " (code "io_refs") " (transitive IO primitive names) alongside " (code "deps") " and " (code "css_classes") ". The derived " (code "is_pure") " property is true when " (code "io_refs") " is empty — the component can render anywhere without server data."))))
(~doc-subsection :title "Selective Expansion"
(p "The partial evaluator " (code "_aser") " now uses per-component IO metadata instead of a global toggle:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "IO-dependent") " → expand server-side (IO must resolve)")
(li (strong "Pure") " → serialize for client (let client render)")
(li (strong "Layout slot context") " → all components still expand (backwards compat via " (code "_expand_components") " context var)"))
(p "A component calling " (code "(highlight ...)") " or " (code "(query ...)") " is IO-dependent. A component with only HTML tags and string ops is pure."))
(~doc-subsection :title "Platform interface additions"
(p "Two new platform functions each host implements:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
(li "(component-io-refs c) → cached IO ref list")
(li "(component-set-io-refs! c refs) → cache IO refs on component")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Components calling (query ...) classified IO-dependent; pure components classified pure")
(li "Existing pages produce identical output (regression)"))))
(li "Components calling (query ...) or (highlight ...) classified IO-dependent")
(li "Pure components (HTML-only) classified pure with empty io_refs")
(li "Transitive IO detection: component calling ~other where ~other calls (current-user) → IO-dependent")
(li "Bootstrapped to both hosts (sx_ref.py + sx-ref.js)")
(li (a :href "/isomorphism/bundle-analyzer" :class "text-violet-700 underline" "Live bundle analyzer") " shows per-page IO classification"))))
;; -----------------------------------------------------------------------
;; Phase 3

View File

@@ -160,7 +160,7 @@
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Dependency graph")
(div :class "bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
(div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words font-mono text-stone-700"
"parser.sx (standalone — no dependencies)
primitives.sx (standalone — declarative registry)
@@ -254,7 +254,7 @@ deps.sx depends on: eval (optional)")))
(p :class "text-stone-600" (get spec "desc"))
(when (get spec "prose")
(p :class "text-sm text-stone-500 leading-relaxed" (get spec "prose")))
(div :class "bg-stone-100 rounded-lg p-5 max-h-72 overflow-y-auto"
(div :class "not-prose bg-stone-100 rounded-lg p-5 max-h-72 overflow-y-auto"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight (get spec "source") "sx"))))))
spec-files))))
@@ -274,7 +274,7 @@ deps.sx depends on: eval (optional)")))
(p :class "text-xs text-stone-400 italic"
"The s-expression source below is the canonical specification. "
"The English description above is a summary.")))
(div :class "bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
(div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words"
(code (highlight spec-source "sx"))))))
@@ -350,7 +350,7 @@ deps.sx depends on: eval (optional)")))
" spec files (parser, eval, primitives, render, adapters, engine, orchestration, boot, cssx) "
"and emits a standalone JavaScript file. Platform bridge functions (DOM operations, fetch, timers) "
"are emitted as native JS implementations.")
(div :class "bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-stone-200"
(div :class "not-prose bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-stone-200"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight bootstrapper-source "python")))))
@@ -361,7 +361,7 @@ deps.sx depends on: eval (optional)")))
(p :class "text-sm text-stone-500"
"The JavaScript below was generated by running the bootstrapper against the current spec files. "
"It is a complete, self-contained SX runtime — parser, evaluator, DOM adapter, engine, and CSS system.")
(div :class "bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-violet-300"
(div :class "not-prose bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-violet-300"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight bootstrapped-output "javascript"))))))))
@@ -391,7 +391,7 @@ deps.sx depends on: eval (optional)")))
" spec files (eval, primitives, render, adapter-html) "
"and emits a standalone Python module. Platform bridge functions (type constructors, environment ops) "
"are emitted as native Python implementations.")
(div :class "bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-stone-200"
(div :class "not-prose bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-stone-200"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight bootstrapper-source "python")))))
@@ -402,7 +402,7 @@ deps.sx depends on: eval (optional)")))
(p :class "text-sm text-stone-500"
"The Python below was generated by running the bootstrapper against the current spec files. "
"It is a complete server-side SX evaluator — eval, primitives, and HTML renderer.")
(div :class "bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-violet-300"
(div :class "not-prose bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-violet-300"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight bootstrapped-output "python"))))))))

View File

@@ -16,7 +16,7 @@
children))
(defcomp ~doc-code (&key code)
(div :class "bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
(div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code))))
(defcomp ~doc-note (&key &rest children)

View File

@@ -12,7 +12,7 @@
(div :class "border border-dashed border-stone-300 rounded p-4 bg-stone-100" children))
(defcomp ~example-source (&key code)
(div :class "bg-stone-100 rounded p-5 mt-3 mx-auto max-w-3xl"
(div :class "not-prose bg-stone-100 rounded p-5 mt-3 mx-auto max-w-3xl"
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code))))
;; --- Click to load demo ---

View File

@@ -9,8 +9,7 @@
(p :class "text-sm text-stone-400"
"© Giles Bradshaw 2026")
(p :class "text-lg text-stone-500 max-w-2xl mx-auto mb-12"
"A hypermedia-driven UI engine that combines htmx's server-first philosophy "
"with React's component model. S-expressions over the wire — no HTML, no JavaScript frameworks.")
"(sx === code === data === protocol === content === behaviour === layout === style === spec === sx)")
(div :class "bg-stone-100 rounded-lg p-6 text-left font-mono text-sm mx-auto max-w-2xl"
(pre :class "leading-relaxed whitespace-pre-wrap" children))))

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,36 @@ def _bundle_analyzer_data() -> dict:
n = len(needed)
pct = round(n / total_components * 100) if total_components else 0
savings = 100 - pct
# IO classification + component details for this page
pure_in_page = 0
io_in_page = 0
page_io_refs: set[str] = set()
comp_details = []
for comp_name in sorted(needed):
val = env.get(comp_name)
if isinstance(val, Component):
is_pure = val.is_pure
if is_pure:
pure_in_page += 1
else:
io_in_page += 1
page_io_refs.update(val.io_refs)
# Reconstruct defcomp source
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(val.body, pretty=True)
source = f"(defcomp ~{val.name} {params_sx}\n {body_sx})"
comp_details.append({
"name": comp_name,
"is-pure": is_pure,
"io-refs": sorted(val.io_refs),
"deps": sorted(val.deps),
"source": source,
})
pages_data.append({
"name": name,
"path": page_def.path,
@@ -293,6 +325,10 @@ 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,
"components": comp_details,
})
pages_data.sort(key=lambda p: p["needed"], reverse=True)
@@ -301,6 +337,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,
}