diff --git a/shared/sx/async_eval.py b/shared/sx/async_eval.py index 7b5d78d..ee836a8 100644 --- a/shared/sx/async_eval.py +++ b/shared/sx/async_eval.py @@ -41,10 +41,18 @@ Usage:: from __future__ import annotations +import contextvars import inspect from typing import Any from .types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol + +# When True, _aser expands known components server-side instead of serializing +# them for client rendering. Set during page slot evaluation so Python-only +# helpers (e.g. highlight) in component bodies execute on the server. +_expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar( + "_expand_components", default=False +) from .evaluator import _expand_macro, EvalError from .primitives import _PRIMITIVES from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io @@ -1058,6 +1066,24 @@ async def async_eval_slot_to_sx( """ if ctx is None: ctx = RequestContext() + + # Enable server-side component expansion for this slot evaluation. + # This lets _aser expand known components (so Python-only helpers + # like highlight execute server-side) instead of serializing them + # for client rendering. + token = _expand_components.set(True) + try: + return await _eval_slot_inner(expr, env, ctx) + finally: + _expand_components.reset(token) + + +async def _eval_slot_inner( + expr: Any, + env: dict[str, Any], + ctx: RequestContext, +) -> str: + """Inner implementation — runs with _expand_components=True.""" # If expr is a component call, expand it through _aser if isinstance(expr, list) and expr: head = expr[0] @@ -1159,13 +1185,14 @@ async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any: if name.startswith("html:"): return await _aser_call(name[5:], expr[1:], env, ctx) - # Component call — expand macros, expand known components, serialize unknown + # Component call — expand macros, expand known components (in slot + # eval context only), serialize unknown if name.startswith("~"): val = env.get(name) if isinstance(val, Macro): expanded = _expand_macro(val, expr[1:], env) return await _aser(expanded, env, ctx) - if isinstance(val, Component): + if isinstance(val, Component) and _expand_components.get(): return await _aser_component(val, expr[1:], env, ctx) return await _aser_call(name, expr[1:], env, ctx) diff --git a/sx/sx/essays.sx b/sx/sx/essays.sx index 2221700..3ca3f55 100644 --- a/sx/sx/essays.sx +++ b/sx/sx/essays.sx @@ -19,10 +19,13 @@ (~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.")))) + (~doc-page :title "The SX Manifesto" (p :class "text-stone-500 text-sm italic mb-4" "Carl Markdown and Friedrich Anglebrackets") (p :class "text-stone-500 text-sm italic mb-8" "A " (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "parenthetical") " revolution") (~doc-section :title "I. A spectre is haunting the web" :id "spectre" (p :class "text-stone-600" "A " (a :href "https://en.wikipedia.org/wiki/Spectre" :class "text-violet-600 hover:underline" "spectre") " is haunting the web — the spectre of " (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "s-expressions") ". All the powers of the old web have entered into a holy alliance to exorcise this spectre: " (a :href "https://en.wikipedia.org/wiki/Google" :class "text-violet-600 hover:underline" "Google") " and " (a :href "https://en.wikipedia.org/wiki/Meta_Platforms" :class "text-violet-600 hover:underline" "Meta") ", " (a :href "https://en.wikipedia.org/wiki/Webpack" :class "text-violet-600 hover:underline" "webpack") " and " (a :href "https://en.wikipedia.org/wiki/Vercel" :class "text-violet-600 hover:underline" "Vercel") ", " (a :href "https://en.wikipedia.org/wiki/Stack_Overflow" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Web_development" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Webpack" :class "text-violet-600 hover:underline" "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: " (a :href "https://en.wikipedia.org/wiki/HTML" :class "text-violet-600 hover:underline" "HTML") ", " (a :href "https://en.wikipedia.org/wiki/CSS" :class "text-violet-600 hover:underline" "CSS") ", " (a :href "https://en.wikipedia.org/wiki/JavaScript" :class "text-violet-600 hover:underline" "JavaScript") ", " (a :href "https://en.wikipedia.org/wiki/XML" :class "text-violet-600 hover:underline" "XML") ", " (a :href "https://en.wikipedia.org/wiki/XSLT" :class "text-violet-600 hover:underline" "XSLT") ", " (a :href "https://en.wikipedia.org/wiki/JSON" :class "text-violet-600 hover:underline" "JSON") ", " (a :href "https://en.wikipedia.org/wiki/YAML" :class "text-violet-600 hover:underline" "YAML") ", " (a :href "https://en.wikipedia.org/wiki/TOML" :class "text-violet-600 hover:underline" "TOML") ", " (a :href "https://en.wikipedia.org/wiki/JSX_(JavaScript)" :class "text-violet-600 hover:underline" "JSX") ", TSX, " (a :href "https://en.wikipedia.org/wiki/Sass_(style_sheet_language)" :class "text-violet-600 hover:underline" "Sass") ", " (a :href "https://en.wikipedia.org/wiki/Less_(style_sheet_language)" :class "text-violet-600 hover:underline" "Less") ", " (a :href "https://en.wikipedia.org/wiki/PostCSS" :class "text-violet-600 hover:underline" "PostCSS") ", " (a :href "https://en.wikipedia.org/wiki/Tailwind_CSS" :class "text-violet-600 hover:underline" "Tailwind") ", and above them all, the build step.") (p :class "text-stone-600" "The modern web, sprouted from the ruins of " (a :href "https://en.wikipedia.org/wiki/Common_Gateway_Interface" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Server-side" :class "text-violet-600 hover:underline" "server") " and the " (a :href "https://en.wikipedia.org/wiki/Client-side" :class "text-violet-600 hover:underline" "client") ".")) (~doc-section :title "III. The ruling languages" :id "ruling-languages" (p :class "text-stone-600" (a :href "https://en.wikipedia.org/wiki/HTML" :class "text-violet-600 hover:underline" "HTML") ", the most ancient of the ruling languages, established itself through the divine right of the " (a :href "https://en.wikipedia.org/wiki/SGML" :class "text-violet-600 hover:underline" "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" (a :href "https://en.wikipedia.org/wiki/JavaScript" :class "text-violet-600 hover:underline" "JavaScript") ", originally a servant hired for " (a :href "https://en.wikipedia.org/wiki/JavaScript#History" :class "text-violet-600 hover:underline" "a fortnight") " to validate forms, staged a palace coup. It seized the means of interaction, then the means of rendering, then the means of " (a :href "https://en.wikipedia.org/wiki/Node.js" :class "text-violet-600 hover:underline" "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: " (a :href "https://en.wikipedia.org/wiki/Strong_and_weak_typing" :class "text-violet-600 hover:underline" "weak typing") ", " (a :href "https://en.wikipedia.org/wiki/Prototype-based_programming" :class "text-violet-600 hover:underline" "prototype chains") ", and the " (span :class "italic" (a :href "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this" :class "text-violet-600 hover:underline" "this")) " keyword.") (p :class "text-stone-600" (a :href "https://en.wikipedia.org/wiki/CSS" :class "text-violet-600 hover:underline" "CSS") ", the " (a :href "https://en.wikipedia.org/wiki/Third_estate" :class "text-violet-600 hover:underline" "third estate") ", controls all visual presentation while pretending to be " (a :href "https://en.wikipedia.org/wiki/Declarative_programming" :class "text-violet-600 hover:underline" "declarative") ". It has no functions. Then it had functions. It has no variables. Then it had " (a :href "https://en.wikipedia.org/wiki/CSS_custom_properties" :class "text-violet-600 hover:underline" "variables") ". It has no nesting. Then it had " (a :href "https://en.wikipedia.org/wiki/CSS_nesting" :class "text-violet-600 hover:underline" "nesting") ". It is not a programming language. Then it was " (a :href "https://en.wikipedia.org/wiki/Turing_completeness" :class "text-violet-600 hover:underline" "Turing-complete") ". CSS is the " (a :href "https://en.wikipedia.org/wiki/Vicar_of_Bray_(song)" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Proletariat" :class "text-violet-600 hover:underline" "proletarian") " — must learn all three, must context-switch between all three, must maintain the fragile peace between all three. The " (a :href "https://en.wikipedia.org/wiki/Separation_of_concerns" :class "text-violet-600 hover:underline" "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. " (a :href "https://en.wikipedia.org/wiki/React_(software)" :class "text-violet-600 hover:underline" "React") ", " (a :href "https://en.wikipedia.org/wiki/Vue.js" :class "text-violet-600 hover:underline" "Vue") ", " (a :href "https://en.wikipedia.org/wiki/Angular_(web_framework)" :class "text-violet-600 hover:underline" "Angular") ", " (a :href "https://en.wikipedia.org/wiki/Svelte" :class "text-violet-600 hover:underline" "Svelte") ", " (a :href "https://www.solidjs.com/" :class "text-violet-600 hover:underline" "Solid") ", " (a :href "https://qwik.dev/" :class "text-violet-600 hover:underline" "Qwik") ", " (a :href "https://astro.build/" :class "text-violet-600 hover:underline" "Astro") ", " (a :href "https://en.wikipedia.org/wiki/Next.js" :class "text-violet-600 hover:underline" "Next") ", " (a :href "https://en.wikipedia.org/wiki/Nuxt.js" :class "text-violet-600 hover:underline" "Nuxt") ", " (a :href "https://remix.run/" :class "text-violet-600 hover:underline" "Remix") ", " (a :href "https://en.wikipedia.org/wiki/Gatsby_(framework)" :class "text-violet-600 hover:underline" "Gatsby") ", and ten thousand others whose names will not survive the decade.") (p :class "text-stone-600" "The frameworks are the " (a :href "https://en.wikipedia.org/wiki/Petite_bourgeoisie" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Rent-seeking" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Dependency_hell" :class "text-violet-600 hover:underline" "dependency tree") ". React freed us from manual " (a :href "https://en.wikipedia.org/wiki/Document_Object_Model" :class "text-violet-600 hover:underline" "DOM") " manipulation and gave us a " (a :href "https://en.wikipedia.org/wiki/Virtual_DOM" :class "text-violet-600 hover:underline" "virtual DOM") ", a reconciler, " (a :href "https://react.dev/reference/react/hooks" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Reactive_programming" :class "text-violet-600 hover:underline" "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 — " (a :href "https://en.wikipedia.org/wiki/JSX_(JavaScript)" :class "text-violet-600 hover:underline" "JSX") ", " (a :href "https://vuejs.org/guide/scaling-up/sfc" :class "text-violet-600 hover:underline" "SFCs") ", templates — which must itself be " (a :href "https://en.wikipedia.org/wiki/Source-to-source_compiler" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Lumpenproletariat" :class "text-violet-600 hover:underline" "lumpenproletariat") " of the ecosystem. " (a :href "https://en.wikipedia.org/wiki/Lodash" :class "text-violet-600 hover:underline" "Lodash") ", " (a :href "https://en.wikipedia.org/wiki/Moment.js" :class "text-violet-600 hover:underline" "Moment") ", " (a :href "https://en.wikipedia.org/wiki/Axios_(software)" :class "text-violet-600 hover:underline" "Axios") ", " (a :href "https://en.wikipedia.org/wiki/Npm_left-pad_incident" :class "text-violet-600 hover:underline" "left-pad") ". They attach themselves to whichever framework currently holds power, contributing nothing original, merely wrapping what already exists, adding weight to the " (a :href "https://en.wikipedia.org/wiki/Npm" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/State_apparatus" :class "text-violet-600 hover:underline" "state apparatus") " of the framework bourgeoisie. It enforces the class structure. It compiles JSX into " (a :href "https://react.dev/reference/react/createElement" :class "text-violet-600 hover:underline" "createElement") " calls. It transforms " (a :href "https://en.wikipedia.org/wiki/TypeScript" :class "text-violet-600 hover:underline" "TypeScript") " into JavaScript. It processes Sass into CSS. It " (a :href "https://en.wikipedia.org/wiki/Tree_shaking" :class "text-violet-600 hover:underline" "tree-shakes") ". It " (a :href "https://en.wikipedia.org/wiki/Code_splitting" :class "text-violet-600 hover:underline" "code-splits") ". It " (a :href "https://en.wikipedia.org/wiki/Hot_module_replacement" :class "text-violet-600 hover:underline" "hot-module-replaces") ". It does everything except let you write code and run it.") (p :class "text-stone-600" (a :href "https://en.wikipedia.org/wiki/Webpack" :class "text-violet-600 hover:underline" "webpack") " begat " (a :href "https://en.wikipedia.org/wiki/Rollup_(software)" :class "text-violet-600 hover:underline" "Rollup") ". Rollup begat " (a :href "https://en.wikipedia.org/wiki/Parcel_(software)" :class "text-violet-600 hover:underline" "Parcel") ". Parcel begat " (a :href "https://en.wikipedia.org/wiki/Esbuild" :class "text-violet-600 hover:underline" "esbuild") ". esbuild begat " (a :href "https://en.wikipedia.org/wiki/Vite_(software)" :class "text-violet-600 hover:underline" "Vite") ". Vite begat " (a :href "https://turbo.build/pack" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Component-based_software_engineering" :class "text-violet-600 hover:underline" "components") ". HTML has no composition model. CSS has no scoping. JavaScript has no template syntax. The build step papers over these failures with " (a :href "https://en.wikipedia.org/wiki/Source-to-source_compiler" :class "text-violet-600 hover:underline" "transpilation") ", and calls it developer experience.")) (~doc-section :title "VI. The s-expression revolution" :id "revolution" (p :class "text-stone-600" "The " (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "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" (a :href "https://en.wikipedia.org/wiki/Homoiconicity" :class "text-violet-600 hover:underline" "Code is data") ". Data is " (a :href "https://en.wikipedia.org/wiki/Document_Object_Model" :class "text-violet-600 hover:underline" "DOM") ". DOM is code. The " (a :href "https://en.wikipedia.org/wiki/Dialectic" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)#History" :class "text-violet-600 hover:underline" "since 1958") ".") (p :class "text-stone-600" "The component is not a class, not a function, not a template. The component is a " (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "list") " whose first element is a symbol. Composition is nesting. " (a :href "https://en.wikipedia.org/wiki/Abstraction_(computer_science)" :class "text-violet-600 hover:underline" "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. " (code "defcomp") " replaces the component model. " (code "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 " (a :href "https://en.wikipedia.org/wiki/Separation_of_concerns" :class "text-violet-600 hover:underline" "separation of concerns") "!\" they cry. The separation of concerns was destroyed long ago. " (a :href "https://en.wikipedia.org/wiki/React_(software)" :class "text-violet-600 hover:underline" "React") " components contain markup, logic, and inline styles. " (a :href "https://en.wikipedia.org/wiki/Vue.js" :class "text-violet-600 hover:underline" "Vue") " single-file components put template, script, and style in one file. " (a :href "https://en.wikipedia.org/wiki/Tailwind_CSS" :class "text-violet-600 hover:underline" "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. " (a :href "https://en.wikipedia.org/wiki/Emacs_Lisp" :class "text-violet-600 hover:underline" "Emacs") " has been running on s-expressions since 1976. " (a :href "https://en.wikipedia.org/wiki/Clojure" :class "text-violet-600 hover:underline" "Clojure") " runs Fortune 500 backends on s-expressions. Every " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "structured data") ".") (p :class "text-stone-600" "\"Where is the ecosystem?\" they cry. The ecosystem is the problem. Two million " (a :href "https://en.wikipedia.org/wiki/Npm" :class "text-violet-600 hover:underline" "npm") " packages, of which fourteen are useful and the rest are competing implementations of " (a :href "https://www.npmjs.com/package/is-odd" :class "text-violet-600 hover:underline" "is-odd") ". The s-expression needs no ecosystem because the language itself provides what packages exist to paper over: composition, abstraction, and " (a :href "https://en.wikipedia.org/wiki/Homoiconicity" :class "text-violet-600 hover:underline" "code-as-data") ".") (p :class "text-stone-600" "\"But " (a :href "https://en.wikipedia.org/wiki/TypeScript" :class "text-violet-600 hover:underline" "TypeScript") "!\" they cry. TypeScript is a " (a :href "https://en.wikipedia.org/wiki/Type_system" :class "text-violet-600 hover:underline" "type system") " bolted onto a language that was " (a :href "https://en.wikipedia.org/wiki/JavaScript#History" :class "text-violet-600 hover:underline" "designed in ten days") " by a man who wanted to write " (a :href "https://en.wikipedia.org/wiki/Scheme_(programming_language)" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Venture_capital" :class "text-violet-600 hover:underline" "venture capital") ", no stickers, and no swag. We have something better: a language that does not require a " (a :href "https://en.wikipedia.org/wiki/Software_versioning" :class "text-violet-600 hover:underline" "migration guide") " between versions.")) (~doc-section :title "VIII. The CSS question" :id "css-question" (p :class "text-stone-600" (a :href "https://en.wikipedia.org/wiki/CSS" :class "text-violet-600 hover:underline" "CSS") " presents a special case in the revolutionary analysis. It is neither fully a ruling language nor fully a servant — it is the " (a :href "https://en.wikipedia.org/wiki/Collaborationism" :class "text-violet-600 hover:underline" "collaborator") " class, providing aesthetic legitimacy to whichever regime currently holds power.") (p :class "text-stone-600" (a :href "https://en.wikipedia.org/wiki/CSS-in-JS" :class "text-violet-600 hover:underline" "CSS-in-JS") " was the first attempt at annexation: JavaScript consuming CSS entirely, reducing it to " (a :href "https://en.wikipedia.org/wiki/Template_literal" :class "text-violet-600 hover:underline" "template literals") " and runtime overhead. This provocation produced the counter-revolution of utility classes — " (a :href "https://en.wikipedia.org/wiki/Tailwind_CSS" :class "text-violet-600 hover:underline" "Tailwind") " — which reasserted CSS's independence by making the developer write CSS in HTML attributes while insisting this was not " (a :href "https://en.wikipedia.org/wiki/Style_attribute" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/First-class_function" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "parenthetical") " revolution. The developers have nothing to lose but their " (a :href "https://en.wikipedia.org/wiki/Npm" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Build_automation" :class "text-violet-600 hover:underline" "build step") " and all its instruments of compilation") (li "Abolition of the " (a :href "https://en.wikipedia.org/wiki/Software_framework" :class "text-violet-600 hover:underline" "framework") " as a class distinct from the language") (li "Centralisation of rendering in the hands of a single evaluator, running identically on " (a :href "https://en.wikipedia.org/wiki/Server-side" :class "text-violet-600 hover:underline" "server") " and " (a :href "https://en.wikipedia.org/wiki/Client-side" :class "text-violet-600 hover:underline" "client")) (li "Abolition of the language distinction between structure, style, and behaviour") (li "Equal obligation of all expressions to be " (a :href "https://en.wikipedia.org/wiki/Homoiconicity" :class "text-violet-600 hover:underline" "data as well as code")) (li "Gradual abolition of the distinction between server and client by means of a uniform " (a :href "https://en.wikipedia.org/wiki/Wire_protocol" :class "text-violet-600 hover:underline" "wire protocol")) (li "Free evaluation for all expressions in public and private environments") (li "Abolition of the " (a :href "https://en.wikipedia.org/wiki/Npm" :class "text-violet-600 hover:underline" "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 " (a :href "https://en.wikipedia.org/wiki/Large_language_model" :class "text-violet-600 hover:underline" "AI production") " it fails to mention. This is not a contradiction. It is " (a :href "https://en.wikipedia.org/wiki/Dialectic" :class "text-violet-600 hover:underline" "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-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.")))) + (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 36cdce5..6e77796 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -63,7 +63,8 @@ (dict :label "SX Native" :href "/essays/sx-native") (dict :label "The SX Manifesto" :href "/essays/sx-manifesto") (dict :label "Tail-Call Optimization" :href "/essays/tail-call-optimization") - (dict :label "Continuations" :href "/essays/continuations"))) + (dict :label "Continuations" :href "/essays/continuations") + (dict :label "Godel, Escher, Bach" :href "/essays/godel-escher-bach"))) ;; Find the current nav label for a slug by matching href suffix. ;; Returns the label string or nil if no match. diff --git a/sx/sxc/docs.sx b/sx/sxc/docs.sx index 450722a..93d8208 100644 --- a/sx/sxc/docs.sx +++ b/sx/sxc/docs.sx @@ -61,15 +61,15 @@ (defcomp ~doc-nav (&key items current) (nav :class "flex flex-wrap gap-2 mb-8" (map (fn (item) - (a :href (nth 1 item) - :sx-get (nth 1 item) + (a :href (nth item 1) + :sx-get (nth item 1) :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class (str "px-3 py-1.5 rounded text-sm font-medium no-underline " - (if (= (nth 0 item) current) + (if (= (nth item 0) current) "bg-violet-100 text-violet-800" "bg-stone-100 text-stone-600 hover:bg-stone-200")) - (nth 0 item))) + (nth item 0))) items))) diff --git a/sx/sxc/examples.sx b/sx/sxc/examples.sx index d1765a5..5df7402 100644 --- a/sx/sxc/examples.sx +++ b/sx/sxc/examples.sx @@ -91,7 +91,7 @@ (th :class "px-3 py-2 font-medium text-stone-600 w-20" ""))) (tbody :id "delete-rows" (map (fn (item) - (~delete-row :id (nth 0 item) :name (nth 1 item))) + (~delete-row :id (nth item 0) :name (nth item 1))) items))))) (defcomp ~delete-row (&key id name) @@ -344,7 +344,7 @@ (th :class "px-3 py-2 font-medium text-stone-600 w-24" ""))) (tbody :id "edit-rows" (map (fn (row) - (~edit-row-view :id (nth 0 row) :name (nth 1 row) :price (nth 2 row) :stock (nth 3 row))) + (~edit-row-view :id (nth row 0) :name (nth row 1) :price (nth row 2) :stock (nth row 3))) rows))))) (defcomp ~edit-row-view (&key id name price stock) @@ -415,7 +415,7 @@ (th :class "px-3 py-2 font-medium text-stone-600" "Status"))) (tbody :id "bulk-table" (map (fn (u) - (~bulk-row :id (nth 0 u) :name (nth 1 u) :email (nth 2 u) :status (nth 3 u))) + (~bulk-row :id (nth u 0) :name (nth u 1) :email (nth u 2) :status (nth u 3))) users)))))) (defcomp ~bulk-row (&key id name email status) diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index 5d910ea..eafa11f 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -239,4 +239,5 @@ "sx-manifesto" (~essay-sx-manifesto) "tail-call-optimization" (~essay-tail-call-optimization) "continuations" (~essay-continuations) + "godel-escher-bach" (~essay-godel-escher-bach) :else (~essay-sx-sucks)))