Spec modularization: - Add (define-module :name) markers to primitives.sx creating 11 modules (7 core, 4 stdlib). Bootstrappers can now selectively include modules. - Add parse_primitives_by_module() to boundary_parser.py. - Remove split-ids primitive; inline at 4 call sites in blog/market queries. Python file split: - primitives.py: slimmed to registry + core primitives only (~350 lines) - primitives_stdlib.py: NEW — stdlib primitives (format, text, style, debug) - primitives_ctx.py: NEW — extracted 12 page context builders from IO - primitives_io.py: add register_io_handler decorator, auto-derive IO_PRIMITIVES from registry, move sync IO bridges here JS parity fixes: - = uses === (strict equality), != uses !== - round supports optional ndigits parameter - concat uses nil-check not falsy-check (preserves 0, "", false) - escape adds single quote entity (') matching Python/markupsafe - assert added (was missing from JS entirely) Bootstrapper modularization: - PRIMITIVES_JS_MODULES / PRIMITIVES_PY_MODULES dicts keyed by module - --modules CLI flag for selective inclusion (core.* always included) - Regenerated sx-ref.js and sx_ref.py with all fixes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
135 lines
4.4 KiB
Python
135 lines
4.4 KiB
Python
"""
|
|
Parse boundary.sx and primitives.sx to extract declared names.
|
|
|
|
Shared by both bootstrap_py.py and bootstrap_js.py, and used at runtime
|
|
by the validation module.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
# Allow standalone use (from bootstrappers) or in-project imports
|
|
try:
|
|
from shared.sx.parser import parse_all
|
|
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
|
|
except ImportError:
|
|
import sys
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
|
sys.path.insert(0, _PROJECT)
|
|
from shared.sx.parser import parse_all
|
|
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
|
|
|
|
|
|
def _ref_dir() -> str:
|
|
return os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def _read_file(filename: str) -> str:
|
|
filepath = os.path.join(_ref_dir(), filename)
|
|
with open(filepath, encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
def _extract_keyword_arg(expr: list, key: str) -> Any:
|
|
"""Extract :key value from a flat keyword-arg list."""
|
|
for i, item in enumerate(expr):
|
|
if isinstance(item, Keyword) and item.name == key and i + 1 < len(expr):
|
|
return expr[i + 1]
|
|
return None
|
|
|
|
|
|
def parse_primitives_sx() -> frozenset[str]:
|
|
"""Parse primitives.sx and return frozenset of declared pure primitive names."""
|
|
by_module = parse_primitives_by_module()
|
|
all_names: set[str] = set()
|
|
for names in by_module.values():
|
|
all_names.update(names)
|
|
return frozenset(all_names)
|
|
|
|
|
|
def parse_primitives_by_module() -> dict[str, frozenset[str]]:
|
|
"""Parse primitives.sx and return primitives grouped by module.
|
|
|
|
Returns:
|
|
Dict mapping module name (e.g. "core.arithmetic") to frozenset of
|
|
primitive names declared under that module.
|
|
"""
|
|
source = _read_file("primitives.sx")
|
|
exprs = parse_all(source)
|
|
modules: dict[str, set[str]] = {}
|
|
current_module = "_unscoped"
|
|
|
|
for expr in exprs:
|
|
if not isinstance(expr, list) or len(expr) < 2:
|
|
continue
|
|
if not isinstance(expr[0], Symbol):
|
|
continue
|
|
|
|
if expr[0].name == "define-module":
|
|
mod_name = expr[1]
|
|
if isinstance(mod_name, Keyword):
|
|
current_module = mod_name.name
|
|
elif isinstance(mod_name, str):
|
|
current_module = mod_name
|
|
|
|
elif expr[0].name == "define-primitive":
|
|
name = expr[1]
|
|
if isinstance(name, str):
|
|
modules.setdefault(current_module, set()).add(name)
|
|
|
|
return {mod: frozenset(names) for mod, names in modules.items()}
|
|
|
|
|
|
def parse_boundary_sx() -> tuple[frozenset[str], dict[str, frozenset[str]]]:
|
|
"""Parse boundary.sx and return (io_names, {service: helper_names}).
|
|
|
|
Returns:
|
|
io_names: frozenset of declared I/O primitive names
|
|
helpers: dict mapping service name to frozenset of helper names
|
|
"""
|
|
source = _read_file("boundary.sx")
|
|
exprs = parse_all(source)
|
|
io_names: set[str] = set()
|
|
helpers: dict[str, set[str]] = {}
|
|
|
|
for expr in exprs:
|
|
if not isinstance(expr, list) or not expr:
|
|
continue
|
|
head = expr[0]
|
|
if not isinstance(head, Symbol):
|
|
continue
|
|
|
|
if head.name == "define-io-primitive":
|
|
name = expr[1]
|
|
if isinstance(name, str):
|
|
io_names.add(name)
|
|
|
|
elif head.name == "define-page-helper":
|
|
name = expr[1]
|
|
service = _extract_keyword_arg(expr, "service")
|
|
if isinstance(name, str) and isinstance(service, str):
|
|
helpers.setdefault(service, set()).add(name)
|
|
|
|
frozen_helpers = {svc: frozenset(names) for svc, names in helpers.items()}
|
|
return frozenset(io_names), frozen_helpers
|
|
|
|
|
|
def parse_boundary_types() -> frozenset[str]:
|
|
"""Parse boundary.sx and return the declared boundary type names."""
|
|
source = _read_file("boundary.sx")
|
|
exprs = parse_all(source)
|
|
for expr in exprs:
|
|
if (isinstance(expr, list) and len(expr) >= 2
|
|
and isinstance(expr[0], Symbol)
|
|
and expr[0].name == "define-boundary-types"):
|
|
type_list = expr[1]
|
|
if isinstance(type_list, list):
|
|
# (list "number" "string" ...)
|
|
return frozenset(
|
|
item for item in type_list
|
|
if isinstance(item, str)
|
|
)
|
|
return frozenset()
|