9 Commits

Author SHA1 Message Date
ec1093d372 Nav redesign: embedded breadcrumb navigation with recursive depth
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m25s
Replace menu bar navigation with in-page nav embedded in content area.
Each page shows logo/tagline/copyright, then a sibling row per trail
level (← prev | Current | next →), then children as button links.

- resolve-nav-path: recursive walk with no depth limit
- find-nav-index: rewritten with recursion (set! broken across closures)
- Walk stops on exact href match (prevents /cssx/ drilling into Overview)
- Unicode chars (©, ←, →) inline instead of \u escapes (SX parser doesn't support them)
- All 38 defpages wrapped in (~sx-doc :path ...) for in-page nav
- Layout returns only root header (nav moved out of blue menu bar)
- Standalone layout variants for sx-web.org (return nil)
- New plans: environment-images, runtime-slicing, typed-sx, nav-redesign, sx-web-platform

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:37:37 +00:00
cad65bcdf1 Add js.sx bootstrapper docs page with G0 bug discovery writeup
Documents the self-hosting process for js.sx including the G0 bug
where Python's `if fn_expr` treated 0/False/"" as falsy, emitting
NIL instead of the correct value. Adds live verification page,
translation differences table, and nav entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 01:44:02 +00:00
e6ca1a5f44 Implement js.sx: self-hosting SX-to-JavaScript bootstrapper
SX-to-JavaScript translator written in SX itself. When executed by the
Python evaluator against the spec files, produces output identical to
the hand-written bootstrap_js.py JSEmitter.

- 1,382 lines, 61 defines
- 431/431 defines match across all 22 spec files (G0 == G1)
- 267 defines in the standard compilation, 151,763 bytes identical
- camelCase mangling, var declarations, function(){} syntax
- Self-tail-recursive optimization (zero-arg → while loops)
- JS-native statement patterns: d[k]=v, arr.push(x), f.name=x

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 01:36:12 +00:00
fd4f13e571 Fix ref_dir UnboundLocalError in self-hosting bootstrapper page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 01:24:03 +00:00
e5acfdcd3c Add live self-hosting bootstrapper page to bootstrappers section
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m15s
- Update plan page with completion status and results
- Add ~bootstrapper-self-hosting-content component with live G0/G1 verification
- Add _self_hosting_data() helper: loads py.sx, runs it, diffs against G0
- Add "Self-Hosting (py.sx)" to bootstrappers nav and index table
- Wire /bootstrappers/self-hosting route

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 01:18:20 +00:00
b4944aa2b6 Implement py.sx: self-hosting SX-to-Python bootstrapper
py.sx is an SX-to-Python translator written in SX. Running it on the
Python evaluator against the spec files produces byte-for-byte identical
output to the hand-written bootstrap_py.py (128/128 defines match,
1490 lines, 88955 bytes).

The bootstrapper bootstraps itself: G0 (Python) == G1 (SX).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 01:12:50 +00:00
e4e8b45cb4 Update py.sx scope: general SX-to-Python translator, not spec-only
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m37s
The translation rules are mechanical and apply to all SX forms — HTML tags,
components, macros, islands, page forms. Bootstrapping the spec is the first
test case (fixed-point proof); phases 6-9 extend to full SX compilation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:54:20 +00:00
db1691d8f5 Add JS bootstrapper plan: js.sx design document
js.sx — SX-to-JavaScript translator + ahead-of-time component compiler.
Two modes: spec bootstrapper (replacing bootstrap_js.py) and component
compiler (server-evaluated SX trees → standalone JS DOM construction).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:52:35 +00:00
192d48d0e3 Add self-hosting bootstrapper plan: py.sx design document
Plan for py.sx — an SX-to-Python translator written in SX that
replaces bootstrap_py.py. Covers translation rules, statement vs
expression modes, mutation/cell variables, five implementation
phases, fixed-point verification, and implications for multi-host
bootstrapping.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:45:07 +00:00
19 changed files with 5746 additions and 678 deletions

View File

@@ -872,7 +872,7 @@ class JSEmitter:
body = fn_expr[2:]
loop_body = self._emit_loop_body(name, body)
return f"var {self._mangle(name)} = function() {{ while(true) {{ {loop_body} }} }};"
val = self.emit(fn_expr) if fn_expr else "NIL"
val = self.emit(fn_expr) if fn_expr is not None else "NIL"
return f"var {self._mangle(name)} = {val};"
def _is_self_tail_recursive(self, name: str, body: list) -> bool:

1377
shared/sx/ref/js.sx Normal file

File diff suppressed because it is too large Load Diff

1182
shared/sx/ref/py.sx Normal file

File diff suppressed because it is too large Load Diff

101
shared/sx/ref/run_js_sx.py Normal file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""
Bootstrap runner: execute js.sx against spec files to produce sx-ref.js.
This is the G1 bootstrapper — js.sx (SX-to-JavaScript translator written in SX)
is loaded into the Python evaluator, which then uses it to translate the
spec .sx files into JavaScript.
The output (transpiled defines only) should be identical to what
bootstrap_js.py's JSEmitter produces.
Usage:
python run_js_sx.py > /tmp/sx_ref_g1.js
"""
from __future__ import annotations
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.types import Symbol
def load_js_sx() -> dict:
"""Load js.sx into an evaluator environment and return it."""
js_sx_path = os.path.join(_HERE, "js.sx")
with open(js_sx_path) as f:
source = f.read()
exprs = parse_all(source)
from shared.sx.evaluator import evaluate, make_env
env = make_env()
for expr in exprs:
evaluate(expr, env)
return env
def extract_defines(source: str) -> list[tuple[str, list]]:
"""Parse .sx source, return list of (name, define-expr) for top-level defines."""
exprs = parse_all(source)
defines = []
for expr in exprs:
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
if expr[0].name == "define":
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
defines.append((name, expr))
return defines
def main():
from shared.sx.evaluator import evaluate
# Load js.sx into evaluator
env = load_js_sx()
# Same file list and order as bootstrap_js.py compile_ref_to_js() with all adapters
sx_files = [
("eval.sx", "eval"),
("render.sx", "render (core)"),
("parser.sx", "parser"),
("adapter-html.sx", "adapter-html"),
("adapter-sx.sx", "adapter-sx"),
("adapter-dom.sx", "adapter-dom"),
("engine.sx", "engine"),
("orchestration.sx", "orchestration"),
("boot.sx", "boot"),
("deps.sx", "deps (component dependency analysis)"),
("router.sx", "router (client-side route matching)"),
("signals.sx", "signals (reactive signal runtime)"),
]
# Translate each spec file using js.sx
for filename, label in sx_files:
filepath = os.path.join(_HERE, filename)
if not os.path.exists(filepath):
continue
with open(filepath) as f:
src = f.read()
defines = extract_defines(src)
# Convert defines to SX-compatible format
sx_defines = [[name, expr] for name, expr in defines]
print(f"\n // === Transpiled from {label} ===\n")
env["_defines"] = sx_defines
result = evaluate(
[Symbol("js-translate-file"), Symbol("_defines")],
env,
)
print(result)
if __name__ == "__main__":
main()

123
shared/sx/ref/run_py_sx.py Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
Bootstrap runner: execute py.sx against spec files to produce sx_ref.py.
This is the G1 bootstrapper — py.sx (SX-to-Python translator written in SX)
is loaded into the Python evaluator, which then uses it to translate the
spec .sx files into Python.
The output should be identical to: python bootstrap_py.py
Usage:
python run_py_sx.py > sx_ref_g1.py
"""
from __future__ import annotations
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.types import Symbol
from shared.sx.ref.bootstrap_py import (
PREAMBLE, PLATFORM_PY, PRIMITIVES_PY_PRE, PRIMITIVES_PY_POST,
PLATFORM_DEPS_PY, FIXUPS_PY, CONTINUATIONS_PY,
ADAPTER_FILES, SPEC_MODULES,
_assemble_primitives_py, public_api_py,
)
def load_py_sx(evaluator_env: dict) -> dict:
"""Load py.sx into an evaluator environment and return it."""
py_sx_path = os.path.join(_HERE, "py.sx")
with open(py_sx_path) as f:
source = f.read()
exprs = parse_all(source)
# Import the evaluator
from shared.sx.evaluator import evaluate, make_env
env = make_env()
for expr in exprs:
evaluate(expr, env)
return env
def extract_defines(source: str) -> list[tuple[str, list]]:
"""Parse .sx source, return list of (name, define-expr) for top-level defines."""
exprs = parse_all(source)
defines = []
for expr in exprs:
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
if expr[0].name == "define":
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
defines.append((name, expr))
return defines
def main():
from shared.sx.evaluator import evaluate
# Load py.sx into evaluator
env = load_py_sx({})
# Get the py-translate-file function
py_translate_file = env.get("py-translate-file")
if py_translate_file is None:
print("ERROR: py-translate-file not found in py.sx environment", file=sys.stderr)
sys.exit(1)
# Same file list and order as bootstrap_py.py compile_ref_to_py()
sx_files = [
("eval.sx", "eval"),
("forms.sx", "forms (server definition forms)"),
("render.sx", "render (core)"),
("adapter-html.sx", "adapter-html"),
("adapter-sx.sx", "adapter-sx"),
("deps.sx", "deps (component dependency analysis)"),
("signals.sx", "signals (reactive signal runtime)"),
]
# Build output — static sections are identical
parts = []
parts.append(PREAMBLE)
parts.append(PLATFORM_PY)
parts.append(PRIMITIVES_PY_PRE)
parts.append(_assemble_primitives_py(None))
parts.append(PRIMITIVES_PY_POST)
parts.append(PLATFORM_DEPS_PY)
# Translate each spec file using py.sx
for filename, label in sx_files:
filepath = os.path.join(_HERE, filename)
if not os.path.exists(filepath):
continue
with open(filepath) as f:
src = f.read()
defines = extract_defines(src)
# Convert defines to SX-compatible format: list of [name, expr] pairs
sx_defines = [[name, expr] for name, expr in defines]
parts.append(f"\n# === Transpiled from {label} ===\n")
# Bind defines as data in env to avoid evaluator trying to execute AST
env["_defines"] = sx_defines
result = evaluate(
[Symbol("py-translate-file"), Symbol("_defines")],
env,
)
parts.append(result)
parts.append(FIXUPS_PY)
parts.append(public_api_py(True, True, True))
print("\n".join(parts))
if __name__ == "__main__":
main()

View File

@@ -1,143 +1,118 @@
;; SX docs layout defcomps — fully self-contained via IO primitives.
;; Registered via register_sx_layout in __init__.py.
;; --- Main nav defcomp: static nav items from MAIN_NAV ---
;; @css aria-selected:bg-violet-200 aria-selected:text-violet-900
(defcomp ~sx-main-nav (&key section)
(let* ((sc "aria-selected:bg-violet-200 aria-selected:text-violet-900")
(items (list
(dict :label "Docs" :href "/docs/introduction")
(dict :label "CSSX" :href "/cssx/")
(dict :label "Reference" :href "/reference/")
(dict :label "Protocols" :href "/protocols/wire-format")
(dict :label "Examples" :href "/examples/click-to-load")
(dict :label "Essays" :href "/essays/")
(dict :label "Philosophy" :href "/philosophy/")
(dict :label "Specs" :href "/specs/")
(dict :label "Bootstrappers" :href "/bootstrappers/")
(dict :label "Testing" :href "/testing/")
(dict :label "Isomorphism" :href "/isomorphism/")
(dict :label "Plans" :href "/plans/")
(dict :label "Reactive Islands" :href "/reactive-islands/"))))
(<> (map (lambda (item)
(~nav-link
:href (get item "href")
:label (get item "label")
:is-selected (when (= (get item "label") section) "true")
:select-colours sc))
items))))
;; --- SX header row ---
(defcomp ~sx-header-row (&key nav child oob)
(~menu-row-sx :id "sx-row" :level 1 :colour "violet"
:link-href "/" :link-label "sx"
:link-label-content (~sx-docs-label)
:nav nav
:child-id "sx-header-child"
:child child
:oob oob))
;; --- Sub-row for section pages ---
(defcomp ~sx-sub-row (&key sub-label sub-href sub-nav selected oob)
(~menu-row-sx :id "sx-sub-row" :level 2 :colour "violet"
:link-href sub-href :link-label sub-label
:selected selected
:nav sub-nav
:oob oob))
;; SX docs layout defcomps + in-page navigation.
;; Layout = root header only. Nav is in-page via ~sx-doc wrapper.
;; ---------------------------------------------------------------------------
;; SX home layout (root + sx header)
;; Nav components — logo header, sibling arrows, children links
;; ---------------------------------------------------------------------------
(defcomp ~sx-layout-full (&key section)
(<> (~root-header-auto)
(~sx-header-row :nav (~sx-main-nav :section section))))
;; @css text-violet-700 text-violet-600 text-violet-500 text-stone-400 text-stone-500 text-stone-600
;; @css hover:text-violet-600 hover:text-violet-700 hover:bg-violet-50
;; @css bg-violet-50 border-violet-200 border
(defcomp ~sx-layout-oob (&key section)
(<> (~sx-header-row
:nav (~sx-main-nav :section section)
:oob true)
(~clear-oob-div :id "sx-header-child")
(~root-header-auto true)))
;; Logo + tagline + copyright — always shown at top of page area.
(defcomp ~sx-header ()
(div :class "max-w-3xl mx-auto px-4 pt-8 pb-4 text-center"
(a :href "/"
:sx-get "/" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
:class "block mb-2"
(span :class "text-4xl font-bold font-mono text-violet-700" "(<sx>)"))
(p :class "text-lg text-stone-500 mb-1"
"Framework free reactive hypermedia")
(p :class "text-xs text-stone-400"
"© Giles Bradshaw 2026")))
(defcomp ~sx-layout-mobile (&key section)
(<> (~mobile-menu-section
:label "sx" :href "/" :level 1 :colour "violet"
:items (~sx-main-nav :section section))
(~root-mobile-auto)))
;; Current section with annotated prev/next siblings.
;; Desktop: prev ← Current → next (horizontal)
;; Mobile: stacked vertically
(defcomp ~nav-sibling-row (&key node siblings)
(let* ((idx (find-nav-index siblings node))
(count (len siblings))
(prev-idx (mod (+ (- idx 1) count) count))
(next-idx (mod (+ idx 1) count))
(prev-node (nth siblings prev-idx))
(next-node (nth siblings next-idx)))
(div :class "max-w-3xl mx-auto px-4 py-2 flex items-center justify-center gap-4"
(a :href (get prev-node "href")
:sx-get (get prev-node "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "text-sm text-stone-500 hover:text-violet-600"
(str "← " (get prev-node "label")))
(a :href (get node "href")
:sx-get (get node "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "text-lg font-semibold text-violet-700 px-4"
(get node "label"))
(a :href (get next-node "href")
:sx-get (get next-node "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "text-sm text-stone-500 hover:text-violet-600"
(str (get next-node "label") " →")))))
;; Children links — shown as clearly clickable buttons.
(defcomp ~nav-children (&key items)
(div :class "max-w-3xl mx-auto px-4 py-3"
(div :class "flex flex-wrap justify-center gap-2"
(map (fn (item)
(a :href (get item "href")
:sx-get (get item "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "px-3 py-1.5 text-sm rounded border border-violet-200 text-violet-700 hover:bg-violet-50 transition-colors"
(get item "label")))
items))))
;; ---------------------------------------------------------------------------
;; SX section layout (root + sx header + sub-row)
;; ~sx-doc — in-page content wrapper with nav
;; Used by every defpage :content to embed nav inside the page content area.
;; ---------------------------------------------------------------------------
(defcomp ~sx-section-layout-full (&key section sub-label sub-href sub-nav selected)
(<> (~root-header-auto)
(~sx-header-row
:nav (~sx-main-nav :section section)
:child (~sx-sub-row :sub-label sub-label :sub-href sub-href
:sub-nav sub-nav :selected selected))))
(defcomp ~sx-section-layout-oob (&key section sub-label sub-href sub-nav selected)
(<> (~oob-header-sx :parent-id "sx-header-child"
:row (~sx-sub-row :sub-label sub-label :sub-href sub-href
:sub-nav sub-nav :selected selected))
(~sx-header-row
:nav (~sx-main-nav :section section)
:oob true)
(~root-header-auto true)))
(defcomp ~sx-section-layout-mobile (&key section sub-label sub-href sub-nav)
(<>
(when sub-nav
(~mobile-menu-section
:label (or sub-label section) :href sub-href :level 2 :colour "violet"
:items sub-nav))
(~mobile-menu-section
:label "sx" :href "/" :level 1 :colour "violet"
:items (~sx-main-nav :section section))
(~root-mobile-auto)))
(defcomp ~sx-doc (&key path &rest children)
(let ((nav-state (resolve-nav-path sx-nav-tree (or path "/"))))
(<>
(div :id "sx-nav" :class "mb-6"
(~sx-header)
;; Sibling arrows for EVERY level in the trail
(map (fn (crumb)
(~nav-sibling-row
:node (get crumb "node")
:siblings (get crumb "siblings")))
(get nav-state "trail"))
;; Children as button links
(when (get nav-state "children")
(~nav-children :items (get nav-state "children"))))
;; Page content follows
children)))
;; ---------------------------------------------------------------------------
;; Standalone layouts (no root header, no auth — for sx-web.org)
;; SX docs layouts root header only (nav is in page content via ~sx-doc)
;; ---------------------------------------------------------------------------
(defcomp ~sx-standalone-layout-full (&key section)
(~sx-header-row :nav (~sx-main-nav :section section)))
(defcomp ~sx-docs-layout-full ()
(~root-header-auto))
(defcomp ~sx-standalone-layout-oob (&key section)
(<> (~sx-header-row
:nav (~sx-main-nav :section section)
:oob true)
(~clear-oob-div :id "sx-header-child")))
;; OOB: just update root header. Nav is in content via ~sx-doc.
(defcomp ~sx-docs-layout-oob ()
(~root-header-auto true))
(defcomp ~sx-standalone-layout-mobile (&key section)
(~mobile-menu-section
:label "sx" :href "/" :level 1 :colour "violet"
:items (~sx-main-nav :section section)))
;; Mobile: just root mobile nav. In-page nav is in content.
(defcomp ~sx-docs-layout-mobile ()
(~root-mobile-auto))
(defcomp ~sx-standalone-section-layout-full (&key section sub-label sub-href sub-nav selected)
(~sx-header-row
:nav (~sx-main-nav :section section)
:child (~sx-sub-row :sub-label sub-label :sub-href sub-href
:sub-nav sub-nav :selected selected)))
;; ---------------------------------------------------------------------------
;; Standalone layouts (no root header — for sx-web.org)
;; ---------------------------------------------------------------------------
(defcomp ~sx-standalone-section-layout-oob (&key section sub-label sub-href sub-nav selected)
(<> (~oob-header-sx :parent-id "sx-header-child"
:row (~sx-sub-row :sub-label sub-label :sub-href sub-href
:sub-nav sub-nav :selected selected))
(~sx-header-row
:nav (~sx-main-nav :section section)
:oob true)))
(defcomp ~sx-standalone-docs-layout-full ()
nil)
(defcomp ~sx-standalone-section-layout-mobile (&key section sub-label sub-href sub-nav)
(<>
(when sub-nav
(~mobile-menu-section
:label (or sub-label section) :href sub-href :level 2 :colour "violet"
:items sub-nav))
(~mobile-menu-section
:label "sx" :href "/" :level 1 :colour "violet"
:items (~sx-main-nav :section section))))
;; Standalone OOB: nothing needed — nav is in content.
(defcomp ~sx-standalone-docs-layout-oob ()
nil)
;; Standalone mobile: nothing — nav is in content.
(defcomp ~sx-standalone-docs-layout-mobile ()
nil)

View File

@@ -159,12 +159,24 @@
:summary "Live demo: #z3 translates SX spec declarations to SMT-LIB verification conditions.")
(dict :label "Theorem Prover" :href "/plans/theorem-prover"
:summary "prove.sx — constraint solver and property prover for SX primitives, written in SX.")
(dict :label "Self-Hosting Bootstrapper" :href "/plans/self-hosting-bootstrapper"
:summary "py.sx — an SX-to-Python translator written in SX. Complete: G0 == G1, 128/128 defines match.")
(dict :label "JS Bootstrapper" :href "/plans/js-bootstrapper"
:summary "js.sx — SX-to-JavaScript translator + ahead-of-time component compiler. Zero-runtime static sites.")
(dict :label "SX-Activity" :href "/plans/sx-activity"
:summary "A new web built on SX — executable content, shared components, parsers, and logic on IPFS, provenance on Bitcoin, all running within your own security context.")
(dict :label "Predictive Prefetching" :href "/plans/predictive-prefetch"
:summary "Prefetch missing component definitions before the user clicks — hover a link, fetch its deps, navigate client-side.")
(dict :label "Content-Addressed Components" :href "/plans/content-addressed-components"
:summary "Components identified by CID, stored on IPFS, fetched from anywhere. Canonical serialization, content verification, federated sharing.")
(dict :label "Environment Images" :href "/plans/environment-images"
:summary "Serialize evaluated environments as content-addressed images. Spec CID → image CID → every endpoint is fully executable and verifiable.")
(dict :label "Runtime Slicing" :href "/plans/runtime-slicing"
:summary "Tier the client runtime by need: L0 hypermedia (~5KB), L1 DOM ops (~8KB), L2 islands (~15KB), L3 full eval (~44KB). Sliced by slice.sx, translated by js.sx.")
(dict :label "Typed SX" :href "/plans/typed-sx"
:summary "Gradual type system for SX. Optional annotations, checked at registration time, zero runtime cost. types.sx — specced, bootstrapped, catches composition errors.")
(dict :label "Nav Redesign" :href "/plans/nav-redesign"
:summary "Replace menu bars with vertical breadcrumb navigation. Logo → section → page, arrows for siblings, children below. No dropdowns, no hamburger, infinite depth.")
(dict :label "Fragment Protocol" :href "/plans/fragment-protocol"
:summary "Structured sexp request/response for cross-service component transfer.")
(dict :label "Glue Decoupling" :href "/plans/glue-decoupling"
@@ -174,7 +186,9 @@
(dict :label "SX CI Pipeline" :href "/plans/sx-ci"
:summary "Build, test, and deploy in s-expressions — CI pipelines as SX components.")
(dict :label "Live Streaming" :href "/plans/live-streaming"
:summary "SSE and WebSocket transports for re-resolving suspense slots after initial page load — live data, real-time collaboration.")))
:summary "SSE and WebSocket transports for re-resolving suspense slots after initial page load — live data, real-time collaboration.")
(dict :label "sx-web Platform" :href "/plans/sx-web-platform"
:summary "sx-web.org as online development platform — embedded Claude Code, IPFS storage, sx-activity publishing, sx-ci testing. Author, stage, test, deploy from the browser.")))
(define reactive-islands-nav-items (list
(dict :label "Overview" :href "/reactive-islands/"
@@ -193,7 +207,9 @@
(define bootstrappers-nav-items (list
(dict :label "Overview" :href "/bootstrappers/")
(dict :label "JavaScript" :href "/bootstrappers/javascript")
(dict :label "Python" :href "/bootstrappers/python")))
(dict :label "Python" :href "/bootstrappers/python")
(dict :label "Self-Hosting (py.sx)" :href "/bootstrappers/self-hosting")
(dict :label "Self-Hosting JS (js.sx)" :href "/bootstrappers/self-hosting-js")))
;; Spec file registry — canonical metadata for spec viewer pages.
;; Python only handles file I/O (read-spec-file); all metadata lives here.
@@ -275,6 +291,7 @@
;; Generic section nav — builds nav links from a list of items.
;; Replaces _nav_items_sx() and all section-specific nav builders in utils.py.
;; KEPT for backward compat with other apps — SX docs uses ~nav-list instead.
(defcomp ~section-nav (&key items current)
(<> (map (fn (item)
(~nav-link
@@ -283,3 +300,88 @@
:is-selected (when (= (get item "label") current) "true")
:select-colours "aria-selected:bg-violet-200 aria-selected:text-violet-900"))
items)))
;; ---------------------------------------------------------------------------
;; Nav tree — hierarchical navigation for SX docs
;; ---------------------------------------------------------------------------
(define sx-nav-tree
{:label "sx" :href "/"
:children (list
{:label "Docs" :href "/docs/" :children docs-nav-items}
{:label "CSSX" :href "/cssx/" :children cssx-nav-items}
{:label "Reference" :href "/reference/" :children reference-nav-items}
{:label "Protocols" :href "/protocols/" :children protocols-nav-items}
{:label "Examples" :href "/examples/" :children examples-nav-items}
{:label "Essays" :href "/essays/" :children essays-nav-items}
{:label "Philosophy" :href "/philosophy/" :children philosophy-nav-items}
{:label "Specs" :href "/specs/" :children specs-nav-items}
{:label "Bootstrappers" :href "/bootstrappers/" :children bootstrappers-nav-items}
{:label "Testing" :href "/testing/" :children testing-nav-items}
{:label "Isomorphism" :href "/isomorphism/" :children isomorphism-nav-items}
{:label "Plans" :href "/plans/" :children plans-nav-items}
{:label "Reactive Islands" :href "/reactive-islands/" :children reactive-islands-nav-items})})
;; Resolve a URL path to a nav trail + children.
;; Returns {:trail [{:node N :siblings S} ...] :children [...] :depth N}
;; Trail is from outermost selected ancestor to deepest.
(define resolve-nav-path
(fn (tree path)
(let ((trail (list)))
(define walk
(fn (node)
(let ((children (get node "children")))
(when children
(let ((match (find-nav-match children path)))
(when match
(append! trail {:node match :siblings children})
;; Only recurse deeper if this wasn't an exact match
;; (exact match = we found our target, stop)
(when (not (= (get match "href") path))
(walk match))))))))
(walk tree)
(let ((depth (len trail)))
(if (= depth 0)
{:trail trail :children (get tree "children") :depth 0}
(let ((deepest (nth trail (- depth 1))))
{:trail trail
:children (get (get deepest "node") "children")
:depth depth}))))))
;; Find a nav item whose href matches the given path (or path prefix).
(define find-nav-match
(fn (items path)
;; Exact match first
(or (some (fn (item)
(when (= (get item "href") path) item))
items)
;; Prefix match: path starts with item href (for /plans/typed-sx matching /plans/)
(some (fn (item)
(let ((href (get item "href")))
(when (and (ends-with? href "/")
(starts-with? path href))
item)))
items)
;; Path contains section: /plans/typed-sx matches section with /plans/ children
(some (fn (item)
(let ((children (get item "children")))
(when children
(when (some (fn (child)
(= (get child "href") path))
children)
item))))
items))))
;; Find the index of a nav item in a list by matching href.
(define find-nav-index
(fn (items node)
(let ((target-href (get node "href"))
(count (len items)))
(define find-loop
(fn (i)
(if (>= i count)
0
(if (= (get (nth items i) "href") target-href)
i
(find-loop (+ i 1))))))
(find-loop 0))))

View File

@@ -0,0 +1,304 @@
;; ---------------------------------------------------------------------------
;; Content-Addressed Environment Images
;; ---------------------------------------------------------------------------
(defcomp ~plan-environment-images-content ()
(~doc-page :title "Content-Addressed Environment Images"
(~doc-section :title "The Idea" :id "idea"
(p "Every served SX endpoint should point back to its spec. The spec CIDs identify the exact evaluator, renderer, parser, and primitives that produced the output. This makes every endpoint " (strong "fully executable") " — anyone with the CIDs can independently reproduce the result.")
(p "But evaluating spec files from source on every cold start is wasteful. The specs are pure — same source always produces the same evaluated environment. So we can serialize the " (em "evaluated") " environment as a content-addressed image: all defcomps, defmacros, bound symbols, resolved closures frozen into a single artifact. The image CID is a function of its contents. Load the image, skip evaluation, get the same result.")
(p "The chain becomes:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Served page") " → CID of the spec that defines its semantics")
(li (strong "Spec CID") " → the evaluator, renderer, parser, primitives that any conforming host can execute")
(li (strong "Image CID") " → the pre-evaluated environment, a cache of (2) that any conforming host can deserialize"))
(p "The spec is the truth. The image is a verified cache. The bootstrapper that compiled the spec for a particular host is an implementation detail — irrelevant to the content address.")
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mt-4"
(p :class "text-violet-900 font-medium" "Prior art")
(p :class "text-violet-800" (a :href "https://github.com/KinaKnowledge/juno-lang" :class "underline" "Juno") " — a self-hosted Lisp-to-JS compiler — implements image persistence: serialize a running environment, restore it later, even bundle it as a standalone HTML document. Their Seedling IDE saves/restores entire development sessions as images. SX can do this more rigorously because our images are content-addressed (Juno's are not) and our components are boundary-enforced pure (Juno's are not).")))
;; -----------------------------------------------------------------------
;; What gets serialized
;; -----------------------------------------------------------------------
(~doc-section :title "What Gets Serialized" :id "what"
(p "An environment image is a snapshot of everything produced by evaluating the spec files. Not the source — the result.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Category")
(th :class "px-3 py-2 font-medium text-stone-600" "Contents")
(th :class "px-3 py-2 font-medium text-stone-600" "Source")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Components")
(td :class "px-3 py-2 text-stone-700" "All " (code "defcomp") " definitions — name, params, body AST, closure bindings, CID, deps, css_classes")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "Service .sx files + shared/sx/templates/"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Macros")
(td :class "px-3 py-2 text-stone-700" "All " (code "defmacro") " definitions — name, params, body AST, closure")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "Spec files"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Bindings")
(td :class "px-3 py-2 text-stone-700" "Top-level " (code "define") " values — constants, lookup tables, configuration")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "Spec files + service .sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Primitives")
(td :class "px-3 py-2 text-stone-700" "Registry of pure primitive names (not implementations — those are host-specific)")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "primitives.sx, boundary.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Spec provenance")
(td :class "px-3 py-2 text-stone-700" "CIDs of the spec files that produced this image")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "eval.sx, render.sx, parser.sx, ...")))))
(p "Notably " (strong "absent") " from the image:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "IO primitive implementations") " — these are host-specific. The image records their " (em "names") " (for boundary enforcement) but not their code.")
(li (strong "Page helpers") " — same reason. " (code "fetch-data") ", " (code "app-url") " etc. are registered by the host app at startup.")
(li (strong "Runtime state") " — no request context, no DB connections, no session data. The image is a pure function's result, not a running process snapshot.")))
;; -----------------------------------------------------------------------
;; Image format
;; -----------------------------------------------------------------------
(~doc-section :title "Image Format" :id "format"
(p "The image is itself an s-expression — the same format the spec is written in. This means the image can be parsed by the same parser, inspected by the same tools, and content-addressed by the same canonical serializer.")
(~doc-code :code (highlight "(sx-image\n :version 1\n :spec-cids {:eval \"bafy...eval\"\n :render \"bafy...render\"\n :parser \"bafy...parser\"\n :primitives \"bafy...prims\"\n :boundary \"bafy...boundary\"\n :signals \"bafy...signals\"}\n\n :components (\n (defcomp ~card (&key title subtitle &rest children)\n (div :class \"card\" (h2 title) (when subtitle (p subtitle)) children))\n (defcomp ~nav (&key items current)\n (nav :class \"nav\" (map (fn (item) ...) items)))\n ;; ... all registered components\n )\n\n :macros (\n (defmacro when (test &rest body)\n (list 'if test (cons 'begin body) nil))\n ;; ... all macros\n )\n\n :bindings (\n (define void-elements (list \"area\" \"base\" \"br\" \"col\" ...))\n (define boolean-attrs (list \"checked\" \"disabled\" ...))\n ;; ... all top-level defines\n )\n\n :primitive-names (\"str\" \"+\" \"-\" \"*\" \"/\" \"=\" \"<\" \">\" ...)\n :io-names (\"fetch-data\" \"call-action\" \"app-url\" ...))" "lisp"))
(p "The " (code ":spec-cids") " field is the key. It links this image back to the exact spec that produced it. Anyone can verify the image by:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "Fetch the spec files by CID")
(li "Evaluate them with a conforming evaluator")
(li "Serialize the resulting environment")
(li "Compare — it must produce the same image")))
;; -----------------------------------------------------------------------
;; Image CID
;; -----------------------------------------------------------------------
(~doc-section :title "Image CID" :id "image-cid"
(p "The image CID is computed by canonical-serializing the entire " (code "(sx-image ...)") " form and hashing it. Same process as component CIDs, just applied to the whole environment.")
(p "The relationship between spec CIDs and image CID is deterministic:")
(~doc-code :code (highlight ";; The image CID is a pure function of the spec CIDs\n;; (assuming a deterministic evaluator, which SX guarantees)\n(define image-cid-from-specs\n (fn (spec-cids)\n ;; 1. Fetch each spec file by CID\n ;; 2. Evaluate all specs in a fresh environment\n ;; 3. Extract components, macros, bindings\n ;; 4. Build (sx-image ...) form\n ;; 5. Canonical serialize\n ;; 6. Hash → CID\n ))" "lisp"))
(p "This means you can compute the expected image CID from the spec CIDs " (em "without") " having the image. If someone hands you an image claiming to be from spec " (code "bafy...eval") ", you can verify it by re-evaluating the spec and comparing CIDs. The image is a verifiable cache.")
(p "In practice, you'd only do this verification once per spec version. After that, the image CID is trusted by content-addressing — same bytes, same hash, forever."))
;; -----------------------------------------------------------------------
;; Endpoint provenance
;; -----------------------------------------------------------------------
(~doc-section :title "Endpoint Provenance" :id "provenance"
(p "Every served page gains a provenance header linking it to the spec that rendered it:")
(~doc-code :code (highlight "HTTP/1.1 200 OK\nContent-Type: text/html\nSX-Spec: bafy...eval,bafy...render,bafy...parser,bafy...prims\nSX-Image: bafy...image\nSX-Page-Components: ~card:bafy...card,~nav:bafy...nav" "http"))
(p "Three levels of verification:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Level")
(th :class "px-3 py-2 font-medium text-stone-600" "What you verify")
(th :class "px-3 py-2 font-medium text-stone-600" "Trust assumption")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Component")
(td :class "px-3 py-2 text-stone-700" "Fetch " (code "~card") " by CID, verify hash")
(td :class "px-3 py-2 text-stone-600" "Trust the evaluator"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Image")
(td :class "px-3 py-2 text-stone-700" "Fetch image by CID, deserialize, re-render page")
(td :class "px-3 py-2 text-stone-600" "Trust the image producer"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Spec")
(td :class "px-3 py-2 text-stone-700" "Fetch specs by CID, re-evaluate, compare image CID")
(td :class "px-3 py-2 text-stone-600" "Trust only the hash function")))))
(p "Level 3 is the nuclear option — full independent verification from source. It's expensive but proves the entire chain. Most consumers will operate at level 1 (component verification) or level 2 (image verification)."))
;; -----------------------------------------------------------------------
;; Cold start optimization
;; -----------------------------------------------------------------------
(~doc-section :title "Cold Start: Images as Cache" :id "cold-start"
(p "The practical motivation: evaluating all spec files + service components on every server restart is slow. An image eliminates this.")
(~doc-subsection :title "Server Startup"
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li "Check if a cached image exists for the current spec CIDs")
(li "If yes: deserialize the image (fast — parsing a single file, no evaluation)")
(li "If no: evaluate spec files from source, build image, cache it")
(li "Register IO primitives and page helpers (host-specific, not in image)")
(li "Ready to serve"))
(~doc-code :code (highlight "(define load-environment\n (fn (spec-cids image-cache-dir)\n (let ((expected-image-cid (image-cid-for-specs spec-cids))\n (cached-path (str image-cache-dir \"/\" expected-image-cid \".sx\")))\n (if (file-exists? cached-path)\n ;; Fast path: deserialize\n (let ((image (parse (read-file cached-path))))\n (if (= (verify-image-cid image) expected-image-cid)\n (deserialize-image image)\n ;; Cache corrupted — rebuild\n (build-and-cache-image spec-cids image-cache-dir)))\n ;; Cold path: evaluate from source\n (build-and-cache-image spec-cids image-cache-dir)))))" "lisp")))
(~doc-subsection :title "Client Boot"
(p "The client already caches component definitions in localStorage keyed by bundle hash. Images extend this: cache the entire evaluated environment, not just individual components.")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "Page ships " (code "SX-Image") " header with image CID")
(li "Client checks localStorage for " (code "sx-image:{cid}"))
(li "If hit: deserialize and boot (no component-by-component parsing)")
(li "If miss: fetch image from origin or IPFS, deserialize, cache")
(li "Same cache-forever semantics as component CIDs — content can't be stale"))
(p "First visit to any SX-powered site: one image fetch. Every subsequent visit: instant boot from cache. Cross-site: if two sites share the same spec CIDs, the image is shared too.")))
;; -----------------------------------------------------------------------
;; Standalone HTML
;; -----------------------------------------------------------------------
(~doc-section :title "Standalone HTML Bundles" :id "standalone"
(p "An image can be inlined into a single HTML document, producing a fully self-contained application with no server dependency:")
(~doc-code :code (highlight "<!doctype html>\n<html>\n<head>\n <script type=\"text/sx-image\">\n (sx-image\n :version 1\n :spec-cids {...}\n :components (...)\n :macros (...)\n :bindings (...))\n </script>\n <script type=\"text/sx-pages\">\n (defpage home :path \"/\" :content (~home-page))\n </script>\n <script src=\"sx-ref.js\"></script>\n</head>\n<body>\n <div id=\"app\"></div>\n <script>\n // Deserialize image, register components, render\n Sx.bootFromImage(document.querySelector('[type=\"text/sx-image\"]'))\n </script>\n</body>\n</html>" "html"))
(p "This document is its own CID. Pin it to IPFS and it's a permanent, executable, verifiable application. No origin server, no CDN, no DNS. The content network is the deployment target.")
(div :class "rounded border border-amber-200 bg-amber-50 p-4 mt-4"
(p :class "text-amber-900 font-medium" "Juno comparison")
(p :class "text-amber-800" "Juno's Seedling IDE already does this — export a running environment as a standalone HTML file. But their images are opaque JavaScript blobs serialized by the runtime. SX images are " (strong "s-expressions") " — parseable, inspectable, content-addressable. You can diff two SX images and see exactly what changed. You can extract a single component from an image by CID. You can merge images from different sources by composing their component lists. The format IS the tooling.")))
;; -----------------------------------------------------------------------
;; Namespace scoping
;; -----------------------------------------------------------------------
(~doc-section :title "Namespaced Environments" :id "namespaces"
(p "As the component library grows across services, a flat environment risks name collisions. Images provide a natural boundary for namespace scoping.")
(~doc-code :code (highlight "(sx-image\n :version 1\n :namespace \"market\"\n :spec-cids {...}\n :extends \"bafy...shared-image\" ;; inherits shared components\n :components (\n ;; market-specific components\n (defcomp ~product-card ...)\n (defcomp ~price-tag ...)\n ))" "lisp"))
(p "Resolution: " (code "market/~product-card") " → look in market image first, then fall through to the shared image (via " (code ":extends") "). Each service produces its own image, layered on top of the shared base.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Image")
(th :class "px-3 py-2 font-medium text-stone-600" "Contents")
(th :class "px-3 py-2 font-medium text-stone-600" "Extends")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "bafy...shared")
(td :class "px-3 py-2 text-stone-700" "~card, ~nav, ~section-nav, ~doc-page, ~doc-code — shared components from " (code "shared/sx/templates/"))
(td :class "px-3 py-2 text-stone-600" "None (root)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "bafy...blog")
(td :class "px-3 py-2 text-stone-700" "~post-card, ~post-body, ~tag-list — blog-specific from " (code "blog/sx/"))
(td :class "px-3 py-2 text-stone-600" "bafy...shared"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "bafy...market")
(td :class "px-3 py-2 text-stone-700" "~product-card, ~price-tag, ~cart-mini — market-specific from " (code "market/sx/"))
(td :class "px-3 py-2 text-stone-600" "bafy...shared"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "bafy...sx-docs")
(td :class "px-3 py-2 text-stone-700" "~doc-section, ~example-source, plans, essays — sx docs from " (code "sx/sx/"))
(td :class "px-3 py-2 text-stone-600" "bafy...shared")))))
(p "The " (code ":extends") " field is a CID, not a name. Image composition is content-addressed: changing the shared image produces a new shared CID, which invalidates all service images that extend it. Exactly the right cascading behavior."))
;; -----------------------------------------------------------------------
;; Spec → Image → Page chain
;; -----------------------------------------------------------------------
(~doc-section :title "The Verification Chain" :id "chain"
(p "The full provenance chain from served page back to source:")
(~doc-code :code (highlight ";; 1. Page served with provenance headers\n;;\n;; SX-Spec: bafy...eval,bafy...render,...\n;; SX-Image: bafy...market-image\n;; SX-Page: (defpage product :path \"/products/<slug>\" ...)\n;;\n;; 2. Verify image → spec\n;;\n;; Fetch specs by CID → evaluate → build image → compare CID\n;; If match: the image was correctly produced from these specs\n;;\n;; 3. Verify page → image\n;;\n;; Deserialize image → evaluate page defn → render\n;; If output matches served HTML: the page was correctly rendered\n;;\n;; 4. Trust chain terminates at the spec\n;;\n;; The spec is self-hosting (eval.sx evaluates itself)\n;; The spec's CID is its identity\n;; No external trust anchor needed beyond the hash function" "lisp"))
(p "This is stronger than code signing. Code signing says " (em "\"this entity vouches for this binary.\"") " Content addressing says " (em "\"this binary is the deterministic output of this source.\"") " No entity needed. No certificate authority. No revocation lists. Just math."))
;; -----------------------------------------------------------------------
;; Implementation phases
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: Image Serialization"
(p "Spec module " (code "image.sx") " — serialize and deserialize evaluated environments.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "serialize-environment") " — walk the env, extract components/macros/bindings, produce " (code "(sx-image ...)") " form")
(li (code "deserialize-image") " — parse image, reconstitute components/macros/bindings into env")
(li (code "image-cid") " — canonical-serialize the image form, hash → CID")
(li "Must handle closure serialization — component closures reference other components by name, which must be re-linked on deserialization")))
(~doc-subsection :title "Phase 2: Spec Provenance"
(p "Compute CIDs for all spec files at startup. Attach to environment metadata.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Hash each spec file's canonical source at load time")
(li "Store in env metadata as " (code ":spec-cids") " dict")
(li "Include in image serialization")))
(~doc-subsection :title "Phase 3: Server-Side Caching"
(p "Cache images on disk keyed by spec CIDs. Skip evaluation on warm restart.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "On startup: compute spec CIDs → derive expected image CID → check cache")
(li "Cache hit: deserialize (parse only, no eval)")
(li "Cache miss: evaluate specs, serialize image, write cache")
(li "Any spec file change → new spec CID → new image CID → cache miss → rebuild")))
(~doc-subsection :title "Phase 4: Client Images"
(p "Ship image CID in response headers. Client caches full env in localStorage.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "SX-Image") " response header with image CID")
(li "Client boot checks localStorage for cached image")
(li "Cache hit: deserialize, skip per-component fetch/parse")
(li "Cache miss: fetch image (single request), deserialize, cache")))
(~doc-subsection :title "Phase 5: Standalone Export"
(p "Generate self-contained HTML with inlined image. Pin to IPFS.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Inline " (code "(sx-image ...)") " as " (code "<script type=\"text/sx-image\">"))
(li "Inline page definitions as " (code "<script type=\"text/sx-pages\">"))
(li "Include sx-ref.js (or link to its CID)")
(li "The resulting HTML is a complete application — pin its CID to IPFS")))
(~doc-subsection :title "Phase 6: Namespaced Images"
(p "Per-service images with " (code ":extends") " for layered composition.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Shared image: components from " (code "shared/sx/templates/"))
(li "Service images: extend shared, add service-specific components")
(li "Resolution: service image → shared image → primitives")
(li "Image merge: combine two images with conflict detection"))))
;; -----------------------------------------------------------------------
;; Dependencies
;; -----------------------------------------------------------------------
(~doc-section :title "Dependencies" :id "dependencies"
(p "What must exist before this plan can execute:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Dependency")
(th :class "px-3 py-2 font-medium text-stone-600" "Status")
(th :class "px-3 py-2 font-medium text-stone-600" "Plan")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Canonical serialization")
(td :class "px-3 py-2 text-stone-700" (span :class "text-red-700 font-medium" "Not started"))
(td :class "px-3 py-2" (a :href "/plans/content-addressed-components" :class "text-violet-700 underline" "Content-Addressed Components") " Phase 1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Component CIDs")
(td :class "px-3 py-2 text-stone-700" (span :class "text-red-700 font-medium" "Not started"))
(td :class "px-3 py-2" (a :href "/plans/content-addressed-components" :class "text-violet-700 underline" "Content-Addressed Components") " Phase 2"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Purity verification")
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Complete"))
(td :class "px-3 py-2 text-stone-600" "deps.sx + boundary.py"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Self-hosting spec")
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Complete"))
(td :class "px-3 py-2 text-stone-600" "eval.sx, render.sx, parser.sx, ..."))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Self-hosting bootstrappers")
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Complete"))
(td :class "px-3 py-2 text-stone-600" "py.sx, js.sx — G0 == G1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "IPFS infrastructure")
(td :class "px-3 py-2 text-stone-700" (span :class "text-green-700 font-medium" "Exists"))
(td :class "px-3 py-2 text-stone-600" "artdag L1/L2, IPFSPin model")))))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Builds on: ") (a :href "/plans/content-addressed-components" :class "underline" "Content-Addressed Components") " (canonical serialization + CIDs), " (a :href "/plans/self-hosting-bootstrapper" :class "underline" "self-hosting bootstrappers") " (spec-first architecture). " (strong "Enables: ") (a :href "/plans/sx-activity" :class "underline" "SX-Activity") " (serverless applications on IPFS).")))))

