(** {1 [erlang_ext] — Erlang-on-SX VM opcode extension (Phase 9h)}

    Registers the Erlang opcode namespace in [Sx_vm_extensions] so that
    [extension-opcode-id "erlang.OP_*"] resolves to a stable id. The SX
    stub dispatcher in [lib/erlang/vm/dispatcher.sx] consults these ids
    (Phase 9i) and falls back to its own local ids when the host
    extension is absent.

    Opcode ids occupy 222-239 in the extension partition (200-247).
    222+ is chosen to clear the test extensions' reserved ids
    (test_reg 210/211, test_ext 220/221) so all three coexist in
    run_tests; production sx_server only registers this one. Names
    mirror the SX stub dispatcher exactly:

    - 222 erlang.OP_PATTERN_TUPLE      - 231 erlang.OP_BIF_HD
    - 223 erlang.OP_PATTERN_LIST       - 232 erlang.OP_BIF_TL
    - 224 erlang.OP_PATTERN_BINARY     - 233 erlang.OP_BIF_ELEMENT
    - 225 erlang.OP_PERFORM            - 234 erlang.OP_BIF_TUPLE_SIZE
    - 226 erlang.OP_HANDLE             - 235 erlang.OP_BIF_LISTS_REVERSE
    - 227 erlang.OP_RECEIVE_SCAN       - 236 erlang.OP_BIF_IS_INTEGER
    - 228 erlang.OP_SPAWN              - 237 erlang.OP_BIF_IS_ATOM
    - 229 erlang.OP_SEND               - 238 erlang.OP_BIF_IS_LIST
    - 230 erlang.OP_BIF_LENGTH         - 239 erlang.OP_BIF_IS_TUPLE

    {2 Handler status}

    The bytecode compiler does not yet emit these opcodes — Erlang
    programs run through the general CEK path and the working
    specialization path is the SX stub dispatcher. So every handler
    here raises a descriptive [Eval_error] rather than silently
    corrupting the VM stack. This keeps the extension honest: the
    namespace is registered and disassembles by name, [extension-opcode-id]
    works, but actually dispatching an opcode (which only happens once a
    future phase teaches the compiler to emit them) fails loudly with a
    pointer to the phase that will wire it. Real stack-machine handlers
    land alongside compiler emission in a later phase. *)

open Sx_types

(** Per-instance state: invocation counter, purely to exercise the
    [extension_state] machinery (mirrors [test_ext]). *)
type Sx_vm_extension.extension_state += ErlangExtState of {
  mutable dispatched : int;
}

let not_wired name =
  raise (Eval_error
    (Printf.sprintf
       "%s: bytecode emission not yet wired (Phase 9j) — \
        Erlang runs via CEK; specialization path is the SX stub \
        dispatcher in lib/erlang/vm/dispatcher.sx"
       name))

