From 5fff83ae7970bb5deee1f25ff4441bac6ff75932 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 5 Mar 2026 16:25:15 +0000 Subject: [PATCH] Add header and event detail pages, fix copyright, rename essay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Detail pages for all 18 HTTP headers with descriptions, example usage, direction badges (request/response/both), and live demos for SX-Prompt, SX-Trigger, SX-Retarget - Detail pages for all 10 DOM events with descriptions, example usage, and live demos for beforeRequest, afterSettle, responseError, validationFailed - Header and event table rows now link to their detail pages - Fix copyright symbol on home page (was literal \u00a9, now actual ©) - Rename "Godel, Escher, Bach" essay to "Strange Loops" with updated summary - Remove duplicate script injection from bootstrapper page Co-Authored-By: Claude Opus 4.6 --- sx/bp/pages/routes.py | 26 +++ sx/content/pages.py | 457 ++++++++++++++++++++++++++++++++++++++++ sx/sx/docs.sx | 26 ++- sx/sx/essays.sx | 2 +- sx/sx/nav-data.sx | 4 +- sx/sx/reference.sx | 35 +++ sx/sx/specs.sx | 7 +- sx/sxc/home.sx | 2 +- sx/sxc/pages/docs.sx | 37 ++++ sx/sxc/pages/helpers.py | 53 ++++- sx/sxc/reference.sx | 148 +++++++++++++ 11 files changed, 779 insertions(+), 18 deletions(-) diff --git a/sx/bp/pages/routes.py b/sx/bp/pages/routes.py index 5091906..61ca0f9 100644 --- a/sx/bp/pages/routes.py +++ b/sx/bp/pages/routes.py @@ -901,4 +901,30 @@ def register(url_prefix: str = "/") -> Blueprint: return Response(generate(), content_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}) + # --- Header demos --- + + @bp.get("/reference/api/trigger-event") + async def ref_trigger_event(): + from shared.sx.helpers import sx_response + now = datetime.now().strftime("%H:%M:%S") + sx_src = f'(span :class "text-stone-800 text-sm" "Loaded at " (strong "{now}") " — check the border!")' + resp = sx_response(sx_src) + resp.headers["SX-Trigger"] = "showNotice" + return resp + + @bp.get("/reference/api/retarget") + async def ref_retarget(): + from shared.sx.helpers import sx_response + now = datetime.now().strftime("%H:%M:%S") + sx_src = f'(span :class "text-violet-700 text-sm" "Retargeted at " (strong "{now}"))' + resp = sx_response(sx_src) + resp.headers["SX-Retarget"] = "#ref-hdr-retarget-alt" + return resp + + # --- Event demos --- + + @bp.get("/reference/api/error-500") + async def ref_error_500(): + return Response("Server error", status=500, content_type="text/plain") + return bp diff --git a/sx/content/pages.py b/sx/content/pages.py index a825342..4816308 100644 --- a/sx/content/pages.py +++ b/sx/content/pages.py @@ -261,6 +261,463 @@ EDIT_ROW_DATA = [ {"id": "4", "name": "Widget D", "price": "45.00", "stock": "67"}, ] +# --------------------------------------------------------------------------- +# Reference: Header detail pages +# --------------------------------------------------------------------------- + +HEADER_DETAILS: dict[str, dict] = { + # --- Request Headers --- + "SX-Request": { + "direction": "request", + "description": ( + "Sent on every sx-initiated request. Allows the server to distinguish " + "AJAX partial requests from full page loads, and return the appropriate " + "response format (fragment vs full page)." + ), + "example": ( + ';; Server-side: check for sx request\n' + '(if (header "SX-Request")\n' + ' ;; Return a fragment\n' + ' (div :class "result" "Partial content")\n' + ' ;; Return full page\n' + ' (~full-page-layout ...))' + ), + }, + "SX-Current-URL": { + "direction": "request", + "description": ( + "Sends the browser's current URL so the server knows where the user is. " + "Useful for server-side logic that depends on context — e.g. highlighting " + "the current nav item, or returning context-appropriate content." + ), + "example": ( + ';; Server reads the current URL to decide context\n' + '(let ((url (header "SX-Current-URL")))\n' + ' (nav\n' + ' (a :href "/docs" :class (if (starts-with? url "/docs") "active" "") "Docs")\n' + ' (a :href "/api" :class (if (starts-with? url "/api") "active" "") "API")))' + ), + }, + "SX-Target": { + "direction": "request", + "description": ( + "Tells the server which element will receive the response. " + "The server can use this to tailor the response — for example, " + "returning different content depending on whether the target is " + "a sidebar, modal, or main panel." + ), + "example": ( + ';; Server checks target to decide response format\n' + '(let ((target (header "SX-Target")))\n' + ' (if (= target "#sidebar")\n' + ' (~compact-summary :data data)\n' + ' (~full-detail :data data)))' + ), + }, + "SX-Components": { + "direction": "request", + "description": ( + "Comma-separated list of component names the client already has cached. " + "The server can skip sending defcomp definitions the client already knows, " + "reducing response size. This is the component caching protocol." + ), + "example": ( + ';; Client sends: SX-Components: ~card,~nav-link,~footer\n' + ';; Server omits those defcomps from the response.\n' + ';; Only new/changed components are sent.\n' + '(response\n' + ' :components (filter-new known-components)\n' + ' :content (~page-content))' + ), + }, + "SX-Css": { + "direction": "request", + "description": ( + "Sends the CSS classes or hash the client already has. " + "The server uses this to send only new CSS rules the client needs, " + "avoiding duplicate rule injection. Part of the on-demand CSS protocol." + ), + "example": ( + ';; Client sends hash of known CSS classes\n' + ';; Server compares and only returns new classes\n' + '(let ((client-css (header "SX-Css")))\n' + ' (set-header "SX-Css-Add"\n' + ' (join "," (diff new-classes client-css))))' + ), + }, + "SX-History-Restore": { + "direction": "request", + "description": ( + "Set to \"true\" when the browser restores a page from history (back/forward). " + "The server can use this to return cached content or skip side effects " + "that should only happen on initial navigation." + ), + "example": ( + ';; Skip analytics on history restore\n' + '(when (not (header "SX-History-Restore"))\n' + ' (track-page-view url))\n' + '(~page-content :data data)' + ), + }, + "SX-Css-Hash": { + "direction": "both", + "description": ( + "Request: 8-character hash of the client's known CSS class set. " + "Response: hash of the cumulative CSS set after this response. " + "Client stores the response hash and sends it on the next request, " + "enabling efficient CSS delta tracking." + ), + "example": ( + ';; Request header: SX-Css-Hash: a1b2c3d4\n' + ';; Server compares hash to decide if CSS diff needed\n' + ';;\n' + ';; Response header: SX-Css-Hash: e5f6g7h8\n' + ';; Client stores new hash for next request' + ), + }, + "SX-Prompt": { + "direction": "request", + "description": ( + "Contains the value entered by the user in a window.prompt() dialog, " + "triggered by the sx-prompt attribute. Allows collecting a single text " + "input without a form." + ), + "example": ( + ';; Button triggers a prompt dialog\n' + '(button :sx-get "/api/rename"\n' + ' :sx-prompt "Enter new name:"\n' + ' "Rename")\n' + '\n' + ';; Server reads the prompted value\n' + '(let ((name (header "SX-Prompt")))\n' + ' (span "Renamed to: " (strong name)))' + ), + "demo": "ref-header-prompt-demo", + }, + # --- Response Headers --- + "SX-Css-Add": { + "direction": "response", + "description": ( + "Comma-separated list of new CSS class names added by this response. " + "The client injects the corresponding CSS rules into the document. " + "Only classes the client doesn't already have are included." + ), + "example": ( + ';; Server response includes new CSS classes\n' + ';; SX-Css-Add: bg-emerald-500,text-white,rounded-xl\n' + ';;\n' + ';; Client automatically injects rules for these\n' + ';; classes from the style dictionary.' + ), + }, + "SX-Trigger": { + "direction": "response", + "description": ( + "Dispatch custom DOM event(s) on the target element after the response " + "is received. Can be a simple event name or JSON for multiple events " + "with detail data. Useful for coordinating UI updates across components." + ), + "example": ( + ';; Simple event\n' + ';; SX-Trigger: itemAdded\n' + ';;\n' + ';; Multiple events with data\n' + ';; SX-Trigger: {"itemAdded": {"id": 42}, "showNotification": {"message": "Saved!"}}\n' + ';;\n' + ';; Listen in SX:\n' + '(div :sx-on:itemAdded "this.querySelector(\'.count\').textContent = event.detail.id")' + ), + "demo": "ref-header-trigger-demo", + }, + "SX-Trigger-After-Swap": { + "direction": "response", + "description": ( + "Like SX-Trigger, but fires after the DOM swap completes. " + "Use this when your event handler needs to reference the new DOM content " + "that was just swapped in." + ), + "example": ( + ';; Server signals that new content needs initialization\n' + ';; SX-Trigger-After-Swap: contentReady\n' + ';;\n' + ';; Client initializes after swap\n' + '(div :sx-on:contentReady "initCharts(this)")' + ), + }, + "SX-Trigger-After-Settle": { + "direction": "response", + "description": ( + "Like SX-Trigger, but fires after the DOM has fully settled — " + "scripts executed, transitions complete. The latest point to react " + "to a response." + ), + "example": ( + ';; SX-Trigger-After-Settle: animationReady\n' + ';;\n' + ';; Trigger animations after everything has settled\n' + '(div :sx-on:animationReady "this.classList.add(\'fade-in\')")' + ), + }, + "SX-Retarget": { + "direction": "response", + "description": ( + "Override the target element for this response. The server can redirect " + "content to a different element than what the client specified in sx-target. " + "Useful for error messages or redirecting content dynamically." + ), + "example": ( + ';; Client targets a form result area\n' + '(form :sx-post "/api/save"\n' + ' :sx-target "#result" ...)\n' + '\n' + ';; Server redirects errors to a different element\n' + ';; SX-Retarget: #error-banner\n' + '(div :class "error" "Validation failed")' + ), + "demo": "ref-header-retarget-demo", + }, + "SX-Reswap": { + "direction": "response", + "description": ( + "Override the swap strategy for this response. The server can change " + "how content is inserted regardless of what the client specified in sx-swap. " + "Useful when the server decides the swap mode based on the result." + ), + "example": ( + ';; Client expects innerHTML swap\n' + '(button :sx-get "/api/check"\n' + ' :sx-target "#panel" :sx-swap "innerHTML" ...)\n' + '\n' + ';; Server overrides to append instead\n' + ';; SX-Reswap: beforeend\n' + '(div :class "notification" "New item added")' + ), + }, + "SX-Redirect": { + "direction": "response", + "description": ( + "Redirect the browser to a new URL using full page navigation. " + "Unlike sx-push-url which does client-side history, this triggers " + "a real browser navigation — useful after form submissions like login or checkout." + ), + "example": ( + ';; After successful login, redirect to dashboard\n' + ';; SX-Redirect: /dashboard\n' + ';;\n' + ';; Server handler:\n' + '(when (valid-credentials? user pass)\n' + ' (set-header "SX-Redirect" "/dashboard")\n' + ' (span "Redirecting..."))' + ), + }, + "SX-Refresh": { + "direction": "response", + "description": ( + "Set to \"true\" to reload the current page. " + "A blunt tool — useful when server-side state has changed significantly " + "and a partial update won't suffice." + ), + "example": ( + ';; After a major state change, force refresh\n' + ';; SX-Refresh: true\n' + ';;\n' + ';; Server handler:\n' + '(when (deploy-complete?)\n' + ' (set-header "SX-Refresh" "true")\n' + ' (span "Deployed — refreshing..."))' + ), + }, + "SX-Location": { + "direction": "response", + "description": ( + "Trigger client-side navigation: fetch the given URL, swap it into " + "#main-panel, and push to browser history. Like clicking an sx-boosted link, " + "but triggered from the server. Can be a URL string or JSON with options." + ), + "example": ( + ';; Simple: navigate to a page\n' + ';; SX-Location: /docs/introduction\n' + ';;\n' + ';; With options:\n' + ';; SX-Location: {"path": "/docs/intro", "target": "#sidebar", "swap": "innerHTML"}' + ), + }, + "SX-Replace-Url": { + "direction": "response", + "description": ( + "Replace the current URL using history.replaceState without creating " + "a new history entry. Useful for normalizing URLs after redirects, " + "or updating the URL to reflect server-resolved state." + ), + "example": ( + ';; Normalize URL after slug resolution\n' + ';; SX-Replace-Url: /docs/introduction\n' + ';;\n' + ';; Server handler:\n' + '(let ((canonical (resolve-slug slug)))\n' + ' (set-header "SX-Replace-Url" canonical)\n' + ' (~doc-content :slug canonical))' + ), + }, +} + +# --------------------------------------------------------------------------- +# Reference: Event detail pages +# --------------------------------------------------------------------------- + +EVENT_DETAILS: dict[str, dict] = { + "sx:beforeRequest": { + "description": ( + "Fired on the triggering element before an sx request is issued. " + "Call event.preventDefault() to cancel the request entirely. " + "Useful for validation, confirmation, or conditional request logic." + ), + "example": ( + ';; Cancel request if form is empty\n' + '(form :sx-post "/api/save"\n' + ' :sx-target "#result"\n' + ' :sx-on:sx:beforeRequest "if (!this.querySelector(\'input\').value) event.preventDefault()"\n' + ' (input :name "data" :placeholder "Required")\n' + ' (button :type "submit" "Save"))' + ), + "demo": "ref-event-before-request-demo", + }, + "sx:afterRequest": { + "description": ( + "Fired on the triggering element after a successful sx response is received, " + "before the swap happens. The response data is available on event.detail. " + "Use this for logging, analytics, or pre-swap side effects." + ), + "example": ( + ';; Log successful requests\n' + '(button :sx-get "/api/data"\n' + ' :sx-target "#result"\n' + ' :sx-on:sx:afterRequest "console.log(\'Response received\', event.detail)"\n' + ' "Load data")' + ), + }, + "sx:afterSwap": { + "description": ( + "Fired after the response content has been swapped into the DOM. " + "The new content is in place but scripts may not have executed yet. " + "Use this to initialize UI on newly inserted content." + ), + "example": ( + ';; Initialize tooltips on new content\n' + '(div :sx-on:sx:afterSwap "initTooltips(this)"\n' + ' (button :sx-get "/api/items"\n' + ' :sx-target "#item-list"\n' + ' "Load items")\n' + ' (div :id "item-list"))' + ), + }, + "sx:afterSettle": { + "description": ( + "Fired after the DOM has fully settled — all scripts executed, transitions " + "complete. This is the safest point to run code that depends on the final " + "state of the DOM after a swap." + ), + "example": ( + ';; Scroll to new content after settle\n' + '(div :sx-on:sx:afterSettle "document.getElementById(\'new-item\').scrollIntoView()"\n' + ' (button :sx-get "/api/append"\n' + ' :sx-target "#list" :sx-swap "beforeend"\n' + ' "Add item")\n' + ' (div :id "list"))' + ), + "demo": "ref-event-after-settle-demo", + }, + "sx:responseError": { + "description": ( + "Fired when the server responds with an HTTP error (4xx or 5xx). " + "event.detail contains the status code and response. " + "Use this for error handling, showing notifications, or retry logic." + ), + "example": ( + ';; Show error notification\n' + '(div :sx-on:sx:responseError "alert(\'Error: \' + event.detail.status)"\n' + ' (button :sx-get "/api/risky"\n' + ' :sx-target "#result"\n' + ' "Try it")\n' + ' (div :id "result"))' + ), + "demo": "ref-event-response-error-demo", + }, + "sx:sendError": { + "description": ( + "Fired when the request fails to send — typically a network error, " + "DNS failure, or CORS issue. Unlike sx:responseError, no HTTP response " + "was received at all." + ), + "example": ( + ';; Handle network failures\n' + '(div :sx-on:sx:sendError "this.querySelector(\'.status\').textContent = \'Offline\'"\n' + ' (button :sx-get "/api/data"\n' + ' :sx-target "#result"\n' + ' "Load")\n' + ' (span :class "status")\n' + ' (div :id "result"))' + ), + }, + "sx:validationFailed": { + "description": ( + "Fired when sx-validate is set and the form fails HTML5 validation. " + "The request is not sent. Use this to show custom validation UI " + "or highlight invalid fields." + ), + "example": ( + ';; Highlight invalid fields\n' + '(form :sx-post "/api/save"\n' + ' :sx-validate "true"\n' + ' :sx-on:sx:validationFailed "this.classList.add(\'shake\')"\n' + ' (input :type "email" :required "true" :name "email"\n' + ' :placeholder "Email (required)")\n' + ' (button :type "submit" "Save"))' + ), + "demo": "ref-event-validation-failed-demo", + }, + "sx:sseOpen": { + "description": ( + "Fired when a Server-Sent Events connection is successfully established. " + "Use this to update connection status indicators." + ), + "example": ( + ';; Show connected status\n' + '(div :sx-sse "/api/stream"\n' + ' :sx-on:sx:sseOpen "this.querySelector(\'.status\').textContent = \'Connected\'"\n' + ' (span :class "status" "Connecting...")\n' + ' (div :id "messages"))' + ), + }, + "sx:sseMessage": { + "description": ( + "Fired when an SSE message is received and swapped into the DOM. " + "event.detail contains the message data. Fires for each individual message." + ), + "example": ( + ';; Count received messages\n' + '(div :sx-sse "/api/stream"\n' + ' :sx-sse-swap "update"\n' + ' :sx-on:sx:sseMessage "this.dataset.count = (parseInt(this.dataset.count||0)+1); this.querySelector(\'.count\').textContent = this.dataset.count"\n' + ' (span :class "count" "0") " messages received"\n' + ' (div :id "stream-content"))' + ), + }, + "sx:sseError": { + "description": ( + "Fired when an SSE connection encounters an error or is closed unexpectedly. " + "Use this to show reconnection status or fall back to polling." + ), + "example": ( + ';; Show disconnected status\n' + '(div :sx-sse "/api/stream"\n' + ' :sx-on:sx:sseError "this.querySelector(\'.status\').textContent = \'Disconnected\'"\n' + ' (span :class "status" "Connecting...")\n' + ' (div :id "messages"))' + ), + }, +} + # --------------------------------------------------------------------------- # Reference: Attribute detail pages # --------------------------------------------------------------------------- diff --git a/sx/sx/docs.sx b/sx/sx/docs.sx index 6c94165..f52ec72 100644 --- a/sx/sx/docs.sx +++ b/sx/sx/docs.sx @@ -34,15 +34,27 @@ (th :class "px-3 py-2 font-medium text-stone-600" "Description"))) (tbody rows))))) -(defcomp ~doc-headers-row (&key name value description) +(defcomp ~doc-headers-row (&key name value description href) (tr :class "border-b border-stone-100" - (td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" name) + (td :class "px-3 py-2 font-mono text-sm whitespace-nowrap" + (if href + (a :href href + :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + :class "text-violet-700 hover:text-violet-900 underline" name) + (span :class "text-violet-700" name))) (td :class "px-3 py-2 font-mono text-sm text-stone-500" value) (td :class "px-3 py-2 text-stone-700 text-sm" description))) -(defcomp ~doc-two-col-row (&key name description) +(defcomp ~doc-two-col-row (&key name description href) (tr :class "border-b border-stone-100" - (td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" name) + (td :class "px-3 py-2 font-mono text-sm whitespace-nowrap" + (if href + (a :href href + :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + :class "text-violet-700 hover:text-violet-900 underline" name) + (span :class "text-violet-700" name))) (td :class "px-3 py-2 text-stone-700 text-sm" description))) (defcomp ~doc-two-col-table (&key title intro col1 col2 rows) @@ -88,7 +100,8 @@ (~doc-headers-row :name (get h "name") :value (get h "value") - :description (get h "desc"))) + :description (get h "desc") + :href (get h "href"))) headers)))) ;; Build two-col table from a list of {name, desc} dicts. @@ -98,7 +111,8 @@ :rows (<> (map (fn (item) (~doc-two-col-row :name (get item "name") - :description (get item "desc"))) + :description (get item "desc") + :href (get item "href"))) items)))) ;; Build all primitives category tables from a {category: [prim, ...]} dict. diff --git a/sx/sx/essays.sx b/sx/sx/essays.sx index e166399..75e54df 100644 --- a/sx/sx/essays.sx +++ b/sx/sx/essays.sx @@ -41,7 +41,7 @@ (~doc-page :title "Tail-Call Optimization in SX" (p :class "text-stone-500 text-sm italic mb-8" "How SX eliminates stack overflow for recursive functions using trampolining — across Python server and JavaScript client.") (~doc-section :title "The problem" :id "problem" (p :class "text-stone-600" "Every language built on a host runtime inherits the host's stack limits. Python defaults to 1,000 frames. JavaScript engines vary — Chrome gives ~10,000, Safari sometimes less. A naive recursive function blows the stack:") (~doc-code :lang "lisp" :code "(define factorial (fn (n)\n (if (= n 0)\n 1\n (* n (factorial (- n 1))))))\n\n;; (factorial 50000) → stack overflow") (p :class "text-stone-600" "This isn't just academic. Tree traversals, state machines, interpreters, and accumulating loops all naturally express as recursion. A general-purpose language that can't recurse deeply isn't general-purpose.")) (~doc-section :title "Tail position" :id "tail-position" (p :class "text-stone-600" "A function call is in tail position when its result IS the result of the enclosing function — nothing more happens after it returns. The call doesn't need to come back to finish work:") (~doc-code :lang "lisp" :code ";; Tail-recursive — the recursive call IS the return value\n(define count-down (fn (n)\n (if (= n 0) \"done\" (count-down (- n 1)))))\n\n;; NOT tail-recursive — multiplication happens AFTER the recursive call\n(define factorial (fn (n)\n (if (= n 0) 1 (* n (factorial (- n 1))))))") (p :class "text-stone-600" "SX identifies tail positions in: if/when branches, the last expression in let/begin/do bodies, cond/case result branches, lambda/component bodies, and macro expansions.")) (~doc-section :title "Trampolining" :id "trampolining" (p :class "text-stone-600" "Instead of recursing, tail calls return a thunk — a deferred (expression, environment) pair. The evaluator's trampoline loop unwraps thunks iteratively:") (~doc-code :lang "lisp" :code ";; Conceptually:\nevaluate(expr, env):\n result = eval(expr, env)\n while result is Thunk:\n result = eval(thunk.expr, thunk.env)\n return result") (p :class "text-stone-600" "One stack frame. Always. The trampoline replaces recursive stack growth with an iterative loop. Non-tail calls still use the stack normally — only tail positions get the thunk treatment.")) (~doc-section :title "What this enables" :id "enables" (p :class "text-stone-600" "Tail-recursive accumulator pattern — the natural loop construct for a language without for/while:") (~doc-code :lang "lisp" :code ";; Sum 1 to n without stack overflow\n(define sum (fn (n acc)\n (if (= n 0) acc (sum (- n 1) (+ acc n)))))\n\n(sum 100000 0) ;; → 5000050000") (p :class "text-stone-600" "Mutual recursion:") (~doc-code :lang "lisp" :code "(define is-even (fn (n) (if (= n 0) true (is-odd (- n 1)))))\n(define is-odd (fn (n) (if (= n 0) false (is-even (- n 1)))))\n\n(is-even 100000) ;; → true") (p :class "text-stone-600" "State machines:") (~doc-code :lang "lisp" :code "(define state-a (fn (input)\n (cond\n (= (first input) \"x\") (state-b (rest input))\n (= (first input) \"y\") (state-a (rest input))\n :else \"rejected\")))\n\n(define state-b (fn (input)\n (if (empty? input) \"accepted\"\n (state-a (rest input)))))") (p :class "text-stone-600" "All three patterns recurse arbitrarily deep with constant stack usage.")) (~doc-section :title "Implementation" :id "implementation" (p :class "text-stone-600" "TCO is implemented identically across all three SX evaluators:") (ul :class "list-disc pl-6 space-y-1 text-stone-600" (li (span :class "font-semibold" "Python sync evaluator") " — shared/sx/evaluator.py") (li (span :class "font-semibold" "Python async evaluator") " — shared/sx/async_eval.py (planned)") (li (span :class "font-semibold" "JavaScript client evaluator") " — sx.js")) (p :class "text-stone-600" "The pattern is the same everywhere: a Thunk type with (expr, env) slots, a trampoline loop in the public evaluate() entry point, and thunk returns from tail positions in the internal evaluator. External consumers (HTML renderer, resolver, higher-order forms) trampoline all eval results.") (p :class "text-stone-600" "The key insight: callers that already work don't need to change. The public sxEval/evaluate API always returns values, never thunks. Only the internal evaluator and special forms know about thunks.")) (~doc-section :title "What about continuations?" :id "continuations" (p :class "text-stone-600" "TCO handles the immediate need: recursive algorithms that don't blow the stack. Continuations (call/cc, delimited continuations) are a separate, larger primitive — they capture the entire evaluation context as a first-class value.") (p :class "text-stone-600" "Having the primitive available doesn't add complexity unless it's invoked. See " (a :href "/essays/continuations" :class "text-violet-600 hover:underline" "the continuations essay") " for what they would enable in SX.")))) (defcomp ~essay-godel-escher-bach () - (~doc-page :title "Godel, Escher, Bach and SX" (p :class "text-stone-500 text-sm italic mb-8" "Strange loops, self-reference, and the tangled hierarchy of a language that defines itself.") (~doc-section :title "The strange loop" :id "strange-loop" (p :class "text-stone-600" "In 1979, Douglas Hofstadter wrote " (a :href "https://en.wikipedia.org/wiki/G%C3%B6del,_Escher,_Bach" :class "text-violet-600 hover:underline" "a book") " about how minds, music, and mathematics all share the same deep structure: the " (a :href "https://en.wikipedia.org/wiki/Strange_loop" :class "text-violet-600 hover:underline" "strange loop") ". A strange loop occurs when you move through a hierarchical system and unexpectedly find yourself back where you started. " (a :href "https://en.wikipedia.org/wiki/Relativity_(M._C._Escher)" :class "text-violet-600 hover:underline" "Escher's impossible staircases") ". " (a :href "https://en.wikipedia.org/wiki/The_Musical_Offering" :class "text-violet-600 hover:underline" "Bach's endlessly rising canons") ". " (a :href "https://en.wikipedia.org/wiki/G%C3%B6del%27s_incompleteness_theorems" :class "text-violet-600 hover:underline" "Godel's theorem") " that uses number theory to make statements about number theory.") (p :class "text-stone-600" "SX has a strange loop. The language is defined in itself. The canonical specification of the SX evaluator, parser, and renderer lives in four " (code ".sx") " files. A bootstrap compiler reads them and emits a working JavaScript evaluator. That evaluator can then parse and evaluate the specification that defines it.") (p :class "text-stone-600" "This is not an accident. It is the point.")) (~doc-section :title "Godel numbering and self-reference" :id "godel" (p :class "text-stone-600" (a :href "https://en.wikipedia.org/wiki/G%C3%B6del_numbering" :class "text-violet-600 hover:underline" "Godel numbering") " works by encoding logical statements as numbers. Once statements are numbers, you can construct a statement that says \"this statement is unprovable\" — and it is true. The system becomes powerful enough to talk about itself the moment its objects and its meta-language become the same thing.") (p :class "text-stone-600" (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "S-expressions") " have this property naturally. Code is data. " (code "(defcomp ~card (&key title) (div title))") " is simultaneously a program (define a component) and a data structure (a list of symbols, keywords, and another list). There is no separate meta-language. The language for writing programs and the language for inspecting, transforming, and generating programs are identical.") (~doc-code :lang "lisp" :code ";; A macro receives code as data and returns code as data\n(defmacro ~when-admin (condition &rest body)\n `(when (get rights \"admin\")\n ,@body))\n\n;; The macro's input and output are both ordinary lists.\n;; There is no template language. No AST wrapper types.\n;; Just lists all the way down.") (p :class "text-stone-600" "This is Godel numbering without the encoding step. In formal logic, you must laboriously map formulas to numbers. In SX, programs are already expressed in the same medium they manipulate. " (a :href "https://en.wikipedia.org/wiki/Map%E2%80%93territory_relation" :class "text-violet-600 hover:underline" "The map is the territory") ".")) (~doc-section :title "Escher: tangled hierarchies" :id "escher" (p :class "text-stone-600" (a :href "https://en.wikipedia.org/wiki/M._C._Escher" :class "text-violet-600 hover:underline" "Escher's") " lithographs depict objects that are simultaneously inside and outside their own frames. " (a :href "https://en.wikipedia.org/wiki/Drawing_Hands" :class "text-violet-600 hover:underline" "A hand draws the hand that draws it") ". " (a :href "https://en.wikipedia.org/wiki/Waterfall_(M._C._Escher)" :class "text-violet-600 hover:underline" "Water flows downhill in a closed loop") ". The image contains the image.") (p :class "text-stone-600" "SX has the same " (a :href "https://en.wikipedia.org/wiki/Tangled_hierarchy" :class "text-violet-600 hover:underline" "tangled hierarchy") " across its rendering pipeline. The server evaluator (" (code "async_eval.py") ") evaluates component definitions. Some of those components produce SX wire format — s-expression source code — that the client evaluator (" (code "sx.js") ") then evaluates into DOM. The output of one evaluator is the input to another. The program produces programs.") (p :class "text-stone-600" "Now add the self-hosting specification. The canonical definition of " (em "how to evaluate SX") " is itself an SX program. The bootstrap compiler reads " (code "eval.sx") " and emits JavaScript. That JavaScript implements " (code "eval-expr") " — the same function defined in " (code "eval.sx") ". The definition and the thing defined occupy the same level. Like " (a :href "https://en.wikipedia.org/wiki/Drawing_Hands" :class "text-violet-600 hover:underline" "Escher's hands") ", each one brings the other into existence.") (p :class "text-stone-600" "This is not merely clever. It has practical consequences. When the specification IS the program, there is no drift between documentation and implementation. The spec cannot lie, because the spec runs.")) (~doc-section :title "Bach: the endlessly rising canon" :id "bach" (p :class "text-stone-600" "Bach's " (a :href "https://en.wikipedia.org/wiki/The_Musical_Offering" :class "text-violet-600 hover:underline" "Musical Offering") " contains canons that rise in pitch with each repetition yet somehow arrive back at the starting key — the " (a :href "https://en.wikipedia.org/wiki/Shepard_tone" :class "text-violet-600 hover:underline" "Shepard tone") " of counterpoint. The sensation is of endless ascent — each level feels higher than the last, yet the structure is cyclic.") (p :class "text-stone-600" "SX's rendering pipeline has this shape. A page request triggers server-side evaluation. The server evaluates components, which produce SX source text. That source is sent to the client. The client evaluates it into DOM. The user interacts with the DOM, triggering an HTTP request. The server evaluates the response — more SX source. The client evaluates it again. Each cycle produces something new (different content, different state), but the process is the same loop, repeating at a higher level.") (~doc-code :lang "lisp" :code ";; Server: evaluate component, produce SX wire format\n(~card :title \"Bach\")\n;; → (div :class \"card\" (h2 \"Bach\"))\n\n;; Client: evaluate SX wire format, produce DOM\n;; →