View File

@@ -0,0 +1,511 @@
;; ---------------------------------------------------------------------------
;; js.sx — Self-Hosting JavaScript Bootstrapper
;; ---------------------------------------------------------------------------
(defcomp ~plan-js-bootstrapper-content ()
(~doc-page :title "js.sx — JavaScript Bootstrapper"
;; -----------------------------------------------------------------------
;; Overview
;; -----------------------------------------------------------------------
(~doc-section :title "Overview" :id "overview"
(p (code "bootstrap_js.py") " is a 4,361-line Python program that reads the "
(code ".sx") " spec files and emits " (code "sx-ref.js") " — the entire "
"browser runtime. Parser, evaluator, three rendering adapters (HTML, SX wire, DOM), "
"the engine (fetch/swap/trigger), orchestration, boot, signals, router, component "
"dependency analysis — all transpiled from " (code ".sx") " spec files into JavaScript.")
(p (code "js.sx") " replaces that Python program with an SX program. "
"Like " (code "py.sx") " for Python, " (code "js.sx") " is an SX-to-JavaScript "
"translator written in SX. But it goes further.")
(p "Because the JS bootstrapper produces " (em "browser code") ", " (code "js.sx")
" can also compile " (em "any") " SX component tree into a standalone JavaScript "
"program. The server evaluates an " (code ".sx") " page definition, calls "
(code "#js") " on the result, and gets a self-contained JS bundle that renders "
"the same output in the browser — no SX runtime needed."))
;; -----------------------------------------------------------------------
;; Two Modes
;; -----------------------------------------------------------------------
(~doc-section :title "Two Compilation Modes" :id "modes"
(~doc-subsection :title "Mode 1: Spec Bootstrapper"
(p "Same job as " (code "bootstrap_js.py") ". Read spec " (code ".sx") " files, "
"emit " (code "sx-ref.js") ".")
(~doc-code :code (highlight ";; Translate eval.sx to JavaScript
(js-translate-file (parse-file \"eval.sx\"))
;; → \"function evalExpr(expr, env) { ... }\"
;; Full bootstrap: all spec modules → sx-ref.js
(js-bootstrap
:adapters (list \"html\" \"sx\" \"dom\" \"engine\" \"boot\")
:modules (list \"deps\" \"router\" \"signals\"))" "lisp"))
(p "The output is identical to " (code "python bootstrap_js.py") ". "
"Verification: " (code "diff <(python bootstrap_js.py) <(python run_js_sx.py)") "."))
(~doc-subsection :title "Mode 2: Component Compiler"
(p "Server-side SX evaluation + " (code "js.sx") " translation = static JS output. "
"Given a component tree that the server has already evaluated (data fetched, "
"conditionals resolved, loops expanded), " (code "js.sx") " compiles the "
"resulting DOM description into a JavaScript program that builds the same DOM.")
(~doc-code :code (highlight ";; Server evaluates the page (fetches data, expands components)
;; Result is a resolved SX tree: (div :class \"...\" (h1 \"Hello\") ...)
;; js.sx compiles that tree to standalone JS
(js-compile-component evaluated-tree)
;; → \"(function(){
;; var el = document.createElement('div');
;; el.className = '...';
;; var h1 = document.createElement('h1');
;; h1.textContent = 'Hello';
;; el.appendChild(h1);
;; return el;
;; })()\"" "lisp"))
(p "This is ahead-of-time compilation. The browser receives JavaScript, "
"not s-expressions. No parser, no evaluator, no runtime. "
"Just DOM construction code.")))
;; -----------------------------------------------------------------------
;; Architecture
;; -----------------------------------------------------------------------
(~doc-section :title "Architecture" :id "architecture"
(p "The JS bootstrapper has more moving parts than the Python one because "
"JavaScript is the " (em "client") " host. The browser runtime includes "
"things Python never needs:")
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
(tr
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Spec Module")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Purpose")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Python?")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Browser?")))
(tbody
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "eval.sx")
(td :class "px-4 py-2" "Core evaluator, special forms, TCO")
(td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "render.sx")
(td :class "px-4 py-2" "Tag registry, void elements, boolean attrs")
(td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "parser.sx")
(td :class "px-4 py-2" "Tokenizer, parser, serializer")
(td :class "px-4 py-2 text-center" "—") (td :class "px-4 py-2 text-center" "Yes"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "adapter-html.sx")
(td :class "px-4 py-2" "Render to HTML string")
(td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "adapter-sx.sx")
(td :class "px-4 py-2" "Serialize to SX wire format")
(td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
(tr :class "border-t border-stone-100 bg-blue-50"
(td :class "px-4 py-2 font-mono" "adapter-dom.sx")
(td :class "px-4 py-2" "Render to live DOM nodes")
(td :class "px-4 py-2 text-center" "—") (td :class "px-4 py-2 text-center" "Yes"))
(tr :class "border-t border-stone-100 bg-blue-50"
(td :class "px-4 py-2 font-mono" "engine.sx")
(td :class "px-4 py-2" "Fetch, swap, trigger, history")
(td :class "px-4 py-2 text-center" "—") (td :class "px-4 py-2 text-center" "Yes"))
(tr :class "border-t border-stone-100 bg-blue-50"
(td :class "px-4 py-2 font-mono" "orchestration.sx")
(td :class "px-4 py-2" "Element scanning, attribute processing")
(td :class "px-4 py-2 text-center" "—") (td :class "px-4 py-2 text-center" "Yes"))
(tr :class "border-t border-stone-100 bg-blue-50"
(td :class "px-4 py-2 font-mono" "boot.sx")
(td :class "px-4 py-2" "Script processing, mount, hydration")
(td :class "px-4 py-2 text-center" "—") (td :class "px-4 py-2 text-center" "Yes"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "signals.sx")
(td :class "px-4 py-2" "Reactive signal runtime")
(td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "deps.sx")
(td :class "px-4 py-2" "Component dependency analysis")
(td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "router.sx")
(td :class "px-4 py-2" "Client-side route matching")
(td :class "px-4 py-2 text-center" "Yes") (td :class "px-4 py-2 text-center" "Yes")))))
(p "Blue rows are browser-only modules. " (code "js.sx") " must handle all of them. "
"The platform interface is also larger: DOM primitives (" (code "dom-create-element")
", " (code "dom-append") ", " (code "dom-set-attr") ", ...), "
"browser APIs (" (code "fetch") ", " (code "history") ", " (code "localStorage")
"), and event handling."))
;; -----------------------------------------------------------------------
;; Translation Rules
;; -----------------------------------------------------------------------
(~doc-section :title "Translation Rules" :id "translation"
(p (code "js.sx") " shares the same pattern as " (code "py.sx") " — expression translator, "
"statement translator, name mangling — but with JavaScript-specific mappings:")
(~doc-subsection :title "Name Mangling"
(p "SX uses kebab-case. JavaScript uses camelCase.")
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
(tr
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "SX")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "JavaScript")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Rule")))
(tbody
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "eval-expr")
(td :class "px-4 py-2 font-mono" "evalExpr")
(td :class "px-4 py-2" "kebab → camelCase"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "nil?")
(td :class "px-4 py-2 font-mono" "isNil")
(td :class "px-4 py-2" "predicate → is-prefix"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "empty?")
(td :class "px-4 py-2 font-mono" "isEmpty")
(td :class "px-4 py-2" "? → is-prefix (general)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "set!")
(td :class "px-4 py-2 font-mono" "—")
(td :class "px-4 py-2" "assignment (no rename)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "dom-create-element")
(td :class "px-4 py-2 font-mono" "domCreateElement")
(td :class "px-4 py-2" "platform function rename"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "delete")
(td :class "px-4 py-2 font-mono" "delete_")
(td :class "px-4 py-2" "JS reserved word escape"))))))
(~doc-subsection :title "Special Forms → JavaScript"
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
(tr
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "SX")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "JavaScript")))
(tbody
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(if c t e)")
(td :class "px-4 py-2 font-mono" "(sxTruthy(c) ? t : e)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(when c body)")
(td :class "px-4 py-2 font-mono" "(sxTruthy(c) ? body : NIL)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(let ((a 1)) body)")
(td :class "px-4 py-2 font-mono" "(function(a) { return body; })(1)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(fn (x) body)")
(td :class "px-4 py-2 font-mono" "function(x) { return body; }"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(define name val)")
(td :class "px-4 py-2 font-mono" "var name = val;"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(and a b c)")
(td :class "px-4 py-2 font-mono" "(sxTruthy(a) ? (sxTruthy(b) ? c : b) : a)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(case x \"a\" 1 ...)")
(td :class "px-4 py-2 font-mono" "sxCase(x, [[\"a\", () => 1], ...])"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(str a b c)")
(td :class "px-4 py-2 font-mono" "sxStr(a, b, c)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "&rest args")
(td :class "px-4 py-2 font-mono" "...args (rest params)"))))))
(~doc-subsection :title "JavaScript Advantages"
(p "JavaScript is easier to target than Python in two key ways:")
(ul :class "list-disc pl-6 space-y-2 text-stone-700"
(li (strong "No mutation problem. ")
"JavaScript closures capture by reference, not by value. "
(code "set!") " from a nested function Just Works — no cell variable "
"hack needed. This eliminates the hardest part of " (code "py.sx") ".")
(li (strong "Expression-oriented. ")
"JavaScript's comma operator, ternary, and IIFEs make "
"almost everything expressible as an expression. "
"The statement/expression duality is less painful than Python."))))
;; -----------------------------------------------------------------------
;; Component Compilation
;; -----------------------------------------------------------------------
(~doc-section :title "Component Compilation" :id "component-compiler"
(p "Mode 2 is the interesting one. The server already evaluates SX page "
"definitions — it fetches data, resolves conditionals, expands components, "
"and produces a complete DOM description as an SX tree. Currently this tree "
"is either:")
(ul :class "list-disc pl-6 space-y-1 text-stone-700"
(li "Rendered to HTML server-side (" (code "render-to-html") ")")
(li "Serialized as SX wire format for the client to render (" (code "aser") ")"))
(p "A third option: " (strong "compile it to JavaScript") ". "
"The SX tree is already fully resolved — no data to fetch, no conditionals "
"to evaluate. It's just a description of DOM nodes. " (code "js.sx")
" walks this tree and emits imperative JavaScript that constructs the same DOM.")
(~doc-subsection :title "What Gets Compiled"
(p "A resolved SX tree like:")
(~doc-code :code (highlight "(div :class \"container\"
(h1 \"Hello\")
(ul (map (fn (item)
(li :class \"item\" (get item \"name\")))
items)))" "lisp"))
(p "After server-side evaluation (with " (code "items") " = "
(code "[{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]") "):")
(~doc-code :code (highlight "(div :class \"container\"
(h1 \"Hello\")
(ul
(li :class \"item\" \"Alice\")
(li :class \"item\" \"Bob\")))" "lisp"))
(p "Compiles to:")
(~doc-code :code (highlight "var _0 = document.createElement('div');
_0.className = 'container';
var _1 = document.createElement('h1');
_1.textContent = 'Hello';
_0.appendChild(_1);
var _2 = document.createElement('ul');
var _3 = document.createElement('li');
_3.className = 'item';
_3.textContent = 'Alice';
_2.appendChild(_3);
var _4 = document.createElement('li');
_4.className = 'item';
_4.textContent = 'Bob';
_2.appendChild(_4);
_0.appendChild(_2);" "javascript")))
(~doc-subsection :title "Why Not Just Use HTML?"
(p "HTML already does this — " (code "innerHTML") " parses and builds DOM. "
"Why compile to JS instead?")
(ul :class "list-disc pl-6 space-y-2 text-stone-700"
(li (strong "Event handlers. ")
"HTML can't express " (code ":on-click") " or " (code ":sx-get")
" — those need JavaScript. The compiled JS can wire up event "
"listeners inline during construction.")
(li (strong "Reactive islands. ")
"Signal bindings (" (code "deref") "), reactive text nodes, and "
"reactive attributes need to register subscriptions during construction. "
"Compiled JS can create signals and wire subscriptions as it builds the DOM.")
(li (strong "No parse overhead. ")
"The browser doesn't need to parse HTML or SX source. "
"The JavaScript engine JIT-compiles the DOM construction code. "
"For large pages, this can be faster than " (code "innerHTML") ".")
(li (strong "Tree-shakeable. ")
"The compiled output only contains what the page uses. "
"No SX parser, no evaluator, no rendering runtime. "
"A static page compiles to pure DOM API calls — zero framework overhead.")
(li (strong "Portable. ")
"The output is a JavaScript module. It works in any JS environment: "
"browser, Node, Deno, Bun. Server-side rendered pages become "
"testable JavaScript programs.")))
(~doc-subsection :title "Hybrid Mode"
(p "Not every page is fully static. Some parts are server-rendered, "
"some are interactive. " (code "js.sx") " handles this with a hybrid approach:")
(ul :class "list-disc pl-6 space-y-2 text-stone-700"
(li (strong "Static subtrees") " → compiled to DOM construction code (no runtime)")
(li (strong "Reactive islands") " → compiled with signal creation + subscriptions "
"(needs signal runtime, ~2KB)")
(li (strong "Hypermedia attributes") " (" (code "sx-get") ", " (code "sx-post")
") → compiled with event listeners + fetch calls "
"(needs engine, ~5KB)")
(li (strong "Client-routed pages") " → full SX runtime included"))
(p "The compiler analyzes the tree and includes only the runtime slices needed. "
"A purely static page ships zero SX runtime. "
"A page with one reactive counter ships just the signal runtime.")))
;; -----------------------------------------------------------------------
;; The Bootstrap Chain
;; -----------------------------------------------------------------------
(~doc-section :title "The Bootstrap Chain" :id "chain"
(p "With both " (code "py.sx") " and " (code "js.sx") ", the full picture:")
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
(tr
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Translator")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Written in")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Outputs")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Replaces")))
(tbody
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "z3.sx")
(td :class "px-4 py-2" "SX")
(td :class "px-4 py-2" "SMT-LIB")
(td :class "px-4 py-2 text-stone-400 italic" "(none — new capability)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "prove.sx")
(td :class "px-4 py-2" "SX")
(td :class "px-4 py-2" "Constraint proofs")
(td :class "px-4 py-2 text-stone-400 italic" "(none — new capability)"))
(tr :class "border-t border-stone-100 bg-violet-50"
(td :class "px-4 py-2 font-mono text-violet-700" "py.sx")
(td :class "px-4 py-2" "SX")
(td :class "px-4 py-2" "Python")
(td :class "px-4 py-2 font-mono" "bootstrap_py.py"))
(tr :class "border-t border-stone-100 bg-blue-50"
(td :class "px-4 py-2 font-mono text-blue-700" "js.sx")
(td :class "px-4 py-2" "SX")
(td :class "px-4 py-2" "JavaScript")
(td :class "px-4 py-2 font-mono" "bootstrap_js.py"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono text-stone-400" "go.sx")
(td :class "px-4 py-2" "SX")
(td :class "px-4 py-2" "Go")
(td :class "px-4 py-2 text-stone-400 italic" "(future host)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono text-stone-400" "rs.sx")
(td :class "px-4 py-2" "SX")
(td :class "px-4 py-2" "Rust")
(td :class "px-4 py-2 text-stone-400 italic" "(future host)")))))
(p "Every translator is an SX program. The only Python left is the platform "
"interface (types, DOM primitives, runtime support functions) and the thin "
"runner script that loads " (code "py.sx") " or " (code "js.sx")
" and feeds it the spec files."))
;; -----------------------------------------------------------------------
;; Implementation Plan
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: Expression Translator"
(p "Core SX-to-JavaScript expression translation.")
(ul :class "list-disc pl-6 space-y-1 text-stone-700"
(li (code "js-mangle") " — SX name → JavaScript identifier (RENAMES + kebab→camelCase)")
(li (code "js-literal") " — atoms: numbers, strings, booleans, nil, symbols, keywords")
(li (code "js-expr") " — recursive expression translator")
(li "Ternary: " (code "if") ", " (code "when") ", " (code "cond") ", " (code "and") ", " (code "or"))
(li (code "let") " → IIFE: " (code "(function(a) { return body; })(val)"))
(li (code "fn") " → " (code "function(x) { return body; }"))
(li (code "str") " → " (code "sxStr(...)"))
(li "Infix: " (code "+") ", " (code "-") ", " (code "*") ", " (code "/") ", "
(code "===") ", " (code "!==") ", " (code "%"))
(li (code "&rest") " → " (code "...args") " (rest parameters)")))
(~doc-subsection :title "Phase 2: Statement Translator"
(p "Top-level and function body statement emission.")
(ul :class "list-disc pl-6 space-y-1 text-stone-700"
(li (code "js-statement") " — emit as JavaScript statement")
(li (code "define") " → " (code "var name = expr;"))
(li (code "set!") " → direct assignment (closures capture by reference)")
(li (code "for-each") " → " (code "for (var i = 0; i < arr.length; i++)") " loop")
(li (code "do") "/" (code "begin") " → comma expression or block")
(li "Function bodies with multiple expressions → explicit " (code "return"))))
(~doc-subsection :title "Phase 3: Spec Bootstrapper"
(p "Process spec files identically to " (code "bootstrap_js.py") ".")
(ul :class "list-disc pl-6 space-y-1 text-stone-700"
(li (code "js-extract-defines") " — parse .sx source, collect top-level defines")
(li (code "js-translate-file") " — translate a list of define expressions")
(li "Adapter selection: parser, html, sx, dom, engine, orchestration, boot")
(li "Dependency resolution: engine requires dom, boot requires engine + parser")
(li "Static sections (IIFE wrapper, platform interface) stay as string templates")))
(~doc-subsection :title "Phase 4: Component Compiler"
(p "Ahead-of-time compilation of evaluated SX trees to JavaScript.")
(ul :class "list-disc pl-6 space-y-1 text-stone-700"
(li (code "js-compile-element") " — emit " (code "createElement") " + attribute setting")
(li (code "js-compile-text") " — emit " (code "textContent") " or " (code "createTextNode"))
(li (code "js-compile-component") " — inline-expand or emit component call")
(li (code "js-compile-island") " — emit signal creation + reactive DOM subscriptions")
(li (code "js-compile-fragment") " — emit " (code "DocumentFragment") " construction")
(li "Runtime slicing: analyze tree → include only necessary runtime modules")))
(~doc-subsection :title "Phase 5: Verification"
(~doc-code :code (highlight "# Mode 1: spec bootstrapper parity
python bootstrap_js.py > sx-ref-g0.js
python run_js_sx.py > sx-ref-g1.js
diff sx-ref-g0.js sx-ref-g1.js # must be empty
# Mode 2: component compilation correctness
# Server renders page → SX tree → compile to JS
# Compare DOM output: runtime-rendered vs compiled
python test_js_compile.py # renders both, diffs DOM" "bash")))
;; -----------------------------------------------------------------------
;; Comparison with py.sx
;; -----------------------------------------------------------------------
(~doc-section :title "Comparison with py.sx" :id "comparison"
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
(tr
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Concern")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" (code "py.sx"))
(th :class "px-4 py-2 text-left font-semibold text-stone-700" (code "js.sx"))))
(tbody
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2" "Naming convention")
(td :class "px-4 py-2" "snake_case")
(td :class "px-4 py-2" "camelCase"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2" "Closures & mutation")
(td :class "px-4 py-2" "Cell variable hack")
(td :class "px-4 py-2" "Direct (reference capture)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2" "Spec modules")
(td :class "px-4 py-2" "eval, render, html, sx, deps, signals")
(td :class "px-4 py-2" "All 12 modules"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2" "Platform interface")
(td :class "px-4 py-2" "~300 lines")
(td :class "px-4 py-2" "~1500 lines (DOM, browser APIs)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2" "RENAMES table")
(td :class "px-4 py-2" "~200 entries")
(td :class "px-4 py-2" "~350 entries"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2" "Component compilation")
(td :class "px-4 py-2 text-stone-400" "N/A")
(td :class "px-4 py-2" "Ahead-of-time DOM compiler"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2" "Estimated size")
(td :class "px-4 py-2" "~800-1000 lines")
(td :class "px-4 py-2" "~1200-1500 lines"))))))
;; -----------------------------------------------------------------------
;; Implications
;; -----------------------------------------------------------------------
(~doc-section :title "Implications" :id "implications"
(~doc-subsection :title "Zero-Runtime Static Sites"
(p "A static page written in SX compiles to a JavaScript program with "
"no SX runtime dependency. The output is just DOM API calls — "
(code "createElement") ", " (code "appendChild") ", " (code "textContent")
". This gives SX a compilation target competitive with Svelte's "
"approach: components compile away, the framework disappears.")
(p "Combined with the " (a :href "/plans/content-addressed-components" "content-addressed components")
" plan, a page's compiled JS could be stored on IPFS by its content hash. "
"The server returns a CID. The browser fetches and executes pre-compiled JavaScript. "
"No parser, no evaluator, no network round-trip for component definitions."))
(~doc-subsection :title "Progressive Enhancement Layers"
(p "The component compiler naturally supports progressive enhancement:")
(ol :class "list-decimal pl-6 space-y-1 text-stone-700"
(li (strong "HTML") " — server renders to HTML string. No JS needed. Works everywhere.")
(li (strong "Compiled JS") " — server compiles to DOM construction code. "
"Event handlers work. No SX runtime. Kilobytes, not megabytes.")
(li (strong "SX runtime") " — full evaluator + engine. Client-side routing, "
"component caching, reactive islands. The current architecture.")
(li (strong "SX + signals") " — full reactive islands. Fine-grained DOM updates."))
(p "Each layer adds capability and weight. The right layer depends on the page. "
"A blog post needs layer 1. An interactive form needs layer 2. "
"A single-page app needs layer 3. A real-time dashboard needs layer 4. "
(code "js.sx") " makes layer 2 possible — it didn't exist before."))
(~doc-subsection :title "The Bootstrap Completion"
(p "With " (code "py.sx") " and " (code "js.sx") " both written in SX:")
(ul :class "list-disc pl-6 space-y-2 text-stone-700"
(li "The " (em "spec") " defines SX semantics (" (code "eval.sx") ", " (code "render.sx") ", ...)")
(li "The " (em "translators") " convert the spec to host languages (" (code "py.sx") ", " (code "js.sx") ")")
(li "The " (em "prover") " verifies the spec's properties (" (code "z3.sx") ", " (code "prove.sx") ")")
(li "All four are written " (em "in") " SX, executable " (em "by") " any SX evaluator"))
(p "The language defines itself, verifies itself, and compiles itself. "
"The Python and JavaScript \"bootstrappers\" are not programs that produce SX — "
"they are SX programs that produce Python and JavaScript. "
"The arrow points the other way."))))))

245
sx/sx/plans/nav-redesign.sx Normal file
View File

@@ -0,0 +1,245 @@
;; ---------------------------------------------------------------------------
;; Navigation Redesign — SX Docs
;; ---------------------------------------------------------------------------
(defcomp ~plan-nav-redesign-content ()
(~doc-page :title "Navigation Redesign"
(~doc-section :title "The Problem" :id "problem"
(p "The current navigation is a horizontal menu bar system: root bar, sx bar, sub-section bar. 13 top-level sections crammed into a scrolling horizontal row. Hover to see dropdowns. Click a section, get a second bar underneath. Click a page, get a third bar. Three stacked bars eating vertical space on every page.")
(p "It's a conventional web pattern and it's bad for this site. SX docs has a deep hierarchy — sections contain subsections contain pages. Horizontal bars can't express depth. They flatten everything into one level and hide the rest behind hover states that don't work on mobile, that obscure content, that require spatial memory of where things are.")
(p "The new nav is vertical, hierarchical, and infinite. No dropdowns. No menu bars. Just a centered breadcrumb trail that expands downward as you drill in."))
;; -----------------------------------------------------------------------
;; Design
;; -----------------------------------------------------------------------
(~doc-section :title "Design" :id "design"
(~doc-subsection :title "Structure"
(p "One vertical column, centered. Each level is a row.")
(~doc-code :code (highlight ";; Home (nothing selected)\n;;\n;; [ sx ]\n;;\n;; Docs CSSX Reference Protocols Examples\n;; Essays Philosophy Specs Bootstrappers\n;; Testing Isomorphism Plans Reactive Islands\n\n\n;; Section selected (e.g. Plans)\n;;\n;; [ sx ]\n;;\n;; < Plans >\n;;\n;; Status Reader Macros Theorem Prover\n;; Self-Hosting JS Bootstrapper SX-Activity\n;; Predictive Prefetching Content-Addressed\n;; Environment Images Runtime Slicing Typed SX\n;; Fragment Protocol ...\n\n\n;; Page selected (e.g. Typed SX under Plans)\n;;\n;; [ sx ]\n;;\n;; < Plans >\n;;\n;; < Typed SX >\n;;\n;; [ page content here ]" "lisp")))
(~doc-subsection :title "Rules"
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Logo at top, centered.") " Always visible. Click = home. The only fixed element.")
(li (strong "Level 1: section list.") " Shown on home page as a wrapped, centered list of links. This is the full menu — no hiding, no hamburger.")
(li (strong "When a section is selected:") " Section name replaces the list. Left arrow and right arrow for sibling navigation (previous/next section). The section's children appear as a new list below.")
(li (strong "When a child is selected:") " Same pattern — child name replaces the list, arrows for siblings, sub-children appear below. Recurse ad infinitum.")
(li (strong "Breadcrumb trail.") " Each selected level stays visible as a single row above the current level. The trail is: logo → section → subsection → page. Each row has arrows. Click any ancestor to navigate up.")
(li (strong "No dropdowns.") " Never. Hover does nothing special. The hierarchy is always visible in the breadcrumb trail.")
(li (strong "No hamburger menu.") " The nav IS the page on the home/index views. On content pages, the breadcrumb trail is compact enough to show without hiding.")
(li (strong "Responsive by default.") " Vertical + centered + wrapped = works at any width. No breakpoint-specific layout needed."))))
;; -----------------------------------------------------------------------
;; Visual language
;; -----------------------------------------------------------------------
(~doc-section :title "Visual Language" :id "visual"
(~doc-subsection :title "Levels"
(p "Each level has decreasing visual weight:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Level")
(th :class "px-3 py-2 font-medium text-stone-600" "Selected state")
(th :class "px-3 py-2 font-medium text-stone-600" "List state")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Logo")
(td :class "px-3 py-2 text-stone-700" "Large, violet, always visible")
(td :class "px-3 py-2 text-stone-600" "—"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Section")
(td :class "px-3 py-2 text-stone-700" "Medium text, violet-700, arrows")
(td :class "px-3 py-2 text-stone-600" "Medium text, stone-600, wrapped inline"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Subsection")
(td :class "px-3 py-2 text-stone-700" "Smaller text, violet-600, arrows")
(td :class "px-3 py-2 text-stone-600" "Small text, stone-500, wrapped inline"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Level 3+")
(td :class "px-3 py-2 text-stone-700" "Same as subsection")
(td :class "px-3 py-2 text-stone-600" "Same as subsection"))))))
(~doc-subsection :title "Arrows"
(p "Left and right arrows are inline with the selected item name. They navigate to the previous/next sibling in the current list. Keyboard accessible: left/right arrow keys when the row is focused.")
(~doc-code :code (highlight ";; Arrow rendering\n;;\n;; < Plans >\n;;\n;; < is a link to /plans/content-addressed-components\n;; (the previous sibling in plans-nav-items)\n;; > is a link to /plans/fragment-protocol\n;; (the next sibling)\n;; \"Plans\" is a link to /plans/ (the section index)\n;;\n;; At the edges, the arrow wraps:\n;; first item: < wraps to last\n;; last item: > wraps to first" "lisp")))
(~doc-subsection :title "Transitions"
(p "Selecting an item: the list fades/collapses, the selected item moves to breadcrumb position, children appear below. This is an L0 morph — the server renders the new state, the client morphs. No JS animation library needed, just CSS transitions on the morph targets.")
(p "Going up: click an ancestor in the breadcrumb. Its children (the level below) expand back into a list. Reverse of the drill-down.")))
;; -----------------------------------------------------------------------
;; Data model
;; -----------------------------------------------------------------------
(~doc-section :title "Data Model" :id "data"
(p "The current nav data is flat — each section has its own " (code "define") ". The new model is a single tree:")
(~doc-code :code (highlight "(define sx-nav-tree\n {:label \"sx\"\n :href \"/\"\n :children (list\n {:label \"Docs\"\n :href \"/docs/introduction\"\n :children docs-nav-items}\n {:label \"CSSX\"\n :href \"/cssx/\"\n :children cssx-nav-items}\n {:label \"Reference\"\n :href \"/reference/\"\n :children reference-nav-items}\n {:label \"Protocols\"\n :href \"/protocols/wire-format\"\n :children protocols-nav-items}\n {:label \"Examples\"\n :href \"/examples/click-to-load\"\n :children examples-nav-items}\n {:label \"Essays\"\n :href \"/essays/\"\n :children essays-nav-items}\n {:label \"Philosophy\"\n :href \"/philosophy/sx-manifesto\"\n :children philosophy-nav-items}\n {:label \"Specs\"\n :href \"/specs/\"\n :children specs-nav-items}\n {:label \"Bootstrappers\"\n :href \"/bootstrappers/\"\n :children bootstrappers-nav-items}\n {:label \"Testing\"\n :href \"/testing/\"\n :children testing-nav-items}\n {:label \"Isomorphism\"\n :href \"/isomorphism/\"\n :children isomorphism-nav-items}\n {:label \"Plans\"\n :href \"/plans/\"\n :children plans-nav-items}\n {:label \"Reactive Islands\"\n :href \"/reactive-islands/\"\n :children reactive-islands-nav-items})})" "lisp"))
(p "The existing per-section lists (" (code "docs-nav-items") ", " (code "plans-nav-items") ", etc.) remain unchanged — they just become the " (code ":children") " of tree nodes. Sub-sections that have their own sub-items can nest further:")
(~doc-code :code (highlight ";; Future: deeper nesting\n{:label \"Plans\"\n :href \"/plans/\"\n :children (list\n {:label \"Status\" :href \"/plans/status\"}\n {:label \"Bootstrappers\" :href \"/plans/self-hosting-bootstrapper\"\n :children (list\n {:label \"py.sx\" :href \"/plans/self-hosting-bootstrapper\"}\n {:label \"js.sx\" :href \"/plans/js-bootstrapper\"})}\n ;; ...\n )}" "lisp"))
(p "The tree depth is unlimited. The nav component recurses."))
;; -----------------------------------------------------------------------
;; Components
;; -----------------------------------------------------------------------
(~doc-section :title "Components" :id "components"
(p "Three new components replace the entire menu bar system:")
(~doc-subsection :title "~sx-logo"
(~doc-code :code (highlight "(defcomp ~sx-logo ()\n (a :href \"/\"\n :sx-get \"/\" :sx-target \"#main-panel\" :sx-select \"#main-panel\"\n :sx-swap \"outerHTML\" :sx-push-url \"true\"\n :class \"block text-center py-4\"\n (span :class \"text-2xl font-bold text-violet-700\" \"sx\")))" "lisp"))
(p "Always at the top. Always centered. The anchor."))
(~doc-subsection :title "~nav-breadcrumb"
(~doc-code :code (highlight "(defcomp ~nav-breadcrumb (&key path siblings level)\n ;; Renders one breadcrumb row: < Label >\n ;; path = the nav tree node for this level\n ;; siblings = list of sibling nodes (for arrow nav)\n ;; level = depth (controls text size/color)\n (let ((idx (find-index siblings path))\n (prev (nth siblings (mod (- idx 1) (len siblings))))\n (next (nth siblings (mod (+ idx 1) (len siblings)))))\n (div :class (str \"flex items-center justify-center gap-3 py-1\"\n (nav-level-classes level))\n (a :href (get prev \"href\")\n :sx-get (get prev \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"text-stone-400 hover:text-violet-600\"\n :aria-label \"Previous\"\n \"<\")\n (a :href (get path \"href\")\n :sx-get (get path \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"font-medium\"\n (get path \"label\"))\n (a :href (get next \"href\")\n :sx-get (get next \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"text-stone-400 hover:text-violet-600\"\n :aria-label \"Next\"\n \">\"))))" "lisp"))
(p "One row per selected level. Shows the current node with left/right arrows to siblings."))
(~doc-subsection :title "~nav-list"
(~doc-code :code (highlight "(defcomp ~nav-list (&key items level)\n ;; Renders a wrapped list of links — the children of the current level\n (div :class (str \"flex flex-wrap justify-center gap-x-4 gap-y-2 py-2\"\n (nav-level-classes level))\n (map (fn (item)\n (a :href (get item \"href\")\n :sx-get (get item \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"hover:text-violet-700 transition-colors\"\n (get item \"label\")))\n items)))" "lisp"))
(p "The children of the current level, rendered as a centered wrapped list of plain links."))
(~doc-subsection :title "~sx-nav — the composition"
(~doc-code :code (highlight "(defcomp ~sx-nav (&key trail children-items level)\n ;; trail = list of {node, siblings} from root to current\n ;; children-items = children of the deepest selected node\n ;; level = depth of children\n (div :class \"max-w-3xl mx-auto px-4\"\n ;; Logo\n (~sx-logo)\n ;; Breadcrumb trail (one row per selected ancestor)\n (map-indexed (fn (i crumb)\n (~nav-breadcrumb\n :path (get crumb \"node\")\n :siblings (get crumb \"siblings\")\n :level (+ i 1)))\n trail)\n ;; Children of the deepest selected node\n (when children-items\n (~nav-list :items children-items :level level))))" "lisp"))
(p "That's the entire navigation. Three small components composed. No bars, no dropdowns, no mobile variants.")))
;; -----------------------------------------------------------------------
;; Path resolution
;; -----------------------------------------------------------------------
(~doc-section :title "Path Resolution" :id "resolution"
(p "Given a URL path, compute the breadcrumb trail and children. This is a tree walk:")
(~doc-code :code (highlight "(define resolve-nav-path\n (fn (tree current-href)\n ;; Walk sx-nav-tree, find the node matching current-href,\n ;; return the trail of ancestors + current children.\n ;;\n ;; Returns: {:trail (list of {:node N :siblings S})\n ;; :children (list) or nil\n ;; :depth number}\n ;;\n ;; Example: current-href = \"/plans/typed-sx\"\n ;; → trail: [{:node Plans :siblings [Docs, CSSX, ...]}\n ;; {:node Typed-SX :siblings [Status, Reader-Macros, ...]}]\n ;; → children: nil (leaf node)\n ;; → depth: 2\n (let ((result (walk-nav-tree tree current-href (list))))\n result)))" "lisp"))
(p "This runs server-side (it's a pure function, no IO). The layout component calls it with the current URL and passes the result to " (code "~sx-nav") ". Same pattern as the current " (code "find-current") " but produces a richer result.")
(p "For sx-get navigations (HTMX swaps), the server re-renders the nav with the new path. The morph diffs the old and new nav — breadcrumb rows appear/disappear, the list changes. CSS transitions handle the visual."))
;; -----------------------------------------------------------------------
;; What goes away
;; -----------------------------------------------------------------------
(~doc-section :title "What Goes Away" :id "removal"
(p "Significant deletion:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Component")
(th :class "px-3 py-2 font-medium text-stone-600" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Why")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~menu-row-sx")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/layout.sx")
(td :class "px-3 py-2 text-stone-600" "Horizontal bar with colour levels — replaced by breadcrumb rows"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~sx-header-row")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "Top menu bar — replaced by logo + breadcrumb"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~sx-sub-row")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "Sub-section bar — replaced by second breadcrumb row"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~sx-main-nav")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "Horizontal nav list — replaced by ~nav-list"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~section-nav")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/nav-data.sx")
(td :class "px-3 py-2 text-stone-600" "Sub-nav builder — replaced by ~nav-list"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~nav-link")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/layout.sx")
(td :class "px-3 py-2 text-stone-600" "Complex link with aria-selected + submenu wrapper — replaced by plain a tags"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~mobile-menu-section")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/layout.sx")
(td :class "px-3 py-2 text-stone-600" "Separate mobile menu — new nav is inherently responsive"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "6 layout variants")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "full/oob/mobile × home/section — replaced by one layout with ~sx-nav"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" ".nav-group CSS")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/shell.sx")
(td :class "px-3 py-2 text-stone-600" "Hover submenu CSS — no submenus to hover")))))
(p "The layout variants collapse from 6 (full/oob/mobile × home/section) to 2 (full/oob). No mobile variant needed — the nav is one column, it works everywhere."))
;; -----------------------------------------------------------------------
;; Layout simplification
;; -----------------------------------------------------------------------
(~doc-section :title "Layout Simplification" :id "layout"
(p "The defpage layout declarations currently specify section, sub-label, sub-href, sub-nav, selected — five params to configure two menu bars. The new layout takes one param: the nav trail.")
(~doc-code :code (highlight ";; Current (verbose, configures two bars)\n(defpage plan-page\n :path \"/plans/<slug>\"\n :layout (:sx-section\n :section \"Plans\"\n :sub-label \"Plans\"\n :sub-href \"/plans/\"\n :sub-nav (~section-nav :items plans-nav-items\n :current (find-current plans-nav-items slug))\n :selected (or (find-current plans-nav-items slug) \"\"))\n :content (...))\n\n;; New (one param, nav computed from URL)\n(defpage plan-page\n :path \"/plans/<slug>\"\n :layout (:sx-docs :path (str \"/plans/\" slug))\n :content (...))" "lisp"))
(p "The layout component computes the nav trail internally from the path and the nav tree. No more passing section names, sub-labels, or pre-built nav components through layout params.")
(~doc-code :code (highlight "(defcomp ~sx-docs-layout-full (&key path)\n (let ((nav-state (resolve-nav-path sx-nav-tree path)))\n (<> (~root-header-auto)\n (~sx-nav\n :trail (get nav-state \"trail\")\n :children-items (get nav-state \"children\")\n :level (get nav-state \"depth\")))))\n\n(defcomp ~sx-docs-layout-oob (&key path)\n (let ((nav-state (resolve-nav-path sx-nav-tree path)))\n (<> (~oob-nav\n :trail (get nav-state \"trail\")\n :children-items (get nav-state \"children\")\n :level (get nav-state \"depth\"))\n (~root-header-auto true))))" "lisp"))
(p "Two layout components instead of twelve. Every defpage in docs.sx simplifies from five layout params to one."))
;; -----------------------------------------------------------------------
;; Scope
;; -----------------------------------------------------------------------
(~doc-section :title "Scope" :id "scope"
(div :class "rounded border border-amber-200 bg-amber-50 p-4 mb-4"
(p :class "text-amber-900 font-medium" "SX docs only — for now")
(p :class "text-amber-800" "This redesign applies to the SX docs app (" (code "sx/") "). The other services (blog, market, events, etc.) keep their current navigation. If the pattern proves out, it can migrate to shared infrastructure and replace the root menu system too."))
(p "What changes:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "sx/sx/nav-data.sx") " — add " (code "sx-nav-tree") " (wraps existing lists, no content change)")
(li (code "sx/sx/layouts.sx") " — rewrite: delete 12 components, add 5 (logo, breadcrumb, list, nav, 2 layouts)")
(li (code "sx/sxc/pages/docs.sx") " — simplify every defpage's " (code ":layout") " declaration")
(li (code "sx/sxc/pages/layouts.py") " — register new layout names")
(li (code "shared/sx/templates/layout.sx") " — no changes needed (shared components untouched, only SX-docs-specific ones change)"))
(p "What doesn't change:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Page content — all " (code "~plan-*-content") ", " (code "~doc-*-content") ", etc. are untouched")
(li "Nav data — all " (code "*-nav-items") " lists are unchanged, just composed into a tree")
(li "Routing — all defpage paths stay the same")
(li "Other services — blog, market, etc. unaffected")))
;; -----------------------------------------------------------------------
;; Implementation
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: Nav tree + resolution"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Add " (code "sx-nav-tree") " to " (code "nav-data.sx") " — compose existing " (code "*-nav-items") " lists into a tree")
(li "Write " (code "resolve-nav-path") " — pure function, tree walk, returns trail + children")
(li "Test: given a path, produces the correct breadcrumb trail and child list")))
(~doc-subsection :title "Phase 2: New components"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Write " (code "~sx-logo") ", " (code "~nav-breadcrumb") ", " (code "~nav-list") ", " (code "~sx-nav"))
(li "Write " (code "~sx-docs-layout-full") " and " (code "~sx-docs-layout-oob"))
(li "Register new layout in " (code "layouts.py"))
(li "Test with one defpage first — verify morph transitions work")))
(~doc-subsection :title "Phase 3: Migrate all defpages"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Update every defpage in " (code "docs.sx") " to use " (code ":layout (:sx-docs :path ...)"))
(li "This is mechanical — replace the 5-param layout block with 1-param")))
(~doc-subsection :title "Phase 4: Delete old components"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Delete " (code "~sx-main-nav") ", " (code "~sx-header-row") ", " (code "~sx-sub-row") ", " (code "~section-nav"))
(li "Delete all 12 SX layout variants from " (code "layouts.sx"))
(li "Delete old layout registrations from " (code "layouts.py"))
(li "Remove " (code ".nav-group") " CSS if no other service uses it"))))))

