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:
@@ -609,6 +609,14 @@
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function componentIoRefs(c) {
|
||||||
|
return c.ioRefs ? c.ioRefs.slice() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentSetIoRefs(c, refs) {
|
||||||
|
c.ioRefs = refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Platform interface — Parser
|
// Platform interface — Parser
|
||||||
@@ -2456,6 +2464,43 @@ callExpr.push(dictGet(kwargs, k)); } }
|
|||||||
return classes;
|
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)
|
// Platform interface — DOM adapter (browser-only)
|
||||||
@@ -3626,6 +3671,10 @@ callExpr.push(dictGet(kwargs, k)); } }
|
|||||||
componentsNeeded: componentsNeeded,
|
componentsNeeded: componentsNeeded,
|
||||||
pageComponentBundle: pageComponentBundle,
|
pageComponentBundle: pageComponentBundle,
|
||||||
pageCssClasses: pageCssClasses,
|
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)"
|
_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;
|
if (typeof module !== "undefined" && module.exports) module.exports = Sx;
|
||||||
else global.Sx = Sx;
|
else global.Sx = Sx;
|
||||||
|
|
||||||
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);
|
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);
|
||||||
|
|||||||
@@ -1332,8 +1332,9 @@ async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
|
|||||||
if isinstance(val, Macro):
|
if isinstance(val, Macro):
|
||||||
expanded = _expand_macro(val, expr[1:], env)
|
expanded = _expand_macro(val, expr[1:], env)
|
||||||
return await _aser(expanded, env, ctx)
|
return await _aser(expanded, env, ctx)
|
||||||
if isinstance(val, Component) and _expand_components.get():
|
if isinstance(val, Component):
|
||||||
return await _aser_component(val, expr[1:], env, ctx)
|
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)
|
return await _aser_call(name, expr[1:], env, ctx)
|
||||||
|
|
||||||
# Serialize-mode special/HO forms (checked BEFORE HTML_TAGS
|
# Serialize-mode special/HO forms (checked BEFORE HTML_TAGS
|
||||||
|
|||||||
@@ -68,6 +68,62 @@ def _compute_all_deps_fallback(env: dict[str, Any]) -> None:
|
|||||||
val.deps = _transitive_deps_fallback(key, env)
|
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]:
|
def _scan_components_from_sx_fallback(source: str) -> set[str]:
|
||||||
import re
|
import re
|
||||||
return {f"~{m}" for m in re.findall(r'\(~([a-zA-Z_][a-zA-Z0-9_\-]*)', source)}
|
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
|
from .ref.sx_ref import components_needed as _ref_cn
|
||||||
return set(_ref_cn(page_sx, env))
|
return set(_ref_cn(page_sx, env))
|
||||||
return _components_needed_fallback(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
|
||||||
|
|||||||
@@ -204,8 +204,9 @@ def register_components(sx_source: str) -> None:
|
|||||||
val.css_classes = set(all_classes)
|
val.css_classes = set(all_classes)
|
||||||
|
|
||||||
# Recompute transitive deps for all components (cheap — just AST walking)
|
# 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_deps(_COMPONENT_ENV)
|
||||||
|
compute_all_io_refs(_COMPONENT_ENV, get_all_io_names())
|
||||||
|
|
||||||
_compute_component_hash()
|
_compute_component_hash()
|
||||||
|
|
||||||
|
|||||||
@@ -502,9 +502,17 @@ class JSEmitter:
|
|||||||
"component-deps": "componentDeps",
|
"component-deps": "componentDeps",
|
||||||
"component-set-deps!": "componentSetDeps",
|
"component-set-deps!": "componentSetDeps",
|
||||||
"component-css-classes": "componentCssClasses",
|
"component-css-classes": "componentCssClasses",
|
||||||
|
"component-io-refs": "componentIoRefs",
|
||||||
|
"component-set-io-refs!": "componentSetIoRefs",
|
||||||
"env-components": "envComponents",
|
"env-components": "envComponents",
|
||||||
"regex-find-all": "regexFindAll",
|
"regex-find-all": "regexFindAll",
|
||||||
"scan-css-classes": "scanCssClasses",
|
"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:
|
if name in RENAMES:
|
||||||
return RENAMES[name]
|
return RENAMES[name]
|
||||||
@@ -1904,6 +1912,14 @@ PLATFORM_DEPS_JS = '''
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function componentIoRefs(c) {
|
||||||
|
return c.ioRefs ? c.ioRefs.slice() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentSetIoRefs(c, refs) {
|
||||||
|
c.ioRefs = refs;
|
||||||
|
}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
PLATFORM_PARSER_JS = r"""
|
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(' componentsNeeded: componentsNeeded,')
|
||||||
api_lines.append(' pageComponentBundle: pageComponentBundle,')
|
api_lines.append(' pageComponentBundle: pageComponentBundle,')
|
||||||
api_lines.append(' pageCssClasses: pageCssClasses,')
|
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(f' _version: "{version}"')
|
||||||
api_lines.append(' };')
|
api_lines.append(' };')
|
||||||
|
|||||||
@@ -247,9 +247,17 @@ class PyEmitter:
|
|||||||
"component-deps": "component_deps",
|
"component-deps": "component_deps",
|
||||||
"component-set-deps!": "component_set_deps",
|
"component-set-deps!": "component_set_deps",
|
||||||
"component-css-classes": "component_css_classes",
|
"component-css-classes": "component_css_classes",
|
||||||
|
"component-io-refs": "component_io_refs",
|
||||||
|
"component-set-io-refs!": "component_set_io_refs",
|
||||||
"env-components": "env_components",
|
"env-components": "env_components",
|
||||||
"regex-find-all": "regex_find_all",
|
"regex-find-all": "regex_find_all",
|
||||||
"scan-css-classes": "scan_css_classes",
|
"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:
|
if name in RENAMES:
|
||||||
return RENAMES[name]
|
return RENAMES[name]
|
||||||
@@ -1982,6 +1990,14 @@ PLATFORM_DEPS_PY = (
|
|||||||
' for m in _re.finditer(r\';;\\s*@css\\s+(.+)\', source):\n'
|
' for m in _re.finditer(r\';;\\s*@css\\s+(.+)\', source):\n'
|
||||||
' classes.update(m.group(1).split())\n'
|
' classes.update(m.group(1).split())\n'
|
||||||
' return list(classes)\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'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,112 @@
|
|||||||
classes)))
|
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))))
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Platform interface summary
|
;; Platform interface summary
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
@@ -223,6 +329,8 @@
|
|||||||
;; (component-deps c) → cached deps list (may be empty)
|
;; (component-deps c) → cached deps list (may be empty)
|
||||||
;; (component-set-deps! c d)→ cache deps on component
|
;; (component-set-deps! c d)→ cache deps on component
|
||||||
;; (component-css-classes c)→ pre-scanned CSS class list
|
;; (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
|
;; (macro-body m) → AST body of macro
|
||||||
;; (env-components env) → list of component names in env
|
;; (env-components env) → list of component names in env
|
||||||
;; (regex-find-all pat src) → list of capture group matches
|
;; (regex-find-all pat src) → list of capture group matches
|
||||||
|
|||||||
@@ -917,6 +917,14 @@ def scan_css_classes(source):
|
|||||||
classes.update(m.group(1).split())
|
classes.update(m.group(1).split())
|
||||||
return list(classes)
|
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 ===
|
# === Transpiled from eval ===
|
||||||
|
|
||||||
@@ -1210,6 +1218,24 @@ page_component_bundle = lambda page_source, env: components_needed(page_source,
|
|||||||
# page-css-classes
|
# 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))
|
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
|
# Fixups -- wire up render adapter dispatch
|
||||||
|
|||||||
@@ -168,6 +168,12 @@ class Component:
|
|||||||
closure: dict[str, Any] = field(default_factory=dict)
|
closure: dict[str, Any] = field(default_factory=dict)
|
||||||
css_classes: set[str] = field(default_factory=set) # pre-scanned :class values
|
css_classes: set[str] = field(default_factory=set) # pre-scanned :class values
|
||||||
deps: set[str] = field(default_factory=set) # transitive component deps (~names)
|
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):
|
def __repr__(self):
|
||||||
return f"<Component ~{self.name}({', '.join(self.params)})>"
|
return f"<Component ~{self.name}({', '.join(self.params)})>"
|
||||||
|
|||||||
@@ -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.
|
;; 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
|
;; Phase 2 adds IO classification: which components are pure vs IO-dependent.
|
||||||
|
;; @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
|
||||||
|
|
||||||
(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"
|
(~doc-page :title "Page Bundle Analyzer"
|
||||||
|
|
||||||
(p :class "text-stone-600 mb-6"
|
(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 "
|
"Each bar shows how many of the "
|
||||||
(strong (str total-components))
|
(strong (str total-components))
|
||||||
" total components a page actually needs, computed by the "
|
" total components a page actually needs, computed by the "
|
||||||
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
|
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
|
||||||
" transitive closure algorithm.")
|
" transitive closure algorithm. "
|
||||||
|
"Phase 2 IO detection classifies each component as pure or IO-dependent.")
|
||||||
|
|
||||||
(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)
|
(~analyzer-stat :label "Total Components" :value (str total-components)
|
||||||
:cls "text-violet-600")
|
:cls "text-violet-600")
|
||||||
(~analyzer-stat :label "Total Macros" :value (str total-macros)
|
(~analyzer-stat :label "Total Macros" :value (str total-macros)
|
||||||
:cls "text-stone-600")
|
:cls "text-stone-600")
|
||||||
(~analyzer-stat :label "Pages Analyzed" :value (str (len pages))
|
(~analyzer-stat :label "Pure Components" :value (str pure-count)
|
||||||
:cls "text-green-600"))
|
: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"
|
(~doc-section :title "Per-Page Bundles" :id "bundles"
|
||||||
(div :class "space-y-3"
|
(div :class "space-y-3"
|
||||||
@@ -31,7 +36,10 @@
|
|||||||
:direct (get page "direct")
|
:direct (get page "direct")
|
||||||
:total total-components
|
:total total-components
|
||||||
:pct (get page "pct")
|
: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")))
|
||||||
pages)))
|
pages)))
|
||||||
|
|
||||||
(~doc-section :title "How It Works" :id "how"
|
(~doc-section :title "How It Works" :id "how"
|
||||||
@@ -39,7 +47,8 @@
|
|||||||
(li (strong "Scan: ") "Regex finds all " (code "(~name") " patterns in the page's content expression.")
|
(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 "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 "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"
|
(p :class "mt-4 text-stone-600"
|
||||||
"The analysis handles circular references (via seen-set), "
|
"The analysis handles circular references (via seen-set), "
|
||||||
"walks all branches of control flow (if/when/cond/case), "
|
"walks all branches of control flow (if/when/cond/case), "
|
||||||
@@ -50,18 +59,24 @@
|
|||||||
(div :class (str "text-3xl font-bold " cls) value)
|
(div :class (str "text-3xl font-bold " cls) value)
|
||||||
(div :class "text-sm text-stone-500 mt-1" label)))
|
(div :class "text-sm text-stone-500 mt-1" label)))
|
||||||
|
|
||||||
(defcomp ~analyzer-row (&key name path needed direct total pct savings)
|
(defcomp ~analyzer-row (&key name path needed direct total pct savings
|
||||||
|
io-refs pure-in-page io-in-page)
|
||||||
(div :class "rounded border border-stone-200 p-4"
|
(div :class "rounded border border-stone-200 p-4"
|
||||||
(div :class "flex items-center justify-between mb-2"
|
(div :class "flex items-center justify-between mb-2"
|
||||||
(div
|
(div
|
||||||
(span :class "font-mono font-semibold text-stone-800" name)
|
(span :class "font-mono font-semibold text-stone-800" name)
|
||||||
(span :class "text-stone-400 text-sm ml-2" path))
|
(span :class "text-stone-400 text-sm ml-2" path))
|
||||||
(div :class "text-right"
|
(div :class "flex items-center gap-2"
|
||||||
(span :class "font-mono text-sm"
|
(span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800"
|
||||||
(span :class "text-violet-700 font-bold" (str needed))
|
(str pure-in-page " pure"))
|
||||||
(span :class "text-stone-400" (str " / " total)))
|
(span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800"
|
||||||
(span :class "ml-2 inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800"
|
(str io-in-page " IO"))
|
||||||
(str savings "% saved"))))
|
(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 "w-full bg-stone-200 rounded-full h-2.5"
|
||||||
(div :class "bg-violet-600 h-2.5 rounded-full transition-all"
|
(div :class "bg-violet-600 h-2.5 rounded-full transition-all"
|
||||||
:style (str "width: " pct "%")))))
|
:style (str "width: " pct "%")))))
|
||||||
|
|||||||
@@ -683,43 +683,61 @@
|
|||||||
;; Phase 2
|
;; 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"
|
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
|
||||||
(p :class "text-violet-900 font-medium" "What it enables")
|
(div :class "flex items-center gap-2 mb-2"
|
||||||
(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.\""))
|
(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"
|
(~doc-subsection :title "IO Detection in the Spec"
|
||||||
(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."))
|
(p "Five new functions in "
|
||||||
|
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
|
||||||
(~doc-subsection :title "Approach"
|
" extend the Phase 1 walker to detect IO primitive references:")
|
||||||
|
|
||||||
(div :class "space-y-4"
|
(div :class "space-y-4"
|
||||||
(div
|
(div
|
||||||
(h4 :class "font-semibold text-stone-700" "1. Automatic IO detection")
|
(h4 :class "font-semibold text-stone-700" "1. IO scanning")
|
||||||
(p "Extend Phase 1 AST walker to check for references to IO_PRIMITIVES names (frag, query, service, current-user, etc.).")
|
(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 "def has_io_deps(name: str, env: dict) -> bool:\n \"\"\"True if component transitively references any IO primitive.\"\"\"\n ..." "python")))
|
(~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
|
(div
|
||||||
(h4 :class "font-semibold text-stone-700" "2. Component metadata")
|
(h4 :class "font-semibold text-stone-700" "2. Transitive IO closure")
|
||||||
(~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")))
|
(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
|
(div
|
||||||
(h4 :class "font-semibold text-stone-700" "3. Selective expansion")
|
(h4 :class "font-semibold text-stone-700" "3. Batch computation")
|
||||||
(p "Refine _aser: instead of checking a global _expand_components flag, check the component's is_pure metadata:")
|
(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."))
|
||||||
(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")))
|
|
||||||
|
|
||||||
(div
|
(div
|
||||||
(h4 :class "font-semibold text-stone-700" "4. Data manifest for pages")
|
(h4 :class "font-semibold text-stone-700" "4. Component metadata")
|
||||||
(p "PageDef produces a declaration of what IO the page needs, enabling Phase 3 (client can prefetch data) and Phase 5 (streaming)."))))
|
(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"
|
(~doc-subsection :title "Verification"
|
||||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||||
(li "Components calling (query ...) classified IO-dependent; pure components classified pure")
|
(li "Components calling (query ...) or (highlight ...) classified IO-dependent")
|
||||||
(li "Existing pages produce identical output (regression)"))))
|
(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
|
;; Phase 3
|
||||||
|
|||||||
@@ -414,7 +414,8 @@
|
|||||||
:selected (or (find-current isomorphism-nav-items slug) ""))
|
:selected (or (find-current isomorphism-nav-items slug) ""))
|
||||||
:content (case slug
|
:content (case slug
|
||||||
"bundle-analyzer" (~bundle-analyzer-content
|
"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)))
|
:else (~plan-isomorphic-content)))
|
||||||
|
|
||||||
(defpage bundle-analyzer
|
(defpage bundle-analyzer
|
||||||
@@ -428,7 +429,8 @@
|
|||||||
:selected "Bundle Analyzer")
|
:selected "Bundle Analyzer")
|
||||||
:data (bundle-analyzer-data)
|
:data (bundle-analyzer-data)
|
||||||
:content (~bundle-analyzer-content
|
: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
|
;; Plans section
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ def _bundle_analyzer_data() -> dict:
|
|||||||
env = get_component_env()
|
env = get_component_env()
|
||||||
total_components = sum(1 for v in env.values() if isinstance(v, Component))
|
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))
|
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 = []
|
pages_data = []
|
||||||
for name, page_def in sorted(get_all_pages("sx").items()):
|
for name, page_def in sorted(get_all_pages("sx").items()):
|
||||||
@@ -286,6 +288,20 @@ def _bundle_analyzer_data() -> dict:
|
|||||||
n = len(needed)
|
n = len(needed)
|
||||||
pct = round(n / total_components * 100) if total_components else 0
|
pct = round(n / total_components * 100) if total_components else 0
|
||||||
savings = 100 - pct
|
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({
|
pages_data.append({
|
||||||
"name": name,
|
"name": name,
|
||||||
"path": page_def.path,
|
"path": page_def.path,
|
||||||
@@ -293,6 +309,9 @@ def _bundle_analyzer_data() -> dict:
|
|||||||
"needed": n,
|
"needed": n,
|
||||||
"pct": pct,
|
"pct": pct,
|
||||||
"savings": savings,
|
"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)
|
pages_data.sort(key=lambda p: p["needed"], reverse=True)
|
||||||
@@ -301,6 +320,8 @@ def _bundle_analyzer_data() -> dict:
|
|||||||
"pages": pages_data,
|
"pages": pages_data,
|
||||||
"total-components": total_components,
|
"total-components": total_components,
|
||||||
"total-macros": total_macros,
|
"total-macros": total_macros,
|
||||||
|
"pure-count": pure_count,
|
||||||
|
"io-count": io_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user