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

@@ -686,11 +686,15 @@ let () =
| None -> raise (Eval_error ("Store not found: " ^ name)))
| _ -> raise (Eval_error "use-store: expected (name)"));
register "clear-stores" (fun _args -> Hashtbl.clear store_registry; Nil);
(* SSR stubs — effect is no-op on server (signals.sx guards with client?),
resource returns loading state. Other browser primitives only appear
inside effect bodies which never execute during SSR. *)
register "effect" (fun _args -> Nil);
register "register-in-scope" (fun _args -> Nil);
(* SSR stubs — resource returns loading state on server.
NOTE: effect and register-in-scope must NOT be registered as primitives
here — the bytecode compiler uses primitive? to decide CALL_PRIM vs
GLOBAL_GET+CALL. If effect is a primitive, bytecoded modules emit
CALL_PRIM which returns Nil instead of calling the real effect function
from core-signals.sx. The server overrides effect in sx_server.ml via
env_bind AFTER compilation. *)
(* register "effect" — REMOVED: see note above *)
(* register "register-in-scope" — REMOVED: see note above *)
(* resource — SSR stub: return signal with {loading: true}, client hydrates real fetch *)
register "resource" (fun _args ->
let state = Hashtbl.create 8 in

View File

@@ -396,6 +396,8 @@ and run vm =
in
(match fn_val with
| NativeFn (_, fn) -> fn args
| VmClosure _ | Lambda _ | Component _ | Island _ ->
Sx_ref.cek_call fn_val (List args)
| _ -> Nil)
with Eval_error msg ->
raise (Eval_error (Printf.sprintf "%s (in CALL_PRIM \"%s\" with %d args)"