Files
rose-ash/hosts/ocaml/lib/extensions
giles f3192f7fda vm-ext: phase D — extensions/ subtree + test_ext + opcode_name lookup
lib/extensions/ becomes the new home for VM extensions, wired in via
(include_subdirs unqualified). README documents the registration
pattern, opcode-ID range conventions (200-209 guest_vm, 210-219
inline test, 220-229 test_ext, 230-247 ports), and naming rules.

extensions/test_ext.ml is the canonical worked example — two
operand-less opcodes (220 push 42, 221 double TOS) carrying a per-
extension state slot (TestExtState invocation counter). Test_ext.register
called from run_tests.ml at the start of the Phase D suite, on top of
the inline test_reg from earlier suites (disjoint opcode IDs).

Sx_vm.opcode_name now consults extension_opcode_name_ref (forward ref
in the same style as extension_dispatch_ref), so disassemble shows
extension opcodes by name instead of UNKNOWN_n. Registry maintains
name_of_id_table and installs the lookup at module init.

Tests: 5 new foundation cases — primitive resolves test_ext name,
end-to-end bytecode (push + double + return → 84), disassemble shows
"test_ext.OP_TEST_PUSH_42" / "test_ext.OP_TEST_DOUBLE_TOS",
unregistered ext opcodes still fall back to UNKNOWN_n, invocation
counter records the two dispatches. +5 pass vs Phase C baseline, no
regressions across 11 conformance suites.
2026-05-15 01:05:30 +00:00
..

SX VM extensions

Each *.ml file here is a VM extension — a first-class OCaml module that registers specialized bytecode opcodes with Sx_vm_extensions. See plans/sx-vm-opcode-extension.md for the design.

Pattern

(* lib/extensions/myport.ml *)
open Sx_types

type Sx_vm_extension.extension_state += MyportState of { ... }

module M : Sx_vm_extension.EXTENSION = struct
  let name = "myport"
  let init () = MyportState { ... }
  let opcodes _st = [
    (id, "myport.OP_NAME", handler);
    ...
  ]
end

let register () = Sx_vm_extensions.register (module M)

Then call Myport.register () once at startup from any binary that should have the extension loaded.

Opcode-ID allocation

Range 200-247 (per Sx_vm_extensions.extension_min / extension_max). Conventions:

Range Use
200-209 reserved for lib/guest/vm/ shared opcodes (chiselled out on 2nd use)
210-219 inline test extensions defined in bin/run_tests.ml
220-229 this directory's test_ext (the canonical template)
230-247 first-come-first-served by language ports (Erlang first)

When a port claims a contiguous block, document it in the table above. The registry rejects collisions at startup with a loud error — there is no silent shadowing.

Naming

Always prefix opcode names with the extension name plus a dot: myport.OP_<NAME>. The prefix is a hard convention so that multiple extensions can share the global opcode-name namespace cleanly.

State

extension_state is an extensible variant. Add your case (e.g. MyportState of { ... }) at the top of your file, return it from init, and pattern-match it inside your handlers. Other extensions cannot see your state — the variant case is private to your module.

Testing

test_ext.ml is the canonical worked example. bin/run_tests.ml calls Test_ext.register (), then drives bytecode that exercises the opcodes end-to-end (push, double, dispatch, disassemble, invocation counter). Mirror this shape when adding a real port's extension.

Build wiring

lib/dune has (include_subdirs unqualified), so any .ml you drop in here is automatically part of the sx library. Module name follows the filename verbatim (test_ext.mlTest_ext).