Refactor SX primitives: modular, isomorphic, general-purpose
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>
This commit is contained in:
@@ -801,14 +801,30 @@ ADAPTER_FILES = {
|
||||
}
|
||||
|
||||
|
||||
def compile_ref_to_py(adapters: list[str] | None = None) -> str:
|
||||
def compile_ref_to_py(
|
||||
adapters: list[str] | None = None,
|
||||
modules: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Read reference .sx files and emit Python.
|
||||
|
||||
Args:
|
||||
adapters: List of adapter names to include.
|
||||
Valid names: html, sx.
|
||||
None = include all server-side adapters.
|
||||
modules: List of primitive module names to include.
|
||||
core.* are always included. stdlib.* are opt-in.
|
||||
None = include all modules (backward compatible).
|
||||
"""
|
||||
# Determine which primitive modules to include
|
||||
prim_modules = None # None = all
|
||||
if modules is not None:
|
||||
prim_modules = [m for m in _ALL_PY_MODULES if m.startswith("core.")]
|
||||
for m in modules:
|
||||
if m not in prim_modules:
|
||||
if m not in PRIMITIVES_PY_MODULES:
|
||||
raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_PY_MODULES)}")
|
||||
prim_modules.append(m)
|
||||
|
||||
ref_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
emitter = PyEmitter()
|
||||
|
||||
@@ -849,7 +865,9 @@ def compile_ref_to_py(adapters: list[str] | None = None) -> str:
|
||||
parts = []
|
||||
parts.append(PREAMBLE)
|
||||
parts.append(PLATFORM_PY)
|
||||
parts.append(PRIMITIVES_PY)
|
||||
parts.append(PRIMITIVES_PY_PRE)
|
||||
parts.append(_assemble_primitives_py(prim_modules))
|
||||
parts.append(PRIMITIVES_PY_POST)
|
||||
|
||||
for label, defines in all_sections:
|
||||
parts.append(f"\n# === Transpiled from {label} ===\n")
|
||||
@@ -1506,29 +1524,14 @@ def aser_special(name, expr, env):
|
||||
return trampoline(result)
|
||||
'''
|
||||
|
||||
PRIMITIVES_PY = '''
|
||||
# =========================================================================
|
||||
# Primitives
|
||||
# =========================================================================
|
||||
# ---------------------------------------------------------------------------
|
||||
# Primitive modules — Python implementations keyed by spec module name.
|
||||
# core.* modules are always included; stdlib.* are opt-in.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Save builtins before shadowing
|
||||
_b_len = len
|
||||
_b_map = map
|
||||
_b_filter = filter
|
||||
_b_range = range
|
||||
_b_list = list
|
||||
_b_dict = dict
|
||||
_b_max = max
|
||||
_b_min = min
|
||||
_b_round = round
|
||||
_b_abs = abs
|
||||
_b_sum = sum
|
||||
_b_zip = zip
|
||||
_b_int = int
|
||||
|
||||
PRIMITIVES = {}
|
||||
|
||||
# Arithmetic
|
||||
PRIMITIVES_PY_MODULES: dict[str, str] = {
|
||||
"core.arithmetic": '''
|
||||
# core.arithmetic
|
||||
PRIMITIVES["+"] = lambda *args: _b_sum(args)
|
||||
PRIMITIVES["-"] = lambda a, b=None: -a if b is None else a - b
|
||||
PRIMITIVES["*"] = lambda *args: _sx_mul(*args)
|
||||
@@ -1551,37 +1554,25 @@ def _sx_mul(*args):
|
||||
for a in args:
|
||||
r *= a
|
||||
return r
|
||||
''',
|
||||
|
||||
# Comparison
|
||||
"core.comparison": '''
|
||||
# core.comparison
|
||||
PRIMITIVES["="] = lambda a, b: a == b
|
||||
PRIMITIVES["!="] = lambda a, b: a != b
|
||||
PRIMITIVES["<"] = lambda a, b: a < b
|
||||
PRIMITIVES[">"] = lambda a, b: a > b
|
||||
PRIMITIVES["<="] = lambda a, b: a <= b
|
||||
PRIMITIVES[">="] = lambda a, b: a >= b
|
||||
''',
|
||||
|
||||
# Logic
|
||||
"core.logic": '''
|
||||
# core.logic
|
||||
PRIMITIVES["not"] = lambda x: not sx_truthy(x)
|
||||
''',
|
||||
|
||||
# String
|
||||
PRIMITIVES["str"] = sx_str
|
||||
PRIMITIVES["upper"] = lambda s: str(s).upper()
|
||||
PRIMITIVES["lower"] = lambda s: str(s).lower()
|
||||
PRIMITIVES["trim"] = lambda s: str(s).strip()
|
||||
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
|
||||
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
||||
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
|
||||
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
|
||||
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
|
||||
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
|
||||
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
|
||||
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
|
||||
|
||||
import re as _re
|
||||
def _strip_tags(s):
|
||||
return _re.sub(r"<[^>]+>", "", s)
|
||||
|
||||
# Predicates
|
||||
"core.predicates": '''
|
||||
# core.predicates
|
||||
PRIMITIVES["nil?"] = lambda x: x is None or x is NIL
|
||||
PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)
|
||||
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
|
||||
@@ -1598,8 +1589,25 @@ PRIMITIVES["contains?"] = lambda c, k: (
|
||||
PRIMITIVES["odd?"] = lambda n: n % 2 != 0
|
||||
PRIMITIVES["even?"] = lambda n: n % 2 == 0
|
||||
PRIMITIVES["zero?"] = lambda n: n == 0
|
||||
''',
|
||||
|
||||
# Collections
|
||||
"core.strings": '''
|
||||
# core.strings
|
||||
PRIMITIVES["str"] = sx_str
|
||||
PRIMITIVES["upper"] = lambda s: str(s).upper()
|
||||
PRIMITIVES["lower"] = lambda s: str(s).lower()
|
||||
PRIMITIVES["trim"] = lambda s: str(s).strip()
|
||||
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
|
||||
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
|
||||
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
|
||||
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
|
||||
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
|
||||
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
|
||||
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
|
||||
''',
|
||||
|
||||
"core.collections": '''
|
||||
# core.collections
|
||||
PRIMITIVES["list"] = lambda *args: _b_list(args)
|
||||
PRIMITIVES["dict"] = lambda *args: {args[i]: args[i+1] for i in _b_range(0, _b_len(args)-1, 2)}
|
||||
PRIMITIVES["range"] = lambda a, b, step=1: _b_list(_b_range(_b_int(a), _b_int(b), _b_int(step)))
|
||||
@@ -1611,22 +1619,20 @@ PRIMITIVES["rest"] = lambda c: c[1:] if c else []
|
||||
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
|
||||
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
|
||||
PRIMITIVES["append"] = lambda c, x: (c or []) + [x]
|
||||
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
|
||||
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
|
||||
''',
|
||||
|
||||
"core.dict": '''
|
||||
# core.dict
|
||||
PRIMITIVES["keys"] = lambda d: _b_list((d or {}).keys())
|
||||
PRIMITIVES["vals"] = lambda d: _b_list((d or {}).values())
|
||||
PRIMITIVES["merge"] = lambda *args: _sx_merge_dicts(*args)
|
||||
PRIMITIVES["assoc"] = lambda d, *kvs: _sx_assoc(d, *kvs)
|
||||
PRIMITIVES["dissoc"] = lambda d, *ks: {k: v for k, v in d.items() if k not in ks}
|
||||
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
|
||||
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
|
||||
PRIMITIVES["into"] = lambda target, coll: (_b_list(coll) if isinstance(target, _b_list) else {p[0]: p[1] for p in coll if isinstance(p, _b_list) and _b_len(p) >= 2})
|
||||
PRIMITIVES["zip"] = lambda *colls: [_b_list(t) for t in _b_zip(*colls)]
|
||||
|
||||
# Format
|
||||
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
|
||||
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
|
||||
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
|
||||
PRIMITIVES["escape"] = escape_html
|
||||
|
||||
def _sx_merge_dicts(*args):
|
||||
out = {}
|
||||
for d in args:
|
||||
@@ -1639,13 +1645,80 @@ def _sx_assoc(d, *kvs):
|
||||
for i in _b_range(0, _b_len(kvs) - 1, 2):
|
||||
out[kvs[i]] = kvs[i + 1]
|
||||
return out
|
||||
''',
|
||||
|
||||
"stdlib.format": '''
|
||||
# stdlib.format
|
||||
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
|
||||
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
|
||||
PRIMITIVES["parse-datetime"] = lambda s: str(s) if s else NIL
|
||||
|
||||
def _sx_parse_int(v, default=0):
|
||||
try:
|
||||
return _b_int(v)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
''',
|
||||
|
||||
"stdlib.text": '''
|
||||
# stdlib.text
|
||||
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
|
||||
PRIMITIVES["escape"] = escape_html
|
||||
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
|
||||
|
||||
import re as _re
|
||||
def _strip_tags(s):
|
||||
return _re.sub(r"<[^>]+>", "", s)
|
||||
''',
|
||||
|
||||
"stdlib.style": '''
|
||||
# stdlib.style — stubs (CSSX needs full runtime)
|
||||
''',
|
||||
|
||||
"stdlib.debug": '''
|
||||
# stdlib.debug
|
||||
PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).throw(RuntimeError(f"Assertion error: {msg}")) if not sx_truthy(cond) else True
|
||||
''',
|
||||
}
|
||||
|
||||
_ALL_PY_MODULES = list(PRIMITIVES_PY_MODULES.keys())
|
||||
|
||||
|
||||
def _assemble_primitives_py(modules: list[str] | None = None) -> str:
|
||||
"""Assemble Python primitive code from selected modules."""
|
||||
if modules is None:
|
||||
modules = _ALL_PY_MODULES
|
||||
parts = []
|
||||
for mod in modules:
|
||||
if mod in PRIMITIVES_PY_MODULES:
|
||||
parts.append(PRIMITIVES_PY_MODULES[mod])
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
PRIMITIVES_PY_PRE = '''
|
||||
# =========================================================================
|
||||
# Primitives
|
||||
# =========================================================================
|
||||
|
||||
# Save builtins before shadowing
|
||||
_b_len = len
|
||||
_b_map = map
|
||||
_b_filter = filter
|
||||
_b_range = range
|
||||
_b_list = list
|
||||
_b_dict = dict
|
||||
_b_max = max
|
||||
_b_min = min
|
||||
_b_round = round
|
||||
_b_abs = abs
|
||||
_b_sum = sum
|
||||
_b_zip = zip
|
||||
_b_int = int
|
||||
|
||||
PRIMITIVES = {}
|
||||
'''
|
||||
|
||||
PRIMITIVES_PY_POST = '''
|
||||
def is_primitive(name):
|
||||
if name in PRIMITIVES:
|
||||
return True
|
||||
@@ -1811,9 +1884,15 @@ def main():
|
||||
default=None,
|
||||
help="Comma-separated adapter names (html,sx). Default: all server-side.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--modules",
|
||||
default=None,
|
||||
help="Comma-separated primitive modules (core.* always included). Default: all.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
adapters = args.adapters.split(",") if args.adapters else None
|
||||
print(compile_ref_to_py(adapters))
|
||||
modules = args.modules.split(",") if args.modules else None
|
||||
print(compile_ref_to_py(adapters, modules))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user