Modular test architecture: per-module test specs for SX

Split monolithic test.sx into composable test specs:
- test-framework.sx: deftest/defsuite macros + assertion helpers
- test-eval.sx: core evaluator + primitives (81 tests)
- test-parser.sx: parser + serializer + round-trips (39 tests)
- test-router.sx: route matching from router.sx (18 tests)
- test-render.sx: HTML adapter rendering (23 tests)

Runners auto-discover specs and test whatever bootstrapped code
is available. Usage: `run.js eval parser router` or just `run.js`.
Legacy mode (`--legacy`) still runs monolithic test.sx.

Router tests use bootstrapped functions (sx_ref.py / sx-browser.js)
because the hand-written evaluator's flat-dict env model doesn't
support set! mutation across lambda closure boundaries.

JS: 161/161. Python: 159/161 (2 parser escape bugs found).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 12:17:13 +00:00
parent 99a78a70b3
commit aab1f3e966
8 changed files with 1506 additions and 37 deletions

494
shared/sx/ref/test-eval.sx Normal file
View 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))))

View 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"))))

View 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))"))))))

View 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 "&lt;script&gt;"))))
(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>")))))

View 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"))))))

View File

@@ -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:

View File

@@ -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 ---

View File

@@ -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}")