(** sx_browser.ml — OCaml SX engine compiled to WASM/JS for browser use. Exposes the CEK machine, parser, and primitives as a global [Sx] object that the thin JS platform layer binds to. *) open Js_of_ocaml open Sx_types (* ================================================================== *) (* Value conversion: OCaml <-> JS *) (* ================================================================== *) (* ------------------------------------------------------------------ *) (* Opaque value handle table *) (* *) (* Non-primitive SX values (lambdas, components, signals, etc.) are *) (* stored in a handle table and represented on the JS side as objects *) (* with an __sx_handle integer key. This preserves identity across *) (* the JS↔OCaml boundary — the same handle always resolves to the *) (* same OCaml value. *) (* *) (* Callable values (Lambda, NativeFn, Continuation) are additionally *) (* wrapped as JS functions so they can be used directly as event *) (* listeners, setTimeout callbacks, etc. *) (* ------------------------------------------------------------------ *) let _next_handle = ref 0 let _handle_table : (int, value) Hashtbl.t = Hashtbl.create 256 (** Store a value in the handle table, return its handle id. *) let alloc_handle (v : value) : int = let id = !_next_handle in incr _next_handle; Hashtbl.replace _handle_table id v; id (** Look up a value by handle. *) let get_handle (id : int) : value = match Hashtbl.find_opt _handle_table id with | Some v -> v | None -> raise (Eval_error (Printf.sprintf "Invalid SX handle: %d" id)) (** Late-bound reference to global env (set after global_env is created). *) let _global_env_ref : env option ref = ref None let get_global_env () = match !_global_env_ref with | Some e -> e | None -> raise (Eval_error "Global env not initialized") (** Call an SX callable through the CEK machine. Constructs (fn arg1 arg2 ...) and evaluates it. *) let call_sx_fn (fn : value) (args : value list) : value = Sx_ref.eval_expr (List (fn :: args)) (Env (get_global_env ())) (** Convert an OCaml SX value to a JS representation. Primitive types map directly. Callable values become JS functions (with __sx_handle). Other compound types become tagged objects (with __sx_handle). *) let rec value_to_js (v : value) : Js.Unsafe.any = match v with | Nil -> Js.Unsafe.inject Js.null | Bool b -> Js.Unsafe.inject (Js.bool b) | Number n -> Js.Unsafe.inject (Js.number_of_float n) | String s -> Js.Unsafe.inject (Js.string s) | Symbol s -> let obj = Js.Unsafe.obj [| ("_type", Js.Unsafe.inject (Js.string "symbol")); ("name", Js.Unsafe.inject (Js.string s)) |] in Js.Unsafe.inject obj | Keyword k -> let obj = Js.Unsafe.obj [| ("_type", Js.Unsafe.inject (Js.string "keyword")); ("name", Js.Unsafe.inject (Js.string k)) |] in Js.Unsafe.inject obj | List items -> let arr = items |> List.map value_to_js |> Array.of_list in let js_arr = Js.array arr in let obj = Js.Unsafe.obj [| ("_type", Js.Unsafe.inject (Js.string "list")); ("items", Js.Unsafe.inject js_arr) |] in Js.Unsafe.inject obj | ListRef r -> let arr = !r |> List.map value_to_js |> Array.of_list in let js_arr = Js.array arr in let obj = Js.Unsafe.obj [| ("_type", Js.Unsafe.inject (Js.string "list")); ("items", Js.Unsafe.inject js_arr) |] in Js.Unsafe.inject obj | Dict d -> let obj = Js.Unsafe.obj [||] in Js.Unsafe.set obj (Js.string "_type") (Js.string "dict"); Hashtbl.iter (fun k v -> Js.Unsafe.set obj (Js.string k) (value_to_js v) ) d; Js.Unsafe.inject obj | RawHTML s -> Js.Unsafe.inject (Js.string s) (* Callable values: wrap as JS functions *) | Lambda _ | NativeFn _ | Continuation _ -> let handle = alloc_handle v in (* Create a JS function that calls back into the CEK machine. Use _tagFn helper (registered on globalThis) to create a function with __sx_handle and _type properties that survive js_of_ocaml return-value wrapping. *) let inner = Js.wrap_callback (fun args_js -> try let arg = js_to_value args_js in let args = match arg with Nil -> [] | _ -> [arg] in let result = call_sx_fn v args in value_to_js result with Eval_error msg -> ignore (Js.Unsafe.meth_call (Js.Unsafe.get Js.Unsafe.global (Js.string "console")) "error" [| Js.Unsafe.inject (Js.string (Printf.sprintf "[sx] callback error: %s" msg)) |]); Js.Unsafe.inject Js.null ) in let tag_fn = Js.Unsafe.get Js.Unsafe.global (Js.string "__sxTagFn") in Js.Unsafe.fun_call tag_fn [| Js.Unsafe.inject inner; Js.Unsafe.inject handle; Js.Unsafe.inject (Js.string (type_of v)) |] (* Non-callable compound values: tagged objects with handle *) | Component c -> let handle = alloc_handle v in let obj = Js.Unsafe.obj [| ("_type", Js.Unsafe.inject (Js.string "component")); ("name", Js.Unsafe.inject (Js.string c.c_name)); ("__sx_handle", Js.Unsafe.inject handle) |] in Js.Unsafe.inject obj | Island i -> let handle = alloc_handle v in let obj = Js.Unsafe.obj [| ("_type", Js.Unsafe.inject (Js.string "island")); ("name", Js.Unsafe.inject (Js.string i.i_name)); ("__sx_handle", Js.Unsafe.inject handle) |] in Js.Unsafe.inject obj | Signal _ -> let handle = alloc_handle v in let obj = Js.Unsafe.obj [| ("_type", Js.Unsafe.inject (Js.string "signal")); ("__sx_handle", Js.Unsafe.inject handle) |] in Js.Unsafe.inject obj | _ -> let handle = alloc_handle v in let obj = Js.Unsafe.obj [| ("_type", Js.Unsafe.inject (Js.string (type_of v))); ("__sx_handle", Js.Unsafe.inject handle) |] in Js.Unsafe.inject obj (** Convert a JS value back to an OCaml SX value. *) and js_to_value (js : Js.Unsafe.any) : value = (* Check null/undefined *) if Js.Unsafe.equals js Js.null || Js.Unsafe.equals js Js.undefined then Nil else let ty = Js.to_string (Js.typeof js) in match ty with | "number" -> Number (Js.float_of_number (Js.Unsafe.coerce js)) | "boolean" -> Bool (Js.to_bool (Js.Unsafe.coerce js)) | "string" -> String (Js.to_string (Js.Unsafe.coerce js)) | "function" -> (* Check for __sx_handle — this is a wrapped SX callable *) let handle_field = Js.Unsafe.get js (Js.string "__sx_handle") in if not (Js.Unsafe.equals handle_field Js.undefined) then let id = Js.float_of_number (Js.Unsafe.coerce handle_field) |> int_of_float in get_handle id else (* Plain JS function — wrap as NativeFn *) NativeFn ("js-callback", fun args -> let js_args = args |> List.map value_to_js |> Array.of_list in let result = Js.Unsafe.fun_call js (Array.map (fun a -> a) js_args) in js_to_value result) | "object" -> (* Check for __sx_handle — this is a wrapped SX value *) let handle_field = Js.Unsafe.get js (Js.string "__sx_handle") in if not (Js.Unsafe.equals handle_field Js.undefined) then begin let id = Js.float_of_number (Js.Unsafe.coerce handle_field) |> int_of_float in get_handle id end else begin (* Check for _type tag *) let type_field = Js.Unsafe.get js (Js.string "_type") in if Js.Unsafe.equals type_field Js.undefined then begin (* Check if it's an array *) let is_arr = Js.to_bool (Js.Unsafe.global##._Array##isArray js) in if is_arr then begin let len_js = Js.Unsafe.get js (Js.string "length") in let n = Js.float_of_number (Js.Unsafe.coerce len_js) |> int_of_float in let items = List.init n (fun i -> js_to_value (Js.array_get (Js.Unsafe.coerce js) i |> Js.Optdef.to_option |> Option.get) ) in List items end else begin (* Plain JS object — convert to dict *) let d = Hashtbl.create 8 in let keys = Js.Unsafe.global##._Object##keys js in let len = keys##.length in for i = 0 to len - 1 do let k = Js.to_string (Js.array_get keys i |> Js.Optdef.to_option |> Option.get) in let v = Js.Unsafe.get js (Js.string k) in Hashtbl.replace d k (js_to_value v) done; Dict d end end else begin let tag = Js.to_string (Js.Unsafe.coerce type_field) in match tag with | "symbol" -> Symbol (Js.to_string (Js.Unsafe.get js (Js.string "name"))) | "keyword" -> Keyword (Js.to_string (Js.Unsafe.get js (Js.string "name"))) | "list" -> let items_js = Js.Unsafe.get js (Js.string "items") in let len = Js.Unsafe.get items_js (Js.string "length") in let n = Js.float_of_number (Js.Unsafe.coerce len) |> int_of_float in let items = List.init n (fun i -> js_to_value (Js.array_get (Js.Unsafe.coerce items_js) i |> Js.Optdef.to_option |> Option.get) ) in List items | "dict" -> let d = Hashtbl.create 8 in let keys = Js.Unsafe.global##._Object##keys js in let len = keys##.length in for i = 0 to len - 1 do let k = Js.to_string (Js.array_get keys i |> Js.Optdef.to_option |> Option.get) in if k <> "_type" then begin let v = Js.Unsafe.get js (Js.string k) in Hashtbl.replace d k (js_to_value v) end done; Dict d | _ -> Nil end end | _ -> Nil (* ================================================================== *) (* Global environment *) (* ================================================================== *) let global_env = make_env () let () = _global_env_ref := Some global_env (* Render mode flag — set true during renderToHtml/loadSource calls that should dispatch HTML tags to the renderer. *) let _sx_render_mode = ref false (* Register JS helpers. __sxTagFn: tag a function with __sx_handle and _type properties. __sxR: side-channel for return values (bypasses Js.wrap_callback which strips custom properties from function objects). *) let () = let tag_fn = Js.Unsafe.pure_js_expr "(function(fn, handle, type) { fn.__sx_handle = handle; fn._type = type; return fn; })" in Js.Unsafe.set Js.Unsafe.global (Js.string "__sxTagFn") tag_fn (** Store a value in the side-channel and return a sentinel. The JS wrapper picks up __sxR instead of the return value. *) let return_via_side_channel (v : Js.Unsafe.any) : Js.Unsafe.any = Js.Unsafe.set Js.Unsafe.global (Js.string "__sxR") v; v (* ================================================================== *) (* Core API functions *) (* ================================================================== *) (** Parse SX source string into a list of values. *) let api_parse src_js = let src = Js.to_string src_js in try let values = Sx_parser.parse_all src in let arr = values |> List.map value_to_js |> Array.of_list in Js.Unsafe.inject (Js.array arr) with Parse_error msg -> Js.Unsafe.inject (Js.string ("Parse error: " ^ msg)) (** Serialize an SX value to source text. *) let api_stringify v_js = let v = js_to_value v_js in Js.Unsafe.inject (Js.string (inspect v)) (** Evaluate a single SX expression in the global environment. *) let api_eval_expr expr_js env_js = let expr = js_to_value expr_js in let _env = if Js.Unsafe.equals env_js Js.undefined then global_env else global_env in try let result = Sx_ref.eval_expr expr (Env _env) in return_via_side_channel (value_to_js result) with Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg)) (** Evaluate SX source string and return the last result. *) let api_eval src_js = let src = Js.to_string src_js in try let exprs = Sx_parser.parse_all src in let env = Env global_env in let result = List.fold_left (fun _acc expr -> Sx_ref.eval_expr expr env ) Nil exprs in return_via_side_channel (value_to_js result) with | Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg)) | Parse_error msg -> Js.Unsafe.inject (Js.string ("Parse error: " ^ msg)) (** Run the CEK machine on an expression, return result. *) let api_cek_run expr_js = let expr = js_to_value expr_js in try let state = Sx_ref.make_cek_state expr (Env global_env) Nil in let result = Sx_ref.cek_run_iterative state in return_via_side_channel (value_to_js result) with Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg)) (** Render SX expression to HTML string. *) let api_render_to_html expr_js = let expr = js_to_value expr_js in let prev = !_sx_render_mode in _sx_render_mode := true; try let html = Sx_render.render_to_html expr global_env in _sx_render_mode := prev; Js.Unsafe.inject (Js.string html) with Eval_error msg -> _sx_render_mode := prev; Js.Unsafe.inject (Js.string ("Error: " ^ msg)) (** Load SX source for side effects (define, defcomp, defmacro). *) let api_load src_js = let src = Js.to_string src_js in try let exprs = Sx_parser.parse_all src in let env = Env global_env in let count = ref 0 in List.iter (fun expr -> ignore (Sx_ref.eval_expr expr env); incr count ) exprs; Js.Unsafe.inject !count with | Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg)) | Parse_error msg -> Js.Unsafe.inject (Js.string ("Parse error: " ^ msg)) (** Get the type of an SX value. *) let api_type_of v_js = let v = js_to_value v_js in Js.Unsafe.inject (Js.string (type_of v)) (** Inspect an SX value (debug string). *) let api_inspect v_js = let v = js_to_value v_js in Js.Unsafe.inject (Js.string (inspect v)) (** Get engine identity. *) let api_engine () = Js.Unsafe.inject (Js.string "ocaml-cek-wasm") (** Register a JS callback as a named native function in the global env. JS callback receives JS-converted args and should return a JS value. *) let api_register_native name_js callback_js = let name = Js.to_string name_js in let native_fn args = let js_args = args |> List.map value_to_js |> Array.of_list in let result = Js.Unsafe.fun_call callback_js [| Js.Unsafe.inject (Js.array js_args) |] in js_to_value result in ignore (env_bind global_env name (NativeFn (name, native_fn))); Js.Unsafe.inject Js.null (** Call an SX callable (lambda, native fn) with JS args. fn_js can be a wrapped SX callable (with __sx_handle) or a JS value. args_js is a JS array of arguments. *) let api_call_fn fn_js args_js = try let fn = js_to_value fn_js in let args_arr = Js.to_array (Js.Unsafe.coerce args_js) in let args = Array.to_list (Array.map js_to_value args_arr) in let result = call_sx_fn fn args in return_via_side_channel (value_to_js result) with | Eval_error msg -> ignore (Js.Unsafe.meth_call (Js.Unsafe.get Js.Unsafe.global (Js.string "console")) "error" [| Js.Unsafe.inject (Js.string (Printf.sprintf "[sx] callFn error: %s" msg)) |]); Js.Unsafe.inject Js.null | exn -> ignore (Js.Unsafe.meth_call (Js.Unsafe.get Js.Unsafe.global (Js.string "console")) "error" [| Js.Unsafe.inject (Js.string (Printf.sprintf "[sx] callFn error: %s" (Printexc.to_string exn))) |]); Js.Unsafe.inject Js.null (** Check if a JS value is a wrapped SX callable. *) let api_is_callable fn_js = if Js.Unsafe.equals fn_js Js.null || Js.Unsafe.equals fn_js Js.undefined then Js.Unsafe.inject (Js.bool false) else let handle_field = Js.Unsafe.get fn_js (Js.string "__sx_handle") in if not (Js.Unsafe.equals handle_field Js.undefined) then begin let id = Js.float_of_number (Js.Unsafe.coerce handle_field) |> int_of_float in let v = get_handle id in Js.Unsafe.inject (Js.bool (is_callable v)) end else Js.Unsafe.inject (Js.bool false) (** Get the parameter count of an SX callable (for zero-arg optimization). *) let api_fn_arity fn_js = let handle_field = Js.Unsafe.get fn_js (Js.string "__sx_handle") in if Js.Unsafe.equals handle_field Js.undefined then Js.Unsafe.inject (Js.number_of_float (-1.0)) else let id = Js.float_of_number (Js.Unsafe.coerce handle_field) |> int_of_float in let v = get_handle id in match v with | Lambda l -> Js.Unsafe.inject (Js.number_of_float (float_of_int (List.length l.l_params))) | _ -> Js.Unsafe.inject (Js.number_of_float (-1.0)) (** Load and evaluate SX source string with error wrapping (for test runner). *) let api_load_source src_js = let src = Js.to_string src_js in try let exprs = Sx_parser.parse_all src in let env = Env global_env in let count = ref 0 in List.iter (fun expr -> ignore (Sx_ref.eval_expr expr env); incr count ) exprs; Js.Unsafe.inject !count with | Eval_error msg -> Js.Unsafe.inject (Js.string ("Error: " ^ msg)) | Parse_error msg -> Js.Unsafe.inject (Js.string ("Parse error: " ^ msg)) | exn -> Js.Unsafe.inject (Js.string ("Error: " ^ Printexc.to_string exn)) (* ================================================================== *) (* Register global Sx object *) (* ================================================================== *) (* ================================================================== *) (* Platform test functions (registered in global env) *) (* ================================================================== *) let () = let bind name fn = ignore (env_bind global_env name (NativeFn (name, fn))) in (* --- Deep equality --- *) let rec deep_equal a b = match a, b with | Nil, Nil -> true | Bool a, Bool b -> a = b | Number a, Number b -> a = b | String a, String b -> a = b | Symbol a, Symbol b -> a = b | Keyword a, Keyword b -> a = b | (List a | ListRef { contents = a }), (List b | ListRef { contents = b }) -> List.length a = List.length b && List.for_all2 deep_equal a b | Dict a, Dict b -> let ka = Hashtbl.fold (fun k _ acc -> k :: acc) a [] in let kb = Hashtbl.fold (fun k _ acc -> k :: acc) b [] in List.length ka = List.length kb && List.for_all (fun k -> Hashtbl.mem b k && deep_equal (match Hashtbl.find_opt a k with Some v -> v | None -> Nil) (match Hashtbl.find_opt b k with Some v -> v | None -> Nil)) ka | Lambda _, Lambda _ -> a == b | NativeFn _, NativeFn _ -> a == b | _ -> false in (* --- try-call --- *) bind "try-call" (fun args -> match args with | [thunk] -> (try ignore (Sx_ref.eval_expr (List [thunk]) (Env global_env)); let d = Hashtbl.create 2 in Hashtbl.replace d "ok" (Bool true); Dict d with | Eval_error msg -> let d = Hashtbl.create 2 in Hashtbl.replace d "ok" (Bool false); Hashtbl.replace d "error" (String msg); Dict d | exn -> let d = Hashtbl.create 2 in Hashtbl.replace d "ok" (Bool false); Hashtbl.replace d "error" (String (Printexc.to_string exn)); Dict d) | _ -> raise (Eval_error "try-call: expected 1 arg")); (* --- Evaluation --- *) bind "cek-eval" (fun args -> match args with | [expr] -> Sx_ref.eval_expr expr (Env global_env) | [expr; env_val] -> Sx_ref.eval_expr expr env_val | _ -> raise (Eval_error "cek-eval: expected 1-2 args")); bind "eval-expr-cek" (fun args -> match args with | [expr] -> Sx_ref.eval_expr expr (Env global_env) | [expr; env_val] -> Sx_ref.eval_expr expr env_val | _ -> raise (Eval_error "eval-expr-cek: expected 1-2 args")); bind "sx-parse" (fun args -> match args with | [String src] -> List (Sx_parser.parse_all src) | _ -> raise (Eval_error "sx-parse: expected string")); (* --- Equality and assertions --- *) bind "equal?" (fun args -> match args with | [a; b] -> Bool (deep_equal a b) | _ -> raise (Eval_error "equal?: expected 2 args")); bind "identical?" (fun args -> match args with | [a; b] -> Bool (a == b) | _ -> raise (Eval_error "identical?: expected 2 args")); bind "assert" (fun args -> match args with | [cond] -> if not (sx_truthy cond) then raise (Eval_error "Assertion failed"); Bool true | [cond; String msg] -> if not (sx_truthy cond) then raise (Eval_error ("Assertion error: " ^ msg)); Bool true | [cond; msg] -> if not (sx_truthy cond) then raise (Eval_error ("Assertion error: " ^ value_to_string msg)); Bool true | _ -> raise (Eval_error "assert: expected 1-2 args")); (* --- List mutation --- *) bind "append!" (fun args -> match args with | [ListRef r; v] -> r := !r @ [v]; ListRef r | [List items; v] -> List (items @ [v]) | _ -> raise (Eval_error "append!: expected list and value")); (* --- Environment ops --- *) bind "make-env" (fun _args -> Env (make_env ())); bind "env-has?" (fun args -> match args with | [Env e; String k] -> Bool (env_has e k) | [Env e; Keyword k] -> Bool (env_has e k) | _ -> raise (Eval_error "env-has?: expected env and key")); bind "env-get" (fun args -> match args with | [Env e; String k] -> env_get e k | [Env e; Keyword k] -> env_get e k | _ -> raise (Eval_error "env-get: expected env and key")); bind "env-bind!" (fun args -> match args with | [Env e; String k; v] -> env_bind e k v | [Env e; Keyword k; v] -> env_bind e k v | _ -> raise (Eval_error "env-bind!: expected env, key, value")); bind "env-set!" (fun args -> match args with | [Env e; String k; v] -> env_set e k v | [Env e; Keyword k; v] -> env_set e k v | _ -> raise (Eval_error "env-set!: expected env, key, value")); bind "env-extend" (fun args -> match args with | [Env e] -> Env (env_extend e) | _ -> raise (Eval_error "env-extend: expected env")); bind "env-merge" (fun args -> match args with | [Env a; Env b] -> Env (env_merge a b) | _ -> raise (Eval_error "env-merge: expected 2 envs")); (* --- Continuation support --- *) bind "make-continuation" (fun args -> match args with | [f] -> let k v = Sx_runtime.sx_call f [v] in Continuation (k, None) | _ -> raise (Eval_error "make-continuation: expected 1 arg")); bind "continuation?" (fun args -> match args with | [Continuation _] -> Bool true | [_] -> Bool false | _ -> raise (Eval_error "continuation?: expected 1 arg")); bind "continuation-fn" (fun args -> match args with | [Continuation (f, _)] -> NativeFn ("continuation-fn-result", fun args -> (match args with [v] -> f v | _ -> f Nil)) | _ -> raise (Eval_error "continuation-fn: expected continuation")); (* --- Missing primitives --- *) bind "make-keyword" (fun args -> match args with | [String s] -> Keyword s | _ -> raise (Eval_error "make-keyword: expected string")); (* --- Test helpers --- *) bind "sx-parse-one" (fun args -> match args with | [String src] -> let exprs = Sx_parser.parse_all src in (match exprs with e :: _ -> e | [] -> Nil) | _ -> raise (Eval_error "sx-parse-one: expected string")); bind "test-env" (fun _args -> Env (env_extend global_env)); (* cek-eval takes a string in the native runner *) bind "cek-eval" (fun args -> match args with | [String s] -> let exprs = Sx_parser.parse_all s in (match exprs with | e :: _ -> Sx_ref.eval_expr e (Env global_env) | [] -> Nil) | [expr] -> Sx_ref.eval_expr expr (Env global_env) | [expr; env_val] -> Sx_ref.eval_expr expr env_val | _ -> raise (Eval_error "cek-eval: expected 1-2 args")); bind "eval-expr-cek" (fun args -> match args with | [expr; e] -> Sx_ref.eval_expr expr e | [expr] -> Sx_ref.eval_expr expr (Env global_env) | _ -> raise (Eval_error "eval-expr-cek: expected 1-2 args")); (* --- Component accessors --- *) bind "component-params" (fun args -> match args with | [Component c] -> List (List.map (fun s -> String s) c.c_params) | _ -> Nil); bind "component-body" (fun args -> match args with | [Component c] -> c.c_body | _ -> Nil); bind "component-has-children" (fun args -> match args with | [Component c] -> Bool c.c_has_children | _ -> Bool false); bind "component-affinity" (fun args -> match args with | [Component c] -> String c.c_affinity | _ -> String "auto"); bind "component-param-types" (fun _args -> Nil); bind "component-set-param-types!" (fun _args -> Nil); (* --- Parser/symbol helpers --- *) bind "keyword-name" (fun args -> match args with | [Keyword k] -> String k | _ -> raise (Eval_error "keyword-name: expected keyword")); bind "symbol-name" (fun args -> match args with | [Symbol s] -> String s | _ -> raise (Eval_error "symbol-name: expected symbol")); bind "sx-serialize" (fun args -> match args with | [v] -> String (inspect v) | _ -> raise (Eval_error "sx-serialize: expected 1 arg")); bind "make-symbol" (fun args -> match args with | [String s] -> Symbol s | [v] -> Symbol (value_to_string v) | _ -> raise (Eval_error "make-symbol: expected 1 arg")); (* --- CEK stepping / introspection --- *) bind "make-cek-state" (fun args -> match args with | [ctrl; env'; kont] -> Sx_ref.make_cek_state ctrl env' kont | _ -> raise (Eval_error "make-cek-state: expected 3 args")); bind "cek-step" (fun args -> match args with | [state] -> Sx_ref.cek_step state | _ -> raise (Eval_error "cek-step: expected 1 arg")); bind "cek-phase" (fun args -> match args with | [state] -> Sx_ref.cek_phase state | _ -> raise (Eval_error "cek-phase: expected 1 arg")); bind "cek-value" (fun args -> match args with | [state] -> Sx_ref.cek_value state | _ -> raise (Eval_error "cek-value: expected 1 arg")); bind "cek-terminal?" (fun args -> match args with | [state] -> Sx_ref.cek_terminal_p state | _ -> raise (Eval_error "cek-terminal?: expected 1 arg")); bind "cek-kont" (fun args -> match args with | [state] -> Sx_ref.cek_kont state | _ -> raise (Eval_error "cek-kont: expected 1 arg")); bind "frame-type" (fun args -> match args with | [frame] -> Sx_ref.frame_type frame | _ -> raise (Eval_error "frame-type: expected 1 arg")); (* --- Strict mode --- *) ignore (env_bind global_env "*strict*" (Bool false)); ignore (env_bind global_env "*prim-param-types*" Nil); bind "set-strict!" (fun args -> match args with | [v] -> Sx_ref._strict_ref := v; ignore (env_set global_env "*strict*" v); Nil | _ -> raise (Eval_error "set-strict!: expected 1 arg")); bind "set-prim-param-types!" (fun args -> match args with | [v] -> Sx_ref._prim_param_types_ref := v; ignore (env_set global_env "*prim-param-types*" v); Nil | _ -> raise (Eval_error "set-prim-param-types!: expected 1 arg")); bind "value-matches-type?" (fun args -> match args with | [v; String expected] -> Sx_ref.value_matches_type_p v (String expected) | _ -> raise (Eval_error "value-matches-type?: expected value and type string")); (* --- Apply --- *) bind "apply" (fun args -> match args with | f :: rest -> let all_args = match List.rev rest with | List last :: prefix -> List.rev prefix @ last | _ -> rest in Sx_runtime.sx_call f all_args | _ -> raise (Eval_error "apply: expected function and args")); (* --- Type system test helpers (for --full tests) --- *) bind "test-prim-types" (fun _args -> let d = Hashtbl.create 40 in List.iter (fun (k, v) -> Hashtbl.replace d k (String v)) [ "+", "number"; "-", "number"; "*", "number"; "/", "number"; "mod", "number"; "inc", "number"; "dec", "number"; "abs", "number"; "min", "number"; "max", "number"; "floor", "number"; "ceil", "number"; "round", "number"; "str", "string"; "upper", "string"; "lower", "string"; "trim", "string"; "join", "string"; "replace", "string"; "format", "string"; "substr", "string"; "=", "boolean"; "<", "boolean"; ">", "boolean"; "<=", "boolean"; ">=", "boolean"; "!=", "boolean"; "not", "boolean"; "nil?", "boolean"; "empty?", "boolean"; "number?", "boolean"; "string?", "boolean"; "boolean?", "boolean"; "list?", "boolean"; "dict?", "boolean"; "symbol?", "boolean"; "keyword?", "boolean"; "contains?", "boolean"; "has-key?", "boolean"; "starts-with?", "boolean"; "ends-with?", "boolean"; "len", "number"; "first", "any"; "rest", "list"; "last", "any"; "nth", "any"; "cons", "list"; "append", "list"; "concat", "list"; "reverse", "list"; "sort", "list"; "slice", "list"; "range", "list"; "flatten", "list"; "keys", "list"; "vals", "list"; "map-dict", "dict"; "assoc", "dict"; "dissoc", "dict"; "merge", "dict"; "dict", "dict"; "get", "any"; "type-of", "string"; ]; Dict d); bind "test-prim-param-types" (fun _args -> let d = Hashtbl.create 10 in let pos name typ = let d2 = Hashtbl.create 2 in Hashtbl.replace d2 "positional" (List [List [String name; String typ]]); Hashtbl.replace d2 "rest-type" Nil; Dict d2 in let pos_rest name typ rt = let d2 = Hashtbl.create 2 in Hashtbl.replace d2 "positional" (List [List [String name; String typ]]); Hashtbl.replace d2 "rest-type" (String rt); Dict d2 in Hashtbl.replace d "+" (pos_rest "a" "number" "number"); Hashtbl.replace d "-" (pos_rest "a" "number" "number"); Hashtbl.replace d "*" (pos_rest "a" "number" "number"); Hashtbl.replace d "/" (pos_rest "a" "number" "number"); Hashtbl.replace d "inc" (pos "n" "number"); Hashtbl.replace d "dec" (pos "n" "number"); Hashtbl.replace d "upper" (pos "s" "string"); Hashtbl.replace d "lower" (pos "s" "string"); Hashtbl.replace d "keys" (pos "d" "dict"); Hashtbl.replace d "vals" (pos "d" "dict"); Dict d); (* --- HTML renderer --- *) Sx_render.setup_render_env global_env; (* Web adapters loaded as SX source at boot time via bundle.sh *) (* Wire up render mode — the CEK machine checks these to dispatch HTML tags and components to the renderer instead of eval. *) Sx_runtime._render_active_p_fn := (fun () -> Bool !_sx_render_mode); Sx_runtime._is_render_expr_fn := (fun expr -> match expr with | List (Symbol tag :: _) -> Bool (Sx_render.is_html_tag tag || tag = "<>" || tag = "raw!") | _ -> Bool false); Sx_runtime._render_expr_fn := (fun expr env -> match env with | Env e -> RawHTML (Sx_render.render_to_html expr e) | _ -> RawHTML (Sx_render.render_to_html expr global_env)); (* --- Scope stack primitives (called by transpiled evaluator via prim_call) --- *) Sx_primitives.register "collect!" (fun args -> match args with [a; b] -> Sx_runtime.sx_collect a b | _ -> Nil); Sx_primitives.register "collected" (fun args -> match args with [a] -> Sx_runtime.sx_collected a | _ -> List []); Sx_primitives.register "clear-collected!" (fun args -> match args with [a] -> Sx_runtime.sx_clear_collected a | _ -> Nil); Sx_primitives.register "emit!" (fun args -> match args with [a; b] -> Sx_runtime.sx_emit a b | _ -> Nil); Sx_primitives.register "emitted" (fun args -> match args with [a] -> Sx_runtime.sx_emitted a | _ -> List []); Sx_primitives.register "context" (fun args -> match args with [a; b] -> Sx_runtime.sx_context a b | [a] -> Sx_runtime.sx_context a Nil | _ -> Nil); (* --- Fragment and raw HTML (always available, not just in render mode) --- *) bind "<>" (fun args -> let parts = List.map (fun a -> match a with | String s -> s | RawHTML s -> s | Nil -> "" | List _ -> Sx_render.render_to_html a global_env | _ -> value_to_string a ) args in RawHTML (String.concat "" parts)); bind "raw!" (fun args -> match args with | [String s] -> RawHTML s | [RawHTML s] -> RawHTML s | [Nil] -> RawHTML "" | _ -> RawHTML (String.concat "" (List.map (fun a -> match a with String s | RawHTML s -> s | _ -> value_to_string a ) args))); (* --- Scope stack functions (used by signals.sx, evaluator scope forms) --- *) bind "scope-push!" (fun args -> match args with | [name; value] -> Sx_runtime.scope_push name value | _ -> raise (Eval_error "scope-push!: expected 2 args")); bind "scope-pop!" (fun args -> match args with | [_name] -> Sx_runtime.scope_pop _name | _ -> raise (Eval_error "scope-pop!: expected 1 arg")); bind "provide-push!" (fun args -> match args with | [name; value] -> Sx_runtime.provide_push name value | _ -> raise (Eval_error "provide-push!: expected 2 args")); bind "provide-pop!" (fun args -> match args with | [_name] -> Sx_runtime.provide_pop _name | _ -> raise (Eval_error "provide-pop!: expected 1 arg")); (* define-page-helper: registers a named page helper — stub for browser *) bind "define-page-helper" (fun args -> match args with | [String _name; _body] -> Nil (* Page helpers are server-side; noop in browser *) | _ -> Nil); (* cek-call: call a function via the CEK machine (used by signals, orchestration) (cek-call fn nil) → call with no args (cek-call fn (list a)) → call with args list (cek-call fn a) → call with single arg *) bind "cek-call" (fun args -> match args with | [f; Nil] -> Sx_ref.eval_expr (List [f]) (Env global_env) | [f; List arg_list] -> Sx_ref.eval_expr (List (f :: arg_list)) (Env global_env) | [f; a] -> Sx_ref.eval_expr (List [f; a]) (Env global_env) | [f] -> Sx_ref.eval_expr (List [f]) (Env global_env) | f :: rest -> Sx_ref.eval_expr (List (f :: rest)) (Env global_env) | _ -> raise (Eval_error "cek-call: expected function and args")); (* not : logical negation (sometimes missing from evaluator prims) *) (if not (Sx_primitives.is_primitive "not") then bind "not" (fun args -> match args with | [v] -> Bool (not (sx_truthy v)) | _ -> raise (Eval_error "not: expected 1 arg"))) let () = let sx = Js.Unsafe.obj [||] in (* __sxWrap: wraps an OCaml API function so that after calling it, the JS side picks up the result from globalThis.__sxR if set. This bypasses js_of_ocaml stripping properties from function return values. *) let wrap = Js.Unsafe.pure_js_expr {|(function(fn) { return function() { globalThis.__sxR = undefined; var r = fn.apply(null, arguments); return globalThis.__sxR !== undefined ? globalThis.__sxR : r; }; })|} in let w fn = Js.Unsafe.fun_call wrap [| Js.Unsafe.inject (Js.wrap_callback fn) |] in (* Core evaluation *) Js.Unsafe.set sx (Js.string "parse") (Js.wrap_callback api_parse); Js.Unsafe.set sx (Js.string "stringify") (Js.wrap_callback api_stringify); Js.Unsafe.set sx (Js.string "eval") (w api_eval); Js.Unsafe.set sx (Js.string "evalExpr") (w api_eval_expr); Js.Unsafe.set sx (Js.string "cekRun") (w api_cek_run); Js.Unsafe.set sx (Js.string "renderToHtml") (Js.wrap_callback api_render_to_html); Js.Unsafe.set sx (Js.string "load") (Js.wrap_callback api_load); Js.Unsafe.set sx (Js.string "typeOf") (Js.wrap_callback api_type_of); Js.Unsafe.set sx (Js.string "inspect") (Js.wrap_callback api_inspect); Js.Unsafe.set sx (Js.string "engine") (Js.wrap_callback api_engine); Js.Unsafe.set sx (Js.string "registerNative") (Js.wrap_callback api_register_native); Js.Unsafe.set sx (Js.string "loadSource") (Js.wrap_callback api_load_source); Js.Unsafe.set sx (Js.string "callFn") (w api_call_fn); Js.Unsafe.set sx (Js.string "isCallable") (Js.wrap_callback api_is_callable); Js.Unsafe.set sx (Js.string "fnArity") (Js.wrap_callback api_fn_arity); (* Expose globally *) Js.Unsafe.set Js.Unsafe.global (Js.string "SxKernel") sx