// vm.c -- DVX BASIC virtual machine implementation
//
// Stack-based p-code interpreter. Executes one instruction per
// basVmStep() call, or runs until completion via basVmRun().
// All I/O is through host-provided callbacks -- the VM itself
// has no platform dependencies.

#include "vm.h"
#include "../compiler/opcodes.h"

#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

// ============================================================
// Prototypes
// ============================================================

// Steps to execute between doEvents yields during subroutine calls.
// This keeps the GUI responsive without excessive overhead from
// calling doEvents on every instruction.
#define SUB_YIELD_INTERVAL 500

static BasCallFrameT *currentFrame(BasVmT *vm);
static void           defaultPrint(void *ctx, const char *text, bool newline);
static BasVmResultE   execArith(BasVmT *vm, uint8_t op);
static BasVmResultE   execCompare(BasVmT *vm, uint8_t op);
static BasVmResultE   execFileOp(BasVmT *vm, uint8_t op);
static BasVmResultE   execLogical(BasVmT *vm, uint8_t op);
static BasVmResultE   execMath(BasVmT *vm, uint8_t op);
static BasVmResultE   execPrint(BasVmT *vm);
static BasVmResultE   execStringOp(BasVmT *vm, uint8_t op);
static bool           pop(BasVmT *vm, BasValueT *val);
static bool           push(BasVmT *vm, BasValueT val);
static int16_t        readInt16(BasVmT *vm);
static uint8_t        readUint8(BasVmT *vm);
static uint16_t       readUint16(BasVmT *vm);
static void           runtimeError(BasVmT *vm, int32_t errNum, const char *msg);


// ============================================================
// runSubLoop -- shared inner loop for basVmCallSub variants
// ============================================================
//
// Executes instructions until callDepth drops back to savedCallDepth
// (meaning the subroutine returned). Periodically yields via the
// doEvents callback to keep the GUI responsive during long-running
// event handlers.

static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning) {
    int32_t stepsSinceYield = 0;
    bool    hadBreakpoint   = false;

    while (vm->running && vm->callDepth > savedCallDepth) {
        BasVmResultE result = basVmStep(vm);

        if (result == BAS_VM_HALTED) {
            break;
        }

        if (result == BAS_VM_BREAKPOINT) {
            // Pause in place: yield to the GUI until the host resumes.
            // This ensures the sub runs to completion before returning
            // to the caller (critical for form init code, event handlers).
            vm->running  = false;
            hadBreakpoint = true;

            // Notify host to update debug UI (highlight line, locals, etc.)
            if (vm->breakpointFn) {
                vm->breakpointFn(vm->breakpointCtx, vm->currentLine);
            }

            while (!vm->running && vm->doEventsFn) {
                if (!vm->doEventsFn(vm->doEventsCtx)) {
                    // Host is shutting down
                    vm->pc      = savedPc;
                    vm->running = savedRunning;
                    return false;
                }
            }

            // User resumed — continue executing the sub
            stepsSinceYield = 0;
            continue;
        }

        if (result != BAS_VM_OK) {
            vm->pc        = savedPc;
            vm->callDepth = savedCallDepth;
            vm->running   = savedRunning;
            return false;
        }

        // Yield periodically to keep the GUI responsive
        if (++stepsSinceYield >= SUB_YIELD_INTERVAL) {
            stepsSinceYield = 0;

            if (vm->doEventsFn) {
                if (!vm->doEventsFn(vm->doEventsCtx)) {
                    vm->running = false;
                    break;
                }
            }
        }
    }

    vm->pc      = savedPc;
    vm->running = savedRunning;

    // If we paused at a breakpoint during this sub, notify the host
    // so it can pause the event loop before any new event fires.
    if (hadBreakpoint && vm->breakpointFn) {
        vm->breakpointFn(vm->breakpointCtx, -1);
    }

    return true;
}


// ============================================================
// basVmCallSub
// ============================================================

bool basVmCallSub(BasVmT *vm, int32_t codeAddr) {
    if (!vm || !vm->module) {
        return false;
    }

    // Reject calls while debugger is paused (break mode between events)
    if (vm->debugPaused) {
        return false;
    }

    if (codeAddr < 0 || codeAddr >= vm->module->codeLen) {
        return false;
    }

    if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE - 1) {
        return false;
    }

    // Save VM state
    int32_t savedPc        = vm->pc;
    int32_t savedCallDepth = vm->callDepth;
    bool    savedRunning   = vm->running;

    // Push a call frame that returns to an invalid address (sentinel)
    // We detect completion when callDepth drops back to savedCallDepth
    BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
    frame->returnPc   = savedPc;
    frame->localCount = BAS_VM_MAX_LOCALS;
    memset(frame->locals, 0, sizeof(frame->locals));

    // Jump to the SUB
    vm->pc      = codeAddr;
    vm->running = true;

    return runSubLoop(vm, savedPc, savedCallDepth, savedRunning);
}


// ============================================================
// basVmCallSubWithArgs
// ============================================================

bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount) {
    if (!vm || !vm->module) {
        return false;
    }

    if (vm->debugPaused) {
        return false;
    }

    if (codeAddr < 0 || codeAddr >= vm->module->codeLen) {
        return false;
    }

    if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE - 1) {
        return false;
    }

    int32_t savedPc        = vm->pc;
    int32_t savedCallDepth = vm->callDepth;
    bool    savedRunning   = vm->running;

    BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
    frame->returnPc   = savedPc;
    frame->localCount = BAS_VM_MAX_LOCALS;
    memset(frame->locals, 0, sizeof(frame->locals));

    // Set arguments as locals (parameter 0 = local 0, etc.)
    for (int32_t i = 0; i < argCount && i < BAS_VM_MAX_LOCALS; i++) {
        frame->locals[i] = basValCopy(args[i]);
    }

    vm->pc      = codeAddr;
    vm->running = true;

    return runSubLoop(vm, savedPc, savedCallDepth, savedRunning);
}


// ============================================================
// basVmCallSubWithArgsOut
// ============================================================

bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount) {
    if (!vm || !vm->module) {
        return false;
    }

    if (vm->debugPaused) {
        return false;
    }

    if (codeAddr < 0 || codeAddr >= vm->module->codeLen) {
        return false;
    }

    if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE - 1) {
        return false;
    }

    int32_t savedPc        = vm->pc;
    int32_t savedCallDepth = vm->callDepth;
    bool    savedRunning   = vm->running;

    BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
    frame->returnPc   = savedPc;
    frame->localCount = BAS_VM_MAX_LOCALS;
    memset(frame->locals, 0, sizeof(frame->locals));

    for (int32_t i = 0; i < argCount && i < BAS_VM_MAX_LOCALS; i++) {
        frame->locals[i] = basValCopy(args[i]);
    }

    vm->pc      = codeAddr;
    vm->running = true;

    bool ok = runSubLoop(vm, savedPc, savedCallDepth, savedRunning);

    // Read back modified locals before the frame is reused.
    // The frame at savedCallDepth still has the data even
    // though callDepth was decremented by RET.
    if (ok && outArgs && outCount > 0) {
        BasCallFrameT *doneFrame = &vm->callStack[savedCallDepth];

        for (int32_t i = 0; i < outCount && i < BAS_VM_MAX_LOCALS; i++) {
            outArgs[i] = basValCopy(doneFrame->locals[i]);
        }
    }

    return ok;
}


// ============================================================
// basVmCreate
// ============================================================

BasVmT *basVmCreate(void) {
    BasVmT *vm = (BasVmT *)calloc(1, sizeof(BasVmT));

    if (!vm) {
        return NULL;
    }

    vm->printFn         = defaultPrint;
    vm->stepOverDepth   = -1;
    vm->stepOutDepth    = -1;
    vm->runToCursorLine = -1;
    basStringSystemInit();
    return vm;
}


// ============================================================
// basVmDestroy
// ============================================================

void basVmDestroy(BasVmT *vm) {
    if (!vm) {
        return;
    }

    // Release stack values
    for (int32_t i = 0; i < vm->sp; i++) {
        basValRelease(&vm->stack[i]);
    }

    // Release global variables
    for (int32_t i = 0; i < BAS_VM_MAX_GLOBALS; i++) {
        basValRelease(&vm->globals[i]);
    }

    // Release call frame locals
    for (int32_t d = 0; d < vm->callDepth; d++) {
        for (int32_t i = 0; i < vm->callStack[d].localCount; i++) {
            basValRelease(&vm->callStack[d].locals[i]);
        }
    }

    // Release FOR stack
    for (int32_t i = 0; i < vm->forDepth; i++) {
        basValRelease(&vm->forStack[i].limit);
        basValRelease(&vm->forStack[i].step);
    }

    // Close files
    for (int32_t i = 0; i < BAS_VM_MAX_FILES; i++) {
        if (vm->files[i].handle) {
            fclose((FILE *)vm->files[i].handle);
        }
    }

    basStringSystemShutdown();
    free(vm);
}


// ============================================================
// basVmGetError
// ============================================================

const char *basVmGetError(const BasVmT *vm) {
    return vm->errorMsg;
}


// ============================================================
// Debugger API
// ============================================================

void basVmSetBreakpoints(BasVmT *vm, int32_t *lines, int32_t count) {
    if (!vm) {
        return;
    }

    vm->breakpoints    = lines;
    vm->breakpointCount = count;
}


void basVmStepInto(BasVmT *vm) {
    if (vm) {
        vm->debugBreak = true;
    }
}


void basVmStepOver(BasVmT *vm) {
    if (vm) {
        vm->stepOverDepth = vm->callDepth;
    }
}


void basVmStepOut(BasVmT *vm) {
    if (vm) {
        vm->stepOutDepth = vm->callDepth;
    }
}


void basVmRunToCursor(BasVmT *vm, int32_t line) {
    if (vm) {
        vm->runToCursorLine = line;
    }
}


int32_t basVmGetCurrentLine(const BasVmT *vm) {
    return vm ? vm->currentLine : 0;
}


// ============================================================
// basVmLoadModule
// ============================================================

void basVmLoadModule(BasVmT *vm, BasModuleT *module) {
    vm->module = module;
    vm->pc     = module->entryPoint;
}


// ============================================================
// basVmPop
// ============================================================

bool basVmPop(BasVmT *vm, BasValueT *val) {
    return pop(vm, val);
}


// ============================================================
// basVmPush
// ============================================================

bool basVmPush(BasVmT *vm, BasValueT val) {
    return push(vm, val);
}


// ============================================================
// basVmReset
// ============================================================

void basVmReset(BasVmT *vm) {
    for (int32_t i = 0; i < vm->sp; i++) {
        basValRelease(&vm->stack[i]);
    }

    for (int32_t i = 0; i < BAS_VM_MAX_GLOBALS; i++) {
        basValRelease(&vm->globals[i]);
    }

    vm->sp             = 0;
    vm->callDepth      = 0;
    vm->forDepth       = 0;
    vm->pc             = vm->module ? vm->module->entryPoint : 0;
    vm->running        = false;
    vm->yielded        = false;
    vm->dataPtr        = 0;
    vm->errorHandler   = 0;
    vm->errorNumber    = 0;
    vm->errorPc        = 0;
    vm->errorNextPc    = 0;
    vm->inErrorHandler = false;
    vm->errorMsg[0]    = '\0';
}


// ============================================================
// basVmRun
// ============================================================

BasVmResultE basVmRun(BasVmT *vm) {
    vm->running   = true;
    vm->yielded   = false;
    vm->stepCount = 0;

    while (vm->running) {
        // Check step limit
        if (vm->stepLimit > 0 && vm->stepCount >= vm->stepLimit) {
            return BAS_VM_STEP_LIMIT;
        }

        // Save PC before each instruction for RESUME support
        int32_t savedPc = vm->pc;
        BasVmResultE result = basVmStep(vm);
        vm->stepCount++;

        if (result != BAS_VM_OK) {
            // If an error handler is set and this is a trappable error,
            // jump to the handler instead of stopping execution
            if (vm->errorHandler != 0 && !vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) {
                vm->errorPc        = savedPc;
                vm->errorNextPc    = vm->pc;
                vm->inErrorHandler = true;
                vm->pc             = vm->errorHandler;
                continue;
            }

            vm->running = false;
            return result;
        }
    }

    return BAS_VM_HALTED;
}


// ============================================================
// basVmSetDoEventsCallback
// ============================================================

void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx) {
    vm->doEventsFn  = fn;
    vm->doEventsCtx = ctx;
}


// ============================================================
// basVmSetCurrentForm
// ============================================================

void basVmSetCurrentForm(BasVmT *vm, void *formRef) {
    vm->currentForm = formRef;
}


// ============================================================
// basVmSetCurrentFormVars
// ============================================================

void basVmSetCurrentFormVars(BasVmT *vm, BasValueT *vars, int32_t count) {
    vm->currentFormVars     = vars;
    vm->currentFormVarCount = count;
}


// ============================================================
// basVmSetStepLimit
// ============================================================

void basVmSetStepLimit(BasVmT *vm, int32_t limit) {
    vm->stepLimit = limit;
}


// ============================================================
// basVmSetInputCallback
// ============================================================

void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx) {
    vm->inputFn  = fn;
    vm->inputCtx = ctx;
}


// ============================================================
// basVmSetPrintCallback
// ============================================================

void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx) {
    vm->printFn  = fn;
    vm->printCtx = ctx;
}


// ============================================================
// basVmSetUiCallbacks
// ============================================================

void basVmSetExternCallbacks(BasVmT *vm, const BasExternCallbacksT *ext) {
    vm->ext = *ext;
}


void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui) {
    vm->ui = *ui;
}


void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql) {
    vm->sql = *sql;
}


// ============================================================
// basVmStep -- execute one instruction
// ============================================================

