Move defhandler/defquery/defaction/defpage/defrelation from hardcoded
evaluator dispatch to web/web-forms.sx extension module, registered via
register-special-form!. Adapters updated to use definition-form? and
dynamically extended form-name lists.
Fix modifier-key clicks (ctrl-click → new tab) in three click handlers:
bindBoostLink, bindClientRouteClick, and orchestration.sx bind-event.
Add event-modifier-key? primitive (eventModifierKey_p for transpiler).
Fix CSSX SSR: ~cssx/flush no longer drains the collected bucket on the
server, so the shell template correctly emits CSSX rules in <head>.
Add missing server-side DOM stubs (create-text-node, dom-append, etc.)
and SSR passthrough for portal/error-boundary/promise-delayed.
Passive event listeners for touch/wheel/scroll to fix touchpad scrolling.
97/97 Playwright demo tests + 4/4 isomorphic SSR tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root causes of missing CSSX classes in SSR:
1. _sx_trampoline_fn in sx_primitives.ml was never wired — call_any in
HO forms (map/filter/for-each) returned unresolved Thunks, so callbacks
like render-lambda-html's param binding never executed. Fixed in
bootstrap.py FIXUPS: wire Sx_primitives._sx_trampoline_fn after eval_expr.
2. adapter-html.sx used (emit! ...) and (emitted ...) which are CEK special
forms (walk kont for ScopeAccFrame), but scope-push!/scope-pop! use the
hashtable. CEK frames and hashtable are two different scope systems.
Fixed: adapter uses scope-emit!/scope-emitted (hashtable primitives).
3. env-* operations (env-has?, env-get, env-bind!, env-set!, env-extend,
env-merge) only accepted Env type. adapter-html.sx passes Dict as env.
Fixed: all env ops go through unwrap_env which handles Dict/Nil.
Also: fix merge conflict in sx/sx/geography/index.sx, remove duplicate
scope primitives from sx_primitives.ml (sx_server.ml registers them).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core VM changes:
- Add VmClosure value variant — inner closures created by OP_CLOSURE are
first-class VM values, not NativeFn wrappers around call_closure
- Convert `run` from recursive to while-loop — zero OCaml stack growth,
true TCO for VmClosure tail calls
- vm_call handles VmClosure by pushing frame on current VM (no new VM
allocation per call)
- Forward ref _vm_call_closure_ref for cross-boundary calls (CEK/primitives)
Compiler (spec/compiler.sx):
- Define hoisting in compile-begin: pre-allocate local slots for all
define forms before compiling any values. Fixes forward references
between inner functions (e.g. read-expr referencing skip-ws in sx-parse)
- scope-define-local made idempotent (skip if slot already exists)
Server (sx_server.ml):
- JIT fail-once sentinel: mark l_compiled as failed after first VM runtime
error. Eliminates thousands of retry attempts per page render.
- HTML tag bindings: register all HTML tags as pass-through NativeFns so
eval-expr can handle (div ...) etc. in island component bodies.
- Log VM FAIL errors with function name before disabling JIT.
SSR fixes:
- adapter-html.sx letrec handler: evaluate bindings in proper letrec scope
(pre-bind nil, then evaluate), render body with render-to-html instead of
eval-expr. Fixes island SSR for components using letrec.
- Add `init` primitive to OCaml kernel (all-but-last of list).
- VmClosure handling in sx_runtime.ml sx_call dispatch.
Tests: 971/971 OCaml (+19 new), 0 failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ocaml branch introduced force-dispose-islands-in for outerHTML
swaps, which destroyed hydrated islands (including their live
signals). This broke the core hypermedia+reactive pattern: the
header island's colour state was lost on navigation, and lakes
weren't being morph-updated.
Reverted to production behaviour: dispose-islands-in skips hydrated
islands. The morph algorithm then preserves them (matching by
data-sx-island name) and only morphs their lake content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sx-mount now checks if the target element has children (server-
rendered HTML). If so, skips the client re-render and only runs
hydration (process-elements, hydrate-islands, hydrate-elements).
This preserves server-rendered CSSX styling and avoids the flash
of unstyled content that occurred when the client replaced the
server HTML before re-rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
letrec in adapter-html.sx: evaluate via CEK (which handles mutual
recursion and returns a thunk), then render-value-to-html unwraps
the thunk and renders the expression with the letrec's local env.
Both islands (~layouts/header and ~home/stepper) now render
server-side with hydration markers and CSS classes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- trampoline resolves Thunk values (sf-letrec returns them for TCO)
- render-to-html handles "thunk" type by unwrapping expr+env
- effect overridden to no-op after loading signals.sx (prevents
reactive loops during SSR — effects are DOM side-effects)
- Added thunk?/thunk-expr/thunk-env primitives
- Added DOM API stubs for SSR (dom-query, schedule-idle, etc.)
Header island renders fully with styling. Stepper island still
fails SSR (letrec + complex body hits "Undefined symbol: div"
in eval path — render mode not active during CEK letrec eval).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server now renders page content as HTML inside <div id="sx-root">,
visible immediately before JavaScript loads. The SX source is still
included in a <script data-mount="#sx-root"> tag for client hydration.
SSR pipeline: after aser produces the SX wire format, parse and
render-to-html it (~17ms for a 22KB page). Islands with reactive
state gracefully fall back to empty — client hydrates them.
Supporting changes:
- Load signals.sx into OCaml kernel (reactive primitives for island SSR)
- Add cek-call and context to kernel env (needed by signals/deref)
- Island-aware component accessors in sx_types.ml
- render-to-html handles Island values (renders as component with fallback)
- Fix 431 (Request Header Fields Too Large): replace SX-Components
header (full component name list) with SX-Components-Hash (12 chars)
- CORS allow SX-Components-Hash header
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- get primitive returns nil for type mismatches (list+string) instead
of raising — matches JS/Python behavior, fixes find-nav-match errors
- scope-peek, collect!, collected, clear-collected! registered as real
primitives in sx_primitives table (not just env bindings) so the CEK
step-sf-context can find them via get-primitive
- step-sf-context checks scope-peek hashtable BEFORE walking CEK
continuation — bridges aser's scope-push!/pop! with CEK's context
- context, emit!, emitted added to SPECIAL_FORM_NAMES and handled in
aser-special (scope operations in aser rendering mode)
- sx-context NativeFn for VM-compiled code paths
- VM execution errors no longer mark functions as permanently failed —
bytecode is correct, errors are from runtime data
- kbd, samp, var added to HTML_TAGS + sx-browser.js rebuilt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Aser serialization: aser-call/fragment now return SxExpr instead of String.
serialize/inspect passes SxExpr through unquoted, preventing the double-
escaping (\" → \\\" ) that broke client-side parsing when aser wire format
was output via raw! into <script> tags. Added make-sx-expr + sx-expr-source
primitives to OCaml and JS hosts.
Binary blob protocol: eval, aser, aser-slot, and sx-page-full now send SX
source as length-prefixed blobs instead of escaped strings. Eliminates pipe
desync from concurrent requests and removes all string-escape round-trips
between Python and OCaml.
Bridge safety: re-entrancy guard (_in_io_handler) raises immediately if an
IO handler tries to call the bridge, preventing silent deadlocks.
Fetch error logging: orchestration.sx error callback now logs method + URL
via log-warn. Platform catches (fetchAndRestore, fetchPreload, bindBoostForm)
also log errors instead of silently swallowing them.
Transpiler fixes: makeEnv, scopePeek, scopeEmit, makeSxExpr added as
platform function definitions + transpiler mappings — were referenced in
transpiled code but never defined as JS functions.
Playwright test infrastructure:
- nav() captures JS errors and fails fast with the actual error message
- Checks for [object Object] rendering artifacts
- New tests: delete-row interaction, full page refresh, back button,
direct load with fresh context, code block content verification
- Default base URL changed to localhost:8013 (standalone dev server)
- docker-compose.dev-sx.yml: port 8013 exposed for local testing
- test-sx-build.sh: build + unit tests + Playwright smoke tests
Geography content: index page component written (sx/sx/geography/index.sx)
describing OCaml evaluator, wire formats, rendering pipeline, and topic
links. Wiring blocked by aser-expand-component children passing issue.
Tests: 1080/1080 JS, 952/952 OCaml, 66/66 Playwright
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix: islands (defisland) pass component? check but must NEVER be
expanded server-side — they use browser-only reactive primitives
(signal, deref, computed). Added (not (island? comp)) guard in
adapter-sx.sx aser component dispatch.
New test file: shared/sx/tests/test_ocaml_helpers.py
- TestHelperInjection: 5 tests — helper IO proxy, 2-arg calls,
aser/aser_slot with helpers, undefined helper error
- TestHelperIOPerformance: 2 tests — 20 sequential IO round-trips
complete in <5s, aser_slot with 5 helpers in <3s
- TestAserSlotClientAffinity: 6 tests — island exclusion, client
affinity exclusion, server affinity expansion, auto affinity
behavior in aser vs aser_slot
eval_sx_url stays on bridge.aser() (server-affinity only) for now.
Switching to aser_slot requires fixing the double-aser issue in
_render_to_sx where content gets re-parsed and re-asered.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs in aser-expand-component (adapter-sx.sx):
- Keyword values were eval'd (eval-expr can't handle <>, HTML tags);
now asered, matching the aser's rendering capabilities
- Missing default nil binding for unset &key params (caused
"Undefined symbol" errors for optional params like header-rows)
- aserCall string-quoted keyword values that were already serialized
SX — now inlines values starting with "(" directly
Server-affinity annotations for layout/nav shells:
- ~shared:layout/app-body, ~shared:layout/oob-sx — page structure
- ~layouts/nav-sibling-row, ~layouts/nav-children — server-side data
- ~layouts/doc already had :affinity :server
- ~cssx/flush marked :affinity :client (browser-only state)
Navigation fix: restore oob_page_sx wrapper for HTMX responses
so #main-panel section exists for sx-select/sx-swap targeting.
OCaml bridge: lazy page helper injection into kernel via IO proxy
(define name (fn (...) (helper "name" ...))) — enables aser_slot
to evaluate highlight/component-source etc. via coroutine bridge.
Playwright tests: added pageerror listener to test_no_console_errors,
new test_navigate_from_home_to_geography for HTMX nav regression.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
adapter-sx.sx: aser-expand-component expands :affinity :server components
inline during SX wire format serialization. Binds keyword args via
eval-expr, children via aser (handles HTML tags), then asers the body.
ocaml_bridge.py: 10MB readline buffer for large spec responses.
nav-data.sx: evaluator.sx filename fix.
Page rendering stays on Python _eval_slot for now — full OCaml rendering
needs the page shell IO (headers, CSRF, CSS) migrated to OCaml IO bridge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major architectural change: page function dispatch and handler execution
now go through the OCaml kernel instead of the Python bootstrapped evaluator.
OCaml integration:
- Page dispatch: bridge.eval() evaluates SX URL expressions (geography, marshes, etc.)
- Handler aser: bridge.aser() serializes handler responses as SX wire format
- _ensure_components loads all .sx files into OCaml kernel (spec, web adapter, handlers)
- defhandler/defpage registered as no-op special forms so handler files load
- helper IO primitive dispatches to Python page helpers + IO handlers
- ok-raw response format for SX wire format (no double-escaping)
- Natural list serialization in eval (no (list ...) wrapper)
- Clean pipe: _read_until_ok always sends io-response on error
SX adapter (aser):
- scope-emit!/scope-peek aliases to avoid CEK special form conflict
- aser-fragment/aser-call: strings starting with "(" pass through unserialized
- Registered cond-scheme?, is-else-clause?, primitive?, get-primitive in kernel
- random-int, parse-int as kernel primitives; json-encode, into via IO bridge
Handler migration:
- All IO calls converted to (helper "name" args...) pattern
- request-arg, request-form, state-get, state-set!, now, component-source etc.
- Fixed bare (effect ...) in island bodies leaking disposer functions as text
- Fixed lower-case → lower, ~search-results → ~examples/search-results
Reactive islands:
- sx-hydrate-islands called after client-side navigation swap
- force-dispose-islands-in for outerHTML swaps (clears hydration markers)
- clear-processed! platform primitive for re-hydration
Content restructuring:
- Design, event bridge, named stores, phase 2 consolidated into reactive overview
- Marshes split into overview + 5 example sub-pages
- Nav links use sx-get/sx-target for client-side navigation
Playwright test suite (sx/tests/test_demos.py):
- 83 tests covering hypermedia demos, reactive islands, marshes, spec explorer
- Server-side rendering, handler interactions, island hydration, navigation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce 8 irreducible host FFI primitives that replace 40+ native DOM
and browser primitives:
host-global — access global object (window/document)
host-get — read property from host object
host-set! — write property on host object
host-call — call method on host object
host-new — construct host object
host-callback — wrap SX function as host callback
host-typeof — check host object type
host-await — await host promise
All DOM and browser operations are now expressible as SX library
functions built on these 8 primitives:
web/lib/dom.sx — createElement, querySelector, appendChild,
setAttribute, addEventListener, classList, etc.
web/lib/browser.sx — localStorage, history, fetch, setTimeout,
promises, console, matchMedia, etc.
The existing native implementations remain as fallback — the library
versions shadow them in transpiled code. Incremental migration: callers
don't change, only the implementation moves from out-of-band to in-band.
JS 957+1080, Python 744, OCaml 952 — zero regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Evaluator: data-first higher-order forms — ho-swap-args auto-detects
(map coll fn) vs (map fn coll), both work. Threading + HO: (-> data
(map fn)) dispatches through CEK HO machinery via quoted-value splice.
17 new tests in test-cek-advanced.sx.
Fix plan pages: add mother-language, isolated-evaluator, rust-wasm-host
to page-functions.sx plan() — were in defpage but missing from URL router.
Aser error handling: pages.py now catches EvalError separately, renders
visible error banner instead of silently sending empty content. All
except blocks include traceback in logs.
Scope primitives: register collect!/collected/clear-collected!/emitted/
emit!/context in shared/sx/primitives.py so hand-written _aser can
resolve them (fixes ~cssx/flush expansion failure).
New test file: shared/sx/tests/test_aser_errors.py — 19 pytest tests
for error propagation through all aser control flow forms.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fundamental environment bugs fixed:
1. env-set! was used for both binding creation (let, define, params)
and mutation (set!). Binding creation must NOT walk the scope chain
— it should set on the immediate env. Only set! should walk.
Fix: introduce env-bind! for all binding creation. env-set! now
exclusively means "mutate existing binding, walk scope chain".
Changed across spec (eval.sx, cek.sx, render.sx) and all web
adapters (dom, html, sx, async, boot, orchestration, forms).
2. makeLambda/makeComponent/makeMacro/makeIsland used merge(env) to
flatten the closure into a plain object, destroying the prototype
chain. This meant set! inside closures couldn't reach the original
binding — it modified a snapshot copy instead.
Fix: store env directly as closure (no merge). The prototype chain
is preserved, so set! walks up to the original scope.
Tests: 499/516 passing (96.7%), up from 485/516.
Fixed: define self-reference, let scope isolation, set! through
closures, counter-via-closure pattern, recursive functions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>