Wire deps.sx into both bootstrappers, rebootstrap Python + JS
deps.sx is now a spec module that both bootstrap_py.py and bootstrap_js.py can include via --spec-modules deps. Platform functions (component-deps, component-set-deps!, component-css-classes, env-components, regex-find-all, scan-css-classes) implemented natively in both Python and JS. - Fix deps.sx: env-get-or → env-get, extract nested define to top-level - bootstrap_py.py: SPEC_MODULES, PLATFORM_DEPS_PY, mangle entries, CLI arg - bootstrap_js.py: SPEC_MODULES, PLATFORM_DEPS_JS, mangle entries, CLI arg - Regenerate sx_ref.py and sx-ref.js with deps module - deps.py: thin dispatcher (SX_USE_REF=1 → bootstrapped, else fallback) - scan_components_from_sx now returns ~prefixed names (consistent with spec) Verified: 541 Python tests pass, JS deps tested with Node.js, both code paths (fallback + bootstrapped) produce identical results. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -532,6 +532,84 @@
|
||||
return NIL;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Platform: deps module — component dependency analysis
|
||||
// =========================================================================
|
||||
|
||||
function componentDeps(c) {
|
||||
return c.deps ? c.deps.slice() : [];
|
||||
}
|
||||
|
||||
function componentSetDeps(c, deps) {
|
||||
c.deps = deps;
|
||||
}
|
||||
|
||||
function componentCssClasses(c) {
|
||||
return c.cssClasses ? c.cssClasses.slice() : [];
|
||||
}
|
||||
|
||||
function envComponents(env) {
|
||||
var names = [];
|
||||
for (var k in env) {
|
||||
var v = env[k];
|
||||
if (v && (v._component || v._macro)) names.push(k);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function regexFindAll(pattern, source) {
|
||||
var re = new RegExp(pattern, "g");
|
||||
var results = [];
|
||||
var m;
|
||||
while ((m = re.exec(source)) !== null) {
|
||||
if (m[1] !== undefined) results.push(m[1]);
|
||||
else results.push(m[0]);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function scanCssClasses(source) {
|
||||
var classes = {};
|
||||
var result = [];
|
||||
var m;
|
||||
var re1 = /:class\s+"([^"]*)"/g;
|
||||
while ((m = re1.exec(source)) !== null) {
|
||||
var parts = m[1].split(/\s+/);
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (parts[i] && !classes[parts[i]]) {
|
||||
classes[parts[i]] = true;
|
||||
result.push(parts[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
var re2 = /:class\s+\(str\s+((?:"[^"]*"\s*)+)\)/g;
|
||||
while ((m = re2.exec(source)) !== null) {
|
||||
var re3 = /"([^"]*)"/g;
|
||||
var m2;
|
||||
while ((m2 = re3.exec(m[1])) !== null) {
|
||||
var parts2 = m2[1].split(/\s+/);
|
||||
for (var j = 0; j < parts2.length; j++) {
|
||||
if (parts2[j] && !classes[parts2[j]]) {
|
||||
classes[parts2[j]] = true;
|
||||
result.push(parts2[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var re4 = /;;\s*@css\s+(.+)/g;
|
||||
while ((m = re4.exec(source)) !== null) {
|
||||
var parts3 = m[1].split(/\s+/);
|
||||
for (var k = 0; k < parts3.length; k++) {
|
||||
if (parts3[k] && !classes[parts3[k]]) {
|
||||
classes[parts3[k]] = true;
|
||||
result.push(parts3[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — Parser
|
||||
// =========================================================================
|
||||
@@ -2303,6 +2381,82 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
var bootInit = function() { return (initCssTracking(), initStyleDict(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||
|
||||
|
||||
// === Transpiled from deps (component dependency analysis) ===
|
||||
|
||||
// scan-refs
|
||||
var scanRefs = function(node) { return (function() {
|
||||
var refs = [];
|
||||
scanRefsWalk(node, refs);
|
||||
return refs;
|
||||
})(); };
|
||||
|
||||
// scan-refs-walk
|
||||
var scanRefsWalk = function(node, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() {
|
||||
var name = symbolName(node);
|
||||
return (isSxTruthy(startsWith(name, "~")) ? (isSxTruthy(!contains(refs, name)) ? append_b(refs, name) : NIL) : NIL);
|
||||
})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanRefsWalk(item, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanRefsWalk(dictGet(node, key), refs); }, keys(node)) : NIL))); };
|
||||
|
||||
// transitive-deps-walk
|
||||
var transitiveDepsWalk = function(n, seen, env) { return (isSxTruthy(!contains(seen, n)) ? (append_b(seen, n), (function() {
|
||||
var val = envGet(env, n);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(componentBody(val))) : (isSxTruthy((typeOf(val) == "macro")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(macroBody(val))) : NIL));
|
||||
})()) : NIL); };
|
||||
|
||||
// transitive-deps
|
||||
var transitiveDeps = function(name, env) { return (function() {
|
||||
var seen = [];
|
||||
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||
transitiveDepsWalk(key, seen, env);
|
||||
return filter(function(x) { return !(x == key); }, seen);
|
||||
})(); };
|
||||
|
||||
// compute-all-deps
|
||||
var computeAllDeps = function(env) { return forEach(function(name) { return (function() {
|
||||
var val = envGet(env, name);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? componentSetDeps(val, transitiveDeps(name, env)) : NIL);
|
||||
})(); }, envComponents(env)); };
|
||||
|
||||
// scan-components-from-source
|
||||
var scanComponentsFromSource = function(source) { return (function() {
|
||||
var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)", source);
|
||||
return map(function(m) { return (String("~") + String(m)); }, matches);
|
||||
})(); };
|
||||
|
||||
// components-needed
|
||||
var componentsNeeded = function(pageSource, env) { return (function() {
|
||||
var direct = scanComponentsFromSource(pageSource);
|
||||
var allNeeded = [];
|
||||
{ var _c = direct; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; if (isSxTruthy(!contains(allNeeded, name))) {
|
||||
allNeeded.push(name);
|
||||
}
|
||||
(function() {
|
||||
var val = envGet(env, name);
|
||||
return (function() {
|
||||
var deps = (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && !isEmpty(componentDeps(val)))) ? componentDeps(val) : transitiveDeps(name, env));
|
||||
return forEach(function(dep) { return (isSxTruthy(!contains(allNeeded, dep)) ? append_b(allNeeded, dep) : NIL); }, deps);
|
||||
})();
|
||||
})(); } }
|
||||
return allNeeded;
|
||||
})(); };
|
||||
|
||||
// page-component-bundle
|
||||
var pageComponentBundle = function(pageSource, env) { return componentsNeeded(pageSource, env); };
|
||||
|
||||
// page-css-classes
|
||||
var pageCssClasses = function(pageSource, env) { return (function() {
|
||||
var needed = componentsNeeded(pageSource, env);
|
||||
var classes = [];
|
||||
{ var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() {
|
||||
var val = envGet(env, name);
|
||||
return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(cls) { return (isSxTruthy(!contains(classes, cls)) ? append_b(classes, cls) : NIL); }, componentCssClasses(val)) : NIL);
|
||||
})(); } }
|
||||
{ var _c = scanCssClasses(pageSource); for (var _i = 0; _i < _c.length; _i++) { var cls = _c[_i]; if (isSxTruthy(!contains(classes, cls))) {
|
||||
classes.push(cls);
|
||||
} } }
|
||||
return classes;
|
||||
})(); };
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// Platform interface — DOM adapter (browser-only)
|
||||
// =========================================================================
|
||||
@@ -3317,6 +3471,87 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
if (typeof aser === "function") PRIMITIVES["aser"] = aser;
|
||||
if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;
|
||||
|
||||
// =========================================================================
|
||||
// Extension: Delimited continuations (shift/reset)
|
||||
// =========================================================================
|
||||
|
||||
function Continuation(fn) { this.fn = fn; }
|
||||
Continuation.prototype._continuation = true;
|
||||
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
|
||||
|
||||
function ShiftSignal(kName, body, env) {
|
||||
this.kName = kName;
|
||||
this.body = body;
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
|
||||
|
||||
var _resetResume = [];
|
||||
|
||||
function sfReset(args, env) {
|
||||
var body = args[0];
|
||||
try {
|
||||
return trampoline(evalExpr(body, env));
|
||||
} catch (e) {
|
||||
if (e instanceof ShiftSignal) {
|
||||
var sig = e;
|
||||
var cont = new Continuation(function(value) {
|
||||
if (value === undefined) value = NIL;
|
||||
_resetResume.push(value);
|
||||
try {
|
||||
return trampoline(evalExpr(body, env));
|
||||
} finally {
|
||||
_resetResume.pop();
|
||||
}
|
||||
});
|
||||
var sigEnv = merge(sig.env);
|
||||
sigEnv[sig.kName] = cont;
|
||||
return trampoline(evalExpr(sig.body, sigEnv));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function sfShift(args, env) {
|
||||
if (_resetResume.length > 0) {
|
||||
return _resetResume[_resetResume.length - 1];
|
||||
}
|
||||
var kName = symbolName(args[0]);
|
||||
var body = args[1];
|
||||
throw new ShiftSignal(kName, body, env);
|
||||
}
|
||||
|
||||
// Wrap evalList to intercept reset/shift
|
||||
var _baseEvalList = evalList;
|
||||
evalList = function(expr, env) {
|
||||
var head = expr[0];
|
||||
if (isSym(head)) {
|
||||
var name = head.name;
|
||||
if (name === "reset") return sfReset(expr.slice(1), env);
|
||||
if (name === "shift") return sfShift(expr.slice(1), env);
|
||||
}
|
||||
return _baseEvalList(expr, env);
|
||||
};
|
||||
|
||||
// Wrap aserSpecial to handle reset/shift in SX wire mode
|
||||
if (typeof aserSpecial === "function") {
|
||||
var _baseAserSpecial = aserSpecial;
|
||||
aserSpecial = function(name, expr, env) {
|
||||
if (name === "reset") return sfReset(expr.slice(1), env);
|
||||
if (name === "shift") return sfShift(expr.slice(1), env);
|
||||
return _baseAserSpecial(name, expr, env);
|
||||
};
|
||||
}
|
||||
|
||||
// Wrap typeOf to recognize continuations
|
||||
var _baseTypeOf = typeOf;
|
||||
typeOf = function(x) {
|
||||
if (x != null && x._continuation) return "continuation";
|
||||
return _baseTypeOf(x);
|
||||
};
|
||||
|
||||
|
||||
// Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes)
|
||||
var parse = sxParse;
|
||||
|
||||
@@ -3385,6 +3620,12 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,
|
||||
getEnv: function() { return componentEnv; },
|
||||
init: typeof bootInit === "function" ? bootInit : null,
|
||||
scanRefs: scanRefs,
|
||||
transitiveDeps: transitiveDeps,
|
||||
computeAllDeps: computeAllDeps,
|
||||
componentsNeeded: componentsNeeded,
|
||||
pageComponentBundle: pageComponentBundle,
|
||||
pageCssClasses: pageCssClasses,
|
||||
_version: "ref-2.0 (boot+cssx+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)"
|
||||
};
|
||||
|
||||
|
||||
@@ -1,56 +1,48 @@
|
||||
"""
|
||||
Component dependency analysis.
|
||||
|
||||
Walks component AST bodies to compute transitive dependency sets.
|
||||
A component's deps are all other components (~name references) it
|
||||
can potentially render, including through control flow branches.
|
||||
Thin host wrapper over bootstrapped deps module from shared/sx/ref/deps.sx.
|
||||
The canonical logic lives in the spec; this module provides Python-typed
|
||||
entry points for the rest of the codebase.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Macro, Symbol
|
||||
|
||||
|
||||
def _scan_ast(node: Any) -> set[str]:
|
||||
"""Scan an AST node for ~component references.
|
||||
def _use_ref() -> bool:
|
||||
return os.environ.get("SX_USE_REF") == "1"
|
||||
|
||||
Walks all branches of control flow (if/when/cond/case) to find
|
||||
every component that *could* be rendered. Returns a set of
|
||||
component names (with ~ prefix).
|
||||
"""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hand-written fallback (used when SX_USE_REF != 1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _scan_ast(node: Any) -> set[str]:
|
||||
refs: set[str] = set()
|
||||
_walk(node, refs)
|
||||
return refs
|
||||
|
||||
|
||||
def _walk(node: Any, refs: set[str]) -> None:
|
||||
"""Recursively walk an AST node collecting ~name references."""
|
||||
if isinstance(node, Symbol):
|
||||
if node.name.startswith("~"):
|
||||
refs.add(node.name)
|
||||
return
|
||||
|
||||
if isinstance(node, list):
|
||||
for item in node:
|
||||
_walk(item, refs)
|
||||
return
|
||||
|
||||
if isinstance(node, dict):
|
||||
for v in node.values():
|
||||
_walk(v, refs)
|
||||
return
|
||||
|
||||
# Literals (str, int, float, bool, None, Keyword) — no refs
|
||||
return
|
||||
|
||||
|
||||
def transitive_deps(name: str, env: dict[str, Any]) -> set[str]:
|
||||
"""Compute transitive component dependencies for *name*.
|
||||
|
||||
Returns the set of all component names (with ~ prefix) that
|
||||
*name* can transitively render, NOT including *name* itself.
|
||||
"""
|
||||
def _transitive_deps_fallback(name: str, env: dict[str, Any]) -> set[str]:
|
||||
seen: set[str] = set()
|
||||
|
||||
def walk(n: str) -> None:
|
||||
@@ -70,37 +62,19 @@ def transitive_deps(name: str, env: dict[str, Any]) -> set[str]:
|
||||
return seen - {key}
|
||||
|
||||
|
||||
def compute_all_deps(env: dict[str, Any]) -> None:
|
||||
"""Compute and cache deps for all Component entries in *env*.
|
||||
|
||||
Mutates each Component's ``deps`` field in place.
|
||||
"""
|
||||
def _compute_all_deps_fallback(env: dict[str, Any]) -> None:
|
||||
for key, val in env.items():
|
||||
if isinstance(val, Component):
|
||||
val.deps = transitive_deps(key, env)
|
||||
val.deps = _transitive_deps_fallback(key, env)
|
||||
|
||||
|
||||
def scan_components_from_sx(source: str) -> set[str]:
|
||||
"""Extract component names referenced in SX source text.
|
||||
|
||||
Uses regex to find (~name patterns in serialized SX wire format.
|
||||
Returns names with ~ prefix, e.g. {"~card", "~nav-link"}.
|
||||
"""
|
||||
def _scan_components_from_sx_fallback(source: str) -> set[str]:
|
||||
import re
|
||||
return set(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)}
|
||||
|
||||
|
||||
def components_needed(page_sx: str, env: dict[str, Any]) -> set[str]:
|
||||
"""Compute the full set of component names needed for a page.
|
||||
|
||||
Scans *page_sx* for direct component references, then computes
|
||||
the transitive closure over the component dependency graph.
|
||||
Returns names with ~ prefix.
|
||||
"""
|
||||
# Direct refs from the page source
|
||||
direct = {f"~{n}" for n in scan_components_from_sx(page_sx)}
|
||||
|
||||
# Transitive closure
|
||||
def _components_needed_fallback(page_sx: str, env: dict[str, Any]) -> set[str]:
|
||||
direct = _scan_components_from_sx_fallback(page_sx)
|
||||
all_needed: set[str] = set()
|
||||
for name in direct:
|
||||
all_needed.add(name)
|
||||
@@ -108,7 +82,52 @@ def components_needed(page_sx: str, env: dict[str, Any]) -> set[str]:
|
||||
if isinstance(val, Component) and val.deps:
|
||||
all_needed.update(val.deps)
|
||||
else:
|
||||
# deps not cached yet — compute on the fly
|
||||
all_needed.update(transitive_deps(name, env))
|
||||
|
||||
all_needed.update(_transitive_deps_fallback(name, env))
|
||||
return all_needed
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API — dispatches to bootstrapped or fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def transitive_deps(name: str, env: dict[str, Any]) -> set[str]:
|
||||
"""Compute transitive component dependencies for *name*.
|
||||
|
||||
Returns the set of all component names (with ~ prefix) that
|
||||
*name* can transitively render, NOT including *name* itself.
|
||||
"""
|
||||
if _use_ref():
|
||||
from .ref.sx_ref import transitive_deps as _ref_td
|
||||
return set(_ref_td(name, env))
|
||||
return _transitive_deps_fallback(name, env)
|
||||
|
||||
|
||||
def compute_all_deps(env: dict[str, Any]) -> None:
|
||||
"""Compute and cache deps for all Component entries in *env*."""
|
||||
if _use_ref():
|
||||
from .ref.sx_ref import compute_all_deps as _ref_cad
|
||||
_ref_cad(env)
|
||||
return
|
||||
_compute_all_deps_fallback(env)
|
||||
|
||||
|
||||
def scan_components_from_sx(source: str) -> set[str]:
|
||||
"""Extract component names referenced in SX source text.
|
||||
|
||||
Returns names with ~ prefix, e.g. {"~card", "~nav-link"}.
|
||||
"""
|
||||
if _use_ref():
|
||||
from .ref.sx_ref import scan_components_from_source as _ref_sc
|
||||
return set(_ref_sc(source))
|
||||
return _scan_components_from_sx_fallback(source)
|
||||
|
||||
|
||||
def components_needed(page_sx: str, env: dict[str, Any]) -> set[str]:
|
||||
"""Compute the full set of component names needed for a page.
|
||||
|
||||
Returns names with ~ prefix.
|
||||
"""
|
||||
if _use_ref():
|
||||
from .ref.sx_ref import components_needed as _ref_cn
|
||||
return set(_ref_cn(page_sx, env))
|
||||
return _components_needed_fallback(page_sx, env)
|
||||
|
||||
@@ -490,6 +490,21 @@ class JSEmitter:
|
||||
"log-info": "logInfo",
|
||||
"log-parse-error": "logParseError",
|
||||
"parse-and-load-style-dict": "parseAndLoadStyleDict",
|
||||
# deps.sx
|
||||
"scan-refs": "scanRefs",
|
||||
"scan-refs-walk": "scanRefsWalk",
|
||||
"transitive-deps": "transitiveDeps",
|
||||
"compute-all-deps": "computeAllDeps",
|
||||
"scan-components-from-source": "scanComponentsFromSource",
|
||||
"components-needed": "componentsNeeded",
|
||||
"page-component-bundle": "pageComponentBundle",
|
||||
"page-css-classes": "pageCssClasses",
|
||||
"component-deps": "componentDeps",
|
||||
"component-set-deps!": "componentSetDeps",
|
||||
"component-css-classes": "componentCssClasses",
|
||||
"env-components": "envComponents",
|
||||
"regex-find-all": "regexFindAll",
|
||||
"scan-css-classes": "scanCssClasses",
|
||||
}
|
||||
if name in RENAMES:
|
||||
return RENAMES[name]
|
||||
@@ -1001,6 +1016,10 @@ ADAPTER_DEPS = {
|
||||
"parser": [],
|
||||
}
|
||||
|
||||
SPEC_MODULES = {
|
||||
"deps": ("deps.sx", "deps (component dependency analysis)"),
|
||||
}
|
||||
|
||||
|
||||
EXTENSION_NAMES = {"continuations"}
|
||||
|
||||
@@ -1091,6 +1110,7 @@ def compile_ref_to_js(
|
||||
adapters: list[str] | None = None,
|
||||
modules: list[str] | None = None,
|
||||
extensions: list[str] | None = None,
|
||||
spec_modules: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Read reference .sx files and emit JavaScript.
|
||||
|
||||
@@ -1104,6 +1124,9 @@ def compile_ref_to_js(
|
||||
extensions: List of optional extensions to include.
|
||||
Valid names: continuations.
|
||||
None = no extensions.
|
||||
spec_modules: List of spec module names to include.
|
||||
Valid names: deps.
|
||||
None = no spec modules.
|
||||
"""
|
||||
ref_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
emitter = JSEmitter()
|
||||
@@ -1131,7 +1154,16 @@ def compile_ref_to_js(
|
||||
for dep in ADAPTER_DEPS.get(a, []):
|
||||
adapter_set.add(dep)
|
||||
|
||||
# Core files always included, then selected adapters
|
||||
# Resolve spec modules
|
||||
spec_mod_set = set()
|
||||
if spec_modules:
|
||||
for sm in spec_modules:
|
||||
if sm not in SPEC_MODULES:
|
||||
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
|
||||
spec_mod_set.add(sm)
|
||||
has_deps = "deps" in spec_mod_set
|
||||
|
||||
# Core files always included, then selected adapters, then spec modules
|
||||
sx_files = [
|
||||
("eval.sx", "eval"),
|
||||
("render.sx", "render (core)"),
|
||||
@@ -1139,6 +1171,8 @@ def compile_ref_to_js(
|
||||
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "cssx", "boot"):
|
||||
if name in adapter_set:
|
||||
sx_files.append(ADAPTER_FILES[name])
|
||||
for name in sorted(spec_mod_set):
|
||||
sx_files.append(SPEC_MODULES[name])
|
||||
|
||||
all_sections = []
|
||||
for filename, label in sx_files:
|
||||
@@ -1190,6 +1224,9 @@ def compile_ref_to_js(
|
||||
parts.append(_assemble_primitives_js(prim_modules))
|
||||
parts.append(PLATFORM_JS_POST)
|
||||
|
||||
if has_deps:
|
||||
parts.append(PLATFORM_DEPS_JS)
|
||||
|
||||
# Parser platform must come before compiled parser.sx
|
||||
if has_parser:
|
||||
parts.append(adapter_platform["parser"])
|
||||
@@ -1211,7 +1248,7 @@ def compile_ref_to_js(
|
||||
parts.append(fixups_js(has_html, has_sx, has_dom))
|
||||
if has_continuations:
|
||||
parts.append(CONTINUATIONS_JS)
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label))
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps))
|
||||
parts.append(EPILOGUE)
|
||||
return "\n".join(parts)
|
||||
|
||||
@@ -1790,6 +1827,85 @@ PLATFORM_JS_POST = '''
|
||||
return NIL;
|
||||
}'''
|
||||
|
||||
PLATFORM_DEPS_JS = '''
|
||||
// =========================================================================
|
||||
// Platform: deps module — component dependency analysis
|
||||
// =========================================================================
|
||||
|
||||
function componentDeps(c) {
|
||||
return c.deps ? c.deps.slice() : [];
|
||||
}
|
||||
|
||||
function componentSetDeps(c, deps) {
|
||||
c.deps = deps;
|
||||
}
|
||||
|
||||
function componentCssClasses(c) {
|
||||
return c.cssClasses ? c.cssClasses.slice() : [];
|
||||
}
|
||||
|
||||
function envComponents(env) {
|
||||
var names = [];
|
||||
for (var k in env) {
|
||||
var v = env[k];
|
||||
if (v && (v._component || v._macro)) names.push(k);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function regexFindAll(pattern, source) {
|
||||
var re = new RegExp(pattern, "g");
|
||||
var results = [];
|
||||
var m;
|
||||
while ((m = re.exec(source)) !== null) {
|
||||
if (m[1] !== undefined) results.push(m[1]);
|
||||
else results.push(m[0]);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function scanCssClasses(source) {
|
||||
var classes = {};
|
||||
var result = [];
|
||||
var m;
|
||||
var re1 = /:class\\s+"([^"]*)"/g;
|
||||
while ((m = re1.exec(source)) !== null) {
|
||||
var parts = m[1].split(/\\s+/);
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (parts[i] && !classes[parts[i]]) {
|
||||
classes[parts[i]] = true;
|
||||
result.push(parts[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
var re2 = /:class\\s+\\(str\\s+((?:"[^"]*"\\s*)+)\\)/g;
|
||||
while ((m = re2.exec(source)) !== null) {
|
||||
var re3 = /"([^"]*)"/g;
|
||||
var m2;
|
||||
while ((m2 = re3.exec(m[1])) !== null) {
|
||||
var parts2 = m2[1].split(/\\s+/);
|
||||
for (var j = 0; j < parts2.length; j++) {
|
||||
if (parts2[j] && !classes[parts2[j]]) {
|
||||
classes[parts2[j]] = true;
|
||||
result.push(parts2[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var re4 = /;;\\s*@css\\s+(.+)/g;
|
||||
while ((m = re4.exec(source)) !== null) {
|
||||
var parts3 = m[1].split(/\\s+/);
|
||||
for (var k = 0; k < parts3.length; k++) {
|
||||
if (parts3[k] && !classes[parts3[k]]) {
|
||||
classes[parts3[k]] = true;
|
||||
result.push(parts3[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
'''
|
||||
|
||||
PLATFORM_PARSER_JS = r"""
|
||||
// =========================================================================
|
||||
// Platform interface — Parser
|
||||
@@ -2836,7 +2952,7 @@ def fixups_js(has_html, has_sx, has_dom):
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label):
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps=False):
|
||||
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
|
||||
if has_parser:
|
||||
parser = '''
|
||||
@@ -2958,6 +3074,13 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has
|
||||
api_lines.append(' init: typeof bootInit === "function" ? bootInit : null,')
|
||||
elif has_orch:
|
||||
api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,')
|
||||
if has_deps:
|
||||
api_lines.append(' scanRefs: scanRefs,')
|
||||
api_lines.append(' transitiveDeps: transitiveDeps,')
|
||||
api_lines.append(' computeAllDeps: computeAllDeps,')
|
||||
api_lines.append(' componentsNeeded: componentsNeeded,')
|
||||
api_lines.append(' pageComponentBundle: pageComponentBundle,')
|
||||
api_lines.append(' pageCssClasses: pageCssClasses,')
|
||||
|
||||
api_lines.append(f' _version: "{version}"')
|
||||
api_lines.append(' };')
|
||||
@@ -3015,6 +3138,8 @@ if __name__ == "__main__":
|
||||
help="Comma-separated primitive modules (core.* always included). Default: all")
|
||||
p.add_argument("--extensions",
|
||||
help="Comma-separated extensions (continuations). Default: none.")
|
||||
p.add_argument("--spec-modules",
|
||||
help="Comma-separated spec modules (deps). Default: none.")
|
||||
p.add_argument("--output", "-o",
|
||||
help="Output file (default: stdout)")
|
||||
args = p.parse_args()
|
||||
@@ -3022,7 +3147,8 @@ if __name__ == "__main__":
|
||||
adapters = args.adapters.split(",") if args.adapters else None
|
||||
modules = args.modules.split(",") if args.modules else None
|
||||
extensions = args.extensions.split(",") if args.extensions else None
|
||||
js = compile_ref_to_js(adapters, modules, extensions)
|
||||
spec_modules = args.spec_modules.split(",") if args.spec_modules else None
|
||||
js = compile_ref_to_js(adapters, modules, extensions, spec_modules)
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
|
||||
@@ -235,6 +235,21 @@ class PyEmitter:
|
||||
"map-dict": "map_dict",
|
||||
"eval-cond": "eval_cond",
|
||||
"process-bindings": "process_bindings",
|
||||
# deps.sx
|
||||
"scan-refs": "scan_refs",
|
||||
"scan-refs-walk": "scan_refs_walk",
|
||||
"transitive-deps": "transitive_deps",
|
||||
"compute-all-deps": "compute_all_deps",
|
||||
"scan-components-from-source": "scan_components_from_source",
|
||||
"components-needed": "components_needed",
|
||||
"page-component-bundle": "page_component_bundle",
|
||||
"page-css-classes": "page_css_classes",
|
||||
"component-deps": "component_deps",
|
||||
"component-set-deps!": "component_set_deps",
|
||||
"component-css-classes": "component_css_classes",
|
||||
"env-components": "env_components",
|
||||
"regex-find-all": "regex_find_all",
|
||||
"scan-css-classes": "scan_css_classes",
|
||||
}
|
||||
if name in RENAMES:
|
||||
return RENAMES[name]
|
||||
@@ -803,6 +818,11 @@ ADAPTER_FILES = {
|
||||
}
|
||||
|
||||
|
||||
SPEC_MODULES = {
|
||||
"deps": ("deps.sx", "deps (component dependency analysis)"),
|
||||
}
|
||||
|
||||
|
||||
EXTENSION_NAMES = {"continuations"}
|
||||
|
||||
# Extension-provided special forms (not in eval.sx core)
|
||||
@@ -889,6 +909,7 @@ def compile_ref_to_py(
|
||||
adapters: list[str] | None = None,
|
||||
modules: list[str] | None = None,
|
||||
extensions: list[str] | None = None,
|
||||
spec_modules: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Read reference .sx files and emit Python.
|
||||
|
||||
@@ -902,6 +923,9 @@ def compile_ref_to_py(
|
||||
extensions: List of optional extensions to include.
|
||||
Valid names: continuations.
|
||||
None = no extensions.
|
||||
spec_modules: List of spec module names to include.
|
||||
Valid names: deps.
|
||||
None = no spec modules.
|
||||
"""
|
||||
# Determine which primitive modules to include
|
||||
prim_modules = None # None = all
|
||||
@@ -926,7 +950,16 @@ def compile_ref_to_py(
|
||||
raise ValueError(f"Unknown adapter: {a!r}. Valid: {', '.join(ADAPTER_FILES)}")
|
||||
adapter_set.add(a)
|
||||
|
||||
# Core files always included, then selected adapters
|
||||
# Resolve spec modules
|
||||
spec_mod_set = set()
|
||||
if spec_modules:
|
||||
for sm in spec_modules:
|
||||
if sm not in SPEC_MODULES:
|
||||
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
|
||||
spec_mod_set.add(sm)
|
||||
has_deps = "deps" in spec_mod_set
|
||||
|
||||
# Core files always included, then selected adapters, then spec modules
|
||||
sx_files = [
|
||||
("eval.sx", "eval"),
|
||||
("forms.sx", "forms (server definition forms)"),
|
||||
@@ -935,6 +968,8 @@ def compile_ref_to_py(
|
||||
for name in ("html", "sx"):
|
||||
if name in adapter_set:
|
||||
sx_files.append(ADAPTER_FILES[name])
|
||||
for name in sorted(spec_mod_set):
|
||||
sx_files.append(SPEC_MODULES[name])
|
||||
|
||||
all_sections = []
|
||||
for filename, label in sx_files:
|
||||
@@ -969,6 +1004,9 @@ def compile_ref_to_py(
|
||||
parts.append(_assemble_primitives_py(prim_modules))
|
||||
parts.append(PRIMITIVES_PY_POST)
|
||||
|
||||
if has_deps:
|
||||
parts.append(PLATFORM_DEPS_PY)
|
||||
|
||||
for label, defines in all_sections:
|
||||
parts.append(f"\n# === Transpiled from {label} ===\n")
|
||||
for name, expr in defines:
|
||||
@@ -979,7 +1017,7 @@ def compile_ref_to_py(
|
||||
parts.append(FIXUPS_PY)
|
||||
if has_continuations:
|
||||
parts.append(CONTINUATIONS_PY)
|
||||
parts.append(public_api_py(has_html, has_sx))
|
||||
parts.append(public_api_py(has_html, has_sx, has_deps))
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
@@ -1903,6 +1941,50 @@ assoc = PRIMITIVES["assoc"]
|
||||
concat = PRIMITIVES["concat"]
|
||||
'''
|
||||
|
||||
|
||||
PLATFORM_DEPS_PY = (
|
||||
'\n'
|
||||
'# =========================================================================\n'
|
||||
'# Platform: deps module — component dependency analysis\n'
|
||||
'# =========================================================================\n'
|
||||
'\n'
|
||||
'import re as _re\n'
|
||||
'\n'
|
||||
'def component_deps(c):\n'
|
||||
' """Return cached deps list for a component (may be empty)."""\n'
|
||||
' return list(c.deps) if hasattr(c, "deps") and c.deps else []\n'
|
||||
'\n'
|
||||
'def component_set_deps(c, deps):\n'
|
||||
' """Cache deps on a component."""\n'
|
||||
' c.deps = set(deps) if not isinstance(deps, set) else deps\n'
|
||||
'\n'
|
||||
'def component_css_classes(c):\n'
|
||||
' """Return pre-scanned CSS class list for a component."""\n'
|
||||
' return list(c.css_classes) if hasattr(c, "css_classes") and c.css_classes else []\n'
|
||||
'\n'
|
||||
'def env_components(env):\n'
|
||||
' """Return list of component/macro names in an environment."""\n'
|
||||
' return [k for k, v in env.items()\n'
|
||||
' if isinstance(v, (Component, Macro))]\n'
|
||||
'\n'
|
||||
'def regex_find_all(pattern, source):\n'
|
||||
' """Return list of capture group 1 matches."""\n'
|
||||
' return [m.group(1) for m in _re.finditer(pattern, source)]\n'
|
||||
'\n'
|
||||
'def scan_css_classes(source):\n'
|
||||
' """Extract CSS class strings from SX source."""\n'
|
||||
' classes = set()\n'
|
||||
' for m in _re.finditer(r\':class\\s+"([^"]*)"\', source):\n'
|
||||
' classes.update(m.group(1).split())\n'
|
||||
' for m in _re.finditer(r\':class\\s+\\(str\\s+((?:"[^"]*"\\s*)+)\\)\', source):\n'
|
||||
' for s in _re.findall(r\'"([^"]*)"\', m.group(1)):\n'
|
||||
' classes.update(s.split())\n'
|
||||
' for m in _re.finditer(r\';;\\s*@css\\s+(.+)\', source):\n'
|
||||
' classes.update(m.group(1).split())\n'
|
||||
' return list(classes)\n'
|
||||
)
|
||||
|
||||
|
||||
FIXUPS_PY = '''
|
||||
# =========================================================================
|
||||
# Fixups -- wire up render adapter dispatch
|
||||
@@ -1996,7 +2078,7 @@ aser_special = _aser_special_with_continuations
|
||||
'''
|
||||
|
||||
|
||||
def public_api_py(has_html: bool, has_sx: bool) -> str:
|
||||
def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str:
|
||||
lines = [
|
||||
'',
|
||||
'# =========================================================================',
|
||||
@@ -2059,11 +2141,17 @@ def main():
|
||||
default=None,
|
||||
help="Comma-separated extensions (continuations). Default: none.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--spec-modules",
|
||||
default=None,
|
||||
help="Comma-separated spec modules (deps). Default: none.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
adapters = args.adapters.split(",") if args.adapters else None
|
||||
modules = args.modules.split(",") if args.modules else None
|
||||
extensions = args.extensions.split(",") if args.extensions else None
|
||||
print(compile_ref_to_py(adapters, modules, extensions))
|
||||
spec_modules = args.spec_modules.split(",") if args.spec_modules else None
|
||||
print(compile_ref_to_py(adapters, modules, extensions, spec_modules))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -8,19 +8,21 @@
|
||||
;; All functions are pure — no IO, no platform-specific operations.
|
||||
;; Each host bootstraps this to native code alongside eval.sx/render.sx.
|
||||
;;
|
||||
;; Platform interface (provided by host):
|
||||
;; From eval.sx platform (already provided by every host):
|
||||
;; (type-of x) → type string
|
||||
;; (symbol-name s) → string name of symbol
|
||||
;; (component-body c) → unevaluated AST of component body
|
||||
;; (component-name c) → string name (without ~)
|
||||
;;
|
||||
;; Already available from eval.sx platform:
|
||||
;; (type-of x), (symbol-name s)
|
||||
;;
|
||||
;; New platform functions for deps:
|
||||
;; (component-body c) → component body AST
|
||||
;; (component-name c) → component name string
|
||||
;; (macro-body m) → macro body AST
|
||||
;; (env-get env k) → value or nil
|
||||
;;
|
||||
;; New platform functions for deps (each host implements):
|
||||
;; (component-deps c) → cached deps list (may be empty)
|
||||
;; (component-set-deps! c d)→ cache deps on component
|
||||
;; (component-css-classes c)→ pre-scanned CSS class list
|
||||
;; (env-components env) → list of component/macro names in env
|
||||
;; (regex-find-all pat src) → list of capture group 1 matches
|
||||
;; (scan-css-classes src) → list of CSS class strings from source
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
@@ -66,24 +68,26 @@
|
||||
;; Given a component name and an environment, compute all components
|
||||
;; that it can transitively render. Handles cycles via seen-set.
|
||||
|
||||
(define transitive-deps-walk
|
||||
(fn (n seen env)
|
||||
(when (not (contains? seen n))
|
||||
(append! seen n)
|
||||
(let ((val (env-get env n)))
|
||||
(cond
|
||||
(= (type-of val) "component")
|
||||
(for-each (fn (ref) (transitive-deps-walk ref seen env))
|
||||
(scan-refs (component-body val)))
|
||||
(= (type-of val) "macro")
|
||||
(for-each (fn (ref) (transitive-deps-walk ref seen env))
|
||||
(scan-refs (macro-body val)))
|
||||
:else nil)))))
|
||||
|
||||
|
||||
(define transitive-deps
|
||||
(fn (name env)
|
||||
(let ((seen (list))
|
||||
(key (if (starts-with? name "~") name (str "~" name))))
|
||||
|
||||
(define walk
|
||||
(fn (n)
|
||||
(when (not (contains? seen n))
|
||||
(append! seen n)
|
||||
(let ((val (env-get-or env n nil)))
|
||||
(cond
|
||||
(= (type-of val) "component")
|
||||
(for-each walk (scan-refs (component-body val)))
|
||||
(= (type-of val) "macro")
|
||||
(for-each walk (scan-refs (macro-body val)))
|
||||
:else nil)))))
|
||||
|
||||
(walk key)
|
||||
(transitive-deps-walk key seen env)
|
||||
(filter (fn (x) (not (= x key))) seen))))
|
||||
|
||||
|
||||
@@ -101,7 +105,7 @@
|
||||
(fn (env)
|
||||
(for-each
|
||||
(fn (name)
|
||||
(let ((val (env-get-or env name nil)))
|
||||
(let ((val (env-get env name)))
|
||||
(when (= (type-of val) "component")
|
||||
(component-set-deps! val (transitive-deps name env)))))
|
||||
(env-components env))))
|
||||
@@ -138,7 +142,7 @@
|
||||
(fn (name)
|
||||
(when (not (contains? all-needed name))
|
||||
(append! all-needed name))
|
||||
(let ((val (env-get-or env name nil)))
|
||||
(let ((val (env-get env name)))
|
||||
(let ((deps (if (and (= (type-of val) "component")
|
||||
(not (empty? (component-deps val))))
|
||||
(component-deps val)
|
||||
@@ -185,7 +189,7 @@
|
||||
;; Collect classes from needed components
|
||||
(for-each
|
||||
(fn (name)
|
||||
(let ((val (env-get-or env name nil)))
|
||||
(let ((val (env-get env name)))
|
||||
(when (= (type-of val) "component")
|
||||
(for-each
|
||||
(fn (cls)
|
||||
@@ -211,7 +215,7 @@
|
||||
;; From eval.sx (already provided):
|
||||
;; (type-of x) → type string
|
||||
;; (symbol-name s) → string name of symbol
|
||||
;; (env-get-or env k d) → value or default
|
||||
;; (env-get env k) → value or nil
|
||||
;;
|
||||
;; New for deps.sx (each host implements):
|
||||
;; (component-body c) → AST body of component
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# WARNING: special-forms.sx declares forms not in eval.sx: reset, shift
|
||||
"""
|
||||
sx_ref.py -- Generated from reference SX evaluator specification.
|
||||
|
||||
@@ -879,6 +878,46 @@ assoc = PRIMITIVES["assoc"]
|
||||
concat = PRIMITIVES["concat"]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Platform: deps module — component dependency analysis
|
||||
# =========================================================================
|
||||
|
||||
import re as _re
|
||||
|
||||
def component_deps(c):
|
||||
"""Return cached deps list for a component (may be empty)."""
|
||||
return list(c.deps) if hasattr(c, "deps") and c.deps else []
|
||||
|
||||
def component_set_deps(c, deps):
|
||||
"""Cache deps on a component."""
|
||||
c.deps = set(deps) if not isinstance(deps, set) else deps
|
||||
|
||||
def component_css_classes(c):
|
||||
"""Return pre-scanned CSS class list for a component."""
|
||||
return list(c.css_classes) if hasattr(c, "css_classes") and c.css_classes else []
|
||||
|
||||
def env_components(env):
|
||||
"""Return list of component/macro names in an environment."""
|
||||
return [k for k, v in env.items()
|
||||
if isinstance(v, (Component, Macro))]
|
||||
|
||||
def regex_find_all(pattern, source):
|
||||
"""Return list of capture group 1 matches."""
|
||||
return [m.group(1) for m in _re.finditer(pattern, source)]
|
||||
|
||||
def scan_css_classes(source):
|
||||
"""Extract CSS class strings from SX source."""
|
||||
classes = set()
|
||||
for m in _re.finditer(r':class\s+"([^"]*)"', source):
|
||||
classes.update(m.group(1).split())
|
||||
for m in _re.finditer(r':class\s+\(str\s+((?:"[^"]*"\s*)+)\)', source):
|
||||
for s in _re.findall(r'"([^"]*)"', m.group(1)):
|
||||
classes.update(s.split())
|
||||
for m in _re.finditer(r';;\s*@css\s+(.+)', source):
|
||||
classes.update(m.group(1).split())
|
||||
return list(classes)
|
||||
|
||||
|
||||
# === Transpiled from eval ===
|
||||
|
||||
# trampoline
|
||||
@@ -1139,6 +1178,39 @@ aser_fragment = lambda children, env: (lambda parts: ('' if sx_truthy(empty_p(pa
|
||||
aser_call = lambda name, args, env: (lambda parts: _sx_begin(reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin((_sx_begin(_sx_append(parts, sx_str(':', keyword_name(arg))), _sx_append(parts, serialize(val))) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(aser(nth(args, (get(state, 'i') + 1)), env)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else (lambda val: _sx_begin((_sx_append(parts, serialize(val)) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'i', (get(state, 'i') + 1))))(aser(arg, env)))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), sx_str('(', join(' ', parts), ')')))([name])
|
||||
|
||||
|
||||
# === Transpiled from deps (component dependency analysis) ===
|
||||
|
||||
# scan-refs
|
||||
scan_refs = lambda node: (lambda refs: _sx_begin(scan_refs_walk(node, refs), refs))([])
|
||||
|
||||
# scan-refs-walk
|
||||
scan_refs_walk = lambda node, refs: ((lambda name: ((_sx_append(refs, name) if sx_truthy((not sx_truthy(contains_p(refs, name)))) else NIL) if sx_truthy(starts_with_p(name, '~')) else NIL))(symbol_name(node)) if sx_truthy((type_of(node) == 'symbol')) else (for_each(lambda item: scan_refs_walk(item, refs), node) if sx_truthy((type_of(node) == 'list')) else (for_each(lambda key: scan_refs_walk(dict_get(node, key), refs), keys(node)) if sx_truthy((type_of(node) == 'dict')) else NIL)))
|
||||
|
||||
# transitive-deps-walk
|
||||
transitive_deps_walk = lambda n, seen, env: (_sx_begin(_sx_append(seen, n), (lambda val: (for_each(lambda ref: transitive_deps_walk(ref, seen, env), scan_refs(component_body(val))) if sx_truthy((type_of(val) == 'component')) else (for_each(lambda ref: transitive_deps_walk(ref, seen, env), 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-deps
|
||||
transitive_deps = lambda name, env: (lambda seen: (lambda key: _sx_begin(transitive_deps_walk(key, seen, env), filter(lambda x: (not sx_truthy((x == key))), seen)))((name if sx_truthy(starts_with_p(name, '~')) else sx_str('~', name))))([])
|
||||
|
||||
# compute-all-deps
|
||||
compute_all_deps = lambda env: for_each(lambda name: (lambda val: (component_set_deps(val, transitive_deps(name, env)) if sx_truthy((type_of(val) == 'component')) else NIL))(env_get(env, name)), env_components(env))
|
||||
|
||||
# scan-components-from-source
|
||||
scan_components_from_source = lambda source: (lambda matches: map(lambda m: sx_str('~', m), matches))(regex_find_all('\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)', source))
|
||||
|
||||
# components-needed
|
||||
components_needed = lambda page_source, env: (lambda direct: (lambda all_needed: _sx_begin(for_each(_sx_fn(lambda name: (
|
||||
(_sx_append(all_needed, name) if sx_truthy((not sx_truthy(contains_p(all_needed, name)))) else NIL),
|
||||
(lambda val: (lambda deps: for_each(lambda dep: (_sx_append(all_needed, dep) if sx_truthy((not sx_truthy(contains_p(all_needed, dep)))) else NIL), deps))((component_deps(val) if sx_truthy(((type_of(val) == 'component') if not sx_truthy((type_of(val) == 'component')) else (not sx_truthy(empty_p(component_deps(val)))))) else transitive_deps(name, env))))(env_get(env, name))
|
||||
)[-1]), direct), all_needed))([]))(scan_components_from_source(page_source))
|
||||
|
||||
# page-component-bundle
|
||||
page_component_bundle = lambda page_source, env: components_needed(page_source, env)
|
||||
|
||||
# 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))
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Fixups -- wire up render adapter dispatch
|
||||
# =========================================================================
|
||||
@@ -1171,6 +1243,64 @@ def _wrap_aser_outputs():
|
||||
aser_fragment = _aser_fragment_wrapped
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Extension: delimited continuations (shift/reset)
|
||||
# =========================================================================
|
||||
|
||||
_RESET_RESUME = [] # stack of resume values; empty = not resuming
|
||||
|
||||
_SPECIAL_FORM_NAMES = _SPECIAL_FORM_NAMES | frozenset(["reset", "shift"])
|
||||
|
||||
def sf_reset(args, env):
|
||||
"""(reset body) -- establish a continuation delimiter."""
|
||||
body = first(args)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
except _ShiftSignal as sig:
|
||||
def cont_fn(value=NIL):
|
||||
_RESET_RESUME.append(value)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
finally:
|
||||
_RESET_RESUME.pop()
|
||||
k = Continuation(cont_fn)
|
||||
sig_env = dict(sig.env)
|
||||
sig_env[sig.k_name] = k
|
||||
return trampoline(eval_expr(sig.body, sig_env))
|
||||
|
||||
def sf_shift(args, env):
|
||||
"""(shift k body) -- capture continuation to nearest reset."""
|
||||
if _RESET_RESUME:
|
||||
return _RESET_RESUME[-1]
|
||||
k_name = symbol_name(first(args))
|
||||
body = nth(args, 1)
|
||||
raise _ShiftSignal(k_name, body, env)
|
||||
|
||||
# Wrap eval_list to inject shift/reset dispatch
|
||||
_base_eval_list = eval_list
|
||||
def _eval_list_with_continuations(expr, env):
|
||||
head = first(expr)
|
||||
if type_of(head) == "symbol":
|
||||
name = symbol_name(head)
|
||||
args = rest(expr)
|
||||
if name == "reset":
|
||||
return sf_reset(args, env)
|
||||
if name == "shift":
|
||||
return sf_shift(args, env)
|
||||
return _base_eval_list(expr, env)
|
||||
eval_list = _eval_list_with_continuations
|
||||
|
||||
# Inject into aser_special
|
||||
_base_aser_special = aser_special
|
||||
def _aser_special_with_continuations(name, expr, env):
|
||||
if name == "reset":
|
||||
return sf_reset(expr[1:], env)
|
||||
if name == "shift":
|
||||
return sf_shift(expr[1:], env)
|
||||
return _base_aser_special(name, expr, env)
|
||||
aser_special = _aser_special_with_continuations
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Public API
|
||||
# =========================================================================
|
||||
|
||||
@@ -147,7 +147,7 @@ class TestScanComponentsFromSx:
|
||||
def test_basic(self):
|
||||
source = '(~card :title "hi" (~badge :label "new"))'
|
||||
refs = scan_components_from_sx(source)
|
||||
assert refs == {"card", "badge"}
|
||||
assert refs == {"~card", "~badge"}
|
||||
|
||||
def test_no_components(self):
|
||||
source = '(div :class "p-4" (p "hello"))'
|
||||
|
||||
Reference in New Issue
Block a user