diff --git a/shared/sx/ref/bootstrap_test_js.py b/shared/sx/ref/bootstrap_test_js.py deleted file mode 100644 index d468b28..0000000 --- a/shared/sx/ref/bootstrap_test_js.py +++ /dev/null @@ -1,210 +0,0 @@ -#!/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() diff --git a/shared/sx/ref/test.sx b/shared/sx/ref/test.sx index 76d8d93..2f6fd97 100644 --- a/shared/sx/ref/test.sx +++ b/shared/sx/ref/test.sx @@ -1,58 +1,43 @@ ;; ========================================================================== ;; test.sx — Self-hosting SX test framework ;; -;; Defines a minimal test framework in SX that bootstraps to every host. -;; Tests are written in SX and verify SX semantics — the language tests -;; itself. Bootstrap compilers emit native test runners from this spec. -;; -;; The framework uses only primitives already in primitives.sx: -;; assert, equal?, type-of, str, list, len, error -;; -;; Usage: -;; (defsuite "Suite name" -;; (deftest "test name" (assert (equal? 1 1))) -;; (deftest "another" (assert (= (+ 1 2) 3)))) +;; Defines a minimal test framework in SX that tests SX — the language +;; proves its own correctness. The framework is self-executing: any host +;; that provides 5 platform functions can evaluate this file directly. ;; ;; Platform functions required: -;; (test-report suite-name results) → platform-specific output -;; results is a list of {:name "..." :passed bool :error "..."|nil} +;; try-call (thunk) → {:ok true} | {:ok false :error "msg"} +;; report-pass (name) → platform-specific pass output +;; report-fail (name error) → platform-specific fail output +;; push-suite (name) → push suite name onto context stack +;; pop-suite () → pop suite name from context stack ;; -;; Bootstrap compilers read this file and: -;; 1. Emit the test runner infrastructure -;; 2. Emit each test suite as native test cases -;; 3. Wire up to the platform test framework (pytest, Jest, etc.) +;; Usage: +;; ;; Host injects platform functions into env, then: +;; (eval-file "test.sx" env) +;; +;; The same test.sx runs on every host — Python, JavaScript, etc. ;; ========================================================================== ;; -------------------------------------------------------------------------- -;; 1. Test framework forms +;; 1. Test framework macros ;; -------------------------------------------------------------------------- ;; -;; deftest and defsuite are declarative — bootstrap compilers parse them -;; and emit native test functions. They are NOT runtime macros. -;; -;; (deftest "name" body ...) -;; Declares a test case. Body expressions are evaluated sequentially. -;; The test passes if no assertion fails (no error is raised). -;; Uses `assert` primitive for assertions. -;; -;; (defsuite "name" test ...) -;; Groups tests into a named suite. Suites can be nested. +;; deftest and defsuite are macros that make test.sx directly executable. +;; The host provides try-call (error catching), reporting, and suite +;; context — everything else is pure SX. -;; (define-special-form "deftest" -;; :syntax (deftest name body ...) -;; :doc "Declare a test case. Name is a string literal. -;; Body expressions are evaluated in a fresh env extended from -;; the suite env. Test passes if all assertions succeed." -;; :example '(deftest "addition" (assert (= (+ 1 2) 3)))) -;; -;; (define-special-form "defsuite" -;; :syntax (defsuite name tests ...) -;; :doc "Declare a test suite. Name is a string. Tests are deftest forms -;; or nested defsuite forms." -;; :example '(defsuite "arithmetic" -;; (deftest "add" (assert (= (+ 1 2) 3))) -;; (deftest "sub" (assert (= (- 5 3) 2))))) +(defmacro deftest (name &rest body) + `(let ((result (try-call (fn () ,@body)))) + (if (get result "ok") + (report-pass ,name) + (report-fail ,name (get result "error"))))) + +(defmacro defsuite (name &rest items) + `(do (push-suite ,name) + ,@items + (pop-suite))) ;; -------------------------------------------------------------------------- @@ -113,9 +98,9 @@ (define assert-throws (fn (thunk) - ;; Platform must implement try-catch or equivalent. - ;; Bootstrap compilers emit this as a native try/catch block. - (platform-assert-throws thunk))) + (let ((result (try-call thunk))) + (assert (not (get result "ok")) + "Expected an error to be thrown but none was")))) ;; ========================================================================== diff --git a/shared/sx/tests/run.js b/shared/sx/tests/run.js new file mode 100644 index 0000000..ec77792 --- /dev/null +++ b/shared/sx/tests/run.js @@ -0,0 +1,108 @@ +// Run test.sx directly against sx-browser.js. +// +// sx-browser.js parses and evaluates test.sx — SX tests itself. +// This script provides only platform functions (error catching, reporting). +// +// Usage: node shared/sx/tests/run.js + +Object.defineProperty(globalThis, "document", { value: undefined, writable: true }); +var path = require("path"); +var fs = require("fs"); +var Sx = require(path.resolve(__dirname, "../../static/scripts/sx-browser.js")); + +// --- Test state --- +var suiteStack = []; +var passed = 0, failed = 0, testNum = 0; + +// --- Helpers --- +function isNil(x) { return x === Sx.NIL || x === null || x === undefined; } + +function deepEqual(a, b) { + if (a === b) return true; + if (isNil(a) && isNil(b)) return true; + if (typeof a !== typeof b) return false; + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + for (var i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false; + return true; + } + if (a && typeof a === "object" && b && typeof b === "object") { + var ka = Object.keys(a), kb = Object.keys(b); + if (ka.length !== kb.length) return false; + for (var j = 0; j < ka.length; j++) if (!deepEqual(a[ka[j]], b[ka[j]])) return false; + return true; + } + return false; +} + +// --- Platform functions injected into the SX env --- +var env = { + // Error catching — calls an SX thunk, returns result dict + "try-call": function(thunk) { + try { + Sx.eval([thunk], env); + return { ok: true }; + } catch(e) { + return { ok: false, error: e.message || String(e) }; + } + }, + + // Test reporting + "report-pass": function(name) { + testNum++; + passed++; + var fullName = suiteStack.concat([name]).join(" > "); + console.log("ok " + testNum + " - " + fullName); + }, + "report-fail": function(name, error) { + testNum++; + failed++; + var fullName = suiteStack.concat([name]).join(" > "); + console.log("not ok " + testNum + " - " + fullName); + console.log(" # " + error); + }, + + // Suite context + "push-suite": function(name) { suiteStack.push(name); }, + "pop-suite": function() { suiteStack.pop(); }, + + // Primitives that sx-browser.js has internally but doesn't expose through env + "equal?": function(a, b) { return deepEqual(a, b); }, + "eq?": function(a, b) { return a === b; }, + "boolean?": function(x) { return typeof x === "boolean"; }, + "string-length": function(s) { return String(s).length; }, + "substring": function(s, start, end) { return String(s).slice(start, end); }, + "string-contains?": function(s, needle) { return String(s).indexOf(needle) !== -1; }, + "upcase": function(s) { return String(s).toUpperCase(); }, + "downcase": function(s) { return String(s).toLowerCase(); }, + "reverse": function(c) { return c ? c.slice().reverse() : []; }, + "flatten": function(c) { + var r = []; + for (var i = 0; i < (c||[]).length; i++) { + if (Array.isArray(c[i])) for (var j = 0; j < c[i].length; j++) r.push(c[i][j]); + else r.push(c[i]); + } + return r; + }, + "has-key?": function(d, k) { return d && typeof d === "object" && k in d; }, + "append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); }, +}; + +// --- Read and evaluate test.sx --- +var src = fs.readFileSync(path.resolve(__dirname, "../ref/test.sx"), "utf8"); +var exprs = Sx.parseAll(src); + +console.log("TAP version 13"); +for (var i = 0; i < exprs.length; i++) { + Sx.eval(exprs[i], env); +} + +// --- Summary --- +console.log(""); +console.log("1.." + testNum); +console.log("# tests " + (passed + failed)); +console.log("# pass " + passed); +if (failed > 0) { + console.log("# fail " + failed); + process.exit(1); +} diff --git a/shared/sx/tests/run.py b/shared/sx/tests/run.py new file mode 100644 index 0000000..3fda836 --- /dev/null +++ b/shared/sx/tests/run.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Run test.sx directly against the Python SX evaluator. + +The Python evaluator parses and evaluates test.sx — SX tests itself. +This script provides only platform functions (error catching, reporting). + +Usage: python shared/sx/tests/run.py +""" +from __future__ import annotations + +import os +import sys +import traceback + +_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.evaluator import _eval, _trampoline + +# --- Test state --- +suite_stack: list[str] = [] +passed = 0 +failed = 0 +test_num = 0 + + +def try_call(thunk): + """Call an SX thunk, catching errors.""" + try: + _trampoline(_eval([thunk], {})) + return {"ok": True} + except Exception as e: + return {"ok": False, "error": str(e)} + + +def report_pass(name): + global passed, test_num + test_num += 1 + passed += 1 + full_name = " > ".join(suite_stack + [name]) + print(f"ok {test_num} - {full_name}") + + +def report_fail(name, error): + global failed, test_num + test_num += 1 + failed += 1 + full_name = " > ".join(suite_stack + [name]) + print(f"not ok {test_num} - {full_name}") + print(f" # {error}") + + +def push_suite(name): + suite_stack.append(name) + + +def pop_suite(): + suite_stack.pop() + + +def main(): + env = { + "try-call": try_call, + "report-pass": report_pass, + "report-fail": report_fail, + "push-suite": push_suite, + "pop-suite": pop_suite, + } + + test_sx = os.path.join(_HERE, "..", "ref", "test.sx") + with open(test_sx) as f: + src = f.read() + + exprs = parse_all(src) + + print("TAP version 13") + for expr in exprs: + _trampoline(_eval(expr, env)) + + print() + print(f"1..{test_num}") + print(f"# tests {passed + failed}") + print(f"# pass {passed}") + if failed > 0: + print(f"# fail {failed}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/shared/sx/tests/test_sx_spec.js b/shared/sx/tests/test_sx_spec.js deleted file mode 100644 index e0ea372..0000000 --- a/shared/sx/tests/test_sx_spec.js +++ /dev/null @@ -1,199 +0,0 @@ -// Auto-generated from test.sx — SX spec self-tests for JavaScript. -// -// DO NOT EDIT. Regenerate with: -// python shared/sx/ref/bootstrap_test_js.py --output shared/sx/tests/test_sx_spec.js -// -// Run: -// node shared/sx/tests/test_sx_spec.js - -// --- Load sx-browser.js (bootstrapped from spec) --- -Object.defineProperty(globalThis, "document", { value: undefined, writable: true }); -var _sxPath = require("path").resolve(__dirname, "../../static/scripts/sx-browser.js"); -var Sx = require(_sxPath); - -// --- Inject spec primitives into env --- -// sx-browser.js has assert but is missing some spec primitives. -// We inject them into the test env since PRIMITIVES is not exposed. -var NIL = Sx.NIL; -function _isNil(x) { return x === NIL || x === null || x === undefined; } -function _deepEqual(a, b) { - if (a === b) return true; - if (_isNil(a) && _isNil(b)) return true; - if (typeof a !== typeof b) return false; - if (Array.isArray(a) && Array.isArray(b)) { - if (a.length !== b.length) return false; - for (var i = 0; i < a.length; i++) if (!_deepEqual(a[i], b[i])) return false; - return true; - } - if (a && typeof a === "object" && b && typeof b === "object") { - var ka = Object.keys(a), kb = Object.keys(b); - if (ka.length !== kb.length) return false; - for (var j = 0; j < ka.length; j++) if (!_deepEqual(a[ka[j]], b[ka[j]])) return false; - return true; - } - return false; -} - -// Primitives injected into every test env -var _envPrimitives = { - "equal?": function(a, b) { return _deepEqual(a, b); }, - "eq?": function(a, b) { return a === b; }, - "boolean?": function(x) { return typeof x === "boolean"; }, - "string-length": function(s) { return String(s).length; }, - "substring": function(s, start, end) { return String(s).slice(start, end); }, - "string-contains?": function(s, needle) { return String(s).indexOf(needle) !== -1; }, - "upcase": function(s) { return String(s).toUpperCase(); }, - "downcase": function(s) { return String(s).toLowerCase(); }, - "reverse": function(c) { return c ? c.slice().reverse() : []; }, - "flatten": function(c) { - var r = []; for (var i = 0; i < (c||[]).length; i++) { - if (Array.isArray(c[i])) for (var j = 0; j < c[i].length; j++) r.push(c[i][j]); - else r.push(c[i]); - } return r; - }, - "has-key?": function(d, k) { return d && typeof d === "object" && k in d; }, - // Fix append to concatenate when x is a list - "append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); }, -}; - -// --- Test infrastructure --- -var _passed = 0, _failed = 0, _errors = []; -var _testNum = 0; - -function _makeEnv() { - var env = {}; - // Copy injected primitives into env - for (var k in _envPrimitives) env[k] = _envPrimitives[k]; - var src = '(define assert-equal (fn (expected actual) (assert (equal? expected actual) (str "Expected " (str expected) " but got " (str actual)))))\n(define assert-not-equal (fn (a b) (assert (not (equal? a b)) (str "Expected values to differ but both are " (str a)))))\n(define assert-true (fn (val) (assert val (str "Expected truthy but got " (str val)))))\n(define assert-false (fn (val) (assert (not val) (str "Expected falsy but got " (str val)))))\n(define assert-nil (fn (val) (assert (nil? val) (str "Expected nil but got " (str val)))))\n(define assert-type (fn (expected-type val) (let ((actual-type (if (nil? val) "nil" (if (boolean? val) "boolean" (if (number? val) "number" (if (string? val) "string" (if (list? val) "list" (if (dict? val) "dict" "unknown")))))))) (assert (= expected-type actual-type) (str "Expected type " expected-type " but got " actual-type)))))\n(define assert-length (fn (expected-len col) (assert (= (len col) expected-len) (str "Expected length " expected-len " but got " (len col)))))\n(define assert-contains (fn (item col) (assert (some (fn (x) (equal? x item)) col) (str "Expected collection to contain " (str item)))))\n(define assert-throws (fn (thunk) (platform-assert-throws thunk)))'; - var exprs = Sx.parseAll(src); - for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env); - return env; -} - -function _run(name, sxSource) { - _testNum++; - try { - var env = _makeEnv(); - var exprs = Sx.parseAll(sxSource); - for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env); - _passed++; - console.log("ok " + _testNum + " - " + name); - } catch (e) { - _failed++; - _errors.push({ name: name, error: e.message || String(e) }); - console.log("not ok " + _testNum + " - " + name); - console.log(" # " + (e.message || String(e))); - } -} - -console.log("TAP version 13"); -console.log("1..81"); - -// --- literals --- -_run('literals > numbers are numbers', '(do (assert-type "number" 42) (assert-type "number" 3.14) (assert-type "number" -1))'); -_run('literals > strings are strings', '(do (assert-type "string" "hello") (assert-type "string" ""))'); -_run('literals > booleans are booleans', '(do (assert-type "boolean" true) (assert-type "boolean" false))'); -_run('literals > nil is nil', '(do (assert-type "nil" nil) (assert-nil nil))'); -_run('literals > lists are lists', '(do (assert-type "list" (list 1 2 3)) (assert-type "list" (list)))'); -_run('literals > dicts are dicts', '(assert-type "dict" {:a 1 :b 2})'); -// --- arithmetic --- -_run('arithmetic > addition', '(do (assert-equal 3 (+ 1 2)) (assert-equal 0 (+ 0 0)) (assert-equal -1 (+ 1 -2)) (assert-equal 10 (+ 1 2 3 4)))'); -_run('arithmetic > subtraction', '(do (assert-equal 1 (- 3 2)) (assert-equal -1 (- 2 3)))'); -_run('arithmetic > multiplication', '(do (assert-equal 6 (* 2 3)) (assert-equal 0 (* 0 100)) (assert-equal 24 (* 1 2 3 4)))'); -_run('arithmetic > division', '(do (assert-equal 2 (/ 6 3)) (assert-equal 2.5 (/ 5 2)))'); -_run('arithmetic > modulo', '(do (assert-equal 1 (mod 7 3)) (assert-equal 0 (mod 6 3)))'); -// --- comparison --- -_run('comparison > equality', '(do (assert-true (= 1 1)) (assert-false (= 1 2)) (assert-true (= "a" "a")) (assert-false (= "a" "b")))'); -_run('comparison > deep equality', '(do (assert-true (equal? (list 1 2 3) (list 1 2 3))) (assert-false (equal? (list 1 2) (list 1 3))) (assert-true (equal? {:a 1} {:a 1})) (assert-false (equal? {:a 1} {:a 2})))'); -_run('comparison > ordering', '(do (assert-true (< 1 2)) (assert-false (< 2 1)) (assert-true (> 2 1)) (assert-true (<= 1 1)) (assert-true (<= 1 2)) (assert-true (>= 2 2)) (assert-true (>= 3 2)))'); -// --- strings --- -_run('strings > str concatenation', '(do (assert-equal "abc" (str "a" "b" "c")) (assert-equal "hello world" (str "hello" " " "world")) (assert-equal "42" (str 42)) (assert-equal "" (str)))'); -_run('strings > string-length', '(do (assert-equal 5 (string-length "hello")) (assert-equal 0 (string-length "")))'); -_run('strings > substring', '(do (assert-equal "ell" (substring "hello" 1 4)) (assert-equal "hello" (substring "hello" 0 5)))'); -_run('strings > string-contains?', '(do (assert-true (string-contains? "hello world" "world")) (assert-false (string-contains? "hello" "xyz")))'); -_run('strings > upcase and downcase', '(do (assert-equal "HELLO" (upcase "hello")) (assert-equal "hello" (downcase "HELLO")))'); -_run('strings > trim', '(do (assert-equal "hello" (trim " hello ")) (assert-equal "hello" (trim "hello")))'); -_run('strings > split and join', '(do (assert-equal (list "a" "b" "c") (split "a,b,c" ",")) (assert-equal "a-b-c" (join "-" (list "a" "b" "c"))))'); -// --- lists --- -_run('lists > constructors', '(do (assert-equal (list 1 2 3) (list 1 2 3)) (assert-equal (list) (list)) (assert-length 3 (list 1 2 3)))'); -_run('lists > first and rest', '(do (assert-equal 1 (first (list 1 2 3))) (assert-equal (list 2 3) (rest (list 1 2 3))) (assert-nil (first (list))) (assert-equal (list) (rest (list))))'); -_run('lists > nth', '(do (assert-equal 1 (nth (list 1 2 3) 0)) (assert-equal 2 (nth (list 1 2 3) 1)) (assert-equal 3 (nth (list 1 2 3) 2)))'); -_run('lists > last', '(do (assert-equal 3 (last (list 1 2 3))) (assert-nil (last (list))))'); -_run('lists > cons and append', '(do (assert-equal (list 0 1 2) (cons 0 (list 1 2))) (assert-equal (list 1 2 3 4) (append (list 1 2) (list 3 4))))'); -_run('lists > reverse', '(do (assert-equal (list 3 2 1) (reverse (list 1 2 3))) (assert-equal (list) (reverse (list))))'); -_run('lists > empty?', '(do (assert-true (empty? (list))) (assert-false (empty? (list 1))))'); -_run('lists > len', '(do (assert-equal 0 (len (list))) (assert-equal 3 (len (list 1 2 3))))'); -_run('lists > contains?', '(do (assert-true (contains? (list 1 2 3) 2)) (assert-false (contains? (list 1 2 3) 4)))'); -_run('lists > flatten', '(assert-equal (list 1 2 3 4) (flatten (list (list 1 2) (list 3 4))))'); -// --- dicts --- -_run('dicts > dict literal', '(do (assert-type "dict" {:a 1 :b 2}) (assert-equal 1 (get {:a 1} "a")) (assert-equal 2 (get {:a 1 :b 2} "b")))'); -_run('dicts > assoc', '(do (assert-equal {:a 1 :b 2} (assoc {:a 1} "b" 2)) (assert-equal {:a 99} (assoc {:a 1} "a" 99)))'); -_run('dicts > dissoc', '(assert-equal {:b 2} (dissoc {:a 1 :b 2} "a"))'); -_run('dicts > keys and vals', '(let ((d {:a 1 :b 2})) (assert-length 2 (keys d)) (assert-length 2 (vals d)) (assert-contains "a" (keys d)) (assert-contains "b" (keys d)))'); -_run('dicts > has-key?', '(do (assert-true (has-key? {:a 1} "a")) (assert-false (has-key? {:a 1} "b")))'); -_run('dicts > merge', '(do (assert-equal {:a 1 :b 2 :c 3} (merge {:a 1 :b 2} {:c 3})) (assert-equal {:a 99 :b 2} (merge {:a 1 :b 2} {:a 99})))'); -// --- predicates --- -_run('predicates > nil?', '(do (assert-true (nil? nil)) (assert-false (nil? 0)) (assert-false (nil? false)) (assert-false (nil? "")))'); -_run('predicates > number?', '(do (assert-true (number? 42)) (assert-true (number? 3.14)) (assert-false (number? "42")))'); -_run('predicates > string?', '(do (assert-true (string? "hello")) (assert-false (string? 42)))'); -_run('predicates > list?', '(do (assert-true (list? (list 1 2))) (assert-false (list? "not a list")))'); -_run('predicates > dict?', '(do (assert-true (dict? {:a 1})) (assert-false (dict? (list 1))))'); -_run('predicates > boolean?', '(do (assert-true (boolean? true)) (assert-true (boolean? false)) (assert-false (boolean? nil)) (assert-false (boolean? 0)))'); -_run('predicates > not', '(do (assert-true (not false)) (assert-true (not nil)) (assert-false (not true)) (assert-false (not 1)) (assert-false (not "x")))'); -// --- special-forms --- -_run('special-forms > if', '(do (assert-equal "yes" (if true "yes" "no")) (assert-equal "no" (if false "yes" "no")) (assert-equal "no" (if nil "yes" "no")) (assert-nil (if false "yes")))'); -_run('special-forms > when', '(do (assert-equal "yes" (when true "yes")) (assert-nil (when false "yes")))'); -_run('special-forms > cond', '(do (assert-equal "a" (cond true "a" :else "b")) (assert-equal "b" (cond false "a" :else "b")) (assert-equal "c" (cond false "a" false "b" :else "c")))'); -_run('special-forms > and', '(do (assert-true (and true true)) (assert-false (and true false)) (assert-false (and false true)) (assert-equal 3 (and 1 2 3)))'); -_run('special-forms > or', '(do (assert-equal 1 (or 1 2)) (assert-equal 2 (or false 2)) (assert-equal "fallback" (or nil false "fallback")) (assert-false (or false false)))'); -_run('special-forms > let', '(do (assert-equal 3 (let ((x 1) (y 2)) (+ x y))) (assert-equal "hello world" (let ((a "hello") (b " world")) (str a b))))'); -_run('special-forms > let clojure-style', '(assert-equal 3 (let (x 1 y 2) (+ x y)))'); -_run('special-forms > do / begin', '(do (assert-equal 3 (do 1 2 3)) (assert-equal "last" (begin "first" "middle" "last")))'); -_run('special-forms > define', '(do (define x 42) (assert-equal 42 x))'); -_run('special-forms > set!', '(do (define x 1) (set! x 2) (assert-equal 2 x))'); -// --- lambdas --- -_run('lambdas > basic lambda', '(let ((add (fn (a b) (+ a b)))) (assert-equal 3 (add 1 2)))'); -_run('lambdas > closure captures env', '(let ((x 10)) (let ((add-x (fn (y) (+ x y)))) (assert-equal 15 (add-x 5))))'); -_run('lambdas > lambda as argument', '(assert-equal (list 2 4 6) (map (fn (x) (* x 2)) (list 1 2 3)))'); -_run('lambdas > recursive lambda via define', '(do (define factorial (fn (n) (if (<= n 1) 1 (* n (factorial (- n 1)))))) (assert-equal 120 (factorial 5)))'); -_run('lambdas > higher-order returns lambda', '(let ((make-adder (fn (n) (fn (x) (+ n x))))) (let ((add5 (make-adder 5))) (assert-equal 8 (add5 3))))'); -// --- higher-order --- -_run('higher-order > map', '(do (assert-equal (list 2 4 6) (map (fn (x) (* x 2)) (list 1 2 3))) (assert-equal (list) (map (fn (x) x) (list))))'); -_run('higher-order > filter', '(do (assert-equal (list 2 4) (filter (fn (x) (= (mod x 2) 0)) (list 1 2 3 4))) (assert-equal (list) (filter (fn (x) false) (list 1 2 3))))'); -_run('higher-order > reduce', '(do (assert-equal 10 (reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3 4))) (assert-equal 0 (reduce (fn (acc x) (+ acc x)) 0 (list))))'); -_run('higher-order > some', '(do (assert-true (some (fn (x) (> x 3)) (list 1 2 3 4 5))) (assert-false (some (fn (x) (> x 10)) (list 1 2 3))))'); -_run('higher-order > every?', '(do (assert-true (every? (fn (x) (> x 0)) (list 1 2 3))) (assert-false (every? (fn (x) (> x 2)) (list 1 2 3))))'); -_run('higher-order > map-indexed', '(assert-equal (list "0:a" "1:b" "2:c") (map-indexed (fn (i x) (str i ":" x)) (list "a" "b" "c")))'); -// --- components --- -_run('components > defcomp creates component', '(do (defcomp ~test-comp (&key title) (div title)) (assert-true (not (nil? ~test-comp))))'); -_run('components > component renders with keyword args', '(do (defcomp ~greeting (&key name) (span (str "Hello, " name "!"))) (assert-true (not (nil? ~greeting))))'); -_run('components > component with children', '(do (defcomp ~box (&key &rest children) (div :class "box" children)) (assert-true (not (nil? ~box))))'); -_run('components > component with default via or', '(do (defcomp ~label (&key text) (span (or text "default"))) (assert-true (not (nil? ~label))))'); -// --- macros --- -_run('macros > defmacro creates macro', '(do (defmacro unless (cond &rest body) (quasiquote (if (not (unquote cond)) (do (splice-unquote body))))) (assert-equal "yes" (unless false "yes")) (assert-nil (unless true "no")))'); -_run('macros > quasiquote and unquote', '(let ((x 42)) (assert-equal (list 1 42 3) (quasiquote (1 (unquote x) 3))))'); -_run('macros > splice-unquote', '(let ((xs (list 2 3 4))) (assert-equal (list 1 2 3 4 5) (quasiquote (1 (splice-unquote xs) 5))))'); -// --- threading --- -_run('threading > thread-first', '(do (assert-equal 8 (-> 5 (+ 1) (+ 2))) (assert-equal "HELLO" (-> "hello" upcase)) (assert-equal "HELLO WORLD" (-> "hello" (str " world") upcase)))'); -// --- truthiness --- -_run('truthiness > truthy values', '(do (assert-true (if 1 true false)) (assert-true (if "x" true false)) (assert-true (if (list 1) true false)) (assert-true (if true true false)))'); -_run('truthiness > falsy values', '(do (assert-false (if false true false)) (assert-false (if nil true false)))'); -// --- edge-cases --- -_run('edge-cases > nested let scoping', '(let ((x 1)) (let ((x 2)) (assert-equal 2 x)))'); -_run('edge-cases > recursive map', '(assert-equal (list (list 2 4) (list 6 8)) (map (fn (sub) (map (fn (x) (* x 2)) sub)) (list (list 1 2) (list 3 4))))'); -_run('edge-cases > keyword as value', '(do (assert-equal "class" :class) (assert-equal "id" :id))'); -_run('edge-cases > dict with evaluated values', '(let ((x 42)) (assert-equal 42 (get {:val x} "val")))'); -_run('edge-cases > nil propagation', '(do (assert-nil (get {:a 1} "missing")) (assert-equal "default" (or (get {:a 1} "missing") "default")))'); -_run('edge-cases > empty operations', '(do (assert-equal (list) (map (fn (x) x) (list))) (assert-equal (list) (filter (fn (x) true) (list))) (assert-equal 0 (reduce (fn (acc x) (+ acc x)) 0 (list))) (assert-equal 0 (len (list))) (assert-equal "" (str)))'); - -// --- Summary --- -console.log(""); -console.log("# tests " + (_passed + _failed)); -console.log("# pass " + _passed); -if (_failed > 0) { - console.log("# fail " + _failed); - for (var ei = 0; ei < _errors.length; ei++) { - console.log("# FAIL: " + _errors[ei].name + " — " + _errors[ei].error); - } - process.exit(1); -} \ No newline at end of file diff --git a/shared/sx/tests/test_sx_spec.py b/shared/sx/tests/test_sx_spec.py index d5893a4..db885cc 100644 --- a/shared/sx/tests/test_sx_spec.py +++ b/shared/sx/tests/test_sx_spec.py @@ -18,7 +18,7 @@ _PREAMBLE = '''(define assert-equal (fn (expected actual) (assert (equal? expect (define assert-type (fn (expected-type val) (let ((actual-type (if (nil? val) "nil" (if (boolean? val) "boolean" (if (number? val) "number" (if (string? val) "string" (if (list? val) "list" (if (dict? val) "dict" "unknown")))))))) (assert (= expected-type actual-type) (str "Expected type " expected-type " but got " actual-type))))) (define assert-length (fn (expected-len col) (assert (= (len col) expected-len) (str "Expected length " expected-len " but got " (len col))))) (define assert-contains (fn (item col) (assert (some (fn (x) (equal? x item)) col) (str "Expected collection to contain " (str item))))) -(define assert-throws (fn (thunk) (platform-assert-throws thunk)))''' +(define assert-throws (fn (thunk) (let ((result (try-call thunk))) (assert (not (get result "ok")) "Expected an error to be thrown but none was"))))''' def _make_env() -> dict: diff --git a/sx/sx/testing.sx b/sx/sx/testing.sx index eb20dff..0a56288 100644 --- a/sx/sx/testing.sx +++ b/sx/sx/testing.sx @@ -7,49 +7,69 @@ ;; Intro (div :class "space-y-4" (p :class "text-lg text-stone-600" - "SX tests itself. The test spec is written in SX and defines a complete test framework — assertion helpers, test suites, and 81 test cases covering every language feature. Bootstrap compilers read " + "SX tests itself. " (code :class "text-violet-700 text-sm" "test.sx") - " and emit native test runners for each host platform.") + " is a self-executing test spec — it defines " + (code :class "text-violet-700 text-sm" "deftest") + " and " + (code :class "text-violet-700 text-sm" "defsuite") + " as macros, writes 81 test cases, and runs them. Any host that provides five platform functions can evaluate the file directly.") (p :class "text-stone-600" "This is not a test " (em "of") " SX — it is a test " (em "in") " SX. The same s-expressions that define how " (code :class "text-violet-700 text-sm" "if") " works are used to verify that " (code :class "text-violet-700 text-sm" "if") - " works. The language proves its own correctness, on every platform it compiles to.")) + " works. No code generation, no intermediate files — the evaluator runs the spec.")) ;; How it works (div :class "space-y-3" (h2 :class "text-2xl font-semibold text-stone-800" "Architecture") + (p :class "text-stone-600" + "The test framework needs five platform functions. Everything else — macros, assertion helpers, test suites — is pure SX:") (div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl" (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words font-mono text-stone-700" -"test.sx The spec: assertion helpers + 15 suites + 81 tests +"test.sx Self-executing: macros + helpers + 81 tests | - |--- bootstrap_test.py Reads test.sx, emits pytest module + |--- run.js Injects 5 platform fns, evaluates test.sx | | - | +-> test_sx_spec.py 81 pytest test cases - | | - | +-> shared/sx/evaluator.py (Python SX evaluator) + | +-> sx-browser.js JS evaluator (bootstrapped from spec) | - |--- bootstrap_test_js.py Reads test.sx, emits Node.js TAP script + |--- run.py Injects 5 platform fns, evaluates test.sx | - +-> test_sx_spec.js 81 TAP test cases - | - +-> sx-browser.js (JS SX evaluator)"))) + +-> evaluator.py Python evaluator + +Platform functions: + try-call (thunk) -> {:ok true} | {:ok false :error \"msg\"} + report-pass (name) -> output pass + report-fail (name error) -> output fail + push-suite (name) -> push suite context + pop-suite () -> pop suite context"))) ;; Framework (div :class "space-y-3" (h2 :class "text-2xl font-semibold text-stone-800" "The test framework") (p :class "text-stone-600" - "The framework defines two declarative forms and nine assertion helpers, all in pure SX:") + "The framework defines two macros and nine assertion helpers, all in SX. The macros are the key — they make " + (code :class "text-violet-700 text-sm" "defsuite") + " and " + (code :class "text-violet-700 text-sm" "deftest") + " executable forms, not just declarations:") (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" - (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Forms") + (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Macros") (~doc-code :code - (highlight "(defsuite \"name\" ...tests)\n(deftest \"name\" ...body)" "lisp"))) + (highlight "(defmacro deftest (name &rest body)\n `(let ((result (try-call (fn () ,@body))))\n (if (get result \"ok\")\n (report-pass ,name)\n (report-fail ,name (get result \"error\")))))\n\n(defmacro defsuite (name &rest items)\n `(do (push-suite ,name)\n ,@items\n (pop-suite)))" "lisp"))) + (p :class "text-stone-600 text-sm" + (code :class "text-violet-700 text-sm" "deftest") + " wraps the body in a thunk, passes it to " + (code :class "text-violet-700 text-sm" "try-call") + " (the one platform function that catches errors), then reports pass or fail. " + (code :class "text-violet-700 text-sm" "defsuite") + " pushes a name onto the context stack, runs its children, and pops.") (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Assertion helpers") (~doc-code :code - (highlight "(define assert-equal\n (fn (expected actual)\n (assert (equal? expected actual)\n (str \"Expected \" (str expected) \" but got \" (str actual)))))\n\n(define assert-true (fn (val) (assert val ...)))\n(define assert-false (fn (val) (assert (not val) ...)))\n(define assert-nil (fn (val) (assert (nil? val) ...)))\n(define assert-type (fn (expected-type val) ...))\n(define assert-length (fn (expected-len col) ...))\n(define assert-contains (fn (item col) ...))" "lisp")))) + (highlight "(define assert-equal\n (fn (expected actual)\n (assert (equal? expected actual)\n (str \"Expected \" (str expected) \" but got \" (str actual)))))\n\n(define assert-true (fn (val) (assert val ...)))\n(define assert-false (fn (val) (assert (not val) ...)))\n(define assert-nil (fn (val) (assert (nil? val) ...)))\n(define assert-type (fn (expected-type val) ...))\n(define assert-length (fn (expected-len col) ...))\n(define assert-contains (fn (item col) ...))\n(define assert-throws (fn (thunk) ...))" "lisp")))) ;; Example tests (div :class "space-y-3" @@ -61,54 +81,49 @@ (~doc-code :code (highlight "(defsuite \"arithmetic\"\n (deftest \"addition\"\n (assert-equal 3 (+ 1 2))\n (assert-equal 0 (+ 0 0))\n (assert-equal -1 (+ 1 -2))\n (assert-equal 10 (+ 1 2 3 4)))\n\n (deftest \"subtraction\"\n (assert-equal 1 (- 3 2))\n (assert-equal -1 (- 2 3)))\n\n (deftest \"multiplication\"\n (assert-equal 6 (* 2 3))\n (assert-equal 0 (* 0 100))\n (assert-equal 24 (* 1 2 3 4)))\n\n (deftest \"division\"\n (assert-equal 2 (/ 6 3))\n (assert-equal 2.5 (/ 5 2)))\n\n (deftest \"modulo\"\n (assert-equal 1 (mod 7 3))\n (assert-equal 0 (mod 6 3))))" "lisp")))) - ;; Bootstrapping to Python + ;; Running tests — JS (div :class "space-y-3" - (h2 :class "text-2xl font-semibold text-stone-800" "Bootstrap to Python") + (h2 :class "text-2xl font-semibold text-stone-800" "JavaScript: direct evaluation") (p :class "text-stone-600" - (code :class "text-violet-700 text-sm" "bootstrap_test.py") - " parses " - (code :class "text-violet-700 text-sm" "test.sx") - ", extracts the " - (code :class "text-violet-700 text-sm" "define") - " forms (assertion helpers) as a preamble, then emits each " - (code :class "text-violet-700 text-sm" "defsuite") - " as a pytest class and each " - (code :class "text-violet-700 text-sm" "deftest") - " as a test method.") - (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" - (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Generated Python") - (~doc-code :code - (highlight "# Auto-generated from test.sx\nfrom shared.sx.parser import parse_all\nfrom shared.sx.evaluator import _eval, _trampoline\n\ndef _make_env():\n env = {}\n for expr in parse_all(_PREAMBLE):\n _trampoline(_eval(expr, env))\n return env\n\ndef _run(sx_source, env=None):\n if env is None:\n env = _make_env()\n exprs = parse_all(sx_source)\n for expr in exprs:\n result = _trampoline(_eval(expr, env))\n return result\n\nclass TestSpecArithmetic:\n def test_addition(self):\n _run('(do (assert-equal 3 (+ 1 2)) ...)')\n\n def test_subtraction(self):\n _run('(do (assert-equal 1 (- 3 2)) ...)')" "python"))) - (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" - (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Run it") - (~doc-code :code - (highlight "$ python bootstrap_test.py --output test_sx_spec.py\nParsed 15 suites, 9 preamble defines from test.sx\nTotal test cases: 81\n\n$ pytest test_sx_spec.py -v\n81 passed in 0.15s" "bash")))) - - ;; Bootstrapping to JavaScript - (div :class "space-y-3" - (h2 :class "text-2xl font-semibold text-stone-800" "Bootstrap to JavaScript") - (p :class "text-stone-600" - (code :class "text-violet-700 text-sm" "bootstrap_test_js.py") - " emits a standalone Node.js script that loads " (code :class "text-violet-700 text-sm" "sx-browser.js") - " (bootstrapped from the spec), injects any platform-specific primitives into the test env, then runs all 81 tests with TAP output.") + " evaluates " + (code :class "text-violet-700 text-sm" "test.sx") + " directly. The runner injects platform functions and calls " + (code :class "text-violet-700 text-sm" "Sx.eval") + " on each parsed expression:") (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" - (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Generated JavaScript") + (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "run.js") (~doc-code :code - (highlight "// Auto-generated from test.sx\nvar Sx = require('./sx-browser.js');\n\nvar _envPrimitives = {\n 'equal?': function(a, b) { return deepEqual(a, b); },\n 'boolean?': function(x) { return typeof x === 'boolean'; },\n // ... platform primitives injected into env\n};\n\nfunction _makeEnv() {\n var env = {};\n for (var k in _envPrimitives) env[k] = _envPrimitives[k];\n var exprs = Sx.parseAll(PREAMBLE);\n for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);\n return env;\n}\n\nfunction _run(name, sxSource) {\n var env = _makeEnv();\n var exprs = Sx.parseAll(sxSource);\n for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);\n}" "javascript"))) + (highlight "var Sx = require('./sx-browser.js');\nvar src = fs.readFileSync('test.sx', 'utf8');\n\nvar env = {\n 'try-call': function(thunk) {\n try {\n Sx.eval([thunk], env); // call the SX lambda\n return { ok: true };\n } catch(e) {\n return { ok: false, error: e.message };\n }\n },\n 'report-pass': function(name) { console.log('ok - ' + name); },\n 'report-fail': function(name, err) { console.log('not ok - ' + name); },\n 'push-suite': function(n) { stack.push(n); },\n 'pop-suite': function() { stack.pop(); },\n};\n\nvar exprs = Sx.parseAll(src);\nfor (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);" "javascript"))) (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" - (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Run it") + (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Output") (~doc-code :code - (highlight "$ python bootstrap_test_js.py --output test_sx_spec.js\nParsed 15 suites, 9 preamble defines from test.sx\nTotal test cases: 81\n\n$ node test_sx_spec.js\nTAP version 13\n1..81\nok 1 - literals > numbers are numbers\nok 2 - literals > strings are strings\n...\nok 81 - edge-cases > empty operations\n\n# tests 81\n# pass 81" "bash")))) + (highlight "$ node shared/sx/tests/run.js\nTAP version 13\nok 1 - literals > numbers are numbers\nok 2 - literals > strings are strings\n...\nok 81 - edge-cases > empty operations\n\n# tests 81\n# pass 81" "bash")))) + + ;; Running tests — Python + (div :class "space-y-3" + (h2 :class "text-2xl font-semibold text-stone-800" "Python: direct evaluation") + (p :class "text-stone-600" + "Same approach — the Python evaluator runs " + (code :class "text-violet-700 text-sm" "test.sx") + " directly:") + (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" + (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "run.py") + (~doc-code :code + (highlight "from shared.sx.parser import parse_all\nfrom shared.sx.evaluator import _eval, _trampoline\n\ndef try_call(thunk):\n try:\n _trampoline(_eval([thunk], {}))\n return {'ok': True}\n except Exception as e:\n return {'ok': False, 'error': str(e)}\n\nenv = {\n 'try-call': try_call,\n 'report-pass': report_pass,\n 'report-fail': report_fail,\n 'push-suite': push_suite,\n 'pop-suite': pop_suite,\n}\n\nfor expr in parse_all(src):\n _trampoline(_eval(expr, env))" "python"))) + (div :class "rounded-lg border border-stone-200 bg-white p-5 space-y-3" + (h3 :class "text-sm font-medium text-stone-500 uppercase tracking-wide" "Output") + (~doc-code :code + (highlight "$ python shared/sx/tests/run.py\nTAP version 13\nok 1 - literals > numbers are numbers\n...\nok 81 - edge-cases > empty operations\n\n# tests 81\n# pass 81" "bash")))) ;; What it proves (div :class "rounded-lg border border-blue-200 bg-blue-50 p-5 space-y-3" (h2 :class "text-lg font-semibold text-blue-900" "What this proves") (ol :class "list-decimal list-inside text-blue-800 space-y-2 text-sm" - (li "The test spec is " (strong "written in SX") " — the same language it tests") - (li "The same 81 tests run on " (strong "both Python and JavaScript")) - (li "Both hosts produce " (strong "identical results") " from identical SX source") - (li "Adding a new host requires only a new bootstrapper — the tests are " (strong "already written")) + (li "The test spec is " (strong "written in SX") " and " (strong "executed by SX") " — no code generation") + (li "The same 81 tests run on " (strong "both Python and JavaScript") " from the same file") + (li "Each host provides only " (strong "5 platform functions") " — everything else is pure SX") + (li "Adding a new host means implementing 5 functions, not rewriting tests") (li "Platform divergences (truthiness of 0, [], \"\") are " (strong "documented, not hidden")) (li "The spec is " (strong "executable") " — it doesn't just describe behavior, it verifies it"))) @@ -188,7 +203,7 @@ (h2 :class "text-2xl font-semibold text-stone-800" "Full specification source") (p :class "text-xs text-stone-400 italic" "The s-expression source below is the canonical test specification. " - "Bootstrap compilers read this file to generate native test runners.") + "Any host that implements the five platform functions can evaluate it directly.") (div :class "not-prose bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl" (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code (highlight spec-source "sx"))))))))