Embedding Guide

How to integrate the Zym scripting language into your C/C++ projects — VM lifecycle, native functions, value system, and two-way FFI.

Critical: Zym has no built-in I/O. All printing, file access, and networking must be provided by the embedder through native functions. The print() function used in examples below must be registered by you before scripts can use it.

Important: No Built-In I/O

Zym's core provides no print, input, or file I/O functions. This gives you complete control over how scripts interact with the outside world. You must register these functions as natives.

Minimal print() Implementation

Here's a basic single-argument print to get started:

C
#include <stdio.h>
#include <math.h>
#include "zym/zym.h"

ZymValue native_print(ZymVM* vm, ZymValue value) {
    if (zym_isString(value)) {
        printf("%s", zym_asCString(value));
    } else if (zym_isNumber(value)) {
        double num = zym_asNumber(value);
        if (isfinite(num) && num == (double)(long long)num
            && num >= -1e15 && num <= 1e15) {
            printf("%.0f", num);  // Integer-like
        } else {
            printf("%g", num);     // Float
        }
    } else if (zym_isBool(value)) {
        printf("%s", zym_asBool(value) ? "true" : "false");
    } else if (zym_isNull(value)) {
        printf("null");
    } else {
        // For lists, maps, structs — use VM's formatter
        zym_printValue(vm, value);
    }
    printf("\n");
    return zym_newNull();
}

void setupPrint(ZymVM* vm) {
    zym_defineNative(vm, "print(value)", native_print);
}

Usage from Zym:

Zym
print("Hello, world!")
print(42)
print([1, 2, 3])

For production use, you might want to register multiple arities to support formatted output (e.g., print("Name: %s, Age: %n", name, age)) with format specifiers like %v (value), %s (string), %n (number), %b (boolean).

VM Lifecycle

Create & Destroy

C
#include "zym/zym.h"

// Create a new VM instance
ZymVM* vm = zym_newVM();

// Register your native functions here...

// Clean up when done
zym_freeVM(vm);

Running Scripts

C
// Run from a string
ZymResult result = zym_interpret(vm, "var x = 42\nprint(x)");

// Run from a file
ZymResult result = zym_interpretFile(vm, "scripts/main.zym");

// Check result
if (result == ZYM_OK) {
    printf("Script ran successfully\n");
} else if (result == ZYM_COMPILE_ERROR) {
    printf("Compile error\n");
} else if (result == ZYM_RUNTIME_ERROR) {
    printf("Runtime error\n");
}

Multiple VM Instances

Each VM is completely independent — they share no state and can run different scripts concurrently (from different threads).

C
ZymVM* vm1 = zym_newVM();
ZymVM* vm2 = zym_newVM();
// Each VM has its own globals, GC, and module state

Error Callback

By default, all error messages (parse errors, compile errors, runtime errors) are written to stderr. For embedded or non-console environments, you can install an error callback to capture these messages yourself.

Callback Signature

typedef void (*ZymErrorCallback)(ZymVM* vm, ZymStatus type, const char* file, int line, const char* message, void* user_data);

Setting the Callback

void zym_setErrorCallback(ZymVM* vm, ZymErrorCallback callback, void* user_data);

Call this after creating the VM and before compiling or running code. Pass NULL as the callback to restore default stderr behavior.

Example: Logging Errors to a Buffer

C
static char error_log[4096];
static int error_log_pos = 0;

void my_error_handler(ZymVM* vm, ZymStatus type,
                      const char* file, int line,
                      const char* message, void* user_data) {
    const char* prefix = (type == ZYM_STATUS_COMPILE_ERROR)
        ? "[compile] " : "[runtime] ";
    error_log_pos += snprintf(error_log + error_log_pos,
        sizeof(error_log) - error_log_pos,
        "%s%s\n", prefix, message);
}

// Usage:
ZymVM* vm = zym_newVM();
zym_setErrorCallback(vm, my_error_handler, NULL);

// All errors now go to error_log instead of stderr
Default behavior: When no callback is set (or set to NULL), all errors are printed to stderr exactly as before — existing code is unaffected.

What Gets Routed

The preprocessor currently reports errors silently (sets a failure status without a message). Preprocessor error reporting through the callback is planned for a future release.

Compilation Pipeline & Bytecode

For production deployments, you can separate compilation from execution. Compile scripts once into bytecode, then load and execute the bytecode multiple times. This eliminates parsing overhead and speeds up startup.

