Persistent Lisp image for sx_eval: smart file reload + IO tracing
sx_eval now accepts files (smart-loaded by mtime — unchanged files skip), trace_io (harness-wrapped IO capture), mock (evaluated platform overrides), and setup params. Definitions survive between calls. sx_harness_eval also uses smart loading. sx_write_file can create new files. New lib/hyperscript/debug.sx: mock DOM platform for instant hyperscript testing — compile and execute HS expressions against simulated elements, see every DOM mutation and wait in the IO trace. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,34 @@ let load_sx_file e path =
|
|||||||
ignore (Sx_ref.eval_expr expr (Env e))
|
ignore (Sx_ref.eval_expr expr (Env e))
|
||||||
) exprs
|
) exprs
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* File mtime cache — like a running Lisp image, only reload changed *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
let file_mtimes : (string, float) Hashtbl.t = Hashtbl.create 32
|
||||||
|
|
||||||
|
let smart_load_file e path =
|
||||||
|
let abs = if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path else path in
|
||||||
|
let cur_mtime = (Unix.stat abs).Unix.st_mtime in
|
||||||
|
let cached = try Some (Hashtbl.find file_mtimes abs) with Not_found -> None in
|
||||||
|
match cached with
|
||||||
|
| Some prev when prev >= cur_mtime -> false (* unchanged *)
|
||||||
|
| _ ->
|
||||||
|
load_sx_file e abs;
|
||||||
|
Hashtbl.replace file_mtimes abs cur_mtime;
|
||||||
|
true (* loaded *)
|
||||||
|
|
||||||
|
let smart_load_files e paths =
|
||||||
|
let loaded = ref [] in
|
||||||
|
List.iter (fun path ->
|
||||||
|
try
|
||||||
|
if smart_load_file e path then
|
||||||
|
loaded := path :: !loaded
|
||||||
|
with exn ->
|
||||||
|
loaded := (Printf.sprintf "%s (error: %s)" path (Printexc.to_string exn)) :: !loaded
|
||||||
|
) paths;
|
||||||
|
List.rev !loaded
|
||||||
|
|
||||||
(* JIT infrastructure — shared VM globals table, kept in sync via env_bind hook *)
|
(* JIT infrastructure — shared VM globals table, kept in sync via env_bind hook *)
|
||||||
let _mcp_vm_globals : (string, value) Hashtbl.t = Hashtbl.create 2048
|
let _mcp_vm_globals : (string, value) Hashtbl.t = Hashtbl.create 2048
|
||||||
let _jit_warned : (string, bool) Hashtbl.t = Hashtbl.create 32
|
let _jit_warned : (string, bool) Hashtbl.t = Hashtbl.create 32
|
||||||
@@ -1633,12 +1661,12 @@ let handle_sx_harness_eval args =
|
|||||||
List.map (fun j -> Yojson.Safe.Util.to_string j) items
|
List.map (fun j -> Yojson.Safe.Util.to_string j) items
|
||||||
| _ -> match file with Some f -> [f] | None -> []
|
| _ -> match file with Some f -> [f] | None -> []
|
||||||
in
|
in
|
||||||
(* Load each file *)
|
(* Smart-load files — only re-evaluate if source changed *)
|
||||||
List.iter (fun f ->
|
let reloaded = smart_load_files e all_files in
|
||||||
try load_sx_file e f
|
List.iter (fun r ->
|
||||||
with exn ->
|
if String.contains r '(' then (* error entries contain parens *)
|
||||||
warnings := Printf.sprintf "Warning: %s: %s" f (Printexc.to_string exn) :: !warnings
|
warnings := Printf.sprintf "Warning: %s" r :: !warnings
|
||||||
) all_files;
|
) reloaded;
|
||||||
(* Run setup expression if provided *)
|
(* Run setup expression if provided *)
|
||||||
(match setup_str with
|
(match setup_str with
|
||||||
| Some s ->
|
| Some s ->
|
||||||
@@ -1649,11 +1677,14 @@ let handle_sx_harness_eval args =
|
|||||||
warnings := Printf.sprintf "Setup error: %s" (Printexc.to_string exn) :: !warnings
|
warnings := Printf.sprintf "Setup error: %s" (Printexc.to_string exn) :: !warnings
|
||||||
) setup_exprs
|
) setup_exprs
|
||||||
| None -> ());
|
| None -> ());
|
||||||
(* Create harness with optional mock overrides *)
|
(* Create harness with optional mock overrides — evaluate so fn exprs become lambdas *)
|
||||||
let mock_arg = match mock_str with
|
let mock_arg = match mock_str with
|
||||||
| Some s ->
|
| Some s ->
|
||||||
let parsed = Sx_parser.parse_all s in
|
let parsed = Sx_parser.parse_all s in
|
||||||
if parsed <> [] then List [Keyword "platform"; List.hd parsed] else List []
|
if parsed <> [] then
|
||||||
|
let evaluated = Sx_ref.eval_expr (List.hd parsed) (Env e) in
|
||||||
|
List [Keyword "platform"; evaluated]
|
||||||
|
else List []
|
||||||
| None -> List []
|
| None -> List []
|
||||||
in
|
in
|
||||||
let session = Sx_ref.cek_call (env_get e "make-harness") mock_arg in
|
let session = Sx_ref.cek_call (env_get e "make-harness") mock_arg in
|
||||||
@@ -1679,11 +1710,13 @@ let handle_sx_harness_eval args =
|
|||||||
let warn_str = if !warnings = [] then "" else
|
let warn_str = if !warnings = [] then "" else
|
||||||
"\n\nWarnings:\n" ^ String.concat "\n" (List.rev !warnings)
|
"\n\nWarnings:\n" ^ String.concat "\n" (List.rev !warnings)
|
||||||
in
|
in
|
||||||
text_result (Printf.sprintf "Result: %s%s%s" (Sx_types.inspect result) log_str warn_str)
|
let reload_str = if reloaded = [] then "" else
|
||||||
|
"\n\nReloaded: " ^ String.concat ", " (List.map Filename.basename reloaded) in
|
||||||
|
text_result (Printf.sprintf "Result: %s%s%s%s" (Sx_types.inspect result) log_str reload_str warn_str)
|
||||||
|
|
||||||
let handle_sx_write_file args =
|
let handle_sx_write_file args =
|
||||||
let open Yojson.Safe.Util in
|
let open Yojson.Safe.Util in
|
||||||
let file = require_file args "file" in
|
let file = args |> member "file" |> to_string in
|
||||||
let source = args |> member "source" |> to_string in
|
let source = args |> member "source" |> to_string in
|
||||||
(* Validate by parsing as CST — preserves comments and formatting *)
|
(* Validate by parsing as CST — preserves comments and formatting *)
|
||||||
(try
|
(try
|
||||||
@@ -1880,12 +1913,62 @@ let handle_sx_comp_usage args =
|
|||||||
let handle_sx_eval args =
|
let handle_sx_eval args =
|
||||||
let open Yojson.Safe.Util in
|
let open Yojson.Safe.Util in
|
||||||
let expr_str = args |> member "expr" |> to_string in
|
let expr_str = args |> member "expr" |> to_string in
|
||||||
let exprs = Sx_parser.parse_all expr_str in
|
|
||||||
let e = !env in
|
let e = !env in
|
||||||
|
(* Smart-load files — only re-evaluate if source changed *)
|
||||||
|
let files_json = try args |> member "files" with _ -> `Null in
|
||||||
|
let file = try args |> member "file" |> to_string_option with _ -> None in
|
||||||
|
let all_files = match files_json with
|
||||||
|
| `List items -> List.map Yojson.Safe.Util.to_string items
|
||||||
|
| _ -> match file with Some f -> [f] | None -> []
|
||||||
|
in
|
||||||
|
let reloaded = smart_load_files e all_files in
|
||||||
|
(* Optional IO tracing via harness *)
|
||||||
|
let trace_io = try args |> member "trace_io" |> to_bool with _ -> false in
|
||||||
|
let session = if trace_io then begin
|
||||||
|
let mock_str = try args |> member "mock" |> to_string_option with _ -> None in
|
||||||
|
let mock_arg = match mock_str with
|
||||||
|
| Some s ->
|
||||||
|
let parsed = Sx_parser.parse_all s in
|
||||||
|
if parsed <> [] then
|
||||||
|
let evaluated = Sx_ref.eval_expr (List.hd parsed) (Env e) in
|
||||||
|
List [Keyword "platform"; evaluated]
|
||||||
|
else List []
|
||||||
|
| None -> List []
|
||||||
|
in
|
||||||
|
let s = Sx_ref.cek_call (env_get e "make-harness") mock_arg in
|
||||||
|
ignore (call_sx "install-interceptors" [s; Env e]);
|
||||||
|
Some s
|
||||||
|
end else None in
|
||||||
|
(* Run setup if provided *)
|
||||||
|
let setup_str = try args |> member "setup" |> to_string_option with _ -> None in
|
||||||
|
(match setup_str with
|
||||||
|
| Some s ->
|
||||||
|
List.iter (fun expr -> ignore (Sx_ref.eval_expr expr (Env e))) (Sx_parser.parse_all s)
|
||||||
|
| None -> ());
|
||||||
|
(* Evaluate *)
|
||||||
|
let exprs = Sx_parser.parse_all expr_str in
|
||||||
let result = List.fold_left (fun _acc expr ->
|
let result = List.fold_left (fun _acc expr ->
|
||||||
Sx_ref.eval_expr expr (Env e)
|
try Sx_ref.eval_expr expr (Env e)
|
||||||
|
with exn -> String (Printf.sprintf "Error: %s" (Printexc.to_string exn))
|
||||||
) Nil exprs in
|
) Nil exprs in
|
||||||
text_result (Sx_runtime.value_to_str result)
|
(* Format output *)
|
||||||
|
let result_str = Sx_runtime.value_to_str result in
|
||||||
|
let reload_str = if reloaded = [] then "" else
|
||||||
|
"\n\nReloaded: " ^ String.concat ", " (List.map Filename.basename reloaded) in
|
||||||
|
let io_str = match session with
|
||||||
|
| Some s ->
|
||||||
|
let log = call_sx "harness-log" [s] in
|
||||||
|
(match log with
|
||||||
|
| List items | ListRef { contents = items } when items <> [] ->
|
||||||
|
"\n\nIO trace:\n" ^ String.concat "\n" (List.map (fun entry ->
|
||||||
|
let op = value_to_string (call_sx "get" [entry; String "op"]) in
|
||||||
|
let args_val = call_sx "get" [entry; String "args"] in
|
||||||
|
Printf.sprintf " %s(%s)" op (Sx_types.inspect args_val)
|
||||||
|
) items)
|
||||||
|
| _ -> "\n\n(no IO calls)")
|
||||||
|
| None -> ""
|
||||||
|
in
|
||||||
|
text_result (result_str ^ reload_str ^ io_str)
|
||||||
|
|
||||||
let handle_sx_guard args =
|
let handle_sx_guard args =
|
||||||
let open Yojson.Safe.Util in
|
let open Yojson.Safe.Util in
|
||||||
@@ -2556,8 +2639,14 @@ let tool_definitions = `List [
|
|||||||
[file_prop; path_prop] ["file"; "path"];
|
[file_prop; path_prop] ["file"; "path"];
|
||||||
tool "sx_wrap_node" "Wrap node in a new form. Use _ as placeholder, e.g. \"(when cond _)\"."
|
tool "sx_wrap_node" "Wrap node in a new form. Use _ as placeholder, e.g. \"(when cond _)\"."
|
||||||
[file_prop; path_prop; ("wrapper", `Assoc [("type", `String "string"); ("description", `String "Wrapper with _ placeholder")])] ["file"; "path"; "wrapper"];
|
[file_prop; path_prop; ("wrapper", `Assoc [("type", `String "string"); ("description", `String "Wrapper with _ placeholder")])] ["file"; "path"; "wrapper"];
|
||||||
tool "sx_eval" "Evaluate an SX expression. Environment has parser + tree-tools + primitives."
|
tool "sx_eval" "Evaluate SX in the persistent image. Definitions survive between calls. Files are smart-loaded (only re-evaluated if source changed on disk). With trace_io=true, wraps in harness to capture all IO calls."
|
||||||
[("expr", `Assoc [("type", `String "string"); ("description", `String "SX expression to evaluate")])] ["expr"];
|
[("expr", `Assoc [("type", `String "string"); ("description", `String "SX expression to evaluate")]);
|
||||||
|
("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load")]);
|
||||||
|
("files", `Assoc [("type", `String "array"); ("items", `Assoc [("type", `String "string")]); ("description", `String "Multiple .sx files to load in order (smart reload — skips unchanged)")]);
|
||||||
|
("setup", `Assoc [("type", `String "string"); ("description", `String "SX expression to run before main eval")]);
|
||||||
|
("trace_io", `Assoc [("type", `String "boolean"); ("description", `String "Wrap in test harness to capture IO trace (default: false)")]);
|
||||||
|
("mock", `Assoc [("type", `String "string"); ("description", `String "Mock platform overrides as SX dict (requires trace_io)")])]
|
||||||
|
["expr"];
|
||||||
tool "sx_guard" "Evaluate with error recovery. Catches errors, shows component trace, and continues. Returns result + any conditions signaled."
|
tool "sx_guard" "Evaluate with error recovery. Catches errors, shows component trace, and continues. Returns result + any conditions signaled."
|
||||||
[("expr", `Assoc [("type", `String "string"); ("description", `String "SX expression to evaluate with error recovery")]);
|
[("expr", `Assoc [("type", `String "string"); ("description", `String "SX expression to evaluate with error recovery")]);
|
||||||
("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")])] ["expr"];
|
("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")])] ["expr"];
|
||||||
|
|||||||
38
lib/hyperscript/debug.sx
Normal file
38
lib/hyperscript/debug.sx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
;; Hyperscript debug harness — mock DOM for instant testing
|
||||||
|
;;
|
||||||
|
;; Load once into the image, then repeatedly call hs-run.
|
||||||
|
;; All DOM ops are intercepted and logged via the test harness.
|
||||||
|
|
||||||
|
;; ── Mock element ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-mock-element
|
||||||
|
(fn
|
||||||
|
(tag id classes)
|
||||||
|
(let
|
||||||
|
((cls-set (reduce (fn (d c) (dict-set d c true)) {} classes)))
|
||||||
|
{:children () :_hs-activated true :tag tag :classes cls-set :text "" :id id :attrs {}})))
|
||||||
|
|
||||||
|
;; ── Mock platform ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
(define hs-mock-platform {:hs-wait (fn (ms) nil) :hs-wait-for (fn (target event) nil) :dom-get-attr (fn (el attr) (get (get el "attrs") attr)) :dom-has-class? (fn (el cls) (dict-has? (get el "classes") cls)) :dom-set-text (fn (el text) (dict-set! el "text" text) nil) :hs-settle (fn (el) nil) :dom-add-class (fn (el cls) (dict-set! (get el "classes") cls true) nil) :dom-query (fn (sel) nil) :dom-remove-class (fn (el cls) (dict-delete! (get el "classes") cls) nil) :dom-listen (fn (target event-name handler) (handler {:target target :type event-name})) :dom-set-attr (fn (el attr val) (dict-set! (get el "attrs") attr val) nil) :dom-query-all (fn (sel) ())})
|
||||||
|
|
||||||
|
;; ── Convenience runner ──────────────────────────────────────────
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-run
|
||||||
|
(fn
|
||||||
|
(src)
|
||||||
|
(let
|
||||||
|
((me (hs-mock-element "div" "test" ()))
|
||||||
|
(sx (hs-to-sx-from-source src)))
|
||||||
|
(let
|
||||||
|
((handler (eval-expr (list (quote fn) (quote (me)) (list (quote let) (quote ((it nil) (event {:target me :type "click"}))) sx)))))
|
||||||
|
(handler me)
|
||||||
|
me))))
|
||||||
|
|
||||||
|
;; ── Element inspection ──────────────────────────────────────────
|
||||||
|
|
||||||
|
(define hs-classes (fn (el) (keys (get el "classes"))))
|
||||||
|
|
||||||
|
(define hs-has-class? (fn (el cls) (dict-has? (get el "classes") cls)))
|
||||||
Reference in New Issue
Block a user