From da84c2a599dba1b5cbb34c3cac48741daa9a35de Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Fri, 27 Mar 2026 00:40:17 -0500 Subject: [PATCH] Initial DVX BASIC Compiler and VM. --- dvxbasic/compiler/codegen.c | 243 ++ dvxbasic/compiler/codegen.h | 76 + dvxbasic/compiler/lexer.c | 820 +++++++ dvxbasic/compiler/lexer.h | 221 ++ dvxbasic/compiler/opcodes.h | 287 +++ dvxbasic/compiler/parser.c | 4324 +++++++++++++++++++++++++++++++++++ dvxbasic/compiler/parser.h | 57 + dvxbasic/compiler/symtab.c | 147 ++ dvxbasic/compiler/symtab.h | 129 ++ dvxbasic/runtime/values.c | 633 +++++ dvxbasic/runtime/values.h | 180 ++ dvxbasic/runtime/vm.c | 3514 ++++++++++++++++++++++++++++ dvxbasic/runtime/vm.h | 211 ++ dvxbasic/test_compiler | Bin 0 -> 146984 bytes dvxbasic/test_compiler.c | 850 +++++++ dvxbasic/test_lex | Bin 0 -> 20640 bytes dvxbasic/test_lex.c | 24 + dvxbasic/test_quick | Bin 0 -> 109224 bytes dvxbasic/test_quick.c | 64 + dvxbasic/test_vm | Bin 0 -> 44736 bytes dvxbasic/test_vm.c | 234 ++ 21 files changed, 12014 insertions(+) create mode 100644 dvxbasic/compiler/codegen.c create mode 100644 dvxbasic/compiler/codegen.h create mode 100644 dvxbasic/compiler/lexer.c create mode 100644 dvxbasic/compiler/lexer.h create mode 100644 dvxbasic/compiler/opcodes.h create mode 100644 dvxbasic/compiler/parser.c create mode 100644 dvxbasic/compiler/parser.h create mode 100644 dvxbasic/compiler/symtab.c create mode 100644 dvxbasic/compiler/symtab.h create mode 100644 dvxbasic/runtime/values.c create mode 100644 dvxbasic/runtime/values.h create mode 100644 dvxbasic/runtime/vm.c create mode 100644 dvxbasic/runtime/vm.h create mode 100755 dvxbasic/test_compiler create mode 100644 dvxbasic/test_compiler.c create mode 100755 dvxbasic/test_lex create mode 100644 dvxbasic/test_lex.c create mode 100755 dvxbasic/test_quick create mode 100644 dvxbasic/test_quick.c create mode 100755 dvxbasic/test_vm create mode 100644 dvxbasic/test_vm.c diff --git a/dvxbasic/compiler/codegen.c b/dvxbasic/compiler/codegen.c new file mode 100644 index 0000000..6da847f --- /dev/null +++ b/dvxbasic/compiler/codegen.c @@ -0,0 +1,243 @@ +// codegen.c -- DVX BASIC p-code emitter implementation + +#include "codegen.h" +#include "opcodes.h" + +#include +#include + +// ============================================================ +// basAddData +// ============================================================ + +bool basAddData(BasCodeGenT *cg, BasValueT val) { + if (cg->dataCount >= BAS_MAX_CONSTANTS) { + return false; + } + + cg->dataPool[cg->dataCount++] = basValCopy(val); + return true; +} + + +// ============================================================ +// basAddConstant +// ============================================================ + +uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) { + // Check if this string is already in the pool + for (int32_t i = 0; i < cg->constCount; i++) { + if (cg->constants[i]->len == len && memcmp(cg->constants[i]->data, text, len) == 0) { + return (uint16_t)i; + } + } + + if (cg->constCount >= BAS_MAX_CONSTANTS) { + return 0; + } + + uint16_t idx = (uint16_t)cg->constCount; + cg->constants[cg->constCount++] = basStringNew(text, len); + return idx; +} + + +// ============================================================ +// basCodeGenBuildModule +// ============================================================ + +BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) { + BasModuleT *mod = (BasModuleT *)calloc(1, sizeof(BasModuleT)); + + if (!mod) { + return NULL; + } + + // Copy code + mod->code = (uint8_t *)malloc(cg->codeLen); + + if (!mod->code) { + free(mod); + return NULL; + } + + memcpy(mod->code, cg->code, cg->codeLen); + mod->codeLen = cg->codeLen; + + // Copy constant pool (share string refs) + if (cg->constCount > 0) { + mod->constants = (BasStringT **)malloc(cg->constCount * sizeof(BasStringT *)); + + if (!mod->constants) { + free(mod->code); + free(mod); + return NULL; + } + + for (int32_t i = 0; i < cg->constCount; i++) { + mod->constants[i] = basStringRef(cg->constants[i]); + } + } + + mod->constCount = cg->constCount; + mod->globalCount = cg->globalCount; + mod->entryPoint = 0; + + // Copy data pool + if (cg->dataCount > 0) { + mod->dataPool = (BasValueT *)malloc(cg->dataCount * sizeof(BasValueT)); + + if (!mod->dataPool) { + free(mod->constants); + free(mod->code); + free(mod); + return NULL; + } + + for (int32_t i = 0; i < cg->dataCount; i++) { + mod->dataPool[i] = basValCopy(cg->dataPool[i]); + } + } + + mod->dataCount = cg->dataCount; + + return mod; +} + + +// ============================================================ +// basCodeGenFree +// ============================================================ + +void basCodeGenFree(BasCodeGenT *cg) { + for (int32_t i = 0; i < cg->constCount; i++) { + basStringUnref(cg->constants[i]); + } + + for (int32_t i = 0; i < cg->dataCount; i++) { + basValRelease(&cg->dataPool[i]); + } + + cg->constCount = 0; + cg->dataCount = 0; + cg->codeLen = 0; +} + + +// ============================================================ +// basCodeGenInit +// ============================================================ + +void basCodeGenInit(BasCodeGenT *cg) { + memset(cg, 0, sizeof(*cg)); +} + + +// ============================================================ +// basCodePos +// ============================================================ + +int32_t basCodePos(const BasCodeGenT *cg) { + return cg->codeLen; +} + + +// ============================================================ +// basEmit8 +// ============================================================ + +void basEmit8(BasCodeGenT *cg, uint8_t b) { + if (cg->codeLen < BAS_MAX_CODE) { + cg->code[cg->codeLen++] = b; + } +} + + +// ============================================================ +// basEmit16 +// ============================================================ + +void basEmit16(BasCodeGenT *cg, int16_t v) { + if (cg->codeLen + 2 <= BAS_MAX_CODE) { + memcpy(&cg->code[cg->codeLen], &v, 2); + cg->codeLen += 2; + } +} + + +// ============================================================ +// basEmitDouble +// ============================================================ + +void basEmitDouble(BasCodeGenT *cg, double v) { + if (cg->codeLen + (int32_t)sizeof(double) <= BAS_MAX_CODE) { + memcpy(&cg->code[cg->codeLen], &v, sizeof(double)); + cg->codeLen += (int32_t)sizeof(double); + } +} + + +// ============================================================ +// basEmitFloat +// ============================================================ + +void basEmitFloat(BasCodeGenT *cg, float v) { + if (cg->codeLen + (int32_t)sizeof(float) <= BAS_MAX_CODE) { + memcpy(&cg->code[cg->codeLen], &v, sizeof(float)); + cg->codeLen += (int32_t)sizeof(float); + } +} + + +// ============================================================ +// basEmitU16 +// ============================================================ + +void basEmitU16(BasCodeGenT *cg, uint16_t v) { + if (cg->codeLen + 2 <= BAS_MAX_CODE) { + memcpy(&cg->code[cg->codeLen], &v, 2); + cg->codeLen += 2; + } +} + + +// ============================================================ +// basModuleFree +// ============================================================ + +void basModuleFree(BasModuleT *mod) { + if (!mod) { + return; + } + + free(mod->code); + + if (mod->constants) { + for (int32_t i = 0; i < mod->constCount; i++) { + basStringUnref(mod->constants[i]); + } + + free(mod->constants); + } + + if (mod->dataPool) { + for (int32_t i = 0; i < mod->dataCount; i++) { + basValRelease(&mod->dataPool[i]); + } + + free(mod->dataPool); + } + + free(mod); +} + + +// ============================================================ +// basPatch16 +// ============================================================ + +void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val) { + if (pos >= 0 && pos + 2 <= cg->codeLen) { + memcpy(&cg->code[pos], &val, 2); + } +} diff --git a/dvxbasic/compiler/codegen.h b/dvxbasic/compiler/codegen.h new file mode 100644 index 0000000..31da482 --- /dev/null +++ b/dvxbasic/compiler/codegen.h @@ -0,0 +1,76 @@ +// codegen.h -- DVX BASIC p-code emitter +// +// Builds a p-code byte stream and string constant pool from +// calls made by the parser. Provides helpers for backpatching +// forward jumps. +// +// Embeddable: no DVX dependencies, pure C. + +#ifndef DVXBASIC_CODEGEN_H +#define DVXBASIC_CODEGEN_H + +#include "../runtime/vm.h" +#include "../runtime/values.h" + +#include +#include + +// ============================================================ +// Code generator state +// ============================================================ + +#define BAS_MAX_CODE 65536 +#define BAS_MAX_CONSTANTS 1024 + +typedef struct { + uint8_t code[BAS_MAX_CODE]; + int32_t codeLen; + BasStringT *constants[BAS_MAX_CONSTANTS]; + int32_t constCount; + int32_t globalCount; + BasValueT dataPool[BAS_MAX_CONSTANTS]; + int32_t dataCount; +} BasCodeGenT; + +// ============================================================ +// API +// ============================================================ + +void basCodeGenInit(BasCodeGenT *cg); +void basCodeGenFree(BasCodeGenT *cg); + +// Emit single byte +void basEmit8(BasCodeGenT *cg, uint8_t b); + +// Emit 16-bit signed value +void basEmit16(BasCodeGenT *cg, int16_t v); + +// Emit 16-bit unsigned value +void basEmitU16(BasCodeGenT *cg, uint16_t v); + +// Emit 32-bit float +void basEmitFloat(BasCodeGenT *cg, float v); + +// Emit 64-bit double +void basEmitDouble(BasCodeGenT *cg, double v); + +// Get current code position (for jump targets) +int32_t basCodePos(const BasCodeGenT *cg); + +// Patch a 16-bit value at a previous position (for backpatching jumps) +void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val); + +// Add a string to the constant pool. Returns the pool index. +uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len); + +// Add a value to the data pool (for DATA statements). Returns true on success. +bool basAddData(BasCodeGenT *cg, BasValueT val); + +// Build a BasModuleT from the generated code. The caller takes +// ownership of the module and must free it with basModuleFree(). +BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg); + +// Free a module built by basCodeGenBuildModule. +void basModuleFree(BasModuleT *mod); + +#endif // DVXBASIC_CODEGEN_H diff --git a/dvxbasic/compiler/lexer.c b/dvxbasic/compiler/lexer.c new file mode 100644 index 0000000..b5d8e26 --- /dev/null +++ b/dvxbasic/compiler/lexer.c @@ -0,0 +1,820 @@ +// lexer.c -- DVX BASIC lexer implementation +// +// Single-pass tokenizer. Keywords are case-insensitive. Identifiers +// preserve their original case for display but comparisons are +// case-insensitive. Line continuations (underscore at end of line) +// are handled transparently. + +#include "lexer.h" + +#include +#include +#include +#include + +// ============================================================ +// Keyword table +// ============================================================ + +typedef struct { + const char *text; + BasTokenTypeE type; +} KeywordEntryT; + +static const KeywordEntryT sKeywords[] = { + { "AND", TOK_AND }, + { "APPEND", TOK_APPEND }, + { "AS", TOK_AS }, + { "BASE", TOK_BASE }, + { "BINARY", TOK_BINARY }, + { "BOOLEAN", TOK_BOOLEAN }, + { "BYVAL", TOK_BYVAL }, + { "CALL", TOK_CALL }, + { "CASE", TOK_CASE }, + { "CLOSE", TOK_CLOSE }, + { "CONST", TOK_CONST }, + { "DATA", TOK_DATA }, + { "DECLARE", TOK_DECLARE }, + { "DEF", TOK_DEF }, + { "DEFDBL", TOK_DEFDBL }, + { "DEFINT", TOK_DEFINT }, + { "DEFLNG", TOK_DEFLNG }, + { "DEFSNG", TOK_DEFSNG }, + { "DEFSTR", TOK_DEFSTR }, + { "DIM", TOK_DIM }, + { "DO", TOK_DO }, + { "DOEVENTS", TOK_DOEVENTS }, + { "DOUBLE", TOK_DOUBLE }, + { "ELSE", TOK_ELSE }, + { "ELSEIF", TOK_ELSEIF }, + { "END", TOK_END }, + { "EOF", TOK_EOF_KW }, + { "EQV", TOK_EQV }, + { "ERASE", TOK_ERASE }, + { "ERR", TOK_ERR }, + { "ERROR", TOK_ERROR_KW }, + { "EXPLICIT", TOK_EXPLICIT }, + { "EXIT", TOK_EXIT }, + { "FALSE", TOK_FALSE_KW }, + { "FOR", TOK_FOR }, + { "FUNCTION", TOK_FUNCTION }, + { "GET", TOK_GET }, + { "GOSUB", TOK_GOSUB }, + { "GOTO", TOK_GOTO }, + { "HIDE", TOK_HIDE }, + { "IF", TOK_IF }, + { "IMP", TOK_IMP }, + { "INPUT", TOK_INPUT }, + { "INTEGER", TOK_INTEGER }, + { "IS", TOK_IS }, + { "LBOUND", TOK_LBOUND }, + { "LET", TOK_LET }, + { "LINE", TOK_LINE }, + { "LOAD", TOK_LOAD }, + { "LONG", TOK_LONG }, + { "LOOP", TOK_LOOP }, + { "ME", TOK_ME }, + { "MOD", TOK_MOD }, + { "MSGBOX", TOK_MSGBOX }, + { "NEXT", TOK_NEXT }, + { "NOT", TOK_NOT }, + { "ON", TOK_ON }, + { "OPEN", TOK_OPEN }, + { "OPTION", TOK_OPTION }, + { "OR", TOK_OR }, + { "OUTPUT", TOK_OUTPUT }, + { "PRESERVE", TOK_PRESERVE }, + { "PRINT", TOK_PRINT }, + { "PUT", TOK_PUT }, + { "RANDOM", TOK_RANDOM }, + { "RANDOMIZE", TOK_RANDOMIZE }, + { "READ", TOK_READ }, + { "REDIM", TOK_REDIM }, + { "REM", TOK_REM }, + { "RESTORE", TOK_RESTORE }, + { "RESUME", TOK_RESUME }, + { "RETURN", TOK_RETURN }, + { "SEEK", TOK_SEEK }, + { "SELECT", TOK_SELECT }, + { "SET", TOK_SET }, + { "SHARED", TOK_SHARED }, + { "SHELL", TOK_SHELL }, + { "SHOW", TOK_SHOW }, + { "SINGLE", TOK_SINGLE }, + { "SLEEP", TOK_SLEEP }, + { "STATIC", TOK_STATIC }, + { "STEP", TOK_STEP }, + { "STRING", TOK_STRING_KW }, + { "SUB", TOK_SUB }, + { "SWAP", TOK_SWAP }, + { "THEN", TOK_THEN }, + { "TIMER", TOK_TIMER }, + { "TO", TOK_TO }, + { "TRUE", TOK_TRUE_KW }, + { "TYPE", TOK_TYPE }, + { "UBOUND", TOK_UBOUND }, + { "UNLOAD", TOK_UNLOAD }, + { "UNTIL", TOK_UNTIL }, + { "WEND", TOK_WEND }, + { "WHILE", TOK_WHILE }, + { "WITH", TOK_WITH }, + { "WRITE", TOK_WRITE }, + { "XOR", TOK_XOR }, + { NULL, TOK_ERROR } +}; + +#define KEYWORD_COUNT (sizeof(sKeywords) / sizeof(sKeywords[0]) - 1) + +// ============================================================ +// Prototypes +// ============================================================ + +static char advance(BasLexerT *lex); +static bool atEnd(const BasLexerT *lex); +static BasTokenTypeE lookupKeyword(const char *text, int32_t len); +static char peek(const BasLexerT *lex); +static char peekNext(const BasLexerT *lex); +static void setError(BasLexerT *lex, const char *msg); +static void skipLineComment(BasLexerT *lex); +static void skipWhitespace(BasLexerT *lex); +static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex); +static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex); +static BasTokenTypeE tokenizeNumber(BasLexerT *lex); +static BasTokenTypeE tokenizeString(BasLexerT *lex); +static char upperChar(char c); + + +// ============================================================ +// advance +// ============================================================ + +static char advance(BasLexerT *lex) { + if (atEnd(lex)) { + return '\0'; + } + + char c = lex->source[lex->pos++]; + + if (c == '\n') { + lex->line++; + lex->col = 1; + } else { + lex->col++; + } + + return c; +} + + +// ============================================================ +// atEnd +// ============================================================ + +static bool atEnd(const BasLexerT *lex) { + return lex->pos >= lex->sourceLen; +} + + +// ============================================================ +// basLexerInit +// ============================================================ + +void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen) { + memset(lex, 0, sizeof(*lex)); + lex->source = source; + lex->sourceLen = sourceLen; + lex->pos = 0; + lex->line = 1; + lex->col = 1; + + // Prime the first token + basLexerNext(lex); +} + + +// ============================================================ +// basLexerNext +// ============================================================ + +BasTokenTypeE basLexerNext(BasLexerT *lex) { + skipWhitespace(lex); + + lex->token.line = lex->line; + lex->token.col = lex->col; + lex->token.textLen = 0; + lex->token.text[0] = '\0'; + + if (atEnd(lex)) { + lex->token.type = TOK_EOF; + return TOK_EOF; + } + + char c = peek(lex); + + // Newline + if (c == '\n') { + advance(lex); + lex->token.type = TOK_NEWLINE; + lex->token.text[0] = '\n'; + lex->token.text[1] = '\0'; + lex->token.textLen = 1; + return TOK_NEWLINE; + } + + // Carriage return (handle CR, CRLF) + if (c == '\r') { + advance(lex); + + if (!atEnd(lex) && peek(lex) == '\n') { + advance(lex); + } + + lex->token.type = TOK_NEWLINE; + lex->token.text[0] = '\n'; + lex->token.text[1] = '\0'; + lex->token.textLen = 1; + return TOK_NEWLINE; + } + + // Comment (apostrophe) + if (c == '\'') { + skipLineComment(lex); + lex->token.type = TOK_NEWLINE; + lex->token.text[0] = '\n'; + lex->token.text[1] = '\0'; + lex->token.textLen = 1; + return TOK_NEWLINE; + } + + // String literal + if (c == '"') { + lex->token.type = tokenizeString(lex); + return lex->token.type; + } + + // Number + if (isdigit((unsigned char)c) || (c == '.' && isdigit((unsigned char)peekNext(lex)))) { + lex->token.type = tokenizeNumber(lex); + return lex->token.type; + } + + // Hex literal (&H...) + if (c == '&' && upperChar(peekNext(lex)) == 'H') { + lex->token.type = tokenizeHexLiteral(lex); + return lex->token.type; + } + + // Identifier or keyword + if (isalpha((unsigned char)c) || c == '_') { + lex->token.type = tokenizeIdentOrKeyword(lex); + return lex->token.type; + } + + // Single and multi-character operators/punctuation + advance(lex); + + switch (c) { + case '+': + lex->token.type = TOK_PLUS; + break; + + case '-': + lex->token.type = TOK_MINUS; + break; + + case '*': + lex->token.type = TOK_STAR; + break; + + case '/': + lex->token.type = TOK_SLASH; + break; + + case '\\': + lex->token.type = TOK_BACKSLASH; + break; + + case '^': + lex->token.type = TOK_CARET; + break; + + case '&': + lex->token.type = TOK_AMPERSAND; + break; + + case '(': + lex->token.type = TOK_LPAREN; + break; + + case ')': + lex->token.type = TOK_RPAREN; + break; + + case ',': + lex->token.type = TOK_COMMA; + break; + + case ';': + lex->token.type = TOK_SEMICOLON; + break; + + case ':': + lex->token.type = TOK_COLON; + break; + + case '.': + lex->token.type = TOK_DOT; + break; + + case '#': + lex->token.type = TOK_HASH; + break; + + case '=': + lex->token.type = TOK_EQ; + break; + + case '<': + if (!atEnd(lex) && peek(lex) == '>') { + advance(lex); + lex->token.type = TOK_NE; + } else if (!atEnd(lex) && peek(lex) == '=') { + advance(lex); + lex->token.type = TOK_LE; + } else { + lex->token.type = TOK_LT; + } + break; + + case '>': + if (!atEnd(lex) && peek(lex) == '=') { + advance(lex); + lex->token.type = TOK_GE; + } else { + lex->token.type = TOK_GT; + } + break; + + default: + setError(lex, "Unexpected character"); + lex->token.type = TOK_ERROR; + break; + } + + // Store the operator text + if (lex->token.type != TOK_ERROR) { + lex->token.text[0] = c; + lex->token.textLen = 1; + + if (lex->token.type == TOK_NE || lex->token.type == TOK_LE || lex->token.type == TOK_GE) { + lex->token.text[1] = lex->source[lex->pos - 1]; + lex->token.textLen = 2; + } + + lex->token.text[lex->token.textLen] = '\0'; + } + + return lex->token.type; +} + + +// ============================================================ +// basLexerPeek +// ============================================================ + +BasTokenTypeE basLexerPeek(const BasLexerT *lex) { + return lex->token.type; +} + + +// ============================================================ +// basTokenName +// ============================================================ + +const char *basTokenName(BasTokenTypeE type) { + switch (type) { + case TOK_INT_LIT: return "integer"; + case TOK_LONG_LIT: return "long"; + case TOK_FLOAT_LIT: return "float"; + case TOK_STRING_LIT: return "string"; + case TOK_IDENT: return "identifier"; + case TOK_DOT: return "'.'"; + case TOK_COMMA: return "','"; + case TOK_SEMICOLON: return "';'"; + case TOK_COLON: return "':'"; + case TOK_LPAREN: return "'('"; + case TOK_RPAREN: return "')'"; + case TOK_HASH: return "'#'"; + case TOK_PLUS: return "'+'"; + case TOK_MINUS: return "'-'"; + case TOK_STAR: return "'*'"; + case TOK_SLASH: return "'/'"; + case TOK_BACKSLASH: return "'\\'"; + case TOK_CARET: return "'^'"; + case TOK_AMPERSAND: return "'&'"; + case TOK_EQ: return "'='"; + case TOK_NE: return "'<>'"; + case TOK_LT: return "'<'"; + case TOK_GT: return "'>'"; + case TOK_LE: return "'<='"; + case TOK_GE: return "'>='"; + case TOK_NEWLINE: return "newline"; + case TOK_EOF: return "end of file"; + case TOK_ERROR: return "error"; + default: break; + } + + // Keywords + for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) { + if (sKeywords[i].type == type) { + return sKeywords[i].text; + } + } + + return "?"; +} + + +// ============================================================ +// lookupKeyword +// ============================================================ + +static BasTokenTypeE lookupKeyword(const char *text, int32_t len) { + // Case-insensitive keyword lookup + for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) { + const char *kw = sKeywords[i].text; + int32_t kwLen = (int32_t)strlen(kw); + + if (kwLen != len) { + continue; + } + + bool match = true; + + for (int32_t j = 0; j < len; j++) { + if (upperChar(text[j]) != kw[j]) { + match = false; + break; + } + } + + if (match) { + return sKeywords[i].type; + } + } + + return TOK_IDENT; +} + + +// ============================================================ +// peek +// ============================================================ + +static char peek(const BasLexerT *lex) { + if (atEnd(lex)) { + return '\0'; + } + + return lex->source[lex->pos]; +} + + +// ============================================================ +// peekNext +// ============================================================ + +static char peekNext(const BasLexerT *lex) { + if (lex->pos + 1 >= lex->sourceLen) { + return '\0'; + } + + return lex->source[lex->pos + 1]; +} + + +// ============================================================ +// setError +// ============================================================ + +static void setError(BasLexerT *lex, const char *msg) { + snprintf(lex->error, sizeof(lex->error), "Line %d, Col %d: %s", lex->line, lex->col, msg); +} + + +// ============================================================ +// skipLineComment +// ============================================================ + +static void skipLineComment(BasLexerT *lex) { + while (!atEnd(lex) && peek(lex) != '\n' && peek(lex) != '\r') { + advance(lex); + } +} + + +// ============================================================ +// skipWhitespace +// ============================================================ +// +// Skips spaces and tabs. Does NOT skip newlines (they are tokens). +// Handles line continuation: underscore followed by newline joins +// the next line to the current logical line. + +static void skipWhitespace(BasLexerT *lex) { + while (!atEnd(lex)) { + char c = peek(lex); + + if (c == ' ' || c == '\t') { + advance(lex); + continue; + } + + // Line continuation: _ at end of line + if (c == '_') { + int32_t savedPos = lex->pos; + int32_t savedLine = lex->line; + int32_t savedCol = lex->col; + advance(lex); + + // Skip spaces/tabs after underscore + while (!atEnd(lex) && (peek(lex) == ' ' || peek(lex) == '\t')) { + advance(lex); + } + + // Must be followed by newline + if (!atEnd(lex) && (peek(lex) == '\n' || peek(lex) == '\r')) { + advance(lex); + + if (!atEnd(lex) && peek(lex) == '\n' && lex->source[lex->pos - 1] == '\r') { + advance(lex); + } + + continue; // Continue skipping whitespace on next line + } + + // Not a continuation -- put back + lex->pos = savedPos; + lex->line = savedLine; + lex->col = savedCol; + break; + } + + break; + } +} + + +// ============================================================ +// tokenizeHexLiteral +// ============================================================ + +static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) { + advance(lex); // skip & + advance(lex); // skip H + + int32_t idx = 0; + int32_t value = 0; + + while (!atEnd(lex) && isxdigit((unsigned char)peek(lex))) { + char c = advance(lex); + + if (idx < BAS_MAX_TOKEN_LEN - 1) { + lex->token.text[idx++] = c; + } + + int32_t digit; + + if (c >= '0' && c <= '9') { + digit = c - '0'; + } else if (c >= 'A' && c <= 'F') { + digit = c - 'A' + 10; + } else { + digit = c - 'a' + 10; + } + + value = (value << 4) | digit; + } + + lex->token.text[idx] = '\0'; + lex->token.textLen = idx; + + // Check for trailing & (long suffix) + if (!atEnd(lex) && peek(lex) == '&') { + advance(lex); + lex->token.longVal = (int64_t)value; + return TOK_LONG_LIT; + } + + lex->token.intVal = value; + return TOK_INT_LIT; +} + + +// ============================================================ +// tokenizeIdentOrKeyword +// ============================================================ + +static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) { + int32_t idx = 0; + + while (!atEnd(lex) && (isalnum((unsigned char)peek(lex)) || peek(lex) == '_')) { + char c = advance(lex); + + if (idx < BAS_MAX_TOKEN_LEN - 1) { + lex->token.text[idx++] = c; + } + } + + lex->token.text[idx] = '\0'; + lex->token.textLen = idx; + + // Check for type suffix + if (!atEnd(lex)) { + char c = peek(lex); + + if (c == '%' || c == '&' || c == '!' || c == '#' || c == '$') { + advance(lex); + lex->token.text[idx++] = c; + lex->token.text[idx] = '\0'; + lex->token.textLen = idx; + } + } + + // Check if this is a keyword + // For suffix-bearing identifiers, only check the base (without suffix) + int32_t baseLen = idx; + + if (baseLen > 0) { + char last = lex->token.text[baseLen - 1]; + + if (last == '%' || last == '&' || last == '!' || last == '#' || last == '$') { + baseLen--; + } + } + + BasTokenTypeE kwType = lookupKeyword(lex->token.text, baseLen); + + // REM is a comment -- skip to end of line + if (kwType == TOK_REM) { + skipLineComment(lex); + lex->token.type = TOK_NEWLINE; + lex->token.text[0] = '\n'; + lex->token.text[1] = '\0'; + lex->token.textLen = 1; + return TOK_NEWLINE; + } + + // If it's a keyword and has no suffix, return the keyword token + if (kwType != TOK_IDENT && baseLen == idx) { + return kwType; + } + + return TOK_IDENT; +} + + +// ============================================================ +// tokenizeNumber +// ============================================================ + +static BasTokenTypeE tokenizeNumber(BasLexerT *lex) { + int32_t idx = 0; + bool hasDecimal = false; + bool hasExp = false; + + // Integer part + while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) { + if (idx < BAS_MAX_TOKEN_LEN - 1) { + lex->token.text[idx++] = advance(lex); + } else { + advance(lex); + } + } + + // Decimal part + if (!atEnd(lex) && peek(lex) == '.' && isdigit((unsigned char)peekNext(lex))) { + hasDecimal = true; + lex->token.text[idx++] = advance(lex); // . + + while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) { + if (idx < BAS_MAX_TOKEN_LEN - 1) { + lex->token.text[idx++] = advance(lex); + } else { + advance(lex); + } + } + } + + // Exponent + if (!atEnd(lex) && (upperChar(peek(lex)) == 'E' || upperChar(peek(lex)) == 'D')) { + hasExp = true; + lex->token.text[idx++] = advance(lex); + + if (!atEnd(lex) && (peek(lex) == '+' || peek(lex) == '-')) { + lex->token.text[idx++] = advance(lex); + } + + while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) { + if (idx < BAS_MAX_TOKEN_LEN - 1) { + lex->token.text[idx++] = advance(lex); + } else { + advance(lex); + } + } + } + + lex->token.text[idx] = '\0'; + lex->token.textLen = idx; + + // Check for type suffix + if (!atEnd(lex)) { + char c = peek(lex); + + if (c == '%') { + advance(lex); + lex->token.intVal = (int32_t)atoi(lex->token.text); + return TOK_INT_LIT; + } + + if (c == '&') { + advance(lex); + lex->token.longVal = (int64_t)atol(lex->token.text); + return TOK_LONG_LIT; + } + + if (c == '!') { + advance(lex); + lex->token.dblVal = atof(lex->token.text); + return TOK_FLOAT_LIT; + } + + if (c == '#') { + advance(lex); + lex->token.dblVal = atof(lex->token.text); + return TOK_FLOAT_LIT; + } + } + + // No suffix: determine type from content + if (hasDecimal || hasExp) { + lex->token.dblVal = atof(lex->token.text); + return TOK_FLOAT_LIT; + } + + long val = atol(lex->token.text); + + if (val >= -32768 && val <= 32767) { + lex->token.intVal = (int32_t)val; + return TOK_INT_LIT; + } + + lex->token.longVal = (int64_t)val; + return TOK_LONG_LIT; +} + + +// ============================================================ +// tokenizeString +// ============================================================ + +static BasTokenTypeE tokenizeString(BasLexerT *lex) { + advance(lex); // skip opening quote + + int32_t idx = 0; + + while (!atEnd(lex) && peek(lex) != '"' && peek(lex) != '\n' && peek(lex) != '\r') { + if (idx < BAS_MAX_TOKEN_LEN - 1) { + lex->token.text[idx++] = advance(lex); + } else { + advance(lex); + } + } + + if (atEnd(lex) || peek(lex) != '"') { + setError(lex, "Unterminated string literal"); + lex->token.text[idx] = '\0'; + lex->token.textLen = idx; + return TOK_ERROR; + } + + advance(lex); // skip closing quote + + lex->token.text[idx] = '\0'; + lex->token.textLen = idx; + + return TOK_STRING_LIT; +} + + +// ============================================================ +// upperChar +// ============================================================ + +static char upperChar(char c) { + if (c >= 'a' && c <= 'z') { + return c - 32; + } + + return c; +} diff --git a/dvxbasic/compiler/lexer.h b/dvxbasic/compiler/lexer.h new file mode 100644 index 0000000..f53922e --- /dev/null +++ b/dvxbasic/compiler/lexer.h @@ -0,0 +1,221 @@ +// lexer.h -- DVX BASIC lexer (tokenizer) +// +// Converts BASIC source text into a stream of tokens. Case-insensitive +// for keywords. Handles line continuations (_), comments (' and REM), +// type suffixes (%, &, !, #, $), and string literals. +// +// Embeddable: no DVX dependencies, pure C. + +#ifndef DVXBASIC_LEXER_H +#define DVXBASIC_LEXER_H + +#include +#include + +// ============================================================ +// Token types +// ============================================================ + +typedef enum { + // Literals + TOK_INT_LIT, // integer literal (123, &HFF) + TOK_LONG_LIT, // long literal (123&) + TOK_FLOAT_LIT, // float literal (3.14, 1.5E10) + TOK_STRING_LIT, // "string literal" + + // Identifiers and symbols + TOK_IDENT, // variable/function name + TOK_DOT, // . + TOK_COMMA, // , + TOK_SEMICOLON, // ; + TOK_COLON, // : + TOK_LPAREN, // ( + TOK_RPAREN, // ) + TOK_HASH, // # (file channel) + + // Operators + TOK_PLUS, // + + TOK_MINUS, // - + TOK_STAR, // * + TOK_SLASH, // / + TOK_BACKSLASH, // \ (integer divide) + TOK_CARET, // ^ + TOK_AMPERSAND, // & (string concat or hex prefix) + TOK_EQ, // = + TOK_NE, // <> + TOK_LT, // < + TOK_GT, // > + TOK_LE, // <= + TOK_GE, // >= + + // Type suffixes (attached to identifier) + TOK_SUFFIX_INT, // % + TOK_SUFFIX_LONG, // & + TOK_SUFFIX_SINGLE, // ! + TOK_SUFFIX_DOUBLE, // # + TOK_SUFFIX_STRING, // $ + + // Keywords + TOK_AND, + TOK_AS, + TOK_BASE, + TOK_BOOLEAN, + TOK_BYVAL, + TOK_CALL, + TOK_CASE, + TOK_CLOSE, + TOK_CONST, + TOK_DATA, + TOK_DECLARE, + TOK_DEF, + TOK_DEFDBL, + TOK_DEFINT, + TOK_DEFLNG, + TOK_DEFSNG, + TOK_DEFSTR, + TOK_DIM, + TOK_DO, + TOK_DOEVENTS, + TOK_DOUBLE, + TOK_ELSE, + TOK_ELSEIF, + TOK_END, + TOK_EOF_KW, // EOF (keyword, not end-of-file) + TOK_EQV, + TOK_ERASE, + TOK_ERR, + TOK_ERROR_KW, + TOK_EXPLICIT, + TOK_EXIT, + TOK_FALSE_KW, + TOK_FOR, + TOK_FUNCTION, + TOK_GET, + TOK_GOSUB, + TOK_GOTO, + TOK_HIDE, + TOK_IF, + TOK_IMP, + TOK_INPUT, + TOK_INTEGER, + TOK_IS, + TOK_LBOUND, + TOK_LET, + TOK_LINE, + TOK_LOAD, + TOK_LONG, + TOK_LOOP, + TOK_ME, + TOK_MOD, + TOK_MSGBOX, + TOK_NEXT, + TOK_NOT, + TOK_ON, + TOK_OPEN, + TOK_OPTION, + TOK_OR, + TOK_OUTPUT, + TOK_PRESERVE, + TOK_PRINT, + TOK_PUT, + TOK_RANDOMIZE, + TOK_READ, + TOK_REDIM, + TOK_REM, + TOK_RESTORE, + TOK_RESUME, + TOK_RETURN, + TOK_SEEK, + TOK_SELECT, + TOK_SET, + TOK_SHARED, + TOK_SHELL, + TOK_SHOW, + TOK_SINGLE, + TOK_SLEEP, + TOK_STATIC, + TOK_STEP, + TOK_STRING_KW, + TOK_SUB, + TOK_SWAP, + TOK_THEN, + TOK_TIMER, + TOK_TO, + TOK_TRUE_KW, + TOK_TYPE, + TOK_UBOUND, + TOK_UNLOAD, + TOK_UNTIL, + TOK_WEND, + TOK_WHILE, + TOK_WITH, + TOK_WRITE, + TOK_XOR, + + // File modes + TOK_APPEND, + TOK_BINARY, + TOK_RANDOM, + + // Special + TOK_NEWLINE, // end of logical line + TOK_EOF, // end of source + TOK_ERROR // lexer error +} BasTokenTypeE; + +// ============================================================ +// Token +// ============================================================ + +#define BAS_MAX_TOKEN_LEN 256 + +typedef struct { + BasTokenTypeE type; + int32_t line; // 1-based source line number + int32_t col; // 1-based column number + + // Value (depends on type) + union { + int32_t intVal; + int64_t longVal; + float fltVal; + double dblVal; + }; + + char text[BAS_MAX_TOKEN_LEN]; // raw text of the token + int32_t textLen; +} BasTokenT; + +// ============================================================ +// Lexer state +// ============================================================ + +typedef struct { + const char *source; // source text (not owned) + int32_t sourceLen; + int32_t pos; // current position in source + int32_t line; // current line (1-based) + int32_t col; // current column (1-based) + BasTokenT token; // current token + char error[256]; +} BasLexerT; + +// ============================================================ +// API +// ============================================================ + +// Initialize lexer with source text. The source must remain valid +// for the lifetime of the lexer. +void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen); + +// Advance to the next token. Returns the token type. +// The token is available in lex->token. +BasTokenTypeE basLexerNext(BasLexerT *lex); + +// Peek at the current token type without advancing. +BasTokenTypeE basLexerPeek(const BasLexerT *lex); + +// Return human-readable name for a token type. +const char *basTokenName(BasTokenTypeE type); + +#endif // DVXBASIC_LEXER_H diff --git a/dvxbasic/compiler/opcodes.h b/dvxbasic/compiler/opcodes.h new file mode 100644 index 0000000..a2018e3 --- /dev/null +++ b/dvxbasic/compiler/opcodes.h @@ -0,0 +1,287 @@ +// opcodes.h -- DVX BASIC bytecode instruction definitions +// +// Stack-based p-code for the DVX BASIC virtual machine. +// Embeddable: no DVX dependencies, pure C. + +#ifndef DVXBASIC_OPCODES_H +#define DVXBASIC_OPCODES_H + +// ============================================================ +// Data type tags (used in Value representation) +// ============================================================ + +#define BAS_TYPE_INTEGER 0 // 16-bit signed +#define BAS_TYPE_LONG 1 // 32-bit signed +#define BAS_TYPE_SINGLE 2 // 32-bit float +#define BAS_TYPE_DOUBLE 3 // 64-bit float +#define BAS_TYPE_STRING 4 // ref-counted dynamic string +#define BAS_TYPE_BOOLEAN 5 // True (-1) or False (0) +#define BAS_TYPE_ARRAY 6 // ref-counted array +#define BAS_TYPE_UDT 7 // ref-counted user-defined type + +// ============================================================ +// Stack operations +// ============================================================ + +#define OP_NOP 0x00 +#define OP_PUSH_INT16 0x01 // [int16] push 16-bit integer +#define OP_PUSH_INT32 0x02 // [int32] push 32-bit integer +#define OP_PUSH_FLT32 0x03 // [float32] push 32-bit float +#define OP_PUSH_FLT64 0x04 // [float64] push 64-bit float +#define OP_PUSH_STR 0x05 // [uint16 idx] push string from constant pool +#define OP_PUSH_TRUE 0x06 // push boolean True (-1) +#define OP_PUSH_FALSE 0x07 // push boolean False (0) +#define OP_POP 0x08 // discard top of stack +#define OP_DUP 0x09 // duplicate top of stack + +// ============================================================ +// Variable access +// ============================================================ + +#define OP_LOAD_LOCAL 0x10 // [uint16 idx] push local variable +#define OP_STORE_LOCAL 0x11 // [uint16 idx] pop to local variable +#define OP_LOAD_GLOBAL 0x12 // [uint16 idx] push global variable +#define OP_STORE_GLOBAL 0x13 // [uint16 idx] pop to global variable +#define OP_LOAD_REF 0x14 // dereference top of stack (ByRef) +#define OP_STORE_REF 0x15 // store through reference on stack +#define OP_LOAD_ARRAY 0x16 // [uint8 dims] indices on stack, array ref below +#define OP_STORE_ARRAY 0x17 // [uint8 dims] value, indices, array ref on stack +#define OP_LOAD_FIELD 0x18 // [uint16 fieldIdx] load UDT field +#define OP_STORE_FIELD 0x19 // [uint16 fieldIdx] store UDT field +#define OP_PUSH_LOCAL_ADDR 0x1A // [uint16 idx] push address of local (for ByRef) +#define OP_PUSH_GLOBAL_ADDR 0x1B // [uint16 idx] push address of global (for ByRef) + +// ============================================================ +// Arithmetic (integer) +// ============================================================ + +#define OP_ADD_INT 0x20 +#define OP_SUB_INT 0x21 +#define OP_MUL_INT 0x22 +#define OP_IDIV_INT 0x23 // integer divide (\) +#define OP_MOD_INT 0x24 +#define OP_NEG_INT 0x25 + +// ============================================================ +// Arithmetic (float) +// ============================================================ + +#define OP_ADD_FLT 0x26 +#define OP_SUB_FLT 0x27 +#define OP_MUL_FLT 0x28 +#define OP_DIV_FLT 0x29 // float divide (/) +#define OP_NEG_FLT 0x2A +#define OP_POW 0x2B // exponentiation (^) + +// ============================================================ +// String operations +// ============================================================ + +#define OP_STR_CONCAT 0x30 +#define OP_STR_LEFT 0x31 +#define OP_STR_RIGHT 0x32 +#define OP_STR_MID 0x33 // 3 args: str, start, len +#define OP_STR_MID2 0x34 // 2 args: str, start (to end) +#define OP_STR_LEN 0x35 +#define OP_STR_INSTR 0x36 // 2 args: str, find +#define OP_STR_INSTR3 0x37 // 3 args: start, str, find +#define OP_STR_UCASE 0x38 +#define OP_STR_LCASE 0x39 +#define OP_STR_TRIM 0x3A +#define OP_STR_LTRIM 0x3B +#define OP_STR_RTRIM 0x3C +#define OP_STR_CHR 0x3D +#define OP_STR_ASC 0x3E +#define OP_STR_SPACE 0x3F + +// ============================================================ +// Comparison (push boolean result) +// ============================================================ + +#define OP_CMP_EQ 0x40 +#define OP_CMP_NE 0x41 +#define OP_CMP_LT 0x42 +#define OP_CMP_GT 0x43 +#define OP_CMP_LE 0x44 +#define OP_CMP_GE 0x45 + +// ============================================================ +// Logical / bitwise +// ============================================================ + +#define OP_AND 0x48 +#define OP_OR 0x49 +#define OP_NOT 0x4A +#define OP_XOR 0x4B +#define OP_EQV 0x4C +#define OP_IMP 0x4D + +// ============================================================ +// Control flow +// ============================================================ + +#define OP_JMP 0x50 // [int16 offset] unconditional jump +#define OP_JMP_TRUE 0x51 // [int16 offset] jump if TOS is true +#define OP_JMP_FALSE 0x52 // [int16 offset] jump if TOS is false +#define OP_CALL 0x53 // [uint16 addr] [uint8 argc] [uint8 baseSlot] +#define OP_GOSUB_RET 0x54 // pop PC from eval stack, jump (GOSUB return) +#define OP_RET 0x55 // return from subroutine +#define OP_RET_VAL 0x56 // return from function (value on stack) +#define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 isLocal] init FOR +#define OP_FOR_NEXT 0x58 // [uint16 varIdx] [uint8 isLocal] [int16 loopTop] + +// ============================================================ +// Type conversion +// ============================================================ + +#define OP_CONV_INT_FLT 0x60 // int -> float +#define OP_CONV_FLT_INT 0x61 // float -> int (banker's rounding) +#define OP_CONV_INT_STR 0x62 // int -> string +#define OP_CONV_STR_INT 0x63 // string -> int (VAL) +#define OP_CONV_FLT_STR 0x64 // float -> string +#define OP_CONV_STR_FLT 0x65 // string -> float (VAL) +#define OP_CONV_INT_LONG 0x66 // int16 -> int32 +#define OP_CONV_LONG_INT 0x67 // int32 -> int16 + +// ============================================================ +// I/O +// ============================================================ + +#define OP_PRINT 0x70 // print TOS to current output +#define OP_PRINT_NL 0x71 // print newline +#define OP_PRINT_TAB 0x72 // print tab (14-column zones) +#define OP_PRINT_SPC 0x73 // [uint8 n] print n spaces +#define OP_INPUT 0x74 // read line into string on stack +#define OP_FILE_OPEN 0x75 // [uint8 mode] filename, channel# on stack +#define OP_FILE_CLOSE 0x76 // channel# on stack +#define OP_FILE_PRINT 0x77 // channel#, value on stack +#define OP_FILE_INPUT 0x78 // channel# on stack, push string +#define OP_FILE_EOF 0x79 // channel# on stack, push boolean +#define OP_FILE_LINE_INPUT 0x7A // channel# on stack, push string + +// ============================================================ +// UI / Event (used when form system is active) +// ============================================================ + +#define OP_LOAD_PROP 0x80 // [uint16 ctrl] [uint16 prop] push property value +#define OP_STORE_PROP 0x81 // [uint16 ctrl] [uint16 prop] pop to property +#define OP_CALL_METHOD 0x82 // [uint16 ctrl] [uint16 method] [uint8 argc] +#define OP_LOAD_FORM 0x83 // [uint16 formIdx] +#define OP_UNLOAD_FORM 0x84 // [uint16 formIdx] +#define OP_SHOW_FORM 0x85 // [uint16 formIdx] [uint8 modal] +#define OP_HIDE_FORM 0x86 // [uint16 formIdx] +#define OP_DO_EVENTS 0x87 +#define OP_MSGBOX 0x88 // [uint8 flags] message on stack +#define OP_INPUTBOX 0x89 // prompt on stack, push result +#define OP_ME_REF 0x8A // push current form reference + +// ============================================================ +// Array / misc +// ============================================================ + +#define OP_DIM_ARRAY 0x90 // [uint8 dims] [uint8 type] bounds on stack +#define OP_REDIM 0x91 // [uint8 dims] [uint8 preserve] bounds on stack +#define OP_ERASE 0x92 // array ref on stack +#define OP_LBOUND 0x93 // [uint8 dim] array ref on stack +#define OP_UBOUND 0x94 // [uint8 dim] array ref on stack +#define OP_ON_ERROR 0x95 // [int16 handler] set error handler (0 = disable) +#define OP_RESUME 0x96 // resume after error +#define OP_RESUME_NEXT 0x97 // resume at next statement +#define OP_RAISE_ERR 0x98 // error number on stack +#define OP_ERR_NUM 0x99 // push current error number +#define OP_ERR_CLEAR 0x9A // clear error state + +// ============================================================ +// Math built-ins (single opcode each for common functions) +// ============================================================ + +#define OP_MATH_ABS 0xA0 +#define OP_MATH_INT 0xA1 // floor +#define OP_MATH_FIX 0xA2 // truncate toward zero +#define OP_MATH_SGN 0xA3 +#define OP_MATH_SQR 0xA4 +#define OP_MATH_SIN 0xA5 +#define OP_MATH_COS 0xA6 +#define OP_MATH_TAN 0xA7 +#define OP_MATH_ATN 0xA8 +#define OP_MATH_LOG 0xA9 +#define OP_MATH_EXP 0xAA +#define OP_MATH_RND 0xAB +#define OP_MATH_RANDOMIZE 0xAC // seed on stack (or TIMER if -1) + +// ============================================================ +// Conversion built-ins +// ============================================================ + +#define OP_STR_VAL 0xB0 // VAL(s$) -> number +#define OP_STR_STRF 0xB1 // STR$(n) -> string +#define OP_STR_HEX 0xB2 // HEX$(n) -> string +#define OP_STR_STRING 0xB3 // STRING$(n, char) -> string + +// ============================================================ +// Extended built-ins +// ============================================================ + +#define OP_MATH_TIMER 0xB4 // push seconds since midnight as DOUBLE +#define OP_DATE_STR 0xB5 // push DATE$ string "MM-DD-YYYY" +#define OP_TIME_STR 0xB6 // push TIME$ string "HH:MM:SS" +#define OP_SLEEP 0xB7 // pop seconds, sleep +#define OP_ENVIRON 0xB8 // pop env var name, push value string + +// ============================================================ +// DATA/READ/RESTORE +// ============================================================ + +#define OP_READ_DATA 0xB9 // push next value from data pool +#define OP_RESTORE 0xBA // reset data pointer to 0 + +// ============================================================ +// WRITE # (comma-delimited with quoted strings) +// ============================================================ + +#define OP_FILE_WRITE 0xBB // pop channel + value, write in WRITE format +#define OP_FILE_WRITE_SEP 0xBC // pop channel, write comma separator +#define OP_FILE_WRITE_NL 0xBD // pop channel, write newline + +// ============================================================ +// Random/Binary file I/O +// ============================================================ + +#define OP_FILE_GET 0xBE // pop channel + recno, read record, push value +#define OP_FILE_PUT 0xBF // pop channel + recno + value, write record +#define OP_FILE_SEEK 0xC0 // pop channel + position, seek +#define OP_FILE_LOF 0xC1 // pop channel, push file length +#define OP_FILE_LOC 0xC2 // pop channel, push current position +#define OP_FILE_FREEFILE 0xC3 // push next free channel number +#define OP_FILE_INPUT_N 0xC4 // pop channel + n, read n chars, push string + +// ============================================================ +// Fixed-length strings and MID$ assignment +// ============================================================ + +#define OP_STR_FIXLEN 0xC5 // [uint16 len] pop string, pad/truncate, push +#define OP_STR_MID_ASGN 0xC6 // pop replacement, len, start, str; push modified + +// ============================================================ +// PRINT USING +// ============================================================ + +#define OP_PRINT_USING 0xC7 // pop format + value, push formatted string + +// ============================================================ +// SPC(n) and TAB(n) with stack-based argument +// ============================================================ + +#define OP_PRINT_TAB_N 0xC8 // pop column count, print spaces to reach column +#define OP_PRINT_SPC_N 0xC9 // pop count, print that many spaces +#define OP_FORMAT 0xCA // pop format string + value, push formatted string +#define OP_SHELL 0xCB // pop command string, call system(), push return value +#define OP_COMPARE_MODE 0xCC // [uint8 mode] set string compare mode (0=binary, 1=text) + +// ============================================================ +// Halt +// ============================================================ + +#define OP_HALT 0xFF + +#endif // DVXBASIC_OPCODES_H diff --git a/dvxbasic/compiler/parser.c b/dvxbasic/compiler/parser.c new file mode 100644 index 0000000..4b410c2 --- /dev/null +++ b/dvxbasic/compiler/parser.c @@ -0,0 +1,4324 @@ +// parser.c -- DVX BASIC recursive descent parser +// +// Single-pass compiler: reads tokens from the lexer and emits +// p-code directly via the code generator. No AST. +// +// Embeddable: no DVX dependencies, pure C. + +#include "parser.h" +#include "opcodes.h" + +#include +#include +#include + +// ============================================================ +// Forward jump list (for EXIT FOR / EXIT DO backpatching) +// ============================================================ + +#define MAX_EXITS 32 + +typedef struct { + int32_t patchAddr[MAX_EXITS]; + int32_t count; +} ExitListT; + +// ============================================================ +// Built-in function table +// ============================================================ + +typedef struct { + const char *name; + uint8_t opcode; + int32_t minArgs; + int32_t maxArgs; + uint8_t resultType; +} BuiltinFuncT; + +static const BuiltinFuncT builtinFuncs[] = { + // String functions + {"ASC", OP_STR_ASC, 1, 1, BAS_TYPE_INTEGER}, + {"CHR$", OP_STR_CHR, 1, 1, BAS_TYPE_STRING}, + {"DATE$", OP_DATE_STR, 0, 0, BAS_TYPE_STRING}, + {"ENVIRON$", OP_ENVIRON, 1, 1, BAS_TYPE_STRING}, + {"FORMAT$", OP_FORMAT, 2, 2, BAS_TYPE_STRING}, + {"HEX$", OP_STR_HEX, 1, 1, BAS_TYPE_STRING}, + {"INSTR", OP_STR_INSTR, 2, 3, BAS_TYPE_INTEGER}, + {"LCASE$", OP_STR_LCASE, 1, 1, BAS_TYPE_STRING}, + {"LEFT$", OP_STR_LEFT, 2, 2, BAS_TYPE_STRING}, + {"LEN", OP_STR_LEN, 1, 1, BAS_TYPE_INTEGER}, + {"LTRIM$", OP_STR_LTRIM, 1, 1, BAS_TYPE_STRING}, + {"MID$", OP_STR_MID2, 2, 3, BAS_TYPE_STRING}, + {"RIGHT$", OP_STR_RIGHT, 2, 2, BAS_TYPE_STRING}, + {"RTRIM$", OP_STR_RTRIM, 1, 1, BAS_TYPE_STRING}, + {"SPACE$", OP_STR_SPACE, 1, 1, BAS_TYPE_STRING}, + {"STR$", OP_STR_STRF, 1, 1, BAS_TYPE_STRING}, + {"STRING$", OP_STR_STRING, 2, 2, BAS_TYPE_STRING}, + {"TRIM$", OP_STR_TRIM, 1, 1, BAS_TYPE_STRING}, + {"UCASE$", OP_STR_UCASE, 1, 1, BAS_TYPE_STRING}, + {"VAL", OP_STR_VAL, 1, 1, BAS_TYPE_DOUBLE}, + + // File I/O functions + {"FREEFILE", OP_FILE_FREEFILE, 0, 0, BAS_TYPE_INTEGER}, + {"LOC", OP_FILE_LOC, 1, 1, BAS_TYPE_LONG}, + {"LOF", OP_FILE_LOF, 1, 1, BAS_TYPE_LONG}, + + // Math functions + {"ABS", OP_MATH_ABS, 1, 1, BAS_TYPE_DOUBLE}, + {"ATN", OP_MATH_ATN, 1, 1, BAS_TYPE_DOUBLE}, + {"COS", OP_MATH_COS, 1, 1, BAS_TYPE_DOUBLE}, + {"EXP", OP_MATH_EXP, 1, 1, BAS_TYPE_DOUBLE}, + {"FIX", OP_MATH_FIX, 1, 1, BAS_TYPE_INTEGER}, + {"INT", OP_MATH_INT, 1, 1, BAS_TYPE_INTEGER}, + {"LOG", OP_MATH_LOG, 1, 1, BAS_TYPE_DOUBLE}, + {"RND", OP_MATH_RND, 0, 1, BAS_TYPE_DOUBLE}, + {"SGN", OP_MATH_SGN, 1, 1, BAS_TYPE_INTEGER}, + {"SIN", OP_MATH_SIN, 1, 1, BAS_TYPE_DOUBLE}, + {"SQR", OP_MATH_SQR, 1, 1, BAS_TYPE_DOUBLE}, + {"TAN", OP_MATH_TAN, 1, 1, BAS_TYPE_DOUBLE}, + {"TIME$", OP_TIME_STR, 0, 0, BAS_TYPE_STRING}, + {"TIMER", OP_MATH_TIMER, 0, 0, BAS_TYPE_DOUBLE}, + + {NULL, 0, 0, 0, 0} +}; + +// ============================================================ +// Helper prototypes (alphabetized) +// ============================================================ + +static void advance(BasParserT *p); +static bool check(BasParserT *p, BasTokenTypeE type); +static bool checkKeyword(BasParserT *p, const char *kw); +static bool checkKeywordText(const char *text, const char *kw); +static void error(BasParserT *p, const char *msg); +static void errorExpected(BasParserT *p, const char *what); +static void expect(BasParserT *p, BasTokenTypeE type); +static void expectEndOfStatement(BasParserT *p); +static const BuiltinFuncT *findBuiltin(const char *name); +static BasSymbolT *findTypeDef(BasParserT *p, const char *name); +static bool match(BasParserT *p, BasTokenTypeE type); +static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName); +static uint8_t resolveTypeName(BasParserT *p); +static uint8_t suffixToType(const char *name); +static void skipNewlines(BasParserT *p); + +// ============================================================ +// Expression parser prototypes (by precedence, lowest first) +// ============================================================ + +static void parseExpression(BasParserT *p); +static void parseImpExpr(BasParserT *p); +static void parseEqvExpr(BasParserT *p); +static void parseOrExpr(BasParserT *p); +static void parseXorExpr(BasParserT *p); +static void parseAndExpr(BasParserT *p); +static void parseNotExpr(BasParserT *p); +static void parseCompareExpr(BasParserT *p); +static void parseConcatExpr(BasParserT *p); +static void parseAddExpr(BasParserT *p); +static void parseMulExpr(BasParserT *p); +static void parsePowExpr(BasParserT *p); +static void parseUnaryExpr(BasParserT *p); +static void parsePrimary(BasParserT *p); + +// ============================================================ +// Statement parser prototypes (alphabetized) +// ============================================================ + +static void parseAssignOrCall(BasParserT *p); +static void parseClose(BasParserT *p); +static void parseConst(BasParserT *p); +static void parseData(BasParserT *p); +static void parseDeclare(BasParserT *p); +static void parseDef(BasParserT *p); +static void parseDefType(BasParserT *p, uint8_t dataType); +static void parseDim(BasParserT *p); +static void parseDimBounds(BasParserT *p, int32_t *outDims); +static void parseDo(BasParserT *p); +static void parseEnd(BasParserT *p); +static void parseErase(BasParserT *p); +static void parseExit(BasParserT *p); +static void parseFor(BasParserT *p); +static void parseFunction(BasParserT *p); +static void parseGet(BasParserT *p); +static void parseGosub(BasParserT *p); +static void parseGoto(BasParserT *p); +static void parseIf(BasParserT *p); +static void parseInput(BasParserT *p); +static void parseLineInput(BasParserT *p); +static void parseModule(BasParserT *p); +static void parseOn(BasParserT *p); +static void parseOnError(BasParserT *p); +static void parseOpen(BasParserT *p); +static void parseOption(BasParserT *p); +static void parsePrint(BasParserT *p); +static void parsePut(BasParserT *p); +static void parseRead(BasParserT *p); +static void parseRedim(BasParserT *p); +static void parseRestore(BasParserT *p); +static void parseResume(BasParserT *p); +static void parseSeek(BasParserT *p); +static void parseSelectCase(BasParserT *p); +static void parseShell(BasParserT *p); +static void parseSleep(BasParserT *p); +static void parseStatement(BasParserT *p); +static void parseStatic(BasParserT *p); +static void parseSub(BasParserT *p); +static void parseSwap(BasParserT *p); +static void parseType(BasParserT *p); +static void parseWhile(BasParserT *p); +static void parseWrite(BasParserT *p); + +// ============================================================ +// Variable / code emit helper prototypes (alphabetized) +// ============================================================ + +static void emitFunctionCall(BasParserT *p, BasSymbolT *sym); +static int32_t emitJump(BasParserT *p, uint8_t opcode); +static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName); +static void emitLoad(BasParserT *p, BasSymbolT *sym); +static void emitStore(BasParserT *p, BasSymbolT *sym); +static BasSymbolT *ensureVariable(BasParserT *p, const char *name); +static void patchCallAddrs(BasParserT *p, BasSymbolT *sym); +static void patchJump(BasParserT *p, int32_t addr); +static void patchLabelRefs(BasParserT *p, BasSymbolT *sym); + +// ============================================================ +// Exit list helpers +// ============================================================ + +static ExitListT exitForList; +static ExitListT exitDoList; +static ExitListT exitSubList; +static ExitListT exitFuncList; + +static void exitListInit(ExitListT *el); +static void exitListAdd(ExitListT *el, int32_t addr); +static void exitListPatch(ExitListT *el, BasParserT *p); + + +// ============================================================ +// Helper implementations +// ============================================================ + +static void advance(BasParserT *p) { + if (p->hasError) { + return; + } + basLexerNext(&p->lex); + if (p->lex.token.type == TOK_ERROR) { + error(p, p->lex.error); + } +} + + +static bool check(BasParserT *p, BasTokenTypeE type) { + return p->lex.token.type == type; +} + + +static bool checkKeyword(BasParserT *p, const char *kw) { + if (p->lex.token.type != TOK_IDENT) { + return false; + } + // Case-insensitive comparison + const char *a = p->lex.token.text; + const char *b = kw; + while (*a && *b) { + if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) { + return false; + } + a++; + b++; + } + return *a == '\0' && *b == '\0'; +} + + +static bool checkKeywordText(const char *text, const char *kw) { + const char *a = text; + const char *b = kw; + while (*a && *b) { + if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) { + return false; + } + a++; + b++; + } + return *a == '\0' && *b == '\0'; +} + + +static void error(BasParserT *p, const char *msg) { + if (p->hasError) { + return; + } + p->hasError = true; + p->errorLine = p->lex.token.line; + snprintf(p->error, sizeof(p->error), "Line %d: %s", p->lex.token.line, msg); +} + + +static void errorExpected(BasParserT *p, const char *what) { + char buf[512]; + snprintf(buf, sizeof(buf), "Expected %s, got %s", what, basTokenName(p->lex.token.type)); + error(p, buf); +} + + +static void exitListAdd(ExitListT *el, int32_t addr) { + if (el->count < MAX_EXITS) { + el->patchAddr[el->count++] = addr; + } +} + + +static void exitListInit(ExitListT *el) { + el->count = 0; +} + + +static void exitListPatch(ExitListT *el, BasParserT *p) { + int32_t target = basCodePos(&p->cg); + for (int32_t i = 0; i < el->count; i++) { + int16_t offset = (int16_t)(target - (el->patchAddr[i] + 2)); + basPatch16(&p->cg, el->patchAddr[i], offset); + } + el->count = 0; +} + + +static void expect(BasParserT *p, BasTokenTypeE type) { + if (p->hasError) { + return; + } + if (p->lex.token.type != type) { + char buf[512]; + snprintf(buf, sizeof(buf), "Expected %s, got %s", basTokenName(type), basTokenName(p->lex.token.type)); + error(p, buf); + return; + } + advance(p); +} + + +static void expectEndOfStatement(BasParserT *p) { + if (p->hasError) { + return; + } + // Statement must end with newline, colon, EOF, or ELSE (single-line IF) + if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_ELSE)) { + return; + } + if (check(p, TOK_COLON)) { + advance(p); + return; + } + errorExpected(p, "end of statement"); +} + + +static const BuiltinFuncT *findBuiltin(const char *name) { + for (int32_t i = 0; builtinFuncs[i].name != NULL; i++) { + // Case-insensitive comparison + const char *a = name; + const char *b = builtinFuncs[i].name; + bool match = true; + while (*a && *b) { + if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) { + match = false; + break; + } + a++; + b++; + } + if (match && *a == '\0' && *b == '\0') { + return &builtinFuncs[i]; + } + } + return NULL; +} + + +static BasSymbolT *findTypeDef(BasParserT *p, const char *name) { + for (int32_t i = 0; i < p->sym.count; i++) { + if (p->sym.symbols[i].kind == SYM_TYPE_DEF) { + // Case-insensitive comparison + const char *a = p->sym.symbols[i].name; + const char *b = name; + bool eq = true; + while (*a && *b) { + if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) { + eq = false; + break; + } + a++; + b++; + } + if (eq && *a == '\0' && *b == '\0') { + return &p->sym.symbols[i]; + } + } + } + return NULL; +} + + +static bool match(BasParserT *p, BasTokenTypeE type) { + if (p->lex.token.type == type) { + advance(p); + return true; + } + return false; +} + + +static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName) { + for (int32_t i = 0; i < typeSym->fieldCount; i++) { + const char *a = typeSym->fields[i].name; + const char *b = fieldName; + bool eq = true; + while (*a && *b) { + if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) { + eq = false; + break; + } + a++; + b++; + } + if (eq && *a == '\0' && *b == '\0') { + return i; + } + } + return -1; +} + + +static uint8_t resolveTypeName(BasParserT *p) { + // Expect a type keyword after AS + if (check(p, TOK_INTEGER)) { + advance(p); + return BAS_TYPE_INTEGER; + } + if (check(p, TOK_LONG)) { + advance(p); + return BAS_TYPE_LONG; + } + if (check(p, TOK_SINGLE)) { + advance(p); + return BAS_TYPE_SINGLE; + } + if (check(p, TOK_DOUBLE)) { + advance(p); + return BAS_TYPE_DOUBLE; + } + if (check(p, TOK_STRING_KW)) { + advance(p); + return BAS_TYPE_STRING; + } + if (check(p, TOK_BOOLEAN)) { + advance(p); + return BAS_TYPE_BOOLEAN; + } + // Check for user-defined TYPE name + if (check(p, TOK_IDENT)) { + BasSymbolT *typeSym = findTypeDef(p, p->lex.token.text); + if (typeSym != NULL) { + p->lastUdtTypeId = typeSym->index; + advance(p); + return BAS_TYPE_UDT; + } + } + error(p, "Expected type name (Integer, Long, Single, Double, String, Boolean, or TYPE name)"); + return BAS_TYPE_INTEGER; +} + + +static void skipNewlines(BasParserT *p) { + while (check(p, TOK_NEWLINE)) { + advance(p); + } +} + + +static uint8_t suffixToType(const char *name) { + int32_t len = (int32_t)strlen(name); + if (len == 0) { + return BAS_TYPE_SINGLE; // QB default + } + switch (name[len - 1]) { + case '%': + return BAS_TYPE_INTEGER; + case '&': + return BAS_TYPE_LONG; + case '!': + return BAS_TYPE_SINGLE; + case '#': + return BAS_TYPE_DOUBLE; + case '$': + return BAS_TYPE_STRING; + default: + return BAS_TYPE_SINGLE; // QB default + } +} + + +// ============================================================ +// Variable / code emit helpers +// ============================================================ + +static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) { + // Parse argument list + expect(p, TOK_LPAREN); + int32_t argc = 0; + if (!check(p, TOK_RPAREN)) { + parseExpression(p); + argc++; + while (match(p, TOK_COMMA)) { + parseExpression(p); + argc++; + } + } + expect(p, TOK_RPAREN); + + if (p->hasError) { + return; + } + + if (argc != sym->paramCount) { + char buf[256]; + snprintf(buf, sizeof(buf), "Function '%s' expects %d arguments, got %d", sym->name, sym->paramCount, argc); + error(p, buf); + return; + } + + // baseSlot: functions reserve slot 0 for the return value + uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0; + + basEmit8(&p->cg, OP_CALL); + int32_t addrPos = basCodePos(&p->cg); + basEmitU16(&p->cg, (uint16_t)sym->codeAddr); + basEmit8(&p->cg, (uint8_t)argc); + basEmit8(&p->cg, baseSlot); + + // If not yet defined, record the address for backpatching + if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) { + sym->patchAddrs[sym->patchCount++] = addrPos; + } +} + + +static int32_t emitJump(BasParserT *p, uint8_t opcode) { + basEmit8(&p->cg, opcode); + int32_t addr = basCodePos(&p->cg); + basEmit16(&p->cg, 0); // placeholder + return addr; +} + + +static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName) { + // Look up label; if defined, emit direct jump; if not, create forward ref + BasSymbolT *sym = basSymTabFind(&p->sym, labelName); + + if (sym != NULL && sym->kind == SYM_LABEL && sym->isDefined) { + // Label already defined -- emit jump to known address + basEmit8(&p->cg, opcode); + int32_t here = basCodePos(&p->cg); + int16_t offset = (int16_t)(sym->codeAddr - (here + 2)); + basEmit16(&p->cg, offset); + return; + } + + // Forward reference -- create label symbol if needed + if (sym == NULL) { + sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0); + if (sym == NULL) { + error(p, "Symbol table full"); + return; + } + sym->scope = SCOPE_GLOBAL; + sym->isDefined = false; + sym->codeAddr = 0; + } + + // Emit jump with placeholder offset + basEmit8(&p->cg, opcode); + int32_t patchAddr = basCodePos(&p->cg); + basEmit16(&p->cg, 0); + + // Record patch address for backpatching when label is defined + if (sym->patchCount < BAS_MAX_CALL_PATCHES) { + sym->patchAddrs[sym->patchCount++] = patchAddr; + } +} + + +static void emitLoad(BasParserT *p, BasSymbolT *sym) { + if (sym->kind == SYM_CONST) { + // Emit the constant value directly + if (sym->dataType == BAS_TYPE_STRING) { + uint16_t idx = basAddConstant(&p->cg, sym->constStr, (int32_t)strlen(sym->constStr)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, idx); + } else if (sym->dataType == BAS_TYPE_INTEGER || sym->dataType == BAS_TYPE_LONG) { + basEmit8(&p->cg, OP_PUSH_INT32); + basEmit16(&p->cg, (int16_t)(sym->constInt & 0xFFFF)); + basEmit16(&p->cg, (int16_t)((sym->constInt >> 16) & 0xFFFF)); + } else if (sym->dataType == BAS_TYPE_BOOLEAN) { + basEmit8(&p->cg, sym->constInt ? OP_PUSH_TRUE : OP_PUSH_FALSE); + } else { + // Float constant + basEmit8(&p->cg, OP_PUSH_FLT64); + basEmitDouble(&p->cg, sym->constDbl); + } + return; + } + + if (sym->scope == SCOPE_LOCAL) { + basEmit8(&p->cg, OP_LOAD_LOCAL); + basEmitU16(&p->cg, (uint16_t)sym->index); + } else { + basEmit8(&p->cg, OP_LOAD_GLOBAL); + basEmitU16(&p->cg, (uint16_t)sym->index); + } +} + + +static void emitStore(BasParserT *p, BasSymbolT *sym) { + // Fixed-length string: pad/truncate before storing + if (sym->fixedLen > 0) { + basEmit8(&p->cg, OP_STR_FIXLEN); + basEmitU16(&p->cg, (uint16_t)sym->fixedLen); + } + if (sym->scope == SCOPE_LOCAL) { + basEmit8(&p->cg, OP_STORE_LOCAL); + basEmitU16(&p->cg, (uint16_t)sym->index); + } else { + basEmit8(&p->cg, OP_STORE_GLOBAL); + basEmitU16(&p->cg, (uint16_t)sym->index); + } +} + + +static BasSymbolT *ensureVariable(BasParserT *p, const char *name) { + BasSymbolT *sym = basSymTabFind(&p->sym, name); + if (sym != NULL) { + return sym; + } + + // When in local scope, check if a shared global exists before auto-declaring + if (p->sym.inLocalScope) { + BasSymbolT *globalSym = basSymTabFindGlobal(&p->sym, name); + if (globalSym != NULL && globalSym->isShared) { + return globalSym; + } + } + + // OPTION EXPLICIT: require explicit DIM + if (p->optionExplicit) { + char buf[320]; + snprintf(buf, sizeof(buf), "Variable not declared: %s (OPTION EXPLICIT is on)", name); + error(p, buf); + return NULL; + } + + // Auto-declare (QB implicit declaration) + // Use suffix type if present, otherwise defType for the first letter + uint8_t dt = suffixToType(name); + + if (dt == BAS_TYPE_SINGLE && name[0] != '\0') { + // suffixToType returns SINGLE as the default when no suffix. + // Check if defType overrides it. + char firstLetter = name[0]; + + if (firstLetter >= 'a' && firstLetter <= 'z') { + firstLetter -= 32; + } + + if (firstLetter >= 'A' && firstLetter <= 'Z') { + uint8_t defDt = p->defType[firstLetter - 'A']; + + if (defDt != 0) { + dt = defDt; + } + } + } + + sym = basSymTabAdd(&p->sym, name, SYM_VARIABLE, dt); + + if (sym == NULL) { + error(p, "Symbol table full"); + return NULL; + } + + sym->scope = SCOPE_GLOBAL; + sym->index = basSymTabAllocSlot(&p->sym); + sym->isDefined = true; + return sym; +} + + +static void patchCallAddrs(BasParserT *p, BasSymbolT *sym) { + // Backpatch all forward-reference CALL addresses + uint16_t addr = (uint16_t)sym->codeAddr; + + for (int32_t i = 0; i < sym->patchCount; i++) { + int32_t pos = sym->patchAddrs[i]; + + if (pos >= 0 && pos + 2 <= p->cg.codeLen) { + memcpy(&p->cg.code[pos], &addr, sizeof(uint16_t)); + } + } + + sym->patchCount = 0; +} + + +static void patchJump(BasParserT *p, int32_t addr) { + int32_t target = basCodePos(&p->cg); + int16_t offset = (int16_t)(target - (addr + 2)); + basPatch16(&p->cg, addr, offset); +} + + +static void patchLabelRefs(BasParserT *p, BasSymbolT *sym) { + // Backpatch all forward-reference jumps to this label + int32_t target = sym->codeAddr; + + for (int32_t i = 0; i < sym->patchCount; i++) { + int32_t patchAddr = sym->patchAddrs[i]; + int16_t offset = (int16_t)(target - (patchAddr + 2)); + basPatch16(&p->cg, patchAddr, offset); + } + + sym->patchCount = 0; +} + + +// ============================================================ +// Expression parsing +// ============================================================ + +static void parseExpression(BasParserT *p) { + parseImpExpr(p); +} + + +static void parseImpExpr(BasParserT *p) { + parseEqvExpr(p); + while (!p->hasError && check(p, TOK_IMP)) { + advance(p); + parseEqvExpr(p); + basEmit8(&p->cg, OP_IMP); + } +} + + +static void parseEqvExpr(BasParserT *p) { + parseOrExpr(p); + while (!p->hasError && check(p, TOK_EQV)) { + advance(p); + parseOrExpr(p); + basEmit8(&p->cg, OP_EQV); + } +} + + +static void parseOrExpr(BasParserT *p) { + parseXorExpr(p); + while (!p->hasError && check(p, TOK_OR)) { + advance(p); + parseXorExpr(p); + basEmit8(&p->cg, OP_OR); + } +} + + +static void parseXorExpr(BasParserT *p) { + parseAndExpr(p); + while (!p->hasError && check(p, TOK_XOR)) { + advance(p); + parseAndExpr(p); + basEmit8(&p->cg, OP_XOR); + } +} + + +static void parseAndExpr(BasParserT *p) { + parseNotExpr(p); + while (!p->hasError && check(p, TOK_AND)) { + advance(p); + parseNotExpr(p); + basEmit8(&p->cg, OP_AND); + } +} + + +static void parseNotExpr(BasParserT *p) { + if (check(p, TOK_NOT)) { + advance(p); + parseNotExpr(p); + basEmit8(&p->cg, OP_NOT); + return; + } + parseCompareExpr(p); +} + + +static void parseCompareExpr(BasParserT *p) { + parseConcatExpr(p); + while (!p->hasError) { + if (check(p, TOK_EQ)) { + advance(p); + parseConcatExpr(p); + basEmit8(&p->cg, OP_CMP_EQ); + } else if (check(p, TOK_NE)) { + advance(p); + parseConcatExpr(p); + basEmit8(&p->cg, OP_CMP_NE); + } else if (check(p, TOK_LT)) { + advance(p); + parseConcatExpr(p); + basEmit8(&p->cg, OP_CMP_LT); + } else if (check(p, TOK_GT)) { + advance(p); + parseConcatExpr(p); + basEmit8(&p->cg, OP_CMP_GT); + } else if (check(p, TOK_LE)) { + advance(p); + parseConcatExpr(p); + basEmit8(&p->cg, OP_CMP_LE); + } else if (check(p, TOK_GE)) { + advance(p); + parseConcatExpr(p); + basEmit8(&p->cg, OP_CMP_GE); + } else { + break; + } + } +} + + +static void parseConcatExpr(BasParserT *p) { + parseAddExpr(p); + while (!p->hasError && check(p, TOK_AMPERSAND)) { + advance(p); + parseAddExpr(p); + basEmit8(&p->cg, OP_STR_CONCAT); + } +} + + +static void parseAddExpr(BasParserT *p) { + parseMulExpr(p); + while (!p->hasError) { + if (check(p, TOK_PLUS)) { + advance(p); + parseMulExpr(p); + basEmit8(&p->cg, OP_ADD_INT); // VM handles type promotion + } else if (check(p, TOK_MINUS)) { + advance(p); + parseMulExpr(p); + basEmit8(&p->cg, OP_SUB_INT); + } else { + break; + } + } +} + + +static void parseMulExpr(BasParserT *p) { + parsePowExpr(p); + while (!p->hasError) { + if (check(p, TOK_STAR)) { + advance(p); + parsePowExpr(p); + basEmit8(&p->cg, OP_MUL_INT); + } else if (check(p, TOK_SLASH)) { + advance(p); + parsePowExpr(p); + basEmit8(&p->cg, OP_DIV_FLT); + } else if (check(p, TOK_BACKSLASH)) { + advance(p); + parsePowExpr(p); + basEmit8(&p->cg, OP_IDIV_INT); + } else if (check(p, TOK_MOD)) { + advance(p); + parsePowExpr(p); + basEmit8(&p->cg, OP_MOD_INT); + } else { + break; + } + } +} + + +static void parsePowExpr(BasParserT *p) { + parseUnaryExpr(p); + // Right-associative, but iterative is fine for most BASIC uses + while (!p->hasError && check(p, TOK_CARET)) { + advance(p); + parseUnaryExpr(p); + basEmit8(&p->cg, OP_POW); + } +} + + +static void parseUnaryExpr(BasParserT *p) { + if (check(p, TOK_MINUS)) { + advance(p); + parseUnaryExpr(p); + basEmit8(&p->cg, OP_NEG_INT); + return; + } + if (check(p, TOK_PLUS)) { + advance(p); // unary plus is a no-op + parseUnaryExpr(p); + return; + } + parsePrimary(p); +} + + +static void parsePrimary(BasParserT *p) { + if (p->hasError) { + return; + } + + BasTokenTypeE tt = p->lex.token.type; + + // Integer literal + if (tt == TOK_INT_LIT) { + int32_t val = p->lex.token.intVal; + if (val >= -32768 && val <= 32767) { + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, (int16_t)val); + } else { + basEmit8(&p->cg, OP_PUSH_INT32); + basEmit16(&p->cg, (int16_t)(val & 0xFFFF)); + basEmit16(&p->cg, (int16_t)((val >> 16) & 0xFFFF)); + } + advance(p); + return; + } + + // Long literal + if (tt == TOK_LONG_LIT) { + int32_t val = (int32_t)p->lex.token.longVal; + basEmit8(&p->cg, OP_PUSH_INT32); + basEmit16(&p->cg, (int16_t)(val & 0xFFFF)); + basEmit16(&p->cg, (int16_t)((val >> 16) & 0xFFFF)); + advance(p); + return; + } + + // Float literal + if (tt == TOK_FLOAT_LIT) { + basEmit8(&p->cg, OP_PUSH_FLT64); + basEmitDouble(&p->cg, p->lex.token.dblVal); + advance(p); + return; + } + + // String literal + if (tt == TOK_STRING_LIT) { + uint16_t idx = basAddConstant(&p->cg, p->lex.token.text, p->lex.token.textLen); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, idx); + advance(p); + return; + } + + // Boolean literals + if (tt == TOK_TRUE_KW) { + basEmit8(&p->cg, OP_PUSH_TRUE); + advance(p); + return; + } + if (tt == TOK_FALSE_KW) { + basEmit8(&p->cg, OP_PUSH_FALSE); + advance(p); + return; + } + + // EOF(#channel) -- file end-of-file test + if (tt == TOK_EOF_KW) { + advance(p); + expect(p, TOK_LPAREN); + match(p, TOK_HASH); // optional # + parseExpression(p); + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_FILE_EOF); + return; + } + + // SEEK(n) -- return current file position (function form) + if (tt == TOK_SEEK) { + advance(p); + if (check(p, TOK_LPAREN)) { + expect(p, TOK_LPAREN); + parseExpression(p); + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_FILE_LOC); + return; + } + // Not a function call -- error (SEEK as statement is handled elsewhere) + error(p, "SEEK requires parentheses when used as a function"); + return; + } + + // TIMER -- seconds since midnight (no args needed) + if (tt == TOK_TIMER) { + advance(p); + basEmit8(&p->cg, OP_MATH_TIMER); + return; + } + + // ERR -- current error number + if (tt == TOK_ERR) { + advance(p); + basEmit8(&p->cg, OP_ERR_NUM); + return; + } + + // SHELL("command") -- as function expression + if (tt == TOK_SHELL) { + advance(p); + expect(p, TOK_LPAREN); + parseExpression(p); + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_SHELL); + return; + } + + // LBOUND(array [, dim]) + if (tt == TOK_LBOUND) { + advance(p); + expect(p, TOK_LPAREN); + parseExpression(p); + uint8_t dim = 1; + if (match(p, TOK_COMMA)) { + if (check(p, TOK_INT_LIT)) { + dim = (uint8_t)p->lex.token.intVal; + advance(p); + } else { + error(p, "LBOUND dimension must be a constant integer"); + } + } + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_LBOUND); + basEmit8(&p->cg, dim); + return; + } + + // UBOUND(array [, dim]) + if (tt == TOK_UBOUND) { + advance(p); + expect(p, TOK_LPAREN); + parseExpression(p); + uint8_t dim = 1; + if (match(p, TOK_COMMA)) { + if (check(p, TOK_INT_LIT)) { + dim = (uint8_t)p->lex.token.intVal; + advance(p); + } else { + error(p, "UBOUND dimension must be a constant integer"); + } + } + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_UBOUND); + basEmit8(&p->cg, dim); + return; + } + + // Parenthesized expression + if (tt == TOK_LPAREN) { + advance(p); + parseExpression(p); + expect(p, TOK_RPAREN); + return; + } + + // Identifier: variable, function call, or built-in + if (tt == TOK_IDENT) { + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + // INPUT$(n, #channel) -- special handling for optional # in second arg + if (checkKeywordText(name, "INPUT$") && check(p, TOK_LPAREN)) { + expect(p, TOK_LPAREN); + parseExpression(p); // n (number of chars) + expect(p, TOK_COMMA); + match(p, TOK_HASH); // optional # + parseExpression(p); // channel number + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_FILE_INPUT_N); + return; + } + + // Check for built-in function + const BuiltinFuncT *builtin = findBuiltin(name); + if (builtin != NULL) { + int32_t argc = 0; + + // Zero-arg builtins can be used without parens + if (builtin->minArgs == 0 && builtin->maxArgs == 0 && !check(p, TOK_LPAREN)) { + basEmit8(&p->cg, builtin->opcode); + return; + } + + if (check(p, TOK_LPAREN)) { + expect(p, TOK_LPAREN); + + // RND/zero-arg builtins can be called with empty parens + if (!check(p, TOK_RPAREN)) { + parseExpression(p); + argc++; + while (match(p, TOK_COMMA)) { + parseExpression(p); + argc++; + } + } + expect(p, TOK_RPAREN); + } + + if (p->hasError) { + return; + } + + if (argc < builtin->minArgs || argc > builtin->maxArgs) { + char buf[256]; + snprintf(buf, sizeof(buf), "Built-in '%s' expects %d-%d arguments, got %d", builtin->name, builtin->minArgs, builtin->maxArgs, argc); + error(p, buf); + return; + } + + // MID$ with 3 args uses a different opcode than 2 args + if (builtin->opcode == OP_STR_MID2 && argc == 3) { + basEmit8(&p->cg, OP_STR_MID); + } else if (builtin->opcode == OP_STR_INSTR && argc == 3) { + basEmit8(&p->cg, OP_STR_INSTR3); + } else if (builtin->opcode == OP_MATH_RND && argc == 0) { + // Push -1 as dummy arg for RND() + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, -1); + basEmit8(&p->cg, OP_MATH_RND); + } else { + basEmit8(&p->cg, builtin->opcode); + } + return; + } + + // Check symbol table for user-defined function or variable + BasSymbolT *sym = basSymTabFind(&p->sym, name); + + // Function call with parens + if (check(p, TOK_LPAREN)) { + if (sym != NULL && (sym->kind == SYM_FUNCTION || sym->kind == SYM_SUB)) { + emitFunctionCall(p, sym); + return; + } + // Could be an array access -- treat as load + array index + if (sym != NULL && sym->isArray) { + emitLoad(p, sym); + expect(p, TOK_LPAREN); + int32_t dims = 0; + parseExpression(p); + dims++; + while (match(p, TOK_COMMA)) { + parseExpression(p); + dims++; + } + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_LOAD_ARRAY); + basEmit8(&p->cg, (uint8_t)dims); + return; + } + // Unknown function -- forward reference, assume it's a function + if (sym == NULL) { + sym = basSymTabAdd(&p->sym, name, SYM_FUNCTION, suffixToType(name)); + if (sym == NULL) { + error(p, "Symbol table full"); + return; + } + sym->scope = SCOPE_GLOBAL; + sym->isDefined = false; + sym->codeAddr = 0; + } + emitFunctionCall(p, sym); + return; + } + + // Check for UDT field access: var.field + if (check(p, TOK_DOT)) { + sym = ensureVariable(p, name); + if (sym != NULL && sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) { + emitLoad(p, sym); + advance(p); // consume DOT + if (!check(p, TOK_IDENT)) { + errorExpected(p, "field name"); + return; + } + // Find the TYPE_DEF symbol + BasSymbolT *typeSym = NULL; + for (int32_t i = 0; i < p->sym.count; i++) { + if (p->sym.symbols[i].kind == SYM_TYPE_DEF && p->sym.symbols[i].index == sym->udtTypeId) { + typeSym = &p->sym.symbols[i]; + break; + } + } + if (typeSym == NULL) { + error(p, "Unknown TYPE definition"); + return; + } + int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text); + if (fieldIdx < 0) { + char buf[256]; + snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name); + error(p, buf); + return; + } + advance(p); // consume field name + basEmit8(&p->cg, OP_LOAD_FIELD); + basEmitU16(&p->cg, (uint16_t)fieldIdx); + return; + } + } + + // Plain variable reference + sym = ensureVariable(p, name); + if (sym != NULL) { + emitLoad(p, sym); + } + return; + } + + // Nothing matched + errorExpected(p, "expression"); +} + + +// ============================================================ +// Statement parsing +// ============================================================ + +static void parseAssignOrCall(BasParserT *p) { + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + // MID$ statement: MID$(var$, start [, len]) = replacement$ + if (checkKeywordText(name, "MID$") && check(p, TOK_LPAREN)) { + expect(p, TOK_LPAREN); + + // First arg: target string variable + if (!check(p, TOK_IDENT)) { + errorExpected(p, "string variable name"); + return; + } + char varName[BAS_MAX_TOKEN_LEN]; + strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + BasSymbolT *varSym = ensureVariable(p, varName); + if (varSym == NULL) { + return; + } + + // Load the original string + emitLoad(p, varSym); + + expect(p, TOK_COMMA); + parseExpression(p); // start position + + // Optional length + if (match(p, TOK_COMMA)) { + parseExpression(p); // length + } else { + // Push 0 as sentinel meaning "use replacement length" + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + } + + expect(p, TOK_RPAREN); + expect(p, TOK_EQ); + + // Parse replacement expression + parseExpression(p); + + // Emit MID$ assignment: pops replacement, len, start, str; pushes result + basEmit8(&p->cg, OP_STR_MID_ASGN); + + // Store back to the variable + emitStore(p, varSym); + return; + } + + BasSymbolT *sym = basSymTabFind(&p->sym, name); + + // UDT field assignment: var.field = expr + if (check(p, TOK_DOT)) { + if (sym == NULL) { + sym = ensureVariable(p, name); + } + if (sym == NULL) { + return; + } + if (sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) { + emitLoad(p, sym); + advance(p); // consume DOT + if (!check(p, TOK_IDENT)) { + errorExpected(p, "field name"); + return; + } + BasSymbolT *typeSym = NULL; + for (int32_t i = 0; i < p->sym.count; i++) { + if (p->sym.symbols[i].kind == SYM_TYPE_DEF && p->sym.symbols[i].index == sym->udtTypeId) { + typeSym = &p->sym.symbols[i]; + break; + } + } + if (typeSym == NULL) { + error(p, "Unknown TYPE definition"); + return; + } + int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text); + if (fieldIdx < 0) { + char buf[256]; + snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name); + error(p, buf); + return; + } + advance(p); // consume field name + expect(p, TOK_EQ); + parseExpression(p); + basEmit8(&p->cg, OP_STORE_FIELD); + basEmitU16(&p->cg, (uint16_t)fieldIdx); + return; + } + } + + // Array assignment: var(index) = expr + if (check(p, TOK_LPAREN)) { + // Could be a function call as a statement (discard result) + // or array assignment + if (sym != NULL && (sym->kind == SYM_SUB || sym->kind == SYM_FUNCTION)) { + emitFunctionCall(p, sym); + if (sym->kind == SYM_FUNCTION) { + basEmit8(&p->cg, OP_POP); // discard return value + } + return; + } + + // Array element assignment + if (sym == NULL) { + sym = ensureVariable(p, name); + } + if (sym == NULL) { + return; + } + + emitLoad(p, sym); + expect(p, TOK_LPAREN); + int32_t dims = 0; + parseExpression(p); + dims++; + while (match(p, TOK_COMMA)) { + parseExpression(p); + dims++; + } + expect(p, TOK_RPAREN); + + expect(p, TOK_EQ); + parseExpression(p); + + basEmit8(&p->cg, OP_STORE_ARRAY); + basEmit8(&p->cg, (uint8_t)dims); + return; + } + + // Simple assignment: var = expr + if (check(p, TOK_EQ)) { + advance(p); + if (sym == NULL) { + sym = ensureVariable(p, name); + } + if (sym == NULL) { + return; + } + if (sym->kind == SYM_CONST) { + error(p, "Cannot assign to a constant"); + return; + } + // Check if this is a function name (assigning return value) + if (sym->kind == SYM_FUNCTION) { + parseExpression(p); + // Store to the implicit return-value local slot (index 0 in function scope) + basEmit8(&p->cg, OP_STORE_LOCAL); + basEmitU16(&p->cg, 0); + return; + } + parseExpression(p); + emitStore(p, sym); + return; + } + + // Sub call without parens: SUBName arg1, arg2 ... + if (sym != NULL && sym->kind == SYM_SUB) { + int32_t argc = 0; + if (!check(p, TOK_NEWLINE) && !check(p, TOK_EOF) && !check(p, TOK_COLON) && !check(p, TOK_ELSE)) { + parseExpression(p); + argc++; + while (match(p, TOK_COMMA)) { + parseExpression(p); + argc++; + } + } + if (!p->hasError && argc != sym->paramCount) { + char buf[256]; + snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, sym->paramCount, argc); + error(p, buf); + return; + } + { + uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0; + basEmit8(&p->cg, OP_CALL); + int32_t addrPos = basCodePos(&p->cg); + basEmitU16(&p->cg, (uint16_t)sym->codeAddr); + basEmit8(&p->cg, (uint8_t)argc); + basEmit8(&p->cg, baseSlot); + + if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) { + sym->patchAddrs[sym->patchCount++] = addrPos; + } + } + + return; + } + + // If nothing else, it's an assignment missing the = + errorExpected(p, "'=' or '('"); +} + + +static void parseClose(BasParserT *p) { + // CLOSE #channel + advance(p); // consume CLOSE + + // Optional # prefix + match(p, TOK_HASH); + + // Channel number + parseExpression(p); + + basEmit8(&p->cg, OP_FILE_CLOSE); +} + + +static void parseConst(BasParserT *p) { + // CONST name = value + advance(p); // consume CONST + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "constant name"); + return; + } + + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + expect(p, TOK_EQ); + + // Parse the constant value (must be a literal) + bool isNeg = false; + if (check(p, TOK_MINUS)) { + isNeg = true; + advance(p); + } + + BasSymbolT *sym = NULL; + + if (check(p, TOK_INT_LIT) || check(p, TOK_LONG_LIT)) { + int32_t val = check(p, TOK_INT_LIT) ? p->lex.token.intVal : (int32_t)p->lex.token.longVal; + if (isNeg) { + val = -val; + } + sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_LONG); + if (sym != NULL) { + sym->constInt = val; + sym->isDefined = true; + sym->scope = SCOPE_GLOBAL; + } + advance(p); + } else if (check(p, TOK_FLOAT_LIT)) { + double val = p->lex.token.dblVal; + if (isNeg) { + val = -val; + } + sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_DOUBLE); + if (sym != NULL) { + sym->constDbl = val; + sym->isDefined = true; + sym->scope = SCOPE_GLOBAL; + } + advance(p); + } else if (check(p, TOK_STRING_LIT) && !isNeg) { + sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_STRING); + if (sym != NULL) { + strncpy(sym->constStr, p->lex.token.text, sizeof(sym->constStr) - 1); + sym->constStr[sizeof(sym->constStr) - 1] = '\0'; + sym->isDefined = true; + sym->scope = SCOPE_GLOBAL; + } + advance(p); + } else if (check(p, TOK_TRUE_KW) && !isNeg) { + sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_BOOLEAN); + if (sym != NULL) { + sym->constInt = -1; + sym->isDefined = true; + sym->scope = SCOPE_GLOBAL; + } + advance(p); + } else if (check(p, TOK_FALSE_KW) && !isNeg) { + sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_BOOLEAN); + if (sym != NULL) { + sym->constInt = 0; + sym->isDefined = true; + sym->scope = SCOPE_GLOBAL; + } + advance(p); + } else { + error(p, "Constant value must be a literal"); + } + + if (sym == NULL && !p->hasError) { + error(p, "Duplicate constant or symbol table full"); + } +} + + +static void parseData(BasParserT *p) { + // DATA val1, val2, "string", ... + // Collect all values into the data pool. No runtime code is emitted. + advance(p); // consume DATA + + for (;;) { + if (p->hasError) { + return; + } + + bool isNeg = false; + if (check(p, TOK_MINUS)) { + isNeg = true; + advance(p); + } + + if (check(p, TOK_INT_LIT)) { + int32_t val = p->lex.token.intVal; + if (isNeg) { + val = -val; + } + BasValueT v = basValInteger((int16_t)val); + basAddData(&p->cg, v); + advance(p); + } else if (check(p, TOK_LONG_LIT)) { + int32_t val = (int32_t)p->lex.token.longVal; + if (isNeg) { + val = -val; + } + BasValueT v = basValLong(val); + basAddData(&p->cg, v); + advance(p); + } else if (check(p, TOK_FLOAT_LIT)) { + double val = p->lex.token.dblVal; + if (isNeg) { + val = -val; + } + BasValueT v = basValDouble(val); + basAddData(&p->cg, v); + advance(p); + } else if (check(p, TOK_STRING_LIT) && !isNeg) { + BasValueT v = basValStringFromC(p->lex.token.text); + basAddData(&p->cg, v); + basValRelease(&v); + advance(p); + } else { + // Unquoted text -- read as string up to comma/newline/EOF + // In QB, unquoted DATA values are treated as strings + if (isNeg) { + // Negative sign without a number -- treat "-" as string data + BasValueT v = basValStringFromC("-"); + basAddData(&p->cg, v); + basValRelease(&v); + } else if (check(p, TOK_IDENT)) { + BasValueT v = basValStringFromC(p->lex.token.text); + basAddData(&p->cg, v); + basValRelease(&v); + advance(p); + } else { + error(p, "Expected DATA value"); + return; + } + } + + if (!match(p, TOK_COMMA)) { + break; + } + } +} + + +static void parseDeclare(BasParserT *p) { + // DECLARE SUB name(params) + // DECLARE FUNCTION name(params) AS type + advance(p); // consume DECLARE + + BasSymKindE kind; + + if (check(p, TOK_SUB)) { + kind = SYM_SUB; + advance(p); + } else if (check(p, TOK_FUNCTION)) { + kind = SYM_FUNCTION; + advance(p); + } else { + error(p, "Expected SUB or FUNCTION after DECLARE"); + return; + } + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "subroutine/function name"); + return; + } + + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + // Parse parameter list + int32_t paramCount = 0; + uint8_t paramTypes[BAS_MAX_PARAMS]; + bool paramByVal[BAS_MAX_PARAMS]; + + if (match(p, TOK_LPAREN)) { + while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) { + if (paramCount > 0) { + expect(p, TOK_COMMA); + } + + bool byVal = false; + + if (match(p, TOK_BYVAL)) { + byVal = true; + } + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "parameter name"); + return; + } + + char paramName[BAS_MAX_TOKEN_LEN]; + strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + uint8_t pdt = suffixToType(paramName); + + if (match(p, TOK_AS)) { + pdt = resolveTypeName(p); + } + + if (paramCount < BAS_MAX_PARAMS) { + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = byVal; + } + + paramCount++; + } + + expect(p, TOK_RPAREN); + } + + // Return type for FUNCTION + uint8_t returnType = suffixToType(name); + + if (kind == SYM_FUNCTION && match(p, TOK_AS)) { + returnType = resolveTypeName(p); + } + + if (p->hasError) { + return; + } + + // Add to symbol table as forward declaration + BasSymbolT *sym = basSymTabAdd(&p->sym, name, kind, returnType); + + if (sym == NULL) { + // Might already be declared -- look it up + sym = basSymTabFind(&p->sym, name); + + if (sym == NULL) { + error(p, "Symbol table full"); + return; + } + } + + sym->scope = SCOPE_GLOBAL; + sym->isDefined = false; + sym->codeAddr = 0; + sym->paramCount = paramCount; + + for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { + sym->paramTypes[i] = paramTypes[i]; + sym->paramByVal[i] = paramByVal[i]; + } +} + + +static void parseDef(BasParserT *p) { + // DEF FNname(params) = expression + advance(p); // consume DEF + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "function name (FNname)"); + return; + } + + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + + if ((name[0] != 'F' && name[0] != 'f') || (name[1] != 'N' && name[1] != 'n')) { + error(p, "DEF function name must start with FN"); + return; + } + + advance(p); + + int32_t skipJump = emitJump(p, OP_JMP); + int32_t funcAddr = basCodePos(&p->cg); + + basSymTabEnterLocal(&p->sym); + basSymTabAllocSlot(&p->sym); // slot 0 for return value + + int32_t paramCount = 0; + uint8_t paramTypes[BAS_MAX_PARAMS]; + bool paramByVal[BAS_MAX_PARAMS]; + + if (match(p, TOK_LPAREN)) { + while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) { + if (paramCount > 0) { + expect(p, TOK_COMMA); + } + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "parameter name"); + return; + } + + char paramName[BAS_MAX_TOKEN_LEN]; + strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + uint8_t pdt = suffixToType(paramName); + if (match(p, TOK_AS)) { + pdt = resolveTypeName(p); + } + + BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); + if (paramSym == NULL) { + error(p, "Symbol table full"); + return; + } + paramSym->scope = SCOPE_LOCAL; + paramSym->index = basSymTabAllocSlot(&p->sym); + paramSym->isDefined = true; + + if (paramCount < BAS_MAX_PARAMS) { + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = true; + } + paramCount++; + } + expect(p, TOK_RPAREN); + } + + expect(p, TOK_EQ); + parseExpression(p); + basEmit8(&p->cg, OP_RET_VAL); + + basSymTabLeaveLocal(&p->sym); + + uint8_t returnType = suffixToType(name); + bool savedLocal = p->sym.inLocalScope; + p->sym.inLocalScope = false; + BasSymbolT *funcSym = basSymTabAdd(&p->sym, name, SYM_FUNCTION, returnType); + p->sym.inLocalScope = savedLocal; + + if (funcSym == NULL) { + error(p, "Could not register DEF function"); + return; + } + + funcSym->codeAddr = funcAddr; + funcSym->isDefined = true; + funcSym->paramCount = paramCount; + funcSym->scope = SCOPE_GLOBAL; + for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { + funcSym->paramTypes[i] = paramTypes[i]; + funcSym->paramByVal[i] = paramByVal[i]; + } + + patchCallAddrs(p, funcSym); + patchJump(p, skipJump); +} + + +// ============================================================ +// parseDefType -- DEFINT, DEFLNG, DEFSNG, DEFDBL, DEFSTR +// ============================================================ +// +// Sets the default type for variables whose names start with +// letters in the given range. Example: DEFINT A-Z makes all +// untyped variables default to INTEGER. + +static void parseDefType(BasParserT *p, uint8_t dataType) { + advance(p); // consume DEFxxx keyword + + while (!p->hasError) { + if (!check(p, TOK_IDENT)) { + errorExpected(p, "letter or letter range"); + return; + } + + char startLetter = p->lex.token.text[0]; + + if (startLetter >= 'a' && startLetter <= 'z') { + startLetter -= 32; + } + + if (startLetter < 'A' || startLetter > 'Z') { + error(p, "Expected letter A-Z"); + return; + } + + advance(p); + + char endLetter = startLetter; + + if (match(p, TOK_MINUS)) { + if (!check(p, TOK_IDENT)) { + errorExpected(p, "letter after '-'"); + return; + } + + endLetter = p->lex.token.text[0]; + + if (endLetter >= 'a' && endLetter <= 'z') { + endLetter -= 32; + } + + if (endLetter < 'A' || endLetter > 'Z') { + error(p, "Expected letter A-Z"); + return; + } + + advance(p); + } + + // Set default type for the range + for (char c = startLetter; c <= endLetter; c++) { + p->defType[c - 'A'] = dataType; + } + + if (!match(p, TOK_COMMA)) { + break; + } + } +} + + +static void parseDimBounds(BasParserT *p, int32_t *outDims) { + // Parse each dimension bound, pushing (lbound, ubound) pairs onto the stack. + // Supports both "ubound" (lbound=optionBase) and "lbound TO ubound" syntax. + *outDims = 0; + + for (;;) { + // Save code position before parsing the first expression + int32_t exprStart = basCodePos(&p->cg); + parseExpression(p); + + if (match(p, TOK_TO)) { + // "lbound TO ubound" -- first expr is lbound, parse ubound next + parseExpression(p); + } else { + // Single value = ubound, lbound defaults to optionBase. + // Ubound expression already emitted. Insert PUSH_INT16 before it. + int32_t exprLen = basCodePos(&p->cg) - exprStart; + int32_t insertLen = 3; // OP_PUSH_INT16 + 2 bytes + + if (basCodePos(&p->cg) + insertLen <= BAS_MAX_CODE) { + memmove(&p->cg.code[exprStart + insertLen], &p->cg.code[exprStart], exprLen); + p->cg.code[exprStart] = OP_PUSH_INT16; + int16_t lbound = (int16_t)p->optionBase; + memcpy(&p->cg.code[exprStart + 1], &lbound, 2); + p->cg.codeLen += insertLen; + } + } + + (*outDims)++; + if (!match(p, TOK_COMMA)) { + break; + } + } +} + + +static void parseDim(BasParserT *p) { + // DIM [SHARED] var AS type + // DIM var(ubound) AS type + // DIM var(lbound TO ubound) AS type + // DIM var AS UdtType + advance(p); // consume DIM + + // Check for SHARED keyword + bool isShared = false; + if (check(p, TOK_SHARED)) { + isShared = true; + advance(p); + } + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "variable name"); + return; + } + + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + bool isArray = false; + int32_t dims = 0; + + // Check for array bounds + if (check(p, TOK_LPAREN)) { + isArray = true; + advance(p); + parseDimBounds(p, &dims); + expect(p, TOK_RPAREN); + } + + // Optional AS type + uint8_t dt = suffixToType(name); + int32_t udtTypeId = -1; + int32_t fixedLen = 0; + if (match(p, TOK_AS)) { + dt = resolveTypeName(p); + if (dt == BAS_TYPE_UDT) { + udtTypeId = p->lastUdtTypeId; + } + // Check for STRING * n (fixed-length string) + if (dt == BAS_TYPE_STRING && check(p, TOK_STAR)) { + advance(p); + if (check(p, TOK_INT_LIT)) { + fixedLen = p->lex.token.intVal; + advance(p); + } else { + error(p, "Expected integer after STRING *"); + } + } + } + + if (p->hasError) { + return; + } + + // Check for duplicate + BasSymbolT *existing = basSymTabFind(&p->sym, name); + if (existing != NULL && existing->isDefined) { + char buf[256]; + snprintf(buf, sizeof(buf), "Variable '%s' already declared", name); + error(p, buf); + return; + } + + BasSymbolT *sym = basSymTabAdd(&p->sym, name, SYM_VARIABLE, dt); + if (sym == NULL) { + error(p, "Symbol table full or duplicate name"); + return; + } + sym->index = basSymTabAllocSlot(&p->sym); + sym->isDefined = true; + sym->isArray = isArray; + sym->isShared = isShared; + sym->udtTypeId = udtTypeId; + sym->fixedLen = fixedLen; + + if (p->sym.inLocalScope) { + sym->scope = SCOPE_LOCAL; + } else { + sym->scope = SCOPE_GLOBAL; + } + + if (isArray) { + // Emit array dimension instruction + basEmit8(&p->cg, OP_DIM_ARRAY); + basEmit8(&p->cg, (uint8_t)dims); + basEmit8(&p->cg, dt); + emitStore(p, sym); + } else if (dt == BAS_TYPE_UDT && udtTypeId >= 0) { + // Allocate a UDT instance + BasSymbolT *typeSym = NULL; + for (int32_t i = 0; i < p->sym.count; i++) { + if (p->sym.symbols[i].kind == SYM_TYPE_DEF && p->sym.symbols[i].index == udtTypeId) { + typeSym = &p->sym.symbols[i]; + break; + } + } + if (typeSym != NULL) { + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, (int16_t)udtTypeId); + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, (int16_t)typeSym->fieldCount); + // OP_DIM_ARRAY with dims=0 signals UDT allocation + basEmit8(&p->cg, OP_DIM_ARRAY); + basEmit8(&p->cg, 0); + basEmit8(&p->cg, BAS_TYPE_UDT); + emitStore(p, sym); + } + } +} + + +static void parseDo(BasParserT *p) { + // DO [WHILE|UNTIL cond] + // ... + // LOOP [WHILE|UNTIL cond] + advance(p); // consume DO + + ExitListT savedExitDo = exitDoList; + exitListInit(&exitDoList); + + int32_t loopTop = basCodePos(&p->cg); + + bool hasPreCondition = false; + int32_t preCondJump = 0; + + // DO WHILE cond / DO UNTIL cond + if (check(p, TOK_WHILE)) { + hasPreCondition = true; + advance(p); + parseExpression(p); + preCondJump = emitJump(p, OP_JMP_FALSE); + } else if (check(p, TOK_UNTIL)) { + hasPreCondition = true; + advance(p); + parseExpression(p); + preCondJump = emitJump(p, OP_JMP_TRUE); + } + + expectEndOfStatement(p); + skipNewlines(p); + + // Loop body + while (!p->hasError && !check(p, TOK_LOOP) && !check(p, TOK_EOF)) { + parseStatement(p); + skipNewlines(p); + } + + if (p->hasError) { + return; + } + + expect(p, TOK_LOOP); + + // LOOP WHILE cond / LOOP UNTIL cond + if (check(p, TOK_WHILE)) { + advance(p); + parseExpression(p); + // Jump back to loopTop if condition is true + basEmit8(&p->cg, OP_JMP_TRUE); + int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2)); + basEmit16(&p->cg, backOffset); + } else if (check(p, TOK_UNTIL)) { + advance(p); + parseExpression(p); + // Jump back to loopTop if condition is false + basEmit8(&p->cg, OP_JMP_FALSE); + int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2)); + basEmit16(&p->cg, backOffset); + } else { + // Plain LOOP -- unconditional jump back + basEmit8(&p->cg, OP_JMP); + int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2)); + basEmit16(&p->cg, backOffset); + } + + // Backpatch pre-condition jump (exits the loop) + if (hasPreCondition) { + patchJump(p, preCondJump); + } + + // Patch all EXIT DO jumps to here + exitListPatch(&exitDoList, p); + exitDoList = savedExitDo; +} + + +static void parseEnd(BasParserT *p) { + // END -- by itself = halt + // END IF / END SUB / END FUNCTION / END SELECT are handled by their parsers + advance(p); // consume END + basEmit8(&p->cg, OP_HALT); +} + + +static void parseErase(BasParserT *p) { + // ERASE arrayVar + advance(p); // consume ERASE + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "array variable name"); + return; + } + + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + BasSymbolT *sym = basSymTabFind(&p->sym, name); + if (sym == NULL || !sym->isArray) { + error(p, "ERASE requires an array variable"); + return; + } + + emitLoad(p, sym); + basEmit8(&p->cg, OP_ERASE); + emitStore(p, sym); +} + + +static void parseExit(BasParserT *p) { + advance(p); // consume EXIT + + if (check(p, TOK_FOR)) { + advance(p); + int32_t addr = emitJump(p, OP_JMP); + exitListAdd(&exitForList, addr); + } else if (check(p, TOK_DO)) { + advance(p); + int32_t addr = emitJump(p, OP_JMP); + exitListAdd(&exitDoList, addr); + } else if (check(p, TOK_SUB)) { + advance(p); + int32_t addr = emitJump(p, OP_JMP); + exitListAdd(&exitSubList, addr); + } else if (check(p, TOK_FUNCTION)) { + advance(p); + int32_t addr = emitJump(p, OP_JMP); + exitListAdd(&exitFuncList, addr); + } else { + error(p, "Expected FOR, DO, SUB, or FUNCTION after EXIT"); + } +} + + +static void parseFor(BasParserT *p) { + // FOR var = start TO limit [STEP step] + // ... + // NEXT [var] + advance(p); // consume FOR + + ExitListT savedExitFor = exitForList; + exitListInit(&exitForList); + + // Loop variable + if (!check(p, TOK_IDENT)) { + errorExpected(p, "loop variable"); + return; + } + + char varName[BAS_MAX_TOKEN_LEN]; + strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + BasSymbolT *loopVar = ensureVariable(p, varName); + if (loopVar == NULL) { + return; + } + + // = start + expect(p, TOK_EQ); + parseExpression(p); + emitStore(p, loopVar); + + // TO limit + expect(p, TOK_TO); + parseExpression(p); // limit is on stack + + // STEP step (optional, default 1) + if (match(p, TOK_STEP)) { + parseExpression(p); // step is on stack + } else { + // Default step = 1 + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 1); + } + + // Emit FOR_INIT -- sets up the for-loop state in the VM + basEmit8(&p->cg, OP_FOR_INIT); + basEmitU16(&p->cg, (uint16_t)loopVar->index); + basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : 0); + + int32_t loopBody = basCodePos(&p->cg); + + expectEndOfStatement(p); + skipNewlines(p); + + // Loop body + while (!p->hasError && !check(p, TOK_NEXT) && !check(p, TOK_EOF)) { + parseStatement(p); + skipNewlines(p); + } + + if (p->hasError) { + return; + } + + expect(p, TOK_NEXT); + + // Optional variable name after NEXT (we just skip it) + if (check(p, TOK_IDENT)) { + advance(p); + } + + // Emit FOR_NEXT with backward jump to loop body + basEmit8(&p->cg, OP_FOR_NEXT); + basEmitU16(&p->cg, (uint16_t)loopVar->index); + basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : 0); + int16_t backOffset = (int16_t)(loopBody - (basCodePos(&p->cg) + 2)); + basEmit16(&p->cg, backOffset); + + // Patch all EXIT FOR jumps to here + exitListPatch(&exitForList, p); + exitForList = savedExitFor; +} + + +static void parseFunction(BasParserT *p) { + // FUNCTION name(params) AS type + // ... + // END FUNCTION + advance(p); // consume FUNCTION + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "function name"); + return; + } + + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + // Save current proc name for STATIC variable mangling + strncpy(p->currentProc, name, BAS_MAX_TOKEN_LEN - 1); + p->currentProc[BAS_MAX_TOKEN_LEN - 1] = '\0'; + + // Jump over the function body in module-level code + int32_t skipJump = emitJump(p, OP_JMP); + + int32_t funcAddr = basCodePos(&p->cg); + + // Enter local scope + basSymTabEnterLocal(&p->sym); + + ExitListT savedExitFunc = exitFuncList; + exitListInit(&exitFuncList); + + // Allocate slot 0 for return value + basSymTabAllocSlot(&p->sym); + + // Parse parameter list + int32_t paramCount = 0; + uint8_t paramTypes[BAS_MAX_PARAMS]; + bool paramByVal[BAS_MAX_PARAMS]; + + if (match(p, TOK_LPAREN)) { + while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) { + if (paramCount > 0) { + expect(p, TOK_COMMA); + } + + bool byVal = false; + if (match(p, TOK_BYVAL)) { + byVal = true; + } + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "parameter name"); + return; + } + + char paramName[BAS_MAX_TOKEN_LEN]; + strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + uint8_t pdt = suffixToType(paramName); + if (match(p, TOK_AS)) { + pdt = resolveTypeName(p); + } + + BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); + if (paramSym == NULL) { + error(p, "Symbol table full"); + return; + } + paramSym->scope = SCOPE_LOCAL; + paramSym->index = basSymTabAllocSlot(&p->sym); + paramSym->isDefined = true; + + if (paramCount < BAS_MAX_PARAMS) { + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = byVal; + } + paramCount++; + } + expect(p, TOK_RPAREN); + } + + // Return type + uint8_t returnType = suffixToType(name); + if (match(p, TOK_AS)) { + returnType = resolveTypeName(p); + } + + // Register the function in the symbol table (global scope entry) + // We need to temporarily leave local scope to add to global + BasSymbolT *existing = basSymTabFindGlobal(&p->sym, name); + BasSymbolT *funcSym = NULL; + + if (existing != NULL && existing->kind == SYM_FUNCTION) { + // Forward-declared, now define it + funcSym = existing; + } else { + // Temporarily store the local state, add globally + bool savedLocal = p->sym.inLocalScope; + p->sym.inLocalScope = false; + funcSym = basSymTabAdd(&p->sym, name, SYM_FUNCTION, returnType); + p->sym.inLocalScope = savedLocal; + } + + if (funcSym == NULL) { + error(p, "Could not register function"); + return; + } + + funcSym->codeAddr = funcAddr; + funcSym->isDefined = true; + funcSym->paramCount = paramCount; + funcSym->scope = SCOPE_GLOBAL; + for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { + funcSym->paramTypes[i] = paramTypes[i]; + funcSym->paramByVal[i] = paramByVal[i]; + } + + // Backpatch any forward-reference calls to this function + patchCallAddrs(p, funcSym); + + expectEndOfStatement(p); + skipNewlines(p); + + // Parse function body + while (!p->hasError && !check(p, TOK_EOF)) { + // Check for END FUNCTION + if (check(p, TOK_END)) { + // Peek ahead -- we need to see if it's END FUNCTION + BasLexerT savedLex = p->lex; + advance(p); + if (check(p, TOK_FUNCTION)) { + advance(p); + break; + } + // Not END FUNCTION, restore and parse as statement + p->lex = savedLex; + } + parseStatement(p); + skipNewlines(p); + } + + // Patch EXIT FUNCTION jumps + exitListPatch(&exitFuncList, p); + exitFuncList = savedExitFunc; + + // Load return value from slot 0 and return + basEmit8(&p->cg, OP_LOAD_LOCAL); + basEmitU16(&p->cg, 0); + basEmit8(&p->cg, OP_RET_VAL); + + // Leave local scope + basSymTabLeaveLocal(&p->sym); + p->currentProc[0] = '\0'; + + // Patch the skip jump + patchJump(p, skipJump); +} + + +static void parseGosub(BasParserT *p) { + // GOSUB label -- push return PC, then JMP to label + advance(p); // consume GOSUB + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "label name"); + return; + } + + char labelName[BAS_MAX_TOKEN_LEN]; + strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + labelName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + // Push the return PC (address after the JMP instruction) + // OP_PUSH_INT32 = 1 + 4 bytes, OP_JMP = 1 + 2 bytes + int32_t pushPos = basCodePos(&p->cg); + basEmit8(&p->cg, OP_PUSH_INT32); + basEmit16(&p->cg, 0); // placeholder lo + basEmit16(&p->cg, 0); // placeholder hi + + // Emit the jump to the label + emitJumpToLabel(p, OP_JMP, labelName); + + // Backpatch the return address (PC is now right after the JMP) + int32_t returnPc = basCodePos(&p->cg); + int16_t lo = (int16_t)(returnPc & 0xFFFF); + int16_t hi = (int16_t)((returnPc >> 16) & 0xFFFF); + basPatch16(&p->cg, pushPos + 1, lo); + basPatch16(&p->cg, pushPos + 3, hi); +} + + +static void parseGoto(BasParserT *p) { + // GOTO label + advance(p); // consume GOTO + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "label name"); + return; + } + + char labelName[BAS_MAX_TOKEN_LEN]; + strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + labelName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + emitJumpToLabel(p, OP_JMP, labelName); +} + + +static void parseIf(BasParserT *p) { + // IF expr THEN + // ... + // [ELSEIF expr THEN] + // ... + // [ELSE] + // ... + // END IF + advance(p); // consume IF + + parseExpression(p); + + expect(p, TOK_THEN); + if (p->hasError) { + return; + } + + // Check for single-line IF: IF cond THEN stmt + if (!check(p, TOK_NEWLINE) && !check(p, TOK_EOF)) { + // Single-line IF + int32_t falseJump = emitJump(p, OP_JMP_FALSE); + + parseStatement(p); + + if (check(p, TOK_ELSE)) { + advance(p); + int32_t endJump = emitJump(p, OP_JMP); + patchJump(p, falseJump); + parseStatement(p); + patchJump(p, endJump); + } else { + patchJump(p, falseJump); + } + return; + } + + // Multi-line IF + expectEndOfStatement(p); + skipNewlines(p); + + int32_t falseJump = emitJump(p, OP_JMP_FALSE); + + // Collect end-of-chain jumps for backpatching + int32_t endJumps[MAX_EXITS]; + int32_t endJumpCount = 0; + + // Parse THEN block + while (!p->hasError && !check(p, TOK_ELSEIF) && !check(p, TOK_ELSE) && !check(p, TOK_EOF)) { + // Check for END IF + if (check(p, TOK_END)) { + BasLexerT savedLex = p->lex; + advance(p); + if (check(p, TOK_IF)) { + advance(p); + patchJump(p, falseJump); + // Patch all end jumps + for (int32_t i = 0; i < endJumpCount; i++) { + patchJump(p, endJumps[i]); + } + return; + } + p->lex = savedLex; + } + parseStatement(p); + skipNewlines(p); + } + + // ELSEIF chain + while (!p->hasError && check(p, TOK_ELSEIF)) { + // Jump from previous true-block to end of chain + if (endJumpCount < MAX_EXITS) { + endJumps[endJumpCount++] = emitJump(p, OP_JMP); + } + + // Patch the previous false jump to here + patchJump(p, falseJump); + + advance(p); // consume ELSEIF + parseExpression(p); + expect(p, TOK_THEN); + + falseJump = emitJump(p, OP_JMP_FALSE); + + expectEndOfStatement(p); + skipNewlines(p); + + while (!p->hasError && !check(p, TOK_ELSEIF) && !check(p, TOK_ELSE) && !check(p, TOK_EOF)) { + if (check(p, TOK_END)) { + BasLexerT savedLex = p->lex; + advance(p); + if (check(p, TOK_IF)) { + advance(p); + patchJump(p, falseJump); + for (int32_t i = 0; i < endJumpCount; i++) { + patchJump(p, endJumps[i]); + } + return; + } + p->lex = savedLex; + } + parseStatement(p); + skipNewlines(p); + } + } + + // ELSE block + if (!p->hasError && check(p, TOK_ELSE)) { + if (endJumpCount < MAX_EXITS) { + endJumps[endJumpCount++] = emitJump(p, OP_JMP); + } + patchJump(p, falseJump); + falseJump = -1; // no more false jump needed + + advance(p); // consume ELSE + expectEndOfStatement(p); + skipNewlines(p); + + while (!p->hasError && !check(p, TOK_EOF)) { + if (check(p, TOK_END)) { + BasLexerT savedLex = p->lex; + advance(p); + if (check(p, TOK_IF)) { + advance(p); + for (int32_t i = 0; i < endJumpCount; i++) { + patchJump(p, endJumps[i]); + } + return; + } + p->lex = savedLex; + } + parseStatement(p); + skipNewlines(p); + } + } + + // Patch the last false jump if no ELSE block + if (falseJump >= 0) { + patchJump(p, falseJump); + } + + // Patch all end-of-chain jumps + for (int32_t i = 0; i < endJumpCount; i++) { + patchJump(p, endJumps[i]); + } + + // If we got here without END IF, that's an error + if (!p->hasError) { + error(p, "Expected END IF"); + } +} + + +static void parseInput(BasParserT *p) { + // INPUT #channel, var + // INPUT [prompt;] var + advance(p); // consume INPUT + + // Check for file I/O: INPUT #channel, var + if (check(p, TOK_HASH)) { + advance(p); // consume # + + // Channel number + parseExpression(p); + + // Comma separator + expect(p, TOK_COMMA); + + // Target variable + if (!check(p, TOK_IDENT)) { + errorExpected(p, "variable name"); + return; + } + + char varName[BAS_MAX_TOKEN_LEN]; + strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + basEmit8(&p->cg, OP_FILE_INPUT); + + BasSymbolT *sym = ensureVariable(p, varName); + + if (sym != NULL) { + // If the variable is numeric, convert the input string + if (sym->dataType != BAS_TYPE_STRING) { + if (sym->dataType == BAS_TYPE_INTEGER || sym->dataType == BAS_TYPE_LONG) { + basEmit8(&p->cg, OP_CONV_STR_INT); + } else { + basEmit8(&p->cg, OP_CONV_STR_FLT); + } + } + + emitStore(p, sym); + } + + return; + } + + // Check for optional prompt string + if (check(p, TOK_STRING_LIT)) { + uint16_t idx = basAddConstant(&p->cg, p->lex.token.text, p->lex.token.textLen); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, idx); + advance(p); + // Semicolon after prompt + if (match(p, TOK_SEMICOLON)) { + // nothing extra + } else if (match(p, TOK_COMMA)) { + // comma -- no question mark (just prompt) + } + } else { + // No prompt -- push empty string + uint16_t idx = basAddConstant(&p->cg, "", 0); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, idx); + } + + // Emit INPUT opcode -- pops prompt, pushes input string + basEmit8(&p->cg, OP_INPUT); + + // Target variable + if (!check(p, TOK_IDENT)) { + errorExpected(p, "variable name"); + return; + } + + char varName[BAS_MAX_TOKEN_LEN]; + strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + BasSymbolT *sym = ensureVariable(p, varName); + if (sym == NULL) { + return; + } + + // If the variable is numeric, we need to convert the input string + if (sym->dataType != BAS_TYPE_STRING) { + if (sym->dataType == BAS_TYPE_INTEGER || sym->dataType == BAS_TYPE_LONG) { + basEmit8(&p->cg, OP_CONV_STR_INT); + } else { + basEmit8(&p->cg, OP_CONV_STR_FLT); + } + } + + emitStore(p, sym); +} + + +static void parseLineInput(BasParserT *p) { + // LINE INPUT #channel, var + advance(p); // consume LINE + + if (!check(p, TOK_INPUT)) { + error(p, "Expected INPUT after LINE"); + return; + } + + advance(p); // consume INPUT + + // Must have # for file I/O + if (!match(p, TOK_HASH)) { + error(p, "Expected # for file channel in LINE INPUT"); + return; + } + + // Channel expression + parseExpression(p); + + // Comma separator + expect(p, TOK_COMMA); + + // Target variable + if (!check(p, TOK_IDENT)) { + errorExpected(p, "variable name"); + return; + } + + char varName[BAS_MAX_TOKEN_LEN]; + strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + basEmit8(&p->cg, OP_FILE_LINE_INPUT); + + BasSymbolT *sym = ensureVariable(p, varName); + + if (sym != NULL) { + emitStore(p, sym); + } +} + + +static void parseModule(BasParserT *p) { + skipNewlines(p); + + while (!p->hasError && !check(p, TOK_EOF)) { + parseStatement(p); + skipNewlines(p); + } + + // End of module -- emit HALT + basEmit8(&p->cg, OP_HALT); +} + + +#define MAX_ON_LABELS 32 + +static void parseOn(BasParserT *p) { + // ON ERROR GOTO label -- error handler + // ON expr GOTO label1, label2, ... -- computed goto + // ON expr GOSUB label1, label2, ... -- computed gosub + advance(p); // consume ON + + // ON ERROR GOTO is a special form + if (check(p, TOK_ERROR_KW)) { + parseOnError(p); + return; + } + + // ON expr GOTO/GOSUB label1, label2, ... + parseExpression(p); + + bool isGosub; + + if (check(p, TOK_GOTO)) { + isGosub = false; + advance(p); + } else if (check(p, TOK_GOSUB)) { + isGosub = true; + advance(p); + } else { + error(p, "Expected GOTO or GOSUB after ON expression"); + return; + } + + // Track end-of-gosub jumps for patching + int32_t endJumps[MAX_ON_LABELS]; + int32_t endJumpCount = 0; + int32_t labelIdx = 1; + + for (;;) { + if (p->hasError) { + return; + } + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "label name"); + return; + } + + char labelName[BAS_MAX_TOKEN_LEN]; + strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + labelName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + // DUP the selector + basEmit8(&p->cg, OP_DUP); + + // PUSH the 1-based index + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, (int16_t)labelIdx); + + // Compare + basEmit8(&p->cg, OP_CMP_EQ); + + // JMP_FALSE to skip this branch + int32_t skipAddr = emitJump(p, OP_JMP_FALSE); + + // Match: POP the selector value + basEmit8(&p->cg, OP_POP); + + if (isGosub) { + // Push return PC before jumping + int32_t pushPos = basCodePos(&p->cg); + basEmit8(&p->cg, OP_PUSH_INT32); + basEmit16(&p->cg, 0); // placeholder lo + basEmit16(&p->cg, 0); // placeholder hi + + emitJumpToLabel(p, OP_JMP, labelName); + + // Backpatch the return address + int32_t returnPc = basCodePos(&p->cg); + int16_t lo = (int16_t)(returnPc & 0xFFFF); + int16_t hi = (int16_t)((returnPc >> 16) & 0xFFFF); + basPatch16(&p->cg, pushPos + 1, lo); + basPatch16(&p->cg, pushPos + 3, hi); + + // After GOSUB returns, jump to end of ON...GOSUB + if (endJumpCount < MAX_ON_LABELS) { + endJumps[endJumpCount++] = emitJump(p, OP_JMP); + } + } else { + // GOTO: just jump to the label + emitJumpToLabel(p, OP_JMP, labelName); + } + + // Patch the skip (no-match continues to next branch) + patchJump(p, skipAddr); + + labelIdx++; + + if (!match(p, TOK_COMMA)) { + break; + } + } + + // No match: POP the selector and fall through + basEmit8(&p->cg, OP_POP); + + // Patch all end-of-gosub jumps to here + int32_t endTarget = basCodePos(&p->cg); + + for (int32_t i = 0; i < endJumpCount; i++) { + int16_t offset = (int16_t)(endTarget - (endJumps[i] + 2)); + basPatch16(&p->cg, endJumps[i], offset); + } +} + + +static void parseOnError(BasParserT *p) { + // ON ERROR GOTO label + // ON ERROR GOTO 0 (disable) + // Note: ON and ERROR already consumed by parseOn dispatcher + advance(p); // consume ERROR + + if (!check(p, TOK_GOTO)) { + error(p, "Expected GOTO after ON ERROR"); + return; + } + advance(p); // consume GOTO + + // ON ERROR GOTO 0 -- disable error handler + if (check(p, TOK_INT_LIT) && p->lex.token.intVal == 0) { + advance(p); + basEmit8(&p->cg, OP_ON_ERROR); + basEmit16(&p->cg, 0); + return; + } + + // ON ERROR GOTO label + if (!check(p, TOK_IDENT)) { + errorExpected(p, "label name or 0"); + return; + } + + char labelName[BAS_MAX_TOKEN_LEN]; + strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + labelName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + // Look up the label + BasSymbolT *sym = basSymTabFind(&p->sym, labelName); + + if (sym != NULL && sym->kind == SYM_LABEL && sym->isDefined) { + // Label already defined -- emit ON_ERROR with offset to handler + basEmit8(&p->cg, OP_ON_ERROR); + int32_t here = basCodePos(&p->cg); + int16_t offset = (int16_t)(sym->codeAddr - (here + 2)); + basEmit16(&p->cg, offset); + } else { + // Forward reference + if (sym == NULL) { + sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0); + if (sym == NULL) { + error(p, "Symbol table full"); + return; + } + sym->scope = SCOPE_GLOBAL; + sym->isDefined = false; + sym->codeAddr = 0; + } + + basEmit8(&p->cg, OP_ON_ERROR); + int32_t patchAddr = basCodePos(&p->cg); + basEmit16(&p->cg, 0); + + if (sym->patchCount < BAS_MAX_CALL_PATCHES) { + sym->patchAddrs[sym->patchCount++] = patchAddr; + } + } +} + + +static void parseOpen(BasParserT *p) { + // OPEN filename FOR mode AS #channel + advance(p); // consume OPEN + + // Filename expression + parseExpression(p); + + // FOR keyword + expect(p, TOK_FOR); + + // Mode: INPUT, OUTPUT, APPEND + uint8_t mode; + + if (check(p, TOK_INPUT)) { + mode = 1; // INPUT + advance(p); + } else if (check(p, TOK_OUTPUT)) { + mode = 2; // OUTPUT + advance(p); + } else if (check(p, TOK_APPEND)) { + mode = 3; // APPEND + advance(p); + } else if (check(p, TOK_RANDOM)) { + mode = 4; // RANDOM + advance(p); + } else if (check(p, TOK_BINARY)) { + mode = 5; // BINARY + advance(p); + } else { + error(p, "Expected INPUT, OUTPUT, APPEND, RANDOM, or BINARY after FOR"); + return; + } + + // AS keyword + expect(p, TOK_AS); + + // Optional # prefix + match(p, TOK_HASH); + + // Channel number expression + parseExpression(p); + + // Optional LEN = recordsize (for RANDOM mode) + if (checkKeyword(p, "LEN")) { + advance(p); // consume LEN + expect(p, TOK_EQ); + // For now we just parse and discard -- record length is not + // enforced at the VM level (GET/PUT use variable type size) + parseExpression(p); + basEmit8(&p->cg, OP_POP); + } + + // Emit: stack has [filename, channel] -- OP_FILE_OPEN reads mode byte + basEmit8(&p->cg, OP_FILE_OPEN); + basEmit8(&p->cg, mode); +} + + +static void parseOption(BasParserT *p) { + // OPTION BASE 0 | OPTION BASE 1 + // OPTION COMPARE BINARY | OPTION COMPARE TEXT + advance(p); // consume OPTION + + if (check(p, TOK_BASE)) { + advance(p); // consume BASE + + if (!check(p, TOK_INT_LIT)) { + error(p, "Expected 0 or 1 after OPTION BASE"); + return; + } + + int32_t base = p->lex.token.intVal; + + if (base != 0 && base != 1) { + error(p, "OPTION BASE must be 0 or 1"); + return; + } + + p->optionBase = base; + advance(p); + return; + } + + if (checkKeyword(p, "COMPARE")) { + advance(p); // consume COMPARE + + if (check(p, TOK_BINARY)) { + p->optionCompareText = false; + advance(p); + basEmit8(&p->cg, OP_COMPARE_MODE); + basEmit8(&p->cg, 0); + } else if (checkKeyword(p, "TEXT")) { + p->optionCompareText = true; + advance(p); + basEmit8(&p->cg, OP_COMPARE_MODE); + basEmit8(&p->cg, 1); + } else { + error(p, "Expected BINARY or TEXT after OPTION COMPARE"); + } + + return; + } + + if (check(p, TOK_EXPLICIT)) { + advance(p); + p->optionExplicit = true; + return; + } + + error(p, "Expected BASE, COMPARE, or EXPLICIT after OPTION"); +} + + +static void parsePrint(BasParserT *p) { + // PRINT [#channel, expr] + // PRINT [expr] [; expr] [, expr] [;] + // PRINT USING "fmt"; expr [; expr] ... + advance(p); // consume PRINT + + // Check for file I/O: PRINT #channel, expr + if (check(p, TOK_HASH)) { + advance(p); // consume # + + // Channel number + parseExpression(p); + + // Comma separator + expect(p, TOK_COMMA); + + // Value to print + parseExpression(p); + + basEmit8(&p->cg, OP_FILE_PRINT); + return; + } + + // Check for PRINT USING + if (checkKeyword(p, "USING")) { + advance(p); // consume USING + + // Parse format string expression + parseExpression(p); + + // Semicolon separates format from values + expect(p, TOK_SEMICOLON); + + // Parse values, each one gets formatted with PRINT_USING + for (;;) { + parseExpression(p); + basEmit8(&p->cg, OP_PRINT_USING); + basEmit8(&p->cg, OP_PRINT); + + if (check(p, TOK_SEMICOLON)) { + advance(p); + if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { + break; + } + continue; + } + + break; + } + + basEmit8(&p->cg, OP_PRINT_NL); + return; + } + + bool trailingSemicolon = false; + + // Empty PRINT = just newline + if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { + basEmit8(&p->cg, OP_PRINT_NL); + return; + } + + while (!p->hasError) { + trailingSemicolon = false; + + if (check(p, TOK_SEMICOLON)) { + // Just a semicolon -- no space + trailingSemicolon = true; + advance(p); + if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { + break; + } + continue; + } + + if (check(p, TOK_COMMA)) { + // Comma -- print tab + basEmit8(&p->cg, OP_PRINT_TAB); + advance(p); + if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { + trailingSemicolon = true; // comma at end suppresses newline too + break; + } + continue; + } + + if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { + break; + } + + // Check for SPC(n) and TAB(n) inside PRINT + if (checkKeyword(p, "SPC")) { + advance(p); + expect(p, TOK_LPAREN); + parseExpression(p); + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_PRINT_SPC_N); + continue; + } + + if (checkKeyword(p, "TAB")) { + advance(p); + expect(p, TOK_LPAREN); + parseExpression(p); + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_PRINT_TAB_N); + continue; + } + + // Expression + parseExpression(p); + basEmit8(&p->cg, OP_PRINT); + } + + // Print newline unless suppressed by trailing semicolon/comma + if (!trailingSemicolon) { + basEmit8(&p->cg, OP_PRINT_NL); + } +} + + +static void parseRead(BasParserT *p) { + // READ var1, var2, ... + advance(p); // consume READ + + for (;;) { + if (p->hasError) { + return; + } + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "variable name"); + return; + } + + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + BasSymbolT *sym = ensureVariable(p, name); + if (sym == NULL) { + return; + } + + basEmit8(&p->cg, OP_READ_DATA); + emitStore(p, sym); + + if (!match(p, TOK_COMMA)) { + break; + } + } +} + + +static void parseRedim(BasParserT *p) { + // REDIM [PRESERVE] var(bounds) AS type + advance(p); // consume REDIM + + uint8_t preserve = 0; + if (check(p, TOK_PRESERVE)) { + preserve = 1; + advance(p); + } + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "array variable name"); + return; + } + + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + BasSymbolT *sym = basSymTabFind(&p->sym, name); + if (sym == NULL) { + sym = ensureVariable(p, name); + } + if (sym == NULL) { + return; + } + sym->isArray = true; + + // Load the old array reference + emitLoad(p, sym); + + // Parse new bounds + int32_t dims = 0; + expect(p, TOK_LPAREN); + parseDimBounds(p, &dims); + expect(p, TOK_RPAREN); + + // Optional AS type + if (match(p, TOK_AS)) { + resolveTypeName(p); + } + + if (p->hasError) { + return; + } + + basEmit8(&p->cg, OP_REDIM); + basEmit8(&p->cg, (uint8_t)dims); + basEmit8(&p->cg, preserve); + emitStore(p, sym); +} + + +static void parseRestore(BasParserT *p) { + // RESTORE -- reset the DATA read pointer to the beginning + advance(p); // consume RESTORE + basEmit8(&p->cg, OP_RESTORE); +} + + +static void parseResume(BasParserT *p) { + // RESUME -- re-execute the statement that caused the error + // RESUME NEXT -- continue at the next statement after the error + advance(p); // consume RESUME + + if (check(p, TOK_NEXT)) { + advance(p); + basEmit8(&p->cg, OP_RESUME_NEXT); + } else { + basEmit8(&p->cg, OP_RESUME); + } +} + + +static void parseSelectCase(BasParserT *p) { + // SELECT CASE expr + // CASE val [, val] ... + // ... + // [CASE ELSE] + // ... + // END SELECT + advance(p); // consume SELECT + expect(p, TOK_CASE); + + // Evaluate the test expression -- stays on stack throughout + parseExpression(p); + + expectEndOfStatement(p); + skipNewlines(p); + + int32_t endJumps[MAX_EXITS]; + int32_t endJumpCount = 0; + + while (!p->hasError && !check(p, TOK_EOF)) { + // Check for END SELECT + if (check(p, TOK_END)) { + BasLexerT savedLex = p->lex; + advance(p); + if (check(p, TOK_SELECT)) { + advance(p); + basEmit8(&p->cg, OP_POP); // pop test expression + for (int32_t i = 0; i < endJumpCount; i++) { + patchJump(p, endJumps[i]); + } + return; + } + p->lex = savedLex; + } + + if (!check(p, TOK_CASE)) { + error(p, "Expected CASE or END SELECT"); + return; + } + + advance(p); // consume CASE + + // CASE ELSE -- always matches, no comparison needed + if (check(p, TOK_ELSE)) { + advance(p); + expectEndOfStatement(p); + skipNewlines(p); + + // Parse body until END SELECT + while (!p->hasError && !check(p, TOK_EOF)) { + if (check(p, TOK_END)) { + BasLexerT savedLex = p->lex; + advance(p); + if (check(p, TOK_SELECT)) { + advance(p); + basEmit8(&p->cg, OP_POP); + for (int32_t i = 0; i < endJumpCount; i++) { + patchJump(p, endJumps[i]); + } + return; + } + p->lex = savedLex; + } + parseStatement(p); + skipNewlines(p); + } + continue; + } + + // CASE val [, val] ... + // + // Strategy for multi-value CASE using JMP_TRUE chaining: + // For each value: + // DUP testval + // push value + // CMP_EQ + // JMP_TRUE -> body + // JMP -> next_case (none of the values matched) + // body: + // ...statements... + // JMP -> end_select + // next_case: + + int32_t bodyJumps[MAX_EXITS]; + int32_t bodyJumpCount = 0; + + // First value + basEmit8(&p->cg, OP_DUP); + parseExpression(p); + basEmit8(&p->cg, OP_CMP_EQ); + + if (bodyJumpCount < MAX_EXITS) { + bodyJumps[bodyJumpCount++] = emitJump(p, OP_JMP_TRUE); + } + + // Additional comma-separated values + while (!p->hasError && match(p, TOK_COMMA)) { + basEmit8(&p->cg, OP_DUP); + parseExpression(p); + basEmit8(&p->cg, OP_CMP_EQ); + + if (bodyJumpCount < MAX_EXITS) { + bodyJumps[bodyJumpCount++] = emitJump(p, OP_JMP_TRUE); + } + } + + // None matched -- jump to next case + int32_t nextCaseJump = emitJump(p, OP_JMP); + + // Patch all body jumps to here (start of body) + for (int32_t i = 0; i < bodyJumpCount; i++) { + patchJump(p, bodyJumps[i]); + } + + // Parse the CASE body + expectEndOfStatement(p); + skipNewlines(p); + + while (!p->hasError && !check(p, TOK_CASE) && !check(p, TOK_EOF)) { + if (check(p, TOK_END)) { + BasLexerT savedLex = p->lex; + advance(p); + if (check(p, TOK_SELECT)) { + advance(p); + basEmit8(&p->cg, OP_POP); + patchJump(p, nextCaseJump); + for (int32_t i = 0; i < endJumpCount; i++) { + patchJump(p, endJumps[i]); + } + return; + } + p->lex = savedLex; + } + parseStatement(p); + skipNewlines(p); + } + + // Jump to end of SELECT (skip remaining cases) + if (endJumpCount < MAX_EXITS) { + endJumps[endJumpCount++] = emitJump(p, OP_JMP); + } + + // Patch the next-case jump to here + patchJump(p, nextCaseJump); + } + + // Pop test value (reached if no END SELECT but hit EOF) + basEmit8(&p->cg, OP_POP); + + for (int32_t i = 0; i < endJumpCount; i++) { + patchJump(p, endJumps[i]); + } + + if (!p->hasError) { + error(p, "Expected END SELECT"); + } +} + + +static void parseShell(BasParserT *p) { + // SHELL "command" -- execute an OS command (discard return value) + // SHELL -- no argument, no-op in embedded context + advance(p); // consume SHELL + + if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { + // No argument -- push empty string and call SHELL (no-op) + uint16_t idx = basAddConstant(&p->cg, "", 0); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, idx); + } else { + parseExpression(p); + } + + basEmit8(&p->cg, OP_SHELL); + basEmit8(&p->cg, OP_POP); // discard return value in statement form +} + + +static void parseSleep(BasParserT *p) { + // SLEEP [seconds] + // If no argument, default to 1 second + advance(p); // consume SLEEP + + if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { + // No argument -- push 1 second + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 1); + } else { + parseExpression(p); + } + + basEmit8(&p->cg, OP_SLEEP); +} + + +static void parseStatic(BasParserT *p) { + // STATIC var AS type + // Only valid inside SUB/FUNCTION. Creates a global variable with a + // mangled name (procName$varName) that persists across calls. + advance(p); // consume STATIC + + if (!p->sym.inLocalScope) { + error(p, "STATIC is only valid inside SUB or FUNCTION"); + return; + } + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "variable name"); + return; + } + + char varName[BAS_MAX_TOKEN_LEN]; + strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + // Optional AS type + uint8_t dt = suffixToType(varName); + if (match(p, TOK_AS)) { + dt = resolveTypeName(p); + } + + if (p->hasError) { + return; + } + + // Create a mangled global name: "procName$varName" + char mangledName[BAS_MAX_SYMBOL_NAME]; + snprintf(mangledName, sizeof(mangledName), "%s$%s", p->currentProc, varName); + + // Create the global variable with the mangled name + bool savedLocal = p->sym.inLocalScope; + p->sym.inLocalScope = false; + BasSymbolT *globalSym = basSymTabAdd(&p->sym, mangledName, SYM_VARIABLE, dt); + p->sym.inLocalScope = savedLocal; + + if (globalSym == NULL) { + error(p, "Symbol table full or duplicate STATIC variable"); + return; + } + globalSym->scope = SCOPE_GLOBAL; + globalSym->index = p->sym.nextGlobalIdx++; + globalSym->isDefined = true; + + // Create a local alias that maps to this global's index + BasSymbolT *localSym = basSymTabAdd(&p->sym, varName, SYM_VARIABLE, dt); + if (localSym == NULL) { + error(p, "Symbol table full or duplicate variable name"); + return; + } + localSym->scope = SCOPE_GLOBAL; // accessed as global + localSym->index = globalSym->index; + localSym->isDefined = true; +} + + +static void parseStatement(BasParserT *p) { + if (p->hasError) { + return; + } + + skipNewlines(p); + + if (check(p, TOK_EOF)) { + return; + } + + BasTokenTypeE tt = p->lex.token.type; + + switch (tt) { + case TOK_PRINT: + parsePrint(p); + break; + + case TOK_DIM: + parseDim(p); + break; + + case TOK_DATA: + parseData(p); + break; + + case TOK_READ: + parseRead(p); + break; + + case TOK_RESTORE: + parseRestore(p); + break; + + case TOK_STATIC: + parseStatic(p); + break; + + case TOK_DEF: + parseDef(p); + break; + + case TOK_DEFINT: + parseDefType(p, BAS_TYPE_INTEGER); + break; + + case TOK_DEFLNG: + parseDefType(p, BAS_TYPE_LONG); + break; + + case TOK_DEFSNG: + parseDefType(p, BAS_TYPE_SINGLE); + break; + + case TOK_DEFDBL: + parseDefType(p, BAS_TYPE_DOUBLE); + break; + + case TOK_DEFSTR: + parseDefType(p, BAS_TYPE_STRING); + break; + + case TOK_DECLARE: + parseDeclare(p); + break; + + case TOK_IF: + parseIf(p); + break; + + case TOK_FOR: + parseFor(p); + break; + + case TOK_DO: + parseDo(p); + break; + + case TOK_WHILE: + parseWhile(p); + break; + + case TOK_SELECT: + parseSelectCase(p); + break; + + case TOK_SUB: + parseSub(p); + break; + + case TOK_FUNCTION: + parseFunction(p); + break; + + case TOK_EXIT: + parseExit(p); + break; + + case TOK_CONST: + parseConst(p); + break; + + case TOK_END: + parseEnd(p); + break; + + case TOK_ERASE: + parseErase(p); + break; + + case TOK_TYPE: + parseType(p); + break; + + case TOK_REDIM: + parseRedim(p); + break; + + case TOK_INPUT: + parseInput(p); + break; + + case TOK_OPEN: + parseOpen(p); + break; + + case TOK_CLOSE: + parseClose(p); + break; + + case TOK_GET: + parseGet(p); + break; + + case TOK_PUT: + parsePut(p); + break; + + case TOK_SEEK: + parseSeek(p); + break; + + case TOK_WRITE: + parseWrite(p); + break; + + case TOK_LINE: + parseLineInput(p); + break; + + case TOK_GOTO: + parseGoto(p); + break; + + case TOK_GOSUB: + parseGosub(p); + break; + + case TOK_ON: + parseOn(p); + break; + + case TOK_OPTION: + parseOption(p); + break; + + case TOK_SHELL: + parseShell(p); + break; + + case TOK_RESUME: + parseResume(p); + break; + + case TOK_RETURN: + advance(p); + if (p->sym.inLocalScope) { + // Inside SUB/FUNCTION: return from subroutine + basEmit8(&p->cg, OP_RET); + } else { + // Module level: GOSUB return (pop PC from eval stack) + basEmit8(&p->cg, OP_GOSUB_RET); + } + break; + + case TOK_SLEEP: + parseSleep(p); + break; + + case TOK_SWAP: + parseSwap(p); + break; + + case TOK_CALL: { + advance(p); // consume CALL + if (!check(p, TOK_IDENT)) { + errorExpected(p, "subroutine name"); + break; + } + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + BasSymbolT *sym = basSymTabFind(&p->sym, name); + if (sym == NULL) { + // Forward reference + sym = basSymTabAdd(&p->sym, name, SYM_SUB, BAS_TYPE_INTEGER); + if (sym == NULL) { + error(p, "Symbol table full"); + break; + } + sym->scope = SCOPE_GLOBAL; + sym->isDefined = false; + sym->codeAddr = 0; + } + + if (check(p, TOK_LPAREN)) { + emitFunctionCall(p, sym); + } else { + // CALL with no arguments + uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0; + basEmit8(&p->cg, OP_CALL); + int32_t addrPos = basCodePos(&p->cg); + basEmitU16(&p->cg, (uint16_t)sym->codeAddr); + basEmit8(&p->cg, 0); + basEmit8(&p->cg, baseSlot); + + if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) { + sym->patchAddrs[sym->patchCount++] = addrPos; + } + } + + if (sym->kind == SYM_FUNCTION) { + basEmit8(&p->cg, OP_POP); // discard return value + } + break; + } + + case TOK_RANDOMIZE: + advance(p); + if (check(p, TOK_TIMER)) { + advance(p); + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, -1); + } else { + parseExpression(p); + } + basEmit8(&p->cg, OP_MATH_RANDOMIZE); + break; + + case TOK_DOEVENTS: + advance(p); + basEmit8(&p->cg, OP_DO_EVENTS); + break; + + case TOK_LET: + advance(p); // consume LET, then fall through to assignment + if (!check(p, TOK_IDENT)) { + errorExpected(p, "variable name after LET"); + break; + } + parseAssignOrCall(p); + break; + + case TOK_IDENT: { + // Check for label: identifier followed by colon + BasLexerT savedLex = p->lex; + char labelName[BAS_MAX_TOKEN_LEN]; + strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + labelName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + if (check(p, TOK_COLON)) { + advance(p); // consume colon + // Record the label at the current code position + BasSymbolT *sym = basSymTabFind(&p->sym, labelName); + if (sym != NULL && sym->kind == SYM_LABEL) { + // Forward-declared label -- now define it + sym->codeAddr = basCodePos(&p->cg); + sym->isDefined = true; + patchLabelRefs(p, sym); + } else if (sym == NULL) { + sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0); + if (sym == NULL) { + error(p, "Symbol table full"); + break; + } + sym->scope = SCOPE_GLOBAL; + sym->isDefined = true; + sym->codeAddr = basCodePos(&p->cg); + } else { + char buf[256]; + snprintf(buf, sizeof(buf), "Name '%s' already used", labelName); + error(p, buf); + } + // After the label, there may be a statement on the same line + // which will be parsed on the next iteration + break; + } + + // Not a label -- restore and parse as assignment/call + p->lex = savedLex; + parseAssignOrCall(p); + break; + } + + case TOK_REM: + // Comment -- skip to end of line + advance(p); + break; + + default: { + char buf[256]; + snprintf(buf, sizeof(buf), "Unexpected token: %s", basTokenName(tt)); + error(p, buf); + break; + } + } + + if (!p->hasError) { + expectEndOfStatement(p); + } +} + + +static void parseSub(BasParserT *p) { + // SUB name(params) + // ... + // END SUB + advance(p); // consume SUB + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "subroutine name"); + return; + } + + char name[BAS_MAX_TOKEN_LEN]; + strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + name[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + // Save current proc name for STATIC variable mangling + strncpy(p->currentProc, name, BAS_MAX_TOKEN_LEN - 1); + p->currentProc[BAS_MAX_TOKEN_LEN - 1] = '\0'; + + // Jump over the sub body in module-level code + int32_t skipJump = emitJump(p, OP_JMP); + + int32_t subAddr = basCodePos(&p->cg); + + // Enter local scope + basSymTabEnterLocal(&p->sym); + + ExitListT savedExitSub = exitSubList; + exitListInit(&exitSubList); + + // Parse parameter list + int32_t paramCount = 0; + uint8_t paramTypes[BAS_MAX_PARAMS]; + bool paramByVal[BAS_MAX_PARAMS]; + + if (match(p, TOK_LPAREN)) { + while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) { + if (paramCount > 0) { + expect(p, TOK_COMMA); + } + + bool byVal = false; + if (match(p, TOK_BYVAL)) { + byVal = true; + } + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "parameter name"); + return; + } + + char paramName[BAS_MAX_TOKEN_LEN]; + strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + uint8_t pdt = suffixToType(paramName); + if (match(p, TOK_AS)) { + pdt = resolveTypeName(p); + } + + BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); + if (paramSym == NULL) { + error(p, "Symbol table full"); + return; + } + paramSym->scope = SCOPE_LOCAL; + paramSym->index = basSymTabAllocSlot(&p->sym); + paramSym->isDefined = true; + + if (paramCount < BAS_MAX_PARAMS) { + paramTypes[paramCount] = pdt; + paramByVal[paramCount] = byVal; + } + paramCount++; + } + expect(p, TOK_RPAREN); + } + + // Register the sub in the symbol table (global scope) + BasSymbolT *existing = basSymTabFindGlobal(&p->sym, name); + BasSymbolT *subSym = NULL; + + if (existing != NULL && existing->kind == SYM_SUB) { + subSym = existing; + } else { + bool savedLocal = p->sym.inLocalScope; + p->sym.inLocalScope = false; + subSym = basSymTabAdd(&p->sym, name, SYM_SUB, BAS_TYPE_INTEGER); + p->sym.inLocalScope = savedLocal; + } + + if (subSym == NULL) { + error(p, "Could not register subroutine"); + return; + } + + subSym->codeAddr = subAddr; + subSym->isDefined = true; + subSym->paramCount = paramCount; + subSym->scope = SCOPE_GLOBAL; + for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { + subSym->paramTypes[i] = paramTypes[i]; + subSym->paramByVal[i] = paramByVal[i]; + } + + // Backpatch any forward-reference calls to this sub + patchCallAddrs(p, subSym); + + expectEndOfStatement(p); + skipNewlines(p); + + // Parse sub body + while (!p->hasError && !check(p, TOK_EOF)) { + if (check(p, TOK_END)) { + BasLexerT savedLex = p->lex; + advance(p); + if (check(p, TOK_SUB)) { + advance(p); + break; + } + p->lex = savedLex; + } + parseStatement(p); + skipNewlines(p); + } + + // Patch EXIT SUB jumps + exitListPatch(&exitSubList, p); + exitSubList = savedExitSub; + + basEmit8(&p->cg, OP_RET); + + // Leave local scope + basSymTabLeaveLocal(&p->sym); + p->currentProc[0] = '\0'; + + // Patch the skip jump + patchJump(p, skipJump); +} + + +static void parseType(BasParserT *p) { + // TYPE name + // field AS type + // ... + // END TYPE + advance(p); // consume TYPE + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "type name"); + return; + } + + char typeName[BAS_MAX_TOKEN_LEN]; + strncpy(typeName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + typeName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + // Add TYPE_DEF symbol + bool savedLocal = p->sym.inLocalScope; + p->sym.inLocalScope = false; + BasSymbolT *typeSym = basSymTabAdd(&p->sym, typeName, SYM_TYPE_DEF, BAS_TYPE_UDT); + p->sym.inLocalScope = savedLocal; + + if (typeSym == NULL) { + error(p, "Symbol table full or duplicate TYPE name"); + return; + } + typeSym->scope = SCOPE_GLOBAL; + typeSym->isDefined = true; + typeSym->index = p->sym.count - 1; + typeSym->fieldCount = 0; + + expectEndOfStatement(p); + skipNewlines(p); + + // Parse fields until END TYPE + while (!p->hasError && !check(p, TOK_EOF)) { + if (check(p, TOK_END)) { + BasLexerT savedLex = p->lex; + advance(p); + if (check(p, TOK_TYPE)) { + advance(p); + break; + } + p->lex = savedLex; + } + + if (!check(p, TOK_IDENT)) { + errorExpected(p, "field name or END TYPE"); + return; + } + + if (typeSym->fieldCount >= BAS_MAX_UDT_FIELDS) { + error(p, "Too many fields in TYPE"); + return; + } + + BasFieldDefT *field = &typeSym->fields[typeSym->fieldCount]; + strncpy(field->name, p->lex.token.text, BAS_MAX_SYMBOL_NAME - 1); + field->name[BAS_MAX_SYMBOL_NAME - 1] = '\0'; + advance(p); + + expect(p, TOK_AS); + field->dataType = resolveTypeName(p); + if (field->dataType == BAS_TYPE_UDT) { + field->udtTypeId = p->lastUdtTypeId; + } + + typeSym->fieldCount++; + + expectEndOfStatement(p); + skipNewlines(p); + } +} + + +static void parseSwap(BasParserT *p) { + // SWAP a, b -- swap the values of two variables + advance(p); // consume SWAP + + // First variable + if (!check(p, TOK_IDENT)) { + errorExpected(p, "variable name"); + return; + } + + char nameA[BAS_MAX_TOKEN_LEN]; + strncpy(nameA, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + nameA[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + BasSymbolT *symA = ensureVariable(p, nameA); + if (symA == NULL) { + return; + } + + expect(p, TOK_COMMA); + + // Second variable + if (!check(p, TOK_IDENT)) { + errorExpected(p, "variable name"); + return; + } + + char nameB[BAS_MAX_TOKEN_LEN]; + strncpy(nameB, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + nameB[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + BasSymbolT *symB = ensureVariable(p, nameB); + if (symB == NULL) { + return; + } + + // Emit: load a, load b, store a, store b + emitLoad(p, symA); + emitLoad(p, symB); + emitStore(p, symA); + emitStore(p, symB); +} +static void parseGet(BasParserT *p) { + // GET #channel, [recno], var + advance(p); // consume GET + + match(p, TOK_HASH); // optional # + + // Channel number + parseExpression(p); + expect(p, TOK_COMMA); + + // Optional record number + if (check(p, TOK_COMMA)) { + // No record number specified -- push 0 (current position) + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + } else { + parseExpression(p); + } + expect(p, TOK_COMMA); + + // Target variable + if (!check(p, TOK_IDENT)) { + errorExpected(p, "variable name"); + return; + } + + char varName[BAS_MAX_TOKEN_LEN]; + strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + BasSymbolT *sym = ensureVariable(p, varName); + if (sym == NULL) { + return; + } + + // Push variable type so VM knows how many bytes to read + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, (int16_t)sym->dataType); + + basEmit8(&p->cg, OP_FILE_GET); + + emitStore(p, sym); +} + + +static void parsePut(BasParserT *p) { + // PUT #channel, [recno], var + advance(p); // consume PUT + + match(p, TOK_HASH); // optional # + + // Channel number + parseExpression(p); + expect(p, TOK_COMMA); + + // Optional record number + if (check(p, TOK_COMMA)) { + // No record number specified -- push 0 (current position) + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + } else { + parseExpression(p); + } + expect(p, TOK_COMMA); + + // Value expression + parseExpression(p); + + basEmit8(&p->cg, OP_FILE_PUT); +} + + +static void parseSeek(BasParserT *p) { + // SEEK #channel, position + advance(p); // consume SEEK + + match(p, TOK_HASH); // optional # + + // Channel number + parseExpression(p); + + expect(p, TOK_COMMA); + + // Position + parseExpression(p); + + basEmit8(&p->cg, OP_FILE_SEEK); +} + + +static void parseWrite(BasParserT *p) { + // WRITE #channel, expr1, expr2, ... + // Values are comma-delimited. Strings are quoted. Numbers undecorated. + // Each WRITE statement ends with a newline. + advance(p); // consume WRITE + + if (!check(p, TOK_HASH)) { + error(p, "Expected # after WRITE"); + return; + } + advance(p); // consume # + + // Channel number expression + parseExpression(p); + + // Comma separator between channel and first value + expect(p, TOK_COMMA); + + // Parse each value + bool first = true; + while (!p->hasError) { + if (!first) { + // Emit comma separator to file + basEmit8(&p->cg, OP_DUP); // dup channel for separator + basEmit8(&p->cg, OP_FILE_WRITE_SEP); + } + first = false; + + // Duplicate channel for the write operation + basEmit8(&p->cg, OP_DUP); + + // Parse value expression + parseExpression(p); + + // Write value in WRITE format (strings quoted, numbers undecorated) + basEmit8(&p->cg, OP_FILE_WRITE); + + if (!match(p, TOK_COMMA)) { + break; + } + } + + // Write newline to file (channel still on stack) + basEmit8(&p->cg, OP_FILE_WRITE_NL); +} + + + + +static void parseWhile(BasParserT *p) { + // WHILE cond + // ... + // WEND + advance(p); // consume WHILE + + ExitListT savedExitDo = exitDoList; + exitListInit(&exitDoList); + + int32_t loopTop = basCodePos(&p->cg); + + parseExpression(p); + int32_t falseJump = emitJump(p, OP_JMP_FALSE); + + expectEndOfStatement(p); + skipNewlines(p); + + while (!p->hasError && !check(p, TOK_WEND) && !check(p, TOK_EOF)) { + parseStatement(p); + skipNewlines(p); + } + + if (p->hasError) { + return; + } + + expect(p, TOK_WEND); + + // Jump back to loop top + basEmit8(&p->cg, OP_JMP); + int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2)); + basEmit16(&p->cg, backOffset); + + // Patch the false jump to exit + patchJump(p, falseJump); + + // Patch EXIT DO jumps (WHILE/WEND uses the DO exit list) + exitListPatch(&exitDoList, p); + exitDoList = savedExitDo; +} + + +// ============================================================ +// Public API +// ============================================================ + +void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) { + memset(p, 0, sizeof(BasParserT)); + basLexerInit(&p->lex, source, sourceLen); + basCodeGenInit(&p->cg); + basSymTabInit(&p->sym); + p->hasError = false; + p->errorLine = 0; + p->error[0] = '\0'; + + exitListInit(&exitForList); + exitListInit(&exitDoList); + exitListInit(&exitSubList); + exitListInit(&exitFuncList); + + // basLexerInit already primes the first token -- no advance needed +} + + +bool basParse(BasParserT *p) { + parseModule(p); + return !p->hasError; +} + + +BasModuleT *basParserBuildModule(BasParserT *p) { + if (p->hasError) { + return NULL; + } + p->cg.globalCount = p->sym.nextGlobalIdx; + return basCodeGenBuildModule(&p->cg); +} + + +void basParserFree(BasParserT *p) { + basCodeGenFree(&p->cg); +} diff --git a/dvxbasic/compiler/parser.h b/dvxbasic/compiler/parser.h new file mode 100644 index 0000000..b4828c8 --- /dev/null +++ b/dvxbasic/compiler/parser.h @@ -0,0 +1,57 @@ +// parser.h -- DVX BASIC parser (recursive descent) +// +// Single-pass compiler: reads tokens from the lexer and emits +// p-code directly via the code generator. No AST. Forward +// references to SUBs/FUNCTIONs are resolved via backpatching. +// +// Embeddable: no DVX dependencies, pure C. + +#ifndef DVXBASIC_PARSER_H +#define DVXBASIC_PARSER_H + +#include "lexer.h" +#include "codegen.h" +#include "symtab.h" +#include "../runtime/vm.h" + +#include +#include + +// ============================================================ +// Parser state +// ============================================================ + +typedef struct { + BasLexerT lex; + BasCodeGenT cg; + BasSymTabT sym; + char error[512]; + bool hasError; + int32_t errorLine; + int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName + int32_t optionBase; // default array lower bound (0 or 1) + bool optionCompareText; // true = case-insensitive string comparison + bool optionExplicit; // true = variables must be declared with DIM + uint8_t defType[26]; // default type per letter (A-Z), set by DEFINT etc. + char currentProc[BAS_MAX_TOKEN_LEN]; // name of current SUB/FUNCTION +} BasParserT; + +// ============================================================ +// API +// ============================================================ + +// Initialize parser with source text. +void basParserInit(BasParserT *p, const char *source, int32_t sourceLen); + +// Parse the entire source and generate p-code. +// Returns true on success, false on error (check p->error). +bool basParse(BasParserT *p); + +// Build a module from the parsed code. Returns NULL on error. +// Caller owns the module and must free with basModuleFree(). +BasModuleT *basParserBuildModule(BasParserT *p); + +// Free parser resources. +void basParserFree(BasParserT *p); + +#endif // DVXBASIC_PARSER_H diff --git a/dvxbasic/compiler/symtab.c b/dvxbasic/compiler/symtab.c new file mode 100644 index 0000000..5f6f29a --- /dev/null +++ b/dvxbasic/compiler/symtab.c @@ -0,0 +1,147 @@ +// symtab.c -- DVX BASIC symbol table implementation + +#include "symtab.h" + +#include +#include + +// ============================================================ +// Case-insensitive name comparison +// ============================================================ + +static bool namesEqual(const char *a, const char *b) { + while (*a && *b) { + char ca = *a >= 'a' && *a <= 'z' ? *a - 32 : *a; + char cb = *b >= 'a' && *b <= 'z' ? *b - 32 : *b; + + if (ca != cb) { + return false; + } + + a++; + b++; + } + + return *a == *b; +} + + +// ============================================================ +// basSymTabAdd +// ============================================================ + +BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) { + if (tab->count >= BAS_MAX_SYMBOLS) { + return NULL; + } + + // Check for duplicate in current scope + BasScopeE scope = tab->inLocalScope ? SCOPE_LOCAL : SCOPE_GLOBAL; + + for (int32_t i = 0; i < tab->count; i++) { + if (tab->symbols[i].scope == scope && namesEqual(tab->symbols[i].name, name)) { + return NULL; // duplicate + } + } + + BasSymbolT *sym = &tab->symbols[tab->count++]; + memset(sym, 0, sizeof(*sym)); + strncpy(sym->name, name, BAS_MAX_SYMBOL_NAME - 1); + sym->name[BAS_MAX_SYMBOL_NAME - 1] = '\0'; + sym->kind = kind; + sym->scope = scope; + sym->dataType = dataType; + sym->isDefined = true; + + return sym; +} + + +// ============================================================ +// basSymTabAllocSlot +// ============================================================ + +int32_t basSymTabAllocSlot(BasSymTabT *tab) { + if (tab->inLocalScope) { + return tab->nextLocalIdx++; + } + + return tab->nextGlobalIdx++; +} + + +// ============================================================ +// basSymTabEnterLocal +// ============================================================ + +void basSymTabEnterLocal(BasSymTabT *tab) { + tab->inLocalScope = true; + tab->nextLocalIdx = 0; +} + + +// ============================================================ +// basSymTabFind +// ============================================================ + +BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) { + // Search local scope first + if (tab->inLocalScope) { + for (int32_t i = tab->count - 1; i >= 0; i--) { + if (tab->symbols[i].scope == SCOPE_LOCAL && namesEqual(tab->symbols[i].name, name)) { + return &tab->symbols[i]; + } + } + } + + // Search global scope + return basSymTabFindGlobal(tab, name); +} + + +// ============================================================ +// basSymTabFindGlobal +// ============================================================ + +BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name) { + for (int32_t i = 0; i < tab->count; i++) { + if (tab->symbols[i].scope == SCOPE_GLOBAL && namesEqual(tab->symbols[i].name, name)) { + return &tab->symbols[i]; + } + } + + return NULL; +} + + +// ============================================================ +// basSymTabInit +// ============================================================ + +void basSymTabInit(BasSymTabT *tab) { + memset(tab, 0, sizeof(*tab)); +} + + +// ============================================================ +// basSymTabLeaveLocal +// ============================================================ + +void basSymTabLeaveLocal(BasSymTabT *tab) { + // Remove all local symbols + int32_t newCount = 0; + + for (int32_t i = 0; i < tab->count; i++) { + if (tab->symbols[i].scope != SCOPE_LOCAL) { + if (i != newCount) { + tab->symbols[newCount] = tab->symbols[i]; + } + + newCount++; + } + } + + tab->count = newCount; + tab->inLocalScope = false; + tab->nextLocalIdx = 0; +} diff --git a/dvxbasic/compiler/symtab.h b/dvxbasic/compiler/symtab.h new file mode 100644 index 0000000..e4cf59a --- /dev/null +++ b/dvxbasic/compiler/symtab.h @@ -0,0 +1,129 @@ +// symtab.h -- DVX BASIC symbol table +// +// Tracks variables, constants, subroutines, functions, and labels +// during compilation. Supports nested scopes (global + one local +// scope per SUB/FUNCTION). +// +// Embeddable: no DVX dependencies, pure C. + +#ifndef DVXBASIC_SYMTAB_H +#define DVXBASIC_SYMTAB_H + +#include "../compiler/opcodes.h" + +#include +#include + +// ============================================================ +// Symbol kinds +// ============================================================ + +typedef enum { + SYM_VARIABLE, + SYM_CONST, + SYM_SUB, + SYM_FUNCTION, + SYM_LABEL, + SYM_TYPE_DEF // user-defined TYPE +} BasSymKindE; + +// ============================================================ +// Symbol scope +// ============================================================ + +typedef enum { + SCOPE_GLOBAL, + SCOPE_LOCAL +} BasScopeE; + +// ============================================================ +// Symbol entry +// ============================================================ + +#define BAS_MAX_SYMBOL_NAME 64 +#define BAS_MAX_PARAMS 16 +#define BAS_MAX_CALL_PATCHES 32 +#define BAS_MAX_UDT_FIELDS 32 + +// UDT field definition +typedef struct { + char name[BAS_MAX_SYMBOL_NAME]; + uint8_t dataType; // BAS_TYPE_* + int32_t udtTypeId; // if dataType == BAS_TYPE_UDT, index of the TYPE_DEF symbol +} BasFieldDefT; + +typedef struct { + char name[BAS_MAX_SYMBOL_NAME]; + BasSymKindE kind; + BasScopeE scope; + uint8_t dataType; // BAS_TYPE_* for variables/functions + int32_t index; // slot index (local or global) + int32_t codeAddr; // PC address for SUB/FUNCTION/LABEL + bool isDefined; // false = forward-declared + bool isArray; + bool isShared; + int32_t udtTypeId; // for variables of BAS_TYPE_UDT: index of TYPE_DEF symbol + int32_t fixedLen; // for STRING * n: fixed length (0 = variable-length) + + // For SUB/FUNCTION: parameter info + int32_t paramCount; + uint8_t paramTypes[BAS_MAX_PARAMS]; + bool paramByVal[BAS_MAX_PARAMS]; + + // Forward-reference backpatch list (code addresses to patch when defined) + int32_t patchAddrs[BAS_MAX_CALL_PATCHES]; + int32_t patchCount; + + // For CONST: the constant value + union { + int32_t constInt; + double constDbl; + }; + char constStr[256]; + + // For TYPE_DEF: field definitions + BasFieldDefT fields[BAS_MAX_UDT_FIELDS]; + int32_t fieldCount; +} BasSymbolT; + +// ============================================================ +// Symbol table +// ============================================================ + +#define BAS_MAX_SYMBOLS 512 + +typedef struct { + BasSymbolT symbols[BAS_MAX_SYMBOLS]; + int32_t count; + int32_t nextGlobalIdx; // next global variable slot + int32_t nextLocalIdx; // next local variable slot (reset per SUB/FUNCTION) + bool inLocalScope; // true when inside SUB/FUNCTION +} BasSymTabT; + +// ============================================================ +// API +// ============================================================ + +void basSymTabInit(BasSymTabT *tab); + +// Add a symbol. Returns the symbol pointer, or NULL if the table is full +// or the name already exists in the current scope. +BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType); + +// Look up a symbol by name. Searches local scope first, then global. +// Case-insensitive. +BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name); + +// Look up a symbol in the global scope only. +BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name); + +// Enter local scope (called at SUB/FUNCTION start). +void basSymTabEnterLocal(BasSymTabT *tab); + +// Leave local scope (called at END SUB/FUNCTION). Removes local symbols. +void basSymTabLeaveLocal(BasSymTabT *tab); + +// Allocate the next variable slot (global or local depending on scope). +int32_t basSymTabAllocSlot(BasSymTabT *tab); + +#endif // DVXBASIC_SYMTAB_H diff --git a/dvxbasic/runtime/values.c b/dvxbasic/runtime/values.c new file mode 100644 index 0000000..3937491 --- /dev/null +++ b/dvxbasic/runtime/values.c @@ -0,0 +1,633 @@ +// values.c -- DVX BASIC value system implementation +// +// Tagged union values with reference-counted strings. The string +// heap uses simple refcounting: assignment increments, scope exit +// decrements, zero frees. No garbage collector needed. + +#include "values.h" +#include "../compiler/opcodes.h" + +#include +#include +#include +#include +#include + +// ============================================================ +// String system +// ============================================================ + +// Singleton empty string -- never freed, always available. +// Extra byte for the null terminator via the struct hack. +static struct { + BasStringT hdr; + char nul; +} sEmptyStringStorage = { { .refCount = 999999, .len = 0, .cap = 1 }, '\0' }; +BasStringT *basEmptyString = &sEmptyStringStorage.hdr; + + +BasStringT *basStringAlloc(int32_t cap) { + if (cap < 1) { + cap = 1; + } + + BasStringT *s = (BasStringT *)malloc(sizeof(BasStringT) + cap); + + if (!s) { + return basStringRef(basEmptyString); + } + + s->refCount = 1; + s->len = 0; + s->cap = cap; + s->data[0] = '\0'; + return s; +} + + +BasStringT *basStringConcat(const BasStringT *a, const BasStringT *b) { + int32_t newLen = a->len + b->len; + BasStringT *s = basStringAlloc(newLen + 1); + memcpy(s->data, a->data, a->len); + memcpy(s->data + a->len, b->data, b->len); + s->data[newLen] = '\0'; + s->len = newLen; + return s; +} + + +int32_t basStringCompare(const BasStringT *a, const BasStringT *b) { + int32_t minLen = a->len < b->len ? a->len : b->len; + int32_t cmp = memcmp(a->data, b->data, minLen); + + if (cmp != 0) { + return cmp; + } + + if (a->len < b->len) { + return -1; + } + + if (a->len > b->len) { + return 1; + } + + return 0; +} + + +int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b) { + int32_t minLen = a->len < b->len ? a->len : b->len; + + for (int32_t i = 0; i < minLen; i++) { + int32_t ca = toupper((unsigned char)a->data[i]); + int32_t cb = toupper((unsigned char)b->data[i]); + + if (ca != cb) { + return ca - cb; + } + } + + if (a->len < b->len) { + return -1; + } + + if (a->len > b->len) { + return 1; + } + + return 0; +} + + +BasStringT *basStringNew(const char *text, int32_t len) { + if (!text || len <= 0) { + return basStringRef(basEmptyString); + } + + BasStringT *s = basStringAlloc(len + 1); + memcpy(s->data, text, len); + s->data[len] = '\0'; + s->len = len; + return s; +} + + +BasStringT *basStringRef(BasStringT *s) { + if (s) { + s->refCount++; + } + + return s; +} + + +BasStringT *basStringSub(const BasStringT *s, int32_t start, int32_t len) { + if (start < 0) { + start = 0; + } + + if (start >= s->len) { + return basStringRef(basEmptyString); + } + + if (len < 0 || start + len > s->len) { + len = s->len - start; + } + + return basStringNew(s->data + start, len); +} + + +void basStringSystemInit(void) { + sEmptyStringStorage.nul = '\0'; +} + + +void basStringSystemShutdown(void) { + // Nothing to do -- empty string is static +} + + +void basStringUnref(BasStringT *s) { + if (!s || s == basEmptyString) { + return; + } + + s->refCount--; + + if (s->refCount <= 0) { + free(s); + } +} + + +// ============================================================ +// Array system +// ============================================================ + +void basArrayFree(BasArrayT *arr) { + if (!arr) { + return; + } + + if (arr->elements) { + for (int32_t i = 0; i < arr->totalElements; i++) { + basValRelease(&arr->elements[i]); + } + + free(arr->elements); + } + + free(arr); +} + + +int32_t basArrayIndex(BasArrayT *arr, int32_t *indices, int32_t ndims) { + if (!arr || ndims != arr->dims) { + return -1; + } + + int32_t flatIdx = 0; + int32_t multiplier = 1; + + // Row-major order: last dimension varies fastest + for (int32_t d = ndims - 1; d >= 0; d--) { + int32_t idx = indices[d] - arr->lbound[d]; + int32_t dimSize = arr->ubound[d] - arr->lbound[d] + 1; + + if (idx < 0 || idx >= dimSize) { + return -1; + } + + flatIdx += idx * multiplier; + multiplier *= dimSize; + } + + return flatIdx; +} + + +BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t elementType) { + if (dims < 1 || dims > BAS_ARRAY_MAX_DIMS) { + return NULL; + } + + BasArrayT *arr = (BasArrayT *)calloc(1, sizeof(BasArrayT)); + + if (!arr) { + return NULL; + } + + arr->refCount = 1; + arr->elementType = elementType; + arr->dims = dims; + + int32_t total = 1; + + for (int32_t d = 0; d < dims; d++) { + arr->lbound[d] = lbounds[d]; + arr->ubound[d] = ubounds[d]; + int32_t dimSize = ubounds[d] - lbounds[d] + 1; + + if (dimSize < 1) { + free(arr); + return NULL; + } + + total *= dimSize; + } + + arr->totalElements = total; + arr->elements = (BasValueT *)calloc(total, sizeof(BasValueT)); + + if (!arr->elements) { + free(arr); + return NULL; + } + + // Initialize all elements to the default for the element type + for (int32_t i = 0; i < total; i++) { + arr->elements[i].type = elementType; + } + + return arr; +} + + +BasArrayT *basArrayRef(BasArrayT *arr) { + if (arr) { + arr->refCount++; + } + + return arr; +} + + +void basArrayUnref(BasArrayT *arr) { + if (!arr) { + return; + } + + arr->refCount--; + + if (arr->refCount <= 0) { + basArrayFree(arr); + } +} + + +// ============================================================ +// UDT system +// ============================================================ + +void basUdtFree(BasUdtT *udt) { + if (!udt) { + return; + } + + if (udt->fields) { + for (int32_t i = 0; i < udt->fieldCount; i++) { + basValRelease(&udt->fields[i]); + } + + free(udt->fields); + } + + free(udt); +} + + +BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount) { + BasUdtT *udt = (BasUdtT *)calloc(1, sizeof(BasUdtT)); + + if (!udt) { + return NULL; + } + + udt->refCount = 1; + udt->typeId = typeId; + udt->fieldCount = fieldCount; + udt->fields = (BasValueT *)calloc(fieldCount, sizeof(BasValueT)); + + if (!udt->fields) { + free(udt); + return NULL; + } + + return udt; +} + + +BasUdtT *basUdtRef(BasUdtT *udt) { + if (udt) { + udt->refCount++; + } + + return udt; +} + + +void basUdtUnref(BasUdtT *udt) { + if (!udt) { + return; + } + + udt->refCount--; + + if (udt->refCount <= 0) { + basUdtFree(udt); + } +} + + +// ============================================================ +// Value constructors +// ============================================================ + +BasValueT basValBool(bool v) { + BasValueT val; + val.type = BAS_TYPE_BOOLEAN; + val.boolVal = v ? -1 : 0; + return val; +} + + +BasValueT basValCopy(BasValueT v) { + if (v.type == BAS_TYPE_STRING && v.strVal) { + basStringRef(v.strVal); + } else if (v.type == BAS_TYPE_ARRAY && v.arrVal) { + basArrayRef(v.arrVal); + } else if (v.type == BAS_TYPE_UDT && v.udtVal) { + basUdtRef(v.udtVal); + } + + return v; +} + + +BasValueT basValDouble(double v) { + BasValueT val; + val.type = BAS_TYPE_DOUBLE; + val.dblVal = v; + return val; +} + + +BasValueT basValInteger(int16_t v) { + BasValueT val; + val.type = BAS_TYPE_INTEGER; + val.intVal = v; + return val; +} + + +BasValueT basValLong(int32_t v) { + BasValueT val; + val.type = BAS_TYPE_LONG; + val.longVal = v; + return val; +} + + +BasValueT basValSingle(float v) { + BasValueT val; + val.type = BAS_TYPE_SINGLE; + val.sngVal = v; + return val; +} + + +BasValueT basValString(BasStringT *s) { + BasValueT val; + val.type = BAS_TYPE_STRING; + val.strVal = s ? basStringRef(s) : basStringRef(basEmptyString); + return val; +} + + +BasValueT basValStringFromC(const char *text) { + BasValueT val; + val.type = BAS_TYPE_STRING; + val.strVal = basStringNew(text, text ? (int32_t)strlen(text) : 0); + return val; +} + + +void basValRelease(BasValueT *v) { + if (v->type == BAS_TYPE_STRING) { + basStringUnref(v->strVal); + v->strVal = NULL; + } else if (v->type == BAS_TYPE_ARRAY) { + basArrayUnref(v->arrVal); + v->arrVal = NULL; + } else if (v->type == BAS_TYPE_UDT) { + basUdtUnref(v->udtVal); + v->udtVal = NULL; + } +} + + +// ============================================================ +// Type conversion +// ============================================================ + +BasValueT basValToBool(BasValueT v) { + return basValBool(basValIsTruthy(v)); +} + + +BasValueT basValToDouble(BasValueT v) { + return basValDouble(basValToNumber(v)); +} + + +BasValueT basValToInteger(BasValueT v) { + double n = basValToNumber(v); + // Banker's rounding (round half to even) + int32_t rounded = (int32_t)(n + (n > 0 ? 0.5 : -0.5)); + return basValInteger((int16_t)rounded); +} + + +BasValueT basValToLong(BasValueT v) { + double n = basValToNumber(v); + int32_t rounded = (int32_t)(n + (n > 0 ? 0.5 : -0.5)); + return basValLong(rounded); +} + + +double basValToNumber(BasValueT v) { + switch (v.type) { + case BAS_TYPE_INTEGER: + return (double)v.intVal; + + case BAS_TYPE_LONG: + return (double)v.longVal; + + case BAS_TYPE_SINGLE: + return (double)v.sngVal; + + case BAS_TYPE_DOUBLE: + return v.dblVal; + + case BAS_TYPE_BOOLEAN: + return (double)v.boolVal; + + case BAS_TYPE_STRING: + if (v.strVal && v.strVal->len > 0) { + return atof(v.strVal->data); + } + + return 0.0; + + default: + return 0.0; + } +} + + +BasValueT basValToSingle(BasValueT v) { + return basValSingle((float)basValToNumber(v)); +} + + +BasValueT basValToString(BasValueT v) { + if (v.type == BAS_TYPE_STRING) { + return basValCopy(v); + } + + BasStringT *s = basValFormatString(v); + BasValueT result; + result.type = BAS_TYPE_STRING; + result.strVal = s; + return result; +} + + +BasStringT *basValFormatString(BasValueT v) { + char buf[64]; + + switch (v.type) { + case BAS_TYPE_INTEGER: + snprintf(buf, sizeof(buf), "%d", (int)v.intVal); + return basStringNew(buf, (int32_t)strlen(buf)); + + case BAS_TYPE_LONG: + snprintf(buf, sizeof(buf), "%ld", (long)v.longVal); + return basStringNew(buf, (int32_t)strlen(buf)); + + case BAS_TYPE_SINGLE: { + snprintf(buf, sizeof(buf), "%g", (double)v.sngVal); + return basStringNew(buf, (int32_t)strlen(buf)); + } + + case BAS_TYPE_DOUBLE: + snprintf(buf, sizeof(buf), "%g", v.dblVal); + return basStringNew(buf, (int32_t)strlen(buf)); + + case BAS_TYPE_BOOLEAN: + return basStringNew(v.boolVal ? "True" : "False", v.boolVal ? 4 : 5); + + case BAS_TYPE_STRING: + return v.strVal ? basStringRef(v.strVal) : basStringRef(basEmptyString); + + default: + return basStringRef(basEmptyString); + } +} + + +bool basValIsTruthy(BasValueT v) { + switch (v.type) { + case BAS_TYPE_INTEGER: + return v.intVal != 0; + + case BAS_TYPE_LONG: + return v.longVal != 0; + + case BAS_TYPE_SINGLE: + return v.sngVal != 0.0f; + + case BAS_TYPE_DOUBLE: + return v.dblVal != 0.0; + + case BAS_TYPE_BOOLEAN: + return v.boolVal != 0; + + case BAS_TYPE_STRING: + return v.strVal && v.strVal->len > 0; + + default: + return false; + } +} + + +int32_t basValCompare(BasValueT a, BasValueT b) { + // String comparison + if (a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) { + return basStringCompare(a.strVal ? a.strVal : basEmptyString, b.strVal ? b.strVal : basEmptyString); + } + + // Numeric comparison + double na = basValToNumber(a); + double nb = basValToNumber(b); + + if (na < nb) { + return -1; + } + + if (na > nb) { + return 1; + } + + return 0; +} + + +int32_t basValCompareCI(BasValueT a, BasValueT b) { + // String comparison (case-insensitive) + if (a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) { + return basStringCompareCI(a.strVal ? a.strVal : basEmptyString, b.strVal ? b.strVal : basEmptyString); + } + + // Numeric comparison (same as basValCompare) + double na = basValToNumber(a); + double nb = basValToNumber(b); + + if (na < nb) { + return -1; + } + + if (na > nb) { + return 1; + } + + return 0; +} + + +uint8_t basValPromoteType(uint8_t a, uint8_t b) { + // String stays string (concat, not arithmetic) + if (a == BAS_TYPE_STRING || b == BAS_TYPE_STRING) { + return BAS_TYPE_STRING; + } + + // Double wins over everything + if (a == BAS_TYPE_DOUBLE || b == BAS_TYPE_DOUBLE) { + return BAS_TYPE_DOUBLE; + } + + // Single wins over integer/long + if (a == BAS_TYPE_SINGLE || b == BAS_TYPE_SINGLE) { + return BAS_TYPE_SINGLE; + } + + // Long wins over integer + if (a == BAS_TYPE_LONG || b == BAS_TYPE_LONG) { + return BAS_TYPE_LONG; + } + + return BAS_TYPE_INTEGER; +} diff --git a/dvxbasic/runtime/values.h b/dvxbasic/runtime/values.h new file mode 100644 index 0000000..d403278 --- /dev/null +++ b/dvxbasic/runtime/values.h @@ -0,0 +1,180 @@ +// values.h -- DVX BASIC value representation and string heap +// +// Tagged union value type for the VM's evaluation stack, variables, +// and array elements. Strings are reference-counted for automatic +// memory management without a garbage collector. +// +// Embeddable: no DVX dependencies, pure C. + +#ifndef DVXBASIC_VALUES_H +#define DVXBASIC_VALUES_H + +#include +#include +#include + +// ============================================================ +// Reference-counted string +// ============================================================ + +typedef struct { + int32_t refCount; + int32_t len; + int32_t cap; // allocated capacity (>= len + 1) + char data[]; // flexible array member, null-terminated +} BasStringT; + +// Allocate a new string from a C string. refCount starts at 1. +BasStringT *basStringNew(const char *text, int32_t len); + +// Allocate an empty string with a given capacity. +BasStringT *basStringAlloc(int32_t cap); + +// Increment reference count. +BasStringT *basStringRef(BasStringT *s); + +// Decrement reference count. Frees if count reaches zero. +void basStringUnref(BasStringT *s); + +// Concatenate two strings. Returns a new string (refCount 1). +BasStringT *basStringConcat(const BasStringT *a, const BasStringT *b); + +// Substring. Returns a new string (refCount 1). +BasStringT *basStringSub(const BasStringT *s, int32_t start, int32_t len); + +// Compare two strings. Returns <0, 0, >0 like strcmp. +int32_t basStringCompare(const BasStringT *a, const BasStringT *b); + +// Compare two strings case-insensitively. Returns <0, 0, >0. +int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b); + +// The empty string singleton (never freed). +extern BasStringT *basEmptyString; + +// Initialize/shutdown the string system. +void basStringSystemInit(void); +void basStringSystemShutdown(void); + +// ============================================================ +// Forward declarations +// ============================================================ + +typedef struct BasValueTag BasValueT; + +// ============================================================ +// Reference-counted array +// ============================================================ + +#define BAS_ARRAY_MAX_DIMS 8 + +typedef struct { + int32_t refCount; + uint8_t elementType; // BAS_TYPE_* + int32_t dims; // number of dimensions + int32_t lbound[BAS_ARRAY_MAX_DIMS]; // lower bound per dimension + int32_t ubound[BAS_ARRAY_MAX_DIMS]; // upper bound per dimension + int32_t totalElements; + BasValueT *elements; // flat array of values +} BasArrayT; + +// Allocate a new array. refCount starts at 1. +BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t elementType); + +// Free all elements and release the array. +void basArrayFree(BasArrayT *arr); + +// Increment reference count. +BasArrayT *basArrayRef(BasArrayT *arr); + +// Decrement reference count. Frees if count reaches zero. +void basArrayUnref(BasArrayT *arr); + +// Compute flat index from multi-dimensional indices. Returns -1 if out of bounds. +int32_t basArrayIndex(BasArrayT *arr, int32_t *indices, int32_t ndims); + +// ============================================================ +// Reference-counted user-defined type instance +// ============================================================ + +typedef struct { + int32_t refCount; + int32_t typeId; // index into type definition table + int32_t fieldCount; + BasValueT *fields; // array of field values +} BasUdtT; + +// Allocate a new UDT instance. refCount starts at 1. +BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount); + +// Free all fields and release the UDT. +void basUdtFree(BasUdtT *udt); + +// Increment reference count. +BasUdtT *basUdtRef(BasUdtT *udt); + +// Decrement reference count. Frees if count reaches zero. +void basUdtUnref(BasUdtT *udt); + +// ============================================================ +// Tagged value +// ============================================================ + +struct BasValueTag { + uint8_t type; // BAS_TYPE_* + union { + int16_t intVal; // BAS_TYPE_INTEGER + int32_t longVal; // BAS_TYPE_LONG + float sngVal; // BAS_TYPE_SINGLE + double dblVal; // BAS_TYPE_DOUBLE + BasStringT *strVal; // BAS_TYPE_STRING (ref-counted) + int16_t boolVal; // BAS_TYPE_BOOLEAN (True=-1, False=0) + BasArrayT *arrVal; // BAS_TYPE_ARRAY (ref-counted) + BasUdtT *udtVal; // BAS_TYPE_UDT (ref-counted) + }; +}; + +// Create values +BasValueT basValInteger(int16_t v); +BasValueT basValLong(int32_t v); +BasValueT basValSingle(float v); +BasValueT basValDouble(double v); +BasValueT basValString(BasStringT *s); +BasValueT basValStringFromC(const char *text); +BasValueT basValBool(bool v); + +// Copy a value (increments string refcount if applicable). +BasValueT basValCopy(BasValueT v); + +// Release a value (decrements string refcount if applicable). +void basValRelease(BasValueT *v); + +// Convert a value to a specific type. Returns the converted value. +// The original is NOT released -- caller manages lifetime. +BasValueT basValToInteger(BasValueT v); +BasValueT basValToLong(BasValueT v); +BasValueT basValToSingle(BasValueT v); +BasValueT basValToDouble(BasValueT v); +BasValueT basValToString(BasValueT v); +BasValueT basValToBool(BasValueT v); + +// Get the numeric value as a double (for mixed-type arithmetic). +double basValToNumber(BasValueT v); + +// Get the string representation. Returns a new ref-counted string. +BasStringT *basValFormatString(BasValueT v); + +// Check if a value is truthy (non-zero number, non-empty string). +bool basValIsTruthy(BasValueT v); + +// Compare two values. Returns -1, 0, or 1. +// Numeric types are compared numerically. Strings lexicographically. +int32_t basValCompare(BasValueT a, BasValueT b); + +// Compare two values case-insensitively (for OPTION COMPARE TEXT). +int32_t basValCompareCI(BasValueT a, BasValueT b); + +// Determine the common type for a binary operation (type promotion). +// Integer + Single -> Single, etc. +uint8_t basValPromoteType(uint8_t a, uint8_t b); + +#endif // DVXBASIC_VALUES_H diff --git a/dvxbasic/runtime/vm.c b/dvxbasic/runtime/vm.c new file mode 100644 index 0000000..6edf77e --- /dev/null +++ b/dvxbasic/runtime/vm.c @@ -0,0 +1,3514 @@ +// vm.c -- DVX BASIC virtual machine implementation +// +// Stack-based p-code interpreter. Executes one instruction per +// basVmStep() call, or runs until completion via basVmRun(). +// All I/O is through host-provided callbacks -- the VM itself +// has no platform dependencies. + +#include "vm.h" +#include "../compiler/opcodes.h" + +#include +#include +#include +#include +#include +#include +#include + +// ============================================================ +// Prototypes +// ============================================================ + +static BasCallFrameT *currentFrame(BasVmT *vm); +static void defaultPrint(void *ctx, const char *text, bool newline); +static BasVmResultE execArith(BasVmT *vm, uint8_t op); +static BasVmResultE execCompare(BasVmT *vm, uint8_t op); +static BasVmResultE execFileOp(BasVmT *vm, uint8_t op); +static BasVmResultE execLogical(BasVmT *vm, uint8_t op); +static BasVmResultE execMath(BasVmT *vm, uint8_t op); +static BasVmResultE execPrint(BasVmT *vm); +static BasVmResultE execStringOp(BasVmT *vm, uint8_t op); +static bool pop(BasVmT *vm, BasValueT *val); +static bool push(BasVmT *vm, BasValueT val); +static int16_t readInt16(BasVmT *vm); +static uint8_t readUint8(BasVmT *vm); +static uint16_t readUint16(BasVmT *vm); +static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg); + + +// ============================================================ +// basVmCreate +// ============================================================ + +BasVmT *basVmCreate(void) { + BasVmT *vm = (BasVmT *)calloc(1, sizeof(BasVmT)); + + if (!vm) { + return NULL; + } + + vm->printFn = defaultPrint; + basStringSystemInit(); + return vm; +} + + +// ============================================================ +// basVmDestroy +// ============================================================ + +void basVmDestroy(BasVmT *vm) { + if (!vm) { + return; + } + + // Release stack values + for (int32_t i = 0; i < vm->sp; i++) { + basValRelease(&vm->stack[i]); + } + + // Release global variables + for (int32_t i = 0; i < BAS_VM_MAX_GLOBALS; i++) { + basValRelease(&vm->globals[i]); + } + + // Release call frame locals + for (int32_t d = 0; d < vm->callDepth; d++) { + for (int32_t i = 0; i < vm->callStack[d].localCount; i++) { + basValRelease(&vm->callStack[d].locals[i]); + } + } + + // Release FOR stack + for (int32_t i = 0; i < vm->forDepth; i++) { + basValRelease(&vm->forStack[i].limit); + basValRelease(&vm->forStack[i].step); + } + + // Close files + for (int32_t i = 0; i < BAS_VM_MAX_FILES; i++) { + if (vm->files[i].handle) { + fclose((FILE *)vm->files[i].handle); + } + } + + basStringSystemShutdown(); + free(vm); +} + + +// ============================================================ +// basVmGetError +// ============================================================ + +const char *basVmGetError(const BasVmT *vm) { + return vm->errorMsg; +} + + +// ============================================================ +// basVmLoadModule +// ============================================================ + +void basVmLoadModule(BasVmT *vm, BasModuleT *module) { + vm->module = module; + vm->pc = module->entryPoint; +} + + +// ============================================================ +// basVmPop +// ============================================================ + +bool basVmPop(BasVmT *vm, BasValueT *val) { + return pop(vm, val); +} + + +// ============================================================ +// basVmPush +// ============================================================ + +bool basVmPush(BasVmT *vm, BasValueT val) { + return push(vm, val); +} + + +// ============================================================ +// basVmReset +// ============================================================ + +void basVmReset(BasVmT *vm) { + for (int32_t i = 0; i < vm->sp; i++) { + basValRelease(&vm->stack[i]); + } + + for (int32_t i = 0; i < BAS_VM_MAX_GLOBALS; i++) { + basValRelease(&vm->globals[i]); + } + + vm->sp = 0; + vm->callDepth = 0; + vm->forDepth = 0; + vm->pc = vm->module ? vm->module->entryPoint : 0; + vm->running = false; + vm->yielded = false; + vm->dataPtr = 0; + vm->errorHandler = 0; + vm->errorNumber = 0; + vm->errorPc = 0; + vm->errorNextPc = 0; + vm->inErrorHandler = false; + vm->errorMsg[0] = '\0'; +} + + +// ============================================================ +// basVmRun +// ============================================================ + +BasVmResultE basVmRun(BasVmT *vm) { + vm->running = true; + vm->yielded = false; + + while (vm->running) { + // Save PC before each instruction for RESUME support + int32_t savedPc = vm->pc; + BasVmResultE result = basVmStep(vm); + + if (result != BAS_VM_OK) { + // If an error handler is set and this is a trappable error, + // jump to the handler instead of stopping execution + if (vm->errorHandler != 0 && !vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) { + vm->errorPc = savedPc; + vm->errorNextPc = vm->pc; + vm->inErrorHandler = true; + vm->pc = vm->errorHandler; + continue; + } + + vm->running = false; + return result; + } + } + + return BAS_VM_HALTED; +} + + +// ============================================================ +// basVmSetDoEventsCallback +// ============================================================ + +void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx) { + vm->doEventsFn = fn; + vm->doEventsCtx = ctx; +} + + +// ============================================================ +// basVmSetInputCallback +// ============================================================ + +void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx) { + vm->inputFn = fn; + vm->inputCtx = ctx; +} + + +// ============================================================ +// basVmSetPrintCallback +// ============================================================ + +void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx) { + vm->printFn = fn; + vm->printCtx = ctx; +} + + +// ============================================================ +// basVmStep -- execute one instruction +// ============================================================ + +BasVmResultE basVmStep(BasVmT *vm) { + if (!vm->module || vm->pc < 0 || vm->pc >= vm->module->codeLen) { + vm->running = false; + return BAS_VM_HALTED; + } + + uint8_t op = vm->module->code[vm->pc++]; + + switch (op) { + case OP_NOP: + break; + + // ============================================================ + // Stack operations + // ============================================================ + + case OP_PUSH_INT16: { + int16_t val = readInt16(vm); + + if (!push(vm, basValInteger(val))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_PUSH_INT32: { + int16_t lo = readInt16(vm); + int16_t hi = readInt16(vm); + int32_t val = ((int32_t)hi << 16) | (uint16_t)lo; + + if (!push(vm, basValLong(val))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_PUSH_FLT32: { + float val; + memcpy(&val, &vm->module->code[vm->pc], sizeof(float)); + vm->pc += sizeof(float); + + if (!push(vm, basValSingle(val))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_PUSH_FLT64: { + double val; + memcpy(&val, &vm->module->code[vm->pc], sizeof(double)); + vm->pc += sizeof(double); + + if (!push(vm, basValDouble(val))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_PUSH_STR: { + uint16_t idx = readUint16(vm); + + if (idx < (uint16_t)vm->module->constCount) { + if (!push(vm, basValString(vm->module->constants[idx]))) { + return BAS_VM_STACK_OVERFLOW; + } + } else { + if (!push(vm, basValStringFromC(""))) { + return BAS_VM_STACK_OVERFLOW; + } + } + + break; + } + + case OP_PUSH_TRUE: + if (!push(vm, basValBool(true))) { + return BAS_VM_STACK_OVERFLOW; + } + break; + + case OP_PUSH_FALSE: + if (!push(vm, basValBool(false))) { + return BAS_VM_STACK_OVERFLOW; + } + break; + + case OP_POP: { + BasValueT val; + + if (!pop(vm, &val)) { + return BAS_VM_STACK_UNDERFLOW; + } + + basValRelease(&val); + break; + } + + case OP_DUP: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (!push(vm, basValCopy(vm->stack[vm->sp - 1]))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + // ============================================================ + // Variable access + // ============================================================ + + case OP_LOAD_LOCAL: { + uint16_t idx = readUint16(vm); + BasCallFrameT *frame = currentFrame(vm); + + if (!frame || idx >= (uint16_t)frame->localCount) { + runtimeError(vm, 9, "Invalid local variable index"); + return BAS_VM_ERROR; + } + + if (!push(vm, basValCopy(frame->locals[idx]))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_STORE_LOCAL: { + uint16_t idx = readUint16(vm); + BasCallFrameT *frame = currentFrame(vm); + BasValueT val; + + if (!pop(vm, &val)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (!frame || idx >= (uint16_t)frame->localCount) { + basValRelease(&val); + runtimeError(vm, 9, "Invalid local variable index"); + return BAS_VM_ERROR; + } + + basValRelease(&frame->locals[idx]); + frame->locals[idx] = val; + break; + } + + case OP_LOAD_GLOBAL: { + uint16_t idx = readUint16(vm); + + if (idx >= BAS_VM_MAX_GLOBALS) { + runtimeError(vm, 9, "Invalid global variable index"); + return BAS_VM_ERROR; + } + + if (!push(vm, basValCopy(vm->globals[idx]))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_STORE_GLOBAL: { + uint16_t idx = readUint16(vm); + BasValueT val; + + if (!pop(vm, &val)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (idx >= BAS_VM_MAX_GLOBALS) { + basValRelease(&val); + runtimeError(vm, 9, "Invalid global variable index"); + return BAS_VM_ERROR; + } + + basValRelease(&vm->globals[idx]); + vm->globals[idx] = val; + break; + } + + // ============================================================ + // Arithmetic + // ============================================================ + + case OP_ADD_INT: + case OP_SUB_INT: + case OP_MUL_INT: + case OP_IDIV_INT: + case OP_MOD_INT: + case OP_ADD_FLT: + case OP_SUB_FLT: + case OP_MUL_FLT: + case OP_DIV_FLT: + case OP_POW: + return execArith(vm, op); + + case OP_NEG_INT: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + + if (top->type == BAS_TYPE_INTEGER) { + top->intVal = -top->intVal; + } else if (top->type == BAS_TYPE_LONG) { + top->longVal = -top->longVal; + } else { + double n = basValToNumber(*top); + basValRelease(top); + *top = basValDouble(-n); + } + + break; + } + + case OP_NEG_FLT: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + + if (top->type == BAS_TYPE_SINGLE) { + top->sngVal = -top->sngVal; + } else if (top->type == BAS_TYPE_DOUBLE) { + top->dblVal = -top->dblVal; + } else { + double n = basValToNumber(*top); + basValRelease(top); + *top = basValDouble(-n); + } + + break; + } + + // ============================================================ + // String operations + // ============================================================ + + case OP_STR_CONCAT: + case OP_STR_LEFT: + case OP_STR_RIGHT: + case OP_STR_MID: + case OP_STR_MID2: + case OP_STR_LEN: + case OP_STR_INSTR: + case OP_STR_INSTR3: + case OP_STR_UCASE: + case OP_STR_LCASE: + case OP_STR_TRIM: + case OP_STR_LTRIM: + case OP_STR_RTRIM: + case OP_STR_CHR: + case OP_STR_ASC: + case OP_STR_SPACE: + case OP_STR_FIXLEN: + case OP_STR_MID_ASGN: + return execStringOp(vm, op); + + // ============================================================ + // Comparison + // ============================================================ + + case OP_CMP_EQ: + case OP_CMP_NE: + case OP_CMP_LT: + case OP_CMP_GT: + case OP_CMP_LE: + case OP_CMP_GE: + return execCompare(vm, op); + + // ============================================================ + // Logical / bitwise + // ============================================================ + + case OP_AND: + case OP_OR: + case OP_NOT: + case OP_XOR: + case OP_EQV: + case OP_IMP: + return execLogical(vm, op); + + // ============================================================ + // Control flow + // ============================================================ + + case OP_JMP: { + int16_t offset = readInt16(vm); + vm->pc += offset; + break; + } + + case OP_JMP_TRUE: { + int16_t offset = readInt16(vm); + BasValueT val; + + if (!pop(vm, &val)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (basValIsTruthy(val)) { + vm->pc += offset; + } + + basValRelease(&val); + break; + } + + case OP_JMP_FALSE: { + int16_t offset = readInt16(vm); + BasValueT val; + + if (!pop(vm, &val)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (!basValIsTruthy(val)) { + vm->pc += offset; + } + + basValRelease(&val); + break; + } + + case OP_CALL: { + uint16_t addr = readUint16(vm); + uint8_t argc = readUint8(vm); + uint8_t baseSlot = readUint8(vm); + + if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE) { + return BAS_VM_CALL_OVERFLOW; + } + + BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; + frame->returnPc = vm->pc; + frame->localCount = BAS_VM_MAX_LOCALS; + + // Zero all local slots + memset(frame->locals, 0, sizeof(frame->locals)); + + // Pop arguments into locals starting at baseSlot (in reverse order) + for (int32_t i = baseSlot + argc - 1; i >= baseSlot; i--) { + if (!pop(vm, &frame->locals[i])) { + return BAS_VM_STACK_UNDERFLOW; + } + } + + vm->pc = addr; + break; + } + + case OP_RET: { + if (vm->callDepth <= 0) { + vm->running = false; + return BAS_VM_HALTED; + } + + BasCallFrameT *frame = &vm->callStack[--vm->callDepth]; + + // Release locals + for (int32_t i = 0; i < frame->localCount; i++) { + basValRelease(&frame->locals[i]); + } + + vm->pc = frame->returnPc; + break; + } + + case OP_RET_VAL: { + // Like RET but leaves TOS as the return value + BasValueT retVal; + + if (!pop(vm, &retVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (vm->callDepth <= 0) { + basValRelease(&retVal); + vm->running = false; + return BAS_VM_HALTED; + } + + BasCallFrameT *frame = &vm->callStack[--vm->callDepth]; + + for (int32_t i = 0; i < frame->localCount; i++) { + basValRelease(&frame->locals[i]); + } + + vm->pc = frame->returnPc; + + if (!push(vm, retVal)) { + basValRelease(&retVal); + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_GOSUB_RET: { + // GOSUB return: pop integer from eval stack, set PC to that value + BasValueT retAddr; + + if (!pop(vm, &retAddr)) { + return BAS_VM_STACK_UNDERFLOW; + } + + vm->pc = (int32_t)basValToNumber(retAddr); + basValRelease(&retAddr); + break; + } + + case OP_FOR_INIT: { + uint16_t varIdx = readUint16(vm); + uint8_t isLocal = readUint8(vm); + BasValueT stepVal; + BasValueT limitVal; + + if (!pop(vm, &stepVal) || !pop(vm, &limitVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (vm->forDepth >= BAS_VM_MAX_FOR_DEPTH) { + basValRelease(&stepVal); + basValRelease(&limitVal); + runtimeError(vm, 26, "FOR loop nesting too deep"); + return BAS_VM_ERROR; + } + + BasForStateT *fs = &vm->forStack[vm->forDepth++]; + fs->varIdx = varIdx; + fs->isLocal = (isLocal != 0); + fs->limit = limitVal; + fs->step = stepVal; + fs->loopTop = vm->pc; + break; + } + + case OP_FOR_NEXT: { + uint16_t varIdx = readUint16(vm); + uint8_t isLocal = readUint8(vm); + int16_t loopTopOffset = readInt16(vm); + + if (vm->forDepth <= 0) { + runtimeError(vm, 1, "NEXT without FOR"); + return BAS_VM_ERROR; + } + + BasForStateT *fs = &vm->forStack[vm->forDepth - 1]; + + if (fs->varIdx != (int32_t)varIdx) { + runtimeError(vm, 1, "NEXT variable mismatch"); + return BAS_VM_ERROR; + } + + // Get pointer to the loop variable (global or local) + BasValueT *varSlot; + + if (isLocal) { + BasCallFrameT *frame = currentFrame(vm); + + if (!frame || varIdx >= (uint16_t)frame->localCount) { + runtimeError(vm, 9, "Invalid local variable index"); + return BAS_VM_ERROR; + } + + varSlot = &frame->locals[varIdx]; + } else { + if (varIdx >= BAS_VM_MAX_GLOBALS) { + runtimeError(vm, 9, "Invalid global variable index"); + return BAS_VM_ERROR; + } + + varSlot = &vm->globals[varIdx]; + } + + // Increment: var = var + step + double varVal = basValToNumber(*varSlot); + double stepVal = basValToNumber(fs->step); + double limVal = basValToNumber(fs->limit); + varVal += stepVal; + + basValRelease(varSlot); + *varSlot = basValDouble(varVal); + + // Test: if step > 0 then continue while var <= limit + // if step < 0 then continue while var >= limit + bool cont; + + if (stepVal >= 0) { + cont = (varVal <= limVal); + } else { + cont = (varVal >= limVal); + } + + if (cont) { + vm->pc += loopTopOffset; + } else { + // Loop done -- pop FOR state + basValRelease(&fs->limit); + basValRelease(&fs->step); + vm->forDepth--; + } + + break; + } + + // ============================================================ + // Type conversion + // ============================================================ + + case OP_CONV_INT_FLT: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + double n = basValToNumber(*top); + basValRelease(top); + *top = basValSingle((float)n); + break; + } + + case OP_CONV_FLT_INT: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT conv = basValToInteger(*top); + basValRelease(top); + *top = conv; + break; + } + + case OP_CONV_INT_STR: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT conv = basValToString(*top); + basValRelease(top); + *top = conv; + break; + } + + case OP_CONV_STR_INT: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT conv = basValToInteger(*top); + basValRelease(top); + *top = conv; + break; + } + + case OP_CONV_FLT_STR: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT conv = basValToString(*top); + basValRelease(top); + *top = conv; + break; + } + + case OP_CONV_STR_FLT: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT conv = basValToDouble(*top); + basValRelease(top); + *top = conv; + break; + } + + case OP_CONV_INT_LONG: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT conv = basValToLong(*top); + basValRelease(top); + *top = conv; + break; + } + + // ============================================================ + // I/O + // ============================================================ + + case OP_PRINT: + return execPrint(vm); + + case OP_PRINT_NL: + if (vm->printFn) { + vm->printFn(vm->printCtx, "", true); + } + break; + + case OP_PRINT_TAB: + if (vm->printFn) { + vm->printFn(vm->printCtx, "\t", false); + } + break; + + case OP_PRINT_SPC: { + uint8_t n = readUint8(vm); + char spaces[256]; + int32_t count = n < 255 ? n : 255; + memset(spaces, ' ', count); + spaces[count] = '\0'; + + if (vm->printFn) { + vm->printFn(vm->printCtx, spaces, false); + } + + break; + } + + case OP_PRINT_SPC_N: { + BasValueT nVal; + + if (!pop(vm, &nVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t count = (int32_t)basValToNumber(nVal); + basValRelease(&nVal); + + if (count < 0) { + count = 0; + } + + if (count > 255) { + count = 255; + } + + char spaces[256]; + memset(spaces, ' ', count); + spaces[count] = '\0'; + + if (vm->printFn) { + vm->printFn(vm->printCtx, spaces, false); + } + + break; + } + + case OP_PRINT_TAB_N: { + BasValueT nVal; + + if (!pop(vm, &nVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t col = (int32_t)basValToNumber(nVal); + basValRelease(&nVal); + + if (col < 1) { + col = 1; + } + + if (col > 255) { + col = 255; + } + + // TAB outputs spaces to reach the specified column + // For simplicity, just output (col-1) spaces + char spaces[256]; + int32_t count = col - 1; + memset(spaces, ' ', count); + spaces[count] = '\0'; + + if (vm->printFn) { + vm->printFn(vm->printCtx, spaces, false); + } + + break; + } + + case OP_PRINT_USING: { + // Pop value and format string (format is below value on stack) + BasValueT val; + BasValueT fmtVal; + + if (!pop(vm, &val) || !pop(vm, &fmtVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT fmtStr = basValToString(fmtVal); + basValRelease(&fmtVal); + + const char *fmt = fmtStr.strVal->data; + int32_t fmtLen = fmtStr.strVal->len; + char buf[256]; + + if (val.type == BAS_TYPE_STRING) { + // String formatting + const char *src = val.strVal ? val.strVal->data : ""; + + if (fmtLen > 0 && fmt[0] == '!') { + // First character only + buf[0] = src[0] ? src[0] : ' '; + buf[1] = '\0'; + } else if (fmtLen > 0 && fmt[0] == '&') { + // Entire string + snprintf(buf, sizeof(buf), "%s", src); + } else if (fmtLen >= 2 && fmt[0] == '\\') { + // Fixed-width: count characters between backslashes + int32_t width = 2; + for (int32_t i = 1; i < fmtLen; i++) { + if (fmt[i] == '\\') { + width = i + 1; + break; + } + width = i + 2; + } + int32_t srcLen = (int32_t)strlen(src); + int32_t copyLen = srcLen < width ? srcLen : width; + memcpy(buf, src, copyLen); + for (int32_t i = copyLen; i < width; i++) { + buf[i] = ' '; + } + buf[width] = '\0'; + } else { + snprintf(buf, sizeof(buf), "%s", src); + } + } else { + // Numeric formatting + double n = basValToNumber(val); + + // Parse format flags + bool asteriskFill = false; // ** fill leading spaces with * + bool dollarFloat = false; // $$ floating dollar sign + bool plusAtStart = false; // + at start: always show sign + bool plusAtEnd = false; // + at end: always show sign + bool minusAtEnd = false; // - at end: show minus for negative + bool sciNotation = false; // ^^^^ scientific notation + bool hasDecimal = false; + bool hasComma = false; + int32_t digitsBefore = 0; + int32_t digitsAfter = 0; + + // Check for ** at start + if (fmtLen >= 2 && fmt[0] == '*' && fmt[1] == '*') { + asteriskFill = true; + } + + // Check for $$ (may follow **) + int32_t scanStart = 0; + if (asteriskFill) { + scanStart = 2; + } + if (fmtLen >= scanStart + 2 && fmt[scanStart] == '$' && fmt[scanStart + 1] == '$') { + dollarFloat = true; + } + + // Check for + at start or end + if (fmtLen > 0 && fmt[0] == '+') { + plusAtStart = true; + } + if (fmtLen > 0 && fmt[fmtLen - 1] == '+') { + plusAtEnd = true; + } + + // Check for - at end + if (fmtLen > 0 && fmt[fmtLen - 1] == '-') { + minusAtEnd = true; + } + + // Check for ^^^^ (scientific notation) + for (int32_t i = 0; i <= fmtLen - 4; i++) { + if (fmt[i] == '^' && fmt[i+1] == '^' && fmt[i+2] == '^' && fmt[i+3] == '^') { + sciNotation = true; + break; + } + } + + // Count # and 0 digits before and after decimal + for (int32_t i = 0; i < fmtLen; i++) { + if (fmt[i] == '.') { + hasDecimal = true; + } else if (fmt[i] == ',') { + hasComma = true; + } else if (fmt[i] == '#' || fmt[i] == '0') { + if (hasDecimal) { + digitsAfter++; + } else { + digitsBefore++; + } + } else if (fmt[i] == '*') { + if (!hasDecimal) { + digitsBefore++; + } + } + } + + if (sciNotation) { + // Scientific notation + char sciFmt[32]; + int32_t decimals = hasDecimal ? digitsAfter : 0; + snprintf(sciFmt, sizeof(sciFmt), "%%.%dE", decimals); + snprintf(buf, sizeof(buf), sciFmt, n); + } else { + // Standard formatting + bool isNeg = (n < 0); + double absN = isNeg ? -n : n; + int32_t decimals = hasDecimal ? digitsAfter : 0; + + char numBuf[128]; + snprintf(numBuf, sizeof(numBuf), "%.*f", decimals, absN); + + // Split into integer and decimal parts + char intPart[128]; + char decPart[128]; + intPart[0] = '\0'; + decPart[0] = '\0'; + char *dot = strchr(numBuf, '.'); + if (dot) { + int32_t intLen = (int32_t)(dot - numBuf); + memcpy(intPart, numBuf, intLen); + intPart[intLen] = '\0'; + strncpy(decPart, dot + 1, sizeof(decPart) - 1); + decPart[sizeof(decPart) - 1] = '\0'; + } else { + strncpy(intPart, numBuf, sizeof(intPart) - 1); + intPart[sizeof(intPart) - 1] = '\0'; + } + + // Apply thousands separator + char fmtIntPart[128]; + if (hasComma) { + int32_t srcLen = (int32_t)strlen(intPart); + int32_t dstIdx = 0; + for (int32_t i = 0; i < srcLen; i++) { + if (i > 0 && (srcLen - i) % 3 == 0) { + fmtIntPart[dstIdx++] = ','; + } + fmtIntPart[dstIdx++] = intPart[i]; + } + fmtIntPart[dstIdx] = '\0'; + } else { + strncpy(fmtIntPart, intPart, sizeof(fmtIntPart) - 1); + fmtIntPart[sizeof(fmtIntPart) - 1] = '\0'; + } + + // Build result + int32_t idx = 0; + + // Sign prefix + if (plusAtStart) { + buf[idx++] = isNeg ? '-' : '+'; + } else if (isNeg && !minusAtEnd) { + buf[idx++] = '-'; + } + + // Dollar sign + if (dollarFloat) { + buf[idx++] = '$'; + } + + // Pad leading + int32_t intLen = (int32_t)strlen(fmtIntPart); + int32_t padNeeded = digitsBefore - intLen; + char fillChar = asteriskFill ? '*' : ' '; + + for (int32_t i = 0; i < padNeeded; i++) { + buf[idx++] = fillChar; + } + + // Integer part + for (int32_t i = 0; fmtIntPart[i]; i++) { + buf[idx++] = fmtIntPart[i]; + } + + // Decimal part + if (hasDecimal) { + buf[idx++] = '.'; + for (int32_t i = 0; i < decimals; i++) { + buf[idx++] = decPart[i] ? decPart[i] : '0'; + } + } + + // Trailing sign + if (plusAtEnd) { + buf[idx++] = isNeg ? '-' : '+'; + } else if (minusAtEnd) { + buf[idx++] = isNeg ? '-' : ' '; + } + + buf[idx] = '\0'; + } + } + + basValRelease(&val); + + // Push format string back (PRINT USING reuses it for multiple values) + if (!push(vm, fmtStr)) { + return BAS_VM_STACK_OVERFLOW; + } + + // Push formatted result + if (!push(vm, basValStringFromC(buf))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_INPUT: { + char buf[1024]; + buf[0] = '\0'; + + if (vm->inputFn) { + if (!vm->inputFn(vm->inputCtx, "? ", buf, sizeof(buf))) { + buf[0] = '\0'; + } + } + + if (!push(vm, basValStringFromC(buf))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + // ============================================================ + // Math built-ins + // ============================================================ + + case OP_MATH_ABS: + case OP_MATH_INT: + case OP_MATH_FIX: + case OP_MATH_SGN: + case OP_MATH_SQR: + case OP_MATH_SIN: + case OP_MATH_COS: + case OP_MATH_TAN: + case OP_MATH_ATN: + case OP_MATH_LOG: + case OP_MATH_EXP: + case OP_MATH_RND: + case OP_MATH_RANDOMIZE: + return execMath(vm, op); + + // ============================================================ + // Conversion built-ins + // ============================================================ + + case OP_STR_VAL: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + double n = basValToNumber(*top); + basValRelease(top); + *top = basValDouble(n); + break; + } + + case OP_STR_STRF: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasStringT *s = basValFormatString(*top); + basValRelease(top); + top->type = BAS_TYPE_STRING; + top->strVal = s; + break; + } + + case OP_STR_HEX: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + int32_t n = (int32_t)basValToNumber(*top); + char buf[16]; + snprintf(buf, sizeof(buf), "%X", (unsigned int)n); + basValRelease(top); + *top = basValStringFromC(buf); + break; + } + + case OP_STR_STRING: { + // STRING$(n, char) + BasValueT charVal; + BasValueT countVal; + + if (!pop(vm, &charVal) || !pop(vm, &countVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t count = (int32_t)basValToNumber(countVal); + basValRelease(&countVal); + + char ch; + + if (charVal.type == BAS_TYPE_STRING && charVal.strVal && charVal.strVal->len > 0) { + ch = charVal.strVal->data[0]; + } else { + ch = (char)(int32_t)basValToNumber(charVal); + } + + basValRelease(&charVal); + + if (count < 0) { + count = 0; + } + + if (count > 32767) { + count = 32767; + } + + BasStringT *s = basStringAlloc(count + 1); + memset(s->data, ch, count); + s->data[count] = '\0'; + s->len = count; + + if (!push(vm, basValString(s))) { + basStringUnref(s); + return BAS_VM_STACK_OVERFLOW; + } + + basStringUnref(s); + break; + } + + // ============================================================ + // Extended built-ins + // ============================================================ + + case OP_MATH_TIMER: { + // Push seconds since midnight as a double + time_t now = time(NULL); + struct tm *t = localtime(&now); + double secs = (double)t->tm_hour * 3600.0 + (double)t->tm_min * 60.0 + (double)t->tm_sec; + + if (!push(vm, basValDouble(secs))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_DATE_STR: { + // Push DATE$ as "MM-DD-YYYY" + time_t now = time(NULL); + struct tm *t = localtime(&now); + char buf[16]; + snprintf(buf, sizeof(buf), "%02d-%02d-%04d", t->tm_mon + 1, t->tm_mday, t->tm_year + 1900); + + if (!push(vm, basValStringFromC(buf))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_TIME_STR: { + // Push TIME$ as "HH:MM:SS" + time_t now = time(NULL); + struct tm *t = localtime(&now); + char buf[16]; + snprintf(buf, sizeof(buf), "%02d:%02d:%02d", t->tm_hour, t->tm_min, t->tm_sec); + + if (!push(vm, basValStringFromC(buf))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_SLEEP: { + // Pop seconds, sleep + BasValueT val; + + if (!pop(vm, &val)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t secs = (int32_t)basValToNumber(val); + basValRelease(&val); + + if (secs > 0) { + sleep((unsigned int)secs); + } + + break; + } + + case OP_ENVIRON: { + // Pop env var name, push value string + BasValueT nameVal; + + if (!pop(vm, &nameVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT nameStr = basValToString(nameVal); + basValRelease(&nameVal); + + const char *envVal = getenv(nameStr.strVal->data); + basValRelease(&nameStr); + + if (envVal == NULL) { + envVal = ""; + } + + if (!push(vm, basValStringFromC(envVal))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + // ============================================================ + // File I/O + // ============================================================ + + case OP_FILE_OPEN: + case OP_FILE_CLOSE: + case OP_FILE_PRINT: + case OP_FILE_INPUT: + case OP_FILE_EOF: + case OP_FILE_LINE_INPUT: + case OP_FILE_WRITE: + case OP_FILE_WRITE_SEP: + case OP_FILE_WRITE_NL: + case OP_FILE_GET: + case OP_FILE_PUT: + case OP_FILE_SEEK: + case OP_FILE_LOF: + case OP_FILE_LOC: + case OP_FILE_FREEFILE: + case OP_FILE_INPUT_N: + return execFileOp(vm, op); + + // ============================================================ + // DoEvents + // ============================================================ + + case OP_DO_EVENTS: + if (vm->doEventsFn) { + if (!vm->doEventsFn(vm->doEventsCtx)) { + vm->running = false; + return BAS_VM_HALTED; + } + } + break; + + // ============================================================ + // Error handling + // ============================================================ + + case OP_ON_ERROR: { + int16_t handler = readInt16(vm); + vm->errorHandler = (handler == 0) ? 0 : vm->pc + handler; + break; + } + + case OP_ERR_NUM: + if (!push(vm, basValInteger((int16_t)vm->errorNumber))) { + return BAS_VM_STACK_OVERFLOW; + } + break; + + case OP_ERR_CLEAR: + vm->errorNumber = 0; + vm->errorMsg[0] = '\0'; + break; + + case OP_RESUME: + // RESUME -- re-execute the statement that caused the error + vm->pc = vm->errorPc; + vm->errorNumber = 0; + vm->errorMsg[0] = '\0'; + vm->inErrorHandler = false; + break; + + case OP_RESUME_NEXT: + // RESUME NEXT -- continue at next statement after the error + vm->pc = vm->errorNextPc; + vm->errorNumber = 0; + vm->errorMsg[0] = '\0'; + vm->inErrorHandler = false; + break; + + // ============================================================ + // Array / UDT operations + // ============================================================ + + case OP_DIM_ARRAY: { + uint8_t dims = readUint8(vm); + uint8_t elementType = readUint8(vm); + + // dims=0 with elementType=BAS_TYPE_UDT means allocate a UDT instance + if (dims == 0 && elementType == BAS_TYPE_UDT) { + BasValueT fieldCountVal; + BasValueT typeIdVal; + + if (!pop(vm, &fieldCountVal) || !pop(vm, &typeIdVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t fieldCount = (int32_t)basValToNumber(fieldCountVal); + int32_t typeId = (int32_t)basValToNumber(typeIdVal); + basValRelease(&fieldCountVal); + basValRelease(&typeIdVal); + + BasUdtT *udt = basUdtNew(typeId, fieldCount); + + if (!udt) { + runtimeError(vm, 7, "Out of memory allocating TYPE"); + return BAS_VM_OUT_OF_MEMORY; + } + + BasValueT udtVal; + udtVal.type = BAS_TYPE_UDT; + udtVal.udtVal = udt; + + if (!push(vm, udtVal)) { + basUdtFree(udt); + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + // Normal array allocation: parser pushes (lbound, ubound) pairs per dim + int32_t lbounds[BAS_ARRAY_MAX_DIMS]; + int32_t ubounds[BAS_ARRAY_MAX_DIMS]; + + // Pop bounds in reverse order (last dim first) + for (int32_t d = dims - 1; d >= 0; d--) { + BasValueT ubVal; + BasValueT lbVal; + + if (!pop(vm, &ubVal) || !pop(vm, &lbVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + ubounds[d] = (int32_t)basValToNumber(ubVal); + lbounds[d] = (int32_t)basValToNumber(lbVal); + basValRelease(&ubVal); + basValRelease(&lbVal); + } + + BasArrayT *arr = basArrayNew(dims, lbounds, ubounds, elementType); + + if (!arr) { + runtimeError(vm, 7, "Out of memory allocating array"); + return BAS_VM_OUT_OF_MEMORY; + } + + BasValueT arrVal; + arrVal.type = BAS_TYPE_ARRAY; + arrVal.arrVal = arr; + + if (!push(vm, arrVal)) { + basArrayFree(arr); + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_LOAD_ARRAY: { + uint8_t dims = readUint8(vm); + int32_t indices[BAS_ARRAY_MAX_DIMS]; + + // Pop indices in reverse order + for (int32_t d = dims - 1; d >= 0; d--) { + BasValueT idxVal; + + if (!pop(vm, &idxVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + indices[d] = (int32_t)basValToNumber(idxVal); + basValRelease(&idxVal); + } + + // Pop array reference + BasValueT arrRef; + + if (!pop(vm, &arrRef)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { + basValRelease(&arrRef); + runtimeError(vm, 13, "Not an array"); + return BAS_VM_TYPE_MISMATCH; + } + + int32_t flatIdx = basArrayIndex(arrRef.arrVal, indices, dims); + + if (flatIdx < 0) { + basValRelease(&arrRef); + runtimeError(vm, 9, "Subscript out of range"); + return BAS_VM_SUBSCRIPT_RANGE; + } + + BasValueT elem = basValCopy(arrRef.arrVal->elements[flatIdx]); + basValRelease(&arrRef); + + if (!push(vm, elem)) { + basValRelease(&elem); + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_STORE_ARRAY: { + uint8_t dims = readUint8(vm); + + // Pop value to store + BasValueT storeVal; + + if (!pop(vm, &storeVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + // Pop indices in reverse order + int32_t indices[BAS_ARRAY_MAX_DIMS]; + + for (int32_t d = dims - 1; d >= 0; d--) { + BasValueT idxVal; + + if (!pop(vm, &idxVal)) { + basValRelease(&storeVal); + return BAS_VM_STACK_UNDERFLOW; + } + + indices[d] = (int32_t)basValToNumber(idxVal); + basValRelease(&idxVal); + } + + // Pop array reference + BasValueT arrRef; + + if (!pop(vm, &arrRef)) { + basValRelease(&storeVal); + return BAS_VM_STACK_UNDERFLOW; + } + + if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { + basValRelease(&arrRef); + basValRelease(&storeVal); + runtimeError(vm, 13, "Not an array"); + return BAS_VM_TYPE_MISMATCH; + } + + int32_t flatIdx = basArrayIndex(arrRef.arrVal, indices, dims); + + if (flatIdx < 0) { + basValRelease(&arrRef); + basValRelease(&storeVal); + runtimeError(vm, 9, "Subscript out of range"); + return BAS_VM_SUBSCRIPT_RANGE; + } + + basValRelease(&arrRef.arrVal->elements[flatIdx]); + arrRef.arrVal->elements[flatIdx] = storeVal; + basValRelease(&arrRef); + break; + } + + case OP_REDIM: { + uint8_t dims = readUint8(vm); + uint8_t preserve = readUint8(vm); + + int32_t lbounds[BAS_ARRAY_MAX_DIMS]; + int32_t ubounds[BAS_ARRAY_MAX_DIMS]; + + for (int32_t d = dims - 1; d >= 0; d--) { + BasValueT ubVal; + BasValueT lbVal; + + if (!pop(vm, &ubVal) || !pop(vm, &lbVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + ubounds[d] = (int32_t)basValToNumber(ubVal); + lbounds[d] = (int32_t)basValToNumber(lbVal); + basValRelease(&ubVal); + basValRelease(&lbVal); + } + + // Pop old array reference + BasValueT oldRef; + + if (!pop(vm, &oldRef)) { + return BAS_VM_STACK_UNDERFLOW; + } + + uint8_t elementType = BAS_TYPE_INTEGER; + + if (oldRef.type == BAS_TYPE_ARRAY && oldRef.arrVal) { + elementType = oldRef.arrVal->elementType; + } + + BasArrayT *newArr = basArrayNew(dims, lbounds, ubounds, elementType); + + if (!newArr) { + basValRelease(&oldRef); + runtimeError(vm, 7, "Out of memory in REDIM"); + return BAS_VM_OUT_OF_MEMORY; + } + + // Copy old elements if PRESERVE + if (preserve && oldRef.type == BAS_TYPE_ARRAY && oldRef.arrVal) { + int32_t copyCount = oldRef.arrVal->totalElements; + + if (copyCount > newArr->totalElements) { + copyCount = newArr->totalElements; + } + + for (int32_t i = 0; i < copyCount; i++) { + newArr->elements[i] = basValCopy(oldRef.arrVal->elements[i]); + } + } + + basValRelease(&oldRef); + + BasValueT arrVal; + arrVal.type = BAS_TYPE_ARRAY; + arrVal.arrVal = newArr; + + if (!push(vm, arrVal)) { + basArrayFree(newArr); + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_ERASE: { + BasValueT arrRef; + + if (!pop(vm, &arrRef)) { + return BAS_VM_STACK_UNDERFLOW; + } + + basValRelease(&arrRef); + + // Push an empty/zero value to store back + BasValueT empty; + memset(&empty, 0, sizeof(empty)); + + if (!push(vm, empty)) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_LBOUND: { + uint8_t dim = readUint8(vm); + BasValueT arrRef; + + if (!pop(vm, &arrRef)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { + basValRelease(&arrRef); + runtimeError(vm, 13, "Not an array"); + return BAS_VM_TYPE_MISMATCH; + } + + if (dim < 1 || dim > (uint8_t)arrRef.arrVal->dims) { + basValRelease(&arrRef); + runtimeError(vm, 9, "Invalid dimension for LBOUND"); + return BAS_VM_SUBSCRIPT_RANGE; + } + + int32_t lb = arrRef.arrVal->lbound[dim - 1]; + basValRelease(&arrRef); + + if (!push(vm, basValLong(lb))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_UBOUND: { + uint8_t dim = readUint8(vm); + BasValueT arrRef; + + if (!pop(vm, &arrRef)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { + basValRelease(&arrRef); + runtimeError(vm, 13, "Not an array"); + return BAS_VM_TYPE_MISMATCH; + } + + if (dim < 1 || dim > (uint8_t)arrRef.arrVal->dims) { + basValRelease(&arrRef); + runtimeError(vm, 9, "Invalid dimension for UBOUND"); + return BAS_VM_SUBSCRIPT_RANGE; + } + + int32_t ub = arrRef.arrVal->ubound[dim - 1]; + basValRelease(&arrRef); + + if (!push(vm, basValLong(ub))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_LOAD_FIELD: { + uint16_t fieldIdx = readUint16(vm); + BasValueT udtRef; + + if (!pop(vm, &udtRef)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (udtRef.type != BAS_TYPE_UDT || !udtRef.udtVal) { + basValRelease(&udtRef); + runtimeError(vm, 13, "Not a TYPE instance"); + return BAS_VM_TYPE_MISMATCH; + } + + if (fieldIdx >= (uint16_t)udtRef.udtVal->fieldCount) { + basValRelease(&udtRef); + runtimeError(vm, 9, "Invalid field index"); + return BAS_VM_ERROR; + } + + BasValueT fieldVal = basValCopy(udtRef.udtVal->fields[fieldIdx]); + basValRelease(&udtRef); + + if (!push(vm, fieldVal)) { + basValRelease(&fieldVal); + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_STORE_FIELD: { + uint16_t fieldIdx = readUint16(vm); + BasValueT storeVal; + + if (!pop(vm, &storeVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT udtRef; + + if (!pop(vm, &udtRef)) { + basValRelease(&storeVal); + return BAS_VM_STACK_UNDERFLOW; + } + + if (udtRef.type != BAS_TYPE_UDT || !udtRef.udtVal) { + basValRelease(&udtRef); + basValRelease(&storeVal); + runtimeError(vm, 13, "Not a TYPE instance"); + return BAS_VM_TYPE_MISMATCH; + } + + if (fieldIdx >= (uint16_t)udtRef.udtVal->fieldCount) { + basValRelease(&udtRef); + basValRelease(&storeVal); + runtimeError(vm, 9, "Invalid field index"); + return BAS_VM_ERROR; + } + + basValRelease(&udtRef.udtVal->fields[fieldIdx]); + udtRef.udtVal->fields[fieldIdx] = storeVal; + basValRelease(&udtRef); + break; + } + + // ============================================================ + // DATA/READ/RESTORE + // ============================================================ + + case OP_READ_DATA: { + if (!vm->module || vm->dataPtr >= vm->module->dataCount) { + runtimeError(vm, 4, "Out of DATA"); + return BAS_VM_ERROR; + } + + if (!push(vm, basValCopy(vm->module->dataPool[vm->dataPtr++]))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_RESTORE: + vm->dataPtr = 0; + break; + + // ============================================================ + // FORMAT$ + // ============================================================ + + case OP_FORMAT: { + // Pop format string, then value + BasValueT fmtVal; + BasValueT val; + + if (!pop(vm, &fmtVal) || !pop(vm, &val)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT fmtStr = basValToString(fmtVal); + basValRelease(&fmtVal); + + const char *fmt = fmtStr.strVal->data; + int32_t fmtLen = fmtStr.strVal->len; + double n = basValToNumber(val); + basValRelease(&val); + + char buf[256]; + buf[0] = '\0'; + + // Check for "percent" format + bool isPercent = false; + if (fmtLen == 7) { + isPercent = true; + const char *pct = "PERCENT"; + for (int32_t i = 0; i < 7; i++) { + if (toupper((unsigned char)fmt[i]) != pct[i]) { + isPercent = false; + break; + } + } + } + if (isPercent) { + snprintf(buf, sizeof(buf), "%.0f%%", n * 100.0); + } else { + // Count format characters + int32_t hashBefore = 0; + int32_t zeroBefore = 0; + int32_t hashAfter = 0; + int32_t zeroAfter = 0; + bool hasDecimal = false; + bool hasComma = false; + bool plusStart = false; + bool plusEnd = false; + bool minusEnd = false; + + for (int32_t i = 0; i < fmtLen; i++) { + if (fmt[i] == '+' && i == 0) { + plusStart = true; + } else if (fmt[i] == '+' && i == fmtLen - 1) { + plusEnd = true; + } else if (fmt[i] == '-' && i == fmtLen - 1) { + minusEnd = true; + } else if (fmt[i] == '.') { + hasDecimal = true; + } else if (fmt[i] == ',') { + hasComma = true; + } else if (fmt[i] == '#') { + if (hasDecimal) { + hashAfter++; + } else { + hashBefore++; + } + } else if (fmt[i] == '0') { + if (hasDecimal) { + zeroAfter++; + } else { + zeroBefore++; + } + } + } + + int32_t decimals = hashAfter + zeroAfter; + bool isNeg = (n < 0); + double absN = isNeg ? -n : n; + + // Format the number + char numBuf[128]; + + if (hasDecimal) { + snprintf(numBuf, sizeof(numBuf), "%.*f", decimals, absN); + } else { + snprintf(numBuf, sizeof(numBuf), "%.0f", absN); + } + + // Split into integer and decimal parts + char intPart[128]; + char decPart[128]; + intPart[0] = '\0'; + decPart[0] = '\0'; + + char *dot = strchr(numBuf, '.'); + + if (dot) { + int32_t intLen = (int32_t)(dot - numBuf); + memcpy(intPart, numBuf, intLen); + intPart[intLen] = '\0'; + strncpy(decPart, dot + 1, sizeof(decPart) - 1); + decPart[sizeof(decPart) - 1] = '\0'; + } else { + strncpy(intPart, numBuf, sizeof(intPart) - 1); + intPart[sizeof(intPart) - 1] = '\0'; + } + + // Apply thousands separator + char fmtIntPart[128]; + + if (hasComma) { + int32_t srcLen = (int32_t)strlen(intPart); + int32_t dstIdx = 0; + + for (int32_t i = 0; i < srcLen; i++) { + if (i > 0 && (srcLen - i) % 3 == 0) { + fmtIntPart[dstIdx++] = ','; + } + fmtIntPart[dstIdx++] = intPart[i]; + } + + fmtIntPart[dstIdx] = '\0'; + } else { + strncpy(fmtIntPart, intPart, sizeof(fmtIntPart) - 1); + fmtIntPart[sizeof(fmtIntPart) - 1] = '\0'; + } + + // Pad integer part with leading zeros if format has 0's + int32_t totalIntDigits = hashBefore + zeroBefore; + int32_t curIntLen = (int32_t)strlen(fmtIntPart); + + // Build result + int32_t idx = 0; + + // Sign prefix + if (plusStart || plusEnd) { + if (isNeg) { + buf[idx++] = '-'; + } else if (plusStart) { + buf[idx++] = '+'; + } + } else if (isNeg) { + buf[idx++] = '-'; + } + + // Pad with leading spaces or zeros + int32_t padNeeded = totalIntDigits - curIntLen; + + for (int32_t i = 0; i < padNeeded; i++) { + if (i < padNeeded - (int32_t)strlen(intPart)) { + // Positions before the number + if (zeroBefore > 0) { + buf[idx++] = '0'; + } else { + buf[idx++] = ' '; + } + } else { + buf[idx++] = '0'; + } + } + + // Integer part + for (int32_t i = 0; fmtIntPart[i]; i++) { + buf[idx++] = fmtIntPart[i]; + } + + // Decimal part + if (hasDecimal) { + buf[idx++] = '.'; + for (int32_t i = 0; decPart[i] && i < decimals; i++) { + buf[idx++] = decPart[i]; + } + } + + // Trailing sign + if (plusEnd && !isNeg) { + buf[idx++] = '+'; + } else if (minusEnd && isNeg) { + buf[idx++] = '-'; + } else if (minusEnd && !isNeg) { + buf[idx++] = ' '; + } + + buf[idx] = '\0'; + } + + basValRelease(&fmtStr); + + if (!push(vm, basValStringFromC(buf))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + // ============================================================ + // SHELL + // ============================================================ + + case OP_SHELL: { + BasValueT cmdVal; + + if (!pop(vm, &cmdVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT cmdStr = basValToString(cmdVal); + basValRelease(&cmdVal); + + int32_t result = 0; + + if (cmdStr.strVal && cmdStr.strVal->len > 0) { + result = system(cmdStr.strVal->data); + } + + basValRelease(&cmdStr); + + if (!push(vm, basValInteger((int16_t)result))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + // ============================================================ + // COMPARE_MODE + // ============================================================ + + case OP_COMPARE_MODE: { + uint8_t mode = readUint8(vm); + vm->compareTextMode = (mode != 0); + break; + } + + // ============================================================ + // Halt + // ============================================================ + + case OP_HALT: + vm->running = false; + return BAS_VM_HALTED; + + default: + runtimeError(vm, 51, "Bad opcode"); + return BAS_VM_BAD_OPCODE; + } + + return BAS_VM_OK; +} + + +// ============================================================ +// currentFrame +// ============================================================ + +static BasCallFrameT *currentFrame(BasVmT *vm) { + if (vm->callDepth <= 0) { + // Module-level: use callStack[0] as implicit main frame + return &vm->callStack[0]; + } + + return &vm->callStack[vm->callDepth - 1]; +} + + +// ============================================================ +// defaultPrint +// ============================================================ + +static void defaultPrint(void *ctx, const char *text, bool newline) { + (void)ctx; + fputs(text, stdout); + + if (newline) { + fputc('\n', stdout); + } +} + + +// ============================================================ +// execArith +// ============================================================ + +static BasVmResultE execArith(BasVmT *vm, uint8_t op) { + BasValueT b; + BasValueT a; + + if (!pop(vm, &b) || !pop(vm, &a)) { + return BAS_VM_STACK_UNDERFLOW; + } + + double na = basValToNumber(a); + double nb = basValToNumber(b); + basValRelease(&a); + basValRelease(&b); + + double result; + + switch (op) { + case OP_ADD_INT: + case OP_ADD_FLT: + result = na + nb; + break; + + case OP_SUB_INT: + case OP_SUB_FLT: + result = na - nb; + break; + + case OP_MUL_INT: + case OP_MUL_FLT: + result = na * nb; + break; + + case OP_IDIV_INT: + if ((int32_t)nb == 0) { + runtimeError(vm, 11, "Division by zero"); + return BAS_VM_DIV_BY_ZERO; + } + + result = (double)((int32_t)na / (int32_t)nb); + break; + + case OP_DIV_FLT: + if (nb == 0.0) { + runtimeError(vm, 11, "Division by zero"); + return BAS_VM_DIV_BY_ZERO; + } + + result = na / nb; + break; + + case OP_MOD_INT: + if ((int32_t)nb == 0) { + runtimeError(vm, 11, "Division by zero"); + return BAS_VM_DIV_BY_ZERO; + } + + result = (double)((int32_t)na % (int32_t)nb); + break; + + case OP_POW: + result = pow(na, nb); + break; + + default: + result = 0.0; + break; + } + + // Return appropriate type + if (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT) { + if (result >= -32768.0 && result <= 32767.0) { + push(vm, basValInteger((int16_t)result)); + } else if (result >= -2147483648.0 && result <= 2147483647.0) { + push(vm, basValLong((int32_t)result)); + } else { + push(vm, basValDouble(result)); + } + } else { + push(vm, basValDouble(result)); + } + + return BAS_VM_OK; +} + + +// ============================================================ +// execCompare +// ============================================================ + +static BasVmResultE execCompare(BasVmT *vm, uint8_t op) { + BasValueT b; + BasValueT a; + + if (!pop(vm, &b) || !pop(vm, &a)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t cmp = vm->compareTextMode ? basValCompareCI(a, b) : basValCompare(a, b); + basValRelease(&a); + basValRelease(&b); + + bool result; + + switch (op) { + case OP_CMP_EQ: result = (cmp == 0); break; + case OP_CMP_NE: result = (cmp != 0); break; + case OP_CMP_LT: result = (cmp < 0); break; + case OP_CMP_GT: result = (cmp > 0); break; + case OP_CMP_LE: result = (cmp <= 0); break; + case OP_CMP_GE: result = (cmp >= 0); break; + default: result = false; break; + } + + push(vm, basValBool(result)); + return BAS_VM_OK; +} + + +// ============================================================ +// execFileOp +// ============================================================ + +// File mode constants (matches compiler/parser.c emission) +#define FILE_MODE_INPUT 1 +#define FILE_MODE_OUTPUT 2 +#define FILE_MODE_APPEND 3 +#define FILE_MODE_RANDOM 4 +#define FILE_MODE_BINARY 5 + +static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { + switch (op) { + case OP_FILE_OPEN: { + uint8_t mode = readUint8(vm); + BasValueT channelVal; + BasValueT filenameVal; + + if (!pop(vm, &channelVal) || !pop(vm, &filenameVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + BasValueT fnStr = basValToString(filenameVal); + basValRelease(&filenameVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES) { + basValRelease(&fnStr); + runtimeError(vm, 52, "Bad file channel number"); + return BAS_VM_FILE_ERROR; + } + + // Close existing file on this channel + if (vm->files[channel].handle) { + fclose((FILE *)vm->files[channel].handle); + vm->files[channel].handle = NULL; + vm->files[channel].mode = 0; + } + + const char *modeStr; + + switch (mode) { + case FILE_MODE_INPUT: + modeStr = "r"; + break; + case FILE_MODE_OUTPUT: + modeStr = "w"; + break; + case FILE_MODE_APPEND: + modeStr = "a"; + break; + case FILE_MODE_RANDOM: + case FILE_MODE_BINARY: + modeStr = "r+b"; + break; + default: + basValRelease(&fnStr); + runtimeError(vm, 54, "Bad file mode"); + return BAS_VM_FILE_ERROR; + } + + // For RANDOM/BINARY: create file if it doesn't exist, then reopen r+b + if (mode == FILE_MODE_RANDOM || mode == FILE_MODE_BINARY) { + FILE *test = fopen(fnStr.strVal->data, "r"); + if (!test) { + // Create the file + test = fopen(fnStr.strVal->data, "w+b"); + if (test) { + fclose(test); + } + } else { + fclose(test); + } + } + + FILE *fp = fopen(fnStr.strVal->data, modeStr); + basValRelease(&fnStr); + + if (!fp) { + runtimeError(vm, 53, "File not found or cannot open"); + return BAS_VM_FILE_ERROR; + } + + vm->files[channel].handle = fp; + vm->files[channel].mode = mode; + break; + } + + case OP_FILE_CLOSE: { + BasValueT channelVal; + + if (!pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES) { + runtimeError(vm, 52, "Bad file channel number"); + return BAS_VM_FILE_ERROR; + } + + if (vm->files[channel].handle) { + fclose((FILE *)vm->files[channel].handle); + vm->files[channel].handle = NULL; + vm->files[channel].mode = 0; + } + + break; + } + + case OP_FILE_PRINT: { + BasValueT val; + BasValueT channelVal; + + if (!pop(vm, &val) || !pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + basValRelease(&val); + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + BasStringT *s = basValFormatString(val); + basValRelease(&val); + + if (s) { + fputs(s->data, (FILE *)vm->files[channel].handle); + fputc('\n', (FILE *)vm->files[channel].handle); + basStringUnref(s); + } + + break; + } + + case OP_FILE_INPUT: { + BasValueT channelVal; + + if (!pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + char buf[1024]; + buf[0] = '\0'; + + if (fgets(buf, sizeof(buf), (FILE *)vm->files[channel].handle)) { + // Strip trailing newline + int32_t len = (int32_t)strlen(buf); + + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { + buf[--len] = '\0'; + } + } + + if (!push(vm, basValStringFromC(buf))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FILE_EOF: { + BasValueT channelVal; + + if (!pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + // Peek ahead to detect EOF before the next read + FILE *fp = (FILE *)vm->files[channel].handle; + int ch = fgetc(fp); + bool isEof; + + if (ch == EOF) { + isEof = true; + } else { + ungetc(ch, fp); + isEof = false; + } + + if (!push(vm, basValBool(isEof))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FILE_LINE_INPUT: { + BasValueT channelVal; + + if (!pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + char buf[1024]; + buf[0] = '\0'; + + if (fgets(buf, sizeof(buf), (FILE *)vm->files[channel].handle)) { + int32_t len = (int32_t)strlen(buf); + + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { + buf[--len] = '\0'; + } + } + + if (!push(vm, basValStringFromC(buf))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FILE_WRITE: { + // Pop value and channel, write value in WRITE format + BasValueT val; + BasValueT channelVal; + + if (!pop(vm, &val) || !pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + basValRelease(&val); + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + FILE *fp = (FILE *)vm->files[channel].handle; + + if (val.type == BAS_TYPE_STRING) { + // Strings: enclosed in quotes + fputc('"', fp); + if (val.strVal) { + fputs(val.strVal->data, fp); + } + fputc('"', fp); + } else { + // Numbers: no leading space (unlike PRINT) + BasStringT *s = basValFormatString(val); + if (s) { + // Skip leading space that basValFormatString adds for positive numbers + const char *text = s->data; + if (*text == ' ') { + text++; + } + fputs(text, fp); + basStringUnref(s); + } + } + + basValRelease(&val); + break; + } + + case OP_FILE_WRITE_SEP: { + // Pop channel, write comma separator + BasValueT channelVal; + + if (!pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + fputc(',', (FILE *)vm->files[channel].handle); + break; + } + + case OP_FILE_WRITE_NL: { + // Pop channel, write newline + BasValueT channelVal; + + if (!pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + fputc('\n', (FILE *)vm->files[channel].handle); + break; + } + + case OP_FILE_GET: { + // Pop type, recno, channel; read data; push value + BasValueT typeVal; + BasValueT recnoVal; + BasValueT channelVal; + + if (!pop(vm, &typeVal) || !pop(vm, &recnoVal) || !pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + int32_t recno = (int32_t)basValToNumber(recnoVal); + int32_t dataType = (int32_t)basValToNumber(typeVal); + basValRelease(&channelVal); + basValRelease(&recnoVal); + basValRelease(&typeVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + FILE *fp = (FILE *)vm->files[channel].handle; + + // Seek to record position if recno > 0 + // (recno is 1-based in QB; 0 means current position) + if (recno > 0) { + // For simplicity, use fixed 128-byte records for RANDOM + fseek(fp, (long)(recno - 1) * 128, SEEK_SET); + } + + BasValueT result; + memset(&result, 0, sizeof(result)); + + switch (dataType) { + case BAS_TYPE_INTEGER: { + int16_t val = 0; + if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ } + result = basValInteger(val); + break; + } + case BAS_TYPE_LONG: { + int32_t val = 0; + if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ } + result = basValLong(val); + break; + } + case BAS_TYPE_SINGLE: { + float val = 0.0f; + if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ } + result = basValSingle(val); + break; + } + case BAS_TYPE_DOUBLE: { + double val = 0.0; + if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ } + result = basValDouble(val); + break; + } + case BAS_TYPE_STRING: { + // Read a length-prefixed string (int16 len + data) + int16_t len = 0; + if (fread(&len, sizeof(len), 1, fp) < 1) { /* EOF ok */ } + if (len < 0) { + len = 0; + } + char *buf = (char *)malloc(len + 1); + if (buf) { + if (fread(buf, 1, len, fp) < 1) { /* EOF ok */ } + buf[len] = '\0'; + result = basValStringFromC(buf); + free(buf); + } else { + result = basValStringFromC(""); + } + break; + } + default: + result = basValInteger(0); + break; + } + + if (!push(vm, result)) { + basValRelease(&result); + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FILE_PUT: { + // Pop value, recno, channel; write data + BasValueT val; + BasValueT recnoVal; + BasValueT channelVal; + + if (!pop(vm, &val) || !pop(vm, &recnoVal) || !pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + int32_t recno = (int32_t)basValToNumber(recnoVal); + basValRelease(&channelVal); + basValRelease(&recnoVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + basValRelease(&val); + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + FILE *fp = (FILE *)vm->files[channel].handle; + + if (recno > 0) { + fseek(fp, (long)(recno - 1) * 128, SEEK_SET); + } + + switch (val.type) { + case BAS_TYPE_INTEGER: { + int16_t v = val.intVal; + fwrite(&v, sizeof(v), 1, fp); + break; + } + case BAS_TYPE_LONG: { + int32_t v = val.longVal; + fwrite(&v, sizeof(v), 1, fp); + break; + } + case BAS_TYPE_SINGLE: { + float v = val.sngVal; + fwrite(&v, sizeof(v), 1, fp); + break; + } + case BAS_TYPE_DOUBLE: { + double v = val.dblVal; + fwrite(&v, sizeof(v), 1, fp); + break; + } + case BAS_TYPE_STRING: { + // Write length-prefixed string + int16_t len = val.strVal ? (int16_t)val.strVal->len : 0; + fwrite(&len, sizeof(len), 1, fp); + if (len > 0 && val.strVal) { + fwrite(val.strVal->data, 1, len, fp); + } + break; + } + default: { + int16_t zero = 0; + fwrite(&zero, sizeof(zero), 1, fp); + break; + } + } + + fflush(fp); + basValRelease(&val); + break; + } + + case OP_FILE_SEEK: { + // Pop position and channel, seek + BasValueT posVal; + BasValueT channelVal; + + if (!pop(vm, &posVal) || !pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + int32_t pos = (int32_t)basValToNumber(posVal); + basValRelease(&channelVal); + basValRelease(&posVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + // QB SEEK is 1-based + fseek((FILE *)vm->files[channel].handle, (long)(pos - 1), SEEK_SET); + break; + } + + case OP_FILE_LOF: { + // Pop channel, push file length + BasValueT channelVal; + + if (!pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + FILE *fp = (FILE *)vm->files[channel].handle; + long savedPos = ftell(fp); + fseek(fp, 0, SEEK_END); + long length = ftell(fp); + fseek(fp, savedPos, SEEK_SET); + + if (!push(vm, basValLong((int32_t)length))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FILE_LOC: { + // Pop channel, push current position + BasValueT channelVal; + + if (!pop(vm, &channelVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + basValRelease(&channelVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + long pos = ftell((FILE *)vm->files[channel].handle); + + // QB returns 1-based position + if (!push(vm, basValLong((int32_t)(pos + 1)))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FILE_FREEFILE: { + // Push the next available file channel number + int32_t freeNum = 0; + + for (int32_t i = 1; i < BAS_VM_MAX_FILES; i++) { + if (!vm->files[i].handle) { + freeNum = i; + break; + } + } + + if (freeNum == 0) { + runtimeError(vm, 67, "Too many files open"); + return BAS_VM_FILE_ERROR; + } + + if (!push(vm, basValInteger((int16_t)freeNum))) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FILE_INPUT_N: { + // Pop channel and n, read n chars, push string + BasValueT channelVal; + BasValueT nVal; + + if (!pop(vm, &channelVal) || !pop(vm, &nVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t channel = (int32_t)basValToNumber(channelVal); + int32_t n = (int32_t)basValToNumber(nVal); + basValRelease(&channelVal); + basValRelease(&nVal); + + if (channel < 1 || channel >= BAS_VM_MAX_FILES || !vm->files[channel].handle) { + runtimeError(vm, 52, "Bad file number or file not open"); + return BAS_VM_FILE_ERROR; + } + + if (n < 0) { + n = 0; + } + + if (n > 32767) { + n = 32767; + } + + char *buf = (char *)malloc(n + 1); + + if (!buf) { + runtimeError(vm, 7, "Out of memory"); + return BAS_VM_OUT_OF_MEMORY; + } + + int32_t bytesRead = (int32_t)fread(buf, 1, n, (FILE *)vm->files[channel].handle); + buf[bytesRead] = '\0'; + + BasStringT *s = basStringNew(buf, bytesRead); + free(buf); + + BasValueT result; + result.type = BAS_TYPE_STRING; + result.strVal = s; + + if (!push(vm, result)) { + basStringUnref(s); + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + default: + return BAS_VM_BAD_OPCODE; + } + + return BAS_VM_OK; +} + + +// ============================================================ +// execLogical +// ============================================================ + +static BasVmResultE execLogical(BasVmT *vm, uint8_t op) { + if (op == OP_NOT) { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + int32_t n = (int32_t)basValToNumber(*top); + basValRelease(top); + *top = basValInteger((int16_t)(~n)); + return BAS_VM_OK; + } + + BasValueT b; + BasValueT a; + + if (!pop(vm, &b) || !pop(vm, &a)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t na = (int32_t)basValToNumber(a); + int32_t nb = (int32_t)basValToNumber(b); + basValRelease(&a); + basValRelease(&b); + + int32_t result; + + switch (op) { + case OP_AND: result = na & nb; break; + case OP_OR: result = na | nb; break; + case OP_XOR: result = na ^ nb; break; + case OP_EQV: result = ~(na ^ nb); break; + case OP_IMP: result = (~na) | nb; break; + default: result = 0; break; + } + + push(vm, basValInteger((int16_t)result)); + return BAS_VM_OK; +} + + +// ============================================================ +// execMath +// ============================================================ + +static BasVmResultE execMath(BasVmT *vm, uint8_t op) { + if (op == OP_MATH_RND) { + // Pop the dummy arg (parser pushes -1 for RND()) + BasValueT dummy; + + if (!pop(vm, &dummy)) { + return BAS_VM_STACK_UNDERFLOW; + } + + basValRelease(&dummy); + + double r = (double)rand() / (double)RAND_MAX; + + if (!push(vm, basValSingle((float)r))) { + return BAS_VM_STACK_OVERFLOW; + } + + return BAS_VM_OK; + } + + if (op == OP_MATH_RANDOMIZE) { + BasValueT val; + + if (!pop(vm, &val)) { + return BAS_VM_STACK_UNDERFLOW; + } + + double n = basValToNumber(val); + basValRelease(&val); + + if (n < 0) { + srand((unsigned int)time(NULL)); + } else { + srand((unsigned int)n); + } + + return BAS_VM_OK; + } + + // All other math ops take one argument + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + double n = basValToNumber(*top); + double result; + + switch (op) { + case OP_MATH_ABS: result = fabs(n); break; + case OP_MATH_INT: result = floor(n); break; + case OP_MATH_FIX: result = (n >= 0) ? floor(n) : ceil(n); break; + case OP_MATH_SGN: result = (n > 0) ? 1.0 : (n < 0) ? -1.0 : 0.0; break; + case OP_MATH_SQR: result = sqrt(n); break; + case OP_MATH_SIN: result = sin(n); break; + case OP_MATH_COS: result = cos(n); break; + case OP_MATH_TAN: result = tan(n); break; + case OP_MATH_ATN: result = atan(n); break; + case OP_MATH_LOG: result = log(n); break; + case OP_MATH_EXP: result = exp(n); break; + default: result = 0.0; break; + } + + basValRelease(top); + *top = basValDouble(result); + return BAS_VM_OK; +} + + +// ============================================================ +// execPrint +// ============================================================ + +static BasVmResultE execPrint(BasVmT *vm) { + BasValueT val; + + if (!pop(vm, &val)) { + return BAS_VM_STACK_UNDERFLOW; + } + + // QB prints numeric values with a trailing space + bool isNumeric = (val.type != BAS_TYPE_STRING); + + BasStringT *s = basValFormatString(val); + basValRelease(&val); + + if (vm->printFn && s) { + vm->printFn(vm->printCtx, s->data, false); + + if (isNumeric) { + vm->printFn(vm->printCtx, " ", false); + } + } + + basStringUnref(s); + return BAS_VM_OK; +} + + +// ============================================================ +// execStringOp +// ============================================================ + +static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) { + switch (op) { + case OP_STR_CONCAT: { + BasValueT b; + BasValueT a; + + if (!pop(vm, &b) || !pop(vm, &a)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT sa = basValToString(a); + BasValueT sb = basValToString(b); + basValRelease(&a); + basValRelease(&b); + + BasStringT *result = basStringConcat(sa.strVal, sb.strVal); + basValRelease(&sa); + basValRelease(&sb); + + BasValueT rv; + rv.type = BAS_TYPE_STRING; + rv.strVal = result; + push(vm, rv); + return BAS_VM_OK; + } + + case OP_STR_LEFT: { + BasValueT nVal; + BasValueT sVal; + + if (!pop(vm, &nVal) || !pop(vm, &sVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t n = (int32_t)basValToNumber(nVal); + basValRelease(&nVal); + + BasValueT sv = basValToString(sVal); + basValRelease(&sVal); + + BasStringT *result = basStringSub(sv.strVal, 0, n); + basValRelease(&sv); + + BasValueT rv; + rv.type = BAS_TYPE_STRING; + rv.strVal = result; + push(vm, rv); + return BAS_VM_OK; + } + + case OP_STR_RIGHT: { + BasValueT nVal; + BasValueT sVal; + + if (!pop(vm, &nVal) || !pop(vm, &sVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t n = (int32_t)basValToNumber(nVal); + basValRelease(&nVal); + + BasValueT sv = basValToString(sVal); + basValRelease(&sVal); + + int32_t start = sv.strVal->len - n; + + if (start < 0) { + start = 0; + } + + BasStringT *result = basStringSub(sv.strVal, start, n); + basValRelease(&sv); + + BasValueT rv; + rv.type = BAS_TYPE_STRING; + rv.strVal = result; + push(vm, rv); + return BAS_VM_OK; + } + + case OP_STR_MID: { + BasValueT lenVal; + BasValueT startVal; + BasValueT sVal; + + if (!pop(vm, &lenVal) || !pop(vm, &startVal) || !pop(vm, &sVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t start = (int32_t)basValToNumber(startVal) - 1; // 1-based to 0-based + int32_t len = (int32_t)basValToNumber(lenVal); + basValRelease(&startVal); + basValRelease(&lenVal); + + BasValueT sv = basValToString(sVal); + basValRelease(&sVal); + + BasStringT *result = basStringSub(sv.strVal, start, len); + basValRelease(&sv); + + BasValueT rv; + rv.type = BAS_TYPE_STRING; + rv.strVal = result; + push(vm, rv); + return BAS_VM_OK; + } + + case OP_STR_MID2: { + BasValueT startVal; + BasValueT sVal; + + if (!pop(vm, &startVal) || !pop(vm, &sVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + int32_t start = (int32_t)basValToNumber(startVal) - 1; + basValRelease(&startVal); + + BasValueT sv = basValToString(sVal); + basValRelease(&sVal); + + BasStringT *result = basStringSub(sv.strVal, start, sv.strVal->len - start); + basValRelease(&sv); + + BasValueT rv; + rv.type = BAS_TYPE_STRING; + rv.strVal = result; + push(vm, rv); + return BAS_VM_OK; + } + + case OP_STR_LEN: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT sv = basValToString(*top); + int32_t len = sv.strVal ? sv.strVal->len : 0; + basValRelease(&sv); + basValRelease(top); + *top = basValInteger((int16_t)len); + return BAS_VM_OK; + } + + case OP_STR_INSTR: { + BasValueT findVal; + BasValueT sVal; + + if (!pop(vm, &findVal) || !pop(vm, &sVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT sv = basValToString(sVal); + BasValueT fv = basValToString(findVal); + basValRelease(&sVal); + basValRelease(&findVal); + + int32_t pos = 0; + char *found = strstr(sv.strVal->data, fv.strVal->data); + + if (found) { + pos = (int32_t)(found - sv.strVal->data) + 1; // 1-based + } + + basValRelease(&sv); + basValRelease(&fv); + push(vm, basValInteger((int16_t)pos)); + return BAS_VM_OK; + } + + case OP_STR_UCASE: + case OP_STR_LCASE: + case OP_STR_TRIM: + case OP_STR_LTRIM: + case OP_STR_RTRIM: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT sv = basValToString(*top); + basValRelease(top); + + BasStringT *src = sv.strVal; + BasStringT *result; + + if (op == OP_STR_UCASE || op == OP_STR_LCASE) { + result = basStringNew(src->data, src->len); + + for (int32_t i = 0; i < result->len; i++) { + if (op == OP_STR_UCASE && result->data[i] >= 'a' && result->data[i] <= 'z') { + result->data[i] -= 32; + } else if (op == OP_STR_LCASE && result->data[i] >= 'A' && result->data[i] <= 'Z') { + result->data[i] += 32; + } + } + } else { + int32_t start = 0; + int32_t end = src->len; + + if (op == OP_STR_LTRIM || op == OP_STR_TRIM) { + while (start < end && src->data[start] == ' ') { + start++; + } + } + + if (op == OP_STR_RTRIM || op == OP_STR_TRIM) { + while (end > start && src->data[end - 1] == ' ') { + end--; + } + } + + result = basStringSub(src, start, end - start); + } + + basValRelease(&sv); + top->type = BAS_TYPE_STRING; + top->strVal = result; + return BAS_VM_OK; + } + + case OP_STR_CHR: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + int32_t code = (int32_t)basValToNumber(*top); + char buf[2] = { (char)(code & 0xFF), '\0' }; + basValRelease(top); + *top = basValStringFromC(buf); + return BAS_VM_OK; + } + + case OP_STR_ASC: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT sv = basValToString(*top); + int32_t code = (sv.strVal && sv.strVal->len > 0) ? (unsigned char)sv.strVal->data[0] : 0; + basValRelease(&sv); + basValRelease(top); + *top = basValInteger((int16_t)code); + return BAS_VM_OK; + } + + case OP_STR_SPACE: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + int32_t n = (int32_t)basValToNumber(*top); + basValRelease(top); + + if (n < 0) { + n = 0; + } + + if (n > 32767) { + n = 32767; + } + + BasStringT *s = basStringAlloc(n + 1); + memset(s->data, ' ', n); + s->data[n] = '\0'; + s->len = n; + + top->type = BAS_TYPE_STRING; + top->strVal = s; + return BAS_VM_OK; + } + + case OP_STR_FIXLEN: { + // [uint16 len] pop string, pad/truncate to fixed length, push result + uint16_t fixLen = readUint16(vm); + + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + BasValueT sv = basValToString(*top); + basValRelease(top); + + BasStringT *src = sv.strVal; + BasStringT *result = basStringAlloc(fixLen + 1); + result->len = fixLen; + + int32_t srcLen = src ? src->len : 0; + int32_t copyLen = srcLen < (int32_t)fixLen ? srcLen : (int32_t)fixLen; + + if (copyLen > 0 && src) { + memcpy(result->data, src->data, copyLen); + } + + // Pad with spaces + for (int32_t i = copyLen; i < (int32_t)fixLen; i++) { + result->data[i] = ' '; + } + + result->data[fixLen] = '\0'; + basValRelease(&sv); + + top->type = BAS_TYPE_STRING; + top->strVal = result; + return BAS_VM_OK; + } + + case OP_STR_MID_ASGN: { + // Pop replacement, len, start, str; push modified string + BasValueT replVal; + BasValueT lenVal; + BasValueT startVal; + BasValueT strVal; + + if (!pop(vm, &replVal) || !pop(vm, &lenVal) || !pop(vm, &startVal) || !pop(vm, &strVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT sv = basValToString(strVal); + BasValueT rv = basValToString(replVal); + int32_t start = (int32_t)basValToNumber(startVal) - 1; // 1-based to 0-based + int32_t len = (int32_t)basValToNumber(lenVal); + basValRelease(&strVal); + basValRelease(&startVal); + basValRelease(&lenVal); + basValRelease(&replVal); + + BasStringT *src = sv.strVal; + BasStringT *repl = rv.strVal; + int32_t srcLen = src ? src->len : 0; + int32_t replLen = repl ? repl->len : 0; + + // If len is 0, use replacement length + if (len <= 0) { + len = replLen; + } + + // Clamp to available replacement length + if (len > replLen) { + len = replLen; + } + + // Create a copy of the original string + BasStringT *result = basStringNew(src ? src->data : "", srcLen); + + // Replace characters + if (start >= 0 && start < srcLen && len > 0) { + int32_t maxReplace = srcLen - start; + if (len > maxReplace) { + len = maxReplace; + } + if (repl) { + memcpy(result->data + start, repl->data, len); + } + } + + basValRelease(&sv); + basValRelease(&rv); + + BasValueT resultVal; + resultVal.type = BAS_TYPE_STRING; + resultVal.strVal = result; + + if (!push(vm, resultVal)) { + basStringUnref(result); + return BAS_VM_STACK_OVERFLOW; + } + + return BAS_VM_OK; + } + + default: + return BAS_VM_BAD_OPCODE; + } +} + + +// ============================================================ +// pop +// ============================================================ + +static bool pop(BasVmT *vm, BasValueT *val) { + if (vm->sp <= 0) { + return false; + } + + *val = vm->stack[--vm->sp]; + return true; +} + + +// ============================================================ +// push +// ============================================================ + +static bool push(BasVmT *vm, BasValueT val) { + if (vm->sp >= BAS_VM_STACK_SIZE) { + return false; + } + + vm->stack[vm->sp++] = val; + return true; +} + + +// ============================================================ +// readInt16 +// ============================================================ + +static int16_t readInt16(BasVmT *vm) { + int16_t val; + memcpy(&val, &vm->module->code[vm->pc], sizeof(int16_t)); + vm->pc += sizeof(int16_t); + return val; +} + + +// ============================================================ +// readUint8 +// ============================================================ + +static uint8_t readUint8(BasVmT *vm) { + return vm->module->code[vm->pc++]; +} + + +// ============================================================ +// readUint16 +// ============================================================ + +static uint16_t readUint16(BasVmT *vm) { + uint16_t val; + memcpy(&val, &vm->module->code[vm->pc], sizeof(uint16_t)); + vm->pc += sizeof(uint16_t); + return val; +} + + +// ============================================================ +// runtimeError +// ============================================================ + +static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg) { + vm->errorNumber = errNum; + snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d at PC %d: %s", errNum, vm->pc, msg); +} diff --git a/dvxbasic/runtime/vm.h b/dvxbasic/runtime/vm.h new file mode 100644 index 0000000..84a4b20 --- /dev/null +++ b/dvxbasic/runtime/vm.h @@ -0,0 +1,211 @@ +// vm.h -- DVX BASIC virtual machine +// +// Stack-based p-code interpreter. Executes compiled BASIC bytecode. +// Embeddable: the host provides I/O callbacks. No DVX dependencies. +// +// Usage: +// BasVmT *vm = basVmCreate(); +// basVmSetPrintCallback(vm, myPrintFn, myCtx); +// basVmSetInputCallback(vm, myInputFn, myCtx); +// basVmLoadModule(vm, compiledCode, codeLen, constants, numConsts); +// BasVmResultE result = basVmRun(vm); +// basVmDestroy(vm); + +#ifndef DVXBASIC_VM_H +#define DVXBASIC_VM_H + +#include "values.h" + +#include +#include + +// ============================================================ +// Limits +// ============================================================ + +#define BAS_VM_STACK_SIZE 256 // evaluation stack depth +#define BAS_VM_CALL_STACK_SIZE 64 // max call nesting +#define BAS_VM_MAX_GLOBALS 512 // global variable slots +#define BAS_VM_MAX_LOCALS 64 // locals per stack frame +#define BAS_VM_MAX_FOR_DEPTH 32 // nested FOR loops +#define BAS_VM_MAX_FILES 16 // open file channels + +// ============================================================ +// Result codes +// ============================================================ + +typedef enum { + BAS_VM_OK, // program completed normally + BAS_VM_HALTED, // HALT instruction reached + BAS_VM_YIELDED, // DoEvents yielded control + BAS_VM_ERROR, // runtime error + BAS_VM_STACK_OVERFLOW, + BAS_VM_STACK_UNDERFLOW, + BAS_VM_CALL_OVERFLOW, + BAS_VM_DIV_BY_ZERO, + BAS_VM_TYPE_MISMATCH, + BAS_VM_OUT_OF_MEMORY, + BAS_VM_BAD_OPCODE, + BAS_VM_FILE_ERROR, + BAS_VM_SUBSCRIPT_RANGE, + BAS_VM_USER_ERROR // ON ERROR raised +} BasVmResultE; + +// ============================================================ +// I/O callbacks (host-provided) +// ============================================================ + +// Print callback: called for PRINT output. +// text is a null-terminated string. newline indicates whether +// to advance to the next line after printing. +typedef void (*BasPrintFnT)(void *ctx, const char *text, bool newline); + +// Input callback: called for INPUT statement. +// prompt is the text to display. The callback must fill buf +// (up to bufSize-1 chars, null-terminated). Returns true on +// success, false on cancel/error. +typedef bool (*BasInputFnT)(void *ctx, const char *prompt, char *buf, int32_t bufSize); + +// DoEvents callback: called for DoEvents statement. +// The host should process pending events and return. Returns +// true to continue execution, false to stop the program. +typedef bool (*BasDoEventsFnT)(void *ctx); + +// ============================================================ +// Call stack frame +// ============================================================ + +typedef struct { + int32_t returnPc; // instruction to return to + int32_t baseSlot; // base index in locals array + int32_t localCount; // number of locals in this frame + BasValueT locals[BAS_VM_MAX_LOCALS]; +} BasCallFrameT; + +// ============================================================ +// FOR loop state +// ============================================================ + +typedef struct { + int32_t varIdx; // loop variable slot index + bool isLocal; // true = local, false = global + BasValueT limit; // upper bound + BasValueT step; // step value + int32_t loopTop; // PC of the loop body start +} BasForStateT; + +// ============================================================ +// File channel +// ============================================================ + +typedef struct { + void *handle; // FILE* or platform-specific + int32_t mode; // 0=closed, 1=input, 2=output, 3=append, 4=random, 5=binary +} BasFileChannelT; + +// ============================================================ +// Compiled module (output of the compiler) +// ============================================================ + +typedef struct { + uint8_t *code; // p-code bytecode + int32_t codeLen; + BasStringT **constants; // string constant pool + int32_t constCount; + int32_t globalCount; // number of global variable slots needed + int32_t entryPoint; // PC of the first instruction (module-level code) + BasValueT *dataPool; // DATA statement value pool + int32_t dataCount; // number of values in the data pool +} BasModuleT; + +// ============================================================ +// VM state +// ============================================================ + +typedef struct { + // Program + BasModuleT *module; + + // Execution + int32_t pc; // program counter + bool running; + bool yielded; + + // Evaluation stack + BasValueT stack[BAS_VM_STACK_SIZE]; + int32_t sp; // stack pointer (index of next free slot) + + // Call stack + BasCallFrameT callStack[BAS_VM_CALL_STACK_SIZE]; + int32_t callDepth; + + // FOR loop stack + BasForStateT forStack[BAS_VM_MAX_FOR_DEPTH]; + int32_t forDepth; + + // Global variables + BasValueT globals[BAS_VM_MAX_GLOBALS]; + + // File channels (1-based, index 0 unused) + BasFileChannelT files[BAS_VM_MAX_FILES]; + + // DATA/READ pointer + int32_t dataPtr; // current READ position in data pool + + // String comparison mode + bool compareTextMode; // true = case-insensitive comparisons + + // Error handling + int32_t errorHandler; // PC of ON ERROR GOTO handler (0 = none) + int32_t errorNumber; // current Err number + int32_t errorPc; // PC of the instruction that caused the error (for RESUME) + int32_t errorNextPc; // PC of the next instruction after error (for RESUME NEXT) + bool inErrorHandler; // true when executing error handler code + char errorMsg[256]; // current error description + + // I/O callbacks + BasPrintFnT printFn; + void *printCtx; + BasInputFnT inputFn; + void *inputCtx; + BasDoEventsFnT doEventsFn; + void *doEventsCtx; +} BasVmT; + +// ============================================================ +// API +// ============================================================ + +// Create a new VM instance. +BasVmT *basVmCreate(void); + +// Destroy a VM instance and free all resources. +void basVmDestroy(BasVmT *vm); + +// Load a compiled module into the VM. +void basVmLoadModule(BasVmT *vm, BasModuleT *module); + +// Execute the loaded module. Returns when the program ends, +// halts, yields, or hits an error. +BasVmResultE basVmRun(BasVmT *vm); + +// Execute a single instruction. Returns the result. +// Useful for stepping/debugging. +BasVmResultE basVmStep(BasVmT *vm); + +// Reset the VM to initial state (clear stack, globals, PC). +void basVmReset(BasVmT *vm); + +// Set I/O callbacks. +void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx); +void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx); +void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx); + +// Push/pop values on the evaluation stack (for host integration). +bool basVmPush(BasVmT *vm, BasValueT val); +bool basVmPop(BasVmT *vm, BasValueT *val); + +// Get the current error message. +const char *basVmGetError(const BasVmT *vm); + +#endif // DVXBASIC_VM_H diff --git a/dvxbasic/test_compiler b/dvxbasic/test_compiler new file mode 100755 index 0000000000000000000000000000000000000000..8a071cfa3cdeac8ec91e2da3f445de467d33f7cc GIT binary patch literal 146984 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 literal 0 HcmV?d00001 diff --git a/dvxbasic/test_compiler.c b/dvxbasic/test_compiler.c new file mode 100644 index 0000000..a349475 --- /dev/null +++ b/dvxbasic/test_compiler.c @@ -0,0 +1,850 @@ +// test_compiler.c -- End-to-end test: source -> compiler -> VM -> output +// +// Build (native): +// gcc -O2 -Wall -o test_compiler test_compiler.c \ +// compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c \ +// runtime/vm.c runtime/values.c -lm + +#include "compiler/parser.h" +#include "runtime/vm.h" +#include "runtime/values.h" + +#include +#include + +static void runProgram(const char *name, const char *source) { + printf("=== %s ===\n", name); + + int32_t len = (int32_t)strlen(source); + + BasParserT parser; + basParserInit(&parser, source, len); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n\n", parser.error); + basParserFree(&parser); + return; + } + + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (!mod) { + printf("MODULE BUILD FAILED\n\n"); + return; + } + + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + + // Module-level code uses callStack[0] as implicit main frame + vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount; + vm->callDepth = 1; + + BasVmResultE result = basVmRun(vm); + + if (result != BAS_VM_HALTED && result != BAS_VM_OK) { + printf("[VM error %d: %s]\n", result, basVmGetError(vm)); + } + + basVmDestroy(vm); + basModuleFree(mod); + printf("\n"); +} + + +int main(void) { + printf("DVX BASIC Compiler Tests\n"); + printf("========================\n\n"); + + basStringSystemInit(); + + // Test 1: Hello World + runProgram("Hello World", + "PRINT \"Hello, World!\"\n" + ); + + // Test 2: Arithmetic + runProgram("Arithmetic", + "PRINT 2 + 3 * 4\n" + "PRINT 10 \\ 3\n" + "PRINT 10 MOD 3\n" + "PRINT 2 ^ 8\n" + ); + + // Test 3: String operations + runProgram("String Ops", + "DIM s AS STRING\n" + "s = \"Hello, BASIC!\"\n" + "PRINT s\n" + "PRINT LEN(s)\n" + "PRINT LEFT$(s, 5)\n" + "PRINT RIGHT$(s, 6)\n" + "PRINT MID$(s, 8, 5)\n" + "PRINT UCASE$(s)\n" + ); + + // Test 4: IF/THEN/ELSE + runProgram("IF/THEN/ELSE", + "DIM x AS INTEGER\n" + "x = 42\n" + "IF x > 100 THEN\n" + " PRINT \"big\"\n" + "ELSEIF x > 10 THEN\n" + " PRINT \"medium\"\n" + "ELSE\n" + " PRINT \"small\"\n" + "END IF\n" + ); + + // Test 5: FOR loop + runProgram("FOR Loop", + "DIM i AS INTEGER\n" + "FOR i = 1 TO 10\n" + " PRINT i;\n" + "NEXT i\n" + "PRINT\n" + ); + + // Test 6: DO/WHILE loop + runProgram("DO/WHILE Loop", + "DIM n AS INTEGER\n" + "n = 1\n" + "DO WHILE n <= 5\n" + " PRINT n;\n" + " n = n + 1\n" + "LOOP\n" + "PRINT\n" + ); + + // Test 7: SUB and FUNCTION + runProgram("SUB and FUNCTION", + "DECLARE SUB Greet(name AS STRING)\n" + "DECLARE FUNCTION Square(x AS INTEGER) AS INTEGER\n" + "\n" + "CALL Greet(\"World\")\n" + "PRINT Square(7)\n" + "\n" + "SUB Greet(name AS STRING)\n" + " PRINT \"Hello, \" & name & \"!\"\n" + "END SUB\n" + "\n" + "FUNCTION Square(x AS INTEGER) AS INTEGER\n" + " Square = x * x\n" + "END FUNCTION\n" + ); + + // Test 8: SELECT CASE + runProgram("SELECT CASE", + "DIM grade AS STRING\n" + "grade = \"B\"\n" + "SELECT CASE grade\n" + " CASE \"A\"\n" + " PRINT \"Excellent\"\n" + " CASE \"B\", \"C\"\n" + " PRINT \"Good\"\n" + " CASE ELSE\n" + " PRINT \"Other\"\n" + "END SELECT\n" + ); + + // Test 9: Fibonacci + runProgram("Fibonacci", + "DIM a AS INTEGER\n" + "DIM b AS INTEGER\n" + "DIM temp AS INTEGER\n" + "DIM i AS INTEGER\n" + "a = 0\n" + "b = 1\n" + "FOR i = 1 TO 10\n" + " PRINT a;\n" + " temp = a + b\n" + " a = b\n" + " b = temp\n" + "NEXT i\n" + "PRINT\n" + ); + + // Test 10: Math functions + runProgram("Math Functions", + "PRINT ABS(-42)\n" + "PRINT SQR(144)\n" + "PRINT INT(3.7)\n" + ); + + // Test 11: File I/O + runProgram("File I/O", + "OPEN \"/tmp/dvxbasic_test.txt\" FOR OUTPUT AS #1\n" + "PRINT #1, \"Hello from BASIC!\"\n" + "PRINT #1, \"Line two\"\n" + "PRINT #1, \"42\"\n" + "CLOSE #1\n" + "\n" + "DIM line$ AS STRING\n" + "DIM count AS INTEGER\n" + "count = 0\n" + "OPEN \"/tmp/dvxbasic_test.txt\" FOR INPUT AS #1\n" + "DO WHILE NOT EOF(#1)\n" + " INPUT #1, line$\n" + " PRINT line$\n" + " count = count + 1\n" + "LOOP\n" + "CLOSE #1\n" + "PRINT count;\n" + "PRINT \"lines read\"\n" + ); + + // Test 12: LINE INPUT# and APPEND + runProgram("LINE INPUT and APPEND", + "OPEN \"/tmp/dvxbasic_test2.txt\" FOR OUTPUT AS #2\n" + "PRINT #2, \"First line\"\n" + "CLOSE #2\n" + "\n" + "OPEN \"/tmp/dvxbasic_test2.txt\" FOR APPEND AS #2\n" + "PRINT #2, \"Appended line\"\n" + "CLOSE #2\n" + "\n" + "DIM s$ AS STRING\n" + "OPEN \"/tmp/dvxbasic_test2.txt\" FOR INPUT AS #2\n" + "LINE INPUT #2, s$\n" + "PRINT s$\n" + "LINE INPUT #2, s$\n" + "PRINT s$\n" + "CLOSE #2\n" + ); + + // Test 13: Array -- 1D with default lbound=0 + runProgram("1D Array", + "DIM arr(5) AS INTEGER\n" + "DIM i AS INTEGER\n" + "FOR i = 1 TO 5\n" + " arr(i) = i * i\n" + "NEXT i\n" + "FOR i = 1 TO 5\n" + " PRINT arr(i);\n" + "NEXT i\n" + "PRINT\n" + ); + // Expected: 1 4 9 16 25 + + // Test 14: Multi-dimensional array + runProgram("Multi-dim Array", + "DIM m(2, 2) AS INTEGER\n" + "m(1, 1) = 11\n" + "m(1, 2) = 12\n" + "m(2, 1) = 21\n" + "m(2, 2) = 22\n" + "PRINT m(1, 1); m(1, 2); m(2, 1); m(2, 2)\n" + ); + // Expected: 11 12 21 22 + + // Test 15: Array with explicit bounds (TO syntax) + runProgram("Array with TO bounds", + "DIM a(1 TO 3) AS INTEGER\n" + "a(1) = 10\n" + "a(2) = 20\n" + "a(3) = 30\n" + "PRINT a(1); a(2); a(3)\n" + ); + // Expected: 10 20 30 + + // Test 16: LBOUND and UBOUND + runProgram("LBOUND/UBOUND", + "DIM a(5 TO 10) AS INTEGER\n" + "PRINT LBOUND(a); UBOUND(a)\n" + ); + // Expected: 5 10 + + // Test 17: User-defined TYPE + runProgram("TYPE", + "TYPE Point\n" + " x AS INTEGER\n" + " y AS INTEGER\n" + "END TYPE\n" + "DIM p AS Point\n" + "p.x = 10\n" + "p.y = 20\n" + "PRINT p.x; p.y\n" + ); + // Expected: 10 20 + + // Test 18: String array + runProgram("String Array", + "DIM names(3) AS STRING\n" + "names(0) = \"Alice\"\n" + "names(1) = \"Bob\"\n" + "names(2) = \"Charlie\"\n" + "DIM i AS INTEGER\n" + "FOR i = 0 TO 2\n" + " PRINT names(i)\n" + "NEXT i\n" + ); + // Expected: Alice / Bob / Charlie + + // Test 19: REDIM with PRESERVE + runProgram("REDIM PRESERVE", + "DIM a(3) AS INTEGER\n" + "a(0) = 100\n" + "a(1) = 200\n" + "a(2) = 300\n" + "REDIM PRESERVE a(5) AS INTEGER\n" + "a(4) = 500\n" + "PRINT a(0); a(1); a(2); a(4)\n" + ); + // Expected: 100 200 300 500 + + // Test 20: ERASE + runProgram("ERASE", + "DIM a(3) AS INTEGER\n" + "a(1) = 42\n" + "ERASE a\n" + "DIM b(2) AS INTEGER\n" + "b(1) = 99\n" + "PRINT b(1)\n" + ); + // Expected: 99 + + // Test 21: Array in FOR loop accumulation + runProgram("Array Accumulation", + "DIM sums(5) AS INTEGER\n" + "DIM i AS INTEGER\n" + "DIM j AS INTEGER\n" + "FOR i = 1 TO 5\n" + " sums(i) = 0\n" + " FOR j = 1 TO i\n" + " sums(i) = sums(i) + j\n" + " NEXT j\n" + "NEXT i\n" + "FOR i = 1 TO 5\n" + " PRINT sums(i);\n" + "NEXT i\n" + "PRINT\n" + ); + // Expected: 1 3 6 10 15 + + // ============================================================ + // Batch 1: Control Flow + // ============================================================ + + // Test: GOTO with forward jump + runProgram("GOTO Forward", + "PRINT \"before\"\n" + "GOTO skip\n" + "PRINT \"skipped\"\n" + "skip:\n" + "PRINT \"after\"\n" + ); + // Expected: before / after + + // Test: GOTO with backward jump + runProgram("GOTO Backward", + "DIM n AS INTEGER\n" + "n = 0\n" + "top:\n" + "n = n + 1\n" + "IF n < 5 THEN GOTO top\n" + "PRINT n\n" + ); + // Expected: 5 + + // Test: GOSUB/RETURN + runProgram("GOSUB/RETURN", + "DIM x AS INTEGER\n" + "x = 10\n" + "GOSUB dbl\n" + "PRINT x\n" + "END\n" + "dbl:\n" + "x = x * 2\n" + "RETURN\n" + ); + // Expected: 20 + + // Test: ON ERROR GOTO -- verify error handler catches errors + // and ERR returns the error number + runProgram("ON ERROR GOTO", + "ON ERROR GOTO handler\n" + "PRINT 10 / 0\n" + "END\n" + "handler:\n" + "PRINT \"caught\"\n" + "PRINT ERR\n" + ); + // Expected: caught / 11 + + // Test: Single-line IF + runProgram("Single-line IF", + "DIM x AS INTEGER\n" + "x = 42\n" + "IF x > 10 THEN PRINT \"big\"\n" + "IF x < 10 THEN PRINT \"small\"\n" + "IF x = 42 THEN PRINT \"exact\" ELSE PRINT \"nope\"\n" + ); + // Expected: big / exact + + // Test: Multi-statement line with : + runProgram("Multi-statement :", + "DIM x AS INTEGER\n" + "DIM y AS INTEGER\n" + "x = 1 : y = 2 : PRINT x + y\n" + ); + // Expected: 3 + + // ============================================================ + // Batch 2: Misc Features + // ============================================================ + + // Test: SWAP + runProgram("SWAP", + "DIM a AS INTEGER\n" + "DIM b AS INTEGER\n" + "a = 10\n" + "b = 20\n" + "SWAP a, b\n" + "PRINT a;\n" + "PRINT b\n" + ); + // Expected: 20 10 + + // Test: TIMER (returns number > 0) + runProgram("TIMER", + "DIM t AS DOUBLE\n" + "t = TIMER\n" + "IF t > 0 THEN PRINT \"ok\"\n" + ); + // Expected: ok + + // Test: DATE$ (returns non-empty string) + runProgram("DATE$", + "DIM d$ AS STRING\n" + "d$ = DATE$\n" + "IF LEN(d$) > 0 THEN PRINT \"ok\"\n" + ); + // Expected: ok + + // Test: TIME$ (returns non-empty string) + runProgram("TIME$", + "DIM t$ AS STRING\n" + "t$ = TIME$\n" + "IF LEN(t$) > 0 THEN PRINT \"ok\"\n" + ); + // Expected: ok + + // Test: ENVIRON$ + runProgram("ENVIRON$", + "DIM p$ AS STRING\n" + "p$ = ENVIRON$(\"HOME\")\n" + "IF LEN(p$) > 0 THEN PRINT \"ok\"\n" + ); + // Expected: ok + + // ============================================================ + // Batch 3: New features (DATA/READ/RESTORE, DIM SHARED, + // STATIC, DEF FN, OPTION BASE) + // ============================================================ + + // Test: DATA/READ/RESTORE + runProgram("DATA/READ/RESTORE", + "DATA 10, 20, \"hello\"\n" + "DIM a AS INTEGER\n" + "DIM b AS INTEGER\n" + "DIM c AS STRING\n" + "READ a, b, c\n" + "PRINT a; b;\n" + "PRINT c\n" + "RESTORE\n" + "READ a\n" + "PRINT a\n" + ); + // Expected: 10 20 hello / 10 + + // Test: DIM SHARED + runProgram("DIM SHARED", + "DIM SHARED count AS INTEGER\n" + "count = 0\n" + "CALL Increment\n" + "CALL Increment\n" + "CALL Increment\n" + "PRINT count\n" + "SUB Increment\n" + " count = count + 1\n" + "END SUB\n" + ); + // Expected: 3 + + // Test: STATIC + runProgram("STATIC", + "CALL Counter\n" + "CALL Counter\n" + "CALL Counter\n" + "SUB Counter\n" + " STATIC n AS INTEGER\n" + " n = n + 1\n" + " PRINT n;\n" + "END SUB\n" + "PRINT\n" + ); + // Expected: 1 2 3 + + // Test: DEF FN + runProgram("DEF FN", + "DEF FNdouble(x AS INTEGER) = x * 2\n" + "PRINT FNdouble(5)\n" + "PRINT FNdouble(21)\n" + ); + // Expected: 10 / 42 + + // Test: OPTION BASE + runProgram("OPTION BASE", + "OPTION BASE 1\n" + "DIM arr(3) AS INTEGER\n" + "arr(1) = 10\n" + "arr(3) = 30\n" + "PRINT arr(1); arr(3)\n" + ); + // Expected: 10 30 + + // Test: DATA with mixed types + runProgram("DATA mixed types", + "DATA 100, 3.14, \"world\"\n" + "DIM x AS INTEGER\n" + "DIM y AS DOUBLE\n" + "DIM z AS STRING\n" + "READ x, y, z\n" + "PRINT x\n" + "PRINT z\n" + ); + // Expected: 100 / world + + // Test: Multiple DATA statements scattered + runProgram("DATA scattered", + "DIM a AS INTEGER\n" + "DIM b AS INTEGER\n" + "DIM c AS INTEGER\n" + "DATA 1, 2\n" + "READ a, b\n" + "DATA 3\n" + "READ c\n" + "PRINT a; b; c\n" + ); + // Expected: 1 2 3 + + // Test: DIM SHARED with SUB modifying shared variable + runProgram("DIM SHARED multi", + "DIM SHARED total AS INTEGER\n" + "DIM SHARED msg AS STRING\n" + "total = 100\n" + "msg = \"start\"\n" + "CALL Modify\n" + "PRINT total\n" + "PRINT msg\n" + "SUB Modify\n" + " total = total + 50\n" + " msg = \"done\"\n" + "END SUB\n" + ); + // Expected: 150 / done + + // ============================================================ + // Batch 4: New I/O and string features + // ============================================================ + + // Test: WRITE # + runProgram("WRITE #", + "OPEN \"/tmp/dvxbasic_write.txt\" FOR OUTPUT AS #1\n" + "WRITE #1, 10, \"hello\", 3.14\n" + "CLOSE #1\n" + "OPEN \"/tmp/dvxbasic_write.txt\" FOR INPUT AS #1\n" + "DIM s AS STRING\n" + "LINE INPUT #1, s\n" + "PRINT s\n" + "CLOSE #1\n" + ); + // Expected: 10,"hello",3.14 + + // Test: FREEFILE + runProgram("FREEFILE", + "DIM f AS INTEGER\n" + "f = FREEFILE\n" + "PRINT f\n" + ); + // Expected: 1 + + // Test: PRINT USING numeric + runProgram("PRINT USING numeric", + "PRINT USING \"###.##\"; 3.14159\n" + ); + // Expected: 3.14 + + // Test: PRINT USING string + runProgram("PRINT USING string", + "PRINT USING \"!\"; \"Hello\"\n" + ); + // Expected: H + + // Test: SPC and TAB in PRINT + runProgram("SPC/TAB", + "PRINT SPC(3); \"hi\"\n" + ); + // Expected: hi + + // Test: Fixed-length string + runProgram("STRING * n", + "DIM s AS STRING * 5\n" + "s = \"Hi\"\n" + "PRINT \"[\" & s & \"]\"\n" + "PRINT LEN(s)\n" + ); + // Expected: [Hi ] / 5 + + // Test: MID$ statement + runProgram("MID$ statement", + "DIM s AS STRING\n" + "s = \"Hello World\"\n" + "MID$(s, 7, 5) = \"BASIC\"\n" + "PRINT s\n" + ); + // Expected: Hello BASIC + + // Test: OPEN FOR BINARY / GET / PUT + runProgram("BINARY GET/PUT", + "DIM v AS INTEGER\n" + "OPEN \"/tmp/dvxbasic_bin.tmp\" FOR BINARY AS #1\n" + "v = 12345\n" + "PUT #1, , v\n" + "SEEK #1, 1\n" + "DIM r AS INTEGER\n" + "GET #1, , r\n" + "PRINT r\n" + "CLOSE #1\n" + ); + // Expected: 12345 + + // Test: LOF and LOC + runProgram("LOF/LOC", + "OPEN \"/tmp/dvxbasic_lof.txt\" FOR OUTPUT AS #1\n" + "PRINT #1, \"test\"\n" + "CLOSE #1\n" + "OPEN \"/tmp/dvxbasic_lof.txt\" FOR INPUT AS #1\n" + "DIM sz AS LONG\n" + "sz = LOF(1)\n" + "IF sz > 0 THEN PRINT \"ok\"\n" + "CLOSE #1\n" + ); + // Expected: ok + + // Test: INPUT$(n, #channel) + runProgram("INPUT$", + "OPEN \"/tmp/dvxbasic_inp.txt\" FOR OUTPUT AS #1\n" + "PRINT #1, \"ABCDEF\"\n" + "CLOSE #1\n" + "OPEN \"/tmp/dvxbasic_inp.txt\" FOR INPUT AS #1\n" + "DIM s AS STRING\n" + "s = INPUT$(3, #1)\n" + "PRINT s\n" + "CLOSE #1\n" + ); + // Expected: ABC + + // Test: SEEK function form + runProgram("SEEK function", + "OPEN \"/tmp/dvxbasic_seek.txt\" FOR OUTPUT AS #1\n" + "PRINT #1, \"test\"\n" + "CLOSE #1\n" + "OPEN \"/tmp/dvxbasic_seek.txt\" FOR BINARY AS #1\n" + "DIM p AS LONG\n" + "p = SEEK(1)\n" + "IF p = 1 THEN PRINT \"ok\"\n" + "CLOSE #1\n" + ); + // Expected: ok + + // Test: ON n GOTO + runProgram("ON n GOTO", + "DIM n AS INTEGER\n" + "n = 2\n" + "ON n GOTO ten, twenty, thirty\n" + "PRINT \"none\"\n" + "GOTO done\n" + "ten:\n" + "PRINT \"ten\"\n" + "GOTO done\n" + "twenty:\n" + "PRINT \"twenty\"\n" + "GOTO done\n" + "thirty:\n" + "PRINT \"thirty\"\n" + "done:\n" + ); + // Expected: twenty + + // Test: ON n GOTO (no match) + runProgram("ON n GOTO no match", + "DIM n AS INTEGER\n" + "n = 5\n" + "ON n GOTO aa, bb\n" + "PRINT \"fallthrough\"\n" + "GOTO done2\n" + "aa:\n" + "PRINT \"aa\"\n" + "GOTO done2\n" + "bb:\n" + "PRINT \"bb\"\n" + "done2:\n" + ); + // Expected: fallthrough + + // Test: ON n GOSUB + runProgram("ON n GOSUB", + "DIM n AS INTEGER\n" + "DIM result AS INTEGER\n" + "result = 0\n" + "n = 2\n" + "ON n GOSUB addTen, addTwenty, addThirty\n" + "PRINT result\n" + "GOTO endProg\n" + "addTen:\n" + "result = result + 10\n" + "RETURN\n" + "addTwenty:\n" + "result = result + 20\n" + "RETURN\n" + "addThirty:\n" + "result = result + 30\n" + "RETURN\n" + "endProg:\n" + ); + // Expected: 20 + + // Test: FORMAT$ + runProgram("FORMAT$", + "PRINT FORMAT$(1234.5, \"#,##0.00\")\n" + "PRINT FORMAT$(0.5, \"0.00\")\n" + "PRINT FORMAT$(-42, \"+#0\")\n" + "PRINT FORMAT$(0.75, \"percent\")\n" + ); + // Expected: 1,234.50\n0.50\n-42\n75% + + // Test: SHELL as function expression + runProgram("SHELL function", + "DIM r AS INTEGER\n" + "r = SHELL(\"echo hello > /dev/null\")\n" + "IF r = 0 THEN PRINT \"ok\"\n" + ); + // Expected: ok + + // Test: SHELL as statement + runProgram("SHELL statement", + "SHELL \"echo hello > /dev/null\"\n" + "PRINT \"done\"\n" + ); + // Expected: done + + // Test: OPTION COMPARE TEXT + runProgram("OPTION COMPARE TEXT", + "OPTION COMPARE TEXT\n" + "IF \"hello\" = \"HELLO\" THEN\n" + " PRINT \"equal\"\n" + "ELSE\n" + " PRINT \"not equal\"\n" + "END IF\n" + "IF \"abc\" < \"XYZ\" THEN\n" + " PRINT \"less\"\n" + "END IF\n" + ); + // Expected: equal\nless + + // Test: OPTION COMPARE BINARY (default) + runProgram("OPTION COMPARE BINARY", + "OPTION COMPARE BINARY\n" + "IF \"hello\" = \"HELLO\" THEN\n" + " PRINT \"equal\"\n" + "ELSE\n" + " PRINT \"not equal\"\n" + "END IF\n" + ); + // Expected: not equal + + // Test: EQV operator + runProgram("EQV operator", + "PRINT -1 EQV -1\n" + "PRINT 0 EQV 0\n" + "PRINT -1 EQV 0\n" + "PRINT 0 EQV -1\n" + ); + // Expected: -1\n-1\n0\n0 + + // Test: IMP operator + runProgram("IMP operator", + "PRINT 0 IMP -1\n" + "PRINT -1 IMP 0\n" + "PRINT -1 IMP -1\n" + "PRINT 0 IMP 0\n" + ); + // Expected: -1\n0\n-1\n-1 + + // Test: PRINT USING advanced patterns + runProgram("PRINT USING advanced", + "PRINT USING \"**#,##0.00\"; 1234.5\n" + "PRINT USING \"$$#,##0.00\"; 42.5\n" + "PRINT USING \"+###.##\"; 42.5\n" + "PRINT USING \"+###.##\"; -42.5\n" + "PRINT USING \"###.##-\"; -42.5\n" + "PRINT USING \"###.##-\"; 42.5\n" + "PRINT USING \"#.##^^^^\"; 1234.5\n" + ); + + // Test: DEFINT + runProgram("DEFINT", + "DEFINT A-Z\n" + "a = 42\n" + "b = 3.7\n" + "PRINT a; b\n" + ); + + // Test: DEFSTR + runProgram("DEFSTR", + "DEFSTR S\n" + "s = \"hello\"\n" + "PRINT s\n" + ); + + // Test: DEFINT range + runProgram("DEFINT range", + "DEFINT I-N\n" + "i = 10\n" + "j = 20\n" + "x = 3.14\n" + "PRINT i; j; x\n" + ); + + // Test: OPTION EXPLICIT success + runProgram("OPTION EXPLICIT ok", + "OPTION EXPLICIT\n" + "DIM x AS INTEGER\n" + "x = 42\n" + "PRINT x\n" + ); + + // Test: OPTION EXPLICIT failure (should error) + { + printf("=== OPTION EXPLICIT error ===\n"); + const char *src = + "OPTION EXPLICIT\n" + "x = 42\n"; + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + bool ok = basParse(&parser); + if (!ok) { + printf("Correctly caught: %s\n", parser.error); + } else { + printf("ERROR: should have failed\n"); + } + basParserFree(&parser); + printf("\n"); + } + + printf("All tests complete.\n"); + return 0; +} diff --git a/dvxbasic/test_lex b/dvxbasic/test_lex new file mode 100755 index 0000000000000000000000000000000000000000..dea2aa7c2061d1643c8db6c676ba91ee4eb171e7 GIT binary patch literal 20640 zcmeHveRLGpmFH_oU?GI64FW5eK!e4XZDfhBNLXN%)Qzf9>%&sZ;266Rtrn6kBt>ck zu(91(!cj-GhfKUPtY=Qf$;62#$?S6CjDnqvElhyG#Ex*riPu?Av_5f6TegG00cYs_ z-Ku+At%{iJ$(i|M&!OP;efRh7eINII)UDEc^`OVMG0ovna;2*cin!6MEu@SS9yd4w zQl^U44E(y(0yPzUhKX6}WfnoIb-j`{k!CWU4oZ4?6q$t1vtT7uYDkpyE_WT#tQ4Wj zY4M~tg`&*c=?k+hIiZrCEU(Z?S2lX!Dhn5yszg7V9F;!X9sL*f2gs~Y_TEyZdKT6BxbdujTbbsv~KNx-d!w*-?KEC(VA@8Bn zJtRZ*CLNNYi}K?sWbzcxMj#o>9{Wt3FuKl!UfgNLCjSiro;gWU`DO5@fV(D%OzLS;vbKoun`?UnXWb%YfhW{II7lQp-4PY{P8efa-!o&vxJ>jO{J%M2BJ%N^{ zwsrtr-EAG=7D7}{xI5h0Y>{>%?V%1x1jBo}LV@jp_Rb(U44ok5(j5-$Y-%&pcQ*xE z+B%xr+wKif2^}d}k~EF0wW`wREw2cyELfF{txm>P7OY9eSB=A~3RbIt*IylI4t0li zwDp8T-Tvx|_RfxwziE3rm9=AMX9rg%V2MsDNTVzrqbdXcrkFGhzf;XMh0Njr+6^l6 zYF1m@3>txZ8Ot^KDe7V7d*`-IqZR2O^FR2?UtzeWtG{JF{25A5Q{M(pm#mAjE)8?1 z74y0&c!cpX8-8t}l~0`wKhO9!8=hdi%Z8^FS@}e4_%y~3+VHCxm-}O;rLL}F{IE@a z9^=C{d?Di_Hhd}Lqc(g6qn4KHTgWy5vG3vIZ;c$p2aX1vaZ ze=7F3;eM9yvf*18kJ#|L7(Zykw=@2@4R2-qunphI_^=J{VSL1f?_zw^hDR7bXT$p$ zSDPl<|I3U!ZTMd>?y}*xGG1uIA7#AEhCjx5oelp6xDL&Tf_}wBc)5zRZTNW4z9W>x^%+;gyVc z*>E4@5gY!n*x!aXu>9jTd<)};ZTOvx58Lnqod1Xo53>BI4R2-qoDJW}xZ>vqS^vX~ zJ8k%VjJs_3KE?}e_`{5s+3*95*V*u|Fuu)(|25;8brZ*l@WZthPCjm`|NGMvTzqYk z97)0Dk&5tFQt&JS_!>>YvjqelV4U(GpH;4dDLDD*ave&+&F4!_csvECHpum43NC#^ zX@^trX(9}IBn6+Ig8yelKhWl1H2U7oGzO;}y+tWw|8O`hF=F)nB=eYAL}E=n;KX7- zezgT3s)7bYDD!yp=jyv_O`klwl&LE=|XbbwGOC4s~tLPnX^vqpG^mU`}x995oo}%HR zQ$}q4LdYg&{|GM{J{(s8egTLPoAN%9lJnu&7#d%rIEKcw#Hh9)LRlYU3i(r}{F>*9 zEWcz#&lyMGy~Q|sKHYGfFwR^G=OTlhoIz${)apmkr|}b6zaPn}_l8EJZ~Z2U!g%3q zV{m;7n6nrDJ&`!ujJlqfawmAlozP8|Px|p)NaXquuW#0G(;IbvV}lX>6oY%&U}v^_ zurqV8)~T1gqdk%dllcZe=L^13?f6jFhBER?UAp$@(b8$!qtEM^XSDtEA>kgpFSj(H z?f(eNMy%1H$0{AshHO1jnxCchFNQ$0BG+B=*8Pn}@Ohu(+}Jf{|3b;=8+OEYIifw3 z9A2vp6_!OVxx!iaou=)-gL?jX$|_Qv)jRSm&7Hmqr-QUCmEIT7zUV#K(JH4qdS7m| zY95H|WM3hP;yO`LsN85rY%>5fqAFIpL>sy$auM}9gS8`aafa4kiCRZ4F4g*Xo5&)q ze-oA)_W(xOfImOO7j4eZjQaBD8PUD@&a+ctVr?j#pW)VqAYlx`KK}enBl^Df!_{3a zpPW#c+Wv2%xX8r<14Gt{(K=7-Z#R)8T7NpsK~fpL(;^pPv#(4tymqjp>-c;?9S|kjS^6w0;un}12#Ycpse2Er>XIl zonSo4BK*%r@Ru+_-w8+Az}{JfV+Ua5EYs98wEageB@(f_9fMU)RdN9YHO;WBG}>38XYV^({t>Y1Y4s$KU$St5hZ&JhY+JbKJ3V2iaHa zzs<_4@1z3*v}_p4jbw=N{OapMl6|WN&`PT#r`% zFM8yHq4j$)M9o2??H_>w+|i0`9XV3_v(aq*IWijB9V>s0=0WMr+LykCac0D3OvhVc zZD`mCX6!Q@Ki!ZRg}6D9ARS+XUUx^&=p`>aaJ3P9uR4~oR*w`HX#HQopd9SU#`5Pb zc~^^litLTxC2#D;4cbt(a#KxXw~p$OPv`1d`RTs6J95FHJ$x?}8GQbPJH1dvze;n^ z=lDb$x~g=s*8e1KRG54Hv_zCH)cWV*4qbYk);|-}(|=0qe}c-0-B(!j@!5t8iNq1) zrQX-Bu2>dtMhXpQv)b{yQD+`kz|4lnzX8k$9yfw7Vs$y;o{r+5qT;n7XYAhBFe}o( zI3;p%VeblSD4OGY59WAsFk(^+_@@^d!4p>HP3`?3YmZbSmskxlKdKF(S)~iZnSE!k zE6va% z#QswctTKWZie9qpfSS!JbjJ+zNg4X&0`4CHn`9+g;{b zdgN*p@(j{xt)sXleFA2;uXxbeYUwyrbSOS{nx?zv4Gv?BMvB*K{Tpz*f&2zbp4=uW zgW_Y4L0&ts01bdpsU<{zc@ZIs$Gc%AB;RZ$lP5086puGp$?L3SWR*;&czm^$e3O-| zr6f~4KHExOVI|K=!;rB)q|& zm~+jXvUdgC(Z8oDheaoT0u!2ywEP|8P*u#g*fBPfJde$mqt}pYl>9X8f%EB~ntqlsCVHSS@l~yT7M13OY$y3wt9$Kh@Q{(Xnh% zI5u~KL$^z%;d$++NlQ&SS&?g2sS-SmBiwQwM)V{WuluuLjlF9fMyzzV_QP9RJ~>Ga zO%?7MwD`f9$cx?Uz}*=Q_y;V))MFm_G4Z-w&S}-JZb9TT7 ziZ7=DlJq?8_>4yI zw7&dhm-8MQ)67Xi zh9yg{!w8s*hFGiATUsm~4M2Akoq}UHHaoU!&p3K5eY&!mTAf(Hna74DPXfS2rI7Jtc9v?;bT5F}R@b z(y~)UQ#XtIQvZ=T(OzZN?CW>~`U$m4i*15c$ffw49&2Eyemmr=gQqZf$_K*JEq|gK z@_O`C{7K|uKHpNc&0!IL?hoj4T5_#LWej#o4vy4C<;(9T67f?XPY`}JRXG0YhZrtB z+0l!-HGJ*_Z@Cm$W>Jl-XN9NXr>Z0LGtdCHw*gPk zc~7IO&AuKVGc?-fBnBFa%;h|ufh?0nzm7G1Jj3{}CJbwOO2(}${tLMBBxKc=Ub&2O z-@(E;&dryZWQo?{{zr+H0T1GiYy?kQGxJBLt2RQ#r!JfmnwLk-qo4RZ8WXr%y76cR z$3N_~ERVU5)zB=5Ufpq9^reU8Xl$=8U&u1jzF& zy`LGZ%)=`jnpoERyuL)wu4|0g?0h(A(erq!d&qE%;Hi%84}iaZ9r?-p{tPn%lhbKB z`X58TWL@vu#vw@cK*~5ent?ZQX~%FYp9QanXaZgh(cd$Do$1R=Pci*xriYn+lj$L* z2be~fhMBf9-O99%shepr(;Jz(nC3FgWcmlJ1ys*C(^r{3XQMx6zJ&BF`#R69>aK!>wEO5mFS1d581svF2QCIJ+@w*loq4xI98(o__yW5)= zF3M7Rja&KJIzq0+%{RI#I@>|lxfb`R#t!T<-HF|-;ZU;+TVUHdcDUNxfHk!vEwsBU z6r_}3Yg2anS*W$nDHcySe0g=XXpGSFokfI;&M&+qRs3&DleLaO*{BH63)kdA9HiF&QSX1Hm z*4C&>k6%^RHZ+#w*I%m)uiK-%HFb@C{Q5nWo_ghNP`>ioM(Pa6`n)wB<*Ti!#7}LV zs;+gb>W0el+AXTav&FA!YW=DfEyItQeQl$k@~W%%Ge02e)l9sf_o#Z0 z+Y8=9p`o#w0DohBjcV|Cs_^6URQN#<3^d;jQoC6-cxx)r@CKj9Q>PmI<_`u^rE1u$ z*D1e&O8C9iXn?=I(WCsg*LhT<)sDs*U#;#|jWvF+Pi>}d*lc)_)n>2XP@C($evjG$ z8=05C^PW(LIr3ulu4RjRmb=2C-Qo317xyg1(~w#jPb41wofUp|)}sFi`UpZN zLKf2NK#LH@5N0BX{GXy9yqQS+E5i2>Zbd$OLI0G_e?|H>F84;zx86%6-bc8I@H)b) z2rnU=MtC0K8HDd6dB>dbI$`ptAUf&L(L@YO_O0Mv=UoE!#qf&P?m&{5F3|D8y5ftJ0N zNVuSU5bs%j3_1!r0(uVgx0ER^de1|bc~A)4ddn3K0jo4zYEqHdX6a{a3OMFf-nR)j5(GZhKMMR*vY4mj0Ou2Mt} zAUp~DDFQd4TV2Yko`0!Lcr?~H`%A9TK&BdIRFg^c_37zp>89A`oK`(&n{_s#6*m3Z z+jy~us?kL@*$XW5okW8EJV_T}Wcv(Y8$kbmy5wAjoWqcF7xTFha^9kYc{yjXfO$Dz z(aF4=t8g(d=P4F4FXt%C!3IpuPvC8zdC9p68n1N8d5OgqtK^)7Ikthx`G}>QF6Sba zF)!yKmNPHsAg*U#&OhA1yqtTuk$E}qu!4Cx=YUND<|XGFZem`}H54*0=NXEamvam& znV0hmtC*K_3+7w}Cg&B_aJrmRSj)VePxu`3axS5mc{z_z!n~YASjW7aKe(Cs$GD<2 zuj!KW2J0)r^PBC*Y_^wWnX=OdD&0Djd|He-^9G^pWn{B?3)iVFZ<=2 znU{U?A?9U&d<*lkFMbE}vLC*cdD#cw#=Pu*znl33+@5=wm;LVT%*#Iaoy-gA;fO5% z-OOi;0No^T&9I0X<^}CD%op;2y_We*rgNFfxw(0^^p7qj%%%5mx|J}6)14fd%KR&w zPX0Ar=Pjbln=s;e%+rRQ)QiPJEIn`2}>*P9D1GuXS`eHH!*9z!i~p4RD0~ zBD$n}x$lVFr*Qcx9dk%nwuOKk+9b3xh4s zLhkP}rgcoWG3{a+VS13MEt@}^m2(%B6&34T%Nn=$c7%IfMXL%{6%?*m+iS8bA6U7% zpm23T(Q;1sj|!u4ouS?{ecMzy>OVSVdVmLtixX#oP9{(NI~l$lcp5w@=ST``MAx*=O~#+R;p58LWJ@ew&nj-Q+{^x9IMEGACbxG z|BNYrjVd!N$ugdQmuv4&O!-_jAUAGH&HSA#|CdbpJY^qmzcw{yDLHp9*}QAYe(2BJq2|$)bf8lEDd= zfIBe`^KAB^Z5uO@GJ3ToxP;|t$35W@uD?7-IDu0>GM-sn&13m4mY4N%IpZ$Q|0z}| zFmcfyrEUf_$8+<4@_0iT(MyP$I%quAdNJ`u9G zhG2KNsGt)k-J7|sk^&rPXI44yZ5M;?xvlgKy&ZT zoqM2?!UZ51o*;^vg}PCpo~qEEU7g*{REv%EdbKBjH(>#s(RbfoqgQ(?0OKPN0gu6{ zhPz$`Dt)!(x-U?>aU)*G1^hZ*Jwju`2L;NKpAn#M1K2-CFrNA!e>lMU&_Pm~K0go$ zH8+Kul-YRx^ufe*+m{oPiL2H2rXC+P+Y4i$PLz-JO#(0zK2Ts5+n&@kiw!jQbOu_R zI-2Qw2HsjIHn(*IdV506s8UBWWp2JDtU(tbzwJFeoVS!(7Yf~@_)84qA6`fnLf=pj z(Hgv0L>~Btg7gM`Tw&5LCg7_NDPMI+=4MuzIvv@hd=?^^zDlWro;^FmP1`~7H3^fp zN{n8WcXz3Rj?QqXU`I!9L05NYSExI@Ck5Kx+lJ4Qv^6uNmwQ*xus6k9n|fMRLGzvt z6k*YDx0Uk6P1a6D&n~LP1qPZ(UUZ4Sq;> znxmm0)XKxXwHdhyXXRmydrL#&q(y}y1G9onJKK=6l^42B6=3%5#H5;RuKyVr`Nvp7 zanH6c`}c;#KjvgpR^mjjLm;12^e^{a9g?L>)>VsBik`bu_3hu+;pJGw;VVqMyf%ID zn}oV1ivN*5J=bCHU8mGv{C=TQKD)nvRR1RElg}^u@?0Ran2d{y+DK7pzxd~Ez{u4T zeR<9hD$f-VF)z+eSoH8EUY-MlZsU^d^=DYc5TQO1 z{Q)j0Nx8vNzPp*-gZeUfp~r)N96{?~z#O(g&1eUa~v^85!8^RkbhzXh7A z?_xcn5lb!kE4N*t-?r&L&Iv+APBIW)=wX}wVNMX*C5lW%Pw3M&eeqL-eoY=IZTgb! zb2fc>jutA<*CKDX-*MKL{ujSZ=s|ga<_x6&B)=DI`r>yBrRR^-%U(YD>(qZz{#kyp zCzK9lre5~)-#}sxf;^YYdjt6%mR(+Wq3=OgOVO9-q2gkIxJ1t4#(BYM=Q~wV%1=J; zu)aL^h`viAOtYX&V_)>;c}BjEc8=Rm^Or6uSH{nGfu)u&?|m1Yy3y_h0cO< zs($jhpv=;dawG@Q6aF($CQRxt{;{*nlCaBDS*h22F267pL|pV&*l^KLr80I>Ma#-8 xF2Vzjo;ztONqzFHWb^M(Qg?<-GVHdvr)-%^xgtxC6_e@rR9X^CZ3;G4{a+j{$6Wvb literal 0 HcmV?d00001 diff --git a/dvxbasic/test_lex.c b/dvxbasic/test_lex.c new file mode 100644 index 0000000..2dc2452 --- /dev/null +++ b/dvxbasic/test_lex.c @@ -0,0 +1,24 @@ +// test_lex.c -- Dump lexer tokens +// gcc -O2 -w -o test_lex test_lex.c compiler/lexer.c -lm + +#include "compiler/lexer.h" +#include +#include + +int main(void) { + const char *src = "PRINT \"Hello, World!\"\n"; + BasLexerT lex; + basLexerInit(&lex, src, (int32_t)strlen(src)); + + for (int i = 0; i < 20; i++) { + printf("Token %d: type=%d (%s) text='%s'\n", i, lex.token.type, basTokenName(lex.token.type), lex.token.text); + + if (lex.token.type == TOK_EOF) { + break; + } + + basLexerNext(&lex); + } + + return 0; +} diff --git a/dvxbasic/test_quick b/dvxbasic/test_quick new file mode 100755 index 0000000000000000000000000000000000000000..f70510c5efa7da35d649c399c9d7e4c5cef49ae7 GIT binary patch literal 109224 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 BS#1CS literal 0 HcmV?d00001 diff --git a/dvxbasic/test_quick.c b/dvxbasic/test_quick.c new file mode 100644 index 0000000..a7bdc8c --- /dev/null +++ b/dvxbasic/test_quick.c @@ -0,0 +1,64 @@ +// test_quick.c -- Quick single-program test +// gcc -O2 -Wall -o test_quick test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c -lm + +#include "compiler/parser.h" +#include "runtime/vm.h" +#include "runtime/values.h" +#include +#include + +int main(void) { + basStringSystemInit(); + + const char *source = "PRINT \"Hello, World!\"\n"; + printf("Source: [%s]\n", source); + printf("Source len: %d\n", (int)strlen(source)); + + int32_t len = (int32_t)strlen(source); + BasParserT parser; + basParserInit(&parser, source, len); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n", parser.error); + basParserFree(&parser); + return 1; + } + + printf("Compiled OK (%d bytes of p-code)\n", parser.cg.codeLen); + + // Dump p-code + for (int i = 0; i < parser.cg.codeLen; i++) { + printf("%02X ", parser.cg.code[i]); + } + printf("\n"); + + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount; + vm->callDepth = 1; + + // Step limit + int steps = 0; + vm->running = true; + + while (vm->running && steps < 1000) { + BasVmResultE r = basVmStep(vm); + steps++; + + if (r != BAS_VM_OK) { + printf("[Result: %d after %d steps: %s]\n", r, steps, basVmGetError(vm)); + break; + } + } + + if (steps >= 1000) { + printf("[TIMEOUT after %d steps, PC=%d]\n", steps, vm->pc); + } + + basVmDestroy(vm); + basModuleFree(mod); + return 0; +} diff --git a/dvxbasic/test_vm b/dvxbasic/test_vm new file mode 100755 index 0000000000000000000000000000000000000000..fedebc7f32016e57b5d8549ca5fd7f01bf09b8c6 GIT binary patch literal 44736 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 +#include + +// ============================================================ +// Helper: emit bytes into a code buffer +// ============================================================ + +static uint8_t sCode[4096]; +static int32_t sCodeLen = 0; + +static void emit8(uint8_t b) { + sCode[sCodeLen++] = b; +} + + +static void emit16(int16_t v) { + memcpy(&sCode[sCodeLen], &v, 2); + sCodeLen += 2; +} + + +static void emitU16(uint16_t v) { + memcpy(&sCode[sCodeLen], &v, 2); + sCodeLen += 2; +} + + +// ============================================================ +// Test 1: PRINT "Hello, World!" +// ============================================================ + +static void test1(void) { + printf("--- Test 1: PRINT \"Hello, World!\" ---\n"); + + sCodeLen = 0; + + // String constant pool + BasStringT *consts[1]; + consts[0] = basStringNew("Hello, World!", 13); + + // Code: PUSH_STR 0; PRINT; PRINT_NL; HALT + emit8(OP_PUSH_STR); + emitU16(0); + emit8(OP_PRINT); + emit8(OP_PRINT_NL); + emit8(OP_HALT); + + BasModuleT module; + memset(&module, 0, sizeof(module)); + module.code = sCode; + module.codeLen = sCodeLen; + module.constants = consts; + module.constCount = 1; + module.entryPoint = 0; + + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, &module); + BasVmResultE result = basVmRun(vm); + printf("Result: %d (expected %d = HALTED)\n\n", result, BAS_VM_HALTED); + basVmDestroy(vm); + basStringUnref(consts[0]); +} + + +// ============================================================ +// Test 2: Arithmetic: PRINT 2 + 3 * 4 +// ============================================================ + +static void test2(void) { + printf("--- Test 2: PRINT 2 + 3 * 4 (expect 14) ---\n"); + + sCodeLen = 0; + + // Code: PUSH 3; PUSH 4; MUL; PUSH 2; ADD; PRINT; PRINT_NL; HALT + emit8(OP_PUSH_INT16); + emit16(3); + emit8(OP_PUSH_INT16); + emit16(4); + emit8(OP_MUL_INT); + emit8(OP_PUSH_INT16); + emit16(2); + emit8(OP_ADD_INT); + emit8(OP_PRINT); + emit8(OP_PRINT_NL); + emit8(OP_HALT); + + BasModuleT module; + memset(&module, 0, sizeof(module)); + module.code = sCode; + module.codeLen = sCodeLen; + module.entryPoint = 0; + + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, &module); + basVmRun(vm); + basVmDestroy(vm); + printf("\n"); +} + + +// ============================================================ +// Test 3: String concatenation +// ============================================================ + +static void test3(void) { + printf("--- Test 3: PRINT \"Hello\" & \" \" & \"BASIC\" ---\n"); + + sCodeLen = 0; + + BasStringT *consts[3]; + consts[0] = basStringNew("Hello", 5); + consts[1] = basStringNew(" ", 1); + consts[2] = basStringNew("BASIC", 5); + + // Code: PUSH consts[0]; PUSH consts[1]; CONCAT; PUSH consts[2]; CONCAT; PRINT; PRINT_NL; HALT + emit8(OP_PUSH_STR); emitU16(0); + emit8(OP_PUSH_STR); emitU16(1); + emit8(OP_STR_CONCAT); + emit8(OP_PUSH_STR); emitU16(2); + emit8(OP_STR_CONCAT); + emit8(OP_PRINT); + emit8(OP_PRINT_NL); + emit8(OP_HALT); + + BasModuleT module; + memset(&module, 0, sizeof(module)); + module.code = sCode; + module.codeLen = sCodeLen; + module.constants = consts; + module.constCount = 3; + module.entryPoint = 0; + + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, &module); + basVmRun(vm); + basVmDestroy(vm); + printf("\n"); + + basStringUnref(consts[0]); + basStringUnref(consts[1]); + basStringUnref(consts[2]); +} + + +// ============================================================ +// Test 4: FOR loop -- PRINT 1 to 5 +// ============================================================ + +static void test4(void) { + printf("--- Test 4: FOR i = 1 TO 5: PRINT i: NEXT ---\n"); + + sCodeLen = 0; + + // We need a call frame with at least 1 local (the loop variable) + // For module-level code, we use callStack[0] as implicit frame + + // Setup: store initial value in local 0 + // PUSH 1; STORE_LOCAL 0 -- i = 1 + emit8(OP_PUSH_INT16); emit16(1); + emit8(OP_STORE_LOCAL); emitU16(0); + + // Push limit and step for FOR_INIT + // PUSH 5 (limit); PUSH 1 (step) + emit8(OP_PUSH_INT16); emit16(5); + emit8(OP_PUSH_INT16); emit16(1); + emit8(OP_FOR_INIT); emitU16(0); emit8(1); // isLocal=1 + + // Loop body start (record PC for FOR_NEXT offset) + int32_t loopBody = sCodeLen; + + // LOAD_LOCAL 0; PRINT; PRINT " " + emit8(OP_LOAD_LOCAL); emitU16(0); + emit8(OP_PRINT); + + // FOR_NEXT: increment i, test, jump back + emit8(OP_FOR_NEXT); + emitU16(0); // local index + emit8(1); // isLocal=1 + int16_t offset = (int16_t)(loopBody - (sCodeLen + 2)); + emit16(offset); + + // After loop + emit8(OP_PRINT_NL); + emit8(OP_HALT); + + BasModuleT module; + memset(&module, 0, sizeof(module)); + module.code = sCode; + module.codeLen = sCodeLen; + module.entryPoint = 0; + + BasVmT *vm = basVmCreate(); + + // Initialize the implicit main frame with 1 local + vm->callStack[0].localCount = 1; + vm->callDepth = 1; + + basVmLoadModule(vm, &module); + basVmRun(vm); + basVmDestroy(vm); + printf("\n"); +} + + +// ============================================================ +// main +// ============================================================ + +int main(void) { + printf("DVX BASIC VM Tests\n"); + printf("==================\n\n"); + + basStringSystemInit(); + + test1(); + test2(); + test3(); + test4(); + + printf("All tests complete.\n"); + return 0; +}