View File

@@ -0,0 +1,282 @@
;; ---------------------------------------------------------------------------
;; Runtime Slicing
;; ---------------------------------------------------------------------------
(defcomp ~plan-runtime-slicing-content ()
(~doc-page :title "Runtime Slicing"
(~doc-section :title "The Problem" :id "problem"
(p "sx-browser.js is the full SX client runtime — evaluator, parser, renderer, engine, morph, signals, routing, orchestration, boot. Every page loads all of it.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Raw")
(th :class "px-3 py-2 font-medium text-stone-600" "Gzipped")
(th :class "px-3 py-2 font-medium text-stone-600" "Min+Gz")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx-browser.js")
(td :class "px-3 py-2 text-stone-700" "354KB")
(td :class "px-3 py-2 text-stone-700" "75KB")
(td :class "px-3 py-2 text-stone-700" "44KB"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx-ref.js")
(td :class "px-3 py-2 text-stone-700" "244KB")
(td :class "px-3 py-2 text-stone-700" "49KB")
(td :class "px-3 py-2 text-stone-700" "29KB"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "React + ReactDOM")
(td :class "px-3 py-2 text-stone-700" "—")
(td :class "px-3 py-2 text-stone-700" "—")
(td :class "px-3 py-2 text-stone-700" "~5KB + ~40KB")))))
(p "Most pages are L0 (pure hypermedia — server renders, client morphs). They don't need the parser, the evaluator, the full primitive set, signals, or client-side routing. They need morph + swap + trigger dispatch. That's a fraction of the runtime.")
(p "The runtime should be sliceable: each page declares what level it operates at, and the bootstrapper emits only the code that level requires."))
;; -----------------------------------------------------------------------
;; Tiers
;; -----------------------------------------------------------------------
(~doc-section :title "Tiers" :id "tiers"
(p "Four tiers, matching the " (a :href "/reactive-islands/plan" :class "text-violet-700 underline" "reactive islands") " levels:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Tier")
(th :class "px-3 py-2 font-medium text-stone-600" "What")
(th :class "px-3 py-2 font-medium text-stone-600" "Modules")
(th :class "px-3 py-2 font-medium text-stone-600" "Target (min+gz)")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L0 Hypermedia")
(td :class "px-3 py-2 text-stone-700" "Morph, swap, trigger dispatch, history")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "engine, boot (partial)")
(td :class "px-3 py-2 text-stone-700" "~5KB"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L1 DOM Ops")
(td :class "px-3 py-2 text-stone-700" "L0 + toggle!, set-attr!, on-event, class-list ops")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "+ DOM adapter (partial)")
(td :class "px-3 py-2 text-stone-700" "~8KB"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L2 Islands")
(td :class "px-3 py-2 text-stone-700" "L1 + signals, computed, effect, defisland hydration")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "+ signals, DOM adapter (full)")
(td :class "px-3 py-2 text-stone-700" "~15KB"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L3 Full Eval")
(td :class "px-3 py-2 text-stone-700" "L2 + parser, evaluator, all primitives, client routing, component resolution")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "everything")
(td :class "px-3 py-2 text-stone-700" "~44KB (current)")))))
(p "90% of typical pages are L0. A blog post, a product listing, a checkout form — server renders, client morphs on navigation. The full evaluator only loads for pages that do client-side rendering or have reactive islands.")
(div :class "rounded border border-amber-200 bg-amber-50 p-4 mt-4"
(p :class "text-amber-900 font-medium" "Progressive loading")
(p :class "text-amber-800" "A user navigating an L0 page downloads ~5KB. If they navigate to an L2 page (reactive island), the delta (~10KB) loads on demand. The runtime grows with need, never speculatively.")))
;; -----------------------------------------------------------------------
;; The slicer is SX
;; -----------------------------------------------------------------------
(~doc-section :title "The Slicer is SX" :id "slicer-is-sx"
(p "Per the " (a :href "/plans/self-hosting-bootstrapper" :class "text-violet-700 underline" "self-hosting principle") ", the slicer is not a build tool — it's a spec module. " (code "slice.sx") " analyzes the spec's own dependency graph and determines which defines belong to which tier.")
(p (code "js.sx") " (the self-hosting SX-to-JavaScript translator) already compiles the full spec. Slicing is a filter: " (code "js.sx") " translates only the defines that " (code "slice.sx") " selects for a given tier.")
(~doc-code :code (highlight ";; slice.sx — determine which defines each tier needs\n;;\n;; Input: the full list of defines from all spec files\n;; Output: a filtered list for the requested tier\n\n(define tier-deps\n ;; Which spec modules each tier requires\n {:L0 (list \"engine\" \"boot-partial\")\n :L1 (list \"engine\" \"boot-partial\" \"dom-partial\")\n :L2 (list \"engine\" \"boot-partial\" \"dom-partial\"\n \"signals\" \"dom-island\")\n :L3 (list \"eval\" \"render\" \"parser\"\n \"engine\" \"orchestration\" \"boot\"\n \"dom\" \"signals\" \"router\")})\n\n(define slice-defines\n (fn (tier all-defines)\n ;; 1. Get the module list for this tier\n ;; 2. Walk each define's dependency references\n ;; 3. Include a define only if ALL its deps are\n ;; satisfiable within the tier's module set\n ;; 4. Return the filtered define list\n (let ((modules (get tier-deps tier)))\n (filter\n (fn (d) (tier-satisfies? modules (define-deps d)))\n all-defines))))" "lisp"))
(p "The pipeline becomes:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (code "slice.sx") " analyzes the spec and produces a define list per tier")
(li (code "js.sx") " translates each define list to JavaScript (same translator, different input)")
(li "The bootstrapper wraps each tier's output with its platform interface (the hand-written JS glue)")
(li "Output: one " (code ".js") " file per tier, or a single file with tier markers for runtime splitting"))
(p "Because " (code "js.sx") " is self-hosting, slicing is just function composition: " (code "(js-translate-file (slice-defines :L0 all-defines))") ". No new translator. No new build tool. The same 1,382-line " (code "js.sx") " that produces the full runtime produces every tier."))
;; -----------------------------------------------------------------------
;; Dependency analysis
;; -----------------------------------------------------------------------
(~doc-section :title "Define-Level Dependency Analysis" :id "deps"
(p "The slicer needs to know which defines reference which other defines. This is a simpler version of what " (code "deps.sx") " does for components — but at the define level, not the component level.")
(~doc-code :code (highlight ";; Walk a define's body AST, collect all symbol references\n;; that resolve to other top-level defines.\n;;\n;; (define morph-children\n;; (fn (old-node new-node)\n;; ...uses morph-node, morph-attrs, create-element...))\n;;\n;; → deps: #{morph-node morph-attrs create-element}\n\n(define define-refs\n (fn (body all-define-names)\n ;; Collect all symbols in body that appear in all-define-names\n (let ((refs (make-set)))\n (walk-ast body\n (fn (node)\n (when (and (symbol? node)\n (set-has? all-define-names (symbol-name node)))\n (set-add! refs (symbol-name node)))))\n refs)))" "lisp"))
(p "From these per-define deps, we build the full dependency graph and compute transitive closures. A tier's define set is the transitive closure of its entry points:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Tier")
(th :class "px-3 py-2 font-medium text-stone-600" "Entry points")
(th :class "px-3 py-2 font-medium text-stone-600" "Pulls in")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L0")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "morph-node, process-swap, dispatch-trigger, push-url")
(td :class "px-3 py-2 text-stone-700" "morph-attrs, morph-children, create-element, extract-swap-config — the morph/swap subgraph"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L1")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "L0 + toggle-class!, set-attr!, add-event-listener!")
(td :class "px-3 py-2 text-stone-700" "DOM manipulation helpers — small subgraph"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L2")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "L1 + signal, deref, reset!, computed, effect, render-dom-island")
(td :class "px-3 py-2 text-stone-700" "Signal runtime + reactive DOM adapter"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "L3")
(td :class "px-3 py-2 font-mono text-sm text-stone-600" "L2 + eval-expr, sx-parse, render-to-dom, resolve-component-by-cid")
(td :class "px-3 py-2 text-stone-700" "Everything — full evaluator, parser, all ~80 primitives"))))))
;; -----------------------------------------------------------------------
;; Platform interface slicing
;; -----------------------------------------------------------------------
(~doc-section :title "Platform Interface Slicing" :id "platform"
(p "The bootstrapped code (from js.sx) is only half the story. Each define also depends on platform primitives — the hand-written JS glue that js.sx doesn't produce. These live in " (code "bootstrap_js.py") "'s PLATFORM_* blocks.")
(p "The slicer must track platform deps too:")
(~doc-code :code (highlight ";; Platform primitives referenced by tier\n;;\n;; L0 needs: createElement, setAttribute, morphAttrs,\n;; fetch (for sx-get/post), pushState, replaceState\n;;\n;; L1 adds: classList.toggle, addEventListener\n;;\n;; L2 adds: createComment, createTextNode, domRemove,\n;; domChildNodes — the reactive DOM primitives\n;;\n;; L3 adds: everything in PLATFORM_PARSER_JS,\n;; full DOM adapter, async IO bridge\n\n(define platform-deps\n {:L0 (list :dom-morph :fetch :history)\n :L1 (list :dom-morph :fetch :history :dom-events)\n :L2 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors)\n :L3 (list :dom-morph :fetch :history :dom-events\n :dom-reactive :signal-constructors\n :parser :evaluator :async-io)})" "lisp"))
(p "The platform JS blocks in " (code "bootstrap_js.py") " are already organized by adapter (" (code "PLATFORM_DOM_JS") ", " (code "PLATFORM_ENGINE_PURE_JS") ", etc). Slicing further subdivides these into the minimal set each tier needs.")
(p "This subdivision also happens in SX: " (code "slice.sx") " declares which platform blocks each tier requires. " (code "js.sx") " doesn't need to change — it translates defines. The bootstrapper script reads the slice spec and assembles the platform preamble accordingly."))
;; -----------------------------------------------------------------------
;; Progressive loading
;; -----------------------------------------------------------------------
(~doc-section :title "Progressive Loading" :id "progressive"
(p "The simplest approach: one file per tier. The server knows each page's tier (from " (code "defpage") " metadata or component analysis) and serves the right script tag.")
(p "Better: a base file (L0) that all pages load, plus tier deltas loaded on demand.")
(~doc-code :code (highlight ";; Server emits the appropriate script for the page's tier\n;;\n;; L0 page (blog post, product listing):\n;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;;\n;; L2 page (reactive island):\n;; <script src=\"/static/scripts/sx-L0.js\"></script>\n;; <script src=\"/static/scripts/sx-L2-delta.js\"></script>\n;;\n;; Client-side navigation from L0 → L2:\n;; 1. L0 runtime handles the swap\n;; 2. New page declares tier=L2 in response header\n;; 3. L0 runtime loads sx-L2-delta.js dynamically\n;; 4. Island hydration proceeds\n\n(define page-tier\n (fn (page)\n ;; Analyze the page's component tree\n ;; If any component is defisland → L2\n ;; If any component uses on-event/toggle! → L1\n ;; Otherwise → L0\n (cond\n ((page-has-islands? page) :L2)\n ((page-has-dom-ops? page) :L1)\n (true :L0))))" "lisp"))
(~doc-subsection :title "SX-Tier Response Header"
(p "The server includes the page's tier in the response:")
(~doc-code :code (highlight "HTTP/1.1 200 OK\nSX-Tier: L0\nSX-Components: ~card:bafy...,~nav:bafy...\n\n;; or for an island page:\nSX-Tier: L2\nSX-Components: ~counter-island:bafy..." "http"))
(p "On client-side navigation, the engine reads " (code "SX-Tier") " from the response. If the new page requires a higher tier than currently loaded, it fetches the delta script before processing the swap. The delta script registers its additional primitives and the swap proceeds."))
(~doc-subsection :title "Cache Behavior"
(p "Each tier file is content-hashed (like the current " (code "sx_js_hash") " mechanism). Cache-forever semantics. A user who visits any L0 page caches the L0 runtime permanently. If they later visit an L2 page, only the ~10KB delta downloads.")
(p "Combined with " (a :href "/plans/environment-images" :class "text-violet-700 underline" "environment images") ": the image CID includes the tier. An L0 image is smaller than an L3 image — it contains fewer primitives, no parser state, no evaluator. The standalone HTML bundle for an L0 page is tiny.")))
;; -----------------------------------------------------------------------
;; Automatic tier detection
;; -----------------------------------------------------------------------
(~doc-section :title "Automatic Tier Detection" :id "auto-detect"
(p (code "deps.sx") " already classifies components as pure or IO-dependent. Extend it to classify pages by tier:")
(~doc-code :code (highlight ";; Extend deps.sx with tier analysis\n;;\n;; Walk the page's component tree:\n;; - Any defisland → L2 minimum\n;; - Any on-event, toggle!, set-attr! call → L1 minimum \n;; - Any client-eval'd component (SX wire + defcomp) → L3\n;; - Otherwise → L0\n;;\n;; The tier is the MAX of all components' requirements.\n\n(define component-tier\n (fn (comp)\n (cond\n ((island? comp) :L2)\n ((has-dom-ops? (component-body comp)) :L1)\n (true :L0))))\n\n(define page-tier\n (fn (page-def)\n (let ((comp-tiers (map component-tier\n (page-all-components page-def))))\n (max-tier comp-tiers))))" "lisp"))
(p "This runs at registration time (same phase as " (code "compute_all_deps") "). Each " (code "PageDef") " gains a " (code "tier") " field. The server uses it to select the script tag. No manual annotation needed — the tier is derived from what the page actually uses."))
;; -----------------------------------------------------------------------
;; What L0 actually needs
;; -----------------------------------------------------------------------
(~doc-section :title "What L0 Actually Needs" :id "l0-detail"
(p "L0 is the critical tier — it's what most pages load. Every byte matters. Let's be precise about what it contains:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Function")
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")
(th :class "px-3 py-2 font-medium text-stone-600" "Source")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "morph-node")
(td :class "px-3 py-2 text-stone-700" "DOM diffing — update existing DOM from new HTML")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "morph-attrs")
(td :class "px-3 py-2 text-stone-700" "Attribute diffing on a single element")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "morph-children")
(td :class "px-3 py-2 text-stone-700" "Child node reconciliation")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "process-swap")
(td :class "px-3 py-2 text-stone-700" "Apply sx-swap directive (innerHTML, outerHTML, etc)")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "dispatch-trigger")
(td :class "px-3 py-2 text-stone-700" "Process sx-trigger attributes (click, submit, load, etc)")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "sx-fetch")
(td :class "px-3 py-2 text-stone-700" "Make sx-get/sx-post requests, process response")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "orchestration.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "push-url / replace-url")
(td :class "px-3 py-2 text-stone-700" "History management for sx-push-url")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "engine.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "boot-triggers")
(td :class "px-3 py-2 text-stone-700" "Scan DOM for sx-* attributes, wire up event listeners")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "boot.sx"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "resolve-suspense")
(td :class "px-3 py-2 text-stone-700" "Fill in streamed suspense slots")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "boot.sx")))))
(p "That's roughly 20-30 defines from engine.sx + orchestration.sx + boot.sx, plus their transitive deps. No parser, no evaluator, no primitives beyond what those defines call internally. The platform JS is just: fetch wrapper, DOM helpers (createElement, setAttribute, morphing), history API, and event delegation.")
(p "Target: " (strong "~5KB min+gz") " — competitive with htmx (10KB) while being semantically richer (morph-based, not innerHTML-based)."))
;; -----------------------------------------------------------------------
;; Build pipeline
;; -----------------------------------------------------------------------
(~doc-section :title "Build Pipeline" :id "pipeline"
(p "The pipeline uses the same tools that already exist — " (code "js.sx") " for translation, " (code "bootstrap_js.py") " for platform assembly — but feeds them filtered define lists.")
(~doc-code :code (highlight ";; Build all tiers\n;;\n;; 1. Load all spec .sx files\n;; 2. Extract all defines (same as current bootstrap)\n;; 3. Run slice.sx to partition defines by tier\n;; 4. For each tier:\n;; a. js.sx translates the tier's define list\n;; b. Platform assembler wraps with minimal platform JS\n;; c. Output: sx-L{n}.js\n;; 5. Compute deltas: L1-delta = L1 - L0, L2-delta = L2 - L1, etc.\n\n;; The bootstrapper script orchestrates this:\n;;\n;; python bootstrap_js.py --tier L0 -o sx-L0.js\n;; python bootstrap_js.py --tier L1 --delta -o sx-L1-delta.js\n;; python bootstrap_js.py --tier L2 --delta -o sx-L2-delta.js\n;; python bootstrap_js.py -o sx-browser.js # full (L3, backward compat)" "lisp"))
(p "The " (code "--delta") " flag emits only the defines not present in the previous tier. The delta file calls " (code "Sx.extend()") " to register its additions into the already-loaded runtime.")
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mt-4"
(p :class "text-violet-900 font-medium" "Self-hosting all the way")
(p :class "text-violet-800" (code "slice.sx") " is spec. " (code "js.sx") " is spec. The bootstrapper script (" (code "bootstrap_js.py") ") is the thin host-specific glue that reads slice output, calls js.sx via the evaluator, and wraps with platform JS. The slicer could itself be bootstrapped to JavaScript and run in a browser build tool — but that's a future concern.")))
;; -----------------------------------------------------------------------
;; Spec modules
;; -----------------------------------------------------------------------
(~doc-section :title "Spec Modules" :id "spec-modules"
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Module")
(th :class "px-3 py-2 font-medium text-stone-600" "Functions")
(th :class "px-3 py-2 font-medium text-stone-600" "Depends on")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "slice.sx")
(td :class "px-3 py-2 text-stone-700" (code "define-refs") ", " (code "define-dep-graph") ", " (code "slice-defines") ", " (code "tier-entry-points") ", " (code "page-tier") ", " (code "component-tier"))
(td :class "px-3 py-2 text-stone-600" "deps.sx (component analysis), eval.sx (AST walking)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "js.sx")
(td :class "px-3 py-2 text-stone-700" (code "js-translate-file") " — already exists, unchanged")
(td :class "px-3 py-2 text-stone-600" "eval.sx (runs on evaluator)")))))
(p "One new spec file (" (code "slice.sx") "), one existing translator (" (code "js.sx") "), one modified host script (" (code "bootstrap_js.py") " gains " (code "--tier") " and " (code "--delta") " flags)."))
;; -----------------------------------------------------------------------
;; Relationships
;; -----------------------------------------------------------------------
(~doc-section :title "Relationships" :id "relationships"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (a :href "/plans/environment-images" :class "text-violet-700 underline" "Environment Images") " — tiered images are smaller. An L0 image omits the parser, evaluator, and most primitives.")
(li (a :href "/plans/content-addressed-components" :class "text-violet-700 underline" "Content-Addressed Components") " — component CID resolution is L3-only. L0 pages don't resolve components client-side.")
(li (a :href "/reactive-islands/plan" :class "text-violet-700 underline" "Reactive Islands") " — L2 tier is defined by island presence. The signal runtime is the L1→L2 delta.")
(li (a :href "/plans/isomorphic-architecture" :class "text-violet-700 underline" "Isomorphic Architecture") " — client-side page rendering is L3. Most pages don't need it."))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") (code "js.sx") " (complete), " (code "deps.sx") " (complete), " (code "bootstrap_js.py") " adapter selection (exists). " (strong "New: ") (code "slice.sx") " spec module.")))))

