diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index a848516b..bf2033cd 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -713,13 +713,13 @@ let register_jit_hook env = Sx_ref.jit_call_hook := Some (fun f args -> match f with | Lambda l -> - let fn_name = match l.l_name with Some n -> n | None -> "?" in (match l.l_compiled with | Some cl when not (Sx_vm.is_jit_failed cl) -> (* Cached bytecode — run on VM, fall back to CEK on runtime error. Log once per function name, then stay quiet. Don't disable. *) (try Some (Sx_vm.call_closure cl args cl.vm_env_ref) with e -> + let fn_name = match l.l_name with Some n -> n | None -> "?" in if not (Hashtbl.mem _jit_warned fn_name) then begin Hashtbl.replace _jit_warned fn_name true; Printf.eprintf "[jit] %s runtime fallback to CEK: %s\n%!" fn_name (Printexc.to_string e) @@ -727,8 +727,8 @@ let register_jit_hook env = None) | Some _ -> None (* compile failed or disabled — CEK handles *) | None -> + let fn_name = match l.l_name with Some n -> n | None -> "?" in if !_jit_compiling then None - else if Hashtbl.mem _jit_warned fn_name then None else begin _jit_compiling := true; let t0 = Unix.gettimeofday () in @@ -743,7 +743,6 @@ let register_jit_hook env = (try Some (Sx_vm.call_closure cl args cl.vm_env_ref) with e -> Printf.eprintf "[jit] %s first-call fallback to CEK: %s\n%!" fn_name (Printexc.to_string e); - l.l_compiled <- Some Sx_vm.jit_failed_sentinel; Hashtbl.replace _jit_warned fn_name true; None) | None -> None @@ -1579,16 +1578,9 @@ let http_inject_shell_statics env static_dir sx_sxc = let project_dir = try Sys.getenv "SX_PROJECT_DIR" with Not_found -> Filename.dirname (Filename.dirname static_dir) in let templates_dir = project_dir ^ "/shared/sx/templates" in - (* Client libraries: all .sx files in templates/client-libs/ *) - let client_libs_dir = templates_dir ^ "/client-libs" in - let extra_libs = - if Sys.file_exists client_libs_dir && Sys.is_directory client_libs_dir then - Array.to_list (Sys.readdir client_libs_dir) - |> List.filter (fun f -> Filename.check_suffix f ".sx") - |> List.sort String.compare - |> List.map (fun f -> client_libs_dir ^ "/" ^ f) - else [] in - let client_libs = (templates_dir ^ "/cssx.sx") :: extra_libs in + let client_libs = [ + templates_dir ^ "/cssx.sx"; + ] in List.iter (fun path -> if Sys.file_exists path then begin let src = In_channel.with_open_text path In_channel.input_all in @@ -1622,20 +1614,7 @@ let http_inject_shell_statics env static_dir sx_sxc = (* Compute file hashes for cache busting *) let sx_js_hash = file_hash (static_dir ^ "/scripts/sx-browser.js") in let body_js_hash = file_hash (static_dir ^ "/scripts/body.js") in - (* Include SX source file hashes so browser cache busts when .sx files change *) - let sx_dir = static_dir ^ "/wasm/sx" in - let sx_files_hash = - if Sys.file_exists sx_dir && Sys.is_directory sx_dir then - let entries = Sys.readdir sx_dir in - Array.sort String.compare entries; - let combined = Array.fold_left (fun acc f -> - if Filename.check_suffix f ".sx" then - acc ^ file_hash (sx_dir ^ "/" ^ f) - else acc - ) "" entries in - String.sub (Digest.string combined |> Digest.to_hex) 0 12 - else "" in - let wasm_hash = file_hash (static_dir ^ "/wasm/sx_browser.bc.wasm.js") ^ sx_files_hash in + let wasm_hash = file_hash (static_dir ^ "/wasm/sx_browser.bc.wasm.js") in (* Read CSS for inline injection *) let tw_css = read_css_file (static_dir ^ "/styles/tw.css") in let basics_css = read_css_file (static_dir ^ "/styles/basics.css") in @@ -1695,7 +1674,16 @@ let http_inject_shell_statics env static_dir sx_sxc = ignore (env_bind env "__shell-body-scripts" Nil); ignore (env_bind env "__shell-inline-css" Nil); ignore (env_bind env "__shell-inline-head-js" Nil); - ignore (env_bind env "__shell-init-sx" Nil); + (* init-sx: trigger client-side render when sx-root is empty (SSR failed). + The boot code hydrates existing islands but doesn't do fresh render. + This script forces a render from page-sx after boot completes. *) + ignore (env_bind env "__shell-init-sx" (String + "document.addEventListener('sx:boot-done', function() { \ + var root = document.getElementById('sx-root'); \ + if (root && !root.innerHTML.trim() && typeof SX !== 'undefined' && SX.renderPage) { \ + SX.renderPage(); \ + } \ + });")); Printf.eprintf "[sx-http] Shell statics: defs=%d hash=%s css=%d js=%s wasm=%s\n%!" (String.length component_defs) component_hash (String.length sx_css) sx_js_hash wasm_hash @@ -1801,9 +1789,7 @@ let http_setup_page_helpers env = SxExpr (Printf.sprintf "(pre :class \"text-sm overflow-x-auto\" (code \"%s\"))" escaped) | _ -> Nil); (* component-source — stub *) - bind "component-source" (fun _args -> String ""); - (* handler-source — stub (returns empty, used by example pages) *) - bind "handler-source" (fun _args -> String "") + bind "component-source" (fun _args -> String "") let http_mode port = let env = make_server_env () in @@ -2022,9 +2008,7 @@ let http_mode port = let is_ajax = List.exists (fun (k, _) -> k = "sx-request" || k = "hx-request") headers in match http_render_page env path headers with | Some html -> - let ct = if is_ajax then "text/sx; charset=utf-8" - else "text/html; charset=utf-8" in - let resp = http_response ~content_type:ct html in + let resp = http_response ~content_type:"text/html; charset=utf-8" html in if not is_ajax then Hashtbl.replace response_cache path resp; resp | None -> http_response ~status:404 "

Not Found

" diff --git a/hosts/ocaml/lib/sx_render.ml b/hosts/ocaml/lib/sx_render.ml index 141ead78..59ec5d14 100644 --- a/hosts/ocaml/lib/sx_render.ml +++ b/hosts/ocaml/lib/sx_render.ml @@ -280,10 +280,7 @@ and render_list_to_html head args env = | _ -> let result = Sx_ref.eval_expr (List (head :: args)) (Env env) in do_render_to_html result env) - with Eval_error _ -> - (* Symbol not in env — might be a primitive; eval the full expression *) - let result = Sx_ref.eval_expr (List (head :: args)) (Env env) in - do_render_to_html result env) + with Eval_error _ -> "") | _ -> let result = Sx_ref.eval_expr (List (head :: args)) (Env env) in do_render_to_html result env @@ -533,13 +530,10 @@ and render_list_buf buf head args env = | _ -> let result = Sx_ref.eval_expr (List (head :: args)) (Env env) in render_to_buf buf result env) - with Eval_error _ -> - (* Symbol not in env — might be a primitive; eval the full expression *) - (try - let result = Sx_ref.eval_expr (List (head :: args)) (Env env) in - render_to_buf buf result env - with Eval_error msg -> - Printf.eprintf "[ssr-skip] %s\n%!" msg)) + with Eval_error msg -> + (* Unknown symbol/component — skip silently during SSR. + The client will render from page-sx. *) + Printf.eprintf "[ssr-skip] %s\n%!" msg) | _ -> (try let result = Sx_ref.eval_expr (List (head :: args)) (Env env) in diff --git a/hosts/ocaml/lib/sx_vm.ml b/hosts/ocaml/lib/sx_vm.ml index bfe523f7..50ef87bc 100644 --- a/hosts/ocaml/lib/sx_vm.ml +++ b/hosts/ocaml/lib/sx_vm.ml @@ -576,12 +576,14 @@ let jit_compile_lambda (l : lambda) globals = let fn_expr = List [Symbol "fn"; param_syms; l.l_body] in let quoted = List [Symbol "quote"; fn_expr] in let result = Sx_ref.eval_expr (List [compile_fn; quoted]) (Env (make_env ())) in - (* Inject closure bindings into globals so GLOBAL_GET can find them. - Only injects values not already present in globals (preserves - existing defines). Mutable closure vars get stale snapshots here - but GLOBAL_SET writes back to vm_closure_env, and GLOBAL_GET - falls through to vm_closure_env if the global is stale. *) + (* If the lambda has closure-captured variables, merge them into globals + so the VM can find them via GLOBAL_GET. The compiler doesn't know + about the enclosing scope, so closure vars get compiled as globals. *) let effective_globals = + (* Use the LIVE globals table directly. Inject only truly local + closure bindings (not already in globals) into the live table. + This ensures GLOBAL_GET always sees the latest define values. + Previous approach copied globals, creating a stale snapshot. *) let closure = l.l_closure in let count = ref 0 in let rec inject env = @@ -623,14 +625,16 @@ let jit_compile_lambda (l : lambda) globals = as a NativeFn if it's callable (so the CEK can dispatch to it). *) (try let value = execute_module outer_code globals in - ignore (fn_name, value, bc); (* resolved — not a closure, CEK handles it *) + Printf.eprintf "[jit] RESOLVED %s: %s (bc[0]=%d)\n%!" + fn_name (type_of value) (if Array.length bc > 0 then bc.(0) else -1); (* If the resolved value is a NativeFn, we can't wrap it as a vm_closure — just let the CEK handle it directly. Return None so the lambda falls through to CEK, which will find the resolved value in the env on next lookup. *) None with _ -> - ignore fn_name; (* non-closure, execution failed — CEK fallback *) + Printf.eprintf "[jit] SKIP %s: non-closure execution failed (bc[0]=%d, len=%d)\n%!" + fn_name (if Array.length bc > 0 then bc.(0) else -1) (Array.length bc); None) end | _ -> diff --git a/web/request-handler.sx b/web/request-handler.sx index 0c32ce6e..57bb371f 100644 --- a/web/request-handler.sx +++ b/web/request-handler.sx @@ -1,43 +1,15 @@ (define - sx-handle-request - (fn - (path headers env) - (let - ((is-ajax (or (has-key? headers "sx-request") (has-key? headers "hx-request"))) - (path-expr (sx-parse-url path)) - (page-ast (sx-eval-page path-expr env))) - (if - (nil? page-ast) - nil - (let - ((nav-path (if (starts-with? path "/sx/") path (str "/sx" path)))) - (if - is-ajax - (sx-render-ajax page-ast nav-path env) - (sx-render-full-page page-ast nav-path env))))))) - -(define - sx-parse-url + sx-url-to-expr (fn (path) - (let - ((p (cond (or (= path "/") (= path "/sx/") (= path "/sx")) "home" (starts-with? path "/sx/") (substring path 4 (string-length path)) (starts-with? path "/") (substring path 1 (string-length path)) :else path))) - (let ((spaced (join " " (split p ".")))) spaced)))) - -(define - sx-eval-page - (fn - (path-expr env) - (let - ((exprs (sx-parse path-expr))) - (when - (not (empty? exprs)) - (let - ((expr (if (= (len exprs) 1) (first exprs) exprs)) - (quoted (sx-auto-quote expr env))) - (let - ((callable (if (symbol? quoted) (list quoted) quoted))) - (cek-try (fn () (eval-expr callable env)) (fn (err) nil)))))))) + (cond + (or (= path "/") (= path "/sx/") (= path "/sx")) + "home" + (starts-with? path "/sx/") + (join " " (split (slice path 4 (len path)) ".")) + (starts-with? path "/") + (join " " (split (slice path 1 (len path)) ".")) + :else path))) (define sx-auto-quote @@ -51,53 +23,73 @@ :else expr))) (define - sx-render-ajax + sx-eval-page (fn - (page-ast nav-path env) - (let - ((wrapped (list (make-symbol "~layouts/doc") :path nav-path page-ast)) - (aser-result (aser (list (make-symbol "quote") wrapped) env))) - (let - ((body-exprs (sx-parse aser-result))) + (path-expr env) + (cek-try + (fn + () (let - ((body-expr (if (= (len body-exprs) 1) (first body-exprs) (cons (make-symbol "<>") body-exprs)))) - (render-to-html body-expr env)))))) + ((exprs (sx-parse path-expr))) + (when + (not (empty? exprs)) + (let + ((expr (if (= (len exprs) 1) (first exprs) exprs)) + (quoted (sx-auto-quote expr env)) + (callable (if (symbol? quoted) (list quoted) quoted))) + (eval-expr callable env))))) + (fn (err) nil)))) (define - sx-render-full-page + sx-handle-request (fn - (page-ast nav-path env) + (path headers env) (let - ((wrapped (list (make-symbol "~layouts/doc") :path nav-path page-ast)) - (full-ast - (list (make-symbol "~shared:layout/app-body") :content wrapped))) - (let - ((aser-result (aser (list (make-symbol "quote") full-ast) env))) + ((is-ajax (or (has-key? headers "sx-request") (has-key? headers "hx-request"))) + (path-expr (sx-url-to-expr path)) + (page-ast (sx-eval-page path-expr env))) + (if + (nil? page-ast) + nil (let - ((body-exprs (sx-parse aser-result))) - (let - ((body-expr (if (= (len body-exprs) 1) (first body-exprs) (cons (make-symbol "<>") body-exprs)))) - (let - ((body-html (render-to-html body-expr env)) - (page-source (serialize full-ast))) - (~shared:shell/sx-page-shell - :title "SX" - :csrf "" - :page-sx page-source - :body-html body-html - :component-defs __shell-component-defs - :component-hash __shell-component-hash - :pages-sx __shell-pages-sx - :sx-css __shell-sx-css - :sx-css-classes __shell-sx-css-classes - :asset-url __shell-asset-url - :sx-js-hash __shell-sx-js-hash - :body-js-hash __shell-body-js-hash - :wasm-hash __shell-wasm-hash - :head-scripts __shell-head-scripts - :body-scripts __shell-body-scripts - :inline-css __shell-inline-css - :inline-head-js __shell-inline-head-js - :init-sx __shell-init-sx - :use-wasm (= (or (env-get env "SX_USE_WASM") "") "1") - :meta-html "")))))))) + ((nav-path (if (starts-with? path "/sx/") path (str "/sx" path)))) + (cek-try + (fn + () + (if + is-ajax + (let + ((content (list (make-symbol "~layouts/doc") :path nav-path page-ast))) + (render-to-html content env)) + (let + ((wrapped (list (make-symbol "~layouts/doc") :path nav-path page-ast)) + (full-ast + (list + (make-symbol "~shared:layout/app-body") + :content wrapped)) + (body-html (render-to-html full-ast env))) + (render-to-html + (list + (make-symbol "~shared:shell/sx-page-shell") + :title "SX" + :csrf "" + :page-sx (serialize full-ast) + :body-html body-html + :component-defs __shell-component-defs + :component-hash __shell-component-hash + :pages-sx __shell-pages-sx + :sx-css __shell-sx-css + :sx-css-classes __shell-sx-css-classes + :asset-url __shell-asset-url + :sx-js-hash __shell-sx-js-hash + :body-js-hash __shell-body-js-hash + :wasm-hash __shell-wasm-hash + :head-scripts __shell-head-scripts + :body-scripts __shell-body-scripts + :inline-css __shell-inline-css + :inline-head-js __shell-inline-head-js + :init-sx __shell-init-sx + :use-wasm true + :meta-html "") + env)))) + (fn (err) (str "

Render error

" err "
"))))))))