From 4534fb9fee6ffd704070f00af1a4bc6f2e4b9a8b Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 5 Mar 2026 19:32:01 +0000 Subject: [PATCH 1/2] Add bootstrap_py.py: transpile SX spec to Python evaluator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors bootstrap_js.py pattern — reads the .sx reference spec files (eval.sx, render.sx, adapter-html.sx) and emits a standalone Python evaluator module (sx_ref.py) that can be compared against the hand-written evaluator.py / html.py. Key transpilation techniques: - Nested IIFE lambdas for let bindings: (lambda a: body)(val) - _sx_case helper for case/type dispatch - Short-circuit and/or via Python ternaries - Functions with set! emitted as def with _cells dict for mutation - for-each with inline fn emitted as Python for loops - Statement-level cond emitted as if/elif/else chains Passes 27/27 comparison tests against hand-written evaluator. Co-Authored-By: Claude Opus 4.6 --- shared/sx/ref/bootstrap_py.py | 1526 +++++++++++++++++++++++++++++++++ shared/sx/ref/sx_ref.py | 824 ++++++++++++++++++ 2 files changed, 2350 insertions(+) create mode 100644 shared/sx/ref/bootstrap_py.py create mode 100644 shared/sx/ref/sx_ref.py diff --git a/shared/sx/ref/bootstrap_py.py b/shared/sx/ref/bootstrap_py.py new file mode 100644 index 0000000..7269407 --- /dev/null +++ b/shared/sx/ref/bootstrap_py.py @@ -0,0 +1,1526 @@ +#!/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-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) -> 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. + """ + 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"), + ("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) + + 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, +) +''' + +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, 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_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("&", "&").replace("<", "<").replace(">", ">").replace('"', """) + + +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 +''' + +PRIMITIVES_PY = ''' +# ========================================================================= +# 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 = {} + +# 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 + +# 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 +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 +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 + +# 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(a, b, 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["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: + 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 + +def _sx_parse_int(v, default=0): + try: + return _b_int(v) + except (ValueError, TypeError): + return default + +def is_primitive(name): + return name in PRIMITIVES + +def get_primitive(name): + return PRIMITIVES.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) +''' + + +def public_api_py(has_html: bool, has_sx: bool) -> str: + lines = [ + '', + '# =========================================================================', + '# Public API', + '# =========================================================================', + '', + ] + 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.", + ) + args = parser.parse_args() + adapters = args.adapters.split(",") if args.adapters else None + print(compile_ref_to_py(adapters)) + + +if __name__ == "__main__": + main() diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py new file mode 100644 index 0000000..4138f0f --- /dev/null +++ b/shared/sx/ref/sx_ref.py @@ -0,0 +1,824 @@ +""" +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, +) + + +# ========================================================================= +# 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, 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_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("&", "&").replace("<", "<").replace(">", ">").replace('"', """) + + +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 + + +# ========================================================================= +# 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 = {} + +# 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 + +# 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 +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 +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 + +# 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(a, b, 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["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: + 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 + +def _sx_parse_int(v, default=0): + try: + return _b_int(v) + except (ValueError, TypeError): + return default + +def is_primitive(name): + return name in PRIMITIVES + +def get_primitive(name): + return PRIMITIVES.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"] + + +# === Transpiled from eval === + +# trampoline +trampoline = lambda val: (lambda result: (trampoline(eval_expr(thunk_expr(result), thunk_env(result))) if sx_truthy(is_thunk(result)) else result))(val) + +# eval-expr +eval_expr = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('dict', lambda: map_dict(lambda k, v: trampoline(eval_expr(v, env)), expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else eval_list(expr, env))), (None, lambda: expr)]) + +# eval-list +eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_define(args, env) if sx_truthy((name == 'defhandler')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env)))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr)) + +# eval-call +eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env))) + +# call-lambda +call_lambda = lambda f, args, caller_env: (lambda params: (lambda local: (error(sx_str((lambda_name(f) if sx_truthy(lambda_name(f)) else 'lambda'), ' expects ', len(params), ' args, got ', len(args))) if sx_truthy((len(args) != len(params))) else _sx_begin(for_each(lambda pair: _sx_dict_set(local, first(pair), nth(pair, 1)), zip(params, args)), make_thunk(lambda_body(f), local))))(env_merge(lambda_closure(f), caller_env)))(lambda_params(f)) + +# call-component +call_component = lambda comp, raw_args, env: (lambda parsed: (lambda kwargs: (lambda children: (lambda local: _sx_begin(for_each(lambda p: _sx_dict_set(local, p, (dict_get(kwargs, p) if sx_truthy(dict_get(kwargs, p)) else NIL)), component_params(comp)), (_sx_dict_set(local, 'children', children) if sx_truthy(component_has_children(comp)) else NIL), make_thunk(component_body(comp), local)))(env_merge(component_closure(comp), env)))(nth(parsed, 1)))(first(parsed)))(parse_keyword_args(raw_args, env)) + +# parse-keyword-args +parse_keyword_args = lambda raw_args, env: (lambda kwargs: (lambda children: (lambda i: _sx_begin(reduce(lambda state, arg: (lambda idx: (lambda skip: (assoc(state, 'skip', False, 'i', (idx + 1)) if sx_truthy(skip) else (_sx_begin(_sx_dict_set(kwargs, keyword_name(arg), trampoline(eval_expr(nth(raw_args, (idx + 1)), env))), assoc(state, 'skip', True, 'i', (idx + 1))) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((idx + 1) < len(raw_args)))) else _sx_begin(_sx_append(children, trampoline(eval_expr(arg, env))), assoc(state, 'i', (idx + 1))))))(get(state, 'skip')))(get(state, 'i')), {'i': 0, 'skip': False}, raw_args), [kwargs, children]))(0))([]))({}) + +# sf-if +sf_if = lambda args, env: (lambda condition: (make_thunk(nth(args, 1), env) if sx_truthy((condition if not sx_truthy(condition) else (not sx_truthy(is_nil(condition))))) else (make_thunk(nth(args, 2), env) if sx_truthy((len(args) > 2)) else NIL)))(trampoline(eval_expr(first(args), env))) + +# sf-when +sf_when = lambda args, env: (lambda condition: (_sx_begin(for_each(lambda e: trampoline(eval_expr(e, env)), slice(args, 1, (len(args) - 1))), make_thunk(last(args), env)) if sx_truthy((condition if not sx_truthy(condition) else (not sx_truthy(is_nil(condition))))) else NIL))(trampoline(eval_expr(first(args), env))) + +# sf-cond +sf_cond = lambda args, env: (sf_cond_scheme(args, env) if sx_truthy(((type_of(first(args)) == 'list') if not sx_truthy((type_of(first(args)) == 'list')) else (len(first(args)) == 2))) else sf_cond_clojure(args, env)) + +# sf-cond-scheme +sf_cond_scheme = lambda clauses, env: (NIL if sx_truthy(empty_p(clauses)) else (lambda clause: (lambda test: (lambda body: (make_thunk(body, env) if sx_truthy((((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))) if sx_truthy(((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else')))) else ((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')))) else (make_thunk(body, env) if sx_truthy(trampoline(eval_expr(test, env))) else sf_cond_scheme(rest(clauses), env))))(nth(clause, 1)))(first(clause)))(first(clauses))) + +# sf-cond-clojure +sf_cond_clojure = lambda clauses, env: (NIL if sx_truthy((len(clauses) < 2)) else (lambda test: (lambda body: (make_thunk(body, env) if sx_truthy((((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')) if sx_truthy(((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else'))) else ((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))))) else (make_thunk(body, env) if sx_truthy(trampoline(eval_expr(test, env))) else sf_cond_clojure(slice(clauses, 2), env))))(nth(clauses, 1)))(first(clauses))) + +# sf-case +sf_case = lambda args, env: (lambda match_val: (lambda clauses: sf_case_loop(match_val, clauses, env))(rest(args)))(trampoline(eval_expr(first(args), env))) + +# sf-case-loop +sf_case_loop = lambda match_val, clauses, env: (NIL if sx_truthy((len(clauses) < 2)) else (lambda test: (lambda body: (make_thunk(body, env) if sx_truthy((((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')) if sx_truthy(((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else'))) else ((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))))) else (make_thunk(body, env) if sx_truthy((match_val == trampoline(eval_expr(test, env)))) else sf_case_loop(match_val, slice(clauses, 2), env))))(nth(clauses, 1)))(first(clauses))) + +# sf-and +sf_and = lambda args, env: (True if sx_truthy(empty_p(args)) else (lambda val: (val if sx_truthy((not sx_truthy(val))) else (val if sx_truthy((len(args) == 1)) else sf_and(rest(args), env))))(trampoline(eval_expr(first(args), env)))) + +# sf-or +sf_or = lambda args, env: (False if sx_truthy(empty_p(args)) else (lambda val: (val if sx_truthy(val) else sf_or(rest(args), env)))(trampoline(eval_expr(first(args), env)))) + +# sf-let +sf_let = lambda args, env: (lambda bindings: (lambda body: (lambda local: _sx_begin((for_each(lambda binding: (lambda vname: _sx_dict_set(local, vname, trampoline(eval_expr(nth(binding, 1), local))))((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else (lambda i: reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_dict_set(local, vname, trampoline(eval_expr(val_expr, local))))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2))))(0)), for_each(lambda e: trampoline(eval_expr(e, local)), slice(body, 0, (len(body) - 1))), make_thunk(last(body), local)))(env_extend(env)))(rest(args)))(first(args)) + +# sf-lambda +sf_lambda = lambda args, env: (lambda params_expr: (lambda body: (lambda param_names: make_lambda(param_names, body, env))(map(lambda p: (symbol_name(p) if sx_truthy((type_of(p) == 'symbol')) else p), params_expr)))(nth(args, 1)))(first(args)) + +# sf-define +sf_define = lambda args, env: (lambda name_sym: (lambda value: _sx_begin((_sx_set_attr(value, 'name', symbol_name(name_sym)) if sx_truthy((is_lambda(value) if not sx_truthy(is_lambda(value)) else is_nil(lambda_name(value)))) else NIL), _sx_dict_set(env, symbol_name(name_sym), value), value))(trampoline(eval_expr(nth(args, 1), env))))(first(args)) + +# sf-defcomp +sf_defcomp = lambda args, env: (lambda name_sym: (lambda params_raw: (lambda body: (lambda comp_name: (lambda parsed: (lambda params: (lambda has_children: (lambda comp: _sx_begin(_sx_dict_set(env, symbol_name(name_sym), comp), comp))(make_component(comp_name, params, has_children, body, env)))(nth(parsed, 1)))(first(parsed)))(parse_comp_params(params_raw)))(strip_prefix(symbol_name(name_sym), '~')))(nth(args, 2)))(nth(args, 1)))(first(args)) + +# parse-comp-params +def parse_comp_params(params_expr): + _cells = {} + params = [] + _cells['has_children'] = False + _cells['in_key'] = False + for p in params_expr: + if sx_truthy((type_of(p) == 'symbol')): + name = symbol_name(p) + if sx_truthy((name == '&key')): + _cells['in_key'] = True + elif sx_truthy((name == '&rest')): + _cells['has_children'] = True + elif sx_truthy((name == '&children')): + _cells['has_children'] = True + elif sx_truthy(_cells['has_children']): + NIL + elif sx_truthy(_cells['in_key']): + params.append(name) + else: + params.append(name) + return [params, _cells['has_children']] + +# sf-defmacro +sf_defmacro = lambda args, env: (lambda name_sym: (lambda params_raw: (lambda body: (lambda parsed: (lambda params: (lambda rest_param: (lambda mac: _sx_begin(_sx_dict_set(env, symbol_name(name_sym), mac), mac))(make_macro(params, rest_param, body, env, symbol_name(name_sym))))(nth(parsed, 1)))(first(parsed)))(parse_macro_params(params_raw)))(nth(args, 2)))(nth(args, 1)))(first(args)) + +# parse-macro-params +def parse_macro_params(params_expr): + _cells = {} + params = [] + _cells['rest_param'] = NIL + reduce(lambda state, p: (assoc(state, 'in-rest', True) if sx_truthy(((type_of(p) == 'symbol') if not sx_truthy((type_of(p) == 'symbol')) else (symbol_name(p) == '&rest'))) else (_sx_begin(_sx_cell_set(_cells, 'rest_param', (symbol_name(p) if sx_truthy((type_of(p) == 'symbol')) else p)), state) if sx_truthy(get(state, 'in-rest')) else _sx_begin(_sx_append(params, (symbol_name(p) if sx_truthy((type_of(p) == 'symbol')) else p)), state))), {'in-rest': False}, params_expr) + return [params, _cells['rest_param']] + +# sf-defstyle +sf_defstyle = lambda args, env: (lambda name_sym: (lambda value: _sx_begin(_sx_dict_set(env, symbol_name(name_sym), value), value))(trampoline(eval_expr(nth(args, 1), env))))(first(args)) + +# sf-defkeyframes +sf_defkeyframes = lambda args, env: (lambda kf_name: (lambda steps: build_keyframes(kf_name, steps, env))(rest(args)))(symbol_name(first(args))) + +# sf-begin +sf_begin = lambda args, env: (NIL if sx_truthy(empty_p(args)) else _sx_begin(for_each(lambda e: trampoline(eval_expr(e, env)), slice(args, 0, (len(args) - 1))), make_thunk(last(args), env))) + +# sf-quote +sf_quote = lambda args, env: (NIL if sx_truthy(empty_p(args)) else first(args)) + +# sf-quasiquote +sf_quasiquote = lambda args, env: qq_expand(first(args), env) + +# qq-expand +qq_expand = lambda template, env: (template if sx_truthy((not sx_truthy((type_of(template) == 'list')))) else ([] if sx_truthy(empty_p(template)) else (lambda head: (trampoline(eval_expr(nth(template, 1), env)) if sx_truthy(((type_of(head) == 'symbol') if not sx_truthy((type_of(head) == 'symbol')) else (symbol_name(head) == 'unquote'))) else reduce(lambda result, item: ((lambda spliced: (concat(result, spliced) if sx_truthy((type_of(spliced) == 'list')) else (result if sx_truthy(is_nil(spliced)) else append(result, spliced))))(trampoline(eval_expr(nth(item, 1), env))) if sx_truthy(((type_of(item) == 'list') if not sx_truthy((type_of(item) == 'list')) else ((len(item) == 2) if not sx_truthy((len(item) == 2)) else ((type_of(first(item)) == 'symbol') if not sx_truthy((type_of(first(item)) == 'symbol')) else (symbol_name(first(item)) == 'splice-unquote'))))) else append(result, qq_expand(item, env))), [], template)))(first(template)))) + +# sf-thread-first +sf_thread_first = lambda args, env: (lambda val: reduce(lambda result, form: ((lambda f: (lambda rest_args: (lambda all_args: (apply(f, all_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else (not sx_truthy(is_lambda(f))))) else (trampoline(call_lambda(f, all_args, env)) if sx_truthy(is_lambda(f)) else error(sx_str('-> form not callable: ', inspect(f))))))(cons(result, rest_args)))(map(lambda a: trampoline(eval_expr(a, env)), rest(form))))(trampoline(eval_expr(first(form), env))) if sx_truthy((type_of(form) == 'list')) else (lambda f: (f(result) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else (not sx_truthy(is_lambda(f))))) else (trampoline(call_lambda(f, [result], env)) if sx_truthy(is_lambda(f)) else error(sx_str('-> form not callable: ', inspect(f))))))(trampoline(eval_expr(form, env)))), val, rest(args)))(trampoline(eval_expr(first(args), env))) + +# sf-set! +sf_set_bang = lambda args, env: (lambda name: (lambda value: _sx_begin(_sx_dict_set(env, name, value), value))(trampoline(eval_expr(nth(args, 1), env))))(symbol_name(first(args))) + +# expand-macro +expand_macro = lambda mac, raw_args, env: (lambda local: _sx_begin(for_each(lambda pair: _sx_dict_set(local, first(pair), (nth(raw_args, nth(pair, 1)) if sx_truthy((nth(pair, 1) < len(raw_args))) else NIL)), map_indexed(lambda i, p: [p, i], macro_params(mac))), (_sx_dict_set(local, macro_rest_param(mac), slice(raw_args, len(macro_params(mac)))) if sx_truthy(macro_rest_param(mac)) else NIL), trampoline(eval_expr(macro_body(mac), local))))(env_merge(macro_closure(mac), env)) + +# ho-map +ho_map = lambda args, env: (lambda f: (lambda coll: map(lambda item: trampoline(call_lambda(f, [item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env))) + +# ho-map-indexed +ho_map_indexed = lambda args, env: (lambda f: (lambda coll: map_indexed(lambda i, item: trampoline(call_lambda(f, [i, item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env))) + +# ho-filter +ho_filter = lambda args, env: (lambda f: (lambda coll: filter(lambda item: trampoline(call_lambda(f, [item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env))) + +# ho-reduce +ho_reduce = lambda args, env: (lambda f: (lambda init: (lambda coll: reduce(lambda acc, item: trampoline(call_lambda(f, [acc, item], env)), init, coll))(trampoline(eval_expr(nth(args, 2), env))))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env))) + +# ho-some +ho_some = lambda args, env: (lambda f: (lambda coll: some(lambda item: trampoline(call_lambda(f, [item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env))) + +# ho-every +ho_every = lambda args, env: (lambda f: (lambda coll: every_p(lambda item: trampoline(call_lambda(f, [item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env))) + +# ho-for-each +ho_for_each = lambda args, env: (lambda f: (lambda coll: for_each(lambda item: trampoline(call_lambda(f, [item], env)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env))) + + +# === Transpiled from render (core) === + +# HTML_TAGS +HTML_TAGS = ['html', 'head', 'body', 'title', 'meta', 'link', 'script', 'style', 'noscript', 'header', 'nav', 'main', 'section', 'article', 'aside', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hgroup', 'div', 'p', 'blockquote', 'pre', 'figure', 'figcaption', 'address', 'details', 'summary', 'a', 'span', 'em', 'strong', 'small', 'b', 'i', 'u', 's', 'mark', 'sub', 'sup', 'abbr', 'cite', 'code', 'time', 'br', 'wbr', 'hr', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col', 'form', 'input', 'textarea', 'select', 'option', 'optgroup', 'button', 'label', 'fieldset', 'legend', 'output', 'datalist', 'img', 'video', 'audio', 'source', 'picture', 'canvas', 'iframe', 'svg', 'math', 'path', 'circle', 'ellipse', 'rect', 'line', 'polyline', 'polygon', 'text', 'tspan', 'g', 'defs', 'use', 'clipPath', 'mask', 'pattern', 'linearGradient', 'radialGradient', 'stop', 'filter', 'feGaussianBlur', 'feOffset', 'feBlend', 'feColorMatrix', 'feComposite', 'feMerge', 'feMergeNode', 'feTurbulence', 'feComponentTransfer', 'feFuncR', 'feFuncG', 'feFuncB', 'feFuncA', 'feDisplacementMap', 'feFlood', 'feImage', 'feMorphology', 'feSpecularLighting', 'feDiffuseLighting', 'fePointLight', 'feSpotLight', 'feDistantLight', 'animate', 'animateTransform', 'foreignObject', 'template', 'slot', 'dialog', 'menu'] + +# VOID_ELEMENTS +VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'] + +# BOOLEAN_ATTRS +BOOLEAN_ATTRS = ['async', 'autofocus', 'autoplay', 'checked', 'controls', 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'inert', 'ismap', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected'] + +# definition-form? +is_definition_form = lambda name: ((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defkeyframes') if sx_truthy((name == 'defkeyframes')) else (name == 'defhandler')))))) + +# parse-element-args +parse_element_args = lambda args, env: (lambda attrs: (lambda children: _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_dict_set(attrs, keyword_name(arg), val), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(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 _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), [attrs, children]))([]))({}) + +# render-attrs +render_attrs = lambda attrs: join('', map(lambda key: (lambda val: (sx_str(' ', key) if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else val)) else ('' if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else (not sx_truthy(val)))) else ('' if sx_truthy(is_nil(val)) else (sx_str(' class="', style_value_class(val), '"') if sx_truthy(((key == 'style') if not sx_truthy((key == 'style')) else is_style_value(val))) else sx_str(' ', key, '="', escape_attr(sx_str(val)), '"'))))))(dict_get(attrs, key)), keys(attrs))) + + +# === Transpiled from adapter-html === + +# render-to-html +render_to_html = lambda expr, env: _sx_case(type_of(expr), [('nil', lambda: ''), ('string', lambda: escape_html(expr)), ('number', lambda: sx_str(expr)), ('boolean', lambda: ('true' if sx_truthy(expr) else 'false')), ('list', lambda: ('' if sx_truthy(empty_p(expr)) else render_list_to_html(expr, env))), ('symbol', lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env)), ('keyword', lambda: escape_html(keyword_name(expr))), ('raw-html', lambda: raw_html_content(expr)), (None, lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env))]) + +# render-value-to-html +render_value_to_html = lambda val, env: _sx_case(type_of(val), [('nil', lambda: ''), ('string', lambda: escape_html(val)), ('number', lambda: sx_str(val)), ('boolean', lambda: ('true' if sx_truthy(val) else 'false')), ('list', lambda: render_list_to_html(val, env)), ('raw-html', lambda: raw_html_content(val)), ('style-value', lambda: style_value_class(val)), (None, lambda: escape_html(sx_str(val)))]) + +# RENDER_HTML_FORMS +RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defmacro', 'defstyle', 'defkeyframes', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each'] + +# render-html-form? +is_render_html_form = lambda name: contains_p(RENDER_HTML_FORMS, name) + +# render-list-to-html +render_list_to_html = lambda expr, env: ('' if sx_truthy(empty_p(expr)) else (lambda head: (join('', map(lambda x: render_value_to_html(x, env), expr)) if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))) else (lambda name: (lambda args: (join('', map(lambda x: render_to_html(x, env), args)) if sx_truthy((name == '<>')) else (join('', map(lambda x: sx_str(trampoline(eval_expr(x, env))), args)) if sx_truthy((name == 'raw!')) else (render_html_element(name, args, env) if sx_truthy(contains_p(HTML_TAGS, name)) else ((lambda val: (render_html_component(val, args, env) if sx_truthy(is_component(val)) else (render_to_html(expand_macro(val, args, env), env) if sx_truthy(is_macro(val)) else error(sx_str('Unknown component: ', name)))))(env_get(env, name)) if sx_truthy(starts_with_p(name, '~')) else (dispatch_html_form(name, expr, env) if sx_truthy(is_render_html_form(name)) else (render_to_html(expand_macro(env_get(env, name), args, env), env) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else render_value_to_html(trampoline(eval_expr(expr, env)), env))))))))(rest(expr)))(symbol_name(head))))(first(expr))) + +# dispatch-html-form +dispatch_html_form = lambda name, expr, env: ((lambda cond_val: (render_to_html(nth(expr, 2), env) if sx_truthy(cond_val) else (render_to_html(nth(expr, 3), env) if sx_truthy((len(expr) > 3)) else '')))(trampoline(eval_expr(nth(expr, 1), env))) if sx_truthy((name == 'if')) else (('' if sx_truthy((not sx_truthy(trampoline(eval_expr(nth(expr, 1), env))))) else join('', map(lambda i: render_to_html(nth(expr, i), env), range(2, len(expr))))) if sx_truthy((name == 'when')) else ((lambda branch: (render_to_html(branch, env) if sx_truthy(branch) else ''))(eval_cond(rest(expr), env)) if sx_truthy((name == 'cond')) else (render_to_html(trampoline(eval_expr(expr, env)), env) if sx_truthy((name == 'case')) else ((lambda local: join('', map(lambda i: render_to_html(nth(expr, i), local), range(2, len(expr)))))(process_bindings(nth(expr, 1), env)) if sx_truthy(((name == 'let') if sx_truthy((name == 'let')) else (name == 'let*'))) else (join('', map(lambda i: render_to_html(nth(expr, i), env), range(1, len(expr)))) if sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))) else (_sx_begin(trampoline(eval_expr(expr, env)), '') if sx_truthy(is_definition_form(name)) else ((lambda f: (lambda coll: join('', map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll)))(trampoline(eval_expr(nth(expr, 2), env))))(trampoline(eval_expr(nth(expr, 1), env))) if sx_truthy((name == 'map')) else ((lambda f: (lambda coll: join('', map_indexed(lambda i, item: (render_lambda_html(f, [i, item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [i, item]), env)), coll)))(trampoline(eval_expr(nth(expr, 2), env))))(trampoline(eval_expr(nth(expr, 1), env))) if sx_truthy((name == 'map-indexed')) else (render_to_html(trampoline(eval_expr(expr, env)), env) if sx_truthy((name == 'filter')) else ((lambda f: (lambda coll: join('', map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll)))(trampoline(eval_expr(nth(expr, 2), env))))(trampoline(eval_expr(nth(expr, 1), env))) if sx_truthy((name == 'for-each')) else render_value_to_html(trampoline(eval_expr(expr, env)), env)))))))))))) + +# render-lambda-html +render_lambda_html = lambda f, args, env: (lambda local: _sx_begin(for_each_indexed(lambda i, p: _sx_dict_set(local, p, nth(args, i)), lambda_params(f)), render_to_html(lambda_body(f), local)))(env_merge(lambda_closure(f), env)) + +# render-html-component +render_html_component = lambda comp, args, env: (lambda kwargs: (lambda children: _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_dict_set(kwargs, keyword_name(arg), val), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(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 _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), (lambda local: _sx_begin(for_each(lambda p: _sx_dict_set(local, p, (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL)), component_params(comp)), (_sx_dict_set(local, 'children', make_raw_html(join('', map(lambda c: render_to_html(c, env), children)))) if sx_truthy(component_has_children(comp)) else NIL), render_to_html(component_body(comp), local)))(env_merge(component_closure(comp), env))))([]))({}) + +# render-html-element +render_html_element = lambda tag, args, env: (lambda parsed: (lambda attrs: (lambda children: (lambda is_void: sx_str('<', tag, render_attrs(attrs), (' />' if sx_truthy(is_void) else sx_str('>', join('', map(lambda c: render_to_html(c, env), children)), ''))))(contains_p(VOID_ELEMENTS, tag)))(nth(parsed, 1)))(first(parsed)))(parse_element_args(args, env)) + + +# ========================================================================= +# 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) + + +# ========================================================================= +# Public API +# ========================================================================= + +# Set HTML as default adapter +_setup_html_adapter() + +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) From 7982a07f94e853aad58a52326a22a1503cec3c82 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 5 Mar 2026 22:05:35 +0000 Subject: [PATCH 2/2] Add adapter-sx.sx transpilation, async wrapper, and SX_USE_REF switching - Transpile adapter-sx.sx (aser) alongside adapter-html.sx for SX wire format - Add platform functions: serialize, escape_string, is_special_form, is_ho_form, aser_special (with proper control-flow-through-aser dispatch) - SxExpr wrapping prevents double-quoting in aser output - async_eval_ref.py: async wrapper with I/O primitives, RequestContext, async_render, async_eval_to_sx, async_eval_slot_to_sx - SX_USE_REF=1 env var switches shared.sx imports to transpiled backend - 68 comparison tests (test_sx_ref.py), 289 total tests passing Co-Authored-By: Claude Opus 4.6 --- shared/sx/__init__.py | 20 +- shared/sx/ref/__init__.py | 7 + shared/sx/ref/async_eval_ref.py | 999 ++++++++++++++++++++++++++++++++ shared/sx/ref/bootstrap_py.py | 250 +++++++- shared/sx/ref/sx_ref.py | 267 ++++++++- shared/sx/tests/test_sx_ref.py | 336 +++++++++++ 6 files changed, 1872 insertions(+), 7 deletions(-) create mode 100644 shared/sx/ref/__init__.py create mode 100644 shared/sx/ref/async_eval_ref.py create mode 100644 shared/sx/tests/test_sx_ref.py diff --git a/shared/sx/__init__.py b/shared/sx/__init__.py index ab02fe4..60b5c29 100644 --- a/shared/sx/__init__.py +++ b/shared/sx/__init__.py @@ -31,11 +31,21 @@ from .parser import ( parse_all, serialize, ) -from .evaluator import ( - EvalError, - evaluate, - make_env, -) +import os as _os + +if _os.environ.get("SX_USE_REF") == "1": + from .ref.sx_ref import ( + EvalError, + evaluate, + make_env, + ) +else: + from .evaluator import ( + EvalError, + evaluate, + make_env, + ) + from .primitives import ( all_primitives, get_primitive, diff --git a/shared/sx/ref/__init__.py b/shared/sx/ref/__init__.py new file mode 100644 index 0000000..302e89a --- /dev/null +++ b/shared/sx/ref/__init__.py @@ -0,0 +1,7 @@ +"""Reference SX evaluator — transpiled from canonical .sx spec files. + +This package provides the bootstrap-compiled evaluator as an alternative +backend to the hand-written evaluator.py / html.py / async_eval.py. + +Enable by setting SX_USE_REF=1 environment variable. +""" diff --git a/shared/sx/ref/async_eval_ref.py b/shared/sx/ref/async_eval_ref.py new file mode 100644 index 0000000..6b01b25 --- /dev/null +++ b/shared/sx/ref/async_eval_ref.py @@ -0,0 +1,999 @@ +"""Async evaluation wrapper for the transpiled reference evaluator. + +Wraps the sync sx_ref.py evaluator with async I/O support, mirroring +the hand-written async_eval.py. Provides the same public API: + + async_eval() — evaluate with I/O primitives + async_render() — render to HTML with I/O + async_eval_to_sx() — evaluate to SX wire format with I/O + async_eval_slot_to_sx() — expand components server-side, then serialize + +The sync transpiled evaluator handles all control flow, special forms, +and lambda/component dispatch. This wrapper adds: + + - RequestContext threading + - I/O primitive interception (query, service, request-arg, etc.) + - Async trampoline for thunks + - SxExpr wrapping for wire format output + +DO NOT EDIT by hand — this is a thin wrapper; the actual eval logic +lives in sx_ref.py (generated) and the I/O primitives in primitives_io.py. +""" + +from __future__ import annotations + +import contextvars +import inspect +from typing import Any + +from ..types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol +from ..parser import SxExpr, serialize +from ..primitives_io import IO_PRIMITIVES, RequestContext, execute_io +from ..html import ( + HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, + escape_text, escape_attr, _RawHTML, css_class_collector, _svg_context, +) + +from . import sx_ref + +# Re-export EvalError from sx_ref +EvalError = sx_ref.EvalError + +# When True, _aser expands known components server-side +_expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar( + "_expand_components_ref", default=False +) + + +# --------------------------------------------------------------------------- +# Async TCO +# --------------------------------------------------------------------------- + +class _AsyncThunk: + __slots__ = ("expr", "env", "ctx") + def __init__(self, expr, env, ctx): + self.expr = expr + self.env = env + self.ctx = ctx + + +async def _async_trampoline(val): + while isinstance(val, _AsyncThunk): + val = await _async_eval(val.expr, val.env, val.ctx) + return val + + +# --------------------------------------------------------------------------- +# Async evaluate — wraps transpiled sync eval with I/O support +# --------------------------------------------------------------------------- + +async def async_eval(expr, env, ctx=None): + """Public entry point: evaluate with I/O primitives.""" + if ctx is None: + ctx = RequestContext() + result = await _async_eval(expr, env, ctx) + while isinstance(result, _AsyncThunk): + result = await _async_eval(result.expr, result.env, result.ctx) + return result + + +async def _async_eval(expr, env, ctx): + """Internal async evaluator. Intercepts I/O primitives, + delegates everything else to the sync transpiled evaluator.""" + # Intercept I/O primitive calls + if isinstance(expr, list) and expr: + head = expr[0] + if isinstance(head, Symbol) and head.name in IO_PRIMITIVES: + args, kwargs = await _parse_io_args(expr[1:], env, ctx) + return await execute_io(head.name, args, kwargs, ctx) + + # For everything else, use the sync transpiled evaluator + result = sx_ref.eval_expr(expr, env) + return sx_ref.trampoline(result) + + +async def _parse_io_args(exprs, env, ctx): + """Parse and evaluate I/O node args (keyword + positional).""" + args = [] + kwargs = {} + i = 0 + while i < len(exprs): + item = exprs[i] + if isinstance(item, Keyword) and i + 1 < len(exprs): + kwargs[item.name] = await async_eval(exprs[i + 1], env, ctx) + i += 2 + else: + args.append(await async_eval(item, env, ctx)) + i += 1 + return args, kwargs + + +# --------------------------------------------------------------------------- +# Async HTML renderer +# --------------------------------------------------------------------------- + +async def async_render(expr, env, ctx=None): + """Render to HTML, awaiting I/O primitives inline.""" + if ctx is None: + ctx = RequestContext() + return await _arender(expr, env, ctx) + + +async def _arender(expr, env, ctx): + if expr is None or expr is NIL or expr is False or expr is True: + return "" + if isinstance(expr, _RawHTML): + return expr.html + if isinstance(expr, str): + return escape_text(expr) + if isinstance(expr, (int, float)): + return escape_text(str(expr)) + if isinstance(expr, Symbol): + val = await async_eval(expr, env, ctx) + return await _arender(val, env, ctx) + if isinstance(expr, Keyword): + return escape_text(expr.name) + if isinstance(expr, list): + if not expr: + return "" + return await _arender_list(expr, env, ctx) + if isinstance(expr, dict): + return "" + return escape_text(str(expr)) + + +async def _arender_list(expr, env, ctx): + head = expr[0] + if isinstance(head, Symbol): + name = head.name + + # I/O primitive + if name in IO_PRIMITIVES: + result = await async_eval(expr, env, ctx) + return await _arender(result, env, ctx) + + # raw! + if name == "raw!": + parts = [] + for arg in expr[1:]: + val = await async_eval(arg, env, ctx) + if isinstance(val, _RawHTML): + parts.append(val.html) + elif isinstance(val, str): + parts.append(val) + elif val is not None and val is not NIL: + parts.append(str(val)) + return "".join(parts) + + # Fragment + if name == "<>": + parts = [await _arender(c, env, ctx) for c in expr[1:]] + return "".join(parts) + + # html: prefix + if name.startswith("html:"): + return await _arender_element(name[5:], expr[1:], env, ctx) + + # Render-aware special forms + arsf = _ASYNC_RENDER_FORMS.get(name) + if arsf is not None: + if name in HTML_TAGS and ( + (len(expr) > 1 and isinstance(expr[1], Keyword)) + or _svg_context.get(False) + ): + return await _arender_element(name, expr[1:], env, ctx) + return await arsf(expr, env, ctx) + + # Macro expansion + if name in env: + val = env[name] + if isinstance(val, Macro): + expanded = sx_ref.trampoline( + sx_ref.expand_macro(val, expr[1:], env) + ) + return await _arender(expanded, env, ctx) + + # HTML tag + if name in HTML_TAGS: + return await _arender_element(name, expr[1:], env, ctx) + + # Component + if name.startswith("~"): + val = env.get(name) + if isinstance(val, Component): + return await _arender_component(val, expr[1:], env, ctx) + + # Custom element + if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword): + return await _arender_element(name, expr[1:], env, ctx) + + # SVG context + if _svg_context.get(False): + return await _arender_element(name, expr[1:], env, ctx) + + # Fallback — evaluate then render + result = await async_eval(expr, env, ctx) + return await _arender(result, env, ctx) + + if isinstance(head, (Lambda, list)): + result = await async_eval(expr, env, ctx) + return await _arender(result, env, ctx) + + # Data list + parts = [await _arender(item, env, ctx) for item in expr] + return "".join(parts) + + +async def _arender_element(tag, args, env, ctx): + attrs = {} + children = [] + i = 0 + while i < len(args): + arg = args[i] + if isinstance(arg, Keyword) and i + 1 < len(args): + attrs[arg.name] = await async_eval(args[i + 1], env, ctx) + i += 2 + else: + children.append(arg) + i += 1 + + # StyleValue → class + style_val = attrs.get("style") + if isinstance(style_val, StyleValue): + from ..css_registry import register_generated_rule + register_generated_rule(style_val) + existing = attrs.get("class") + if existing and existing is not NIL and existing is not False: + attrs["class"] = f"{existing} {style_val.class_name}" + else: + attrs["class"] = style_val.class_name + del attrs["style"] + + class_val = attrs.get("class") + if class_val is not None and class_val is not NIL and class_val is not False: + collector = css_class_collector.get(None) + if collector is not None: + collector.update(str(class_val).split()) + + parts = [f"<{tag}"] + for attr_name, attr_val in attrs.items(): + if attr_val is None or attr_val is NIL or attr_val is False: + continue + if attr_name in BOOLEAN_ATTRS: + if attr_val: + parts.append(f" {attr_name}") + elif attr_val is True: + parts.append(f" {attr_name}") + else: + parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"') + parts.append(">") + opening = "".join(parts) + + if tag in VOID_ELEMENTS: + return opening + + token = None + if tag in ("svg", "math"): + token = _svg_context.set(True) + try: + child_parts = [await _arender(c, env, ctx) for c in children] + finally: + if token is not None: + _svg_context.reset(token) + + return f"{opening}{''.join(child_parts)}" + + +async def _arender_component(comp, args, env, ctx): + kwargs = {} + children = [] + i = 0 + while i < len(args): + arg = args[i] + if isinstance(arg, Keyword) and i + 1 < len(args): + kwargs[arg.name] = await async_eval(args[i + 1], env, ctx) + i += 2 + else: + children.append(arg) + i += 1 + local = dict(comp.closure) + local.update(env) + for p in comp.params: + local[p] = kwargs.get(p, NIL) + if comp.has_children: + child_html = [await _arender(c, env, ctx) for c in children] + local["children"] = _RawHTML("".join(child_html)) + return await _arender(comp.body, local, ctx) + + +async def _arender_lambda(fn, args, env, ctx): + local = dict(fn.closure) + local.update(env) + for p, v in zip(fn.params, args): + local[p] = v + return await _arender(fn.body, local, ctx) + + +# --------------------------------------------------------------------------- +# Render-aware special forms +# --------------------------------------------------------------------------- + +async def _arsf_if(expr, env, ctx): + cond = await async_eval(expr[1], env, ctx) + if cond and cond is not NIL: + return await _arender(expr[2], env, ctx) + return await _arender(expr[3], env, ctx) if len(expr) > 3 else "" + + +async def _arsf_when(expr, env, ctx): + cond = await async_eval(expr[1], env, ctx) + if cond and cond is not NIL: + return "".join([await _arender(b, env, ctx) for b in expr[2:]]) + return "" + + +async def _arsf_cond(expr, env, ctx): + clauses = expr[1:] + if not clauses: + return "" + if isinstance(clauses[0], list) and len(clauses[0]) == 2: + for clause in clauses: + test = clause[0] + if isinstance(test, Symbol) and test.name in ("else", ":else"): + return await _arender(clause[1], env, ctx) + if isinstance(test, Keyword) and test.name == "else": + return await _arender(clause[1], env, ctx) + if await async_eval(test, env, ctx): + return await _arender(clause[1], env, ctx) + else: + i = 0 + while i < len(clauses) - 1: + test, result = clauses[i], clauses[i + 1] + if isinstance(test, Keyword) and test.name == "else": + return await _arender(result, env, ctx) + if isinstance(test, Symbol) and test.name in (":else", "else"): + return await _arender(result, env, ctx) + if await async_eval(test, env, ctx): + return await _arender(result, env, ctx) + i += 2 + return "" + + +async def _arsf_let(expr, env, ctx): + bindings = expr[1] + local = dict(env) + if isinstance(bindings, list): + if bindings and isinstance(bindings[0], list): + for b in bindings: + var = b[0] + vname = var.name if isinstance(var, Symbol) else var + local[vname] = await async_eval(b[1], local, ctx) + elif len(bindings) % 2 == 0: + for i in range(0, len(bindings), 2): + var = bindings[i] + vname = var.name if isinstance(var, Symbol) else var + local[vname] = await async_eval(bindings[i + 1], local, ctx) + return "".join([await _arender(b, local, ctx) for b in expr[2:]]) + + +async def _arsf_begin(expr, env, ctx): + return "".join([await _arender(sub, env, ctx) for sub in expr[1:]]) + + +async def _arsf_define(expr, env, ctx): + await async_eval(expr, env, ctx) + return "" + + +async def _arsf_map(expr, env, ctx): + fn = await async_eval(expr[1], env, ctx) + coll = await async_eval(expr[2], env, ctx) + parts = [] + for item in coll: + if isinstance(fn, Lambda): + parts.append(await _arender_lambda(fn, (item,), env, ctx)) + elif callable(fn): + r = fn(item) + if inspect.iscoroutine(r): + r = await r + parts.append(await _arender(r, env, ctx)) + else: + parts.append(await _arender(item, env, ctx)) + return "".join(parts) + + +async def _arsf_map_indexed(expr, env, ctx): + fn = await async_eval(expr[1], env, ctx) + coll = await async_eval(expr[2], env, ctx) + parts = [] + for i, item in enumerate(coll): + if isinstance(fn, Lambda): + parts.append(await _arender_lambda(fn, (i, item), env, ctx)) + elif callable(fn): + r = fn(i, item) + if inspect.iscoroutine(r): + r = await r + parts.append(await _arender(r, env, ctx)) + else: + parts.append(await _arender(item, env, ctx)) + return "".join(parts) + + +async def _arsf_filter(expr, env, ctx): + result = await async_eval(expr, env, ctx) + return await _arender(result, env, ctx) + + +async def _arsf_for_each(expr, env, ctx): + fn = await async_eval(expr[1], env, ctx) + coll = await async_eval(expr[2], env, ctx) + parts = [] + for item in coll: + if isinstance(fn, Lambda): + parts.append(await _arender_lambda(fn, (item,), env, ctx)) + elif callable(fn): + r = fn(item) + if inspect.iscoroutine(r): + r = await r + parts.append(await _arender(r, env, ctx)) + else: + parts.append(await _arender(item, env, ctx)) + return "".join(parts) + + +_ASYNC_RENDER_FORMS = { + "if": _arsf_if, + "when": _arsf_when, + "cond": _arsf_cond, + "let": _arsf_let, + "let*": _arsf_let, + "begin": _arsf_begin, + "do": _arsf_begin, + "define": _arsf_define, + "defstyle": _arsf_define, + "defkeyframes": _arsf_define, + "defcomp": _arsf_define, + "defmacro": _arsf_define, + "defhandler": _arsf_define, + "map": _arsf_map, + "map-indexed": _arsf_map_indexed, + "filter": _arsf_filter, + "for-each": _arsf_for_each, +} + + +# --------------------------------------------------------------------------- +# Async SX wire format (aser) +# --------------------------------------------------------------------------- + +async def async_eval_to_sx(expr, env, ctx=None): + """Evaluate and produce SX source string (wire format).""" + if ctx is None: + ctx = RequestContext() + result = await _aser(expr, env, ctx) + if isinstance(result, SxExpr): + return result + if result is None or result is NIL: + return SxExpr("") + if isinstance(result, str): + return SxExpr(result) + return SxExpr(serialize(result)) + + +async def async_eval_slot_to_sx(expr, env, ctx=None): + """Like async_eval_to_sx but expands component calls server-side.""" + if ctx is None: + ctx = RequestContext() + token = _expand_components.set(True) + try: + return await _eval_slot_inner(expr, env, ctx) + finally: + _expand_components.reset(token) + + +async def _eval_slot_inner(expr, env, ctx): + if isinstance(expr, list) and expr: + head = expr[0] + if isinstance(head, Symbol) and head.name.startswith("~"): + comp = env.get(head.name) + if isinstance(comp, Component): + result = await _aser_component(comp, expr[1:], env, ctx) + if isinstance(result, SxExpr): + return result + if result is None or result is NIL: + return SxExpr("") + if isinstance(result, str): + return SxExpr(result) + return SxExpr(serialize(result)) + result = await _aser(expr, env, ctx) + result = await _maybe_expand_component_result(result, env, ctx) + if isinstance(result, SxExpr): + return result + if result is None or result is NIL: + return SxExpr("") + if isinstance(result, str): + return SxExpr(result) + return SxExpr(serialize(result)) + + +async def _maybe_expand_component_result(result, env, ctx): + raw = None + if isinstance(result, SxExpr): + raw = str(result).strip() + elif isinstance(result, str): + raw = result.strip() + if raw and raw.startswith("(~"): + from ..parser import parse_all + parsed = parse_all(raw) + if parsed: + return await async_eval_slot_to_sx(parsed[0], env, ctx) + return result + + +async def _aser(expr, env, ctx): + """Evaluate for SX wire format — serialize rendering forms, evaluate control flow.""" + if isinstance(expr, (int, float, bool)): + return expr + if isinstance(expr, SxExpr): + return expr + if isinstance(expr, str): + return expr + if expr is None or expr is NIL: + return NIL + + if isinstance(expr, Symbol): + name = expr.name + if name in env: + return env[name] + if sx_ref.is_primitive(name): + return sx_ref.get_primitive(name) + if name == "true": + return True + if name == "false": + return False + if name == "nil": + return NIL + raise EvalError(f"Undefined symbol: {name}") + + if isinstance(expr, Keyword): + return expr.name + + if isinstance(expr, dict): + return {k: await _aser(v, env, ctx) for k, v in expr.items()} + + if not isinstance(expr, list): + return expr + if not expr: + return [] + + head = expr[0] + if not isinstance(head, (Symbol, Lambda, list)): + return [await _aser(x, env, ctx) for x in expr] + + if isinstance(head, Symbol): + name = head.name + + # I/O primitives + if name in IO_PRIMITIVES: + args, kwargs = await _parse_io_args(expr[1:], env, ctx) + return await execute_io(name, args, kwargs, ctx) + + # Fragment + if name == "<>": + return await _aser_fragment(expr[1:], env, ctx) + + # raw! + if name == "raw!": + return await _aser_call("raw!", expr[1:], env, ctx) + + # html: prefix + if name.startswith("html:"): + return await _aser_call(name[5:], expr[1:], env, ctx) + + # Component call + if name.startswith("~"): + val = env.get(name) + if isinstance(val, Macro): + expanded = sx_ref.trampoline( + sx_ref.expand_macro(val, expr[1:], env) + ) + return await _aser(expanded, env, ctx) + if isinstance(val, Component) and _expand_components.get(): + return await _aser_component(val, expr[1:], env, ctx) + return await _aser_call(name, expr[1:], env, ctx) + + # Serialize-mode special/HO forms + sf = _ASER_FORMS.get(name) + if sf is not None: + if name in HTML_TAGS and ( + (len(expr) > 1 and isinstance(expr[1], Keyword)) + or _svg_context.get(False) + ): + return await _aser_call(name, expr[1:], env, ctx) + return await sf(expr, env, ctx) + + # HTML tag + if name in HTML_TAGS: + return await _aser_call(name, expr[1:], env, ctx) + + # Macro + if name in env: + val = env[name] + if isinstance(val, Macro): + expanded = sx_ref.trampoline( + sx_ref.expand_macro(val, expr[1:], env) + ) + return await _aser(expanded, env, ctx) + + # Custom element + if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword): + return await _aser_call(name, expr[1:], env, ctx) + + # SVG context + if _svg_context.get(False): + return await _aser_call(name, expr[1:], env, ctx) + + # Function/lambda call + fn = await async_eval(head, env, ctx) + args = [await async_eval(a, env, ctx) for a in expr[1:]] + + if callable(fn) and not isinstance(fn, (Lambda, Component)): + result = fn(*args) + if inspect.iscoroutine(result): + return await result + return result + if isinstance(fn, Lambda): + local = dict(fn.closure) + local.update(env) + for p, v in zip(fn.params, args): + local[p] = v + return await _aser(fn.body, local, ctx) + if isinstance(fn, Component): + return await _aser_call(f"~{fn.name}", expr[1:], env, ctx) + raise EvalError(f"Not callable: {fn!r}") + + +async def _aser_fragment(children, env, ctx): + parts = [] + for child in children: + result = await _aser(child, env, ctx) + if isinstance(result, list): + for item in result: + if item is not NIL and item is not None: + parts.append(serialize(item)) + elif result is not NIL and result is not None: + parts.append(serialize(result)) + if not parts: + return SxExpr("") + return SxExpr("(<> " + " ".join(parts) + ")") + + +async def _aser_component(comp, args, env, ctx): + kwargs = {} + children = [] + i = 0 + while i < len(args): + arg = args[i] + if isinstance(arg, Keyword) and i + 1 < len(args): + kwargs[arg.name] = await _aser(args[i + 1], env, ctx) + i += 2 + else: + children.append(arg) + i += 1 + local = dict(comp.closure) + local.update(env) + for p in comp.params: + local[p] = kwargs.get(p, NIL) + if comp.has_children: + child_parts = [serialize(await _aser(c, env, ctx)) for c in children] + local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")") + return await _aser(comp.body, local, ctx) + + +async def _aser_call(name, args, env, ctx): + token = None + if name in ("svg", "math"): + token = _svg_context.set(True) + try: + parts = [name] + extra_class = None + i = 0 + while i < len(args): + arg = args[i] + if isinstance(arg, Keyword) and i + 1 < len(args): + val = await _aser(args[i + 1], env, ctx) + if val is not NIL and val is not None: + if arg.name == "style" and isinstance(val, StyleValue): + from ..css_registry import register_generated_rule + register_generated_rule(val) + extra_class = val.class_name + else: + parts.append(f":{arg.name}") + if isinstance(val, list): + live = [v for v in val if v is not NIL and v is not None] + items = [serialize(v) for v in live] + if not items: + parts.append("nil") + elif any(isinstance(v, SxExpr) for v in live): + parts.append("(<> " + " ".join(items) + ")") + else: + parts.append("(list " + " ".join(items) + ")") + else: + parts.append(serialize(val)) + i += 2 + else: + result = await _aser(arg, env, ctx) + if result is not NIL and result is not None: + if isinstance(result, list): + for item in result: + if item is not NIL and item is not None: + parts.append(serialize(item)) + else: + parts.append(serialize(result)) + i += 1 + if extra_class: + _merge_class_into_parts(parts, extra_class) + return SxExpr("(" + " ".join(parts) + ")") + finally: + if token is not None: + _svg_context.reset(token) + + +def _merge_class_into_parts(parts, class_name): + for i, p in enumerate(parts): + if p == ":class" and i + 1 < len(parts): + existing = parts[i + 1] + if existing.startswith('"') and existing.endswith('"'): + parts[i + 1] = existing[:-1] + " " + class_name + '"' + else: + parts[i + 1] = f'(str {existing} " {class_name}")' + return + parts.insert(1, f'"{class_name}"') + parts.insert(1, ":class") + + +# --------------------------------------------------------------------------- +# Aser-mode special forms +# --------------------------------------------------------------------------- + +async def _assf_if(expr, env, ctx): + cond = await async_eval(expr[1], env, ctx) + if cond and cond is not NIL: + return await _aser(expr[2], env, ctx) + return await _aser(expr[3], env, ctx) if len(expr) > 3 else NIL + + +async def _assf_when(expr, env, ctx): + cond = await async_eval(expr[1], env, ctx) + if cond and cond is not NIL: + result = NIL + for body_expr in expr[2:]: + result = await _aser(body_expr, env, ctx) + return result + return NIL + + +async def _assf_let(expr, env, ctx): + bindings = expr[1] + local = dict(env) + if isinstance(bindings, list): + if bindings and isinstance(bindings[0], list): + for b in bindings: + var = b[0] + vname = var.name if isinstance(var, Symbol) else var + local[vname] = await _aser(b[1], local, ctx) + elif len(bindings) % 2 == 0: + for i in range(0, len(bindings), 2): + var = bindings[i] + vname = var.name if isinstance(var, Symbol) else var + local[vname] = await _aser(bindings[i + 1], local, ctx) + result = NIL + for body_expr in expr[2:]: + result = await _aser(body_expr, local, ctx) + return result + + +async def _assf_cond(expr, env, ctx): + clauses = expr[1:] + if not clauses: + return NIL + if isinstance(clauses[0], list) and len(clauses[0]) == 2: + for clause in clauses: + test = clause[0] + if isinstance(test, Symbol) and test.name in ("else", ":else"): + return await _aser(clause[1], env, ctx) + if isinstance(test, Keyword) and test.name == "else": + return await _aser(clause[1], env, ctx) + if await async_eval(test, env, ctx): + return await _aser(clause[1], env, ctx) + else: + i = 0 + while i < len(clauses) - 1: + test, result = clauses[i], clauses[i + 1] + if isinstance(test, Keyword) and test.name == "else": + return await _aser(result, env, ctx) + if isinstance(test, Symbol) and test.name in (":else", "else"): + return await _aser(result, env, ctx) + if await async_eval(test, env, ctx): + return await _aser(result, env, ctx) + i += 2 + return NIL + + +async def _assf_case(expr, env, ctx): + match_val = await async_eval(expr[1], env, ctx) + clauses = expr[2:] + i = 0 + while i < len(clauses) - 1: + test, result = clauses[i], clauses[i + 1] + if isinstance(test, Keyword) and test.name == "else": + return await _aser(result, env, ctx) + if isinstance(test, Symbol) and test.name in (":else", "else"): + return await _aser(result, env, ctx) + if match_val == await async_eval(test, env, ctx): + return await _aser(result, env, ctx) + i += 2 + return NIL + + +async def _assf_begin(expr, env, ctx): + result = NIL + for sub in expr[1:]: + result = await _aser(sub, env, ctx) + return result + + +async def _assf_define(expr, env, ctx): + await async_eval(expr, env, ctx) + return NIL + + +async def _assf_and(expr, env, ctx): + result = True + for arg in expr[1:]: + result = await async_eval(arg, env, ctx) + if not result: + return result + return result + + +async def _assf_or(expr, env, ctx): + result = False + for arg in expr[1:]: + result = await async_eval(arg, env, ctx) + if result: + return result + return result + + +async def _assf_lambda(expr, env, ctx): + params_expr = expr[1] + param_names = [] + for p in params_expr: + if isinstance(p, Symbol): + param_names.append(p.name) + elif isinstance(p, str): + param_names.append(p) + return Lambda(param_names, expr[2], dict(env)) + + +async def _assf_quote(expr, env, ctx): + return expr[1] if len(expr) > 1 else NIL + + +async def _assf_thread_first(expr, env, ctx): + result = await async_eval(expr[1], env, ctx) + for form in expr[2:]: + if isinstance(form, list): + fn = await async_eval(form[0], env, ctx) + fn_args = [result] + [await async_eval(a, env, ctx) for a in form[1:]] + else: + fn = await async_eval(form, env, ctx) + fn_args = [result] + if callable(fn) and not isinstance(fn, (Lambda, Component)): + result = fn(*fn_args) + if inspect.iscoroutine(result): + result = await result + elif isinstance(fn, Lambda): + local = dict(fn.closure) + local.update(env) + for p, v in zip(fn.params, fn_args): + local[p] = v + result = await async_eval(fn.body, local, ctx) + else: + raise EvalError(f"-> form not callable: {fn!r}") + return result + + +async def _assf_set_bang(expr, env, ctx): + value = await async_eval(expr[2], env, ctx) + env[expr[1].name] = value + return value + + +# Aser-mode HO forms + +async def _asho_map(expr, env, ctx): + fn = await async_eval(expr[1], env, ctx) + coll = await async_eval(expr[2], env, ctx) + results = [] + for item in coll: + if isinstance(fn, Lambda): + local = dict(fn.closure) + local.update(env) + local[fn.params[0]] = item + results.append(await _aser(fn.body, local, ctx)) + elif callable(fn): + r = fn(item) + results.append(await r if inspect.iscoroutine(r) else r) + else: + raise EvalError(f"map requires callable, got {type(fn).__name__}") + return results + + +async def _asho_map_indexed(expr, env, ctx): + fn = await async_eval(expr[1], env, ctx) + coll = await async_eval(expr[2], env, ctx) + results = [] + for i, item in enumerate(coll): + if isinstance(fn, Lambda): + local = dict(fn.closure) + local.update(env) + local[fn.params[0]] = i + local[fn.params[1]] = item + results.append(await _aser(fn.body, local, ctx)) + elif callable(fn): + r = fn(i, item) + results.append(await r if inspect.iscoroutine(r) else r) + else: + raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}") + return results + + +async def _asho_filter(expr, env, ctx): + return await async_eval(expr, env, ctx) + + +async def _asho_for_each(expr, env, ctx): + fn = await async_eval(expr[1], env, ctx) + coll = await async_eval(expr[2], env, ctx) + results = [] + for item in coll: + if isinstance(fn, Lambda): + local = dict(fn.closure) + local.update(env) + local[fn.params[0]] = item + results.append(await _aser(fn.body, local, ctx)) + elif callable(fn): + r = fn(item) + results.append(await r if inspect.iscoroutine(r) else r) + return results + + +_ASER_FORMS = { + "if": _assf_if, + "when": _assf_when, + "cond": _assf_cond, + "case": _assf_case, + "and": _assf_and, + "or": _assf_or, + "let": _assf_let, + "let*": _assf_let, + "lambda": _assf_lambda, + "fn": _assf_lambda, + "define": _assf_define, + "defstyle": _assf_define, + "defkeyframes": _assf_define, + "defcomp": _assf_define, + "defmacro": _assf_define, + "defhandler": _assf_define, + "begin": _assf_begin, + "do": _assf_begin, + "quote": _assf_quote, + "->": _assf_thread_first, + "set!": _assf_set_bang, + "map": _asho_map, + "map-indexed": _asho_map_indexed, + "filter": _asho_filter, + "for-each": _asho_for_each, +} diff --git a/shared/sx/ref/bootstrap_py.py b/shared/sx/ref/bootstrap_py.py index 7269407..d47f8f6 100644 --- a/shared/sx/ref/bootstrap_py.py +++ b/shared/sx/ref/bootstrap_py.py @@ -884,6 +884,7 @@ from typing import Any from shared.sx.types import ( NIL, Symbol, Keyword, Lambda, Component, Macro, StyleValue, ) +from shared.sx.parser import SxExpr ''' PLATFORM_PY = ''' @@ -971,6 +972,8 @@ def type_of(x): 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): @@ -1240,6 +1243,228 @@ 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("", "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) ''' PRIMITIVES_PY = ''' @@ -1338,7 +1563,7 @@ PRIMITIVES["zero?"] = lambda n: n == 0 # 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(a, b, step)) +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 @@ -1465,6 +1690,25 @@ def _setup_html_adapter(): 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 ''' @@ -1476,6 +1720,10 @@ def public_api_py(has_html: bool, has_sx: bool) -> str: '# =========================================================================', '', ] + 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()') diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index 4138f0f..498fb4a 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -19,6 +19,7 @@ from typing import Any from shared.sx.types import ( NIL, Symbol, Keyword, Lambda, Component, Macro, StyleValue, ) +from shared.sx.parser import SxExpr # ========================================================================= @@ -105,6 +106,8 @@ def type_of(x): 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): @@ -376,6 +379,228 @@ def _sx_cell_set(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("", "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) + + # ========================================================================= # Primitives # ========================================================================= @@ -471,7 +696,7 @@ PRIMITIVES["zero?"] = lambda n: n == 0 # 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(a, b, step)) +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 @@ -782,6 +1007,24 @@ render_html_component = lambda comp, args, env: (lambda kwargs: (lambda children render_html_element = lambda tag, args, env: (lambda parsed: (lambda attrs: (lambda children: (lambda is_void: sx_str('<', tag, render_attrs(attrs), (' />' if sx_truthy(is_void) else sx_str('>', join('', map(lambda c: render_to_html(c, env), children)), ''))))(contains_p(VOID_ELEMENTS, tag)))(nth(parsed, 1)))(first(parsed)))(parse_element_args(args, env)) +# === Transpiled from adapter-sx === + +# render-to-sx +render_to_sx = lambda expr, env: (lambda result: (result if sx_truthy((type_of(result) == 'string')) else serialize(result)))(aser(expr, env)) + +# aser +aser = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else aser_list(expr, env))), (None, lambda: expr)]) + +# aser-list +aser_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: aser(x, env), expr) if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))) else (lambda name: (aser_fragment(args, env) if sx_truthy((name == '<>')) else (aser_call(name, args, env) if sx_truthy(starts_with_p(name, '~')) else (aser_call(name, args, env) if sx_truthy(contains_p(HTML_TAGS, name)) else (aser_special(name, expr, env) if sx_truthy((is_special_form(name) if sx_truthy(is_special_form(name)) else is_ho_form(name))) else (aser(expand_macro(env_get(env, name), args, env), env) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (lambda f: (lambda evaled_args: (apply(f, evaled_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (trampoline(call_lambda(f, evaled_args, env)) if sx_truthy(is_lambda(f)) else (aser_call(sx_str('~', component_name(f)), args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))))))))(symbol_name(head))))(rest(expr)))(first(expr)) + +# aser-fragment +aser_fragment = lambda children, env: (lambda parts: ('' if sx_truthy(empty_p(parts)) else sx_str('(<> ', join(' ', map(serialize, parts)), ')')))(filter(lambda x: (not sx_truthy(is_nil(x))), map(lambda c: aser(c, env), children))) + +# aser-call +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]) + + # ========================================================================= # Fixups -- wire up render adapter dispatch # ========================================================================= @@ -795,10 +1038,32 @@ def _setup_sx_adapter(): _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 + + # ========================================================================= # Public API # ========================================================================= +# Wrap aser outputs to return SxExpr +_wrap_aser_outputs() + # Set HTML as default adapter _setup_html_adapter() diff --git a/shared/sx/tests/test_sx_ref.py b/shared/sx/tests/test_sx_ref.py new file mode 100644 index 0000000..ad8dc46 --- /dev/null +++ b/shared/sx/tests/test_sx_ref.py @@ -0,0 +1,336 @@ +"""Tests for the transpiled sx_ref.py evaluator. + +Runs the same test cases as test_evaluator.py and test_html.py but +against the bootstrap-compiled evaluator to verify correctness. +""" + +import pytest +from shared.sx.parser import parse +from shared.sx.types import Symbol, Keyword, NIL, Lambda, Component, Macro +from shared.sx.ref import sx_ref + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def ev(text, env=None): + """Parse and evaluate a single expression via sx_ref.""" + return sx_ref.evaluate(parse(text), env) + + +def render(text, env=None): + """Parse and render via sx_ref.""" + return sx_ref.render(parse(text), env) + + +# --------------------------------------------------------------------------- +# Literals and lookups +# --------------------------------------------------------------------------- + +class TestLiterals: + def test_int(self): + assert ev("42") == 42 + + def test_string(self): + assert ev('"hello"') == "hello" + + def test_true(self): + assert ev("true") is True + + def test_nil(self): + assert ev("nil") is NIL + + def test_symbol_lookup(self): + assert ev("x", {"x": 10}) == 10 + + def test_undefined_symbol(self): + with pytest.raises(sx_ref.EvalError, match="Undefined symbol"): + ev("xyz") + + def test_keyword_evaluates_to_name(self): + assert ev(":foo") == "foo" + + +# --------------------------------------------------------------------------- +# Arithmetic +# --------------------------------------------------------------------------- + +class TestArithmetic: + def test_add(self): + assert ev("(+ 1 2 3)") == 6 + + def test_sub(self): + assert ev("(- 10 3)") == 7 + + def test_mul(self): + assert ev("(* 2 3 4)") == 24 + + def test_div(self): + assert ev("(/ 10 4)") == 2.5 + + def test_mod(self): + assert ev("(mod 7 3)") == 1 + + +# --------------------------------------------------------------------------- +# Special forms +# --------------------------------------------------------------------------- + +class TestSpecialForms: + def test_if_true(self): + assert ev("(if true 1 2)") == 1 + + def test_if_false(self): + assert ev("(if false 1 2)") == 2 + + def test_if_no_else(self): + assert ev("(if false 1)") is NIL + + def test_when_true(self): + assert ev("(when true 42)") == 42 + + def test_when_false(self): + assert ev("(when false 42)") is NIL + + def test_and_short_circuit(self): + assert ev("(and true true 3)") == 3 + assert ev("(and true false 3)") is False + + def test_or_short_circuit(self): + assert ev("(or false false 3)") == 3 + assert ev("(or false 2 3)") == 2 + + def test_let_scheme_style(self): + assert ev("(let ((x 10) (y 20)) (+ x y))") == 30 + + def test_let_clojure_style(self): + assert ev("(let (x 10 y 20) (+ x y))") == 30 + + def test_let_sequential(self): + assert ev("(let ((x 1) (y (+ x 1))) y)") == 2 + + def test_begin(self): + assert ev("(begin 1 2 3)") == 3 + + def test_quote(self): + result = ev("(quote (a b c))") + assert result == [Symbol("a"), Symbol("b"), Symbol("c")] + + def test_cond_clojure(self): + assert ev("(cond false 1 true 2 :else 3)") == 2 + + def test_cond_else(self): + assert ev("(cond false 1 false 2 :else 99)") == 99 + + def test_case(self): + assert ev('(case 2 1 "one" 2 "two" :else "other")') == "two" + + def test_thread_first(self): + assert ev("(-> 5 (+ 3) (* 2))") == 16 + + def test_define(self): + env = {} + ev("(define x 42)", env) + assert env["x"] == 42 + + +# --------------------------------------------------------------------------- +# Lambda +# --------------------------------------------------------------------------- + +class TestLambda: + def test_create_and_call(self): + assert ev("((fn (x) (* x x)) 5)") == 25 + + def test_closure(self): + result = ev("(let ((a 10)) ((fn (x) (+ x a)) 5))") + assert result == 15 + + def test_higher_order(self): + result = ev("(let ((double (fn (x) (* x 2)))) (double 7))") + assert result == 14 + + +# --------------------------------------------------------------------------- +# Collections +# --------------------------------------------------------------------------- + +class TestCollections: + def test_list_constructor(self): + assert ev("(list 1 2 3)") == [1, 2, 3] + + def test_dict_constructor(self): + assert ev("(dict :a 1 :b 2)") == {"a": 1, "b": 2} + + def test_get_dict(self): + assert ev('(get {:a 1 :b 2} "a")') == 1 + + def test_get_list(self): + assert ev("(get (list 10 20 30) 1)") == 20 + + def test_first_last_rest(self): + assert ev("(first (list 1 2 3))") == 1 + assert ev("(last (list 1 2 3))") == 3 + assert ev("(rest (list 1 2 3))") == [2, 3] + + def test_len(self): + assert ev("(len (list 1 2 3))") == 3 + + def test_concat(self): + assert ev("(concat (list 1 2) (list 3 4))") == [1, 2, 3, 4] + + def test_cons(self): + assert ev("(cons 0 (list 1 2))") == [0, 1, 2] + + def test_merge(self): + assert ev("(merge {:a 1} {:b 2} {:a 3})") == {"a": 3, "b": 2} + + def test_empty(self): + assert ev("(empty? (list))") is True + assert ev("(empty? (list 1))") is False + assert ev("(empty? nil)") is True + + +# --------------------------------------------------------------------------- +# Higher-order forms +# --------------------------------------------------------------------------- + +class TestHigherOrder: + def test_map(self): + assert ev("(map (fn (x) (* x x)) (list 1 2 3 4))") == [1, 4, 9, 16] + + def test_filter(self): + assert ev("(filter (fn (x) (> x 2)) (list 1 2 3 4))") == [3, 4] + + def test_reduce(self): + assert ev("(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3))") == 6 + + def test_some(self): + assert ev("(some (fn (x) (> x 3)) (list 1 2 3 4 5))") is True + + def test_for_each(self): + result = ev("(for-each (fn (x) x) (list 1 2 3))") + assert result is NIL + + +# --------------------------------------------------------------------------- +# String ops +# --------------------------------------------------------------------------- + +class TestStrings: + def test_str(self): + assert ev('(str "hello" " " "world")') == "hello world" + + def test_upper_lower(self): + assert ev('(upper "hello")') == "HELLO" + assert ev('(lower "HELLO")') == "hello" + + def test_split_join(self): + assert ev('(split "a,b,c" ",")') == ["a", "b", "c"] + assert ev('(join "-" (list "a" "b"))') == "a-b" + + def test_starts_ends(self): + assert ev('(starts-with? "hello" "hel")') is True + assert ev('(ends-with? "hello" "llo")') is True + + +# --------------------------------------------------------------------------- +# Components +# --------------------------------------------------------------------------- + +class TestComponents: + def test_defcomp_and_render(self): + env = {} + ev("(defcomp ~box (&key title) (div :class \"box\" title))", env) + result = render("(~box :title \"hi\")", env) + assert result == '
hi
' + + def test_defcomp_with_children(self): + env = {} + ev("(defcomp ~wrap (&rest children) (div children))", env) + result = render('(~wrap (span "a") (span "b"))', env) + assert result == '
ab
' + + +# --------------------------------------------------------------------------- +# HTML rendering +# --------------------------------------------------------------------------- + +class TestHTMLRendering: + def test_basic_element(self): + assert render("(div)") == "
" + + def test_text_content(self): + assert render('(p "hello")') == "

