diff --git a/hosts/javascript/platform.py b/hosts/javascript/platform.py index 4ba2fe6..f8f782f 100644 --- a/hosts/javascript/platform.py +++ b/hosts/javascript/platform.py @@ -1131,7 +1131,12 @@ PRIMITIVES_JS_MODULES: dict[str, str] = { PRIMITIVES["emitted"] = sxEmitted; // Aliases for aser adapter (avoids CEK special form conflict on server) var scopeEmit = sxEmit; - var scopePeek = sxEmitted; + function scopePeek(name) { + if (_scopeStacks[name] && _scopeStacks[name].length) { + return _scopeStacks[name][_scopeStacks[name].length - 1].value; + } + return NIL; + } PRIMITIVES["scope-emit!"] = scopeEmit; PRIMITIVES["scope-peek"] = scopePeek; ''', diff --git a/hosts/ocaml/bin/dune b/hosts/ocaml/bin/dune index 8524650..42bf9df 100644 --- a/hosts/ocaml/bin/dune +++ b/hosts/ocaml/bin/dune @@ -1,3 +1,3 @@ (executables - (names run_tests debug_set sx_server) + (names run_tests debug_set sx_server integration_tests) (libraries sx unix)) diff --git a/hosts/ocaml/bin/integration_tests.ml b/hosts/ocaml/bin/integration_tests.ml new file mode 100644 index 0000000..21cc68f --- /dev/null +++ b/hosts/ocaml/bin/integration_tests.ml @@ -0,0 +1,383 @@ +(** Integration tests — exercises the full rendering pipeline. + + Loads spec files + web adapters into a server-like env, then renders + HTML expressions. Catches "Undefined symbol" errors that only surface + when the full stack is loaded (not caught by spec unit tests). + + Usage: + dune exec bin/integration_tests.exe *) + +module Sx_types = Sx.Sx_types +module Sx_parser = Sx.Sx_parser +module Sx_primitives = Sx.Sx_primitives +module Sx_runtime = Sx.Sx_runtime +module Sx_ref = Sx.Sx_ref +module Sx_render = Sx.Sx_render + +open Sx_types + +let pass_count = ref 0 +let fail_count = ref 0 + +let assert_eq name expected actual = + if expected = actual then begin + incr pass_count; + Printf.printf " PASS: %s\n%!" name + end else begin + incr fail_count; + Printf.printf " FAIL: %s\n expected: %s\n got: %s\n%!" name expected actual + end + +let assert_contains name needle haystack = + let rec find i = + if i + String.length needle > String.length haystack then false + else if String.sub haystack i (String.length needle) = needle then true + else find (i + 1) + in + if String.length needle > 0 && find 0 then begin + incr pass_count; + Printf.printf " PASS: %s\n%!" name + end else begin + incr fail_count; + Printf.printf " FAIL: %s — expected to contain %S in %S\n%!" name needle haystack + end + +let assert_no_error name f = + try + ignore (f ()); + incr pass_count; + Printf.printf " PASS: %s\n%!" name + with + | Eval_error msg -> + incr fail_count; + Printf.printf " FAIL: %s — %s\n%!" name msg + | exn -> + incr fail_count; + Printf.printf " FAIL: %s — %s\n%!" name (Printexc.to_string exn) + +(* Build a server-like env with rendering support *) +let make_integration_env () = + let env = make_env () in + let bind (n : string) fn = + ignore (Sx_types.env_bind env n (NativeFn (n, fn))) + in + + Sx_render.setup_render_env env; + + (* HTML tag functions — same as sx_server.ml *) + List.iter (fun tag -> + ignore (env_bind env tag + (NativeFn ("html:" ^ tag, fun args -> List (Symbol tag :: args)))) + ) Sx_render.html_tags; + + (* Platform primitives needed by spec/render.sx and adapters *) + 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 "escape-html" (fun args -> + match args with [String s] -> String (Sx_render.escape_html s) | _ -> String ""); + bind "escape-attr" (fun args -> + match args with [String s] -> String (Sx_render.escape_html s) | _ -> String ""); + bind "escape-string" (fun args -> + match args with [String s] -> String (Sx_render.escape_html s) | _ -> String ""); + bind "is-html-tag?" (fun args -> + match args with [String s] -> Bool (Sx_render.is_html_tag s) | _ -> Bool false); + bind "is-void-element?" (fun args -> + match args with [String s] -> Bool (Sx_render.is_void s) | _ -> Bool false); + bind "is-boolean-attr?" (fun args -> + match args with [String s] -> Bool (Sx_render.is_boolean_attr s) | _ -> Bool false); + + (* Mutable operations needed by adapter code *) + 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")); + bind "dict-set!" (fun args -> + match args with + | [Dict d; String k; v] -> Hashtbl.replace d k v; v + | [Dict d; Keyword k; v] -> Hashtbl.replace d k v; v + | _ -> Nil); + bind "dict-has?" (fun args -> + match args with + | [Dict d; String k] -> Bool (Hashtbl.mem d k) + | [Dict d; Keyword k] -> Bool (Hashtbl.mem d k) + | _ -> Bool false); + bind "dict-get" (fun args -> + match args with + | [Dict d; String k] -> (match Hashtbl.find_opt d k with Some v -> v | None -> Nil) + | [Dict d; Keyword k] -> (match Hashtbl.find_opt d k with Some v -> v | None -> Nil) + | _ -> Nil); + bind "empty-dict?" (fun args -> + match args with + | [Dict d] -> Bool (Hashtbl.length d = 0) + | _ -> Bool true); + bind "mutable-list" (fun _args -> ListRef (ref [])); + + (* Symbol/keyword accessors needed by adapter-html.sx *) + bind "symbol-name" (fun args -> + match args with [Symbol s] -> String s | _ -> raise (Eval_error "symbol-name: expected symbol")); + bind "keyword-name" (fun args -> + match args with [Keyword k] -> String k | _ -> raise (Eval_error "keyword-name: expected keyword")); + bind "make-symbol" (fun args -> + match args with [String s] -> Symbol s | _ -> raise (Eval_error "make-symbol: expected string")); + bind "make-keyword" (fun args -> + match args with [String s] -> Keyword s | _ -> raise (Eval_error "make-keyword: expected string")); + + (* Type predicates needed by adapters *) + bind "island?" (fun args -> match args with [Island _] -> Bool true | _ -> Bool false); + bind "component?" (fun args -> match args with [Component _] -> Bool true | _ -> Bool false); + bind "macro?" (fun args -> match args with [Macro _] -> Bool true | _ -> Bool false); + bind "lambda?" (fun args -> match args with [Lambda _] -> Bool true | _ -> Bool false); + bind "spread?" (fun args -> match args with [Spread _] -> Bool true | _ -> Bool false); + bind "spread-attrs" (fun args -> + match args with + | [Spread pairs] -> let d = Hashtbl.create 8 in + List.iter (fun (k, v) -> Hashtbl.replace d k v) pairs; Dict d + | _ -> Nil); + bind "component-name" (fun args -> match args with [Component c] -> String c.c_name | [Island i] -> String i.i_name | _ -> Nil); + bind "component-params" (fun args -> match args with [Component c] -> List (List.map (fun s -> String s) c.c_params) | _ -> List []); + bind "component-body" (fun args -> match args with [Component c] -> c.c_body | _ -> Nil); + bind "component-closure" (fun args -> match args with [Component c] -> Env c.c_closure | _ -> 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 "lambda-params" (fun args -> match args with [Lambda l] -> List (List.map (fun s -> String s) l.l_params) | _ -> List []); + bind "lambda-body" (fun args -> match args with [Lambda l] -> l.l_body | _ -> Nil); + bind "lambda-closure" (fun args -> match args with [Lambda l] -> Env l.l_closure | _ -> Nil); + bind "lambda-name" (fun args -> match args with [Lambda l] -> (match l.l_name with Some n -> String n | None -> Nil) | _ -> Nil); + bind "set-lambda-name!" (fun args -> match args with [Lambda l; String n] -> l.l_name <- Some n; Nil | _ -> Nil); + + (* Environment operations *) + bind "env-extend" (fun args -> + match args with [Env e] -> Env (env_extend e) | _ -> Env (env_extend env)); + bind "env-bind!" (fun args -> + match args with [Env e; String k; v] -> env_bind e k v | _ -> Nil); + bind "env-set!" (fun args -> + match args with [Env e; String k; v] -> env_set e k v | _ -> Nil); + bind "env-get" (fun args -> + match args with [Env e; String k] -> env_get e k | _ -> Nil); + bind "env-has?" (fun args -> + match args with [Env e; String k] -> Bool (env_has e k) | _ -> Bool false); + bind "env-merge" (fun args -> + match args with [Env a; Env b] -> Env (env_merge a b) | _ -> Nil); + bind "make-env" (fun _args -> Env (make_env ())); + + (* Eval/trampoline — needed by adapters *) + bind "eval-expr" (fun args -> + match args with + | [expr; e] -> Sx_ref.eval_expr expr e + | _ -> Nil); + bind "trampoline" (fun args -> + match args with + | [Thunk (e, env)] -> Sx_ref.eval_expr e (Env env) + | [v] -> v | _ -> Nil); + bind "call-lambda" (fun args -> + match args with + | [f; List a] -> Sx_runtime.sx_call f a + | [f; a] -> Sx_runtime.sx_call f [a] + | _ -> Nil); + bind "expand-macro" (fun args -> + match args with + | [Macro m; List macro_args; _env] -> + let local = env_extend m.m_closure in + let rec bind_params ps as' = match ps, as' with + | [], rest -> + (match m.m_rest_param with Some rp -> ignore (env_bind local rp (List rest)) | None -> ()) + | p :: ps_rest, a :: as_rest -> + ignore (env_bind local p a); bind_params ps_rest as_rest + | _ :: _, [] -> () + in + bind_params m.m_params macro_args; + Sx_ref.eval_expr m.m_body (Env local) + | _ -> Nil); + + (* Scope/provide — needed by adapter-html.sx and the CEK evaluator. + Must be registered as primitives (prim_call) not just env bindings. *) + let scope_stacks : (string, value list) Hashtbl.t = Hashtbl.create 8 in + let scope_emitted : (string, value list) Hashtbl.t = Hashtbl.create 8 in + let scope_push name v = + let stack = try Hashtbl.find scope_stacks name with Not_found -> [] in + Hashtbl.replace scope_stacks name (v :: stack); Nil in + let scope_pop name = + (match Hashtbl.find_opt scope_stacks name with + | Some (_ :: rest) -> Hashtbl.replace scope_stacks name rest + | _ -> ()); Nil in + let scope_peek name = + match Hashtbl.find_opt scope_stacks name with + | Some (v :: _) -> v | _ -> Nil in + let scope_emit name v = + let items = try Hashtbl.find scope_emitted name with Not_found -> [] in + Hashtbl.replace scope_emitted name (items @ [v]); Nil in + let emitted name = + match Hashtbl.find_opt scope_emitted name with Some l -> List l | None -> List [] in + (* Register as both env bindings AND primitives *) + bind "scope-push!" (fun args -> match args with [String n; v] -> scope_push n v | _ -> Nil); + bind "scope-pop!" (fun args -> match args with [String n] -> scope_pop n | _ -> Nil); + bind "scope-peek" (fun args -> match args with [String n] -> scope_peek n | _ -> Nil); + bind "scope-emit!" (fun args -> match args with [String n; v] -> scope_emit n v | _ -> Nil); + bind "emitted" (fun args -> match args with [String n] -> emitted n | _ -> List []); + (* Also register as primitives for prim_call *) + Sx_primitives.register "scope-push!" (fun args -> match args with [String n; v] -> scope_push n v | _ -> Nil); + Sx_primitives.register "scope-pop!" (fun args -> match args with [String n] -> scope_pop n | _ -> Nil); + Sx_primitives.register "scope-peek" (fun args -> match args with [String n] -> scope_peek n | _ -> Nil); + Sx_primitives.register "scope-emit!" (fun args -> match args with [String n; v] -> scope_emit n v | _ -> Nil); + Sx_primitives.register "emitted" (fun args -> match args with [String n] -> emitted n | _ -> List []); + + (* Render-mode flags *) + ignore (env_bind env "*render-active*" (Bool false)); + bind "set-render-active!" (fun args -> + match args with [v] -> ignore (env_set env "*render-active*" v); Nil | _ -> Nil); + bind "render-active?" (fun _args -> + try env_get env "*render-active*" with _ -> Bool false); + bind "definition-form?" (fun args -> + match args with + | [String s] -> Bool (List.mem s ["define"; "defcomp"; "defisland"; "defmacro"; + "defstyle"; "defhandler"; "deftype"; "defeffect"; "defquery"; "defaction"; "defrelation"]) + | _ -> Bool false); + + (* Signal stubs for SSR *) + bind "signal" (fun args -> match args with [v] -> v | _ -> Nil); + bind "computed" (fun args -> match args with [f] -> Sx_runtime.sx_call f [] | _ -> Nil); + bind "deref" (fun args -> match args with [v] -> v | _ -> Nil); + bind "reset!" (fun _args -> Nil); + bind "swap!" (fun _args -> Nil); + bind "effect" (fun _args -> Nil); + bind "batch" (fun _args -> Nil); + + (* DOM stubs *) + bind "create-text-node" (fun args -> match args with [String s] -> String s | _ -> Nil); + bind "create-fragment" (fun _args -> Nil); + bind "dom-create-element" (fun _args -> Nil); + bind "dom-append" (fun _args -> Nil); + bind "dom-set-attr" (fun _args -> Nil); + bind "dom-set-prop" (fun _args -> Nil); + bind "dom-get-attr" (fun _args -> Nil); + bind "dom-query" (fun _args -> Nil); + bind "dom-body" (fun _args -> Nil); + + (* Misc stubs *) + bind "random-int" (fun args -> + match args with + | [Number lo; Number hi] -> Number (lo +. Float.round (Random.float (hi -. lo))) + | _ -> Number 0.0); + bind "expand-components?" (fun _args -> Bool false); + bind "freeze-scope" (fun _args -> Nil); + bind "freeze-signal" (fun _args -> Nil); + bind "thaw-from-sx" (fun _args -> Nil); + bind "local-storage-get" (fun _args -> Nil); + bind "local-storage-set" (fun _args -> Nil); + bind "schedule-idle" (fun _args -> Nil); + bind "run-post-render-hooks" (fun _args -> Nil); + bind "freeze-to-sx" (fun _args -> String ""); + + env + + +let () = + Printexc.record_backtrace true; + + (* Find project root *) + let rec find_root dir = + let candidate = Filename.concat dir "spec/render.sx" in + if Sys.file_exists candidate then dir + else let parent = Filename.dirname dir in + if parent = dir then Sys.getcwd () else find_root parent + in + let root = find_root (Sys.getcwd ()) in + let spec p = Filename.concat (Filename.concat root "spec") p in + let web p = Filename.concat (Filename.concat root "web") p in + + let env = make_integration_env () in + + (* Load spec + adapters *) + Printf.printf "Loading spec + adapters...\n%!"; + let load path = + if Sys.file_exists path then begin + let exprs = Sx_parser.parse_file path in + List.iter (fun expr -> ignore (Sx_ref.eval_expr expr (Env env))) exprs; + Printf.printf " loaded %s (%d defs)\n%!" (Filename.basename path) (List.length exprs) + end else + Printf.printf " SKIP %s (not found)\n%!" path + in + load (spec "parser.sx"); + load (spec "render.sx"); + load (web "signals.sx"); + load (web "adapter-html.sx"); + load (web "adapter-sx.sx"); + + (* Helper: render SX source string to HTML *) + let render_html src = + let exprs = Sx_parser.parse_all src in + let expr = match exprs with [e] -> e | _ -> Nil in + Sx_render.render_to_html expr env + in + + (* Helper: call SX render-to-html via the adapter *) + let sx_render_html src = + let exprs = Sx_parser.parse_all src in + let expr = match exprs with [e] -> e | _ -> Nil in + let call = List [Symbol "render-to-html"; List [Symbol "quote"; expr]; Env env] in + match Sx_ref.eval_expr call (Env env) with + | String s | RawHTML s -> s + | v -> value_to_string v + in + + (* ================================================================== *) + Printf.printf "\nSuite: native renderer — HTML tags\n%!"; + assert_eq "div" "
hello
" (render_html "(div \"hello\")"); + assert_eq "div with class" "
text
" (render_html "(div :class \"card\" \"text\")"); + assert_eq "nested tags" "

inner

" (render_html "(div (p \"inner\"))"); + assert_eq "void element" "
" (render_html "(br)"); + assert_eq "h1" "

Title

" (render_html "(h1 \"Title\")"); + assert_eq "span with attrs" "text" (render_html "(span :class \"bold\" \"text\")"); + + (* ================================================================== *) + Printf.printf "\nSuite: SX adapter render-to-html — HTML tags\n%!"; + assert_no_error "div doesn't throw" (fun () -> sx_render_html "(div \"hello\")"); + assert_contains "div produces tag" "" (sx_render_html "(div (p \"inner\"))"); + assert_no_error "h1 doesn't throw" (fun () -> sx_render_html "(h1 \"Title\")"); + assert_no_error "span doesn't throw" (fun () -> sx_render_html "(span :class \"bold\" \"text\")"); + assert_no_error "table doesn't throw" (fun () -> sx_render_html "(table (tr (td \"cell\")))"); + + (* ================================================================== *) + Printf.printf "\nSuite: SX adapter — special forms in HTML context\n%!"; + assert_contains "when true renders" "

" (sx_render_html "(when true (p \"yes\"))"); + assert_eq "when false empty" "" (sx_render_html "(when false (p \"no\"))"); + assert_contains "if true branch" "yes" (sx_render_html "(if true (span \"yes\") (span \"no\"))"); + assert_contains "if false branch" "no" (sx_render_html "(if false (span \"yes\") (span \"no\"))"); + assert_contains "let in render" "hello" (sx_render_html "(let ((x \"hello\")) (p x))"); + + (* ================================================================== *) + Printf.printf "\nSuite: SX adapter — letrec in HTML context\n%!"; + assert_no_error "letrec with div body" (fun () -> + sx_render_html "(letrec ((x 42)) (div (str x)))"); + assert_contains "letrec renders body" "

" (sx_render_html "(letrec ((x 42)) (div (str x)))"); + assert_no_error "letrec with side effects then div" (fun () -> + sx_render_html "(letrec ((x 1) (y 2)) (let ((z (+ x y))) (div (str z))))"); + + (* ================================================================== *) + Printf.printf "\nSuite: SX adapter — components\n%!"; + assert_no_error "defcomp + render" (fun () -> + ignore (Sx_ref.eval_expr + (List.hd (Sx_parser.parse_all "(defcomp ~test-card (&key title &rest children) (div :class \"card\" (h2 title) children))")) + (Env env)); + sx_render_html "(~test-card :title \"Hi\" (p \"body\"))"); + assert_contains "component renders div" " + Sx_ref.eval_expr (List [Symbol "div"; Keyword "class"; String "foo"; String "hi"]) (Env env)); + assert_no_error "eval (span) returns list" (fun () -> + Sx_ref.eval_expr (List [Symbol "span"; String "text"]) (Env env)); + + (* ================================================================== *) + Printf.printf "\n"; + Printf.printf "============================================================\n"; + Printf.printf "Integration: %d passed, %d failed\n" !pass_count !fail_count; + Printf.printf "============================================================\n"; + if !fail_count > 0 then exit 1 diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 020adaf..51f14fe 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -908,6 +908,32 @@ let make_server_env () = CEK, cache result). cek_call checks this before CEK dispatch. *) let _jit_compiling = ref false (* re-entrancy guard *) +(** Functions allowed for JIT compilation. Others go straight to CEK. + Populated with compiler internals at registration time; extended + dynamically via (jit-allow name) command. *) +module StringSet = Set.Make(String) +let jit_allowlist = ref (StringSet.of_list [ + (* Compiler internals *) + "compile"; "compile-module"; "compile-expr"; "compile-symbol"; + "compile-dict"; "compile-list"; "compile-if"; "compile-when"; + "compile-and"; "compile-or"; "compile-begin"; "compile-let"; + "compile-letrec"; "compile-lambda"; "compile-define"; "compile-set"; + "compile-quote"; "compile-cond"; "compile-case"; "compile-case-clauses"; + "compile-thread"; "compile-thread-step"; "compile-defcomp"; + "compile-defmacro"; "compile-quasiquote"; "compile-qq-expr"; + "compile-qq-list"; "compile-call"; + "make-emitter"; "make-pool"; "make-scope"; "pool-add"; + "scope-define-local"; "scope-resolve"; + "emit-byte"; "emit-u16"; "emit-i16"; "emit-op"; "emit-const"; + "current-offset"; "patch-i16"; + (* Parser *) + "sx-parse"; "sx-serialize"; "sx-serialize-dict"; + (* Pure transforms *) + "aser"; "resolve-nav-path"; "tw"; "serialize"; + "render-to-html"; "render-attrs"; "render-html-element"; + "merge-spread-attrs"; "cssx-process-token"; + "signal"; "computed"; "freeze-scope"; "freeze-signal"; +]) let register_jit_hook env = Sx_ref.jit_call_hook := Some (fun f args -> @@ -920,29 +946,28 @@ let register_jit_hook env = (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 - Printf.eprintf "[jit-hook] VM FAIL %s: %s (disabling JIT)\n%!" fn_name (Printexc.to_string e); + Printf.eprintf "[jit] DISABLED %s — %s\n%!" fn_name (Printexc.to_string e); l.l_compiled <- Some Sx_vm.jit_failed_sentinel; None) - | Some _ -> None (* compile failed — CEK handles *) + | 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 not (StringSet.mem fn_name !jit_allowlist) then None else begin - let fn_name = match l.l_name with Some n -> n | None -> "?" in _jit_compiling := true; let t0 = Unix.gettimeofday () in let compiled = Sx_vm.jit_compile_lambda l env.bindings in let dt = Unix.gettimeofday () -. t0 in _jit_compiling := false; - Printf.eprintf "[jit-hook] %s compile %s in %.3fs\n%!" + Printf.eprintf "[jit] %s compile %s in %.3fs\n%!" fn_name (match compiled with Some _ -> "OK" | None -> "FAIL") dt; match compiled with | Some cl -> l.l_compiled <- Some cl; - (* Run on VM, fall back to CEK on runtime error *) (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 - Printf.eprintf "[jit-hook] VM FAIL (first call) %s: %s (disabling JIT)\n%!" fn_name (Printexc.to_string e); + Printf.eprintf "[jit] DISABLED %s — %s\n%!" fn_name (Printexc.to_string e); l.l_compiled <- Some Sx_vm.jit_failed_sentinel; None) | None -> None @@ -1106,6 +1131,10 @@ let rec dispatch env cmd = Printf.eprintf "[jit] Pre-compiled %d compiler functions in %.3fs\n%!" !count dt; send_ok () + | List [Symbol "jit-allow"; String name] -> + jit_allowlist := StringSet.add name !jit_allowlist; + send_ok () + | List [Symbol "aser-slot"; String src] -> (* Expand ALL components server-side. Uses batch IO mode. Calls aser via CEK — the JIT hook compiles it on first call. *) @@ -1449,10 +1478,69 @@ let cli_mode mode = Printf.eprintf "Error: %s\n" (Printexc.to_string exn); exit 1) +let test_mode () = + let env = make_server_env () in + (* Load full spec + adapter stack *) + let base = try Sys.getenv "SX_SPEC_DIR" with Not_found -> "spec" in + let web_base = try Sys.getenv "SX_WEB_DIR" with Not_found -> "web" in + let files = [ + Filename.concat base "parser.sx"; + Filename.concat base "render.sx"; + Filename.concat base "compiler.sx"; + Filename.concat web_base "signals.sx"; + Filename.concat web_base "adapter-html.sx"; + Filename.concat web_base "adapter-sx.sx"; + ] in + cli_load_files env files; + (* Register JIT *) + register_jit_hook env; + (* Load any --load files *) + let load_files = ref [] in + let eval_exprs = ref [] in + let args = Array.to_list Sys.argv in + let rec scan = function + | "--load" :: path :: rest -> load_files := path :: !load_files; scan rest + | "--eval" :: expr :: rest -> eval_exprs := expr :: !eval_exprs; scan rest + | _ :: rest -> scan rest + | [] -> () + in scan args; + cli_load_files env (List.rev !load_files); + if !eval_exprs <> [] then + List.iter (fun src -> + try + let exprs = Sx_parser.parse_all src in + let result = List.fold_left (fun _ e -> + Sx_ref.eval_expr e (Env env)) Nil exprs in + Printf.printf "%s\n%!" (serialize_value result) + with + | Eval_error msg -> Printf.eprintf "Error: %s\n%!" msg; exit 1 + | exn -> Printf.eprintf "Error: %s\n%!" (Printexc.to_string exn); exit 1 + ) (List.rev !eval_exprs) + else begin + (* Read from stdin *) + let buf = Buffer.create 4096 in + (try while true do + let line = input_line stdin in + Buffer.add_string buf line; Buffer.add_char buf '\n' + done with End_of_file -> ()); + let src = String.trim (Buffer.contents buf) in + if src <> "" then begin + try + let exprs = Sx_parser.parse_all src in + let result = List.fold_left (fun _ e -> + Sx_ref.eval_expr e (Env env)) Nil exprs in + Printf.printf "%s\n%!" (serialize_value result) + with + | Eval_error msg -> Printf.eprintf "Error: %s\n%!" msg; exit 1 + | exn -> Printf.eprintf "Error: %s\n%!" (Printexc.to_string exn); exit 1 + end + end + let () = (* Check for CLI mode flags *) let args = Array.to_list Sys.argv in - if List.mem "--render" args then cli_mode "render" + if List.mem "--test" args then test_mode () + else if List.mem "--render" args then cli_mode "render" else if List.mem "--aser-slot" args then cli_mode "aser-slot" else if List.mem "--aser" args then cli_mode "aser" else begin diff --git a/hosts/ocaml/lib/sx_vm.ml b/hosts/ocaml/lib/sx_vm.ml index 73ee4a3..0115919 100644 --- a/hosts/ocaml/lib/sx_vm.ml +++ b/hosts/ocaml/lib/sx_vm.ml @@ -84,13 +84,10 @@ let closure_to_value cl = let _vm_insn_count = ref 0 let _vm_call_count = ref 0 let _vm_cek_count = ref 0 -let _vm_closure_call_count = ref 0 -let _vm_max_depth = ref 0 -let vm_reset_counters () = _vm_insn_count := 0; _vm_call_count := 0; _vm_cek_count := 0; - _vm_closure_call_count := 0; _vm_max_depth := 0 +let vm_reset_counters () = _vm_insn_count := 0; _vm_call_count := 0; _vm_cek_count := 0 let vm_report_counters () = - Printf.eprintf "[vm-perf] insns=%d calls=%d cek_fallbacks=%d vm_closure=%d max_depth=%d\n%!" - !_vm_insn_count !_vm_call_count !_vm_cek_count !_vm_closure_call_count !_vm_max_depth + Printf.eprintf "[vm-perf] insns=%d calls=%d cek_fallbacks=%d\n%!" + !_vm_insn_count !_vm_call_count !_vm_cek_count (** Push a VM closure frame onto the current VM — no new VM allocation. This is the fast path for intra-VM closure calls. *) @@ -128,9 +125,6 @@ let code_from_value v = Used for entry points: JIT Lambda calls, module execution, cross-boundary. *) let rec call_closure cl args globals = incr _vm_call_count; - if !_vm_call_count mod 10000 = 0 then - Printf.eprintf "[vm-debug] call_closure count=%d name=%s\n%!" - !_vm_call_count (match cl.vm_name with Some n -> n | None -> "anon"); let vm = create globals in push_closure_frame vm cl args; (try run vm with e -> raise e); @@ -144,13 +138,6 @@ and vm_call vm f args = match f with | VmClosure cl -> (* Fast path: push frame on current VM — no allocation, enables TCO *) - incr _vm_closure_call_count; - let depth = List.length vm.frames + 1 in - if depth > !_vm_max_depth then _vm_max_depth := depth; - if !_vm_closure_call_count mod 100000 = 0 then - Printf.eprintf "[vm-debug] VmClosure calls=%d depth=%d name=%s\n%!" - !_vm_closure_call_count depth - (match cl.vm_name with Some n -> n | None -> "anon"); push_closure_frame vm cl args | NativeFn (_name, fn) -> let result = fn args in @@ -202,12 +189,6 @@ and run vm = if frame.ip >= Array.length bc then vm.frames <- [] (* bytecode exhausted — stop *) else begin - incr _vm_insn_count; - if !_vm_insn_count mod 1000000 = 0 then begin - let fn_name = match frame.closure.vm_name with Some n -> n | None -> "?" in - Printf.eprintf "[vm-debug] insns=%dM in=%s ip=%d depth=%d sp=%d\n%!" - (!_vm_insn_count / 1000000) fn_name frame.ip (List.length vm.frames) vm.sp - end; let saved_ip = frame.ip in let op = bc.(frame.ip) in frame.ip <- frame.ip + 1; diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index c703113..c960f32 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-23T23:36:04Z"; + var SX_VERSION = "2026-03-23T23:55:49Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -512,7 +512,12 @@ PRIMITIVES["emitted"] = sxEmitted; // Aliases for aser adapter (avoids CEK special form conflict on server) var scopeEmit = sxEmit; - var scopePeek = sxEmitted; + function scopePeek(name) { + if (_scopeStacks[name] && _scopeStacks[name].length) { + return _scopeStacks[name][_scopeStacks[name].length - 1].value; + } + return NIL; + } PRIMITIVES["scope-emit!"] = scopeEmit; PRIMITIVES["scope-peek"] = scopePeek; @@ -1589,13 +1594,7 @@ PRIMITIVES["step-sf-lambda"] = stepSfLambda; var val = NIL; var body = NIL; (isSxTruthy((isSxTruthy((len(restArgs) >= 2)) && isSxTruthy((typeOf(first(restArgs)) == "keyword")) && (keywordName(first(restArgs)) == "value"))) ? ((val = trampoline(evalExpr(nth(restArgs, 1), env))), (body = slice(restArgs, 2))) : (body = restArgs)); - scopePush(name, val); - return (function() { - var result = NIL; - { var _c = body; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; result = trampoline(evalExpr(expr, env)); } } - scopePop(name); - return makeCekValue(result, env, kont); -})(); + return (isSxTruthy(isEmpty(body)) ? makeCekValue(NIL, env, kont) : makeCekState(first(body), env, kontPush(makeScopeAccFrame(name, val, rest(body), env), kont))); })(); }; PRIMITIVES["step-sf-scope"] = stepSfScope; @@ -1604,13 +1603,7 @@ PRIMITIVES["step-sf-scope"] = stepSfScope; var name = trampoline(evalExpr(first(args), env)); var val = trampoline(evalExpr(nth(args, 1), env)); var body = slice(args, 2); - scopePush(name, val); - return (function() { - var result = NIL; - { var _c = body; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; result = trampoline(evalExpr(expr, env)); } } - scopePop(name); - return makeCekValue(result, env, kont); -})(); + return (isSxTruthy(isEmpty(body)) ? makeCekValue(NIL, env, kont) : makeCekState(first(body), env, kontPush(makeProvideFrame(name, val, rest(body), env), kont))); })(); }; PRIMITIVES["step-sf-provide"] = stepSfProvide; @@ -1618,8 +1611,8 @@ PRIMITIVES["step-sf-provide"] = stepSfProvide; var stepSfContext = function(args, env, kont) { return (function() { var name = trampoline(evalExpr(first(args), env)); var defaultVal = (isSxTruthy((len(args) >= 2)) ? trampoline(evalExpr(nth(args, 1), env)) : NIL); - var val = scopePeek(name); - return makeCekValue((isSxTruthy(isNil(val)) ? defaultVal : val), env, kont); + var frame = kontFindProvide(kont, name); + return makeCekValue((isSxTruthy(isNil(frame)) ? defaultVal : get(frame, "value")), env, kont); })(); }; PRIMITIVES["step-sf-context"] = stepSfContext; @@ -1627,7 +1620,10 @@ PRIMITIVES["step-sf-context"] = stepSfContext; var stepSfEmit = function(args, env, kont) { return (function() { var name = trampoline(evalExpr(first(args), env)); var val = trampoline(evalExpr(nth(args, 1), env)); - scopeEmit(name, val); + var frame = kontFindScopeAcc(kont, name); + if (isSxTruthy(frame)) { + frame["emitted"] = append(get(frame, "emitted"), [val]); +} return makeCekValue(NIL, env, kont); })(); }; PRIMITIVES["step-sf-emit"] = stepSfEmit; @@ -1635,8 +1631,8 @@ PRIMITIVES["step-sf-emit"] = stepSfEmit; // step-sf-emitted var stepSfEmitted = function(args, env, kont) { return (function() { var name = trampoline(evalExpr(first(args), env)); - var val = scopePeek(name); - return makeCekValue((isSxTruthy(isNil(val)) ? [] : val), env, kont); + var frame = kontFindScopeAcc(kont, name); + return makeCekValue((isSxTruthy(isNil(frame)) ? [] : get(frame, "emitted")), env, kont); })(); }; PRIMITIVES["step-sf-emitted"] = stepSfEmitted; diff --git a/spec/evaluator.sx b/spec/evaluator.sx index 5ee1b45..bb5ea83 100644 --- a/spec/evaluator.sx +++ b/spec/evaluator.sx @@ -1440,11 +1440,12 @@ (make-cek-value (sf-lambda args env) env kont))) ;; scope: evaluate name, then push ScopeFrame -;; scope/provide/context/emit!/emitted — ALL use hashtable stacks. -;; One world: the aser and CEK share the same scope mechanism. -;; No continuation frame walking — scope-push!/pop!/peek are the primitives. +;; scope/provide/context/emit!/emitted — CEK frame-based. +;; provide/scope push proper CEK frames onto the continuation so that +;; shift/reset can capture and restore them correctly. +;; context/emit!/emitted walk the kont to find the relevant frame. -;; scope: push scope, evaluate body, pop scope. +;; scope: push ScopeAccFrame, evaluate body expressions via continuation. ;; (scope name body...) or (scope name :value v body...) (define step-sf-scope (fn (args env kont) @@ -1458,48 +1459,50 @@ (do (set! val (trampoline (eval-expr (nth rest-args 1) env))) (set! body (slice rest-args 2))) (set! body rest-args)) - (scope-push! name val) - (let ((result nil)) - (for-each (fn (expr) (set! result (trampoline (eval-expr expr env)))) body) - (scope-pop! name) - (make-cek-value result env kont))))) + (if (empty? body) + (make-cek-value nil env kont) + (make-cek-state + (first body) env + (kont-push (make-scope-acc-frame name val (rest body) env) kont)))))) -;; provide: sugar for scope with value. +;; provide: push ProvideFrame, evaluate body expressions via continuation. (define step-sf-provide (fn (args env kont) (let ((name (trampoline (eval-expr (first args) env))) (val (trampoline (eval-expr (nth args 1) env))) (body (slice args 2))) - (scope-push! name val) - (let ((result nil)) - (for-each (fn (expr) (set! result (trampoline (eval-expr expr env)))) body) - (scope-pop! name) - (make-cek-value result env kont))))) + (if (empty? body) + (make-cek-value nil env kont) + (make-cek-state + (first body) env + (kont-push (make-provide-frame name val (rest body) env) kont)))))) -;; context: read from scope stack. +;; context: walk kont for nearest ProvideFrame with matching name. (define step-sf-context (fn (args env kont) (let ((name (trampoline (eval-expr (first args) env))) (default-val (if (>= (len args) 2) (trampoline (eval-expr (nth args 1) env)) nil)) - (val (scope-peek name))) - (make-cek-value (if (nil? val) default-val val) env kont)))) + (frame (kont-find-provide kont name))) + (make-cek-value (if (nil? frame) default-val (get frame "value")) env kont)))) -;; emit!: append to scope accumulator. +;; emit!: walk kont for nearest ScopeAccFrame, append to its emitted list. (define step-sf-emit (fn (args env kont) (let ((name (trampoline (eval-expr (first args) env))) - (val (trampoline (eval-expr (nth args 1) env)))) - (scope-emit! name val) + (val (trampoline (eval-expr (nth args 1) env))) + (frame (kont-find-scope-acc kont name))) + (when frame + (dict-set! frame "emitted" (append (get frame "emitted") (list val)))) (make-cek-value nil env kont)))) -;; emitted: read accumulated scope values. +;; emitted: walk kont for nearest ScopeAccFrame, return its emitted list. (define step-sf-emitted (fn (args env kont) (let ((name (trampoline (eval-expr (first args) env))) - (val (scope-peek name))) - (make-cek-value (if (nil? val) (list) val) env kont)))) + (frame (kont-find-scope-acc kont name))) + (make-cek-value (if (nil? frame) (list) (get frame "emitted")) env kont)))) ;; reset: push ResetFrame, evaluate body (define step-sf-reset diff --git a/spec/tests/test-parser.sx b/spec/tests/test-parser.sx index 7e97e1f..9b61e3b 100644 --- a/spec/tests/test-parser.sx +++ b/spec/tests/test-parser.sx @@ -568,3 +568,43 @@ (assert-equal 3 (len r)) (assert-equal (list "a" (list "b") (list "c")) r)))) ) + +(defsuite "define-as-local" + (deftest "define inside fn creates local, not global" + ;; When define is inside a fn body, recursive calls must each + ;; get their own copy. If define writes to global, recursive + ;; calls overwrite each other. + (let ((result + (let ((counter 0)) + (letrec + ((make-counter (fn () + (define my-val counter) + (set! counter (inc counter)) + my-val))) + (list (make-counter) (make-counter) (make-counter)))))) + (assert-equal (list 0 1 2) result))) + + (deftest "define inside fn with self-recursion via define" + ;; read-list-loop pattern: define a function that calls itself + (let ((result + (let ((items (list))) + (define go (fn (n) + (when (< n 3) + (append! items n) + (go (inc n))))) + (go 0) + items))) + (assert-equal (list 0 1 2) result))) + + (deftest "recursive define inside letrec fn doesn't overwrite" + ;; Each call to make-list creates its own 'loop' local + (let ((make-list (fn (items) + (let ((result (list))) + (define loop (fn (i) + (when (< i (len items)) + (append! result (nth items i)) + (loop (inc i))))) + (loop 0) + result)))) + (assert-equal (list "a" "b") (make-list (list "a" "b"))) + (assert-equal (list 1 2 3) (make-list (list 1 2 3))))))