Bach

\n\n;; User clicks → server evaluates → SX → client evaluates → DOM\n;; The canon rises. The key is the same.") (p :class "text-stone-600" "With the self-hosting spec, another voice enters the canon. The specification is evaluated at build time (by the bootstrap compiler) to produce the evaluator. The evaluator is evaluated at runtime (by the browser) to produce the page. The page describes the specification. Each level feeds the next, and the last feeds the first.")) (~doc-section :title "Isomorphism" :id "isomorphism" (p :class "text-stone-600" "Hofstadter's central insight is that Godel, Escher, and Bach are all doing the same thing in different media: constructing systems that can " (a :href "https://en.wikipedia.org/wiki/Self-reference" :class "text-violet-600 hover:underline" "represent themselves") ". The power — and the paradox — comes from self-reference.") (p :class "text-stone-600" "Most programming languages avoid self-reference. They are implemented in a different language (C, Rust, Go). Their specification is in English prose. Their AST is a separate data structure from their source syntax. There are clear levels: the language, the implementation of the language, the specification of the language. Each level is expressed in a different medium.") (p :class "text-stone-600" "SX collapses these levels:") (ul :class "list-disc pl-6 space-y-1 text-stone-600" (li (span :class "font-semibold" "Source syntax") " = data structure (s-expressions are both)") (li (span :class "font-semibold" "Specification") " = program (" (code "eval.sx") " is executable)") (li (span :class "font-semibold" "Server output") " = client input (SX wire format)") (li (span :class "font-semibold" "Code") " = content (this essay is an s-expression)")) (p :class "text-stone-600" "This is not mere elegance. Each collapsed level is one fewer translation boundary, one fewer place where meaning can be lost, one fewer surface for bugs. When the specification is the implementation, the specification is correct by construction. When the wire format is the source syntax, serialization is identity. When code and data share a representation, " (a :href "https://en.wikipedia.org/wiki/Homoiconicity" :class "text-violet-600 hover:underline" "metaprogramming is just programming") ".")) (~doc-section :title "The MU puzzle" :id "mu-puzzle" (p :class "text-stone-600" "GEB opens with the " (a :href "https://en.wikipedia.org/wiki/MU_puzzle" :class "text-violet-600 hover:underline" "MU puzzle") ": given the string " (code "MI") " and a set of transformation rules, can you produce " (code "MU") "? You cannot. But you can only prove this by stepping outside the system and reasoning about it from above — by noticing an invariant that the rules preserve.") (p :class "text-stone-600" "Self-hosting languages let you step outside from inside. The SX evaluator is an SX program. You can inspect it, test it, transform it — using SX. You can write an SX program that reads " (code "eval.sx") " and checks properties of the evaluator. The meta-level and the object-level are the same level.") (p :class "text-stone-600" "This is what Godel did. He showed that sufficiently powerful " (a :href "https://en.wikipedia.org/wiki/Formal_system" :class "text-violet-600 hover:underline" "formal systems") " can encode questions about themselves. S-expressions have been doing it " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)#History" :class "text-violet-600 hover:underline" "since 1958") ". SX carries the tradition forward — into the browser, across the HTTP boundary, through the render loop, and back again.")) (~doc-section :title "The loop closes" :id "the-loop-closes" (p :class "text-stone-600" "Hofstadter argued that " (a :href "https://en.wikipedia.org/wiki/I_Am_a_Strange_Loop" :class "text-violet-600 hover:underline" "strange loops give rise to what we call \"I\"") " — that consciousness is a self-referential pattern recognizing itself. He was talking about brains. But the structural argument — that self-reference creates something qualitatively different from external description — applies more broadly.") (p :class "text-stone-600" "A language that can define itself has a kind of autonomy that externally-defined languages lack. It is not dependent on a specific host. The SX specification in " (code "eval.sx") " can be compiled to JavaScript, Python, Rust, WASM — any target the bootstrap compiler supports. The language carries its own definition with it. It can reproduce itself in any medium that supports computation.") (p :class "text-stone-600" "SX is not a framework. Frameworks impose structure — you write code that the framework calls. SX does not do that. It is not just a language either, though it has a parser, evaluator, and type system. It is something closer to a " (em "paradigm") " — a coherent way of thinking about what the web is. Code is data. Server and client share the same evaluator. The wire format is the source syntax. The language defines itself. These are not features. They are consequences of a single design choice: " (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "s-expressions") " as the universal representation.") (p :class "text-stone-600" "Hofstadter spent 777 pages describing systems that cross their own boundaries, talk about themselves in their own vocabulary, and generate coherent behaviour from recursive self-reference. SX is one of those systems. The loop closes.")))) + (~doc-page :title "Strange Loops" (p :class "text-stone-500 text-sm italic mb-8" "Self-reference, and the tangled hierarchy of a language that defines itself.") (~doc-section :title "The strange loop" :id "strange-loop" (p :class "text-stone-600" "In 1979, Douglas Hofstadter wrote " (a :href "https://en.wikipedia.org/wiki/G%C3%B6del,_Escher,_Bach" :class "text-violet-600 hover:underline" "a book") " about how minds, music, and mathematics all share the same deep structure: the " (a :href "https://en.wikipedia.org/wiki/Strange_loop" :class "text-violet-600 hover:underline" "strange loop") ". A strange loop occurs when you move through a hierarchical system and unexpectedly find yourself back where you started. " (a :href "https://en.wikipedia.org/wiki/Relativity_(M._C._Escher)" :class "text-violet-600 hover:underline" "Escher's impossible staircases") ". " (a :href "https://en.wikipedia.org/wiki/The_Musical_Offering" :class "text-violet-600 hover:underline" "Bach's endlessly rising canons") ". " (a :href "https://en.wikipedia.org/wiki/G%C3%B6del%27s_incompleteness_theorems" :class "text-violet-600 hover:underline" "Godel's theorem") " that uses number theory to make statements about number theory.") (p :class "text-stone-600" "SX has a strange loop. The language is defined in itself. The canonical specification of the SX evaluator, parser, and renderer lives in four " (code ".sx") " files. A bootstrap compiler reads them and emits a working JavaScript evaluator. That evaluator can then parse and evaluate the specification that defines it.") (p :class "text-stone-600" "This is not an accident. It is the point.")) (~doc-section :title "Godel numbering and self-reference" :id "godel" (p :class "text-stone-600" (a :href "https://en.wikipedia.org/wiki/G%C3%B6del_numbering" :class "text-violet-600 hover:underline" "Godel numbering") " works by encoding logical statements as numbers. Once statements are numbers, you can construct a statement that says \"this statement is unprovable\" — and it is true. The system becomes powerful enough to talk about itself the moment its objects and its meta-language become the same thing.") (p :class "text-stone-600" (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "S-expressions") " have this property naturally. Code is data. " (code "(defcomp ~card (&key title) (div title))") " is simultaneously a program (define a component) and a data structure (a list of symbols, keywords, and another list). There is no separate meta-language. The language for writing programs and the language for inspecting, transforming, and generating programs are identical.") (~doc-code :lang "lisp" :code ";; A macro receives code as data and returns code as data\n(defmacro ~when-admin (condition &rest body)\n `(when (get rights \"admin\")\n ,@body))\n\n;; The macro's input and output are both ordinary lists.\n;; There is no template language. No AST wrapper types.\n;; Just lists all the way down.") (p :class "text-stone-600" "This is Godel numbering without the encoding step. In formal logic, you must laboriously map formulas to numbers. In SX, programs are already expressed in the same medium they manipulate. " (a :href "https://en.wikipedia.org/wiki/Map%E2%80%93territory_relation" :class "text-violet-600 hover:underline" "The map is the territory") ".")) (~doc-section :title "Escher: tangled hierarchies" :id "escher" (p :class "text-stone-600" (a :href "https://en.wikipedia.org/wiki/M._C._Escher" :class "text-violet-600 hover:underline" "Escher's") " lithographs depict objects that are simultaneously inside and outside their own frames. " (a :href "https://en.wikipedia.org/wiki/Drawing_Hands" :class "text-violet-600 hover:underline" "A hand draws the hand that draws it") ". " (a :href "https://en.wikipedia.org/wiki/Waterfall_(M._C._Escher)" :class "text-violet-600 hover:underline" "Water flows downhill in a closed loop") ". The image contains the image.") (p :class "text-stone-600" "SX has the same " (a :href "https://en.wikipedia.org/wiki/Tangled_hierarchy" :class "text-violet-600 hover:underline" "tangled hierarchy") " across its rendering pipeline. The server evaluator (" (code "async_eval.py") ") evaluates component definitions. Some of those components produce SX wire format — s-expression source code — that the client evaluator (" (code "sx.js") ") then evaluates into DOM. The output of one evaluator is the input to another. The program produces programs.") (p :class "text-stone-600" "Now add the self-hosting specification. The canonical definition of " (em "how to evaluate SX") " is itself an SX program. The bootstrap compiler reads " (code "eval.sx") " and emits JavaScript. That JavaScript implements " (code "eval-expr") " — the same function defined in " (code "eval.sx") ". The definition and the thing defined occupy the same level. Like " (a :href "https://en.wikipedia.org/wiki/Drawing_Hands" :class "text-violet-600 hover:underline" "Escher's hands") ", each one brings the other into existence.") (p :class "text-stone-600" "This is not merely clever. It has practical consequences. When the specification IS the program, there is no drift between documentation and implementation. The spec cannot lie, because the spec runs.")) (~doc-section :title "Bach: the endlessly rising canon" :id "bach" (p :class "text-stone-600" "Bach's " (a :href "https://en.wikipedia.org/wiki/The_Musical_Offering" :class "text-violet-600 hover:underline" "Musical Offering") " contains canons that rise in pitch with each repetition yet somehow arrive back at the starting key — the " (a :href "https://en.wikipedia.org/wiki/Shepard_tone" :class "text-violet-600 hover:underline" "Shepard tone") " of counterpoint. The sensation is of endless ascent — each level feels higher than the last, yet the structure is cyclic.") (p :class "text-stone-600" "SX's rendering pipeline has this shape. A page request triggers server-side evaluation. The server evaluates components, which produce SX source text. That source is sent to the client. The client evaluates it into DOM. The user interacts with the DOM, triggering an HTTP request. The server evaluates the response — more SX source. The client evaluates it again. Each cycle produces something new (different content, different state), but the process is the same loop, repeating at a higher level.") (~doc-code :lang "lisp" :code ";; Server: evaluate component, produce SX wire format\n(~card :title \"Bach\")\n;; → (div :class \"card\" (h2 \"Bach\"))\n\n;; Client: evaluate SX wire format, produce DOM\n;; →

