Compare commits
4 Commits
99a78a70b3
...
3b3c904953
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b3c904953 | |||
| 3119b8e310 | |||
| aab1f3e966 | |||
| 79025b9913 |
@@ -1,5 +1,5 @@
|
||||
// sx-test-runner.js — Run test.sx in the browser using sx-browser.js.
|
||||
// Loaded on the /specs/testing page. Uses the Sx global.
|
||||
// sx-test-runner.js — Run SX test specs in the browser using sx-browser.js.
|
||||
// Supports both legacy (monolithic test.sx) and modular (per-spec) modes.
|
||||
(function() {
|
||||
var NIL = Sx.NIL;
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
@@ -21,13 +21,9 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
window.sxRunTests = function(srcId, outId, btnId) {
|
||||
var src = document.getElementById(srcId).textContent;
|
||||
var out = document.getElementById(outId);
|
||||
var btn = document.getElementById(btnId);
|
||||
|
||||
// --- Platform functions shared across all specs ---
|
||||
function makeEnv() {
|
||||
var stack = [], passed = 0, failed = 0, num = 0, lines = [];
|
||||
|
||||
var env = {
|
||||
"try-call": function(thunk) {
|
||||
try {
|
||||
@@ -49,6 +45,7 @@
|
||||
"push-suite": function(name) { stack.push(name); },
|
||||
"pop-suite": function() { stack.pop(); },
|
||||
|
||||
// Primitives that sx-browser.js may not expose in env
|
||||
"equal?": function(a, b) { return deepEqual(a, b); },
|
||||
"eq?": function(a, b) { return a === b; },
|
||||
"boolean?": function(x) { return typeof x === "boolean"; },
|
||||
@@ -68,28 +65,159 @@
|
||||
},
|
||||
"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]); },
|
||||
"for-each-indexed": function(f, coll) {
|
||||
for (var i = 0; i < (coll||[]).length; i++) Sx.eval([f, i, coll[i]], env);
|
||||
},
|
||||
"for-each": function(f, coll) {
|
||||
for (var i = 0; i < (coll||[]).length; i++) Sx.eval([f, coll[i]], env);
|
||||
},
|
||||
"dict-set!": function(d, k, v) { if (d) d[k] = v; },
|
||||
"dict-has?": function(d, k) { return d && typeof d === "object" && k in d; },
|
||||
"dict-get": function(d, k) { return d ? d[k] : undefined; },
|
||||
"starts-with?": function(s, prefix) { return String(s).indexOf(prefix) === 0; },
|
||||
"ends-with?": function(s, suffix) { var str = String(s); return str.indexOf(suffix) === str.length - suffix.length; },
|
||||
"slice": function(s, start, end) { return end !== undefined ? s.slice(start, end) : s.slice(start); },
|
||||
"inc": function(n) { return n + 1; },
|
||||
"append!": function(arr, item) { if (Array.isArray(arr)) arr.push(item); },
|
||||
"dict": function() { return {}; },
|
||||
|
||||
// --- Parser platform functions ---
|
||||
"sx-parse": function(source) { return Sx.parseAll(source); },
|
||||
"sx-serialize": function(val) {
|
||||
if (val === NIL || val === null || val === undefined) return "nil";
|
||||
if (typeof val === "boolean") return val ? "true" : "false";
|
||||
if (typeof val === "number") return String(val);
|
||||
if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
||||
if (val && (val._sym || val._sx_symbol)) return val.name;
|
||||
if (val && (val._kw || val._sx_keyword)) return ":" + val.name;
|
||||
if (Array.isArray(val)) return "(" + val.map(function(x) { return env["sx-serialize"](x); }).join(" ") + ")";
|
||||
if (val && typeof val === "object") {
|
||||
var parts = [];
|
||||
Object.keys(val).forEach(function(k) { parts.push(":" + k); parts.push(env["sx-serialize"](val[k])); });
|
||||
return "{" + parts.join(" ") + "}";
|
||||
}
|
||||
return String(val);
|
||||
},
|
||||
"make-symbol": function(name) { return Sx.sym ? Sx.sym(name) : { _sx_symbol: true, name: name, toString: function() { return name; } }; },
|
||||
"make-keyword": function(name) { return Sx.kw ? Sx.kw(name) : { _sx_keyword: true, name: name, toString: function() { return name; } }; },
|
||||
"symbol-name": function(s) { return s && s.name ? s.name : String(s); },
|
||||
"keyword-name": function(k) { return k && k.name ? k.name : String(k); },
|
||||
|
||||
// --- Render platform function ---
|
||||
"render-html": function(sxSource) {
|
||||
if (!Sx.renderToHtml) throw new Error("render-to-html not available");
|
||||
var exprs = Sx.parseAll(sxSource);
|
||||
var result = "";
|
||||
for (var i = 0; i < exprs.length; i++) result += Sx.renderToHtml(exprs[i], env);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
return { env: env, getResults: function() { return { passed: passed, failed: failed, num: num, lines: lines }; } };
|
||||
}
|
||||
|
||||
function evalSource(src, env) {
|
||||
var exprs = Sx.parseAll(src);
|
||||
for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);
|
||||
}
|
||||
|
||||
function loadRouterFromBootstrap(env) {
|
||||
if (Sx.splitPathSegments) {
|
||||
env["split-path-segments"] = Sx.splitPathSegments;
|
||||
env["parse-route-pattern"] = Sx.parseRoutePattern;
|
||||
env["match-route-segments"] = Sx.matchRouteSegments;
|
||||
env["match-route"] = Sx.matchRoute;
|
||||
env["find-matching-route"] = Sx.findMatchingRoute;
|
||||
env["make-route-segment"] = Sx.makeRouteSegment;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Legacy runner (monolithic test.sx) ---
|
||||
window.sxRunTests = function(srcId, outId, btnId) {
|
||||
var src = document.getElementById(srcId).textContent;
|
||||
var out = document.getElementById(outId);
|
||||
var btn = document.getElementById(btnId);
|
||||
var ctx = makeEnv();
|
||||
|
||||
try {
|
||||
var t0 = performance.now();
|
||||
var exprs = Sx.parseAll(src);
|
||||
for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);
|
||||
evalSource(src, ctx.env);
|
||||
var elapsed = Math.round(performance.now() - t0);
|
||||
lines.push("");
|
||||
lines.push("1.." + num);
|
||||
lines.push("# tests " + (passed + failed));
|
||||
lines.push("# pass " + passed);
|
||||
if (failed > 0) lines.push("# fail " + failed);
|
||||
lines.push("# time " + elapsed + "ms");
|
||||
var r = ctx.getResults();
|
||||
r.lines.push("");
|
||||
r.lines.push("1.." + r.num);
|
||||
r.lines.push("# tests " + (r.passed + r.failed));
|
||||
r.lines.push("# pass " + r.passed);
|
||||
if (r.failed > 0) r.lines.push("# fail " + r.failed);
|
||||
r.lines.push("# time " + elapsed + "ms");
|
||||
} catch(e) {
|
||||
lines.push("");
|
||||
lines.push("FATAL: " + (e.message || String(e)));
|
||||
var r = ctx.getResults();
|
||||
r.lines.push("");
|
||||
r.lines.push("FATAL: " + (e.message || String(e)));
|
||||
}
|
||||
|
||||
out.textContent = lines.join("\n");
|
||||
out.textContent = r.lines.join("\n");
|
||||
out.style.display = "block";
|
||||
btn.textContent = passed + "/" + (passed + failed) + " passed" + (failed === 0 ? "" : " (" + failed + " failed)");
|
||||
btn.className = failed > 0
|
||||
btn.textContent = r.passed + "/" + (r.passed + r.failed) + " passed" + (r.failed === 0 ? "" : " (" + r.failed + " failed)");
|
||||
btn.className = r.failed > 0
|
||||
? "px-4 py-2 rounded-md bg-red-600 text-white font-medium text-sm cursor-default"
|
||||
: "px-4 py-2 rounded-md bg-green-600 text-white font-medium text-sm cursor-default";
|
||||
};
|
||||
|
||||
// --- Modular runner (per-spec or all) ---
|
||||
var SPECS = {
|
||||
"eval": { needs: [] },
|
||||
"parser": { needs: ["sx-parse"] },
|
||||
"router": { needs: [] },
|
||||
"render": { needs: ["render-html"] },
|
||||
};
|
||||
|
||||
window.sxRunModularTests = function(specName, outId, btnId) {
|
||||
var out = document.getElementById(outId);
|
||||
var btn = document.getElementById(btnId);
|
||||
var ctx = makeEnv();
|
||||
var specs = specName === "all" ? Object.keys(SPECS) : [specName];
|
||||
|
||||
try {
|
||||
var t0 = performance.now();
|
||||
|
||||
// Load framework
|
||||
var fwEl = document.getElementById("test-framework-source");
|
||||
if (fwEl) {
|
||||
evalSource(fwEl.textContent, ctx.env);
|
||||
}
|
||||
|
||||
for (var si = 0; si < specs.length; si++) {
|
||||
var sn = specs[si];
|
||||
if (!SPECS[sn]) continue;
|
||||
|
||||
// Load router from bootstrap if needed
|
||||
if (sn === "router") loadRouterFromBootstrap(ctx.env);
|
||||
|
||||
// Find spec source — either per-spec textarea or embedded in overview
|
||||
var specEl = document.getElementById("test-spec-" + sn);
|
||||
if (specEl) {
|
||||
evalSource(specEl.textContent, ctx.env);
|
||||
}
|
||||
}
|
||||
|
||||
var elapsed = Math.round(performance.now() - t0);
|
||||
var r = ctx.getResults();
|
||||
r.lines.push("");
|
||||
r.lines.push("1.." + r.num);
|
||||
r.lines.push("# tests " + (r.passed + r.failed));
|
||||
r.lines.push("# pass " + r.passed);
|
||||
if (r.failed > 0) r.lines.push("# fail " + r.failed);
|
||||
r.lines.push("# time " + elapsed + "ms");
|
||||
} catch(e) {
|
||||
var r = ctx.getResults();
|
||||
r.lines.push("");
|
||||
r.lines.push("FATAL: " + (e.message || String(e)));
|
||||
}
|
||||
|
||||
out.textContent = r.lines.join("\n");
|
||||
out.style.display = "block";
|
||||
btn.textContent = r.passed + "/" + (r.passed + r.failed) + " passed" + (r.failed === 0 ? "" : " (" + r.failed + " failed)");
|
||||
btn.className = r.failed > 0
|
||||
? "px-4 py-2 rounded-md bg-red-600 text-white font-medium text-sm cursor-default"
|
||||
: "px-4 py-2 rounded-md bg-green-600 text-white font-medium text-sm cursor-default";
|
||||
};
|
||||
|
||||
494
shared/sx/ref/test-eval.sx
Normal file
494
shared/sx/ref/test-eval.sx
Normal file
@@ -0,0 +1,494 @@
|
||||
;; ==========================================================================
|
||||
;; test-eval.sx — Tests for the core evaluator and primitives
|
||||
;;
|
||||
;; Requires: test-framework.sx loaded first.
|
||||
;; Modules tested: eval.sx, primitives.sx
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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}))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Components
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "components"
|
||||
(deftest "defcomp creates component"
|
||||
(defcomp ~test-comp (&key title)
|
||||
(div title))
|
||||
(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)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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.
|
||||
)
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 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))))
|
||||
86
shared/sx/ref/test-framework.sx
Normal file
86
shared/sx/ref/test-framework.sx
Normal file
@@ -0,0 +1,86 @@
|
||||
;; ==========================================================================
|
||||
;; test-framework.sx — Reusable test macros and assertion helpers
|
||||
;;
|
||||
;; Loaded first by all test runners. Provides deftest, defsuite, and
|
||||
;; assertion helpers. Requires 5 platform functions from the host:
|
||||
;;
|
||||
;; 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
|
||||
;;
|
||||
;; Any host that provides these 5 functions can run any test spec.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 1. Test framework macros
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(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)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 2. Assertion helpers — defined in SX, available in test bodies
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(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)
|
||||
(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)
|
||||
(let ((result (try-call thunk)))
|
||||
(assert (not (get result "ok"))
|
||||
"Expected an error to be thrown but none was"))))
|
||||
222
shared/sx/ref/test-parser.sx
Normal file
222
shared/sx/ref/test-parser.sx
Normal file
@@ -0,0 +1,222 @@
|
||||
;; ==========================================================================
|
||||
;; test-parser.sx — Tests for the SX parser and serializer
|
||||
;;
|
||||
;; Requires: test-framework.sx loaded first.
|
||||
;; Modules tested: parser.sx
|
||||
;;
|
||||
;; Platform functions required (beyond test framework):
|
||||
;; sx-parse (source) -> list of AST expressions
|
||||
;; sx-serialize (expr) -> SX source string
|
||||
;; make-symbol (name) -> Symbol value
|
||||
;; make-keyword (name) -> Keyword value
|
||||
;; symbol-name (sym) -> string
|
||||
;; keyword-name (kw) -> string
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Literal parsing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "parser-literals"
|
||||
(deftest "parse integers"
|
||||
(assert-equal (list 42) (sx-parse "42"))
|
||||
(assert-equal (list 0) (sx-parse "0"))
|
||||
(assert-equal (list -7) (sx-parse "-7")))
|
||||
|
||||
(deftest "parse floats"
|
||||
(assert-equal (list 3.14) (sx-parse "3.14"))
|
||||
(assert-equal (list -0.5) (sx-parse "-0.5")))
|
||||
|
||||
(deftest "parse strings"
|
||||
(assert-equal (list "hello") (sx-parse "\"hello\""))
|
||||
(assert-equal (list "") (sx-parse "\"\"")))
|
||||
|
||||
(deftest "parse escape: newline"
|
||||
(assert-equal (list "a\nb") (sx-parse "\"a\\nb\"")))
|
||||
|
||||
(deftest "parse escape: tab"
|
||||
(assert-equal (list "a\tb") (sx-parse "\"a\\tb\"")))
|
||||
|
||||
(deftest "parse escape: quote"
|
||||
(assert-equal (list "a\"b") (sx-parse "\"a\\\"b\"")))
|
||||
|
||||
(deftest "parse booleans"
|
||||
(assert-equal (list true) (sx-parse "true"))
|
||||
(assert-equal (list false) (sx-parse "false")))
|
||||
|
||||
(deftest "parse nil"
|
||||
(assert-equal (list nil) (sx-parse "nil")))
|
||||
|
||||
(deftest "parse keywords"
|
||||
(let ((result (sx-parse ":hello")))
|
||||
(assert-length 1 result)
|
||||
(assert-equal "hello" (keyword-name (first result)))))
|
||||
|
||||
(deftest "parse symbols"
|
||||
(let ((result (sx-parse "foo")))
|
||||
(assert-length 1 result)
|
||||
(assert-equal "foo" (symbol-name (first result))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Composite parsing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "parser-lists"
|
||||
(deftest "parse empty list"
|
||||
(let ((result (sx-parse "()")))
|
||||
(assert-length 1 result)
|
||||
(assert-equal (list) (first result))))
|
||||
|
||||
(deftest "parse list of numbers"
|
||||
(let ((result (sx-parse "(1 2 3)")))
|
||||
(assert-length 1 result)
|
||||
(assert-equal (list 1 2 3) (first result))))
|
||||
|
||||
(deftest "parse nested lists"
|
||||
(let ((result (sx-parse "(1 (2 3) 4)")))
|
||||
(assert-length 1 result)
|
||||
(assert-equal (list 1 (list 2 3) 4) (first result))))
|
||||
|
||||
(deftest "parse square brackets as list"
|
||||
(let ((result (sx-parse "[1 2 3]")))
|
||||
(assert-length 1 result)
|
||||
(assert-equal (list 1 2 3) (first result))))
|
||||
|
||||
(deftest "parse mixed types"
|
||||
(let ((result (sx-parse "(42 \"hello\" true nil)")))
|
||||
(assert-length 1 result)
|
||||
(let ((lst (first result)))
|
||||
(assert-equal 42 (nth lst 0))
|
||||
(assert-equal "hello" (nth lst 1))
|
||||
(assert-equal true (nth lst 2))
|
||||
(assert-nil (nth lst 3))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Dict parsing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "parser-dicts"
|
||||
(deftest "parse empty dict"
|
||||
(let ((result (sx-parse "{}")))
|
||||
(assert-length 1 result)
|
||||
(assert-type "dict" (first result))))
|
||||
|
||||
(deftest "parse dict with keyword keys"
|
||||
(let ((result (sx-parse "{:a 1 :b 2}")))
|
||||
(assert-length 1 result)
|
||||
(let ((d (first result)))
|
||||
(assert-type "dict" d)
|
||||
(assert-equal 1 (get d "a"))
|
||||
(assert-equal 2 (get d "b")))))
|
||||
|
||||
(deftest "parse dict with string values"
|
||||
(let ((result (sx-parse "{:name \"alice\"}")))
|
||||
(assert-length 1 result)
|
||||
(assert-equal "alice" (get (first result) "name")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Comments and whitespace
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "parser-whitespace"
|
||||
(deftest "skip line comments"
|
||||
(assert-equal (list 42) (sx-parse ";; comment\n42"))
|
||||
(assert-equal (list 1 2) (sx-parse "1 ;; middle\n2")))
|
||||
|
||||
(deftest "skip whitespace"
|
||||
(assert-equal (list 42) (sx-parse " 42 "))
|
||||
(assert-equal (list 1 2) (sx-parse " 1 \n\t 2 ")))
|
||||
|
||||
(deftest "parse multiple top-level expressions"
|
||||
(assert-length 3 (sx-parse "1 2 3"))
|
||||
(assert-equal (list 1 2 3) (sx-parse "1 2 3")))
|
||||
|
||||
(deftest "empty input"
|
||||
(assert-equal (list) (sx-parse "")))
|
||||
|
||||
(deftest "only comments"
|
||||
(assert-equal (list) (sx-parse ";; just a comment\n;; another"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Quote sugar
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "parser-quote-sugar"
|
||||
(deftest "quasiquote"
|
||||
(let ((result (sx-parse "`foo")))
|
||||
(assert-length 1 result)
|
||||
(let ((expr (first result)))
|
||||
(assert-type "list" expr)
|
||||
(assert-equal "quasiquote" (symbol-name (first expr))))))
|
||||
|
||||
(deftest "unquote"
|
||||
(let ((result (sx-parse ",foo")))
|
||||
(assert-length 1 result)
|
||||
(let ((expr (first result)))
|
||||
(assert-type "list" expr)
|
||||
(assert-equal "unquote" (symbol-name (first expr))))))
|
||||
|
||||
(deftest "splice-unquote"
|
||||
(let ((result (sx-parse ",@foo")))
|
||||
(assert-length 1 result)
|
||||
(let ((expr (first result)))
|
||||
(assert-type "list" expr)
|
||||
(assert-equal "splice-unquote" (symbol-name (first expr)))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Serializer
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "serializer"
|
||||
(deftest "serialize number"
|
||||
(assert-equal "42" (sx-serialize 42)))
|
||||
|
||||
(deftest "serialize string"
|
||||
(assert-equal "\"hello\"" (sx-serialize "hello")))
|
||||
|
||||
(deftest "serialize boolean"
|
||||
(assert-equal "true" (sx-serialize true))
|
||||
(assert-equal "false" (sx-serialize false)))
|
||||
|
||||
(deftest "serialize nil"
|
||||
(assert-equal "nil" (sx-serialize nil)))
|
||||
|
||||
(deftest "serialize keyword"
|
||||
(assert-equal ":foo" (sx-serialize (make-keyword "foo"))))
|
||||
|
||||
(deftest "serialize symbol"
|
||||
(assert-equal "bar" (sx-serialize (make-symbol "bar"))))
|
||||
|
||||
(deftest "serialize list"
|
||||
(assert-equal "(1 2 3)" (sx-serialize (list 1 2 3))))
|
||||
|
||||
(deftest "serialize empty list"
|
||||
(assert-equal "()" (sx-serialize (list))))
|
||||
|
||||
(deftest "serialize nested"
|
||||
(assert-equal "(1 (2 3) 4)" (sx-serialize (list 1 (list 2 3) 4)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Round-trip: parse then serialize
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "parser-roundtrip"
|
||||
(deftest "roundtrip number"
|
||||
(assert-equal "42" (sx-serialize (first (sx-parse "42")))))
|
||||
|
||||
(deftest "roundtrip string"
|
||||
(assert-equal "\"hello\"" (sx-serialize (first (sx-parse "\"hello\"")))))
|
||||
|
||||
(deftest "roundtrip list"
|
||||
(assert-equal "(1 2 3)" (sx-serialize (first (sx-parse "(1 2 3)")))))
|
||||
|
||||
(deftest "roundtrip nested"
|
||||
(assert-equal "(a (b c))"
|
||||
(sx-serialize (first (sx-parse "(a (b c))"))))))
|
||||
167
shared/sx/ref/test-render.sx
Normal file
167
shared/sx/ref/test-render.sx
Normal file
@@ -0,0 +1,167 @@
|
||||
;; ==========================================================================
|
||||
;; test-render.sx — Tests for the HTML rendering adapter
|
||||
;;
|
||||
;; Requires: test-framework.sx loaded first.
|
||||
;; Modules tested: render.sx, adapter-html.sx
|
||||
;;
|
||||
;; Platform functions required (beyond test framework):
|
||||
;; render-html (sx-source) -> HTML string
|
||||
;; Parses the sx-source string, evaluates via render-to-html in a
|
||||
;; fresh env, and returns the resulting HTML string.
|
||||
;; (This is a test-only convenience that wraps parse + render-to-html.)
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Basic element rendering
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "render-elements"
|
||||
(deftest "simple div"
|
||||
(assert-equal "<div>hello</div>"
|
||||
(render-html "(div \"hello\")")))
|
||||
|
||||
(deftest "nested elements"
|
||||
(assert-equal "<div><span>hi</span></div>"
|
||||
(render-html "(div (span \"hi\"))")))
|
||||
|
||||
(deftest "multiple children"
|
||||
(assert-equal "<div><p>a</p><p>b</p></div>"
|
||||
(render-html "(div (p \"a\") (p \"b\"))")))
|
||||
|
||||
(deftest "text content"
|
||||
(assert-equal "<p>hello world</p>"
|
||||
(render-html "(p \"hello\" \" world\")")))
|
||||
|
||||
(deftest "number content"
|
||||
(assert-equal "<span>42</span>"
|
||||
(render-html "(span 42)"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Attributes
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "render-attrs"
|
||||
(deftest "string attribute"
|
||||
(let ((html (render-html "(div :id \"main\" \"content\")")))
|
||||
(assert-true (string-contains? html "id=\"main\""))
|
||||
(assert-true (string-contains? html "content"))))
|
||||
|
||||
(deftest "class attribute"
|
||||
(let ((html (render-html "(div :class \"foo bar\" \"x\")")))
|
||||
(assert-true (string-contains? html "class=\"foo bar\""))))
|
||||
|
||||
(deftest "multiple attributes"
|
||||
(let ((html (render-html "(a :href \"/home\" :class \"link\" \"Home\")")))
|
||||
(assert-true (string-contains? html "href=\"/home\""))
|
||||
(assert-true (string-contains? html "class=\"link\""))
|
||||
(assert-true (string-contains? html "Home")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Void elements
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "render-void"
|
||||
(deftest "br is self-closing"
|
||||
(assert-equal "<br />" (render-html "(br)")))
|
||||
|
||||
(deftest "img with attrs"
|
||||
(let ((html (render-html "(img :src \"pic.jpg\" :alt \"A pic\")")))
|
||||
(assert-true (string-contains? html "<img"))
|
||||
(assert-true (string-contains? html "src=\"pic.jpg\""))
|
||||
(assert-true (string-contains? html "/>"))
|
||||
;; void elements should not have a closing tag
|
||||
(assert-false (string-contains? html "</img>"))))
|
||||
|
||||
(deftest "input is self-closing"
|
||||
(let ((html (render-html "(input :type \"text\" :name \"q\")")))
|
||||
(assert-true (string-contains? html "<input"))
|
||||
(assert-true (string-contains? html "/>")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Boolean attributes
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "render-boolean-attrs"
|
||||
(deftest "true boolean attr emits name only"
|
||||
(let ((html (render-html "(input :disabled true :type \"text\")")))
|
||||
(assert-true (string-contains? html "disabled"))
|
||||
;; Should NOT have disabled="true"
|
||||
(assert-false (string-contains? html "disabled=\""))))
|
||||
|
||||
(deftest "false boolean attr omitted"
|
||||
(let ((html (render-html "(input :disabled false :type \"text\")")))
|
||||
(assert-false (string-contains? html "disabled")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Fragments
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "render-fragments"
|
||||
(deftest "fragment renders children without wrapper"
|
||||
(assert-equal "<p>a</p><p>b</p>"
|
||||
(render-html "(<> (p \"a\") (p \"b\"))")))
|
||||
|
||||
(deftest "empty fragment"
|
||||
(assert-equal ""
|
||||
(render-html "(<>)"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; HTML escaping
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "render-escaping"
|
||||
(deftest "text content is escaped"
|
||||
(let ((html (render-html "(p \"<script>alert(1)</script>\")")))
|
||||
(assert-false (string-contains? html "<script>"))
|
||||
(assert-true (string-contains? html "<script>"))))
|
||||
|
||||
(deftest "attribute values are escaped"
|
||||
(let ((html (render-html "(div :title \"a\\\"b\" \"x\")")))
|
||||
(assert-true (string-contains? html "title=")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Control flow in render context
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "render-control-flow"
|
||||
(deftest "if renders correct branch"
|
||||
(assert-equal "<p>yes</p>"
|
||||
(render-html "(if true (p \"yes\") (p \"no\"))"))
|
||||
(assert-equal "<p>no</p>"
|
||||
(render-html "(if false (p \"yes\") (p \"no\"))")))
|
||||
|
||||
(deftest "when renders or skips"
|
||||
(assert-equal "<p>ok</p>"
|
||||
(render-html "(when true (p \"ok\"))"))
|
||||
(assert-equal ""
|
||||
(render-html "(when false (p \"ok\"))")))
|
||||
|
||||
(deftest "map renders list"
|
||||
(assert-equal "<li>1</li><li>2</li><li>3</li>"
|
||||
(render-html "(map (fn (x) (li x)) (list 1 2 3))")))
|
||||
|
||||
(deftest "let in render context"
|
||||
(assert-equal "<p>hello</p>"
|
||||
(render-html "(let ((x \"hello\")) (p x))"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Component rendering
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "render-components"
|
||||
(deftest "component with keyword args"
|
||||
(assert-equal "<h1>Hello</h1>"
|
||||
(render-html "(do (defcomp ~title (&key text) (h1 text)) (~title :text \"Hello\"))")))
|
||||
|
||||
(deftest "component with children"
|
||||
(let ((html (render-html "(do (defcomp ~box (&key &rest children) (div :class \"box\" children)) (~box (p \"inside\")))")))
|
||||
(assert-true (string-contains? html "class=\"box\""))
|
||||
(assert-true (string-contains? html "<p>inside</p>")))))
|
||||
125
shared/sx/ref/test-router.sx
Normal file
125
shared/sx/ref/test-router.sx
Normal file
@@ -0,0 +1,125 @@
|
||||
;; ==========================================================================
|
||||
;; test-router.sx — Tests for client-side route matching
|
||||
;;
|
||||
;; Requires: test-framework.sx loaded first.
|
||||
;; Modules tested: router.sx
|
||||
;;
|
||||
;; No additional platform functions needed — router.sx is pure.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; split-path-segments
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "split-path-segments"
|
||||
(deftest "root path"
|
||||
(assert-equal (list) (split-path-segments "/")))
|
||||
|
||||
(deftest "single segment"
|
||||
(assert-equal (list "docs") (split-path-segments "/docs")))
|
||||
|
||||
(deftest "multiple segments"
|
||||
(assert-equal (list "docs" "hello") (split-path-segments "/docs/hello")))
|
||||
|
||||
(deftest "trailing slash stripped"
|
||||
(assert-equal (list "docs") (split-path-segments "/docs/")))
|
||||
|
||||
(deftest "deep path"
|
||||
(assert-equal (list "a" "b" "c" "d") (split-path-segments "/a/b/c/d"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; parse-route-pattern
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "parse-route-pattern"
|
||||
(deftest "static pattern"
|
||||
(let ((segs (parse-route-pattern "/docs/intro")))
|
||||
(assert-length 2 segs)
|
||||
(assert-equal "literal" (get (first segs) "type"))
|
||||
(assert-equal "docs" (get (first segs) "value"))
|
||||
(assert-equal "literal" (get (nth segs 1) "type"))
|
||||
(assert-equal "intro" (get (nth segs 1) "value"))))
|
||||
|
||||
(deftest "pattern with param"
|
||||
(let ((segs (parse-route-pattern "/docs/<slug>")))
|
||||
(assert-length 2 segs)
|
||||
(assert-equal "literal" (get (first segs) "type"))
|
||||
(assert-equal "docs" (get (first segs) "value"))
|
||||
(assert-equal "param" (get (nth segs 1) "type"))
|
||||
(assert-equal "slug" (get (nth segs 1) "value"))))
|
||||
|
||||
(deftest "multiple params"
|
||||
(let ((segs (parse-route-pattern "/users/<uid>/posts/<pid>")))
|
||||
(assert-length 4 segs)
|
||||
(assert-equal "param" (get (nth segs 1) "type"))
|
||||
(assert-equal "uid" (get (nth segs 1) "value"))
|
||||
(assert-equal "param" (get (nth segs 3) "type"))
|
||||
(assert-equal "pid" (get (nth segs 3) "value"))))
|
||||
|
||||
(deftest "root pattern"
|
||||
(assert-equal (list) (parse-route-pattern "/"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; match-route
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "match-route"
|
||||
(deftest "exact match returns empty params"
|
||||
(let ((result (match-route "/docs/intro" "/docs/intro")))
|
||||
(assert-true (not (nil? result)))
|
||||
(assert-length 0 (keys result))))
|
||||
|
||||
(deftest "param match extracts value"
|
||||
(let ((result (match-route "/docs/hello" "/docs/<slug>")))
|
||||
(assert-true (not (nil? result)))
|
||||
(assert-equal "hello" (get result "slug"))))
|
||||
|
||||
(deftest "no match returns nil"
|
||||
(assert-nil (match-route "/docs/hello" "/essays/<slug>"))
|
||||
(assert-nil (match-route "/docs" "/docs/<slug>")))
|
||||
|
||||
(deftest "segment count mismatch returns nil"
|
||||
(assert-nil (match-route "/a/b/c" "/a/<b>"))
|
||||
(assert-nil (match-route "/a" "/a/b")))
|
||||
|
||||
(deftest "root matches root"
|
||||
(let ((result (match-route "/" "/")))
|
||||
(assert-true (not (nil? result)))))
|
||||
|
||||
(deftest "multiple params extracted"
|
||||
(let ((result (match-route "/users/42/posts/99" "/users/<uid>/posts/<pid>")))
|
||||
(assert-true (not (nil? result)))
|
||||
(assert-equal "42" (get result "uid"))
|
||||
(assert-equal "99" (get result "pid")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; find-matching-route
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "find-matching-route"
|
||||
(deftest "finds first matching route"
|
||||
(let ((routes (list
|
||||
{:pattern "/docs/" :parsed (parse-route-pattern "/docs/") :name "docs-index"}
|
||||
{:pattern "/docs/<slug>" :parsed (parse-route-pattern "/docs/<slug>") :name "docs-page"})))
|
||||
(let ((result (find-matching-route "/docs/hello" routes)))
|
||||
(assert-true (not (nil? result)))
|
||||
(assert-equal "docs-page" (get result "name"))
|
||||
(assert-equal "hello" (get (get result "params") "slug")))))
|
||||
|
||||
(deftest "returns nil for no match"
|
||||
(let ((routes (list
|
||||
{:pattern "/docs/<slug>" :parsed (parse-route-pattern "/docs/<slug>") :name "docs-page"})))
|
||||
(assert-nil (find-matching-route "/essays/hello" routes))))
|
||||
|
||||
(deftest "matches exact routes before param routes"
|
||||
(let ((routes (list
|
||||
{:pattern "/docs/" :parsed (parse-route-pattern "/docs/") :name "docs-index"}
|
||||
{:pattern "/docs/<slug>" :parsed (parse-route-pattern "/docs/<slug>") :name "docs-page"})))
|
||||
;; /docs/ should match docs-index, not docs-page
|
||||
(let ((result (find-matching-route "/docs/" routes)))
|
||||
(assert-true (not (nil? result)))
|
||||
(assert-equal "docs-index" (get result "name"))))))
|
||||
@@ -1,16 +1,21 @@
|
||||
;; ==========================================================================
|
||||
;; test.sx — Self-hosting SX test framework
|
||||
;; test.sx — Self-hosting SX test suite (backward-compatible entry point)
|
||||
;;
|
||||
;; 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.
|
||||
;; This file includes the test framework and core eval tests inline.
|
||||
;; It exists for backward compatibility — runners that load "test.sx"
|
||||
;; get the same 81 tests as before.
|
||||
;;
|
||||
;; For modular testing, runners should instead load:
|
||||
;; 1. test-framework.sx (macros + assertions)
|
||||
;; 2. One or more test specs: test-eval.sx, test-parser.sx,
|
||||
;; test-router.sx, test-render.sx, etc.
|
||||
;;
|
||||
;; Platform functions required:
|
||||
;; 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
|
||||
;; 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
|
||||
;;
|
||||
;; Usage:
|
||||
;; ;; Host injects platform functions into env, then:
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// Run test.sx directly against sx-browser.js.
|
||||
// Run SX test specs against sx-browser.js.
|
||||
//
|
||||
// sx-browser.js parses and evaluates test.sx — SX tests itself.
|
||||
// sx-browser.js parses and evaluates test specs — SX tests itself.
|
||||
// This script provides only platform functions (error catching, reporting).
|
||||
//
|
||||
// Usage: node shared/sx/tests/run.js
|
||||
// Usage:
|
||||
// node shared/sx/tests/run.js # run all available specs
|
||||
// node shared/sx/tests/run.js eval # run only test-eval.sx
|
||||
// node shared/sx/tests/run.js eval parser router # run specific specs
|
||||
// node shared/sx/tests/run.js --legacy # run monolithic test.sx
|
||||
|
||||
Object.defineProperty(globalThis, "document", { value: undefined, writable: true });
|
||||
var path = require("path");
|
||||
@@ -86,15 +90,149 @@ var env = {
|
||||
},
|
||||
"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]); },
|
||||
"for-each-indexed": function(f, coll) {
|
||||
for (var i = 0; i < (coll||[]).length; i++) {
|
||||
Sx.eval([f, i, coll[i]], env);
|
||||
}
|
||||
},
|
||||
"dict-set!": function(d, k, v) { if (d) d[k] = v; },
|
||||
"dict-has?": function(d, k) { return d && typeof d === "object" && k in d; },
|
||||
"dict-get": function(d, k) { return d ? d[k] : undefined; },
|
||||
"starts-with?": function(s, prefix) { return String(s).indexOf(prefix) === 0; },
|
||||
"ends-with?": function(s, suffix) { var str = String(s); return str.indexOf(suffix) === str.length - suffix.length; },
|
||||
"slice": function(s, start, end) { return end !== undefined ? s.slice(start, end) : s.slice(start); },
|
||||
"inc": function(n) { return n + 1; },
|
||||
"append!": function(arr, item) { if (Array.isArray(arr)) arr.push(item); },
|
||||
"dict": function() { return {}; },
|
||||
"for-each": function(f, coll) {
|
||||
for (var i = 0; i < (coll||[]).length; i++) {
|
||||
Sx.eval([f, coll[i]], env);
|
||||
}
|
||||
},
|
||||
|
||||
// --- Parser platform functions (for test-parser.sx) ---
|
||||
"sx-parse": function(source) { return Sx.parseAll(source); },
|
||||
"sx-serialize": function(val) {
|
||||
// Basic serializer for test roundtrips
|
||||
if (val === Sx.NIL || val === null || val === undefined) return "nil";
|
||||
if (typeof val === "boolean") return val ? "true" : "false";
|
||||
if (typeof val === "number") return String(val);
|
||||
if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
||||
// Check Symbol/Keyword BEFORE generic object — they are objects too
|
||||
if (val && (val._sym || val._sx_symbol)) return val.name;
|
||||
if (val && (val._kw || val._sx_keyword)) return ":" + val.name;
|
||||
if (Array.isArray(val)) return "(" + val.map(function(x) { return env["sx-serialize"](x); }).join(" ") + ")";
|
||||
if (val && typeof val === "object") {
|
||||
var parts = [];
|
||||
Object.keys(val).forEach(function(k) {
|
||||
parts.push(":" + k);
|
||||
parts.push(env["sx-serialize"](val[k]));
|
||||
});
|
||||
return "{" + parts.join(" ") + "}";
|
||||
}
|
||||
return String(val);
|
||||
},
|
||||
"make-symbol": function(name) { return Sx.sym ? Sx.sym(name) : { _sx_symbol: true, name: name, toString: function() { return name; } }; },
|
||||
"make-keyword": function(name) { return Sx.kw ? Sx.kw(name) : { _sx_keyword: true, name: name, toString: function() { return name; } }; },
|
||||
"symbol-name": function(s) { return s && s.name ? s.name : String(s); },
|
||||
"keyword-name": function(k) { return k && k.name ? k.name : String(k); },
|
||||
|
||||
// --- Render platform function (for test-render.sx) ---
|
||||
"render-html": function(sxSource) {
|
||||
if (!Sx.renderToHtml) throw new Error("render-to-html not available — html adapter not bootstrapped");
|
||||
var exprs = Sx.parseAll(sxSource);
|
||||
var result = "";
|
||||
for (var i = 0; i < exprs.length; i++) {
|
||||
result += Sx.renderToHtml(exprs[i], env);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
// --- Read and evaluate test.sx ---
|
||||
var src = fs.readFileSync(path.resolve(__dirname, "../ref/test.sx"), "utf8");
|
||||
var exprs = Sx.parseAll(src);
|
||||
// --- Resolve which test specs to run ---
|
||||
var refDir = path.resolve(__dirname, "../ref");
|
||||
var args = process.argv.slice(2);
|
||||
|
||||
console.log("TAP version 13");
|
||||
for (var i = 0; i < exprs.length; i++) {
|
||||
Sx.eval(exprs[i], env);
|
||||
// Available spec modules and their platform requirements
|
||||
var SPECS = {
|
||||
"eval": { file: "test-eval.sx", needs: [] },
|
||||
"parser": { file: "test-parser.sx", needs: ["sx-parse"] },
|
||||
"router": { file: "test-router.sx", needs: [] },
|
||||
"render": { file: "test-render.sx", needs: ["render-html"] },
|
||||
};
|
||||
|
||||
function evalFile(filename) {
|
||||
var filepath = path.resolve(refDir, filename);
|
||||
if (!fs.existsSync(filepath)) {
|
||||
console.log("# SKIP " + filename + " (file not found)");
|
||||
return;
|
||||
}
|
||||
var src = fs.readFileSync(filepath, "utf8");
|
||||
var exprs = Sx.parseAll(src);
|
||||
for (var i = 0; i < exprs.length; i++) {
|
||||
Sx.eval(exprs[i], env);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy mode — run monolithic test.sx
|
||||
if (args[0] === "--legacy") {
|
||||
console.log("TAP version 13");
|
||||
evalFile("test.sx");
|
||||
} else {
|
||||
// Determine which specs to run
|
||||
var specsToRun;
|
||||
if (args.length > 0) {
|
||||
specsToRun = args;
|
||||
} else {
|
||||
// Auto-discover: run all specs whose platform functions are available
|
||||
specsToRun = Object.keys(SPECS);
|
||||
}
|
||||
|
||||
console.log("TAP version 13");
|
||||
|
||||
// Always load framework first
|
||||
evalFile("test-framework.sx");
|
||||
|
||||
// Load router.sx if testing router (it defines the functions being tested)
|
||||
for (var si = 0; si < specsToRun.length; si++) {
|
||||
var specName = specsToRun[si];
|
||||
var spec = SPECS[specName];
|
||||
if (!spec) {
|
||||
console.log("# SKIP unknown spec: " + specName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check platform requirements
|
||||
var canRun = true;
|
||||
for (var ni = 0; ni < spec.needs.length; ni++) {
|
||||
if (!(spec.needs[ni] in env)) {
|
||||
console.log("# SKIP " + specName + " (missing: " + spec.needs[ni] + ")");
|
||||
canRun = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!canRun) continue;
|
||||
|
||||
// Load prerequisite spec modules
|
||||
if (specName === "router") {
|
||||
// Use bootstrapped router functions from sx-browser.js.
|
||||
// The bare evaluator can't run router.sx faithfully because set!
|
||||
// inside lambda closures doesn't propagate (dict copies, not cells).
|
||||
if (Sx.splitPathSegments) {
|
||||
env["split-path-segments"] = Sx.splitPathSegments;
|
||||
env["parse-route-pattern"] = Sx.parseRoutePattern;
|
||||
env["match-route-segments"] = Sx.matchRouteSegments;
|
||||
env["match-route"] = Sx.matchRoute;
|
||||
env["find-matching-route"] = Sx.findMatchingRoute;
|
||||
env["make-route-segment"] = Sx.makeRouteSegment;
|
||||
} else {
|
||||
evalFile("router.sx");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("# --- " + specName + " ---");
|
||||
evalFile(spec.file);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Summary ---
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run test.sx directly against the Python SX evaluator.
|
||||
"""Run SX test specs against the Python SX evaluator.
|
||||
|
||||
The Python evaluator parses and evaluates test.sx — SX tests itself.
|
||||
The Python evaluator parses and evaluates test specs — SX tests itself.
|
||||
This script provides only platform functions (error catching, reporting).
|
||||
|
||||
Usage: python shared/sx/tests/run.py
|
||||
Usage:
|
||||
python shared/sx/tests/run.py # run all available specs
|
||||
python shared/sx/tests/run.py eval # run only test-eval.sx
|
||||
python shared/sx/tests/run.py eval parser router # run specific specs
|
||||
python shared/sx/tests/run.py --legacy # run monolithic test.sx
|
||||
"""
|
||||
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
|
||||
from shared.sx.evaluator import _eval, _trampoline, _call_lambda
|
||||
from shared.sx.types import Symbol, Keyword, Lambda, NIL
|
||||
|
||||
# --- Test state ---
|
||||
suite_stack: list[str] = []
|
||||
@@ -29,7 +33,7 @@ test_num = 0
|
||||
def try_call(thunk):
|
||||
"""Call an SX thunk, catching errors."""
|
||||
try:
|
||||
_trampoline(_eval([thunk], {}))
|
||||
_trampoline(_eval([thunk], env))
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
@@ -60,25 +64,253 @@ 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,
|
||||
}
|
||||
# --- Parser platform functions ---
|
||||
|
||||
test_sx = os.path.join(_HERE, "..", "ref", "test.sx")
|
||||
with open(test_sx) as f:
|
||||
def sx_parse(source):
|
||||
"""Parse SX source string into list of AST expressions."""
|
||||
return parse_all(source)
|
||||
|
||||
|
||||
def sx_serialize(val):
|
||||
"""Serialize an AST value to SX source text."""
|
||||
if val is None or val is NIL:
|
||||
return "nil"
|
||||
if isinstance(val, bool):
|
||||
return "true" if val else "false"
|
||||
if isinstance(val, (int, float)):
|
||||
return str(val)
|
||||
if isinstance(val, str):
|
||||
escaped = val.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
if isinstance(val, Symbol):
|
||||
return val.name
|
||||
if isinstance(val, Keyword):
|
||||
return f":{val.name}"
|
||||
if isinstance(val, list):
|
||||
inner = " ".join(sx_serialize(x) for x in val)
|
||||
return f"({inner})"
|
||||
if isinstance(val, dict):
|
||||
parts = []
|
||||
for k, v in val.items():
|
||||
parts.append(f":{k}")
|
||||
parts.append(sx_serialize(v))
|
||||
return "{" + " ".join(parts) + "}"
|
||||
return str(val)
|
||||
|
||||
|
||||
def make_symbol(name):
|
||||
return Symbol(name)
|
||||
|
||||
|
||||
def make_keyword(name):
|
||||
return Keyword(name)
|
||||
|
||||
|
||||
def symbol_name(sym):
|
||||
if isinstance(sym, Symbol):
|
||||
return sym.name
|
||||
return str(sym)
|
||||
|
||||
|
||||
def keyword_name(kw):
|
||||
if isinstance(kw, Keyword):
|
||||
return kw.name
|
||||
return str(kw)
|
||||
|
||||
|
||||
# --- Render platform function ---
|
||||
|
||||
def render_html(sx_source):
|
||||
"""Parse SX source and render to HTML via the bootstrapped evaluator."""
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import render_to_html as _render_to_html
|
||||
except ImportError:
|
||||
raise RuntimeError("render-to-html not available — sx_ref.py not built")
|
||||
exprs = parse_all(sx_source)
|
||||
render_env = dict(env)
|
||||
result = ""
|
||||
for expr in exprs:
|
||||
result += _render_to_html(expr, render_env)
|
||||
return result
|
||||
|
||||
|
||||
# --- Spec registry ---
|
||||
|
||||
SPECS = {
|
||||
"eval": {"file": "test-eval.sx", "needs": []},
|
||||
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
|
||||
"router": {"file": "test-router.sx", "needs": []},
|
||||
"render": {"file": "test-render.sx", "needs": ["render-html"]},
|
||||
}
|
||||
|
||||
REF_DIR = os.path.join(_HERE, "..", "ref")
|
||||
|
||||
|
||||
def eval_file(filename, env):
|
||||
"""Load and evaluate an SX file."""
|
||||
filepath = os.path.join(REF_DIR, filename)
|
||||
if not os.path.exists(filepath):
|
||||
print(f"# SKIP {filename} (file not found)")
|
||||
return
|
||||
with open(filepath) as f:
|
||||
src = f.read()
|
||||
|
||||
exprs = parse_all(src)
|
||||
|
||||
print("TAP version 13")
|
||||
for expr in exprs:
|
||||
_trampoline(_eval(expr, env))
|
||||
|
||||
|
||||
# --- Build env ---
|
||||
env = {
|
||||
"try-call": try_call,
|
||||
"report-pass": report_pass,
|
||||
"report-fail": report_fail,
|
||||
"push-suite": push_suite,
|
||||
"pop-suite": pop_suite,
|
||||
# Parser platform functions
|
||||
"sx-parse": sx_parse,
|
||||
"sx-serialize": sx_serialize,
|
||||
"make-symbol": make_symbol,
|
||||
"make-keyword": make_keyword,
|
||||
"symbol-name": symbol_name,
|
||||
"keyword-name": keyword_name,
|
||||
# Render platform function
|
||||
"render-html": render_html,
|
||||
# Extra primitives needed by spec modules (router.sx, deps.sx)
|
||||
"for-each-indexed": "_deferred", # replaced below
|
||||
"dict-set!": "_deferred",
|
||||
"dict-has?": "_deferred",
|
||||
"dict-get": "_deferred",
|
||||
"append!": "_deferred",
|
||||
"inc": lambda n: n + 1,
|
||||
}
|
||||
|
||||
|
||||
def _call_sx(fn, args, caller_env):
|
||||
"""Call an SX lambda or native function with args."""
|
||||
if isinstance(fn, Lambda):
|
||||
return _trampoline(_call_lambda(fn, list(args), caller_env))
|
||||
return fn(*args)
|
||||
|
||||
|
||||
def _for_each_indexed(fn, coll):
|
||||
"""for-each-indexed that respects set! in lambda closures.
|
||||
|
||||
The hand-written evaluator copies envs on lambda calls, which breaks
|
||||
set! mutation of outer scope. We eval directly in the closure dict
|
||||
to match the bootstrapped semantics (cell-based mutation).
|
||||
"""
|
||||
if isinstance(fn, Lambda):
|
||||
closure = fn.closure
|
||||
for i, item in enumerate(coll or []):
|
||||
# Bind params directly in the closure (no copy)
|
||||
for p, v in zip(fn.params, [i, item]):
|
||||
closure[p] = v
|
||||
_trampoline(_eval(fn.body, closure))
|
||||
else:
|
||||
for i, item in enumerate(coll or []):
|
||||
fn(i, item)
|
||||
return NIL
|
||||
|
||||
|
||||
def _dict_set(d, k, v):
|
||||
if isinstance(d, dict):
|
||||
d[k] = v
|
||||
return NIL
|
||||
|
||||
|
||||
def _dict_has(d, k):
|
||||
return isinstance(d, dict) and k in d
|
||||
|
||||
|
||||
def _dict_get(d, k):
|
||||
if isinstance(d, dict):
|
||||
return d.get(k, NIL)
|
||||
return NIL
|
||||
|
||||
|
||||
def _append_mut(lst, item):
|
||||
if isinstance(lst, list):
|
||||
lst.append(item)
|
||||
return NIL
|
||||
|
||||
|
||||
env["for-each-indexed"] = _for_each_indexed
|
||||
env["dict-set!"] = _dict_set
|
||||
env["dict-has?"] = _dict_has
|
||||
env["dict-get"] = _dict_get
|
||||
env["append!"] = _append_mut
|
||||
|
||||
|
||||
def _load_router_from_bootstrap(env):
|
||||
"""Load router functions from the bootstrapped sx_ref.py.
|
||||
|
||||
The hand-written evaluator can't run router.sx faithfully because
|
||||
set! inside lambda closures doesn't propagate to outer scopes
|
||||
(the evaluator uses dict copies, not cells). The bootstrapped code
|
||||
compiles set! to cell-based mutation, so we import from there.
|
||||
"""
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import (
|
||||
split_path_segments,
|
||||
parse_route_pattern,
|
||||
match_route_segments,
|
||||
match_route,
|
||||
find_matching_route,
|
||||
make_route_segment,
|
||||
)
|
||||
env["split-path-segments"] = split_path_segments
|
||||
env["parse-route-pattern"] = parse_route_pattern
|
||||
env["match-route-segments"] = match_route_segments
|
||||
env["match-route"] = match_route
|
||||
env["find-matching-route"] = find_matching_route
|
||||
env["make-route-segment"] = make_route_segment
|
||||
except ImportError:
|
||||
# Fallback: eval router.sx directly (may fail on set! scoping)
|
||||
eval_file("router.sx", env)
|
||||
|
||||
|
||||
def main():
|
||||
global passed, failed, test_num
|
||||
|
||||
args = sys.argv[1:]
|
||||
|
||||
# Legacy mode
|
||||
if args and args[0] == "--legacy":
|
||||
print("TAP version 13")
|
||||
eval_file("test.sx", env)
|
||||
else:
|
||||
# Determine which specs to run
|
||||
specs_to_run = args if args else list(SPECS.keys())
|
||||
|
||||
print("TAP version 13")
|
||||
|
||||
# Always load framework first
|
||||
eval_file("test-framework.sx", env)
|
||||
|
||||
for spec_name in specs_to_run:
|
||||
spec = SPECS.get(spec_name)
|
||||
if not spec:
|
||||
print(f"# SKIP unknown spec: {spec_name}")
|
||||
continue
|
||||
|
||||
# Check platform requirements
|
||||
can_run = True
|
||||
for need in spec["needs"]:
|
||||
if need not in env:
|
||||
print(f"# SKIP {spec_name} (missing: {need})")
|
||||
can_run = False
|
||||
break
|
||||
if not can_run:
|
||||
continue
|
||||
|
||||
# Load prerequisite spec modules
|
||||
if spec_name == "router":
|
||||
_load_router_from_bootstrap(env)
|
||||
|
||||
print(f"# --- {spec_name} ---")
|
||||
eval_file(spec["file"], env)
|
||||
|
||||
# Summary
|
||||
print()
|
||||
print(f"1..{test_num}")
|
||||
print(f"# tests {passed + failed}")
|
||||
|
||||
@@ -64,3 +64,8 @@
|
||||
:params ()
|
||||
:returns "dict"
|
||||
:service "sx")
|
||||
|
||||
(define-page-helper "run-modular-tests"
|
||||
:params (spec-name)
|
||||
:returns "dict"
|
||||
:service "sx")
|
||||
|
||||
199
sx/sx/essays.sx
199
sx/sx/essays.sx
@@ -478,3 +478,202 @@
|
||||
"Not because they were designed for AI. " (a :href "https://en.wikipedia.org/wiki/John_McCarthy_(computer_scientist)" :class "text-violet-600 hover:underline" "McCarthy") " invented them in 1958, decades before anyone imagined language models. But the properties that make s-expressions elegant for humans — minimalism, uniformity, composability, homoiconicity — turn out to be exactly the properties that make them tractable for machines. The simplest possible syntax is also the most machine-friendly syntax. This is not a coincidence. It is a consequence of what simplicity means.")
|
||||
(p :class "text-stone-600"
|
||||
"The era of AI-generated software is not coming. It is here. The question is which representations survive contact with it. The ones with the lowest syntax tax, the most uniform structure, and the tightest feedback loops will win — not because they are trendy, but because they are what the machines can actually produce reliably. S-expressions have been waiting sixty-seven years for a generation mechanism worthy of their simplicity. They finally have one."))))
|
||||
|
||||
(defcomp ~essay-no-alternative ()
|
||||
(~doc-page :title "There Is No Alternative"
|
||||
(p :class "text-stone-500 text-sm italic mb-8"
|
||||
"Every attempt to escape s-expressions leads back to s-expressions. This is not an accident.")
|
||||
|
||||
(~doc-section :title "The claim" :id "claim"
|
||||
(p :class "text-stone-600"
|
||||
"SX uses s-expressions. When people encounter this, the first reaction is usually: " (em "why not use something more modern?") " Fair question. The answer is that there is nothing more modern. There are only things that are more " (em "familiar") " — and familiarity is not the same as fitness.")
|
||||
(p :class "text-stone-600"
|
||||
"This essay examines what SX actually needs from its representation, surveys the alternatives, and shows that every candidate either fails to meet the requirements or converges toward s-expressions under a different name. The conclusion is uncomfortable but unavoidable: for what SX does, there is no alternative."))
|
||||
|
||||
(~doc-section :title "The requirements" :id "requirements"
|
||||
(p :class "text-stone-600"
|
||||
"SX is not just a templating language. It is a language that serves simultaneously as:")
|
||||
(ul :class "space-y-2 text-stone-600"
|
||||
(li (strong "Markup") " — structure for HTML pages, components, layouts")
|
||||
(li (strong "Programming language") " — conditionals, iteration, functions, macros, closures")
|
||||
(li (strong "Wire format") " — what the server sends to the client over HTTP")
|
||||
(li (strong "Data notation") " — configuration, page definitions, component registries")
|
||||
(li (strong "Spec language") " — the SX specification is written in SX")
|
||||
(li (strong "Metaprogramming substrate") " — macros that read, transform, and generate code"))
|
||||
(p :class "text-stone-600"
|
||||
"Any replacement must handle all six roles with a single syntax. Not six syntaxes awkwardly interleaved — one. This constraint alone eliminates most candidates, because most representations were designed for one of these roles and are ill-suited to the others.")
|
||||
(p :class "text-stone-600"
|
||||
"Beyond versatility, the representation must be:")
|
||||
(ul :class "space-y-2 text-stone-600"
|
||||
(li (strong "Homoiconic") " — code must be data and data must be code, because macros and self-hosting require it")
|
||||
(li (strong "Parseable in one pass") " — no forward references, no context-dependent grammar, because the wire format must be parseable by a minimal client")
|
||||
(li (strong "Structurally validatable") " — a syntactically valid expression must be checkable without evaluation, because untrusted code from federated nodes must be validated before execution")
|
||||
(li (strong "Token-efficient") " — minimal syntactic overhead, because the representation travels over the network and is processed by LLMs with finite context windows")
|
||||
(li (strong "Composable by nesting") " — no special composition mechanisms, because the same operation (putting a list inside a list) must work for markup, logic, and data")))
|
||||
|
||||
(~doc-section :title "The candidates" :id "candidates"
|
||||
|
||||
(~doc-subsection :title "XML / HTML"
|
||||
(p :class "text-stone-600"
|
||||
"The obvious first thought. XML is a tree. HTML is markup. Why not use angle brackets?")
|
||||
(p :class "text-stone-600"
|
||||
"XML fails on homoiconicity. The distinction between elements, attributes, text nodes, processing instructions, CDATA sections, entity references, and namespaces means the representation has multiple structural categories that cannot freely substitute for each other. An attribute is not an element. A text node is not a processing instruction. You cannot take an arbitrary XML fragment and use it as code, because XML has no concept of evaluation — it is a serialization format for trees, not a language.")
|
||||
(p :class "text-stone-600"
|
||||
"XML also fails on token efficiency. " (code "<div class=\"card\"><h2>Title</h2></div>") " versus " (code "(div :class \"card\" (h2 \"Title\"))") ". The closing tags carry zero information — they are pure redundancy. Over a full application, this redundancy compounds into significantly more bytes on the wire and significantly more tokens in an LLM context window.")
|
||||
(p :class "text-stone-600"
|
||||
"XSLT attempted to make XML a programming language. The result is universally regarded as a cautionary tale. Trying to express conditionals and iteration in a format designed for document markup produces something that is bad at both."))
|
||||
|
||||
(~doc-subsection :title "JSON"
|
||||
(p :class "text-stone-600"
|
||||
"JSON is data notation. It has objects, arrays, strings, numbers, booleans, and null. It parses in one pass. It validates structurally. It is ubiquitous.")
|
||||
(p :class "text-stone-600"
|
||||
"JSON is not homoiconic because it has no concept of evaluation. It is " (em "inert") " data. To make JSON a programming language, you must invent a convention for representing code — and every such convention reinvents s-expressions with worse ergonomics:")
|
||||
(~doc-code :code (highlight ";; JSON \"code\" (actual example from various JSON-based DSLs)\n{\"if\": [{\">\": [\"$.count\", 0]},\n {\"map\": [\"$.items\", {\"fn\": [\"item\", {\"get\": [\"item\", \"name\"]}]}]},\n {\"literal\": \"No items\"}]}\n\n;; The same thing in s-expressions\n(if (> count 0)\n (map (fn (item) (get item \"name\")) items)\n \"No items\")" "lisp"))
|
||||
(p :class "text-stone-600"
|
||||
"The JSON version is an s-expression encoded in JSON's syntax — lists-of-lists with a head element that determines semantics. It has strictly more punctuation (colons, commas, braces, brackets, quotes around keys) and strictly less readability. Every JSON-based DSL that reaches sufficient complexity converges on this pattern and then wishes it had just used s-expressions."))
|
||||
|
||||
(~doc-subsection :title "YAML"
|
||||
(p :class "text-stone-600"
|
||||
"YAML is the other common data notation. It adds indentation sensitivity, anchors, aliases, multi-line strings, type coercion, and a " (a :href "https://yaml.org/spec/1.2.2/" :class "text-violet-600 hover:underline" "specification") " that is 240 pages long. The spec for SX's parser is 200 lines.")
|
||||
(p :class "text-stone-600"
|
||||
"Indentation sensitivity is a direct disqualifier for wire formats. Whitespace must survive serialization, transmission, minification, and reconstruction exactly — a fragility that s-expressions do not have. YAML also fails on structural validation: the " (a :href "https://en.wikipedia.org/wiki/Norway_problem" :class "text-violet-600 hover:underline" "Norway problem") " (" (code "NO") " parsed as boolean " (code "false") ") demonstrates that YAML's type coercion makes structural validation impossible without semantic knowledge of the schema.")
|
||||
(p :class "text-stone-600"
|
||||
"YAML is not homoiconic. It has no evaluation model. Like JSON, any attempt to encode logic in YAML produces s-expressions with worse syntax."))
|
||||
|
||||
(~doc-subsection :title "JSX / Template literals"
|
||||
(p :class "text-stone-600"
|
||||
"JSX is the closest mainstream technology to what SX does — it embeds markup in a programming language. But JSX is not a representation; it is a compile target. " (code "<Card title=\"Hi\">content</Card>") " compiles to " (code "React.createElement(Card, {title: \"Hi\"}, \"content\")") ". The angle-bracket syntax is sugar that does not survive to runtime.")
|
||||
(p :class "text-stone-600"
|
||||
"This means JSX cannot be a wire format — the client must have the compiler. It cannot be a spec language — you cannot write a JSX spec in JSX without a build step. It cannot be a data notation — it requires JavaScript evaluation context. JSX handles exactly one of the six roles (markup) and delegates the others to JavaScript, CSS, JSON, and whatever build tool assembles them.")
|
||||
(p :class "text-stone-600"
|
||||
"Template literals (tagged templates in JavaScript, Jinja, ERB, etc.) are string interpolation. They embed code in strings or strings in code, depending on which layer you consider primary. Neither direction produces a homoiconic representation. You cannot write a macro that reads a template literal and transforms it as data, because the template literal is a string — opaque, uninspectable, and unstructured."))
|
||||
|
||||
(~doc-subsection :title "Tcl"
|
||||
(p :class "text-stone-600"
|
||||
"Tcl is the most interesting near-miss. \"Everything is a string\" is a radical simplification. The syntax is minimal: commands are words separated by spaces, braces group without substitution, brackets evaluate. Tcl is effectively homoiconic — code is strings, strings are code, and " (code "eval") " is the universal mechanism.")
|
||||
(p :class "text-stone-600"
|
||||
"Where Tcl falls short is structural validation. Because everything is a string, you cannot check that a Tcl program is well-formed without evaluating it. Unmatched braces inside string values are indistinguishable from syntax errors without context. S-expressions have a trivial structural check — balanced parentheses — that requires no evaluation and no context. For sandboxed evaluation of untrusted code (federated expressions from other nodes), this difference is decisive.")
|
||||
(p :class "text-stone-600"
|
||||
"Tcl also lacks native tree structure. Lists are flat strings that are parsed on demand. Nested structure exists by convention, not by grammar. This makes composition more fragile than s-expressions, where nesting is the fundamental structural primitive."))
|
||||
|
||||
(~doc-subsection :title "Rebol / Red"
|
||||
(p :class "text-stone-600"
|
||||
"Rebol is the strongest alternative. It is homoiconic — code is data. It has minimal syntax. It has dialecting — the ability to create domain-specific languages within the language. It is a single representation for code, data, and markup. " (a :href "https://en.wikipedia.org/wiki/Rebol" :class "text-violet-600 hover:underline" "Carl Sassenrath") " designed it explicitly to solve the problems that SX also targets.")
|
||||
(p :class "text-stone-600"
|
||||
"Rebol's limitation is practical, not theoretical. The language is obscure. Modern AI models have almost no training data for it — generating reliable Rebol would require extensive fine-tuning or few-shot prompting. There is no ecosystem of libraries, no community producing components, no federation protocol. And Rebol's type system (over 40 built-in datatypes including " (code "url!") ", " (code "email!") ", " (code "money!") ") makes the parser substantially more complex than s-expressions, which have essentially one composite type: the list.")
|
||||
(p :class "text-stone-600"
|
||||
"Rebol demonstrates that the design space around s-expressions has room for variation. But the variations add complexity without adding expressiveness — and in the current landscape, complexity kills AI compatibility and adoption equally."))
|
||||
|
||||
(~doc-subsection :title "Forth / stack-based"
|
||||
(p :class "text-stone-600"
|
||||
"Forth has the most minimal syntax imaginable: words separated by spaces. No parentheses, no brackets, no delimiters. The program is a flat sequence of tokens. This is simpler than s-expressions.")
|
||||
(p :class "text-stone-600"
|
||||
"But Forth's simplicity is deceptive. The flat token stream encodes tree structure " (em "implicitly") " via stack effects. Understanding what a Forth program does requires mentally simulating the stack — tracking what each word pushes and pops. This is precisely the kind of implicit state tracking that both humans and AI models struggle with. A nested s-expression makes structure " (em "visible") ". A Forth program hides it.")
|
||||
(p :class "text-stone-600"
|
||||
"For markup, this is fatal. " (code "3 1 + 4 * 2 /") " is arithmetic. Now imagine a page layout expressed as stack operations. The nesting that makes " (code "(div (h2 \"Title\") (p \"Body\"))") " self-evident becomes an exercise in mental bookkeeping. UI is trees. Stack languages are not.")))
|
||||
|
||||
(~doc-section :title "The convergence" :id "convergence"
|
||||
(p :class "text-stone-600"
|
||||
"Every alternative either fails to meet the requirements or reinvents s-expressions:")
|
||||
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Candidate")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Homoiconic")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Structural validation")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Token-efficient")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Tree-native")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "AI-trainable")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "XML/HTML")
|
||||
(td :class "px-3 py-2 text-red-600" "No")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-red-600" "No")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "JSON")
|
||||
(td :class "px-3 py-2 text-red-600" "No")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-red-600" "No")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "YAML")
|
||||
(td :class "px-3 py-2 text-red-600" "No")
|
||||
(td :class "px-3 py-2 text-red-600" "No")
|
||||
(td :class "px-3 py-2 text-amber-600" "Moderate")
|
||||
(td :class "px-3 py-2 text-red-600" "No")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "JSX")
|
||||
(td :class "px-3 py-2 text-red-600" "No")
|
||||
(td :class "px-3 py-2 text-red-600" "Needs compiler")
|
||||
(td :class "px-3 py-2 text-amber-600" "Moderate")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Tcl")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-red-600" "No")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-red-600" "No")
|
||||
(td :class "px-3 py-2 text-red-600" "No"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Rebol/Red")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-amber-600" "Complex")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-red-600" "No"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Forth")
|
||||
(td :class "px-3 py-2 text-amber-600" "Sort of")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-green-700" "Yes")
|
||||
(td :class "px-3 py-2 text-red-600" "No")
|
||||
(td :class "px-3 py-2 text-red-600" "No"))
|
||||
(tr :class "border-b border-stone-200 bg-violet-50"
|
||||
(td :class "px-3 py-2 font-semibold text-violet-800" "S-expressions")
|
||||
(td :class "px-3 py-2 text-green-700 font-semibold" "Yes")
|
||||
(td :class "px-3 py-2 text-green-700 font-semibold" "Yes")
|
||||
(td :class "px-3 py-2 text-green-700 font-semibold" "Yes")
|
||||
(td :class "px-3 py-2 text-green-700 font-semibold" "Yes")
|
||||
(td :class "px-3 py-2 text-green-700 font-semibold" "Yes")))))
|
||||
(p :class "text-stone-600"
|
||||
"No candidate achieves all five properties. The closest — Rebol — fails on AI trainability, which is not a theoretical concern but a practical one: a representation that AI cannot generate reliably is a representation that cannot participate in the coming decade of software development."))
|
||||
|
||||
(~doc-section :title "Why not invent something new?" :id "invent"
|
||||
(p :class "text-stone-600"
|
||||
"The objection might be: fine, existing alternatives fall short, but why not design a new representation that has all these properties without the parentheses?")
|
||||
(p :class "text-stone-600"
|
||||
"Try it. You need a tree structure (for markup and composition). You need a uniform representation (for homoiconicity). You need a delimiter that is unambiguous (for one-pass parsing and structural validation). You need minimum syntactic overhead (for token efficiency).")
|
||||
(p :class "text-stone-600"
|
||||
"A tree needs a way to mark where a node begins and ends. The minimal delimiter pair is two characters — one for open, one for close. S-expressions use " (code "(") " and " (code ")") ". You could use " (code "[") " and " (code "]") ", or " (code "{") " and " (code "}") ", or " (code "BEGIN") " and " (code "END") ", or indentation. But " (code "[list]") " and " (code "{list}") " are just s-expressions with different brackets. " (code "BEGIN/END") " adds token overhead. Indentation adds whitespace sensitivity, which breaks wire-format reliability.")
|
||||
(p :class "text-stone-600"
|
||||
"You could try eliminating delimiters entirely and using a binary format. But binary formats are not human-readable, not human-writable, and not inspectable in a terminal — which means they cannot serve as a programming language. The developer experience of reading and writing code requires a text-based representation, and the minimal text-based tree delimiter is a matched pair of single characters.")
|
||||
(p :class "text-stone-600"
|
||||
"You could try significant whitespace — indentation-based nesting like Python or Haskell. This works for programming languages where the code is stored in files and processed by a single toolchain. It does not work for wire formats, where the representation must survive HTTP transfer, server-side generation, client-side parsing, minification, storage in databases, embedding in script tags, and concatenation with other expressions. Whitespace-sensitive formats are fragile in exactly the contexts where SX operates.")
|
||||
(p :class "text-stone-600"
|
||||
"Every path through the design space either arrives at parenthesized prefix notation — s-expressions — or introduces complexity that violates one of the requirements. This is not a failure of imagination. It is a consequence of the requirements being simultaneously demanding and precise. The solution space has one optimum, and McCarthy found it in 1958."))
|
||||
|
||||
(~doc-section :title "The parentheses objection" :id "parentheses"
|
||||
(p :class "text-stone-600"
|
||||
"The real objection to s-expressions is not technical. It is aesthetic. People do not like parentheses. They look unfamiliar. They feel old. They trigger memories of computer science lectures about recursive descent parsers.")
|
||||
(p :class "text-stone-600"
|
||||
"This is a human problem, not a representation problem. And it is a human problem with a known trajectory: every programmer who has used a Lisp for more than a few weeks stops seeing the parentheses. They see the tree. The delimiters become invisible, like the spaces between words in English. You do not see spaces. You see words. Lisp programmers do not see parentheses. They see structure.")
|
||||
(p :class "text-stone-600"
|
||||
"More to the point: in the world we are entering, most code will be generated by AI and rendered by machines. The human reads the " (em "output") " — the rendered page, the test results, the behaviour. The s-expressions are an intermediate representation that the human steers but does not need to manually type or visually parse character-by-character. The aesthetic objection dissolves when the representation is a conversation between the human's intent and the machine's generation, not something the human stares at in a text editor.")
|
||||
(p :class "text-stone-600"
|
||||
"The author of SX has never opened the codebase in an editor. Every file was created through " (a :href "https://claude.ai/" :class "text-violet-600 hover:underline" "Claude") " in a terminal. The parentheses are between the human and the machine, and neither one minds them."))
|
||||
|
||||
(~doc-section :title "The conclusion" :id "conclusion"
|
||||
(p :class "text-stone-600"
|
||||
"S-expressions are the minimal tree representation. They are the only widely-known homoiconic notation. They have trivial structural validation, maximum token efficiency, and native composability. They are well-represented in AI training data. Every alternative either fails on one of these criteria or converges toward s-expressions under a different name.")
|
||||
(p :class "text-stone-600"
|
||||
"This is not a claim that s-expressions are the best syntax for every programming language. They are not. Python's indentation-based syntax is better for imperative scripting. Haskell's layout rules are better for type-heavy functional programming. SQL is better for relational queries.")
|
||||
(p :class "text-stone-600"
|
||||
"The claim is narrower and stronger: for a system that must simultaneously serve as markup, programming language, wire format, data notation, spec language, and metaprogramming substrate — with homoiconicity, one-pass parsing, structural validation, token efficiency, and composability — there is no alternative to s-expressions. Not because alternatives have not been tried. Not because the design space has not been explored. But because the requirements, when stated precisely, admit exactly one family of solutions, and that family is the one McCarthy discovered sixty-seven years ago.")
|
||||
(p :class "text-stone-600"
|
||||
"The name for this, borrowed from a " (a :href "https://en.wikipedia.org/wiki/There_is_no_alternative" :class "text-violet-600 hover:underline" "different context entirely") ", is " (em "TINA") " — there is no alternative. Not as a political slogan, but as a mathematical observation. When you need a minimal, homoiconic, structurally-validatable, token-efficient, tree-native, AI-compatible representation for the web, you need s-expressions. Everything else is either less capable or isomorphic."))))
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
(dict :label "Essays" :href "/essays/")
|
||||
(dict :label "Specs" :href "/specs/")
|
||||
(dict :label "Bootstrappers" :href "/bootstrappers/")
|
||||
(dict :label "Testing" :href "/testing/")
|
||||
(dict :label "Isomorphism" :href "/isomorphism/")
|
||||
(dict :label "Plans" :href "/plans/"))))
|
||||
(<> (map (lambda (item)
|
||||
|
||||
@@ -82,6 +82,8 @@
|
||||
:summary "The web's HTML/CSS/JS split separates the framework's concerns, not your application's. Real separation is domain-specific.")
|
||||
(dict :label "SX and AI" :href "/essays/sx-and-ai"
|
||||
:summary "Why s-expressions are the most AI-friendly representation for web interfaces.")
|
||||
(dict :label "There Is No Alternative" :href "/essays/no-alternative"
|
||||
:summary "Every attempt to escape s-expressions leads back to s-expressions. This is not an accident.")
|
||||
(dict :label "sx sucks" :href "/essays/sx-sucks"
|
||||
:summary "An honest accounting of everything wrong with SX and why you probably shouldn't use it.")))
|
||||
|
||||
@@ -105,8 +107,15 @@
|
||||
(dict :label "Continuations" :href "/specs/continuations")
|
||||
(dict :label "call/cc" :href "/specs/callcc")
|
||||
(dict :label "Deps" :href "/specs/deps")
|
||||
(dict :label "Router" :href "/specs/router")
|
||||
(dict :label "Testing" :href "/specs/testing")))
|
||||
(dict :label "Router" :href "/specs/router")))
|
||||
|
||||
(define testing-nav-items (list
|
||||
(dict :label "Overview" :href "/testing/")
|
||||
(dict :label "Evaluator" :href "/testing/eval")
|
||||
(dict :label "Parser" :href "/testing/parser")
|
||||
(dict :label "Router" :href "/testing/router")
|
||||
(dict :label "Renderer" :href "/testing/render")
|
||||
(dict :label "Runners" :href "/testing/runners")))
|
||||
|
||||
(define isomorphism-nav-items (list
|
||||
(dict :label "Roadmap" :href "/isomorphism/")
|
||||
@@ -199,10 +208,7 @@
|
||||
:prose "The deps module analyzes component dependency graphs and classifies components as pure or IO-dependent. Phase 1 (bundling): walks component AST bodies to find transitive ~component references, computes the minimal set needed per page, and collects per-page CSS classes from only the used components. Phase 2 (IO detection): scans component ASTs for references to IO primitive names (from boundary.sx declarations — frag, query, service, current-user, highlight, etc.), computes transitive IO refs through the component graph, and caches the result on each component. Components with no transitive IO refs are pure — they can render anywhere without server data. IO-dependent components must expand server-side. The spec provides the classification; each host's async partial evaluator acts on it (expand IO-dependent server-side, serialize pure for client). All functions are pure — each host bootstraps them to native code via --spec-modules deps. Platform functions (component-deps, component-set-deps!, component-css-classes, component-io-refs, component-set-io-refs!, env-components, regex-find-all, scan-css-classes) are implemented natively per target.")
|
||||
(dict :slug "router" :filename "router.sx" :title "Router"
|
||||
:desc "Client-side route matching — Flask-style pattern parsing, segment matching, route table search."
|
||||
:prose "The router module provides pure functions for matching URL paths against Flask-style route patterns (e.g. /docs/<slug>). Used by client-side routing (Phase 3) to determine if a page can be rendered locally without a server roundtrip. split-path-segments breaks a path into segments, parse-route-pattern converts patterns into typed segment descriptors, match-route-segments tests a path against a parsed pattern returning extracted params, and find-matching-route searches a route table for the first match. No platform interface needed — uses only pure string and list primitives. Bootstrapped via --spec-modules deps,router.")
|
||||
(dict :slug "testing" :filename "test.sx" :title "Testing"
|
||||
:desc "Self-hosting test framework — SX tests SX. Bootstraps to pytest and Node.js TAP."
|
||||
:prose "The test spec 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. The framework uses only primitives already in primitives.sx (assert, equal?, type-of, str, list, len) plus assertion helpers defined in SX (assert-equal, assert-true, assert-false, assert-nil, assert-type, assert-length, assert-contains). Two bootstrap compilers read test.sx and emit native test files: bootstrap_test.py produces a pytest module, bootstrap_test_js.py produces a Node.js TAP script. The same 81 tests run on both platforms, verifying cross-host parity.")))
|
||||
:prose "The router module provides pure functions for matching URL paths against Flask-style route patterns (e.g. /docs/<slug>). Used by client-side routing (Phase 3) to determine if a page can be rendered locally without a server roundtrip. split-path-segments breaks a path into segments, parse-route-pattern converts patterns into typed segment descriptors, match-route-segments tests a path against a parsed pattern returning extracted params, and find-matching-route searches a route table for the first match. No platform interface needed — uses only pure string and list primitives. Bootstrapped via --spec-modules deps,router.")))
|
||||
|
||||
(define all-spec-items (concat core-spec-items (concat adapter-spec-items (concat browser-spec-items (concat extension-spec-items module-spec-items)))))
|
||||
|
||||
|
||||
446
sx/sx/testing.sx
446
sx/sx/testing.sx
@@ -1,246 +1,288 @@
|
||||
;; Testing spec page — SX tests SX.
|
||||
;; Testing section — SX tests SX across every host.
|
||||
;; Modular test specs: eval, parser, router, render.
|
||||
;; Each page shows spec source, runs server-side, offers browser-side run button.
|
||||
|
||||
(defcomp ~spec-testing-content (&key spec-source server-results)
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Overview page
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~testing-overview-content (&key server-results framework-source eval-source parser-source router-source render-source)
|
||||
(~doc-page :title "Testing"
|
||||
(div :class "space-y-8"
|
||||
|
||||
;; Intro
|
||||
(div :class "space-y-4"
|
||||
(p :class "text-lg text-stone-600"
|
||||
"SX tests itself. "
|
||||
(code :class "text-violet-700 text-sm" "test.sx")
|
||||
" is a self-executing test spec — it defines "
|
||||
"SX tests itself. Test specs are written in SX and executed by SX evaluators on every host. "
|
||||
"The same assertions run in Python, Node.js, and in the browser — from the same source files.")
|
||||
(p :class "text-stone-600"
|
||||
"The framework defines two macros ("
|
||||
(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. No code generation, no intermediate files — the evaluator runs the spec."))
|
||||
") and nine assertion helpers, all in SX. Each host provides only "
|
||||
(strong "five platform functions")
|
||||
" — everything else is pure SX."))
|
||||
|
||||
;; Server-side results (ran when this page was rendered)
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Server: Python evaluator")
|
||||
(p :class "text-stone-600"
|
||||
"The Python evaluator ran "
|
||||
(code :class "text-violet-700 text-sm" "test.sx")
|
||||
" when this page loaded — "
|
||||
(strong (str (get server-results "passed") "/" (get server-results "total") " passed"))
|
||||
" in " (str (get server-results "elapsed-ms")) "ms.")
|
||||
(pre :class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
|
||||
(get server-results "output")))
|
||||
|
||||
;; Client-side test runner
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Browser: JavaScript evaluator")
|
||||
(p :class "text-stone-600"
|
||||
"This page loaded "
|
||||
(code :class "text-violet-700 text-sm" "sx-browser.js")
|
||||
" to render itself. The same evaluator can run "
|
||||
(code :class "text-violet-700 text-sm" "test.sx")
|
||||
" right here:")
|
||||
(div :class "flex items-center gap-4"
|
||||
(button :id "test-btn"
|
||||
:class "px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 cursor-pointer"
|
||||
:onclick "sxRunTests('test-sx-source','test-output','test-btn')"
|
||||
"Run 81 tests"))
|
||||
(pre :id "test-output"
|
||||
:class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
|
||||
:style "display:none"
|
||||
"")
|
||||
;; Hidden: raw test.sx source for the browser runner
|
||||
(textarea :id "test-sx-source" :style "display:none" spec-source)
|
||||
;; Load the test runner script
|
||||
(script :src (asset-url "/scripts/sx-test-runner.js")))
|
||||
|
||||
;; How it works
|
||||
;; Architecture
|
||||
(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 Self-executing: macros + helpers + 81 tests
|
||||
"test-framework.sx Macros + assertion helpers (loaded first)
|
||||
|
|
||||
|--- browser sx-browser.js evaluates test.sx in this page
|
||||
|
|
||||
|--- run.js Injects 5 platform fns, evaluates test.sx
|
||||
| |
|
||||
| +-> sx-browser.js JS evaluator (bootstrapped from spec)
|
||||
|
|
||||
|--- run.py Injects 5 platform fns, evaluates test.sx
|
||||
|
|
||||
+-> evaluator.py Python evaluator
|
||||
+-- test-eval.sx 81 tests: evaluator + primitives
|
||||
+-- test-parser.sx 39 tests: tokenizer, parser, serializer
|
||||
+-- test-router.sx 18 tests: route matching + param extraction
|
||||
+-- test-render.sx 23 tests: HTML rendering + components
|
||||
|
||||
Platform functions (5 total — everything else is pure SX):
|
||||
Runners:
|
||||
run.js Node.js — injects platform fns, runs specs
|
||||
run.py Python — injects platform fns, runs specs
|
||||
sx-test-runner.js Browser — runs specs in this page
|
||||
|
||||
Platform functions (5 total):
|
||||
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")))
|
||||
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 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" "Macros")
|
||||
(~doc-code :code
|
||||
(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) ...))\n(define assert-throws (fn (thunk) ...))" "lisp"))))
|
||||
Per-spec platform functions:
|
||||
parser: sx-parse, sx-serialize, make-symbol, make-keyword, ...
|
||||
router: (none — pure spec, uses bootstrapped functions)
|
||||
render: render-html (wraps parse + render-to-html)")))
|
||||
|
||||
;; Example tests
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Example: SX testing SX")
|
||||
(p :class "text-stone-600"
|
||||
"The test suites cover every language feature. Here is the arithmetic suite testing the evaluator's arithmetic primitives:")
|
||||
(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" "From test.sx")
|
||||
(~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"))))
|
||||
;; Server results
|
||||
(when server-results
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Server: Python evaluator")
|
||||
(p :class "text-stone-600"
|
||||
"Ran all test specs when this page loaded — "
|
||||
(strong (str (get server-results "passed") "/" (get server-results "total") " passed"))
|
||||
" in " (str (get server-results "elapsed-ms")) "ms.")
|
||||
(pre :class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
|
||||
(get server-results "output"))))
|
||||
|
||||
;; Running tests — JS
|
||||
;; Browser test runner
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "JavaScript: direct evaluation")
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Browser: JavaScript evaluator")
|
||||
(p :class "text-stone-600"
|
||||
"Run all test specs in the browser using "
|
||||
(code :class "text-violet-700 text-sm" "sx-browser.js")
|
||||
" 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" "run.js")
|
||||
(~doc-code :code
|
||||
(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" "Output")
|
||||
(~doc-code :code
|
||||
(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"))))
|
||||
":")
|
||||
(div :class "flex items-center gap-4"
|
||||
(button :id "test-btn-all"
|
||||
:class "px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 cursor-pointer"
|
||||
:onclick "sxRunModularTests('all','test-output-all','test-btn-all')"
|
||||
"Run all tests"))
|
||||
(pre :id "test-output-all"
|
||||
:class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
|
||||
:style "display:none"
|
||||
"")
|
||||
;; Hidden: all spec sources for the browser runner
|
||||
(textarea :id "test-framework-source" :style "display:none" framework-source)
|
||||
(textarea :id "test-spec-eval" :style "display:none" eval-source)
|
||||
(textarea :id "test-spec-parser" :style "display:none" parser-source)
|
||||
(textarea :id "test-spec-router" :style "display:none" router-source)
|
||||
(textarea :id "test-spec-render" :style "display:none" render-source)
|
||||
(script :src (asset-url "/scripts/sx-test-runner.js")))
|
||||
|
||||
;; Running tests — Python
|
||||
;; Test spec index
|
||||
(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"))))
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Test specs")
|
||||
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
(a :href "/testing/eval" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
|
||||
(h3 :class "font-semibold text-stone-800" "Evaluator")
|
||||
(p :class "text-sm text-stone-500" "81 tests — literals, arithmetic, strings, lists, dicts, special forms, lambdas, components, macros")
|
||||
(p :class "text-xs text-violet-600 mt-1" "test-eval.sx"))
|
||||
(a :href "/testing/parser" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
|
||||
(h3 :class "font-semibold text-stone-800" "Parser")
|
||||
(p :class "text-sm text-stone-500" "39 tests — tokenization, parsing, escape sequences, quote sugar, serialization, round-trips")
|
||||
(p :class "text-xs text-violet-600 mt-1" "test-parser.sx"))
|
||||
(a :href "/testing/router" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
|
||||
(h3 :class "font-semibold text-stone-800" "Router")
|
||||
(p :class "text-sm text-stone-500" "18 tests — path splitting, pattern parsing, segment matching, parameter extraction")
|
||||
(p :class "text-xs text-violet-600 mt-1" "test-router.sx"))
|
||||
(a :href "/testing/render" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
|
||||
(h3 :class "font-semibold text-stone-800" "Renderer")
|
||||
(p :class "text-sm text-stone-500" "23 tests — elements, attributes, void elements, fragments, escaping, control flow, components")
|
||||
(p :class "text-xs text-violet-600 mt-1" "test-render.sx"))))
|
||||
|
||||
;; 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") " and " (strong "executed by SX") " — no code generation")
|
||||
(li "The same 81 tests run on " (strong "Python, Node.js, and in the browser") " from the same file")
|
||||
(li "Test specs are " (strong "written in SX") " and " (strong "executed by SX") " — no code generation")
|
||||
(li "The same tests run on " (strong "Python, Node.js, and in the browser") " from the same files")
|
||||
(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") " — click the button above to prove it")))
|
||||
(li "Modular specs test each part independently — evaluator, parser, router, renderer")
|
||||
(li "Per-spec platform functions extend the 5-function contract for module-specific capabilities")
|
||||
(li "Platform divergences are " (strong "exposed by the tests") ", not hidden"))))))
|
||||
|
||||
;; Test suites
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Per-spec test page (reusable for eval, parser, router, render)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~testing-spec-content (&key spec-name spec-title spec-desc spec-source framework-source server-results)
|
||||
(~doc-page :title spec-title
|
||||
(div :class "space-y-8"
|
||||
|
||||
;; Description
|
||||
(div :class "space-y-4"
|
||||
(p :class "text-lg text-stone-600" spec-desc))
|
||||
|
||||
;; Server-side results
|
||||
(when server-results
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Server: Python evaluator")
|
||||
(p :class "text-stone-600"
|
||||
"Ran "
|
||||
(code :class "text-violet-700 text-sm" (str "test-" spec-name ".sx"))
|
||||
" when this page loaded — "
|
||||
(strong (str (get server-results "passed") "/" (get server-results "total") " passed"))
|
||||
" in " (str (get server-results "elapsed-ms")) "ms.")
|
||||
(pre :class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
|
||||
(get server-results "output"))))
|
||||
|
||||
;; Browser test runner
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "All 15 test suites")
|
||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Suite")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Tests")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Covers")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "literals")
|
||||
(td :class "px-3 py-2" "6")
|
||||
(td :class "px-3 py-2 text-stone-700" "number, string, boolean, nil, list, dict type checking"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "arithmetic")
|
||||
(td :class "px-3 py-2" "5")
|
||||
(td :class "px-3 py-2 text-stone-700" "+, -, *, /, mod with edge cases"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "comparison")
|
||||
(td :class "px-3 py-2" "3")
|
||||
(td :class "px-3 py-2 text-stone-700" "=, equal?, <, >, <=, >="))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "strings")
|
||||
(td :class "px-3 py-2" "7")
|
||||
(td :class "px-3 py-2 text-stone-700" "str, string-length, substring, contains?, upcase, trim, split/join"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "lists")
|
||||
(td :class "px-3 py-2" "10")
|
||||
(td :class "px-3 py-2 text-stone-700" "first, rest, nth, last, cons, append, reverse, empty?, contains?, flatten"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "dicts")
|
||||
(td :class "px-3 py-2" "6")
|
||||
(td :class "px-3 py-2 text-stone-700" "literals, get, assoc, dissoc, keys/vals, has-key?, merge"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "predicates")
|
||||
(td :class "px-3 py-2" "7")
|
||||
(td :class "px-3 py-2 text-stone-700" "nil?, number?, string?, list?, dict?, boolean?, not"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "special-forms")
|
||||
(td :class "px-3 py-2" "10")
|
||||
(td :class "px-3 py-2 text-stone-700" "if, when, cond, and, or, let, let (Clojure), do/begin, define, set!"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "lambdas")
|
||||
(td :class "px-3 py-2" "5")
|
||||
(td :class "px-3 py-2 text-stone-700" "basic, closures, as argument, recursion, higher-order returns"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "higher-order")
|
||||
(td :class "px-3 py-2" "6")
|
||||
(td :class "px-3 py-2 text-stone-700" "map, filter, reduce, some, every?, map-indexed"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "components")
|
||||
(td :class "px-3 py-2" "4")
|
||||
(td :class "px-3 py-2 text-stone-700" "defcomp, &key params, &rest children, defaults"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "macros")
|
||||
(td :class "px-3 py-2" "3")
|
||||
(td :class "px-3 py-2 text-stone-700" "defmacro, quasiquote/unquote, splice-unquote"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "threading")
|
||||
(td :class "px-3 py-2" "1")
|
||||
(td :class "px-3 py-2 text-stone-700" "-> thread-first macro"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "truthiness")
|
||||
(td :class "px-3 py-2" "2")
|
||||
(td :class "px-3 py-2 text-stone-700" "truthy/falsy values (platform-universal subset)"))
|
||||
(tr
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "edge-cases")
|
||||
(td :class "px-3 py-2" "6")
|
||||
(td :class "px-3 py-2 text-stone-700" "nested scoping, recursive map, keywords, dict eval, nil propagation, empty ops"))))))
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Browser: JavaScript evaluator")
|
||||
(p :class "text-stone-600"
|
||||
"Run this spec in the browser:")
|
||||
(div :class "flex items-center gap-4"
|
||||
(button :id (str "test-btn-" spec-name)
|
||||
:class "px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 cursor-pointer"
|
||||
:onclick (str "sxRunModularTests('" spec-name "','test-output-" spec-name "','test-btn-" spec-name "')")
|
||||
(str "Run " spec-title)))
|
||||
(pre :id (str "test-output-" spec-name)
|
||||
:class "text-sm font-mono bg-stone-900 text-green-400 rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto"
|
||||
:style "display:none"
|
||||
"")
|
||||
;; Hidden: spec source + framework source for the browser runner
|
||||
(textarea :id (str "test-spec-" spec-name) :style "display:none" spec-source)
|
||||
(textarea :id "test-framework-source" :style "display:none" framework-source)
|
||||
(script :src (asset-url "/scripts/sx-test-runner.js")))
|
||||
|
||||
;; Full source
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Full specification source")
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Specification source")
|
||||
(p :class "text-xs text-stone-400 italic"
|
||||
"The s-expression source below is the canonical test specification. "
|
||||
"Any host that implements the five platform functions can evaluate it directly.")
|
||||
(str "test-" spec-name ".sx")
|
||||
" — the canonical test specification for this module.")
|
||||
(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"))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Runners page
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~testing-runners-content ()
|
||||
(~doc-page :title "Test Runners"
|
||||
(div :class "space-y-8"
|
||||
|
||||
(div :class "space-y-4"
|
||||
(p :class "text-lg text-stone-600"
|
||||
"Three runners execute the same test specs on different hosts. Each injects the five platform functions and any per-spec extensions, then evaluates the SX source directly.")
|
||||
(p :class "text-stone-600"
|
||||
"All runners produce "
|
||||
(a :href "https://testanything.org/" :class "text-violet-700 underline" "TAP")
|
||||
" output and accept module names as arguments."))
|
||||
|
||||
;; Node.js runner
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Node.js: run.js")
|
||||
(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" "Usage")
|
||||
(~doc-code :code
|
||||
(highlight "# Run all specs\nnode shared/sx/tests/run.js\n\n# Run specific specs\nnode shared/sx/tests/run.js eval parser\n\n# Legacy mode (monolithic test.sx)\nnode shared/sx/tests/run.js --legacy" "bash")))
|
||||
(p :class "text-stone-600 text-sm"
|
||||
"Uses "
|
||||
(code :class "text-violet-700 text-sm" "sx-browser.js")
|
||||
" as the evaluator. Router tests use bootstrapped functions from "
|
||||
(code :class "text-violet-700 text-sm" "Sx.splitPathSegments")
|
||||
" etc. Render tests use "
|
||||
(code :class "text-violet-700 text-sm" "Sx.renderToHtml")
|
||||
"."))
|
||||
|
||||
;; Python runner
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Python: run.py")
|
||||
(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" "Usage")
|
||||
(~doc-code :code
|
||||
(highlight "# Run all specs\npython shared/sx/tests/run.py\n\n# Run specific specs\npython shared/sx/tests/run.py eval parser\n\n# Legacy mode (monolithic test.sx)\npython shared/sx/tests/run.py --legacy" "bash")))
|
||||
(p :class "text-stone-600 text-sm"
|
||||
"Uses the hand-written Python evaluator ("
|
||||
(code :class "text-violet-700 text-sm" "evaluator.py")
|
||||
"). Router tests import bootstrapped functions from "
|
||||
(code :class "text-violet-700 text-sm" "sx_ref.py")
|
||||
" because the hand-written evaluator's "
|
||||
(code :class "text-violet-700 text-sm" "set!")
|
||||
" doesn't propagate across lambda closure copies."))
|
||||
|
||||
;; Browser runner
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Browser: sx-test-runner.js")
|
||||
(p :class "text-stone-600"
|
||||
"Runs in the browser on each test page. The spec source is embedded in a hidden textarea; the runner parses and evaluates it using the same "
|
||||
(code :class "text-violet-700 text-sm" "sx-browser.js")
|
||||
" that renders the page itself.")
|
||||
(p :class "text-stone-600"
|
||||
"This is the strongest proof of cross-host parity — the browser evaluator that users depend on is the same one running the tests."))
|
||||
|
||||
;; Platform functions table
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Platform functions")
|
||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Function")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Scope")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")))
|
||||
(tbody
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "try-call")
|
||||
(td :class "px-3 py-2" "All specs")
|
||||
(td :class "px-3 py-2 text-stone-700" "Call a thunk, catch errors, return result dict"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "report-pass")
|
||||
(td :class "px-3 py-2" "All specs")
|
||||
(td :class "px-3 py-2 text-stone-700" "Report a passing test"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "report-fail")
|
||||
(td :class "px-3 py-2" "All specs")
|
||||
(td :class "px-3 py-2 text-stone-700" "Report a failing test with error message"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "push-suite")
|
||||
(td :class "px-3 py-2" "All specs")
|
||||
(td :class "px-3 py-2 text-stone-700" "Push suite name onto context stack"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "pop-suite")
|
||||
(td :class "px-3 py-2" "All specs")
|
||||
(td :class "px-3 py-2 text-stone-700" "Pop suite name from context stack"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx-parse")
|
||||
(td :class "px-3 py-2" "Parser")
|
||||
(td :class "px-3 py-2 text-stone-700" "Parse SX source to AST"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx-serialize")
|
||||
(td :class "px-3 py-2" "Parser")
|
||||
(td :class "px-3 py-2 text-stone-700" "Serialize AST back to SX source text"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "render-html")
|
||||
(td :class "px-3 py-2" "Renderer")
|
||||
(td :class "px-3 py-2 text-stone-700" "Parse SX source and render to HTML string"))))))
|
||||
|
||||
;; Adding a new host
|
||||
(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" "Adding a new host")
|
||||
(ol :class "list-decimal list-inside text-blue-800 space-y-2 text-sm"
|
||||
(li "Implement the 5 platform functions in your target language")
|
||||
(li "Load " (code :class "text-sm" "test-framework.sx") " — it defines deftest/defsuite macros")
|
||||
(li "Load the spec file(s) — the evaluator runs the tests automatically")
|
||||
(li "Add per-spec platform functions if testing parser or renderer")
|
||||
(li "Compare TAP output across hosts — divergences reveal platform-specific semantics"))))))
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"server-architecture" (~essay-server-architecture)
|
||||
"separation-of-concerns" (~essay-separation-of-concerns)
|
||||
"sx-and-ai" (~essay-sx-and-ai)
|
||||
"no-alternative" (~essay-no-alternative)
|
||||
:else (~essays-index-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -343,9 +344,6 @@
|
||||
:filename (get item "filename") :href (str "/specs/" (get item "slug"))
|
||||
:source (read-spec-file (get item "filename"))))
|
||||
extension-spec-items))
|
||||
"testing" (~spec-testing-content
|
||||
:spec-source (read-spec-file "test.sx")
|
||||
:server-results (run-spec-tests))
|
||||
:else (let ((spec (find-spec slug)))
|
||||
(if spec
|
||||
(~spec-detail-content
|
||||
@@ -515,3 +513,74 @@
|
||||
"glue-decoupling" (~plan-glue-decoupling-content)
|
||||
"social-sharing" (~plan-social-sharing-content)
|
||||
:else (~plans-index-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Testing section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage testing-index
|
||||
:path "/testing/"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Testing"
|
||||
:sub-label "Testing"
|
||||
:sub-href "/testing/"
|
||||
:sub-nav (~section-nav :items testing-nav-items :current "Overview")
|
||||
:selected "Overview")
|
||||
:data (run-modular-tests "all")
|
||||
:content (~testing-overview-content
|
||||
:server-results server-results
|
||||
:framework-source (read-spec-file "test-framework.sx")
|
||||
:eval-source (read-spec-file "test-eval.sx")
|
||||
:parser-source (read-spec-file "test-parser.sx")
|
||||
:router-source (read-spec-file "test-router.sx")
|
||||
:render-source (read-spec-file "test-render.sx")))
|
||||
|
||||
(defpage testing-page
|
||||
:path "/testing/<slug>"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Testing"
|
||||
:sub-label "Testing"
|
||||
:sub-href "/testing/"
|
||||
:sub-nav (~section-nav :items testing-nav-items
|
||||
:current (find-current testing-nav-items slug))
|
||||
:selected (or (find-current testing-nav-items slug) ""))
|
||||
:data (case slug
|
||||
"eval" (run-modular-tests "eval")
|
||||
"parser" (run-modular-tests "parser")
|
||||
"router" (run-modular-tests "router")
|
||||
"render" (run-modular-tests "render")
|
||||
:else (dict))
|
||||
:content (case slug
|
||||
"eval" (~testing-spec-content
|
||||
:spec-name "eval"
|
||||
:spec-title "Evaluator Tests"
|
||||
:spec-desc "81 tests covering the core evaluator and all primitives — literals, arithmetic, comparison, strings, lists, dicts, predicates, special forms, lambdas, higher-order functions, components, macros, threading, and edge cases."
|
||||
:spec-source (read-spec-file "test-eval.sx")
|
||||
:framework-source (read-spec-file "test-framework.sx")
|
||||
:server-results server-results)
|
||||
"parser" (~testing-spec-content
|
||||
:spec-name "parser"
|
||||
:spec-title "Parser Tests"
|
||||
:spec-desc "39 tests covering tokenization and parsing — integers, floats, strings, escape sequences, booleans, nil, keywords, symbols, lists, dicts, whitespace, comments, quote sugar, serialization, and round-trips."
|
||||
:spec-source (read-spec-file "test-parser.sx")
|
||||
:framework-source (read-spec-file "test-framework.sx")
|
||||
:server-results server-results)
|
||||
"router" (~testing-spec-content
|
||||
:spec-name "router"
|
||||
:spec-title "Router Tests"
|
||||
:spec-desc "18 tests covering client-side route matching — path splitting, pattern parsing, segment matching, parameter extraction, and route table search."
|
||||
:spec-source (read-spec-file "test-router.sx")
|
||||
:framework-source (read-spec-file "test-framework.sx")
|
||||
:server-results server-results)
|
||||
"render" (~testing-spec-content
|
||||
:spec-name "render"
|
||||
:spec-title "Renderer Tests"
|
||||
:spec-desc "23 tests covering HTML rendering — elements, attributes, void elements, boolean attributes, fragments, escaping, control flow, and component rendering."
|
||||
:spec-source (read-spec-file "test-render.sx")
|
||||
:framework-source (read-spec-file "test-framework.sx")
|
||||
:server-results server-results)
|
||||
"runners" (~testing-runners-content)
|
||||
:else (~testing-overview-content
|
||||
:server-results server-results)))
|
||||
|
||||
@@ -25,6 +25,7 @@ def _register_sx_helpers() -> None:
|
||||
"routing-analyzer-data": _routing_analyzer_data,
|
||||
"data-test-data": _data_test_data,
|
||||
"run-spec-tests": _run_spec_tests,
|
||||
"run-modular-tests": _run_modular_tests,
|
||||
})
|
||||
|
||||
|
||||
@@ -562,6 +563,199 @@ def _run_spec_tests() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _run_modular_tests(spec_name: str) -> dict:
|
||||
"""Run modular test specs against the Python SX evaluator.
|
||||
|
||||
spec_name: "eval", "parser", "router", "render", or "all".
|
||||
Returns dict with server-results key containing results per spec.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
from shared.sx.parser import parse_all
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.types import Symbol, Keyword, Lambda, NIL
|
||||
|
||||
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
|
||||
if not os.path.isdir(ref_dir):
|
||||
ref_dir = "/app/shared/sx/ref"
|
||||
|
||||
suite_stack: list[str] = []
|
||||
passed = 0
|
||||
failed = 0
|
||||
test_num = 0
|
||||
lines: list[str] = []
|
||||
|
||||
def try_call(thunk):
|
||||
try:
|
||||
_trampoline(_eval([thunk], env))
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
def report_pass(name):
|
||||
nonlocal passed, test_num
|
||||
test_num += 1
|
||||
passed += 1
|
||||
lines.append("ok " + str(test_num) + " - " + " > ".join(suite_stack + [name]))
|
||||
|
||||
def report_fail(name, error):
|
||||
nonlocal failed, test_num
|
||||
test_num += 1
|
||||
failed += 1
|
||||
full = " > ".join(suite_stack + [name])
|
||||
lines.append("not ok " + str(test_num) + " - " + full)
|
||||
lines.append(" # " + str(error))
|
||||
|
||||
def push_suite(name):
|
||||
suite_stack.append(name)
|
||||
|
||||
def pop_suite():
|
||||
suite_stack.pop()
|
||||
|
||||
def sx_parse(source):
|
||||
return parse_all(source)
|
||||
|
||||
def sx_serialize(val):
|
||||
if val is None or val is NIL:
|
||||
return "nil"
|
||||
if isinstance(val, bool):
|
||||
return "true" if val else "false"
|
||||
if isinstance(val, (int, float)):
|
||||
return str(val)
|
||||
if isinstance(val, str):
|
||||
escaped = val.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
if isinstance(val, Symbol):
|
||||
return val.name
|
||||
if isinstance(val, Keyword):
|
||||
return f":{val.name}"
|
||||
if isinstance(val, list):
|
||||
inner = " ".join(sx_serialize(x) for x in val)
|
||||
return f"({inner})"
|
||||
if isinstance(val, dict):
|
||||
parts = []
|
||||
for k, v in val.items():
|
||||
parts.append(f":{k}")
|
||||
parts.append(sx_serialize(v))
|
||||
return "{" + " ".join(parts) + "}"
|
||||
return str(val)
|
||||
|
||||
def render_html(sx_source):
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import render_to_html as _render_to_html
|
||||
except ImportError:
|
||||
return "<!-- render-to-html not available -->"
|
||||
exprs = parse_all(sx_source)
|
||||
render_env = dict(env)
|
||||
result = ""
|
||||
for expr in exprs:
|
||||
result += _render_to_html(expr, render_env)
|
||||
return result
|
||||
|
||||
def _call_sx(fn, args, caller_env):
|
||||
if isinstance(fn, Lambda):
|
||||
from shared.sx.evaluator import _call_lambda
|
||||
return _trampoline(_call_lambda(fn, list(args), caller_env))
|
||||
return fn(*args)
|
||||
|
||||
def _for_each_indexed(fn, coll):
|
||||
if isinstance(fn, Lambda):
|
||||
closure = fn.closure
|
||||
for i, item in enumerate(coll or []):
|
||||
for p, v in zip(fn.params, [i, item]):
|
||||
closure[p] = v
|
||||
_trampoline(_eval(fn.body, closure))
|
||||
else:
|
||||
for i, item in enumerate(coll or []):
|
||||
fn(i, item)
|
||||
return NIL
|
||||
|
||||
env = {
|
||||
"try-call": try_call,
|
||||
"report-pass": report_pass,
|
||||
"report-fail": report_fail,
|
||||
"push-suite": push_suite,
|
||||
"pop-suite": pop_suite,
|
||||
"sx-parse": sx_parse,
|
||||
"sx-serialize": sx_serialize,
|
||||
"make-symbol": lambda name: Symbol(name),
|
||||
"make-keyword": lambda name: Keyword(name),
|
||||
"symbol-name": lambda sym: sym.name if isinstance(sym, Symbol) else str(sym),
|
||||
"keyword-name": lambda kw: kw.name if isinstance(kw, Keyword) else str(kw),
|
||||
"render-html": render_html,
|
||||
"for-each-indexed": _for_each_indexed,
|
||||
"dict-set!": lambda d, k, v: (d.__setitem__(k, v), NIL)[-1] if isinstance(d, dict) else NIL,
|
||||
"dict-has?": lambda d, k: isinstance(d, dict) and k in d,
|
||||
"dict-get": lambda d, k: d.get(k, NIL) if isinstance(d, dict) else NIL,
|
||||
"append!": lambda lst, item: (lst.append(item), NIL)[-1] if isinstance(lst, list) else NIL,
|
||||
"inc": lambda n: n + 1,
|
||||
}
|
||||
|
||||
def eval_file(filename):
|
||||
filepath = os.path.join(ref_dir, filename)
|
||||
if not os.path.exists(filepath):
|
||||
return
|
||||
with open(filepath) as f:
|
||||
src = f.read()
|
||||
exprs = parse_all(src)
|
||||
for expr in exprs:
|
||||
_trampoline(_eval(expr, env))
|
||||
|
||||
SPECS = {
|
||||
"eval": {"file": "test-eval.sx", "needs": []},
|
||||
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
|
||||
"router": {"file": "test-router.sx", "needs": []},
|
||||
"render": {"file": "test-render.sx", "needs": ["render-html"]},
|
||||
}
|
||||
|
||||
specs_to_run = list(SPECS.keys()) if spec_name == "all" else [spec_name]
|
||||
|
||||
t0 = time.monotonic()
|
||||
|
||||
# Load framework
|
||||
eval_file("test-framework.sx")
|
||||
|
||||
for sn in specs_to_run:
|
||||
spec = SPECS.get(sn)
|
||||
if not spec:
|
||||
continue
|
||||
|
||||
# Load router from bootstrap if needed
|
||||
if sn == "router":
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import (
|
||||
split_path_segments,
|
||||
parse_route_pattern,
|
||||
match_route_segments,
|
||||
match_route,
|
||||
find_matching_route,
|
||||
make_route_segment,
|
||||
)
|
||||
env["split-path-segments"] = split_path_segments
|
||||
env["parse-route-pattern"] = parse_route_pattern
|
||||
env["match-route-segments"] = match_route_segments
|
||||
env["match-route"] = match_route
|
||||
env["find-matching-route"] = find_matching_route
|
||||
env["make-route-segment"] = make_route_segment
|
||||
except ImportError:
|
||||
eval_file("router.sx")
|
||||
|
||||
eval_file(spec["file"])
|
||||
|
||||
elapsed = round((time.monotonic() - t0) * 1000)
|
||||
|
||||
return {
|
||||
"server-results": {
|
||||
"passed": passed,
|
||||
"failed": failed,
|
||||
"total": passed + failed,
|
||||
"elapsed-ms": elapsed,
|
||||
"output": "\n".join(lines),
|
||||
"spec": spec_name,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _data_test_data() -> dict:
|
||||
"""Return test data for the client-side data rendering test page.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user