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>
This commit is contained in:
2026-03-31 16:56:31 +00:00
parent 4cb4551753
commit a7efcaf679
28 changed files with 232 additions and 199 deletions

View File

@@ -342,11 +342,16 @@
(=
(dom-get-attr old-node "data-sx-island")
(dom-get-attr new-node "data-sx-island")))
(morph-island-children old-node new-node)
(do
(sync-attrs old-node new-node)
(morph-island-children old-node new-node))
(or
(not (= (dom-node-type old-node) (dom-node-type new-node)))
(not (= (dom-node-name old-node) (dom-node-name new-node))))
(dom-replace-child (dom-parent old-node) (dom-clone new-node) old-node)
(dom-replace-child
(dom-parent old-node)
(dom-clone new-node true)
old-node)
(or (= (dom-node-type old-node) 3) (= (dom-node-type old-node) 8))
(when
(not (= (dom-text-content old-node) (dom-text-content new-node)))
@@ -411,25 +416,37 @@
(let
((old-kids (dom-child-list old-parent))
(new-kids (dom-child-list new-parent))
(old-by-id
(reduce
(fn
((acc :as dict) kid)
(let
((id (let ((raw (dom-id kid))) (if (empty? raw) nil raw))))
(if id (do (dict-set! acc id kid) acc) acc)))
(dict)
old-kids))
(oi 0))
(old-by-id (dict))
(old-idx-by-id (dict))
(consumed (dict))
(oi 0)
(idx 0))
(for-each
(fn
(kid)
(let
((id (dom-id kid)))
(when
(and id (not (empty? id)))
(dict-set! old-by-id id kid)
(dict-set! old-idx-by-id id idx)))
(set! idx (inc idx)))
old-kids)
(for-each
(fn
(new-child)
(let
((match-id (let ((raw-id (dom-id new-child))) (if (empty? raw-id) nil raw-id)))
((raw-id (dom-id new-child))
(match-id (if (and raw-id (not (empty? raw-id))) raw-id nil))
(match-by-id (if match-id (dict-get old-by-id match-id) nil)))
(cond
(and match-by-id (not (nil? match-by-id)))
(do
(let
((matched-idx (dict-get old-idx-by-id match-id)))
(when
matched-idx
(dict-set! consumed (str matched-idx) true)))
(when
(and
(< oi (len old-kids))
@@ -443,20 +460,25 @@
(< oi (len old-kids))
(let
((old-child (nth old-kids oi)))
(if
(and (not (empty? (dom-id old-child))) (not match-id))
(dom-insert-before
old-parent
(dom-clone new-child)
old-child)
(do (morph-node old-child new-child) (set! oi (inc oi)))))
:else (dom-append old-parent (dom-clone new-child)))))
(let
((old-id (dom-id old-child)))
(if
(and old-id (not (empty? old-id)) (not match-id))
(dom-insert-before
old-parent
(dom-clone new-child true)
old-child)
(do
(dict-set! consumed (str oi) true)
(morph-node old-child new-child)
(set! oi (inc oi))))))
:else (dom-append old-parent (dom-clone new-child true)))))
new-kids)
(for-each
(fn
((i :as number))
(i)
(when
(>= i oi)
(not (dict-get consumed (str i)))
(let
((leftover (nth old-kids i)))
(when
@@ -465,7 +487,7 @@
(not (dom-has-attr? leftover "sx-preserve"))
(not (dom-has-attr? leftover "sx-ignore")))
(dom-remove-child old-parent leftover)))))
(range oi (len old-kids))))))
(range 0 (len old-kids))))))
(define
morph-island-children
@@ -588,7 +610,7 @@
(morph-children target wrapper)))
"outerHTML"
(let
((parent (dom-parent target)) (new-el (dom-clone new-nodes)))
((parent (dom-parent target)) (new-el (dom-clone new-nodes true)))
(if
(dom-is-fragment? new-nodes)
(let
@@ -596,7 +618,7 @@
(if
fc
(do
(set! new-el (dom-clone fc))
(set! new-el (dom-clone fc true))
(dom-replace-child parent new-el target)
(let
((sib (dom-next-sibling fc)))