From 4534fb9fee6ffd704070f00af1a4bc6f2e4b9a8b Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 5 Mar 2026 19:32:01 +0000 Subject: [PATCH] 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)