Make test.sx self-executing: evaluators run it directly, no codegen
test.sx now defines deftest/defsuite as macros. Any host that provides 5 platform functions (try-call, report-pass, report-fail, push-suite, pop-suite) can evaluate the file directly — no bootstrap compilation step needed for JS. - Added defmacro for deftest (wraps body in thunk, catches via try-call) - Added defmacro for defsuite (push/pop suite context stack) - Created run.js: sx-browser.js evaluates test.sx directly (81/81 pass) - Created run.py: Python evaluator evaluates test.sx directly (81/81 pass) - Deleted bootstrap_test_js.py and generated test_sx_spec.js - Updated testing docs page to reflect self-executing architecture Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
@@ -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"))))
|
||||
|
||||
|
||||
;; ==========================================================================
|
||||
|
||||
Reference in New Issue
Block a user