View File

@@ -0,0 +1,236 @@
;; ---------------------------------------------------------------------------
;; Self-Hosting Bootstrapper — py.sx (COMPLETE)
;; ---------------------------------------------------------------------------
;; @css bg-green-100 text-green-800 bg-green-50 border-green-200 text-green-700 text-green-600
(defcomp ~plan-self-hosting-bootstrapper-content ()
(~doc-page :title "Self-Hosting Bootstrapper"
;; -----------------------------------------------------------------------
;; Status banner
;; -----------------------------------------------------------------------
(div :class "rounded-lg bg-green-50 border border-green-200 p-4 mb-8"
(div :class "flex items-center gap-3"
(span :class "inline-flex items-center rounded-full bg-green-100 px-3 py-1 text-sm font-semibold text-green-800"
"Complete")
(p :class "text-green-700 text-sm"
(code "py.sx") " is implemented and verified. G0 == G1: 128/128 defines match, "
"1490 lines, 88,955 bytes — byte-for-byte identical. "
(a :href "/bootstrappers/self-hosting" :class "underline text-green-600 font-medium"
"See live verification."))))
;; -----------------------------------------------------------------------
;; The Idea
;; -----------------------------------------------------------------------
(~doc-section :title "The Idea" :id "idea"
(p "We have " (code "bootstrap_py.py") " — a Python program that reads "
(code ".sx") " spec files and emits " (code "sx_ref.py")
", a standalone Python evaluator. It's a compiler written in the host language.")
(p "We also have " (code "z3.sx") " — a translator written in SX itself that "
"converts SX spec declarations into SMT-LIB for Z3. It's proof that "
"SX can generate code in another language.")
(p (strong "py.sx") " — an SX-to-Python translator written in SX. "
"Feed it through the existing Python evaluator against the spec files and it produces "
(code "sx_ref.py") ". The bootstrapper bootstraps itself.")
(p (code "py.sx") " produces output identical to " (code "bootstrap_py.py")
". The Python bootstrapper is redundant. "
"The spec defines itself."))
;; -----------------------------------------------------------------------
;; Results
;; -----------------------------------------------------------------------
(~doc-section :title "Results" :id "results"
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
(tr
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Metric")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Value")))
(tbody
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "py.sx size")
(td :class "px-4 py-2 font-mono" "1,182 lines"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "bootstrap_py.py size (replaced)")
(td :class "px-4 py-2 font-mono" "902 lines (PyEmitter class)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "Defines translated")
(td :class "px-4 py-2 font-mono" "128/128 exact match"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "Spec files processed")
(td :class "px-4 py-2 font-mono" "7 (eval, forms, render, adapter-html, adapter-sx, deps, signals)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "Output size")
(td :class "px-4 py-2 font-mono" "1,490 lines / 88,955 bytes"))
(tr :class "border-t border-stone-100 bg-green-50"
(td :class "px-4 py-2 font-semibold text-green-700" "G0 == G1")
(td :class "px-4 py-2 font-semibold text-green-700" "Identical (diff is empty)"))))))
;; -----------------------------------------------------------------------
;; Architecture
;; -----------------------------------------------------------------------
(~doc-section :title "Architecture" :id "architecture"
(p "Three bootstrapper generations:")
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
(tr
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Gen")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Source")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Runs on")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Reads")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Produces")))
(tbody
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono text-stone-600" "G0")
(td :class "px-4 py-2" (code "bootstrap_py.py"))
(td :class "px-4 py-2" "Python (manual)")
(td :class "px-4 py-2" (code "eval.sx") ", " (code "render.sx") ", ...")
(td :class "px-4 py-2" (code "sx_ref.py")))
(tr :class "border-t border-stone-100 bg-green-50"
(td :class "px-4 py-2 font-mono text-green-700" "G1")
(td :class "px-4 py-2" (code "py.sx"))
(td :class "px-4 py-2" "Python evaluator + SX")
(td :class "px-4 py-2" (code "eval.sx") ", " (code "render.sx") ", ...")
(td :class "px-4 py-2 font-semibold text-green-700" (code "sx_ref.py") " (identical)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono text-stone-600" "G2")
(td :class "px-4 py-2" (code "py.sx"))
(td :class "px-4 py-2" (code "sx_ref.py") " from G1 + SX")
(td :class "px-4 py-2" (code "eval.sx") ", " (code "render.sx") ", ...")
(td :class "px-4 py-2" (code "sx_ref.py") " (fixed-point)")))))
(p "G0 is the hand-written compiler. G1 replaces it with SX. "
"G2 proves the fixed-point: " (code "py.sx") " compiled by its own output "
"produces the same output. That's the definition of self-hosting."))
;; -----------------------------------------------------------------------
;; Translation Rules
;; -----------------------------------------------------------------------
(~doc-section :title "Translation Rules" :id "translation"
(~doc-subsection :title "Name Mangling"
(p "SX identifiers become valid Python identifiers. "
"The RENAMES dict (200+ entries) handles explicit mappings; "
"general rules handle the rest:")
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
(tr
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "SX")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Python")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Rule")))
(tbody
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "eval-expr")
(td :class "px-4 py-2 font-mono" "eval_expr")
(td :class "px-4 py-2" "kebab → snake"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "nil?")
(td :class "px-4 py-2 font-mono" "is_nil")
(td :class "px-4 py-2" "predicate rename"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "empty?")
(td :class "px-4 py-2 font-mono" "empty_p")
(td :class "px-4 py-2" "? → _p (general)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "set!")
(td :class "px-4 py-2 font-mono" "set_b")
(td :class "px-4 py-2" "! → _b"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "type")
(td :class "px-4 py-2 font-mono" "type_")
(td :class "px-4 py-2" "Python reserved word escape"))))))
(~doc-subsection :title "Special Forms"
(p "Each SX special form maps to a Python expression pattern:")
(div :class "overflow-x-auto rounded border border-stone-200 my-4"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
(tr
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "SX")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Python")))
(tbody
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(if c t e)")
(td :class "px-4 py-2 font-mono" "(t if sx_truthy(c) else e)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(let ((a 1)) body)")
(td :class "px-4 py-2 font-mono" "(lambda a: body)(1)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(fn (x) body)")
(td :class "px-4 py-2 font-mono" "lambda x: body"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(and a b c)")
(td :class "px-4 py-2 font-mono" "(a if not sx_truthy(a) else ...)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 font-mono" "(case x \"a\" 1)")
(td :class "px-4 py-2 font-mono" "_sx_case(x, [(\"a\", lambda: 1)])"))))))
(~doc-subsection :title "Mutation: set! and Cell Variables"
(p "Python closures can read but not rebind outer variables. "
(code "py.sx") " detects " (code "set!") " targets that cross lambda boundaries "
"and routes them through a " (code "_cells") " dict:")
(~doc-code :code (highlight ";; SX ;; Python
(define counter def counter():
(fn () _cells = {}
(let ((n 0)) _cells['n'] = 0
(fn () return lambda: _sx_begin(
(set! n (+ n 1)) _sx_cell_set(_cells, 'n', ...),
n)))) _cells['n'])" "python"))
(p "This is the hardest translation rule. " (code "py-find-nested-set-vars")
" scans the AST for " (code "set!") " inside nested " (code "fn") " bodies. "
(code "py-emit-define-as-def") " emits a " (code "def") " with " (code "_cells")
" dict instead of a lambda.")))
;; -----------------------------------------------------------------------
;; Scope
;; -----------------------------------------------------------------------
(~doc-section :title "Scope" :id "scope"
(p (code "py.sx") " is a general-purpose SX-to-Python translator. "
"The translation rules are mechanical and apply to " (em "all") " SX, "
"not just the spec subset.")
(p "The spec files are the " (em "first test case") " — because we can verify correctness "
"by diffing G0 and G1 output. But the translator itself is target-agnostic: "
"it translates SX syntax, not SX semantics. Every SX form has a "
"straightforward Python equivalent.")
(p "The roadmap:")
(ul :class "list-disc pl-6 space-y-1 text-stone-700"
(li (code "py.sx") " → Python — " (strong "done"))
(li (code "js.sx") " → JavaScript (replaces " (code "bootstrap_js.py") ")")
(li (code "go.sx") " → Go (new host)")
(li (code "rs.sx") " → Rust (new host)")
(li (code "hs.sx") " → Haskell (new host)"))
(p "Each new host only needs: (1) a minimal evaluator to run "
(code "{host}.sx") " once, and (2) the platform interface in the target language."))
;; -----------------------------------------------------------------------
;; Implications
;; -----------------------------------------------------------------------
(~doc-section :title "Implications" :id "implications"
(~doc-subsection :title "Practical"
(p "One less Python file to maintain. Changes to the transpilation logic "
"are written in SX and tested with the SX test harness. The spec and its "
"compiler live in the same language."))
(~doc-subsection :title "Architectural"
(p "With " (code "z3.sx") " (SMT-LIB) and " (code "py.sx") " (Python), "
"the pattern is clear: SX translators are SX programs. "
"Adding a new target language means writing one " (code ".sx") " file, "
"not a new Python compiler."))
(~doc-subsection :title "Philosophical"
(p "A self-hosting bootstrapper is a fixed point. The spec defines behavior. "
"The translator is itself defined in terms of that behavior. Running the "
"translator on the spec produces a program that can run the translator on "
"the spec and get the same program.")
(p "Gödel showed that any sufficiently powerful formal system can encode statements "
"about itself. A self-hosting bootstrapper is the constructive version: "
"the system doesn't just " (em "talk") " about itself, it " (em "builds") " itself.")))))