module M : Sx_vm_extension.EXTENSION = struct
  let name = "erlang"
  let init () = ErlangExtState { dispatched = 0 }

  let opcodes st =
    let bump () = match st with
      | ErlangExtState s -> s.dispatched <- s.dispatched + 1
      | _ -> ()
    in
    let op id nm =
      (id, nm, (fun (_vm : Sx_vm.vm) (_frame : Sx_vm.frame) ->
        bump (); not_wired nm))
    in
    (* Phase 10b vertical slice: one REAL register-machine handler.
       erlang.OP_BIF_LENGTH (230) — pops an Erlang list off the VM
       stack and pushes its length. Proves the full path works:
       extension-opcode-id -> bytecode -> Sx_vm dispatch fallthrough
       -> this handler -> correct stack result. The remaining 17
       opcodes still raise not_wired until their handlers + compiler
       emission land. Erlang lists are tagged dicts:
         nil  = {"tag" -> String "nil"}
         cons = {"tag" -> String "cons"; "head" -> v; "tail" -> v} *)
    let er_tag d =
      match Hashtbl.find_opt d "tag" with
      | Some (String s) -> s | _ -> ""
    in
    let op_bif_length =
      (230, "erlang.OP_BIF_LENGTH",
       (fun (vm : Sx_vm.vm) (_frame : Sx_vm.frame) ->
         bump ();
         let v = Sx_vm.pop vm in
         let rec walk acc node =
           match node with
           | Dict d ->
             (match er_tag d with
              | "nil" -> acc
              | "cons" ->
                (match Hashtbl.find_opt d "tail" with
                 | Some t -> walk (acc + 1) t
                 | None -> raise (Eval_error
                     "erlang.OP_BIF_LENGTH: cons cell without :tail"))
              | _ -> raise (Eval_error
                  "erlang.OP_BIF_LENGTH: not a proper list"))
           | _ -> raise (Eval_error
               "erlang.OP_BIF_LENGTH: not a proper list")
         in
         Sx_vm.push vm (Integer (walk 0 v))))
    in
    (* Phase 10b — simple hot-BIF handlers. Erlang bool is the atom
       {"tag"->"atom"; "name"->"true"|"false"}; mk_atom builds it. *)
    let mk_atom nm =
      let h = Hashtbl.create 2 in
      Hashtbl.replace h "tag" (String "atom");
      Hashtbl.replace h "name" (String nm);
      Dict h
    in
    let er_bool b = mk_atom (if b then "true" else "false") in
    let is_tag v t = match v with
      | Dict d -> er_tag d = t
      | _ -> false
    in
    let op_bif_hd =
      (231, "erlang.OP_BIF_HD",
       (fun (vm : Sx_vm.vm) _f ->
         bump ();
         match Sx_vm.pop vm with
         | Dict d when er_tag d = "cons" ->
           (match Hashtbl.find_opt d "head" with
            | Some h -> Sx_vm.push vm h
            | None -> raise (Eval_error "erlang.OP_BIF_HD: cons without :head"))
         | _ -> raise (Eval_error "erlang.OP_BIF_HD: not a cons")))
    in
    let op_bif_tl =
      (232, "erlang.OP_BIF_TL",
       (fun (vm : Sx_vm.vm) _f ->
         bump ();
         match Sx_vm.pop vm with
         | Dict d when er_tag d = "cons" ->
           (match Hashtbl.find_opt d "tail" with
            | Some t -> Sx_vm.push vm t
            | None -> raise (Eval_error "erlang.OP_BIF_TL: cons without :tail"))
         | _ -> raise (Eval_error "erlang.OP_BIF_TL: not a cons")))
    in
    let op_bif_tuple_size =
      (234, "erlang.OP_BIF_TUPLE_SIZE",
       (fun (vm : Sx_vm.vm) _f ->
         bump ();
         match Sx_vm.pop vm with
         | Dict d when er_tag d = "tuple" ->
           let n = match Hashtbl.find_opt d "elements" with
             | Some (List es) -> List.length es
             | Some (ListRef r) -> List.length !r
             | _ -> raise (Eval_error
                 "erlang.OP_BIF_TUPLE_SIZE: tuple without :elements")
           in
           Sx_vm.push vm (Integer n)
         | _ -> raise (Eval_error "erlang.OP_BIF_TUPLE_SIZE: not a tuple")))
    in
    let op_bif_is_integer =
      (236, "erlang.OP_BIF_IS_INTEGER",
       (fun (vm : Sx_vm.vm) _f ->
         bump ();
         let v = Sx_vm.pop vm in
         Sx_vm.push vm (er_bool (match v with Integer _ -> true | _ -> false))))
    in
    let op_bif_is_atom =
      (237, "erlang.OP_BIF_IS_ATOM",
       (fun (vm : Sx_vm.vm) _f ->
         bump ();
         let v = Sx_vm.pop vm in
         Sx_vm.push vm (er_bool (is_tag v "atom"))))
    in
    let op_bif_is_list =
      (238, "erlang.OP_BIF_IS_LIST",
       (fun (vm : Sx_vm.vm) _f ->
         bump ();
         let v = Sx_vm.pop vm in
         Sx_vm.push vm (er_bool (is_tag v "cons" || is_tag v "nil"))))
    in
    let op_bif_is_tuple =
      (239, "erlang.OP_BIF_IS_TUPLE",
       (fun (vm : Sx_vm.vm) _f ->
         bump ();
         let v = Sx_vm.pop vm in
         Sx_vm.push vm (er_bool (is_tag v "tuple"))))
    in
    (* element/2 and lists:reverse/1 — pure stack transforms (no
       bytecode operands). Calling convention: args pushed left→right,
       so element/2 stack is [.. Index Tuple] (Tuple on top). Erlang
       element/2 is 1-indexed. *)
    let op_bif_element =
      (233, "erlang.OP_BIF_ELEMENT",
       (fun (vm : Sx_vm.vm) _f ->
         bump ();
         let tup = Sx_vm.pop vm in
         let idx = Sx_vm.pop vm in
         match tup, idx with
         | Dict d, Integer i when er_tag d = "tuple" ->
           let es = match Hashtbl.find_opt d "elements" with
             | Some (List es) -> es
             | Some (ListRef r) -> !r
             | _ -> raise (Eval_error
                 "erlang.OP_BIF_ELEMENT: tuple without :elements")
           in
           let n = List.length es in
           if i < 1 || i > n then
             raise (Eval_error
               (Printf.sprintf
                  "erlang.OP_BIF_ELEMENT: index %d out of range 1..%d" i n))
           else
             Sx_vm.push vm (List.nth es (i - 1))
         | _, Integer _ ->
           raise (Eval_error "erlang.OP_BIF_ELEMENT: 2nd arg not a tuple")
         | _ ->
           raise (Eval_error "erlang.OP_BIF_ELEMENT: 1st arg not an integer")))
    in
    let op_bif_lists_reverse =
      (235, "erlang.OP_BIF_LISTS_REVERSE",
       (fun (vm : Sx_vm.vm) _f ->
         bump ();
         let v = Sx_vm.pop vm in
         let mk_nil () =
           let h = Hashtbl.create 1 in
           Hashtbl.replace h "tag" (String "nil"); Dict h in
         let mk_cons hd tl =
           let h = Hashtbl.create 3 in
           Hashtbl.replace h "tag" (String "cons");
           Hashtbl.replace h "head" hd;
           Hashtbl.replace h "tail" tl;
           Dict h in
         let rec rev acc node =
           match node with
           | Dict d ->
             (match er_tag d with
              | "nil" -> acc
              | "cons" ->
                let hd = match Hashtbl.find_opt d "head" with
                  | Some x -> x
                  | None -> raise (Eval_error
                      "erlang.OP_BIF_LISTS_REVERSE: cons without :head") in
                let tl = match Hashtbl.find_opt d "tail" with
                  | Some x -> x
                  | None -> raise (Eval_error
                      "erlang.OP_BIF_LISTS_REVERSE: cons without :tail") in
                rev (mk_cons hd acc) tl
              | _ -> raise (Eval_error
                  "erlang.OP_BIF_LISTS_REVERSE: not a proper list"))
           | _ -> raise (Eval_error
               "erlang.OP_BIF_LISTS_REVERSE: not a proper list")
         in
         Sx_vm.push vm (rev (mk_nil ()) v)))
    in
    [
      op 222 "erlang.OP_PATTERN_TUPLE";
      op 223 "erlang.OP_PATTERN_LIST";
      op 224 "erlang.OP_PATTERN_BINARY";
      op 225 "erlang.OP_PERFORM";
      op 226 "erlang.OP_HANDLE";
      op 227 "erlang.OP_RECEIVE_SCAN";
      op 228 "erlang.OP_SPAWN";
      op 229 "erlang.OP_SEND";
      op_bif_length;
      op_bif_hd;
      op_bif_tl;
      op_bif_element;
      op_bif_tuple_size;
      op_bif_lists_reverse;
      op_bif_is_integer;
      op_bif_is_atom;
      op_bif_is_list;
      op_bif_is_tuple;
    ]
end

(** Register [erlang] in [Sx_vm_extensions]. Idempotent only by failing
    loudly — calling twice raises [Failure]. sx_server calls this once
    at startup. *)
let register () = Sx_vm_extensions.register (module M : Sx_vm_extension.EXTENSION)

(** Read the dispatch counter from the live registry state. [None] if
    [register] hasn't run. *)
let dispatch_count () =
  match Sx_vm_extensions.state_of_extension "erlang" with
  | Some (ErlangExtState s) -> Some s.dispatched
  | _ -> None
