10076 lines
308 KiB
C
10076 lines
308 KiB
C
// The MIT License (MIT)
|
|
//
|
|
// Copyright (C) 2026 Scott Duensing
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to
|
|
// deal in the Software without restriction, including without limitation the
|
|
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
// sell copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
// IN THE SOFTWARE.
|
|
|
|
// vm.c -- DVX BASIC virtual machine implementation
|
|
//
|
|
// Stack-based p-code interpreter. Executes one instruction per
|
|
// basVmStep() call, or runs until completion via basVmRun().
|
|
// PRINT/INPUT, forms and extern calls go through host-provided
|
|
// callbacks; file, directory and path statements use the C library
|
|
// (stdio, dirent, fnmatch, stat) and the DVX platform helpers.
|
|
|
|
#include "vm.h"
|
|
#include "../compiler/opcodes.h"
|
|
#include "basErrors.h"
|
|
#include "dates.h"
|
|
#include "dvxPlat.h"
|
|
#include "dvxPrefs.h"
|
|
#include "screen.h"
|
|
|
|
#include <ctype.h>
|
|
#include <dirent.h>
|
|
#include <fnmatch.h>
|
|
#include <limits.h>
|
|
#include <math.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
// Capacity of the work buffers used by the numeric formatter shared by
|
|
// OP_PRINT_USING and OP_FORMAT. Bounds the worst-case output of a
|
|
// pathological format string or numeric magnitude.
|
|
#define BAS_FORMAT_BUF_SIZE 256
|
|
|
|
// Fixed (non-argument) stack operands an OP_CALL_METHOD carries: the
|
|
// method-name value and the control-reference value, both below the args.
|
|
#define BAS_CALL_METHOD_FIXED_OPERANDS 2
|
|
|
|
// File attribute bits returned/accepted by GETATTR/SETATTR (BASIC vbReadOnly/
|
|
// vbDirectory).
|
|
#define BAS_ATTR_READONLY 1 // file is read-only
|
|
#define BAS_ATTR_DIRECTORY 16 // path is a directory
|
|
|
|
// Scratch buffer capacities used by individual opcode handlers.
|
|
#define BAS_INPUT_PROMPT_LEN 512 // OP_INPUT: user prompt + "? "
|
|
#define BAS_FILE_LINE_CHUNK 1024 // INPUT #/LINE INPUT #: fgets chunk while growing a line
|
|
#define BAS_RADIX_STR_LEN 16 // HEX$/OCT$ output ("FFFFFFFF", "37777777777")
|
|
#define BAS_TIME_STR_LEN 16 // TIME$ "HH:MM:SS"
|
|
#define BAS_DATE_STR_LEN 32 // DATE$ "MM-DD-YYYY"
|
|
#define BAS_SCI_FMT_LEN 32 // printf format built for PRINT USING ^^^^
|
|
#define BAS_FORMAT_PERCENT "PERCENT" // FORMAT$ named format
|
|
#define BAS_FORMAT_OVERFLOW_MARK '%' // PRINT USING: value wider than its field
|
|
#define BAS_USING_ESCAPE '_' // PRINT USING: next format character is literal
|
|
#define BAS_PRINTER_NEWLINE "\r\n" // line end sent to a printer
|
|
#define BAS_PRINTER_NEWLINE_LEN 2
|
|
#define BAS_PRINTER_NAME_PRN "PRN" // the printer in the settings
|
|
#define BAS_PRINTER_NAME_LPT "LPT" // LPT1..LPT3
|
|
#define BAS_PRINTER_NAME_MAX 5 // "LPT1:" / "PRN:"
|
|
#define BAS_PRINTER_FIRST_DIGIT '1'
|
|
#define BAS_SPOOL_CHUNK 512 // spool bytes sent per printer write
|
|
#define BAS_LOC_SEQ_BLOCK 128 // LOC on a sequential channel counts 128-byte blocks (QB)
|
|
#define BAS_SCI_MARKER "^^^^" // PRINT USING scientific-notation marker
|
|
|
|
// Directory permission bits for MKDIR on POSIX hosts (rwxr-xr-x).
|
|
#define BAS_MKDIR_MODE 0755
|
|
|
|
// Largest count STRING$/SPACE$/INPUT$ accept (VB3 string space limit).
|
|
#define BAS_STRING_FUNC_MAX 65535
|
|
|
|
// References a REDIM PRESERVE source array holds when the REDIM'd variable
|
|
// is its only owner: the variable itself plus the copy the statement
|
|
// loaded onto the eval stack. More means another variable shares it, so
|
|
// preserved TYPE elements are cloned rather than aliased.
|
|
#define BAS_REDIM_OWNER_REFS 2
|
|
|
|
// Classic BASIC FUNCTION result slot (the compiler stores the return
|
|
// value in local 0; the epilogue is LOAD_LOCAL 0 + RET_VAL).
|
|
#define BAS_FUNC_RESULT_SLOT 0
|
|
|
|
// TIMER: seconds per hour/minute and microseconds per second.
|
|
#define BAS_SECS_PER_HOUR 3600.0
|
|
#define BAS_SECS_PER_MINUTE 60.0
|
|
#define BAS_USEC_PER_SEC 1000000.0
|
|
#define BAS_SECS_PER_DAY 86400.0
|
|
|
|
// struct tm counts years from 1900.
|
|
#define BAS_TM_YEAR_BASE 1900
|
|
|
|
// OP_BUILTIN: fields of DATESERIAL / TIMESERIAL, the "all" value of
|
|
// counts and start positions (REPLACE$ count, SPLIT limit, INSTRREV
|
|
// start) and the delimiter SPLIT and JOIN$ use when none is given.
|
|
#define BAS_SERIAL_PARTS 3
|
|
#define BAS_COUNT_ALL (-1)
|
|
#define BAS_DEFAULT_DELIMITER " "
|
|
|
|
// SLEEP: pause between event pumps while waiting (microseconds).
|
|
#define BAS_SLEEP_POLL_USEC 10000
|
|
|
|
// BEEP / SOUND / PLAY / FRAMEWAIT waiting without an event pump: pause
|
|
// between checks (microseconds).
|
|
#define BAS_WAIT_POLL_USEC 1000
|
|
|
|
// RND: rand() scaled by RAND_MAX + 1 so the result stays below 1.0, and
|
|
// the multiplier that spreads a negative RND(n) argument's fraction into
|
|
// the reseed value (RND(-1.5) and RND(-1) must seed differently).
|
|
#define BAS_RND_SCALE ((double)RAND_MAX + 1.0)
|
|
#define BAS_RND_SEED_SPREAD 65536.0
|
|
|
|
// A seed is reduced modulo 2^32 before it becomes srand's unsigned int, so
|
|
// huge and negative seeds convert without undefined behaviour.
|
|
#define BAS_RND_SEED_MODULUS 4294967296.0
|
|
|
|
// RGB packing: bit offsets and per-channel mask.
|
|
#define BAS_RGB_RED_SHIFT 16
|
|
#define BAS_RGB_GREEN_SHIFT 8
|
|
#define BAS_RGB_CHANNEL_MASK 0xFF
|
|
#define BAS_CHAR_MASK 0xFF
|
|
|
|
// Accesses of an object member: a read, or an assignment made with Let
|
|
// (obj.p = v) or Set (Set obj.p = v).
|
|
#define BAS_MEMBER_OP_GET 0
|
|
#define BAS_MEMBER_OP_LET 1
|
|
#define BAS_MEMBER_OP_SET 2
|
|
|
|
// findMember: rank of a member that cannot serve the access.
|
|
#define BAS_MEMBER_NO_RANK 2
|
|
|
|
// Collection members, the most arguments Add takes (item, key, before,
|
|
// after), and how its item array grows.
|
|
#define BAS_COLL_ADD "Add"
|
|
#define BAS_COLL_COUNT "Count"
|
|
#define BAS_COLL_ITEM "Item"
|
|
#define BAS_COLL_REMOVE "Remove"
|
|
#define BAS_COLL_ADD_ARGS 4
|
|
#define BAS_COLL_MIN_CAP 4
|
|
#define BAS_COLL_GROWTH 2
|
|
|
|
// TYPENAME / TYPEOF names that are not class or control names, and the
|
|
// suffix of an array's type name ("Integer()").
|
|
#define BAS_TYPENAME_COLLECTION "Collection"
|
|
#define BAS_TYPENAME_CONTROL "Control"
|
|
#define BAS_TYPENAME_FORM "Form"
|
|
#define BAS_TYPENAME_NOTHING "Nothing"
|
|
#define BAS_TYPENAME_OBJECT "Object"
|
|
#define BAS_TYPENAME_UDT "UserType"
|
|
#define BAS_ARRAY_SUFFIX "()"
|
|
#define BAS_ARRAY_SUFFIX_LEN 3
|
|
|
|
// ISNUMERIC: the characters that start an exponent, and the length of
|
|
// the &H / &O prefix.
|
|
#define BAS_EXPONENT_CHARS "eEdD"
|
|
#define BAS_RADIX_PREFIX_LEN 2
|
|
|
|
// BASIC's True as a bit pattern (every bit set).
|
|
#define BAS_TRUE_BITS (-1)
|
|
|
|
// Divergence between the PRINT USING and FORMAT$ numeric formatters,
|
|
// captured as options so a single helper serves both call sites.
|
|
typedef struct BasNumFormatT {
|
|
bool hasDecimal;
|
|
bool hasComma;
|
|
bool plusAtStart;
|
|
bool plusAtEnd;
|
|
bool minusAtEnd;
|
|
bool asteriskFill;
|
|
bool dollarFloat;
|
|
bool markOverflow; // PRINT USING: prefix '%' when the value does not fit the field
|
|
int32_t width; // field width the result is right-aligned in
|
|
int32_t minIntDigits; // integer digits forced by '0' placeholders (zero padded)
|
|
int32_t digitsAfter;
|
|
} BasNumFormatT;
|
|
|
|
// One decoded TYPE layout entry (see BAS_LAYOUT_* in opcodes.h).
|
|
typedef struct BasLayoutEntryT {
|
|
uint8_t type;
|
|
int32_t fixedLen; // STRING: 0 = variable length
|
|
int32_t typeId; // UDT
|
|
int32_t fieldCount; // UDT
|
|
} BasLayoutEntryT;
|
|
|
|
// Output sink for PUT: writes to fp, or with fp NULL only counts the
|
|
// bytes (a RANDOM PUT measures its record before writing it).
|
|
typedef struct BasPutSinkT {
|
|
FILE *fp;
|
|
int32_t bytes;
|
|
} BasPutSinkT;
|
|
|
|
static BasVmResultE allocArray(BasVmT *vm, int32_t dims, uint8_t elementType, bool preserve, BasArrayT **outArr);
|
|
static int32_t argCompare(BasVmT *vm, BasValueT v, bool *outText);
|
|
static BasStringT *argString(BasValueT *arg);
|
|
static void arrayClear(BasArrayT *arr);
|
|
static BasValueT arrValue(BasArrayT *arr);
|
|
static BasVmResultE basVmStepExec(BasVmT *vm);
|
|
static BasVmResultE boundOp(BasVmT *vm, uint8_t op);
|
|
static int32_t builtinDate(uint8_t fn, BasValueT *args, BasValueT *out);
|
|
static int32_t builtinDefaults(uint8_t fn, BasValueT *args, int32_t argc);
|
|
static int32_t builtinObject(BasVmT *vm, uint8_t fn, BasValueT *args, BasValueT *out);
|
|
static int32_t builtinString(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t argc, BasValueT *out);
|
|
static BasVmResultE callMember(BasVmT *vm, BasObjectT *obj, const BasClassMemberT *m, BasValueT *args, int32_t argc, bool wantResult);
|
|
static int32_t clampComponent(int32_t c);
|
|
static void clearErrorState(BasVmT *vm);
|
|
static bool closeChannel(BasVmT *vm, int32_t channel);
|
|
static BasVmResultE collectionAccess(BasVmT *vm, BasObjectT *coll, const char *name, BasValueT *args, int32_t argc, int32_t op);
|
|
static int32_t collectionAdd(BasObjectT *coll, BasValueT *args, int32_t argc);
|
|
static int32_t collectionFind(const BasObjectT *coll, BasValueT index, int32_t *outPos);
|
|
static int32_t collectionKeyPos(const BasObjectT *coll, const BasStringT *key);
|
|
static BasStringT *commandArg(const char *args, int32_t n);
|
|
static BasCallFrameT *currentFrame(BasVmT *vm);
|
|
static void defaultPrint(void *ctx, const char *text, bool newline);
|
|
static void derefArg(BasValueT *arg);
|
|
static BasValueT *derefSlot(BasValueT *slot);
|
|
static void dirClose(BasVmT *vm);
|
|
static const char *dirNext(BasVmT *vm);
|
|
static void discardFrame(BasVmT *vm);
|
|
static bool dispatchError(BasVmT *vm, BasVmResultE result, int32_t floorDepth, int32_t faultPc);
|
|
static BasVmResultE divByZero(BasVmT *vm);
|
|
static int64_t divMod32(uint8_t op, int32_t ia, int32_t ib);
|
|
static void dropArgs(BasValueT *args, int32_t count);
|
|
static void dropValues(BasVmT *vm, int32_t count);
|
|
static int32_t eachCount(BasValueT group, int32_t *outCount);
|
|
static void emptyOperand(BasValueT *v, BasValueT other);
|
|
static const char *errorText(int32_t errNum);
|
|
static BasVmResultE execArith(BasVmT *vm, uint8_t op);
|
|
static BasVmResultE execBuiltin(BasVmT *vm);
|
|
static BasVmResultE execCall(BasVmT *vm, bool keepMe);
|
|
static BasVmResultE execCompare(BasVmT *vm, uint8_t op);
|
|
static BasVmResultE execConvert(BasVmT *vm, uint8_t op);
|
|
static BasVmResultE execDevice(BasVmT *vm, uint8_t op);
|
|
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 execLprint(BasVmT *vm, uint8_t phase);
|
|
static BasVmResultE execMath(BasVmT *vm, uint8_t op);
|
|
static BasVmResultE execPrint(BasVmT *vm);
|
|
static BasVmResultE execStrAppend(BasVmT *vm);
|
|
static BasVmResultE execStringOp(BasVmT *vm, uint8_t op);
|
|
static BasStringT *fillString(char ch, int32_t count);
|
|
static int32_t filterScan(const BasArrayT *arr, const BasStringT *match, bool keep, bool ignoreCase, BasArrayT *out);
|
|
static const BasClassMemberT *findMember(const BasClassInfoT *cls, const char *name, int32_t op);
|
|
static BasValueT forCounterValue(uint8_t varType, bool stepFractional, double v);
|
|
static void formatNumber(double n, const BasNumFormatT *f, char *out, size_t outSize);
|
|
static int32_t forStackFloor(BasVmT *vm);
|
|
static void forStackTrim(BasVmT *vm, int32_t newDepth);
|
|
static int32_t indexArray(const BasArrayT *arr, BasValueT *args, int32_t argc, int32_t *outIdx);
|
|
static BasValueT iniRead(const char *path, const char *sec, const char *key, BasStringT *defStr);
|
|
static BasVmResultE iniWrite(BasVmT *vm, const char *path, const char *sec, const char *key, const char *val);
|
|
static BasValueT integralValue(double v);
|
|
static BasValueT intResult(int32_t v);
|
|
static int32_t intValue(BasValueT v);
|
|
static BasStringT *ioReadBytes(FILE *fp, int32_t len, bool pad);
|
|
static BasStringT *ioReadPrefixedString(FILE *fp);
|
|
static BasValueT ioReadScalar(FILE *fp, uint8_t type);
|
|
static void ioWritePrefixedString(BasPutSinkT *sink, const BasStringT *s);
|
|
static void ioWriteScalar(BasPutSinkT *sink, uint8_t type, BasValueT val);
|
|
static bool isIntegerType(uint8_t type);
|
|
static bool isOneDimArray(BasValueT v);
|
|
static int32_t joinScan(const BasArrayT *arr, const BasStringT *delim, char *out);
|
|
static bool layoutNext(const BasStringT *layout, int32_t *pos, BasLayoutEntryT *out);
|
|
static void localNow(struct tm *out, int32_t *outUsec);
|
|
static BasVmResultE logicalNull(BasVmT *vm, uint8_t op, BasValueT a, BasValueT b);
|
|
static void lprintWrite(BasVmT *vm, const char *data, int32_t len);
|
|
static BasVmResultE mathRangeError(BasVmT *vm, double result);
|
|
static int32_t memberRank(uint8_t kind, int32_t op);
|
|
static BasValueT *memberSlot(BasVmT *vm, uint16_t idx);
|
|
static BasVmResultE memberVarAccess(BasVmT *vm, BasObjectT *obj, const BasClassMemberT *m, BasValueT *args, int32_t argc, BasValueT *value, int32_t op);
|
|
static BasVmResultE newObject(BasVmT *vm, uint16_t cls);
|
|
static BasArrayT *newVector(int32_t count, uint8_t elementType);
|
|
static int32_t nextStatementPc(BasVmT *vm, int32_t start, bool includeStart);
|
|
static BasVmResultE noFormRuntime(BasVmT *vm);
|
|
static bool nullOperands(BasVmT *vm, BasValueT *a, BasValueT *b, BasVmResultE *rc);
|
|
static BasVmResultE objectAccess(BasVmT *vm, BasObjectT *obj, const char *name, BasValueT *args, int32_t argc, BasValueT *value, int32_t op);
|
|
static int32_t objectCheck(BasValueT v, uint16_t cls);
|
|
static const BasClassInfoT *objectClass(const BasVmT *vm, const BasObjectT *obj);
|
|
static BasVmResultE objectDefault(BasVmT *vm, int32_t argc, bool isStore);
|
|
static BasVmResultE objectMemberOp(BasVmT *vm, BasValueT objVal, BasValueT nameVal, BasValueT *args, int32_t argc, BasValueT *value, int32_t op);
|
|
static const char *objectTypeName(const BasVmT *vm, BasValueT v);
|
|
static BasVmResultE openPrinterChannel(BasVmT *vm, int32_t channel, int32_t port, uint8_t mode, int32_t recLen);
|
|
static bool operandFits(BasVmT *vm, int32_t size);
|
|
static void padArg(BasValueT *args, int32_t *argc, int32_t idx, BasValueT value);
|
|
static void parseNumFormat(const char *fmt, int32_t fmtLen, BasNumFormatT *f, bool *outSci);
|
|
static bool pop(BasVmT *vm, BasValueT *val);
|
|
static bool popArgs(BasVmT *vm, int32_t count, ...);
|
|
static BasVmResultE popArrayElement(BasVmT *vm, int32_t dims, BasValueT *outArr, int32_t *outIdx);
|
|
static void popCallFrame(BasVmT *vm);
|
|
static BasVmResultE popFileChannel(BasVmT *vm, int32_t *outChannel, bool requireOpen);
|
|
static bool popPair(BasVmT *vm, BasValueT *a, BasValueT *b);
|
|
static bool popStringArg(BasVmT *vm, BasValueT *out);
|
|
static void primeModuleFrame(BasVmT *vm);
|
|
static int32_t *printColumnOf(BasVmT *vm);
|
|
static bool printerDevice(const char *name, int32_t *outPort);
|
|
static void printText(BasVmT *vm, const char *text, bool newline);
|
|
static int32_t procLocalCount(const BasVmT *vm, int32_t codeAddr, int32_t minCount);
|
|
static bool push(BasVmT *vm, BasValueT val);
|
|
static BasCallFrameT *pushFrame(BasVmT *vm, int32_t returnPc, int32_t codeAddr, int32_t minLocals);
|
|
static void putBounded(char *out, int32_t *idx, int32_t limit, char c);
|
|
static BasVmResultE putValue(BasVmT *vm, BasPutSinkT *sink, int32_t mode, BasValueT val, const BasStringT *layout);
|
|
static void queueTerminate(void *ctx, BasObjectT *obj);
|
|
static BasVmResultE raiseError(BasVmT *vm, int32_t errNum);
|
|
static BasStringT *readField(FILE *fp);
|
|
static int16_t readInt16(BasVmT *vm);
|
|
static BasStringT *readLine(FILE *fp);
|
|
static void readRaw(FILE *fp, void *dst, size_t size);
|
|
static uint16_t readUint16(BasVmT *vm);
|
|
static uint8_t readUint8(BasVmT *vm);
|
|
static BasVmResultE recordBegin(BasVmT *vm, int32_t channel, int32_t recno, long *start);
|
|
static BasVmResultE recordEnd(BasVmT *vm, int32_t channel, long start, bool writing);
|
|
static void releaseVmState(BasVmT *vm);
|
|
static int32_t replaceScan(const BasStringT *s, int32_t start, const BasStringT *find, const BasStringT *repl, int32_t count, bool ignoreCase, char *out);
|
|
static BasVmResultE roundedOperand(BasVmT *vm, BasValueT v, int32_t *out);
|
|
static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning);
|
|
static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg);
|
|
static BasValueT *scopedVarSlot(BasVmT *vm, uint16_t varIdx, uint8_t scopeTag);
|
|
static void seedRandom(double seed);
|
|
static BasVmResultE seekRecord(BasVmT *vm, int32_t channel, int32_t pos);
|
|
static bool sendSpool(BasVmT *vm, BasFileChannelT *ch);
|
|
static void sinkFill(BasPutSinkT *sink, char c, int32_t count);
|
|
static void sinkWrite(BasPutSinkT *sink, const void *data, int32_t len);
|
|
static bool sleepSeconds(BasVmT *vm, int32_t secs);
|
|
static int32_t splitScan(const BasStringT *s, const BasStringT *delim, int32_t limit, bool ignoreCase, BasArrayT *out);
|
|
static BasVmResultE startTerminate(BasVmT *vm);
|
|
static BasVmResultE storeConvert(BasVmT *vm, uint8_t dataType, BasValueT *val);
|
|
static bool storeElement(BasVmT *vm, BasValueT *target, BasValueT val);
|
|
static void storeValue(BasValueT *target, BasValueT val);
|
|
static int32_t stringFind(const BasStringT *haystack, int32_t start, const BasStringT *needle, bool ignoreCase);
|
|
static bool stringMatchAt(const BasStringT *haystack, int32_t i, const BasStringT *needle, bool ignoreCase);
|
|
static BasValueT strValue(BasStringT *s);
|
|
static void sweepObjects(BasVmT *vm);
|
|
static bool textIsNumeric(const BasStringT *s);
|
|
static const char *typeNameOf(const BasVmT *vm, uint8_t type, int32_t typeId);
|
|
static BasValueT udtDetach(BasValueT val);
|
|
static BasUdtT *udtFromLayout(BasVmT *vm, const BasStringT *layout, int32_t *pos, int32_t typeId, int32_t fieldCount, int32_t depth);
|
|
static BasVmResultE udtGet(BasVmT *vm, FILE *fp, BasUdtT *udt, const BasStringT *layout, int32_t *pos, int32_t depth);
|
|
static BasVmResultE udtPut(BasVmT *vm, BasPutSinkT *sink, const BasUdtT *udt, const BasStringT *layout, int32_t *pos, int32_t depth);
|
|
static BasVmResultE uiTarget(BasVmT *vm, bool hasCallback, BasValueT target);
|
|
static int32_t usingFieldLen(const char *fmt, int32_t fmtLen, int32_t pos);
|
|
static BasStringT *usingFormat(BasVmT *vm, const BasStringT *fmt, BasValueT val, bool last);
|
|
static bool validArrayDims(BasVmT *vm, int32_t dims);
|
|
static int32_t varTypeOf(uint8_t type);
|
|
static bool waitWhile(BasVmT *vm, bool (*busy)(void *ctx));
|
|
static double wallSeconds(void);
|
|
static BasValueT zeroValue(uint8_t type);
|
|
|
|
|
|
// Shared body of OP_DIM_ARRAY / OP_REDIM: pops the UDT typeId/fieldCount
|
|
// pair (UDT element type only) and the (lbound, ubound) pairs for each
|
|
// dimension and allocates the array. With preserve set (REDIM PRESERVE)
|
|
// the old array reference left on top of the stack supplies every element
|
|
// whose subscripts exist in both shapes, element by element, so a changed
|
|
// row stride keeps each value at its own subscript. UDT elements not
|
|
// preserved get fresh instances. On success *outArr holds a refCount-1
|
|
// array.
|
|
static BasVmResultE allocArray(BasVmT *vm, int32_t dims, uint8_t elementType, bool preserve, BasArrayT **outArr) {
|
|
if (!validArrayDims(vm, dims)) {
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
int32_t udtTypeId = -1;
|
|
int32_t udtFieldCnt = 0;
|
|
BasValueT layoutVal;
|
|
|
|
memset(&layoutVal, 0, sizeof(layoutVal));
|
|
|
|
if (elementType == BAS_TYPE_UDT) {
|
|
BasValueT fcVal;
|
|
BasValueT tiVal;
|
|
|
|
if (!popArgs(vm, 3, &fcVal, &tiVal, &layoutVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
udtFieldCnt = basValToInt32(fcVal);
|
|
udtTypeId = basValToInt32(tiVal);
|
|
basValRelease(&fcVal);
|
|
basValRelease(&tiVal);
|
|
|
|
if (layoutVal.type != BAS_TYPE_STRING || !layoutVal.strVal) {
|
|
basValRelease(&layoutVal);
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Bad TYPE layout");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
}
|
|
|
|
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 (!popArgs(vm, 2, &ubVal, &lbVal)) {
|
|
basValRelease(&layoutVal);
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
ubounds[d] = basValToInt32(ubVal);
|
|
lbounds[d] = basValToInt32(lbVal);
|
|
basValRelease(&ubVal);
|
|
basValRelease(&lbVal);
|
|
}
|
|
|
|
BasArrayT *arr = basArrayNew(dims, lbounds, ubounds, elementType);
|
|
|
|
if (!arr) {
|
|
basValRelease(&layoutVal);
|
|
runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory allocating array");
|
|
return BAS_VM_OUT_OF_MEMORY;
|
|
}
|
|
|
|
const BasArrayT *oldArr = NULL;
|
|
|
|
if (preserve && vm->sp > 0) {
|
|
const BasValueT *top = &vm->stack[vm->sp - 1];
|
|
|
|
if (top->type == BAS_TYPE_ARRAY && top->arrVal && top->arrVal->dims == dims) {
|
|
oldArr = top->arrVal;
|
|
}
|
|
}
|
|
|
|
bool oldShared = oldArr && oldArr->refCount > BAS_REDIM_OWNER_REFS;
|
|
int32_t idx[BAS_ARRAY_MAX_DIMS];
|
|
|
|
memcpy(idx, lbounds, sizeof(int32_t) * dims);
|
|
|
|
for (int32_t flat = 0; flat < arr->totalElements; flat++) {
|
|
int32_t oldFlat = oldArr ? basArrayIndex((BasArrayT *)oldArr, idx, dims) : -1;
|
|
|
|
if (oldFlat >= 0) {
|
|
BasValueT v = basValCopy(oldArr->elements[oldFlat]);
|
|
|
|
basValRelease(&arr->elements[flat]);
|
|
arr->elements[flat] = oldShared ? udtDetach(v) : v;
|
|
} else if (elementType == BAS_TYPE_UDT) {
|
|
int32_t pos = 0;
|
|
BasUdtT *udt = udtFromLayout(vm, layoutVal.strVal, &pos, udtTypeId, udtFieldCnt, 0);
|
|
|
|
if (!udt) {
|
|
basArrayFree(arr);
|
|
basValRelease(&layoutVal);
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
arr->elements[flat].type = BAS_TYPE_UDT;
|
|
arr->elements[flat].udtVal = udt;
|
|
}
|
|
|
|
// Advance the subscript counter, last dimension fastest.
|
|
for (int32_t d = dims - 1; d >= 0; d--) {
|
|
if (++idx[d] <= ubounds[d]) {
|
|
break;
|
|
}
|
|
|
|
idx[d] = lbounds[d];
|
|
}
|
|
}
|
|
|
|
basValRelease(&layoutVal);
|
|
*outArr = arr;
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Resolves an optional compare argument of the string functions
|
|
// (vbBinaryCompare, vbTextCompare or vbUseCompareOption). Returns 0 and
|
|
// sets *outText, else the BASIC error number.
|
|
static int32_t argCompare(BasVmT *vm, BasValueT v, bool *outText) {
|
|
int32_t mode = basValToInt32(v);
|
|
|
|
if (mode == BAS_COMPARE_USE_OPTION) {
|
|
*outText = vm->compareTextMode;
|
|
} else if (mode == BAS_COMPARE_MODE_BINARY || mode == BAS_COMPARE_MODE_TEXT) {
|
|
*outText = (mode == BAS_COMPARE_MODE_TEXT);
|
|
} else {
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
// The string an OP_BUILTIN argument holds, converting the argument in
|
|
// place (the caller releases every argument afterwards).
|
|
static BasStringT *argString(BasValueT *arg) {
|
|
if (arg->type != BAS_TYPE_STRING || !arg->strVal) {
|
|
BasValueT sv = basValToString(*arg);
|
|
|
|
basValRelease(arg);
|
|
*arg = sv;
|
|
}
|
|
|
|
return arg->strVal;
|
|
}
|
|
|
|
|
|
// ERASE on a fixed array: every element goes back to its declared zero
|
|
// value (TYPE elements keep their instance and have each field reset).
|
|
static void arrayClear(BasArrayT *arr) {
|
|
for (int32_t i = 0; i < arr->totalElements; i++) {
|
|
BasValueT *elem = &arr->elements[i];
|
|
|
|
if (elem->type == BAS_TYPE_UDT && elem->udtVal) {
|
|
BasUdtT *udt = elem->udtVal;
|
|
|
|
for (int32_t f = 0; f < udt->fieldCount; f++) {
|
|
BasValueT zero = zeroValue(udt->fields[f].type);
|
|
basValRelease(&udt->fields[f]);
|
|
udt->fields[f] = zero;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
BasValueT zero = zeroValue(arr->elementType);
|
|
basValRelease(elem);
|
|
*elem = zero;
|
|
}
|
|
}
|
|
|
|
|
|
// Wraps an array the caller already owns a reference to (no extra ref).
|
|
static BasValueT arrValue(BasArrayT *arr) {
|
|
BasValueT v;
|
|
v.type = BAS_TYPE_ARRAY;
|
|
v.arrVal = arr;
|
|
return v;
|
|
}
|
|
|
|
|
|
bool basVmCallSub(BasVmT *vm, int32_t codeAddr) {
|
|
// Thin wrapper: no args, no out-args. basVmCallSubWithArgsOut handles
|
|
// NULL args / zero counts and contains the full validation + frame setup.
|
|
return basVmCallSubWithArgsOut(vm, codeAddr, NULL, 0, NULL, BAS_MEMBER_OP_GET);
|
|
}
|
|
|
|
|
|
bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount) {
|
|
// Thin wrapper: in-args only, no out-args.
|
|
return basVmCallSubWithArgsOut(vm, codeAddr, args, argCount, NULL, 0);
|
|
}
|
|
|
|
|
|
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 || argCount < 0 || argCount > BAS_VM_MAX_LOCALS) {
|
|
return false;
|
|
}
|
|
|
|
int32_t savedPc = vm->pc;
|
|
int32_t savedCallDepth = vm->callDepth;
|
|
bool savedRunning = vm->running;
|
|
BasCallFrameT *frame = pushFrame(vm, savedPc, codeAddr, argCount);
|
|
|
|
for (int32_t i = 0; i < argCount; i++) {
|
|
frame->locals[i] = basValCopy(args[i]);
|
|
}
|
|
|
|
vm->pc = codeAddr;
|
|
vm->running = true;
|
|
|
|
// Arrange for the returning frame's locals to be copied into outArgs by
|
|
// OP_RET/OP_RET_VAL, BEFORE those opcodes release the locals. Reading
|
|
// them after runSubLoop returns would be a use-after-free because the
|
|
// RET path has already released every local in the frame. The fields
|
|
// are not reentrant, so save and restore them around the call in case
|
|
// the running sub itself makes another out-arg call.
|
|
BasValueT *savedOutArgs = vm->outArgs;
|
|
int32_t savedOutArgCount = vm->outArgCount;
|
|
int32_t savedOutArgFrame = vm->outArgFrame;
|
|
|
|
vm->outArgs = (outArgs && outCount > 0) ? outArgs : NULL;
|
|
vm->outArgCount = outCount;
|
|
vm->outArgFrame = savedCallDepth;
|
|
|
|
bool ok = runSubLoop(vm, savedPc, savedCallDepth, savedRunning);
|
|
|
|
vm->outArgs = savedOutArgs;
|
|
vm->outArgCount = savedOutArgCount;
|
|
vm->outArgFrame = savedOutArgFrame;
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
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;
|
|
vm->outArgFrame = -1;
|
|
return vm;
|
|
}
|
|
|
|
|
|
void basVmDestroy(BasVmT *vm) {
|
|
if (!vm) {
|
|
return;
|
|
}
|
|
|
|
// No Class_Terminate once the program is being torn down
|
|
for (BasObjectT *o = vm->liveObjects; o; o = o->liveNext) {
|
|
o->finalize = NULL;
|
|
}
|
|
|
|
// Release eval stack, globals, call-frame locals and FOR-stack values
|
|
releaseVmState(vm);
|
|
|
|
while (vm->terminateQueue) {
|
|
BasObjectT *obj = vm->terminateQueue;
|
|
|
|
vm->terminateQueue = obj->termNext;
|
|
basObjectUnref(obj);
|
|
}
|
|
|
|
sweepObjects(vm);
|
|
|
|
for (int32_t i = 0; i < BAS_VM_MAX_FILES; i++) {
|
|
closeChannel(vm, i);
|
|
}
|
|
|
|
if (vm->lprintJob) {
|
|
vm->printer.close(vm->printer.ctx, vm->lprintJob);
|
|
}
|
|
|
|
dirClose(vm);
|
|
basScreenDestroy(vm->screen);
|
|
free(vm->commandArgs);
|
|
free(vm->externCache);
|
|
free(vm);
|
|
}
|
|
|
|
|
|
bool basVmDoEvents(BasVmT *vm) {
|
|
basScreenPresent(vm);
|
|
|
|
if (vm->doEventsFn && !vm->doEventsFn(vm->doEventsCtx)) {
|
|
return false;
|
|
}
|
|
|
|
return !vm->stopRequested;
|
|
}
|
|
|
|
|
|
const char *basVmGetError(const BasVmT *vm) {
|
|
return vm ? vm->errorMsg : "";
|
|
}
|
|
|
|
|
|
void basVmLoadModule(BasVmT *vm, BasModuleT *module) {
|
|
if (!module) {
|
|
vm->module = NULL;
|
|
snprintf(vm->errorMsg, sizeof(vm->errorMsg), "No program loaded");
|
|
return;
|
|
}
|
|
|
|
if (module->globalCount > BAS_VM_MAX_GLOBALS) {
|
|
vm->module = NULL;
|
|
snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Program needs %d global variables; the limit is %d", (int)module->globalCount, (int)BAS_VM_MAX_GLOBALS);
|
|
return;
|
|
}
|
|
|
|
// One extern-cache slot per constant, so OP_CALL_EXTERN finds a
|
|
// resolved function by its name index without searching.
|
|
BasExternCacheEntryT *cache = NULL;
|
|
|
|
if (module->constCount > 0) {
|
|
cache = (BasExternCacheEntryT *)calloc(module->constCount, sizeof(BasExternCacheEntryT));
|
|
|
|
if (!cache) {
|
|
vm->module = NULL;
|
|
snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Out of memory loading program");
|
|
return;
|
|
}
|
|
}
|
|
|
|
free(vm->externCache);
|
|
vm->externCache = cache;
|
|
vm->externCacheCount = module->constCount;
|
|
vm->module = module;
|
|
vm->pc = module->entryPoint;
|
|
clearErrorState(vm);
|
|
|
|
// Initialize globals whose declared type requires a non-numeric
|
|
// default. Uninitialized slots are zero (type=INTEGER, value=0),
|
|
// which breaks STRING operations: `DIM s AS STRING : s = s + "x"`
|
|
// would do numeric addition instead of concatenation. STATIC
|
|
// variables also alias global slots, so the same issue applies.
|
|
// globalInits survives the compiler's strip pass, unlike debugVars.
|
|
for (int32_t i = 0; i < module->globalInitCount; i++) {
|
|
BasGlobalInitT *g = &module->globalInits[i];
|
|
|
|
if (g->index < 0 || g->index >= BAS_VM_MAX_GLOBALS) {
|
|
continue;
|
|
}
|
|
|
|
if (g->dataType == BAS_TYPE_STRING) {
|
|
basValRelease(&vm->globals[g->index]);
|
|
vm->globals[g->index].type = BAS_TYPE_STRING;
|
|
vm->globals[g->index].strVal = basStringNew("", 0);
|
|
}
|
|
}
|
|
|
|
// Prime the implicit main (module-level) frame. The IDE and test
|
|
// harness used to set this by hand; doing it here makes every entry
|
|
// path (including compiled apps via the stub) consistent.
|
|
primeModuleFrame(vm);
|
|
vm->stmtPc = -1;
|
|
}
|
|
|
|
|
|
void basVmRequestStop(BasVmT *vm) {
|
|
vm->stopRequested = true;
|
|
vm->running = false;
|
|
}
|
|
|
|
|
|
BasVmResultE basVmRun(BasVmT *vm) {
|
|
// basVmLoadModule refused the module (too many globals): report why
|
|
// instead of returning a clean HALTED for a program that never ran.
|
|
if (!vm->module && vm->errorMsg[0] != '\0') {
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
// The host asked the program to stop (its screen was closed)
|
|
if (vm->stopRequested) {
|
|
return BAS_VM_HALTED;
|
|
}
|
|
|
|
vm->running = true;
|
|
vm->stepCount = 0;
|
|
|
|
while (vm->running) {
|
|
// Check step limit: the host gets a turn, with the screen shown
|
|
if (vm->stepLimit > 0 && vm->stepCount >= vm->stepLimit) {
|
|
basScreenPresent(vm);
|
|
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) {
|
|
// Debugger pauses are not errors: hand them straight back to
|
|
// the host without consulting ON ERROR handlers.
|
|
if (result == BAS_VM_BREAKPOINT) {
|
|
vm->running = false;
|
|
return result;
|
|
}
|
|
|
|
// Trappable error with a handler in scope: the dispatcher has
|
|
// unwound to the handler frame and pointed pc at it.
|
|
if (dispatchError(vm, result, 0, savedPc)) {
|
|
continue;
|
|
}
|
|
|
|
// END, the end of the program or an error: show what it drew
|
|
vm->running = false;
|
|
basScreenPresent(vm);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Host callbacks (property setters, method dispatch, etc.) can
|
|
// abort execution by writing an error message and clearing
|
|
// vm->running. The step itself returns OK because the VM did
|
|
// nothing wrong; the failure happened on the host side. If the
|
|
// host left an error message, surface it as BAS_VM_ERROR so the
|
|
// caller (IDE, stub) can navigate to the line and stop the
|
|
// program instead of treating the halt as a normal END.
|
|
if (vm->errorMsg[0] != '\0') {
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
basScreenPresent(vm);
|
|
return BAS_VM_HALTED;
|
|
}
|
|
|
|
|
|
void basVmRunToCursor(BasVmT *vm, int32_t line) {
|
|
if (vm) {
|
|
vm->runToCursorLine = line;
|
|
}
|
|
}
|
|
|
|
|
|
void basVmSetBreakpoints(BasVmT *vm, int32_t *lines, int32_t count) {
|
|
if (!vm) {
|
|
return;
|
|
}
|
|
|
|
vm->breakpoints = lines;
|
|
vm->breakpointCount = count;
|
|
}
|
|
|
|
|
|
bool basVmSetCommand(BasVmT *vm, const char *args) {
|
|
free(vm->commandArgs);
|
|
vm->commandArgs = (args && args[0]) ? strdup(args) : NULL;
|
|
return vm->commandArgs != NULL || !args || !args[0];
|
|
}
|
|
|
|
|
|
void basVmSetCurrentForm(BasVmT *vm, void *formRef) {
|
|
vm->currentForm = formRef;
|
|
}
|
|
|
|
|
|
void basVmSetCurrentFormVars(BasVmT *vm, BasValueT *vars, int32_t count) {
|
|
vm->currentFormVars = vars;
|
|
vm->currentFormVarCount = count;
|
|
}
|
|
|
|
|
|
void basVmSetDeviceCallbacks(BasVmT *vm, const BasDeviceCallbacksT *device) {
|
|
vm->device = *device;
|
|
}
|
|
|
|
|
|
void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx) {
|
|
vm->doEventsFn = fn;
|
|
vm->doEventsCtx = ctx;
|
|
}
|
|
|
|
|
|
void basVmSetExternCallbacks(BasVmT *vm, const BasExternCallbacksT *ext) {
|
|
vm->ext = *ext;
|
|
}
|
|
|
|
|
|
void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx) {
|
|
vm->inputFn = fn;
|
|
vm->inputCtx = ctx;
|
|
}
|
|
|
|
|
|
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx) {
|
|
vm->printFn = fn;
|
|
vm->printCtx = ctx;
|
|
}
|
|
|
|
|
|
void basVmSetPrinterCallbacks(BasVmT *vm, const BasPrinterCallbacksT *printer) {
|
|
vm->printer = *printer;
|
|
}
|
|
|
|
|
|
void basVmSetScreenHost(BasVmT *vm, const BasScreenHostT *host) {
|
|
vm->screenHost = *host;
|
|
}
|
|
|
|
|
|
void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql) {
|
|
vm->sql = *sql;
|
|
}
|
|
|
|
|
|
void basVmSetStepLimit(BasVmT *vm, int32_t limit) {
|
|
vm->stepLimit = limit;
|
|
}
|
|
|
|
|
|
void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui) {
|
|
vm->ui = *ui;
|
|
}
|
|
|
|
|
|
BasVmResultE basVmStep(BasVmT *vm) {
|
|
basValNullUsed = false;
|
|
|
|
BasVmResultE result = basVmStepExec(vm);
|
|
|
|
// A string allocation that failed during the step substituted an
|
|
// empty string; surface it as Out of string space instead of letting
|
|
// the program continue on lost data.
|
|
if (basStringAllocFailed) {
|
|
basStringAllocFailed = false;
|
|
|
|
if (result == BAS_VM_OK) {
|
|
runtimeError(vm, BAS_ERR_OUT_OF_STRING, "Out of string space");
|
|
result = BAS_VM_OUT_OF_MEMORY;
|
|
}
|
|
}
|
|
|
|
// A Null reached a conversion: a number or text was needed
|
|
if (basValNullUsed) {
|
|
basValNullUsed = false;
|
|
|
|
if (result == BAS_VM_OK) {
|
|
result = raiseError(vm, BAS_ERR_INVALID_NULL);
|
|
}
|
|
}
|
|
|
|
// Class_Terminate of an object the instruction let go of runs now, as
|
|
// a call made between this instruction and the next
|
|
if (result == BAS_VM_OK && vm->terminateQueue) {
|
|
result = startTerminate(vm);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
// Executes one instruction: the body of basVmStep, which adds the
|
|
// string-allocation check around it.
|
|
static BasVmResultE basVmStepExec(BasVmT *vm) {
|
|
// A prior step's operand read ran past code[] and clamped pc to codeLen.
|
|
// Check this BEFORE the halt guard below, otherwise pc == codeLen would
|
|
// report a clean BAS_VM_HALTED and mask the corruption. badOperand can
|
|
// only have been set while vm->module was non-NULL, so no NULL check here.
|
|
if (vm->badOperand) {
|
|
vm->badOperand = false;
|
|
vm->running = false;
|
|
return BAS_VM_BAD_OPCODE;
|
|
}
|
|
|
|
if (!vm->module || vm->pc < 0 || vm->pc >= vm->module->codeLen) {
|
|
vm->running = false;
|
|
return BAS_VM_HALTED;
|
|
}
|
|
|
|
vm->currentOpPc = vm->pc;
|
|
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);
|
|
// Reconstruct through unsigned math: left-shifting a negative
|
|
// sign-extended high half would be signed-overflow UB.
|
|
int32_t val = (int32_t)(((uint32_t)(uint16_t)hi << 16) | (uint16_t)lo);
|
|
|
|
if (!push(vm, basValLong(val))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_PUSH_FLT64: {
|
|
double val = 0.0;
|
|
|
|
if (operandFits(vm, (int32_t)sizeof(double))) {
|
|
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);
|
|
BasStringT *str = (idx < (uint16_t)vm->module->constCount) ? vm->module->constants[idx] : basEmptyString;
|
|
|
|
if (!push(vm, basValString(str))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_PUSH_SPECIAL: {
|
|
uint8_t kind = readUint8(vm);
|
|
BasValueT val = (kind == BAS_SPECIAL_NULL) ? basValNull() : basValEmpty();
|
|
|
|
if (kind == BAS_SPECIAL_NOTHING) {
|
|
val = basValObject(NULL);
|
|
}
|
|
|
|
if (!push(vm, val)) {
|
|
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;
|
|
}
|
|
|
|
case OP_STMT:
|
|
// Release-build statement boundary: same bookkeeping as OP_LINE
|
|
// minus the line number and debugger checks.
|
|
vm->stmtSp = vm->sp;
|
|
vm->stmtPc = vm->currentOpPc;
|
|
break;
|
|
|
|
// ============================================================
|
|
// Variable access
|
|
// ============================================================
|
|
|
|
case OP_LOAD_LOCAL: {
|
|
uint16_t idx = readUint16(vm);
|
|
BasCallFrameT *frame = currentFrame(vm);
|
|
|
|
if (idx >= (uint16_t)frame->localCount) {
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid local variable index");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
// ByRef: if the local holds a reference, dereference it
|
|
BasValueT *slot = derefSlot(&frame->locals[idx]);
|
|
|
|
if (!slot) {
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Subscript out of range");
|
|
return BAS_VM_SUBSCRIPT_RANGE;
|
|
}
|
|
|
|
if (!push(vm, basValCopy(*slot))) {
|
|
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 (idx >= (uint16_t)frame->localCount) {
|
|
basValRelease(&val);
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid local variable index");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
// ByRef: if the local holds a reference, store through it
|
|
BasValueT *slot = derefSlot(&frame->locals[idx]);
|
|
|
|
if (!slot) {
|
|
basValRelease(&val);
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Subscript out of range");
|
|
return BAS_VM_SUBSCRIPT_RANGE;
|
|
}
|
|
|
|
storeValue(slot, val);
|
|
break;
|
|
}
|
|
|
|
case OP_LOAD_GLOBAL: {
|
|
uint16_t idx = readUint16(vm);
|
|
|
|
if (idx >= BAS_VM_MAX_GLOBALS) {
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "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, BAS_ERR_SUBSCRIPT_RANGE, "Invalid global variable index");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
storeValue(&vm->globals[idx], val);
|
|
break;
|
|
}
|
|
|
|
case OP_PUSH_LOCAL_ADDR: {
|
|
uint16_t idx = readUint16(vm);
|
|
BasCallFrameT *frame = currentFrame(vm);
|
|
|
|
if (idx >= (uint16_t)frame->localCount) {
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid local variable index");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
// A local that is itself a ByRef parameter passes its
|
|
// reference straight through (an ELEM_REF copy counts the
|
|
// array again), so a callee two levels down still reaches the
|
|
// original variable. Only a plain slot gets a new reference.
|
|
BasValueT *slot = &frame->locals[idx];
|
|
BasValueT ref;
|
|
|
|
if (slot->type == BAS_TYPE_REF || slot->type == BAS_TYPE_ELEM_REF) {
|
|
ref = basValCopy(*slot);
|
|
} else {
|
|
ref.type = BAS_TYPE_REF;
|
|
ref.refVal = slot;
|
|
}
|
|
|
|
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, BAS_ERR_SUBSCRIPT_RANGE, "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_MEMBER: {
|
|
BasValueT *slot = memberSlot(vm, readUint16(vm));
|
|
|
|
if (!slot) {
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
if (!push(vm, basValCopy(*slot))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_STORE_MEMBER: {
|
|
uint16_t idx = readUint16(vm);
|
|
BasValueT val;
|
|
|
|
if (!pop(vm, &val)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasValueT *slot = memberSlot(vm, idx);
|
|
|
|
if (!slot) {
|
|
basValRelease(&val);
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
storeValue(slot, val);
|
|
break;
|
|
}
|
|
|
|
case OP_PUSH_MEMBER_ADDR: {
|
|
// The caller's frame holds Me for the whole call, so the slot
|
|
// outlives the callee that writes through the reference.
|
|
BasValueT *slot = memberSlot(vm, readUint16(vm));
|
|
|
|
if (!slot) {
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
BasValueT ref;
|
|
ref.type = BAS_TYPE_REF;
|
|
ref.refVal = slot;
|
|
|
|
if (!push(vm, ref)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// ============================================================
|
|
// Arithmetic
|
|
// ============================================================
|
|
|
|
case OP_ADD_INT:
|
|
case OP_SUB_INT:
|
|
case OP_MUL_INT:
|
|
case OP_IDIV_INT:
|
|
case OP_MOD_INT:
|
|
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];
|
|
|
|
// -Null is Null; -Empty is 0
|
|
if (top->type == BAS_TYPE_NULL) {
|
|
break;
|
|
}
|
|
|
|
if (top->type == BAS_TYPE_EMPTY) {
|
|
*top = basValInteger(0);
|
|
}
|
|
|
|
// Negating the most negative INTEGER / LONG does not fit the
|
|
// type: promote exactly as 0 - x does in execArith. (The
|
|
// literal -2147483648 is folded by the compiler and never
|
|
// reaches here.)
|
|
if (top->type == BAS_TYPE_INTEGER) {
|
|
*top = (top->intVal == INT16_MIN) ? basValLong(-(int32_t)INT16_MIN) : basValInteger((int16_t)-top->intVal);
|
|
} else if (top->type == BAS_TYPE_LONG) {
|
|
*top = (top->longVal == INT32_MIN) ? basValDouble(-(double)INT32_MIN) : basValLong(-top->longVal);
|
|
} 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);
|
|
|
|
case OP_STR_APPEND:
|
|
return execStrAppend(vm);
|
|
|
|
// ============================================================
|
|
// 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);
|
|
|
|
case OP_IS: {
|
|
BasValueT a;
|
|
BasValueT b;
|
|
|
|
if (!popPair(vm, &a, &b)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
bool objects = basValIsObject(a) && basValIsObject(b);
|
|
bool same = objects && a.type == b.type && ((a.type == BAS_TYPE_OBJECT) ? a.objVal == b.objVal : a.instVal == b.instVal);
|
|
|
|
basValRelease(&a);
|
|
basValRelease(&b);
|
|
|
|
if (!objects) {
|
|
return raiseError(vm, BAS_ERR_OBJECT_REQUIRED);
|
|
}
|
|
|
|
if (!push(vm, basValBool(same))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_OBJ_CHECK: {
|
|
uint16_t cls = readUint16(vm);
|
|
|
|
if (vm->sp < 1) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t err = objectCheck(vm->stack[vm->sp - 1], cls);
|
|
|
|
if (err != 0) {
|
|
return raiseError(vm, err);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// ============================================================
|
|
// 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:
|
|
case OP_CALL_ME:
|
|
return execCall(vm, op == OP_CALL_ME);
|
|
|
|
case OP_RET: {
|
|
// The module frame (depth 1) is never popped: a RET reached at
|
|
// module level (corrupt bytecode) ends the program, otherwise
|
|
// later local accesses would use a frame teardown cannot see.
|
|
if (vm->callDepth <= 1) {
|
|
vm->running = false;
|
|
return BAS_VM_HALTED;
|
|
}
|
|
|
|
// A SUB or Property Let/Set reached through an object where a
|
|
// value is expected leaves Empty.
|
|
bool pushResult = vm->callStack[vm->callDepth - 1].pushResult;
|
|
|
|
popCallFrame(vm);
|
|
|
|
if (pushResult && !push(vm, basValEmpty())) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
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 <= 1) {
|
|
basValRelease(&retVal);
|
|
vm->running = false;
|
|
return BAS_VM_HALTED;
|
|
}
|
|
|
|
popCallFrame(vm);
|
|
|
|
if (!push(vm, retVal)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_GOSUB: {
|
|
int16_t offset = readInt16(vm);
|
|
|
|
if (vm->gosubDepth >= BAS_VM_GOSUB_STACK_SIZE) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
vm->gosubStack[vm->gosubDepth++] = vm->pc;
|
|
vm->pc += offset;
|
|
break;
|
|
}
|
|
|
|
case OP_GOSUB_RET: {
|
|
// Only a GOSUB made by the current SUB/FUNCTION (or the module
|
|
// level) can be returned from.
|
|
int32_t gosubFloor = (vm->callDepth > 0) ? vm->callStack[vm->callDepth - 1].savedGosubDepth : 0;
|
|
|
|
if (vm->gosubDepth <= gosubFloor) {
|
|
runtimeError(vm, BAS_ERR_RETURN_NO_GOSUB, errorText(BAS_ERR_RETURN_NO_GOSUB));
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
vm->pc = vm->gosubStack[--vm->gosubDepth];
|
|
break;
|
|
}
|
|
|
|
case OP_FOR_INIT: {
|
|
uint16_t varIdx = readUint16(vm);
|
|
uint8_t scopeTag = readUint8(vm);
|
|
int16_t skipOffset = readInt16(vm);
|
|
BasValueT stepVal;
|
|
BasValueT limitVal;
|
|
|
|
if (!popArgs(vm, 2, &stepVal, &limitVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasValueT *varSlot = scopedVarSlot(vm, varIdx, scopeTag);
|
|
|
|
if (!varSlot) {
|
|
basValRelease(&stepVal);
|
|
basValRelease(&limitVal);
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
// Re-entering a FOR whose frame is still live (a GOTO back to
|
|
// the FOR statement, or a jump that skipped its NEXT) must not
|
|
// stack a second frame: discard the old context and everything
|
|
// nested inside it, as classic BASIC does. Only frames owned
|
|
// by the current call frame are considered.
|
|
int32_t forFloor = forStackFloor(vm);
|
|
|
|
for (int32_t k = vm->forDepth - 1; k >= forFloor; k--) {
|
|
if (vm->forStack[k].varIdx == (int32_t)varIdx && vm->forStack[k].scopeTag == scopeTag) {
|
|
forStackTrim(vm, k);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (vm->forDepth >= BAS_VM_MAX_FOR_DEPTH) {
|
|
basValRelease(&stepVal);
|
|
basValRelease(&limitVal);
|
|
runtimeError(vm, BAS_ERR_FOR_NESTING, "FOR loop nesting too deep");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
double stepNum = basValToNumber(stepVal);
|
|
double limNum = basValToNumber(limitVal);
|
|
double curVal = basValToNumber(*varSlot);
|
|
|
|
// Entry check: if the loop's range is already empty (e.g.
|
|
// FOR i = 10 TO 5 with positive step), skip the body without
|
|
// pushing FOR state. QBASIC semantics: body executes zero
|
|
// times when the range is empty.
|
|
bool enter = (stepNum >= 0) ? (curVal <= limNum) : (curVal >= limNum);
|
|
|
|
if (!enter) {
|
|
basValRelease(&stepVal);
|
|
basValRelease(&limitVal);
|
|
vm->pc += skipOffset;
|
|
break;
|
|
}
|
|
|
|
BasForStateT *fs = &vm->forStack[vm->forDepth++];
|
|
fs->varIdx = varIdx;
|
|
fs->scopeTag = scopeTag;
|
|
fs->integral = isIntegerType(limitVal.type) && isIntegerType(stepVal.type);
|
|
fs->stepFractional = (stepNum != floor(stepNum));
|
|
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, BAS_ERR_NEXT_WITHOUT_FOR, "NEXT without FOR");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
BasForStateT *fs = &vm->forStack[vm->forDepth - 1];
|
|
|
|
if (fs->varIdx != (int32_t)varIdx || fs->scopeTag != scopeTag) {
|
|
// A jump out of an inner FOR (EXIT DO, GOTO) leaves its
|
|
// frame on the FOR stack. Search down to this call frame's
|
|
// floor for the loop this NEXT belongs to and discard the
|
|
// stale frames above it, matching classic BASIC semantics.
|
|
int32_t forFloor = forStackFloor(vm);
|
|
int32_t matchIdx = -1;
|
|
|
|
for (int32_t k = vm->forDepth - 2; k >= forFloor; k--) {
|
|
if (vm->forStack[k].varIdx == (int32_t)varIdx && vm->forStack[k].scopeTag == scopeTag) {
|
|
matchIdx = k;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (matchIdx < 0) {
|
|
runtimeError(vm, BAS_ERR_NEXT_WITHOUT_FOR, "NEXT variable mismatch");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
forStackTrim(vm, matchIdx + 1);
|
|
fs = &vm->forStack[matchIdx];
|
|
}
|
|
|
|
BasValueT *varSlot = scopedVarSlot(vm, varIdx, scopeTag);
|
|
|
|
if (!varSlot) {
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
// Increment var by step and test against the limit: continue
|
|
// while var <= limit for a non-negative step, var >= limit for
|
|
// a negative one. INTEGER/LONG counters with an INTEGER/LONG
|
|
// limit and step stay in integer math.
|
|
uint8_t varType = varSlot->type;
|
|
bool cont;
|
|
|
|
if (fs->integral && isIntegerType(varType)) {
|
|
int32_t stepInt = intValue(fs->step);
|
|
int32_t limInt = intValue(fs->limit);
|
|
int64_t next = (int64_t)intValue(*varSlot) + stepInt;
|
|
|
|
cont = (stepInt >= 0) ? (next <= limInt) : (next >= limInt);
|
|
*varSlot = forCounterValue(varType, false, (double)next);
|
|
} else {
|
|
double stepNum = basValToNumber(fs->step);
|
|
double varVal = basValToNumber(*varSlot) + stepNum;
|
|
|
|
cont = (stepNum >= 0) ? (varVal <= basValToNumber(fs->limit)) : (varVal >= basValToNumber(fs->limit));
|
|
basValRelease(varSlot);
|
|
*varSlot = forCounterValue(varType, fs->stepFractional, varVal);
|
|
}
|
|
|
|
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, BAS_ERR_NEXT_WITHOUT_FOR, "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:
|
|
case OP_CONV_FLT_INT:
|
|
case OP_CONV_STR_INT:
|
|
case OP_CONV_INT_STR:
|
|
case OP_CONV_STR_FLT:
|
|
case OP_CONV_INT_LONG:
|
|
case OP_CONV_DATE:
|
|
case OP_CONV_BOOL:
|
|
return execConvert(vm, op);
|
|
|
|
// ============================================================
|
|
// I/O
|
|
// ============================================================
|
|
|
|
case OP_PRINT:
|
|
return execPrint(vm);
|
|
|
|
case OP_PRINT_NL:
|
|
printText(vm, "", true);
|
|
break;
|
|
|
|
case OP_LPRINT:
|
|
return execLprint(vm, readUint8(vm));
|
|
|
|
case OP_PRINT_TAB: {
|
|
// Comma: advance to the next print zone.
|
|
int32_t column = *printColumnOf(vm);
|
|
int32_t target = (column / BAS_PRINT_ZONE_WIDTH + 1) * BAS_PRINT_ZONE_WIDTH;
|
|
char spaces[BAS_PRINT_ZONE_WIDTH + 1];
|
|
int32_t count = target - column;
|
|
|
|
memset(spaces, ' ', count);
|
|
spaces[count] = '\0';
|
|
printText(vm, spaces, false);
|
|
break;
|
|
}
|
|
|
|
case OP_PRINT_SPC_N: {
|
|
BasValueT nVal;
|
|
|
|
if (!pop(vm, &nVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t count = basValToInt32(nVal);
|
|
basValRelease(&nVal);
|
|
|
|
if (count < 0) {
|
|
count = 0;
|
|
}
|
|
|
|
if (count > BAS_PRINT_SPC_MAX) {
|
|
count = BAS_PRINT_SPC_MAX;
|
|
}
|
|
|
|
char spaces[BAS_PRINT_SPC_MAX + 1];
|
|
memset(spaces, ' ', count);
|
|
spaces[count] = '\0';
|
|
printText(vm, spaces, false);
|
|
break;
|
|
}
|
|
|
|
case OP_PRINT_TAB_N: {
|
|
BasValueT nVal;
|
|
|
|
if (!pop(vm, &nVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t col = basValToInt32(nVal);
|
|
basValRelease(&nVal);
|
|
|
|
if (col < 1) {
|
|
col = 1;
|
|
}
|
|
|
|
if (col > BAS_PRINT_SPC_MAX) {
|
|
col = BAS_PRINT_SPC_MAX;
|
|
}
|
|
|
|
// TAB(n) pads to column n (1-based). When the cursor is
|
|
// already past it, QB starts a new line first.
|
|
if (*printColumnOf(vm) > col - 1) {
|
|
printText(vm, "", true);
|
|
}
|
|
|
|
char spaces[BAS_PRINT_SPC_MAX + 1];
|
|
int32_t count = col - 1 - *printColumnOf(vm);
|
|
|
|
memset(spaces, ' ', count);
|
|
spaces[count] = '\0';
|
|
printText(vm, spaces, false);
|
|
break;
|
|
}
|
|
|
|
case OP_PRINT_USING: {
|
|
// [uint8 flags] pop value and format string (format is below
|
|
// value on stack); push the format back for the next value,
|
|
// then the literal text and formatted field for this one.
|
|
uint8_t flags = readUint8(vm);
|
|
BasValueT val;
|
|
BasValueT fmtVal;
|
|
|
|
if (!popArgs(vm, 2, &val, &fmtVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasValueT fmtStr = basValToString(fmtVal);
|
|
basValRelease(&fmtVal);
|
|
|
|
if (flags & BAS_USING_FIRST) {
|
|
vm->usingPos = 0;
|
|
}
|
|
|
|
BasStringT *out = usingFormat(vm, fmtStr.strVal, val, (flags & BAS_USING_LAST) != 0);
|
|
basValRelease(&val);
|
|
|
|
if (!out) {
|
|
basValRelease(&fmtStr);
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "PRINT USING field does not match the value");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
if (!push(vm, fmtStr)) {
|
|
basStringUnref(out);
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
if (!push(vm, strValue(out))) {
|
|
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[BAS_INPUT_PROMPT_LEN];
|
|
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[BAS_INPUT_LINE_LEN];
|
|
buf[0] = '\0';
|
|
|
|
if (vm->screen) {
|
|
if (!basScreenInput(vm, promptBuf, buf, sizeof(buf))) {
|
|
buf[0] = '\0';
|
|
}
|
|
} else 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);
|
|
|
|
case OP_RGB: {
|
|
// RGB(r, g, b) -> long color 0x00RRGGBB
|
|
BasValueT vb;
|
|
BasValueT vg;
|
|
BasValueT vr;
|
|
|
|
if (!popArgs(vm, 3, &vb, &vg, &vr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t r = clampComponent(basValToInt32(vr));
|
|
int32_t g = clampComponent(basValToInt32(vg));
|
|
int32_t b = clampComponent(basValToInt32(vb));
|
|
|
|
basValRelease(&vr);
|
|
basValRelease(&vg);
|
|
basValRelease(&vb);
|
|
|
|
if (!push(vm, basValLong((r << BAS_RGB_RED_SHIFT) | (g << BAS_RGB_GREEN_SHIFT) | b))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_GET_RED:
|
|
case OP_GET_GREEN:
|
|
case OP_GET_BLUE: {
|
|
BasValueT vc;
|
|
|
|
if (!pop(vm, &vc)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t color = basValToInt32(vc);
|
|
basValRelease(&vc);
|
|
|
|
int32_t component;
|
|
|
|
if (op == OP_GET_RED) {
|
|
component = (color >> BAS_RGB_RED_SHIFT) & BAS_RGB_CHANNEL_MASK;
|
|
} else if (op == OP_GET_GREEN) {
|
|
component = (color >> BAS_RGB_GREEN_SHIFT) & BAS_RGB_CHANNEL_MASK;
|
|
} else {
|
|
component = color & BAS_RGB_CHANNEL_MASK;
|
|
}
|
|
|
|
if (!push(vm, basValInteger((int16_t)component))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// ============================================================
|
|
// 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];
|
|
BasValueT num = *top;
|
|
|
|
// Same digits as PRINT (basValFormatString), so STR$ and PRINT
|
|
// never disagree; booleans and strings go through as numbers.
|
|
if (num.type == BAS_TYPE_BOOLEAN || num.type == BAS_TYPE_STRING) {
|
|
num = basValDouble(basValToNumber(num));
|
|
}
|
|
|
|
BasStringT *digits = basValFormatString(num);
|
|
BasStringT *result;
|
|
|
|
// A date is its date text, with no sign position
|
|
if (digits->data[0] == '-' || num.type == BAS_TYPE_DATE) {
|
|
result = basStringRef(digits);
|
|
} else {
|
|
BasStringT *space = basStringNew(" ", 1);
|
|
result = basStringConcat(space, digits);
|
|
basStringUnref(space);
|
|
}
|
|
|
|
basStringUnref(digits);
|
|
basValRelease(top);
|
|
*top = strValue(result);
|
|
break;
|
|
}
|
|
|
|
case OP_STR_HEX: {
|
|
if (vm->sp < 1) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasValueT *top = &vm->stack[vm->sp - 1];
|
|
int32_t n = basValToInt32(*top);
|
|
char buf[BAS_RADIX_STR_LEN];
|
|
|
|
snprintf(buf, sizeof(buf), "%X", (unsigned int)n);
|
|
basValRelease(top);
|
|
*top = basValStringFromC(buf);
|
|
break;
|
|
}
|
|
|
|
case OP_STR_OCT: {
|
|
if (vm->sp < 1) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasValueT *top = &vm->stack[vm->sp - 1];
|
|
int32_t n = basValToInt32(*top);
|
|
char buf[BAS_RADIX_STR_LEN];
|
|
|
|
snprintf(buf, sizeof(buf), "%o", (unsigned int)n);
|
|
basValRelease(top);
|
|
*top = basValStringFromC(buf);
|
|
break;
|
|
}
|
|
|
|
case OP_STR_STRING: {
|
|
// STRING$(n, char)
|
|
BasValueT charVal;
|
|
BasValueT countVal;
|
|
|
|
if (!popArgs(vm, 2, &charVal, &countVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t count = basValToInt32(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)(basValToInt32(charVal) & BAS_CHAR_MASK);
|
|
}
|
|
|
|
basValRelease(&charVal);
|
|
|
|
if (count < 0 || count > BAS_STRING_FUNC_MAX) {
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
if (!push(vm, strValue(fillString(ch, count)))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// ============================================================
|
|
// Extended built-ins
|
|
// ============================================================
|
|
|
|
case OP_MATH_TIMER: {
|
|
// Push seconds since midnight as a double, with sub-second
|
|
// resolution so timing loops do not stall for up to a second.
|
|
struct tm t;
|
|
int32_t usec;
|
|
|
|
localNow(&t, &usec);
|
|
|
|
double secs = (double)t.tm_hour * BAS_SECS_PER_HOUR + (double)t.tm_min * BAS_SECS_PER_MINUTE + (double)t.tm_sec + (double)usec / BAS_USEC_PER_SEC;
|
|
|
|
if (!push(vm, basValDouble(secs))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_DATE_STR: {
|
|
// Push DATE$ as "MM-DD-YYYY"
|
|
struct tm t;
|
|
char buf[BAS_DATE_STR_LEN];
|
|
|
|
localNow(&t, NULL);
|
|
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"
|
|
struct tm t;
|
|
char buf[BAS_TIME_STR_LEN];
|
|
|
|
localNow(&t, NULL);
|
|
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: {
|
|
BasValueT val;
|
|
|
|
if (!pop(vm, &val)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t secs = basValToInt32(val);
|
|
basValRelease(&val);
|
|
|
|
if (!sleepSeconds(vm, secs)) {
|
|
vm->running = false;
|
|
return BAS_VM_HALTED;
|
|
}
|
|
|
|
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_PUT_UDT:
|
|
case OP_FILE_SEEK:
|
|
case OP_FILE_SEEK_GET:
|
|
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);
|
|
|
|
case OP_BEEP:
|
|
case OP_SOUND:
|
|
case OP_PLAY:
|
|
case OP_PLAY_COUNT:
|
|
case OP_STICK:
|
|
case OP_STRIG:
|
|
case OP_FRAME_WAIT:
|
|
return execDevice(vm, op);
|
|
|
|
case OP_BUILTIN:
|
|
return execBuiltin(vm);
|
|
|
|
// ============================================================
|
|
// DoEvents
|
|
// ============================================================
|
|
|
|
case OP_DO_EVENTS:
|
|
if (!basVmDoEvents(vm)) {
|
|
vm->running = false;
|
|
return BAS_VM_HALTED;
|
|
}
|
|
break;
|
|
|
|
// ============================================================
|
|
// Error handling
|
|
// ============================================================
|
|
|
|
case OP_ON_ERROR: {
|
|
int16_t handler = readInt16(vm);
|
|
int32_t target = (handler == 0) ? 0 : vm->pc + handler;
|
|
|
|
// Record the handler on the current call frame. The error
|
|
// dispatcher walks frames (innermost outward) to find a
|
|
// handler and unwinds the stack to that frame before
|
|
// jumping. Module-level code lives in frame 0.
|
|
currentFrame(vm)->errorHandler = target;
|
|
break;
|
|
}
|
|
|
|
case OP_ERR_NUM:
|
|
if (!push(vm, basValLong(vm->errorNumber))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
break;
|
|
|
|
case OP_ERL:
|
|
if (!push(vm, basValLong(vm->errorLine))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
break;
|
|
|
|
case OP_RESUME:
|
|
case OP_RESUME_NEXT: {
|
|
// RESUME re-executes the statement that raised the error;
|
|
// RESUME NEXT continues at the statement after it. Only this
|
|
// frame's own running handler can resume (QBASIC error 20).
|
|
BasCallFrameT *frame = currentFrame(vm);
|
|
|
|
if (!frame->inHandler) {
|
|
runtimeError(vm, BAS_ERR_RESUME_WITHOUT_ERR, "RESUME without error");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
int32_t target = (op == OP_RESUME) ? frame->resumePc : frame->resumeNextPc;
|
|
|
|
if (target < 0) {
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "RESUME NEXT has no statement to continue at");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
vm->pc = target;
|
|
frame->inHandler = false;
|
|
clearErrorState(vm);
|
|
break;
|
|
}
|
|
|
|
case OP_RAISE_ERR: {
|
|
BasValueT errVal;
|
|
|
|
if (!pop(vm, &errVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t errNum = basValToInt32(errVal);
|
|
basValRelease(&errVal);
|
|
runtimeError(vm, errNum, errorText(errNum));
|
|
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;
|
|
BasValueT layoutVal;
|
|
|
|
if (!popArgs(vm, 3, &fieldCountVal, &typeIdVal, &layoutVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t fieldCount = basValToInt32(fieldCountVal);
|
|
int32_t typeId = basValToInt32(typeIdVal);
|
|
basValRelease(&fieldCountVal);
|
|
basValRelease(&typeIdVal);
|
|
|
|
if (layoutVal.type != BAS_TYPE_STRING || !layoutVal.strVal) {
|
|
basValRelease(&layoutVal);
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Bad TYPE layout");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
int32_t pos = 0;
|
|
BasUdtT *udt = udtFromLayout(vm, layoutVal.strVal, &pos, typeId, fieldCount, 0);
|
|
|
|
basValRelease(&layoutVal);
|
|
|
|
if (!udt) {
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
BasValueT udtVal;
|
|
udtVal.type = BAS_TYPE_UDT;
|
|
udtVal.udtVal = udt;
|
|
|
|
if (!push(vm, udtVal)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
BasArrayT *arr;
|
|
BasVmResultE rc = allocArray(vm, dims, elementType, false, &arr);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
BasValueT arrVal;
|
|
arrVal.type = BAS_TYPE_ARRAY;
|
|
arrVal.arrVal = arr;
|
|
|
|
if (!push(vm, arrVal)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_LOAD_ARRAY: {
|
|
uint8_t dims = readUint8(vm);
|
|
|
|
// obj(args): the default member of a class instance or Collection
|
|
if (vm->sp > dims && vm->stack[vm->sp - 1 - dims].type == BAS_TYPE_INSTANCE) {
|
|
return objectDefault(vm, dims, false);
|
|
}
|
|
|
|
BasValueT arrRef;
|
|
int32_t flatIdx;
|
|
BasVmResultE rc = popArrayElement(vm, dims, &arrRef, &flatIdx);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
BasValueT elem = basValCopy(arrRef.arrVal->elements[flatIdx]);
|
|
basValRelease(&arrRef);
|
|
|
|
if (!push(vm, elem)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_PUSH_ARR_ADDR: {
|
|
// Pass `arr(i)` as a BYREF parameter: push a BAS_TYPE_ELEM_REF
|
|
// that owns a counted reference to the array plus the element
|
|
// index. Holding the reference keeps the element storage alive
|
|
// even if the callee REDIMs or ERASEs the array variable, and
|
|
// the index is resolved on every load/store instead of caching
|
|
// a raw element pointer.
|
|
uint8_t dims = readUint8(vm);
|
|
BasValueT arrRef;
|
|
int32_t flatIdx;
|
|
BasVmResultE rc = popArrayElement(vm, dims, &arrRef, &flatIdx);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
// The popped arrRef's reference count transfers to the REF.
|
|
BasValueT ref;
|
|
ref.type = BAS_TYPE_ELEM_REF;
|
|
ref.elemRef.arr = arrRef.arrVal;
|
|
ref.elemRef.idx = flatIdx;
|
|
|
|
if (!push(vm, ref)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_STORE_ARRAY:
|
|
case OP_STORE_ARRAY_FIELD: {
|
|
// [uint8 dims] (STORE_ARRAY_FIELD adds [uint16 fieldIdx]): pop
|
|
// the value, then the subscripts and array reference.
|
|
uint8_t dims = readUint8(vm);
|
|
uint16_t fieldIdx = (op == OP_STORE_ARRAY_FIELD) ? readUint16(vm) : 0;
|
|
BasValueT storeVal;
|
|
|
|
// obj(args) = value: the default member's Property Let
|
|
if (op == OP_STORE_ARRAY && vm->sp > dims + 1 && vm->stack[vm->sp - 2 - dims].type == BAS_TYPE_INSTANCE) {
|
|
return objectDefault(vm, dims, true);
|
|
}
|
|
|
|
if (!pop(vm, &storeVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasValueT arrRef;
|
|
int32_t flatIdx;
|
|
BasVmResultE rc = popArrayElement(vm, dims, &arrRef, &flatIdx);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
basValRelease(&storeVal);
|
|
return rc;
|
|
}
|
|
|
|
BasValueT *target = &arrRef.arrVal->elements[flatIdx];
|
|
|
|
if (op == OP_STORE_ARRAY_FIELD) {
|
|
// Element must be a UDT
|
|
if (target->type != BAS_TYPE_UDT || !target->udtVal) {
|
|
basValRelease(&arrRef);
|
|
basValRelease(&storeVal);
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Array element is not a TYPE instance");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
if (fieldIdx >= (uint16_t)target->udtVal->fieldCount) {
|
|
basValRelease(&arrRef);
|
|
basValRelease(&storeVal);
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid field index");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
target = &target->udtVal->fields[fieldIdx];
|
|
}
|
|
|
|
bool stored = storeElement(vm, target, storeVal);
|
|
basValRelease(&arrRef);
|
|
|
|
if (!stored) {
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_REDIM: {
|
|
uint8_t dims = readUint8(vm);
|
|
uint8_t preserve = readUint8(vm);
|
|
uint8_t elementType = readUint8(vm);
|
|
|
|
// Same stack layout as OP_DIM_ARRAY (bounds, then UDT typeId and
|
|
// fieldCount), with the old array reference underneath; PRESERVE
|
|
// copies its elements while it is still on the stack.
|
|
BasArrayT *newArr;
|
|
BasVmResultE rc = allocArray(vm, dims, elementType, preserve != 0, &newArr);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
BasValueT oldRef;
|
|
|
|
if (!pop(vm, &oldRef)) {
|
|
basArrayUnref(newArr);
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
basValRelease(&oldRef);
|
|
|
|
BasValueT arrVal;
|
|
arrVal.type = BAS_TYPE_ARRAY;
|
|
arrVal.arrVal = newArr;
|
|
|
|
if (!push(vm, arrVal)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_ERASE: {
|
|
// [uint8 mode] A fixed array keeps its bounds and has every
|
|
// element reset; a dynamic array is released and stays
|
|
// undimensioned until the next REDIM.
|
|
uint8_t mode = readUint8(vm);
|
|
BasValueT arrRef;
|
|
|
|
if (!pop(vm, &arrRef)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (mode == BAS_ERASE_ZERO && arrRef.type == BAS_TYPE_ARRAY && arrRef.arrVal) {
|
|
arrayClear(arrRef.arrVal);
|
|
|
|
if (!push(vm, arrRef)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
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:
|
|
case OP_UBOUND:
|
|
return boundOp(vm, op);
|
|
|
|
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, BAS_ERR_TYPE_MISMATCH, "Not a TYPE instance");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
if (fieldIdx >= (uint16_t)udtRef.udtVal->fieldCount) {
|
|
basValRelease(&udtRef);
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid field index");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
BasValueT fieldVal = basValCopy(udtRef.udtVal->fields[fieldIdx]);
|
|
basValRelease(&udtRef);
|
|
|
|
if (!push(vm, 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, BAS_ERR_TYPE_MISMATCH, "Not a TYPE instance");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
if (fieldIdx >= (uint16_t)udtRef.udtVal->fieldCount) {
|
|
basValRelease(&udtRef);
|
|
basValRelease(&storeVal);
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid field index");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
bool stored = storeElement(vm, &udtRef.udtVal->fields[fieldIdx], storeVal);
|
|
basValRelease(&udtRef);
|
|
|
|
if (!stored) {
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// ============================================================
|
|
// DATA/READ/RESTORE
|
|
// ============================================================
|
|
|
|
case OP_READ_DATA: {
|
|
if (vm->dataPtr >= vm->module->dataCount) {
|
|
runtimeError(vm, BAS_ERR_OUT_OF_DATA, "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 (!popArgs(vm, 2, &fmtVal, &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);
|
|
|
|
char buf[BAS_FORMAT_BUF_SIZE];
|
|
buf[0] = '\0';
|
|
|
|
if (strcasecmp(fmt, BAS_FORMAT_PERCENT) == 0) {
|
|
snprintf(buf, sizeof(buf), "%.0f%%", n * 100.0);
|
|
} else if (basDateIsPattern(fmt) || (val.type == BAS_TYPE_DATE && fmtLen == 0)) {
|
|
// A date pattern formats the value as a date (a number as
|
|
// a serial, a string as the date it holds).
|
|
double serial = 0.0;
|
|
int32_t err = basValToDateSerial(val, &serial);
|
|
|
|
if (err != 0) {
|
|
basValRelease(&val);
|
|
basValRelease(&fmtStr);
|
|
return raiseError(vm, err);
|
|
}
|
|
|
|
basDateFormat(serial, fmt, buf, sizeof(buf));
|
|
} else {
|
|
BasNumFormatT f;
|
|
bool sciNotation;
|
|
|
|
parseNumFormat(fmt, fmtLen, &f, &sciNotation);
|
|
formatNumber(n, &f, buf, sizeof(buf));
|
|
}
|
|
|
|
basValRelease(&val);
|
|
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 == BAS_COMPARE_MODE_TEXT);
|
|
break;
|
|
}
|
|
|
|
// ============================================================
|
|
// Halt
|
|
// ============================================================
|
|
|
|
case OP_END:
|
|
case OP_HALT:
|
|
// Any error state left here belongs to a handler that ended
|
|
// the program, which has dealt with it: clear it so the host
|
|
// does not report a trapped error as an unhandled one.
|
|
clearErrorState(vm);
|
|
vm->running = false;
|
|
vm->ended = vm->ended || (op == OP_END);
|
|
return BAS_VM_HALTED;
|
|
|
|
// ============================================================
|
|
// UI / Event opcodes (name-based resolution)
|
|
// ============================================================
|
|
|
|
case OP_LOAD_PROP: {
|
|
BasValueT propNameVal;
|
|
BasValueT ctrlRefVal;
|
|
|
|
if (!popArgs(vm, 2, &propNameVal, &ctrlRefVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (ctrlRefVal.type == BAS_TYPE_INSTANCE) {
|
|
return objectMemberOp(vm, ctrlRefVal, propNameVal, NULL, 0, NULL, BAS_MEMBER_OP_GET);
|
|
}
|
|
|
|
BasVmResultE rc = uiTarget(vm, vm->ui.getProp != NULL, ctrlRefVal);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
basValRelease(&propNameVal);
|
|
basValRelease(&ctrlRefVal);
|
|
return rc;
|
|
}
|
|
|
|
BasValueT sv = basValToString(propNameVal);
|
|
BasValueT result = vm->ui.getProp(vm->ui.ctx, ctrlRefVal.objVal, sv.strVal->data);
|
|
|
|
basValRelease(&sv);
|
|
basValRelease(&propNameVal);
|
|
basValRelease(&ctrlRefVal);
|
|
|
|
if (!push(vm, result)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_STORE_PROP: {
|
|
BasValueT value;
|
|
BasValueT propNameVal;
|
|
BasValueT ctrlRefVal;
|
|
|
|
if (!popArgs(vm, 3, &value, &propNameVal, &ctrlRefVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (ctrlRefVal.type == BAS_TYPE_INSTANCE) {
|
|
return objectMemberOp(vm, ctrlRefVal, propNameVal, NULL, 0, &value, BAS_MEMBER_OP_LET);
|
|
}
|
|
|
|
BasVmResultE rc = uiTarget(vm, vm->ui.setProp != NULL, ctrlRefVal);
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
BasValueT sv = basValToString(propNameVal);
|
|
vm->ui.setProp(vm->ui.ctx, ctrlRefVal.objVal, sv.strVal->data, value);
|
|
basValRelease(&sv);
|
|
}
|
|
|
|
basValRelease(&value);
|
|
basValRelease(&propNameVal);
|
|
basValRelease(&ctrlRefVal);
|
|
return rc;
|
|
}
|
|
|
|
case OP_CALL_METHOD: {
|
|
uint8_t argc = readUint8(vm);
|
|
int32_t argCount = argc;
|
|
BasValueT methodNameVal;
|
|
BasValueT ctrlRefVal;
|
|
BasValueT args[BAS_VM_MAX_CALL_ARGS];
|
|
|
|
// Reject an over-limit argc rather than silently truncating, which
|
|
// would leak the un-popped args and desync the eval stack. Drain
|
|
// every pushed operand (args + method name + control ref) first to
|
|
// keep the stack balanced, then raise the error.
|
|
if (argc > BAS_VM_MAX_CALL_ARGS) {
|
|
dropValues(vm, argc + BAS_CALL_METHOD_FIXED_OPERANDS);
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Too many arguments in method call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
// Pop args in reverse
|
|
|
|
for (int32_t i = argCount - 1; i >= 0; i--) {
|
|
if (!pop(vm, &args[i])) {
|
|
for (int32_t j = i + 1; j < argCount; j++) {
|
|
basValRelease(&args[j]);
|
|
}
|
|
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
}
|
|
|
|
if (!pop(vm, &methodNameVal)) {
|
|
for (int32_t i = 0; i < argCount; i++) {
|
|
basValRelease(&args[i]);
|
|
}
|
|
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (!pop(vm, &ctrlRefVal)) {
|
|
// methodNameVal was already popped -- release it too or
|
|
// its reference leaks on this underflow path.
|
|
basValRelease(&methodNameVal);
|
|
|
|
for (int32_t i = 0; i < argCount; i++) {
|
|
basValRelease(&args[i]);
|
|
}
|
|
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (ctrlRefVal.type == BAS_TYPE_INSTANCE) {
|
|
return objectMemberOp(vm, ctrlRefVal, methodNameVal, args, argCount, NULL, BAS_MEMBER_OP_GET);
|
|
}
|
|
|
|
BasValueT result = basValInteger(0);
|
|
BasVmResultE rc = uiTarget(vm, vm->ui.callMethod != NULL, ctrlRefVal);
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
// A host method takes values: ByRef arguments meant for a
|
|
// class member arrive as references
|
|
for (int32_t i = 0; i < argCount; i++) {
|
|
derefArg(&args[i]);
|
|
}
|
|
|
|
BasValueT sv = basValToString(methodNameVal);
|
|
result = vm->ui.callMethod(vm->ui.ctx, ctrlRefVal.objVal, sv.strVal->data, args, argCount);
|
|
basValRelease(&sv);
|
|
}
|
|
|
|
for (int32_t i = 0; i < argCount; i++) {
|
|
basValRelease(&args[i]);
|
|
}
|
|
|
|
basValRelease(&methodNameVal);
|
|
basValRelease(&ctrlRefVal);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
if (!push(vm, result)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
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);
|
|
|
|
if (!vm->ui.loadForm) {
|
|
return noFormRuntime(vm);
|
|
}
|
|
|
|
if (!push(vm, basValObject(formRef))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_UNLOAD_FORM: {
|
|
BasValueT formVal;
|
|
|
|
if (!pop(vm, &formVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasVmResultE rc = uiTarget(vm, vm->ui.unloadForm != NULL, formVal);
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
vm->ui.unloadForm(vm->ui.ctx, formVal.objVal);
|
|
}
|
|
|
|
basValRelease(&formVal);
|
|
return rc;
|
|
}
|
|
|
|
case OP_SHOW_FORM: {
|
|
uint8_t modal = readUint8(vm);
|
|
BasValueT formVal;
|
|
|
|
if (!pop(vm, &formVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasVmResultE rc = uiTarget(vm, vm->ui.showForm != NULL, formVal);
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
vm->ui.showForm(vm->ui.ctx, formVal.objVal, modal != 0);
|
|
}
|
|
|
|
basValRelease(&formVal);
|
|
return rc;
|
|
}
|
|
|
|
case OP_HIDE_FORM: {
|
|
BasValueT formVal;
|
|
|
|
if (!pop(vm, &formVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasVmResultE rc = uiTarget(vm, vm->ui.hideForm != NULL, formVal);
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
vm->ui.hideForm(vm->ui.ctx, formVal.objVal);
|
|
}
|
|
|
|
basValRelease(&formVal);
|
|
return rc;
|
|
}
|
|
|
|
case OP_MSGBOX: {
|
|
// Stack: [message, flags, title] -- title on top
|
|
BasValueT titleVal;
|
|
BasValueT flagsVal;
|
|
BasValueT msgVal;
|
|
|
|
if (!popArgs(vm, 3, &titleVal, &flagsVal, &msgVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t flags = basValToInt32(flagsVal);
|
|
int32_t result = 1; // default OK
|
|
|
|
if (vm->ui.msgBox) {
|
|
BasValueT sv = basValToString(msgVal);
|
|
BasValueT tv = basValToString(titleVal);
|
|
result = vm->ui.msgBox(vm->ui.ctx, sv.strVal->data, flags, tv.strVal->data);
|
|
basValRelease(&sv);
|
|
basValRelease(&tv);
|
|
}
|
|
|
|
basValRelease(&titleVal);
|
|
basValRelease(&flagsVal);
|
|
basValRelease(&msgVal);
|
|
|
|
if (!vm->ui.msgBox) {
|
|
return noFormRuntime(vm);
|
|
}
|
|
|
|
if (!push(vm, basValInteger((int16_t)result))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_INPUTBOX: {
|
|
BasValueT defaultVal;
|
|
BasValueT titleVal;
|
|
BasValueT promptVal;
|
|
|
|
if (!popArgs(vm, 3, &defaultVal, &titleVal, &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 (!vm->ui.inputBox) {
|
|
return noFormRuntime(vm);
|
|
}
|
|
|
|
if (!push(vm, strValue(result ? result : basStringRef(basEmptyString)))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_ME_REF:
|
|
if (!push(vm, basValObject(vm->currentForm))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
|
|
case OP_ME_OBJ: {
|
|
BasValueT me = currentFrame(vm)->me;
|
|
|
|
if (me.type != BAS_TYPE_INSTANCE || !me.instVal) {
|
|
return raiseError(vm, BAS_ERR_OBJECT_NOT_SET);
|
|
}
|
|
|
|
if (!push(vm, basValCopy(me))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_NEW_OBJ:
|
|
return newObject(vm, readUint16(vm));
|
|
|
|
case OP_PROP_ASSIGN: {
|
|
uint8_t argc = readUint8(vm);
|
|
uint8_t flags = readUint8(vm);
|
|
BasValueT args[BAS_VM_MAX_CALL_ARGS];
|
|
BasValueT value;
|
|
BasValueT nameVal;
|
|
BasValueT objVal;
|
|
|
|
if (argc > BAS_VM_MAX_CALL_ARGS) {
|
|
dropValues(vm, argc + BAS_CALL_METHOD_FIXED_OPERANDS + 1);
|
|
return raiseError(vm, BAS_ERR_ILLEGAL_FUNC_CALL);
|
|
}
|
|
|
|
if (vm->sp < argc + BAS_CALL_METHOD_FIXED_OPERANDS + 1) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
pop(vm, &value);
|
|
vm->sp -= argc;
|
|
memcpy(args, &vm->stack[vm->sp], (size_t)argc * sizeof(BasValueT));
|
|
pop(vm, &nameVal);
|
|
pop(vm, &objVal);
|
|
|
|
if (objVal.type == BAS_TYPE_INSTANCE) {
|
|
return objectMemberOp(vm, objVal, nameVal, args, argc, &value, (flags & BAS_ASSIGN_SET) ? BAS_MEMBER_OP_SET : BAS_MEMBER_OP_LET);
|
|
}
|
|
|
|
// A form or control property takes no arguments
|
|
BasVmResultE rc = (argc > 0) ? raiseError(vm, BAS_ERR_NO_MEMBER) : uiTarget(vm, vm->ui.setProp != NULL, objVal);
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
BasValueT sv = basValToString(nameVal);
|
|
vm->ui.setProp(vm->ui.ctx, objVal.objVal, sv.strVal->data, value);
|
|
basValRelease(&sv);
|
|
}
|
|
|
|
dropArgs(args, argc);
|
|
basValRelease(&value);
|
|
basValRelease(&nameVal);
|
|
basValRelease(&objVal);
|
|
return rc;
|
|
}
|
|
|
|
case OP_CREATE_CTRL: {
|
|
BasValueT nameVal;
|
|
BasValueT typeVal;
|
|
BasValueT formVal;
|
|
|
|
if (!popArgs(vm, 3, &nameVal, &typeVal, &formVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
void *ctrlRef = NULL;
|
|
BasVmResultE rc = uiTarget(vm, vm->ui.createCtrl != NULL, formVal);
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
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);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
if (!push(vm, basValObject(ctrlRef))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_CREATE_CTRL_EX: {
|
|
// Stack: [formRef, typeStr, nameStr, parentRef] -- parent on top
|
|
BasValueT parentVal;
|
|
BasValueT nameVal;
|
|
BasValueT typeVal;
|
|
BasValueT formVal;
|
|
|
|
if (!popArgs(vm, 4, &parentVal, &nameVal, &typeVal, &formVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
void *ctrlRef = NULL;
|
|
BasVmResultE rc = uiTarget(vm, vm->ui.createCtrlEx != NULL, formVal);
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
BasValueT sv1 = basValToString(typeVal);
|
|
BasValueT sv2 = basValToString(nameVal);
|
|
void *parentRef = (parentVal.type == BAS_TYPE_OBJECT) ? parentVal.objVal : NULL;
|
|
ctrlRef = vm->ui.createCtrlEx(vm->ui.ctx, formVal.objVal, sv1.strVal->data, sv2.strVal->data, parentRef);
|
|
basValRelease(&sv1);
|
|
basValRelease(&sv2);
|
|
}
|
|
|
|
basValRelease(&parentVal);
|
|
basValRelease(&nameVal);
|
|
basValRelease(&typeVal);
|
|
basValRelease(&formVal);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
if (!push(vm, basValObject(ctrlRef))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FIND_CTRL: {
|
|
BasValueT nameVal;
|
|
BasValueT formVal;
|
|
|
|
if (!popArgs(vm, 2, &nameVal, &formVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
// a.b.c on an object: member b of a
|
|
if (formVal.type == BAS_TYPE_INSTANCE) {
|
|
return objectMemberOp(vm, formVal, nameVal, NULL, 0, NULL, BAS_MEMBER_OP_GET);
|
|
}
|
|
|
|
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);
|
|
|
|
if (!vm->ui.findCtrl) {
|
|
return noFormRuntime(vm);
|
|
}
|
|
|
|
if (!push(vm, basValObject(ctrlRef))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FIND_CTRL_IDX: {
|
|
// Stack: [formRef, ctrlName, index] -- index on top
|
|
BasValueT idxVal;
|
|
BasValueT nameVal;
|
|
BasValueT formVal;
|
|
|
|
if (!popArgs(vm, 3, &idxVal, &nameVal, &formVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
// a.b(i).c on an object: member b of a with argument i
|
|
if (formVal.type == BAS_TYPE_INSTANCE) {
|
|
return objectMemberOp(vm, formVal, nameVal, &idxVal, 1, NULL, BAS_MEMBER_OP_GET);
|
|
}
|
|
|
|
void *ctrlRef = NULL;
|
|
|
|
if (vm->ui.findCtrlIdx) {
|
|
void *formRef = (formVal.type == BAS_TYPE_OBJECT) ? formVal.objVal : vm->currentForm;
|
|
int32_t index = basValToInt32(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);
|
|
|
|
if (!vm->ui.findCtrlIdx) {
|
|
return noFormRuntime(vm);
|
|
}
|
|
|
|
if (!push(vm, basValObject(ctrlRef))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_CREATE_FORM: {
|
|
// Stack: [nameStr, width, height] -- height on top
|
|
BasValueT heightVal;
|
|
BasValueT widthVal;
|
|
BasValueT nameVal;
|
|
|
|
if (!popArgs(vm, 3, &heightVal, &widthVal, &nameVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
void *formRef = NULL;
|
|
|
|
if (vm->ui.createForm) {
|
|
BasValueT sv = basValToString(nameVal);
|
|
int32_t w = basValToInt32(widthVal);
|
|
int32_t h = basValToInt32(heightVal);
|
|
formRef = vm->ui.createForm(vm->ui.ctx, sv.strVal->data, w, h);
|
|
basValRelease(&sv);
|
|
}
|
|
|
|
basValRelease(&nameVal);
|
|
basValRelease(&widthVal);
|
|
basValRelease(&heightVal);
|
|
|
|
if (!vm->ui.createForm) {
|
|
return noFormRuntime(vm);
|
|
}
|
|
|
|
if (!push(vm, basValObject(formRef))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_SET_EVENT: {
|
|
// Stack: [ctrlRef, eventNameStr, handlerNameStr] -- handler on top
|
|
BasValueT handlerVal;
|
|
BasValueT eventVal;
|
|
BasValueT ctrlVal;
|
|
|
|
if (!popArgs(vm, 3, &handlerVal, &eventVal, &ctrlVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasVmResultE rc = uiTarget(vm, vm->ui.setEvent != NULL, ctrlVal);
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
BasValueT sv1 = basValToString(eventVal);
|
|
BasValueT sv2 = basValToString(handlerVal);
|
|
vm->ui.setEvent(vm->ui.ctx, ctrlVal.objVal, sv1.strVal->data, sv2.strVal->data);
|
|
basValRelease(&sv1);
|
|
basValRelease(&sv2);
|
|
}
|
|
|
|
basValRelease(&handlerVal);
|
|
basValRelease(&eventVal);
|
|
basValRelease(&ctrlVal);
|
|
return rc;
|
|
}
|
|
|
|
case OP_REMOVE_CTRL: {
|
|
// Stack: [formRef, ctrlNameStr] -- name on top
|
|
BasValueT nameVal;
|
|
BasValueT formVal;
|
|
|
|
if (!popArgs(vm, 2, &nameVal, &formVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasVmResultE rc = uiTarget(vm, vm->ui.removeCtrl != NULL, formVal);
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
BasValueT sv = basValToString(nameVal);
|
|
vm->ui.removeCtrl(vm->ui.ctx, formVal.objVal, sv.strVal->data);
|
|
basValRelease(&sv);
|
|
}
|
|
|
|
basValRelease(&nameVal);
|
|
basValRelease(&formVal);
|
|
return rc;
|
|
}
|
|
|
|
// ============================================================
|
|
// Form-level variables
|
|
// ============================================================
|
|
|
|
case OP_LOAD_FORM_VAR: {
|
|
uint16_t idx = readUint16(vm);
|
|
|
|
if (!vm->currentFormVars || idx >= (uint16_t)vm->currentFormVarCount) {
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Form variable access outside form context");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
if (!push(vm, basValCopy(vm->currentFormVars[idx]))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
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, BAS_ERR_SUBSCRIPT_RANGE, "Form variable access outside form context");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
storeValue(&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, BAS_ERR_SUBSCRIPT_RANGE, "Form variable address outside form context");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
BasValueT ref;
|
|
ref.type = BAS_TYPE_REF;
|
|
ref.refVal = &vm->currentFormVars[idx];
|
|
|
|
if (!push(vm, ref)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
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);
|
|
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 : "";
|
|
void *funcPtr = NULL;
|
|
|
|
// The cache holds one entry per function-name constant; a hit
|
|
// must also come from the same library.
|
|
BasExternCacheEntryT *cached = (funcNameIdx < (uint16_t)vm->externCacheCount) ? &vm->externCache[funcNameIdx] : NULL;
|
|
|
|
if (cached && cached->funcPtr && cached->libNameIdx == libNameIdx) {
|
|
funcPtr = cached->funcPtr;
|
|
} else {
|
|
if (vm->ext.resolveExtern) {
|
|
funcPtr = vm->ext.resolveExtern(vm->ext.ctx, libName, funcName);
|
|
}
|
|
|
|
if (!funcPtr) {
|
|
char msg[BAS_VM_ERROR_MSG_LEN];
|
|
|
|
dropValues(vm, argc);
|
|
snprintf(msg, sizeof(msg), "External function not found: %s", funcName[0] ? funcName : "?");
|
|
runtimeError(vm, BAS_ERR_EXTERNAL_NOT_FOUND, msg);
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
if (cached) {
|
|
cached->funcPtr = funcPtr;
|
|
cached->libNameIdx = libNameIdx;
|
|
}
|
|
}
|
|
|
|
// Reject an over-limit argc rather than silently truncating, which
|
|
// would leak the un-popped args and desync the eval stack. Drain
|
|
// the pushed args (extern has no extra fixed operands) to keep the
|
|
// stack balanced, then raise the error.
|
|
if (argc > BAS_VM_MAX_CALL_ARGS) {
|
|
dropValues(vm, argc);
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Too many arguments in external call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
// Pop arguments (reverse order)
|
|
BasValueT args[BAS_VM_MAX_CALL_ARGS];
|
|
int32_t argCount = argc;
|
|
|
|
for (int32_t i = argCount - 1; i >= 0; i--) {
|
|
if (!pop(vm, &args[i])) {
|
|
for (int32_t j = i + 1; j < argCount; j++) {
|
|
basValRelease(&args[j]);
|
|
}
|
|
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
}
|
|
|
|
// Call through host callback
|
|
BasValueT result = basValInteger(0);
|
|
|
|
if (vm->ext.callExtern) {
|
|
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)
|
|
if (!push(vm, result)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// ============================================================
|
|
// Debugger statement boundaries
|
|
// ============================================================
|
|
|
|
case OP_LINE: {
|
|
uint16_t lineNum = readUint16(vm);
|
|
vm->currentLine = lineNum;
|
|
|
|
// Snapshot the eval-stack depth at this statement boundary so an
|
|
// ON ERROR GOTO dispatch can release any operands left half-pushed
|
|
// by an error mid-expression and reset sp to here. Also remember
|
|
// where the statement starts: RESUME re-executes it from here and
|
|
// RESUME NEXT scans from here for the following statement.
|
|
vm->stmtSp = vm->sp;
|
|
vm->stmtPc = vm->currentOpPc;
|
|
|
|
// 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_COMMAND: {
|
|
// Replace the argument index on top with COMMAND$(n)
|
|
if (vm->sp < 1) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasValueT *top = &vm->stack[vm->sp - 1];
|
|
int32_t n = basValToInt32(*top);
|
|
|
|
if (n < 0) {
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
basValRelease(top);
|
|
*top = strValue(commandArg(vm->commandArgs, n));
|
|
break;
|
|
}
|
|
|
|
case OP_APP_PATH:
|
|
case OP_APP_CONFIG:
|
|
case OP_APP_DATA: {
|
|
const char *path = (op == OP_APP_PATH) ? vm->appPath : (op == OP_APP_CONFIG) ? vm->appConfig : vm->appData;
|
|
|
|
if (!push(vm, basValStringFromC(path))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_INI_READ: {
|
|
// Stack: file, section, key, default -> result string
|
|
BasValueT defVal;
|
|
BasValueT keyVal;
|
|
BasValueT secVal;
|
|
BasValueT fileVal;
|
|
|
|
if (!popArgs(vm, 4, &defVal, &keyVal, &secVal, &fileVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasStringT *fileStr = basValFormatString(fileVal);
|
|
BasStringT *secStr = basValFormatString(secVal);
|
|
BasStringT *keyStr = basValFormatString(keyVal);
|
|
BasStringT *defStr = basValFormatString(defVal);
|
|
BasValueT resultVal = iniRead(fileStr->data, secStr->data, keyStr->data, defStr);
|
|
|
|
basStringUnref(fileStr);
|
|
basStringUnref(secStr);
|
|
basStringUnref(keyStr);
|
|
basStringUnref(defStr);
|
|
basValRelease(&fileVal);
|
|
basValRelease(&secVal);
|
|
basValRelease(&keyVal);
|
|
basValRelease(&defVal);
|
|
|
|
if (!push(vm, resultVal)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_INI_WRITE: {
|
|
// Stack: file, section, key, value
|
|
BasValueT valVal;
|
|
BasValueT keyVal;
|
|
BasValueT secVal;
|
|
BasValueT fileVal;
|
|
|
|
if (!popArgs(vm, 4, &valVal, &keyVal, &secVal, &fileVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasStringT *fileStr = basValFormatString(fileVal);
|
|
BasStringT *secStr = basValFormatString(secVal);
|
|
BasStringT *keyStr = basValFormatString(keyVal);
|
|
BasStringT *valStr = basValFormatString(valVal);
|
|
BasVmResultE rc = iniWrite(vm, fileStr->data, secStr->data, keyStr->data, valStr->data);
|
|
|
|
basStringUnref(fileStr);
|
|
basStringUnref(secStr);
|
|
basStringUnref(keyStr);
|
|
basStringUnref(valStr);
|
|
basValRelease(&fileVal);
|
|
basValRelease(&secVal);
|
|
basValRelease(&keyVal);
|
|
basValRelease(&valVal);
|
|
return rc;
|
|
}
|
|
|
|
default:
|
|
runtimeError(vm, BAS_ERR_BAD_OPCODE, "Bad opcode");
|
|
return BAS_VM_BAD_OPCODE;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
void basVmStepInto(BasVmT *vm) {
|
|
if (vm) {
|
|
vm->debugBreak = true;
|
|
}
|
|
}
|
|
|
|
|
|
void basVmStepOut(BasVmT *vm) {
|
|
if (vm) {
|
|
vm->stepOutDepth = vm->callDepth;
|
|
}
|
|
}
|
|
|
|
|
|
void basVmStepOver(BasVmT *vm) {
|
|
if (vm) {
|
|
vm->stepOverDepth = vm->callDepth;
|
|
}
|
|
}
|
|
|
|
|
|
// OP_LBOUND / OP_UBOUND: [uint8 dim] array ref on stack -> LONG bound.
|
|
static BasVmResultE boundOp(BasVmT *vm, uint8_t op) {
|
|
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, BAS_ERR_TYPE_MISMATCH, "Not an array");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
if (dim < 1 || dim > (uint8_t)arrRef.arrVal->dims) {
|
|
basValRelease(&arrRef);
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, (op == OP_LBOUND) ? "Invalid dimension for LBOUND" : "Invalid dimension for UBOUND");
|
|
return BAS_VM_SUBSCRIPT_RANGE;
|
|
}
|
|
|
|
int32_t bound = (op == OP_LBOUND) ? arrRef.arrVal->lbound[dim - 1] : arrRef.arrVal->ubound[dim - 1];
|
|
basValRelease(&arrRef);
|
|
|
|
if (!push(vm, basValLong(bound))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// The date functions of OP_BUILTIN. args holds every argument (defaults
|
|
// filled in). Returns 0 and sets *out, else the BASIC error number.
|
|
static int32_t builtinDate(uint8_t fn, BasValueT *args, BasValueT *out) {
|
|
double serial = 0.0;
|
|
int32_t err = 0;
|
|
BasDateIntervalE interval;
|
|
|
|
switch (fn) {
|
|
case BAS_FN_NOW:
|
|
case BAS_FN_DATE:
|
|
case BAS_FN_TIME: {
|
|
struct tm t;
|
|
double datePart = 0.0;
|
|
double timePart = 0.0;
|
|
|
|
localNow(&t, NULL);
|
|
|
|
if (fn != BAS_FN_TIME && !basDateSerial(t.tm_year + BAS_TM_YEAR_BASE, t.tm_mon + 1, t.tm_mday, &datePart)) {
|
|
return BAS_ERR_OVERFLOW;
|
|
}
|
|
|
|
if (fn != BAS_FN_DATE && !basTimeSerial(t.tm_hour, t.tm_min, t.tm_sec, &timePart)) {
|
|
return BAS_ERR_OVERFLOW;
|
|
}
|
|
|
|
*out = basValDate(datePart + timePart);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_DATESERIAL:
|
|
case BAS_FN_TIMESERIAL: {
|
|
int32_t parts[BAS_SERIAL_PARTS];
|
|
|
|
for (int32_t i = 0; i < BAS_SERIAL_PARTS; i++) {
|
|
if (!basValRoundToInt32(args[i], INT32_MIN, INT32_MAX, &parts[i])) {
|
|
return BAS_ERR_OVERFLOW;
|
|
}
|
|
}
|
|
|
|
bool ok = (fn == BAS_FN_DATESERIAL) ? basDateSerial(parts[0], parts[1], parts[2], &serial) : basTimeSerial(parts[0], parts[1], parts[2], &serial);
|
|
|
|
if (!ok) {
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
*out = basValDate(serial);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_ISDATE:
|
|
*out = basValBool(args[0].type == BAS_TYPE_DATE || (args[0].type == BAS_TYPE_STRING && basValToDateSerial(args[0], &serial) == 0));
|
|
return 0;
|
|
|
|
case BAS_FN_MONTHNAME:
|
|
case BAS_FN_WEEKDAYNAME: {
|
|
int32_t n = basValToInt32(args[0]);
|
|
bool abbr = basValIsTruthy(args[1]);
|
|
const char *name;
|
|
|
|
if (fn == BAS_FN_MONTHNAME) {
|
|
name = basDateMonthName(n, abbr);
|
|
} else {
|
|
int32_t first = basValToInt32(args[2]);
|
|
|
|
if (first < BAS_DATE_USE_SYSTEM_DAY || first > BAS_DATE_SATURDAY || n < 1 || n > BAS_DATE_DAYS_PER_WEEK) {
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
// n counts from the first day of the week
|
|
if (first == BAS_DATE_USE_SYSTEM_DAY) {
|
|
first = BAS_DATE_SUNDAY;
|
|
}
|
|
|
|
name = basDateWeekdayName((n - 1 + first - 1) % BAS_DATE_DAYS_PER_WEEK + 1, abbr);
|
|
}
|
|
|
|
if (name == NULL) {
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
*out = basValStringFromC(name);
|
|
return 0;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Every other function reads a date: the third argument of DATEADD
|
|
// and DATEDIFF (the second date), the second of DATEPART and the first
|
|
// otherwise. The interval of the three is the first.
|
|
int32_t dateArg = 0;
|
|
|
|
if (fn == BAS_FN_DATEADD || fn == BAS_FN_DATEDIFF) {
|
|
dateArg = 2;
|
|
} else if (fn == BAS_FN_DATEPART) {
|
|
dateArg = 1;
|
|
}
|
|
|
|
err = basValToDateSerial(args[dateArg], &serial);
|
|
|
|
if (err != 0) {
|
|
return err;
|
|
}
|
|
|
|
if ((fn == BAS_FN_DATEADD || fn == BAS_FN_DATEDIFF || fn == BAS_FN_DATEPART) && !basDateParseInterval(argString(&args[0])->data, &interval)) {
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
BasDatePartsT parts;
|
|
|
|
basDateSplit(serial, &parts);
|
|
|
|
switch (fn) {
|
|
case BAS_FN_DATEVALUE:
|
|
*out = basValDate((double)parts.dayNumber);
|
|
return 0;
|
|
|
|
case BAS_FN_TIMEVALUE:
|
|
*out = basValDate((double)parts.secOfDay / BAS_SECS_PER_DAY);
|
|
return 0;
|
|
|
|
case BAS_FN_YEAR:
|
|
*out = basValInteger((int16_t)parts.year);
|
|
return 0;
|
|
|
|
case BAS_FN_MONTH:
|
|
*out = basValInteger((int16_t)parts.month);
|
|
return 0;
|
|
|
|
case BAS_FN_DAY:
|
|
*out = basValInteger((int16_t)parts.day);
|
|
return 0;
|
|
|
|
case BAS_FN_HOUR:
|
|
*out = basValInteger((int16_t)parts.hour);
|
|
return 0;
|
|
|
|
case BAS_FN_MINUTE:
|
|
*out = basValInteger((int16_t)parts.minute);
|
|
return 0;
|
|
|
|
case BAS_FN_SECOND:
|
|
*out = basValInteger((int16_t)parts.second);
|
|
return 0;
|
|
|
|
case BAS_FN_DATEADD: {
|
|
double n = trunc(basValToNumber(args[1]));
|
|
double result;
|
|
|
|
if (!(n >= INT32_MIN && n <= INT32_MAX)) {
|
|
return BAS_ERR_OVERFLOW;
|
|
}
|
|
|
|
if (!basDateAdd(interval, (int32_t)n, serial, &result)) {
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
*out = basValDate(result);
|
|
return 0;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// WEEKDAY, DATEDIFF and DATEPART take a first day of the week (and
|
|
// the last two a first week of the year) after their dates.
|
|
int32_t firstDayArg = (fn == BAS_FN_WEEKDAY) ? 1 : dateArg + 1;
|
|
int32_t firstDay = basValToInt32(args[firstDayArg]);
|
|
int32_t firstWeek = (fn == BAS_FN_WEEKDAY) ? BAS_DATE_FIRST_JAN1 : basValToInt32(args[firstDayArg + 1]);
|
|
|
|
if (firstDay < BAS_DATE_USE_SYSTEM_DAY || firstDay > BAS_DATE_SATURDAY || firstWeek < BAS_DATE_USE_SYSTEM_WEEK || firstWeek > BAS_DATE_FIRST_FULL_WEEK) {
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
if (fn == BAS_FN_WEEKDAY) {
|
|
*out = basValInteger((int16_t)basDateWeekday(serial, firstDay));
|
|
return 0;
|
|
}
|
|
|
|
if (fn == BAS_FN_DATEPART) {
|
|
*out = basValInteger((int16_t)basDatePart(interval, serial, firstDay, firstWeek));
|
|
return 0;
|
|
}
|
|
|
|
// DATEDIFF: the first date is args[1], the second (serial) args[2]
|
|
double from;
|
|
|
|
err = basValToDateSerial(args[1], &from);
|
|
|
|
if (err != 0) {
|
|
return err;
|
|
}
|
|
|
|
int64_t diff = basDateDiff(interval, from, serial, firstDay);
|
|
|
|
if (diff < INT32_MIN || diff > INT32_MAX) {
|
|
return BAS_ERR_OVERFLOW;
|
|
}
|
|
|
|
*out = basValLong((int32_t)diff);
|
|
return 0;
|
|
}
|
|
|
|
|
|
// Pads the optional trailing arguments of an OP_BUILTIN call with their
|
|
// defaults. Returns the new argument count.
|
|
static int32_t builtinDefaults(uint8_t fn, BasValueT *args, int32_t argc) {
|
|
switch (fn) {
|
|
case BAS_FN_REPLACE:
|
|
padArg(args, &argc, 3, basValLong(1));
|
|
padArg(args, &argc, 4, basValLong(BAS_COUNT_ALL));
|
|
padArg(args, &argc, 5, basValLong(BAS_COMPARE_USE_OPTION));
|
|
break;
|
|
|
|
case BAS_FN_SPLIT:
|
|
padArg(args, &argc, 1, basValStringFromC(BAS_DEFAULT_DELIMITER));
|
|
padArg(args, &argc, 2, basValLong(BAS_COUNT_ALL));
|
|
padArg(args, &argc, 3, basValLong(BAS_COMPARE_USE_OPTION));
|
|
break;
|
|
|
|
case BAS_FN_JOIN:
|
|
padArg(args, &argc, 1, basValStringFromC(BAS_DEFAULT_DELIMITER));
|
|
break;
|
|
|
|
case BAS_FN_INSTRREV:
|
|
padArg(args, &argc, 2, basValLong(BAS_COUNT_ALL));
|
|
padArg(args, &argc, 3, basValLong(BAS_COMPARE_USE_OPTION));
|
|
break;
|
|
|
|
case BAS_FN_STRCOMP:
|
|
padArg(args, &argc, 2, basValLong(BAS_COMPARE_USE_OPTION));
|
|
break;
|
|
|
|
case BAS_FN_FILTER:
|
|
padArg(args, &argc, 2, basValBool(true));
|
|
padArg(args, &argc, 3, basValLong(BAS_COMPARE_USE_OPTION));
|
|
break;
|
|
|
|
case BAS_FN_WEEKDAY:
|
|
padArg(args, &argc, 1, basValLong(BAS_DATE_SUNDAY));
|
|
break;
|
|
|
|
case BAS_FN_DATEDIFF:
|
|
padArg(args, &argc, 3, basValLong(BAS_DATE_SUNDAY));
|
|
padArg(args, &argc, 4, basValLong(BAS_DATE_FIRST_JAN1));
|
|
break;
|
|
|
|
case BAS_FN_DATEPART:
|
|
padArg(args, &argc, 2, basValLong(BAS_DATE_SUNDAY));
|
|
padArg(args, &argc, 3, basValLong(BAS_DATE_FIRST_JAN1));
|
|
break;
|
|
|
|
case BAS_FN_MONTHNAME:
|
|
padArg(args, &argc, 1, basValBool(false));
|
|
break;
|
|
|
|
case BAS_FN_WEEKDAYNAME:
|
|
padArg(args, &argc, 1, basValBool(false));
|
|
padArg(args, &argc, 2, basValLong(BAS_DATE_SUNDAY));
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return argc;
|
|
}
|
|
|
|
|
|
// The object and Variant functions of OP_BUILTIN (BAS_FN_OBJECT_FIRST ..
|
|
// BAS_FN_OBJECT_LAST). Returns 0 and sets *out, else the BASIC error
|
|
// number.
|
|
static int32_t builtinObject(BasVmT *vm, uint8_t fn, BasValueT *args, BasValueT *out) {
|
|
BasValueT v = args[0];
|
|
|
|
switch (fn) {
|
|
case BAS_FN_VARTYPE: {
|
|
int32_t vt = varTypeOf(v.type);
|
|
|
|
if (v.type == BAS_TYPE_ARRAY && v.arrVal) {
|
|
vt = BAS_VT_ARRAY + varTypeOf(v.arrVal->elementType);
|
|
}
|
|
|
|
*out = basValInteger((int16_t)vt);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_TYPENAME: {
|
|
char buf[BAS_MAX_IDENT + BAS_ARRAY_SUFFIX_LEN];
|
|
const char *name = basValIsObject(v) ? objectTypeName(vm, v) : typeNameOf(vm, v.type, v.type == BAS_TYPE_UDT && v.udtVal ? v.udtVal->typeId : -1);
|
|
|
|
if (v.type == BAS_TYPE_ARRAY && v.arrVal) {
|
|
snprintf(buf, sizeof(buf), "%s%s", typeNameOf(vm, v.arrVal->elementType, -1), BAS_ARRAY_SUFFIX);
|
|
name = buf;
|
|
}
|
|
|
|
*out = basValStringFromC(name);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_ISEMPTY:
|
|
*out = basValBool(v.type == BAS_TYPE_EMPTY);
|
|
return 0;
|
|
|
|
case BAS_FN_ISNULL:
|
|
*out = basValBool(v.type == BAS_TYPE_NULL);
|
|
return 0;
|
|
|
|
case BAS_FN_ISNUMERIC: {
|
|
bool numeric = isIntegerType(v.type) || v.type == BAS_TYPE_SINGLE || v.type == BAS_TYPE_DOUBLE || v.type == BAS_TYPE_BOOLEAN || v.type == BAS_TYPE_EMPTY;
|
|
|
|
if (v.type == BAS_TYPE_STRING && v.strVal) {
|
|
numeric = textIsNumeric(v.strVal);
|
|
}
|
|
|
|
*out = basValBool(numeric);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_ISOBJECT:
|
|
*out = basValBool(basValIsObject(v));
|
|
return 0;
|
|
|
|
case BAS_FN_ISARRAY:
|
|
*out = basValBool(v.type == BAS_TYPE_ARRAY);
|
|
return 0;
|
|
|
|
case BAS_FN_CVAR:
|
|
*out = basValCopy(v);
|
|
return 0;
|
|
|
|
case BAS_FN_TYPEOF: {
|
|
if (!basValIsObject(v)) {
|
|
return BAS_ERR_OBJECT_REQUIRED;
|
|
}
|
|
|
|
const char *want = argString(&args[1])->data;
|
|
const char *actual = objectTypeName(vm, v);
|
|
bool isHost = (v.type == BAS_TYPE_OBJECT && v.objVal);
|
|
bool match = strcasecmp(actual, want) == 0;
|
|
|
|
// Object is every object, Control every control on a form
|
|
if (strcasecmp(want, BAS_TYPENAME_OBJECT) == 0) {
|
|
match = !(v.type == BAS_TYPE_OBJECT && !v.objVal);
|
|
} else if (strcasecmp(want, BAS_TYPENAME_CONTROL) == 0) {
|
|
match = isHost && strcasecmp(actual, BAS_TYPENAME_FORM) != 0;
|
|
}
|
|
|
|
*out = basValBool(match);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_EACH_COUNT:
|
|
case BAS_FN_EACH_ITEM: {
|
|
int32_t count = 0;
|
|
int32_t err = eachCount(v, &count);
|
|
|
|
if (err != 0 || fn == BAS_FN_EACH_COUNT) {
|
|
*out = basValLong(count);
|
|
return err;
|
|
}
|
|
|
|
int32_t i = basValToInt32(args[1]);
|
|
|
|
if (i < 0 || i >= count) {
|
|
return BAS_ERR_SUBSCRIPT_RANGE;
|
|
}
|
|
|
|
*out = basValCopy((v.type == BAS_TYPE_ARRAY) ? v.arrVal->elements[i] : v.instVal->items[i].value);
|
|
return 0;
|
|
}
|
|
|
|
default:
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
}
|
|
|
|
|
|
// The string, array and choice functions of OP_BUILTIN. args holds every
|
|
// argument (defaults filled in). Returns 0 and sets *out, else the BASIC
|
|
// error number.
|
|
static int32_t builtinString(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t argc, BasValueT *out) {
|
|
bool text = false;
|
|
int32_t err;
|
|
|
|
switch (fn) {
|
|
case BAS_FN_REPLACE: {
|
|
BasStringT *s = argString(&args[0]);
|
|
BasStringT *find = argString(&args[1]);
|
|
BasStringT *repl = argString(&args[2]);
|
|
int32_t start = basValToInt32(args[3]);
|
|
int32_t count = basValToInt32(args[4]);
|
|
|
|
err = argCompare(vm, args[5], &text);
|
|
|
|
if (err != 0 || start < 1 || count < BAS_COUNT_ALL) {
|
|
return err != 0 ? err : BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
// VB returns the text from start on, with the replacements made
|
|
int32_t len = replaceScan(s, start - 1, find, repl, count, text, NULL);
|
|
BasStringT *result = basStringAlloc(len + 1);
|
|
|
|
if (result->cap >= len + 1) {
|
|
replaceScan(s, start - 1, find, repl, count, text, result->data);
|
|
result->len = len;
|
|
result->data[len] = '\0';
|
|
}
|
|
|
|
*out = strValue(result);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_SPLIT:
|
|
case BAS_FN_FILTER: {
|
|
int32_t limit = BAS_COUNT_ALL;
|
|
bool keep = true;
|
|
|
|
// SPLIT(s, delim, limit, compare) / FILTER(array, match, include, compare)
|
|
if (fn == BAS_FN_SPLIT) {
|
|
argString(&args[0]);
|
|
limit = basValToInt32(args[2]);
|
|
} else {
|
|
if (!isOneDimArray(args[0])) {
|
|
return BAS_ERR_TYPE_MISMATCH;
|
|
}
|
|
|
|
keep = basValIsTruthy(args[2]);
|
|
}
|
|
|
|
argString(&args[1]);
|
|
err = argCompare(vm, args[3], &text);
|
|
|
|
if (err != 0 || limit < BAS_COUNT_ALL) {
|
|
return err != 0 ? err : BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
// Count the pieces, then fill an array of exactly that many
|
|
int32_t n = (fn == BAS_FN_SPLIT) ? splitScan(args[0].strVal, args[1].strVal, limit, text, NULL) : filterScan(args[0].arrVal, args[1].strVal, keep, text, NULL);
|
|
BasArrayT *arr = newVector(n, BAS_TYPE_STRING);
|
|
|
|
if (!arr) {
|
|
return BAS_ERR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
if (fn == BAS_FN_SPLIT) {
|
|
splitScan(args[0].strVal, args[1].strVal, limit, text, arr);
|
|
} else {
|
|
filterScan(args[0].arrVal, args[1].strVal, keep, text, arr);
|
|
}
|
|
|
|
*out = arrValue(arr);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_JOIN: {
|
|
if (!isOneDimArray(args[0])) {
|
|
return BAS_ERR_TYPE_MISMATCH;
|
|
}
|
|
|
|
BasStringT *delim = argString(&args[1]);
|
|
int32_t len = joinScan(args[0].arrVal, delim, NULL);
|
|
BasStringT *result = basStringAlloc(len + 1);
|
|
|
|
if (result->cap >= len + 1) {
|
|
joinScan(args[0].arrVal, delim, result->data);
|
|
result->len = len;
|
|
result->data[len] = '\0';
|
|
}
|
|
|
|
*out = strValue(result);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_INSTRREV: {
|
|
BasStringT *s = argString(&args[0]);
|
|
BasStringT *find = argString(&args[1]);
|
|
int32_t start = basValToInt32(args[2]);
|
|
|
|
err = argCompare(vm, args[3], &text);
|
|
|
|
if (err != 0 || start == 0 || start < BAS_COUNT_ALL) {
|
|
return err != 0 ? err : BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
if (start == BAS_COUNT_ALL) {
|
|
start = s->len;
|
|
}
|
|
|
|
int32_t pos = 0;
|
|
|
|
if (s->len > 0 && start <= s->len) {
|
|
pos = start;
|
|
|
|
if (find->len > 0) {
|
|
pos = 0;
|
|
|
|
for (int32_t i = start - find->len; i >= 0 && pos == 0; i--) {
|
|
if (stringMatchAt(s, i, find, text)) {
|
|
pos = i + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
*out = basValLong(pos);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_STRCOMP: {
|
|
BasStringT *a = argString(&args[0]);
|
|
BasStringT *b = argString(&args[1]);
|
|
|
|
err = argCompare(vm, args[2], &text);
|
|
|
|
if (err != 0) {
|
|
return err;
|
|
}
|
|
|
|
int32_t cmp = text ? basStringCompareCI(a, b) : basStringCompare(a, b);
|
|
|
|
*out = basValInteger((int16_t)((cmp > 0) - (cmp < 0)));
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_STRREVERSE: {
|
|
BasStringT *s = argString(&args[0]);
|
|
BasStringT *result = basStringAlloc(s->len + 1);
|
|
|
|
if (result->cap >= s->len + 1) {
|
|
for (int32_t i = 0; i < s->len; i++) {
|
|
result->data[i] = s->data[s->len - 1 - i];
|
|
}
|
|
|
|
result->len = s->len;
|
|
result->data[s->len] = '\0';
|
|
}
|
|
|
|
*out = strValue(result);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_LSET:
|
|
case BAS_FN_RSET: {
|
|
// The target keeps its length; the value is cut or padded
|
|
if (args[0].type != BAS_TYPE_STRING) {
|
|
return BAS_ERR_TYPE_MISMATCH;
|
|
}
|
|
|
|
int32_t width = argString(&args[0])->len;
|
|
BasStringT *value = argString(&args[1]);
|
|
int32_t copy = DVX_MIN(value->len, width);
|
|
int32_t offset = (fn == BAS_FN_RSET) ? width - copy : 0;
|
|
BasStringT *result = fillString(' ', width);
|
|
|
|
if (result->len == width) {
|
|
memcpy(result->data + offset, value->data, copy);
|
|
}
|
|
|
|
*out = strValue(result);
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_IIF:
|
|
*out = basValCopy(basValIsTruthy(args[0]) ? args[1] : args[2]);
|
|
return 0;
|
|
|
|
case BAS_FN_CHOOSE: {
|
|
// An index outside the choices gives Null (VB)
|
|
double index = trunc(basValToNumber(args[0]));
|
|
|
|
*out = (index >= 1 && index < argc) ? basValCopy(args[(int32_t)index]) : basValNull();
|
|
return 0;
|
|
}
|
|
|
|
case BAS_FN_PARAMARRAY: {
|
|
// (elementType, values...): the values move into a 0-based
|
|
// array; each keeps the type the compiler gave it.
|
|
int32_t elementType = basValToInt32(args[0]);
|
|
|
|
if (elementType > BAS_TYPE_BOOLEAN && elementType != BAS_TYPE_OBJECT && elementType != BAS_TYPE_DATE && elementType != BAS_TYPE_VARIANT) {
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
BasArrayT *arr = newVector(argc - 1, (uint8_t)elementType);
|
|
|
|
if (!arr) {
|
|
return BAS_ERR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
for (int32_t i = 1; i < argc; i++) {
|
|
basValRelease(&arr->elements[i - 1]);
|
|
arr->elements[i - 1] = args[i];
|
|
args[i] = basValInteger(0);
|
|
}
|
|
|
|
*out = arrValue(arr);
|
|
return 0;
|
|
}
|
|
|
|
default:
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
}
|
|
|
|
|
|
// Runs procedure member m of obj: the arguments (taken over) become its
|
|
// parameters, omitted Optional ones their type's zero, the rest of a
|
|
// ParamArray's packed into an array. The procedure runs as a call made from here
|
|
// with obj as its Me; wantResult makes a SUB (or Property Let/Set) leave
|
|
// Empty where the caller expects a value. args has room for
|
|
// BAS_VM_MAX_CALL_ARGS + 1 values.
|
|
static BasVmResultE callMember(BasVmT *vm, BasObjectT *obj, const BasClassMemberT *m, BasValueT *args, int32_t argc, bool wantResult) {
|
|
const BasProcEntryT *proc = &vm->module->procs[m->index];
|
|
int32_t paramCount = proc->paramCount;
|
|
int32_t baseSlot = proc->isFunction ? 1 : 0;
|
|
bool packed = (m->flags & BAS_MEMBER_PARAMARRAY) != 0;
|
|
|
|
if (argc < m->requiredParams || (argc > paramCount && !packed)) {
|
|
dropArgs(args, argc);
|
|
return raiseError(vm, BAS_ERR_WRONG_ARGS);
|
|
}
|
|
|
|
if (packed) {
|
|
int32_t first = paramCount - 1;
|
|
int32_t extra = (argc > first) ? argc - first : 0;
|
|
BasArrayT *arr = newVector(extra, BAS_TYPE_VARIANT);
|
|
|
|
if (!arr) {
|
|
dropArgs(args, argc);
|
|
return raiseError(vm, BAS_ERR_OUT_OF_MEMORY);
|
|
}
|
|
|
|
for (int32_t i = 0; i < extra; i++) {
|
|
derefArg(&args[first + i]);
|
|
basValRelease(&arr->elements[i]);
|
|
arr->elements[i] = args[first + i];
|
|
}
|
|
|
|
args[first] = arrValue(arr);
|
|
argc = paramCount;
|
|
}
|
|
|
|
if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE) {
|
|
dropArgs(args, argc);
|
|
return BAS_VM_CALL_OVERFLOW;
|
|
}
|
|
|
|
BasCallFrameT *frame = pushFrame(vm, vm->pc, proc->codeAddr, baseSlot + paramCount);
|
|
|
|
for (int32_t i = 0; i < paramCount; i++) {
|
|
frame->locals[baseSlot + i] = (i < argc) ? udtDetach(args[i]) : zeroValue(m->paramTypes[i]);
|
|
}
|
|
|
|
frame->me = basValInstance(basObjectRef(obj));
|
|
frame->pushResult = wantResult && !proc->isFunction;
|
|
vm->pc = proc->codeAddr;
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Clamps an RGB channel value into [0, BAS_RGB_COMPONENT_MAX].
|
|
static int32_t clampComponent(int32_t c) {
|
|
if (c < 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (c > BAS_RGB_COMPONENT_MAX) {
|
|
return BAS_RGB_COMPONENT_MAX;
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
|
|
// Clears the Err state (ERR, ERL and the error message).
|
|
static void clearErrorState(BasVmT *vm) {
|
|
vm->errorNumber = 0;
|
|
vm->errorLine = 0;
|
|
vm->errorMsg[0] = '\0';
|
|
}
|
|
|
|
|
|
// Closes a file channel if it is open and marks it free.
|
|
// A printer channel's spooled output goes to the printer first; false
|
|
// when that failed or was cancelled.
|
|
static bool closeChannel(BasVmT *vm, int32_t channel) {
|
|
BasFileChannelT *ch = &vm->files[channel];
|
|
bool ok = true;
|
|
|
|
if (ch->printJob) {
|
|
ok = sendSpool(vm, ch);
|
|
}
|
|
|
|
if (ch->handle) {
|
|
fclose((FILE *)ch->handle);
|
|
}
|
|
|
|
ch->handle = NULL;
|
|
ch->printJob = NULL;
|
|
ch->mode = BAS_FILE_MODE_CLOSED;
|
|
return ok;
|
|
}
|
|
|
|
|
|
// Collection members: Add item [, key] [, before] [, after], Remove
|
|
// index-or-key, Item index-or-key (the default member, name NULL) and
|
|
// Count. Arguments given as Empty count as omitted. Takes over args.
|
|
static BasVmResultE collectionAccess(BasVmT *vm, BasObjectT *coll, const char *name, BasValueT *args, int32_t argc, int32_t op) {
|
|
BasValueT result = basValEmpty();
|
|
int32_t err = 0;
|
|
int32_t pos = 0;
|
|
|
|
for (int32_t i = 0; i < argc; i++) {
|
|
derefArg(&args[i]);
|
|
}
|
|
|
|
if (!name || strcasecmp(name, BAS_COLL_ITEM) == 0) {
|
|
err = (op != BAS_MEMBER_OP_GET) ? BAS_ERR_NO_MEMBER : ((argc != 1) ? BAS_ERR_WRONG_ARGS : collectionFind(coll, args[0], &pos));
|
|
|
|
if (err == 0) {
|
|
result = basValCopy(coll->items[pos].value);
|
|
}
|
|
} else if (strcasecmp(name, BAS_COLL_COUNT) == 0) {
|
|
err = (argc != 0) ? BAS_ERR_WRONG_ARGS : 0;
|
|
result = basValLong(coll->itemCount);
|
|
} else if (strcasecmp(name, BAS_COLL_ADD) == 0) {
|
|
err = (argc < 1 || argc > BAS_COLL_ADD_ARGS) ? BAS_ERR_WRONG_ARGS : collectionAdd(coll, args, argc);
|
|
} else if (strcasecmp(name, BAS_COLL_REMOVE) == 0) {
|
|
err = (argc != 1) ? BAS_ERR_WRONG_ARGS : collectionFind(coll, args[0], &pos);
|
|
|
|
if (err == 0) {
|
|
BasCollItemT gone = coll->items[pos];
|
|
|
|
coll->itemCount--;
|
|
memmove(&coll->items[pos], &coll->items[pos + 1], (size_t)(coll->itemCount - pos) * sizeof(BasCollItemT));
|
|
basStringUnref(gone.key);
|
|
basValRelease(&gone.value);
|
|
}
|
|
} else {
|
|
err = BAS_ERR_NO_MEMBER;
|
|
}
|
|
|
|
dropArgs(args, argc);
|
|
|
|
if (err != 0) {
|
|
basValRelease(&result);
|
|
return raiseError(vm, err);
|
|
}
|
|
|
|
// Add and Remove are statements, but the caller may still want a value
|
|
if (op == BAS_MEMBER_OP_GET && !push(vm, result)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Collection.Add item, key, before, after (args[1..] may be missing or
|
|
// Empty). Returns 0 or the BASIC error number. The item is copied.
|
|
static int32_t collectionAdd(BasObjectT *coll, BasValueT *args, int32_t argc) {
|
|
bool hasKey = argc > 1 && args[1].type != BAS_TYPE_EMPTY;
|
|
bool hasBefore = argc > 2 && args[2].type != BAS_TYPE_EMPTY;
|
|
bool hasAfter = argc > 3 && args[3].type != BAS_TYPE_EMPTY;
|
|
int32_t pos = coll->itemCount;
|
|
int32_t err = 0;
|
|
|
|
if (hasKey && args[1].type != BAS_TYPE_STRING) {
|
|
return BAS_ERR_TYPE_MISMATCH;
|
|
}
|
|
|
|
if (hasBefore && hasAfter) {
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
if (hasBefore) {
|
|
err = collectionFind(coll, args[2], &pos);
|
|
} else if (hasAfter) {
|
|
err = collectionFind(coll, args[3], &pos);
|
|
pos++;
|
|
}
|
|
|
|
if (err != 0) {
|
|
return err;
|
|
}
|
|
|
|
if (hasKey && collectionKeyPos(coll, args[1].strVal) >= 0) {
|
|
return BAS_ERR_DUPLICATE_KEY;
|
|
}
|
|
|
|
if (coll->itemCount == coll->itemCap) {
|
|
int32_t cap = coll->itemCap ? coll->itemCap * BAS_COLL_GROWTH : BAS_COLL_MIN_CAP;
|
|
BasCollItemT *items = (BasCollItemT *)realloc(coll->items, (size_t)cap * sizeof(BasCollItemT));
|
|
|
|
if (!items) {
|
|
return BAS_ERR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
coll->items = items;
|
|
coll->itemCap = cap;
|
|
}
|
|
|
|
memmove(&coll->items[pos + 1], &coll->items[pos], (size_t)(coll->itemCount - pos) * sizeof(BasCollItemT));
|
|
coll->items[pos].key = hasKey ? basStringRef(args[1].strVal) : NULL;
|
|
coll->items[pos].value = basValCopy(args[0]);
|
|
coll->itemCount++;
|
|
return 0;
|
|
}
|
|
|
|
|
|
// The 0-based position of the item index (1-based) or key (a string,
|
|
// matched without regard to case) names. Returns 0 and sets *outPos,
|
|
// else the BASIC error number.
|
|
static int32_t collectionFind(const BasObjectT *coll, BasValueT index, int32_t *outPos) {
|
|
if (index.type == BAS_TYPE_STRING) {
|
|
int32_t pos = collectionKeyPos(coll, index.strVal);
|
|
|
|
if (pos < 0) {
|
|
return BAS_ERR_ILLEGAL_FUNC_CALL;
|
|
}
|
|
|
|
*outPos = pos;
|
|
return 0;
|
|
}
|
|
|
|
if (!isIntegerType(index.type) && index.type != BAS_TYPE_SINGLE && index.type != BAS_TYPE_DOUBLE) {
|
|
return BAS_ERR_TYPE_MISMATCH;
|
|
}
|
|
|
|
int32_t n = basValToInt32(index);
|
|
|
|
if (n < 1 || n > coll->itemCount) {
|
|
return BAS_ERR_SUBSCRIPT_RANGE;
|
|
}
|
|
|
|
*outPos = n - 1;
|
|
return 0;
|
|
}
|
|
|
|
|
|
// The position of the item with key (case ignored), or -1.
|
|
static int32_t collectionKeyPos(const BasObjectT *coll, const BasStringT *key) {
|
|
for (int32_t i = 0; i < coll->itemCount; i++) {
|
|
if (coll->items[i].key && basStringCompareCI(coll->items[i].key, key) == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
// COMMAND$(n): the whole launch argument string for n = 0, else its nth
|
|
// argument without quotes ("" when there are fewer). An argument is
|
|
// never longer than the whole string, so that sizes the result.
|
|
static BasStringT *commandArg(const char *args, int32_t n) {
|
|
const char *p = args ? args : "";
|
|
int32_t len = (int32_t)strlen(p);
|
|
|
|
if (n == 0) {
|
|
return basStringNew(p, len);
|
|
}
|
|
|
|
BasStringT *s = basStringAlloc(len + 1);
|
|
bool quoted = false;
|
|
|
|
if (s->cap <= len) {
|
|
return s;
|
|
}
|
|
|
|
for (int32_t i = 0; i < n && p; i++) {
|
|
p = dvxNextArg(p, s->data, s->cap, "ed);
|
|
}
|
|
|
|
if (!p) {
|
|
s->data[0] = '\0';
|
|
}
|
|
|
|
s->len = (int32_t)strlen(s->data);
|
|
return s;
|
|
}
|
|
|
|
|
|
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];
|
|
}
|
|
|
|
|
|
static void defaultPrint(void *ctx, const char *text, bool newline) {
|
|
(void)ctx;
|
|
fputs(text, stdout);
|
|
|
|
if (newline) {
|
|
fputc('\n', stdout);
|
|
}
|
|
}
|
|
|
|
|
|
// Turns a ByRef argument into the value it refers to (a host method, a
|
|
// variable member or a Collection takes values).
|
|
static void derefArg(BasValueT *arg) {
|
|
if (arg->type != BAS_TYPE_REF && arg->type != BAS_TYPE_ELEM_REF) {
|
|
return;
|
|
}
|
|
|
|
BasValueT *slot = derefSlot(arg);
|
|
BasValueT v = slot ? basValCopy(*slot) : basValEmpty();
|
|
|
|
basValRelease(arg);
|
|
*arg = v;
|
|
}
|
|
|
|
|
|
// Resolves a variable slot through any ByRef indirection it holds: a plain
|
|
// slot is returned as-is, a BAS_TYPE_REF yields the referenced slot, and a
|
|
// BAS_TYPE_ELEM_REF yields the array element it names (NULL if the index
|
|
// no longer exists in that array).
|
|
static BasValueT *derefSlot(BasValueT *slot) {
|
|
if (slot->type == BAS_TYPE_REF) {
|
|
return slot->refVal;
|
|
}
|
|
|
|
if (slot->type == BAS_TYPE_ELEM_REF) {
|
|
BasArrayT *arr = slot->elemRef.arr;
|
|
|
|
if (!arr || slot->elemRef.idx < 0 || slot->elemRef.idx >= arr->totalElements) {
|
|
return NULL;
|
|
}
|
|
|
|
return &arr->elements[slot->elemRef.idx];
|
|
}
|
|
|
|
return slot;
|
|
}
|
|
|
|
|
|
// Dir$ state lives in BasVmT (vm->dirHandle/dirPattern/dirPath) so that a
|
|
// second embedded VM cannot close another VM's in-progress enumeration.
|
|
static void dirClose(BasVmT *vm) {
|
|
if (vm->dirHandle) {
|
|
closedir((DIR *)vm->dirHandle);
|
|
vm->dirHandle = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// Next Dir$ match. Like QB/VB Dir$ without a directory attribute, only
|
|
// files are returned: ".", ".." and subdirectories are skipped.
|
|
static const char *dirNext(BasVmT *vm) {
|
|
if (!vm->dirHandle) {
|
|
return NULL;
|
|
}
|
|
|
|
struct dirent *ent;
|
|
|
|
while ((ent = readdir((DIR *)vm->dirHandle)) != NULL) {
|
|
if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
|
|
continue;
|
|
}
|
|
|
|
if (fnmatch(vm->dirPattern, ent->d_name, FNM_CASEFOLD) != 0) {
|
|
continue;
|
|
}
|
|
|
|
char full[DVX_MAX_PATH];
|
|
struct stat st;
|
|
|
|
snprintf(full, sizeof(full), "%s/%s", vm->dirPath, ent->d_name);
|
|
|
|
if (stat(full, &st) == 0 && S_ISDIR(st.st_mode)) {
|
|
continue;
|
|
}
|
|
|
|
return ent->d_name;
|
|
}
|
|
|
|
dirClose(vm);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// Releases the top call frame without returning through it: its locals
|
|
// are released, its FOR frames dropped and the caller's statement
|
|
// boundary restored. Used when an error unwinds frames (dispatchError,
|
|
// runSubLoop's failure and shutdown paths); no out-arg copy-back and no
|
|
// error-state change, unlike popCallFrame.
|
|
static void discardFrame(BasVmT *vm) {
|
|
BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1];
|
|
|
|
for (int32_t i = 0; i < frame->localCount; i++) {
|
|
basValRelease(&frame->locals[i]);
|
|
}
|
|
|
|
basValRelease(&frame->me);
|
|
forStackTrim(vm, frame->savedForDepth);
|
|
vm->gosubDepth = frame->savedGosubDepth;
|
|
vm->stmtPc = frame->savedStmtPc;
|
|
vm->stmtSp = frame->savedStmtSp;
|
|
vm->callDepth--;
|
|
}
|
|
|
|
|
|
// ON ERROR dispatch shared by basVmRun and runSubLoop. Given a failing
|
|
// step result, finds the innermost frame (never below floorDepth) whose
|
|
// ON ERROR GOTO handler can trap -- one is installed and is not already
|
|
// running; an error raised inside a running handler passes to the
|
|
// caller's handler, as in VB. When one exists, the frames above it are
|
|
// discarded, the eval stack is truncated to the failing statement's
|
|
// boundary, the frame's RESUME / RESUME NEXT targets are recorded and pc
|
|
// points at the handler; returns true. Returns false, leaving every
|
|
// frame in place for the host and debugger, when the error is untrapped.
|
|
//
|
|
// Without the unwind an error raised inside a called SUB would fire the
|
|
// outer SUB's handler but OP_RET would then pop the inner frame and resume
|
|
// after the call site (effectively RESUME NEXT semantics), which is not
|
|
// what ON ERROR promises.
|
|
static bool dispatchError(BasVmT *vm, BasVmResultE result, int32_t floorDepth, int32_t faultPc) {
|
|
if (result == BAS_VM_HALTED || result == BAS_VM_BAD_OPCODE) {
|
|
return false;
|
|
}
|
|
|
|
// Eval/call stack exhaustion bypasses runtimeError(), so give the
|
|
// handler a real ERR value instead of 0.
|
|
if (result == BAS_VM_STACK_OVERFLOW || result == BAS_VM_STACK_UNDERFLOW || result == BAS_VM_CALL_OVERFLOW) {
|
|
runtimeError(vm, BAS_ERR_OUT_OF_STACK, "Out of stack space");
|
|
}
|
|
|
|
int32_t trapDepth = vm->callDepth;
|
|
|
|
while (trapDepth > floorDepth) {
|
|
const BasCallFrameT *frame = &vm->callStack[trapDepth - 1];
|
|
|
|
if (frame->errorHandler != 0 && !frame->inHandler) {
|
|
break;
|
|
}
|
|
|
|
trapDepth--;
|
|
}
|
|
|
|
if (trapDepth <= floorDepth) {
|
|
return false;
|
|
}
|
|
|
|
// Discard the frames above the handler's. RESUME must land in the
|
|
// handler frame's code, never in a discarded callee's: the resume
|
|
// point becomes the call that led out of the handler frame.
|
|
int32_t resumeStart = faultPc;
|
|
bool unwound = false;
|
|
|
|
while (vm->callDepth > trapDepth) {
|
|
resumeStart = vm->callStack[vm->callDepth - 1].returnPc;
|
|
unwound = true;
|
|
discardFrame(vm);
|
|
}
|
|
|
|
// Release any operands the failed statement half-pushed before the
|
|
// error fired, then truncate the eval stack back to the statement
|
|
// boundary so trapped errors in a loop don't leak values and grow sp
|
|
// without bound.
|
|
while (vm->sp > vm->stmtSp) {
|
|
basValRelease(&vm->stack[--vm->sp]);
|
|
}
|
|
|
|
// The eval stack is now at the failing statement's boundary, so RESUME
|
|
// re-runs that statement from its start and RESUME NEXT continues where
|
|
// nextStatementPc says the statement ends -- never mid-statement.
|
|
BasCallFrameT *frame = &vm->callStack[trapDepth - 1];
|
|
|
|
frame->inHandler = true;
|
|
frame->resumePc = (vm->stmtPc >= 0) ? vm->stmtPc : resumeStart;
|
|
frame->resumeNextPc = nextStatementPc(vm, resumeStart, unwound);
|
|
vm->errorPc = frame->resumePc;
|
|
vm->pc = frame->errorHandler;
|
|
return true;
|
|
}
|
|
|
|
|
|
static BasVmResultE divByZero(BasVmT *vm) {
|
|
runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero");
|
|
return BAS_VM_DIV_BY_ZERO;
|
|
}
|
|
|
|
|
|
// \ and MOD of 32-bit operands (ib != 0) in 32-bit arithmetic: a 64-bit
|
|
// division is a libgcc call on the 486. ib == -1 is handled without
|
|
// dividing because INT32_MIN / -1 traps; that one quotient does not fit
|
|
// 32 bits, so the result comes back widened for the caller to promote.
|
|
static int64_t divMod32(uint8_t op, int32_t ia, int32_t ib) {
|
|
if (ib == -1) {
|
|
return (op == OP_IDIV_INT) ? -(int64_t)ia : 0;
|
|
}
|
|
|
|
return (op == OP_IDIV_INT) ? ia / ib : ia % ib;
|
|
}
|
|
|
|
|
|
// Releases count values taken off the stack.
|
|
static void dropArgs(BasValueT *args, int32_t count) {
|
|
for (int32_t i = 0; i < count; i++) {
|
|
basValRelease(&args[i]);
|
|
}
|
|
}
|
|
|
|
|
|
// Pops and releases up to count values (drains operands on an error path).
|
|
static void dropValues(BasVmT *vm, int32_t count) {
|
|
for (int32_t i = 0; i < count && vm->sp > 0; i++) {
|
|
basValRelease(&vm->stack[--vm->sp]);
|
|
}
|
|
}
|
|
|
|
|
|
// Items FOR EACH walks in group: an array's elements or a Collection's
|
|
// items. Returns 0 and sets *outCount, else the BASIC error number.
|
|
static int32_t eachCount(BasValueT group, int32_t *outCount) {
|
|
if (group.type == BAS_TYPE_ARRAY && group.arrVal) {
|
|
*outCount = group.arrVal->totalElements;
|
|
return 0;
|
|
}
|
|
|
|
if (group.type == BAS_TYPE_INSTANCE && group.instVal && group.instVal->classIdx == BAS_OBJ_COLLECTION) {
|
|
*outCount = group.instVal->itemCount;
|
|
return 0;
|
|
}
|
|
|
|
return basValIsObject(group) ? BAS_ERR_NOT_COLLECTION : BAS_ERR_TYPE_MISMATCH;
|
|
}
|
|
|
|
|
|
// An Empty operand of a binary operator takes part as "" beside a
|
|
// string and as the Integer 0 otherwise.
|
|
static void emptyOperand(BasValueT *v, BasValueT other) {
|
|
if (v->type == BAS_TYPE_EMPTY) {
|
|
*v = (other.type == BAS_TYPE_STRING) ? basValString(NULL) : basValInteger(0);
|
|
}
|
|
}
|
|
|
|
|
|
// Standard message for a runtime error number (ERROR n raises it with the
|
|
// same text the VM uses when it raises that error itself).
|
|
static const char *errorText(int32_t errNum) {
|
|
switch (errNum) {
|
|
case BAS_ERR_NEXT_WITHOUT_FOR:
|
|
return "NEXT without FOR";
|
|
|
|
case BAS_ERR_RETURN_NO_GOSUB:
|
|
return "RETURN without GOSUB";
|
|
|
|
case BAS_ERR_OUT_OF_DATA:
|
|
return "Out of DATA";
|
|
|
|
case BAS_ERR_ILLEGAL_FUNC_CALL:
|
|
return "Illegal function call";
|
|
|
|
case BAS_ERR_OVERFLOW:
|
|
return "Overflow";
|
|
|
|
case BAS_ERR_OUT_OF_MEMORY:
|
|
return "Out of memory";
|
|
|
|
case BAS_ERR_SUBSCRIPT_RANGE:
|
|
return "Subscript out of range";
|
|
|
|
case BAS_ERR_DIV_BY_ZERO:
|
|
return "Division by zero";
|
|
|
|
case BAS_ERR_TYPE_MISMATCH:
|
|
return "Type mismatch";
|
|
|
|
case BAS_ERR_OUT_OF_STRING:
|
|
return "Out of string space";
|
|
|
|
case BAS_ERR_RESUME_WITHOUT_ERR:
|
|
return "RESUME without error";
|
|
|
|
case BAS_ERR_FOR_NESTING:
|
|
return "FOR loop nesting too deep";
|
|
|
|
case BAS_ERR_OUT_OF_STACK:
|
|
return "Out of stack space";
|
|
|
|
case BAS_ERR_BAD_FILE_NUM:
|
|
return "Bad file number";
|
|
|
|
case BAS_ERR_FILE_NOT_FOUND:
|
|
return "File not found";
|
|
|
|
case BAS_ERR_BAD_FILE_MODE:
|
|
return "Bad file mode";
|
|
|
|
case BAS_ERR_DEVICE_IO:
|
|
return "Device I/O error";
|
|
|
|
case BAS_ERR_FILE_EXISTS:
|
|
return "File already exists";
|
|
|
|
case BAS_ERR_BAD_RECORD_LEN:
|
|
return "Bad record length";
|
|
|
|
case BAS_ERR_BAD_RECORD_NUM:
|
|
return "Bad record number";
|
|
|
|
case BAS_ERR_TOO_MANY_FILES:
|
|
return "Too many files";
|
|
|
|
case BAS_ERR_DEVICE_UNAVAILABLE:
|
|
return "Device unavailable";
|
|
|
|
case BAS_ERR_PATH_FILE_ACCESS:
|
|
return "Path/File access error";
|
|
|
|
case BAS_ERR_PATH_NOT_FOUND:
|
|
return "Path not found";
|
|
|
|
case BAS_ERR_OBJECT_NOT_SET:
|
|
return "Object variable not set";
|
|
|
|
case BAS_ERR_INVALID_NULL:
|
|
return "Invalid use of Null";
|
|
|
|
case BAS_ERR_OBJECT_REQUIRED:
|
|
return "Object required";
|
|
|
|
case BAS_ERR_NO_MEMBER:
|
|
return "Object doesn't support this property or method";
|
|
|
|
case BAS_ERR_WRONG_ARGS:
|
|
return "Wrong number of arguments or invalid property assignment";
|
|
|
|
case BAS_ERR_NOT_COLLECTION:
|
|
return "Object not a collection";
|
|
|
|
case BAS_ERR_DUPLICATE_KEY:
|
|
return "This key is already associated with an element of this collection";
|
|
|
|
default:
|
|
return "User-defined error";
|
|
}
|
|
}
|
|
|
|
|
|
static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
|
|
BasValueT a;
|
|
BasValueT b;
|
|
|
|
if (!popPair(vm, &a, &b)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasVmResultE nullRc;
|
|
|
|
if (nullOperands(vm, &a, &b, &nullRc)) {
|
|
return nullRc;
|
|
}
|
|
|
|
// Empty takes part as "" beside a string, else as 0
|
|
emptyOperand(&a, b);
|
|
emptyOperand(&b, a);
|
|
|
|
// VB behavior: + on two strings concatenates
|
|
if (op == OP_ADD_INT && a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) {
|
|
BasStringT *sa = a.strVal ? a.strVal : basEmptyString;
|
|
BasStringT *sb = b.strVal ? b.strVal : basEmptyString;
|
|
BasStringT *cat = basStringConcat(sa, sb);
|
|
|
|
basValRelease(&a);
|
|
basValRelease(&b);
|
|
|
|
if (!push(vm, strValue(cat))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
// Integer-integer fast path -- avoids double conversion and FPU for
|
|
// tight integer loops. Only promotes on overflow/division range.
|
|
if (op != OP_POW && op != OP_DIV_FLT && isIntegerType(a.type) && isIntegerType(b.type)) {
|
|
int32_t ia = intValue(a);
|
|
int32_t ib = intValue(b);
|
|
int64_t wide = 0;
|
|
bool handled = true;
|
|
|
|
switch (op) {
|
|
case OP_ADD_INT:
|
|
wide = (int64_t)ia + (int64_t)ib;
|
|
break;
|
|
|
|
case OP_SUB_INT:
|
|
wide = (int64_t)ia - (int64_t)ib;
|
|
break;
|
|
|
|
case OP_MUL_INT:
|
|
wide = (int64_t)ia * (int64_t)ib;
|
|
break;
|
|
|
|
case OP_IDIV_INT:
|
|
case OP_MOD_INT:
|
|
if (ib == 0) {
|
|
return divByZero(vm);
|
|
}
|
|
|
|
wide = divMod32(op, ia, ib);
|
|
break;
|
|
|
|
default:
|
|
handled = false;
|
|
break;
|
|
}
|
|
|
|
if (handled && wide >= INT32_MIN && wide <= INT32_MAX) {
|
|
if (!push(vm, intResult((int32_t)wide))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
}
|
|
|
|
// Capture operand types BEFORE release so we know whether to keep
|
|
// the result as a float. The parser always emits OP_ADD_INT (the
|
|
// VM promotes based on operand types), so the opcode alone can't
|
|
// tell us whether this is an integer or floating-point op.
|
|
uint8_t aType = a.type;
|
|
uint8_t bType = b.type;
|
|
bool aDate = (aType == BAS_TYPE_DATE);
|
|
bool bDate = (bType == BAS_TYPE_DATE);
|
|
bool hadFloat = (aType == BAS_TYPE_SINGLE || aType == BAS_TYPE_DOUBLE || bType == BAS_TYPE_SINGLE || bType == BAS_TYPE_DOUBLE || aDate || bDate);
|
|
|
|
// A DATE takes part as its instant on the linear time line, so adding
|
|
// days to a date before 12/30/1899 keeps its time of day.
|
|
double na = aDate ? basDateToLinear(a.dblVal) : basValToNumber(a);
|
|
double nb = bDate ? basDateToLinear(b.dblVal) : basValToNumber(b);
|
|
basValRelease(&a);
|
|
basValRelease(&b);
|
|
|
|
double result;
|
|
|
|
switch (op) {
|
|
case OP_ADD_INT:
|
|
result = na + nb;
|
|
break;
|
|
|
|
case OP_SUB_INT:
|
|
result = na - nb;
|
|
break;
|
|
|
|
case OP_MUL_INT:
|
|
result = na * nb;
|
|
break;
|
|
|
|
case OP_IDIV_INT:
|
|
case OP_MOD_INT: {
|
|
// \ and MOD round their operands to LONG first (11.5 \ 2 = 6,
|
|
// 7.5 MOD 2 = 0) and raise Overflow when they do not fit.
|
|
int32_t ia;
|
|
int32_t ib;
|
|
BasVmResultE rc = roundedOperand(vm, basValDouble(na), &ia);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
rc = roundedOperand(vm, basValDouble(nb), &ib);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
if (ib == 0) {
|
|
return divByZero(vm);
|
|
}
|
|
|
|
result = (double)divMod32(op, ia, ib);
|
|
break;
|
|
}
|
|
|
|
case OP_DIV_FLT:
|
|
if (nb == 0.0) {
|
|
return divByZero(vm);
|
|
}
|
|
|
|
result = na / nb;
|
|
break;
|
|
|
|
case OP_POW:
|
|
result = pow(na, nb);
|
|
break;
|
|
|
|
default:
|
|
result = 0.0;
|
|
break;
|
|
}
|
|
|
|
// BASIC never yields infinity or NaN: an out-of-range result is an
|
|
// Overflow, an undefined one (negative base, fractional power) an
|
|
// Illegal function call.
|
|
if (!isfinite(result)) {
|
|
return mathRangeError(vm, result);
|
|
}
|
|
|
|
// Return appropriate type. An op that would normally produce an
|
|
// integer result (OP_ADD_INT, etc.) still has to yield a float when
|
|
// either operand was a float -- otherwise 1.5 + 2.25 truncates to 3.
|
|
bool intOp = (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT);
|
|
|
|
// DATE + n, n + DATE and DATE - n are dates; DATE - DATE is the number
|
|
// of days between them.
|
|
if ((op == OP_ADD_INT && (aDate || bDate)) || (op == OP_SUB_INT && aDate && !bDate)) {
|
|
double serial;
|
|
|
|
if (!basDateFromLinear(result, &serial)) {
|
|
return raiseError(vm, BAS_ERR_OVERFLOW);
|
|
}
|
|
|
|
if (!push(vm, basValDate(serial))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
if (!push(vm, (intOp && !hadFloat) ? integralValue(result) : basValDouble(result))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// OP_BUILTIN: [uint8 fn] [uint8 argc]. Pops the arguments (pushed in
|
|
// source order), fills in omitted optional ones and pushes the result.
|
|
static BasVmResultE execBuiltin(BasVmT *vm) {
|
|
uint8_t fn = readUint8(vm);
|
|
int32_t argc = readUint8(vm);
|
|
int32_t minArgs = 0;
|
|
int32_t maxArgs = 0;
|
|
BasValueT args[BAS_BUILTIN_MAX_ARGS];
|
|
|
|
if (!basBuiltinArity(fn, &minArgs, &maxArgs) || argc < minArgs || argc > maxArgs) {
|
|
dropValues(vm, argc);
|
|
runtimeError(vm, BAS_ERR_BAD_OPCODE, "Bad opcode");
|
|
return BAS_VM_BAD_OPCODE;
|
|
}
|
|
|
|
if (vm->sp < argc) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
// Take ownership of the arguments straight off the stack
|
|
vm->sp -= argc;
|
|
memcpy(args, &vm->stack[vm->sp], (size_t)argc * sizeof(BasValueT));
|
|
argc = builtinDefaults(fn, args, argc);
|
|
|
|
// The date functions are numbered NOW through WEEKDAYNAME, the program
|
|
// screen's BAS_FN_SCREEN_FIRST through _LAST.
|
|
BasValueT result = basValInteger(0);
|
|
int32_t err = 0;
|
|
|
|
if (fn >= BAS_FN_NOW && fn <= BAS_FN_WEEKDAYNAME) {
|
|
err = builtinDate(fn, args, &result);
|
|
} else if (fn >= BAS_FN_OBJECT_FIRST && fn <= BAS_FN_OBJECT_LAST) {
|
|
err = builtinObject(vm, fn, args, &result);
|
|
} else if (fn >= BAS_FN_SCREEN_FIRST && fn <= BAS_FN_SCREEN_LAST) {
|
|
err = basScreenExec(vm, fn, args, argc, &result);
|
|
} else {
|
|
err = builtinString(vm, fn, args, argc, &result);
|
|
}
|
|
|
|
for (int32_t i = 0; i < argc; i++) {
|
|
basValRelease(&args[i]);
|
|
}
|
|
|
|
if (err != 0) {
|
|
return raiseError(vm, err);
|
|
}
|
|
|
|
if (!push(vm, result)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// OP_CALL / OP_CALL_ME: [uint16 addr] [uint8 argc] [uint8 baseSlot]. The
|
|
// arguments move from the stack into the callee's locals; keepMe hands
|
|
// the caller's Me on (a class calling one of its own procedures).
|
|
static BasVmResultE execCall(BasVmT *vm, bool keepMe) {
|
|
uint16_t addr = readUint16(vm);
|
|
uint8_t argc = readUint8(vm);
|
|
uint8_t baseSlot = readUint8(vm);
|
|
|
|
// Guard the locals window: baseSlot and argc come straight from
|
|
// bytecode, and a corrupt module could otherwise make the arg copy
|
|
// below write past locals[]. Drain the pushed args first so the eval
|
|
// stack stays balanced.
|
|
if ((int32_t)baseSlot + (int32_t)argc > BAS_VM_MAX_LOCALS) {
|
|
dropValues(vm, argc);
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "CALL argument slots out of range");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE) {
|
|
return BAS_VM_CALL_OVERFLOW;
|
|
}
|
|
|
|
BasValueT me = keepMe ? basValCopy(currentFrame(vm)->me) : basValEmpty();
|
|
BasCallFrameT *frame = pushFrame(vm, vm->pc, addr, baseSlot + argc);
|
|
|
|
frame->me = me;
|
|
|
|
// Pop arguments into locals starting at baseSlot (in reverse order). A
|
|
// BYVAL TYPE argument must be the callee's own copy.
|
|
for (int32_t i = baseSlot + argc - 1; i >= baseSlot; i--) {
|
|
if (!pop(vm, &frame->locals[i])) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
frame->locals[i] = udtDetach(frame->locals[i]);
|
|
}
|
|
|
|
// The callee starts with no handler; if it raises an error the
|
|
// dispatcher will unwind to the nearest frame with one.
|
|
vm->pc = addr;
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
static BasVmResultE execCompare(BasVmT *vm, uint8_t op) {
|
|
BasValueT a;
|
|
BasValueT b;
|
|
|
|
if (!popPair(vm, &a, &b)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasVmResultE nullRc;
|
|
|
|
if (nullOperands(vm, &a, &b, &nullRc)) {
|
|
return nullRc;
|
|
}
|
|
|
|
int32_t cmp;
|
|
|
|
// Same-type integer fast path: loop conditions like i < n hit this on
|
|
// every iteration. Skip basValCompare's mixed-type handling.
|
|
if (a.type == BAS_TYPE_INTEGER && b.type == BAS_TYPE_INTEGER) {
|
|
cmp = (a.intVal < b.intVal) ? -1 : (a.intVal > b.intVal ? 1 : 0);
|
|
} else if (a.type == BAS_TYPE_LONG && b.type == BAS_TYPE_LONG) {
|
|
cmp = (a.longVal < b.longVal) ? -1 : (a.longVal > b.longVal ? 1 : 0);
|
|
} else {
|
|
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;
|
|
}
|
|
|
|
if (!push(vm, basValBool(result))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// The conversion opcodes (CINT, CLNG, CSNG, CDBL, CSTR, CDATE, CBOOL and
|
|
// the conversions stores into typed variables make), on the value on top
|
|
// of the stack.
|
|
static BasVmResultE execConvert(BasVmT *vm, uint8_t op) {
|
|
if (vm->sp < 1) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasValueT *top = &vm->stack[vm->sp - 1];
|
|
BasValueT conv = *top;
|
|
int32_t n = 0;
|
|
|
|
switch (op) {
|
|
case OP_CONV_INT_FLT:
|
|
conv = basValSingle((float)basValToNumber(*top));
|
|
break;
|
|
|
|
case OP_CONV_FLT_INT:
|
|
case OP_CONV_STR_INT:
|
|
if (!basValRoundToInt32(*top, INT16_MIN, INT16_MAX, &n)) {
|
|
return raiseError(vm, BAS_ERR_OVERFLOW);
|
|
}
|
|
|
|
conv = basValInteger((int16_t)n);
|
|
break;
|
|
|
|
case OP_CONV_INT_STR:
|
|
conv = basValToString(*top);
|
|
break;
|
|
|
|
case OP_CONV_STR_FLT:
|
|
conv = basValToDouble(*top);
|
|
break;
|
|
|
|
case OP_CONV_INT_LONG:
|
|
if (!basValRoundToInt32(*top, INT32_MIN, INT32_MAX, &n)) {
|
|
return raiseError(vm, BAS_ERR_OVERFLOW);
|
|
}
|
|
|
|
conv = basValLong(n);
|
|
break;
|
|
|
|
case OP_CONV_DATE: {
|
|
double serial = 0.0;
|
|
int32_t err = basValToDateSerial(*top, &serial);
|
|
|
|
if (err != 0) {
|
|
return raiseError(vm, err);
|
|
}
|
|
|
|
conv = basValDate(serial);
|
|
break;
|
|
}
|
|
|
|
case OP_CONV_BOOL:
|
|
conv = basValBool((top->type == BAS_TYPE_STRING) ? (top->strVal && top->strVal->len > 0) : (basValToNumber(*top) != 0.0));
|
|
break;
|
|
|
|
default:
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
basValRelease(top);
|
|
*top = conv;
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// BEEP, SOUND, PLAY, PLAY(n), STICK, STRIG and FRAMEWAIT through the
|
|
// host's device callbacks (BasDeviceCallbacksT), with QBasic's argument
|
|
// ranges. The statements then wait, keeping the host's events running,
|
|
// for as long as the host says.
|
|
static BasVmResultE execDevice(BasVmT *vm, uint8_t op) {
|
|
const BasDeviceCallbacksT *d = &vm->device;
|
|
BasValueT a;
|
|
BasValueT b;
|
|
|
|
switch (op) {
|
|
case OP_BEEP:
|
|
if (d->beep) {
|
|
d->beep(d->ctx);
|
|
}
|
|
|
|
break;
|
|
|
|
case OP_SOUND: {
|
|
if (!popArgs(vm, 2, &b, &a)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
double hz = basValToNumber(a);
|
|
double ticks = basValToNumber(b);
|
|
|
|
basValRelease(&a);
|
|
basValRelease(&b);
|
|
|
|
if (hz < BAS_SOUND_HZ_MIN || hz > BAS_SOUND_HZ_MAX || ticks < 0.0 || ticks > BAS_SOUND_TICKS_MAX) {
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
if (d->sound) {
|
|
d->sound(d->ctx, hz, ticks);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_PLAY: {
|
|
if (!popStringArg(vm, &a)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
bool ok = !d->play || d->play(d->ctx, a.strVal->data);
|
|
|
|
basValRelease(&a);
|
|
|
|
if (!ok) {
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call (a mistake in the PLAY string)");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FRAME_WAIT: {
|
|
if (!pop(vm, &a)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t hz = basValToInt32(a);
|
|
|
|
basValRelease(&a);
|
|
|
|
if (hz < 1) {
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
if (!d->frameStart || !d->frameBusy) {
|
|
runtimeError(vm, BAS_ERR_DEVICE_UNAVAILABLE, errorText(BAS_ERR_DEVICE_UNAVAILABLE));
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
d->frameStart(d->ctx, hz);
|
|
|
|
if (!waitWhile(vm, d->frameBusy)) {
|
|
vm->running = false;
|
|
return BAS_VM_HALTED;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
default: {
|
|
// PLAY(n), STICK(n), STRIG(n): one argument, one result
|
|
if (!pop(vm, &a)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t n = basValToInt32(a);
|
|
int32_t result = 0;
|
|
|
|
basValRelease(&a);
|
|
|
|
if ((op == OP_STICK && (n < 0 || n >= BAS_STICK_COUNT)) || (op == OP_STRIG && (n < 0 || n >= BAS_STRIG_COUNT))) {
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
if (op == OP_PLAY_COUNT && d->playCount) {
|
|
result = d->playCount(d->ctx);
|
|
} else if (op == OP_STICK && d->stick) {
|
|
result = d->stick(d->ctx, n);
|
|
} else if (op == OP_STRIG && d->strig) {
|
|
result = d->strig(d->ctx, n);
|
|
}
|
|
|
|
return push(vm, basValLong(result)) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
}
|
|
|
|
// BEEP, SOUND and PLAY wait while the music is in the foreground.
|
|
if (d->soundBusy && !waitWhile(vm, d->soundBusy)) {
|
|
vm->running = false;
|
|
return BAS_VM_HALTED;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
|
|
switch (op) {
|
|
case OP_FILE_OPEN: {
|
|
uint8_t mode = readUint8(vm);
|
|
BasValueT recLenVal;
|
|
|
|
if (!pop(vm, &recLenVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t recLen = basValToInt32(recLenVal);
|
|
basValRelease(&recLenVal);
|
|
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, false);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
// popFileChannel consumed only the channel; the filename
|
|
// operand below it must come off too or the untrapped error
|
|
// path strands it on the eval stack. A channel underflow
|
|
// means the stack was empty, so this drop is a no-op there.
|
|
dropValues(vm, 1);
|
|
return chResult;
|
|
}
|
|
|
|
BasValueT fnStr;
|
|
|
|
if (!popStringArg(vm, &fnStr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (recLen < 1 || recLen > BAS_VM_MAX_RECORD_LEN) {
|
|
basValRelease(&fnStr);
|
|
runtimeError(vm, BAS_ERR_BAD_RECORD_LEN, "Bad record length");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
const char *modeStr;
|
|
|
|
switch (mode) {
|
|
case BAS_FILE_MODE_INPUT:
|
|
modeStr = "rb";
|
|
break;
|
|
|
|
case BAS_FILE_MODE_OUTPUT:
|
|
modeStr = "wb";
|
|
break;
|
|
|
|
case BAS_FILE_MODE_APPEND:
|
|
modeStr = "ab";
|
|
break;
|
|
|
|
case BAS_FILE_MODE_RANDOM:
|
|
case BAS_FILE_MODE_BINARY:
|
|
modeStr = "r+b";
|
|
break;
|
|
|
|
default:
|
|
basValRelease(&fnStr);
|
|
runtimeError(vm, BAS_ERR_BAD_FILE_MODE, "Bad file mode");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
int32_t port;
|
|
|
|
if (printerDevice(fnStr.strVal->data, &port)) {
|
|
basValRelease(&fnStr);
|
|
return openPrinterChannel(vm, channel, port, mode, recLen);
|
|
}
|
|
|
|
// A valid OPEN replaces whatever the channel had open
|
|
closeChannel(vm, channel);
|
|
|
|
// For RANDOM/BINARY: create file if it doesn't exist, then reopen r+b
|
|
if (mode == BAS_FILE_MODE_RANDOM || mode == BAS_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);
|
|
}
|
|
}
|
|
|
|
FILE *fp = fopen(fnStr.strVal->data, modeStr);
|
|
basValRelease(&fnStr);
|
|
|
|
if (!fp) {
|
|
runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found or cannot open");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
vm->files[channel].handle = fp;
|
|
vm->files[channel].mode = mode;
|
|
vm->files[channel].recLen = recLen;
|
|
break;
|
|
}
|
|
|
|
case OP_FILE_CLOSE: {
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, false);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
return chResult;
|
|
}
|
|
|
|
if (!closeChannel(vm, channel)) {
|
|
runtimeError(vm, BAS_ERR_DEVICE_IO, errorText(BAS_ERR_DEVICE_IO));
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FILE_PRINT: {
|
|
// Writes one value without a trailing newline. The parser emits
|
|
// OP_FILE_WRITE_NL separately when the PRINT statement finishes
|
|
// without a trailing ; so multi-value PRINT with `;` separator
|
|
// works correctly. Values are formatted exactly as PRINT does.
|
|
BasValueT val;
|
|
|
|
if (!pop(vm, &val)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, true);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
basValRelease(&val);
|
|
return chResult;
|
|
}
|
|
|
|
BasStringT *s = basValPrintString(val);
|
|
basValRelease(&val);
|
|
fwrite(s->data, 1, s->len, (FILE *)vm->files[channel].handle);
|
|
basStringUnref(s);
|
|
break;
|
|
}
|
|
|
|
case OP_FILE_INPUT:
|
|
case OP_FILE_LINE_INPUT: {
|
|
// INPUT # reads one comma-delimited, optionally quoted field;
|
|
// LINE INPUT # reads the whole line. Neither has a length cap.
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, true);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
return chResult;
|
|
}
|
|
|
|
FILE *fp = (FILE *)vm->files[channel].handle;
|
|
BasStringT *s = (op == OP_FILE_INPUT) ? readField(fp) : readLine(fp);
|
|
|
|
if (!push(vm, strValue(s))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FILE_EOF: {
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, true);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
return chResult;
|
|
}
|
|
|
|
FILE *fp = (FILE *)vm->files[channel].handle;
|
|
bool isEof;
|
|
|
|
if (vm->files[channel].mode == BAS_FILE_MODE_INPUT) {
|
|
// Peek one byte: no seek, so the stdio read buffer survives
|
|
// the usual DO WHILE NOT EOF(n) loop.
|
|
int32_t c = fgetc(fp);
|
|
|
|
isEof = (c == EOF);
|
|
|
|
if (!isEof) {
|
|
ungetc(c, fp);
|
|
}
|
|
} else {
|
|
// Read/write channels: a read-ahead would need a seek before
|
|
// the next write, so compare against the end instead.
|
|
long curPos = ftell(fp);
|
|
|
|
fseek(fp, 0, SEEK_END);
|
|
|
|
long endPos = ftell(fp);
|
|
|
|
fseek(fp, curPos, SEEK_SET);
|
|
isEof = (curPos >= endPos);
|
|
}
|
|
|
|
if (!push(vm, basValBool(isEof))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FILE_WRITE: {
|
|
// Pop value and channel, write value in WRITE format
|
|
BasValueT val;
|
|
|
|
if (!pop(vm, &val)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, true);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
basValRelease(&val);
|
|
return chResult;
|
|
}
|
|
|
|
FILE *fp = (FILE *)vm->files[channel].handle;
|
|
|
|
// Numbers: no leading space (unlike PRINT). Strings and
|
|
// dates: enclosed in quotes, so INPUT # reads a date back whole.
|
|
BasStringT *s = basValFormatString(val);
|
|
bool quoted = (val.type == BAS_TYPE_STRING || val.type == BAS_TYPE_DATE);
|
|
|
|
if (quoted) {
|
|
fputc('"', fp);
|
|
}
|
|
|
|
fwrite(s->data, 1, s->len, fp);
|
|
|
|
if (quoted) {
|
|
fputc('"', fp);
|
|
}
|
|
|
|
basStringUnref(s);
|
|
|
|
basValRelease(&val);
|
|
break;
|
|
}
|
|
|
|
case OP_FILE_WRITE_SEP:
|
|
case OP_FILE_WRITE_NL: {
|
|
// Pop channel, write the comma separator or record newline
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, true);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
return chResult;
|
|
}
|
|
|
|
fputc((op == OP_FILE_WRITE_SEP) ? ',' : '\n', (FILE *)vm->files[channel].handle);
|
|
break;
|
|
}
|
|
|
|
case OP_FILE_GET: {
|
|
// Pop type, [current string value | udt + layout], recno,
|
|
// channel; read data; push value. A STRING target carries
|
|
// its current value so BINARY mode knows how many bytes to
|
|
// read; a TYPE target carries the instance to fill in place
|
|
// and the compiler's field layout.
|
|
BasValueT typeVal;
|
|
|
|
if (!pop(vm, &typeVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t dataType = basValToInt32(typeVal);
|
|
int32_t strLen = 0;
|
|
BasValueT udtVal;
|
|
BasValueT layoutVal;
|
|
|
|
basValRelease(&typeVal);
|
|
memset(&udtVal, 0, sizeof(udtVal));
|
|
memset(&layoutVal, 0, sizeof(layoutVal));
|
|
|
|
if (dataType == BAS_TYPE_STRING) {
|
|
BasValueT curVal;
|
|
|
|
if (!pop(vm, &curVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
strLen = (curVal.type == BAS_TYPE_STRING && curVal.strVal) ? curVal.strVal->len : 0;
|
|
basValRelease(&curVal);
|
|
} else if (dataType == BAS_TYPE_UDT) {
|
|
if (!popArgs(vm, 2, &layoutVal, &udtVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (layoutVal.type != BAS_TYPE_STRING || !layoutVal.strVal || udtVal.type != BAS_TYPE_UDT || !udtVal.udtVal) {
|
|
basValRelease(&layoutVal);
|
|
basValRelease(&udtVal);
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not a TYPE instance");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
}
|
|
|
|
BasValueT recnoVal;
|
|
|
|
if (!pop(vm, &recnoVal)) {
|
|
basValRelease(&layoutVal);
|
|
basValRelease(&udtVal);
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t recno = basValToInt32(recnoVal);
|
|
basValRelease(&recnoVal);
|
|
|
|
int32_t channel;
|
|
long start = 0;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, true);
|
|
|
|
if (chResult == BAS_VM_OK) {
|
|
chResult = recordBegin(vm, channel, recno, &start);
|
|
}
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
basValRelease(&layoutVal);
|
|
basValRelease(&udtVal);
|
|
return chResult;
|
|
}
|
|
|
|
FILE *fp = (FILE *)vm->files[channel].handle;
|
|
BasValueT result;
|
|
|
|
switch (dataType) {
|
|
case BAS_TYPE_INTEGER:
|
|
case BAS_TYPE_LONG:
|
|
case BAS_TYPE_SINGLE:
|
|
case BAS_TYPE_DOUBLE:
|
|
case BAS_TYPE_BOOLEAN:
|
|
result = ioReadScalar(fp, (uint8_t)dataType);
|
|
break;
|
|
|
|
case BAS_TYPE_STRING:
|
|
// BINARY reads as many raw bytes as the variable
|
|
// already holds (QB semantics); RANDOM records carry
|
|
// an int16 length prefix written by PUT.
|
|
if (vm->files[channel].mode == BAS_FILE_MODE_BINARY) {
|
|
result = strValue(ioReadBytes(fp, strLen, false));
|
|
} else {
|
|
result = strValue(ioReadPrefixedString(fp));
|
|
}
|
|
|
|
break;
|
|
|
|
case BAS_TYPE_UDT: {
|
|
int32_t pos = 0;
|
|
BasVmResultE rc = udtGet(vm, fp, udtVal.udtVal, layoutVal.strVal, &pos, 0);
|
|
|
|
basValRelease(&layoutVal);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
basValRelease(&udtVal);
|
|
return rc;
|
|
}
|
|
|
|
result = udtVal;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
result = basValInteger(0);
|
|
break;
|
|
}
|
|
|
|
BasVmResultE endResult = recordEnd(vm, channel, start, false);
|
|
|
|
if (endResult != BAS_VM_OK) {
|
|
basValRelease(&result);
|
|
return endResult;
|
|
}
|
|
|
|
if (!push(vm, result)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FILE_PUT:
|
|
case OP_FILE_PUT_UDT: {
|
|
// PUT: pop value, recno, channel. PUT_UDT: pop layout, udt,
|
|
// recno, channel. The record is measured first so a value
|
|
// longer than a RANDOM record raises error 59 before a single
|
|
// byte is written over the next record.
|
|
BasValueT layoutVal;
|
|
BasValueT val;
|
|
BasValueT recnoVal;
|
|
|
|
memset(&layoutVal, 0, sizeof(layoutVal));
|
|
|
|
if (op == OP_FILE_PUT_UDT && !pop(vm, &layoutVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (!popArgs(vm, 2, &val, &recnoVal)) {
|
|
basValRelease(&layoutVal);
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t recno = basValToInt32(recnoVal);
|
|
basValRelease(&recnoVal);
|
|
|
|
int32_t channel;
|
|
BasVmResultE rc = popFileChannel(vm, &channel, true);
|
|
|
|
if (rc == BAS_VM_OK && op == OP_FILE_PUT_UDT && (layoutVal.type != BAS_TYPE_STRING || !layoutVal.strVal || val.type != BAS_TYPE_UDT || !val.udtVal)) {
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not a TYPE instance");
|
|
rc = BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
BasPutSinkT measure = { NULL, 0 };
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
rc = putValue(vm, &measure, vm->files[channel].mode, val, layoutVal.strVal);
|
|
}
|
|
|
|
if (rc == BAS_VM_OK && vm->files[channel].mode == BAS_FILE_MODE_RANDOM && measure.bytes > vm->files[channel].recLen) {
|
|
runtimeError(vm, BAS_ERR_BAD_RECORD_LEN, "Record longer than LEN");
|
|
rc = BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
long start = 0;
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
rc = recordBegin(vm, channel, recno, &start);
|
|
}
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
BasPutSinkT out = { (FILE *)vm->files[channel].handle, 0 };
|
|
|
|
putValue(vm, &out, vm->files[channel].mode, val, layoutVal.strVal);
|
|
rc = recordEnd(vm, channel, start, true);
|
|
}
|
|
|
|
basValRelease(&layoutVal);
|
|
basValRelease(&val);
|
|
return rc;
|
|
}
|
|
|
|
case OP_FILE_SEEK: {
|
|
// SEEK #n, pos: a record number on RANDOM channels, a byte
|
|
// position otherwise; both are 1-based.
|
|
BasValueT posVal;
|
|
|
|
if (!pop(vm, &posVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t pos = basValToInt32(posVal);
|
|
basValRelease(&posVal);
|
|
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, true);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
return chResult;
|
|
}
|
|
|
|
if (pos < 1) {
|
|
runtimeError(vm, BAS_ERR_BAD_RECORD_NUM, "Bad record number");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
return seekRecord(vm, channel, pos);
|
|
}
|
|
|
|
case OP_FILE_LOF: {
|
|
// Pop channel, push file length
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, true);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
return chResult;
|
|
}
|
|
|
|
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_SEEK_GET: {
|
|
// SEEK(n): the 1-based record number the next GET/PUT uses on a
|
|
// RANDOM channel, the 1-based byte position otherwise.
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, true);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
return chResult;
|
|
}
|
|
|
|
long pos = ftell((FILE *)vm->files[channel].handle);
|
|
|
|
if (vm->files[channel].mode == BAS_FILE_MODE_RANDOM) {
|
|
pos /= vm->files[channel].recLen;
|
|
}
|
|
|
|
if (!push(vm, basValLong((int32_t)(pos + 1)))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FILE_LOC: {
|
|
// Pop channel, push LOC
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, true);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
return chResult;
|
|
}
|
|
|
|
// QB: BINARY reports the last byte read or written, RANDOM the
|
|
// last record number, sequential modes the 128-byte block count.
|
|
long pos = ftell((FILE *)vm->files[channel].handle);
|
|
int32_t loc;
|
|
|
|
if (vm->files[channel].mode == BAS_FILE_MODE_BINARY) {
|
|
loc = (int32_t)pos;
|
|
} else if (vm->files[channel].mode == BAS_FILE_MODE_RANDOM) {
|
|
loc = (int32_t)(pos / vm->files[channel].recLen);
|
|
} else {
|
|
loc = (int32_t)(pos / BAS_LOC_SEQ_BLOCK);
|
|
}
|
|
|
|
if (!push(vm, basValLong(loc))) {
|
|
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, BAS_ERR_TOO_MANY_FILES, "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
|
|
int32_t channel;
|
|
BasVmResultE chResult = popFileChannel(vm, &channel, true);
|
|
|
|
if (chResult != BAS_VM_OK) {
|
|
// Same shape as OP_FILE_OPEN: the count operand below the
|
|
// channel must be drained on the error path.
|
|
dropValues(vm, 1);
|
|
return chResult;
|
|
}
|
|
|
|
BasValueT nVal;
|
|
|
|
if (!pop(vm, &nVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t n = basValToInt32(nVal);
|
|
basValRelease(&nVal);
|
|
|
|
if (n < 0 || n > BAS_STRING_FUNC_MAX) {
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
if (!push(vm, strValue(ioReadBytes((FILE *)vm->files[channel].handle, n, false)))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
return BAS_VM_BAD_OPCODE;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
|
|
switch (op) {
|
|
case OP_FS_KILL: {
|
|
BasValueT fnStr;
|
|
|
|
if (!popStringArg(vm, &fnStr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t rc = remove(fnStr.strVal->data);
|
|
basValRelease(&fnStr);
|
|
|
|
if (rc != 0) {
|
|
runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FS_NAME: {
|
|
BasValueT newStr;
|
|
BasValueT oldStr;
|
|
|
|
if (!popStringArg(vm, &newStr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (!popStringArg(vm, &oldStr)) {
|
|
basValRelease(&newStr);
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t rc = rename(oldStr.strVal->data, newStr.strVal->data);
|
|
basValRelease(&oldStr);
|
|
basValRelease(&newStr);
|
|
|
|
if (rc != 0) {
|
|
runtimeError(vm, BAS_ERR_FILE_EXISTS, "File already exists or rename failed");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FS_FILECOPY: {
|
|
BasValueT dstStr;
|
|
BasValueT srcStr;
|
|
|
|
if (!popStringArg(vm, &dstStr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (!popStringArg(vm, &srcStr)) {
|
|
basValRelease(&dstStr);
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
bool copied = platformCopyFile(srcStr.strVal->data, dstStr.strVal->data);
|
|
|
|
basValRelease(&srcStr);
|
|
basValRelease(&dstStr);
|
|
|
|
if (!copied) {
|
|
runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found or cannot create destination");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FS_MKDIR: {
|
|
BasValueT pathStr;
|
|
|
|
if (!popStringArg(vm, &pathStr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
int32_t rc = mkdir(pathStr.strVal->data);
|
|
#else
|
|
int32_t rc = mkdir(pathStr.strVal->data, BAS_MKDIR_MODE);
|
|
#endif
|
|
|
|
basValRelease(&pathStr);
|
|
|
|
if (rc != 0) {
|
|
runtimeError(vm, BAS_ERR_PATH_FILE_ACCESS, "Path/File access error");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FS_RMDIR: {
|
|
BasValueT pathStr;
|
|
|
|
if (!popStringArg(vm, &pathStr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t rc = rmdir(pathStr.strVal->data);
|
|
basValRelease(&pathStr);
|
|
|
|
if (rc != 0) {
|
|
runtimeError(vm, BAS_ERR_PATH_FILE_ACCESS, "Path/File access error");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FS_CHDIR: {
|
|
BasValueT pathStr;
|
|
|
|
if (!popStringArg(vm, &pathStr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t rc = platformChdir(pathStr.strVal->data);
|
|
basValRelease(&pathStr);
|
|
|
|
if (rc != 0) {
|
|
runtimeError(vm, BAS_ERR_PATH_NOT_FOUND, "Path not found");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FS_CHDRIVE: {
|
|
BasValueT driveStr;
|
|
|
|
if (!popStringArg(vm, &driveStr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
// Drive letters are a DOS-ism; platformChdir is a no-op on
|
|
// platforms without them.
|
|
if (driveStr.strVal->data[0]) {
|
|
char drivePath[4];
|
|
drivePath[0] = driveStr.strVal->data[0];
|
|
drivePath[1] = ':';
|
|
drivePath[2] = '\0';
|
|
platformChdir(drivePath);
|
|
}
|
|
|
|
basValRelease(&driveStr);
|
|
break;
|
|
}
|
|
|
|
case OP_FS_CURDIR: {
|
|
char cwd[DVX_MAX_PATH];
|
|
|
|
if (!getcwd(cwd, sizeof(cwd))) {
|
|
cwd[0] = '\0';
|
|
}
|
|
|
|
if (!push(vm, basValStringFromC(cwd))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FS_DIR: {
|
|
BasValueT patStr;
|
|
|
|
if (!popStringArg(vm, &patStr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
dirClose(vm);
|
|
|
|
// 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(vm->dirPath)) {
|
|
dirLen = (int32_t)sizeof(vm->dirPath) - 1;
|
|
}
|
|
|
|
memcpy(vm->dirPath, pat, dirLen);
|
|
vm->dirPath[dirLen] = '\0';
|
|
snprintf(vm->dirPattern, sizeof(vm->dirPattern), "%s", lastSep + 1);
|
|
} else {
|
|
snprintf(vm->dirPath, sizeof(vm->dirPath), ".");
|
|
snprintf(vm->dirPattern, sizeof(vm->dirPattern), "%s", pat);
|
|
}
|
|
|
|
basValRelease(&patStr);
|
|
|
|
if (vm->dirPattern[0] == '\0') {
|
|
snprintf(vm->dirPattern, sizeof(vm->dirPattern), "*");
|
|
}
|
|
|
|
vm->dirHandle = opendir(vm->dirPath);
|
|
|
|
if (!push(vm, basValStringFromC(dirNext(vm)))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FS_DIR_NEXT: {
|
|
if (!push(vm, basValStringFromC(dirNext(vm)))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FS_FILELEN: {
|
|
BasValueT fnStr;
|
|
|
|
if (!popStringArg(vm, &fnStr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
struct stat st;
|
|
int32_t rc = stat(fnStr.strVal->data, &st);
|
|
|
|
basValRelease(&fnStr);
|
|
|
|
if (rc != 0) {
|
|
runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
if (!push(vm, basValLong((int32_t)st.st_size))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FS_GETATTR: {
|
|
BasValueT fnStr;
|
|
|
|
if (!popStringArg(vm, &fnStr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
struct stat st;
|
|
int32_t attrs = 0;
|
|
int32_t rc = stat(fnStr.strVal->data, &st);
|
|
|
|
basValRelease(&fnStr);
|
|
|
|
if (rc != 0) {
|
|
runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
if (S_ISDIR(st.st_mode)) {
|
|
attrs |= BAS_ATTR_DIRECTORY;
|
|
}
|
|
|
|
if (!(st.st_mode & S_IWUSR)) {
|
|
attrs |= BAS_ATTR_READONLY;
|
|
}
|
|
|
|
if (!push(vm, basValInteger((int16_t)attrs))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case OP_FS_SETATTR: {
|
|
BasValueT attrVal;
|
|
BasValueT fnStr;
|
|
|
|
if (!pop(vm, &attrVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (!popStringArg(vm, &fnStr)) {
|
|
basValRelease(&attrVal);
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t attrs = basValToInt32(attrVal);
|
|
basValRelease(&attrVal);
|
|
|
|
struct stat st;
|
|
|
|
if (stat(fnStr.strVal->data, &st) != 0) {
|
|
basValRelease(&fnStr);
|
|
runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
mode_t mode = st.st_mode;
|
|
|
|
if (attrs & BAS_ATTR_READONLY) {
|
|
mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH);
|
|
} else {
|
|
mode |= S_IWUSR;
|
|
}
|
|
|
|
int32_t rc = chmod(fnStr.strVal->data, mode);
|
|
|
|
basValRelease(&fnStr);
|
|
|
|
if (rc != 0) {
|
|
runtimeError(vm, BAS_ERR_PATH_FILE_ACCESS, "Path/File access error");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
return BAS_VM_BAD_OPCODE;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
static BasVmResultE execLogical(BasVmT *vm, uint8_t op) {
|
|
// Bitwise operators work on LONG operands: round (never truncate) so
|
|
// NOT 0.6 is NOT 1 = -2, and raise Overflow when a value does not fit.
|
|
if (op == OP_NOT) {
|
|
if (vm->sp < 1) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasValueT *top = &vm->stack[vm->sp - 1];
|
|
int32_t n;
|
|
|
|
// NOT Null is Null
|
|
if (top->type == BAS_TYPE_NULL) {
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
BasVmResultE rc = roundedOperand(vm, *top, &n);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
basValRelease(top);
|
|
int32_t r = ~n;
|
|
|
|
// Promote to LONG when the result exceeds INTEGER range, matching
|
|
// execArith -- truncation corrupts LONG masks like NOT &H10000.
|
|
*top = intResult(r);
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
BasValueT a;
|
|
BasValueT b;
|
|
|
|
if (!popPair(vm, &a, &b)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (a.type == BAS_TYPE_NULL || b.type == BAS_TYPE_NULL) {
|
|
return logicalNull(vm, op, a, b);
|
|
}
|
|
|
|
int32_t na;
|
|
int32_t nb;
|
|
BasVmResultE rc = roundedOperand(vm, a, &na);
|
|
|
|
if (rc == BAS_VM_OK) {
|
|
rc = roundedOperand(vm, b, &nb);
|
|
}
|
|
|
|
basValRelease(&a);
|
|
basValRelease(&b);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Promote to LONG when the result exceeds INTEGER range, matching
|
|
// execArith -- truncation corrupts LONG masks like c& AND &HFF0000.
|
|
if (!push(vm, intResult(result))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// LPRINT statement start (open the program's print job on first use)
|
|
// and end (report a failed or cancelled output; the job is closed so
|
|
// the next LPRINT starts a new one).
|
|
static BasVmResultE execLprint(BasVmT *vm, uint8_t phase) {
|
|
if (phase == BAS_LPRINT_BEGIN) {
|
|
if (!vm->lprintJob && vm->printer.open) {
|
|
vm->lprintJob = vm->printer.open(vm->printer.ctx, BAS_PRINTER_DEFAULT);
|
|
}
|
|
|
|
if (!vm->lprintJob) {
|
|
runtimeError(vm, BAS_ERR_DEVICE_UNAVAILABLE, errorText(BAS_ERR_DEVICE_UNAVAILABLE));
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
vm->lprinting = true;
|
|
vm->lprintFailed = false;
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
vm->lprinting = false;
|
|
|
|
if (vm->lprintFailed) {
|
|
vm->printer.close(vm->printer.ctx, vm->lprintJob);
|
|
vm->lprintJob = NULL;
|
|
vm->lprintColumn = 0;
|
|
runtimeError(vm, BAS_ERR_DEVICE_IO, errorText(BAS_ERR_DEVICE_IO));
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
static BasVmResultE execMath(BasVmT *vm, uint8_t op) {
|
|
if (op == OP_MATH_RND) {
|
|
// QBasic RND(n): n > 0 the next number, n = 0 the last one again,
|
|
// n < 0 reseeds from n first (the same n always gives the same
|
|
// number). The result is a DOUBLE in [0, 1).
|
|
BasValueT argVal;
|
|
|
|
if (!pop(vm, &argVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
double n = basValToNumber(argVal);
|
|
basValRelease(&argVal);
|
|
|
|
if (n < 0) {
|
|
seedRandom(n * BAS_RND_SEED_SPREAD);
|
|
}
|
|
|
|
if (n != 0) {
|
|
vm->rndLast = (double)rand() / BAS_RND_SCALE;
|
|
}
|
|
|
|
if (!push(vm, basValDouble(vm->rndLast))) {
|
|
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 == BAS_RANDOMIZE_TIMER_SEED) {
|
|
srand((unsigned int)time(NULL));
|
|
} else {
|
|
seedRandom(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];
|
|
|
|
// ABS, INT, FIX and SGN of Null are Null
|
|
if (top->type == BAS_TYPE_NULL && (op == OP_MATH_ABS || op == OP_MATH_INT || op == OP_MATH_FIX || op == OP_MATH_SGN)) {
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
double n = basValToNumber(*top);
|
|
double result;
|
|
|
|
// Domain errors are Illegal function call in BASIC, never NaN.
|
|
if ((op == OP_MATH_SQR && n < 0) || (op == OP_MATH_LOG && n <= 0)) {
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (!isfinite(result)) {
|
|
return mathRangeError(vm, result);
|
|
}
|
|
|
|
basValRelease(top);
|
|
|
|
// INT, FIX and SGN return the INTEGER type the compiler's builtin
|
|
// table declares, widening to LONG / DOUBLE only when the value does
|
|
// not fit; the other functions return DOUBLE.
|
|
if (op == OP_MATH_INT || op == OP_MATH_FIX || op == OP_MATH_SGN) {
|
|
*top = integralValue(result);
|
|
} else {
|
|
*top = basValDouble(result);
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
static BasVmResultE execPrint(BasVmT *vm) {
|
|
BasValueT val;
|
|
|
|
if (!pop(vm, &val)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasStringT *s = basValPrintString(val);
|
|
basValRelease(&val);
|
|
printText(vm, s->data, false);
|
|
basStringUnref(s);
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// OP_STR_APPEND: var = var + x (or var & x). The variable is not on the
|
|
// eval stack, so when it holds the only reference to its string the value
|
|
// is appended in place; otherwise the result is computed exactly as
|
|
// OP_ADD_INT / OP_STR_CONCAT would and stored back.
|
|
static BasVmResultE execStrAppend(BasVmT *vm) {
|
|
uint8_t flags = readUint8(vm);
|
|
uint8_t scopeTag = readUint8(vm);
|
|
uint16_t varIdx = readUint16(vm);
|
|
bool isAmp = (flags & BAS_APPEND_AMP) != 0;
|
|
BasValueT b;
|
|
|
|
if (!pop(vm, &b)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasValueT *slot = scopedVarSlot(vm, varIdx, scopeTag);
|
|
|
|
if (!slot) {
|
|
basValRelease(&b);
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
if (slot->type == BAS_TYPE_STRING && slot->strVal && (isAmp || b.type == BAS_TYPE_STRING)) {
|
|
BasValueT sb = basValToString(b);
|
|
|
|
basValRelease(&b);
|
|
|
|
if (basStringAppend(&slot->strVal, sb.strVal)) {
|
|
basValRelease(&sb);
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
b = sb;
|
|
}
|
|
|
|
if (!push(vm, basValCopy(*slot))) {
|
|
basValRelease(&b);
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
if (!push(vm, b)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
BasVmResultE rc = isAmp ? execStringOp(vm, OP_STR_CONCAT) : execArith(vm, OP_ADD_INT);
|
|
BasValueT result;
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
if (!pop(vm, &result)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
// The operators never run BASIC code, so slot is still valid.
|
|
storeValue(slot, result);
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
|
|
switch (op) {
|
|
case OP_STR_CONCAT: {
|
|
BasValueT sb;
|
|
BasValueT sa;
|
|
|
|
// Null & x is x; Null & Null stays Null
|
|
if (vm->sp >= 2 && vm->stack[vm->sp - 1].type == BAS_TYPE_NULL && vm->stack[vm->sp - 2].type == BAS_TYPE_NULL) {
|
|
vm->sp--;
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
for (int32_t i = 1; i <= 2 && i <= vm->sp; i++) {
|
|
if (vm->stack[vm->sp - i].type == BAS_TYPE_NULL) {
|
|
vm->stack[vm->sp - i] = basValString(NULL);
|
|
}
|
|
}
|
|
|
|
if (!popStringArg(vm, &sb)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (!popStringArg(vm, &sa)) {
|
|
basValRelease(&sb);
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasStringT *result = basStringConcat(sa.strVal, sb.strVal);
|
|
basValRelease(&sa);
|
|
basValRelease(&sb);
|
|
|
|
if (!push(vm, strValue(result))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
case OP_STR_LEFT:
|
|
case OP_STR_RIGHT: {
|
|
BasValueT nVal;
|
|
BasValueT sv;
|
|
|
|
if (!pop(vm, &nVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (!popStringArg(vm, &sv)) {
|
|
basValRelease(&nVal);
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t n = basValToInt32(nVal);
|
|
basValRelease(&nVal);
|
|
|
|
if (n < 0) {
|
|
basValRelease(&sv);
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
int32_t start = 0;
|
|
|
|
if (op == OP_STR_RIGHT) {
|
|
start = sv.strVal->len - n;
|
|
|
|
if (start < 0) {
|
|
start = 0;
|
|
}
|
|
}
|
|
|
|
BasStringT *result = basStringSub(sv.strVal, start, n);
|
|
basValRelease(&sv);
|
|
|
|
if (!push(vm, strValue(result))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
case OP_STR_MID:
|
|
case OP_STR_MID2: {
|
|
// MID$(s$, start[, len]); a missing len means "to the end".
|
|
BasValueT lenVal;
|
|
BasValueT startVal;
|
|
BasValueT sv;
|
|
int32_t len = -1;
|
|
|
|
if (op == OP_STR_MID) {
|
|
if (!pop(vm, &lenVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
len = basValToInt32(lenVal);
|
|
basValRelease(&lenVal);
|
|
}
|
|
|
|
if (!pop(vm, &startVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t start = basValToInt32(startVal);
|
|
basValRelease(&startVal);
|
|
|
|
if (!popStringArg(vm, &sv)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (start < 1 || (op == OP_STR_MID && len < 0)) {
|
|
basValRelease(&sv);
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
BasStringT *result = basStringSub(sv.strVal, start - 1, len);
|
|
basValRelease(&sv);
|
|
|
|
if (!push(vm, strValue(result))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
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->len;
|
|
basValRelease(&sv);
|
|
basValRelease(top);
|
|
*top = basValLong(len);
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
case OP_STR_INSTR:
|
|
case OP_STR_INSTR3: {
|
|
// INSTR([start,] string, find); positions are 1-based, LONG.
|
|
BasValueT fv;
|
|
BasValueT sv;
|
|
BasValueT startVal;
|
|
int32_t startPos = 0;
|
|
|
|
if (!popStringArg(vm, &fv)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (!popStringArg(vm, &sv)) {
|
|
basValRelease(&fv);
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (op == OP_STR_INSTR3) {
|
|
if (!pop(vm, &startVal)) {
|
|
basValRelease(&fv);
|
|
basValRelease(&sv);
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
startPos = basValToInt32(startVal) - 1;
|
|
basValRelease(&startVal);
|
|
|
|
if (startPos < 0) {
|
|
basValRelease(&fv);
|
|
basValRelease(&sv);
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
}
|
|
|
|
int32_t pos = 0;
|
|
|
|
// OPTION COMPARE TEXT makes the search case-insensitive, as it
|
|
// does for the comparison operators.
|
|
if (startPos < sv.strVal->len) {
|
|
int32_t found = stringFind(sv.strVal, startPos, fv.strVal, vm->compareTextMode);
|
|
|
|
if (found >= 0) {
|
|
pos = found + 1;
|
|
}
|
|
}
|
|
|
|
basValRelease(&sv);
|
|
basValRelease(&fv);
|
|
|
|
if (!push(vm, basValLong(pos))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
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] -= ('a' - 'A');
|
|
} else if (op == OP_STR_LCASE && result->data[i] >= 'A' && result->data[i] <= 'Z') {
|
|
result->data[i] += ('a' - 'A');
|
|
}
|
|
}
|
|
} 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 = strValue(result);
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
case OP_STR_CHR: {
|
|
if (vm->sp < 1) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
// A one-byte string, length-sized so CHR$(0) is a real NUL
|
|
// character rather than an empty string.
|
|
BasValueT *top = &vm->stack[vm->sp - 1];
|
|
char ch = (char)(basValToInt32(*top) & BAS_CHAR_MASK);
|
|
basValRelease(top);
|
|
*top = strValue(basStringNew(&ch, 1));
|
|
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->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 = basValToInt32(*top);
|
|
|
|
if (n < 0 || n > BAS_STRING_FUNC_MAX) {
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
basValRelease(top);
|
|
*top = strValue(fillString(' ', n));
|
|
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);
|
|
|
|
if (result->cap >= (int32_t)fixLen + 1) {
|
|
int32_t copyLen = DVX_MIN(src->len, (int32_t)fixLen);
|
|
|
|
result->len = fixLen;
|
|
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 = strValue(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 (!popArgs(vm, 4, &replVal, &lenVal, &startVal, &strVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
BasValueT sv = basValToString(strVal);
|
|
BasValueT rv = basValToString(replVal);
|
|
int32_t start = basValToInt32(startVal) - 1; // 1-based to 0-based
|
|
int32_t len = basValToInt32(lenVal);
|
|
basValRelease(&strVal);
|
|
basValRelease(&startVal);
|
|
basValRelease(&lenVal);
|
|
basValRelease(&replVal);
|
|
|
|
if (start < 0 || (len < 0 && len != BAS_MID_LEN_OMITTED)) {
|
|
basValRelease(&sv);
|
|
basValRelease(&rv);
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
BasStringT *src = sv.strVal;
|
|
BasStringT *repl = rv.strVal;
|
|
int32_t srcLen = src->len;
|
|
int32_t replLen = repl->len;
|
|
|
|
// An omitted length replaces as much of the replacement as
|
|
// fits; an explicit 0 replaces nothing.
|
|
if (len == BAS_MID_LEN_OMITTED) {
|
|
len = replLen;
|
|
}
|
|
|
|
// Clamp to available replacement length
|
|
if (len > replLen) {
|
|
len = replLen;
|
|
}
|
|
|
|
// Create a copy of the original string
|
|
BasStringT *result = basStringNew(src->data, srcLen);
|
|
|
|
// Replace characters
|
|
if (start < srcLen && len > 0) {
|
|
int32_t maxReplace = srcLen - start;
|
|
|
|
if (len > maxReplace) {
|
|
len = maxReplace;
|
|
}
|
|
|
|
memcpy(result->data + start, repl->data, len);
|
|
}
|
|
|
|
basValRelease(&sv);
|
|
basValRelease(&rv);
|
|
|
|
if (!push(vm, strValue(result))) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
default:
|
|
return BAS_VM_BAD_OPCODE;
|
|
}
|
|
}
|
|
|
|
|
|
// A new string of count copies of ch.
|
|
static BasStringT *fillString(char ch, int32_t count) {
|
|
BasStringT *s = basStringAlloc(count + 1);
|
|
|
|
if (s->cap >= count + 1) {
|
|
memset(s->data, ch, count);
|
|
s->data[count] = '\0';
|
|
s->len = count;
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
|
|
// FILTER: the elements of arr (as strings) that contain match when keep
|
|
// is set, or that do not when it is clear. Returns how many there are;
|
|
// with out non-NULL they are also stored into out's elements.
|
|
static int32_t filterScan(const BasArrayT *arr, const BasStringT *match, bool keep, bool ignoreCase, BasArrayT *out) {
|
|
int32_t n = 0;
|
|
|
|
for (int32_t i = 0; i < arr->totalElements; i++) {
|
|
BasValueT sv = basValToString(arr->elements[i]);
|
|
|
|
if ((stringFind(sv.strVal, 0, match, ignoreCase) >= 0) == keep) {
|
|
if (out) {
|
|
basValRelease(&out->elements[n]);
|
|
out->elements[n] = basValCopy(sv);
|
|
}
|
|
|
|
n++;
|
|
}
|
|
|
|
basValRelease(&sv);
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
|
|
// The public member of cls that a read (BAS_MEMBER_OP_GET), Let or Set
|
|
// named name reaches (name NULL: the default member). A read takes a
|
|
// variable, Property Get, Function or Sub; a Let a variable or Property
|
|
// Let, else Property Set; a Set a variable or Property Set, else Property
|
|
// Let. NULL when there is none.
|
|
static const BasClassMemberT *findMember(const BasClassInfoT *cls, const char *name, int32_t op) {
|
|
const BasClassMemberT *best = NULL;
|
|
int32_t bestRank = BAS_MEMBER_NO_RANK;
|
|
|
|
for (int32_t i = 0; i < cls->memberCount; i++) {
|
|
const BasClassMemberT *m = &cls->members[i];
|
|
bool match = name ? strcasecmp(m->name, name) == 0 : (m->flags & BAS_MEMBER_DEFAULT) != 0;
|
|
int32_t rank = match ? memberRank(m->kind, op) : BAS_MEMBER_NO_RANK;
|
|
|
|
if (rank < bestRank) {
|
|
best = m;
|
|
bestRank = rank;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
|
|
// Shared numeric formatter for OP_PRINT_USING (numeric fields) and OP_FORMAT.
|
|
// Renders n per the flags in f, right-aligned in f->width: the sign (or a
|
|
// trailing sign), the floating dollar, the thousands-separated integer part
|
|
// zero-extended to f->minIntDigits, the decimals and the trailing sign make
|
|
// up the body; the remaining positions are filled with spaces or asterisks.
|
|
// Every cursor write is bounded against outSize, and every intermediate
|
|
// buffer is sized to BAS_FORMAT_BUF_SIZE and clamped, so a pathological
|
|
// format string or numeric magnitude cannot overflow the caller's buffer.
|
|
// The value a FOR counter of varType holds after an increment to v. The
|
|
// counter keeps its type: SINGLE stays SINGLE and INTEGER/LONG widen to
|
|
// LONG/DOUBLE only when v leaves their range, so the value read back next
|
|
// iteration matches the one the continue test used (an INTEGER counter
|
|
// crossing 32767 must not wrap). An integer counter stepped by a
|
|
// fractional STEP becomes DOUBLE so it can progress (FOR a = 0 TO 6.3
|
|
// STEP 0.2 with a holding the INTEGER 0).
|
|
static BasValueT forCounterValue(uint8_t varType, bool stepFractional, double v) {
|
|
if (varType == BAS_TYPE_SINGLE) {
|
|
return basValSingle((float)v);
|
|
}
|
|
|
|
if (varType == BAS_TYPE_INTEGER && !stepFractional) {
|
|
return integralValue(v);
|
|
}
|
|
|
|
if (varType == BAS_TYPE_LONG && !stepFractional && v >= (double)INT32_MIN && v <= (double)INT32_MAX) {
|
|
return basValLong((int32_t)v);
|
|
}
|
|
|
|
return basValDouble(v);
|
|
}
|
|
|
|
|
|
static void formatNumber(double n, const BasNumFormatT *f, char *out, size_t outSize) {
|
|
bool isNeg = (n < 0);
|
|
double absN = isNeg ? -n : n;
|
|
int32_t decimals = f->hasDecimal ? f->digitsAfter : 0;
|
|
int32_t limit = (int32_t)outSize - 1;
|
|
int32_t idx = 0;
|
|
char numBuf[BAS_FORMAT_BUF_SIZE];
|
|
|
|
// A magnitude or decimal count too wide for the work buffer cannot be
|
|
// laid out digit by digit: fall back to scientific notation (behind
|
|
// the '%' overflow mark for PRINT USING) rather than emit a truncated,
|
|
// wrong digit string.
|
|
if (snprintf(numBuf, sizeof(numBuf), "%.*f", (int)decimals, absN) >= (int)sizeof(numBuf)) {
|
|
char mark[2] = { f->markOverflow ? BAS_FORMAT_OVERFLOW_MARK : '\0', '\0' };
|
|
|
|
snprintf(out, outSize, "%s%E", mark, n);
|
|
return;
|
|
}
|
|
|
|
// Split into integer and decimal parts.
|
|
char intPart[BAS_FORMAT_BUF_SIZE];
|
|
char decPart[BAS_FORMAT_BUF_SIZE];
|
|
char *dot = strchr(numBuf, '.');
|
|
|
|
intPart[0] = '\0';
|
|
decPart[0] = '\0';
|
|
|
|
if (dot) {
|
|
int32_t intLen = (int32_t)(dot - numBuf);
|
|
|
|
if (intLen > (int32_t)sizeof(intPart) - 1) {
|
|
intLen = (int32_t)sizeof(intPart) - 1;
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
// Zero-extend to the '0' placeholders, then apply the thousands separator.
|
|
char fmtIntPart[BAS_FORMAT_BUF_SIZE];
|
|
int32_t intLen = (int32_t)strlen(intPart);
|
|
int32_t zeroPad = f->minIntDigits - intLen;
|
|
int32_t dstIdx = 0;
|
|
|
|
if (zeroPad < 0) {
|
|
zeroPad = 0;
|
|
}
|
|
|
|
for (int32_t i = 0; i < zeroPad + intLen; i++) {
|
|
if (f->hasComma && i > 0 && (zeroPad + intLen - i) % 3 == 0) {
|
|
putBounded(fmtIntPart, &dstIdx, (int32_t)sizeof(fmtIntPart) - 1, ',');
|
|
}
|
|
|
|
putBounded(fmtIntPart, &dstIdx, (int32_t)sizeof(fmtIntPart) - 1, (i < zeroPad) ? '0' : intPart[i - zeroPad]);
|
|
}
|
|
|
|
fmtIntPart[dstIdx] = '\0';
|
|
|
|
// Sign placement: a leading '+' position always shows the sign; a
|
|
// trailing '+' or '-' position carries it at the end; otherwise a
|
|
// negative value gets a floating leading '-'.
|
|
char lead = '\0';
|
|
char trail = '\0';
|
|
|
|
if (f->plusAtStart) {
|
|
lead = isNeg ? '-' : '+';
|
|
} else if (f->plusAtEnd) {
|
|
trail = isNeg ? '-' : '+';
|
|
} else if (f->minusAtEnd) {
|
|
trail = isNeg ? '-' : ' ';
|
|
} else if (isNeg) {
|
|
lead = '-';
|
|
}
|
|
|
|
int32_t decLen = (int32_t)strlen(decPart);
|
|
int32_t bodyLen = (lead ? 1 : 0) + (f->dollarFloat ? 1 : 0) + dstIdx + (f->hasDecimal ? 1 + decimals : 0) + (trail ? 1 : 0);
|
|
int32_t pad = f->width - bodyLen;
|
|
|
|
if (pad < 0) {
|
|
pad = 0;
|
|
|
|
if (f->markOverflow) {
|
|
putBounded(out, &idx, limit, BAS_FORMAT_OVERFLOW_MARK);
|
|
}
|
|
}
|
|
|
|
for (int32_t i = 0; i < pad; i++) {
|
|
putBounded(out, &idx, limit, f->asteriskFill ? '*' : ' ');
|
|
}
|
|
|
|
if (lead) {
|
|
putBounded(out, &idx, limit, lead);
|
|
}
|
|
|
|
if (f->dollarFloat) {
|
|
putBounded(out, &idx, limit, '$');
|
|
}
|
|
|
|
for (int32_t i = 0; fmtIntPart[i]; i++) {
|
|
putBounded(out, &idx, limit, fmtIntPart[i]);
|
|
}
|
|
|
|
// decimals comes from the runtime format string, so it can exceed the
|
|
// digits snprintf produced into decPart -- never read past decLen.
|
|
if (f->hasDecimal) {
|
|
putBounded(out, &idx, limit, '.');
|
|
|
|
for (int32_t i = 0; i < decimals; i++) {
|
|
putBounded(out, &idx, limit, (i < decLen) ? decPart[i] : '0');
|
|
}
|
|
}
|
|
|
|
if (trail) {
|
|
putBounded(out, &idx, limit, trail);
|
|
}
|
|
|
|
out[idx] = '\0';
|
|
}
|
|
|
|
|
|
// Lowest forStack index the currently-executing call frame may unwind
|
|
// to. FOR frames below this belong to callers and must never be
|
|
// touched by NEXT mismatch recovery or FOR re-entry cleanup.
|
|
static int32_t forStackFloor(BasVmT *vm) {
|
|
if (vm->callDepth > 0) {
|
|
return vm->callStack[vm->callDepth - 1].savedForDepth;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
// Release FOR-stack entries above newDepth and truncate the stack to it.
|
|
// Used when returning from a call frame, unwinding to an error handler,
|
|
// and discarding stale frames left by EXIT DO / GOTO jumps out of FORs.
|
|
static void forStackTrim(BasVmT *vm, int32_t newDepth) {
|
|
while (vm->forDepth > newDepth) {
|
|
BasForStateT *fs = &vm->forStack[--vm->forDepth];
|
|
basValRelease(&fs->limit);
|
|
basValRelease(&fs->step);
|
|
}
|
|
}
|
|
|
|
|
|
// Flat index of args[0..argc) in arr. Returns 0 and sets *outIdx, else
|
|
// the BASIC error number.
|
|
static int32_t indexArray(const BasArrayT *arr, BasValueT *args, int32_t argc, int32_t *outIdx) {
|
|
int32_t indices[BAS_ARRAY_MAX_DIMS];
|
|
|
|
if (argc > BAS_ARRAY_MAX_DIMS) {
|
|
return BAS_ERR_SUBSCRIPT_RANGE;
|
|
}
|
|
|
|
for (int32_t i = 0; i < argc; i++) {
|
|
derefArg(&args[i]);
|
|
indices[i] = basValToInt32(args[i]);
|
|
}
|
|
|
|
*outIdx = basArrayIndex((BasArrayT *)arr, indices, argc);
|
|
return (*outIdx < 0) ? BAS_ERR_SUBSCRIPT_RANGE : 0;
|
|
}
|
|
|
|
|
|
// IniRead: the value of key in [sec] of path through dvxPrefs, or a
|
|
// copy of defStr when the file, section or key is missing.
|
|
static BasValueT iniRead(const char *path, const char *sec, const char *key, BasStringT *defStr) {
|
|
PrefsHandleT *h = prefsLoad(path);
|
|
const char *value = prefsGetString(h, sec, key, NULL);
|
|
BasValueT result = value ? basValStringFromC(value) : basValString(defStr);
|
|
|
|
prefsClose(h);
|
|
return result;
|
|
}
|
|
|
|
|
|
// IniWrite: sets key = value in [sec] of path through dvxPrefs. Every
|
|
// other line is written back unchanged (lines end in CR LF); a missing
|
|
// key is added after its section's last key and a missing section at the
|
|
// end of the file. A file that exists but cannot be read is never
|
|
// rewritten, and a failed save leaves the old file intact (error 75).
|
|
static BasVmResultE iniWrite(BasVmT *vm, const char *path, const char *sec, const char *key, const char *val) {
|
|
PrefsHandleT *h = prefsLoad(path);
|
|
|
|
if (!h) {
|
|
runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory");
|
|
return BAS_VM_OUT_OF_MEMORY;
|
|
}
|
|
|
|
prefsSetString(h, sec, key, val);
|
|
|
|
bool saved = prefsSave(h);
|
|
|
|
prefsClose(h);
|
|
|
|
if (!saved) {
|
|
runtimeError(vm, BAS_ERR_PATH_FILE_ACCESS, "Cannot write INI file");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// A whole-number result as the narrowest of INTEGER, LONG and DOUBLE that
|
|
// holds it (the promotion rule execArith uses).
|
|
static BasValueT integralValue(double v) {
|
|
if (v >= (double)INT32_MIN && v <= (double)INT32_MAX) {
|
|
return intResult((int32_t)v);
|
|
}
|
|
|
|
return basValDouble(v);
|
|
}
|
|
|
|
|
|
// An integer result as INTEGER when it fits, LONG otherwise.
|
|
static BasValueT intResult(int32_t v) {
|
|
return (v >= INT16_MIN && v <= INT16_MAX) ? basValInteger((int16_t)v) : basValLong(v);
|
|
}
|
|
|
|
|
|
// Integer value of an INTEGER or LONG (callers check isIntegerType).
|
|
static int32_t intValue(BasValueT v) {
|
|
return (v.type == BAS_TYPE_INTEGER) ? (int32_t)v.intVal : v.longVal;
|
|
}
|
|
|
|
|
|
// Reads up to len raw bytes into a new string sized by the bytes actually
|
|
// read, so a short record never exposes uninitialized heap. With pad set
|
|
// a short read is space-filled to len instead: a fixed-length TYPE field
|
|
// always holds exactly its declared length.
|
|
static BasStringT *ioReadBytes(FILE *fp, int32_t len, bool pad) {
|
|
BasStringT *s = basStringAlloc(len + 1);
|
|
|
|
if (s->cap < len + 1) {
|
|
return s;
|
|
}
|
|
|
|
int32_t got = (int32_t)fread(s->data, 1, len, fp);
|
|
|
|
if (pad) {
|
|
memset(s->data + got, ' ', len - got);
|
|
got = len;
|
|
}
|
|
|
|
s->data[got] = '\0';
|
|
s->len = got;
|
|
return s;
|
|
}
|
|
|
|
|
|
// GET of a variable-length string as PUT wrote it: an int16 length
|
|
// prefix, then the bytes.
|
|
static BasStringT *ioReadPrefixedString(FILE *fp) {
|
|
int16_t prefix = 0;
|
|
|
|
readRaw(fp, &prefix, sizeof(prefix));
|
|
return ioReadBytes(fp, (prefix < 0) ? 0 : prefix, false);
|
|
}
|
|
|
|
|
|
// GET of one numeric value in its binary form (INTEGER and BOOLEAN 2
|
|
// bytes, LONG 4, SINGLE 4, DOUBLE 8). Shared by scalar GET and TYPE fields.
|
|
static BasValueT ioReadScalar(FILE *fp, uint8_t type) {
|
|
switch (type) {
|
|
case BAS_TYPE_LONG: {
|
|
int32_t val = 0;
|
|
readRaw(fp, &val, sizeof(val));
|
|
return basValLong(val);
|
|
}
|
|
|
|
case BAS_TYPE_SINGLE: {
|
|
float val = 0.0f;
|
|
readRaw(fp, &val, sizeof(val));
|
|
return basValSingle(val);
|
|
}
|
|
|
|
case BAS_TYPE_DOUBLE:
|
|
case BAS_TYPE_DATE: {
|
|
double val = 0.0;
|
|
readRaw(fp, &val, sizeof(val));
|
|
return (type == BAS_TYPE_DATE) ? basValDate(val) : basValDouble(val);
|
|
}
|
|
|
|
case BAS_TYPE_BOOLEAN: {
|
|
int16_t val = 0;
|
|
readRaw(fp, &val, sizeof(val));
|
|
return basValBool(val != 0);
|
|
}
|
|
|
|
default: {
|
|
int16_t val = 0;
|
|
readRaw(fp, &val, sizeof(val));
|
|
return basValInteger(val);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// PUT counterpart of ioReadPrefixedString. The length is clamped before
|
|
// the int16 cast so an over-long string can't wrap to a negative prefix.
|
|
static void ioWritePrefixedString(BasPutSinkT *sink, const BasStringT *s) {
|
|
int32_t slen = s ? s->len : 0;
|
|
|
|
if (slen > INT16_MAX) {
|
|
slen = INT16_MAX;
|
|
}
|
|
|
|
int16_t len = (int16_t)slen;
|
|
|
|
sinkWrite(sink, &len, sizeof(len));
|
|
sinkWrite(sink, s ? s->data : "", len);
|
|
}
|
|
|
|
|
|
// PUT counterpart of ioReadScalar; val is converted to the layout type.
|
|
static void ioWriteScalar(BasPutSinkT *sink, uint8_t type, BasValueT val) {
|
|
switch (type) {
|
|
case BAS_TYPE_LONG: {
|
|
int32_t v = basValToInt32(val);
|
|
sinkWrite(sink, &v, sizeof(v));
|
|
break;
|
|
}
|
|
|
|
case BAS_TYPE_SINGLE: {
|
|
float v = (float)basValToNumber(val);
|
|
sinkWrite(sink, &v, sizeof(v));
|
|
break;
|
|
}
|
|
|
|
case BAS_TYPE_DOUBLE:
|
|
case BAS_TYPE_DATE: {
|
|
double v = basValToNumber(val);
|
|
sinkWrite(sink, &v, sizeof(v));
|
|
break;
|
|
}
|
|
|
|
case BAS_TYPE_BOOLEAN: {
|
|
int16_t v = basValIsTruthy(val) ? -1 : 0;
|
|
sinkWrite(sink, &v, sizeof(v));
|
|
break;
|
|
}
|
|
|
|
default: {
|
|
int16_t v = (int16_t)basValToInt32(val);
|
|
sinkWrite(sink, &v, sizeof(v));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// INTEGER or LONG: the types the integer fast paths handle.
|
|
static bool isIntegerType(uint8_t type) {
|
|
return type == BAS_TYPE_INTEGER || type == BAS_TYPE_LONG;
|
|
}
|
|
|
|
|
|
// A one-dimensional array (what JOIN$ and FILTER take).
|
|
static bool isOneDimArray(BasValueT v) {
|
|
return v.type == BAS_TYPE_ARRAY && v.arrVal && v.arrVal->dims == 1;
|
|
}
|
|
|
|
|
|
// JOIN$: the elements of arr as strings with delim between them. Returns
|
|
// the length; with out non-NULL the text is also written there (not
|
|
// terminated).
|
|
static int32_t joinScan(const BasArrayT *arr, const BasStringT *delim, char *out) {
|
|
int32_t len = 0;
|
|
|
|
for (int32_t i = 0; i < arr->totalElements; i++) {
|
|
BasValueT sv = basValToString(arr->elements[i]);
|
|
|
|
if (i > 0) {
|
|
if (out) {
|
|
memcpy(out + len, delim->data, delim->len);
|
|
}
|
|
|
|
len += delim->len;
|
|
}
|
|
|
|
if (out) {
|
|
memcpy(out + len, sv.strVal->data, sv.strVal->len);
|
|
}
|
|
|
|
len += sv.strVal->len;
|
|
basValRelease(&sv);
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
|
|
// Reads the next TYPE layout entry (see BAS_LAYOUT_* in opcodes.h).
|
|
// Returns false when the layout is exhausted or truncated.
|
|
static bool layoutNext(const BasStringT *layout, int32_t *pos, BasLayoutEntryT *out) {
|
|
if (*pos >= layout->len) {
|
|
return false;
|
|
}
|
|
|
|
const uint8_t *d = (const uint8_t *)layout->data;
|
|
|
|
memset(out, 0, sizeof(*out));
|
|
out->type = d[*pos];
|
|
*pos += 1;
|
|
|
|
if (out->type == BAS_TYPE_STRING) {
|
|
if (*pos + 2 > layout->len) {
|
|
return false;
|
|
}
|
|
|
|
out->fixedLen = d[*pos] | (d[*pos + 1] << 8);
|
|
*pos += 2;
|
|
} else if (out->type == BAS_TYPE_UDT) {
|
|
if (*pos + 4 > layout->len) {
|
|
return false;
|
|
}
|
|
|
|
out->typeId = d[*pos] | (d[*pos + 1] << 8);
|
|
out->fieldCount = d[*pos + 2] | (d[*pos + 3] << 8);
|
|
*pos += 4;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Local wall-clock time, with the sub-second part in microseconds when
|
|
// outUsec is non-NULL. Shared by TIMER, DATE$ and TIME$.
|
|
static void localNow(struct tm *out, int32_t *outUsec) {
|
|
struct timeval tv;
|
|
|
|
gettimeofday(&tv, NULL);
|
|
|
|
time_t secs = tv.tv_sec;
|
|
struct tm *local = localtime(&secs);
|
|
|
|
if (local) {
|
|
*out = *local;
|
|
} else {
|
|
memset(out, 0, sizeof(*out));
|
|
}
|
|
|
|
if (outUsec) {
|
|
*outUsec = (int32_t)tv.tv_usec;
|
|
}
|
|
}
|
|
|
|
|
|
// AND / OR / XOR / EQV / IMP with a Null operand, as VB: False AND Null is
|
|
// False, True OR Null is True, False IMP Null and Null IMP True are True;
|
|
// everything else is Null. Takes over both operands.
|
|
static BasVmResultE logicalNull(BasVmT *vm, uint8_t op, BasValueT a, BasValueT b) {
|
|
bool aNull = (a.type == BAS_TYPE_NULL);
|
|
bool bNull = (b.type == BAS_TYPE_NULL);
|
|
int32_t other = 0;
|
|
BasValueT result = basValNull();
|
|
|
|
if (!(aNull && bNull) && !basValRoundToInt32(aNull ? b : a, INT32_MIN, INT32_MAX, &other)) {
|
|
basValRelease(&a);
|
|
basValRelease(&b);
|
|
return raiseError(vm, BAS_ERR_OVERFLOW);
|
|
}
|
|
|
|
bool known = !(aNull && bNull);
|
|
|
|
if (known && op == OP_AND && other == 0) {
|
|
result = intResult(0);
|
|
} else if (known && op == OP_OR && other == BAS_TRUE_BITS) {
|
|
result = intResult(BAS_TRUE_BITS);
|
|
} else if (known && op == OP_IMP && ((bNull && other == 0) || (aNull && other == BAS_TRUE_BITS))) {
|
|
result = intResult(BAS_TRUE_BITS);
|
|
}
|
|
|
|
basValRelease(&a);
|
|
basValRelease(&b);
|
|
return push(vm, result) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
|
|
// Send LPRINT output; after a failure the rest of the statement is dropped.
|
|
static void lprintWrite(BasVmT *vm, const char *data, int32_t len) {
|
|
if (!vm->lprintFailed && len > 0) {
|
|
vm->lprintFailed = !vm->printer.write(vm->printer.ctx, vm->lprintJob, data, len);
|
|
}
|
|
}
|
|
|
|
|
|
// Raises the error for a math result BASIC cannot represent: Overflow for
|
|
// an infinity, Illegal function call for NaN (an undefined result).
|
|
static BasVmResultE mathRangeError(BasVmT *vm, double result) {
|
|
if (isnan(result)) {
|
|
runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
|
|
} else {
|
|
runtimeError(vm, BAS_ERR_OVERFLOW, "Overflow");
|
|
}
|
|
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
|
|
// How well a member of kind serves an access op: 0 as intended, 1 as a
|
|
// fallback (Let through Property Set and back), BAS_MEMBER_NO_RANK not.
|
|
static int32_t memberRank(uint8_t kind, int32_t op) {
|
|
switch (kind) {
|
|
case BAS_MEMBER_VAR:
|
|
return 0;
|
|
|
|
case BAS_MEMBER_GET:
|
|
case BAS_MEMBER_FUNCTION:
|
|
case BAS_MEMBER_SUB:
|
|
return (op == BAS_MEMBER_OP_GET) ? 0 : BAS_MEMBER_NO_RANK;
|
|
|
|
case BAS_MEMBER_LET:
|
|
return (op == BAS_MEMBER_OP_LET) ? 0 : ((op == BAS_MEMBER_OP_SET) ? 1 : BAS_MEMBER_NO_RANK);
|
|
|
|
case BAS_MEMBER_SET:
|
|
return (op == BAS_MEMBER_OP_SET) ? 0 : ((op == BAS_MEMBER_OP_LET) ? 1 : BAS_MEMBER_NO_RANK);
|
|
|
|
default:
|
|
return BAS_MEMBER_NO_RANK;
|
|
}
|
|
}
|
|
|
|
|
|
// Member variable idx of the running procedure's Me. Raises the error
|
|
// and returns NULL outside a class instance.
|
|
static BasValueT *memberSlot(BasVmT *vm, uint16_t idx) {
|
|
BasValueT me = currentFrame(vm)->me;
|
|
|
|
if (me.type == BAS_TYPE_INSTANCE && me.instVal && idx < (uint16_t)me.instVal->fieldCount) {
|
|
return &me.instVal->fields[idx];
|
|
}
|
|
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Member variable used outside a class instance");
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// A public variable member read or assigned through an object: obj.v,
|
|
// obj.v(i) (an array element, or the default member of an object held
|
|
// there) and the same with a value (Let / Set; argc does not count it).
|
|
// Takes over args and value.
|
|
static BasVmResultE memberVarAccess(BasVmT *vm, BasObjectT *obj, const BasClassMemberT *m, BasValueT *args, int32_t argc, BasValueT *value, int32_t op) {
|
|
if (m->index < 0 || m->index >= obj->fieldCount) {
|
|
dropArgs(args, argc);
|
|
|
|
if (value) {
|
|
basValRelease(value);
|
|
}
|
|
|
|
return raiseError(vm, BAS_ERR_NO_MEMBER);
|
|
}
|
|
|
|
BasValueT *field = &obj->fields[m->index];
|
|
|
|
// obj.v(args): an element of the array v holds, or v's default member
|
|
if (argc > 0) {
|
|
if (field->type == BAS_TYPE_INSTANCE && field->instVal) {
|
|
BasObjectT *inner = basObjectRef(field->instVal);
|
|
BasVmResultE rc = objectAccess(vm, inner, NULL, args, argc, value, op);
|
|
|
|
basObjectUnref(inner);
|
|
return rc;
|
|
}
|
|
|
|
int32_t flat = 0;
|
|
int32_t err = (field->type == BAS_TYPE_ARRAY && field->arrVal) ? indexArray(field->arrVal, args, argc, &flat) : BAS_ERR_TYPE_MISMATCH;
|
|
|
|
dropArgs(args, argc);
|
|
|
|
if (err != 0) {
|
|
if (value) {
|
|
basValRelease(value);
|
|
}
|
|
|
|
return raiseError(vm, err);
|
|
}
|
|
|
|
if (value) {
|
|
return storeElement(vm, &field->arrVal->elements[flat], *value) ? BAS_VM_OK : BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
return push(vm, basValCopy(field->arrVal->elements[flat])) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
if (!value) {
|
|
return push(vm, basValCopy(*field)) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
derefArg(value);
|
|
|
|
BasVmResultE rc = storeConvert(vm, m->dataType, value);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
// The conversion ran no BASIC code, so the object still holds field
|
|
storeValue(field, *value);
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// OP_NEW_OBJ: pushes a new Collection, or a new instance of class cls
|
|
// and runs its initialisation (member variables, then Class_Initialize)
|
|
// as a call made from here.
|
|
static BasVmResultE newObject(BasVmT *vm, uint16_t cls) {
|
|
if (cls == BAS_NEW_COLLECTION) {
|
|
BasObjectT *coll = basObjectNew(BAS_OBJ_COLLECTION, 0, &vm->liveObjects);
|
|
|
|
if (!coll) {
|
|
return raiseError(vm, BAS_ERR_OUT_OF_MEMORY);
|
|
}
|
|
|
|
return push(vm, basValInstance(coll)) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
if (cls >= vm->module->classCount) {
|
|
runtimeError(vm, BAS_ERR_BAD_OPCODE, "Bad class number");
|
|
return BAS_VM_BAD_OPCODE;
|
|
}
|
|
|
|
if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE) {
|
|
return BAS_VM_CALL_OVERFLOW;
|
|
}
|
|
|
|
const BasClassInfoT *info = &vm->module->classes[cls];
|
|
BasObjectT *obj = basObjectNew(cls, info->varCount, &vm->liveObjects);
|
|
|
|
if (!obj) {
|
|
return raiseError(vm, BAS_ERR_OUT_OF_MEMORY);
|
|
}
|
|
|
|
// One reference for the stack, one for the initialisation's Me
|
|
basObjectRef(obj);
|
|
|
|
if (!push(vm, basValInstance(obj))) {
|
|
basObjectUnref(obj);
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
// Class_Terminate only for an object that exists
|
|
if (info->terminateProc >= 0) {
|
|
obj->finalize = queueTerminate;
|
|
obj->finalizeCtx = vm;
|
|
}
|
|
|
|
// A class with no set-up code (none compiled) is ready as it is
|
|
if (info->initCodeAddr < 0) {
|
|
basObjectUnref(obj);
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
BasCallFrameT *frame = pushFrame(vm, vm->pc, info->initCodeAddr, 0);
|
|
|
|
frame->me = basValInstance(obj);
|
|
vm->pc = info->initCodeAddr;
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// A one-dimensional 0-based array of count zero elements of elementType
|
|
// (LBOUND 0, UBOUND -1 when count is 0). NULL when out of memory.
|
|
static BasArrayT *newVector(int32_t count, uint8_t elementType) {
|
|
if (count == 0) {
|
|
return basArrayNewEmpty(elementType);
|
|
}
|
|
|
|
int32_t lb = 0;
|
|
int32_t ub = count - 1;
|
|
|
|
return basArrayNew(1, &lb, &ub, elementType);
|
|
}
|
|
|
|
|
|
// Returns the bytecode address the error dispatcher records for RESUME
|
|
// NEXT: where execution continues with the eval stack at the failing
|
|
// statement's boundary. Walks forward from start -- the failing
|
|
// instruction, or with includeStart the return address of a call whose
|
|
// callee was unwound -- to the first of:
|
|
// - the next statement boundary, or a structural opcode that IS what runs
|
|
// after the statement (a loop-closing OP_JMP/OP_FOR_NEXT, a SUB's OP_RET,
|
|
// OP_END/OP_HALT, a FUNCTION's LOAD_LOCAL result + RET_VAL epilogue);
|
|
// - a conditional jump (IF/ELSEIF/WHILE/DO/LOOP condition): resume as if
|
|
// it were taken when it jumps forward and not taken when it jumps
|
|
// backward, so a failed condition skips its block or leaves its loop;
|
|
// - OP_FOR_INIT (a failed FOR header): continue past the whole loop.
|
|
// A GOSUB is stepped over so the walk continues at its return address.
|
|
// Returns -1 when the walk reaches an
|
|
// opcode that would pop values the trap already released or runs off the
|
|
// end of the code.
|
|
static int32_t nextStatementPc(BasVmT *vm, int32_t start, bool includeStart) {
|
|
const uint8_t *code = vm->module->code;
|
|
int32_t codeLen = vm->module->codeLen;
|
|
int32_t pc = start;
|
|
|
|
while (pc >= 0 && pc < codeLen) {
|
|
uint8_t op = code[pc];
|
|
int32_t operand = basOpcodeOperandSize(op);
|
|
|
|
if (operand < 0 || pc + 1 + operand > codeLen) {
|
|
return -1;
|
|
}
|
|
|
|
int32_t after = pc + 1 + operand;
|
|
|
|
if (pc > start || includeStart) {
|
|
if (op == OP_LINE || op == OP_STMT || op == OP_JMP || op == OP_FOR_NEXT || op == OP_RET || op == OP_END || op == OP_HALT) {
|
|
return pc;
|
|
}
|
|
|
|
if (op == OP_LOAD_LOCAL && after < codeLen && code[after] == OP_RET_VAL && basReadU16LE(code + pc + 1) == BAS_FUNC_RESULT_SLOT) {
|
|
return pc;
|
|
}
|
|
|
|
if (op == OP_JMP_TRUE || op == OP_JMP_FALSE) {
|
|
int16_t offset = basReadI16LE(code + pc + 1);
|
|
|
|
return (offset > 0) ? after + offset : after;
|
|
}
|
|
|
|
if (op == OP_FOR_INIT) {
|
|
return after + basReadI16LE(code + pc + BAS_FOR_OFFSET_OPERAND);
|
|
}
|
|
|
|
if (op == OP_RET_VAL || op == OP_GOSUB_RET) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
pc = after;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
// Form/control opcode executed with no form runtime attached (headless
|
|
// runs such as basrun or the test harnesses).
|
|
static BasVmResultE noFormRuntime(BasVmT *vm) {
|
|
runtimeError(vm, BAS_ERR_NO_FORM_RUNTIME, "No form runtime");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
|
|
// The Null and object rules of an arithmetic or comparison operator: when
|
|
// either operand is Null both are dropped and Null is pushed; a class
|
|
// instance or Collection has no value to compute with (Type mismatch --
|
|
// compare objects with IS). Returns true when the operator is done
|
|
// (*rc set).
|
|
static bool nullOperands(BasVmT *vm, BasValueT *a, BasValueT *b, BasVmResultE *rc) {
|
|
bool isObj = (a->type == BAS_TYPE_INSTANCE || b->type == BAS_TYPE_INSTANCE);
|
|
|
|
if (!isObj && a->type != BAS_TYPE_NULL && b->type != BAS_TYPE_NULL) {
|
|
return false;
|
|
}
|
|
|
|
basValRelease(a);
|
|
basValRelease(b);
|
|
|
|
if (isObj) {
|
|
*rc = raiseError(vm, BAS_ERR_TYPE_MISMATCH);
|
|
} else {
|
|
*rc = push(vm, basValNull()) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// obj.name(args) on a class instance or Collection (name NULL: the
|
|
// default member): a read (BAS_MEMBER_OP_GET), or with value a Let or
|
|
// Set. Takes over args and value. A read pushes its value, at once or
|
|
// when the member procedure returns.
|
|
static BasVmResultE objectAccess(BasVmT *vm, BasObjectT *obj, const char *name, BasValueT *args, int32_t argc, BasValueT *value, int32_t op) {
|
|
BasValueT callArgs[BAS_VM_MAX_CALL_ARGS + 1];
|
|
int32_t total = argc;
|
|
|
|
if (argc > 0) {
|
|
memcpy(callArgs, args, (size_t)argc * sizeof(BasValueT));
|
|
}
|
|
|
|
// Property Let / Set take the value as their last argument
|
|
if (value) {
|
|
callArgs[total++] = *value;
|
|
}
|
|
|
|
if (obj->classIdx == BAS_OBJ_COLLECTION) {
|
|
return collectionAccess(vm, obj, name, callArgs, total, op);
|
|
}
|
|
|
|
const BasClassInfoT *cls = objectClass(vm, obj);
|
|
const BasClassMemberT *m = cls ? findMember(cls, name, op) : NULL;
|
|
|
|
if (!m) {
|
|
dropArgs(callArgs, total);
|
|
return raiseError(vm, BAS_ERR_NO_MEMBER);
|
|
}
|
|
|
|
if (m->kind == BAS_MEMBER_VAR) {
|
|
return memberVarAccess(vm, obj, m, callArgs, argc, value ? &callArgs[argc] : NULL, op);
|
|
}
|
|
|
|
return callMember(vm, obj, m, callArgs, total, op == BAS_MEMBER_OP_GET);
|
|
}
|
|
|
|
|
|
// OP_OBJ_CHECK: 0 when v may be stored where class cls (an index,
|
|
// BAS_NEW_COLLECTION or BAS_OBJ_CHECK_ANY) is declared -- an object of
|
|
// that class, or Nothing -- else the BASIC error number.
|
|
static int32_t objectCheck(BasValueT v, uint16_t cls) {
|
|
if (!basValIsObject(v)) {
|
|
return BAS_ERR_OBJECT_REQUIRED;
|
|
}
|
|
|
|
if (cls == BAS_OBJ_CHECK_ANY || (v.type == BAS_TYPE_OBJECT && !v.objVal)) {
|
|
return 0;
|
|
}
|
|
|
|
int32_t want = (cls == BAS_NEW_COLLECTION) ? BAS_OBJ_COLLECTION : (int32_t)cls;
|
|
|
|
return (v.type == BAS_TYPE_INSTANCE && v.instVal && v.instVal->classIdx == want) ? 0 : BAS_ERR_TYPE_MISMATCH;
|
|
}
|
|
|
|
|
|
// The class module an instance belongs to (NULL for a Collection).
|
|
static const BasClassInfoT *objectClass(const BasVmT *vm, const BasObjectT *obj) {
|
|
if (obj->classIdx < 0 || obj->classIdx >= vm->module->classCount) {
|
|
return NULL;
|
|
}
|
|
|
|
return &vm->module->classes[obj->classIdx];
|
|
}
|
|
|
|
|
|
// obj(args) (OP_LOAD_ARRAY) or obj(args) = value (OP_STORE_ARRAY) on a
|
|
// class instance or Collection: its default member. The operands are on
|
|
// the stack (the value on top for a store).
|
|
static BasVmResultE objectDefault(BasVmT *vm, int32_t argc, bool isStore) {
|
|
BasValueT args[BAS_VM_MAX_CALL_ARGS];
|
|
BasValueT value;
|
|
BasValueT objVal;
|
|
|
|
if (argc > BAS_VM_MAX_CALL_ARGS) {
|
|
dropValues(vm, argc + 1 + (isStore ? 1 : 0));
|
|
return raiseError(vm, BAS_ERR_WRONG_ARGS);
|
|
}
|
|
|
|
if (isStore) {
|
|
pop(vm, &value);
|
|
}
|
|
|
|
vm->sp -= argc;
|
|
memcpy(args, &vm->stack[vm->sp], (size_t)argc * sizeof(BasValueT));
|
|
pop(vm, &objVal);
|
|
|
|
BasVmResultE rc = objectAccess(vm, objVal.instVal, NULL, args, argc, isStore ? &value : NULL, isStore ? BAS_MEMBER_OP_LET : BAS_MEMBER_OP_GET);
|
|
|
|
basValRelease(&objVal);
|
|
return rc;
|
|
}
|
|
|
|
|
|
// objectAccess for the member-access opcodes: takes over the object and
|
|
// member-name values as well.
|
|
static BasVmResultE objectMemberOp(BasVmT *vm, BasValueT objVal, BasValueT nameVal, BasValueT *args, int32_t argc, BasValueT *value, int32_t op) {
|
|
BasValueT sv = basValToString(nameVal);
|
|
BasVmResultE rc = objectAccess(vm, objVal.instVal, sv.strVal->data, args, argc, value, op);
|
|
|
|
basValRelease(&sv);
|
|
basValRelease(&nameVal);
|
|
basValRelease(&objVal);
|
|
return rc;
|
|
}
|
|
|
|
|
|
// TypeName of an object: "Nothing", the class name, "Collection", or what
|
|
// the host calls a form or control ("Object" when it does not say).
|
|
static const char *objectTypeName(const BasVmT *vm, BasValueT v) {
|
|
if (v.type == BAS_TYPE_INSTANCE && v.instVal) {
|
|
const BasClassInfoT *cls = objectClass(vm, v.instVal);
|
|
|
|
return cls ? cls->name : BAS_TYPENAME_COLLECTION;
|
|
}
|
|
|
|
if (v.type != BAS_TYPE_OBJECT || !v.objVal) {
|
|
return BAS_TYPENAME_NOTHING;
|
|
}
|
|
|
|
const char *name = vm->ui.typeName ? vm->ui.typeName(vm->ui.ctx, v.objVal) : NULL;
|
|
|
|
return name ? name : BAS_TYPENAME_OBJECT;
|
|
}
|
|
|
|
|
|
// OPEN "LPTn:" / "PRN" FOR OUTPUT or APPEND: the channel writes to an
|
|
// anonymous spool file that goes to the printer at CLOSE.
|
|
static BasVmResultE openPrinterChannel(BasVmT *vm, int32_t channel, int32_t port, uint8_t mode, int32_t recLen) {
|
|
if (mode != BAS_FILE_MODE_OUTPUT && mode != BAS_FILE_MODE_APPEND) {
|
|
runtimeError(vm, BAS_ERR_BAD_FILE_MODE, errorText(BAS_ERR_BAD_FILE_MODE));
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
closeChannel(vm, channel);
|
|
|
|
void *job = vm->printer.open ? vm->printer.open(vm->printer.ctx, port) : NULL;
|
|
|
|
if (!job) {
|
|
runtimeError(vm, BAS_ERR_DEVICE_UNAVAILABLE, errorText(BAS_ERR_DEVICE_UNAVAILABLE));
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
FILE *spool = tmpfile();
|
|
|
|
if (!spool) {
|
|
vm->printer.close(vm->printer.ctx, job);
|
|
runtimeError(vm, BAS_ERR_PATH_FILE_ACCESS, "Cannot create the printer spool file");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
vm->files[channel].handle = spool;
|
|
vm->files[channel].printJob = job;
|
|
vm->files[channel].mode = mode;
|
|
vm->files[channel].recLen = recLen;
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Returns whether `size` bytes can be read at vm->pc without running past
|
|
// the end of code[]. On overflow it flags the VM and clamps pc to codeLen so
|
|
// the next basVmStep entry raises BAS_VM_BAD_OPCODE. Compared as
|
|
// `codeLen - size` (not `pc + size`) to avoid signed overflow; codeLen is
|
|
// always >= 0 and size is a small operand width.
|
|
static inline bool operandFits(BasVmT *vm, int32_t size) {
|
|
if (vm->pc < 0 || vm->pc > vm->module->codeLen - size) {
|
|
vm->badOperand = true;
|
|
vm->pc = vm->module->codeLen;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Appends value as argument idx when the call left it out (the arguments
|
|
// before it are all present); otherwise value is released.
|
|
static void padArg(BasValueT *args, int32_t *argc, int32_t idx, BasValueT value) {
|
|
if (*argc == idx) {
|
|
args[(*argc)++] = value;
|
|
} else {
|
|
basValRelease(&value);
|
|
}
|
|
}
|
|
|
|
|
|
// Scans a PRINT USING / FORMAT$ numeric format string into f. Both
|
|
// statements share the placeholder grammar: '#' and '0' (and '*' before
|
|
// the decimal point) are digit positions, '0' also fixes the minimum
|
|
// number of integer digits, '.' and ',' set the decimal and thousands
|
|
// flags, a leading '+' or a trailing '+'/'-' selects the sign style,
|
|
// leading "**" and "$$" select fills, and "^^^^" requests scientific
|
|
// notation (reported through outSci). The field width is the length of
|
|
// the format string.
|
|
static void parseNumFormat(const char *fmt, int32_t fmtLen, BasNumFormatT *f, bool *outSci) {
|
|
memset(f, 0, sizeof(*f));
|
|
|
|
int32_t scanStart = (fmtLen > 0 && fmt[0] == '+') ? 1 : 0;
|
|
|
|
f->plusAtStart = (scanStart == 1);
|
|
f->asteriskFill = (fmtLen >= scanStart + 2 && fmt[scanStart] == '*' && fmt[scanStart + 1] == '*');
|
|
|
|
if (f->asteriskFill) {
|
|
scanStart += 2;
|
|
}
|
|
|
|
f->dollarFloat = (fmtLen >= scanStart + 2 && fmt[scanStart] == '$' && fmt[scanStart + 1] == '$');
|
|
f->plusAtEnd = (fmtLen > scanStart && fmt[fmtLen - 1] == '+');
|
|
f->minusAtEnd = (fmtLen > scanStart && fmt[fmtLen - 1] == '-');
|
|
f->width = fmtLen;
|
|
*outSci = (strstr(fmt, BAS_SCI_MARKER) != NULL);
|
|
|
|
int32_t intDigits = 0;
|
|
|
|
for (int32_t i = 0; i < fmtLen; i++) {
|
|
if (fmt[i] == '.') {
|
|
f->hasDecimal = true;
|
|
} else if (fmt[i] == ',') {
|
|
f->hasComma = true;
|
|
} else if (fmt[i] == '#' || fmt[i] == '0' || (fmt[i] == '*' && !f->hasDecimal)) {
|
|
if (f->hasDecimal) {
|
|
f->digitsAfter++;
|
|
} else {
|
|
intDigits++;
|
|
|
|
if (fmt[i] == '0' && f->minIntDigits == 0) {
|
|
// Every position from the first '0' to the decimal
|
|
// point is a forced digit.
|
|
f->minIntDigits = -intDigits;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (f->minIntDigits < 0) {
|
|
f->minIntDigits = intDigits + f->minIntDigits + 1;
|
|
}
|
|
}
|
|
|
|
|
|
static bool pop(BasVmT *vm, BasValueT *val) {
|
|
if (vm->sp <= 0) {
|
|
return false;
|
|
}
|
|
|
|
*val = vm->stack[--vm->sp];
|
|
return true;
|
|
}
|
|
|
|
|
|
// Pops count values, top of stack first, into the BasValueT pointers that
|
|
// follow. On underflow every value already popped is released so a
|
|
// malformed program cannot leak them, and false is returned.
|
|
static bool popArgs(BasVmT *vm, int32_t count, ...) {
|
|
va_list ap;
|
|
|
|
va_start(ap, count);
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
BasValueT *out = va_arg(ap, BasValueT *);
|
|
|
|
if (!pop(vm, out)) {
|
|
va_end(ap);
|
|
va_start(ap, count);
|
|
|
|
for (int32_t j = 0; j < i; j++) {
|
|
basValRelease(va_arg(ap, BasValueT *));
|
|
}
|
|
|
|
va_end(ap);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
va_end(ap);
|
|
return true;
|
|
}
|
|
|
|
|
|
// Pops dims subscripts and the array reference beneath them and resolves
|
|
// the element they name. On success *outArr holds the popped (counted)
|
|
// array reference, which the caller releases, and *outIdx the flat element
|
|
// index. On failure everything popped is released and the error raised.
|
|
static BasVmResultE popArrayElement(BasVmT *vm, int32_t dims, BasValueT *outArr, int32_t *outIdx) {
|
|
if (!validArrayDims(vm, dims)) {
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
int32_t indices[BAS_ARRAY_MAX_DIMS];
|
|
|
|
// Subscripts were pushed first dimension first, so pop in reverse
|
|
for (int32_t d = dims - 1; d >= 0; d--) {
|
|
BasValueT idxVal;
|
|
|
|
if (!pop(vm, &idxVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
indices[d] = basValToInt32(idxVal);
|
|
basValRelease(&idxVal);
|
|
}
|
|
|
|
if (!pop(vm, outArr)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
if (outArr->type != BAS_TYPE_ARRAY || !outArr->arrVal) {
|
|
basValRelease(outArr);
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not an array");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
*outIdx = basArrayIndex(outArr->arrVal, indices, dims);
|
|
|
|
if (*outIdx < 0) {
|
|
basValRelease(outArr);
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Subscript out of range");
|
|
return BAS_VM_SUBSCRIPT_RANGE;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Pops the top call frame (the shared SUB/FUNCTION return epilogue used by
|
|
// OP_RET and OP_RET_VAL). The caller MUST have already verified callDepth > 0.
|
|
// Copies out-args, releases locals and restores PC.
|
|
static void popCallFrame(BasVmT *vm) {
|
|
BasCallFrameT *frame = &vm->callStack[--vm->callDepth];
|
|
|
|
// Copy out-args (for basVmCallSubWithArgsOut) BEFORE releasing the
|
|
// locals -- reading them after release is a use-after-free.
|
|
if (vm->outArgs && vm->callDepth == vm->outArgFrame) {
|
|
for (int32_t i = 0; i < vm->outArgCount && i < frame->localCount; i++) {
|
|
vm->outArgs[i] = basValCopy(frame->locals[i]);
|
|
}
|
|
|
|
vm->outArgFrame = -1;
|
|
}
|
|
|
|
// Release locals and Me
|
|
for (int32_t i = 0; i < frame->localCount; i++) {
|
|
basValRelease(&frame->locals[i]);
|
|
}
|
|
|
|
basValRelease(&frame->me);
|
|
|
|
// Discard any FOR frames the callee left open (EXIT SUB/FUNCTION
|
|
// from inside a FOR body jumps straight to this epilogue) and
|
|
// restore the caller's statement boundary for error dispatch.
|
|
forStackTrim(vm, frame->savedForDepth);
|
|
vm->gosubDepth = frame->savedGosubDepth;
|
|
vm->stmtPc = frame->savedStmtPc;
|
|
vm->stmtSp = frame->savedStmtSp;
|
|
vm->pc = frame->returnPc;
|
|
|
|
// A procedure returning from inside its own running handler has dealt
|
|
// with the error: QBASIC documents that an unresumed handler is cleared
|
|
// when the procedure that installed it returns. A procedure that merely
|
|
// installed a handler leaves the caller's Err state alone.
|
|
if (frame->inHandler) {
|
|
clearErrorState(vm);
|
|
}
|
|
|
|
frame->errorHandler = 0;
|
|
frame->inHandler = false;
|
|
}
|
|
|
|
|
|
static BasVmResultE popFileChannel(BasVmT *vm, int32_t *outChannel, bool requireOpen) {
|
|
BasValueT channelVal;
|
|
|
|
if (!pop(vm, &channelVal)) {
|
|
return BAS_VM_STACK_UNDERFLOW;
|
|
}
|
|
|
|
int32_t channel = basValToInt32(channelVal);
|
|
basValRelease(&channelVal);
|
|
|
|
if (channel < 1 || channel >= BAS_VM_MAX_FILES || (requireOpen && !vm->files[channel].handle)) {
|
|
runtimeError(vm, BAS_ERR_BAD_FILE_NUM, requireOpen ? "Bad file number or file not open" : "Bad file channel number");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
*outChannel = channel;
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Pops the two operands of a binary operator: *b from the top of the
|
|
// stack, *a from beneath it.
|
|
static bool popPair(BasVmT *vm, BasValueT *a, BasValueT *b) {
|
|
if (vm->sp < 2) {
|
|
return false;
|
|
}
|
|
|
|
*b = vm->stack[vm->sp - 1];
|
|
*a = vm->stack[vm->sp - 2];
|
|
vm->sp -= 2;
|
|
return true;
|
|
}
|
|
|
|
|
|
// Pops one operand and converts it to a STRING value (*out always holds a
|
|
// non-NULL strVal on success). Returns false on stack underflow.
|
|
static bool popStringArg(BasVmT *vm, BasValueT *out) {
|
|
BasValueT raw;
|
|
|
|
if (!pop(vm, &raw)) {
|
|
return false;
|
|
}
|
|
|
|
*out = basValToString(raw);
|
|
basValRelease(&raw);
|
|
return true;
|
|
}
|
|
|
|
|
|
// Primes callStack[0] as the implicit main (module-level) frame. Module
|
|
// code runs in frame 0; without callDepth >= 1 the error dispatcher's
|
|
// unwind never inspects that frame, so a module-level ON ERROR GOTO never
|
|
// traps. Module-level variables are globals, so the frame has no locals.
|
|
// Frames an untrapped error left on a reused VM are released first.
|
|
static void primeModuleFrame(BasVmT *vm) {
|
|
while (vm->callDepth > 1) {
|
|
discardFrame(vm);
|
|
}
|
|
|
|
vm->callDepth = 1;
|
|
vm->gosubDepth = 0;
|
|
vm->callStack[0].localCount = 0;
|
|
vm->callStack[0].errorHandler = 0;
|
|
vm->callStack[0].inHandler = false;
|
|
vm->callStack[0].savedForDepth = 0;
|
|
vm->callStack[0].savedGosubDepth = 0;
|
|
vm->callStack[0].savedStmtPc = -1;
|
|
vm->callStack[0].savedStmtSp = 0;
|
|
vm->callStack[0].pushResult = false;
|
|
memset(&vm->callStack[0].me, 0, sizeof(vm->callStack[0].me));
|
|
|
|
// Hosts that run module-level expressions (the IDE's watch evaluator)
|
|
// widen localCount themselves, so leave the slots zeroed.
|
|
memset(vm->callStack[0].locals, 0, sizeof(vm->callStack[0].locals));
|
|
}
|
|
|
|
|
|
// The output column of what PRINT is writing to: the screen, or the
|
|
// printer during an LPRINT.
|
|
static int32_t *printColumnOf(BasVmT *vm) {
|
|
if (vm->lprinting) {
|
|
return &vm->lprintColumn;
|
|
}
|
|
|
|
// The program screen keeps its own cursor (LOCATE moves it)
|
|
if (vm->screen) {
|
|
vm->printColumn = basScreenColumn(vm->screen);
|
|
}
|
|
|
|
return &vm->printColumn;
|
|
}
|
|
|
|
|
|
// True when name is a printer device: "LPT1".."LPT3" (outPort 0..2) or
|
|
// "PRN" (BAS_PRINTER_DEFAULT), in any case, with or without a colon.
|
|
static bool printerDevice(const char *name, int32_t *outPort) {
|
|
char dev[BAS_PRINTER_NAME_MAX + 1];
|
|
int32_t len = (int32_t)strlen(name);
|
|
|
|
if (len > 0 && name[len - 1] == ':') {
|
|
len--;
|
|
}
|
|
|
|
if (len >= (int32_t)sizeof(dev)) {
|
|
return false;
|
|
}
|
|
|
|
for (int32_t i = 0; i < len; i++) {
|
|
dev[i] = (char)toupper((unsigned char)name[i]);
|
|
}
|
|
|
|
dev[len] = '\0';
|
|
|
|
if (strcmp(dev, BAS_PRINTER_NAME_PRN) == 0) {
|
|
*outPort = BAS_PRINTER_DEFAULT;
|
|
return true;
|
|
}
|
|
|
|
int32_t lptLen = (int32_t)strlen(BAS_PRINTER_NAME_LPT);
|
|
|
|
if (len == lptLen + 1 && strncmp(dev, BAS_PRINTER_NAME_LPT, (size_t)lptLen) == 0 && dev[lptLen] >= BAS_PRINTER_FIRST_DIGIT && dev[lptLen] < BAS_PRINTER_FIRST_DIGIT + BAS_PRINTER_PORTS) {
|
|
*outPort = dev[lptLen] - BAS_PRINTER_FIRST_DIGIT;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// Every PRINT byte goes through here so the output column is known for
|
|
// comma zones and TAB().
|
|
static void printText(BasVmT *vm, const char *text, bool newline) {
|
|
int32_t *column = printColumnOf(vm);
|
|
|
|
// Once the program has its screen, PRINT writes there
|
|
if (!vm->lprinting && vm->screen) {
|
|
basScreenPrint(vm->screen, text, newline);
|
|
*column = basScreenColumn(vm->screen);
|
|
return;
|
|
}
|
|
|
|
if (vm->lprinting) {
|
|
lprintWrite(vm, text, (int32_t)strlen(text));
|
|
|
|
if (newline) {
|
|
lprintWrite(vm, BAS_PRINTER_NEWLINE, BAS_PRINTER_NEWLINE_LEN);
|
|
}
|
|
} else if (vm->printFn) {
|
|
vm->printFn(vm->printCtx, text, newline);
|
|
}
|
|
|
|
for (const char *c = text; *c; c++) {
|
|
*column = (*c == '\n') ? 0 : *column + 1;
|
|
}
|
|
|
|
if (newline) {
|
|
*column = 0;
|
|
}
|
|
}
|
|
|
|
|
|
// Local-slot count for a call to codeAddr: the procedure's recorded local
|
|
// count (the proc table is sorted by code address), never less than
|
|
// minCount (the slots the arguments fill). Code without a proc entry gets
|
|
// the full BAS_VM_MAX_LOCALS. The compiler and the module loader reject a
|
|
// count over BAS_VM_MAX_LOCALS; the final bound keeps the frame inside
|
|
// locals[] whatever built the module.
|
|
static int32_t procLocalCount(const BasVmT *vm, int32_t codeAddr, int32_t minCount) {
|
|
const BasProcEntryT *procs = vm->module->procs;
|
|
int32_t lo = 0;
|
|
int32_t hi = vm->module->procCount - 1;
|
|
int32_t count = BAS_VM_MAX_LOCALS;
|
|
|
|
while (lo <= hi) {
|
|
int32_t mid = lo + (hi - lo) / 2;
|
|
|
|
if (procs[mid].codeAddr < codeAddr) {
|
|
lo = mid + 1;
|
|
} else if (procs[mid].codeAddr > codeAddr) {
|
|
hi = mid - 1;
|
|
} else {
|
|
count = (procs[mid].localCount < minCount) ? minCount : procs[mid].localCount;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return (count > BAS_VM_MAX_LOCALS) ? BAS_VM_MAX_LOCALS : count;
|
|
}
|
|
|
|
|
|
// Pushes val, taking ownership of it: on stack overflow the value is
|
|
// released here so no caller has to remember to.
|
|
static bool push(BasVmT *vm, BasValueT val) {
|
|
if (vm->sp >= BAS_VM_STACK_SIZE) {
|
|
basValRelease(&val);
|
|
return false;
|
|
}
|
|
|
|
vm->stack[vm->sp++] = val;
|
|
return true;
|
|
}
|
|
|
|
|
|
// Pushes a call frame for a call to codeAddr returning to returnPc (the
|
|
// caller has checked the call-stack depth). Only the procedure's own
|
|
// local slots are zeroed; every local access is bounded by localCount,
|
|
// and popCallFrame/discardFrame release exactly those slots.
|
|
static BasCallFrameT *pushFrame(BasVmT *vm, int32_t returnPc, int32_t codeAddr, int32_t minLocals) {
|
|
BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
|
|
|
|
frame->returnPc = returnPc;
|
|
frame->localCount = procLocalCount(vm, codeAddr, minLocals);
|
|
frame->errorHandler = 0;
|
|
frame->inHandler = false;
|
|
frame->savedForDepth = vm->forDepth;
|
|
frame->savedGosubDepth = vm->gosubDepth;
|
|
frame->savedStmtPc = vm->stmtPc;
|
|
frame->savedStmtSp = vm->stmtSp;
|
|
frame->pushResult = false;
|
|
memset(&frame->me, 0, sizeof(frame->me));
|
|
memset(frame->locals, 0, sizeof(BasValueT) * frame->localCount);
|
|
return frame;
|
|
}
|
|
|
|
|
|
// Bounded single-character append used throughout formatNumber: writes c at
|
|
// out[*idx] and advances *idx only while it stays under limit, so no formatter
|
|
// cursor write can overflow the caller's buffer.
|
|
static void putBounded(char *out, int32_t *idx, int32_t limit, char c) {
|
|
if (*idx < limit) {
|
|
out[(*idx)++] = c;
|
|
}
|
|
}
|
|
|
|
|
|
// Encodes one PUT value into sink: numerics in their binary form, strings
|
|
// raw on BINARY channels and length-prefixed otherwise, a TYPE instance
|
|
// field by field from layout (non-NULL only for PUT of a TYPE).
|
|
static BasVmResultE putValue(BasVmT *vm, BasPutSinkT *sink, int32_t mode, BasValueT val, const BasStringT *layout) {
|
|
if (layout) {
|
|
int32_t pos = 0;
|
|
|
|
return udtPut(vm, sink, val.udtVal, layout, &pos, 0);
|
|
}
|
|
|
|
switch (val.type) {
|
|
case BAS_TYPE_INTEGER:
|
|
case BAS_TYPE_LONG:
|
|
case BAS_TYPE_SINGLE:
|
|
case BAS_TYPE_DOUBLE:
|
|
case BAS_TYPE_BOOLEAN:
|
|
ioWriteScalar(sink, val.type, val);
|
|
break;
|
|
|
|
case BAS_TYPE_STRING:
|
|
if (mode == BAS_FILE_MODE_BINARY) {
|
|
if (val.strVal) {
|
|
sinkWrite(sink, val.strVal->data, val.strVal->len);
|
|
}
|
|
} else {
|
|
ioWritePrefixedString(sink, val.strVal);
|
|
}
|
|
|
|
break;
|
|
|
|
default: {
|
|
int16_t zero = 0;
|
|
sinkWrite(sink, &zero, sizeof(zero));
|
|
break;
|
|
}
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Finalizer of instances whose class has a Class_Terminate: the object
|
|
// (now unreferenced) waits in the VM's queue, alive again, until
|
|
// basVmStep starts its Class_Terminate after the current instruction.
|
|
static void queueTerminate(void *ctx, BasObjectT *obj) {
|
|
BasVmT *vm = (BasVmT *)ctx;
|
|
|
|
obj->refCount = 1;
|
|
obj->finalize = NULL;
|
|
obj->termNext = vm->terminateQueue;
|
|
vm->terminateQueue = obj;
|
|
}
|
|
|
|
|
|
// Raises BASIC error errNum with its standard message.
|
|
static BasVmResultE raiseError(BasVmT *vm, int32_t errNum) {
|
|
runtimeError(vm, errNum, errorText(errNum));
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
|
|
// INPUT #: reads one field the way WRITE # wrote it -- leading blanks are
|
|
// skipped, a quoted field runs to the closing quote, an unquoted field runs
|
|
// to the next comma or end of line, and the field's delimiter (comma or
|
|
// line end) is consumed. Returns the field as a new string.
|
|
static BasStringT *readField(FILE *fp) {
|
|
int32_t ch = fgetc(fp);
|
|
|
|
while (ch == ' ' || ch == '\t') {
|
|
ch = fgetc(fp);
|
|
}
|
|
|
|
if (ch == EOF) {
|
|
return basStringRef(basEmptyString);
|
|
}
|
|
|
|
// An empty line between records is its own (empty) field.
|
|
if (ch == '\n' || ch == '\r') {
|
|
if (ch == '\r') {
|
|
ch = fgetc(fp);
|
|
|
|
if (ch != '\n' && ch != EOF) {
|
|
ungetc(ch, fp);
|
|
}
|
|
}
|
|
|
|
return basStringRef(basEmptyString);
|
|
}
|
|
|
|
int32_t cap = BAS_FILE_LINE_CHUNK;
|
|
int32_t len = 0;
|
|
char *buf = (char *)malloc(cap);
|
|
bool quoted = (ch == '"');
|
|
|
|
if (!buf) {
|
|
return basStringRef(basEmptyString);
|
|
}
|
|
|
|
if (!quoted) {
|
|
ungetc(ch, fp);
|
|
}
|
|
|
|
for (;;) {
|
|
ch = fgetc(fp);
|
|
|
|
if (ch == EOF) {
|
|
break;
|
|
}
|
|
|
|
if (quoted) {
|
|
if (ch == '"') {
|
|
// Skip to the field delimiter that follows the closing quote.
|
|
do {
|
|
ch = fgetc(fp);
|
|
} while (ch == ' ' || ch == '\t');
|
|
|
|
if (ch != ',' && ch != '\n' && ch != '\r' && ch != EOF) {
|
|
ungetc(ch, fp);
|
|
}
|
|
|
|
break;
|
|
}
|
|
} else if (ch == ',' || ch == '\n' || ch == '\r') {
|
|
break;
|
|
}
|
|
|
|
if (len + 1 >= cap) {
|
|
int32_t newCap = cap * 2;
|
|
char *grown = (char *)realloc(buf, newCap);
|
|
|
|
if (!grown) {
|
|
break;
|
|
}
|
|
|
|
buf = grown;
|
|
cap = newCap;
|
|
}
|
|
|
|
buf[len++] = (char)ch;
|
|
}
|
|
|
|
// Swallow the LF of a CR LF pair so the next field starts cleanly.
|
|
if (ch == '\r') {
|
|
ch = fgetc(fp);
|
|
|
|
if (ch != '\n' && ch != EOF) {
|
|
ungetc(ch, fp);
|
|
}
|
|
}
|
|
|
|
// Unquoted fields drop trailing blanks, as QB does (the growth check
|
|
// above always leaves room for the terminator).
|
|
if (!quoted) {
|
|
buf[len] = '\0';
|
|
len = dvxTrimRight(buf);
|
|
}
|
|
|
|
BasStringT *s = basStringNew(buf, len);
|
|
free(buf);
|
|
return s;
|
|
}
|
|
|
|
|
|
// memcpy with constant size is folded to a single load by the compiler and
|
|
// is alignment-safe (bytecode operands aren't guaranteed 2-byte aligned).
|
|
static inline int16_t readInt16(BasVmT *vm) {
|
|
int16_t val = 0;
|
|
|
|
if (operandFits(vm, (int32_t)sizeof(int16_t))) {
|
|
memcpy(&val, &vm->module->code[vm->pc], sizeof(int16_t));
|
|
vm->pc += sizeof(int16_t);
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
|
|
// LINE INPUT #: reads a whole line of any length (fgets chunks are joined
|
|
// until the newline arrives), with the trailing CR/LF stripped. Returns an
|
|
// empty string at end of file.
|
|
static BasStringT *readLine(FILE *fp) {
|
|
int32_t cap = BAS_FILE_LINE_CHUNK;
|
|
int32_t len = 0;
|
|
char *buf = (char *)malloc(cap);
|
|
|
|
if (!buf) {
|
|
return basStringRef(basEmptyString);
|
|
}
|
|
|
|
buf[0] = '\0';
|
|
|
|
while (fgets(buf + len, cap - len, fp)) {
|
|
len += (int32_t)strlen(buf + len);
|
|
|
|
if (len > 0 && buf[len - 1] == '\n') {
|
|
break;
|
|
}
|
|
|
|
if (len + 1 >= cap - 1) {
|
|
int32_t newCap = cap * 2;
|
|
char *grown = (char *)realloc(buf, newCap);
|
|
|
|
if (!grown) {
|
|
break;
|
|
}
|
|
|
|
buf = grown;
|
|
cap = newCap;
|
|
}
|
|
}
|
|
|
|
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
|
|
len--;
|
|
}
|
|
|
|
BasStringT *s = basStringNew(buf, len);
|
|
free(buf);
|
|
return s;
|
|
}
|
|
|
|
|
|
// GET/PUT record read whose short-read result is deliberately ignored: a
|
|
// record past end of file yields the caller's zero default.
|
|
static void readRaw(FILE *fp, void *dst, size_t size) {
|
|
size_t got = fread(dst, 1, size, fp);
|
|
(void)got;
|
|
}
|
|
|
|
|
|
static inline uint16_t readUint16(BasVmT *vm) {
|
|
uint16_t val = 0;
|
|
|
|
if (operandFits(vm, (int32_t)sizeof(uint16_t))) {
|
|
memcpy(&val, &vm->module->code[vm->pc], sizeof(uint16_t));
|
|
vm->pc += sizeof(uint16_t);
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
|
|
static inline uint8_t readUint8(BasVmT *vm) {
|
|
if (!operandFits(vm, (int32_t)sizeof(uint8_t))) {
|
|
return 0;
|
|
}
|
|
|
|
return vm->module->code[vm->pc++];
|
|
}
|
|
|
|
|
|
// Positions a channel for GET/PUT (see seekRecord; 0 keeps the current
|
|
// position, a negative number is Bad record number) and stores the start
|
|
// offset in *start so recordEnd can frame the transfer.
|
|
static BasVmResultE recordBegin(BasVmT *vm, int32_t channel, int32_t recno, long *start) {
|
|
if (recno < 0) {
|
|
runtimeError(vm, BAS_ERR_BAD_RECORD_NUM, "Bad record number");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
if (recno > 0) {
|
|
BasVmResultE rc = seekRecord(vm, channel, recno);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
}
|
|
|
|
*start = ftell((FILE *)vm->files[channel].handle);
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Completes a GET/PUT. RANDOM channels always transfer whole LEN-byte
|
|
// records: a PUT pads the record with zeros and a GET skips to the next
|
|
// record; a GET that read past LEN raises error 59 (PUT measures its
|
|
// record beforehand).
|
|
static BasVmResultE recordEnd(BasVmT *vm, int32_t channel, long start, bool writing) {
|
|
FILE *fp = (FILE *)vm->files[channel].handle;
|
|
|
|
if (vm->files[channel].mode == BAS_FILE_MODE_RANDOM) {
|
|
long recLen = vm->files[channel].recLen;
|
|
long used = ftell(fp) - start;
|
|
|
|
if (used > recLen) {
|
|
runtimeError(vm, BAS_ERR_BAD_RECORD_LEN, "Record longer than LEN");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
if (writing) {
|
|
for (long i = used; i < recLen; i++) {
|
|
fputc(0, fp);
|
|
}
|
|
} else {
|
|
fseek(fp, start + recLen, SEEK_SET);
|
|
}
|
|
}
|
|
|
|
if (writing) {
|
|
fflush(fp);
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Releases every refcounted value the VM holds: the eval stack, the
|
|
// globals, all call-frame locals across callDepth, and the FOR-stack
|
|
// limit/step across forDepth. Must run BEFORE the caller zeroes
|
|
// sp/callDepth/forDepth, because the loops use those counts.
|
|
static void releaseVmState(BasVmT *vm) {
|
|
// Release eval 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 and Me
|
|
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]);
|
|
}
|
|
|
|
basValRelease(&vm->callStack[d].me);
|
|
}
|
|
|
|
// Release FOR stack
|
|
for (int32_t i = 0; i < vm->forDepth; i++) {
|
|
basValRelease(&vm->forStack[i].limit);
|
|
basValRelease(&vm->forStack[i].step);
|
|
}
|
|
}
|
|
|
|
|
|
// REPLACE$: s from byte start on with up to count (BAS_COUNT_ALL: every)
|
|
// occurrences of find replaced by repl. Returns the result length; with
|
|
// out non-NULL the result is also written there (not terminated).
|
|
static int32_t replaceScan(const BasStringT *s, int32_t start, const BasStringT *find, const BasStringT *repl, int32_t count, bool ignoreCase, char *out) {
|
|
int32_t len = 0;
|
|
int32_t done = 0;
|
|
int32_t pos = start;
|
|
|
|
while (pos < s->len && find->len > 0 && (count == BAS_COUNT_ALL || done < count)) {
|
|
int32_t hit = stringFind(s, pos, find, ignoreCase);
|
|
|
|
if (hit < 0) {
|
|
break;
|
|
}
|
|
|
|
if (out) {
|
|
memcpy(out + len, s->data + pos, hit - pos);
|
|
memcpy(out + len + hit - pos, repl->data, repl->len);
|
|
}
|
|
|
|
len += hit - pos + repl->len;
|
|
pos = hit + find->len;
|
|
done++;
|
|
}
|
|
|
|
if (pos < s->len) {
|
|
if (out) {
|
|
memcpy(out + len, s->data + pos, s->len - pos);
|
|
}
|
|
|
|
len += s->len - pos;
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
|
|
// Rounds a numeric operand to LONG (banker's rounding) for the integer
|
|
// operators; raises Overflow when it does not fit.
|
|
static BasVmResultE roundedOperand(BasVmT *vm, BasValueT v, int32_t *out) {
|
|
if (!basValRoundToInt32(v, INT32_MIN, INT32_MAX, out)) {
|
|
runtimeError(vm, BAS_ERR_OVERFLOW, "Overflow");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// 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) {
|
|
int32_t stepPc = vm->pc; // save for ON ERROR dispatch
|
|
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: drop the paused frames like
|
|
// every other exit path before handing back.
|
|
while (vm->callDepth > savedCallDepth) {
|
|
discardFrame(vm);
|
|
}
|
|
|
|
vm->pc = savedPc;
|
|
vm->running = savedRunning;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// User resumed -- continue executing the sub
|
|
stepsSinceYield = 0;
|
|
continue;
|
|
}
|
|
|
|
if (result != BAS_VM_OK) {
|
|
// Try ON ERROR GOTO within the current sub-call boundary
|
|
// (savedCallDepth) so event handlers fired via basVmCallSub
|
|
// behave like module-level code when it comes to trapping.
|
|
if (dispatchError(vm, result, savedCallDepth, stepPc)) {
|
|
stepsSinceYield = 0;
|
|
continue;
|
|
}
|
|
|
|
// Untrapped: the frames still hold refcounted locals; discard
|
|
// them so force-restoring callDepth cannot orphan them above
|
|
// releaseVmState's reach. Not popCallFrame: no out-arg
|
|
// copy-back on the failure path, and it would clear the error
|
|
// state the host reads.
|
|
while (vm->callDepth > savedCallDepth) {
|
|
discardFrame(vm);
|
|
}
|
|
|
|
vm->pc = savedPc;
|
|
vm->running = savedRunning;
|
|
return false;
|
|
}
|
|
|
|
// Yield periodically to keep the GUI responsive
|
|
if (++stepsSinceYield >= BAS_VM_DEFAULT_STEP_SLICE) {
|
|
stepsSinceYield = 0;
|
|
|
|
if (!basVmDoEvents(vm)) {
|
|
vm->running = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// END/OP_HALT and doEvents aborts break out of the loop with the
|
|
// callee's frames still pushed (OP_RET never ran for them). Pop them
|
|
// properly -- out-arg copy-back, local release, FOR-stack trim --
|
|
// instead of force-lowering callDepth, which would strand the frames'
|
|
// refcounted locals above callDepth where releaseVmState never walks,
|
|
// and would drop ByRef out-args (e.g. Cancel = 1 set before END).
|
|
while (vm->callDepth > savedCallDepth) {
|
|
popCallFrame(vm);
|
|
}
|
|
|
|
vm->pc = savedPc;
|
|
vm->running = savedRunning;
|
|
|
|
// Show what the handler drew before the host carries on
|
|
basScreenPresent(vm);
|
|
|
|
// 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;
|
|
}
|
|
|
|
|
|
static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg) {
|
|
vm->errorNumber = errNum;
|
|
vm->errorLine = vm->currentLine;
|
|
vm->lprinting = false;
|
|
|
|
if (vm->currentLine > 0) {
|
|
snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d on line %d" BAS_VM_ERROR_TEXT_SEP "%s", (int)errNum, (int)vm->currentLine, msg);
|
|
} else {
|
|
snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d at PC %d" BAS_VM_ERROR_TEXT_SEP "%s", (int)errNum, (int)vm->currentOpPc, msg);
|
|
}
|
|
}
|
|
|
|
|
|
// Resolves a variable operand (local, form or global slot, by scope tag)
|
|
// through any ByRef indirection, the way OP_LOAD_LOCAL/OP_STORE_LOCAL do,
|
|
// so FOR or an append on a ByRef parameter reaches the caller's variable. Raises the runtime error
|
|
// and returns NULL when the slot does not exist.
|
|
static BasValueT *scopedVarSlot(BasVmT *vm, uint16_t varIdx, uint8_t scopeTag) {
|
|
BasValueT *slot = NULL;
|
|
const char *what = "Invalid global variable index";
|
|
|
|
if (scopeTag == SCOPE_MEMBER) {
|
|
return memberSlot(vm, varIdx);
|
|
}
|
|
|
|
if (scopeTag == SCOPE_LOCAL) {
|
|
BasCallFrameT *frame = currentFrame(vm);
|
|
|
|
what = "Invalid local variable index";
|
|
|
|
if (varIdx < (uint16_t)frame->localCount) {
|
|
slot = &frame->locals[varIdx];
|
|
}
|
|
} else if (scopeTag == SCOPE_FORM) {
|
|
what = "Invalid form variable index";
|
|
|
|
if (vm->currentFormVars && varIdx < (uint16_t)vm->currentFormVarCount) {
|
|
slot = &vm->currentFormVars[varIdx];
|
|
}
|
|
} else if (varIdx < BAS_VM_MAX_GLOBALS) {
|
|
slot = &vm->globals[varIdx];
|
|
}
|
|
|
|
if (!slot) {
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, what);
|
|
return NULL;
|
|
}
|
|
|
|
slot = derefSlot(slot);
|
|
|
|
if (!slot) {
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Subscript out of range");
|
|
}
|
|
|
|
return slot;
|
|
}
|
|
|
|
|
|
// srand with the integer part of seed taken modulo 2^32 (a non-finite
|
|
// seed counts as 0).
|
|
static void seedRandom(double seed) {
|
|
double reduced = isfinite(seed) ? fmod(seed, BAS_RND_SEED_MODULUS) : 0.0;
|
|
|
|
srand((unsigned int)(int64_t)reduced);
|
|
}
|
|
|
|
|
|
// Positions a channel at a 1-based record number (RANDOM) or byte
|
|
// position (every other mode) for GET/PUT and SEEK. A position whose
|
|
// byte offset does not fit a long raises Bad record number.
|
|
static BasVmResultE seekRecord(BasVmT *vm, int32_t channel, int32_t pos) {
|
|
long unit = (vm->files[channel].mode == BAS_FILE_MODE_RANDOM) ? vm->files[channel].recLen : 1;
|
|
|
|
if ((long)(pos - 1) > LONG_MAX / unit) {
|
|
runtimeError(vm, BAS_ERR_BAD_RECORD_NUM, "Bad record number");
|
|
return BAS_VM_FILE_ERROR;
|
|
}
|
|
|
|
fseek((FILE *)vm->files[channel].handle, (long)(pos - 1) * unit, SEEK_SET);
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// A printer channel's spool to its job with CR LF line ends (PRINT #
|
|
// writes LF), then the job is closed. False when anything failed.
|
|
static bool sendSpool(BasVmT *vm, BasFileChannelT *ch) {
|
|
FILE *fp = (FILE *)ch->handle;
|
|
char in[BAS_SPOOL_CHUNK];
|
|
char out[BAS_SPOOL_CHUNK * BAS_PRINTER_NEWLINE_LEN];
|
|
bool ok = true;
|
|
bool prevCr = false;
|
|
size_t n;
|
|
|
|
rewind(fp);
|
|
|
|
while (ok && (n = fread(in, 1, sizeof(in), fp)) > 0) {
|
|
int32_t len = 0;
|
|
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (in[i] == '\n' && !prevCr) {
|
|
out[len++] = '\r';
|
|
}
|
|
|
|
prevCr = (in[i] == '\r');
|
|
out[len++] = in[i];
|
|
}
|
|
|
|
ok = vm->printer.write(vm->printer.ctx, ch->printJob, out, len);
|
|
}
|
|
|
|
return vm->printer.close(vm->printer.ctx, ch->printJob) && ok;
|
|
}
|
|
|
|
|
|
// Appends count copies of c to a PUT sink (fixed-length string padding).
|
|
static void sinkFill(BasPutSinkT *sink, char c, int32_t count) {
|
|
for (int32_t i = 0; i < count; i++) {
|
|
sinkWrite(sink, &c, 1);
|
|
}
|
|
}
|
|
|
|
|
|
// Appends len bytes to a PUT sink (counted only when it has no file).
|
|
static void sinkWrite(BasPutSinkT *sink, const void *data, int32_t len) {
|
|
if (sink->fp && len > 0) {
|
|
fwrite(data, 1, len, sink->fp);
|
|
}
|
|
|
|
sink->bytes += len;
|
|
}
|
|
|
|
|
|
// SLEEP: waits secs seconds while pumping the host's event loop, so the
|
|
// GUI keeps running; SLEEP 0 (or a negative count) yields to the host
|
|
// once. Returns false when the host asks the program to stop. Without an
|
|
// event pump (headless runs) the process just sleeps.
|
|
static bool sleepSeconds(BasVmT *vm, int32_t secs) {
|
|
if (!vm->doEventsFn) {
|
|
if (secs > 0) {
|
|
sleep((unsigned int)secs);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
double deadline = wallSeconds() + secs;
|
|
|
|
do {
|
|
if (!basVmDoEvents(vm)) {
|
|
return false;
|
|
}
|
|
|
|
if (secs > 0) {
|
|
usleep(BAS_SLEEP_POLL_USEC);
|
|
}
|
|
} while (wallSeconds() < deadline);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// SPLIT: the pieces of s between occurrences of delim, at most limit of
|
|
// them (BAS_COUNT_ALL: no limit; the last piece keeps the rest). Returns
|
|
// how many there are; with out non-NULL they are also stored into out's
|
|
// elements. An empty s has no pieces; an empty delim gives s whole.
|
|
static int32_t splitScan(const BasStringT *s, const BasStringT *delim, int32_t limit, bool ignoreCase, BasArrayT *out) {
|
|
int32_t n = 0;
|
|
int32_t pos = 0;
|
|
|
|
if (s->len == 0 || limit == 0) {
|
|
return 0;
|
|
}
|
|
|
|
while (true) {
|
|
int32_t hit = (delim->len > 0 && (limit == BAS_COUNT_ALL || n < limit - 1)) ? stringFind(s, pos, delim, ignoreCase) : -1;
|
|
int32_t end = (hit < 0) ? s->len : hit;
|
|
|
|
if (out) {
|
|
basValRelease(&out->elements[n]);
|
|
out->elements[n] = strValue(basStringSub(s, pos, end - pos));
|
|
}
|
|
|
|
n++;
|
|
|
|
if (hit < 0) {
|
|
return n;
|
|
}
|
|
|
|
pos = hit + delim->len;
|
|
}
|
|
}
|
|
|
|
|
|
// Starts the Class_Terminate of the next queued instance as a call made
|
|
// between two instructions. The call's Me holds the queue's reference;
|
|
// when Class_Terminate returns the object is freed (unless it stored a
|
|
// reference to itself somewhere).
|
|
static BasVmResultE startTerminate(BasVmT *vm) {
|
|
BasObjectT *obj = vm->terminateQueue;
|
|
|
|
vm->terminateQueue = obj->termNext;
|
|
obj->termNext = NULL;
|
|
|
|
const BasClassInfoT *cls = objectClass(vm, obj);
|
|
|
|
if (!cls || cls->terminateProc < 0 || vm->callDepth >= BAS_VM_CALL_STACK_SIZE) {
|
|
basObjectUnref(obj);
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
int32_t addr = vm->module->procs[cls->terminateProc].codeAddr;
|
|
BasCallFrameT *frame = pushFrame(vm, vm->pc, addr, 0);
|
|
|
|
frame->me = basValInstance(obj);
|
|
vm->pc = addr;
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Converts *val (in place) to a declared type the way a store into a
|
|
// variable of that type does (the conversion opcode basConvOpForStore names).
|
|
static BasVmResultE storeConvert(BasVmT *vm, uint8_t dataType, BasValueT *val) {
|
|
uint8_t op = basConvOpForStore(dataType);
|
|
|
|
if (op == OP_NOP) {
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
if (!push(vm, *val)) {
|
|
return BAS_VM_STACK_OVERFLOW;
|
|
}
|
|
|
|
BasVmResultE rc = execConvert(vm, op);
|
|
|
|
pop(vm, val);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
basValRelease(val);
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
|
|
// Stores into an array element or TYPE field. An array reference is
|
|
// refused there: the compiler never emits one (elements and fields are
|
|
// scalars or TYPE instances) and a corrupt module storing an array
|
|
// inside itself would build a reference cycle nothing ever frees.
|
|
static bool storeElement(BasVmT *vm, BasValueT *target, BasValueT val) {
|
|
if (val.type == BAS_TYPE_ARRAY) {
|
|
basValRelease(&val);
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Array reference cannot be stored in an element");
|
|
return false;
|
|
}
|
|
|
|
storeValue(target, val);
|
|
return true;
|
|
}
|
|
|
|
|
|
// Replaces *target with val, releasing the old value. A TYPE value that is
|
|
// still shared with another variable is deep-copied first so assignment has
|
|
// value semantics (b = a must not alias a).
|
|
static void storeValue(BasValueT *target, BasValueT val) {
|
|
basValRelease(target);
|
|
*target = udtDetach(val);
|
|
}
|
|
|
|
|
|
// Length-aware substring search (strings may contain NUL bytes). Returns
|
|
// the 0-based offset of the first occurrence of needle in haystack at or
|
|
// after start, or -1; ignoreCase compares ASCII letters case-insensitively.
|
|
// An empty needle matches at start (INSTR semantics).
|
|
static int32_t stringFind(const BasStringT *haystack, int32_t start, const BasStringT *needle, bool ignoreCase) {
|
|
if (needle->len == 0) {
|
|
return start;
|
|
}
|
|
|
|
for (int32_t i = start; i + needle->len <= haystack->len; i++) {
|
|
if (stringMatchAt(haystack, i, needle, ignoreCase)) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
// True when needle occurs in haystack at byte i (the caller keeps i +
|
|
// needle->len within haystack).
|
|
static bool stringMatchAt(const BasStringT *haystack, int32_t i, const BasStringT *needle, bool ignoreCase) {
|
|
if (!ignoreCase) {
|
|
return memcmp(haystack->data + i, needle->data, needle->len) == 0;
|
|
}
|
|
|
|
for (int32_t k = 0; k < needle->len; k++) {
|
|
if (toupper((unsigned char)haystack->data[i + k]) != toupper((unsigned char)needle->data[k])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Wraps a string the caller already owns a reference to (no extra ref).
|
|
static BasValueT strValue(BasStringT *s) {
|
|
BasValueT v;
|
|
v.type = BAS_TYPE_STRING;
|
|
v.strVal = s;
|
|
return v;
|
|
}
|
|
|
|
|
|
// Releases every object the VM made that is still alive when it is
|
|
// destroyed. Each object is held while all of them let go of what they
|
|
// hold (breaking reference cycles), then the holds are dropped: objects
|
|
// nothing else refers to are freed, the rest (still referenced by the
|
|
// host) leave the VM's list and live on. No Class_Terminate runs.
|
|
static void sweepObjects(BasVmT *vm) {
|
|
for (BasObjectT *o = vm->liveObjects; o; o = o->liveNext) {
|
|
o->finalize = NULL;
|
|
o->refCount++;
|
|
}
|
|
|
|
for (BasObjectT *o = vm->liveObjects; o; o = o->liveNext) {
|
|
basObjectClear(o);
|
|
}
|
|
|
|
BasObjectT *o = vm->liveObjects;
|
|
|
|
while (o) {
|
|
BasObjectT *next = o->liveNext;
|
|
|
|
basObjectUnref(o);
|
|
o = next;
|
|
}
|
|
|
|
for (o = vm->liveObjects; o; o = vm->liveObjects) {
|
|
vm->liveObjects = o->liveNext;
|
|
o->liveList = NULL;
|
|
o->livePrev = NULL;
|
|
o->liveNext = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// ISNUMERIC of a string: optional blanks, a sign, digits with an
|
|
// optional point and exponent (or an &H / &O number), optional blanks.
|
|
static bool textIsNumeric(const BasStringT *s) {
|
|
const char *p = s->data;
|
|
const char *end = s->data + s->len;
|
|
int32_t digits = 0;
|
|
|
|
while (p < end && *p == ' ') {
|
|
p++;
|
|
}
|
|
|
|
if (p < end && (*p == '+' || *p == '-')) {
|
|
p++;
|
|
}
|
|
|
|
if (end - p >= BAS_RADIX_PREFIX_LEN && p[0] == '&' && (toupper((unsigned char)p[1]) == 'H' || toupper((unsigned char)p[1]) == 'O')) {
|
|
bool hex = toupper((unsigned char)p[1]) == 'H';
|
|
|
|
for (p += BAS_RADIX_PREFIX_LEN; p < end && (hex ? isxdigit((unsigned char)*p) : (*p >= '0' && *p <= '7')); p++) {
|
|
digits++;
|
|
}
|
|
} else {
|
|
for (; p < end && isdigit((unsigned char)*p); p++) {
|
|
digits++;
|
|
}
|
|
|
|
if (p < end && *p == '.') {
|
|
for (p++; p < end && isdigit((unsigned char)*p); p++) {
|
|
digits++;
|
|
}
|
|
}
|
|
|
|
if (digits > 0 && p < end && strchr(BAS_EXPONENT_CHARS, *p)) {
|
|
int32_t expDigits = 0;
|
|
|
|
p++;
|
|
|
|
if (p < end && (*p == '+' || *p == '-')) {
|
|
p++;
|
|
}
|
|
|
|
for (; p < end && isdigit((unsigned char)*p); p++) {
|
|
expDigits++;
|
|
}
|
|
|
|
digits = (expDigits > 0) ? digits : 0;
|
|
}
|
|
}
|
|
|
|
while (p < end && *p == ' ') {
|
|
p++;
|
|
}
|
|
|
|
return digits > 0 && p == end;
|
|
}
|
|
|
|
|
|
// TypeName of a value of a non-object type (typeId names a TYPE).
|
|
static const char *typeNameOf(const BasVmT *vm, uint8_t type, int32_t typeId) {
|
|
switch (type) {
|
|
case BAS_TYPE_EMPTY:
|
|
return "Empty";
|
|
|
|
case BAS_TYPE_NULL:
|
|
return "Null";
|
|
|
|
case BAS_TYPE_INTEGER:
|
|
return "Integer";
|
|
|
|
case BAS_TYPE_LONG:
|
|
return "Long";
|
|
|
|
case BAS_TYPE_SINGLE:
|
|
return "Single";
|
|
|
|
case BAS_TYPE_DOUBLE:
|
|
return "Double";
|
|
|
|
case BAS_TYPE_DATE:
|
|
return "Date";
|
|
|
|
case BAS_TYPE_STRING:
|
|
return "String";
|
|
|
|
case BAS_TYPE_BOOLEAN:
|
|
return "Boolean";
|
|
|
|
case BAS_TYPE_VARIANT:
|
|
return "Variant";
|
|
|
|
case BAS_TYPE_OBJECT:
|
|
case BAS_TYPE_INSTANCE:
|
|
return BAS_TYPENAME_OBJECT;
|
|
|
|
case BAS_TYPE_UDT:
|
|
for (int32_t i = 0; i < vm->module->debugUdtDefCount; i++) {
|
|
if (vm->module->debugUdtDefs[i].typeId == typeId) {
|
|
return vm->module->debugUdtDefs[i].name;
|
|
}
|
|
}
|
|
|
|
return BAS_TYPENAME_UDT;
|
|
|
|
default:
|
|
return "Unknown";
|
|
}
|
|
}
|
|
|
|
|
|
// Gives val its own TYPE instance when the one it carries is shared. The
|
|
// shared reference is released; a clone failure leaves the value shared.
|
|
static BasValueT udtDetach(BasValueT val) {
|
|
if (val.type != BAS_TYPE_UDT || !val.udtVal || val.udtVal->refCount <= 1) {
|
|
return val;
|
|
}
|
|
|
|
BasUdtT *copy = basUdtClone(val.udtVal);
|
|
|
|
if (!copy) {
|
|
return val;
|
|
}
|
|
|
|
basUdtUnref(val.udtVal);
|
|
val.udtVal = copy;
|
|
return val;
|
|
}
|
|
|
|
|
|
// Builds a TYPE instance from its layout: fixed strings start as spaces,
|
|
// variable strings empty, numerics as typed zeros, nested types are
|
|
// built recursively. NULL on a malformed layout or out of memory.
|
|
static BasUdtT *udtFromLayout(BasVmT *vm, const BasStringT *layout, int32_t *pos, int32_t typeId, int32_t fieldCount, int32_t depth) {
|
|
if (depth >= BAS_LAYOUT_MAX_DEPTH || fieldCount < 0) {
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Bad TYPE layout");
|
|
return NULL;
|
|
}
|
|
|
|
BasUdtT *udt = basUdtNew(typeId, fieldCount);
|
|
|
|
if (!udt) {
|
|
runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory allocating TYPE");
|
|
return NULL;
|
|
}
|
|
|
|
for (int32_t i = 0; i < fieldCount; i++) {
|
|
BasLayoutEntryT entry;
|
|
|
|
if (!layoutNext(layout, pos, &entry)) {
|
|
basUdtUnref(udt);
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Bad TYPE layout");
|
|
return NULL;
|
|
}
|
|
|
|
if (entry.type == BAS_TYPE_UDT) {
|
|
BasUdtT *child = udtFromLayout(vm, layout, pos, entry.typeId, entry.fieldCount, depth + 1);
|
|
|
|
if (!child) {
|
|
basUdtUnref(udt);
|
|
return NULL;
|
|
}
|
|
|
|
udt->fields[i].type = BAS_TYPE_UDT;
|
|
udt->fields[i].udtVal = child;
|
|
} else if (entry.type == BAS_TYPE_STRING && entry.fixedLen > 0) {
|
|
udt->fields[i] = strValue(fillString(' ', entry.fixedLen));
|
|
} else {
|
|
udt->fields[i] = zeroValue(entry.type);
|
|
}
|
|
}
|
|
|
|
return udt;
|
|
}
|
|
|
|
|
|
// GET into a TYPE instance: walks the compiler's layout (one entry per
|
|
// field, nested types inline) and fills each field in place.
|
|
static BasVmResultE udtGet(BasVmT *vm, FILE *fp, BasUdtT *udt, const BasStringT *layout, int32_t *pos, int32_t depth) {
|
|
if (depth >= BAS_LAYOUT_MAX_DEPTH) {
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "TYPE nesting too deep");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
for (int32_t i = 0; i < udt->fieldCount; i++) {
|
|
BasLayoutEntryT entry;
|
|
|
|
if (!layoutNext(layout, pos, &entry)) {
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Bad TYPE layout");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
uint8_t type = entry.type;
|
|
int32_t fixedLen = entry.fixedLen;
|
|
BasValueT *field = &udt->fields[i];
|
|
|
|
if (type == BAS_TYPE_UDT) {
|
|
if (field->type != BAS_TYPE_UDT || !field->udtVal) {
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not a TYPE instance");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
BasVmResultE rc = udtGet(vm, fp, field->udtVal, layout, pos, depth + 1);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (type == BAS_TYPE_STRING) {
|
|
storeValue(field, strValue(fixedLen > 0 ? ioReadBytes(fp, fixedLen, true) : ioReadPrefixedString(fp)));
|
|
continue;
|
|
}
|
|
|
|
storeValue(field, ioReadScalar(fp, type));
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// PUT counterpart of udtGet.
|
|
static BasVmResultE udtPut(BasVmT *vm, BasPutSinkT *sink, const BasUdtT *udt, const BasStringT *layout, int32_t *pos, int32_t depth) {
|
|
if (depth >= BAS_LAYOUT_MAX_DEPTH) {
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "TYPE nesting too deep");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
for (int32_t i = 0; i < udt->fieldCount; i++) {
|
|
BasLayoutEntryT entry;
|
|
|
|
if (!layoutNext(layout, pos, &entry)) {
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Bad TYPE layout");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
uint8_t type = entry.type;
|
|
int32_t fixedLen = entry.fixedLen;
|
|
BasValueT field = udt->fields[i];
|
|
|
|
if (type == BAS_TYPE_UDT) {
|
|
if (field.type != BAS_TYPE_UDT || !field.udtVal) {
|
|
runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not a TYPE instance");
|
|
return BAS_VM_TYPE_MISMATCH;
|
|
}
|
|
|
|
BasVmResultE rc = udtPut(vm, sink, field.udtVal, layout, pos, depth + 1);
|
|
|
|
if (rc != BAS_VM_OK) {
|
|
return rc;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (type == BAS_TYPE_STRING) {
|
|
BasValueT sv = basValToString(field);
|
|
|
|
if (fixedLen > 0) {
|
|
int32_t copyLen = DVX_MIN(sv.strVal->len, fixedLen);
|
|
|
|
sinkWrite(sink, sv.strVal->data, copyLen);
|
|
sinkFill(sink, ' ', fixedLen - copyLen);
|
|
} else {
|
|
ioWritePrefixedString(sink, sv.strVal);
|
|
}
|
|
|
|
basValRelease(&sv);
|
|
continue;
|
|
}
|
|
|
|
ioWriteScalar(sink, type, field);
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Checks a form/control opcode can run: the host must provide the
|
|
// callback (no form runtime otherwise) and the target must be an object
|
|
// reference -- an unset object variable holds a number and raises Object
|
|
// variable not set. A NULL object is passed on for the host to report.
|
|
static BasVmResultE uiTarget(BasVmT *vm, bool hasCallback, BasValueT target) {
|
|
if (!hasCallback) {
|
|
return noFormRuntime(vm);
|
|
}
|
|
|
|
if (target.type != BAS_TYPE_OBJECT) {
|
|
runtimeError(vm, BAS_ERR_OBJECT_NOT_SET, "Object variable not set");
|
|
return BAS_VM_ERROR;
|
|
}
|
|
|
|
return BAS_VM_OK;
|
|
}
|
|
|
|
|
|
// Length of the PRINT USING field starting at fmt[pos], or 0 when the
|
|
// character there is literal text. String fields are '!', '&' and a
|
|
// backslash pair; numeric fields start with '#', '.', "**", "$$" or a
|
|
// '+' followed by one of those, run over the digit, comma and point
|
|
// positions, and may end in "^^^^" and a trailing sign.
|
|
static int32_t usingFieldLen(const char *fmt, int32_t fmtLen, int32_t pos) {
|
|
char c = fmt[pos];
|
|
|
|
if (c == '!' || c == '&') {
|
|
return 1;
|
|
}
|
|
|
|
if (c == '\\') {
|
|
for (int32_t i = pos + 1; i < fmtLen; i++) {
|
|
if (fmt[i] == '\\') {
|
|
return i - pos + 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int32_t i = pos;
|
|
bool hasFill = false;
|
|
bool leadPlus = (c == '+');
|
|
|
|
if (leadPlus) {
|
|
i++;
|
|
}
|
|
|
|
if (i + 1 < fmtLen && fmt[i] == '*' && fmt[i + 1] == '*') {
|
|
hasFill = true;
|
|
i += 2;
|
|
}
|
|
|
|
if (i + 1 < fmtLen && fmt[i] == '$' && fmt[i + 1] == '$') {
|
|
hasFill = true;
|
|
i += 2;
|
|
}
|
|
|
|
// A numeric field needs a digit position ('#' or ".#") or a fill;
|
|
// a lone '+' (for example the end of "Total+") is literal text.
|
|
if (!hasFill && (i >= fmtLen || (fmt[i] != '#' && !(fmt[i] == '.' && i + 1 < fmtLen && fmt[i + 1] == '#')))) {
|
|
return 0;
|
|
}
|
|
|
|
while (i < fmtLen && (fmt[i] == '#' || fmt[i] == '0' || fmt[i] == ',' || fmt[i] == '.')) {
|
|
i++;
|
|
}
|
|
|
|
if (i + (int32_t)strlen(BAS_SCI_MARKER) <= fmtLen && strncmp(fmt + i, BAS_SCI_MARKER, strlen(BAS_SCI_MARKER)) == 0) {
|
|
i += (int32_t)strlen(BAS_SCI_MARKER);
|
|
}
|
|
|
|
if (!leadPlus && i < fmtLen && (fmt[i] == '+' || fmt[i] == '-')) {
|
|
i++;
|
|
}
|
|
|
|
return i - pos;
|
|
}
|
|
|
|
|
|
// Formats one PRINT USING value: emits the literal text from the current
|
|
// scan position up to the next field, the field rendered from val, and,
|
|
// for the last value of the statement, the literal text after it. The
|
|
// scan wraps to the start of the format when it runs out of fields.
|
|
// Returns NULL when the field type does not match the value.
|
|
static BasStringT *usingFormat(BasVmT *vm, const BasStringT *fmt, BasValueT val, bool last) {
|
|
int32_t valLen = (val.type == BAS_TYPE_STRING && val.strVal) ? val.strVal->len : 0;
|
|
int32_t cap = fmt->len * 2 + valLen + BAS_FORMAT_BUF_SIZE + 1;
|
|
char *buf = (char *)malloc(cap);
|
|
|
|
if (!buf) {
|
|
return basStringRef(basEmptyString);
|
|
}
|
|
|
|
int32_t idx = 0;
|
|
int32_t limit = cap - 1;
|
|
int32_t fieldLen = 0;
|
|
bool wrapped = false;
|
|
|
|
// Literal text up to the next field; wrap at most once.
|
|
while (vm->usingPos < fmt->len || !wrapped) {
|
|
if (vm->usingPos >= fmt->len) {
|
|
wrapped = true;
|
|
vm->usingPos = 0;
|
|
|
|
if (fmt->len == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
char c = fmt->data[vm->usingPos];
|
|
|
|
if (c == BAS_USING_ESCAPE && vm->usingPos + 1 < fmt->len) {
|
|
putBounded(buf, &idx, limit, fmt->data[vm->usingPos + 1]);
|
|
vm->usingPos += 2;
|
|
continue;
|
|
}
|
|
|
|
fieldLen = usingFieldLen(fmt->data, fmt->len, vm->usingPos);
|
|
|
|
if (fieldLen > 0) {
|
|
break;
|
|
}
|
|
|
|
putBounded(buf, &idx, limit, c);
|
|
vm->usingPos++;
|
|
}
|
|
|
|
if (fieldLen == 0) {
|
|
// No field anywhere in the format: the value prints as-is.
|
|
BasStringT *plain = basValFormatString(val);
|
|
|
|
for (int32_t i = 0; i < plain->len; i++) {
|
|
putBounded(buf, &idx, limit, plain->data[i]);
|
|
}
|
|
|
|
basStringUnref(plain);
|
|
buf[idx] = '\0';
|
|
|
|
BasStringT *result = basStringNew(buf, idx);
|
|
free(buf);
|
|
return result;
|
|
}
|
|
|
|
const char *field = fmt->data + vm->usingPos;
|
|
bool strField = (field[0] == '!' || field[0] == '&' || field[0] == '\\');
|
|
|
|
vm->usingPos += fieldLen;
|
|
|
|
if (strField != (val.type == BAS_TYPE_STRING)) {
|
|
free(buf);
|
|
return NULL;
|
|
}
|
|
|
|
if (strField) {
|
|
const char *src = val.strVal ? val.strVal->data : "";
|
|
int32_t srcLen = val.strVal ? val.strVal->len : 0;
|
|
int32_t width = (field[0] == '!') ? 1 : (field[0] == '&') ? srcLen : fieldLen;
|
|
|
|
for (int32_t i = 0; i < width; i++) {
|
|
putBounded(buf, &idx, limit, (i < srcLen) ? src[i] : ' ');
|
|
}
|
|
} else {
|
|
double n = basValToNumber(val);
|
|
BasNumFormatT f;
|
|
bool sciNotation;
|
|
char numBuf[BAS_FORMAT_BUF_SIZE];
|
|
|
|
parseNumFormat(field, fieldLen, &f, &sciNotation);
|
|
f.markOverflow = true;
|
|
|
|
if (sciNotation) {
|
|
char sciFmt[BAS_SCI_FMT_LEN];
|
|
int32_t decimals = f.hasDecimal ? f.digitsAfter : 0;
|
|
|
|
snprintf(sciFmt, sizeof(sciFmt), "%%%d.%dE", (int)fieldLen, (int)decimals);
|
|
snprintf(numBuf, sizeof(numBuf), sciFmt, n);
|
|
} else {
|
|
formatNumber(n, &f, numBuf, sizeof(numBuf));
|
|
}
|
|
|
|
for (int32_t i = 0; numBuf[i]; i++) {
|
|
putBounded(buf, &idx, limit, numBuf[i]);
|
|
}
|
|
}
|
|
|
|
// Trailing literal text after the last value, up to the next field.
|
|
if (last) {
|
|
while (vm->usingPos < fmt->len) {
|
|
char c = fmt->data[vm->usingPos];
|
|
|
|
if (c == BAS_USING_ESCAPE && vm->usingPos + 1 < fmt->len) {
|
|
putBounded(buf, &idx, limit, fmt->data[vm->usingPos + 1]);
|
|
vm->usingPos += 2;
|
|
continue;
|
|
}
|
|
|
|
if (usingFieldLen(fmt->data, fmt->len, vm->usingPos) > 0) {
|
|
break;
|
|
}
|
|
|
|
putBounded(buf, &idx, limit, c);
|
|
vm->usingPos++;
|
|
}
|
|
}
|
|
|
|
buf[idx] = '\0';
|
|
|
|
BasStringT *result = basStringNew(buf, idx);
|
|
free(buf);
|
|
return result;
|
|
}
|
|
|
|
|
|
static bool validArrayDims(BasVmT *vm, int32_t dims) {
|
|
if (dims < 1 || dims > BAS_ARRAY_MAX_DIMS) {
|
|
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid array dimension count");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// VARTYPE of a type tag (VB's vbInteger ... vbVariant).
|
|
static int32_t varTypeOf(uint8_t type) {
|
|
switch (type) {
|
|
case BAS_TYPE_EMPTY:
|
|
return BAS_VT_EMPTY;
|
|
|
|
case BAS_TYPE_NULL:
|
|
return BAS_VT_NULL;
|
|
|
|
case BAS_TYPE_INTEGER:
|
|
return BAS_VT_INTEGER;
|
|
|
|
case BAS_TYPE_LONG:
|
|
return BAS_VT_LONG;
|
|
|
|
case BAS_TYPE_SINGLE:
|
|
return BAS_VT_SINGLE;
|
|
|
|
case BAS_TYPE_DOUBLE:
|
|
return BAS_VT_DOUBLE;
|
|
|
|
case BAS_TYPE_DATE:
|
|
return BAS_VT_DATE;
|
|
|
|
case BAS_TYPE_STRING:
|
|
return BAS_VT_STRING;
|
|
|
|
case BAS_TYPE_BOOLEAN:
|
|
return BAS_VT_BOOLEAN;
|
|
|
|
case BAS_TYPE_VARIANT:
|
|
return BAS_VT_VARIANT;
|
|
|
|
case BAS_TYPE_UDT:
|
|
return BAS_VT_USERTYPE;
|
|
|
|
default:
|
|
return BAS_VT_OBJECT;
|
|
}
|
|
}
|
|
|
|
|
|
// Keep the host's events running until busy says the wait is over (or
|
|
// just check again after a pause without an event pump). False when the
|
|
// host asks the program to stop.
|
|
static bool waitWhile(BasVmT *vm, bool (*busy)(void *ctx)) {
|
|
while (busy(vm->device.ctx)) {
|
|
if (!vm->doEventsFn) {
|
|
usleep(BAS_WAIT_POLL_USEC);
|
|
} else if (!basVmDoEvents(vm)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Wall-clock time in seconds (sub-second resolution), for SLEEP deadlines.
|
|
static double wallSeconds(void) {
|
|
struct timeval tv;
|
|
|
|
gettimeofday(&tv, NULL);
|
|
return (double)tv.tv_sec + (double)tv.tv_usec / BAS_USEC_PER_SEC;
|
|
}
|
|
|
|
|
|
// The declared zero of a data type: "" for strings, False for booleans,
|
|
// a typed numeric zero otherwise.
|
|
static BasValueT zeroValue(uint8_t type) {
|
|
switch (type) {
|
|
case BAS_TYPE_STRING:
|
|
return strValue(basStringRef(basEmptyString));
|
|
|
|
case BAS_TYPE_BOOLEAN:
|
|
return basValBool(false);
|
|
|
|
case BAS_TYPE_LONG:
|
|
return basValLong(0);
|
|
|
|
case BAS_TYPE_SINGLE:
|
|
return basValSingle(0.0f);
|
|
|
|
case BAS_TYPE_DOUBLE:
|
|
return basValDouble(0.0);
|
|
|
|
case BAS_TYPE_DATE:
|
|
return basValDate(0.0);
|
|
|
|
case BAS_TYPE_VARIANT:
|
|
return basValEmpty();
|
|
|
|
case BAS_TYPE_OBJECT:
|
|
return basValObject(NULL);
|
|
|
|
default:
|
|
return basValInteger(0);
|
|
}
|
|
}
|