(** VM bytecode benchmark — measures throughput of the VM (compiled bytecode).

    Loads the SX compiler via CEK, then for each test:
      1. Define the function via CEK (as a Lambda).
      2. Trigger JIT compilation via Sx_vm.jit_compile_lambda.
      3. Call the compiled VmClosure repeatedly via Sx_vm.call_closure.

    This measures pure VM execution time on the JIT path. *)

open Sx_types

let load_compiler env globals =
  let compiler_path =
    if Sys.file_exists "lib/compiler.sx" then "lib/compiler.sx"
    else if Sys.file_exists "../../lib/compiler.sx" then "../../lib/compiler.sx"
    else if Sys.file_exists "../../../lib/compiler.sx" then "../../../lib/compiler.sx"
    else failwith "compiler.sx not found"
  in
  let ic = open_in compiler_path in
  let src = really_input_string ic (in_channel_length ic) in
  close_in ic;
  let exprs = Sx_parser.parse_all src in
  List.iter (fun e -> ignore (Sx_ref.eval_expr e (Env env))) exprs;
  let rec sync e =
    Hashtbl.iter (fun id v ->
      let name = Sx_types.unintern id in
      Hashtbl.replace globals name v) e.bindings;
    match e.parent with Some p -> sync p | None -> ()
  in
  sync env

let _make_globals env =
  let g = Hashtbl.create 512 in
  Hashtbl.iter (fun name fn ->
    Hashtbl.replace g name (NativeFn (name, fn))
  ) Sx_primitives.primitives;
  let rec sync e =
    Hashtbl.iter (fun id v ->
      let name = Sx_types.unintern id in
      if not (Hashtbl.mem g name) then Hashtbl.replace g name v) e.bindings;
    match e.parent with Some p -> sync p | None -> ()
  in
  sync env;
  g

let define_fn env globals name params body_src =
  (* Define via CEK so we get a Lambda value with proper closure. *)
  let body_expr = match Sx_parser.parse_all body_src with
    | [e] -> e
    | _ -> failwith "expected one body expression"
  in
  let param_syms = List (List.map (fun p -> Symbol p) params) in
  let define_expr = List [Symbol "define"; Symbol name; List [Symbol "fn"; param_syms; body_expr]] in
  ignore (Sx_ref.eval_expr define_expr (Env env));
  (* Sync env to globals so JIT can resolve free vars. *)
  let rec sync e =
    Hashtbl.iter (fun id v ->
      let n = Sx_types.unintern id in
      Hashtbl.replace globals n v) e.bindings;
    match e.parent with Some p -> sync p | None -> ()
  in
  sync env;
  (* Now find the Lambda and JIT-compile it. *)
  let lam_val = Hashtbl.find globals name in
  match lam_val with
  | Lambda l ->
    (match Sx_vm.jit_compile_lambda l globals with
     | Some cl ->
       l.l_compiled <- Some cl;
       Hashtbl.replace globals name (NativeFn (name, fun args ->
         Sx_vm.call_closure cl args globals));
       cl
     | None ->
       failwith (Printf.sprintf "JIT failed for %s" name))
  | _ -> failwith (Printf.sprintf "%s is not a Lambda after define" name)

let bench_call name cl globals args iters =
  let times = ref [] in
  for _ = 1 to iters do
    Gc.full_major ();
    let t0 = Unix.gettimeofday () in
    let _r = Sx_vm.call_closure cl args globals in
    let t1 = Unix.gettimeofday () in
    times := (t1 -. t0) :: !times
  done;
  let sorted = List.sort compare !times in
  let median = List.nth sorted (iters / 2) in
  let min_t = List.nth sorted 0 in
  let max_t = List.nth sorted (iters - 1) in
  Printf.printf "  %-22s  min=%8.2fms  median=%8.2fms  max=%8.2fms\n%!"
    name (min_t *. 1000.0) (median *. 1000.0) (max_t *. 1000.0);
  median

let () =
  let iters =
    if Array.length Sys.argv > 1
    then int_of_string Sys.argv.(1)
    else 7
  in
  Printf.printf "VM (bytecode/JIT) benchmark (%d runs each, taking median)\n%!" iters;
  Printf.printf "========================================================\n%!";

  let env = Sx_types.make_env () in
  let bind n fn = ignore (Sx_types.env_bind env n (NativeFn (n, fn))) in
  (* Seed env with primitives as NativeFn so CEK lookups work. *)
  Hashtbl.iter (fun name fn ->
    Hashtbl.replace env.bindings (Sx_types.intern name) (NativeFn (name, fn))
  ) Sx_primitives.primitives;
  (* Helpers the SX compiler relies on but aren't kernel primitives. *)
  bind "symbol-name" (fun args -> match args with
    | [Symbol s] -> String s | _ -> raise (Eval_error "symbol-name"));
  bind "keyword-name" (fun args -> match args with
    | [Keyword k] -> String k | _ -> raise (Eval_error "keyword-name"));
  bind "make-symbol" (fun args -> match args with
    | [String s] -> Symbol s
    | [v] -> Symbol (Sx_types.value_to_string v)
    | _ -> raise (Eval_error "make-symbol"));
  bind "sx-serialize" (fun args -> match args with
    | [v] -> String (Sx_types.inspect v)
    | _ -> raise (Eval_error "sx-serialize"));
  let globals = Hashtbl.create 1024 in
  Hashtbl.iter (fun name fn ->
    Hashtbl.replace globals name (NativeFn (name, fn))
  ) Sx_primitives.primitives;
  Printf.printf "Loading compiler.sx ... %!";
  let t0 = Unix.gettimeofday () in
  load_compiler env globals;
  Printf.printf "%.0fms\n%!" ((Unix.gettimeofday () -. t0) *. 1000.0);

  (* fib(22) — recursive call benchmark *)
  let fib_cl = define_fn env globals "fib" ["n"]
    "(if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))" in
  let _ = bench_call "fib(22)" fib_cl globals [Number 22.0] iters in

  (* tight loop *)
  let loop_cl = define_fn env globals "loop" ["n"; "acc"]
    "(if (= n 0) acc (loop (- n 1) (+ acc 1)))" in
  let _ = bench_call "loop(200000)" loop_cl globals [Number 200000.0; Number 0.0] iters in

  (* sum-to *)
  let sum_cl = define_fn env globals "sum_to" ["n"; "acc"]
    "(if (= n 0) acc (sum_to (- n 1) (+ acc n)))" in
  let _ = bench_call "sum-to(50000)" sum_cl globals [Number 50000.0; Number 0.0] iters in

  (* count-lt: comparison-heavy *)
  let cnt_cl = define_fn env globals "count_lt" ["n"; "acc"]
    "(if (= n 0) acc (count_lt (- n 1) (if (< n 10000) (+ acc 1) acc)))" in
  let _ = bench_call "count-lt(20000)" cnt_cl globals [Number 20000.0; Number 0.0] iters in

  (* count-eq: equality-heavy on multiples of 7 *)
  let eq_cl = define_fn env globals "count_eq" ["n"; "acc"]
    "(if (= n 0) acc (count_eq (- n 1) (if (= 0 (- n (* 7 (/ n 7)))) (+ acc 1) acc)))" in
  let _ = bench_call "count-eq(20000)" eq_cl globals [Number 20000.0; Number 0.0] iters in

  Printf.printf "\nDone.\n%!"
