ZymVM API

Self-hosting and nested virtual machines — create isolated VM instances, compile source dynamically, and call functions across VM boundaries.

Overview

The ZymVM API enables self-hosting by allowing Zym scripts to create and manage independent VM instances. Each nested VM runs in complete isolation with its own heap and stack, garbage collector, and full native library access. Values are automatically reconstructed (deep copied) when passed between VMs.

Isolation. Functions, structs, and enums cannot be passed between VMs — they are automatically replaced with null during value reconstruction.

Creating VMs

ZymVM()

Creates a new isolated virtual machine instance. Each VM has its own memory space, includes all native functions, and starts unloaded (no bytecode).

var vm = ZymVM()

Compiling Source

ZymVM can compile Zym source code to bytecode directly. Compilation methods return a Buffer containing the bytecode, which can be saved to disk, passed to load(), or handed to another VM.

vm.compileFile(path)

Compiles a .zym source file to bytecode. Handles preprocessing and module resolution automatically using the file’s directory as the base path.

Returns: A Buffer containing compiled bytecode. Raises a runtime error on failure.

var vm = ZymVM()
var bytecode = vm.compileFile("scripts/plugin.zym")

// Save bytecode to disk
fileWriteBuffer("plugin.zbc", bytecode)

// Or load it into the same VM
vm.load(bytecode)
vm.compileSource(source)

Compiles a source code string to bytecode. No module resolution is performed.

Returns: A Buffer containing compiled bytecode. Raises a runtime error on failure.

var vm = ZymVM()
var src = "func add(a, b) { return a + b }"
var bytecode = vm.compileSource(src)
vm.load(bytecode)

if (vm.call("add", 3, 4)) {
    print(vm.getCallResult())  // 7
}

Loading Bytecode

vm.load(buffer)

Loads compiled bytecode from a Buffer into the VM and executes global initialization.

Returns: true if loaded successfully, false if loading failed.

var bytecode = fileReadBuffer("program.zbc")

if (vm.load(bytecode)) {
    print("Bytecode loaded successfully")
} else {
    print("Failed to load bytecode")
}
vm.loadFile(path)

Compiles a .zym source file and loads it into the VM in one step. Equivalent to compileFile(path) followed by load().

Returns: true if compilation and loading succeeded, false otherwise.

var vm = ZymVM()

if (vm.loadFile("plugins/enemy.zym")) {
    print("Plugin loaded")
    if (vm.hasFunction("init", 0)) {
        vm.call("init")
    }
}
vm.loadSource(source)

Compiles a source string and loads it into the VM in one step. Equivalent to compileSource(source) followed by load().

Returns: true if compilation and loading succeeded, false otherwise.

var vm = ZymVM()

var code = "func greet(name) { return \"Hello, \" + name + \"!\" }"
if (vm.loadSource(code)) {
    if (vm.call("greet", "World")) {
        print(vm.getCallResult())  // Hello, World!
    }
}

Function Queries

vm.hasFunction(name, arity)

Checks if a function with the given name and parameter count exists in the loaded bytecode.

Returns: true if function exists with exact arity match, false otherwise.

if (vm.hasFunction("main", 1)) {
    print("Found main() with 1 parameter")
}

if (vm.hasFunction("init", 0)) {
    print("Found init() with no parameters")
}

Calling Functions

vm.call(name, ...args)

Calls a function in the nested VM with automatic argument reconstruction. Supports 0 to 8 arguments.

Returns: true if executed successfully, false if function doesn’t exist or execution failed.

// Call with no arguments
vm.call("init")

// Call with 1 argument
vm.call("processData", 42)

// Call with multiple arguments
vm.call("complexFunction", "hello", [1, 2, 3], {"key": "value"})

// Call with up to 8 arguments
vm.call("manyParams", 1, 2, 3, 4, 5, 6, 7, 8)

Getting Results

vm.getCallResult()

Retrieves the return value from the last successful function call. Return values are reconstructed from nested to parent VM.

Returns: The reconstructed return value, or null if no call has been made or last call failed.

if (vm.call("calculate", 10, 20)) {
    var result = vm.getCallResult()
    print("Result: %v", result)
}

VM Lifecycle

vm.end()

Manually frees the nested VM and all its resources. The VM becomes unusable after this call. Optional — VMs are automatically cleaned up by GC when no longer referenced.