View File

@@ -0,0 +1,102 @@
;; sx-web.org — Online Development Platform
;; Plan: transform sx-web.org from documentation site into a live development
;; environment where content is authored, tested, and deployed in the browser.
(defcomp ~plan-sx-web-platform-content ()
(~doc-page :title "sx-web.org Development Platform"
(~doc-section :title "Vision" :id "vision"
(p "sx-web.org becomes the development environment for itself. "
"Authors write essays, examples, components, and specs directly in the browser. "
"Changes are planned, staged, tested, and deployed without leaving the site. "
"The documentation is not about the platform — it " (em "is") " the platform.")
(p "Every artifact is content-addressed on IPFS. Every change flows through sx-activity. "
"Every deployment runs through sx-ci. Claude Code is embedded as the AI pair programmer. "
"The entire development lifecycle happens over the web, using the same SX primitives "
"that the platform is built from."))
(~doc-section :title "Architecture" :id "architecture"
(p "The platform composes existing SX subsystems into a unified workflow:")
(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 px-3 font-semibold text-stone-700" "Layer")
(th :class "py-2 px-3 font-semibold text-stone-700" "System")
(th :class "py-2 px-3 font-semibold text-stone-700" "Role")))
(tbody :class "text-stone-600"
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Author")
(td :class "py-2 px-3" "Embedded editor + Claude Code")
(td :class "py-2 px-3" "Write SX in the browser with AI assistance"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Stage")
(td :class "py-2 px-3" "Content-addressed components")
(td :class "py-2 px-3" "CID-identified artifacts, preview before publish"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Test")
(td :class "py-2 px-3" "sx-ci")
(td :class "py-2 px-3" "Run test suites against staged changes"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Publish")
(td :class "py-2 px-3" "sx-activity")
(td :class "py-2 px-3" "Federated distribution via ActivityPub"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Store")
(td :class "py-2 px-3" "IPFS")
(td :class "py-2 px-3" "Content-addressed storage, permanent availability"))
(tr :class "border-b border-stone-100"
(td :class "py-2 px-3 font-semibold" "Verify")
(td :class "py-2 px-3" "Environment images")
(td :class "py-2 px-3" "Spec CID \u2192 image CID \u2192 endpoint provenance"))))))
(~doc-section :title "Embedded Claude Code" :id "claude-code"
(p "Claude Code sessions run inside the browser as reactive islands. "
"The AI has access to the full SX component environment — it can read specs, "
"write components, run tests, and propose changes. All within the user's security context.")
(p "The session produces SX diffs — not text patches, but structural changes to the component tree. "
"These diffs are first-class SX values that can be inspected, composed, reverted, and replayed.")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li "Read any component definition, spec file, or plan")
(li "Write new components (essays, examples, specs)")
(li "Modify existing components with structural diffs")
(li "Run sx-ci test suites against proposed changes")
(li "Stage changes as content-addressed preview")
(li "Publish via sx-activity when approved")))
(~doc-section :title "Workflow" :id "workflow"
(p "A typical session — adding a new essay:")
(ol :class "space-y-3 text-stone-600 list-decimal pl-5"
(li (strong "Author: ") "Open Claude Code session on sx-web.org. "
"Describe the essay topic. Claude writes the defcomp in SX.")
(li (strong "Preview: ") "The component renders live in the browser. "
"Author reviews, requests changes. Claude iterates.")
(li (strong "Stage: ") "Component is serialized, CID computed, stored to IPFS. "
"A preview URL is generated from the CID.")
(li (strong "Test: ") "sx-ci runs: component renders without error, "
"all referenced components exist, CSS classes are valid, links resolve.")
(li (strong "Publish: ") "sx-activity broadcasts the new component. "
"Federated subscribers receive it. The nav tree updates automatically.")
(li (strong "Verify: ") "Anyone can follow the CID chain from the served page "
"back to the spec that generated the evaluator that rendered it.")))
(~doc-section :title "Content Types" :id "content-types"
(p "Anything that can be a defcomp can be authored on the platform:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Essays") " — opinion pieces, rationales, explorations")
(li (strong "Examples") " — interactive demos with live code")
(li (strong "Specs") " — new spec modules authored and tested in-browser")
(li (strong "Plans") " — architecture documents with embedded diagrams")
(li (strong "Components") " — reusable UI components shared via IPFS")
(li (strong "Tests") " — defsuite/deftest written and executed live")))
(~doc-section :title "Prerequisites" :id "prerequisites"
(p "Systems that must be complete before the platform can work:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Reactive islands (L2+)") " — for the editor and preview panes")
(li (strong "Content-addressed components") " — CID computation and IPFS storage")
(li (strong "sx-activity") " — federated publish/subscribe")
(li (strong "sx-ci") " — test pipelines as SX components")
(li (strong "Runtime slicing") " — L3 full eval in browser for live preview")
(li (strong "Environment images") " — for provenance verification")
(li (strong "Claude Code API integration") " — embedded sessions via API")))))

