Commit Graph

379 Commits

Author SHA1 Message Date
f4f8715d06 WASM rebuild + playground page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:36:44 +00:00
9b060ef8c5 Bytecode compiler: desugar let-match, fix SPA navigation
The bytecode compiler now handles let-match (both variants):
- Variant 1: (let-match name expr {:k v} body...) — named binding + destructure
- Variant 2: (let-match {:k v} expr body...) — pattern-only destructure

Desugars to sequential let + get calls — no new opcodes needed.

This was the last blocker for SPA navigation. The bytecoded orchestration
and router modules used let-match which compiled to CALL_PRIM "let-match"
(undefined at runtime). Now desugared at compile time.

Also synced dist/sx/ sources with web/ and recompiled all 26 .sxbc modules.

2650/2650 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:31:17 +00:00
aee4770a6a Lazy module loading: compiler loads on demand, playground page
- load-library! native: islands can declare module dependencies at
  hydration time, triggering on-demand .sxbc loading
- JIT compiler lazy-load: compiler.sxbc loads via setTimeout after boot,
  eliminating "JIT: compiler not loaded" errors
- _import_hook on sx_types: infrastructure for hosts to resolve import
  suspensions inside eval_expr (server wiring deferred to Step 8)
- Playground page (/sx/(tools.(playground))): REPL island that lazy-loads
  the compiler module when navigated to — demonstrates the full
  lazy loading pipeline

Known remaining issues:
- SPA navigation broken for pages using let-match (orchestration.sx,
  router.sx) — bytecode compiler doesn't handle let-match special form
- Server-side "IO suspension in non-IO context" during http_load_files —
  needs cek_run import handling (deferred to Step 8)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 20:34:08 +00:00
4baed1853c OCaml runtime: R7RS parameters, VM closure introspection, import suspension
- R7RS parameter primitives (make-parameter, parameter?, parameterize support)
- VM closure get_val introspection (vm-code, vm-upvalues, vm-name, vm-globals)
- Lazy list caching on vm_code for transpiled VM performance
- VM import suspension: check_io_suspension + resume_module for browser lazy loading
- 23 new R7RS tests (parameter-basic, parameterize-basic, syntax-rules-basic)
- Playwright bytecode-loading spec + WASM rebuild

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 18:48:51 +00:00
2727577702 VM import suspension for browser lazy loading
Bytecode compiler now emits OP_PERFORM for (import ...) and compiles
(define-library ...) bodies. The VM stores the import request in
globals["__io_request"] and stops the run loop — no exceptions needed.
vm-execute-module returns a suspension dict, vm-resume-module continues.

Browser: sx_browser.ml detects suspension dicts from execute_module and
returns JS {suspended, op, request, resume} objects. The sx-platform.js
while loop handles cascading suspensions via handleImportSuspension.

13 modules load via .sxbc bytecode in 226ms (manifest-driven), both
islands hydrate, all handlers wired. 2650/2650 tests pass including
6 new vm-import-suspension tests.

Also: consolidated sx-platform-2.js → sx-platform.js, fixed
vm-execute-module missing code-from-value call, fixed bootstrap.py
protocol registry transpiler issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 17:11:12 +00:00
e46cdf3d4d Wire transpiled VM as active execute_module — 2644 tests pass
The transpiled VM (sx_vm_ref.ml, from lib/vm.sx) is now the ACTIVE
bytecode execution engine. sx_server.ml and sx_browser.ml call
Sx_vm_ref.execute_module instead of Sx_vm.execute_module.

Results:
- OCaml tests: 2644 passed, 0 failed
- WASM tests: 32 passed, 0 failed
- Browser: zero errors, zero warnings, islands hydrate
- Server: pages render, JIT compiles, all routes work

The VM logic now lives in ONE place: lib/vm.sx (SX).
OCaml gets it via transpilation (bootstrap_vm.py).
JS/browser gets it via bytecode compilation (compile-modules.js).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:34:11 +00:00
cd61c049e3 vm.sx feature parity: JIT dispatch, active VM tracking, CEK fallback
Add missing features to lib/vm.sx that sx_vm.ml has:
- *active-vm* mutable global for HO primitive callback VM reuse
- *jit-compile-fn* platform-settable JIT compilation hook
- try-jit-call: check lambda-compiled, attempt JIT, fallback to CEK
- vm-call: VmClosure→push-frame, Lambda→try-jit, Component→CEK
- vm-call-closure: save/restore *active-vm* around execution
- vm-push-frame: refactored to use accessor functions
- cek-call-or-suspend: preamble-provided CEK interop

