#!/usr/bin/env python3 """ Bootstrap compiler: test.sx -> Node.js test script. Reads test.sx and emits a standalone JavaScript file that runs each deftest case using sx-browser.js's evaluator. Reports results as TAP. Uses the bootstrapped sx-browser.js (from the spec), NOT sx.js. Usage: python bootstrap_test_js.py --output shared/sx/tests/test_sx_spec.js node shared/sx/tests/test_sx_spec.js """ from __future__ import annotations import os import sys import tempfile import argparse _HERE = os.path.dirname(os.path.abspath(__file__)) _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", "..")) sys.path.insert(0, _PROJECT) from shared.sx.ref.bootstrap_test import _parse_test_sx, _sx_to_source, _slugify def _js_escape(s: str) -> str: """Escape a string for embedding in JS single-quoted string.""" return s.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') def _emit_js(suites: list[dict], preamble: list) -> str: """Emit a Node.js test script from parsed suites.""" preamble_sx = "\\n".join(_js_escape(_sx_to_source(expr)) for expr in preamble) lines = [] lines.append('// Auto-generated from test.sx — SX spec self-tests for JavaScript.') lines.append('//') lines.append('// DO NOT EDIT. Regenerate with:') lines.append('// python shared/sx/ref/bootstrap_test_js.py --output shared/sx/tests/test_sx_spec.js') lines.append('//') lines.append('// Run:') lines.append('// node shared/sx/tests/test_sx_spec.js') lines.append('') lines.append('// --- Load sx-browser.js (bootstrapped from spec) ---') lines.append('Object.defineProperty(globalThis, "document", { value: undefined, writable: true });') lines.append('var _sxPath = require("path").resolve(__dirname, "../../static/scripts/sx-browser.js");') lines.append('var Sx = require(_sxPath);') lines.append('') lines.append('// --- Inject spec primitives into env ---') lines.append('// sx-browser.js has assert but is missing some spec primitives.') lines.append('// We inject them into the test env since PRIMITIVES is not exposed.') lines.append('var NIL = Sx.NIL;') lines.append('function _isNil(x) { return x === NIL || x === null || x === undefined; }') lines.append('function _deepEqual(a, b) {') lines.append(' if (a === b) return true;') lines.append(' if (_isNil(a) && _isNil(b)) return true;') lines.append(' if (typeof a !== typeof b) return false;') lines.append(' if (Array.isArray(a) && Array.isArray(b)) {') lines.append(' if (a.length !== b.length) return false;') lines.append(' for (var i = 0; i < a.length; i++) if (!_deepEqual(a[i], b[i])) return false;') lines.append(' return true;') lines.append(' }') lines.append(' if (a && typeof a === "object" && b && typeof b === "object") {') lines.append(' var ka = Object.keys(a), kb = Object.keys(b);') lines.append(' if (ka.length !== kb.length) return false;') lines.append(' for (var j = 0; j < ka.length; j++) if (!_deepEqual(a[ka[j]], b[ka[j]])) return false;') lines.append(' return true;') lines.append(' }') lines.append(' return false;') lines.append('}') lines.append('') lines.append('// Primitives injected into every test env') lines.append('var _envPrimitives = {') lines.append(' "equal?": function(a, b) { return _deepEqual(a, b); },') lines.append(' "eq?": function(a, b) { return a === b; },') lines.append(' "boolean?": function(x) { return typeof x === "boolean"; },') lines.append(' "string-length": function(s) { return String(s).length; },') lines.append(' "substring": function(s, start, end) { return String(s).slice(start, end); },') lines.append(' "string-contains?": function(s, needle) { return String(s).indexOf(needle) !== -1; },') lines.append(' "upcase": function(s) { return String(s).toUpperCase(); },') lines.append(' "downcase": function(s) { return String(s).toLowerCase(); },') lines.append(' "reverse": function(c) { return c ? c.slice().reverse() : []; },') lines.append(' "flatten": function(c) {') lines.append(' var r = []; for (var i = 0; i < (c||[]).length; i++) {') lines.append(' if (Array.isArray(c[i])) for (var j = 0; j < c[i].length; j++) r.push(c[i][j]);') lines.append(' else r.push(c[i]);') lines.append(' } return r;') lines.append(' },') lines.append(' "has-key?": function(d, k) { return d && typeof d === "object" && k in d; },') lines.append(' // Fix append to concatenate when x is a list') lines.append(' "append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); },') lines.append('};') lines.append('') lines.append('// --- Test infrastructure ---') lines.append('var _passed = 0, _failed = 0, _errors = [];') lines.append('var _testNum = 0;') lines.append('') lines.append('function _makeEnv() {') lines.append(' var env = {};') lines.append(' // Copy injected primitives into env') lines.append(' for (var k in _envPrimitives) env[k] = _envPrimitives[k];') lines.append(f" var src = '{preamble_sx}';") lines.append(' var exprs = Sx.parseAll(src);') lines.append(' for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);') lines.append(' return env;') lines.append('}') lines.append('') lines.append('function _run(name, sxSource) {') lines.append(' _testNum++;') lines.append(' try {') lines.append(' var env = _makeEnv();') lines.append(' var exprs = Sx.parseAll(sxSource);') lines.append(' for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);') lines.append(' _passed++;') lines.append(' console.log("ok " + _testNum + " - " + name);') lines.append(' } catch (e) {') lines.append(' _failed++;') lines.append(' _errors.push({ name: name, error: e.message || String(e) });') lines.append(' console.log("not ok " + _testNum + " - " + name);') lines.append(' console.log(" # " + (e.message || String(e)));') lines.append(' }') lines.append('}') lines.append('') # Count total tests total = _count_tests(suites) lines.append(f'console.log("TAP version 13");') lines.append(f'console.log("1..{total}");') lines.append('') # Emit test calls for suite in suites: _emit_js_suite(suite, lines, prefix="") lines.append('') lines.append('// --- Summary ---') lines.append('console.log("");') lines.append('console.log("# tests " + (_passed + _failed));') lines.append('console.log("# pass " + _passed);') lines.append('if (_failed > 0) {') lines.append(' console.log("# fail " + _failed);') lines.append(' for (var ei = 0; ei < _errors.length; ei++) {') lines.append(' console.log("# FAIL: " + _errors[ei].name + " — " + _errors[ei].error);') lines.append(' }') lines.append(' process.exit(1);') lines.append('}') return "\n".join(lines) def _count_tests(items: list[dict]) -> int: total = 0 for item in items: if item["type"] == "test": total += 1 elif item["type"] == "suite": total += _count_tests(item["tests"]) return total def _emit_js_suite(suite: dict, lines: list[str], prefix: str): """Emit test calls for a suite.""" suite_prefix = f"{prefix}{suite['name']} > " if prefix else f"{suite['name']} > " lines.append(f'// --- {suite["name"]} ---') for item in suite["tests"]: if item["type"] == "test": _emit_js_test(item, lines, suite_prefix) elif item["type"] == "suite": _emit_js_suite(item, lines, suite_prefix) def _emit_js_test(test: dict, lines: list[str], prefix: str): """Emit a single test call.""" body_parts = [_sx_to_source(expr) for expr in test["body"]] if len(body_parts) == 1: sx_source = body_parts[0] else: sx_source = "(do " + " ".join(body_parts) + ")" name = f'{prefix}{test["name"]}' lines.append(f"_run('{_js_escape(name)}', '{_js_escape(sx_source)}');") def main(): parser = argparse.ArgumentParser(description="Bootstrap test.sx to Node.js") parser.add_argument("--output", "-o", help="Output file path") parser.add_argument("--dry-run", action="store_true", help="Print to stdout") args = parser.parse_args() test_sx = os.path.join(_HERE, "test.sx") suites, preamble = _parse_test_sx(test_sx) print(f"Parsed {len(suites)} suites, {len(preamble)} preamble defines from test.sx", file=sys.stderr) total = _count_tests(suites) print(f"Total test cases: {total}", file=sys.stderr) output = _emit_js(suites, preamble) if args.output and not args.dry_run: with open(args.output, "w") as f: f.write(output) print(f"Wrote {args.output}", file=sys.stderr) else: print(output) if __name__ == "__main__": main()