jinja_bridge.py was creating Component objects for both defcomp AND
defisland forms. Islands need Island objects so the serializer emits
defisland (not defcomp) in the client component bundle. Without this,
client-side islands don't get data-sx-island attributes, hydration
fails, and all reactive signals (colour cycling, stepper) stop working.
Add Playwright test: islands hydrate, stepper buttons update count,
reactive colour cycling works on click.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes the 5993-line bootstrapped Python evaluator (sx_ref.py) and all
code that depended on it exclusively. Both bootstrappers (JS + OCaml)
now use a new synchronous OCaml bridge (ocaml_sync.py) to run the
transpiler. JS build produces identical output; OCaml bootstrap produces
byte-identical sx_ref.ml.
Key changes:
- New shared/sx/ocaml_sync.py: sync subprocess bridge to sx_server.exe
- hosts/javascript/bootstrap.py: serialize defines → temp file → OCaml eval
- hosts/ocaml/bootstrap.py: same pattern for OCaml transpiler
- shared/sx/{html,async_eval,resolver,jinja_bridge,handlers,pages,deps,helpers}:
stub or remove sx_ref imports; runtime uses OCaml bridge (SX_USE_OCAML=1)
- sx/sxc/pages: parse defpage/defhandler from AST instead of Python eval
- hosts/ocaml/lib/sx_primitives.ml: append handles non-list 2nd arg per spec
- Deleted: sx_ref.py, async_eval_ref.py, 6 Python test runners, misc ref/ files
Test results: JS 1078/1078, OCaml 1114/1114.
sx_docs SSR has pre-existing rendering issues to investigate separately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compiler fixes:
- Upvalue re-lookup returns own position (uv-index), not parent slot
- Spec: cek-call uses (make-env) not (dict) — OCaml Dict≠Env
- Bootstrap post-processes transpiler Dict→Env for cek_call
VM runtime fixes:
- compile_adapter evaluates constant defines (SPECIAL_FORM_NAMES etc.)
via execute_module instead of wrapping as NativeFn closures
- Native primitives: map-indexed, some, every?
- Nil-safe HO forms: map/filter/for-each/some/every? accept nil as empty
- expand-components? set in kernel env (not just VM globals)
- unwrap_env diagnostic: reports actual type received
sx-page-full command:
- Single OCaml call: aser-slot body + render-to-html shell
- Eliminates two pipe round-trips (was: aser-slot→Python→shell render)
- Shell statics (component_defs, CSS, pages_sx) cached in Python,
injected into kernel once, referenced by symbol in per-request command
- Large blobs use placeholder tokens — Python splices post-render,
pipe transfers ~51KB instead of 2MB
Performance (warm):
- Server total: 0.55s (was ~2s)
- aser-slot VM: 0.3s, shell render: 0.01s, pipe: 0.06s
- kwargs computation: 0.000s (cached)
SX_STANDALONE mode for sx_docs dev (skips fragment fetches).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OCaml kernel (sx_server.ml):
- Batch IO mode for aser-slot: batchable helpers (highlight,
component-source) return placeholders during evaluation instead
of blocking on stdin. After aser completes, all batched requests
are flushed to Python at once.
- Python processes them concurrently with asyncio.gather.
- Placeholders (using «IO:N» markers) are replaced with actual
values in the result string.
- Non-batchable IO (query, action, ctx, request-arg) still uses
blocking mode — their results drive control flow.
Python bridge (ocaml_bridge.py):
- _read_until_ok handles batched protocol: collects io-request
lines with numeric IDs, processes on (io-done N) with gather.
- IO result cache for pure helpers — eliminates redundant calls.
- _handle_io_request strips batch ID from request format.
Component caching (jinja_bridge.py):
- Hash computed from FULL component env (all names + bodies),
not per-page subset. Stable across all pages — browser caches
once, no re-download on navigation between pages.
- invalidate_component_hash() called on hot-reload.
Tests: 15/15 OCaml helper tests pass (2 new batch IO tests).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix O(n²) postprocessing: compute_all_deps/io_refs/hash were called
per-file (92x for sx app). Now deferred to single finalize_components()
call after all files load.
- Add pickle cache in shared/sx/.cache/ keyed by file mtimes+sizes.
Cache stores fully-processed Component/Island/Macro objects with deps,
io_refs, and css_classes pre-computed. Closures stripped before pickle,
rebuilt from global env after restore.
- Smart finalization: cached loads skip deps/io_refs recomputation
(already in pickle), only recompute component hash.
- Fix ~sx-header → ~layouts/header ref in docs-content.sx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Component names now reflect filesystem location using / as path separator
and : as namespace separator for shared components:
~sx-header → ~layouts/header
~layout-app-body → ~shared:layout/app-body
~blog-admin-dashboard → ~admin/dashboard
209 files, 4,941 replacements across all services.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EvalError moved to types.py. All 27 files updated to import eval_expr,
trampoline, call_lambda, etc. directly from shared.sx.ref.sx_ref instead
of through the evaluator.py indirection layer. 320/320 spec tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Islands survive hypermedia swaps: morph-node skips hydrated
data-sx-island elements when the same island exists in new content.
dispose-islands-in skips hydrated islands to prevent premature cleanup.
- @client directive: .sx files marked ;; @client send define forms to browser
- CSSX client-side: cssxgroup renamed (no hyphen) to avoid isRenderExpr
matching it as a custom element — was producing [object HTMLElement]
- Island wrappers: div→span to avoid block-in-inline HTML parse breakage
- ~sx-header is now a defisland with inline reactive colour cycling
- bootstrap_js.py defaults output to shared/static/scripts/sx-browser.js
- Deleted stale sx-ref.js (sx-browser.js is the canonical browser build)
- Hegelian Synthesis essay: dialectic of hypertext and reactivity
- component-source helper handles Island types for docs pretty-printing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Configurable page shell (~sx-page-shell kwargs + SX_SHELL app config)
so each app controls its own assets — sx docs loads only sx-browser.js
- SX-evaluated sx-on:* handlers (eval-expr instead of new Function)
with DOM primitives registered in PRIMITIVES table
- data-init boot mode for pure SX initialization scripts
- Jiggle animation on links while fetching
- Nav: 3-column grid for centered alignment, is-leaf sizing,
fix map-indexed param order (index, item), guard mod-by-zero
- Async route eval failure now falls back to server fetch
instead of silently rendering nothing
- Remove duplicate h1 title from ~doc-page
- Re-bootstrap sx-ref.js + sx-browser.js
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- z3.sx: SX-to-SMT-LIB translator written in SX (359 lines), replaces Python translation logic
- prove.sx: SMT-LIB satisfiability checker in SX — proves all 91 primitives sat by construction
- Parser: support unicode characters (em-dash, accented letters) in symbols
- Auto-resolve reader macros: #name finds name-translate in component env, no Python registration
- Platform primitives: type-of, symbol-name, keyword-name, sx-parse registered in primitives.py
- Cond heuristic: predicates ending in ? recognized as Clojure-style tests
- Library loading: z3.sx loaded at startup with reload callbacks for hot-reload ordering
- reader_z3.py: rewritten as thin shell delegating to z3.sx
- Split monolithic .sx files: essays (22), plans (13), reactive-islands (6) into separate files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rebuild sx-browser.js with signals spec module (was missing entirely)
- Register signal functions (signal, deref, effect, computed, etc.) as
PRIMITIVES so runtime-evaluated SX code in island bodies can call them
- Add reactive deref detection in adapter-dom.sx: (deref sig) in island
scope creates reactive-text node instead of static text
- Add Island SSR support in html.py (_render_island with data-sx-island)
- Add Island bundling in jinja_bridge.py (defisland defs sent to client)
- Update deps.py to track Island dependencies alongside Component
- Add defisland to _ASER_FORMS in async_eval.py
- Add clear-interval platform primitive (was missing)
- Create four live demo islands: counter, temperature, imperative, stopwatch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire async rendering into client-side routing: pages whose component
trees reference IO primitives (highlight, current-user, etc.) now
render client-side via Promise-aware asyncRenderToDom. IO calls proxy
through /sx/io/<name> endpoint, which falls back to page helpers.
- Add has-io flag to page registry entries (helpers.py)
- Remove IO purity filter — include IO-dependent components in bundles
- Extend try-client-route with 4 paths: pure, data, IO, data+IO
- Convert tryAsyncEvalContent to callback style, add platform mapping
- IO proxy falls back to page helpers (highlight works via proxy)
- Demo page: /isomorphism/async-io with inline highlight calls
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pages whose component trees reference IO primitives (e.g. highlight)
cannot render client-side, so exclude their deps from the client bundle.
This prevents "Undefined symbol: highlight" errors on pages like
bundle-analyzer while still allowing pure data pages like data-test
to render client-side with caching.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Per-page bundling now unions deps from all :data pages in the service,
so navigating between data pages uses client-side rendering + cache
instead of expensive server fetch + SX parse.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend the spec with IO scanning functions (scan-io-refs, transitive-io-refs,
compute-all-io-refs, component-pure?) that detect IO primitive references in
component ASTs. Components are classified as pure (no IO deps, safe for client
rendering) or IO-dependent (must expand server-side).
The partial evaluator (_aser) now uses per-component IO metadata instead of
the global _expand_components toggle: IO-dependent components expand server-
side, pure components serialize for client. Layout slot context still expands
all components for backwards compat.
Spec: 5 new functions + 2 platform interface additions in deps.sx
Host: io_refs field + is_pure property on Component, compute_all_io_refs()
Bootstrap: both sx_ref.py and sx-ref.js updated with IO functions
Bundle analyzer: shows pure/IO classification per page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
boundary.sx files use define-page-helper which isn't an SX eval form —
they're parsed by boundary_parser.py. Exclude them from load_sx_dir()
to prevent EvalError on startup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jinja_bridge, helpers, handlers, query_executor now conditionally
import from ref/sx_ref and ref/async_eval_ref when SX_USE_REF=1.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Evaluator: add _Thunk + _trampoline for tail-call optimization in
lambdas, components, if/when/cond/case/let/begin. All callers in
html.py, resolver.py, handlers.py, pages.py, jinja_bridge.py, and
query_registry.py unwrap thunks at non-tail positions.
SX docs: update tagline to "s-expressions for the web", rewrite intro
to reflect that SX replaces most JavaScript need, fix "What sx is not"
to acknowledge macros and TCO exist.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server computes SHA-256 hash of all component source at startup.
Client signals its cached hash via cookie (sx-comp-hash). On full
page load: cookie match → server sends empty script tag with just
the hash; mismatch → sends full source. Client loads from
localStorage on hit, parses inline + caches on miss.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Parse tw.css into per-class lookup registry at startup
- Pre-scan component CSS classes at registration time (avoid per-request regex)
- Compress SX-Css header: 8-char hash replaces full class list (LRU cache)
- Add ;@css comment annotation for dynamically constructed class names
- Safelist bg-sky-{100..400} in Tailwind config for menu-row-sx dynamic shades
- Client sends/receives hash, falls back gracefully on cache miss
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename all sexp directories, files, identifiers, and references to sx.
artdag/ excluded (separate media processing DSL).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>