Transpiled output (sx_vm_ref.ml) now has 12 functions (was 9):
  *active-vm*, *jit-compile-fn*, try-jit-call, vm-call,
  vm-resolve-ho-form, vm-call-external, env-walk, env-walk-set!,
  vm-run, vm-step, vm-call-closure, vm-execute-module

48 preamble functions (native OCaml type access).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:00:07 +00:00
fc2b5e502f Step 5p6 lazy loading + Step 6b VM transpilation prep
Lazy module loading (Step 5 piece 6 completion):
- Add define-library wrappers + import declarations to 13 source .sx files
- compile-modules.js generates module-manifest.json with dependency graph
- compile-modules.js strips define-library/import before bytecode compilation
  (VM doesn't handle these as special forms)
- sx-platform.js replaces hardcoded 24-file loadWebStack() with manifest-driven
  recursive loader — only downloads modules the page needs
- Result: 12 modules loaded (was 24), zero errors, zero warnings
- Fallback to full load if manifest missing

VM transpilation prep (Step 6b):
- Refactor lib/vm.sx: 20 accessor functions replace raw dict access
- Factor out collect-n-from-stack, collect-n-pairs, pad-n-nils helpers
- bootstrap_vm.py: transpiles 9 VM logic functions to OCaml
- sx_vm_ref.ml: proof that vm.sx transpiles (preamble has stubs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 12:18:41 +00:00
7b4c918773 Recompile all 26 .sxbc with define-library wrappers + fix eval/JIT
All 26 browser modules recompiled with define-library/import forms.
Compilation works without vm-compile-adapter (JIT pre-compilation
hangs with library wrappers in some JIT paths — skipped for now,
CEK compilation is ~34s total).

Key fixes:
- eval command: import-aware loop that handles define-library/import
  locally without touching the Python bridge pipe (avoids deadlock)
- compile-modules.js: skip vm-compile-adapter, bump timeout

2621/2621 OCaml tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:08:00 +00:00
1dd4c87d64 Step 5: CEK IO suspension + R7RS modules (define-library/import)
Third CEK phase "io-suspended": perform suspends evaluation, host
resolves IO, cek-resume feeds result back. VM OP_PERFORM (opcode 112)
enables JIT-compiled functions to suspend. VM→CEK→suspend chain
propagates suspension across the JIT/CEK boundary via pending_cek.

R7RS define-library creates isolated environments with export control.
import checks the library registry and suspends for unknown libraries,
enabling lazy on-demand loading. Import qualifiers: only, prefix.

Server-side cek_run_with_io handles suspension by dispatching IO
requests to the Python bridge and resuming. guard composes cleanly
with perform for structured error recovery across IO boundaries.

2598/2598 tests (30 new: 15 core suspension, 3 JIT, 1 cross-boundary,
9 modules, 2 error handling). Zero regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:55:43 +00:00
8e5cf2a5d5 Rebuild WASM browser bundles after zero-patch retranspile
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:06:11 +00:00
508a0017a7 Update WASM browser bundles for R7RS evaluator changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:26:06 +00:00
ede05c26f5 IO registry: defio declares platform suspension points
Core SX has zero IO — platforms extend __io-registry via (defio name
:category :data/:code/:effect ...). The server web platform declares 44
operations in web/io.sx. batchable_helpers now derived from registry
(:batchable true) instead of hardcoded list. Startup validation warns if
bound IO ops lack registry entries. Browser gets empty registry, ready
for step 5 (IO suspension).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:21:48 +00:00
869f49bc01 Merge sx-tools: test coverage + bug fixes + Playwright fixes
- 7 new test files (~268 tests): stdlib, adapter-html, adapter-dom,
  boot-helpers, page-helpers, layout, tw-layout
- Fix component-pure? transitive scan, render-target crash on unknown
  components, &rest param binding (String vs Symbol), swap! extra args
- Fix 5 Playwright marshes tests: timing + test logic
- 2522/2522 OCaml tests, 173/173 Playwright tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

# Conflicts:
#	shared/static/wasm/sx/orchestration.sxbc
#	shared/static/wasm/sx_browser.bc.js
#	shared/static/wasm/sx_browser.bc.wasm.js
#	sx/sx/not-found.sx
#	tests/playwright/isomorphic.spec.js
2026-04-02 18:59:45 +00:00
6d5c410d68 Uncommitted sx-tools changes: WASM bundles, Playwright specs, engine fixes
WASM browser bundles rebuilt with latest kernel. Playwright test specs
updated (helpers, navigation, handler-responses, hypermedia-handlers,
isomorphic, SPA navigation). Engine/boot/orchestration SX files updated.
Handler examples and not-found page refreshed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:58:38 +00:00
a64b693a09 Remove old CSSX system — ~tw is the sole CSS engine
Phase 1 Step 1 of the architecture roadmap. The old cssx.sx
(cssx-resolve, cssx-process-token, cssx-template, old tw function)
is superseded by the ~tw component system in tw.sx.

- Delete shared/sx/templates/cssx.sx
- Remove cssx.sx from all load lists (sx_server.ml, run_tests.ml,
  mcp_tree.ml, compile-modules.js, bundle.sh, sx-build-all.sh)
- Replace (tw "tokens") inline style calls with (~tw :tokens "tokens")
  in layouts.sx and not-found.sx
- Remove _css-hash / init-css-tracking / SX-Css header plumbing
  (dead code — ~tw/flush + flush-collected-styles handle CSS now)
- Remove sx-css-classes param and meta tag from shell template
- Update stale data-cssx references to data-sx-css in tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:18:07 +00:00
670295bf01 Fix CSS styles lost during SPA navigation
The text/sx AJAX response path (handle-sx-response) never called
hoist-head-elements, so <style> elements stayed in #sx-content instead
of moving to <head>. Additionally, CSS rules collected during client-side
island hydration were never flushed to the DOM.

- Add hoist-head-elements call to handle-sx-response (matching
  handle-html-response which already had it)
- Add flush-collected-styles helper that drains collected CSS rules
  into a <style data-sx-css> element in <head>
- Call flush after island hydration in post-swap, boot-init, and
  run-post-render-hooks to catch reactive re-renders
- Unify on data-sx-css attribute (existing convention) in ~tw/flush
  and shell template, removing the ad-hoc data-cssx attribute

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:08:22 +00:00
1098dd3794 Revert render-dom-lake to original, simplify stepper lake
Reverted render-dom-lake SSR reuse — it broke OOB swaps (claimed
old lake elements during morph, stale content in copyright). The
framework's morphing handles lake updates correctly already.

Stepper: lake passes nil on client (prevents raw SX flash), effect
always calls rebuild-preview (no initial-render flag needed). Server
renders the expression for SSR; client rebuilds via render-to-dom
after boot when ~tw is available.

Removed initial-render dict flag — unnecessary complexity.

Copyright route not updating is a pre-existing issue: render-dom-island
renders the header island inline during OOB content rendering (sets
island-hydrated mark), but the copyright lake content doesn't reflect
the new path. Separate investigation needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:59:39 +00:00
c578dedbcc Revert render-dom-lake child re-rendering — broke stepper preview
The change to re-render children when non-empty caused the stepper's
lake to show raw SX again (the ~tw expressions in steps-to-preview
were rendered as text). Reverted to the simpler approach: reuse the
existing SSR element and preserve its content entirely.

The copyright route not updating during SPA nav is a pre-existing
issue (path is an island parameter, not a reactive signal).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:41:31 +00:00
bea8779aea render-dom-lake: mark reused lakes to prevent SPA nav conflicts
After reusing an SSR lake element, set data-sx-lake-claimed attribute.
Subsequent dom-query uses :not([data-sx-lake-claimed]) to skip already-
reused elements, preventing SPA navigation from picking up stale lakes
from previous pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:30:50 +00:00
547d271571 Fix stepper: lake SSR preservation + stack rebuild after stepping
Three fixes:

1. Framework: render-dom-lake preserves SSR elements during hydration.
   When client-side render-to-dom encounters a lake with an existing
   DOM element (from SSR), it reuses that element instead of creating
   a new one. This prevents the SSR HTML from being replaced with
   unresolvable raw SX expressions (~tw calls).

2. Stepper: skip rebuild-preview on initial hydration. Uses a non-
   reactive dict flag (not a signal) to avoid triggering the effect
   twice. On first run, just initializes the DOM stack from the
   existing SSR content by computing open-element depth from step
   types and walking lastElementChild.

3. Stepper: rebuild-preview computes correct DOM stack after re-render.
   Same depth computation + DOM walk approach. This fixes the bug where
   do-step after do-back would append elements to the wrong parent
   (e.g. "sx" span outside h1).

Also: increased code view font-size from 0.5rem to 0.85rem.

Playwright tests:
- lake never shows raw SX during hydration (mutation observer)
- back 6 + forward 6 keeps all 4 spans inside h1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:18:16 +00:00
58a122a73a Fix stepper: mutable-list for append! in split-tag and steps-to-preview
The stepper's split-tag and steps-to-preview used (list) with append!,
but in the WASM kernel (list) creates immutable List values — append!
returns a new list without mutating, so children accumulate nowhere.

Changed all accumulator initializations to (mutable-list):
- split-tag: cch, cat, spreads
- steps-to-preview: bc-loop inner children, initial call
- result and tokens lists in the parsing setup

Also includes WASM rebuild with append! primitive and &rest fixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:33:15 +00:00
7a002bf2ab Rebuild WASM/JS browser bundles with &rest and append! fixes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:19:53 +00:00
d40a9c6796 sx-tools: WASM kernel updates, TW/CSSX rework, content refresh, new debugging tools
Build tooling: updated OCaml bootstrapper, compile-modules, bundle.sh, sx-build-all.
WASM browser: rebuilt sx_browser.bc.js/wasm, sx-platform-2.js, .sxbc bytecode files.
CSSX/Tailwind: reworked cssx.sx templates and tw-layout, added tw-type support.
Content: refreshed essays, plans, geography, reactive islands, docs, demos, handlers.
New tools: bisect_sxbc.sh, test-spa.js, render-trace.sx, morph playwright spec.
Tests: added test-match.sx, test-examples.sx, updated test-tw.sx and web tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:31:57 +00:00
7651260fc7 Fix CSS styles lost during SPA navigation
The text/sx AJAX response path (handle-sx-response) never called
hoist-head-elements, so <style> elements stayed in #sx-content instead
of moving to <head>. Additionally, CSS rules collected during client-side
island hydration were never flushed to the DOM.

- Add hoist-head-elements call to handle-sx-response (matching
  handle-html-response which already had it)
- Add flush-collected-styles helper that drains collected CSS rules
  into a <style data-sx-css> element in <head>
- Call flush after island hydration in post-swap, boot-init, and
  run-post-render-hooks to catch reactive re-renders
- Unify on data-sx-css attribute (existing convention) in ~tw/flush
  and shell template, removing the ad-hoc data-cssx attribute

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 09:37:58 +00:00
b3718c06d0 Fix keyboard shortcuts + reset-on-submit (JS platform workarounds)
WASM host-callbacks on document/body don't fire, and the CSS selector
[sx-on\:] doesn't work in the WASM query engine. Work around both
issues in sx-platform-2.js:

1. Keyboard shortcuts: global document keyup listener matches elements
   by sx-trigger attribute containing key=='X', calls execute-request
   via K.eval
2. sx-on: inline handlers: scan all elements for sx-on:* attributes,
   bind JS Function handlers. Handle HTML attribute lowercasing
   (afterSwap → afterswap) by listening for both camelCase and
   lowercase event name forms
3. bind-event from: modifier resolves "body"/"document"/"window" to
   direct references (dom-body, dom-document, dom-window)
4. Native SX key filter for [condition] triggers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 23:25:36 +00:00
683e334546 Fix keyboard shortcuts + trigger filter + sx-on event mapping
1. parse-trigger-spec: strip [condition] from event names, store as
   "filter" modifier
2. bind-event: native SX filter for key=='X' patterns (extracts key
   char and checks event.key + not-input guard)
3. bind-event from: modifier: resolve "body"/"document"/"window" to
   direct DOM references instead of dom-query
4. sx-platform-2.js: global keyboard dispatch — WASM host-callbacks
   on document/body don't fire, so keyboard triggers with from:body
   are handled from JS, calling execute-request via K.eval
5. bind-inline-handlers: map afterSwap/beforeRequest to sx: prefix,
   eval JS bodies via Function constructor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 23:19:25 +00:00
235d73d837 Engine: trigger filter support, sx-on event mapping, JS inline handlers
1. parse-trigger-spec: strip [condition] from event names, store as
   "filter" modifier (e.g. keyup[key=='s'] → event="keyup", filter=...)
2. bind-event: evaluate filter conditions via JS Function constructor
   when filter modifier is present
3. bind-inline-handlers: map afterSwap/beforeRequest etc. to sx:*
   event names (matching what the engine dispatches)
4. bind-inline-handlers: detect JS syntax in body (contains ".") and
   eval via Function instead of SX parse — enables this.reset() etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:56:24 +00:00
13eb701518 Fix 3 handler examples: put-patch names, value-select keys, active-search primitive
1. Rename ex-pp-* handlers to ex-putpatch-* to match URL slugs
   (putpatch-edit-all, putpatch, putpatch-cancel)
2. Fix value-select-data: keyword keys (:Languages) → string keys
   ("Languages") so request-arg string lookup matches
3. Fix dom-value → element-value in orchestration.sx bind-event
   changed modifier (dom-value was never defined, element-value is)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:14:48 +00:00
f5f58ea47e Fix polling interval leak on SPA navigation
The poll trigger in bind-triggers called set-interval but discarded the
interval ID, so polls continued firing after the element was removed from
the DOM. Now the callback checks el.isConnected each tick and self-clears
when the element is gone (HTMX-style cleanup).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:44:57 +00:00
d1b49db057 Fix tag name case in default trigger detection, refactor engine tests
Tag names from dom-tag-name are lowercase (not uppercase) in the WASM
kernel — fix FORM/INPUT/SELECT/TEXTAREA comparisons in get-default-trigger.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:22:22 +00:00
46f77c3b1e Adapter fixes, orchestration updates, example content + SPA tests
From other session: adapter-html/sx/dom fixes, orchestration
improvements, examples-content refactoring, SPA navigation test
updates, WASM copies synced.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:35:49 +00:00
584445a843 SPA navigation, page component refactors, WASM rebuild
Refactor page components (docs, examples, specs, reference, layouts)
and adapters (adapter-sx, boot-helpers, orchestration) across sx/ and
web/ directories. Add Playwright SPA navigation tests. Rebuild WASM
kernel with updated bytecode. Add OCaml primitives for request handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:00:51 +00:00
fe6115f2fc Rebuild sx-browser.js with named-let set! fix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 23:45:17 +00:00
b62dfb25e5 Highlight returns SX tree, rendered to HTML/DOM by pipeline
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>
2026-03-31 23:12:36 +00:00
83b4afcd7a Fix named-let set! scoping + convert test to deftest
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>
2026-03-31 21:34:19 +00:00
fcb7e2ccaf Fix named let: trampoline call_lambda result in sf_named_let
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>
2026-03-31 18:09:56 +00:00
6550e9b2e4 Fix build-all.sh: deploy platform JS to shared/static/wasm/
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>
2026-03-31 17:05:31 +00:00
4eeb7777aa Fix platform JS: load .sxbc text format, not .sxbc.json
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>
2026-03-31 17:02:55 +00:00
a7efcaf679 Fix hydration: effect was a no-op primitive, bytecode compiler emitted CALL_PRIM
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>
2026-03-31 16:56:31 +00:00
521782d579 Fix WASM reactive signals: unify context/scope, fix flush-subscribers
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>
2026-03-31 15:12:25 +00:00
e1770499df Sync WASM kernel with resource/effect SSR primitives
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>
2026-03-31 10:56:53 +00:00
e2b29fb9f3 Fix effect SSR: rebind after file load instead of guarding spec
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>
2026-03-31 10:55:04 +00:00
128dbe1b25 Live demo islands with highlighted source: SSR-safe effect, native component-source
- 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>
2026-03-31 10:45:22 +00:00
d9aa19cfe9 Revert VM HOF primitives — broke OCaml JIT CALL_PRIM dispatch
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>
2026-03-31 10:18:07 +00:00
d26029fee2 Fix call-lambda signal mutation, rebuild browser JS, reformat reactive-islands
- 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>
2026-03-31 10:05:02 +00:00
9cf67e7354 Fix morph-children: treat empty-string element IDs as nil
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>
2026-03-31 09:29:39 +00:00
f828fb023b Fix 73 JS test failures: match transpiler, sxEq, deref frame, signals, stepper lib
Evaluator fixes (from broken match refactor in 8bba02f):
- Deref frame: use CEK state `value`, not `(get frame "value")`
- Deref frame: restore `(context "sx-reactive" nil)` (was undefined `get-tracking-context`)
- Scope-acc frame: restore missing `(get frame "value")` arg to make-scope-acc-frame
- Add missing `thread-insert-arg` helper for thread-first non-HO branch

Transpiler (hosts/javascript/transpiler.sx):
- Add `match` special form handler (IIFE with chained if/return, `_` wildcard)
- Replace `=`/`!=` infix `==` with `sxEq()` function call for proper symbol equality

JS platform (hosts/javascript/platform.py):
- Add `sxEq` for structural symbol/keyword comparison
- Add `componentFile`, `sort`, `defStore`/`useStore`/`clearStores` primitives
- Add `length`/`map`/`for-each`/`reduce` as VM-compatible HOF primitives
- Fix `SYM` → `makeSymbol` references

New files:
- sx/sx/stepper-lib.sx: extracted split-tag, build-code-tokens, steps-to-preview

JS tests: 0 → 1582/1585 passing (3 remaining are VM closure interop)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 08:33:27 +00:00
9ce8659f74 Fix signal-add-sub! losing subscribers after remove, fix build pipeline
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>
2026-03-31 07:36:36 +00:00
5abc947ac7 Fix morph-children: empty-id keying, consumed-set cleanup, island attr sync
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>
2026-03-30 23:58:23 +00:00