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:
2026-03-06 11:55:32 +00:00
parent 6739343a06
commit 4c97b03dda
7 changed files with 692 additions and 84 deletions

View File

@@ -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__":