Bach

\n\n;; User clicks → server evaluates → SX → client evaluates → DOM\n;; The canon rises. The key is the same.") (p :class "text-stone-600" "With the self-hosting spec, another voice enters the canon. The specification is evaluated at build time (by the bootstrap compiler) to produce the evaluator. The evaluator is evaluated at runtime (by the browser) to produce the page. The page describes the specification. Each level feeds the next, and the last feeds the first.")) (~doc-section :title "Isomorphism" :id "isomorphism" (p :class "text-stone-600" "Hofstadter's central insight is that Godel, Escher, and Bach are all doing the same thing in different media: constructing systems that can " (a :href "https://en.wikipedia.org/wiki/Self-reference" :class "text-violet-600 hover:underline" "represent themselves") ". The power — and the paradox — comes from self-reference.") (p :class "text-stone-600" "Most programming languages avoid self-reference. They are implemented in a different language (C, Rust, Go). Their specification is in English prose. Their AST is a separate data structure from their source syntax. There are clear levels: the language, the implementation of the language, the specification of the language. Each level is expressed in a different medium.") (p :class "text-stone-600" "SX collapses these levels:") (ul :class "list-disc pl-6 space-y-1 text-stone-600" (li (span :class "font-semibold" "Source syntax") " = data structure (s-expressions are both)") (li (span :class "font-semibold" "Specification") " = program (" (code "eval.sx") " is executable)") (li (span :class "font-semibold" "Server output") " = client input (SX wire format)") (li (span :class "font-semibold" "Code") " = content (this essay is an s-expression)")) (p :class "text-stone-600" "This is not mere elegance. Each collapsed level is one fewer translation boundary, one fewer place where meaning can be lost, one fewer surface for bugs. When the specification is the implementation, the specification is correct by construction. When the wire format is the source syntax, serialization is identity. When code and data share a representation, " (a :href "https://en.wikipedia.org/wiki/Homoiconicity" :class "text-violet-600 hover:underline" "metaprogramming is just programming") ".")) (~doc-section :title "The MU puzzle" :id "mu-puzzle" (p :class "text-stone-600" "GEB opens with the " (a :href "https://en.wikipedia.org/wiki/MU_puzzle" :class "text-violet-600 hover:underline" "MU puzzle") ": given the string " (code "MI") " and a set of transformation rules, can you produce " (code "MU") "? You cannot. But you can only prove this by stepping outside the system and reasoning about it from above — by noticing an invariant that the rules preserve.") (p :class "text-stone-600" "Self-hosting languages let you step outside from inside. The SX evaluator is an SX program. You can inspect it, test it, transform it — using SX. You can write an SX program that reads " (code "eval.sx") " and checks properties of the evaluator. The meta-level and the object-level are the same level.") (p :class "text-stone-600" "This is what Godel did. He showed that sufficiently powerful " (a :href "https://en.wikipedia.org/wiki/Formal_system" :class "text-violet-600 hover:underline" "formal systems") " can encode questions about themselves. S-expressions have been doing it " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)#History" :class "text-violet-600 hover:underline" "since 1958") ". SX carries the tradition forward — into the browser, across the HTTP boundary, through the render loop, and back again.")) (~doc-section :title "The loop closes" :id "the-loop-closes" (p :class "text-stone-600" "Hofstadter argued that " (a :href "https://en.wikipedia.org/wiki/I_Am_a_Strange_Loop" :class "text-violet-600 hover:underline" "strange loops give rise to what we call \"I\"") " — that consciousness is a self-referential pattern recognizing itself. He was talking about brains. But the structural argument — that self-reference creates something qualitatively different from external description — applies more broadly.") (p :class "text-stone-600" "A language that can define itself has a kind of autonomy that externally-defined languages lack. It is not dependent on a specific host. The SX specification in " (code "eval.sx") " can be compiled to JavaScript, Python, Rust, WASM — any target the bootstrap compiler supports. The language carries its own definition with it. It can reproduce itself in any medium that supports computation.") (p :class "text-stone-600" "SX is not a framework. Frameworks impose structure — you write code that the framework calls. SX does not do that. It is not just a language either, though it has a parser, evaluator, and type system. It is something closer to a " (em "paradigm") " — a coherent way of thinking about what the web is. Code is data. Server and client share the same evaluator. The wire format is the source syntax. The language defines itself. These are not features. They are consequences of a single design choice: " (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "s-expressions") " as the universal representation.") (p :class "text-stone-600" "Hofstadter spent 777 pages describing systems that cross their own boundaries, talk about themselves in their own vocabulary, and generate coherent behaviour from recursive self-reference. SX is one of those systems. The loop closes.")))) (defcomp ~essay-continuations () (~doc-page :title "Continuations and call/cc" (p :class "text-stone-500 text-sm italic mb-8" "What first-class continuations would enable in SX — on both the server (Python) and client (JavaScript).") (~doc-section :title "What is a continuation?" :id "what" (p :class "text-stone-600" "A continuation is the rest of a computation. At any point during evaluation, the continuation is everything that would happen next. call/cc (call-with-current-continuation) captures that \"rest of the computation\" as a first-class function that you can store, pass around, and invoke later — possibly multiple times.") (~doc-code :lang "lisp" :code ";; call/cc captures \"what happens next\" as k\n(+ 1 (call/cc (fn (k)\n (k 41)))) ;; → 42\n\n;; k is \"add 1 to this and return it\"\n;; (k 41) jumps back to that point with 41") (p :class "text-stone-600" "The key property: invoking a continuation abandons the current computation and resumes from where the continuation was captured. It is a controlled, first-class goto.")) (~doc-section :title "Server-side: suspendable rendering" :id "server" (p :class "text-stone-600" "The strongest case for continuations on the server is suspendable rendering — the ability for a component to pause mid-render while waiting for data, then resume exactly where it left off.") (~doc-code :lang "lisp" :code ";; Hypothetical: component suspends at a data boundary\n(defcomp ~user-profile (&key user-id)\n (let ((user (suspend (query :user user-id))))\n (div :class \"p-4\"\n (h2 (get user \"name\"))\n (p (get user \"bio\")))))") (p :class "text-stone-600" "Today, all data must be fetched before render_to_sx is called — Python awaits every query, assembles a complete data dict, then passes it to the evaluator. With continuations, the evaluator could yield at (suspend ...), the server flushes what it has so far, and resumes when the data arrives. This is React Suspense, but for server-side s-expressions.") (p :class "text-stone-600" "Streaming follows naturally. The server renders the page shell immediately, captures continuations at slow data boundaries, and flushes partial SX responses as each resolves. The client receives a stream of s-expression chunks and incrementally builds the DOM.") (p :class "text-stone-600" "Error boundaries also become first-class. Capture a continuation at a component boundary. If any child fails, invoke the continuation with fallback content instead of letting the exception propagate up through Python. The evaluator handles it, not the host language.")) (~doc-section :title "Client-side: linear async flows" :id "client" (p :class "text-stone-600" "On the client, continuations eliminate callback nesting for interactive flows. A confirmation dialog becomes a synchronous-looking expression:") (~doc-code :lang "lisp" :code "(let ((answer (call/cc show-confirm-dialog)))\n (if answer\n (delete-item item-id)\n (noop)))") (p :class "text-stone-600" "show-confirm-dialog receives the continuation, renders a modal, and wires the Yes/No buttons to invoke the continuation with true or false. The let binding reads top-to-bottom. No promises, no callbacks, no state machine.") (p :class "text-stone-600" "Multi-step forms — wizard-style UIs where each step captures a continuation. The back button literally invokes a saved continuation, restoring the exact evaluation state:") (~doc-code :lang "lisp" :code "(define wizard\n (fn ()\n (let* ((name (call/cc (fn (k) (render-step-1 k))))\n (email (call/cc (fn (k) (render-step-2 k name))))\n (plan (call/cc (fn (k) (render-step-3 k name email)))))\n (submit-registration name email plan))))") (p :class "text-stone-600" "Each render-step-N shows a form and wires the \"Next\" button to invoke k with the form value. The \"Back\" button invokes the previous step\'s continuation. The wizard logic is a straight-line let* binding, not a state machine.")) (~doc-section :title "Cooperative scheduling" :id "scheduling" (p :class "text-stone-600" "Delimited continuations (shift/reset rather than full call/cc) enable cooperative multitasking within the evaluator. A long render can yield control:") (~doc-code :lang "lisp" :code ";; Render a large list, yielding every 100 items\n(define render-chunk\n (fn (items n)\n (when (> n 100)\n (yield) ;; delimited continuation — suspends, resumes next frame\n (set! n 0))\n (when (not (empty? items))\n (render-item (first items))\n (render-chunk (rest items) (+ n 1)))))") (p :class "text-stone-600" "This is cooperative concurrency without threads, without promises, without requestAnimationFrame callbacks. The evaluator's trampoline loop already has the right shape — it just needs to be able to park a thunk and resume it later instead of immediately.")) (~doc-section :title "Undo as continuation" :id "undo" (p :class "text-stone-600" "If you capture a continuation before a state mutation, the continuation IS the undo operation. Invoking it restores the computation to exactly the state it was in before the mutation happened.") (~doc-code :lang "lisp" :code "(define with-undo\n (fn (action)\n (let ((restore (call/cc (fn (k) k))))\n (action)\n restore)))\n\n;; Usage:\n(let ((undo (with-undo (fn () (delete-item 42)))))\n ;; later...\n (undo \"anything\")) ;; item 42 is back") (p :class "text-stone-600" "No command pattern, no reverse operations, no state snapshots. The continuation captures the entire computation state. This is the most elegant undo mechanism possible — and the most expensive in memory, which is the trade-off.")) (~doc-section :title "Implementation" :id "implementation" (p :class "text-stone-600" "SX already has the foundation. The TCO trampoline returns thunks from tail positions — a continuation is a thunk that can be stored and resumed later instead of being immediately trampolined.") (p :class "text-stone-600" "The minimal implementation: delimited continuations via shift/reset. These are strictly less powerful than full call/cc but cover the practical use cases (suspense, cooperative scheduling, linear async flows) without the footguns (capturing continuations across async boundaries, re-entering completed computations).") (p :class "text-stone-600" "Full call/cc is also possible. The evaluator is already continuation-passing-style-adjacent — the thunk IS a continuation, just one that's always immediately invoked. Making it first-class means letting user code hold a reference to it.") (p :class "text-stone-600" "The key insight: having the primitive available doesn't make the evaluator harder to reason about. Only code that calls call/cc pays the complexity cost. Components that don't use continuations behave exactly as they do today.") (p :class "text-stone-600" "In fact, continuations can be easier to reason about than the hacks people build to avoid them. Without call/cc, you get callback pyramids, state machines with explicit transition tables, command pattern undo stacks, Promise chains, manual CPS transforms, and framework-specific hooks like React's useEffect/useSuspense/useTransition. Each is a partial, ad-hoc reinvention of continuations — with its own rules, edge cases, and leaky abstractions.") (p :class "text-stone-600" "A wizard form built with continuations is a straight-line let* binding. The same wizard built without them is a state machine with a current-step variable, a data accumulator, forward/backward transition logic, and a render function that switches on step number. The continuation version has fewer moving parts. It is more declarative. It is easier to read.") (p :class "text-stone-600" "The complexity doesn't disappear when you remove continuations from a language. It moves into user code, where it's harder to get right and harder to compose.")) (~doc-section :title "What this means for SX" :id "meaning" (p :class "text-stone-600" "SX started as a rendering language. TCO made it capable of arbitrary recursion. Macros made it extensible. Continuations would make it a full computational substrate — a language where control flow itself is a first-class value.") (p :class "text-stone-600" "The practical benefits are real: streaming server rendering, linear client-side interaction flows, cooperative scheduling, and elegant undo. These aren't theoretical — they're patterns that React, Clojure, and Scheme have proven work.") (p :class "text-stone-600" "The evaluator is already 90% of the way there. The remaining 10% unlocks an entirely new class of UI patterns — and eliminates an entire class of workarounds.")))) diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 635d66f..eae4de0 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -71,8 +71,8 @@ :summary "How SX implements proper tail calls via trampolining in a language that doesn't have them.") (dict :label "Continuations" :href "/essays/continuations" :summary "First-class continuations in a tree-walking evaluator — theory and implementation.") - (dict :label "Godel, Escher, Bach" :href "/essays/godel-escher-bach" - :summary "Self-reference, strange loops, and what a self-hosting language has to do with GEB.") + (dict :label "Strange Loops" :href "/essays/godel-escher-bach" + :summary "Self-reference, and the tangled hierarchy of a language that defines itself.") (dict :label "The Reflexive Web" :href "/essays/reflexive-web" :summary "A web where pages can inspect, modify, and extend their own rendering pipeline.") (dict :label "sx sucks" :href "/essays/sx-sucks" diff --git a/sx/sx/reference.sx b/sx/sx/reference.sx index 9bcd7d2..8637e07 100644 --- a/sx/sx/reference.sx +++ b/sx/sx/reference.sx @@ -1,4 +1,5 @@ ;; Reference page layouts — receive data from Python primitives +;; @css bg-blue-100 text-blue-700 bg-emerald-100 text-emerald-700 bg-amber-100 text-amber-700 (defcomp ~reference-attrs-content (&key req-table beh-table uniq-table) (~doc-page :title "Attribute Reference" @@ -45,6 +46,40 @@ "Trigger the demo to see the raw response the server sends.") (~doc-placeholder :id wire-placeholder-id))))) +(defcomp ~reference-header-detail-content (&key title direction description + example-code demo) + (~doc-page :title title + (let ((badge-class (if (= direction "request") + "bg-blue-100 text-blue-700" + (if (= direction "response") + "bg-emerald-100 text-emerald-700" + "bg-amber-100 text-amber-700"))) + (badge-label (if (= direction "request") "Request Header" + (if (= direction "response") "Response Header" + "Request & Response")))) + (div :class "flex items-center gap-3 mb-4" + (span :class (str "text-xs font-medium px-2 py-1 rounded " badge-class) + badge-label))) + (p :class "text-stone-600 mb-6" description) + (when demo + (~example-card :title "Demo" + (~example-demo demo))) + (when example-code + (<> + (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage") + (~example-source :code (highlight example-code "lisp")))))) + +(defcomp ~reference-event-detail-content (&key title description example-code demo) + (~doc-page :title title + (p :class "text-stone-600 mb-6" description) + (when demo + (~example-card :title "Demo" + (~example-demo demo))) + (when example-code + (<> + (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Example usage") + (~example-source :code (highlight example-code "lisp")))))) + (defcomp ~reference-attr-not-found (&key slug) (~doc-page :title "Not Found" (p :class "text-stone-600" diff --git a/sx/sx/specs.sx b/sx/sx/specs.sx index 1a10a5a..08d02dc 100644 --- a/sx/sx/specs.sx +++ b/sx/sx/specs.sx @@ -293,8 +293,8 @@ boot.sx depends on: cssx, orchestration, adapter-dom, render"))) " spec files, runs the Python bootstrapper, and displays both the compiler source and its generated JavaScript output. " "The generated code below is live — it was produced by the bootstrapper at page load time, not served from a static file.") (p :class "text-xs text-stone-400 italic" - "The bootstrapped JavaScript is also injected as a script tag on this page. " - "Open the browser console to verify it loaded.")) + "The sx-browser.js powering this page IS the bootstrapped output. " + "This page re-runs the bootstrapper to display the source and result.")) (div :class "space-y-3" (div :class "flex items-baseline gap-3" @@ -318,8 +318,7 @@ boot.sx depends on: cssx, orchestration, adapter-dom, render"))) "It is a complete, self-contained SX runtime — parser, evaluator, DOM adapter, engine, and CSS system.") (div :class "bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-violet-300" (pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words" - (code (highlight bootstrapped-output "javascript")))) - (script :type "text/javascript" bootstrapped-output))))) + (code (highlight bootstrapped-output "javascript")))))))) ;; --------------------------------------------------------------------------- ;; Not found diff --git a/sx/sxc/home.sx b/sx/sxc/home.sx index 8a64317..bd967e9 100644 --- a/sx/sxc/home.sx +++ b/sx/sxc/home.sx @@ -7,7 +7,7 @@ (p :class "text-2xl text-stone-600 mb-4" "s-expressions for the web") (p :class "text-sm text-stone-400" - "\u00a9 Giles Bradshaw 2026") + "© Giles Bradshaw 2026") (p :class "text-lg text-stone-500 max-w-2xl mx-auto mb-12" "A hypermedia-driven UI engine that combines htmx's server-first philosophy " "with React's component model. S-expressions over the wire — no HTML, no JavaScript frameworks.") diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index f6f5a91..c388b3e 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -115,6 +115,43 @@ :handler-code attr-handler :wire-placeholder-id attr-wire-id))) +(defpage reference-header-detail + :path "/reference/headers/" + :auth :public + :layout (:sx-section + :section "Reference" + :sub-label "Reference" + :sub-href "/reference/" + :sub-nav (~section-nav :items reference-nav-items :current "Headers") + :selected "Headers") + :data (header-detail-data slug) + :content (if header-not-found + (~reference-attr-not-found :slug slug) + (~reference-header-detail-content + :title header-title + :direction header-direction + :description header-description + :example-code header-example + :demo header-demo))) + +(defpage reference-event-detail + :path "/reference/events/" + :auth :public + :layout (:sx-section + :section "Reference" + :sub-label "Reference" + :sub-href "/reference/" + :sub-nav (~section-nav :items reference-nav-items :current "Events") + :selected "Events") + :data (event-detail-data slug) + :content (if event-not-found + (~reference-attr-not-found :slug slug) + (~reference-event-detail-content + :title event-title + :description event-description + :example-code event-example + :demo event-demo))) + ;; --------------------------------------------------------------------------- ;; Protocols section ;; --------------------------------------------------------------------------- diff --git a/sx/sxc/pages/helpers.py b/sx/sxc/pages/helpers.py index 68000c0..51bc168 100644 --- a/sx/sxc/pages/helpers.py +++ b/sx/sxc/pages/helpers.py @@ -16,6 +16,8 @@ def _register_sx_helpers() -> None: "primitives-data": _primitives_data, "reference-data": _reference_data, "attr-detail-data": _attr_detail_data, + "header-detail-data": _header_detail_data, + "event-detail-data": _event_detail_data, "read-spec-file": _read_spec_file, "bootstrapper-data": _bootstrapper_data, }) @@ -39,7 +41,7 @@ def _reference_data(slug: str) -> dict: from content.pages import ( REQUEST_ATTRS, BEHAVIOR_ATTRS, SX_UNIQUE_ATTRS, REQUEST_HEADERS, RESPONSE_HEADERS, - EVENTS, JS_API, ATTR_DETAILS, + EVENTS, JS_API, ATTR_DETAILS, HEADER_DETAILS, ) if slug == "attributes": @@ -63,18 +65,22 @@ def _reference_data(slug: str) -> dict: elif slug == "headers": return { "req-headers": [ - {"name": n, "value": v, "desc": d} + {"name": n, "value": v, "desc": d, + "href": f"/reference/headers/{n}" if n in HEADER_DETAILS else None} for n, v, d in REQUEST_HEADERS ], "resp-headers": [ - {"name": n, "value": v, "desc": d} + {"name": n, "value": v, "desc": d, + "href": f"/reference/headers/{n}" if n in HEADER_DETAILS else None} for n, v, d in RESPONSE_HEADERS ], } elif slug == "events": + from content.pages import EVENT_DETAILS return { "events-list": [ - {"name": n, "desc": d} + {"name": n, "desc": d, + "href": f"/reference/events/{n}" if n in EVENT_DETAILS else None} for n, d in EVENTS ], } @@ -190,3 +196,42 @@ def _attr_detail_data(slug: str) -> dict: "attr-demo": SxExpr(f"(~{demo_name})") if demo_name else None, "attr-wire-id": wire_id, } + + +def _header_detail_data(slug: str) -> dict: + """Return header detail data for a specific header slug.""" + from content.pages import HEADER_DETAILS + from shared.sx.helpers import SxExpr + + detail = HEADER_DETAILS.get(slug) + if not detail: + return {"header-not-found": True} + + demo_name = detail.get("demo") + return { + "header-not-found": None, + "header-title": slug, + "header-direction": detail["direction"], + "header-description": detail["description"], + "header-example": detail.get("example"), + "header-demo": SxExpr(f"(~{demo_name})") if demo_name else None, + } + + +def _event_detail_data(slug: str) -> dict: + """Return event detail data for a specific event slug.""" + from content.pages import EVENT_DETAILS + from shared.sx.helpers import SxExpr + + detail = EVENT_DETAILS.get(slug) + if not detail: + return {"event-not-found": True} + + demo_name = detail.get("demo") + return { + "event-not-found": None, + "event-title": slug, + "event-description": detail["description"], + "event-example": detail.get("example"), + "event-demo": SxExpr(f"(~{demo_name})") if demo_name else None, + } diff --git a/sx/sxc/reference.sx b/sx/sxc/reference.sx index bf36f65..d207a5c 100644 --- a/sx/sxc/reference.sx +++ b/sx/sxc/reference.sx @@ -643,3 +643,151 @@ "Connecting to SSE stream...")) (p :class "text-xs text-stone-400" "Server pushes time updates every 2 seconds via Server-Sent Events."))) + +;; =========================================================================== +;; Header detail demos +;; =========================================================================== + +;; --------------------------------------------------------------------------- +;; SX-Prompt header demo +;; --------------------------------------------------------------------------- + +(defcomp ~ref-header-prompt-demo () + (div :class "space-y-3" + (button + :sx-get "/reference/api/prompt-echo" + :sx-target "#ref-hdr-prompt-result" + :sx-swap "innerHTML" + :sx-prompt "Enter your name:" + :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" + "Prompt & send") + (div :id "ref-hdr-prompt-result" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" + "Click to enter a name via prompt — the value is sent as the SX-Prompt header."))) + +;; --------------------------------------------------------------------------- +;; SX-Trigger response header demo +;; --------------------------------------------------------------------------- + +(defcomp ~ref-header-trigger-demo () + (div :class "space-y-3" + (button + :sx-get "/reference/api/trigger-event" + :sx-target "#ref-hdr-trigger-result" + :sx-swap "innerHTML" + :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" + "Load with trigger") + (div :id "ref-hdr-trigger-result" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" + :sx-on:showNotice "this.style.borderColor = '#8b5cf6'; this.style.borderWidth = '2px'" + "Click — the server response includes SX-Trigger: showNotice, which highlights this box."))) + +;; --------------------------------------------------------------------------- +;; SX-Retarget response header demo +;; --------------------------------------------------------------------------- + +(defcomp ~ref-header-retarget-demo () + (div :class "space-y-3" + (button + :sx-get "/reference/api/retarget" + :sx-target "#ref-hdr-retarget-main" + :sx-swap "innerHTML" + :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" + "Load (server retargets)") + (div :class "grid grid-cols-2 gap-3" + (div :class "rounded border border-stone-200 p-3" + (div :class "text-xs text-stone-400 mb-1" "Original target") + (div :id "ref-hdr-retarget-main" :class "text-sm text-stone-500" "Waiting...")) + (div :class "rounded border border-stone-200 p-3" + (div :class "text-xs text-stone-400 mb-1" "Retarget destination") + (div :id "ref-hdr-retarget-alt" :class "text-sm text-stone-500" "Waiting..."))))) + +;; =========================================================================== +;; Event detail demos +;; =========================================================================== + +;; --------------------------------------------------------------------------- +;; sx:beforeRequest event demo +;; --------------------------------------------------------------------------- + +(defcomp ~ref-event-before-request-demo () + (div :class "space-y-3" + (div :class "flex gap-2 items-center" + (input :id "ref-evt-br-input" :type "text" :placeholder "Type something first..." + :class "flex-1 px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500") + (button + :sx-get "/reference/api/time" + :sx-target "#ref-evt-br-result" + :sx-swap "innerHTML" + :sx-on:sx:beforeRequest "if (!document.getElementById('ref-evt-br-input').value) { event.preventDefault(); document.getElementById('ref-evt-br-result').textContent = 'Cancelled — input is empty!'; }" + :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" + "Load")) + (div :id "ref-evt-br-result" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" + "Request is cancelled via preventDefault() if the input is empty."))) + +;; --------------------------------------------------------------------------- +;; sx:afterSettle event demo +;; --------------------------------------------------------------------------- + +(defcomp ~ref-event-after-settle-demo () + (div :class "space-y-3" + (button + :sx-get "/reference/api/swap-item" + :sx-target "#ref-evt-settle-list" + :sx-swap "beforeend" + :sx-on:sx:afterSettle "var items = document.querySelectorAll('#ref-evt-settle-list > div'); if (items.length) items[items.length-1].scrollIntoView({behavior:'smooth'})" + :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" + "Add item (scrolls after settle)") + (div :id "ref-evt-settle-list" + :class "p-3 rounded border border-stone-200 space-y-1 max-h-32 overflow-y-auto" + (div :class "text-sm text-stone-500" "Items will be appended and scrolled into view.")))) + +;; --------------------------------------------------------------------------- +;; sx:responseError event demo +;; --------------------------------------------------------------------------- + +(defcomp ~ref-event-response-error-demo () + (div :class "space-y-3" + (button + :sx-get "/reference/api/error-500" + :sx-target "#ref-evt-err-result" + :sx-swap "innerHTML" + :sx-on:sx:responseError "var s=document.getElementById('ref-evt-err-status'); s.style.display='block'; s.textContent='Error ' + (event.detail ? event.detail.status || '?' : '?') + ' received'" + :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" + "Call failing endpoint") + (div :id "ref-evt-err-status" + :class "p-2 rounded bg-red-50 text-red-600 text-sm" + :style "display: none" + "") + (div :id "ref-evt-err-result" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" + "Click to trigger an error — the sx:responseError event fires."))) + +;; --------------------------------------------------------------------------- +;; sx:validationFailed event demo +;; --------------------------------------------------------------------------- +;; @css invalid:border-red-400 + +(defcomp ~ref-event-validation-failed-demo () + (div :class "space-y-3" + (form + :sx-post "/reference/api/greet" + :sx-target "#ref-evt-vf-result" + :sx-swap "innerHTML" + :sx-validate "true" + :sx-on:sx:validationFailed "document.getElementById('ref-evt-vf-status').style.display = 'block'" + :class "flex gap-2" + (input :type "email" :name "email" :required "true" + :placeholder "Email (required)" + :class "flex-1 px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500 invalid:border-red-400") + (button :type "submit" + :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" + "Submit")) + (div :id "ref-evt-vf-status" + :class "p-2 rounded bg-amber-50 text-amber-700 text-sm" + :style "display: none" + "Validation failed — form was not submitted.") + (div :id "ref-evt-vf-result" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" + "Submit with empty/invalid email to trigger the event.")))