364
sx/sx/plans/typed-sx.sx Normal file
View File

@@ -0,0 +1,364 @@
;; ---------------------------------------------------------------------------
;; Typed SX — Gradual Type System
;; ---------------------------------------------------------------------------
(defcomp ~plan-typed-sx-content ()
(~doc-page :title "Typed SX"
(~doc-section :title "The Opportunity" :id "opportunity"
(p "SX already has types. Every primitive in " (code "primitives.sx") " declares " (code ":returns \"number\"") " or " (code ":returns \"boolean\"") ". Every IO primitive in " (code "boundary.sx") " declares " (code ":returns \"dict?\"") " or " (code ":returns \"any\"") ". Component params are named. The information exists — nobody checks it.")
(p "A gradual type system makes this information useful. Annotations are optional. Unannotated code works exactly as before. Annotated code gets checked at registration time — zero runtime cost, errors before any request is served. The checker is a spec module (" (code "types.sx") "), bootstrapped to every host.")
(p "This is not Haskell. SX doesn't need a type system to be correct — " (a :href "/plans/theorem-prover" :class "text-violet-700 underline" "prove.sx") " already verifies primitive properties by exhaustive search. Types serve a different purpose: they catch " (strong "composition errors") " — wrong argument passed to a component, mismatched return type piped into another function, missing keyword arg. The kind of bug you find by reading the stack trace and slapping your forehead."))
;; -----------------------------------------------------------------------
;; What already exists
;; -----------------------------------------------------------------------
(~doc-section :title "What Already Exists" :id "existing"
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Feature")
(th :class "px-3 py-2 font-medium text-stone-600" "Where")
(th :class "px-3 py-2 font-medium text-stone-600" "Types today")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Primitive return types")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "primitives.sx :returns")
(td :class "px-3 py-2 text-stone-600" "\"number\", \"string\", \"boolean\", \"list\", \"dict\", \"any\""))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "IO primitive return types")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "boundary.sx :returns")
(td :class "px-3 py-2 text-stone-600" "Same + \"dict?\", \"string?\", \"element\" — nullable types already appear"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Primitive param names")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "primitives.sx :params")
(td :class "px-3 py-2 text-stone-600" "Named but untyped: " (code "(a b)") ", " (code "(&rest args)")))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Component param names")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "eval.sx parse-comp-params")
(td :class "px-3 py-2 text-stone-600" (code "&key") " params, " (code "&rest") " children — named, untyped"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Runtime type predicates")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "primitives.sx")
(td :class "px-3 py-2 text-stone-600" (code "number?") ", " (code "string?") ", " (code "list?") ", " (code "dict?") ", " (code "nil?") ", " (code "symbol?") " — all exist"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Purity classification")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "deps.sx + boundary.py")
(td :class "px-3 py-2 text-stone-600" "Pure vs IO — a binary type at the component level"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Property verification")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "prove.sx")
(td :class "px-3 py-2 text-stone-600" "Algebraic properties (commutativity, transitivity) verified by bounded model checking")))))
(p "The foundation is solid. Primitives already have return types. Params already have names. Boundary enforcement already does structural analysis. Types extend this — they don't replace it."))
;; -----------------------------------------------------------------------
;; Type language
;; -----------------------------------------------------------------------
(~doc-section :title "Type Language" :id "type-language"
(p "Small, practical, no type theory PhD required.")
(~doc-subsection :title "Base types"
(~doc-code :code (highlight ";; Atomic types\nnumber string boolean nil symbol keyword element\n\n;; Nullable (already used in boundary.sx)\nstring? ;; (or string nil)\ndict? ;; (or dict nil)\nnumber? ;; (or number nil)\n\n;; The top type — anything goes\nany\n\n;; The bottom type — never returns (e.g. abort)\nnever" "lisp")))
(~doc-subsection :title "Compound types"
(~doc-code :code (highlight ";; Collections with element types\n(list-of number) ;; list where every element is a number\n(list-of string) ;; list of strings\n(list-of any) ;; list (same as untyped)\n(dict-of string number) ;; dict with string keys, number values\n(dict-of string any) ;; dict with string keys (typical kwargs)\n\n;; Union types\n(or string number) ;; either string or number\n(or string nil) ;; same as string?\n\n;; Function types\n(-> number number) ;; number → number\n(-> string string boolean) ;; (string, string) → boolean\n(-> (list-of any) number) ;; list → number\n(-> &rest any number) ;; variadic → number" "lisp")))
(~doc-subsection :title "Component types"
(~doc-code :code (highlight ";; Component type is its keyword signature\n;; Derived automatically from defcomp — no annotation needed\n\n(comp :title string :price number &rest element)\n;; keyword args children type\n\n;; This is NOT a new syntax for defcomp.\n;; It's the TYPE that a defcomp declaration produces.\n;; The checker infers it from parse-comp-params + annotations." "lisp")))
(p "That's it. No generics, no higher-kinded types, no dependent types, no type classes. Just: what goes in, what comes out, can it be nil."))
;; -----------------------------------------------------------------------
;; Annotation syntax
;; -----------------------------------------------------------------------
(~doc-section :title "Annotation Syntax" :id "syntax"
(p "Annotations are optional. Three places they can appear:")
(~doc-subsection :title "1. Component params"
(~doc-code :code (highlight ";; Current (unchanged, still works)\n(defcomp ~product-card (&key title price image-url &rest children)\n (div ...))\n\n;; Annotated — colon after param name\n(defcomp ~product-card (&key (title : string)\n (price : number)\n (image-url : string?)\n &rest children)\n (div ...))\n\n;; Parenthesized pairs: (name : type)\n;; Unannotated params default to `any`\n;; &rest children is always (list-of element)" "lisp"))
(p "The " (code "(name : type)") " syntax is unambiguous — a 3-element list where the second element is the symbol " (code ":") ". The parser already handles lists inside parameter lists. " (code "parse-comp-params") " gains a branch: if a param is a list of length 3 with " (code ":") " in the middle, extract name and type."))
(~doc-subsection :title "2. Define/lambda return types"
(~doc-code :code (highlight ";; Current (unchanged)\n(define total-price\n (fn (items)\n (reduce + 0 (map (fn (i) (get i \"price\")) items))))\n\n;; Annotated — :returns after params\n(define total-price\n (fn ((items : (list-of dict)) :returns number)\n (reduce + 0 (map (fn (i) (get i \"price\")) items))))" "lisp"))
(p (code ":returns") " is already the convention in " (code "primitives.sx") " and " (code "boundary.sx") ". Same keyword, same position (after params), same meaning."))
(~doc-subsection :title "3. Let bindings"
(~doc-code :code (highlight ";; Current (unchanged)\n(let ((x (compute-value)))\n (+ x 1))\n\n;; Annotated\n(let (((x : number) (compute-value)))\n (+ x 1))\n\n;; Usually unnecessary — the checker infers let binding\n;; types from the right-hand side. Only annotate when\n;; the inference is ambiguous (e.g. the RHS returns `any`)." "lisp"))
(p "All annotations are syntactically backward-compatible. Unannotated code parses and runs identically. The annotations are simply ignored by evaluators that don't have the type checker loaded."))
;; -----------------------------------------------------------------------
;; Type checking
;; -----------------------------------------------------------------------
(~doc-section :title "Type Checking" :id "checking"
(p "The checker runs at registration time — after " (code "compute_all_deps") ", before serving. It walks every component's body AST and verifies that call sites match declared signatures.")
(~doc-subsection :title "What it checks"
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Primitive calls:") " " (code "(+ \"hello\" 3)") " — " (code "+") " expects numbers, got a string. Error.")
(li (strong "Component calls:") " " (code "(~product-card :title 42)") " — " (code ":title") " declared as " (code "string") ", got " (code "number") ". Error.")
(li (strong "Missing required params:") " " (code "(~product-card :price 29.99)") " — " (code ":title") " not provided, no default. Error.")
(li (strong "Unknown keyword args:") " " (code "(~product-card :title \"Hi\" :colour \"red\")") " — " (code ":colour") " not in param list. Warning.")
(li (strong "Nil safety:") " " (code "(+ 1 (get user \"age\"))") " — " (code "get") " returns " (code "any") " (might be nil). " (code "+") " expects " (code "number") ". Warning: possible nil.")
(li (strong "Thread-first type flow:") " " (code "(-> items (filter active?) (map name) (join \", \"))") " — checks each step's output matches the next step's input.")))
(~doc-subsection :title "What it does NOT check"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (strong "Runtime values.") " " (code "(if condition 42 \"hello\")") " — the type is " (code "(or number string)") ". The checker doesn't know which branch executes.")
(li (strong "Dict key presence.") " " (code "(get user \"name\")") " — the checker knows " (code "get") " returns " (code "any") " but doesn't track which keys a dict has. (Future: typed dicts/records.)")
(li (strong "Termination.") " That's " (a :href "/plans/theorem-prover" :class "text-violet-700 underline" "prove.sx") "'s domain.")
(li (strong "Effects.") " Purity is already enforced by " (code "deps.sx") " + boundary. Types don't duplicate it.")))
(~doc-subsection :title "Inference"
(p "Most types are inferred, not annotated. The checker knows:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Literal types: " (code "42") " → " (code "number") ", " (code "\"hi\"") " → " (code "string") ", " (code "true") " → " (code "boolean") ", " (code "nil") " → " (code "nil"))
(li "Primitive return types: " (code "(+ a b)") " → " (code "number") ", " (code "(str x)") " → " (code "string") ", " (code "(empty? x)") " → " (code "boolean"))
(li "Let bindings: " (code "(let ((x 42)) ...)") " → " (code "x : number"))
(li "If/cond narrowing: " (code "(if (nil? x) \"default\" (str x))") " — in the else branch, " (code "x") " is not nil")
(li "Component return: always " (code "element"))
(li "Map/filter propagation: " (code "(map name items)") " → " (code "(list-of string)") " if " (code "name") " returns " (code "string")))
(p "In practice, most component bodies need zero annotations. The checker infers types from literals and primitive return declarations. Annotations are for the edges: params coming from outside, and ambiguous flows.")))
;; -----------------------------------------------------------------------
;; Gradual semantics
;; -----------------------------------------------------------------------
(~doc-section :title "Gradual Semantics" :id "gradual"
(p "The type " (code "any") " is the escape hatch. It's compatible with everything — passes every check, accepts every value. Unannotated params are " (code "any") ". The return type of " (code "get") " is " (code "any") ". This means:")
(ul :class "list-disc pl-5 text-stone-700 space-y-2"
(li (strong "Fully untyped code:") " All params are " (code "any") ", all returns are " (code "any") ". The checker has nothing to verify. No errors, no warnings. Exactly the same as today.")
(li (strong "Partially typed:") " Some components annotate params, others don't. The checker verifies annotated call sites and skips untyped ones. You get value proportional to effort.")
(li (strong "Fully typed:") " Every component, every lambda, every let binding. The checker catches every composition error. Maximum value, maximum annotation cost."))
(p "The practical sweet spot: " (strong "annotate component params, nothing else.") " Components are the public API — the boundary between independent pieces of code. Their params are the contract. Internal lambdas and let bindings benefit less from annotations because the checker can infer their types from context.")
(~doc-code :code (highlight ";; Sweet spot: annotate the interface, infer the rest\n(defcomp ~price-display (&key (price : number)\n (currency : string)\n (sale-price : number?))\n ;; Everything below is inferred:\n ;; formatted → string (str returns string)\n ;; discount → number (- returns number)\n ;; has-sale → boolean (and returns boolean)\n (let ((formatted (str currency (format-number price 2)))\n (has-sale (and sale-price (< sale-price price)))\n (discount (if has-sale\n (round (* 100 (/ (- price sale-price) price)))\n 0)))\n (div :class \"price\"\n (span :class (if has-sale \"line-through text-stone-400\" \"font-bold\")\n formatted)\n (when has-sale\n (span :class \"text-green-700 font-bold ml-2\"\n (str currency (format-number sale-price 2))\n (span :class \"text-xs ml-1\" (str \"(-\" discount \"%)\")))))))" "lisp")))
;; -----------------------------------------------------------------------
;; Error reporting
;; -----------------------------------------------------------------------
(~doc-section :title "Error Reporting" :id "errors"
(p "Type errors are reported at registration time with source location, expected type, actual type, and the full call chain.")
(~doc-code :code (highlight ";; Example error output:\n;;\n;; TYPE ERROR in ~checkout-summary (checkout.sx:34)\n;;\n;; (str \"Total: \" (compute-total items))\n;; ^^^^^^^^^^^^^^^^^\n;; Argument 2 of `str` expects: string\n;; Got: number (from compute-total :returns number)\n;;\n;; Fix: (str \"Total: \" (str (compute-total items)))\n;;\n;;\n;; TYPE ERROR in ~product-page (products.sx:12)\n;;\n;; (~product-card :title product-name :price \"29.99\")\n;; ^^^^^^\n;; Keyword :price of ~product-card expects: number\n;; Got: string (literal \"29.99\")\n;;\n;; Fix: (~product-card :title product-name :price 29.99)" "lisp"))
(p "Severity levels:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Level")
(th :class "px-3 py-2 font-medium text-stone-600" "When")
(th :class "px-3 py-2 font-medium text-stone-600" "Behavior")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-red-700" "Error")
(td :class "px-3 py-2 text-stone-700" "Definite type mismatch: " (code "number") " where " (code "string") " expected")
(td :class "px-3 py-2 text-stone-600" "Strict mode: startup crash. Permissive: logged warning."))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-amber-700" "Warning")
(td :class "px-3 py-2 text-stone-700" "Possible mismatch: " (code "any") " where " (code "number") " expected, unknown kwarg")
(td :class "px-3 py-2 text-stone-600" "Logged, never crashes"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-500" "Info")
(td :class "px-3 py-2 text-stone-700" "Annotation suggestion: frequently-called untyped component")
(td :class "px-3 py-2 text-stone-600" "Dev mode only")))))
(p (code "SX_TYPE_STRICT=1") " (env var, like " (code "SX_BOUNDARY_STRICT") ") makes type errors fatal at startup. Absent = permissive. Same pattern as boundary enforcement."))
;; -----------------------------------------------------------------------
;; Nil narrowing
;; -----------------------------------------------------------------------
(~doc-section :title "Nil Narrowing" :id "nil"
(p "The most common real-world type error in SX: passing a possibly-nil value where a non-nil is required. " (code "get") " returns " (code "any") " (might be nil). " (code "current-user") " returns " (code "dict?") " (explicitly nullable). Piping these into " (code "str") " or arithmetic without checking is the #1 source of runtime errors.")
(~doc-code :code (highlight ";; Before: runtime error if user is nil\n(defcomp ~greeting (&key (user : dict?))\n (h1 (str \"Hello, \" (get user \"name\"))))\n ;; ^^^ TYPE WARNING: user is dict?, get needs non-nil first arg\n\n;; After: checker enforces nil handling\n(defcomp ~greeting (&key (user : dict?))\n (if user\n (h1 (str \"Hello, \" (get user \"name\")))\n ;; In this branch, checker narrows user to `dict` (not nil)\n (h1 \"Hello, guest\")))\n ;; No warning — nil case handled" "lisp"))
(p "Narrowing rules:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "(if x then else)") " — in " (code "then") ", " (code "x") " is narrowed to exclude " (code "nil") " and " (code "false"))
(li (code "(when x body)") " — in " (code "body") ", " (code "x") " is narrowed")
(li (code "(nil? x)") " in an if test — " (code "then") " branch: " (code "x") " is " (code "nil") ", " (code "else") " branch: " (code "x") " is non-nil")
(li (code "(string? x)") " in an if test — " (code "then") " branch: " (code "x") " is " (code "string")))
(p "This is standard flow typing, nothing exotic. TypeScript does the same thing with " (code "if (x !== null)") " narrowing."))
;; -----------------------------------------------------------------------
;; Component signature verification
;; -----------------------------------------------------------------------
(~doc-section :title "Component Signature Verification" :id "signatures"
(p "The highest-value check: verifying that component call sites match declared signatures. This is where most bugs live.")
(~doc-code :code (highlight ";; Definition\n(defcomp ~product-card (&key (title : string)\n (price : number)\n (image-url : string?)\n &rest children)\n ...)\n\n;; Call site checks:\n(~product-card :title \"Widget\" :price 29.99) ;; OK\n(~product-card :title \"Widget\") ;; ERROR: :price required\n(~product-card :title 42 :price 29.99) ;; ERROR: :title expects string\n(~product-card :title \"Widget\" :price 29.99\n (p \"Description\") (p \"Details\")) ;; OK: children\n(~product-card :titel \"Widget\" :price 29.99) ;; WARNING: :titel unknown\n ;; (did you mean :title?)" "lisp"))
(p "The checker walks every component call in every component body. For each call:")
(ol :class "list-decimal pl-5 text-stone-700 space-y-1"
(li "Look up the callee component in env")
(li "Match provided keyword args against declared params")
(li "Check each arg's inferred type against the param's declared type")
(li "Report missing required params (those without defaults)")
(li "Report unknown keyword args (with Levenshtein suggestion)"))
(p "This catches the majority of composition bugs. A renamed param, a swapped argument, a missing required field — all caught before serving."))
;; -----------------------------------------------------------------------
;; Thread-first type flow
;; -----------------------------------------------------------------------
(~doc-section :title "Thread-First Type Flow" :id "thread-first"
(p "The " (code "->") " (thread-first) form is SX's primary composition operator. Type checking it means verifying each step's output matches the next step's input:")
(~doc-code :code (highlight ";; (-> items\n;; (filter active?) ;; (list-of dict) → (list-of dict)\n;; (map name) ;; (list-of dict) → (list-of string)\n;; (join \", \")) ;; (list-of string) → string\n;;\n;; Type flow: (list-of dict) → (list-of dict) → (list-of string) → string\n;; Each step's output is the next step's first argument.\n\n;; ERROR example:\n;; (-> items\n;; (filter active?)\n;; (join \", \") ;; join expects (list-of string),\n;; (map name)) ;; got string — wrong order!\n;;\n;; TYPE ERROR: step 3 (map) expects (list-of any) as first arg\n;; got: string (from join)" "lisp"))
(p "The checker threads the inferred type through each step. If any step's input type doesn't match the previous step's output type, it reports the exact point where the pipeline breaks."))
;; -----------------------------------------------------------------------
;; Relationship to prove.sx
;; -----------------------------------------------------------------------
(~doc-section :title "Types vs Proofs" :id "types-vs-proofs"
(p (a :href "/plans/theorem-prover" :class "text-violet-700 underline" "prove.sx") " and types.sx are complementary, not competing:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "")
(th :class "px-3 py-2 font-medium text-stone-600" "types.sx")
(th :class "px-3 py-2 font-medium text-stone-600" "prove.sx")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-700" "Checks")
(td :class "px-3 py-2 text-stone-700" "Composition: does A's output fit B's input?")
(td :class "px-3 py-2 text-stone-700" "Properties: is + commutative? Is sort stable?"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-700" "Scope")
(td :class "px-3 py-2 text-stone-700" "All component bodies, every call site")
(td :class "px-3 py-2 text-stone-700" "Primitives only (declared in primitives.sx)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-700" "Method")
(td :class "px-3 py-2 text-stone-700" "Type inference + checking (fast, O(n) AST walk)")
(td :class "px-3 py-2 text-stone-700" "Bounded model checking (exhaustive, slower)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-700" "When")
(td :class "px-3 py-2 text-stone-700" "Registration time (every startup)")
(td :class "px-3 py-2 text-stone-700" "CI / on-demand (not every startup)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-700" "Catches")
(td :class "px-3 py-2 text-stone-700" "Wrong arg type, missing param, nil misuse")
(td :class "px-3 py-2 text-stone-700" "Algebraic law violations, edge case failures")))))
(p "Types answer: " (em "\"does this code fit together?\"") " Proofs answer: " (em "\"does this code do the right thing?\"") " Both are spec modules, both bootstrapped, both run without external tools."))
;; -----------------------------------------------------------------------
;; Implementation
;; -----------------------------------------------------------------------
(~doc-section :title "Implementation" :id "implementation"
(~doc-subsection :title "Phase 1: Type Registry"
(p "Build the type registry from existing declarations.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Parse " (code ":returns") " from " (code "primitives.sx") " and " (code "boundary.sx") " into a type map: " (code "primitive-name → return-type"))
(li "Parse " (code ":params") " declarations into param type maps (currently untyped — default to " (code "any") ")")
(li "Compute component signatures from " (code "parse-comp-params") " + any type annotations")
(li "Store in env as metadata alongside existing component/primitive objects")))
(~doc-subsection :title "Phase 2: Type Inference Engine"
(p "Walk AST, infer types bottom-up.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Literals → concrete types")
(li "Primitive calls → look up return type in registry")
(li "Component calls → " (code "element"))
(li "Let bindings → RHS inferred type")
(li "If/when/cond → union of branch types, with narrowing in branches")
(li "Lambda → " (code "(-> param-types return-type)") " from body inference")
(li "Map/filter → propagate element types through the transform")))
(~doc-subsection :title "Phase 3: Type Checker"
(p "Compare inferred types at call sites against declared types.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Subtype check: " (code "number") " <: " (code "any") ", " (code "string") " <: " (code "string?") ", " (code "nil") " <: " (code "string?"))
(li "Error on definite mismatch: " (code "number") " vs " (code "string"))
(li "Warn on possible mismatch: " (code "any") " vs " (code "number") " (might work, might not)")
(li "Component kwarg checking: required params, unknown kwargs, type mismatches")))
(~doc-subsection :title "Phase 4: Annotation Parsing"
(p "Extend " (code "parse-comp-params") " and " (code "sf-defcomp") " to recognize type annotations.")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "(name : type)") " in param lists → extract type, store in component metadata")
(li (code ":returns type") " in lambda/fn bodies → store as declared return type")
(li "Backward compatible: unannotated params remain " (code "any"))))
(~doc-subsection :title "Phase 5: Typed Primitives"
(p "Add param types to " (code "primitives.sx") " declarations.")
(~doc-code :code (highlight ";; Current\n(define-primitive \"+\"\n :params (&rest args)\n :returns \"number\"\n :doc \"Sum all arguments.\")\n\n;; Extended\n(define-primitive \"+\"\n :params (&rest (args : number))\n :returns \"number\"\n :doc \"Sum all arguments.\")" "lisp"))
(p "This is the biggest payoff for effort: ~80 primitives gain param types, enabling the checker to catch every mistyped primitive call across the entire codebase.")))
;; -----------------------------------------------------------------------
;; Spec module
;; -----------------------------------------------------------------------
(~doc-section :title "Spec Module" :id "spec-module"
(p (code "types.sx") " — the type checker, written in SX, bootstrapped to every host.")
(div :class "overflow-x-auto rounded border border-stone-200 mb-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" "Function")
(th :class "px-3 py-2 font-medium text-stone-600" "Purpose")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "infer-type")
(td :class "px-3 py-2 text-stone-700" "Infer the type of an AST node in a given type environment"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "check-call")
(td :class "px-3 py-2 text-stone-700" "Check a function/component call against declared signature"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "check-component")
(td :class "px-3 py-2 text-stone-700" "Type-check an entire component body"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "check-all")
(td :class "px-3 py-2 text-stone-700" "Check all registered components, return error/warning list"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "subtype?")
(td :class "px-3 py-2 text-stone-700" "Is type A a subtype of type B?"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "narrow-type")
(td :class "px-3 py-2 text-stone-700" "Narrow a type based on a predicate test (nil?, string?, etc)"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "type-union")
(td :class "px-3 py-2 text-stone-700" "Compute the union of two types"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "parse-type-annotation")
(td :class "px-3 py-2 text-stone-700" "Parse a type expression from annotation syntax"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "build-type-registry")
(td :class "px-3 py-2 text-stone-700" "Build type map from primitives.sx + boundary.sx declarations")))))
(p "The checker is ~300-500 lines of SX. It's an AST walk with a type environment — structurally similar to " (code "deps.sx") " (which walks ASTs to find IO refs) and " (code "prove.sx") " (which walks ASTs to generate verification conditions). Same pattern, different question."))
;; -----------------------------------------------------------------------
;; Relationships
;; -----------------------------------------------------------------------
(~doc-section :title "Relationships" :id "relationships"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (a :href "/plans/theorem-prover" :class "text-violet-700 underline" "Theorem Prover") " — prove.sx verifies primitive properties; types.sx verifies composition. Complementary.")
(li (a :href "/plans/content-addressed-components" :class "text-violet-700 underline" "Content-Addressed Components") " — component manifests gain type signatures. A consumer knows param types before fetching the source.")
(li (a :href "/plans/environment-images" :class "text-violet-700 underline" "Environment Images") " — the type registry serializes into the image. Type checking happens once at image build time, not on every startup.")
(li (a :href "/plans/runtime-slicing" :class "text-violet-700 underline" "Runtime Slicing") " — types.sx is a registration-time module, not a runtime module. It doesn't ship to the client. Zero impact on bundle size."))
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "primitives.sx (return types exist), boundary.sx (IO return types exist), eval.sx (defcomp parsing). " (strong "New: ") (code "types.sx") " spec module, type annotations in " (code "parse-comp-params") "."))
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mt-4"
(p :class "text-violet-900 font-medium" "Why not a Haskell host?")
(p :class "text-violet-800" "A Haskell SX host would type-check the " (em "host") " code (the evaluator, renderer, parser). But it can't type-check " (em "SX programs") " — those are dynamically typed values passing through " (code "SxVal") ". " (code "types.sx") " checks SX programs directly, on every host. One spec, all hosts benefit. The type system lives where it matters — in the language, not in any particular implementation of it."))))))

