Files
rose-ash/shared/sx/ref/bootstrap_py.py
giles f77d7350dd 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>
2026-03-06 01:45:29 +00:00

1900 lines
63 KiB
Python

#!/usr/bin/env python3
"""
Bootstrap compiler: reference SX evaluator -> Python.
Reads the .sx reference specification and emits a standalone Python
evaluator module (sx_ref.py) that can be compared against the hand-written
evaluator.py / html.py / async_eval.py.
The compiler translates the restricted SX subset used in eval.sx/render.sx
into idiomatic Python. Platform interface functions are emitted as
native Python implementations.
Usage:
python bootstrap_py.py > sx_ref.py
"""
from __future__ import annotations
import os
import sys
# Add project root to path for imports
_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
# ---------------------------------------------------------------------------
# SX -> Python transpiler
# ---------------------------------------------------------------------------
class PyEmitter:
"""Transpile an SX AST node to Python source code."""
def __init__(self):
self.indent = 0
def emit(self, expr) -> str:
"""Emit a Python expression from an SX AST node."""
# Bool MUST be checked before int (bool is subclass of int in Python)
if isinstance(expr, bool):
return "True" if expr else "False"
if isinstance(expr, (int, float)):
return str(expr)
if isinstance(expr, str):
return self._py_string(expr)
if expr is None or expr is SX_NIL:
return "NIL"
if isinstance(expr, Symbol):
return self._emit_symbol(expr.name)
if isinstance(expr, Keyword):
return self._py_string(expr.name)
if isinstance(expr, list):
return self._emit_list(expr)
return str(expr)
def emit_statement(self, expr, indent: int = 0) -> str:
"""Emit a Python statement from an SX AST node."""
pad = " " * indent
if isinstance(expr, list) and expr:
head = expr[0]
if isinstance(head, Symbol):
name = head.name
if name == "define":
return self._emit_define(expr, indent)
if name == "set!":
return f"{pad}{self._mangle(expr[1].name)} = {self.emit(expr[2])}"
if name == "when":
return self._emit_when_stmt(expr, indent)
if name == "do" or name == "begin":
return "\n".join(self.emit_statement(e, indent) for e in expr[1:])
if name == "for-each":
return self._emit_for_each_stmt(expr, indent)
if name == "dict-set!":
return f"{pad}{self.emit(expr[1])}[{self.emit(expr[2])}] = {self.emit(expr[3])}"
if name == "append!":
return f"{pad}{self.emit(expr[1])}.append({self.emit(expr[2])})"
if name == "env-set!":
return f"{pad}{self.emit(expr[1])}[{self.emit(expr[2])}] = {self.emit(expr[3])}"
if name == "set-lambda-name!":
return f"{pad}{self.emit(expr[1])}.name = {self.emit(expr[2])}"
return f"{pad}{self.emit(expr)}"
# --- Symbol emission ---
def _emit_symbol(self, name: str) -> str:
mangled = self._mangle(name)
cell_vars = getattr(self, '_current_cell_vars', set())
if mangled in cell_vars:
return f"_cells[{self._py_string(mangled)}]"
return mangled
def _mangle(self, name: str) -> str:
"""Convert SX identifier to valid Python identifier."""
RENAMES = {
"nil": "NIL",
"true": "True",
"false": "False",
"nil?": "is_nil",
"type-of": "type_of",
"symbol-name": "symbol_name",
"keyword-name": "keyword_name",
"make-lambda": "make_lambda",
"make-component": "make_component",
"make-macro": "make_macro",
"make-thunk": "make_thunk",
"make-handler-def": "make_handler_def",
"make-query-def": "make_query_def",
"make-action-def": "make_action_def",
"make-page-def": "make_page_def",
"make-symbol": "make_symbol",
"make-keyword": "make_keyword",
"lambda-params": "lambda_params",
"lambda-body": "lambda_body",
"lambda-closure": "lambda_closure",
"lambda-name": "lambda_name",
"set-lambda-name!": "set_lambda_name",
"component-params": "component_params",
"component-body": "component_body",
"component-closure": "component_closure",
"component-has-children?": "component_has_children",
"component-name": "component_name",
"macro-params": "macro_params",
"macro-rest-param": "macro_rest_param",
"macro-body": "macro_body",
"macro-closure": "macro_closure",
"thunk?": "is_thunk",
"thunk-expr": "thunk_expr",
"thunk-env": "thunk_env",
"callable?": "is_callable",
"lambda?": "is_lambda",
"component?": "is_component",
"macro?": "is_macro",
"primitive?": "is_primitive",
"get-primitive": "get_primitive",
"env-has?": "env_has",
"env-get": "env_get",
"env-set!": "env_set",
"env-extend": "env_extend",
"env-merge": "env_merge",
"dict-set!": "dict_set",
"dict-get": "dict_get",
"dict-has?": "dict_has",
"dict-delete!": "dict_delete",
"eval-expr": "eval_expr",
"eval-list": "eval_list",
"eval-call": "eval_call",
"is-render-expr?": "is_render_expr",
"render-expr": "render_expr",
"call-lambda": "call_lambda",
"call-component": "call_component",
"parse-keyword-args": "parse_keyword_args",
"parse-comp-params": "parse_comp_params",
"parse-macro-params": "parse_macro_params",
"expand-macro": "expand_macro",
"render-to-html": "render_to_html",
"render-to-sx": "render_to_sx",
"render-value-to-html": "render_value_to_html",
"render-list-to-html": "render_list_to_html",
"render-html-element": "render_html_element",
"render-html-component": "render_html_component",
"parse-element-args": "parse_element_args",
"render-attrs": "render_attrs",
"aser-list": "aser_list",
"aser-fragment": "aser_fragment",
"aser-call": "aser_call",
"aser-special": "aser_special",
"sf-if": "sf_if",
"sf-when": "sf_when",
"sf-cond": "sf_cond",
"sf-cond-scheme": "sf_cond_scheme",
"sf-cond-clojure": "sf_cond_clojure",
"sf-case": "sf_case",
"sf-case-loop": "sf_case_loop",
"sf-and": "sf_and",
"sf-or": "sf_or",
"sf-let": "sf_let",
"sf-lambda": "sf_lambda",
"sf-define": "sf_define",
"sf-defcomp": "sf_defcomp",
"sf-defmacro": "sf_defmacro",
"sf-begin": "sf_begin",
"sf-quote": "sf_quote",
"sf-quasiquote": "sf_quasiquote",
"sf-thread-first": "sf_thread_first",
"sf-set!": "sf_set_bang",
"qq-expand": "qq_expand",
"ho-map": "ho_map",
"ho-map-indexed": "ho_map_indexed",
"ho-filter": "ho_filter",
"ho-reduce": "ho_reduce",
"ho-some": "ho_some",
"ho-every": "ho_every",
"ho-for-each": "ho_for_each",
"sf-defstyle": "sf_defstyle",
"sf-defkeyframes": "sf_defkeyframes",
"build-keyframes": "build_keyframes",
"style-value?": "is_style_value",
"style-value-class": "style_value_class",
"kf-name": "kf_name",
"special-form?": "is_special_form",
"ho-form?": "is_ho_form",
"strip-prefix": "strip_prefix",
"escape-html": "escape_html",
"escape-attr": "escape_attr",
"escape-string": "escape_string",
"raw-html-content": "raw_html_content",
"HTML_TAGS": "HTML_TAGS",
"VOID_ELEMENTS": "VOID_ELEMENTS",
"BOOLEAN_ATTRS": "BOOLEAN_ATTRS",
# render.sx core
"definition-form?": "is_definition_form",
# adapter-html.sx
"RENDER_HTML_FORMS": "RENDER_HTML_FORMS",
"render-html-form?": "is_render_html_form",
"dispatch-html-form": "dispatch_html_form",
"render-lambda-html": "render_lambda_html",
"make-raw-html": "make_raw_html",
# adapter-sx.sx
"render-to-sx": "render_to_sx",
"aser": "aser",
# Primitives that need exact aliases
"contains?": "contains_p",
"starts-with?": "starts_with_p",
"ends-with?": "ends_with_p",
"empty?": "empty_p",
"every?": "every_p",
"for-each": "for_each",
"for-each-indexed": "for_each_indexed",
"map-indexed": "map_indexed",
"map-dict": "map_dict",
"eval-cond": "eval_cond",
"process-bindings": "process_bindings",
}
if name in RENAMES:
return RENAMES[name]
# General mangling
result = name
# Handle trailing ? and !
if result.endswith("?"):
result = result[:-1] + "_p"
elif result.endswith("!"):
result = result[:-1] + "_b"
# Kebab to snake_case
result = result.replace("-", "_")
# Avoid Python keyword conflicts
if result in ("list", "dict", "range", "filter"):
result = result # keep as-is, these are our SX aliases
return result
# --- List emission ---
def _emit_list(self, expr: list) -> str:
if not expr:
return "[]"
head = expr[0]
if not isinstance(head, Symbol):
# Data list
return "[" + ", ".join(self.emit(x) for x in expr) + "]"
name = head.name
handler = getattr(
self,
f"_sf_{name.replace('-', '_').replace('!', '_b').replace('?', '_p')}",
None,
)
if handler:
return handler(expr)
# Built-in forms
if name in ("fn", "lambda"):
return self._emit_fn(expr)
if name in ("let", "let*"):
return self._emit_let(expr)
if name == "if":
return self._emit_if(expr)
if name == "when":
return self._emit_when(expr)
if name == "cond":
return self._emit_cond(expr)
if name == "case":
return self._emit_case(expr)
if name == "and":
return self._emit_and(expr)
if name == "or":
return self._emit_or(expr)
if name == "not":
return f"(not sx_truthy({self.emit(expr[1])}))"
if name in ("do", "begin"):
return self._emit_do(expr)
if name == "list":
return "[" + ", ".join(self.emit(x) for x in expr[1:]) + "]"
if name == "dict":
return self._emit_dict_literal(expr)
if name == "quote":
return self._emit_quote(expr[1])
if name == "set!":
# set! in expression context — use nonlocal_cells dict for mutation
# from nested lambdas (Python closures can read but not rebind outer vars)
varname = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
py_var = self._mangle(varname)
return f"_sx_cell_set(_cells, {self._py_string(py_var)}, {self.emit(expr[2])})"
if name == "str":
parts = [self.emit(x) for x in expr[1:]]
return "sx_str(" + ", ".join(parts) + ")"
# Mutation forms that can appear in expression context
if name == "append!":
return f"_sx_append({self.emit(expr[1])}, {self.emit(expr[2])})"
if name == "dict-set!":
return f"_sx_dict_set({self.emit(expr[1])}, {self.emit(expr[2])}, {self.emit(expr[3])})"
if name == "env-set!":
return f"_sx_dict_set({self.emit(expr[1])}, {self.emit(expr[2])}, {self.emit(expr[3])})"
if name == "set-lambda-name!":
return f"_sx_set_attr({self.emit(expr[1])}, 'name', {self.emit(expr[2])})"
# Infix operators
if name in ("+", "-", "*", "/", "=", "!=", "<", ">", "<=", ">=", "mod"):
return self._emit_infix(name, expr[1:])
if name == "inc":
return f"({self.emit(expr[1])} + 1)"
if name == "dec":
return f"({self.emit(expr[1])} - 1)"
# Regular function call
fn_name = self._mangle(name)
args = ", ".join(self.emit(x) for x in expr[1:])
return f"{fn_name}({args})"
# --- Special form emitters ---
def _emit_fn(self, expr) -> str:
params = expr[1]
body = expr[2:]
param_names = []
for p in params:
if isinstance(p, Symbol):
param_names.append(self._mangle(p.name))
else:
param_names.append(str(p))
params_str = ", ".join(param_names)
if len(body) == 1:
body_py = self.emit(body[0])
return f"lambda {params_str}: {body_py}"
# Multi-expression body: need a local function
lines = []
lines.append(f"_sx_fn(lambda {params_str}: (")
for b in body[:-1]:
lines.append(f" {self.emit(b)},")
lines.append(f" {self.emit(body[-1])}")
lines.append(")[-1])")
return "\n".join(lines)
def _emit_let(self, expr) -> str:
bindings = expr[1]
body = expr[2:]
assignments = []
if isinstance(bindings, list):
if bindings and isinstance(bindings[0], list):
# Scheme-style: ((name val) ...)
for b in bindings:
vname = b[0].name if isinstance(b[0], Symbol) else str(b[0])
assignments.append((self._mangle(vname), self.emit(b[1])))
else:
# Clojure-style: (name val name val ...)
for i in range(0, len(bindings), 2):
vname = bindings[i].name if isinstance(bindings[i], Symbol) else str(bindings[i])
assignments.append((self._mangle(vname), self.emit(bindings[i + 1])))
# Nested IIFE for sequential let (each binding can see previous ones):
# (lambda a: (lambda b: body)(val_b))(val_a)
body_parts = [self.emit(b) for b in body]
if len(body) == 1:
body_str = body_parts[0]
else:
body_str = f"_sx_begin({', '.join(body_parts)})"
# Build from inside out
result = body_str
for name, val in reversed(assignments):
result = f"(lambda {name}: {result})({val})"
return result
def _emit_if(self, expr) -> str:
cond = self.emit(expr[1])
then = self.emit(expr[2])
els = self.emit(expr[3]) if len(expr) > 3 else "NIL"
return f"({then} if sx_truthy({cond}) else {els})"
def _emit_when(self, expr) -> str:
cond = self.emit(expr[1])
body_parts = expr[2:]
if len(body_parts) == 1:
return f"({self.emit(body_parts[0])} if sx_truthy({cond}) else NIL)"
body = ", ".join(self.emit(b) for b in body_parts)
return f"(_sx_begin({body}) if sx_truthy({cond}) else NIL)"
def _emit_when_stmt(self, expr, indent: int = 0) -> str:
pad = " " * indent
cond = self.emit(expr[1])
body_parts = expr[2:]
lines = [f"{pad}if sx_truthy({cond}):"]
for b in body_parts:
lines.append(self.emit_statement(b, indent + 1))
return "\n".join(lines)
def _emit_cond(self, expr) -> str:
clauses = expr[1:]
if not clauses:
return "NIL"
is_scheme = (
all(isinstance(c, list) and len(c) == 2 for c in clauses)
and not any(isinstance(c, Keyword) for c in clauses)
)
if is_scheme:
return self._cond_scheme(clauses)
return self._cond_clojure(clauses)
def _cond_scheme(self, clauses) -> str:
if not clauses:
return "NIL"
clause = clauses[0]
test = clause[0]
body = clause[1]
if isinstance(test, Symbol) and test.name in ("else", ":else"):
return self.emit(body)
if isinstance(test, Keyword) and test.name == "else":
return self.emit(body)
return f"({self.emit(body)} if sx_truthy({self.emit(test)}) else {self._cond_scheme(clauses[1:])})"
def _cond_clojure(self, clauses) -> str:
if len(clauses) < 2:
return "NIL"
test = clauses[0]
body = clauses[1]
if isinstance(test, Keyword) and test.name == "else":
return self.emit(body)
if isinstance(test, Symbol) and test.name in ("else", ":else"):
return self.emit(body)
return f"({self.emit(body)} if sx_truthy({self.emit(test)}) else {self._cond_clojure(clauses[2:])})"
def _emit_case(self, expr) -> str:
match_expr = self.emit(expr[1])
clauses = expr[2:]
return f"_sx_case({match_expr}, [{self._case_pairs(clauses)}])"
def _case_pairs(self, clauses) -> str:
pairs = []
i = 0
while i < len(clauses) - 1:
test = clauses[i]
body = clauses[i + 1]
if isinstance(test, Keyword) and test.name == "else":
pairs.append(f"(None, lambda: {self.emit(body)})")
elif isinstance(test, Symbol) and test.name in ("else", ":else"):
pairs.append(f"(None, lambda: {self.emit(body)})")
else:
pairs.append(f"({self.emit(test)}, lambda: {self.emit(body)})")
i += 2
return ", ".join(pairs)
def _emit_and(self, expr) -> str:
parts = [self.emit(x) for x in expr[1:]]
if len(parts) == 1:
return parts[0]
# Use Python's native and for short-circuit evaluation.
# Last value returned as-is; prior values tested with sx_truthy.
# (and a b c) -> (a if not sx_truthy(a) else (b if not sx_truthy(b) else c))
result = parts[-1]
for p in reversed(parts[:-1]):
result = f"({p} if not sx_truthy({p}) else {result})"
return result
def _emit_or(self, expr) -> str:
if len(expr) == 2:
return self.emit(expr[1])
parts = [self.emit(x) for x in expr[1:]]
# Use Python's short-circuit pattern:
# (or a b c) -> (a if sx_truthy(a) else (b if sx_truthy(b) else c))
result = parts[-1]
for p in reversed(parts[:-1]):
result = f"({p} if sx_truthy({p}) else {result})"
return result
def _emit_do(self, expr) -> str:
return self._emit_do_inner(expr[1:])
def _emit_do_inner(self, exprs) -> str:
if len(exprs) == 1:
return self.emit(exprs[0])
parts = [self.emit(e) for e in exprs]
return "_sx_begin(" + ", ".join(parts) + ")"
def _emit_dict_literal(self, expr) -> str:
pairs = expr[1:]
parts = []
i = 0
while i < len(pairs) - 1:
key = pairs[i]
val = pairs[i + 1]
if isinstance(key, Keyword):
parts.append(f"{self._py_string(key.name)}: {self.emit(val)}")
else:
parts.append(f"{self.emit(key)}: {self.emit(val)}")
i += 2
return "{" + ", ".join(parts) + "}"
def _emit_infix(self, op: str, args: list) -> str:
PY_OPS = {"=": "==", "!=": "!=", "mod": "%"}
py_op = PY_OPS.get(op, op)
if len(args) == 1 and op == "-":
return f"(-{self.emit(args[0])})"
return f"({self.emit(args[0])} {py_op} {self.emit(args[1])})"
def _emit_define(self, expr, indent: int = 0) -> str:
pad = " " * indent
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
val_expr = expr[2]
# If value is a lambda/fn, check if body uses set! on let-bound vars
# and emit as def for proper mutation support
if (isinstance(val_expr, list) and val_expr and
isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")
and self._body_uses_set(val_expr)):
return self._emit_define_as_def(name, val_expr, indent)
val = self.emit(val_expr)
return f"{pad}{self._mangle(name)} = {val}"
def _body_uses_set(self, fn_expr) -> bool:
"""Check if a fn expression's body (recursively) uses set!."""
def _has_set(node):
if not isinstance(node, list) or not node:
return False
head = node[0]
if isinstance(head, Symbol) and head.name == "set!":
return True
return any(_has_set(child) for child in node if isinstance(child, list))
body = fn_expr[2:]
return any(_has_set(b) for b in body)
def _emit_define_as_def(self, name: str, fn_expr, indent: int = 0) -> str:
"""Emit a define with fn value as a proper def statement.
This is used for functions that contain set! — Python closures can't
rebind outer lambda params, so we need proper def + local variables.
Variables mutated by set! from nested lambdas use a _cells dict.
"""
pad = " " * indent
params = fn_expr[1]
body = fn_expr[2:]
param_names = []
for p in params:
if isinstance(p, Symbol):
param_names.append(self._mangle(p.name))
else:
param_names.append(str(p))
params_str = ", ".join(param_names)
py_name = self._mangle(name)
# Find set! target variables that are used from nested lambda scopes
nested_set_vars = self._find_nested_set_vars(body)
lines = [f"{pad}def {py_name}({params_str}):"]
if nested_set_vars:
lines.append(f"{pad} _cells = {{}}")
# Emit body with cell var tracking
old_cells = getattr(self, '_current_cell_vars', set())
self._current_cell_vars = nested_set_vars
self._emit_body_stmts(body, lines, indent + 1)
self._current_cell_vars = old_cells
return "\n".join(lines)
def _find_nested_set_vars(self, body) -> set[str]:
"""Find variable names that are set! from within nested fn/lambda bodies."""
result = set()
def _scan(node, in_nested_fn=False):
if not isinstance(node, list) or not node:
return
head = node[0]
if isinstance(head, Symbol):
if head.name in ("fn", "lambda") and in_nested_fn:
# Already nested, keep scanning
for child in node[2:]:
_scan(child, True)
return
if head.name in ("fn", "lambda"):
# Entering nested fn
for child in node[2:]:
_scan(child, True)
return
if head.name == "set!" and in_nested_fn:
var = node[1].name if isinstance(node[1], Symbol) else str(node[1])
result.add(self._mangle(var))
for child in node:
if isinstance(child, list):
_scan(child, in_nested_fn)
for b in body:
_scan(b)
return result
def _emit_body_stmts(self, body: list, lines: list, indent: int) -> None:
"""Emit body expressions as statements into lines list.
Handles let as local variable declarations, and returns the last
expression.
"""
pad = " " * indent
for i, expr in enumerate(body):
is_last = (i == len(body) - 1)
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
name = expr[0].name
if name in ("let", "let*"):
self._emit_let_as_stmts(expr, lines, indent, is_last)
continue
if name in ("do", "begin"):
sub_body = expr[1:]
if is_last:
self._emit_body_stmts(sub_body, lines, indent)
else:
for sub in sub_body:
lines.append(self.emit_statement(sub, indent))
continue
if is_last:
lines.append(f"{pad}return {self.emit(expr)}")
else:
lines.append(self.emit_statement(expr, indent))
def _emit_let_as_stmts(self, expr, lines: list, indent: int, is_last: bool) -> None:
"""Emit a let expression as local variable declarations."""
pad = " " * indent
bindings = expr[1]
body = expr[2:]
cell_vars = getattr(self, '_current_cell_vars', set())
if isinstance(bindings, list):
if bindings and isinstance(bindings[0], list):
# Scheme-style: ((name val) ...)
for b in bindings:
vname = b[0].name if isinstance(b[0], Symbol) else str(b[0])
mangled = self._mangle(vname)
if mangled in cell_vars:
lines.append(f"{pad}_cells[{self._py_string(mangled)}] = {self.emit(b[1])}")
else:
lines.append(f"{pad}{mangled} = {self.emit(b[1])}")
else:
# Clojure-style: (name val name val ...)
for j in range(0, len(bindings), 2):
vname = bindings[j].name if isinstance(bindings[j], Symbol) else str(bindings[j])
mangled = self._mangle(vname)
if mangled in cell_vars:
lines.append(f"{pad}_cells[{self._py_string(mangled)}] = {self.emit(bindings[j + 1])}")
else:
lines.append(f"{pad}{mangled} = {self.emit(bindings[j + 1])}")
if is_last:
self._emit_body_stmts(body, lines, indent)
else:
for b in body:
self._emit_stmt_recursive(b, lines, indent)
def _emit_for_each_stmt(self, expr, indent: int = 0) -> str:
pad = " " * indent
fn_expr = expr[1]
coll_expr = expr[2]
coll = self.emit(coll_expr)
# If fn is an inline lambda, emit a for loop
if isinstance(fn_expr, list) and isinstance(fn_expr[0], Symbol) and fn_expr[0].name == "fn":
params = fn_expr[1]
body = fn_expr[2:]
p = params[0].name if isinstance(params[0], Symbol) else str(params[0])
p_py = self._mangle(p)
lines = [f"{pad}for {p_py} in {coll}:"]
# Emit body as statements with proper let/set! handling
self._emit_loop_body(body, lines, indent + 1)
return "\n".join(lines)
fn = self.emit(fn_expr)
return f"{pad}for _item in {coll}:\n{pad} {fn}(_item)"
def _emit_loop_body(self, body: list, lines: list, indent: int) -> None:
"""Emit loop body as statements. Handles let, when, set!, cond properly."""
pad = " " * indent
for expr in body:
self._emit_stmt_recursive(expr, lines, indent)
def _emit_stmt_recursive(self, expr, lines: list, indent: int) -> None:
"""Emit an expression as statement(s), recursing into control flow."""
pad = " " * indent
if not isinstance(expr, list) or not expr:
lines.append(self.emit_statement(expr, indent))
return
head = expr[0]
if not isinstance(head, Symbol):
lines.append(self.emit_statement(expr, indent))
return
name = head.name
if name == "set!":
varname = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
mangled = self._mangle(varname)
cell_vars = getattr(self, '_current_cell_vars', set())
if mangled in cell_vars:
lines.append(f"{pad}_cells[{self._py_string(mangled)}] = {self.emit(expr[2])}")
else:
lines.append(f"{pad}{mangled} = {self.emit(expr[2])}")
elif name in ("let", "let*"):
self._emit_let_as_stmts(expr, lines, indent, False)
elif name == "when":
cond = self.emit(expr[1])
lines.append(f"{pad}if sx_truthy({cond}):")
for b in expr[2:]:
self._emit_stmt_recursive(b, lines, indent + 1)
elif name == "cond":
self._emit_cond_stmt(expr, lines, indent)
elif name in ("do", "begin"):
for b in expr[1:]:
self._emit_stmt_recursive(b, lines, indent)
elif name == "if":
cond = self.emit(expr[1])
lines.append(f"{pad}if sx_truthy({cond}):")
self._emit_stmt_recursive(expr[2], lines, indent + 1)
if len(expr) > 3:
lines.append(f"{pad}else:")
self._emit_stmt_recursive(expr[3], lines, indent + 1)
elif name == "append!":
lines.append(f"{pad}{self.emit(expr[1])}.append({self.emit(expr[2])})")
elif name == "dict-set!":
lines.append(f"{pad}{self.emit(expr[1])}[{self.emit(expr[2])}] = {self.emit(expr[3])}")
elif name == "env-set!":
lines.append(f"{pad}{self.emit(expr[1])}[{self.emit(expr[2])}] = {self.emit(expr[3])}")
else:
lines.append(self.emit_statement(expr, indent))
def _emit_cond_stmt(self, expr, lines: list, indent: int) -> None:
"""Emit cond as if/elif/else chain."""
pad = " " * indent
clauses = expr[1:]
# Detect scheme vs clojure style
is_scheme = (
all(isinstance(c, list) and len(c) == 2 for c in clauses)
and not any(isinstance(c, Keyword) for c in clauses)
)
first_clause = True
if is_scheme:
for clause in clauses:
test, body = clause[0], clause[1]
if isinstance(test, Symbol) and test.name in ("else", ":else"):
lines.append(f"{pad}else:")
elif isinstance(test, Keyword) and test.name == "else":
lines.append(f"{pad}else:")
else:
kw = "if" if first_clause else "elif"
lines.append(f"{pad}{kw} sx_truthy({self.emit(test)}):")
first_clause = False
self._emit_stmt_recursive(body, lines, indent + 1)
else:
i = 0
while i < len(clauses) - 1:
test, body = clauses[i], clauses[i + 1]
if isinstance(test, Keyword) and test.name == "else":
lines.append(f"{pad}else:")
elif isinstance(test, Symbol) and test.name in ("else", ":else"):
lines.append(f"{pad}else:")
else:
kw = "if" if first_clause else "elif"
lines.append(f"{pad}{kw} sx_truthy({self.emit(test)}):")
first_clause = False
self._emit_stmt_recursive(body, lines, indent + 1)
i += 2
def _emit_quote(self, expr) -> str:
"""Emit a quoted expression as a Python literal AST."""
if isinstance(expr, bool):
return "True" if expr else "False"
if isinstance(expr, (int, float)):
return str(expr)
if isinstance(expr, str):
return self._py_string(expr)
if expr is None or expr is SX_NIL:
return "NIL"
if isinstance(expr, Symbol):
return f"Symbol({self._py_string(expr.name)})"
if isinstance(expr, Keyword):
return f"Keyword({self._py_string(expr.name)})"
if isinstance(expr, list):
return "[" + ", ".join(self._emit_quote(x) for x in expr) + "]"
return str(expr)
def _py_string(self, s: str) -> str:
return repr(s)
# ---------------------------------------------------------------------------
# Bootstrap compiler
# ---------------------------------------------------------------------------
def extract_defines(source: str) -> list[tuple[str, list]]:
"""Parse .sx source, return list of (name, define-expr) for top-level defines."""
exprs = parse_all(source)
defines = []
for expr in exprs:
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
if expr[0].name == "define":
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
defines.append((name, expr))
return defines
ADAPTER_FILES = {
"html": ("adapter-html.sx", "adapter-html"),
"sx": ("adapter-sx.sx", "adapter-sx"),
}
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()
# Resolve adapter set
if adapters is None:
adapter_set = set(ADAPTER_FILES.keys())
else:
adapter_set = set()
for a in adapters:
if a not in ADAPTER_FILES:
raise ValueError(f"Unknown adapter: {a!r}. Valid: {', '.join(ADAPTER_FILES)}")
adapter_set.add(a)
# Core files always included, then selected adapters
sx_files = [
("eval.sx", "eval"),
("forms.sx", "forms (server definition forms)"),
("render.sx", "render (core)"),
]
for name in ("html", "sx"):
if name in adapter_set:
sx_files.append(ADAPTER_FILES[name])
all_sections = []
for filename, label in sx_files:
filepath = os.path.join(ref_dir, filename)
if not os.path.exists(filepath):
continue
with open(filepath) as f:
src = f.read()
defines = extract_defines(src)
all_sections.append((label, defines))
# Build output
has_html = "html" in adapter_set
has_sx = "sx" in adapter_set
parts = []
parts.append(PREAMBLE)
parts.append(PLATFORM_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")
for name, expr in defines:
parts.append(f"# {name}")
parts.append(emitter.emit_statement(expr))
parts.append("")
parts.append(FIXUPS_PY)
parts.append(public_api_py(has_html, has_sx))
return "\n".join(parts)
# ---------------------------------------------------------------------------
# Static Python sections
# ---------------------------------------------------------------------------
PREAMBLE = '''\
"""
sx_ref.py -- Generated from reference SX evaluator specification.
Bootstrap-compiled from shared/sx/ref/{eval,render,adapter-html,adapter-sx}.sx
Compare against hand-written evaluator.py / html.py for correctness verification.
DO NOT EDIT -- regenerate with: python bootstrap_py.py
"""
from __future__ import annotations
import math
from typing import Any
# =========================================================================
# Types (reuse existing types)
# =========================================================================
from shared.sx.types import (
NIL, Symbol, Keyword, Lambda, Component, Macro, StyleValue,
HandlerDef, QueryDef, ActionDef, PageDef,
)
from shared.sx.parser import SxExpr
'''
PLATFORM_PY = '''
# =========================================================================
# Platform interface -- Python implementation
# =========================================================================
class _Thunk:
"""Deferred evaluation for TCO."""
__slots__ = ("expr", "env")
def __init__(self, expr, env):
self.expr = expr
self.env = env
class _RawHTML:
"""Marker for pre-rendered HTML that should not be escaped."""
__slots__ = ("html",)
def __init__(self, html: str):
self.html = html
def sx_truthy(x):
"""SX truthiness: everything is truthy except False, None, and NIL."""
if x is False:
return False
if x is None or x is NIL:
return False
return True
def sx_str(*args):
"""SX str: concatenate string representations, skipping nil."""
parts = []
for a in args:
if a is None or a is NIL:
continue
parts.append(str(a))
return "".join(parts)
def sx_and(*args):
"""SX and: return last truthy value or first falsy."""
result = True
for a in args:
if not sx_truthy(a):
return a
result = a
return result
def sx_or(*args):
"""SX or: return first truthy value or last value."""
for a in args:
if sx_truthy(a):
return a
return args[-1] if args else False
def _sx_begin(*args):
"""Evaluate all args (for side effects), return last."""
return args[-1] if args else NIL
def _sx_case(match_val, pairs):
"""Case dispatch: pairs is [(test_val, body_fn), ...]. None test = else."""
for test, body_fn in pairs:
if test is None: # :else clause
return body_fn()
if match_val == test:
return body_fn()
return NIL
def _sx_fn(f):
"""Identity wrapper for multi-expression lambda bodies."""
return f
def type_of(x):
if x is None or x is NIL:
return "nil"
if isinstance(x, bool):
return "boolean"
if isinstance(x, (int, float)):
return "number"
if isinstance(x, SxExpr):
return "sx-expr"
if isinstance(x, str):
return "string"
if isinstance(x, Symbol):
return "symbol"
if isinstance(x, Keyword):
return "keyword"
if isinstance(x, _Thunk):
return "thunk"
if isinstance(x, Lambda):
return "lambda"
if isinstance(x, Component):
return "component"
if isinstance(x, Macro):
return "macro"
if isinstance(x, _RawHTML):
return "raw-html"
if isinstance(x, StyleValue):
return "style-value"
if isinstance(x, list):
return "list"
if isinstance(x, dict):
return "dict"
return "unknown"
def symbol_name(s):
return s.name
def keyword_name(k):
return k.name
def make_symbol(n):
return Symbol(n)
def make_keyword(n):
return Keyword(n)
def make_lambda(params, body, env):
return Lambda(params=list(params), body=body, closure=dict(env))
def make_component(name, params, has_children, body, env):
return Component(name=name, params=list(params), has_children=has_children,
body=body, closure=dict(env))
def make_macro(params, rest_param, body, env, name=None):
return Macro(params=list(params), rest_param=rest_param, body=body,
closure=dict(env), name=name)
def make_handler_def(name, params, body, env):
return HandlerDef(name=name, params=list(params), body=body, closure=dict(env))
def make_query_def(name, params, doc, body, env):
return QueryDef(name=name, params=list(params), doc=doc, body=body, closure=dict(env))
def make_action_def(name, params, doc, body, env):
return ActionDef(name=name, params=list(params), doc=doc, body=body, closure=dict(env))
def make_page_def(name, slots, env):
path = slots.get("path", "")
auth_val = slots.get("auth", "public")
if isinstance(auth_val, Keyword):
auth = auth_val.name
elif isinstance(auth_val, list):
auth = [item.name if isinstance(item, Keyword) else str(item) for item in auth_val]
else:
auth = str(auth_val) if auth_val else "public"
layout = slots.get("layout")
if isinstance(layout, Keyword):
layout = layout.name
cache = None
return PageDef(
name=name, path=path, auth=auth, layout=layout, cache=cache,
data_expr=slots.get("data"), content_expr=slots.get("content"),
filter_expr=slots.get("filter"), aside_expr=slots.get("aside"),
menu_expr=slots.get("menu"), closure=dict(env),
)
def make_thunk(expr, env):
return _Thunk(expr, env)
def lambda_params(f):
return f.params
def lambda_body(f):
return f.body
def lambda_closure(f):
return f.closure
def lambda_name(f):
return f.name
def set_lambda_name(f, n):
f.name = n
def component_params(c):
return c.params
def component_body(c):
return c.body
def component_closure(c):
return c.closure
def component_has_children(c):
return c.has_children
def component_name(c):
return c.name
def macro_params(m):
return m.params
def macro_rest_param(m):
return m.rest_param
def macro_body(m):
return m.body
def macro_closure(m):
return m.closure
def is_thunk(x):
return isinstance(x, _Thunk)
def thunk_expr(t):
return t.expr
def thunk_env(t):
return t.env
def is_callable(x):
return callable(x) or isinstance(x, Lambda)
def is_lambda(x):
return isinstance(x, Lambda)
def is_component(x):
return isinstance(x, Component)
def is_macro(x):
return isinstance(x, Macro)
def is_style_value(x):
return isinstance(x, StyleValue)
def style_value_class(x):
return x.class_name
def env_has(env, name):
return name in env
def env_get(env, name):
return env.get(name, NIL)
def env_set(env, name, val):
env[name] = val
def env_extend(env):
return dict(env)
def env_merge(base, overlay):
result = dict(base)
result.update(overlay)
return result
def dict_set(d, k, v):
d[k] = v
def dict_get(d, k):
v = d.get(k)
return v if v is not None else NIL
def dict_has(d, k):
return k in d
def dict_delete(d, k):
d.pop(k, None)
def is_render_expr(expr):
"""Check if expression is an HTML element, component, or fragment."""
if not isinstance(expr, list) or not expr:
return False
h = expr[0]
if not isinstance(h, Symbol):
return False
n = h.name
return (n == "<>" or n == "raw!" or
n.startswith("~") or n.startswith("html:") or
n in HTML_TAGS or
("-" in n and len(expr) > 1 and isinstance(expr[1], Keyword)))
# Render dispatch -- set by adapter
_render_expr_fn = None
def render_expr(expr, env):
if _render_expr_fn:
return _render_expr_fn(expr, env)
return expr
def strip_prefix(s, prefix):
return s[len(prefix):] if s.startswith(prefix) else s
def error(msg):
raise EvalError(msg)
def inspect(x):
return repr(x)
def escape_html(s):
s = str(s)
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
def escape_attr(s):
return escape_html(s)
def raw_html_content(x):
return x.html
def make_raw_html(s):
return _RawHTML(s)
class EvalError(Exception):
pass
def _sx_append(lst, item):
"""Append item to list, return the item (for expression context)."""
lst.append(item)
return item
def _sx_dict_set(d, k, v):
"""Set key in dict, return the value (for expression context)."""
d[k] = v
return v
def _sx_set_attr(obj, attr, val):
"""Set attribute on object, return the value."""
setattr(obj, attr, val)
return val
def _sx_cell_set(cells, name, val):
"""Set a mutable cell value. Returns the value."""
cells[name] = val
return val
def escape_string(s):
"""Escape a string for SX serialization."""
return (str(s)
.replace("\\\\", "\\\\\\\\")
.replace('"', '\\\\"')
.replace("\\n", "\\\\n")
.replace("\\t", "\\\\t")
.replace("</script", "<\\\\/script"))
def serialize(val):
"""Serialize an SX value to SX source text."""
t = type_of(val)
if t == "sx-expr":
return val.source
if t == "nil":
return "nil"
if t == "boolean":
return "true" if val else "false"
if t == "number":
return str(val)
if t == "string":
return '"' + escape_string(val) + '"'
if t == "symbol":
return symbol_name(val)
if t == "keyword":
return ":" + keyword_name(val)
if t == "raw-html":
escaped = escape_string(raw_html_content(val))
return '(raw! "' + escaped + '")'
if t == "style-value":
return '"' + style_value_class(val) + '"'
if t == "list":
if not val:
return "()"
items = [serialize(x) for x in val]
return "(" + " ".join(items) + ")"
if t == "dict":
items = []
for k, v in val.items():
items.append(":" + str(k))
items.append(serialize(v))
return "{" + " ".join(items) + "}"
if callable(val):
return "nil"
return str(val)
_SPECIAL_FORM_NAMES = frozenset([
"if", "when", "cond", "case", "and", "or",
"let", "let*", "lambda", "fn",
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
"defhandler", "defpage", "defquery", "defaction", "defrelation",
"begin", "do", "quote", "quasiquote",
"->", "set!",
])
_HO_FORM_NAMES = frozenset([
"map", "map-indexed", "filter", "reduce",
"some", "every?", "for-each",
])
def is_special_form(name):
return name in _SPECIAL_FORM_NAMES
def is_ho_form(name):
return name in _HO_FORM_NAMES
def aser_special(name, expr, env):
"""Evaluate a special/HO form in aser mode.
Control flow forms evaluate conditions normally but render branches
through aser (serializing tags/components instead of rendering HTML).
Definition forms evaluate for side effects and return nil.
"""
# Control flow — evaluate conditions, aser branches
args = expr[1:]
if name == "if":
cond_val = trampoline(eval_expr(args[0], env))
if sx_truthy(cond_val):
return aser(args[1], env)
return aser(args[2], env) if _b_len(args) > 2 else NIL
if name == "when":
cond_val = trampoline(eval_expr(args[0], env))
if sx_truthy(cond_val):
result = NIL
for body in args[1:]:
result = aser(body, env)
return result
return NIL
if name == "cond":
clauses = args
if clauses and isinstance(clauses[0], _b_list) and _b_len(clauses[0]) == 2:
for clause in clauses:
test = clause[0]
if isinstance(test, Symbol) and test.name in ("else", ":else"):
return aser(clause[1], env)
if isinstance(test, Keyword) and test.name == "else":
return aser(clause[1], env)
if sx_truthy(trampoline(eval_expr(test, env))):
return aser(clause[1], env)
else:
i = 0
while i < _b_len(clauses) - 1:
test = clauses[i]
result = clauses[i + 1]
if isinstance(test, Keyword) and test.name == "else":
return aser(result, env)
if isinstance(test, Symbol) and test.name in (":else", "else"):
return aser(result, env)
if sx_truthy(trampoline(eval_expr(test, env))):
return aser(result, env)
i += 2
return NIL
if name == "case":
match_val = trampoline(eval_expr(args[0], env))
clauses = args[1:]
i = 0
while i < _b_len(clauses) - 1:
test = clauses[i]
result = clauses[i + 1]
if isinstance(test, Keyword) and test.name == "else":
return aser(result, env)
if isinstance(test, Symbol) and test.name in (":else", "else"):
return aser(result, env)
if match_val == trampoline(eval_expr(test, env)):
return aser(result, env)
i += 2
return NIL
if name in ("let", "let*"):
bindings = args[0]
local = _b_dict(env)
if isinstance(bindings, _b_list):
if bindings and isinstance(bindings[0], _b_list):
for b in bindings:
var = b[0]
vname = var.name if isinstance(var, Symbol) else var
local[vname] = trampoline(eval_expr(b[1], local))
else:
for i in _b_range(0, _b_len(bindings), 2):
var = bindings[i]
vname = var.name if isinstance(var, Symbol) else var
local[vname] = trampoline(eval_expr(bindings[i + 1], local))
result = NIL
for body in args[1:]:
result = aser(body, local)
return result
if name in ("begin", "do"):
result = NIL
for body in args:
result = aser(body, env)
return result
if name == "and":
result = True
for arg in args:
result = trampoline(eval_expr(arg, env))
if not sx_truthy(result):
return result
return result
if name == "or":
result = False
for arg in args:
result = trampoline(eval_expr(arg, env))
if sx_truthy(result):
return result
return result
# HO forms in aser mode — map/for-each render through aser
if name == "map":
fn = trampoline(eval_expr(args[0], env))
coll = trampoline(eval_expr(args[1], env))
results = []
for item in coll:
if isinstance(fn, Lambda):
local = _b_dict(fn.closure)
local.update(env)
local[fn.params[0]] = item
results.append(aser(fn.body, local))
elif callable(fn):
results.append(fn(item))
else:
raise EvalError("map requires callable")
return results
if name == "map-indexed":
fn = trampoline(eval_expr(args[0], env))
coll = trampoline(eval_expr(args[1], env))
results = []
for i, item in enumerate(coll):
if isinstance(fn, Lambda):
local = _b_dict(fn.closure)
local.update(env)
local[fn.params[0]] = i
local[fn.params[1]] = item
results.append(aser(fn.body, local))
elif callable(fn):
results.append(fn(i, item))
else:
raise EvalError("map-indexed requires callable")
return results
if name == "for-each":
fn = trampoline(eval_expr(args[0], env))
coll = trampoline(eval_expr(args[1], env))
results = []
for item in coll:
if isinstance(fn, Lambda):
local = _b_dict(fn.closure)
local.update(env)
local[fn.params[0]] = item
results.append(aser(fn.body, local))
elif callable(fn):
fn(item)
return results if results else NIL
# Definition forms — evaluate for side effects
if name in ("define", "defcomp", "defmacro", "defstyle", "defkeyframes",
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
trampoline(eval_expr(expr, env))
return NIL
# Lambda/fn, quote, quasiquote, set!, -> : evaluate normally
result = eval_expr(expr, env)
return trampoline(result)
'''
# ---------------------------------------------------------------------------
# Primitive modules — Python implementations keyed by spec module name.
# core.* modules are always included; stdlib.* are opt-in.
# ---------------------------------------------------------------------------
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)
PRIMITIVES["/"] = lambda a, b: a / b
PRIMITIVES["mod"] = lambda a, b: a % b
PRIMITIVES["inc"] = lambda n: n + 1
PRIMITIVES["dec"] = lambda n: n - 1
PRIMITIVES["abs"] = _b_abs
PRIMITIVES["floor"] = math.floor
PRIMITIVES["ceil"] = math.ceil
PRIMITIVES["round"] = _b_round
PRIMITIVES["min"] = _b_min
PRIMITIVES["max"] = _b_max
PRIMITIVES["sqrt"] = math.sqrt
PRIMITIVES["pow"] = lambda x, n: x ** n
PRIMITIVES["clamp"] = lambda x, lo, hi: _b_max(lo, _b_min(hi, x))
def _sx_mul(*args):
r = 1
for a in args:
r *= a
return r
''',
"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
''',
"core.logic": '''
# core.logic
PRIMITIVES["not"] = lambda x: not sx_truthy(x)
''',
"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)
PRIMITIVES["list?"] = lambda x: isinstance(x, _b_list)
PRIMITIVES["dict?"] = lambda x: isinstance(x, _b_dict)
PRIMITIVES["empty?"] = lambda c: (
c is None or c is NIL or
(isinstance(c, (_b_list, str, _b_dict)) and _b_len(c) == 0)
)
PRIMITIVES["contains?"] = lambda c, k: (
str(k) in c if isinstance(c, str) else
k in c
)
PRIMITIVES["odd?"] = lambda n: n % 2 != 0
PRIMITIVES["even?"] = lambda n: n % 2 == 0
PRIMITIVES["zero?"] = lambda n: n == 0
''',
"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)))
PRIMITIVES["get"] = lambda c, k, default=NIL: c.get(k, default) if isinstance(c, _b_dict) else (c[k] if isinstance(c, (_b_list, str)) and isinstance(k, _b_int) and 0 <= k < _b_len(c) else default)
PRIMITIVES["len"] = lambda c: _b_len(c) if c is not None and c is not NIL else 0
PRIMITIVES["first"] = lambda c: c[0] if c and _b_len(c) > 0 else NIL
PRIMITIVES["last"] = lambda c: c[-1] if c and _b_len(c) > 0 else NIL
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["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)]
def _sx_merge_dicts(*args):
out = {}
for d in args:
if d and d is not NIL and isinstance(d, _b_dict):
out.update(d)
return out
def _sx_assoc(d, *kvs):
out = _b_dict(d) if d and d is not NIL else {}
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
from shared.sx.primitives import get_primitive as _ext_get
return _ext_get(name) is not None
def get_primitive(name):
p = PRIMITIVES.get(name)
if p is not None:
return p
from shared.sx.primitives import get_primitive as _ext_get
return _ext_get(name)
# Higher-order helpers used by transpiled code
def map(fn, coll):
return [fn(x) for x in coll]
def map_indexed(fn, coll):
return [fn(i, item) for i, item in enumerate(coll)]
def filter(fn, coll):
return [x for x in coll if sx_truthy(fn(x))]
def reduce(fn, init, coll):
acc = init
for item in coll:
acc = fn(acc, item)
return acc
def some(fn, coll):
for item in coll:
r = fn(item)
if sx_truthy(r):
return r
return NIL
def every_p(fn, coll):
for item in coll:
if not sx_truthy(fn(item)):
return False
return True
def for_each(fn, coll):
for item in coll:
fn(item)
return NIL
def for_each_indexed(fn, coll):
for i, item in enumerate(coll):
fn(i, item)
return NIL
def map_dict(fn, d):
return {k: fn(k, v) for k, v in d.items()}
# Aliases used directly by transpiled code
first = PRIMITIVES["first"]
last = PRIMITIVES["last"]
rest = PRIMITIVES["rest"]
nth = PRIMITIVES["nth"]
len = PRIMITIVES["len"]
is_nil = PRIMITIVES["nil?"]
empty_p = PRIMITIVES["empty?"]
contains_p = PRIMITIVES["contains?"]
starts_with_p = PRIMITIVES["starts-with?"]
ends_with_p = PRIMITIVES["ends-with?"]
slice = PRIMITIVES["slice"]
get = PRIMITIVES["get"]
append = PRIMITIVES["append"]
cons = PRIMITIVES["cons"]
keys = PRIMITIVES["keys"]
join = PRIMITIVES["join"]
range = PRIMITIVES["range"]
apply = lambda f, args: f(*args)
assoc = PRIMITIVES["assoc"]
concat = PRIMITIVES["concat"]
'''
FIXUPS_PY = '''
# =========================================================================
# Fixups -- wire up render adapter dispatch
# =========================================================================
def _setup_html_adapter():
global _render_expr_fn
_render_expr_fn = lambda expr, env: render_list_to_html(expr, env)
def _setup_sx_adapter():
global _render_expr_fn
_render_expr_fn = lambda expr, env: aser_list(expr, env)
# Wrap aser_call and aser_fragment to return SxExpr
# so serialize() won't double-quote them
_orig_aser_call = None
_orig_aser_fragment = None
def _wrap_aser_outputs():
global aser_call, aser_fragment, _orig_aser_call, _orig_aser_fragment
_orig_aser_call = aser_call
_orig_aser_fragment = aser_fragment
def _aser_call_wrapped(name, args, env):
result = _orig_aser_call(name, args, env)
return SxExpr(result) if isinstance(result, str) else result
def _aser_fragment_wrapped(children, env):
result = _orig_aser_fragment(children, env)
return SxExpr(result) if isinstance(result, str) else result
aser_call = _aser_call_wrapped
aser_fragment = _aser_fragment_wrapped
'''
def public_api_py(has_html: bool, has_sx: bool) -> str:
lines = [
'',
'# =========================================================================',
'# Public API',
'# =========================================================================',
'',
]
if has_sx:
lines.append('# Wrap aser outputs to return SxExpr')
lines.append('_wrap_aser_outputs()')
lines.append('')
if has_html:
lines.append('# Set HTML as default adapter')
lines.append('_setup_html_adapter()')
lines.append('')
lines.extend([
'def evaluate(expr, env=None):',
' """Evaluate expr in env and return the result."""',
' if env is None:',
' env = {}',
' result = eval_expr(expr, env)',
' while is_thunk(result):',
' result = eval_expr(thunk_expr(result), thunk_env(result))',
' return result',
'',
'',
'def render(expr, env=None):',
' """Render expr to HTML string."""',
' if env is None:',
' env = {}',
' return render_to_html(expr, env)',
'',
'',
'def make_env(**kwargs):',
' """Create an environment dict with initial bindings."""',
' return dict(kwargs)',
])
return '\n'.join(lines)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
import argparse
parser = argparse.ArgumentParser(description="Bootstrap SX spec -> Python")
parser.add_argument(
"--adapters",
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
modules = args.modules.split(",") if args.modules else None
print(compile_ref_to_py(adapters, modules))
if __name__ == "__main__":
main()