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.
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.ml → Test_ext).