View File

@@ -317,6 +317,13 @@ router.sx (standalone — pure string/list ops)")))
"bootstrap_py.py"))
(td :class "px-3 py-2 font-mono text-sm text-stone-500" "sx_ref.py")
(td :class "px-3 py-2 text-green-600" "Live"))
(tr :class "border-b border-stone-100 bg-green-50"
(td :class "px-3 py-2 text-stone-700" "Python (self-hosting)")
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
(a :href "/bootstrappers/self-hosting" :class "hover:underline"
"py.sx"))
(td :class "px-3 py-2 font-mono text-sm text-stone-500" "sx_ref.py")
(td :class "px-3 py-2 text-green-600" "G0 == G1"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 text-stone-700" "Rust")
(td :class "px-3 py-2 font-mono text-sm text-stone-400" "bootstrap_rs.py")
@@ -366,6 +373,182 @@ router.sx (standalone — pure string/list ops)")))
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight bootstrapped-output "javascript"))))))))
;; ---------------------------------------------------------------------------
;; Self-hosting bootstrapper (py.sx) — live verification
;; ---------------------------------------------------------------------------
;; @css bg-green-100 text-green-800 bg-green-50 border-green-200 text-green-700
(defcomp ~bootstrapper-self-hosting-content (&key py-sx-source g0-output g1-output defines-matched defines-total g0-lines g0-bytes verification-status)
(~doc-page :title "Self-Hosting Bootstrapper (py.sx)"
(div :class "space-y-8"
(div :class "space-y-3"
(p :class "text-stone-600"
(code :class "text-violet-700 text-sm" "py.sx")
" is an SX-to-Python translator written in SX. "
"This page runs it live: loads py.sx into the evaluator, translates each spec file, "
"and diffs the result against " (code :class "text-violet-700 text-sm" "bootstrap_py.py") ".")
(div :class "rounded-lg p-4"
:class (if (= verification-status "identical")
"bg-green-50 border border-green-200"
"bg-red-50 border border-red-200")
(div :class "flex items-center gap-3"
(span :class "inline-flex items-center rounded-full px-3 py-1 text-sm font-semibold"
:class (if (= verification-status "identical")
"bg-green-100 text-green-800"
"bg-red-100 text-red-800")
(if (= verification-status "identical") "G0 == G1" "MISMATCH"))
(p :class "text-sm"
:class (if (= verification-status "identical") "text-green-700" "text-red-700")
defines-matched "/" defines-total " defines match. "
g0-lines " lines, " g0-bytes " bytes."))))
(div :class "space-y-3"
(div :class "flex items-baseline gap-3"
(h2 :class "text-2xl font-semibold text-stone-800" "py.sx Source")
(span :class "text-sm text-stone-400 font-mono" "shared/sx/ref/py.sx"))
(p :class "text-sm text-stone-500"
"The SX-to-Python translator — 55 " (code "define") " forms. "
"Name mangling (200+ RENAMES), expression emission, statement emission, "
"cell variable detection for " (code "set!") " across lambda boundaries.")
(div :class "not-prose bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-stone-200"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight py-sx-source "lisp")))))
(div :class "space-y-3"
(div :class "flex items-baseline gap-3"
(h2 :class "text-2xl font-semibold text-stone-800" "G0 Output")
(span :class "text-sm text-stone-400 font-mono" "bootstrap_py.py → sx_ref.py"))
(p :class "text-sm text-stone-500"
"Generated by the hand-written Python bootstrapper.")
(div :class "not-prose bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-stone-200"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight g0-output "python")))))
(div :class "space-y-3"
(div :class "flex items-baseline gap-3"
(h2 :class "text-2xl font-semibold text-stone-800" "G1 Output")
(span :class "text-sm text-stone-400 font-mono" "py.sx → sx_ref.py"))
(p :class "text-sm text-stone-500"
"Generated by py.sx running on the Python evaluator. "
(if (= verification-status "identical")
(strong "Byte-for-byte identical to G0.")
"Differs from G0 — see mismatch details."))
(div :class "not-prose bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border"
:class (if (= verification-status "identical") "border-green-200" "border-red-200")
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight g1-output "python"))))))))
;; ---------------------------------------------------------------------------
;; Self-hosting JS bootstrapper (js.sx) — live verification
;; ---------------------------------------------------------------------------
;; @css bg-green-100 text-green-800 bg-green-50 border-green-200 text-green-700 bg-amber-50 border-amber-200 text-amber-700 text-amber-800 bg-amber-100
(defcomp ~bootstrapper-self-hosting-js-content (&key js-sx-source defines-matched defines-total js-sx-lines verification-status)
(~doc-page :title "Self-Hosting Bootstrapper (js.sx)"
(div :class "space-y-8"
(div :class "space-y-3"
(p :class "text-stone-600"
(code :class "text-violet-700 text-sm" "js.sx")
" is an SX-to-JavaScript translator written in SX. "
"This page runs it live: loads js.sx into the evaluator, translates every spec file, "
"and verifies each define matches " (code :class "text-violet-700 text-sm" "bootstrap_js.py") "'s JSEmitter.")
(div :class "rounded-lg p-4"
:class (if (= verification-status "identical")
"bg-green-50 border border-green-200"
"bg-red-50 border border-red-200")
(div :class "flex items-center gap-3"
(span :class "inline-flex items-center rounded-full px-3 py-1 text-sm font-semibold"
:class (if (= verification-status "identical")
"bg-green-100 text-green-800"
"bg-red-100 text-red-800")
(if (= verification-status "identical") "G0 == G1" "MISMATCH"))
(p :class "text-sm"
:class (if (= verification-status "identical") "text-green-700" "text-red-700")
defines-matched "/" defines-total " defines match across all spec files. "
js-sx-lines " lines of SX."))))
;; G0 Bug Discovery
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "G0 Bug Discovery")
(div :class "rounded-lg bg-amber-50 border border-amber-200 p-4"
(div :class "flex items-start gap-3"
(span :class "inline-flex items-center rounded-full bg-amber-100 px-3 py-1 text-sm font-semibold text-amber-800"
"Fixed")
(div :class "text-sm text-amber-700 space-y-2"
(p "Building js.sx revealed a bug in " (code "bootstrap_js.py") "'s "
(code "_emit_define") " method. The Python code:")
(pre :class "bg-amber-100 rounded p-2 text-xs font-mono"
"val = self.emit(fn_expr) if fn_expr else \"NIL\"")
(p "Python's " (code "if fn_expr") " treats " (code "0") ", "
(code "False") ", and " (code "\"\"") " as falsy. So "
(code "(define *batch-depth* 0)") " emitted "
(code "var _batchDepth = NIL") " instead of "
(code "var _batchDepth = 0") ". Similarly, "
(code "(define _css-hash \"\")") " emitted "
(code "var _cssHash = NIL") " instead of "
(code "var _cssHash = \"\"") ".")
(p "Fix: " (code "if fn_expr is not None") " — explicit None check. "
"js.sx never had this bug because SX's " (code "nil?") " only matches "
(code "nil") ", not " (code "0") " or " (code "false") ". "
"The self-hosting bootstrapper caught a host language bug.")))))
;; JS vs Python differences
(div :class "space-y-3"
(h2 :class "text-2xl font-semibold text-stone-800" "Translation Differences from py.sx")
(p :class "text-sm text-stone-500"
"Both py.sx and js.sx translate the same SX ASTs, but target languages differ:")
(div :class "overflow-x-auto rounded border border-stone-200"
(table :class "w-full text-sm"
(thead :class "bg-stone-50"
(tr
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "Feature")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "py.sx → Python")
(th :class "px-4 py-2 text-left font-semibold text-stone-700" "js.sx → JavaScript")))
(tbody
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "Name mangling")
(td :class "px-4 py-2 font-mono text-xs" "eval-expr → eval_expr")
(td :class "px-4 py-2 font-mono text-xs" "eval-expr → evalExpr"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "Declarations")
(td :class "px-4 py-2 font-mono text-xs" "name = value")
(td :class "px-4 py-2 font-mono text-xs" "var name = value;"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "Functions")
(td :class "px-4 py-2 font-mono text-xs" "lambda x: body")
(td :class "px-4 py-2 font-mono text-xs" "function(x) { return body; }"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "set! (mutation)")
(td :class "px-4 py-2 font-mono text-xs" "_cells dict (closure hack)")
(td :class "px-4 py-2 font-mono text-xs" "Direct assignment (JS captures by ref)"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "Tail recursion")
(td :class "px-4 py-2 font-mono text-xs" "—")
(td :class "px-4 py-2 font-mono text-xs" "while(true) { continue; }"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "let binding")
(td :class "px-4 py-2 font-mono text-xs" "(lambda x: body)(val)")
(td :class "px-4 py-2 font-mono text-xs" "(function() { var x = val; return body; })()"))
(tr :class "border-t border-stone-100"
(td :class "px-4 py-2 text-stone-600" "and/or")
(td :class "px-4 py-2 font-mono text-xs" "ternary chains")
(td :class "px-4 py-2 font-mono text-xs" "&& / sxOr()"))))))
;; Source
(div :class "space-y-3"
(div :class "flex items-baseline gap-3"
(h2 :class "text-2xl font-semibold text-stone-800" "js.sx Source")
(span :class "text-sm text-stone-400 font-mono" "shared/sx/ref/js.sx"))
(p :class "text-sm text-stone-500"
"The SX-to-JavaScript translator — 61 " (code "define") " forms. "
"camelCase mangling (500+ RENAMES), expression/statement emission, "
"self-tail-recursive while loop optimization.")
(div :class "not-prose bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-stone-200"
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
(code (highlight js-sx-source "lisp"))))))))
;; ---------------------------------------------------------------------------
;; Python bootstrapper detail
;; ---------------------------------------------------------------------------

