Merge branch 'worktree-iso-phase-4' into macros
This commit is contained in:
@@ -103,7 +103,8 @@
|
||||
(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 "Router" :href "/specs/router")
|
||||
(dict :label "Testing" :href "/specs/testing")))
|
||||
|
||||
(define isomorphism-nav-items (list
|
||||
(dict :label "Roadmap" :href "/isomorphism/")
|
||||
@@ -196,7 +197,10 @@
|
||||
: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.")))
|
||||
: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.")))
|
||||
|
||||
(define all-spec-items (concat core-spec-items (concat adapter-spec-items (concat browser-spec-items (concat extension-spec-items module-spec-items)))))
|
||||
|
||||
|
||||
234
sx/sx/testing.sx
Normal file
234
sx/sx/testing.sx
Normal file
@@ -0,0 +1,234 @@
|
||||
;; Testing spec page — SX tests SX.
|
||||
|
||||
(defcomp ~spec-testing-content (&key spec-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 "
|
||||
(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."))
|
||||
|
||||
;; Live test runner
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Run in browser")
|
||||
(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 — SX testing SX, in your browser:")
|
||||
(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
|
||||
(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
|
||||
|
|
||||
|--- 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
|
||||
|
||||
Platform functions (5 total — everything else is pure SX):
|
||||
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")))
|
||||
|
||||
;; 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"))))
|
||||
|
||||
;; 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"))))
|
||||
|
||||
;; Running tests — JS
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "JavaScript: direct evaluation")
|
||||
(p :class "text-stone-600"
|
||||
(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"))))
|
||||
|
||||
;; Running tests — Python
|
||||
(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"))))
|
||||
|
||||
;; 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 "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")))
|
||||
|
||||
;; Test suites
|
||||
(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"))))))
|
||||
|
||||
;; Full source
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Full 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.")
|
||||
(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"))))))))
|
||||
@@ -342,6 +342,8 @@
|
||||
: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"))
|
||||
:else (let ((spec (find-spec slug)))
|
||||
(if spec
|
||||
(~spec-detail-content
|
||||
|
||||
Reference in New Issue
Block a user