highlight.sx now returns a list of (span :class "..." "text") elements
instead of a string. The rendering pipeline handles the rest:
- Server render-to-html: produces <span class="...">text</span>
- Client render-to-dom: produces DOM span elements
- Aser: serializes spans as SX for client rendering
Key fixes:
- hl-span uses (make-keyword "class") not :class (keywords evaluate
to strings in list context)
- render-sx-tokens returns flat list of spans (no wrapper)
- hl-escape is identity (no escaping needed for tree values)
- highlight.sx added to browser bundle + platform loader
- ~docs/code renders src directly as child of pre
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add missing ./sx/sxc:/app/sxc:ro volume mount to dev-sx compose —
container was using stale image copy of docs.sx where ~docs/code had
(&key code) instead of (&key src), so highlight output was silently
discarded
- Register HTML tags as special forms in WASM browser kernel so keyword
attrs are preserved during render-to-dom
- Add trace-boot, hydrate-debug, eval-at modes to sx-inspect.js for
debugging boot phases and island hydration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Named let's sf-named-let used call-lambda which returns a thunk that
was never trampolined. The body executed in a disconnected env, so
set! couldn't reach outer let bindings. Fixed by using cek-call which
evaluates through the full CEK machine with proper env chain.
Also converted test-named-let-set.sx from assert= (uses broken = for
lists) to deftest/assert-equal (uses deep equal?).
JS standard: 1120/1120, JS full: 1600/1600. Zero failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New tests: scope/context, multi-signal, reactive DOM, VM HO forms.
Fix rest primitive to not error-log on nil input (defensive).
1 remaining pre-existing failure: named-let set! scoping in JS
bootstrapper — not related to WASM/hydration changes.
32/32 WASM, 1593/1594 JS full.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
call_lambda returns a thunk (for TCO) but sf_named_let was passing
it directly to the CEK machine without trampolining. The loop body
never executed — set! mutations were lost and the loop returned
immediately.
One-line fix: wrap call_lambda result in trampoline.
All 87 Node tests now pass:
- test-named-let-set.js: 9/9 (was 3/9)
- test-highlight.js: 7/7 (was 1/7)
- test-smoke.js: 19/19
- test-reactive-islands.sx: 22/22
- test-reactive-islands.js: 39/39
Note: server-side source display still empty because the JIT
compiler handles named let differently (VM bytecode path, not
tree-walk). The JIT fix is separate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Named let creates a loop continuation but set! inside the loop body
does not mutate bindings in the enclosing let scope. Affects both
the WASM kernel and native OCaml CEK evaluator.
6 failing Node tests cover:
- set! counter (simplest case)
- set! counter with named let params
- set! list accumulator via append
- append! + set! counter combo
- set! string concatenation
- nested named let set!
3 baselines pass: plain let set!, functional named let, plain append!
Also adds spec/tests/test-named-let-set.sx (7 assertions, first
fails and aborts — confirms bug exists in spec test suite too).
This is the root cause of empty source code blocks on all example
pages: tokenize-sx uses set! in named let → empty tokens →
highlight returns "(<> )" → empty <code> blocks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SX-level VM couldn't find for-each/map/etc. because they're CEK
special forms, not primitives in the JS host. Added vm-resolve-ho-form
which creates wrapper functions that dispatch callbacks through
vm-call-external (handling VmClosure callbacks correctly).
Also fixed vm-call dispatch order: Lambda/Component checked before
generic callable? to avoid calling SX Lambdas as JS functions.
JS full: 1585/1585 (was 1582/3 failed). All 3 pre-existing failures fixed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: set! inside named let loop does not persist mutations
to the outer scope in the WASM kernel. tokenize-sx uses this pattern
(set! acc (append acc ...)) inside (let loop () ...) and gets empty
tokens. This makes highlight return "(<> )" for all inputs, causing
empty source code blocks on every reactive island example page.
6 failing tests document the bug at each level:
- set! in named let loop (root cause)
- tokenize-sx (empty tokens)
- highlight (empty output)
- component-source + highlight (end-to-end)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
component-source from data/helpers.sx was overriding the native
OCaml version. The SX version calls env-get with wrong arity (1 arg
vs required 2), producing empty source. Re-bind the native version
in SSR overrides after file loading.
Note: source code still not visible because highlight function
returns empty — separate issue in the aser rendering pipeline.
Also adds:
- spec/tests/test-reactive-islands.sx — 22 SX-native tests for all
14 reactive island demos (render + signal logic + DOM)
- tests/node/run-sx-tests.js — Node runner for SX test files
- tests/node/test-reactive-islands.js — 39 Node/happy-dom tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests moved from inline JS assertions to web/tests/test-wasm-browser.sx
using the standard deftest/defsuite/assert-equal framework. The JS driver
(test_wasm_native.js) now just boots the kernel, loads modules, and runs
the SX test file.
15/15 source, 15/15 bytecode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
build-all.sh was missing the sx-platform.js → sx-platform-2.js copy,
so the served file was stale (still requesting .sxbc.json instead of
.sxbc).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bytecode compiler generates .sxbc (SX text format), not .sxbc.json.
Updated loadBytecodeFile to fetch .sxbc and use load-sxbc for parsing.
Eliminates 404s for non-existent .sxbc.json files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stops 404s for bytecoded module files — the server now serves .sxbc
files with the correct content type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: sx_primitives.ml registered "effect" as a native no-op (for SSR).
The bytecode compiler's (primitive? "effect") returned true, so it emitted
OP_CALL_PRIM instead of OP_GLOBAL_GET + OP_CALL. The VM's CALL_PRIM handler
found the native Nil-returning stub and never called the real effect function
from core-signals.sx.
Fix: Remove effect and register-in-scope from the primitives table. The server
overrides them via env_bind in sx_server.ml (after compilation), which doesn't
affect primitive? checks.
Also: VM CALL_PRIM now falls back to cek_call for non-NativeFn values (safety
net for any other functions that get misclassified).
15/15 source mode, 15/15 bytecode mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The reactive-spread test used host-get "className" which doesn't exist
on Node.js DOM stubs. Changed to dom-get-attr "class". Also added
render-to-dom CSSX-in-island-scope test verifying the full hydration
pattern produces correct class attributes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Boots the full WASM kernel (js_of_ocaml mode) + 24 .sxbc modules
into a happy-dom DOM environment. Provides helpers for SX eval,
DOM queries, island hydration, and click simulation.
Smoke test covers: kernel eval, DOM manipulation via SX, component
rendering, signals (reset!/swap!/computed), effects, and island
hydration with button rendering — 19/19 pass in ~2.5s.
Known limitation: reactive text nodes inside islands don't re-render
after click (signal updates but cek-reactive-text doesn't flush).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
reactive-spread (module-loaded via K.load) doesn't apply CSSX classes
to DOM elements. Returns null instead of "sx-text-center". This is the
root cause of hydration stripping all styling — every rendering function
is define-bound from module load.
12 pass, 1 fail — the reactive-spread test is the blocker.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three root causes for reactive attribute updates not propagating in WASM:
1. `context` CEK special form only searched kont provide frames, missing
`scope-push!` entries in the native scope_stacks hashtable. Unified by
adding scope_stacks fallback to step_sf_context.
2. `flush-subscribers` used bare `(sub)` call which failed to invoke
complex closures in for-each HO callbacks. Changed to `(cek-call sub nil)`.
3. Test eagerly evaluated `(deref s)` before render-to-dom saw it.
Fixed tests to use quoted expressions matching real browser boot.
WASM native: 10/10, WASM shell: 26/26.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The OCaml build produced new WASM assets after adding resource stub
and effect/register-in-scope SSR overrides to sx_primitives.ml and
sx_server.ml. Browser needs matching WASM to boot correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revert (when (client?) ...) guard in signals.sx — it broke JS tests
since client? is false in Node.js too.
Instead, rebind effect and register-in-scope as no-ops in sx_server.ml
AFTER all .sx files load. The SX definition from signals.sx is replaced
only in the OCaml SSR context. JS tests and WASM browser keep the real
effect implementation.
Remove redundant browser primitive stubs from sx_primitives.ml — only
resource SSR stub needed (effect override moved to server setup).
JS tests: 1582/1585 (3 VM closure interop remain)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- signals.sx: guard effect body with (when (client?) ...) so effects
are no-op during SSR — only 2 stubs needed (effect, register-in-scope)
- sx_primitives.ml: add resource SSR stub (returns signal {loading: true}),
remove 27 unnecessary browser primitive stubs
- sx_server.ml: native component-source that looks up Component/Island
from env and pretty-prints the definition (replaces broken Python helper)
- reactive-islands/index.sx: Examples section with all 15 live demos
inline + highlighted source via component-source
- reactive-islands/demo.sx: replace 14 hardcoded highlight strings with
(component-source "~name") calls for always-current source
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Calling defisland components during SSR evaluates their bodies, which
reference client-only primitives (resource, def-store, etc.) that don't
exist in the OCaml SSR environment. Show only highlighted source code
instead — the islands still render via client hydration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Registering map/for-each/reduce as PRIMITIVES caused the compiler to
emit CALL_PRIM for them everywhere. The OCaml VM's call-primitive
can't invoke VM closures, causing "Undefined symbol: resource" crashes.
Revert vm.sx to original CALL_PRIM handler. Remove map/for-each/reduce
from JS PRIMITIVES so compiler emits OP_CALL instead (handled by
vm-call which dispatches correctly).
3 JS VM tests remain failing (VM closure interop) but production is stable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each demo island now has a live rendering and its highlighted SX source
code via (component-source), matching the geography/cek.sx pattern.
14 subsections covering all 15 demo islands.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- sx_browser.ml: use cek_call instead of sx_call in call-lambda to
avoid eval_expr deep-copying Dict values (breaks signal mutation)
- sx-browser.js: rebuilt with latest transpiler changes
- reactive-islands/index.sx: pretty-printed (no semantic changes)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Handle for-each/map/reduce in VM's CALL_PRIM handler instead of JS
primitives. VM closures are called via vm-call-closure (fresh VM,
shared upvalue cells). Native callables dispatch directly.
Named let doesn't work inside vm-step context — use explicit define
+ recursion for reduce loop.
JS tests: 1585/1585 (0 failures)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dom-id returns "" for elements without an id attribute, and "" is
truthy in SX. This caused morph-children to build spurious entries
in old-by-id keyed by "", then match unrelated children via the
empty key — advancing oi past id-keyed children like #main-content
and skipping them in cleanup.
Three changes in morph-children (engine.sx):
- old-by-id reduce: normalize empty dom-id to nil so id-less
elements are excluded from the lookup dict
- match-id binding: normalize empty dom-id to nil so new children
without ids don't spuriously match old children
- Case 2 skip condition: use (not (empty? ...)) instead of bare
(dom-id old-child) to avoid treating "" as a real id
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add pretty-print, read-file, env-list-typed primitives to OCaml kernel.
Convert Python reference data (attrs, headers, events, primitives) to SX
data files. Implement page helpers (component-source, handler-source,
read-spec-file, reference-data, etc.) as pure SX functions.
The helper dispatcher in HTTP mode looks up named functions in the env
and calls them directly, replacing the Python IO bridge path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
signal-add-sub! used (append! subscribers f) which returns a new list
for immutable List but discards the result — after signal-remove-sub!
replaces the subscribers list via dict-set!, re-adding subscribers
silently fails. Counter island only worked once (0→1 then stuck).
Fix: use (dict-set! s "subscribers" (append ...)) to explicitly update
the dict field, matching signal-remove-sub!'s pattern.
Build pipeline fixes:
- sx-build-all.sh now bundles spec→dist and recompiles .sxbc bytecode
- compile-modules.js syncs .sx source files alongside .sxbc to wasm/sx/
- Per-file cache busting: wasm, platform JS, and sxbc each get own hash
- bundle.sh adds cssx.sx to dist
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs in the DOM morph algorithm (web/engine.sx):
1. Empty-id keying: dom-id returns "" (not nil) for elements without an
id attribute, and "" is truthy in SX. Every id-less element was stored
under key "" in old-by-id, causing all new children to match the same
old element via the keyed branch — collapsing all children into one.
Fix: guard with (and id (not (empty? id))) in map building and matching.
2. Cleanup bug: the oi-cursor cleanup (range oi len) removed keyed elements
that were matched and moved from positions >= oi, and failed to remove
unmatched elements at positions < oi. Fix: track consumed indices in a
dict and remove all unconsumed elements regardless of position.
3. Island attr sync: morph-node delegated to morph-island-children without
first syncing the island element's own attributes (e.g. data-sx-state).
Fix: call sync-attrs before morph-island-children.
Also: pass explicit `true` to all dom-clone calls (deep clone parameter).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JIT compiler:
- Fix jit_compile_lambda: resolve `compile` via symbol lookup in env
instead of embedding VmClosure in AST (CEK dispatches differently)
- Register eval-defcomp/eval-defisland/eval-defmacro runtime helpers
in browser kernel for bytecoded defcomp forms
- Disable broken .sxbc.json path (missing arity in nested code blocks),
use .sxbc text format only
- Mark JIT-failed closures as sentinel to stop retrying
CSSX in browser:
- Add cssx.sx symlink + cssx.sxbc to browser web stack
- Add flush-cssx! to orchestration.sx post-swap for SPA nav
- Add cssx.sx to compile-modules.js and mcp_tree.ml bytecode lists
SPA navigation:
- Fix double-fetch: check e.defaultPrevented in click delegation
(bind-event already handled the click)
- Fix layout destruction: change nav links from outerHTML to innerHTML
swap (outerHTML destroyed #main-panel when response lacked it)
- Guard JS popstate handler when SX engine is booted
- Rename sx-platform.js → sx-platform-2.js to bust immutable cache
Playwright tests:
- Add trackErrors() helper to all test specs
- Add SPA DOM comparison test (SPA nav vs fresh load)
- Add single-fetch + no-duplicate-elements test
- Improve MCP tool output: show failure details and error messages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AJAX requests (SX-Request: true) now render on main thread instead
of queueing behind slow full-page renders in worker pool
- Remove pushState from click handler — handle-history does it after
swap succeeds, preventing double-push that triggered popstate handler
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: _env_bind_hook mirrored ALL env_bind calls (including
lambda parameter bindings) to the shared VM globals table. Factory
functions like make-page-fn that return closures capturing different
values for the same param names (default-name, prefix, suffix) would
have the last call's values overwrite all previous closures' captured
state in globals. OP_GLOBAL_GET reads globals first, so all closures
returned the last factory call's values.
Fix: only sync root-env bindings (parent=None) to VM globals. Lambda
parameter bindings stay in their local env, found via vm_closure_env
fallback in OP_GLOBAL_GET.
Also in this commit:
- OP_CLOSURE propagates parent vm_closure_env to child closures
- Remove JIT globals injection (closure vars found via env chain)
- sx_server.ml: SX-Request header → returns text/sx (aser only)
- sx_server.ml: diagnostic endpoint GET /sx/_debug/{env,eval,route}
- sx_server.ml: page helper stubs for deep page rendering
- sx_server.ml: skip client-libs/ dir (browser-only definitions)
- adapter-html.sx: unknown components → HTML comment (not error)
- sx-platform.js: .sxbc fallback loader for bytecode modules
- Delete sx_http.ml (standalone HTTP server, unused)
- Delete stale .sxbc.json files (arity=0 bug, replaced by .sxbc)
- 7 new closure isolation tests in test-closure-isolation.sx
- mcp_tree.ml: emit arity + upvalue-count in .sxbc.json output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sx_compiler.compile/compile_module now emit arity (local slot count) in
the bytecode dict. MCP sx_build_bytecode serializes arity into .sxbc.json.
sx-platform.js passes arity through to K.loadModule(). Without this, the
VM allocated only 16 local slots per module frame.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browser kernel:
- Add `parse` native fn (matches server: unwrap single, list for multiple)
- Restore env==global_env guard on _env_bind_hook (let bindings must not
leak to _vm_globals — caused JIT CSSX "Not callable: nil" errors)
- Add _env_bind_hook call in env_set_id so set! mutations sync to VM globals
- Fire _vm_global_set_hook from OP_DEFINE so VM defines sync back to CEK env
CEK evaluator:
- Replace recursive cek_run with iterative while loop using sx_truthy
(previous attempt used strict Bool true matching, broke in wasm_of_ocaml)
- Remove dead cek_run_iterative function
Web modules:
- Remove find-matching-route and parse-route-pattern stubs from
boot-helpers.sx that shadowed real implementations from router.sx
- Sync boot-helpers.sx to dist/static dirs for bytecode compilation
Platform (sx-platform.js):
- Set data-sx-ready attribute after boot completes (was only in boot-init
which sx-platform.js doesn't call — it steps through boot manually)
- Add document-level click delegation for a[sx-get] links as workaround
for bytecoded bind-event not attaching per-element listeners (VM closure
issue under investigation — bind-event runs but dom-add-listener calls
don't result in addEventListener)
Tests:
- New test_kernel.js: 24 tests covering env sync, parse, route matching,
host FFI/preventDefault, deep recursion
- New navigation test: "sx-get link fetches SX not HTML and preserves layout"
(currently catches layout breakage after SPA swap — known issue)
Known remaining issues:
- JIT CSSX failures: closure-captured variables resolve to nil in VM bytecode
- SPA content swap via execute-request breaks page layout
- Bytecoded bind-event doesn't attach per-element addEventListener (root
cause unknown — when listen-target guard appears to block despite element
being valid)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The deployed sx_browser.bc.wasm.js was actually the js_of_ocaml output
(pure JS), not the wasm_of_ocaml loader. Nothing synced the correct
build output from _build/ to shared/static/wasm/.
- sx_build target=ocaml now auto-syncs WASM kernel + JS fallback + assets
- sx-build-all.sh syncs after dune build
- Correct 68KB WASM loader replaces 3.6MB JS imposter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
boot-init now sets data-sx-ready on <html> and dispatches an sx:ready
CustomEvent after all islands are hydrated. Playwright tests use this
instead of networkidle + hard-coded sleeps (50+ seconds eliminated).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Iterative cek_run broke page-script parsing in browser. Reverted to
recursive — bytecode compilation overflow handled by native Sx_compiler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Node.js compile-modules.js with direct Sx_compiler.compile_module
calls in mcp_tree.ml. No subprocess, no JIT warm-up, no Node.js.
23 files compile in 1.9 seconds.
Also includes rebuilt WASM kernel (iterative cek_run) and all 23
bytecode modules recompiled with native compiler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite compile-modules.js to use the native OCaml sx_server binary
instead of the js_of_ocaml kernel in Node.js. Compiles 23 modules in
23s (was 3+ minutes). Uses batch epoch protocol with latin1 encoding
to preserve byte positions for multi-byte UTF-8 content.
- Add compile-blob server command: parse source natively, compile via
SX compile-module, return bytecode dict
- Fix orchestration.sxbc.json and boot.sxbc.json — never compiled
successfully with the old JS kernel, now work with native compiler
- Auto-copy compiled bytecode to shared/static/wasm/sx/ for serving
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CEK evaluator's cek_run was recursive (calls itself via cek_step).
Native OCaml handles deep recursion but wasm_of_ocaml compiles to JS
which has ~10K frame stack limit. Complex expressions (bytecode compiler)
exceeded this, causing "Maximum call stack size exceeded" in all WASM
bytecode compilation.
Replace recursive cek_run with iterative while loop — same semantics,
zero stack growth. Fixes sx_build_bytecode and browser-side evaluation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove prism.js, sweetalert2, body.js, sx-browser.js from shell —
only WASM kernel (sx_browser.bc.wasm.js + sx-platform.js) loads
- Restore request-handler.sx integration: SX handles routing + AJAX
detection, OCaml does aser → SSR → shell render pipeline
- AJAX fragment support: SX-Request header returns content fragment
(~14KB) instead of full page (~858KB), cached with "ajax:" prefix
- Fix language/applications/etc page functions to return empty fragment
instead of nil (was causing 404s)
- Shared JIT VM globals: env_bind hook mirrors ALL bindings to a single
shared globals table — eliminates stale-snapshot class of JIT bugs
- Add native `parse` function for components that need SX parsing
- Clean up unused shell params (sx-js-hash, body-js-hash, head-scripts,
body-scripts, use-wasm) from shell.sx, helpers.py, and server.ml
14/32 Playwright tests pass (navigation, SSR, isomorphic, geography).
Remaining failures are client-side (WASM bytecode 404s block hydration).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite geography-demos, demo-interactions, and reactive-nav specs
to use 300-500ms waits instead of 2-3s sleeps. Each test still does
a fresh page.goto() for isolation (no knock-on failures) but benefits
from server response cache for near-instant page loads.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>