The Pipeline

  1. Preprocess (optional — only if using macros like #define)
  2. Compile source to bytecode chunk
  3. Serialize chunk to buffer
  4. Deserialize buffer in runtime VM
  5. Execute loaded chunk

Separate Compile & Run VMs

C
// Error callback — prints to console (same as default, shown for illustration)
void on_error(ZymVM* vm, ZymStatus type, const char* file,
             int line, const char* message, void* user_data) {
    const char* prefix = (type == ZYM_STATUS_COMPILE_ERROR)
        ? "COMPILE" : "RUNTIME";
    fprintf(stderr, "[%s] %s\n", prefix, message);
}

// ===== COMPILATION VM (ephemeral) =====
ZymVM* compile_vm = zym_newVM();
zym_setErrorCallback(compile_vm, on_error, NULL);

// CRITICAL: Register natives in compile VM
// Compiler needs them for name mangling and parameter qualifiers
setupNatives(compile_vm);

ZymChunk* chunk = zym_newChunk(compile_vm);
ZymLineMap* line_map = zym_newLineMap(compile_vm);

const char* source = "func add(a, b) { return a + b; }";

// Step 1: Optional preprocessing (skip if not using macros)
const char* processed = NULL;
if (zym_preprocess(compile_vm, source, line_map, &processed) != ZYM_STATUS_OK) {
    // Handle error
}

// Step 2: Compile (use processed if you preprocessed, else source)
ZymCompilerConfig config = { .include_line_info = true };
const char* to_compile = processed ? processed : source;
zym_compile(compile_vm, to_compile, chunk, line_map, "script.zym", config);

// Step 3: Serialize to bytecode (allocates via malloc)
char* bytecode;
size_t bytecode_size;
zym_serializeChunk(compile_vm, config, chunk, &bytecode, &bytecode_size);

// ===== RUNTIME VM (long-lived) =====
ZymVM* run_vm = zym_newVM();
zym_setErrorCallback(run_vm, on_error, NULL);

// CRITICAL: Register natives in run VM
// Runtime needs them for actual function pointers
setupNatives(run_vm);

ZymChunk* loaded_chunk = zym_newChunk(run_vm);

// Step 4: Deserialize (automatically sets vm->chunk for GC safety)
zym_deserializeChunk(run_vm, loaded_chunk, bytecode, bytecode_size);

// Step 5: Execute
zym_runChunk(run_vm, loaded_chunk);

// Cleanup
zym_freeChunk(compile_vm, chunk);
zym_freeChunk(run_vm, loaded_chunk);
zym_freeLineMap(compile_vm, line_map);
zym_freeVM(compile_vm);
zym_freeVM(run_vm);
free(bytecode);
Important: Native functions must be registered in both VMs:
  • Compile VM: Compiler resolves mangled names (funcName@arity) and reads parameter qualifiers (ref, val, clone)
  • Run VM: Runtime needs actual function pointers for execution

Saving & Loading Bytecode Files

C
// Save compiled bytecode to file
FILE* f = fopen("script.zymc", "wb");
fwrite(bytecode, 1, bytecode_size, f);
fclose(f);

// Later: Load bytecode from file
f = fopen("script.zymc", "rb");
fseek(f, 0, SEEK_END);
size_t size = ftell(f);
rewind(f);
char* loaded_bytecode = malloc(size);
fread(loaded_bytecode, 1, size, f);
fclose(f);

// Deserialize and execute
ZymChunk* runtime_chunk = zym_newChunk(run_vm);
zym_deserializeChunk(run_vm, runtime_chunk, loaded_bytecode, size);
zym_runChunk(run_vm, runtime_chunk);

free(loaded_bytecode);

Value System

All Zym values are 64-bit NaN-boxed ZymValue. You create and inspect them with helper functions.

Creating Values

C
ZymValue num     = zym_newNumber(42.0);
ZymValue str     = zym_newString(vm, "hello");
ZymValue boolean = zym_newBool(1);
ZymValue null_v  = zym_newNull();
ZymValue list    = zym_newList(vm);
ZymValue map     = zym_newMap(vm);

Type Checking

C
zym_isNumber(value)    // true if number
zym_isString(value)    // true if string
zym_isBool(value)      // true if boolean
zym_isNull(value)      // true if null
zym_isList(value)      // true if list
zym_isMap(value)       // true if map

Extracting Values

C
double n          = zym_asNumber(value);
const char* s     = zym_asCString(value);
int b             = zym_asBool(value);

List Operations (from C)

C
ZymValue list = zym_newList(vm);
zym_listPush(vm, list, zym_newNumber(1));
zym_listPush(vm, list, zym_newNumber(2));
zym_listPush(vm, list, zym_newNumber(3));

int len = zym_listLength(list);           // 3
ZymValue first = zym_listGet(list, 0);    // 1

Map Operations (from C)

C
ZymValue map = zym_newMap(vm);
zym_mapSet(vm, map, "name", zym_newString(vm, "Alice"));
zym_mapSet(vm, map, "age", zym_newNumber(30));

ZymValue name = zym_mapGet(vm, map, "name");
int count = zym_mapSize(map);   // 2

NaN-Boxing Explained

Zym uses NaN-boxing to pack all value types into a single 64-bit uint64_t. This enables efficient value passing without heap allocation for primitives.

Value types:

Safe vs Unsafe Extraction

Always check types before extraction to avoid undefined behavior:

C — UNSAFE (fast, no checks)
if (zym_isNumber(value)) {
    double num = zym_asNumber(value);  // No overhead after check
}
C — SAFE (returns false on mismatch)
double num;
if (zym_toNumber(value, &num)) {
    printf("Got number: %g\n", num);
} else {
    printf("Not a number\n");
}

Structs & Enums

Structs and enums require a schema defined in Zym script before C code can create instances:

Zym — define schema first
struct Point {
    x;
    y;
}

enum Color {
    RED, GREEN, BLUE
}
C — create instances after schema defined
// Create struct
ZymValue point = zym_newStruct(vm, "Point");
if (zym_isStruct(point)) {
    zym_structSet(vm, point, "x", zym_newNumber(10));
    zym_structSet(vm, point, "y", zym_newNumber(20));
    ZymValue x = zym_structGet(vm, point, "x");
}

// Create enum
ZymValue color = zym_newEnum(vm, "Color", "RED");
if (zym_isEnum(color)) {
    const char* variant = zym_enumGetVariant(vm, color);  // "RED"
}

References

References are mutable storage locations that can be dereferenced and assigned:

C
if (zym_isReference(ref)) {
    // Dereference to get underlying value
    ZymValue val = zym_deref(vm, ref);

    // Modify through reference
    zym_refSet(vm, ref, zym_newNumber(100));
}

Value Inspection

C
// Get type name as string
const char* type = zym_typeName(value);  // "string", "number", "list", etc.

// Convert any value to string representation
ZymValue str = zym_valueToString(vm, value);
const char* repr = zym_asCString(str);

// Print any value to stdout (same format as Zym's print)
zym_printValue(vm, value);

Native Functions

Native functions bridge C code into Zym scripts.

Signature Format

The signature string tells the VM the function name and its parameter names, with optional qualifiers.

"functionName(param1, param2)"            // basic
"add(a, b)"                                // two params
"swap(ref a, ref b)"                       // ref params
"process(val data)"                        // val (deep copy)

Registering a Native Function

C
// The native function implementation
ZymValue native_add(ZymVM* vm, ZymValue a, ZymValue b) {
    return zym_newNumber(zym_asNumber(a) + zym_asNumber(b));
}

// Register it
zym_defineNative(vm, "add(a, b)", native_add);

Now Zym scripts can call add(3, 4) and get 7.

Parameter Qualifiers

Zym supports rich parameter semantics via qualifiers in the signature string:

QualifierBehaviorUse Case
(none) Pass by shared reference Default: lists/maps shared, primitives copied
ref Pass mutable reference Modify caller's variable
val Structural value copy Copy container structure, refs preserved
clone Deep recursive copy Fully independent copy, breaks all aliases
slot Pass binding, not value Rebind caller's variable

REF Example: Modify Caller's Variable

C
ZymValue native_increment(ZymVM* vm, ZymValue ref) {
    ZymValue val = zym_deref(vm, ref);  // Dereference to get actual value
    if (!zym_isNumber(val)) {
        zym_runtimeError(vm, "increment() requires a number reference");
        return ZYM_ERROR;
    }

    ZymValue result = zym_newNumber(zym_asNumber(val) + 1.0);
    zym_refSet(vm, ref, result);  // Write back through reference
    return result;
}

// Register with 'ref' qualifier
zym_defineNative(vm, "native_increment(ref x)", native_increment);

// In Zym script:
// var count = 0;
// native_increment(count);  // count is now 1

VAL Example: Structural Copy

Container structure copied, but references preserved:

C
ZymValue native_reverse(ZymVM* vm, ZymValue list) {
    if (!zym_isList(list)) {
        zym_runtimeError(vm, "reverse() requires a list");
        return ZYM_ERROR;
    }

    // List structure is copied by VM before this function is called (due to 'val').
    // Elements themselves are shared (primitives copied, objects/refs preserved).
    int len = zym_listLength(list);
    for (int i = 0; i < len / 2; i++) {
        ZymValue temp = zym_listGet(vm, list, i);
        ZymValue other = zym_listGet(vm, list, len - 1 - i);
        zym_listSet(vm, list, i, other);
        zym_listSet(vm, list, len - 1 - i, temp);
    }
    return list;
}

zym_defineNative(vm, "native_reverse(val list)", native_reverse);

// In Zym script:
// var a = [1, 2, 3];
// var b = native_reverse(a);  // a unchanged, b is reversed structural copy

Error Handling

Always return ZYM_ERROR after calling zym_runtimeError():

C
ZymValue native_divide(ZymVM* vm, ZymValue a, ZymValue b) {
    if (!zym_isNumber(a) || !zym_isNumber(b)) {
        zym_runtimeError(vm, "divide() requires two numbers");
        return ZYM_ERROR;
    }

    double divisor = zym_asNumber(b);
    if (divisor == 0.0) {
        zym_runtimeError(vm, "Division by zero");
        return ZYM_ERROR;
    }

    return zym_newNumber(zym_asNumber(a) / divisor);
}

Defining Global Variables

Use zym_defineGlobal() to expose a pre-created value directly in Zym's global scope. Unlike zym_defineNative() which registers a callable function, this sets a global variable that scripts can access by name — ideal for singleton objects or constant values.

ZymStatus zym_defineGlobal(ZymVM* vm, const char* name, ZymValue value);

Returns: ZYM_STATUS_OK on success, ZYM_STATUS_COMPILE_ERROR on failure.

Singleton Module Pattern

A common pattern is to create a map of native closures (a "module object") and expose it as a singleton global. This avoids requiring users to call a factory function and gives direct access:

C
// Create a singleton Console module
ZymValue consoleObj = createConsoleModule(vm);  // returns a map of closures

// Register as a global — scripts access it directly
zym_defineGlobal(vm, "Console", consoleObj);

// In Zym script:
// Console.write("Hello")      ← direct access, no factory call needed
// var w = Console.getWidth()  ← singleton shared across all code

Simple Constants

C
// Expose constants to scripts
zym_defineGlobal(vm, "VERSION", zym_newString(vm, "1.0.0"));
zym_defineGlobal(vm, "MAX_PLAYERS", zym_newNumber(16));
zym_defineGlobal(vm, "DEBUG", zym_newBool(false));

// In Zym script:
// print(VERSION)       // "1.0.0"
// print(MAX_PLAYERS)   // 16
Note: GC protection is handled automatically — zym_defineGlobal() roots the value during insertion. However, the value must be alive (not collected) when you call this function.

Native Closures

Native closures enable you to bind external C modules (file handles, database connections, sockets) to Zym without exposing internal details. Each closure has:

Complete Native Closure Example

C — File Module with Native Closures
// 1. Define private data structure
typedef struct {
    FILE* handle;
    char* path;
    bool is_open;
} FileHandle;

// 2. Define cleanup function (called by GC)
void file_cleanup(ZymVM* vm, void* native_data) {
    FileHandle* file = (FileHandle*)native_data;
    if (file->is_open) {
        fclose(file->handle);
    }
    free(file->path);
    free(file);
}

// 3. Define closure methods (context is first parameter)
ZymValue file_read(ZymVM* vm, ZymValue context, ZymValue lengthVal) {
    // Extract private data from context
    FileHandle* file = (FileHandle*)zym_getNativeData(context);

    if (!file->is_open) {
        zym_runtimeError(vm, "File is not open");
        return ZYM_ERROR;
    }

    if (!zym_isNumber(lengthVal)) {
        zym_runtimeError(vm, "read() requires a number");
        return ZYM_ERROR;
    }

    size_t length = (size_t)zym_asNumber(lengthVal);
    char* buffer = malloc(length + 1);
    size_t read_count = fread(buffer, 1, length, file->handle);
    buffer[read_count] = '\0';

    ZymValue result = zym_newString(vm, buffer);
    free(buffer);
    return result;
}

ZymValue file_close(ZymVM* vm, ZymValue context) {
    FileHandle* file = (FileHandle*)zym_getNativeData(context);

    if (file->is_open) {
        fclose(file->handle);
        file->is_open = false;
    }

    return zym_newNull();
}

// 4. Factory function to create file objects
ZymValue native_openFile(ZymVM* vm, ZymValue pathVal) {
    if (!zym_isString(pathVal)) {
        zym_runtimeError(vm, "openFile() requires a string");
        return ZYM_ERROR;
    }

    const char* path = zym_asCString(pathVal);

    // Allocate private data
    FileHandle* file = malloc(sizeof(FileHandle));
    file->handle = fopen(path, "r");
    file->path = strdup(path);
    file->is_open = file->handle != NULL;

    if (!file->is_open) {
        free(file->path);
        free(file);
        zym_runtimeError(vm, "Failed to open file: %s", path);
        return ZYM_ERROR;
    }

    // Create context with finalizer
    ZymValue context = zym_createNativeContext(vm, file, file_cleanup);
    zym_pushRoot(vm, context);  // Protect during construction

    // Create closures bound to this context
    ZymValue readMethod = zym_createNativeClosure(vm, "read(length)", file_read, context);
    zym_pushRoot(vm, readMethod);

    ZymValue closeMethod = zym_createNativeClosure(vm, "close()", file_close, context);
    zym_pushRoot(vm, closeMethod);

    // Create map to hold the file "object"
    ZymValue fileObj = zym_newMap(vm);
    zym_pushRoot(vm, fileObj);

    zym_mapSet(vm, fileObj, "path", pathVal);
    zym_mapSet(vm, fileObj, "read", readMethod);
    zym_mapSet(vm, fileObj, "close", closeMethod);

    // Pop temp roots
    zym_popRoot(vm);  // fileObj
    zym_popRoot(vm);  // closeMethod
    zym_popRoot(vm);  // readMethod
    zym_popRoot(vm);  // context

    return fileObj;
}

// Register the factory function
zym_defineNative(vm, "openFile(path)", native_openFile);

Usage from Zym:

Zym
var file = openFile("data.txt")
var content = file.read(100)
print(content)
file.close()  // Explicit close

// Or rely on GC (finalizer runs automatically):
var file2 = openFile("other.txt")
var data = file2.read(50)
// When file2 goes out of scope and is collected, finalizer closes the file

Two-Way FFI

Calling Script Functions from C

You can retrieve and call Zym functions from C code.

C
// Get a script function by name
ZymValue fn = zym_getGlobal(vm, "myCallback");

// Call it with arguments
ZymValue args[] = { zym_newNumber(10), zym_newNumber(20) };
ZymValue result = zym_call(vm, fn, args, 2);

Setting Globals from C

C
// Expose a constant to scripts
zym_setGlobal(vm, "VERSION", zym_newString(vm, "1.0.0"));
zym_setGlobal(vm, "MAX_PLAYERS", zym_newNumber(64));

Function Overloading (Dispatchers)

Register multiple arities of the same function.

C
// Zero args
ZymValue native_greet0(ZymVM* vm) {
    return zym_newString(vm, "Hello!");
}
// One arg
ZymValue native_greet1(ZymVM* vm, ZymValue name) {
    // ... build greeting string ...
}

zym_defineNative(vm, "greet()", native_greet0);
zym_defineNative(vm, "greet(name)", native_greet1);

Garbage Collection

When creating Zym objects in C, you must protect them from the GC until they are safely rooted (e.g., assigned to a variable or pushed to a list).

Temporary Roots

C
// Push a temporary GC root
zym_pushTempRoot(vm, value);

// ... do allocations that might trigger GC ...

// Pop when safe
zym_popTempRoot(vm);
Rule of thumb: If you create a Zym object and then call any function that might allocate (including zym_newString, zym_newList, etc.), protect the first object with zym_pushTempRoot.

GC Control from C

C
// Pause/resume
zym_gcPause(vm);
zym_gcResume(vm);

// Force a collection cycle
zym_gcCycle(vm);

// Query memory usage
size_t bytes = zym_gcBytesTracked(vm);

Module System

Zym provides a compile-time module system for organizing code across multiple files. Modules are loaded, preprocessed, and combined into a single compilation unit before being compiled to bytecode.

Module Loading Architecture

Entry File → Preprocess → Module Loader → Combined Source → Compiler → Bytecode
                                    ↓
                            Module Files (preprocessed individually)

Key Features:

Basic Module Usage

Module file (math.zym):

Zym
// Math module - provides basic operations

func add(a, b) {
    return a + b
}

func multiply(a, b) {
    return a * b
}

var PI = 3.14159

// Export values by returning a map
return {
    add: add,
    multiply: multiply,
    PI: PI
}

Entry file (main.zym):

Zym
// Load module (path relative to this file)
var math = load("math.zym")

// Use exported values
var sum = math.add(5, 3)
var product = math.multiply(4, 7)
print(math.PI)

Module Loader Integration

Include the module loader header:

C
#include "zym/module_loader.h"

Module Callback

The module loader requires a callback that reads and preprocesses each module file:

C
// Result type returned by callback
typedef struct {
    char* source;          // Preprocessed source (caller will free)
    ZymLineMap* line_map;  // Line map from preprocessing (caller will free)
} ModuleReadResult;

// Callback signature
typedef ModuleReadResult (*ModuleReadCallback)(
    const char* path,
    void* user_data
);

// Example implementation
static ModuleReadResult moduleCallback(const char* path, void* user_data) {
    ZymVM* vm = (ZymVM*)user_data;
    ModuleReadResult result = { .source = NULL, .line_map = NULL };

    // 1. Read the file
    char* raw_source = readFile(path);
    if (!raw_source) return result;

    // 2. Preprocess it
    ZymLineMap* module_map = zym_newLineMap(vm);
    const char* preprocessed = NULL;
    ZymStatus status = zym_preprocess(vm, raw_source, module_map, &preprocessed);

    free(raw_source);

    if (status != ZYM_STATUS_OK) {
        zym_freeLineMap(vm, module_map);
        return result;
    }

    result.source = (char*)preprocessed;
    result.line_map = module_map;
    return result;
}

Complete Integration Example

C
int main(void) {
    ZymVM* vm = zym_newVM();
    ZymChunk* chunk = zym_newChunk(vm);
    ZymLineMap* line_map = zym_newLineMap(vm);

    // Read and preprocess entry file
    const char* entry_file = "main.zym";
    char* entry_source = readFile(entry_file);
    const char* preprocessed_entry = NULL;
    zym_preprocess(vm, entry_source, line_map, &preprocessed_entry);

    // Load modules
    ModuleLoadResult* modules = loadModules(
        vm,
        preprocessed_entry,      // Already preprocessed entry source
        line_map,                // Line map from preprocessing entry
        entry_file,              // Entry file path (for relative resolution)
        moduleCallback,          // Callback to read/preprocess modules
        vm,                      // User data (VM for preprocessing)
        true,                    // Use human-readable debug names
        true,                    // Write debug output
        "module_debug.zym"       // Debug output path
    );

    if (modules->has_error) {
        fprintf(stderr, "Module loading failed: %s\n", modules->error_message);
        freeModuleLoadResult(vm, modules);
        return 1;
    }

    printf("Loaded %d module(s)\n", modules->module_count);

    // Compile combined source
    ZymCompilerConfig config = { .include_line_info = true };
    if (zym_compile(vm, modules->combined_source, chunk,
                     modules->line_map, entry_file, config) != ZYM_STATUS_OK) {
        fprintf(stderr, "Compilation failed\n");
        freeModuleLoadResult(vm, modules);
        return 1;
    }

    // Execute
    zym_runChunk(vm, chunk);

    // Cleanup
    freeModuleLoadResult(vm, modules);
    free(entry_source);
    zym_freeChunk(vm, chunk);
    zym_freeLineMap(vm, line_map);
    zym_freeVM(vm);

    return 0;
}

How Module Loading Works

  1. Entry file processing: Entry file read and preprocessed by host
  2. Dependency discovery: Module loader scans for load("path") calls
  3. Recursive loading: For each module:
    • Resolves path relative to containing file
    • Calls your callback to read and preprocess
    • Scans for nested load() calls
    • Repeats recursively
  4. Module wrapping: Each module wrapped as function: func __module_<hash>() { <module code> }
  5. Load transformation: load("math.zym")__module_467954409()
  6. Combination: All module functions emitted first, entry file transformed and appended
  7. Compilation: Combined source compiled as one unit to single bytecode chunk

Path Resolution

Module paths are resolved relative to the file containing the load() call:

project/
├── main.zym           (loads "lib/math.zym")
├── lib/
│   ├── math.zym       (loads "helpers.zym")
│   └── helpers.zym

Resolution:

Circular Dependency Detection

The module loader detects circular dependencies at compile time:

Zym — a.zym
var b = load("b.zym")
return { data: "A" }
Zym — b.zym
var a = load("a.zym")  // Error: Circular dependency!
return { data: "B" }

Error: Module loading failed: Circular dependency detected: b.zym

Debug Output

When write_debug_output is true, the module loader writes the combined source:

module_debug.zym
// ===== Module Loader Debug Output =====
// Entry: main.zym
// Loaded 2 module(s):
//   - main.zym
//   - lib/math.zym
// =======================================

func __module_467954409() {
    func add(a, b) {
        return a + b
    }

    return {
        add: add
    }
}

var math = __module_467954409()
var sum = math.add(5, 3)
print(sum)

This is useful for debugging module loading issues and verifying transformations.

Modules with Serialization

Modules are resolved before compilation, so serialized bytecode contains all modules:

C
// Compile with modules
ModuleLoadResult* modules = loadModules(...);
zym_compile(vm, modules->combined_source, chunk, modules->line_map, "entry.zym", config);

// Serialize (contains all modules)
char* bytecode;
size_t size;
zym_serializeChunk(vm, config, chunk, &bytecode, &size);

// Later, deserialize and run (no module loading needed)
ZymChunk* loaded = zym_newChunk(run_vm);
zym_deserializeChunk(run_vm, loaded, bytecode, size);
zym_runChunk(run_vm, loaded);  // Works! All modules embedded

Preemption

Preemption lets you limit how many VM instructions execute before control returns to the host. This is essential for sandboxing untrusted scripts, building cooperative schedulers, and preventing infinite loops from hanging your application.

How It Works

The VM maintains a yield budget — a countdown of instructions. Each time the VM dispatches an instruction, it decrements the budget. When the budget reaches zero (or a yield is explicitly requested), one of two things happens:

  1. If a script callback is registered — the VM calls it in-place (no C-stack unwinding). The callback can capture a continuation, do bookkeeping, or simply return.
  2. If no callback is set — the VM suspends and returns ZYM_STATUS_YIELD to the host. You resume later with zym_resume().

Basic Host-Side Preemption

The simplest pattern: let scripts run with a budget, and handle yields in a loop.

C
ZymVM* vm = zym_newVM();
setupNatives(vm);

// Enable preemption from script (or do it from C before running)
// Scripts call: Preempt.enable() and Preempt.setTimeslice(1000)

ZymStatus status = zym_runChunk(vm, chunk);

// The "preempt pump" — handle yields in a loop
while (status == ZYM_STATUS_YIELD) {
    // Opportunity to do host-side work:
    // - Check timeouts or resource limits
    // - Process events
    // - Decide whether to continue or abort
    status = zym_resume(vm);
}

if (status != ZYM_STATUS_OK) {
    fprintf(stderr, "Script error\n");
}

zym_freeVM(vm);

Calling Functions with Preemption

When calling script functions from C with zym_call(), the same yield handling applies:

C
ZymStatus result = zym_call(vm, "update", 1, zym_newNumber(delta_time));
while (result == ZYM_STATUS_YIELD) {
    result = zym_resume(vm);
}
if (result != ZYM_STATUS_OK) {
    // Handle error
}

Script-Side Callback (In-VM Preemption)

For fully script-driven scheduling (e.g., fiber schedulers), register a callback from C using zym_setPreemptCallback(). When the budget expires, the VM calls this function inside the current execution context — no C-stack unwinding.

C
// Option 1: Set callback from C (before running script)
// You need a ZymValue closure — typically obtained from script globals
zym_runChunk(vm, chunk);  // Script defines the callback

// Option 2: Script sets its own callback
// From script: Preempt.setCallback(func() { ... })

The callback runs with preemption automatically disabled to prevent recursive preemption. The disable depth is restored when the callback returns.

Preemption API

FunctionDescription
zym_resume(vm)Continue execution after ZYM_STATUS_YIELD
zym_setPreemptCallback(vm, callback)Register a ZymValue closure as the preemption handler
Status CodeMeaning
ZYM_STATUS_OKExecution completed successfully
ZYM_STATUS_YIELDVM suspended (preemption with no script callback)
ZYM_STATUS_RUNTIME_ERRORScript error occurred
ZYM_STATUS_COMPILE_ERRORCompilation failed

Sandboxing Example

Use preemption to limit untrusted script execution time:

C
// In script: enable preemption with a tight budget
// Preempt.enable()
// Preempt.setTimeslice(10000)

ZymStatus status = zym_runChunk(vm, chunk);
int yields = 0;
int max_yields = 1000;  // Safety limit

while (status == ZYM_STATUS_YIELD) {
    yields++;
    if (yields > max_yields) {
        fprintf(stderr, "Script exceeded execution limit\n");
        break;
    }
    status = zym_resume(vm);
}
Script-side API: The Preempt module is available automatically in every VM. Scripts can call Preempt.enable(), Preempt.setTimeslice(), Preempt.setCallback(), etc. See the CLI Preemption and Continuations & Preemption docs for the full script-side API.

Comprehensive API Reference

VM Lifecycle

FunctionDescription
zym_newVM()Create a new VM instance
zym_freeVM(vm)Destroy a VM and free all memory
zym_setErrorCallback(vm, cb, data)Set error callback (NULL restores stderr default)

Compilation & Execution

FunctionDescription
zym_newChunk(vm)Create a new bytecode chunk
zym_freeChunk(vm, chunk)Free a bytecode chunk
zym_newLineMap(vm)Create a new line map for error reporting
zym_freeLineMap(vm, map)Free a line map
zym_preprocess(vm, src, map, out)Preprocess source (optional, for macros only)
zym_compile(vm, src, chunk, map, file, config)Compile source to bytecode
zym_runChunk(vm, chunk)Execute a compiled chunk (may return ZYM_STATUS_YIELD)
zym_resume(vm)Continue execution after a yield
zym_setPreemptCallback(vm, callback)Register script closure as preemption handler
zym_serializeChunk(vm, config, chunk, buf, size)Serialize chunk to buffer (allocates via malloc)
zym_deserializeChunk(vm, chunk, buf, size)Deserialize buffer to chunk

Module System

FunctionDescription
loadModules(...)Load and combine modules into single compilation unit
freeModuleLoadResult(vm, result)Free module load result

Type Checking

FunctionDescription
zym_isNull(val)Check if value is null
zym_isBool(val)Check if value is boolean
zym_isNumber(val)Check if value is number
zym_isString(val)Check if value is string
zym_isList(val)Check if value is list
zym_isMap(val)Check if value is map
zym_isStruct(val)Check if value is struct
zym_isEnum(val)Check if value is enum
zym_isFunction(val)Check if value is function
zym_isReference(val)Check if value is reference
zym_isClosure(val)Check if value is native/script closure
zym_typeName(val)Get type name as string (e.g., "string", "number")

Value Creation

FunctionDescription
zym_newNull()Create null value
zym_newBool(b)Create boolean value
zym_newNumber(n)Create number value
zym_newString(vm, s)Create string value (null-terminated)
zym_newStringN(vm, s, len)Create string value with explicit length
zym_newList(vm)Create empty list
zym_newMap(vm)Create empty map
zym_newStruct(vm, name)Create struct (requires script-defined schema)
zym_newEnum(vm, name, variant)Create enum (requires script-defined schema)

Value Extraction

FunctionDescription
zym_asNumber(val)Extract number (unsafe, fast)
zym_asBool(val)Extract bool (unsafe, fast)
zym_asCString(val)Extract C string (unsafe, VM-owned)
zym_toNumber(val, out)Extract number (safe, returns false on mismatch)
zym_toBool(val, out)Extract bool (safe, returns false on mismatch)
zym_toString(val, out, len)Extract string (safe, returns char count)
zym_toStringBytes(val, out, len)Extract string (safe, returns byte count)

Value Inspection

FunctionDescription
zym_stringLength(val)Get UTF-8 character count
zym_stringByteLength(val)Get raw byte count
zym_valueToString(vm, val)Convert any value to string representation
zym_printValue(vm, val)Print any value to stdout

List Operations

FunctionDescription
zym_listLength(list)Get list length
zym_listGet(vm, list, idx)Get element at index (returns ZYM_ERROR if out of bounds)
zym_listSet(vm, list, idx, val)Set element at index
zym_listAppend(vm, list, val)Append element to end
zym_listInsert(vm, list, idx, val)Insert element at index
zym_listRemove(vm, list, idx)Remove element at index

Map Operations

FunctionDescription
zym_mapSize(map)Get map size (number of keys)
zym_mapGet(vm, map, key)Get value by key (returns ZYM_ERROR if not found)
zym_mapSet(vm, map, key, val)Set key-value pair
zym_mapHas(map, key)Check if key exists
zym_mapDelete(vm, map, key)Delete key
zym_mapForEach(vm, map, fn, data)Iterate over map entries

Struct & Enum Operations

FunctionDescription
zym_structGet(vm, s, field)Get struct field value
zym_structSet(vm, s, field, val)Set struct field value
zym_structHasField(s, field)Check if field exists
zym_structGetName(s)Get struct type name
zym_structFieldCount(s)Get number of fields
zym_structFieldNameAt(s, idx)Get field name by index
zym_enumGetName(vm, e)Get enum type name
zym_enumGetVariant(vm, e)Get enum variant name
zym_enumVariantIndex(vm, e)Get enum variant index
zym_enumEquals(a, b)Compare two enum values

Reference Operations

FunctionDescription
zym_deref(vm, ref)Dereference to get underlying value
zym_refSet(vm, ref, val)Write through reference

Native Functions

FunctionDescription
zym_defineNative(vm, sig, fn)Register native function with signature
zym_defineGlobal(vm, name, val)Define a global variable accessible from scripts
zym_hasFunction(vm, name, arity)Check if function exists (uses mangled names)
zym_call(vm, name, argc, ...)Call script function (varargs)
zym_callv(vm, name, argc, argv)Call script function (array)
zym_getCallResult(vm)Get return value after successful call
zym_runtimeError(vm, fmt, ...)Report runtime error (printf-style)

Native Closures

FunctionDescription
zym_createNativeContext(vm, data, finalizer)Create context with private data and cleanup
zym_getNativeData(context)Extract private data from context
zym_createNativeClosure(vm, sig, fn, ctx)Create closure bound to context
zym_getClosureContext(closure)Extract context from native closure

Function Overloading

FunctionDescription
zym_createDispatcher(vm)Create dispatcher (max 8 overloads)
zym_addOverload(vm, disp, closure)Add arity-based overload to dispatcher

Native References

FunctionDescription
zym_createNativeReference(vm, ctx, offset, get, set)Create reference to C struct field with optional hooks

GC Protection

FunctionDescription
zym_pushRoot(vm, val)Protect value from GC (heap objects only)
zym_popRoot(vm)Release GC protection (must balance with pushRoot)
zym_peekRoot(vm, depth)Inspect root stack (0 = top)

Calling Script Functions

FunctionDescription
zym_hasFunction(vm, name, arity)Check if a script function exists
zym_call(vm, name, argc, ...)Call script function (varargs, may return ZYM_STATUS_YIELD)
zym_callv(vm, name, argc, argv)Call script function (array args)
zym_callClosurev(vm, closure, argc, argv)Call a closure value directly
zym_getCallResult(vm)Get return value of last call

Error Handling

FunctionDescription
zym_setErrorCallback(vm, cb, data)Set error callback (NULL restores stderr)
zym_runtimeError(vm, fmt, ...)Report a runtime error from native code

Debugging

FunctionDescription
disassembleChunk(chunk, name)Disassemble entire chunk to stdout
disassembleChunkToFile(chunk, name, f)Disassemble chunk to file
disassembleInstruction(chunk, offset)Disassemble single instruction

See also: Language GuideGC APIContinuations API