(** {1 [test_ext] — canonical example VM extension}

    A minimal extension demonstrating the registration pattern from
    [plans/sx-vm-opcode-extension.md].  The opcode IDs (220, 221) sit at
    the top of the extension range, well clear of anything a real
    language port would claim.

    Two operand-less opcodes:

    - [test_ext.OP_TEST_PUSH_42] (220) — pushes the integer 42.
    - [test_ext.OP_TEST_DOUBLE_TOS] (221) — pops the integer on TOS,
      pushes 2× it.

    These are the smallest stack manipulations that prove the extension
    mechanism wires through end-to-end (registry → dispatch → human-
    readable disassembly).  Real ports (Erlang Phase 9, future Haskell
    perf phases) replace this template with their own opcode set.

    Loading: [Test_ext.register ()] adds the extension to
    [Sx_vm_extensions].  Run-time binaries that want the test opcodes
    available call this once at startup.  Unit tests in
    [bin/run_tests.ml] do exactly that. *)

open Sx_types

(** Per-instance state for [test_ext].  Counts how many times the
    handlers ran — purely so the extension has *some* state, exercising
    the [extension_state] machinery. *)
type Sx_vm_extension.extension_state += TestExtState of {
  mutable invocations : int;
}

module M : Sx_vm_extension.EXTENSION = struct
  let name = "test_ext"
  let init () = TestExtState { invocations = 0 }

  let opcodes st =
    let bump () = match st with
      | TestExtState s -> s.invocations <- s.invocations + 1
      | _ -> ()
    in
    [
      (220, "test_ext.OP_TEST_PUSH_42",
       (fun vm _frame -> bump (); Sx_vm.push vm (Integer 42)));

      (221, "test_ext.OP_TEST_DOUBLE_TOS",
       (fun vm _frame ->
         bump ();
         let v = Sx_vm.pop vm in
         match v with
         | Integer n -> Sx_vm.push vm (Integer (n * 2))
         | _ -> raise (Eval_error
             "test_ext.OP_TEST_DOUBLE_TOS: TOS is not an integer")));
    ]
end

(** Register [test_ext] in [Sx_vm_extensions].  Idempotent only by
    failing loudly — calling twice raises [Failure].  Binaries call this
    once at startup; tests may [_reset_for_tests] then re-register. *)
let register () = Sx_vm_extensions.register (module M : Sx_vm_extension.EXTENSION)

(** Read the invocation counter from the live registry state.  Returns
    [None] if [register] hasn't been called yet. *)
let invocation_count () =
  match Sx_vm_extensions.state_of_extension "test_ext" with
  | Some (TestExtState s) -> Some s.invocations
  | _ -> None