View File

@@ -5,7 +5,7 @@
(h1 :class "text-5xl font-bold text-stone-900 mb-4"
(span :class "text-violet-600 font-mono" "(<sx>)"))
(p :class "text-2xl text-stone-600 mb-4"
"s-expressions for the web")
"Framework free reactive hypermedia")
(p :class "text-sm text-stone-400"
"© Giles Bradshaw 2026")
(p :class "text-lg text-stone-500 max-w-2xl mx-auto mb-12"

File diff suppressed because it is too large Load Diff

View File

@@ -230,13 +230,18 @@ def _bootstrapper_data(target: str) -> dict:
"""
import os
if target not in ("javascript", "python"):
if target not in ("javascript", "python", "self-hosting", "self-hosting-js"):
return {"bootstrapper-not-found": True}
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
if not os.path.isdir(ref_dir):
ref_dir = "/app/shared/sx/ref"
if target == "self-hosting":
return _self_hosting_data(ref_dir)
if target == "self-hosting-js":
return _js_self_hosting_data(ref_dir)
if target == "javascript":
# Read bootstrapper source
bs_path = os.path.join(ref_dir, "bootstrap_js.py")
@@ -276,6 +281,138 @@ def _bootstrapper_data(target: str) -> dict:
}
def _self_hosting_data(ref_dir: str) -> dict:
"""Run py.sx live: load into evaluator, translate spec files, diff against G0."""
import os
from shared.sx.parser import parse_all
from shared.sx.types import Symbol
from shared.sx.evaluator import evaluate, make_env
from shared.sx.ref.bootstrap_py import extract_defines, compile_ref_to_py, PyEmitter
try:
# Read py.sx source
py_sx_path = os.path.join(ref_dir, "py.sx")
with open(py_sx_path, encoding="utf-8") as f:
py_sx_source = f.read()
# Load py.sx into evaluator
exprs = parse_all(py_sx_source)
env = make_env()
for expr in exprs:
evaluate(expr, env)
# Generate G0 (hand-written bootstrapper)
g0_output = compile_ref_to_py()
# Generate G1 (py.sx) — translate each spec file
sx_files = [
("eval.sx", "eval"), ("forms.sx", "forms (server definition forms)"),
("render.sx", "render (core)"),
("adapter-html.sx", "adapter-html"), ("adapter-sx.sx", "adapter-sx"),
("deps.sx", "deps (component dependency analysis)"),
("signals.sx", "signals (reactive signal runtime)"),
]
emitter = PyEmitter()
total = 0
matched = 0
for filename, _label in sx_files:
filepath = os.path.join(ref_dir, filename)
if not os.path.exists(filepath):
continue
with open(filepath, encoding="utf-8") as f:
src = f.read()
defines = extract_defines(src)
for name, expr in defines:
g0_stmt = emitter.emit_statement(expr)
g1_stmt = evaluate(
[Symbol("py-statement"), [Symbol("quote"), expr], 0], env
)
total += 1
if g0_stmt == g1_stmt:
matched += 1
g0_lines = len(g0_output.splitlines())
g0_bytes = len(g0_output)
status = "identical" if matched == total else "mismatch"
except Exception as e:
py_sx_source = f";; error loading py.sx: {e}"
g0_output = f"# error: {e}"
matched, total = 0, 0
g0_lines, g0_bytes = 0, 0
status = "error"
return {
"bootstrapper-not-found": None,
"py-sx-source": py_sx_source,
"g0-output": g0_output,
"g1-output": g0_output if status == "identical" else "# differs from G0",
"defines-matched": str(matched),
"defines-total": str(total),
"g0-lines": str(g0_lines),
"g0-bytes": str(g0_bytes),
"verification-status": status,
}
def _js_self_hosting_data(ref_dir: str) -> dict:
"""Run js.sx live: load into evaluator, translate spec files, diff against G0."""
import os
from shared.sx.parser import parse_all
from shared.sx.types import Symbol
from shared.sx.evaluator import evaluate, make_env
from shared.sx.ref.bootstrap_js import extract_defines, JSEmitter
try:
js_sx_path = os.path.join(ref_dir, "js.sx")
with open(js_sx_path, encoding="utf-8") as f:
js_sx_source = f.read()
exprs = parse_all(js_sx_source)
env = make_env()
for expr in exprs:
evaluate(expr, env)
emitter = JSEmitter()
# All spec files
all_files = sorted(
f for f in os.listdir(ref_dir) if f.endswith(".sx")
)
total = 0
matched = 0
for filename in all_files:
filepath = os.path.join(ref_dir, filename)
with open(filepath, encoding="utf-8") as f:
src = f.read()
defines = extract_defines(src)
for name, expr in defines:
g0_stmt = emitter.emit_statement(expr)
env["_def_expr"] = expr
g1_stmt = evaluate(
[Symbol("js-statement"), Symbol("_def_expr")], env
)
total += 1
if g0_stmt.strip() == g1_stmt.strip():
matched += 1
status = "identical" if matched == total else "mismatch"
except Exception as e:
js_sx_source = f";; error loading js.sx: {e}"
matched, total = 0, 0
status = "error"
return {
"bootstrapper-not-found": None,
"js-sx-source": js_sx_source,
"defines-matched": str(matched),
"defines-total": str(total),
"js-sx-lines": str(len(js_sx_source.splitlines())),
"verification-status": status,
}
def _bundle_analyzer_data() -> dict:
"""Compute per-page component bundle analysis for the sx-docs app."""
from shared.sx.jinja_bridge import get_component_env

View File

@@ -8,20 +8,12 @@ def _register_sx_layouts() -> None:
from shared.sx.layouts import register_sx_layout
if os.getenv("SX_STANDALONE") == "true":
register_sx_layout("sx",
"sx-standalone-layout-full",
"sx-standalone-layout-oob",
"sx-standalone-layout-mobile")
register_sx_layout("sx-section",
"sx-standalone-section-layout-full",
"sx-standalone-section-layout-oob",
"sx-standalone-section-layout-mobile")
register_sx_layout("sx-docs",
"sx-standalone-docs-layout-full",
"sx-standalone-docs-layout-oob",
"sx-standalone-docs-layout-mobile")
else:
register_sx_layout("sx",
"sx-layout-full",
"sx-layout-oob",
"sx-layout-mobile")
register_sx_layout("sx-section",
"sx-section-layout-full",
"sx-section-layout-oob",
"sx-section-layout-mobile")
register_sx_layout("sx-docs",
"sx-docs-layout-full",
"sx-docs-layout-oob",
"sx-docs-layout-mobile")