Paths containing (api.) are intercepted before page routing and
dispatched directly to the api function. The handler result is
rendered to SX wire format and returned without layout wrapping.
This fixes the issue where handler URLs went through page routing,
causing the handler result to be passed as a slug to the page
function, and the response to be wrapped in the full page layout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comprehensive interaction tests covering GET handlers (click-to-load,
tabs, polling, search, filter, keyboard, lazy-load), POST handlers
(form-submission, inline-validation, reset), mutation handlers
(delete-row, edit-row, inline-edit, bulk-update, put-patch), and
special handlers (dialogs, oob-swaps, animations, progress-bar, retry,
json-encoding, vals-and-headers).
Tests use trackErrors + waitForSxReady. Will pass once server is
restarted with request primitives and handler dispatch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Request primitives (now, state-get/set!/clear!, request-form/arg,
into, request-headers-all, etc.) added to test runner environment.
17 new tests covering all primitives with round-trip, default value,
and type assertion checks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OCaml server:
- Accept POST/PUT/PATCH/DELETE for /sx/ paths (was GET-only)
- Parse request body, query string, set per-request context
- Add 16 request primitives: now, state-get/set!, request-form/arg,
request-json, request-header(s), request-method/body, into, etc.
- URL-encoded body parser for form submissions
Handler dispatch (sx/sx/handlers/dispatch.sx):
- `api` function routes URL paths like (api "click") to handler:ex-click
- `call-handler` checks HTTP method, binds params, evaluates body
- Handlers defined via defhandler in handlers/examples.sx now reachable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The OCaml parser doesn't handle \xe2\x80\x94 (em-dash). Replaced
with -- throughout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
build-reference-data, build-attr-detail, etc. were undefined because
page-helpers.sx wasn't in the explicit core_files list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These were lost during an earlier sx_replace_node edit. Both use
case expressions to route slugs to the correct data tables.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same pattern as tools — passthrough returning nil fell through to
parent geography page. Now returns empty string so layout renders
with correct nav context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The tools page function returned nil when no child route matched,
causing a 404. Now returns empty string so the layout renders.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Route errors and missing pages now show a styled error message inside
the normal layout (header, nav still work) instead of bare "nil" text
or a raw "Not Found" page. AJAX errors return renderable SX error
fragments instead of "nil" strings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>