New test files: - test-cek-advanced.sx (63): deep nesting, complex calls, macro interaction, environment stress, edge cases - test-signals-advanced.sx (24): signal types, computed chains, effects, batch, swap patterns - test-integration.sx (38): parse-eval roundtrip, render pipeline, macro-render, data-driven rendering, error recovery, complex patterns Bugs found: - -> (thread-first) doesn't work with HO special forms (map, filter) because they're dispatched by name, not as env values. Documented as known limitation — use nested calls instead of ->. - batch returns nil, not thunk's return value - upcase not a primitive (use upper) Data-first HO forms attempted but reverted — the swap logic in ho-setup-dispatch caused subtle paren/nesting issues. Needs more careful implementation in a future session. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
317 lines
11 KiB
Python
317 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Run SX spec tests using the bootstrapped Python evaluator.
|
|
|
|
Usage:
|
|
python3 hosts/python/tests/run_tests.py # all spec tests
|
|
python3 hosts/python/tests/run_tests.py test-primitives # specific test
|
|
python3 hosts/python/tests/run_tests.py --full # include optional modules
|
|
"""
|
|
from __future__ import annotations
|
|
import os, sys
|
|
|
|
# Increase recursion limit for TCO tests (Python's default 1000 is too low)
|
|
sys.setrecursionlimit(5000)
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
|
_SPEC_TESTS = os.path.join(_PROJECT, "spec", "tests")
|
|
sys.path.insert(0, _PROJECT)
|
|
|
|
from shared.sx.ref.sx_ref import sx_parse as parse_all
|
|
from shared.sx.ref import sx_ref
|
|
from shared.sx.ref.sx_ref import (
|
|
make_env, env_get, env_has, env_set, env_extend, env_merge,
|
|
)
|
|
from shared.sx.types import (
|
|
NIL, Symbol, Keyword, Lambda, Component, Island, Macro,
|
|
)
|
|
|
|
# Use tree-walk evaluator
|
|
eval_expr = sx_ref._tree_walk_eval_expr
|
|
trampoline = sx_ref._tree_walk_trampoline
|
|
sx_ref.eval_expr = eval_expr
|
|
sx_ref.trampoline = trampoline
|
|
|
|
# Check for --full flag
|
|
full_build = "--full" in sys.argv
|
|
|
|
# Build env with primitives
|
|
env = make_env()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test infrastructure
|
|
# ---------------------------------------------------------------------------
|
|
_suite_stack: list[str] = []
|
|
_pass_count = 0
|
|
_fail_count = 0
|
|
|
|
|
|
def _try_call(thunk):
|
|
try:
|
|
trampoline(eval_expr([thunk], env))
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
return {"ok": False, "error": str(e)}
|
|
|
|
|
|
def _report_pass(name):
|
|
global _pass_count
|
|
_pass_count += 1
|
|
ctx = " > ".join(_suite_stack)
|
|
print(f" PASS: {ctx} > {name}")
|
|
return NIL
|
|
|
|
|
|
def _report_fail(name, error):
|
|
global _fail_count
|
|
_fail_count += 1
|
|
ctx = " > ".join(_suite_stack)
|
|
print(f" FAIL: {ctx} > {name}: {error}")
|
|
return NIL
|
|
|
|
|
|
def _push_suite(name):
|
|
_suite_stack.append(name)
|
|
print(f"{' ' * (len(_suite_stack)-1)}Suite: {name}")
|
|
return NIL
|
|
|
|
|
|
def _pop_suite():
|
|
if _suite_stack:
|
|
_suite_stack.pop()
|
|
return NIL
|
|
|
|
|
|
env["try-call"] = _try_call
|
|
env["report-pass"] = _report_pass
|
|
env["report-fail"] = _report_fail
|
|
env["push-suite"] = _push_suite
|
|
env["pop-suite"] = _pop_suite
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _deep_equal(a, b):
|
|
if a is b:
|
|
return True
|
|
if a is NIL and b is NIL:
|
|
return True
|
|
if a is NIL or b is NIL:
|
|
return a is None and b is NIL or b is None and a is NIL
|
|
if type(a) != type(b):
|
|
# number comparison: int vs float
|
|
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
|
|
return a == b
|
|
return False
|
|
if isinstance(a, list):
|
|
if len(a) != len(b):
|
|
return False
|
|
return all(_deep_equal(x, y) for x, y in zip(a, b))
|
|
if isinstance(a, dict):
|
|
ka = {k for k in a if k != "_nil"}
|
|
kb = {k for k in b if k != "_nil"}
|
|
if ka != kb:
|
|
return False
|
|
return all(_deep_equal(a[k], b[k]) for k in ka)
|
|
return a == b
|
|
|
|
|
|
env["equal?"] = _deep_equal
|
|
env["identical?"] = lambda a, b: a is b
|
|
|
|
|
|
def _test_env():
|
|
return make_env()
|
|
|
|
|
|
def _sx_parse(source):
|
|
return parse_all(source)
|
|
|
|
|
|
def _sx_parse_one(source):
|
|
exprs = parse_all(source)
|
|
return exprs[0] if exprs else NIL
|
|
|
|
|
|
env["test-env"] = _test_env
|
|
env["sx-parse"] = _sx_parse
|
|
env["sx-parse-one"] = _sx_parse_one
|
|
env["cek-eval"] = lambda s: trampoline(eval_expr(parse_all(s)[0], make_env())) if parse_all(s) else NIL
|
|
env["eval-expr-cek"] = lambda expr, e=None: trampoline(eval_expr(expr, e or env))
|
|
|
|
# Env operations
|
|
env["env-get"] = env_get
|
|
env["env-has?"] = env_has
|
|
env["env-set!"] = env_set
|
|
env["env-bind!"] = lambda e, k, v: e.__setitem__(k, v) or v
|
|
env["env-extend"] = env_extend
|
|
env["env-merge"] = env_merge
|
|
|
|
# Missing primitives
|
|
env["upcase"] = lambda s: str(s).upper()
|
|
env["downcase"] = lambda s: str(s).lower()
|
|
env["make-keyword"] = lambda name: Keyword(name)
|
|
env["make-symbol"] = lambda name: Symbol(name)
|
|
env["string-length"] = lambda s: len(str(s))
|
|
env["dict-get"] = lambda d, k: d.get(k, NIL) if isinstance(d, dict) else NIL
|
|
env["apply"] = lambda f, *args: f(*args[-1]) if args and isinstance(args[-1], list) else f()
|
|
|
|
# Render helpers
|
|
def _render_html(src, e=None):
|
|
if isinstance(src, str):
|
|
parsed = parse_all(src)
|
|
if not parsed:
|
|
return ""
|
|
expr = parsed[0] if len(parsed) == 1 else [Symbol("do")] + parsed
|
|
result = sx_ref.render_to_html(expr, e or make_env())
|
|
# Reset render mode
|
|
sx_ref._render_mode = False
|
|
return result
|
|
result = sx_ref.render_to_html(src, e or env)
|
|
sx_ref._render_mode = False
|
|
return result
|
|
|
|
|
|
env["render-html"] = _render_html
|
|
env["render-to-html"] = _render_html
|
|
env["string-contains?"] = lambda s, sub: str(sub) in str(s)
|
|
|
|
# Type system helpers
|
|
env["test-prim-types"] = lambda: {
|
|
"+": "number", "-": "number", "*": "number", "/": "number",
|
|
"mod": "number", "inc": "number", "dec": "number",
|
|
"abs": "number", "min": "number", "max": "number",
|
|
"str": "string", "upper": "string", "lower": "string",
|
|
"trim": "string", "join": "string", "replace": "string",
|
|
"=": "boolean", "<": "boolean", ">": "boolean",
|
|
"<=": "boolean", ">=": "boolean",
|
|
"not": "boolean", "nil?": "boolean", "empty?": "boolean",
|
|
"number?": "boolean", "string?": "boolean", "boolean?": "boolean",
|
|
"list?": "boolean", "dict?": "boolean",
|
|
"contains?": "boolean", "has-key?": "boolean",
|
|
"starts-with?": "boolean", "ends-with?": "boolean",
|
|
"len": "number", "first": "any", "rest": "list",
|
|
"last": "any", "nth": "any", "cons": "list",
|
|
"append": "list", "concat": "list", "reverse": "list",
|
|
"sort": "list", "slice": "list", "range": "list",
|
|
"flatten": "list", "keys": "list", "vals": "list",
|
|
"assoc": "dict", "dissoc": "dict", "merge": "dict", "dict": "dict",
|
|
"get": "any", "type-of": "string",
|
|
}
|
|
env["test-prim-param-types"] = lambda: {
|
|
"+": {"positional": [["a", "number"]], "rest-type": "number"},
|
|
"-": {"positional": [["a", "number"]], "rest-type": "number"},
|
|
"*": {"positional": [["a", "number"]], "rest-type": "number"},
|
|
"/": {"positional": [["a", "number"]], "rest-type": "number"},
|
|
"inc": {"positional": [["n", "number"]], "rest-type": NIL},
|
|
"dec": {"positional": [["n", "number"]], "rest-type": NIL},
|
|
"upper": {"positional": [["s", "string"]], "rest-type": NIL},
|
|
"lower": {"positional": [["s", "string"]], "rest-type": NIL},
|
|
"keys": {"positional": [["d", "dict"]], "rest-type": NIL},
|
|
"vals": {"positional": [["d", "dict"]], "rest-type": NIL},
|
|
}
|
|
env["component-param-types"] = lambda c: getattr(c, "_param_types", NIL)
|
|
env["component-set-param-types!"] = lambda c, t: setattr(c, "_param_types", t) or NIL
|
|
env["component-params"] = lambda c: c.params
|
|
env["component-body"] = lambda c: c.body
|
|
env["component-has-children"] = lambda c: c.has_children
|
|
env["component-affinity"] = lambda c: getattr(c, "affinity", "auto")
|
|
|
|
# Type accessors
|
|
env["callable?"] = lambda x: callable(x) or isinstance(x, (Lambda, Component, Island))
|
|
env["lambda?"] = lambda x: isinstance(x, Lambda)
|
|
env["component?"] = lambda x: isinstance(x, Component)
|
|
env["island?"] = lambda x: isinstance(x, Island)
|
|
env["macro?"] = lambda x: isinstance(x, Macro)
|
|
env["thunk?"] = sx_ref.is_thunk
|
|
env["thunk-expr"] = sx_ref.thunk_expr
|
|
env["thunk-env"] = sx_ref.thunk_env
|
|
env["make-thunk"] = sx_ref.make_thunk
|
|
env["make-lambda"] = sx_ref.make_lambda
|
|
env["make-component"] = sx_ref.make_component
|
|
env["make-macro"] = sx_ref.make_macro
|
|
env["lambda-params"] = lambda f: f.params
|
|
env["lambda-body"] = lambda f: f.body
|
|
env["lambda-closure"] = lambda f: f.closure
|
|
env["lambda-name"] = lambda f: f.name
|
|
env["set-lambda-name!"] = lambda f, n: setattr(f, "name", n) or NIL
|
|
env["component-closure"] = lambda c: c.closure
|
|
env["component-name"] = lambda c: c.name
|
|
env["component-has-children?"] = lambda c: c.has_children
|
|
env["macro-params"] = lambda m: m.params
|
|
env["macro-rest-param"] = lambda m: m.rest_param
|
|
env["macro-body"] = lambda m: m.body
|
|
env["macro-closure"] = lambda m: m.closure
|
|
env["symbol-name"] = lambda s: s.name if isinstance(s, Symbol) else str(s)
|
|
env["keyword-name"] = lambda k: k.name if isinstance(k, Keyword) else str(k)
|
|
env["sx-serialize"] = sx_ref.sx_serialize if hasattr(sx_ref, "sx_serialize") else lambda x: str(x)
|
|
env["is-render-expr?"] = lambda expr: False
|
|
env["render-active?"] = lambda: False
|
|
env["render-expr"] = lambda expr, env: NIL
|
|
|
|
# Strict mode stubs (not yet bootstrapped to Python — no-ops for now)
|
|
env["set-strict!"] = lambda val: NIL
|
|
env["set-prim-param-types!"] = lambda types: NIL
|
|
env["value-matches-type?"] = lambda val, t: True
|
|
env["*strict*"] = False
|
|
env["primitive?"] = lambda name: name in env
|
|
env["get-primitive"] = lambda name: env.get(name, NIL)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Load test framework
|
|
# ---------------------------------------------------------------------------
|
|
framework_src = open(os.path.join(_SPEC_TESTS, "test-framework.sx")).read()
|
|
for expr in parse_all(framework_src):
|
|
trampoline(eval_expr(expr, env))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Determine which tests to run
|
|
# ---------------------------------------------------------------------------
|
|
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
|
|
|
# Tests requiring optional modules (only with --full)
|
|
REQUIRES_FULL = {"test-continuations.sx", "test-continuations-advanced.sx", "test-types.sx", "test-freeze.sx", "test-strict.sx", "test-cek.sx", "test-cek-advanced.sx", "test-signals-advanced.sx"}
|
|
|
|
test_files = []
|
|
if args:
|
|
for arg in args:
|
|
name = arg if arg.endswith(".sx") else f"{arg}.sx"
|
|
p = os.path.join(_SPEC_TESTS, name)
|
|
if os.path.exists(p):
|
|
test_files.append(p)
|
|
else:
|
|
print(f"Test file not found: {name}")
|
|
else:
|
|
for f in sorted(os.listdir(_SPEC_TESTS)):
|
|
if f.startswith("test-") and f.endswith(".sx") and f != "test-framework.sx":
|
|
if not full_build and f in REQUIRES_FULL:
|
|
print(f"Skipping {f} (requires --full)")
|
|
continue
|
|
test_files.append(os.path.join(_SPEC_TESTS, f))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Run tests
|
|
# ---------------------------------------------------------------------------
|
|
for test_file in test_files:
|
|
name = os.path.basename(test_file)
|
|
print("=" * 60)
|
|
print(f"Running {name}")
|
|
print("=" * 60)
|
|
try:
|
|
src = open(test_file).read()
|
|
exprs = parse_all(src)
|
|
for expr in exprs:
|
|
trampoline(eval_expr(expr, env))
|
|
except Exception as e:
|
|
print(f"ERROR in {name}: {e}")
|
|
_fail_count += 1
|
|
|
|
# Summary
|
|
print("=" * 60)
|
|
print(f"Results: {_pass_count} passed, {_fail_count} failed")
|
|
print("=" * 60)
|
|
sys.exit(1 if _fail_count > 0 else 0)
|