Commit Graph

16 Commits

Author SHA1 Message Date
c72a5af04d WIP: pre-existing changes from WASM browser work + test infrastructure
Accumulated changes from WASM browser development sessions:
- sx_runtime.ml: signal subscription + notify, env unwrap tolerance
- sx_browser.bc.js: rebuilt js_of_ocaml browser kernel
- sx_browser.bc.wasm.js + assets: WASM browser kernel build
- sx-platform.js browser tests (test_js, test_platform, test_wasm)
- Playwright sx-inspect.js: interactive page inspector tool
- harness-web.sx: DOM assertion updates
- deploy.sh, Dockerfile, dune-project: build config updates
- test-stepper.sx: stepper unit tests
- reader-macro-demo plan update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:40:38 +00:00
10576f86d1 WASM browser build: interned env keys, async kernel boot, bundled .sx platform
- Symbol interning in sx_types.ml: env lookups use int keys (intern/unintern)
  to avoid repeated string hashing in scope chain walks
- sx-platform.js: poll for SxKernel availability (WASM init is async)
- shell.sx: load sx_browser.bc.wasm.js when SX_USE_WASM=1
- bundle.sh: fix .sx file paths (web-signals.sx rename)
- browser/dune: target byte+js+wasm modes
- Bundle 23 .sx platform files for browser (dom, signals, router, boot, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:37:42 +00:00
9e079c9c19 Add (cyst) form: isolated reactive subtree that survives parent re-renders
New language feature: (cyst [:key id] body...) creates a DOM container
with its own island scope that persists across parent reactive re-renders.
On first render, the body is evaluated in a fresh with-island-scope and
the resulting DOM is cached. On subsequent renders, the cached DOM node
is returned if still connected to the document.

This solves the fundamental problem of nesting reactive islands inside
other islands' render trees — the child island's DOM (with its event
handlers and signal subscriptions) survives when the parent re-renders.

Implementation: *memo-cache* dict keyed by cyst id. render-dom checks
isConnected before returning cached node. Each cyst gets its own
disposer list via with-island-scope.

Usage in sx-tools: defisland render preview now wrapped in (cyst :key
full-name ...). Real mouse clicks work — counter increments, temperature
converts, computed signals update. Verified on both local and live site.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 08:13:27 +00:00
b53a0fabea Inline test runner: 5/5 temperature converter tests passing
Fixed three fundamental issues:
1. cek-try arg passing: handler was called with raw string instead of
   (List [String msg]), causing "lambda expects 1 args, got N" errors
2. Silent island hydration failures: hydrate-island now wraps body
   render in cek-try, displaying red error box with stack trace instead
   of empty div. No more silent failures.
3. swap! thunk leak: apply result wasn't trampolined, storing thunks
   as signal values instead of evaluated results

Also fixed: assert= uses = instead of equal? for value comparison,
assert-signal-value uses deref instead of signal-value, HTML entity
decoding in script tag test source via host-call replaceAll.

Temperature converter demo page now shows live test results:
  ✓ initial celsius is 20
  ✓ computed fahrenheit = celsius * 1.8 + 32
  ✓ +5 increments celsius
  ✓ fahrenheit updates on celsius change
  ✓ multiple clicks accumulate

1116/1116 OCaml tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:51:30 +00:00
4b733e71b0 Reactive expressions: auto-wrap deref-containing exprs in computed signals
Added contains-deref? predicate to adapter-dom.sx. When rendering a text
expression that contains (deref ...) inside an island scope, the adapter
now wraps it in (reactive-text (computed (fn () (eval-expr expr env)))).
This tracks signal dependencies through arbitrary expressions like
(str (deref celsius) "°C") and (+ (* (deref celsius) 1.8) 32).

Previously only bare (deref sig) was reactive. Now compound expressions
like string interpolation and arithmetic over signals update in real
time. The temperature converter preview in sx-tools is fully reactive:
clicking +5/-5 updates both °C and °F displays live.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 23:59:03 +00:00
31ed8b20f4 Remove click buffer, add CSS cursor:pointer for island controls
The click buffer (capture + stopPropagation + replay) caused more
harm than good: synchronous XHR blocks the main thread during kernel
load, so there's no window where clicks can be captured. The buffer
was eating clicks after hydration due to property name mismatches.

Replace with pure CSS: buttons/links/[role=button] inside islands
get cursor:pointer from SSR. No JS needed, works immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:50:12 +00:00
27059c0581 Pending pulse animation for pre-hydration clicks
Buttons clicked before hydration get a subtle pulse animation
(sx-pending class) showing the click was captured. The animation
is removed when the click is replayed after hydration, or cleared
on boot completion as a fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:49:02 +00:00
ebbdec8f4c Fix orchestration.sx parse error, add parser line/col diagnostics
The parser was reporting "Unexpected char: )" with no position info.
Added line number, column, and byte position to all parse errors.

Root cause: bind-sse-swap had one extra close paren that naive paren
counting missed because a "(" exists inside a string literal on L1074
(starts-with? trimmed "("). Parse-aware counting (skipping strings
and comments) correctly identified the imbalance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:29:28 +00:00
f0f16d24bc Fix bridge newline escaping, stepper optimization, dom-listen dedup
- ocaml_sync.py: escape newlines in eval/load_source to prevent
  protocol desync (bridge crashed on any multi-line SX)
- Stepper: do-back uses rebuild-preview (O(1) render) instead of
  replaying all steps. Hydration effect same. Cookie save on button
  click only.
- dom.sx: remove duplicate dom-listen (was shadowing the one at
  line 351 that adapter-dom.sx's dom-on wraps)
- orchestration.sx: fix bind-sse-swap close paren count
- safe_eq: Dict equality via __host_handle for DOM node identity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:14:10 +00:00
fb8dbeba9f Fix dom-listen: rename dom-on to dom-listen in dom.sx
adapter-dom.sx defines dom-on as a wrapper around dom-listen (adds
post-render hooks). But dom-listen was never defined — my earlier
dom-on in dom.sx was overwritten by the adapter's version. Rename
to dom-listen so the adapter's dom-on can call it.

This fixes click handlers not firing on island buttons (stepper,
stopwatch, counter, etc.).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:56:34 +00:00
0c8c0b6426 Cache-bust .sx files, optimize stepper back/hydration
Platform:
- sx-platform.js: extract ?v= query from script tag URL, append to
  all .sx file XHR requests. Prevents stale cached .sx files.

Stepper performance:
- do-back: use rebuild-preview (pure SX→DOM render) instead of
  replaying every do-step from 0. O(1) instead of O(n).
- Hydration effect: same rebuild-preview instead of step replay.
- Cookie save moved from do-step to button on-click handlers only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:54:43 +00:00
1a3ee40e0d Event buffering during hydration gap
Clicks on island elements before hydration completes are captured
and replayed after boot finishes:

- shell.sx: inline script (capture phase) buffers clicks on
  [data-sx-island] elements that aren't hydrated yet into window._sxQ
- boot.sx: after hydration + process-elements, replays buffered clicks
  by calling target.click() on elements still connected to the DOM

This makes SSR islands feel interactive immediately — the user can
click a button while the SX kernel is still loading/hydrating, and
the action fires as soon as the handler is wired up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:45:04 +00:00
998536f52d Cache-bust wasm scripts, fix orchestration.sx paren balance
- Add wasm_hash (MD5 of sx_browser.bc.js) to shell template
- Script tags: /wasm/sx_browser.bc.js?v={hash}, /wasm/sx-platform.js?v={hash}
- Pass wasm_hash through helpers.py and ocaml_bridge.py
- Fix missing close paren in bind-sse-swap (broke SX parsing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:21:51 +00:00
1e42eb62a2 Fix create-text-node in DOM adapter: host handle Dicts are DOM nodes
The DOM adapter treated all Dict values as empty (create-fragment).
But DOM nodes (text nodes, elements) from create-text-node/host-call
are wrapped as Dict { __host_handle: N } by js_of_ocaml. Now checks
for __host_handle and passes them through as DOM nodes.

Fixes stopwatch button text ("Start") and timer display ("0.0s")
which were missing because create-text-node results were discarded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:10:28 +00:00
eacde62806 Fix navigation: outerHTML swap, island markers, host handle equality, dispose
Navigation pipeline now works end-to-end:
- outerHTML swap uses dom-replace-child instead of morph-node (morph has
  a CEK continuation issue with nested for-each that needs separate fix)
- swap-dom-nodes returns the new element for outerHTML so post-swap
  hydrates the correct (new) DOM, not the detached old element
- sx-render uses marker mode: islands rendered as empty span[data-sx-island]
  markers, hydrated by post-swap. Prevents duplicate content from island
  body expansion + SX response nav rows.
- dispose-island (singular) called on old island before morph, not just
  dispose-islands-in (which only disposes sub-islands)

OCaml runtime:
- safe_eq: Dict equality checks __host_handle for DOM node identity
  (js_to_value creates new Dict wrappers per call, breaking physical ==)
- contains?: same host handle check
- to_string: trampoline thunks (fixes <thunk> display)
- as_number: trampoline thunks (fixes arithmetic on leaked thunks)

DOM platform:
- dom-remove, dom-attr-list (name/value pairs), dom-child-list (SX list),
  dom-is-active-element?, dom-is-input-element?, dom-is-child-of?, dom-on

All 5 reactive-nav Playwright tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:19:29 +00:00
07bbcaf1bb OCaml VM browser: safe equality, thunk trampolining, platform functions, nav pipeline
Core runtime fixes:
- Safe equality (=, !=): physical equality for dicts/lambdas/signals,
  structural only for acyclic types. Prevents infinite loops on circular
  signal subscriber chains.
- contains?: same safe comparison (physical first, structural for simple types)
- Thunk trampolining in as_number and to_string: leaked thunks auto-resolve
  instead of showing <thunk> or erroring "Expected number, got thunk"
- Diagnostic first error: shows actual type received

Island hydration fixes:
- adapter-dom.sx: skip scope-emit for spreads inside islands (was tripling classes)
- schedule-idle: wrap callback to absorb requestIdleCallback deadline arg
- home-stepper: remove spread-specific highlighting (all tokens same style per step)

Platform functions (boot-helpers.sx):
- fetch-request: 3-arg interface (config, success-fn, error-fn) with promise chain
- build-request-body: form serialization for GET/POST
- strip-component-scripts / extract-response-css: SX text processing
- Navigation: bind-boost-link, bind-client-route-click via execute-request
- Loading state: show-indicator, disable-elements, clear-loading-state
- DOM extras: dom-remove, dom-attr-list (name/value pairs), dom-child-list (SX list),
  dom-is-active-element?, dom-is-input-element?, dom-is-child-of?, dom-on,
  dom-parse-html-document, dom-body-inner-html, create-script-clone
- All remaining stubs: csrf-token, loaded-component-names, observe-intersection,
  event-source-connect/listen, with-transition, cross-origin?, etc.

Navigation pipeline:
- browser-push-state/replace-state: accept 1-arg (URL only) or 3-arg
- boot.sx: wire popstate listener to handle-popstate
- URL updates working via handle-history + pushState fix

Morph debugging (WIP):
- dom-child-list returns proper SX list (was JS Array)
- dom-query accepts optional root element for scoped queries
- Navigation fetches and renders SX responses, URL updates, but morph
  doesn't replace content div (investigating dom-child-list on new elements)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 12:57:24 +00:00