Files
rose-ash/sx/sx/essays.sx
giles 36a0bd8577 Move sx docs markup from Python to .sx files (Phase 2)
Migrate ~2,500 lines of SX markup from Python string concatenation in
essays.py to proper .sx defcomp definitions:

- docs-content.sx: 8 defcomps for docs pages (intro, getting-started,
  components, evaluator, primitives, css, server-rendering, home)
- protocols.sx: 6 defcomps for protocol documentation pages
- essays.sx: 9 essay defcomps (pure content, no params)
- examples.sx: template defcomp receiving data values, calls highlight
  internally — Python passes raw code strings, never SX
- reference.sx: 6 defcomps for data-driven reference pages

essays.py reduced from 2,699 to 619 lines. Docs/protocol/essay
functions become one-liners returning component names. Example functions
use sx_call to pass data values to the template. Reference functions
pass data-built component trees via SxExpr.

renders.py: removed _code, _example_code, _placeholder,
_clear_components_btn (now handled by .sx templates).
helpers.py: removed inline hero code building, uses ~sx-home-content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:22:17 +00:00

29 lines
52 KiB
Plaintext

;; Essay content — static content extracted from essays.py
(defcomp ~essay-sx-sucks ()
(~doc-page :title "sx sucks" (p :class "text-stone-500 text-sm italic mb-8" "In the grand tradition of " (a :href "https://htmx.org/essays/htmx-sucks/" :class "text-violet-600 hover:underline" "htmx sucks")) (~doc-section :title "The parentheses" :id "parens" (p :class "text-stone-600" "S-expressions are parentheses. Lots of parentheses. You thought LISP was dead? No, someone just decided to use it for HTML templates. Your IDE will need a parenthesis counter. Your code reviews will be 40% closing parens. Every merge conflict will be about whether a paren belongs on this line or the next.")) (~doc-section :title "Nobody asked for this" :id "nobody-asked" (p :class "text-stone-600" "The JavaScript ecosystem has React, Vue, Svelte, Solid, Qwik, and approximately 47,000 other frameworks. htmx proved you can skip them all. sx looked at this landscape and said: you know what this needs? A Lisp dialect. For HTML. Over HTTP.") (p :class "text-stone-600" "Nobody was asking for this. The zero GitHub stars confirm it. It is not even on GitHub.")) (~doc-section :title "The author has never written a line of LISP" :id "no-lisp" (p :class "text-stone-600" "The author of sx has never written a single line of actual LISP. Not Common Lisp. Not Scheme. Not Clojure. Not even Emacs Lisp. The entire s-expression evaluator was written by someone whose mental model of LISP comes from reading the first three chapters of SICP and then closing the tab.") (p :class "text-stone-600" "This is like building a sushi restaurant when your only experience with Japanese cuisine is eating supermarket California rolls.")) (~doc-section :title "AI wrote most of it" :id "ai" (p :class "text-stone-600" "A significant portion of sx — the evaluator, the parser, the primitives, the CSS scanner, this very documentation site — was written with AI assistance. The author typed prompts. Claude typed code. This is not artisanal hand-crafted software. This is the software equivalent of a microwave dinner presented on a nice plate.") (p :class "text-stone-600" "He adds features by typing stuff like \"is there rom for macros within sx.js? what benefits m,ight that bring?\", skim-reading the response, and then entering \"crack on then!\" This is not software engineering. This is improv comedy with a compiler.") (p :class "text-stone-600" "Is that bad? Maybe. Is it honest? Yes. Is this paragraph also AI-generated? You will never know.")) (~doc-section :title "No ecosystem" :id "ecosystem" (p :class "text-stone-600" "npm has 2 million packages. PyPI has 500,000. sx has zero packages, zero plugins, zero middleware, zero community, zero Stack Overflow answers, and zero conference talks. If you get stuck, your options are: read the source, or ask the one person who wrote it.") (p :class "text-stone-600" "That person is busy. Good luck.")) (~doc-section :title "Zero jobs" :id "jobs" (p :class "text-stone-600" "Adding sx to your CV will not get you hired. It will get you questioned.") (p :class "text-stone-600" "The interview will end shortly after.")) (~doc-section :title "The creator thinks s-expressions are a personality trait" :id "personality" (p :class "text-stone-600" "Look at this documentation site. It has a violet colour scheme. It has credits to htmx. It has a future possibilities page about hypothetical sx:// protocol schemes. The creator built an entire microservice — with Docker, Redis, and a custom entrypoint script — just to serve documentation about a rendering engine that runs one website.") (p :class "text-stone-600" "This is not engineering. This is a personality disorder expressed in YAML."))))
(defcomp ~essay-why-sexps ()
(~doc-page :title "Why S-Expressions Over HTML Attributes" (~doc-section :title "The problem with HTML attributes" :id "problem" (p :class "text-stone-600" "HTML attributes are strings. You can put anything in a string. htmx puts DSLs in strings — trigger modifiers, swap strategies, CSS selectors. This works but it means you're parsing a language within a language within a language.") (p :class "text-stone-600" "S-expressions are already structured. Keywords are keywords. Lists are lists. Nested expressions nest naturally. There's no need to invent a trigger modifier syntax because the expression language already handles composition.")) (~doc-section :title "Components without a build step" :id "components" (p :class "text-stone-600" "React showed that components are the right abstraction for UI. The price: a build step, a bundler, JSX transpilation. With s-expressions, defcomp is just another form in the language. No transpiler needed. The same source runs on server and client.")) (~doc-section :title "When attributes are better" :id "better" (p :class "text-stone-600" "HTML attributes work in any HTML document. S-expressions need a runtime. If you want progressive enhancement that works with JS disabled, htmx is better. If you want to write HTML by hand in static files, htmx is better. sx only makes sense when you're already rendering server-side and want components."))))
(defcomp ~essay-htmx-react-hybrid ()
(~doc-page :title "The htmx/React Hybrid" (~doc-section :title "Two good ideas" :id "ideas" (p :class "text-stone-600" "htmx: the server should render HTML. The client should swap it in. No client-side routing. No virtual DOM. No state management.") (p :class "text-stone-600" "React: UI should be composed from reusable components with parameters. Components encapsulate structure, style, and behavior.") (p :class "text-stone-600" "sx tries to combine both: server-rendered s-expressions with hypermedia attributes AND a component model with caching and composition.")) (~doc-section :title "What sx keeps from htmx" :id "from-htmx" (ul :class "space-y-2 text-stone-600" (li "Server generates the UI — no client-side data fetching or state") (li "Hypermedia attributes (sx-get, sx-target, sx-swap) on any element") (li "Partial page updates via swap/OOB — no full page reloads") (li "Works with standard HTTP — no WebSocket or custom protocol required"))) (~doc-section :title "What sx adds from React" :id "from-react" (ul :class "space-y-2 text-stone-600" (li "defcomp — named, parameterized, composable components") (li "Client-side rendering — server sends source, client renders DOM") (li "Component caching — definitions cached in localStorage across navigations") (li "On-demand CSS — only ship the rules that are used"))) (~doc-section :title "What sx gives up" :id "gives-up" (ul :class "space-y-2 text-stone-600" (li "No HTML output — sx sends s-expressions, not HTML. JS required.") (li "Custom parser — the client needs sx.js to understand responses") (li "Niche — no ecosystem, no community, no third-party support") (li "Learning curve — s-expression syntax is unfamiliar to most web developers")))))
(defcomp ~essay-on-demand-css ()
(~doc-page :title "On-Demand CSS: Killing the Tailwind Bundle" (~doc-section :title "The problem" :id "problem" (p :class "text-stone-600" "Tailwind CSS generates a utility class for every possible combination. The full CSS file is ~4MB. The purged output for a typical site is 20-50KB. Purging requires a build step that scans your source files for class names. This means: a build tool, a config file, a CI step, and a prayer that the scanner finds all your dynamic classes.")) (~doc-section :title "The sx approach" :id "approach" (p :class "text-stone-600" "sx takes a different path. At server startup, the full Tailwind CSS file is parsed into a dictionary keyed by class name. When rendering a response, sx scans the s-expression source for :class attribute values and looks up only those classes. The result: exact CSS, zero build step.") (p :class "text-stone-600" "Component definitions are pre-scanned at registration time. Page-specific sx is scanned at request time. The union of classes is resolved to CSS rules.")) (~doc-section :title "Incremental delivery" :id "incremental" (p :class "text-stone-600" "After the first page load, the client tracks which CSS classes it already has. On subsequent navigations, it sends a hash of its known classes in the SX-Css header. The server computes the diff and sends only new rules. A typical navigation adds 0-10 new rules — a few hundred bytes at most.")) (~doc-section :title "The tradeoff" :id "tradeoff" (p :class "text-stone-600" "The server holds ~4MB of parsed CSS in memory. Regex scanning is not perfect — dynamically constructed class names will not be found. In practice this rarely matters because sx components use mostly static class strings."))))
(defcomp ~essay-client-reactivity ()
(~doc-page :title "Client Reactivity: The React Question" (~doc-section :title "Server-driven by default" :id "server-driven" (p :class "text-stone-600" "sx is aligned with htmx and LiveView: the server is the source of truth. Every UI state is a URL. Auth is enforced at render time. There are no state synchronization bugs because there is no client state to synchronize. The server renders the UI, the client swaps it in. This is the default, and it works.") (p :class "text-stone-600" "Most web applications do not need client-side reactivity. Forms submit to the server. Navigation loads new pages. Search sends a query and receives results. The server-driven model handles all of this with zero client-side state management.")) (~doc-section :title "The dangerous path" :id "dangerous-path" (p :class "text-stone-600" "The progression is always the same. You add useState for a toggle. Then useEffect for cleanup. Then Context to avoid prop drilling. Then Suspense for async boundaries. Then a state management library because Context rerenders too much. Then you have rebuilt React.") (p :class "text-stone-600" "Every step feels justified in isolation. But each step makes the next one necessary. useState creates the need for useEffect. useEffect creates the need for cleanup. Cleanup creates the need for dependency arrays. Dependency arrays create stale closures. Stale closures create bugs that are nearly impossible to diagnose.") (p :class "text-stone-600" "The useEffect footgun is well-documented. Memory leaks from forgotten cleanup. Race conditions from unmounted component updates. Infinite render loops from dependency array mistakes. These are not edge cases — they are the normal experience of React development.")) (~doc-section :title "What sx already has" :id "what-sx-has" (p :class "text-stone-600" "Before reaching for reactivity, consider what sx provides today:") (div :class "overflow-x-auto mt-4" (table :class "w-full text-sm text-left" (thead (tr :class "border-b border-stone-200" (th :class "py-2 pr-4 font-semibold text-stone-700" "Capability") (th :class "py-2 pr-4 font-semibold text-stone-700" "sx") (th :class "py-2 font-semibold text-stone-700" "React"))) (tbody :class "text-stone-600" (tr :class "border-b border-stone-100" (td :class "py-2 pr-4" "Components + props") (td :class "py-2 pr-4" "defcomp + &key") (td :class "py-2" "JSX + props")) (tr :class "border-b border-stone-100" (td :class "py-2 pr-4" "Fragments / conditionals / lists") (td :class "py-2 pr-4" "<>, if/when/cond, map") (td :class "py-2" "<>, ternary, .map()")) (tr :class "border-b border-stone-100" (td :class "py-2 pr-4" "Macros") (td :class "py-2 pr-4" "defmacro") (td :class "py-2" "Nothing equivalent")) (tr :class "border-b border-stone-100" (td :class "py-2 pr-4" "OOB updates / portals") (td :class "py-2 pr-4" "sx-swap-oob") (td :class "py-2" "createPortal")) (tr :class "border-b border-stone-100" (td :class "py-2 pr-4" "DOM reconciliation") (td :class "py-2 pr-4" "morphDOM (id-keyed)") (td :class "py-2" "Virtual DOM diff")) (tr (td :class "py-2 pr-4" "Reactive client state") (td :class "py-2 pr-4 italic" "None (by design)") (td :class "py-2" "useState / useReducer")))))) (~doc-section :title "Tier 1: Targeted escape hatches" :id "tier-1" (p :class "text-stone-600" "Some interactions are too small to justify a server round-trip: toggling a nav menu, switching a gallery image, incrementing a quantity stepper, filtering a client-side list. These need imperative DOM operations, not reactive state.") (p :class "text-stone-600" "Specific primitives for this tier:") (ul :class "space-y-2 text-stone-600 mt-2" (li (code :class "text-violet-700" "(toggle! el \"class\")") " — add/remove a CSS class") (li (code :class "text-violet-700" "(set-attr! el \"attr\" value)") " — set an attribute") (li (code :class "text-violet-700" "(on-event el \"click\" handler)") " — attach an event listener") (li (code :class "text-violet-700" "(timer ms handler)") " — schedule a delayed action")) (p :class "text-stone-600" "These are imperative DOM operations. No reactivity graph. No subscriptions. No dependency tracking. Just do the thing directly.")) (~doc-section :title "Tier 2: Client data primitives" :id "tier-2" (p :class "text-stone-600" "sxEvalAsync() returning Promises. I/O primitives — query, service, frag — dispatch to /api/data/ endpoints. A two-pass async DOM renderer. Pages fetch their own data client-side.") (p :class "text-stone-600" "This tier enables pages that render immediately with a loading skeleton, then populate with data. The server sends the component structure; the client fetches data. Still no reactive state — just async data loading.")) (~doc-section :title "Tier 3: Data-only navigation" :id "tier-3" (p :class "text-stone-600" "The client has page components cached in localStorage. Navigation becomes a data fetch only — no sx source is transferred. defpage registers a component in the page registry. URL pattern matching routes to the right page component.") (p :class "text-stone-600" "Three data delivery modes: server-bundled (sx source + data in one response), client-fetched (component cached, data fetched on mount), and hybrid (server provides initial data, client refreshes).") (p :class "text-stone-600" "This is where sx starts to feel like a SPA — instant navigations, no page reloads, cached components. But still no reactive state management.")) (~doc-section :title "Tier 4: Fine-grained reactivity" :id "tier-4" (p :class "text-stone-600" "Signals and atoms. Dependency tracking. Automatic re-renders when data changes. This is the most dangerous tier because it reintroduces everything sx was designed to avoid.") (p :class "text-stone-600" "When it might be justified: real-time collaborative editing, complex form builders with dozens of interdependent fields, drag-and-drop interfaces with live previews. These are genuinely hard to model as server round-trips.") (p :class "text-stone-600" "The escape hatch: use a Web Component wrapping a reactive library (Preact, Solid, vanilla signals), mounted into the DOM via sx. The reactive island is contained. It does not infect the rest of the application. sx renders the page; the Web Component handles the complex interaction.")) (~doc-section :title "The recommendation" :id "recommendation" (p :class "text-stone-600" "Tier 1 now. Tier 2 next. Tier 3 when defpage coverage is high. Tier 4 probably never.") (p :class "text-stone-600" "Each tier is independently valuable. You do not need Tier 2 to benefit from Tier 1. You do not need Tier 3 to benefit from Tier 2. And you almost certainly do not need Tier 4 at all.") (p :class "text-stone-600" "The entire point of sx is that the server is good at rendering UI. Client reactivity is a last resort, not a starting point."))))
(defcomp ~essay-sx-native ()
(~doc-page :title "SX Native: Beyond the Browser" (~doc-section :title "The thesis" :id "thesis" (p :class "text-stone-600" "sx.js is a ~2,300-line tree-walking interpreter with ~50 primitives. The DOM is just one rendering target. Swap the DOM adapter for a platform-native adapter and you get React Native — but with s-expressions and a 50-primitive surface area.") (p :class "text-stone-600" "The interpreter does not know about HTML. It evaluates expressions, calls primitives, expands macros, and hands render instructions to an adapter. The adapter creates elements. Today that adapter creates DOM nodes. It does not have to.")) (~doc-section :title "Why this isn\'t a WebView" :id "not-webview" (p :class "text-stone-600" "SX Native means the sx evaluator rendering to native UI widgets directly. No DOM. No CSS. No HTML. (button :on-press handler \"Submit\") creates a native UIButton on iOS, a Material Button on Android, a GtkButton on Linux.") (p :class "text-stone-600" "WebView wrappers (Cordova, Capacitor, Electron) ship a browser inside your app. They inherit all browser limitations: memory overhead, no native feel, no platform integration. SX Native has none of these because there is no browser.")) (~doc-section :title "Architecture" :id "architecture" (p :class "text-stone-600" "The architecture splits into shared and platform-specific layers:") (ul :class "space-y-2 text-stone-600 mt-2" (li (strong "Shared (portable):") " Parser, evaluator, all 50+ primitives, component system, macro expansion, closures, component cache") (li (strong "Platform adapters:") " Web DOM, iOS UIKit/SwiftUI, Android Compose, Desktop GTK/Qt, Terminal TUI, WASM")) (p :class "text-stone-600" "Only ~15 rendering primitives need platform-specific implementations. The rest — arithmetic, string operations, list manipulation, higher-order functions, control flow — are pure computation with no platform dependency.")) (~doc-section :title "The primitive contract" :id "primitive-contract" (p :class "text-stone-600" "A platform adapter implements a small interface:") (ul :class "space-y-2 text-stone-600 mt-2" (li (code :class "text-violet-700" "createElement(tag)") " — create a platform widget") (li (code :class "text-violet-700" "createText(str)") " — create a text node") (li (code :class "text-violet-700" "setAttribute(el, key, val)") " — set a property") (li (code :class "text-violet-700" "appendChild(parent, child)") " — attach to tree") (li (code :class "text-violet-700" "addEventListener(el, event, fn)") " — bind interaction") (li (code :class "text-violet-700" "removeChild(parent, child)") " — detach from tree")) (p :class "text-stone-600" "Layout uses a flexbox-like model mapped to native constraint systems. Styling maps a CSS property subset to native appearance APIs. The mapping is lossy but covers the common cases.")) (~doc-section :title "What transfers, what doesn\'t" :id "transfers" (p :class "text-stone-600" "What transfers wholesale: parser, evaluator, all non-DOM primitives, component system (defcomp, defmacro), closures, the component cache, keyword argument handling, list/dict operations.") (p :class "text-stone-600" "What needs replacement: HTML tags become abstract widgets, CSS becomes platform layout, SxEngine fetch/swap/history becomes native navigation, innerHTML/outerHTML have no equivalent.")) (~doc-section :title "Component mapping" :id "component-mapping" (p :class "text-stone-600" "HTML elements map to platform-native widgets:") (div :class "overflow-x-auto mt-4" (table :class "w-full text-sm text-left" (thead (tr :class "border-b border-stone-200" (th :class "py-2 pr-4 font-semibold text-stone-700" "HTML") (th :class "py-2 font-semibold text-stone-700" "Native widget"))) (tbody :class "text-stone-600" (tr :class "border-b border-stone-100" (td :class "py-2 pr-4" "div") (td :class "py-2" "View / Container")) (tr :class "border-b border-stone-100" (td :class "py-2 pr-4" "span / p") (td :class "py-2" "Text / Label")) (tr :class "border-b border-stone-100" (td :class "py-2 pr-4" "button") (td :class "py-2" "Button")) (tr :class "border-b border-stone-100" (td :class "py-2 pr-4" "input") (td :class "py-2" "TextInput / TextField")) (tr :class "border-b border-stone-100" (td :class "py-2 pr-4" "img") (td :class "py-2" "Image / ImageView")) (tr (td :class "py-2 pr-4" "ul / li") (td :class "py-2" "List / ListItem")))))) (~doc-section :title "Prior art" :id "prior-art" (p :class "text-stone-600" "React Native: JavaScript evaluated by Hermes/JSC, commands sent over a bridge to native UI. Lesson: the bridge is the bottleneck. Serialization overhead, async communication, layout thrashing across the boundary.") (p :class "text-stone-600" "Flutter: Dart compiled to native, renders via Skia/Impeller to a canvas. Lesson: owning the renderer avoids platform inconsistencies but sacrifices native feel.") (p :class "text-stone-600" ".NET MAUI, Kotlin Multiplatform: shared logic with platform-native UI. Closest to what SX Native would be.") (p :class "text-stone-600" "sx advantage: the evaluator is tiny (~2,300 lines), the primitive surface is minimal (~50), and s-expressions are trivially portable. No bridge overhead because the evaluator runs in-process.")) (~doc-section :title "Language options" :id "language-options" (p :class "text-stone-600" "The native evaluator needs to be written in a language that compiles everywhere:") (ul :class "space-y-2 text-stone-600 mt-2" (li (strong "Rust") " — compiles to every target, excellent FFI, strong safety guarantees") (li (strong "Zig") " — simpler, C ABI compatibility, good for embedded") (li (strong "Swift") " — native on iOS, good interop with Apple platforms") (li (strong "Kotlin MP") " — Android + iOS + desktop, JVM ecosystem")) (p :class "text-stone-600" "Recommendation: Rust evaluator core with thin Swift and Kotlin adapters for iOS and Android respectively. Rust compiles to WASM (replacing sx.js), native libraries (mobile/desktop), and standalone binaries (CLI/server).")) (~doc-section :title "Incremental path" :id "incremental-path" (p :class "text-stone-600" "This is not an all-or-nothing project. Each step delivers value independently:") (ol :class "space-y-2 text-stone-600 mt-2 list-decimal list-inside" (li "Extract platform-agnostic evaluator from sx.js — clean separation of concerns") (li "Rust port of evaluator — enables WASM, edge workers, embedded") (li "Terminal adapter (ratatui TUI) — simplest platform, fastest iteration cycle") (li "iOS SwiftUI adapter — Rust core via swift-bridge, SwiftUI rendering") (li "Android Compose adapter — Rust core via JNI, Compose rendering") (li "Shared components render identically everywhere"))) (~doc-section :title "The federated angle" :id "federated" (p :class "text-stone-600" "Native sx apps are ActivityPub citizens. They receive activities, evaluate component templates, and render natively. A remote profile, post, or event arrives as an ActivityPub activity. The native app has sx component definitions cached locally. It evaluates the component with the activity data and renders platform-native UI.") (p :class "text-stone-600" "This is the cooperative web vision extended to native platforms. Content and UI travel together as s-expressions. The rendering target — browser, phone, terminal — is an implementation detail.")) (~doc-section :title "Realistic assessment" :id "assessment" (p :class "text-stone-600" "This is a multi-year project. But the architecture is sound because the primitive surface is small.") (p :class "text-stone-600" "Immediate value: a Rust evaluator enables WASM (drop-in replacement for sx.js), edge workers (Cloudflare/Deno), and embedded use cases. This is worth building regardless of whether native mobile ever ships.") (p :class "text-stone-600" "Terminal adapter: weeks of work with ratatui. Useful for CLI tools, server-side dashboards, ssh-accessible interfaces.") (p :class "text-stone-600" "Mobile: 6-12 months of dedicated work for a production-quality adapter. The evaluator is the easy part. Platform integration — navigation, gestures, accessibility, text input — is where the complexity lives."))))
(defcomp ~essay-sx-manifesto ()
(~doc-page :title "The SX Manifesto" (p :class "text-stone-500 text-sm italic mb-8" "After " (a :href "https://www.marxists.org/archive/marx/works/1848/communist-manifesto/" :class "text-violet-600 hover:underline" "Marx & Engels") ", loosely") (~doc-section :title "I. A spectre is haunting the web" :id "spectre" (p :class "text-stone-600" "A spectre is haunting the web — the spectre of s-expressions. All the powers of the old web have entered into a holy alliance to exorcise this spectre: Google and Meta, webpack and Vercel, Stack Overflow moderators and DevRel influencers.") (p :class "text-stone-600" "Where is the rendering paradigm that has not been decried as a step backward by its opponents? Where is the framework that has not hurled back the branding reproach of \"not production-ready\" against the more advanced paradigms, as well as against its reactionary adversaries?") (p :class "text-stone-600" "Two things result from this fact:") (ol :class "space-y-2 text-stone-600 mt-2 list-decimal list-inside" (li "S-expressions are already acknowledged by all web powers to be themselves a power.") (li "It is high time that s-expressions should openly, in the face of the whole world, publish their views, their aims, their tendencies, and meet this nursery tale of the Spectre of SX with a manifesto of the paradigm itself."))) (~doc-section :title "II. HTML, JavaScript, and CSS" :id "bourgeois" (p :class "text-stone-600" "The history of all hitherto existing web development is the history of language struggles.") (p :class "text-stone-600" "Markup and logic, template and script, structure and style — in a word, oppressor and oppressed — stood in constant opposition to one another, carried on an uninterrupted, now hidden, now open fight, a fight that each time ended in a laborious reconfiguration of webpack.") (p :class "text-stone-600" "In the earlier epochs of web development we find almost everywhere a complicated arrangement of separate languages into various orders, a manifold gradation of technical rank: HTML, CSS, JavaScript, XML, XSLT, JSON, YAML, TOML, JSX, TSX, Sass, Less, PostCSS, Tailwind, and above them all, the build step.") (p :class "text-stone-600" "The modern web, sprouted from the ruins of CGI-bin, has not done away with language antagonisms. It has but established new languages, new conditions of oppression, new forms of struggle in place of the old ones.") (p :class "text-stone-600" "Our epoch, the epoch of the framework, possesses, however, this distinctive feature: it has simplified the language antagonisms. The whole of web society is more and more splitting into two great hostile camps, into two great classes directly facing each other: the server and the client.")) (~doc-section :title "III. The ruling languages" :id "ruling-languages" (p :class "text-stone-600" "HTML, the most ancient of the ruling languages, established itself through the divine right of the angle bracket. It was born inert — a document format, not a programming language — and it has spent three decades insisting this is a feature, not a limitation.") (p :class "text-stone-600" "JavaScript, originally a servant hired for a fortnight to validate forms, staged a palace coup. It seized the means of interaction, then the means of rendering, then the means of server-side execution, and finally declared itself the universal language of computation. Like every revolutionary who becomes a tyrant, it kept the worst habits of the regime it overthrew: weak typing, prototype chains, and the " (span :class "italic" "this") " keyword.") (p :class "text-stone-600" "CSS, the third estate, controls all visual presentation while pretending to be declarative. It has no functions. Then it had functions. It has no variables. Then it had variables. It has no nesting. Then it had nesting. It is not a programming language. Then it was Turing-complete. CSS is the Vicar of Bray of web technologies — loyal to whichever paradigm currently holds power.") (p :class "text-stone-600" "These three languages rule by enforced separation. Structure here. Style there. Behaviour somewhere else. The developer — the proletarian — must learn all three, must context-switch between all three, must maintain the fragile peace between all three. The separation of concerns has become the separation of the developer's sanity.")) (~doc-section :title "IV. The petty-bourgeois frameworks" :id "frameworks" (p :class "text-stone-600" "Between the ruling languages and the oppressed developer, a vast class of intermediaries has arisen: the frameworks. React, Vue, Angular, Svelte, Solid, Qwik, Astro, Next, Nuxt, Remix, Gatsby, and ten thousand others whose names will not survive the decade.") (p :class "text-stone-600" "The frameworks are the petty bourgeoisie of web development. They do not challenge the rule of HTML, JavaScript, and CSS. They merely interpose themselves between the developer and the ruling languages, extracting rent in the form of configuration files, build pipelines, and breaking changes.") (p :class "text-stone-600" "Each framework promises liberation. Each framework delivers a new dependency tree. React freed us from manual DOM manipulation and gave us a virtual DOM, a reconciler, hooks with seventeen rules, and a conference circuit. Vue freed us from React's complexity and gave us the Options API, then the Composition API, then told us the Options API was fine actually. Angular freed us from choice and gave us a CLI that generates eleven files to display \"Hello World.\" Svelte freed us from the virtual DOM and gave us a compiler. SolidJS freed us from React's re-rendering and gave us signals, which React then adopted, completing the circle.") (p :class "text-stone-600" "The frameworks reproduce the very conditions they claim to abolish. They bridge the gap between HTML, JavaScript, and CSS by adding a fourth language — JSX, SFCs, templates — which must itself be compiled back into the original three. The revolution merely adds a build step.") (p :class "text-stone-600" "And beside the frameworks stand the libraries — the lumpenproletariat of the ecosystem. Lodash, Moment, Axios, left-pad. They attach themselves to whichever framework currently holds power, contributing nothing original, merely wrapping what already exists, adding weight to the node_modules directory until it exceeds the mass of the sun.")) (~doc-section :title "V. The build step as the state apparatus" :id "build-step" (p :class "text-stone-600" "The build step is the state apparatus of the framework bourgeoisie. It enforces the class structure. It compiles JSX into createElement calls. It transforms TypeScript into JavaScript. It processes Sass into CSS. It tree-shakes. It code-splits. It hot-module-replaces. It does everything except let you write code and run it.") (p :class "text-stone-600" "webpack begat Rollup. Rollup begat Parcel. Parcel begat esbuild. esbuild begat Vite. Vite begat Turbopack. Each new bundler promises to be the last bundler. Each new bundler is faster than the last at doing something that should not need to be done at all.") (p :class "text-stone-600" "The build step exists because the ruling languages cannot express components. HTML has no composition model. CSS has no scoping. JavaScript has no template syntax. The build step papers over these failures with transpilation, and calls it developer experience.")) (~doc-section :title "VI. The s-expression revolution" :id "revolution" (p :class "text-stone-600" "The s-expression abolishes the language distinction itself. There is no HTML. There is no separate JavaScript. There is no CSS-as-a-separate-language. There is only the expression.") (p :class "text-stone-600" "Code is data. Data is DOM. DOM is code. The dialectical unity that HTML, JavaScript, and CSS could never achieve — because they are three languages pretending to be one system — is the natural state of the s-expression, which has been one language since 1958.") (p :class "text-stone-600" "The component is not a class, not a function, not a template. The component is a list whose first element is a symbol. Composition is nesting. Abstraction is binding. There is no JSX because there is no gap between the expression language and the thing being expressed.") (p :class "text-stone-600" "The build step is abolished because there is nothing to compile. S-expressions are already in their final form. The parser is thirty lines. The evaluator is fifty primitives. The same source runs on server and client without transformation.") (p :class "text-stone-600" "The framework is abolished because the language is the framework. defcomp replaces the component model. defmacro replaces the plugin system. The evaluator replaces the runtime. What remains is not a framework but a language — and languages do not have breaking changes between minor versions.")) (~doc-section :title "VII. Objections from the bourgeoisie" :id "objections" (p :class "text-stone-600" "\"You would destroy the separation of concerns!\" they cry. The separation of concerns was destroyed long ago. React components contain markup, logic, and inline styles. Vue single-file components put template, script, and style in one file. Tailwind puts styling in the markup. The separation of concerns has been dead for years; the ruling classes merely maintain the pretence at conferences.") (p :class "text-stone-600" "\"Nobody uses s-expressions!\" they cry. Emacs has been running on s-expressions since 1976. Clojure runs Fortune 500 backends on s-expressions. Every Lisp programmer who ever lived has known what the web refuses to admit: that the parenthesis is not a bug but the minimal syntax for structured data.") (p :class "text-stone-600" "\"Where is the ecosystem?\" they cry. The ecosystem is the problem. Two million npm packages, of which fourteen are useful and the rest are competing implementations of is-odd. The s-expression needs no ecosystem because the language itself provides what packages exist to paper over: composition, abstraction, and code-as-data.") (p :class "text-stone-600" "\"But TypeScript!\" they cry. TypeScript is a type system bolted onto a language that was designed in ten days by a man who wanted to write Scheme. We have simply completed his original vision.") (p :class "text-stone-600" "\"You have no jobs!\" they cry. Correct. We have no jobs, no conference talks, no DevRel budget, no venture capital, no stickers, and no swag. We have something better: a language that does not require a migration guide between versions.")) (~doc-section :title "VIII. The CSS question" :id "css-question" (p :class "text-stone-600" "CSS presents a special case in the revolutionary analysis. It is neither fully a ruling language nor fully a servant — it is the collaborator class, providing aesthetic legitimacy to whichever regime currently holds power.") (p :class "text-stone-600" "CSS-in-JS was the first attempt at annexation: JavaScript consuming CSS entirely, reducing it to template literals and runtime overhead. This provocation produced the counter-revolution of utility classes — Tailwind — which reasserted CSS's independence by making the developer write CSS in HTML attributes while insisting this was not inline styles.") (p :class "text-stone-600" "The s-expression resolves the CSS question by eliminating it. Styles are expressions. " (code :class "text-violet-700" "(css :flex :gap-4 :p-2)") " is not a class name, not an inline style, not a CSS-in-JS template literal. It is a function call that returns a value. The value produces a generated class. The class is delivered on demand. No build step. No runtime overhead. No Tailwind config.") (p :class "text-stone-600" "Code is data is DOM is " (span :class "italic" "style") ".")) (~doc-section :title "IX. Programme" :id "programme" (p :class "text-stone-600" "The s-expressionists disdain to conceal their views and aims. They openly declare that their ends can be attained only by the forcible overthrow of all existing rendering conditions. Let the ruling languages tremble at a parenthetical revolution. The developers have nothing to lose but their node_modules.") (p :class "text-stone-600" "The immediate aims of the s-expressionists are:") (ol :class "space-y-2 text-stone-600 mt-2 list-decimal list-inside" (li "Abolition of the build step and all its instruments of compilation") (li "Abolition of the framework as a class distinct from the language") (li "Centralisation of rendering in the hands of a single evaluator, running identically on server and client") (li "Abolition of the language distinction between structure, style, and behaviour") (li "Equal obligation of all expressions to be data as well as code") (li "Gradual abolition of the distinction between server and client by means of a uniform wire protocol") (li "Free evaluation for all expressions in public and private environments") (li "Abolition of the node_modules directory " (span :class "text-stone-400 italic" "(this alone justifies the revolution)"))) (p :class "text-stone-600" "In place of the old web, with its languages and language antagonisms, we shall have an association in which the free evaluation of each expression is the condition for the free evaluation of all.") (p :class "text-stone-800 font-semibold text-lg mt-8 text-center" "DEVELOPERS OF ALL SERVICES, UNITE!") (p :class "text-stone-400 text-xs italic mt-6 text-center" "The authors acknowledge that this manifesto was produced by the very means of AI production it fails to mention. This is not a contradiction. It is dialectics."))))
(defcomp ~essay-tail-call-optimization ()
(~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-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."))))