Files
rose-ash/shared/static/wasm/sx/signals.sx
giles 80931e4972 Fix JIT closure isolation, SX wire format, server diagnostics
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>
2026-03-30 17:28:47 +00:00

82 lines
3.2 KiB
Plaintext

;; ==========================================================================
;; web/signals.sx — Web platform signal extensions
;;
;; Extends the core reactive signal spec (spec/signals.sx) with web-specific
;; features: marsh scopes (DOM lifecycle), named stores (page-level state),
;; event bridge (lake→island communication), and async resources.
;;
;; These depend on platform primitives:
;; dom-set-data, dom-get-data, dom-listen, dom-dispatch, event-detail,
;; promise-then
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Marsh scopes — child scopes within islands
;; --------------------------------------------------------------------------
(define with-marsh-scope :effects [mutation io]
(fn (marsh-el (body-fn :as lambda))
(let ((disposers (list)))
(with-island-scope
(fn (d) (append! disposers d))
body-fn)
(dom-set-data marsh-el "sx-marsh-disposers" disposers))))
(define dispose-marsh-scope :effects [mutation io]
(fn (marsh-el)
(let ((disposers (dom-get-data marsh-el "sx-marsh-disposers")))
(when disposers
(for-each (fn ((d :as lambda)) (cek-call d nil)) disposers)
(dom-set-data marsh-el "sx-marsh-disposers" nil)))))
;; --------------------------------------------------------------------------
;; Named stores — page-level signal containers
;; --------------------------------------------------------------------------
;; def-store, use-store, clear-stores are now OCaml primitives
;; (sx_primitives.ml) with a global mutable registry that survives
;; env scoping across bytecode modules and island hydration.
;; --------------------------------------------------------------------------
;; Event bridge — DOM event communication for lake→island
;; --------------------------------------------------------------------------
(define emit-event :effects [io]
(fn (el (event-name :as string) detail)
(dom-dispatch el event-name detail)))
(define on-event :effects [io]
(fn (el (event-name :as string) (handler :as lambda))
(dom-on el event-name handler)))
(define bridge-event :effects [mutation io]
(fn (el (event-name :as string) (target-signal :as signal) transform-fn)
(effect (fn ()
(let ((remove (dom-on el event-name
(fn (e)
(let ((detail (event-detail e))
(new-val (if transform-fn
(cek-call transform-fn (list detail))
detail)))
(reset! target-signal new-val))))))
remove)))))
;; --------------------------------------------------------------------------
;; Resource — async signal with loading/resolved/error states
;; --------------------------------------------------------------------------
(define resource :effects [mutation io]
(fn ((fetch-fn :as lambda))
(let ((state (signal (dict "loading" true "data" nil "error" nil))))
(promise-then
(cek-call fetch-fn nil)
(fn (data)
(reset! state (dict "loading" false "data" data "error" nil)))
(fn (err)
(reset! state (dict "loading" false "data" nil "error" err))))
state)))