DVX_GUI/apps/dvxbasic/runtime/vm.c

5532 lines
165 KiB
C

// 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 <dirent.h>
#include <fnmatch.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.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 void dirClose(void);
static BasVmResultE execFileOp(BasVmT *vm, uint8_t op);
static BasVmResultE execFsOp(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);
}
}
// Close Dir$ iterator
dirClose();
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);
case OP_FS_KILL:
case OP_FS_NAME:
case OP_FS_FILECOPY:
case OP_FS_MKDIR:
case OP_FS_RMDIR:
case OP_FS_CHDIR:
case OP_FS_CHDRIVE:
case OP_FS_CURDIR:
case OP_FS_DIR:
case OP_FS_DIR_NEXT:
case OP_FS_FILELEN:
case OP_FS_GETATTR:
case OP_FS_SETATTR:
return execFsOp(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 *libName = libNameIdx < (uint16_t)vm->module->constCount
? vm->module->constants[libNameIdx]->data : "";
const char *funcName = funcNameIdx < (uint16_t)vm->module->constCount
? vm->module->constants[funcNameIdx]->data : "";
result = vm->ext.callExtern(vm->ext.ctx, funcPtr, libName, 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;
}
// ============================================================
// execFsOp -- filesystem operations (Kill, Name, MkDir, etc.)
// ============================================================
// Dir$ state -- persists across Dir$() calls within a VM session
static DIR *sDirHandle = NULL;
static char sDirPattern[260];
static char sDirPath[260];
static void dirClose(void) {
if (sDirHandle) {
closedir(sDirHandle);
sDirHandle = NULL;
}
}
static const char *dirNext(void) {
if (!sDirHandle) {
return NULL;
}
struct dirent *ent;
while ((ent = readdir(sDirHandle)) != NULL) {
if (fnmatch(sDirPattern, ent->d_name, FNM_CASEFOLD) == 0) {
return ent->d_name;
}
}
dirClose();
return NULL;
}
static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
switch (op) {
case OP_FS_KILL: {
BasValueT fnVal;
if (!pop(vm, &fnVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT fnStr = basValToString(fnVal);
basValRelease(&fnVal);
if (remove(fnStr.strVal->data) != 0) {
basValRelease(&fnStr);
runtimeError(vm, 53, "File not found");
return BAS_VM_FILE_ERROR;
}
basValRelease(&fnStr);
break;
}
case OP_FS_NAME: {
BasValueT newVal;
BasValueT oldVal;
if (!pop(vm, &newVal) || !pop(vm, &oldVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT newStr = basValToString(newVal);
BasValueT oldStr = basValToString(oldVal);
basValRelease(&newVal);
basValRelease(&oldVal);
if (rename(oldStr.strVal->data, newStr.strVal->data) != 0) {
basValRelease(&oldStr);
basValRelease(&newStr);
runtimeError(vm, 58, "File already exists or rename failed");
return BAS_VM_FILE_ERROR;
}
basValRelease(&oldStr);
basValRelease(&newStr);
break;
}
case OP_FS_FILECOPY: {
BasValueT dstVal;
BasValueT srcVal;
if (!pop(vm, &dstVal) || !pop(vm, &srcVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT dstStr = basValToString(dstVal);
BasValueT srcStr = basValToString(srcVal);
basValRelease(&dstVal);
basValRelease(&srcVal);
FILE *fin = fopen(srcStr.strVal->data, "rb");
FILE *fout = NULL;
if (fin) {
fout = fopen(dstStr.strVal->data, "wb");
}
basValRelease(&srcStr);
basValRelease(&dstStr);
if (!fin || !fout) {
if (fin) {
fclose(fin);
}
if (fout) {
fclose(fout);
}
runtimeError(vm, 53, "File not found or cannot create destination");
return BAS_VM_FILE_ERROR;
}
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), fin)) > 0) {
fwrite(buf, 1, n, fout);
}
fclose(fin);
fclose(fout);
break;
}
case OP_FS_MKDIR: {
BasValueT pathVal;
if (!pop(vm, &pathVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT pathStr = basValToString(pathVal);
basValRelease(&pathVal);
#ifdef _WIN32
int rc = mkdir(pathStr.strVal->data);
#else
int rc = mkdir(pathStr.strVal->data, 0755);
#endif
if (rc != 0) {
basValRelease(&pathStr);
runtimeError(vm, 75, "Path/File access error");
return BAS_VM_FILE_ERROR;
}
basValRelease(&pathStr);
break;
}
case OP_FS_RMDIR: {
BasValueT pathVal;
if (!pop(vm, &pathVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT pathStr = basValToString(pathVal);
basValRelease(&pathVal);
if (rmdir(pathStr.strVal->data) != 0) {
basValRelease(&pathStr);
runtimeError(vm, 75, "Path/File access error");
return BAS_VM_FILE_ERROR;
}
basValRelease(&pathStr);
break;
}
case OP_FS_CHDIR: {
BasValueT pathVal;
if (!pop(vm, &pathVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT pathStr = basValToString(pathVal);
basValRelease(&pathVal);
if (chdir(pathStr.strVal->data) != 0) {
basValRelease(&pathStr);
runtimeError(vm, 76, "Path not found");
return BAS_VM_FILE_ERROR;
}
basValRelease(&pathStr);
break;
}
case OP_FS_CHDRIVE: {
BasValueT driveVal;
if (!pop(vm, &driveVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT driveStr = basValToString(driveVal);
basValRelease(&driveVal);
// On DOS, change to the drive's root directory
if (driveStr.strVal->data[0]) {
char drivePath[4];
drivePath[0] = driveStr.strVal->data[0];
drivePath[1] = ':';
drivePath[2] = '\0';
chdir(drivePath);
}
basValRelease(&driveStr);
break;
}
case OP_FS_CURDIR: {
char cwd[260];
if (!getcwd(cwd, sizeof(cwd))) {
cwd[0] = '\0';
}
BasStringT *s = basStringNew(cwd, (int32_t)strlen(cwd));
BasValueT result;
result.type = BAS_TYPE_STRING;
result.strVal = s;
if (!push(vm, result)) {
basStringUnref(s);
return BAS_VM_STACK_OVERFLOW;
}
break;
}
case OP_FS_DIR: {
BasValueT patVal;
if (!pop(vm, &patVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT patStr = basValToString(patVal);
basValRelease(&patVal);
dirClose();
// Split pattern into directory + filename pattern
const char *pat = patStr.strVal->data;
const char *lastSep = NULL;
for (const char *c = pat; *c; c++) {
if (*c == '/' || *c == '\\') {
lastSep = c;
}
}
if (lastSep) {
int32_t dirLen = (int32_t)(lastSep - pat);
if (dirLen >= (int32_t)sizeof(sDirPath)) {
dirLen = (int32_t)sizeof(sDirPath) - 1;
}
memcpy(sDirPath, pat, dirLen);
sDirPath[dirLen] = '\0';
snprintf(sDirPattern, sizeof(sDirPattern), "%s", lastSep + 1);
} else {
snprintf(sDirPath, sizeof(sDirPath), ".");
snprintf(sDirPattern, sizeof(sDirPattern), "%s", pat);
}
basValRelease(&patStr);
if (sDirPattern[0] == '\0') {
snprintf(sDirPattern, sizeof(sDirPattern), "*");
}
sDirHandle = opendir(sDirPath);
const char *match = dirNext();
const char *text = match ? match : "";
BasStringT *s = basStringNew(text, (int32_t)strlen(text));
BasValueT result;
result.type = BAS_TYPE_STRING;
result.strVal = s;
if (!push(vm, result)) {
basStringUnref(s);
return BAS_VM_STACK_OVERFLOW;
}
break;
}
case OP_FS_DIR_NEXT: {
const char *match = dirNext();
const char *text = match ? match : "";
BasStringT *s = basStringNew(text, (int32_t)strlen(text));
BasValueT result;
result.type = BAS_TYPE_STRING;
result.strVal = s;
if (!push(vm, result)) {
basStringUnref(s);
return BAS_VM_STACK_OVERFLOW;
}
break;
}
case OP_FS_FILELEN: {
BasValueT fnVal;
if (!pop(vm, &fnVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT fnStr = basValToString(fnVal);
basValRelease(&fnVal);
struct stat st;
int32_t size = 0;
if (stat(fnStr.strVal->data, &st) == 0) {
size = (int32_t)st.st_size;
}
basValRelease(&fnStr);
BasValueT result;
result.type = BAS_TYPE_LONG;
result.longVal = size;
if (!push(vm, result)) {
return BAS_VM_STACK_OVERFLOW;
}
break;
}
case OP_FS_GETATTR: {
BasValueT fnVal;
if (!pop(vm, &fnVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT fnStr = basValToString(fnVal);
basValRelease(&fnVal);
struct stat st;
int32_t attrs = 0;
if (stat(fnStr.strVal->data, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
attrs |= 16; // vbDirectory
}
if (!(st.st_mode & S_IWUSR)) {
attrs |= 1; // vbReadOnly
}
}
basValRelease(&fnStr);
BasValueT result;
result.type = BAS_TYPE_INTEGER;
result.intVal = (int16_t)attrs;
if (!push(vm, result)) {
return BAS_VM_STACK_OVERFLOW;
}
break;
}
case OP_FS_SETATTR: {
BasValueT attrVal;
BasValueT fnVal;
if (!pop(vm, &attrVal) || !pop(vm, &fnVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT fnStr = basValToString(fnVal);
int32_t attrs = (int32_t)basValToNumber(attrVal);
basValRelease(&fnVal);
basValRelease(&attrVal);
struct stat st;
if (stat(fnStr.strVal->data, &st) == 0) {
mode_t mode = st.st_mode;
if (attrs & 1) {
mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH);
} else {
mode |= S_IWUSR;
}
chmod(fnStr.strVal->data, mode);
}
basValRelease(&fnStr);
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);
}
}