Add self-hosting SX test spec: 81 tests bootstrap to Python + JS
The test framework is written in SX and tests SX — the language proves its own correctness. test.sx defines assertion helpers (assert-equal, assert-true, assert-type, etc.) and 15 test suites covering literals, arithmetic, comparison, strings, lists, dicts, predicates, special forms, lambdas, higher-order forms, components, macros, threading, truthiness, and edge cases. Two bootstrap compilers emit native tests from the same spec: - bootstrap_test.py → pytest (81/81 pass) - bootstrap_test_js.py → Node.js TAP using sx-browser.js (81/81 pass) Also adds missing primitives to spec and Python evaluator: boolean?, string-length, substring, string-contains?, upcase, downcase, reverse, flatten, has-key?. Fixes number? to exclude booleans, append to concatenate lists. Includes testing docs page in SX app at /specs/testing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
245
shared/sx/ref/bootstrap_test.py
Normal file
245
shared/sx/ref/bootstrap_test.py
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bootstrap compiler: test.sx -> pytest test module.
|
||||
|
||||
Reads test.sx and emits a Python test file that runs each deftest
|
||||
as a pytest test case, grouped into classes by defsuite.
|
||||
|
||||
The emitted tests use the SX evaluator to run SX test bodies,
|
||||
verifying that the Python implementation matches the spec.
|
||||
|
||||
Usage:
|
||||
python bootstrap_test.py --output shared/sx/tests/test_sx_spec.py
|
||||
pytest shared/sx/tests/test_sx_spec.py -v
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
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.parser import parse_all
|
||||
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
|
||||
|
||||
|
||||
def _slugify(name: str) -> str:
|
||||
"""Convert a test/suite name to a valid Python identifier."""
|
||||
s = name.lower().strip()
|
||||
s = re.sub(r'[^a-z0-9]+', '_', s)
|
||||
s = s.strip('_')
|
||||
return s
|
||||
|
||||
|
||||
def _sx_to_source(expr) -> str:
|
||||
"""Convert an SX AST node back to SX source string."""
|
||||
if isinstance(expr, bool):
|
||||
return "true" if expr else "false"
|
||||
if isinstance(expr, (int, float)):
|
||||
return str(expr)
|
||||
if isinstance(expr, str):
|
||||
escaped = expr.replace('\\', '\\\\').replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
if expr is None or expr is SX_NIL:
|
||||
return "nil"
|
||||
if isinstance(expr, Symbol):
|
||||
return expr.name
|
||||
if isinstance(expr, Keyword):
|
||||
return f":{expr.name}"
|
||||
if isinstance(expr, dict):
|
||||
pairs = []
|
||||
for k, v in expr.items():
|
||||
pairs.append(f":{k} {_sx_to_source(v)}")
|
||||
return "{" + " ".join(pairs) + "}"
|
||||
if isinstance(expr, list):
|
||||
if not expr:
|
||||
return "()"
|
||||
return "(" + " ".join(_sx_to_source(e) for e in expr) + ")"
|
||||
return str(expr)
|
||||
|
||||
|
||||
def _parse_test_sx(path: str) -> tuple[list[dict], list]:
|
||||
"""Parse test.sx and return (suites, preamble_exprs).
|
||||
|
||||
Preamble exprs are define forms (assertion helpers) that must be
|
||||
evaluated before tests run. Suites contain the actual test cases.
|
||||
"""
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
|
||||
exprs = parse_all(content)
|
||||
suites = []
|
||||
preamble = []
|
||||
|
||||
for expr in exprs:
|
||||
if not isinstance(expr, list) or not expr:
|
||||
continue
|
||||
head = expr[0]
|
||||
if isinstance(head, Symbol) and head.name == "defsuite":
|
||||
suite = _parse_suite(expr)
|
||||
if suite:
|
||||
suites.append(suite)
|
||||
elif isinstance(head, Symbol) and head.name == "define":
|
||||
preamble.append(expr)
|
||||
|
||||
return suites, preamble
|
||||
|
||||
|
||||
def _parse_suite(expr: list) -> dict | None:
|
||||
"""Parse a (defsuite "name" ...) form."""
|
||||
if len(expr) < 2:
|
||||
return None
|
||||
|
||||
name = expr[1]
|
||||
if not isinstance(name, str):
|
||||
return None
|
||||
|
||||
tests = []
|
||||
for child in expr[2:]:
|
||||
if not isinstance(child, list) or not child:
|
||||
continue
|
||||
head = child[0]
|
||||
if isinstance(head, Symbol):
|
||||
if head.name == "deftest":
|
||||
test = _parse_test(child)
|
||||
if test:
|
||||
tests.append(test)
|
||||
elif head.name == "defsuite":
|
||||
sub = _parse_suite(child)
|
||||
if sub:
|
||||
tests.append(sub)
|
||||
|
||||
return {"type": "suite", "name": name, "tests": tests}
|
||||
|
||||
|
||||
def _parse_test(expr: list) -> dict | None:
|
||||
"""Parse a (deftest "name" body ...) form."""
|
||||
if len(expr) < 3:
|
||||
return None
|
||||
name = expr[1]
|
||||
if not isinstance(name, str):
|
||||
return None
|
||||
body = expr[2:]
|
||||
return {"type": "test", "name": name, "body": body}
|
||||
|
||||
|
||||
def _emit_py(suites: list[dict], preamble: list) -> str:
|
||||
"""Emit a pytest module from parsed suites."""
|
||||
# Serialize preamble (assertion helpers) as SX source
|
||||
preamble_sx = "\n".join(_sx_to_source(expr) for expr in preamble)
|
||||
preamble_escaped = preamble_sx.replace('\\', '\\\\').replace("'", "\\'")
|
||||
|
||||
lines = []
|
||||
lines.append('"""Auto-generated from test.sx — SX spec self-tests.')
|
||||
lines.append('')
|
||||
lines.append('DO NOT EDIT. Regenerate with:')
|
||||
lines.append(' python shared/sx/ref/bootstrap_test.py --output shared/sx/tests/test_sx_spec.py')
|
||||
lines.append('"""')
|
||||
lines.append('from __future__ import annotations')
|
||||
lines.append('')
|
||||
lines.append('import pytest')
|
||||
lines.append('from shared.sx.parser import parse_all')
|
||||
lines.append('from shared.sx.evaluator import _eval, _trampoline')
|
||||
lines.append('')
|
||||
lines.append('')
|
||||
lines.append(f"_PREAMBLE = '''{preamble_escaped}'''")
|
||||
lines.append('')
|
||||
lines.append('')
|
||||
lines.append('def _make_env() -> dict:')
|
||||
lines.append(' """Create a fresh env with assertion helpers loaded."""')
|
||||
lines.append(' env = {}')
|
||||
lines.append(' for expr in parse_all(_PREAMBLE):')
|
||||
lines.append(' _trampoline(_eval(expr, env))')
|
||||
lines.append(' return env')
|
||||
lines.append('')
|
||||
lines.append('')
|
||||
lines.append('def _run(sx_source: str, env: dict | None = None) -> object:')
|
||||
lines.append(' """Evaluate SX source and return the result."""')
|
||||
lines.append(' if env is None:')
|
||||
lines.append(' env = _make_env()')
|
||||
lines.append(' exprs = parse_all(sx_source)')
|
||||
lines.append(' result = None')
|
||||
lines.append(' for expr in exprs:')
|
||||
lines.append(' result = _trampoline(_eval(expr, env))')
|
||||
lines.append(' return result')
|
||||
lines.append('')
|
||||
|
||||
for suite in suites:
|
||||
_emit_suite(suite, lines, indent=0)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _emit_suite(suite: dict, lines: list[str], indent: int):
|
||||
"""Emit a pytest class for a suite."""
|
||||
class_name = f"TestSpec{_slugify(suite['name']).title().replace('_', '')}"
|
||||
pad = " " * indent
|
||||
lines.append(f'{pad}class {class_name}:')
|
||||
lines.append(f'{pad} """test.sx suite: {suite["name"]}"""')
|
||||
lines.append('')
|
||||
|
||||
for item in suite["tests"]:
|
||||
if item["type"] == "test":
|
||||
_emit_test(item, lines, indent + 1)
|
||||
elif item["type"] == "suite":
|
||||
_emit_suite(item, lines, indent + 1)
|
||||
|
||||
lines.append('')
|
||||
|
||||
|
||||
def _emit_test(test: dict, lines: list[str], indent: int):
|
||||
"""Emit a pytest test method."""
|
||||
method_name = f"test_{_slugify(test['name'])}"
|
||||
pad = " " * indent
|
||||
|
||||
# Convert body expressions to SX source
|
||||
body_parts = []
|
||||
for expr in test["body"]:
|
||||
body_parts.append(_sx_to_source(expr))
|
||||
|
||||
# Wrap in (do ...) if multiple expressions, or use single
|
||||
if len(body_parts) == 1:
|
||||
sx_source = body_parts[0]
|
||||
else:
|
||||
sx_source = "(do " + " ".join(body_parts) + ")"
|
||||
|
||||
# Escape for Python string
|
||||
sx_escaped = sx_source.replace('\\', '\\\\').replace("'", "\\'")
|
||||
|
||||
lines.append(f"{pad}def {method_name}(self):")
|
||||
lines.append(f"{pad} _run('{sx_escaped}')")
|
||||
lines.append('')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Bootstrap test.sx to pytest")
|
||||
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_tests = sum(
|
||||
sum(1 for t in s["tests"] if t["type"] == "test")
|
||||
for s in suites
|
||||
)
|
||||
print(f"Total test cases: {total_tests}", file=sys.stderr)
|
||||
|
||||
output = _emit_py(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()
|
||||
210
shared/sx/ref/bootstrap_test_js.py
Normal file
210
shared/sx/ref/bootstrap_test_js.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/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()
|
||||
@@ -208,10 +208,16 @@
|
||||
:returns "boolean"
|
||||
:doc "True if x is nil/null/None.")
|
||||
|
||||
(define-primitive "boolean?"
|
||||
:params (x)
|
||||
:returns "boolean"
|
||||
:doc "True if x is a boolean (true or false). Must be checked before
|
||||
number? on platforms where booleans are numeric subtypes.")
|
||||
|
||||
(define-primitive "number?"
|
||||
:params (x)
|
||||
:returns "boolean"
|
||||
:doc "True if x is a number (int or float).")
|
||||
:doc "True if x is a number (int or float). Excludes booleans.")
|
||||
|
||||
(define-primitive "string?"
|
||||
:params (x)
|
||||
@@ -277,11 +283,36 @@
|
||||
:returns "string"
|
||||
:doc "Uppercase string.")
|
||||
|
||||
(define-primitive "upcase"
|
||||
:params (s)
|
||||
:returns "string"
|
||||
:doc "Alias for upper. Uppercase string.")
|
||||
|
||||
(define-primitive "lower"
|
||||
:params (s)
|
||||
:returns "string"
|
||||
:doc "Lowercase string.")
|
||||
|
||||
(define-primitive "downcase"
|
||||
:params (s)
|
||||
:returns "string"
|
||||
:doc "Alias for lower. Lowercase string.")
|
||||
|
||||
(define-primitive "string-length"
|
||||
:params (s)
|
||||
:returns "number"
|
||||
:doc "Length of string in characters.")
|
||||
|
||||
(define-primitive "substring"
|
||||
:params (s start end)
|
||||
:returns "string"
|
||||
:doc "Extract substring from start (inclusive) to end (exclusive).")
|
||||
|
||||
(define-primitive "string-contains?"
|
||||
:params (s needle)
|
||||
:returns "boolean"
|
||||
:doc "True if string s contains substring needle.")
|
||||
|
||||
(define-primitive "trim"
|
||||
:params (s)
|
||||
:returns "string"
|
||||
@@ -382,13 +413,23 @@
|
||||
(define-primitive "append"
|
||||
:params (coll x)
|
||||
:returns "list"
|
||||
:doc "Append x to end of coll (returns new list).")
|
||||
:doc "If x is a list, concatenate. Otherwise append x as single element.")
|
||||
|
||||
(define-primitive "append!"
|
||||
:params (coll x)
|
||||
:returns "list"
|
||||
:doc "Mutate coll by appending x in-place. Returns coll.")
|
||||
|
||||
(define-primitive "reverse"
|
||||
:params (coll)
|
||||
:returns "list"
|
||||
:doc "Return coll in reverse order.")
|
||||
|
||||
(define-primitive "flatten"
|
||||
:params (coll)
|
||||
:returns "list"
|
||||
:doc "Flatten one level of nesting. Nested lists become top-level elements.")
|
||||
|
||||
(define-primitive "chunk-every"
|
||||
:params (coll n)
|
||||
:returns "list"
|
||||
@@ -421,6 +462,11 @@
|
||||
:returns "dict"
|
||||
:doc "Merge dicts left to right. Later keys win. Skips nil.")
|
||||
|
||||
(define-primitive "has-key?"
|
||||
:params (d key)
|
||||
:returns "boolean"
|
||||
:doc "True if dict d contains key.")
|
||||
|
||||
(define-primitive "assoc"
|
||||
:params (d &rest pairs)
|
||||
:returns "dict"
|
||||
|
||||
612
shared/sx/ref/test.sx
Normal file
612
shared/sx/ref/test.sx
Normal file
@@ -0,0 +1,612 @@
|
||||
;; ==========================================================================
|
||||
;; 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))))
|
||||
;;
|
||||
;; Platform functions required:
|
||||
;; (test-report suite-name results) → platform-specific output
|
||||
;; results is a list of {:name "..." :passed bool :error "..."|nil}
|
||||
;;
|
||||
;; 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.)
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 1. Test framework forms
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; 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.
|
||||
|
||||
;; (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)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 2. Assertion helpers — defined in SX, available in test bodies
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; These are regular functions (not special forms). They use the `assert`
|
||||
;; primitive underneath but provide better error messages.
|
||||
|
||||
(define assert-equal
|
||||
(fn (expected actual)
|
||||
(assert (equal? expected actual)
|
||||
(str "Expected " (str expected) " but got " (str actual)))))
|
||||
|
||||
(define assert-not-equal
|
||||
(fn (a b)
|
||||
(assert (not (equal? a b))
|
||||
(str "Expected values to differ but both are " (str a)))))
|
||||
|
||||
(define assert-true
|
||||
(fn (val)
|
||||
(assert val (str "Expected truthy but got " (str val)))))
|
||||
|
||||
(define assert-false
|
||||
(fn (val)
|
||||
(assert (not val) (str "Expected falsy but got " (str val)))))
|
||||
|
||||
(define assert-nil
|
||||
(fn (val)
|
||||
(assert (nil? val) (str "Expected nil but got " (str val)))))
|
||||
|
||||
(define assert-type
|
||||
(fn (expected-type val)
|
||||
;; Implemented via predicate dispatch since type-of is a platform
|
||||
;; function not available in all hosts. Uses nested if to avoid
|
||||
;; Scheme-style cond detection for 2-element predicate calls.
|
||||
;; Boolean checked before number (subtypes on some platforms).
|
||||
(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 must implement try-catch or equivalent.
|
||||
;; Bootstrap compilers emit this as a native try/catch block.
|
||||
(platform-assert-throws thunk)))
|
||||
|
||||
|
||||
;; ==========================================================================
|
||||
;; 3. Test suites — SX testing SX
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3a. Literals and types
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "literals"
|
||||
(deftest "numbers are numbers"
|
||||
(assert-type "number" 42)
|
||||
(assert-type "number" 3.14)
|
||||
(assert-type "number" -1))
|
||||
|
||||
(deftest "strings are strings"
|
||||
(assert-type "string" "hello")
|
||||
(assert-type "string" ""))
|
||||
|
||||
(deftest "booleans are booleans"
|
||||
(assert-type "boolean" true)
|
||||
(assert-type "boolean" false))
|
||||
|
||||
(deftest "nil is nil"
|
||||
(assert-type "nil" nil)
|
||||
(assert-nil nil))
|
||||
|
||||
(deftest "lists are lists"
|
||||
(assert-type "list" (list 1 2 3))
|
||||
(assert-type "list" (list)))
|
||||
|
||||
(deftest "dicts are dicts"
|
||||
(assert-type "dict" {:a 1 :b 2})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3b. Arithmetic
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "arithmetic"
|
||||
(deftest "addition"
|
||||
(assert-equal 3 (+ 1 2))
|
||||
(assert-equal 0 (+ 0 0))
|
||||
(assert-equal -1 (+ 1 -2))
|
||||
(assert-equal 10 (+ 1 2 3 4)))
|
||||
|
||||
(deftest "subtraction"
|
||||
(assert-equal 1 (- 3 2))
|
||||
(assert-equal -1 (- 2 3)))
|
||||
|
||||
(deftest "multiplication"
|
||||
(assert-equal 6 (* 2 3))
|
||||
(assert-equal 0 (* 0 100))
|
||||
(assert-equal 24 (* 1 2 3 4)))
|
||||
|
||||
(deftest "division"
|
||||
(assert-equal 2 (/ 6 3))
|
||||
(assert-equal 2.5 (/ 5 2)))
|
||||
|
||||
(deftest "modulo"
|
||||
(assert-equal 1 (mod 7 3))
|
||||
(assert-equal 0 (mod 6 3))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3c. Comparison
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "comparison"
|
||||
(deftest "equality"
|
||||
(assert-true (= 1 1))
|
||||
(assert-false (= 1 2))
|
||||
(assert-true (= "a" "a"))
|
||||
(assert-false (= "a" "b")))
|
||||
|
||||
(deftest "deep equality"
|
||||
(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})))
|
||||
|
||||
(deftest "ordering"
|
||||
(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))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3d. String operations
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "strings"
|
||||
(deftest "str concatenation"
|
||||
(assert-equal "abc" (str "a" "b" "c"))
|
||||
(assert-equal "hello world" (str "hello" " " "world"))
|
||||
(assert-equal "42" (str 42))
|
||||
(assert-equal "" (str)))
|
||||
|
||||
(deftest "string-length"
|
||||
(assert-equal 5 (string-length "hello"))
|
||||
(assert-equal 0 (string-length "")))
|
||||
|
||||
(deftest "substring"
|
||||
(assert-equal "ell" (substring "hello" 1 4))
|
||||
(assert-equal "hello" (substring "hello" 0 5)))
|
||||
|
||||
(deftest "string-contains?"
|
||||
(assert-true (string-contains? "hello world" "world"))
|
||||
(assert-false (string-contains? "hello" "xyz")))
|
||||
|
||||
(deftest "upcase and downcase"
|
||||
(assert-equal "HELLO" (upcase "hello"))
|
||||
(assert-equal "hello" (downcase "HELLO")))
|
||||
|
||||
(deftest "trim"
|
||||
(assert-equal "hello" (trim " hello "))
|
||||
(assert-equal "hello" (trim "hello")))
|
||||
|
||||
(deftest "split and join"
|
||||
(assert-equal (list "a" "b" "c") (split "a,b,c" ","))
|
||||
(assert-equal "a-b-c" (join "-" (list "a" "b" "c")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3e. List operations
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "lists"
|
||||
(deftest "constructors"
|
||||
(assert-equal (list 1 2 3) (list 1 2 3))
|
||||
(assert-equal (list) (list))
|
||||
(assert-length 3 (list 1 2 3)))
|
||||
|
||||
(deftest "first and rest"
|
||||
(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))))
|
||||
|
||||
(deftest "nth"
|
||||
(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)))
|
||||
|
||||
(deftest "last"
|
||||
(assert-equal 3 (last (list 1 2 3)))
|
||||
(assert-nil (last (list))))
|
||||
|
||||
(deftest "cons and append"
|
||||
(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))))
|
||||
|
||||
(deftest "reverse"
|
||||
(assert-equal (list 3 2 1) (reverse (list 1 2 3)))
|
||||
(assert-equal (list) (reverse (list))))
|
||||
|
||||
(deftest "empty?"
|
||||
(assert-true (empty? (list)))
|
||||
(assert-false (empty? (list 1))))
|
||||
|
||||
(deftest "len"
|
||||
(assert-equal 0 (len (list)))
|
||||
(assert-equal 3 (len (list 1 2 3))))
|
||||
|
||||
(deftest "contains?"
|
||||
(assert-true (contains? (list 1 2 3) 2))
|
||||
(assert-false (contains? (list 1 2 3) 4)))
|
||||
|
||||
(deftest "flatten"
|
||||
(assert-equal (list 1 2 3 4) (flatten (list (list 1 2) (list 3 4))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3f. Dict operations
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "dicts"
|
||||
(deftest "dict literal"
|
||||
(assert-type "dict" {:a 1 :b 2})
|
||||
(assert-equal 1 (get {:a 1} "a"))
|
||||
(assert-equal 2 (get {:a 1 :b 2} "b")))
|
||||
|
||||
(deftest "assoc"
|
||||
(assert-equal {:a 1 :b 2} (assoc {:a 1} "b" 2))
|
||||
(assert-equal {:a 99} (assoc {:a 1} "a" 99)))
|
||||
|
||||
(deftest "dissoc"
|
||||
(assert-equal {:b 2} (dissoc {:a 1 :b 2} "a")))
|
||||
|
||||
(deftest "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))))
|
||||
|
||||
(deftest "has-key?"
|
||||
(assert-true (has-key? {:a 1} "a"))
|
||||
(assert-false (has-key? {:a 1} "b")))
|
||||
|
||||
(deftest "merge"
|
||||
(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}))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3g. Predicates
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "predicates"
|
||||
(deftest "nil?"
|
||||
(assert-true (nil? nil))
|
||||
(assert-false (nil? 0))
|
||||
(assert-false (nil? false))
|
||||
(assert-false (nil? "")))
|
||||
|
||||
(deftest "number?"
|
||||
(assert-true (number? 42))
|
||||
(assert-true (number? 3.14))
|
||||
(assert-false (number? "42")))
|
||||
|
||||
(deftest "string?"
|
||||
(assert-true (string? "hello"))
|
||||
(assert-false (string? 42)))
|
||||
|
||||
(deftest "list?"
|
||||
(assert-true (list? (list 1 2)))
|
||||
(assert-false (list? "not a list")))
|
||||
|
||||
(deftest "dict?"
|
||||
(assert-true (dict? {:a 1}))
|
||||
(assert-false (dict? (list 1))))
|
||||
|
||||
(deftest "boolean?"
|
||||
(assert-true (boolean? true))
|
||||
(assert-true (boolean? false))
|
||||
(assert-false (boolean? nil))
|
||||
(assert-false (boolean? 0)))
|
||||
|
||||
(deftest "not"
|
||||
(assert-true (not false))
|
||||
(assert-true (not nil))
|
||||
(assert-false (not true))
|
||||
(assert-false (not 1))
|
||||
(assert-false (not "x"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3h. Special forms
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "special-forms"
|
||||
(deftest "if"
|
||||
(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")))
|
||||
|
||||
(deftest "when"
|
||||
(assert-equal "yes" (when true "yes"))
|
||||
(assert-nil (when false "yes")))
|
||||
|
||||
(deftest "cond"
|
||||
(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")))
|
||||
|
||||
(deftest "and"
|
||||
(assert-true (and true true))
|
||||
(assert-false (and true false))
|
||||
(assert-false (and false true))
|
||||
(assert-equal 3 (and 1 2 3)))
|
||||
|
||||
(deftest "or"
|
||||
(assert-equal 1 (or 1 2))
|
||||
(assert-equal 2 (or false 2))
|
||||
(assert-equal "fallback" (or nil false "fallback"))
|
||||
(assert-false (or false false)))
|
||||
|
||||
(deftest "let"
|
||||
(assert-equal 3 (let ((x 1) (y 2)) (+ x y)))
|
||||
(assert-equal "hello world"
|
||||
(let ((a "hello") (b " world")) (str a b))))
|
||||
|
||||
(deftest "let clojure-style"
|
||||
(assert-equal 3 (let (x 1 y 2) (+ x y))))
|
||||
|
||||
(deftest "do / begin"
|
||||
(assert-equal 3 (do 1 2 3))
|
||||
(assert-equal "last" (begin "first" "middle" "last")))
|
||||
|
||||
(deftest "define"
|
||||
(define x 42)
|
||||
(assert-equal 42 x))
|
||||
|
||||
(deftest "set!"
|
||||
(define x 1)
|
||||
(set! x 2)
|
||||
(assert-equal 2 x)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3i. Lambda and closures
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "lambdas"
|
||||
(deftest "basic lambda"
|
||||
(let ((add (fn (a b) (+ a b))))
|
||||
(assert-equal 3 (add 1 2))))
|
||||
|
||||
(deftest "closure captures env"
|
||||
(let ((x 10))
|
||||
(let ((add-x (fn (y) (+ x y))))
|
||||
(assert-equal 15 (add-x 5)))))
|
||||
|
||||
(deftest "lambda as argument"
|
||||
(assert-equal (list 2 4 6)
|
||||
(map (fn (x) (* x 2)) (list 1 2 3))))
|
||||
|
||||
(deftest "recursive lambda via define"
|
||||
(define factorial
|
||||
(fn (n) (if (<= n 1) 1 (* n (factorial (- n 1))))))
|
||||
(assert-equal 120 (factorial 5)))
|
||||
|
||||
(deftest "higher-order returns lambda"
|
||||
(let ((make-adder (fn (n) (fn (x) (+ n x)))))
|
||||
(let ((add5 (make-adder 5)))
|
||||
(assert-equal 8 (add5 3))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3j. Higher-order forms
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "higher-order"
|
||||
(deftest "map"
|
||||
(assert-equal (list 2 4 6)
|
||||
(map (fn (x) (* x 2)) (list 1 2 3)))
|
||||
(assert-equal (list) (map (fn (x) x) (list))))
|
||||
|
||||
(deftest "filter"
|
||||
(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))))
|
||||
|
||||
(deftest "reduce"
|
||||
(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))))
|
||||
|
||||
(deftest "some"
|
||||
(assert-true (some (fn (x) (> x 3)) (list 1 2 3 4 5)))
|
||||
(assert-false (some (fn (x) (> x 10)) (list 1 2 3))))
|
||||
|
||||
(deftest "every?"
|
||||
(assert-true (every? (fn (x) (> x 0)) (list 1 2 3)))
|
||||
(assert-false (every? (fn (x) (> x 2)) (list 1 2 3))))
|
||||
|
||||
(deftest "map-indexed"
|
||||
(assert-equal (list "0:a" "1:b" "2:c")
|
||||
(map-indexed (fn (i x) (str i ":" x)) (list "a" "b" "c")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3k. Components
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "components"
|
||||
(deftest "defcomp creates component"
|
||||
(defcomp ~test-comp (&key title)
|
||||
(div title))
|
||||
;; Component is bound and not nil
|
||||
(assert-true (not (nil? ~test-comp))))
|
||||
|
||||
(deftest "component renders with keyword args"
|
||||
(defcomp ~greeting (&key name)
|
||||
(span (str "Hello, " name "!")))
|
||||
(assert-true (not (nil? ~greeting))))
|
||||
|
||||
(deftest "component with children"
|
||||
(defcomp ~box (&key &rest children)
|
||||
(div :class "box" children))
|
||||
(assert-true (not (nil? ~box))))
|
||||
|
||||
(deftest "component with default via or"
|
||||
(defcomp ~label (&key text)
|
||||
(span (or text "default")))
|
||||
(assert-true (not (nil? ~label)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3l. Macros
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "macros"
|
||||
(deftest "defmacro creates macro"
|
||||
(defmacro unless (cond &rest body)
|
||||
`(if (not ,cond) (do ,@body)))
|
||||
(assert-equal "yes" (unless false "yes"))
|
||||
(assert-nil (unless true "no")))
|
||||
|
||||
(deftest "quasiquote and unquote"
|
||||
(let ((x 42))
|
||||
(assert-equal (list 1 42 3) `(1 ,x 3))))
|
||||
|
||||
(deftest "splice-unquote"
|
||||
(let ((xs (list 2 3 4)))
|
||||
(assert-equal (list 1 2 3 4 5) `(1 ,@xs 5)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3m. Threading macro
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "threading"
|
||||
(deftest "thread-first"
|
||||
(assert-equal 8 (-> 5 (+ 1) (+ 2)))
|
||||
(assert-equal "HELLO" (-> "hello" upcase))
|
||||
(assert-equal "HELLO WORLD"
|
||||
(-> "hello"
|
||||
(str " world")
|
||||
upcase))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3n. Truthiness
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "truthiness"
|
||||
(deftest "truthy values"
|
||||
(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)))
|
||||
|
||||
(deftest "falsy values"
|
||||
(assert-false (if false true false))
|
||||
(assert-false (if nil true false)))
|
||||
|
||||
;; NOTE: empty list, zero, and empty string truthiness is
|
||||
;; platform-dependent. Python treats all three as falsy.
|
||||
;; JavaScript treats [] as truthy but 0 and "" as falsy.
|
||||
;; These tests are omitted — each bootstrapper should emit
|
||||
;; platform-specific truthiness tests instead.
|
||||
)
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 3o. Edge cases and regression tests
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "edge-cases"
|
||||
(deftest "nested let scoping"
|
||||
(let ((x 1))
|
||||
(let ((x 2))
|
||||
(assert-equal 2 x))
|
||||
;; outer x should be unchanged by inner let
|
||||
;; (this tests that let creates a new scope)
|
||||
))
|
||||
|
||||
(deftest "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)))))
|
||||
|
||||
(deftest "keyword as value"
|
||||
(assert-equal "class" :class)
|
||||
(assert-equal "id" :id))
|
||||
|
||||
(deftest "dict with evaluated values"
|
||||
(let ((x 42))
|
||||
(assert-equal 42 (get {:val x} "val"))))
|
||||
|
||||
(deftest "nil propagation"
|
||||
(assert-nil (get {:a 1} "missing"))
|
||||
(assert-equal "default" (or (get {:a 1} "missing") "default")))
|
||||
|
||||
(deftest "empty operations"
|
||||
(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))))
|
||||
Reference in New Issue
Block a user