BasVmResultE basVmStep(BasVmT *vm) {
    if (!vm->module || vm->pc < 0 || vm->pc >= vm->module->codeLen) {
        vm->running = false;
        return BAS_VM_HALTED;
    }

    uint8_t op = vm->module->code[vm->pc++];

    switch (op) {
        case OP_NOP:
            break;

        // ============================================================
        // Stack operations
        // ============================================================

        case OP_PUSH_INT16: {
            int16_t val = readInt16(vm);

            if (!push(vm, basValInteger(val))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_PUSH_INT32: {
            int16_t lo = readInt16(vm);
            int16_t hi = readInt16(vm);
            int32_t val = ((int32_t)hi << 16) | (uint16_t)lo;

            if (!push(vm, basValLong(val))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_PUSH_FLT32: {
            float val;
            memcpy(&val, &vm->module->code[vm->pc], sizeof(float));
            vm->pc += sizeof(float);

            if (!push(vm, basValSingle(val))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_PUSH_FLT64: {
            double val;
            memcpy(&val, &vm->module->code[vm->pc], sizeof(double));
            vm->pc += sizeof(double);

            if (!push(vm, basValDouble(val))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_PUSH_STR: {
            uint16_t idx = readUint16(vm);

            if (idx < (uint16_t)vm->module->constCount) {
                if (!push(vm, basValString(vm->module->constants[idx]))) {
                    return BAS_VM_STACK_OVERFLOW;
                }
            } else {
                if (!push(vm, basValStringFromC(""))) {
                    return BAS_VM_STACK_OVERFLOW;
                }
            }

            break;
        }

        case OP_PUSH_TRUE:
            if (!push(vm, basValBool(true))) {
                return BAS_VM_STACK_OVERFLOW;
            }
            break;

        case OP_PUSH_FALSE:
            if (!push(vm, basValBool(false))) {
                return BAS_VM_STACK_OVERFLOW;
            }
            break;

        case OP_POP: {
            BasValueT val;

            if (!pop(vm, &val)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            basValRelease(&val);
            break;
        }

        case OP_DUP: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (!push(vm, basValCopy(vm->stack[vm->sp - 1]))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        // ============================================================
        // Variable access
        // ============================================================

        case OP_LOAD_LOCAL: {
            uint16_t idx = readUint16(vm);
            BasCallFrameT *frame = currentFrame(vm);

            if (!frame || idx >= (uint16_t)frame->localCount) {
                runtimeError(vm, 9, "Invalid local variable index");
                return BAS_VM_ERROR;
            }

            // ByRef: if the local holds a reference, dereference it
            BasValueT *slot = &frame->locals[idx];
            BasValueT val = (slot->type == BAS_TYPE_REF) ? *slot->refVal : *slot;

            if (!push(vm, basValCopy(val))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_STORE_LOCAL: {
            uint16_t idx = readUint16(vm);
            BasCallFrameT *frame = currentFrame(vm);
            BasValueT val;

            if (!pop(vm, &val)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (!frame || idx >= (uint16_t)frame->localCount) {
                basValRelease(&val);
                runtimeError(vm, 9, "Invalid local variable index");
                return BAS_VM_ERROR;
            }

            // ByRef: if the local holds a reference, store through it
            BasValueT *slot = &frame->locals[idx];

            if (slot->type == BAS_TYPE_REF) {
                basValRelease(slot->refVal);
                *slot->refVal = val;
            } else {
                basValRelease(slot);
                *slot = val;
            }

            break;
        }

        case OP_LOAD_GLOBAL: {
            uint16_t idx = readUint16(vm);

            if (idx >= BAS_VM_MAX_GLOBALS) {
                runtimeError(vm, 9, "Invalid global variable index");
                return BAS_VM_ERROR;
            }

            if (!push(vm, basValCopy(vm->globals[idx]))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_STORE_GLOBAL: {
            uint16_t idx = readUint16(vm);
            BasValueT val;

            if (!pop(vm, &val)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (idx >= BAS_VM_MAX_GLOBALS) {
                basValRelease(&val);
                runtimeError(vm, 9, "Invalid global variable index");
                return BAS_VM_ERROR;
            }

            basValRelease(&vm->globals[idx]);
            vm->globals[idx] = val;
            break;
        }

        case OP_PUSH_LOCAL_ADDR: {
            uint16_t idx = readUint16(vm);
            BasCallFrameT *frame = currentFrame(vm);

            if (!frame || idx >= (uint16_t)frame->localCount) {
                runtimeError(vm, 9, "Invalid local variable index");
                return BAS_VM_ERROR;
            }

            BasValueT ref;
            ref.type   = BAS_TYPE_REF;
            ref.refVal = &frame->locals[idx];

            if (!push(vm, ref)) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_PUSH_GLOBAL_ADDR: {
            uint16_t idx = readUint16(vm);

            if (idx >= BAS_VM_MAX_GLOBALS) {
                runtimeError(vm, 9, "Invalid global variable index");
                return BAS_VM_ERROR;
            }

            BasValueT ref;
            ref.type   = BAS_TYPE_REF;
            ref.refVal = &vm->globals[idx];

            if (!push(vm, ref)) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_LOAD_REF: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];

            if (top->type != BAS_TYPE_REF || !top->refVal) {
                runtimeError(vm, 9, "Expected reference");
                return BAS_VM_ERROR;
            }

            BasValueT val = basValCopy(*top->refVal);
            *top = val;
            break;
        }

        case OP_STORE_REF: {
            BasValueT val;
            BasValueT ref;

            if (!pop(vm, &val) || !pop(vm, &ref)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (ref.type != BAS_TYPE_REF || !ref.refVal) {
                basValRelease(&val);
                runtimeError(vm, 9, "Expected reference");
                return BAS_VM_ERROR;
            }

            basValRelease(ref.refVal);
            *ref.refVal = val;
            break;
        }

        // ============================================================
        // Arithmetic
        // ============================================================

        case OP_ADD_INT:
        case OP_SUB_INT:
        case OP_MUL_INT:
        case OP_IDIV_INT:
        case OP_MOD_INT:
        case OP_ADD_FLT:
        case OP_SUB_FLT:
        case OP_MUL_FLT:
        case OP_DIV_FLT:
        case OP_POW:
            return execArith(vm, op);

        case OP_NEG_INT: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];

            if (top->type == BAS_TYPE_INTEGER) {
                top->intVal = -top->intVal;
            } else if (top->type == BAS_TYPE_LONG) {
                top->longVal = -top->longVal;
            } else {
                double n = basValToNumber(*top);
                basValRelease(top);
                *top = basValDouble(-n);
            }

            break;
        }

        case OP_NEG_FLT: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];

            if (top->type == BAS_TYPE_SINGLE) {
                top->sngVal = -top->sngVal;
            } else if (top->type == BAS_TYPE_DOUBLE) {
                top->dblVal = -top->dblVal;
            } else {
                double n = basValToNumber(*top);
                basValRelease(top);
                *top = basValDouble(-n);
            }

            break;
        }

        // ============================================================
        // String operations
        // ============================================================

        case OP_STR_CONCAT:
        case OP_STR_LEFT:
        case OP_STR_RIGHT:
        case OP_STR_MID:
        case OP_STR_MID2:
        case OP_STR_LEN:
        case OP_STR_INSTR:
        case OP_STR_INSTR3:
        case OP_STR_UCASE:
        case OP_STR_LCASE:
        case OP_STR_TRIM:
        case OP_STR_LTRIM:
        case OP_STR_RTRIM:
        case OP_STR_CHR:
        case OP_STR_ASC:
        case OP_STR_SPACE:
        case OP_STR_FIXLEN:
        case OP_STR_MID_ASGN:
            return execStringOp(vm, op);

        // ============================================================
        // Comparison
        // ============================================================

        case OP_CMP_EQ:
        case OP_CMP_NE:
        case OP_CMP_LT:
        case OP_CMP_GT:
        case OP_CMP_LE:
        case OP_CMP_GE:
            return execCompare(vm, op);

        // ============================================================
        // Logical / bitwise
        // ============================================================

        case OP_AND:
        case OP_OR:
        case OP_NOT:
        case OP_XOR:
        case OP_EQV:
        case OP_IMP:
            return execLogical(vm, op);

        // ============================================================
        // Control flow
        // ============================================================

        case OP_JMP: {
            int16_t offset = readInt16(vm);
            vm->pc += offset;
            break;
        }

        case OP_JMP_TRUE: {
            int16_t offset = readInt16(vm);
            BasValueT val;

            if (!pop(vm, &val)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (basValIsTruthy(val)) {
                vm->pc += offset;
            }

            basValRelease(&val);
            break;
        }

        case OP_JMP_FALSE: {
            int16_t offset = readInt16(vm);
            BasValueT val;

            if (!pop(vm, &val)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (!basValIsTruthy(val)) {
                vm->pc += offset;
            }

            basValRelease(&val);
            break;
        }

        case OP_CALL: {
            uint16_t addr     = readUint16(vm);
            uint8_t  argc     = readUint8(vm);
            uint8_t  baseSlot = readUint8(vm);

            if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE) {
                return BAS_VM_CALL_OVERFLOW;
            }

            BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
            frame->returnPc   = vm->pc;
            frame->localCount = BAS_VM_MAX_LOCALS;

            // Zero all local slots
            memset(frame->locals, 0, sizeof(frame->locals));

            // Pop arguments into locals starting at baseSlot (in reverse order)
            for (int32_t i = baseSlot + argc - 1; i >= baseSlot; i--) {
                if (!pop(vm, &frame->locals[i])) {
                    return BAS_VM_STACK_UNDERFLOW;
                }
            }

            vm->pc = addr;
            break;
        }

        case OP_RET: {
            if (vm->callDepth <= 0) {
                vm->running = false;
                return BAS_VM_HALTED;
            }

            BasCallFrameT *frame = &vm->callStack[--vm->callDepth];

            // Release locals
            for (int32_t i = 0; i < frame->localCount; i++) {
                basValRelease(&frame->locals[i]);
            }

            vm->pc = frame->returnPc;
            break;
        }

        case OP_RET_VAL: {
            // Like RET but leaves TOS as the return value
            BasValueT retVal;

            if (!pop(vm, &retVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (vm->callDepth <= 0) {
                basValRelease(&retVal);
                vm->running = false;
                return BAS_VM_HALTED;
            }

            BasCallFrameT *frame = &vm->callStack[--vm->callDepth];

            for (int32_t i = 0; i < frame->localCount; i++) {
                basValRelease(&frame->locals[i]);
            }

            vm->pc = frame->returnPc;

            if (!push(vm, retVal)) {
                basValRelease(&retVal);
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_GOSUB_RET: {
            // GOSUB return: pop integer from eval stack, set PC to that value
            BasValueT retAddr;

            if (!pop(vm, &retAddr)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            vm->pc = (int32_t)basValToNumber(retAddr);
            basValRelease(&retAddr);
            break;
        }

        case OP_FOR_INIT: {
            uint16_t varIdx   = readUint16(vm);
            uint8_t  scopeTag = readUint8(vm);
            BasValueT stepVal;
            BasValueT limitVal;

            if (!pop(vm, &stepVal) || !pop(vm, &limitVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (vm->forDepth >= BAS_VM_MAX_FOR_DEPTH) {
                basValRelease(&stepVal);
                basValRelease(&limitVal);
                runtimeError(vm, 26, "FOR loop nesting too deep");
                return BAS_VM_ERROR;
            }

            BasForStateT *fs = &vm->forStack[vm->forDepth++];
            fs->varIdx  = varIdx;
            fs->scopeTag = scopeTag;
            fs->limit   = limitVal;
            fs->step    = stepVal;
            fs->loopTop = vm->pc;
            break;
        }

        case OP_FOR_NEXT: {
            uint16_t varIdx        = readUint16(vm);
            uint8_t  scopeTag      = readUint8(vm);
            int16_t  loopTopOffset = readInt16(vm);

            if (vm->forDepth <= 0) {
                runtimeError(vm, 1, "NEXT without FOR");
                return BAS_VM_ERROR;
            }

            BasForStateT *fs = &vm->forStack[vm->forDepth - 1];

            if (fs->varIdx != (int32_t)varIdx) {
                runtimeError(vm, 1, "NEXT variable mismatch");
                return BAS_VM_ERROR;
            }

            // Get pointer to the loop variable
            BasValueT *varSlot;

            if (scopeTag == 1) {
                BasCallFrameT *frame = currentFrame(vm);

                if (!frame || varIdx >= (uint16_t)frame->localCount) {
                    runtimeError(vm, 9, "Invalid local variable index");
                    return BAS_VM_ERROR;
                }

                varSlot = &frame->locals[varIdx];
            } else if (scopeTag == 2) {
                if (!vm->currentFormVars || varIdx >= (uint16_t)vm->currentFormVarCount) {
                    runtimeError(vm, 9, "Invalid form variable index");
                    return BAS_VM_ERROR;
                }

                varSlot = &vm->currentFormVars[varIdx];
            } else {
                if (varIdx >= BAS_VM_MAX_GLOBALS) {
                    runtimeError(vm, 9, "Invalid global variable index");
                    return BAS_VM_ERROR;
                }

                varSlot = &vm->globals[varIdx];
            }

            // Increment: var = var + step
            double varVal  = basValToNumber(*varSlot);
            double stepVal = basValToNumber(fs->step);
            double limVal  = basValToNumber(fs->limit);
            varVal += stepVal;

            basValRelease(varSlot);
            *varSlot = basValDouble(varVal);

            // Test: if step > 0 then continue while var <= limit
            //       if step < 0 then continue while var >= limit
            bool cont;

            if (stepVal >= 0) {
                cont = (varVal <= limVal);
            } else {
                cont = (varVal >= limVal);
            }

            if (cont) {
                vm->pc += loopTopOffset;
            } else {
                // Loop done -- pop FOR state
                basValRelease(&fs->limit);
                basValRelease(&fs->step);
                vm->forDepth--;
            }

            break;
        }

        case OP_FOR_POP: {
            if (vm->forDepth <= 0) {
                runtimeError(vm, 1, "FOR stack underflow");
                return BAS_VM_ERROR;
            }

            BasForStateT *fs = &vm->forStack[--vm->forDepth];
            basValRelease(&fs->limit);
            basValRelease(&fs->step);
            break;
        }

        // ============================================================
        // Type conversion
        // ============================================================

        case OP_CONV_INT_FLT: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            double n = basValToNumber(*top);
            basValRelease(top);
            *top = basValSingle((float)n);
            break;
        }

        case OP_CONV_FLT_INT: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            BasValueT conv = basValToInteger(*top);
            basValRelease(top);
            *top = conv;
            break;
        }

        case OP_CONV_INT_STR: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            BasValueT conv = basValToString(*top);
            basValRelease(top);
            *top = conv;
            break;
        }

        case OP_CONV_STR_INT: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            BasValueT conv = basValToInteger(*top);
            basValRelease(top);
            *top = conv;
            break;
        }

        case OP_CONV_FLT_STR: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            BasValueT conv = basValToString(*top);
            basValRelease(top);
            *top = conv;
            break;
        }

        case OP_CONV_STR_FLT: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            BasValueT conv = basValToDouble(*top);
            basValRelease(top);
            *top = conv;
            break;
        }

        case OP_CONV_INT_LONG: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            BasValueT conv = basValToLong(*top);
            basValRelease(top);
            *top = conv;
            break;
        }

        case OP_CONV_LONG_INT: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            BasValueT conv = basValToInteger(*top);
            basValRelease(top);
            *top = conv;
            break;
        }

        // ============================================================
        // I/O
        // ============================================================

        case OP_PRINT:
            return execPrint(vm);

        case OP_PRINT_NL:
            if (vm->printFn) {
                vm->printFn(vm->printCtx, "", true);
            }
            break;

        case OP_PRINT_TAB:
            if (vm->printFn) {
                vm->printFn(vm->printCtx, "\t", false);
            }
            break;

        case OP_PRINT_SPC: {
            uint8_t n = readUint8(vm);
            char spaces[256];
            int32_t count = n < 255 ? n : 255;
            memset(spaces, ' ', count);
            spaces[count] = '\0';

            if (vm->printFn) {
                vm->printFn(vm->printCtx, spaces, false);
            }

            break;
        }

        case OP_PRINT_SPC_N: {
            BasValueT nVal;

            if (!pop(vm, &nVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t count = (int32_t)basValToNumber(nVal);
            basValRelease(&nVal);

            if (count < 0) {
                count = 0;
            }

            if (count > 255) {
                count = 255;
            }

            char spaces[256];
            memset(spaces, ' ', count);
            spaces[count] = '\0';

            if (vm->printFn) {
                vm->printFn(vm->printCtx, spaces, false);
            }

            break;
        }

        case OP_PRINT_TAB_N: {
            BasValueT nVal;

            if (!pop(vm, &nVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t col = (int32_t)basValToNumber(nVal);
            basValRelease(&nVal);

            if (col < 1) {
                col = 1;
            }

            if (col > 255) {
                col = 255;
            }

            // TAB outputs spaces to reach the specified column
            // For simplicity, just output (col-1) spaces
            char spaces[256];
            int32_t count = col - 1;
            memset(spaces, ' ', count);
            spaces[count] = '\0';

            if (vm->printFn) {
                vm->printFn(vm->printCtx, spaces, false);
            }

            break;
        }

        case OP_PRINT_USING: {
            // Pop value and format string (format is below value on stack)
            BasValueT val;
            BasValueT fmtVal;

            if (!pop(vm, &val) || !pop(vm, &fmtVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT fmtStr = basValToString(fmtVal);
            basValRelease(&fmtVal);

            const char *fmt = fmtStr.strVal->data;
            int32_t fmtLen  = fmtStr.strVal->len;
            char buf[256];

            if (val.type == BAS_TYPE_STRING) {
                // String formatting
                const char *src = val.strVal ? val.strVal->data : "";

                if (fmtLen > 0 && fmt[0] == '!') {
                    // First character only
                    buf[0] = src[0] ? src[0] : ' ';
                    buf[1] = '\0';
                } else if (fmtLen > 0 && fmt[0] == '&') {
                    // Entire string
                    snprintf(buf, sizeof(buf), "%s", src);
                } else if (fmtLen >= 2 && fmt[0] == '\\') {
                    // Fixed-width: count characters between backslashes
                    int32_t width = 2;
                    for (int32_t i = 1; i < fmtLen; i++) {
                        if (fmt[i] == '\\') {
                            width = i + 1;
                            break;
                        }
                        width = i + 2;
                    }
                    int32_t srcLen  = (int32_t)strlen(src);
                    int32_t copyLen = srcLen < width ? srcLen : width;
                    memcpy(buf, src, copyLen);
                    for (int32_t i = copyLen; i < width; i++) {
                        buf[i] = ' ';
                    }
                    buf[width] = '\0';
                } else {
                    snprintf(buf, sizeof(buf), "%s", src);
                }
            } else {
                // Numeric formatting
                double n = basValToNumber(val);

                // Parse format flags
                bool asteriskFill = false;    // ** fill leading spaces with *
                bool dollarFloat  = false;    // $$ floating dollar sign
                bool plusAtStart  = false;    // + at start: always show sign
                bool plusAtEnd    = false;    // + at end: always show sign
                bool minusAtEnd  = false;    // - at end: show minus for negative
                bool sciNotation  = false;    // ^^^^ scientific notation
                bool hasDecimal   = false;
                bool hasComma     = false;
                int32_t digitsBefore = 0;
                int32_t digitsAfter  = 0;

                // Check for ** at start
                if (fmtLen >= 2 && fmt[0] == '*' && fmt[1] == '*') {
                    asteriskFill = true;
                }

                // Check for $$ (may follow **)
                int32_t scanStart = 0;
                if (asteriskFill) {
                    scanStart = 2;
                }
                if (fmtLen >= scanStart + 2 && fmt[scanStart] == '$' && fmt[scanStart + 1] == '$') {
                    dollarFloat = true;
                }

                // Check for + at start or end
                if (fmtLen > 0 && fmt[0] == '+') {
                    plusAtStart = true;
                }
                if (fmtLen > 0 && fmt[fmtLen - 1] == '+') {
                    plusAtEnd = true;
                }

                // Check for - at end
                if (fmtLen > 0 && fmt[fmtLen - 1] == '-') {
                    minusAtEnd = true;
                }

                // Check for ^^^^ (scientific notation)
                for (int32_t i = 0; i <= fmtLen - 4; i++) {
                    if (fmt[i] == '^' && fmt[i+1] == '^' && fmt[i+2] == '^' && fmt[i+3] == '^') {
                        sciNotation = true;
                        break;
                    }
                }

                // Count # and 0 digits before and after decimal
                for (int32_t i = 0; i < fmtLen; i++) {
                    if (fmt[i] == '.') {
                        hasDecimal = true;
                    } else if (fmt[i] == ',') {
                        hasComma = true;
                    } else if (fmt[i] == '#' || fmt[i] == '0') {
                        if (hasDecimal) {
                            digitsAfter++;
                        } else {
                            digitsBefore++;
                        }
                    } else if (fmt[i] == '*') {
                        if (!hasDecimal) {
                            digitsBefore++;
                        }
                    }
                }

                if (sciNotation) {
                    // Scientific notation
                    char sciFmt[32];
                    int32_t decimals = hasDecimal ? digitsAfter : 0;
                    snprintf(sciFmt, sizeof(sciFmt), "%%.%dE", (int)decimals);
                    snprintf(buf, sizeof(buf), sciFmt, n);
                } else {
                    // Standard formatting
                    bool isNeg = (n < 0);
                    double absN = isNeg ? -n : n;
                    int32_t decimals = hasDecimal ? digitsAfter : 0;

                    char numBuf[128];
                    snprintf(numBuf, sizeof(numBuf), "%.*f", (int)decimals, absN);

                    // Split into integer and decimal parts
                    char intPart[128];
                    char decPart[128];
                    intPart[0] = '\0';
                    decPart[0] = '\0';
                    char *dot = strchr(numBuf, '.');
                    if (dot) {
                        int32_t intLen = (int32_t)(dot - numBuf);
                        memcpy(intPart, numBuf, intLen);
                        intPart[intLen] = '\0';
                        strncpy(decPart, dot + 1, sizeof(decPart) - 1);
                        decPart[sizeof(decPart) - 1] = '\0';
                    } else {
                        strncpy(intPart, numBuf, sizeof(intPart) - 1);
                        intPart[sizeof(intPart) - 1] = '\0';
                    }

                    // Apply thousands separator
                    char fmtIntPart[128];
                    if (hasComma) {
                        int32_t srcLen = (int32_t)strlen(intPart);
                        int32_t dstIdx = 0;
                        for (int32_t i = 0; i < srcLen; i++) {
                            if (i > 0 && (srcLen - i) % 3 == 0) {
                                fmtIntPart[dstIdx++] = ',';
                            }
                            fmtIntPart[dstIdx++] = intPart[i];
                        }
                        fmtIntPart[dstIdx] = '\0';
                    } else {
                        strncpy(fmtIntPart, intPart, sizeof(fmtIntPart) - 1);
                        fmtIntPart[sizeof(fmtIntPart) - 1] = '\0';
                    }

                    // Build result
                    int32_t idx = 0;

                    // Sign prefix
                    if (plusAtStart) {
                        buf[idx++] = isNeg ? '-' : '+';
                    } else if (isNeg && !minusAtEnd) {
                        buf[idx++] = '-';
                    }

                    // Dollar sign
                    if (dollarFloat) {
                        buf[idx++] = '$';
                    }

                    // Pad leading
                    int32_t intLen = (int32_t)strlen(fmtIntPart);
                    int32_t padNeeded = digitsBefore - intLen;
                    char fillChar = asteriskFill ? '*' : ' ';

                    for (int32_t i = 0; i < padNeeded; i++) {
                        buf[idx++] = fillChar;
                    }

                    // Integer part
                    for (int32_t i = 0; fmtIntPart[i]; i++) {
                        buf[idx++] = fmtIntPart[i];
                    }

                    // Decimal part
                    if (hasDecimal) {
                        buf[idx++] = '.';
                        for (int32_t i = 0; i < decimals; i++) {
                            buf[idx++] = decPart[i] ? decPart[i] : '0';
                        }
                    }

                    // Trailing sign
                    if (plusAtEnd) {
                        buf[idx++] = isNeg ? '-' : '+';
                    } else if (minusAtEnd) {
                        buf[idx++] = isNeg ? '-' : ' ';
                    }

                    buf[idx] = '\0';
                }
            }

            basValRelease(&val);

            // Push format string back (PRINT USING reuses it for multiple values)
            if (!push(vm, fmtStr)) {
                return BAS_VM_STACK_OVERFLOW;
            }

            // Push formatted result
            if (!push(vm, basValStringFromC(buf))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_INPUT: {
            // Pop the prompt string pushed by the parser
            BasValueT promptVal;

            if (!pop(vm, &promptVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            // Build the full prompt: "user prompt? "
            char promptBuf[512];
            const char *userPrompt = "";

            if (promptVal.type == BAS_TYPE_STRING && promptVal.strVal) {
                userPrompt = promptVal.strVal->data;
            }

            if (userPrompt[0]) {
                snprintf(promptBuf, sizeof(promptBuf), "%s? ", userPrompt);
            } else {
                snprintf(promptBuf, sizeof(promptBuf), "? ");
            }

            char buf[1024];
            buf[0] = '\0';

            if (vm->inputFn) {
                if (!vm->inputFn(vm->inputCtx, promptBuf, buf, sizeof(buf))) {
                    buf[0] = '\0';
                }
            }

            basValRelease(&promptVal);

            if (!push(vm, basValStringFromC(buf))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        // ============================================================
        // Math built-ins
        // ============================================================

        case OP_MATH_ABS:
        case OP_MATH_INT:
        case OP_MATH_FIX:
        case OP_MATH_SGN:
        case OP_MATH_SQR:
        case OP_MATH_SIN:
        case OP_MATH_COS:
        case OP_MATH_TAN:
        case OP_MATH_ATN:
        case OP_MATH_LOG:
        case OP_MATH_EXP:
        case OP_MATH_RND:
        case OP_MATH_RANDOMIZE:
            return execMath(vm, op);

        // ============================================================
        // Conversion built-ins
        // ============================================================

        case OP_STR_VAL: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            double n = basValToNumber(*top);
            basValRelease(top);
            *top = basValDouble(n);
            break;
        }

        case OP_STR_STRF: {
            // STR$(n): VB convention -- leading space for positive, "-" for negative
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            double n = basValToNumber(*top);
            basValRelease(top);

            char buf[64];

            if (n >= 0.0) {
                snprintf(buf, sizeof(buf), " %g", n);
            } else {
                snprintf(buf, sizeof(buf), "%g", n);
            }

            top->type   = BAS_TYPE_STRING;
            top->strVal = basStringNew(buf, (int32_t)strlen(buf));
            break;
        }

        case OP_STR_HEX: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            int32_t n = (int32_t)basValToNumber(*top);
            char buf[16];
            snprintf(buf, sizeof(buf), "%X", (unsigned int)n);
            basValRelease(top);
            *top = basValStringFromC(buf);
            break;
        }

        case OP_STR_STRING: {
            // STRING$(n, char)
            BasValueT charVal;
            BasValueT countVal;

            if (!pop(vm, &charVal) || !pop(vm, &countVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t count = (int32_t)basValToNumber(countVal);
            basValRelease(&countVal);

            char ch;

            if (charVal.type == BAS_TYPE_STRING && charVal.strVal && charVal.strVal->len > 0) {
                ch = charVal.strVal->data[0];
            } else {
                ch = (char)(int32_t)basValToNumber(charVal);
            }

            basValRelease(&charVal);

            if (count < 0) {
                count = 0;
            }

            if (count > 32767) {
                count = 32767;
            }

            BasStringT *s = basStringAlloc(count + 1);
            memset(s->data, ch, count);
            s->data[count] = '\0';
            s->len = count;

            if (!push(vm, basValString(s))) {
                basStringUnref(s);
                return BAS_VM_STACK_OVERFLOW;
            }

            basStringUnref(s);
            break;
        }

        // ============================================================
        // Extended built-ins
        // ============================================================

        case OP_MATH_TIMER: {
            // Push seconds since midnight as a double
            time_t now = time(NULL);
            struct tm *t = localtime(&now);
            double secs = (double)t->tm_hour * 3600.0 + (double)t->tm_min * 60.0 + (double)t->tm_sec;

            if (!push(vm, basValDouble(secs))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_DATE_STR: {
            // Push DATE$ as "MM-DD-YYYY"
            time_t now = time(NULL);
            struct tm *t = localtime(&now);
            char buf[32];
            snprintf(buf, sizeof(buf), "%02d-%02d-%04d", t->tm_mon + 1, t->tm_mday, t->tm_year + 1900);

            if (!push(vm, basValStringFromC(buf))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_TIME_STR: {
            // Push TIME$ as "HH:MM:SS"
            time_t now = time(NULL);
            struct tm *t = localtime(&now);
            char buf[16];
            snprintf(buf, sizeof(buf), "%02d:%02d:%02d", t->tm_hour, t->tm_min, t->tm_sec);

            if (!push(vm, basValStringFromC(buf))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_SLEEP: {
            // Pop seconds, sleep
            BasValueT val;

            if (!pop(vm, &val)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t secs = (int32_t)basValToNumber(val);
            basValRelease(&val);

            if (secs > 0) {
                sleep((unsigned int)secs);
            }

            break;
        }

        case OP_ENVIRON: {
            // Pop env var name, push value string
            BasValueT nameVal;

            if (!pop(vm, &nameVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT nameStr = basValToString(nameVal);
            basValRelease(&nameVal);

            const char *envVal = getenv(nameStr.strVal->data);
            basValRelease(&nameStr);

            if (envVal == NULL) {
                envVal = "";
            }

            if (!push(vm, basValStringFromC(envVal))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        // ============================================================
        // File I/O
        // ============================================================

        case OP_FILE_OPEN:
        case OP_FILE_CLOSE:
        case OP_FILE_PRINT:
        case OP_FILE_INPUT:
        case OP_FILE_EOF:
        case OP_FILE_LINE_INPUT:
        case OP_FILE_WRITE:
        case OP_FILE_WRITE_SEP:
        case OP_FILE_WRITE_NL:
        case OP_FILE_GET:
        case OP_FILE_PUT:
        case OP_FILE_SEEK:
        case OP_FILE_LOF:
        case OP_FILE_LOC:
        case OP_FILE_FREEFILE:
        case OP_FILE_INPUT_N:
            return execFileOp(vm, op);

        // ============================================================
        // DoEvents
        // ============================================================

        case OP_DO_EVENTS:
            if (vm->doEventsFn) {
                if (!vm->doEventsFn(vm->doEventsCtx)) {
                    vm->running = false;
                    return BAS_VM_HALTED;
                }
            }
            break;

        // ============================================================
        // Error handling
        // ============================================================

        case OP_ON_ERROR: {
            int16_t handler = readInt16(vm);
            vm->errorHandler = (handler == 0) ? 0 : vm->pc + handler;
            break;
        }

        case OP_ERR_NUM:
            if (!push(vm, basValInteger((int16_t)vm->errorNumber))) {
                return BAS_VM_STACK_OVERFLOW;
            }
            break;

        case OP_ERR_CLEAR:
            vm->errorNumber = 0;
            vm->errorMsg[0] = '\0';
            break;

        case OP_RESUME:
            // RESUME -- re-execute the statement that caused the error
            vm->pc             = vm->errorPc;
            vm->errorNumber    = 0;
            vm->errorMsg[0]    = '\0';
            vm->inErrorHandler = false;
            break;

        case OP_RESUME_NEXT:
            // RESUME NEXT -- continue at next statement after the error
            vm->pc             = vm->errorNextPc;
            vm->errorNumber    = 0;
            vm->errorMsg[0]    = '\0';
            vm->inErrorHandler = false;
            break;

        case OP_RAISE_ERR: {
            BasValueT errVal;

            if (!pop(vm, &errVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t errNum = (int32_t)basValToNumber(errVal);
            basValRelease(&errVal);
            runtimeError(vm, errNum, "User-defined error");
            return BAS_VM_ERROR;
        }

        // ============================================================
        // Array / UDT operations
        // ============================================================

        case OP_DIM_ARRAY: {
            uint8_t dims        = readUint8(vm);
            uint8_t elementType = readUint8(vm);

            // dims=0 with elementType=BAS_TYPE_UDT means allocate a UDT instance
            if (dims == 0 && elementType == BAS_TYPE_UDT) {
                BasValueT fieldCountVal;
                BasValueT typeIdVal;

                if (!pop(vm, &fieldCountVal) || !pop(vm, &typeIdVal)) {
                    return BAS_VM_STACK_UNDERFLOW;
                }

                int32_t fieldCount = (int32_t)basValToNumber(fieldCountVal);
                int32_t typeId     = (int32_t)basValToNumber(typeIdVal);
                basValRelease(&fieldCountVal);
                basValRelease(&typeIdVal);

                BasUdtT *udt = basUdtNew(typeId, fieldCount);

                if (!udt) {
                    runtimeError(vm, 7, "Out of memory allocating TYPE");
                    return BAS_VM_OUT_OF_MEMORY;
                }

                BasValueT udtVal;
                udtVal.type   = BAS_TYPE_UDT;
                udtVal.udtVal = udt;

                if (!push(vm, udtVal)) {
                    basUdtFree(udt);
                    return BAS_VM_STACK_OVERFLOW;
                }

                break;
            }

            // For UDT arrays, parser pushes typeId and fieldCount after bounds
            int32_t udtTypeId   = -1;
            int32_t udtFieldCnt = 0;

            if (elementType == BAS_TYPE_UDT) {
                BasValueT fcVal;
                BasValueT tiVal;

                if (!pop(vm, &fcVal) || !pop(vm, &tiVal)) {
                    return BAS_VM_STACK_UNDERFLOW;
                }

                udtFieldCnt = (int32_t)basValToNumber(fcVal);
                udtTypeId   = (int32_t)basValToNumber(tiVal);
                basValRelease(&fcVal);
                basValRelease(&tiVal);
            }

            // Normal array allocation: parser pushes (lbound, ubound) pairs per dim
            int32_t lbounds[BAS_ARRAY_MAX_DIMS];
            int32_t ubounds[BAS_ARRAY_MAX_DIMS];

            // Pop bounds in reverse order (last dim first)
            for (int32_t d = dims - 1; d >= 0; d--) {
                BasValueT ubVal;
                BasValueT lbVal;

                if (!pop(vm, &ubVal) || !pop(vm, &lbVal)) {
                    return BAS_VM_STACK_UNDERFLOW;
                }

                ubounds[d] = (int32_t)basValToNumber(ubVal);
                lbounds[d] = (int32_t)basValToNumber(lbVal);
                basValRelease(&ubVal);
                basValRelease(&lbVal);
            }

            BasArrayT *arr = basArrayNew(dims, lbounds, ubounds, elementType);

            if (!arr) {
                runtimeError(vm, 7, "Out of memory allocating array");
                return BAS_VM_OUT_OF_MEMORY;
            }

            // Initialize UDT array elements with proper UDT instances
            if (elementType == BAS_TYPE_UDT && udtTypeId >= 0) {
                for (int32_t i = 0; i < arr->totalElements; i++) {
                    BasUdtT *udt = basUdtNew(udtTypeId, udtFieldCnt);

                    if (!udt) {
                        basArrayFree(arr);
                        runtimeError(vm, 7, "Out of memory allocating TYPE array elements");
                        return BAS_VM_OUT_OF_MEMORY;
                    }

                    arr->elements[i].type   = BAS_TYPE_UDT;
                    arr->elements[i].udtVal = udt;
                }
            }

            BasValueT arrVal;
            arrVal.type   = BAS_TYPE_ARRAY;
            arrVal.arrVal = arr;

            if (!push(vm, arrVal)) {
                basArrayFree(arr);
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_LOAD_ARRAY: {
            uint8_t dims = readUint8(vm);
            int32_t indices[BAS_ARRAY_MAX_DIMS];

            // Pop indices in reverse order
            for (int32_t d = dims - 1; d >= 0; d--) {
                BasValueT idxVal;

                if (!pop(vm, &idxVal)) {
                    return BAS_VM_STACK_UNDERFLOW;
                }

                indices[d] = (int32_t)basValToNumber(idxVal);
                basValRelease(&idxVal);
            }

            // Pop array reference
            BasValueT arrRef;

            if (!pop(vm, &arrRef)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) {
                basValRelease(&arrRef);
                runtimeError(vm, 13, "Not an array");
                return BAS_VM_TYPE_MISMATCH;
            }

            int32_t flatIdx = basArrayIndex(arrRef.arrVal, indices, dims);

            if (flatIdx < 0) {
                basValRelease(&arrRef);
                runtimeError(vm, 9, "Subscript out of range");
                return BAS_VM_SUBSCRIPT_RANGE;
            }

            BasValueT elem = basValCopy(arrRef.arrVal->elements[flatIdx]);
            basValRelease(&arrRef);

            if (!push(vm, elem)) {
                basValRelease(&elem);
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_STORE_ARRAY: {
            uint8_t dims = readUint8(vm);

            // Pop value to store
            BasValueT storeVal;

            if (!pop(vm, &storeVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            // Pop indices in reverse order
            int32_t indices[BAS_ARRAY_MAX_DIMS];

            for (int32_t d = dims - 1; d >= 0; d--) {
                BasValueT idxVal;

                if (!pop(vm, &idxVal)) {
                    basValRelease(&storeVal);
                    return BAS_VM_STACK_UNDERFLOW;
                }

                indices[d] = (int32_t)basValToNumber(idxVal);
                basValRelease(&idxVal);
            }

            // Pop array reference
            BasValueT arrRef;

            if (!pop(vm, &arrRef)) {
                basValRelease(&storeVal);
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) {
                basValRelease(&arrRef);
                basValRelease(&storeVal);
                runtimeError(vm, 13, "Not an array");
                return BAS_VM_TYPE_MISMATCH;
            }

            int32_t flatIdx = basArrayIndex(arrRef.arrVal, indices, dims);

            if (flatIdx < 0) {
                basValRelease(&arrRef);
                basValRelease(&storeVal);
                runtimeError(vm, 9, "Subscript out of range");
                return BAS_VM_SUBSCRIPT_RANGE;
            }

            basValRelease(&arrRef.arrVal->elements[flatIdx]);
            arrRef.arrVal->elements[flatIdx] = storeVal;
            basValRelease(&arrRef);
            break;
        }

        case OP_STORE_ARRAY_FIELD: {
            uint8_t dims = readUint8(vm);
            uint16_t fieldIdx = readUint16(vm);

            // Pop value to store
            BasValueT storeVal;

            if (!pop(vm, &storeVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            // Pop indices in reverse order
            int32_t indices[BAS_ARRAY_MAX_DIMS];

            for (int32_t d = dims - 1; d >= 0; d--) {
                BasValueT idxVal;

                if (!pop(vm, &idxVal)) {
                    basValRelease(&storeVal);
                    return BAS_VM_STACK_UNDERFLOW;
                }

                indices[d] = (int32_t)basValToNumber(idxVal);
                basValRelease(&idxVal);
            }

            // Pop array reference
            BasValueT arrRef;

            if (!pop(vm, &arrRef)) {
                basValRelease(&storeVal);
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) {
                basValRelease(&arrRef);
                basValRelease(&storeVal);
                runtimeError(vm, 13, "Not an array");
                return BAS_VM_TYPE_MISMATCH;
            }

            int32_t flatIdx = basArrayIndex(arrRef.arrVal, indices, dims);

            if (flatIdx < 0) {
                basValRelease(&arrRef);
                basValRelease(&storeVal);
                runtimeError(vm, 9, "Subscript out of range");
                return BAS_VM_SUBSCRIPT_RANGE;
            }

            // Element must be a UDT
            BasValueT *elem = &arrRef.arrVal->elements[flatIdx];

            if (elem->type != BAS_TYPE_UDT || !elem->udtVal) {
                basValRelease(&arrRef);
                basValRelease(&storeVal);
                runtimeError(vm, 13, "Array element is not a TYPE instance");
                return BAS_VM_TYPE_MISMATCH;
            }

            if (fieldIdx >= (uint16_t)elem->udtVal->fieldCount) {
                basValRelease(&arrRef);
                basValRelease(&storeVal);
                runtimeError(vm, 9, "Invalid field index");
                return BAS_VM_ERROR;
            }

            basValRelease(&elem->udtVal->fields[fieldIdx]);
            elem->udtVal->fields[fieldIdx] = storeVal;
            basValRelease(&arrRef);
            break;
        }

        case OP_REDIM: {
            uint8_t dims     = readUint8(vm);
            uint8_t preserve = readUint8(vm);

            int32_t lbounds[BAS_ARRAY_MAX_DIMS];
            int32_t ubounds[BAS_ARRAY_MAX_DIMS];

            for (int32_t d = dims - 1; d >= 0; d--) {
                BasValueT ubVal;
                BasValueT lbVal;

                if (!pop(vm, &ubVal) || !pop(vm, &lbVal)) {
                    return BAS_VM_STACK_UNDERFLOW;
                }

                ubounds[d] = (int32_t)basValToNumber(ubVal);
                lbounds[d] = (int32_t)basValToNumber(lbVal);
                basValRelease(&ubVal);
                basValRelease(&lbVal);
            }

            // Pop old array reference
            BasValueT oldRef;

            if (!pop(vm, &oldRef)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            uint8_t elementType = BAS_TYPE_INTEGER;

            if (oldRef.type == BAS_TYPE_ARRAY && oldRef.arrVal) {
                elementType = oldRef.arrVal->elementType;
            }

            BasArrayT *newArr = basArrayNew(dims, lbounds, ubounds, elementType);

            if (!newArr) {
                basValRelease(&oldRef);
                runtimeError(vm, 7, "Out of memory in REDIM");
                return BAS_VM_OUT_OF_MEMORY;
            }

            // Copy old elements if PRESERVE
            if (preserve && oldRef.type == BAS_TYPE_ARRAY && oldRef.arrVal) {
                int32_t copyCount = oldRef.arrVal->totalElements;

                if (copyCount > newArr->totalElements) {
                    copyCount = newArr->totalElements;
                }

                for (int32_t i = 0; i < copyCount; i++) {
                    newArr->elements[i] = basValCopy(oldRef.arrVal->elements[i]);
                }
            }

            basValRelease(&oldRef);

            BasValueT arrVal;
            arrVal.type   = BAS_TYPE_ARRAY;
            arrVal.arrVal = newArr;

            if (!push(vm, arrVal)) {
                basArrayFree(newArr);
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_ERASE: {
            BasValueT arrRef;

            if (!pop(vm, &arrRef)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            basValRelease(&arrRef);

            // Push an empty/zero value to store back
            BasValueT empty;
            memset(&empty, 0, sizeof(empty));

            if (!push(vm, empty)) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_LBOUND: {
            uint8_t dim = readUint8(vm);
            BasValueT arrRef;

            if (!pop(vm, &arrRef)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) {
                basValRelease(&arrRef);
                runtimeError(vm, 13, "Not an array");
                return BAS_VM_TYPE_MISMATCH;
            }

            if (dim < 1 || dim > (uint8_t)arrRef.arrVal->dims) {
                basValRelease(&arrRef);
                runtimeError(vm, 9, "Invalid dimension for LBOUND");
                return BAS_VM_SUBSCRIPT_RANGE;
            }

            int32_t lb = arrRef.arrVal->lbound[dim - 1];
            basValRelease(&arrRef);

            if (!push(vm, basValLong(lb))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_UBOUND: {
            uint8_t dim = readUint8(vm);
            BasValueT arrRef;

            if (!pop(vm, &arrRef)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) {
                basValRelease(&arrRef);
                runtimeError(vm, 13, "Not an array");
                return BAS_VM_TYPE_MISMATCH;
            }

            if (dim < 1 || dim > (uint8_t)arrRef.arrVal->dims) {
                basValRelease(&arrRef);
                runtimeError(vm, 9, "Invalid dimension for UBOUND");
                return BAS_VM_SUBSCRIPT_RANGE;
            }

            int32_t ub = arrRef.arrVal->ubound[dim - 1];
            basValRelease(&arrRef);

            if (!push(vm, basValLong(ub))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_LOAD_FIELD: {
            uint16_t fieldIdx = readUint16(vm);
            BasValueT udtRef;

            if (!pop(vm, &udtRef)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (udtRef.type != BAS_TYPE_UDT || !udtRef.udtVal) {
                basValRelease(&udtRef);
                runtimeError(vm, 13, "Not a TYPE instance");
                return BAS_VM_TYPE_MISMATCH;
            }

            if (fieldIdx >= (uint16_t)udtRef.udtVal->fieldCount) {
                basValRelease(&udtRef);
                runtimeError(vm, 9, "Invalid field index");
                return BAS_VM_ERROR;
            }

            BasValueT fieldVal = basValCopy(udtRef.udtVal->fields[fieldIdx]);
            basValRelease(&udtRef);

            if (!push(vm, fieldVal)) {
                basValRelease(&fieldVal);
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_STORE_FIELD: {
            uint16_t fieldIdx = readUint16(vm);
            BasValueT storeVal;

            if (!pop(vm, &storeVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT udtRef;

            if (!pop(vm, &udtRef)) {
                basValRelease(&storeVal);
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (udtRef.type != BAS_TYPE_UDT || !udtRef.udtVal) {
                basValRelease(&udtRef);
                basValRelease(&storeVal);
                runtimeError(vm, 13, "Not a TYPE instance");
                return BAS_VM_TYPE_MISMATCH;
            }

            if (fieldIdx >= (uint16_t)udtRef.udtVal->fieldCount) {
                basValRelease(&udtRef);
                basValRelease(&storeVal);
                runtimeError(vm, 9, "Invalid field index");
                return BAS_VM_ERROR;
            }

            basValRelease(&udtRef.udtVal->fields[fieldIdx]);
            udtRef.udtVal->fields[fieldIdx] = storeVal;
            basValRelease(&udtRef);
            break;
        }

        // ============================================================
        // DATA/READ/RESTORE
        // ============================================================

        case OP_READ_DATA: {
            if (!vm->module || vm->dataPtr >= vm->module->dataCount) {
                runtimeError(vm, 4, "Out of DATA");
                return BAS_VM_ERROR;
            }

            if (!push(vm, basValCopy(vm->module->dataPool[vm->dataPtr++]))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_RESTORE:
            vm->dataPtr = 0;
            break;

        // ============================================================
        // FORMAT$
        // ============================================================

        case OP_FORMAT: {
            // Pop format string, then value
            BasValueT fmtVal;
            BasValueT val;

            if (!pop(vm, &fmtVal) || !pop(vm, &val)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT fmtStr = basValToString(fmtVal);
            basValRelease(&fmtVal);

            const char *fmt = fmtStr.strVal->data;
            int32_t    fmtLen = fmtStr.strVal->len;
            double     n = basValToNumber(val);
            basValRelease(&val);

            char buf[256];
            buf[0] = '\0';

            // Check for "percent" format
            bool isPercent = false;
            if (fmtLen == 7) {
                isPercent = true;
                const char *pct = "PERCENT";
                for (int32_t i = 0; i < 7; i++) {
                    if (toupper((unsigned char)fmt[i]) != pct[i]) {
                        isPercent = false;
                        break;
                    }
                }
            }
            if (isPercent) {
                snprintf(buf, sizeof(buf), "%.0f%%", n * 100.0);
            } else {
                // Count format characters
                int32_t hashBefore  = 0;
                int32_t zeroBefore  = 0;
                int32_t hashAfter   = 0;
                int32_t zeroAfter   = 0;
                bool    hasDecimal  = false;
                bool    hasComma    = false;
                bool    plusStart   = false;
                bool    plusEnd     = false;
                bool    minusEnd   = false;

                for (int32_t i = 0; i < fmtLen; i++) {
                    if (fmt[i] == '+' && i == 0) {
                        plusStart = true;
                    } else if (fmt[i] == '+' && i == fmtLen - 1) {
                        plusEnd = true;
                    } else if (fmt[i] == '-' && i == fmtLen - 1) {
                        minusEnd = true;
                    } else if (fmt[i] == '.') {
                        hasDecimal = true;
                    } else if (fmt[i] == ',') {
                        hasComma = true;
                    } else if (fmt[i] == '#') {
                        if (hasDecimal) {
                            hashAfter++;
                        } else {
                            hashBefore++;
                        }
                    } else if (fmt[i] == '0') {
                        if (hasDecimal) {
                            zeroAfter++;
                        } else {
                            zeroBefore++;
                        }
                    }
                }

                int32_t decimals = hashAfter + zeroAfter;
                bool isNeg = (n < 0);
                double absN = isNeg ? -n : n;

                // Format the number
                char numBuf[128];

                if (hasDecimal) {
                    snprintf(numBuf, sizeof(numBuf), "%.*f", (int)decimals, absN);
                } else {
                    snprintf(numBuf, sizeof(numBuf), "%.0f", absN);
                }

                // Split into integer and decimal parts
                char intPart[128];
                char decPart[128];
                intPart[0] = '\0';
                decPart[0] = '\0';

                char *dot = strchr(numBuf, '.');

                if (dot) {
                    int32_t intLen = (int32_t)(dot - numBuf);
                    memcpy(intPart, numBuf, intLen);
                    intPart[intLen] = '\0';
                    strncpy(decPart, dot + 1, sizeof(decPart) - 1);
                    decPart[sizeof(decPart) - 1] = '\0';
                } else {
                    strncpy(intPart, numBuf, sizeof(intPart) - 1);
                    intPart[sizeof(intPart) - 1] = '\0';
                }

                // Apply thousands separator
                char fmtIntPart[128];

                if (hasComma) {
                    int32_t srcLen = (int32_t)strlen(intPart);
                    int32_t dstIdx = 0;

                    for (int32_t i = 0; i < srcLen; i++) {
                        if (i > 0 && (srcLen - i) % 3 == 0) {
                            fmtIntPart[dstIdx++] = ',';
                        }
                        fmtIntPart[dstIdx++] = intPart[i];
                    }

                    fmtIntPart[dstIdx] = '\0';
                } else {
                    strncpy(fmtIntPart, intPart, sizeof(fmtIntPart) - 1);
                    fmtIntPart[sizeof(fmtIntPart) - 1] = '\0';
                }

                // Pad integer part with leading zeros if format has 0's
                int32_t totalIntDigits = hashBefore + zeroBefore;
                int32_t curIntLen = (int32_t)strlen(fmtIntPart);

                // Build result
                int32_t idx = 0;

                // Sign prefix
                if (plusStart || plusEnd) {
                    if (isNeg) {
                        buf[idx++] = '-';
                    } else if (plusStart) {
                        buf[idx++] = '+';
                    }
                } else if (isNeg) {
                    buf[idx++] = '-';
                }

                // Pad with leading spaces or zeros
                int32_t padNeeded = totalIntDigits - curIntLen;

                for (int32_t i = 0; i < padNeeded; i++) {
                    if (i < padNeeded - (int32_t)strlen(intPart)) {
                        // Positions before the number
                        if (zeroBefore > 0) {
                            buf[idx++] = '0';
                        } else {
                            buf[idx++] = ' ';
                        }
                    } else {
                        buf[idx++] = '0';
                    }
                }

                // Integer part
                for (int32_t i = 0; fmtIntPart[i]; i++) {
                    buf[idx++] = fmtIntPart[i];
                }

                // Decimal part
                if (hasDecimal) {
                    buf[idx++] = '.';
                    for (int32_t i = 0; decPart[i] && i < decimals; i++) {
                        buf[idx++] = decPart[i];
                    }
                }

                // Trailing sign
                if (plusEnd && !isNeg) {
                    buf[idx++] = '+';
                } else if (minusEnd && isNeg) {
                    buf[idx++] = '-';
                } else if (minusEnd && !isNeg) {
                    buf[idx++] = ' ';
                }

                buf[idx] = '\0';
            }

            basValRelease(&fmtStr);

            if (!push(vm, basValStringFromC(buf))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        // ============================================================
        // SHELL
        // ============================================================

        case OP_SHELL: {
            BasValueT cmdVal;

            if (!pop(vm, &cmdVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT cmdStr = basValToString(cmdVal);
            basValRelease(&cmdVal);

            int32_t result = 0;

            if (cmdStr.strVal && cmdStr.strVal->len > 0) {
                result = system(cmdStr.strVal->data);
            }

            basValRelease(&cmdStr);

            if (!push(vm, basValInteger((int16_t)result))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        // ============================================================
        // COMPARE_MODE
        // ============================================================

        case OP_COMPARE_MODE: {
            uint8_t mode = readUint8(vm);
            vm->compareTextMode = (mode != 0);
            break;
        }

        // ============================================================
        // Halt
        // ============================================================

        case OP_END:
            vm->running = false;
            vm->ended   = true;
            return BAS_VM_HALTED;

        case OP_HALT:
            vm->running = false;
            return BAS_VM_HALTED;

        // ============================================================
        // UI / Event opcodes (name-based resolution)
        // ============================================================

        case OP_LOAD_PROP: {
            BasValueT propNameVal;
            BasValueT ctrlRefVal;

            if (!pop(vm, &propNameVal) || !pop(vm, &ctrlRefVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (vm->ui.getProp && ctrlRefVal.type == BAS_TYPE_OBJECT) {
                BasValueT sv = basValToString(propNameVal);
                BasValueT result = vm->ui.getProp(vm->ui.ctx, ctrlRefVal.objVal, sv.strVal->data);
                basValRelease(&sv);
                push(vm, result);
            } else {
                push(vm, basValInteger(0));
            }

            basValRelease(&propNameVal);
            basValRelease(&ctrlRefVal);
            break;
        }

        case OP_STORE_PROP: {
            BasValueT value;
            BasValueT propNameVal;
            BasValueT ctrlRefVal;

            if (!pop(vm, &value) || !pop(vm, &propNameVal) || !pop(vm, &ctrlRefVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (vm->ui.setProp && ctrlRefVal.type == BAS_TYPE_OBJECT) {
                BasValueT sv = basValToString(propNameVal);
                vm->ui.setProp(vm->ui.ctx, ctrlRefVal.objVal, sv.strVal->data, value);
                basValRelease(&sv);
            }

            basValRelease(&value);
            basValRelease(&propNameVal);
            basValRelease(&ctrlRefVal);
            break;
        }

        case OP_CALL_METHOD: {
            uint8_t argc = readUint8(vm);
            BasValueT methodNameVal;
            BasValueT ctrlRefVal;

            // Pop args in reverse
            BasValueT args[16];
            int32_t argCount = argc < 16 ? argc : 16;

            for (int32_t i = argCount - 1; i >= 0; i--) {
                if (!pop(vm, &args[i])) {
                    return BAS_VM_STACK_UNDERFLOW;
                }
            }

            if (!pop(vm, &methodNameVal) || !pop(vm, &ctrlRefVal)) {
                for (int32_t i = 0; i < argCount; i++) {
                    basValRelease(&args[i]);
                }

                return BAS_VM_STACK_UNDERFLOW;
            }

            if (vm->ui.callMethod && ctrlRefVal.type == BAS_TYPE_OBJECT) {
                BasValueT sv = basValToString(methodNameVal);
                BasValueT result = vm->ui.callMethod(vm->ui.ctx, ctrlRefVal.objVal, sv.strVal->data, args, argCount);
                basValRelease(&sv);
                push(vm, result);
            } else {
                push(vm, basValInteger(0));
            }

            for (int32_t i = 0; i < argCount; i++) {
                basValRelease(&args[i]);
            }

            basValRelease(&methodNameVal);
            basValRelease(&ctrlRefVal);
            break;
        }

        case OP_LOAD_FORM: {
            BasValueT nameVal;

            if (!pop(vm, &nameVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            void *formRef = NULL;

            if (vm->ui.loadForm) {
                BasValueT sv = basValToString(nameVal);
                formRef = vm->ui.loadForm(vm->ui.ctx, sv.strVal->data);
                basValRelease(&sv);
            }

            // Set as current form for subsequent FIND_CTRL calls
            if (formRef) {
                vm->currentForm = formRef;
            }

            basValRelease(&nameVal);
            push(vm, basValObject(formRef));
            break;
        }

        case OP_UNLOAD_FORM: {
            BasValueT formVal;

            if (!pop(vm, &formVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (vm->ui.unloadForm && formVal.type == BAS_TYPE_OBJECT) {
                vm->ui.unloadForm(vm->ui.ctx, formVal.objVal);
            }

            basValRelease(&formVal);
            break;
        }

        case OP_SHOW_FORM: {
            uint8_t modal = readUint8(vm);
            BasValueT formVal;

            if (!pop(vm, &formVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (vm->ui.showForm && formVal.type == BAS_TYPE_OBJECT) {
                vm->ui.showForm(vm->ui.ctx, formVal.objVal, modal != 0);
            }

            basValRelease(&formVal);
            break;
        }

        case OP_HIDE_FORM: {
            BasValueT formVal;

            if (!pop(vm, &formVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (vm->ui.hideForm && formVal.type == BAS_TYPE_OBJECT) {
                vm->ui.hideForm(vm->ui.ctx, formVal.objVal);
            }

            basValRelease(&formVal);
            break;
        }

        case OP_MSGBOX: {
            // Stack: [message, flags] — flags on top
            BasValueT flagsVal;
            BasValueT msgVal;

            if (!pop(vm, &flagsVal) || !pop(vm, &msgVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t flags  = (int32_t)basValToNumber(flagsVal);
            int32_t result = 1; // default OK

            if (vm->ui.msgBox) {
                BasValueT sv = basValToString(msgVal);
                result = vm->ui.msgBox(vm->ui.ctx, sv.strVal->data, flags);
                basValRelease(&sv);
            }

            basValRelease(&flagsVal);
            basValRelease(&msgVal);
            push(vm, basValInteger((int16_t)result));
            break;
        }

        case OP_INPUTBOX: {
            BasValueT defaultVal;
            BasValueT titleVal;
            BasValueT promptVal;

            if (!pop(vm, &defaultVal) || !pop(vm, &titleVal) || !pop(vm, &promptVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasStringT *result = NULL;

            if (vm->ui.inputBox) {
                BasValueT sp = basValToString(promptVal);
                BasValueT st = basValToString(titleVal);
                BasValueT sd = basValToString(defaultVal);
                result = vm->ui.inputBox(vm->ui.ctx, sp.strVal->data, st.strVal->data, sd.strVal->data);
                basValRelease(&sp);
                basValRelease(&st);
                basValRelease(&sd);
            }

            basValRelease(&defaultVal);
            basValRelease(&titleVal);
            basValRelease(&promptVal);

            if (result) {
                BasValueT rv;
                rv.type   = BAS_TYPE_STRING;
                rv.strVal = result;
                push(vm, rv);
            } else {
                push(vm, basValStringFromC(""));
            }

            break;
        }

        case OP_ME_REF:
            push(vm, basValObject(vm->currentForm));
            break;

        case OP_CREATE_CTRL: {
            BasValueT nameVal;
            BasValueT typeVal;
            BasValueT formVal;

            if (!pop(vm, &nameVal) || !pop(vm, &typeVal) || !pop(vm, &formVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            void *ctrlRef = NULL;

            if (vm->ui.createCtrl && formVal.type == BAS_TYPE_OBJECT) {
                BasValueT sv1 = basValToString(typeVal);
                BasValueT sv2 = basValToString(nameVal);
                ctrlRef = vm->ui.createCtrl(vm->ui.ctx, formVal.objVal, sv1.strVal->data, sv2.strVal->data);
                basValRelease(&sv1);
                basValRelease(&sv2);
            }

            basValRelease(&nameVal);
            basValRelease(&typeVal);
            basValRelease(&formVal);
            push(vm, basValObject(ctrlRef));
            break;
        }

        case OP_FIND_CTRL: {
            BasValueT nameVal;
            BasValueT formVal;

            if (!pop(vm, &nameVal) || !pop(vm, &formVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            void *ctrlRef = NULL;

            if (vm->ui.findCtrl) {
                // Use current form if formRef is NULL or not an object
                void *formRef = (formVal.type == BAS_TYPE_OBJECT) ? formVal.objVal : vm->currentForm;
                BasValueT sv = basValToString(nameVal);
                ctrlRef = vm->ui.findCtrl(vm->ui.ctx, formRef, sv.strVal->data);
                basValRelease(&sv);
            }

            basValRelease(&nameVal);
            basValRelease(&formVal);
            push(vm, basValObject(ctrlRef));
            break;
        }

        case OP_CTRL_REF: {
            uint16_t nameIdx = readUint16(vm);
            const char *ctrlName = "";

            if (nameIdx < (uint16_t)vm->module->constCount) {
                ctrlName = vm->module->constants[nameIdx]->data;
            }

            void *ctrlRef = NULL;

            if (vm->ui.findCtrl && vm->currentForm) {
                ctrlRef = vm->ui.findCtrl(vm->ui.ctx, vm->currentForm, ctrlName);
            }

            push(vm, basValObject(ctrlRef));
            break;
        }

        case OP_FIND_CTRL_IDX: {
            // Stack: [formRef, ctrlName, index] -- index on top
            BasValueT idxVal;
            BasValueT nameVal;
            BasValueT formVal;

            if (!pop(vm, &idxVal) || !pop(vm, &nameVal) || !pop(vm, &formVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            void *ctrlRef = NULL;

            if (vm->ui.findCtrlIdx) {
                void *formRef = (formVal.type == BAS_TYPE_OBJECT) ? formVal.objVal : vm->currentForm;
                int32_t index = (int32_t)basValToNumber(idxVal);
                BasValueT sv  = basValToString(nameVal);
                ctrlRef = vm->ui.findCtrlIdx(vm->ui.ctx, formRef, sv.strVal->data, index);
                basValRelease(&sv);
            }

            basValRelease(&idxVal);
            basValRelease(&nameVal);
            basValRelease(&formVal);
            push(vm, basValObject(ctrlRef));
            break;
        }

        // ============================================================
        // Form-level variables
        // ============================================================

        case OP_LOAD_FORM_VAR: {
            uint16_t idx = readUint16(vm);

            if (!vm->currentFormVars || idx >= (uint16_t)vm->currentFormVarCount) {
                runtimeError(vm, 9, "Form variable access outside form context");
                return BAS_VM_ERROR;
            }

            push(vm, basValCopy(vm->currentFormVars[idx]));
            break;
        }

        case OP_STORE_FORM_VAR: {
            uint16_t idx = readUint16(vm);
            BasValueT val;

            if (!pop(vm, &val)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            if (!vm->currentFormVars || idx >= (uint16_t)vm->currentFormVarCount) {
                basValRelease(&val);
                runtimeError(vm, 9, "Form variable access outside form context");
                return BAS_VM_ERROR;
            }

            basValRelease(&vm->currentFormVars[idx]);
            vm->currentFormVars[idx] = val;
            break;
        }

        case OP_PUSH_FORM_ADDR: {
            uint16_t idx = readUint16(vm);

            if (!vm->currentFormVars || idx >= (uint16_t)vm->currentFormVarCount) {
                runtimeError(vm, 9, "Form variable address outside form context");
                return BAS_VM_ERROR;
            }

            BasValueT ref;
            ref.type   = BAS_TYPE_REF;
            ref.refVal = &vm->currentFormVars[idx];
            push(vm, ref);
            break;
        }

        // ============================================================
        // External library calls
        // ============================================================

        case OP_CALL_EXTERN: {
            uint16_t libNameIdx  = readUint16(vm);
            uint16_t funcNameIdx = readUint16(vm);
            uint8_t  argc        = readUint8(vm);
            uint8_t  retType     = readUint8(vm);

            // Look up in cache
            void *funcPtr = NULL;
            int32_t cacheIdx = -1;

            for (int32_t i = 0; i < vm->externCacheCount; i++) {
                if (vm->externCache[i].libNameIdx == libNameIdx &&
                    vm->externCache[i].funcNameIdx == funcNameIdx) {
                    funcPtr  = vm->externCache[i].funcPtr;
                    cacheIdx = i;
                    break;
                }
            }

            // Resolve on first call
            if (cacheIdx < 0) {
                const char *libName  = "";
                const char *funcName = "";

                if (libNameIdx < (uint16_t)vm->module->constCount) {
                    libName = vm->module->constants[libNameIdx]->data;
                }

                if (funcNameIdx < (uint16_t)vm->module->constCount) {
                    funcName = vm->module->constants[funcNameIdx]->data;
                }

                if (vm->ext.resolveExtern) {
                    funcPtr = vm->ext.resolveExtern(vm->ext.ctx, libName, funcName);
                }

                if (!funcPtr) {
                    // Pop args to keep stack balanced
                    for (int32_t i = 0; i < argc; i++) {
                        BasValueT tmp;
                        pop(vm, &tmp);
                        basValRelease(&tmp);
                    }

                    char msg[256];
                    const char *fn = funcNameIdx < (uint16_t)vm->module->constCount
                                     ? vm->module->constants[funcNameIdx]->data : "?";
                    snprintf(msg, sizeof(msg), "External function not found: %s", fn);
                    runtimeError(vm, 453, msg);
                    return BAS_VM_ERROR;
                }

                // Cache it
                if (vm->externCacheCount < BAS_EXTERN_CACHE_SIZE) {
                    vm->externCache[vm->externCacheCount].libNameIdx  = libNameIdx;
                    vm->externCache[vm->externCacheCount].funcNameIdx = funcNameIdx;
                    vm->externCache[vm->externCacheCount].funcPtr     = funcPtr;
                    vm->externCacheCount++;
                }
            }

            // Pop arguments (reverse order)
            BasValueT args[16];
            int32_t argCount = argc < 16 ? argc : 16;

            for (int32_t i = argCount - 1; i >= 0; i--) {
                if (!pop(vm, &args[i])) {
                    return BAS_VM_STACK_UNDERFLOW;
                }
            }

            // Call through host callback
            BasValueT result = basValInteger(0);

            if (vm->ext.callExtern) {
                const char *funcName = funcNameIdx < (uint16_t)vm->module->constCount
                                       ? vm->module->constants[funcNameIdx]->data : "";
                result = vm->ext.callExtern(vm->ext.ctx, funcPtr, funcName, args, argCount, retType);
            }

            for (int32_t i = 0; i < argCount; i++) {
                basValRelease(&args[i]);
            }

            // Push return value (void functions still push a dummy 0)
            push(vm, result);
            break;
        }

        // ============================================================
        // SQL database operations
        // ============================================================

        case OP_SQL_OPEN: {
            BasValueT pathVal;
            if (!pop(vm, &pathVal)) { return BAS_VM_STACK_UNDERFLOW; }
            int32_t handle = 0;
            if (vm->sql.sqlOpen) {
                BasValueT sv = basValToString(pathVal);
                handle = vm->sql.sqlOpen(sv.strVal->data);
                basValRelease(&sv);
            }
            basValRelease(&pathVal);
            push(vm, basValLong(handle));
            break;
        }

        case OP_SQL_CLOSE: {
            BasValueT dbVal;
            if (!pop(vm, &dbVal)) { return BAS_VM_STACK_UNDERFLOW; }
            if (vm->sql.sqlClose) {
                vm->sql.sqlClose((int32_t)basValToNumber(dbVal));
            }
            basValRelease(&dbVal);
            break;
        }

        case OP_SQL_EXEC: {
            BasValueT sqlVal;
            BasValueT dbVal;
            if (!pop(vm, &sqlVal) || !pop(vm, &dbVal)) { return BAS_VM_STACK_UNDERFLOW; }
            bool ok = false;
            if (vm->sql.sqlExec) {
                BasValueT sv = basValToString(sqlVal);
                ok = vm->sql.sqlExec((int32_t)basValToNumber(dbVal), sv.strVal->data);
                basValRelease(&sv);
            }
            basValRelease(&sqlVal);
            basValRelease(&dbVal);
            push(vm, ok ? basValInteger(-1) : basValInteger(0));
            break;
        }

        case OP_SQL_ERROR: {
            BasValueT dbVal;
            if (!pop(vm, &dbVal)) { return BAS_VM_STACK_UNDERFLOW; }
            const char *err = "";
            if (vm->sql.sqlError) {
                err = vm->sql.sqlError((int32_t)basValToNumber(dbVal));
            }
            basValRelease(&dbVal);
            push(vm, basValStringFromC(err ? err : ""));
            break;
        }

        case OP_SQL_QUERY: {
            BasValueT sqlVal;
            BasValueT dbVal;
            if (!pop(vm, &sqlVal) || !pop(vm, &dbVal)) { return BAS_VM_STACK_UNDERFLOW; }
            int32_t handle = 0;
            if (vm->sql.sqlQuery) {
                BasValueT sv = basValToString(sqlVal);
                handle = vm->sql.sqlQuery((int32_t)basValToNumber(dbVal), sv.strVal->data);
                basValRelease(&sv);
            }
            basValRelease(&sqlVal);
            basValRelease(&dbVal);
            push(vm, basValLong(handle));
            break;
        }

        case OP_SQL_NEXT: {
            BasValueT rsVal;
            if (!pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; }
            bool ok = false;
            if (vm->sql.sqlNext) {
                ok = vm->sql.sqlNext((int32_t)basValToNumber(rsVal));
            }
            basValRelease(&rsVal);
            push(vm, ok ? basValInteger(-1) : basValInteger(0));
            break;
        }

        case OP_SQL_EOF: {
            BasValueT rsVal;
            if (!pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; }
            bool eof = true;
            if (vm->sql.sqlEof) {
                eof = vm->sql.sqlEof((int32_t)basValToNumber(rsVal));
            }
            basValRelease(&rsVal);
            push(vm, eof ? basValInteger(-1) : basValInteger(0));
            break;
        }

        case OP_SQL_FIELD_COUNT: {
            BasValueT rsVal;
            if (!pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; }
            int32_t count = 0;
            if (vm->sql.sqlFieldCount) {
                count = vm->sql.sqlFieldCount((int32_t)basValToNumber(rsVal));
            }
            basValRelease(&rsVal);
            push(vm, basValLong(count));
            break;
        }

        case OP_SQL_FIELD_NAME: {
            BasValueT colVal;
            BasValueT rsVal;
            if (!pop(vm, &colVal) || !pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; }
            const char *name = "";
            if (vm->sql.sqlFieldName) {
                name = vm->sql.sqlFieldName((int32_t)basValToNumber(rsVal), (int32_t)basValToNumber(colVal));
            }
            basValRelease(&colVal);
            basValRelease(&rsVal);
            push(vm, basValStringFromC(name ? name : ""));
            break;
        }

        case OP_SQL_FIELD_TEXT: {
            BasValueT colVal;
            BasValueT rsVal;
            if (!pop(vm, &colVal) || !pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; }
            const char *text = "";
            if (vm->sql.sqlFieldText) {
                text = vm->sql.sqlFieldText((int32_t)basValToNumber(rsVal), (int32_t)basValToNumber(colVal));
            }
            basValRelease(&colVal);
            basValRelease(&rsVal);
            push(vm, basValStringFromC(text ? text : ""));
            break;
        }

        case OP_SQL_FIELD_BYNAME: {
            BasValueT nameVal;
            BasValueT rsVal;
            if (!pop(vm, &nameVal) || !pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; }
            const char *text = "";
            if (vm->sql.sqlFieldByName) {
                BasValueT sv = basValToString(nameVal);
                text = vm->sql.sqlFieldByName((int32_t)basValToNumber(rsVal), sv.strVal->data);
                basValRelease(&sv);
            }
            basValRelease(&nameVal);
            basValRelease(&rsVal);
            push(vm, basValStringFromC(text ? text : ""));
            break;
        }

        case OP_SQL_FIELD_INT: {
            BasValueT colVal;
            BasValueT rsVal;
            if (!pop(vm, &colVal) || !pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; }
            int32_t val = 0;
            if (vm->sql.sqlFieldInt) {
                val = vm->sql.sqlFieldInt((int32_t)basValToNumber(rsVal), (int32_t)basValToNumber(colVal));
            }
            basValRelease(&colVal);
            basValRelease(&rsVal);
            push(vm, basValLong(val));
            break;
        }

        case OP_SQL_FIELD_DBL: {
            BasValueT colVal;
            BasValueT rsVal;
            if (!pop(vm, &colVal) || !pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; }
            double val = 0.0;
            if (vm->sql.sqlFieldDbl) {
                val = vm->sql.sqlFieldDbl((int32_t)basValToNumber(rsVal), (int32_t)basValToNumber(colVal));
            }
            basValRelease(&colVal);
            basValRelease(&rsVal);
            push(vm, basValDouble(val));
            break;
        }

        case OP_SQL_FREE_RESULT: {
            BasValueT rsVal;
            if (!pop(vm, &rsVal)) { return BAS_VM_STACK_UNDERFLOW; }
            if (vm->sql.sqlFreeResult) {
                vm->sql.sqlFreeResult((int32_t)basValToNumber(rsVal));
            }
            basValRelease(&rsVal);
            break;
        }

        case OP_SQL_AFFECTED: {
            BasValueT dbVal;
            if (!pop(vm, &dbVal)) { return BAS_VM_STACK_UNDERFLOW; }
            int32_t rows = 0;
            if (vm->sql.sqlAffectedRows) {
                rows = vm->sql.sqlAffectedRows((int32_t)basValToNumber(dbVal));
            }
            basValRelease(&dbVal);
            push(vm, basValLong(rows));
            break;
        }

        case OP_LINE: {
            uint16_t lineNum = readUint16(vm);
            vm->currentLine  = lineNum;

            // Step into: break at any OP_LINE
            if (vm->debugBreak) {
                vm->debugBreak = false;
                return BAS_VM_BREAKPOINT;
            }

            // Step over: break when call depth returns to target level
            if (vm->stepOverDepth >= 0 && vm->callDepth <= vm->stepOverDepth) {
                vm->stepOverDepth = -1;
                return BAS_VM_BREAKPOINT;
            }

            // Step out: break when call depth drops below target
            if (vm->stepOutDepth >= 0 && vm->callDepth < vm->stepOutDepth) {
                vm->stepOutDepth = -1;
                return BAS_VM_BREAKPOINT;
            }

            // Run to cursor
            if (vm->runToCursorLine >= 0 && (int32_t)lineNum == vm->runToCursorLine) {
                vm->runToCursorLine = -1;
                return BAS_VM_BREAKPOINT;
            }

            // Breakpoint check (linear scan, typically < 20 entries)
            for (int32_t i = 0; i < vm->breakpointCount; i++) {
                if (vm->breakpoints[i] == (int32_t)lineNum) {
                    return BAS_VM_BREAKPOINT;
                }
            }

            break;
        }

        case OP_APP_PATH: {
            push(vm, basValStringFromC(vm->appPath));
            break;
        }

        case OP_APP_CONFIG: {
            push(vm, basValStringFromC(vm->appConfig));
            break;
        }

        case OP_APP_DATA: {
            push(vm, basValStringFromC(vm->appData));
            break;
        }

        case OP_INI_READ: {
            // Stack: file, section, key, default -> result string
            BasValueT defVal, keyVal, secVal, fileVal;
            if (!pop(vm, &defVal) || !pop(vm, &keyVal) || !pop(vm, &secVal) || !pop(vm, &fileVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }
            BasStringT *fileStr = basValFormatString(fileVal);
            BasStringT *secStr  = basValFormatString(secVal);
            BasStringT *keyStr  = basValFormatString(keyVal);
            BasStringT *defStr  = basValFormatString(defVal);
            const char *result  = defStr->data;

            FILE *fp = fopen(fileStr->data, "r");
            if (fp) {
                char line[512];
                bool inSection = false;
                while (fgets(line, sizeof(line), fp)) {
                    // Strip trailing whitespace
                    char *end = line + strlen(line) - 1;
                    while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ')) {
                        *end-- = '\0';
                    }
                    char *p = line;
                    while (*p == ' ' || *p == '\t') { p++; }
                    if (*p == '\0' || *p == ';' || *p == '#') { continue; }
                    if (*p == '[') {
                        char *rb = strchr(p, ']');
                        if (rb) {
                            *rb = '\0';
                            inSection = (strcasecmp(p + 1, secStr->data) == 0);
                        }
                        continue;
                    }
                    if (inSection) {
                        char *eq = strchr(p, '=');
                        if (eq) {
                            *eq = '\0';
                            char *k = p;
                            char *ke = eq - 1;
                            while (ke >= k && (*ke == ' ' || *ke == '\t')) { *ke-- = '\0'; }
                            if (strcasecmp(k, keyStr->data) == 0) {
                                char *v = eq + 1;
                                while (*v == ' ' || *v == '\t') { v++; }
                                result = v;
                                break;
                            }
                        }
                    }
                }
                fclose(fp);
            }

            push(vm, basValStringFromC(result));
            basStringUnref(fileStr);
            basStringUnref(secStr);
            basStringUnref(keyStr);
            basStringUnref(defStr);
            basValRelease(&fileVal);
            basValRelease(&secVal);
            basValRelease(&keyVal);
            basValRelease(&defVal);
            break;
        }

        case OP_INI_WRITE: {
            // Stack: file, section, key, value
            BasValueT valVal, keyVal, secVal, fileVal;
            if (!pop(vm, &valVal) || !pop(vm, &keyVal) || !pop(vm, &secVal) || !pop(vm, &fileVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }
            BasStringT *fileStr = basValFormatString(fileVal);
            BasStringT *secStr  = basValFormatString(secVal);
            BasStringT *keyStr  = basValFormatString(keyVal);
            BasStringT *valStr  = basValFormatString(valVal);

            // Read existing file into memory
            char *content = NULL;
            int32_t contentLen = 0;
            FILE *fp = fopen(fileStr->data, "r");
            if (fp) {
                fseek(fp, 0, SEEK_END);
                contentLen = (int32_t)ftell(fp);
                fseek(fp, 0, SEEK_SET);
                content = (char *)malloc(contentLen + 1);
                if (content) {
                    contentLen = (int32_t)fread(content, 1, contentLen, fp);
                    content[contentLen] = '\0';
                }
                fclose(fp);
            }

            // Write updated file
            fp = fopen(fileStr->data, "w");
            if (fp) {
                bool keyWritten = false;
                bool inSection = false;
                bool sectionFound = false;

                if (content) {
                    char *line = content;
                    while (line && *line) {
                        char *eol = strchr(line, '\n');
                        char lineBuf[512];
                        int32_t ll;
                        if (eol) {
                            ll = (int32_t)(eol - line);
                            if (ll > 0 && line[ll - 1] == '\r') { ll--; }
                            if (ll >= (int32_t)sizeof(lineBuf)) { ll = (int32_t)sizeof(lineBuf) - 1; }
                            memcpy(lineBuf, line, ll);
                            lineBuf[ll] = '\0';
                            line = eol + 1;
                        } else {
                            snprintf(lineBuf, sizeof(lineBuf), "%s", line);
                            ll = (int32_t)strlen(lineBuf);
                            line = NULL;
                        }

                        char *p = lineBuf;
                        while (*p == ' ' || *p == '\t') { p++; }

                        if (*p == '[') {
                            // Write pending key before leaving section
                            if (inSection && !keyWritten) {
                                fprintf(fp, "%s = %s\n", keyStr->data, valStr->data);
                                keyWritten = true;
                            }
                            char *rb = strchr(p, ']');
                            if (rb) {
                                char secName[256];
                                int32_t sn = (int32_t)(rb - p - 1);
                                if (sn >= (int32_t)sizeof(secName)) { sn = (int32_t)sizeof(secName) - 1; }
                                memcpy(secName, p + 1, sn);
                                secName[sn] = '\0';
                                inSection = (strcasecmp(secName, secStr->data) == 0);
                                if (inSection) { sectionFound = true; }
                            }
                            fprintf(fp, "%s\n", lineBuf);
                            continue;
                        }

                        if (inSection && !keyWritten) {
                            char *eq = strchr(p, '=');
                            if (eq) {
                                char k[256];
                                int32_t kl = (int32_t)(eq - p);
                                while (kl > 0 && (p[kl - 1] == ' ' || p[kl - 1] == '\t')) { kl--; }
                                if (kl >= (int32_t)sizeof(k)) { kl = (int32_t)sizeof(k) - 1; }
                                memcpy(k, p, kl);
                                k[kl] = '\0';
                                if (strcasecmp(k, keyStr->data) == 0) {
                                    fprintf(fp, "%s = %s\n", keyStr->data, valStr->data);
                                    keyWritten = true;
                                    continue;
                                }
                            }
                        }

                        fprintf(fp, "%s\n", lineBuf);
                    }
                }

                // Key not found in existing section — append
                if (!keyWritten) {
                    if (!sectionFound) {
                        fprintf(fp, "[%s]\n", secStr->data);
                    } else if (inSection) {
                        // Still in the right section at EOF — just append
                    }
                    fprintf(fp, "%s = %s\n", keyStr->data, valStr->data);
                }

                fclose(fp);
            }

            free(content);
            basStringUnref(fileStr);
            basStringUnref(secStr);
            basStringUnref(keyStr);
            basStringUnref(valStr);
            basValRelease(&fileVal);
            basValRelease(&secVal);
            basValRelease(&keyVal);
            basValRelease(&valVal);
            break;
        }

        default:
            runtimeError(vm, 51, "Bad opcode");
            return BAS_VM_BAD_OPCODE;
    }

    return BAS_VM_OK;
}


// ============================================================
// currentFrame
// ============================================================

static BasCallFrameT *currentFrame(BasVmT *vm) {
    if (vm->callDepth <= 0) {
        // Module-level: use callStack[0] as implicit main frame
        return &vm->callStack[0];
    }

    return &vm->callStack[vm->callDepth - 1];
}


// ============================================================
// defaultPrint
// ============================================================

static void defaultPrint(void *ctx, const char *text, bool newline) {
    (void)ctx;
    fputs(text, stdout);

    if (newline) {
        fputc('\n', stdout);
    }
}


// ============================================================
// execArith
// ============================================================

static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
    BasValueT b;
    BasValueT a;

    if (!pop(vm, &b) || !pop(vm, &a)) {
        return BAS_VM_STACK_UNDERFLOW;
    }

    // VB behavior: + on two strings concatenates
    if ((op == OP_ADD_INT || op == OP_ADD_FLT) && a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) {
        BasStringT *sa = a.strVal ? a.strVal : basStringNew("", 0);
        BasStringT *sb = b.strVal ? b.strVal : basStringNew("", 0);
        int32_t newLen  = sa->len + sb->len;
        BasStringT *cat;

        if (newLen > 0) {
            char *buf = (char *)malloc(newLen + 1);
            memcpy(buf, sa->data, sa->len);
            memcpy(buf + sa->len, sb->data, sb->len);
            buf[newLen] = '\0';
            cat = basStringNew(buf, newLen);
            free(buf);
        } else {
            cat = basStringNew("", 0);
        }

        basValRelease(&a);
        basValRelease(&b);

        BasValueT result;
        result.type   = BAS_TYPE_STRING;
        result.strVal = cat;
        push(vm, result);
        return BAS_VM_OK;
    }

    double na = basValToNumber(a);
    double nb = basValToNumber(b);
    basValRelease(&a);
    basValRelease(&b);

    double result;

    switch (op) {
        case OP_ADD_INT:
        case OP_ADD_FLT:
            result = na + nb;
            break;

        case OP_SUB_INT:
        case OP_SUB_FLT:
            result = na - nb;
            break;

        case OP_MUL_INT:
        case OP_MUL_FLT:
            result = na * nb;
            break;

        case OP_IDIV_INT:
            if ((int32_t)nb == 0) {
                runtimeError(vm, 11, "Division by zero");
                return BAS_VM_DIV_BY_ZERO;
            }

            result = (double)((int32_t)na / (int32_t)nb);
            break;

        case OP_DIV_FLT:
            if (nb == 0.0) {
                runtimeError(vm, 11, "Division by zero");
                return BAS_VM_DIV_BY_ZERO;
            }

            result = na / nb;
            break;

        case OP_MOD_INT:
            if ((int32_t)nb == 0) {
                runtimeError(vm, 11, "Division by zero");
                return BAS_VM_DIV_BY_ZERO;
            }

            result = (double)((int32_t)na % (int32_t)nb);
            break;

        case OP_POW:
            result = pow(na, nb);
            break;

        default:
            result = 0.0;
            break;
    }

    // Return appropriate type
    if (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT) {
        if (result >= -32768.0 && result <= 32767.0) {
            push(vm, basValInteger((int16_t)result));
        } else if (result >= -2147483648.0 && result <= 2147483647.0) {
            push(vm, basValLong((int32_t)result));
        } else {
            push(vm, basValDouble(result));
        }
    } else {
        push(vm, basValDouble(result));
    }

    return BAS_VM_OK;
}


// ============================================================
// execCompare
// ============================================================

static BasVmResultE execCompare(BasVmT *vm, uint8_t op) {
    BasValueT b;
    BasValueT a;

    if (!pop(vm, &b) || !pop(vm, &a)) {
        return BAS_VM_STACK_UNDERFLOW;
    }

    int32_t cmp = vm->compareTextMode ? basValCompareCI(a, b) : basValCompare(a, b);
    basValRelease(&a);
    basValRelease(&b);

    bool result;

    switch (op) {
        case OP_CMP_EQ:  result = (cmp == 0); break;
        case OP_CMP_NE:  result = (cmp != 0); break;
        case OP_CMP_LT:  result = (cmp < 0);  break;
        case OP_CMP_GT:  result = (cmp > 0);  break;
        case OP_CMP_LE:  result = (cmp <= 0); break;
        case OP_CMP_GE:  result = (cmp >= 0); break;
        default:         result = false;       break;
    }

    push(vm, basValBool(result));
    return BAS_VM_OK;
}


// ============================================================
// execFileOp
// ============================================================

// File mode constants (matches compiler/parser.c emission)
#define FILE_MODE_INPUT   1
#define FILE_MODE_OUTPUT  2
#define FILE_MODE_APPEND  3
#define FILE_MODE_RANDOM  4
#define FILE_MODE_BINARY  5

static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
    switch (op) {
        case OP_FILE_OPEN: {
            uint8_t mode = readUint8(vm);
            BasValueT channelVal;
            BasValueT filenameVal;

            if (!pop(vm, &channelVal) || !pop(vm, &filenameVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            basValRelease(&channelVal);

            BasValueT fnStr = basValToString(filenameVal);
            basValRelease(&filenameVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES) {
                basValRelease(&fnStr);
                runtimeError(vm, 52, "Bad file channel number");
                return BAS_VM_FILE_ERROR;
            }

            // Close existing file on this channel
            if (vm->files[channel].handle) {
                fclose((FILE *)vm->files[channel].handle);
                vm->files[channel].handle = NULL;
                vm->files[channel].mode   = 0;
            }

            const char *modeStr;

            switch (mode) {
                case FILE_MODE_INPUT:
                    modeStr = "r";
                    break;
                case FILE_MODE_OUTPUT:
                    modeStr = "w";
                    break;
                case FILE_MODE_APPEND:
                    modeStr = "a";
                    break;
                case FILE_MODE_RANDOM:
                case FILE_MODE_BINARY:
                    modeStr = "r+b";
                    break;
                default:
                    basValRelease(&fnStr);
                    runtimeError(vm, 54, "Bad file mode");
                    return BAS_VM_FILE_ERROR;
            }

            // For RANDOM/BINARY: create file if it doesn't exist, then reopen r+b
            if (mode == FILE_MODE_RANDOM || mode == FILE_MODE_BINARY) {
                FILE *test = fopen(fnStr.strVal->data, "r");
                if (!test) {
                    // Create the file
                    test = fopen(fnStr.strVal->data, "w+b");
                    if (test) {
                        fclose(test);
                    }
                } else {
                    fclose(test);
                }
            }

            FILE *fp = fopen(fnStr.strVal->data, modeStr);
            basValRelease(&fnStr);

            if (!fp) {
                runtimeError(vm, 53, "File not found or cannot open");
                return BAS_VM_FILE_ERROR;
            }

            vm->files[channel].handle = fp;
            vm->files[channel].mode   = mode;
            break;
        }

        case OP_FILE_CLOSE: {
            BasValueT channelVal;

            if (!pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            basValRelease(&channelVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES) {
                runtimeError(vm, 52, "Bad file channel number");
                return BAS_VM_FILE_ERROR;
            }

            if (vm->files[channel].handle) {
                fclose((FILE *)vm->files[channel].handle);
                vm->files[channel].handle = NULL;
                vm->files[channel].mode   = 0;
            }

            break;
        }

        case OP_FILE_PRINT: {
            BasValueT val;
            BasValueT channelVal;

            if (!pop(vm, &val) || !pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            basValRelease(&channelVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                basValRelease(&val);
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            BasStringT *s = basValFormatString(val);
            basValRelease(&val);

            if (s) {
                fputs(s->data, (FILE *)vm->files[channel].handle);
                fputc('\n', (FILE *)vm->files[channel].handle);
                basStringUnref(s);
            }

            break;
        }

        case OP_FILE_INPUT: {
            BasValueT channelVal;

            if (!pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            basValRelease(&channelVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            char buf[1024];
            buf[0] = '\0';

            if (fgets(buf, sizeof(buf), (FILE *)vm->files[channel].handle)) {
                // Strip trailing newline
                int32_t len = (int32_t)strlen(buf);

                while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
                    buf[--len] = '\0';
                }
            }

            if (!push(vm, basValStringFromC(buf))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_FILE_EOF: {
            BasValueT channelVal;

            if (!pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            basValRelease(&channelVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            // Peek ahead to detect EOF before the next read
            FILE *fp = (FILE *)vm->files[channel].handle;
            int ch   = fgetc(fp);
            bool isEof;

            if (ch == EOF) {
                isEof = true;
            } else {
                ungetc(ch, fp);
                isEof = false;
            }

            if (!push(vm, basValBool(isEof))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_FILE_LINE_INPUT: {
            BasValueT channelVal;

            if (!pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            basValRelease(&channelVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            char buf[1024];
            buf[0] = '\0';

            if (fgets(buf, sizeof(buf), (FILE *)vm->files[channel].handle)) {
                int32_t len = (int32_t)strlen(buf);

                while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
                    buf[--len] = '\0';
                }
            }

            if (!push(vm, basValStringFromC(buf))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_FILE_WRITE: {
            // Pop value and channel, write value in WRITE format
            BasValueT val;
            BasValueT channelVal;

            if (!pop(vm, &val) || !pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            basValRelease(&channelVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                basValRelease(&val);
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            FILE *fp = (FILE *)vm->files[channel].handle;

            if (val.type == BAS_TYPE_STRING) {
                // Strings: enclosed in quotes
                fputc('"', fp);
                if (val.strVal) {
                    fputs(val.strVal->data, fp);
                }
                fputc('"', fp);
            } else {
                // Numbers: no leading space (unlike PRINT)
                BasStringT *s = basValFormatString(val);
                if (s) {
                    // Skip leading space that basValFormatString adds for positive numbers
                    const char *text = s->data;
                    if (*text == ' ') {
                        text++;
                    }
                    fputs(text, fp);
                    basStringUnref(s);
                }
            }

            basValRelease(&val);
            break;
        }

        case OP_FILE_WRITE_SEP: {
            // Pop channel, write comma separator
            BasValueT channelVal;

            if (!pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            basValRelease(&channelVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            fputc(',', (FILE *)vm->files[channel].handle);
            break;
        }

        case OP_FILE_WRITE_NL: {
            // Pop channel, write newline
            BasValueT channelVal;

            if (!pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            basValRelease(&channelVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            fputc('\n', (FILE *)vm->files[channel].handle);
            break;
        }

        case OP_FILE_GET: {
            // Pop type, recno, channel; read data; push value
            BasValueT typeVal;
            BasValueT recnoVal;
            BasValueT channelVal;

            if (!pop(vm, &typeVal) || !pop(vm, &recnoVal) || !pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel  = (int32_t)basValToNumber(channelVal);
            int32_t recno    = (int32_t)basValToNumber(recnoVal);
            int32_t dataType = (int32_t)basValToNumber(typeVal);
            basValRelease(&channelVal);
            basValRelease(&recnoVal);
            basValRelease(&typeVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            FILE *fp = (FILE *)vm->files[channel].handle;

            // Seek to record position if recno > 0
            // (recno is 1-based in QB; 0 means current position)
            if (recno > 0) {
                // For simplicity, use fixed 128-byte records for RANDOM
                fseek(fp, (long)(recno - 1) * 128, SEEK_SET);
            }

            BasValueT result;
            memset(&result, 0, sizeof(result));

            switch (dataType) {
                case BAS_TYPE_INTEGER: {
                    int16_t val = 0;
                    if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ }
                    result = basValInteger(val);
                    break;
                }
                case BAS_TYPE_LONG: {
                    int32_t val = 0;
                    if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ }
                    result = basValLong(val);
                    break;
                }
                case BAS_TYPE_SINGLE: {
                    float val = 0.0f;
                    if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ }
                    result = basValSingle(val);
                    break;
                }
                case BAS_TYPE_DOUBLE: {
                    double val = 0.0;
                    if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ }
                    result = basValDouble(val);
                    break;
                }
                case BAS_TYPE_STRING: {
                    // Read a length-prefixed string (int16 len + data)
                    int16_t len = 0;
                    if (fread(&len, sizeof(len), 1, fp) < 1) { /* EOF ok */ }
                    if (len < 0) {
                        len = 0;
                    }
                    char *buf = (char *)malloc(len + 1);
                    if (buf) {
                        if (fread(buf, 1, len, fp) < 1) { /* EOF ok */ }
                        buf[len] = '\0';
                        result = basValStringFromC(buf);
                        free(buf);
                    } else {
                        result = basValStringFromC("");
                    }
                    break;
                }
                default:
                    result = basValInteger(0);
                    break;
            }

            if (!push(vm, result)) {
                basValRelease(&result);
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_FILE_PUT: {
            // Pop value, recno, channel; write data
            BasValueT val;
            BasValueT recnoVal;
            BasValueT channelVal;

            if (!pop(vm, &val) || !pop(vm, &recnoVal) || !pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            int32_t recno   = (int32_t)basValToNumber(recnoVal);
            basValRelease(&channelVal);
            basValRelease(&recnoVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                basValRelease(&val);
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            FILE *fp = (FILE *)vm->files[channel].handle;

            if (recno > 0) {
                fseek(fp, (long)(recno - 1) * 128, SEEK_SET);
            }

            switch (val.type) {
                case BAS_TYPE_INTEGER: {
                    int16_t v = val.intVal;
                    fwrite(&v, sizeof(v), 1, fp);
                    break;
                }
                case BAS_TYPE_LONG: {
                    int32_t v = val.longVal;
                    fwrite(&v, sizeof(v), 1, fp);
                    break;
                }
                case BAS_TYPE_SINGLE: {
                    float v = val.sngVal;
                    fwrite(&v, sizeof(v), 1, fp);
                    break;
                }
                case BAS_TYPE_DOUBLE: {
                    double v = val.dblVal;
                    fwrite(&v, sizeof(v), 1, fp);
                    break;
                }
                case BAS_TYPE_STRING: {
                    // Write length-prefixed string
                    int16_t len = val.strVal ? (int16_t)val.strVal->len : 0;
                    fwrite(&len, sizeof(len), 1, fp);
                    if (len > 0 && val.strVal) {
                        fwrite(val.strVal->data, 1, len, fp);
                    }
                    break;
                }
                default: {
                    int16_t zero = 0;
                    fwrite(&zero, sizeof(zero), 1, fp);
                    break;
                }
            }

            fflush(fp);
            basValRelease(&val);
            break;
        }

        case OP_FILE_SEEK: {
            // Pop position and channel, seek
            BasValueT posVal;
            BasValueT channelVal;

            if (!pop(vm, &posVal) || !pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            int32_t pos     = (int32_t)basValToNumber(posVal);
            basValRelease(&channelVal);
            basValRelease(&posVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            // QB SEEK is 1-based
            fseek((FILE *)vm->files[channel].handle, (long)(pos - 1), SEEK_SET);
            break;
        }

        case OP_FILE_LOF: {
            // Pop channel, push file length
            BasValueT channelVal;

            if (!pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            basValRelease(&channelVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            FILE *fp = (FILE *)vm->files[channel].handle;
            long savedPos = ftell(fp);
            fseek(fp, 0, SEEK_END);
            long length = ftell(fp);
            fseek(fp, savedPos, SEEK_SET);

            if (!push(vm, basValLong((int32_t)length))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_FILE_LOC: {
            // Pop channel, push current position
            BasValueT channelVal;

            if (!pop(vm, &channelVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            basValRelease(&channelVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            long pos = ftell((FILE *)vm->files[channel].handle);

            // QB returns 1-based position
            if (!push(vm, basValLong((int32_t)(pos + 1)))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_FILE_FREEFILE: {
            // Push the next available file channel number
            int32_t freeNum = 0;

            for (int32_t i = 1; i < BAS_VM_MAX_FILES; i++) {
                if (!vm->files[i].handle) {
                    freeNum = i;
                    break;
                }
            }

            if (freeNum == 0) {
                runtimeError(vm, 67, "Too many files open");
                return BAS_VM_FILE_ERROR;
            }

            if (!push(vm, basValInteger((int16_t)freeNum))) {
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        case OP_FILE_INPUT_N: {
            // Pop channel and n, read n chars, push string
            BasValueT channelVal;
            BasValueT nVal;

            if (!pop(vm, &channelVal) || !pop(vm, &nVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t channel = (int32_t)basValToNumber(channelVal);
            int32_t n       = (int32_t)basValToNumber(nVal);
            basValRelease(&channelVal);
            basValRelease(&nVal);

            if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) {
                runtimeError(vm, 52, "Bad file number or file not open");
                return BAS_VM_FILE_ERROR;
            }

            if (n < 0) {
                n = 0;
            }

            if (n > 32767) {
                n = 32767;
            }

            char *buf = (char *)malloc(n + 1);

            if (!buf) {
                runtimeError(vm, 7, "Out of memory");
                return BAS_VM_OUT_OF_MEMORY;
            }

            int32_t bytesRead = (int32_t)fread(buf, 1, n, (FILE *)vm->files[channel].handle);
            buf[bytesRead] = '\0';

            BasStringT *s = basStringNew(buf, bytesRead);
            free(buf);

            BasValueT result;
            result.type   = BAS_TYPE_STRING;
            result.strVal = s;

            if (!push(vm, result)) {
                basStringUnref(s);
                return BAS_VM_STACK_OVERFLOW;
            }

            break;
        }

        default:
            return BAS_VM_BAD_OPCODE;
    }

    return BAS_VM_OK;
}


// ============================================================
// execLogical
// ============================================================

static BasVmResultE execLogical(BasVmT *vm, uint8_t op) {
    if (op == OP_NOT) {
        if (vm->sp < 1) {
            return BAS_VM_STACK_UNDERFLOW;
        }

        BasValueT *top = &vm->stack[vm->sp - 1];
        int32_t n      = (int32_t)basValToNumber(*top);
        basValRelease(top);
        *top = basValInteger((int16_t)(~n));
        return BAS_VM_OK;
    }

    BasValueT b;
    BasValueT a;

    if (!pop(vm, &b) || !pop(vm, &a)) {
        return BAS_VM_STACK_UNDERFLOW;
    }

    int32_t na = (int32_t)basValToNumber(a);
    int32_t nb = (int32_t)basValToNumber(b);
    basValRelease(&a);
    basValRelease(&b);

    int32_t result;

    switch (op) {
        case OP_AND: result = na & nb;         break;
        case OP_OR:  result = na | nb;         break;
        case OP_XOR: result = na ^ nb;         break;
        case OP_EQV: result = ~(na ^ nb);      break;
        case OP_IMP: result = (~na) | nb;      break;
        default:     result = 0;               break;
    }

    push(vm, basValInteger((int16_t)result));
    return BAS_VM_OK;
}


// ============================================================
// execMath
// ============================================================

static BasVmResultE execMath(BasVmT *vm, uint8_t op) {
    if (op == OP_MATH_RND) {
        // Pop the dummy arg (parser pushes -1 for RND())
        BasValueT dummy;

        if (!pop(vm, &dummy)) {
            return BAS_VM_STACK_UNDERFLOW;
        }

        basValRelease(&dummy);

        double r = (double)rand() / (double)RAND_MAX;

        if (!push(vm, basValSingle((float)r))) {
            return BAS_VM_STACK_OVERFLOW;
        }

        return BAS_VM_OK;
    }

    if (op == OP_MATH_RANDOMIZE) {
        BasValueT val;

        if (!pop(vm, &val)) {
            return BAS_VM_STACK_UNDERFLOW;
        }

        double n = basValToNumber(val);
        basValRelease(&val);

        if (n < 0) {
            srand((unsigned int)time(NULL));
        } else {
            srand((unsigned int)n);
        }

        return BAS_VM_OK;
    }

    // All other math ops take one argument
    if (vm->sp < 1) {
        return BAS_VM_STACK_UNDERFLOW;
    }

    BasValueT *top = &vm->stack[vm->sp - 1];
    double n       = basValToNumber(*top);
    double result;

    switch (op) {
        case OP_MATH_ABS: result = fabs(n);  break;
        case OP_MATH_INT: result = floor(n); break;
        case OP_MATH_FIX: result = (n >= 0) ? floor(n) : ceil(n); break;
        case OP_MATH_SGN: result = (n > 0) ? 1.0 : (n < 0) ? -1.0 : 0.0; break;
        case OP_MATH_SQR: result = sqrt(n);  break;
        case OP_MATH_SIN: result = sin(n);   break;
        case OP_MATH_COS: result = cos(n);   break;
        case OP_MATH_TAN: result = tan(n);   break;
        case OP_MATH_ATN: result = atan(n);  break;
        case OP_MATH_LOG: result = log(n);   break;
        case OP_MATH_EXP: result = exp(n);   break;
        default:          result = 0.0;      break;
    }

    basValRelease(top);
    *top = basValDouble(result);
    return BAS_VM_OK;
}


// ============================================================
// execPrint
// ============================================================

static BasVmResultE execPrint(BasVmT *vm) {
    BasValueT val;

    if (!pop(vm, &val)) {
        return BAS_VM_STACK_UNDERFLOW;
    }

    // QB prints numeric values with a trailing space
    bool isNumeric = (val.type != BAS_TYPE_STRING);

    BasStringT *s = basValFormatString(val);
    basValRelease(&val);

    if (vm->printFn && s) {
        vm->printFn(vm->printCtx, s->data, false);

        if (isNumeric) {
            vm->printFn(vm->printCtx, " ", false);
        }
    }

    basStringUnref(s);
    return BAS_VM_OK;
}


// ============================================================
// execStringOp
// ============================================================

static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
    switch (op) {
        case OP_STR_CONCAT: {
            BasValueT b;
            BasValueT a;

            if (!pop(vm, &b) || !pop(vm, &a)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT sa = basValToString(a);
            BasValueT sb = basValToString(b);
            basValRelease(&a);
            basValRelease(&b);

            BasStringT *result = basStringConcat(sa.strVal, sb.strVal);
            basValRelease(&sa);
            basValRelease(&sb);

            BasValueT rv;
            rv.type   = BAS_TYPE_STRING;
            rv.strVal = result;
            push(vm, rv);
            return BAS_VM_OK;
        }

        case OP_STR_LEFT: {
            BasValueT nVal;
            BasValueT sVal;

            if (!pop(vm, &nVal) || !pop(vm, &sVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t n = (int32_t)basValToNumber(nVal);
            basValRelease(&nVal);

            BasValueT sv = basValToString(sVal);
            basValRelease(&sVal);

            BasStringT *result = basStringSub(sv.strVal, 0, n);
            basValRelease(&sv);

            BasValueT rv;
            rv.type   = BAS_TYPE_STRING;
            rv.strVal = result;
            push(vm, rv);
            return BAS_VM_OK;
        }

        case OP_STR_RIGHT: {
            BasValueT nVal;
            BasValueT sVal;

            if (!pop(vm, &nVal) || !pop(vm, &sVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t n = (int32_t)basValToNumber(nVal);
            basValRelease(&nVal);

            BasValueT sv = basValToString(sVal);
            basValRelease(&sVal);

            int32_t start = sv.strVal->len - n;

            if (start < 0) {
                start = 0;
            }

            BasStringT *result = basStringSub(sv.strVal, start, n);
            basValRelease(&sv);

            BasValueT rv;
            rv.type   = BAS_TYPE_STRING;
            rv.strVal = result;
            push(vm, rv);
            return BAS_VM_OK;
        }

        case OP_STR_MID: {
            BasValueT lenVal;
            BasValueT startVal;
            BasValueT sVal;

            if (!pop(vm, &lenVal) || !pop(vm, &startVal) || !pop(vm, &sVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t start = (int32_t)basValToNumber(startVal) - 1;  // 1-based to 0-based
            int32_t len   = (int32_t)basValToNumber(lenVal);
            basValRelease(&startVal);
            basValRelease(&lenVal);

            BasValueT sv = basValToString(sVal);
            basValRelease(&sVal);

            BasStringT *result = basStringSub(sv.strVal, start, len);
            basValRelease(&sv);

            BasValueT rv;
            rv.type   = BAS_TYPE_STRING;
            rv.strVal = result;
            push(vm, rv);
            return BAS_VM_OK;
        }

        case OP_STR_MID2: {
            BasValueT startVal;
            BasValueT sVal;

            if (!pop(vm, &startVal) || !pop(vm, &sVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t start = (int32_t)basValToNumber(startVal) - 1;
            basValRelease(&startVal);

            BasValueT sv = basValToString(sVal);
            basValRelease(&sVal);

            BasStringT *result = basStringSub(sv.strVal, start, sv.strVal->len - start);
            basValRelease(&sv);

            BasValueT rv;
            rv.type   = BAS_TYPE_STRING;
            rv.strVal = result;
            push(vm, rv);
            return BAS_VM_OK;
        }

        case OP_STR_LEN: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            BasValueT sv   = basValToString(*top);
            int32_t len    = sv.strVal ? sv.strVal->len : 0;
            basValRelease(&sv);
            basValRelease(top);
            *top = basValInteger((int16_t)len);
            return BAS_VM_OK;
        }

        case OP_STR_INSTR: {
            BasValueT findVal;
            BasValueT sVal;

            if (!pop(vm, &findVal) || !pop(vm, &sVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT sv = basValToString(sVal);
            BasValueT fv = basValToString(findVal);
            basValRelease(&sVal);
            basValRelease(&findVal);

            int32_t pos = 0;
            char *found = strstr(sv.strVal->data, fv.strVal->data);

            if (found) {
                pos = (int32_t)(found - sv.strVal->data) + 1;  // 1-based
            }

            basValRelease(&sv);
            basValRelease(&fv);
            push(vm, basValInteger((int16_t)pos));
            return BAS_VM_OK;
        }

        case OP_STR_INSTR3: {
            // INSTR(start, string, find)
            BasValueT findVal;
            BasValueT sVal;
            BasValueT startVal;

            if (!pop(vm, &findVal) || !pop(vm, &sVal) || !pop(vm, &startVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            int32_t startPos = (int32_t)basValToNumber(startVal) - 1; // 0-based
            basValRelease(&startVal);

            BasValueT sv = basValToString(sVal);
            BasValueT fv = basValToString(findVal);
            basValRelease(&sVal);
            basValRelease(&findVal);

            int32_t pos = 0;

            if (startPos >= 0 && startPos < sv.strVal->len) {
                char *found = strstr(sv.strVal->data + startPos, fv.strVal->data);

                if (found) {
                    pos = (int32_t)(found - sv.strVal->data) + 1; // 1-based
                }
            }

            basValRelease(&sv);
            basValRelease(&fv);
            push(vm, basValInteger((int16_t)pos));
            return BAS_VM_OK;
        }

        case OP_STR_UCASE:
        case OP_STR_LCASE:
        case OP_STR_TRIM:
        case OP_STR_LTRIM:
        case OP_STR_RTRIM: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            BasValueT sv   = basValToString(*top);
            basValRelease(top);

            BasStringT *src = sv.strVal;
            BasStringT *result;

            if (op == OP_STR_UCASE || op == OP_STR_LCASE) {
                result = basStringNew(src->data, src->len);

                for (int32_t i = 0; i < result->len; i++) {
                    if (op == OP_STR_UCASE && result->data[i] >= 'a' && result->data[i] <= 'z') {
                        result->data[i] -= 32;
                    } else if (op == OP_STR_LCASE && result->data[i] >= 'A' && result->data[i] <= 'Z') {
                        result->data[i] += 32;
                    }
                }
            } else {
                int32_t start = 0;
                int32_t end   = src->len;

                if (op == OP_STR_LTRIM || op == OP_STR_TRIM) {
                    while (start < end && src->data[start] == ' ') {
                        start++;
                    }
                }

                if (op == OP_STR_RTRIM || op == OP_STR_TRIM) {
                    while (end > start && src->data[end - 1] == ' ') {
                        end--;
                    }
                }

                result = basStringSub(src, start, end - start);
            }

            basValRelease(&sv);
            top->type   = BAS_TYPE_STRING;
            top->strVal = result;
            return BAS_VM_OK;
        }

        case OP_STR_CHR: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            int32_t code   = (int32_t)basValToNumber(*top);
            char buf[2]    = { (char)(code & 0xFF), '\0' };
            basValRelease(top);
            *top = basValStringFromC(buf);
            return BAS_VM_OK;
        }

        case OP_STR_ASC: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            BasValueT sv   = basValToString(*top);
            int32_t code   = (sv.strVal && sv.strVal->len > 0) ? (unsigned char)sv.strVal->data[0] : 0;
            basValRelease(&sv);
            basValRelease(top);
            *top = basValInteger((int16_t)code);
            return BAS_VM_OK;
        }

        case OP_STR_SPACE: {
            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            int32_t n      = (int32_t)basValToNumber(*top);
            basValRelease(top);

            if (n < 0) {
                n = 0;
            }

            if (n > 32767) {
                n = 32767;
            }

            BasStringT *s = basStringAlloc(n + 1);
            memset(s->data, ' ', n);
            s->data[n] = '\0';
            s->len = n;

            top->type   = BAS_TYPE_STRING;
            top->strVal = s;
            return BAS_VM_OK;
        }

        case OP_STR_FIXLEN: {
            // [uint16 len] pop string, pad/truncate to fixed length, push result
            uint16_t fixLen = readUint16(vm);

            if (vm->sp < 1) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT *top = &vm->stack[vm->sp - 1];
            BasValueT sv   = basValToString(*top);
            basValRelease(top);

            BasStringT *src = sv.strVal;
            BasStringT *result = basStringAlloc(fixLen + 1);
            result->len = fixLen;

            int32_t srcLen = src ? src->len : 0;
            int32_t copyLen = srcLen < (int32_t)fixLen ? srcLen : (int32_t)fixLen;

            if (copyLen > 0 && src) {
                memcpy(result->data, src->data, copyLen);
            }

            // Pad with spaces
            for (int32_t i = copyLen; i < (int32_t)fixLen; i++) {
                result->data[i] = ' ';
            }

            result->data[fixLen] = '\0';
            basValRelease(&sv);

            top->type   = BAS_TYPE_STRING;
            top->strVal = result;
            return BAS_VM_OK;
        }

        case OP_STR_MID_ASGN: {
            // Pop replacement, len, start, str; push modified string
            BasValueT replVal;
            BasValueT lenVal;
            BasValueT startVal;
            BasValueT strVal;

            if (!pop(vm, &replVal) || !pop(vm, &lenVal) || !pop(vm, &startVal) || !pop(vm, &strVal)) {
                return BAS_VM_STACK_UNDERFLOW;
            }

            BasValueT sv    = basValToString(strVal);
            BasValueT rv    = basValToString(replVal);
            int32_t start   = (int32_t)basValToNumber(startVal) - 1; // 1-based to 0-based
            int32_t len     = (int32_t)basValToNumber(lenVal);
            basValRelease(&strVal);
            basValRelease(&startVal);
            basValRelease(&lenVal);
            basValRelease(&replVal);

            BasStringT *src  = sv.strVal;
            BasStringT *repl = rv.strVal;
            int32_t srcLen   = src ? src->len : 0;
            int32_t replLen  = repl ? repl->len : 0;

            // If len is 0, use replacement length
            if (len <= 0) {
                len = replLen;
            }

            // Clamp to available replacement length
            if (len > replLen) {
                len = replLen;
            }

            // Create a copy of the original string
            BasStringT *result = basStringNew(src ? src->data : "", srcLen);

            // Replace characters
            if (start >= 0 && start < srcLen && len > 0) {
                int32_t maxReplace = srcLen - start;
                if (len > maxReplace) {
                    len = maxReplace;
                }
                if (repl) {
                    memcpy(result->data + start, repl->data, len);
                }
            }

            basValRelease(&sv);
            basValRelease(&rv);

            BasValueT resultVal;
            resultVal.type   = BAS_TYPE_STRING;
            resultVal.strVal = result;

            if (!push(vm, resultVal)) {
                basStringUnref(result);
                return BAS_VM_STACK_OVERFLOW;
            }

            return BAS_VM_OK;
        }

        default:
            return BAS_VM_BAD_OPCODE;
    }
}


// ============================================================
// pop
// ============================================================

static bool pop(BasVmT *vm, BasValueT *val) {
    if (vm->sp <= 0) {
        return false;
    }

    *val = vm->stack[--vm->sp];
    return true;
}


// ============================================================
// push
// ============================================================

static bool push(BasVmT *vm, BasValueT val) {
    if (vm->sp >= BAS_VM_STACK_SIZE) {
        return false;
    }

    vm->stack[vm->sp++] = val;
    return true;
}


// ============================================================
// readInt16
// ============================================================

static int16_t readInt16(BasVmT *vm) {
    int16_t val;
    memcpy(&val, &vm->module->code[vm->pc], sizeof(int16_t));
    vm->pc += sizeof(int16_t);
    return val;
}


// ============================================================
// readUint8
// ============================================================

static uint8_t readUint8(BasVmT *vm) {
    return vm->module->code[vm->pc++];
}


// ============================================================
// readUint16
// ============================================================

static uint16_t readUint16(BasVmT *vm) {
    uint16_t val;
    memcpy(&val, &vm->module->code[vm->pc], sizeof(uint16_t));
    vm->pc += sizeof(uint16_t);
    return val;
}


// ============================================================
// runtimeError
// ============================================================

static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg) {
    vm->errorNumber = errNum;

    if (vm->currentLine > 0) {
        snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d on line %d: %s", (int)errNum, (int)vm->currentLine, msg);
    } else {
        snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d at PC %d: %s", (int)errNum, (int)vm->pc, msg);
    }
}
