From abeb4551da1a1e655aa1116e0f547312d2ff6ed3 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 5 Mar 2026 23:58:20 +0000 Subject: [PATCH] Add server architecture essay to sx-docs Documents the boundary enforcement model: three tiers, boundary types, runtime validation, the SX-in-Python rule, and the multi-language story. Co-Authored-By: Claude Opus 4.6 --- sx/sx/essays.sx | 111 +++++++++++++++++++++++++++++++++++++++++++ sx/sx/nav-data.sx | 2 + sx/sxc/pages/docs.sx | 1 + 3 files changed, 114 insertions(+) diff --git a/sx/sx/essays.sx b/sx/sx/essays.sx index 75e54df..8cb6d2d 100644 --- a/sx/sx/essays.sx +++ b/sx/sx/essays.sx @@ -148,3 +148,114 @@ (li (strong "Composable trust.") " The sandbox mechanism means you can give an AI agent " (em "exactly") " the capabilities it needs — no more. Trust is expressed as a set of available primitives, not as an all-or-nothing API key.")) (p :class "text-stone-600" "None of these require breakthroughs in AI. They require a web that speaks a reflexive language. " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " solved the language problem in 1958. SX solves the distribution problem. AI provides the agency. The three together produce something that none of them achieves alone: a web that can reason about itself.")))) + +(defcomp ~essay-server-architecture () + (~doc-page :title "Server Architecture" + (p :class "text-stone-500 text-sm italic mb-8" + "How SX enforces the boundary between host language and embedded language, why that boundary matters, and what it looks like across different target languages.") + + (~doc-section :title "The island constraint" :id "island" + (p :class "text-stone-600" + "SX is an embedded language. It runs inside a host — Python on the server, JavaScript in the browser. The central architectural constraint is that SX is a " (strong "pure island") ": the evaluator sees values in and values out. No host objects leak into the SX environment. No SX expressions reach into host internals. Every interaction between SX and the host passes through a declared, validated boundary.") + (p :class "text-stone-600" + "This is not a performance optimization or a convenience. It is the property that makes self-hosting possible. If host objects can leak into SX environments, then the spec files depend on host-specific types. If SX expressions can call host functions directly, the evaluator's behavior varies per host. Neither of those is compatible with a single specification that bootstraps to multiple targets.") + (p :class "text-stone-600" + "The constraint: " (strong "nothing crosses the boundary unless it is declared in a spec file and its type is one of the boundary types") ".")) + + (~doc-section :title "Three tiers" :id "tiers" + (p :class "text-stone-600" + "Host functions that SX can call are organized into three tiers, each with different trust levels and declaration requirements:") + (div :class "space-y-4" + (div :class "border rounded-lg p-4 border-stone-200" + (h3 :class "font-semibold text-stone-800 mb-2" "Tier 1: Pure primitives") + (p :class "text-stone-600 text-sm" + "Declared in " (code :class "text-violet-700 text-sm" "primitives.sx") ". About 80 functions — arithmetic, string operations, list operations, dict operations, type predicates. All pure: values in, values out, no side effects. These are the only host functions visible to the spec itself. Every bootstrapper must implement all of them.")) + (div :class "border rounded-lg p-4 border-stone-200" + (h3 :class "font-semibold text-stone-800 mb-2" "Tier 2: I/O primitives") + (p :class "text-stone-600 text-sm" + "Declared in " (code :class "text-violet-700 text-sm" "boundary.sx") ". About 34 functions — cross-service queries, fragment fetching, request context access, URL generation. Async and side-effectful. They need host context (HTTP request, database connection, config). The SX resolver identifies these in the render tree, gathers them, executes them in parallel, and substitutes results back in.")) + (div :class "border rounded-lg p-4 border-stone-200" + (h3 :class "font-semibold text-stone-800 mb-2" "Tier 3: Page helpers") + (p :class "text-stone-600 text-sm" + "Also declared in " (code :class "text-violet-700 text-sm" "boundary.sx") ". Service-scoped Python functions registered via " (code :class "text-violet-700 text-sm" "register_page_helpers()") ". They provide data for specific page types — syntax highlighting, reference table data, bootstrapper output. Each helper is bound to a specific service and available only in that service's page evaluation environment.")))) + + (~doc-section :title "Boundary types" :id "types" + (p :class "text-stone-600" + "Only these types may cross the host-SX boundary:") + (~doc-code :code (highlight "(define-boundary-types\n (list \"number\" \"string\" \"boolean\" \"nil\" \"keyword\"\n \"list\" \"dict\" \"sx-source\" \"style-value\"))" "lisp")) + (p :class "text-stone-600" + "No Python " (code :class "text-violet-700 text-sm" "datetime") " objects. No ORM models. No Quart request objects. If a host function returns a " (code :class "text-violet-700 text-sm" "datetime") ", it must convert to an ISO string before crossing. If it returns a database row, it must convert to a plain dict. The boundary validation checks this recursively — lists and dicts have their elements checked too.") + (p :class "text-stone-600" + "The " (code :class "text-violet-700 text-sm" "sx-source") " type is SX source text wrapped in an " (code :class "text-violet-700 text-sm" "SxExpr") " marker. It allows the host to pass pre-rendered SX markup into the tree — but only the host can create it. SX code cannot construct SxExpr values; it can only receive them from the boundary.")) + + (~doc-section :title "Enforcement" :id "enforcement" + (p :class "text-stone-600" + "The boundary contract is enforced at three points, each corresponding to a tier:") + (div :class "space-y-3" + (div :class "bg-stone-100 rounded p-4" + (p :class "text-sm text-stone-700" + (strong "Primitive registration. ") "When " (code :class "text-violet-700 text-sm" "@register_primitive") " decorates a function, it calls " (code :class "text-violet-700 text-sm" "validate_primitive(name)") ". If the name is not declared in " (code :class "text-violet-700 text-sm" "primitives.sx") ", the service fails to start.")) + (div :class "bg-stone-100 rounded p-4" + (p :class "text-sm text-stone-700" + (strong "I/O handler registration. ") "When " (code :class "text-violet-700 text-sm" "primitives_io.py") " builds the " (code :class "text-violet-700 text-sm" "_IO_HANDLERS") " dict, each name is validated against " (code :class "text-violet-700 text-sm" "boundary.sx") ". Undeclared I/O primitives crash the import.")) + (div :class "bg-stone-100 rounded p-4" + (p :class "text-sm text-stone-700" + (strong "Page helper registration. ") "When a service calls " (code :class "text-violet-700 text-sm" "register_page_helpers(service, helpers)") ", each helper name is validated against " (code :class "text-violet-700 text-sm" "boundary.sx") " for that service. Undeclared helpers fail. Return values are wrapped to pass through " (code :class "text-violet-700 text-sm" "validate_boundary_value()") "."))) + (p :class "text-stone-600" + "All three checks are controlled by the " (code :class "text-violet-700 text-sm" "SX_BOUNDARY_STRICT") " environment variable. With " (code :class "text-violet-700 text-sm" "\"1\"") " (the production default), violations crash at startup. Without it, they log warnings. The strict mode is set in both " (code :class "text-violet-700 text-sm" "docker-compose.yml") " and " (code :class "text-violet-700 text-sm" "docker-compose.dev.yml") ".")) + + (~doc-section :title "The SX-in-Python rule" :id "sx-in-python" + (p :class "text-stone-600" + "One enforcement that is not automated but equally important: " (strong "SX source code must not be constructed as Python strings") ". S-expressions belong in " (code :class "text-violet-700 text-sm" ".sx") " files. Python belongs in " (code :class "text-violet-700 text-sm" ".py") " files. If you see a Python f-string that builds " (code :class "text-violet-700 text-sm" "(div :class ...)") ", that is a boundary violation.") + (p :class "text-stone-600" + "The correct pattern: Python returns " (strong "data") " (dicts, lists, strings). " (code :class "text-violet-700 text-sm" ".sx") " files receive data via keyword args and compose the markup. The only exception is " (code :class "text-violet-700 text-sm" "SxExpr") " wrappers for pre-rendered fragments — and those should be built with " (code :class "text-violet-700 text-sm" "sx_call()") " or " (code :class "text-violet-700 text-sm" "_sx_fragment()") ", never with f-strings.") + (~doc-code :code (highlight ";; CORRECT: .sx file composes markup from data\n(defcomp ~my-page (&key items)\n (div :class \"space-y-4\"\n (map (fn (item)\n (div :class \"border rounded p-3\"\n (h3 (get item \"title\"))\n (p (get item \"desc\"))))\n items)))" "lisp")) + (~doc-code :code (highlight "# CORRECT: Python returns data\ndef _my_page_data():\n return {\"items\": [{\"title\": \"A\", \"desc\": \"B\"}]}\n\n# WRONG: Python builds SX source\ndef _my_page_data():\n return SxExpr(f'(div (h3 \"{title}\"))') # NO" "python"))) + + (~doc-section :title "Why this matters for multiple languages" :id "languages" + (p :class "text-stone-600" + "The boundary contract is target-agnostic. " (code :class "text-violet-700 text-sm" "boundary.sx") " and " (code :class "text-violet-700 text-sm" "primitives.sx") " declare what crosses the boundary. Each bootstrapper reads those declarations and emits the strongest enforcement the target language supports:") + (div :class "overflow-x-auto rounded border border-stone-200 my-4" + (table :class "w-full text-left text-sm" + (thead (tr :class "border-b border-stone-200 bg-stone-100" + (th :class "px-3 py-2 font-medium text-stone-600" "Target") + (th :class "px-3 py-2 font-medium text-stone-600" "Enforcement") + (th :class "px-3 py-2 font-medium text-stone-600" "Mechanism"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "Python") + (td :class "px-3 py-2 text-stone-600" "Runtime") + (td :class "px-3 py-2 text-stone-500 text-sm" "Frozen sets + validation at registration")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "JavaScript") + (td :class "px-3 py-2 text-stone-600" "Runtime") + (td :class "px-3 py-2 text-stone-500 text-sm" "Set guards on registerPrimitive()")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "Rust") + (td :class "px-3 py-2 text-stone-600" "Compile-time") + (td :class "px-3 py-2 text-stone-500 text-sm" "SxValue enum, trait bounds on primitive fns")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "Haskell") + (td :class "px-3 py-2 text-stone-600" "Compile-time") + (td :class "px-3 py-2 text-stone-500 text-sm" "ADT + typeclass constraints")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "TypeScript") + (td :class "px-3 py-2 text-stone-600" "Compile-time") + (td :class "px-3 py-2 text-stone-500 text-sm" "Discriminated union types"))))) + (p :class "text-stone-600" + "In Python and JavaScript, boundary violations are caught at startup. In Rust, Haskell, or TypeScript, they would be caught at compile time — a function that returns a non-boundary type simply would not type-check. The spec is the same; the enforcement level rises with the type system.") + (p :class "text-stone-600" + "This is the payoff of the pure island constraint. Because SX never touches host internals, a bootstrapper for a new target only needs to implement the declared primitives and boundary functions. The evaluator, renderer, parser, and all components work unchanged. One spec, every target, same guarantees.")) + + (~doc-section :title "The spec as contract" :id "contract" + (p :class "text-stone-600" + "The boundary enforcement files form a closed contract:") + (ul :class "space-y-2 text-stone-600" + (li (code :class "text-violet-700 text-sm" "primitives.sx") " — declares every pure function SX can call") + (li (code :class "text-violet-700 text-sm" "boundary.sx") " — declares every I/O function and page helper") + (li (code :class "text-violet-700 text-sm" "boundary_parser.py") " — reads both files, extracts declared names") + (li (code :class "text-violet-700 text-sm" "boundary.py") " — runtime validation (validate_primitive, validate_io, validate_helper, validate_boundary_value)")) + (p :class "text-stone-600" + "If you add a new host function and forget to declare it, the service will not start. If you return a disallowed type, the validation will catch it. The spec files are not documentation — they are the mechanism. The bootstrappers read them. The validators parse them. The contract is enforced by the same files that describe it.") + (p :class "text-stone-600" + "This closes the loop on self-hosting. The SX spec defines the language. The boundary spec defines the edge. The bootstrappers generate implementations from both. And the generated code validates itself against the spec at startup. The spec is the implementation is the contract is the spec.")))) diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 6a25367..14536bb 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -75,6 +75,8 @@ :summary "Self-reference, and the tangled hierarchy of a language that defines itself.") (dict :label "The Reflexive Web" :href "/essays/reflexive-web" :summary "A web where pages can inspect, modify, and extend their own rendering pipeline.") + (dict :label "Server Architecture" :href "/essays/server-architecture" + :summary "How SX enforces the boundary between host and embedded language, and what it looks like across targets.") (dict :label "sx sucks" :href "/essays/sx-sucks" :summary "An honest accounting of everything wrong with SX and why you probably shouldn't use it."))) diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index 6bd6213..6dac22e 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -278,6 +278,7 @@ "continuations" (~essay-continuations) "godel-escher-bach" (~essay-godel-escher-bach) "reflexive-web" (~essay-reflexive-web) + "server-architecture" (~essay-server-architecture) :else (~essays-index-content))) ;; ---------------------------------------------------------------------------