diff --git a/docker-compose.dev-sx.yml b/docker-compose.dev-sx.yml index 5d5c5a7..558e5ef 100644 --- a/docker-compose.dev-sx.yml +++ b/docker-compose.dev-sx.yml @@ -17,6 +17,7 @@ services: SX_OCAML_BIN: "/app/bin/sx_server" SX_BOUNDARY_STRICT: "1" SX_DEV: "1" + OCAMLRUNPARAM: "b" ports: - "8013:8000" volumes: diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 6d6c6ff..9f3ec4a 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -150,6 +150,16 @@ let () = Sx_primitives.register "scope-peek" (fun args -> (match stack with v :: _ -> v | [] -> Nil) | _ -> Nil) +let () = Sx_primitives.register "context" (fun args -> + match args with + | [String name] | [String name; _] -> + let stack = try Hashtbl.find _scope_stacks name with Not_found -> [] in + (match stack, args with + | v :: _, _ -> v + | [], [_; default_val] -> default_val + | [], _ -> Nil) + | _ -> Nil) + (** collect! — lazy scope accumulator. Creates root scope if missing, emits value (deduplicates). Used by cssx and spread components. *) let () = Sx_primitives.register "collect!" (fun args -> @@ -376,13 +386,29 @@ let make_server_env () = | [List items; v] -> List (items @ [v]) | _ -> raise (Eval_error "append!: expected list and value")); - (* HTML renderer *) + (* HTML renderer — OCaml render module provides the shell renderer; + adapter-html.sx provides the SX-level render-to-html *) Sx_render.setup_render_env env; (* Render-mode flags *) bind "set-render-active!" (fun _args -> Nil); bind "render-active?" (fun _args -> Bool true); + (* Raw HTML — platform primitives for adapter-html.sx *) + bind "make-raw-html" (fun args -> + match args with [String s] -> RawHTML s | [v] -> RawHTML (value_to_string v) | _ -> Nil); + bind "raw-html-content" (fun args -> + match args with [RawHTML s] -> String s | [String s] -> String s | _ -> String ""); + bind "empty-dict?" (fun args -> + match args with [Dict d] -> Bool (Hashtbl.length d = 0) | _ -> Bool true); + bind "for-each-indexed" (fun args -> + match args with + | [fn_val; List items] | [fn_val; ListRef { contents = items }] -> + List.iteri (fun i item -> + ignore (Sx_ref.eval_expr (List [fn_val; Number (float_of_int i); item]) (Env env)) + ) items; Nil + | _ -> Nil); + (* Scope stack — platform primitives for render-time dynamic scope. Used by aser for spread/provide/emit patterns. Module-level so step-sf-context can check it via get-primitive. *) @@ -764,21 +790,27 @@ let make_server_env () = bind "component-params" (fun args -> match args with | [Component c] -> List (List.map (fun s -> String s) c.c_params) + | [Island i] -> List (List.map (fun s -> String s) i.i_params) | _ -> Nil); bind "component-body" (fun args -> match args with | [Component c] -> c.c_body + | [Island i] -> i.i_body | _ -> Nil); - bind "component-has-children" (fun args -> + let has_children_impl = NativeFn ("component-has-children?", fun args -> match args with | [Component c] -> Bool c.c_has_children - | _ -> Bool false); + | [Island i] -> Bool i.i_has_children + | _ -> Bool false) in + ignore (env_bind env "component-has-children" has_children_impl); + ignore (env_bind env "component-has-children?" has_children_impl); bind "component-affinity" (fun args -> match args with | [Component c] -> String c.c_affinity + | [Island _] -> String "client" | _ -> String "auto"); bind "keyword-name" (fun args -> diff --git a/hosts/ocaml/lib/sx_primitives.ml b/hosts/ocaml/lib/sx_primitives.ml index 715fb75..090f1c0 100644 --- a/hosts/ocaml/lib/sx_primitives.ml +++ b/hosts/ocaml/lib/sx_primitives.ml @@ -30,7 +30,7 @@ let as_number = function | Bool false -> 0.0 | Nil -> 0.0 | String s -> (match float_of_string_opt s with Some n -> n | None -> Float.nan) - | v -> raise (Eval_error ("Expected number, got " ^ type_of v)) + | v -> raise (Eval_error ("Expected number, got " ^ type_of v ^ ": " ^ (match v with Dict d -> (match Hashtbl.find_opt d "__signal" with Some _ -> "signal{value=" ^ (match Hashtbl.find_opt d "value" with Some v' -> value_to_string v' | None -> "?") ^ "}" | None -> "dict") | _ -> ""))) let as_string = function | String s -> s diff --git a/hosts/ocaml/lib/sx_render.ml b/hosts/ocaml/lib/sx_render.ml index 4b3c2e9..591dbd5 100644 --- a/hosts/ocaml/lib/sx_render.ml +++ b/hosts/ocaml/lib/sx_render.ml @@ -256,17 +256,20 @@ and render_list_to_html head args env = let v = env_get env name in (match v with | Component _ -> render_component v args env - | Island i -> - (* Islands: render initial HTML server-side (like React SSR). - Log failures so we can fix them. *) + | Island _i -> + (* Islands: SSR via the SX render-to-html from adapter-html.sx. + It handles deref/signal/computed through the CEK correctly, + and renders island bodies with hydration markers. *) (try - let c = { c_name = i.i_name; c_params = i.i_params; - c_has_children = i.i_has_children; c_body = i.i_body; - c_closure = i.i_closure; c_affinity = "client"; - c_compiled = None } in - render_component (Component c) args env + let call_expr = List (Symbol name :: args) in + let quoted = List [Symbol "quote"; call_expr] in + let render_call = List [Symbol "render-to-html"; quoted; Env env] in + let result = Sx_ref.eval_expr render_call (Env env) in + (match result with + | String s | RawHTML s -> s + | _ -> value_to_string result) with e -> - Printf.eprintf "[ssr-island] ~%s FAILED: %s\n%!" i.i_name (Printexc.to_string e); + Printf.eprintf "[ssr-island] ~%s FAILED: %s\n%s\n%!" _i.i_name (Printexc.to_string e) (Printexc.get_backtrace ()); "") | Macro m -> let expanded = expand_macro m args env in diff --git a/hosts/ocaml/lib/sx_runtime.ml b/hosts/ocaml/lib/sx_runtime.ml index bce3212..1b5f14e 100644 --- a/hosts/ocaml/lib/sx_runtime.ml +++ b/hosts/ocaml/lib/sx_runtime.ml @@ -346,7 +346,10 @@ let is_else_clause v = | _ -> Bool false (* Signal accessors *) -let signal_value s = match s with Signal sig' -> sig'.s_value | _ -> raise (Eval_error "not a signal") +let signal_value s = match s with + | Signal sig' -> sig'.s_value + | Dict d -> (match Hashtbl.find_opt d "value" with Some v -> v | None -> Nil) + | _ -> raise (Eval_error "not a signal") let signal_set_value s v = match s with Signal sig' -> sig'.s_value <- v; v | _ -> raise (Eval_error "not a signal") let signal_subscribers s = match s with Signal sig' -> List (List.map (fun _ -> Nil) sig'.s_subscribers) | _ -> List [] let signal_add_sub_b _s _f = Nil diff --git a/hosts/ocaml/lib/sx_types.ml b/hosts/ocaml/lib/sx_types.ml index 8425fd2..3801ee0 100644 --- a/hosts/ocaml/lib/sx_types.ml +++ b/hosts/ocaml/lib/sx_types.ml @@ -300,7 +300,10 @@ let is_component = function Component _ -> true | _ -> false let is_island = function Island _ -> true | _ -> false let is_macro = function Macro _ -> true | _ -> false let is_thunk = function Thunk _ -> true | _ -> false -let is_signal = function Signal _ -> true | _ -> false +let is_signal = function + | Signal _ -> true + | Dict d -> Hashtbl.mem d "__signal" + | _ -> false let is_callable = function | Lambda _ | NativeFn _ | Continuation (_, _) -> true diff --git a/shared/sx/ocaml_bridge.py b/shared/sx/ocaml_bridge.py index 2a611f8..dc1e8fd 100644 --- a/shared/sx/ocaml_bridge.py +++ b/shared/sx/ocaml_bridge.py @@ -401,7 +401,7 @@ class OcamlBridge: # signals.sx provides reactive primitives for island SSR) web_dir = os.path.join(os.path.dirname(__file__), "../../web") if os.path.isdir(web_dir): - for web_file in ["signals.sx", "adapter-sx.sx"]: + for web_file in ["signals.sx", "adapter-html.sx", "adapter-sx.sx"]: path = os.path.normpath(os.path.join(web_dir, web_file)) if os.path.isfile(path): all_files.append(path)