hello

" + + def test_attributes(self): + result = render('(a :href "/about" "link")') + assert result == 'link' + + def test_void_element(self): + result = render('(br)') + assert result == "
" + + def test_nested(self): + result = render('(div (p "a") (p "b"))') + assert result == "

a

b

" + + def test_fragment(self): + result = render('(<> (span "a") (span "b"))') + assert result == "ab" + + def test_conditional_rendering(self): + result = render('(if true (span "yes") (span "no"))') + assert result == "yes" + + def test_map_rendering(self): + result = render('(map (fn (x) (li x)) (list "a" "b"))') + assert result == "
  • a
  • b
  • " + + def test_html_escaping(self): + result = render('(span "bold")') + assert result == "<b>bold</b>" + + +# --------------------------------------------------------------------------- +# Aser (SX wire format) +# --------------------------------------------------------------------------- + +class TestAser: + def test_render_to_sx_basic(self): + expr = parse("(div :class \"foo\" \"hello\")") + result = sx_ref.render_to_sx(expr, {}) + assert result == '(div :class "foo" "hello")' + + def test_component_not_expanded(self): + expr = parse('(~card :title "hi")') + result = sx_ref.render_to_sx(expr, {}) + assert result == '(~card :title "hi")' + + def test_fragment(self): + expr = parse('(<> "a" "b")') + result = sx_ref.render_to_sx(expr, {}) + assert result == '(<> "a" "b")' + + def test_let_evaluates(self): + expr = parse('(let ((x 5)) x)') + result = sx_ref.render_to_sx(expr, {}) + assert result == "5" + + def test_if_evaluates(self): + expr = parse('(if true "yes" "no")') + result = sx_ref.render_to_sx(expr, {}) + assert result == 'yes' # strings pass through unserialized + + +# --------------------------------------------------------------------------- +# Macros +# --------------------------------------------------------------------------- + +class TestMacros: + def test_defmacro_and_expand(self): + env = {} + ev("(defmacro unless (test body) (list (quote if) (list (quote not) test) body))", env) + result = ev('(unless false "ran")', env) + assert result == "ran"