var vm = ZymVM()
vm.load(bytecode)
vm.call("doWork")
vm.end()  // Explicitly clean up
Automatic cleanup. VMs are automatically freed when they go out of scope and are collected by the garbage collector. Call end() explicitly in long-running programs to free resources immediately.

Value Passing

Values are reconstructed (deep copied) when passing between VMs:

Type Behavior
null Copied directly
boolean Copied directly
number Copied directly
string Reconstructed in target VM
list Recursively reconstructed with all elements
map Recursively reconstructed with all key-value pairs
Buffer Fully reconstructed with data, position, and settings
function Replaced with null
struct Replaced with null
enum Replaced with null

References are automatically dereferenced before reconstruction. Buffers are deep-copied, preserving all data, position, capacity, auto-grow, and endianness settings.

Complete Examples

Compile and Run a Source File

var vm = ZymVM()

if (vm.loadFile("scripts/math.zym")) {
    if (vm.hasFunction("add", 2)) {
        vm.call("add", 10, 20)
        print("10 + 20 = %v", vm.getCallResult())
    }
}

vm.end()

Dynamic Code Evaluation

var vm = ZymVM()

var code = "
func factorial(n) {
    if (n <= 1) return 1
    return n * factorial(n - 1)
}
"

if (vm.loadSource(code)) {
    vm.call("factorial", 10)
    print("10! = %v", vm.getCallResult())
}

vm.end()

Compile to Bytecode and Save

var vm = ZymVM()

// Compile a source file to bytecode
var bytecode = vm.compileFile("app/main.zym")

// Save the bytecode to disk for later use
fileWriteBuffer("app/main.zbc", bytecode)
print("Compiled and saved bytecode")

// Optionally load and run it right away
vm.load(bytecode)
vm.call("main")

vm.end()

Sandbox Testing

func testInSandbox(source) {
    var vm = ZymVM()

    if (!vm.loadSource(source)) {
        vm.end()
        return {"success": false, "error": "Failed to compile/load"}
    }

    if (vm.hasFunction("test", 0)) {
        if (vm.call("test")) {
            var result = vm.getCallResult()
            vm.end()
            return {"success": true, "result": result}
        }
    }

    vm.end()
    return {"success": false, "error": "No test function"}
}

// Use it
var testResult = testInSandbox("func test() { return 42 }")
if (testResult["success"]) {
    print("Test passed: %v", testResult["result"])
} else {
    print("Test failed: %v", testResult["error"])
}

Hot Reloading

var currentVM = null

func loadModule(path) {
    // Clean up old VM
    if (currentVM != null) {
        currentVM.end()
    }

    // Load new version directly from source
    currentVM = ZymVM()
    if (currentVM.loadFile(path)) {
        print("Module loaded successfully")
        return true
    } else {
        print("Failed to load module")
        currentVM.end()
        currentVM = null
        return false
    }
}

// Usage
loadModule("plugin.zym")
if (currentVM != null && currentVM.hasFunction("processData", 1)) {
    currentVM.call("processData", 42)
    print("Result: %v", currentVM.getCallResult())
}

// Reload with new version
loadModule("plugin_v2.zym")
if (currentVM != null && currentVM.hasFunction("processData", 1)) {
    currentVM.call("processData", 42)
    print("New result: %v", currentVM.getCallResult())
}

Error Handling

Safe pattern
var vm = ZymVM()

if (vm.loadFile("script.zym")) {
    if (vm.hasFunction("main", 1)) {
        if (vm.call("main", inputData)) {
            var result = vm.getCallResult()
            print("Success: %v", result)
        } else {
            print("Call failed")
        }
    } else {
        print("Function not found")
    }
} else {
    print("Load failed")
}

vm.end()

API Quick Reference

Method Description
ZymVM() Create a new isolated VM instance
vm.compileFile(path) Compile a .zym file to a bytecode Buffer
vm.compileSource(source) Compile a source string to a bytecode Buffer
vm.load(buffer) Load bytecode from a Buffer
vm.loadFile(path) Compile a .zym file and load it
vm.loadSource(source) Compile a source string and load it
vm.hasFunction(name, arity) Check if a function exists with given arity
vm.call(name, ...args) Call a function with 0–8 arguments
vm.getCallResult() Get the return value from the last call
vm.end() Manually free the VM and its resources