DVX_GUI/src/apps/kpunch/dvxbasic/compiler/parser.c
2026-04-22 20:33:49 -05:00

6449 lines
192 KiB
C

// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// 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 "thirdparty/stb_ds_wrap.h"
#include <ctype.h>
#include <stdio.h>
#include <string.h>
// ============================================================
// Forward jump list (for EXIT FOR / EXIT DO backpatching)
// ============================================================
typedef struct {
int32_t *patchAddr; // stb_ds dynamic array
} 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},
{"OCT$", OP_STR_OCT, 1, 1, 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},
// Conversion functions
{"CBOOL", OP_CONV_BOOL, 1, 1, BAS_TYPE_BOOLEAN},
{"CDBL", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_DOUBLE},
{"CINT", OP_CONV_FLT_INT, 1, 1, BAS_TYPE_INTEGER},
{"CLNG", OP_CONV_INT_LONG, 1, 1, BAS_TYPE_LONG},
{"CSNG", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_SINGLE},
{"CSTR", OP_CONV_INT_STR, 1, 1, BAS_TYPE_STRING},
// 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},
{"GETBLUE", OP_GET_BLUE, 1, 1, BAS_TYPE_INTEGER},
{"GETGREEN", OP_GET_GREEN, 1, 1, BAS_TYPE_INTEGER},
{"GETRED", OP_GET_RED, 1, 1, BAS_TYPE_INTEGER},
{"INT", OP_MATH_INT, 1, 1, BAS_TYPE_INTEGER},
{"LOG", OP_MATH_LOG, 1, 1, BAS_TYPE_DOUBLE},
{"RGB", OP_RGB, 3, 3, BAS_TYPE_LONG},
{"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}
};
// ============================================================
// Exit list helpers
// ============================================================
static ExitListT exitForList;
static ExitListT exitDoList;
static ExitListT exitSubList;
static ExitListT exitFuncList;
// ============================================================
// Module-scope declarations
// ============================================================
// ============================================================
// Prototypes
// ============================================================
static void addPredefConst(BasParserT *p, const char *name, int32_t val);
static void addPredefConsts(BasParserT *p);
static void advance(BasParserT *p);
bool basParse(BasParserT *p);
BasModuleT *basParserBuildModule(BasParserT *p);
void basParserFree(BasParserT *p);
void basParserInit(BasParserT *p, const char *source, int32_t sourceLen);
static bool check(BasParserT *p, BasTokenTypeE type);
static bool checkCtrlArrayAccess(BasParserT *p);
static bool checkKeyword(BasParserT *p, const char *kw);
static bool checkKeywordText(const char *text, const char *kw);
static void collectDebugGlobals(BasParserT *p);
static void collectDebugLocals(BasParserT *p, int32_t procIndex);
static void emitByRefArg(BasParserT *p);
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 void emitUdtInit(BasParserT *p, int32_t udtTypeId);
static BasSymbolT *ensureVariable(BasParserT *p, const char *name);
static void error(BasParserT *p, const char *msg);
static void errorExpected(BasParserT *p, const char *what);
static void exitListAdd(ExitListT *el, int32_t addr);
static void exitListInit(ExitListT *el);
static void exitListPatch(ExitListT *el, BasParserT *p);
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 BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId);
static bool match(BasParserT *p, BasTokenTypeE type);
static void parseAddExpr(BasParserT *p);
static void parseAndExpr(BasParserT *p);
static void parseAssignOrCall(BasParserT *p);
static void parseBeginForm(BasParserT *p);
static void parseChDir(BasParserT *p);
static void parseChDrive(BasParserT *p);
static void parseClose(BasParserT *p);
static void parseCompareExpr(BasParserT *p);
static void parseConcatExpr(BasParserT *p);
static void parseConst(BasParserT *p);
static void parseData(BasParserT *p);
static void parseDeclare(BasParserT *p);
static void parseDeclareLibrary(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 parseEndForm(BasParserT *p);
static void parseEqvExpr(BasParserT *p);
static void parseErase(BasParserT *p);
static void parseExit(BasParserT *p);
static void parseExpression(BasParserT *p);
static void parseFileCopy(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 parseImpExpr(BasParserT *p);
static void parseInput(BasParserT *p);
static void parseKill(BasParserT *p);
static void parseLineInput(BasParserT *p);
static void parseMkDir(BasParserT *p);
static void parseModule(BasParserT *p);
static void prescanSignatures(BasParserT *p);
static void parseMulExpr(BasParserT *p);
static void parseName(BasParserT *p);
static void parseNotExpr(BasParserT *p);
static void parseOn(BasParserT *p);
static void parseOnError(BasParserT *p);
static void parseOpen(BasParserT *p);
static void parseOption(BasParserT *p);
static void parseOrExpr(BasParserT *p);
static void parsePowExpr(BasParserT *p);
static void parsePrimary(BasParserT *p);
static void parsePrint(BasParserT *p);
static void parsePut(BasParserT *p);
static void parseRead(BasParserT *p);
static void parseRedim(BasParserT *p);
static void parseRemoveControl(BasParserT *p);
static void parseRestore(BasParserT *p);
static void parseResume(BasParserT *p);
static void parseRmDir(BasParserT *p);
static void parseSeek(BasParserT *p);
static void parseSelectCase(BasParserT *p);
static void parseSetAttr(BasParserT *p);
static void parseSetEvent(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 parseUnaryExpr(BasParserT *p);
static void parseWhile(BasParserT *p);
static void parseWrite(BasParserT *p);
static void parseXorExpr(BasParserT *p);
static void patchCallAddrs(BasParserT *p, BasSymbolT *sym);
static void patchJump(BasParserT *p, int32_t addr);
static void patchLabelRefs(BasParserT *p, BasSymbolT *sym);
static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName);
static uint8_t resolveTypeName(BasParserT *p);
static void skipNewlines(BasParserT *p);
static uint8_t suffixToType(const char *name);
static void addPredefConst(BasParserT *p, const char *name, int32_t val) {
BasSymbolT *sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_LONG);
if (sym) {
sym->constInt = val;
sym->isDefined = true;
sym->scope = SCOPE_GLOBAL;
}
}
static void addPredefConsts(BasParserT *p) {
// MsgBox button flags (VB3 compatible)
addPredefConst(p, "vbOKOnly", 0x0000);
addPredefConst(p, "vbOKCancel", 0x0001);
addPredefConst(p, "vbYesNo", 0x0002);
addPredefConst(p, "vbYesNoCancel", 0x0003);
addPredefConst(p, "vbRetryCancel", 0x0004);
// MsgBox icon flags
addPredefConst(p, "vbInformation", 0x0010);
addPredefConst(p, "vbExclamation", 0x0020);
addPredefConst(p, "vbCritical", 0x0030);
addPredefConst(p, "vbQuestion", 0x0040);
// MsgBox return values
addPredefConst(p, "vbOK", 1);
addPredefConst(p, "vbCancel", 2);
addPredefConst(p, "vbYes", 3);
addPredefConst(p, "vbNo", 4);
addPredefConst(p, "vbRetry", 5);
// Show mode flags
addPredefConst(p, "vbModal", 1);
// File attribute constants
addPredefConst(p, "vbNormal", 0);
addPredefConst(p, "vbReadOnly", 1);
addPredefConst(p, "vbHidden", 2);
addPredefConst(p, "vbSystem", 4);
addPredefConst(p, "vbDirectory", 16);
addPredefConst(p, "vbArchive", 32);
}
static void advance(BasParserT *p) {
if (p->hasError) {
return;
}
p->prevLine = p->lex.token.line;
basLexerNext(&p->lex);
if (p->lex.token.type == TOK_ERROR) {
error(p, p->lex.error);
}
}
bool basParse(BasParserT *p) {
parseModule(p);
return !p->hasError;
}
BasModuleT *basParserBuildModule(BasParserT *p) {
if (p->hasError) {
return NULL;
}
// Collect global and form-scope variables for the debugger
collectDebugGlobals(p);
p->cg.globalCount = p->sym.nextGlobalIdx;
return basCodeGenBuildModuleWithProcs(&p->cg, &p->sym);
}
void basParserFree(BasParserT *p) {
basCodeGenFree(&p->cg);
// Free per-symbol dynamic arrays, then the symbol struct itself
for (int32_t i = 0; i < p->sym.count; i++) {
arrfree(p->sym.symbols[i]->patchAddrs);
arrfree(p->sym.symbols[i]->fields);
free(p->sym.symbols[i]);
}
arrfree(p->sym.symbols);
p->sym.symbols = NULL;
p->sym.count = 0;
}
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);
p->formInitJmpAddr = -1;
p->formInitCodeStart = -1;
addPredefConsts(p);
// basLexerInit already primes the first token -- no advance needed
}
void basParserSetValidator(BasParserT *p, const BasCtrlValidatorT *v) {
p->validator = v;
}
static bool check(BasParserT *p, BasTokenTypeE type) {
return p->lex.token.type == type;
}
// Check if current token '(' is followed by a matching ')' then '.'.
// This disambiguates control array access Name(idx).Property from
// function calls Name(args). Saves and restores lexer state.
// Must be called when current token is TOK_LPAREN.
static bool checkCtrlArrayAccess(BasParserT *p) {
BasLexerT savedLex = p->lex;
bool savedErr = p->hasError;
basLexerNext(&p->lex); // consume (
int32_t depth = 1;
while (depth > 0 && p->lex.token.type != TOK_EOF && !p->hasError) {
if (p->lex.token.type == TOK_LPAREN) {
depth++;
} else if (p->lex.token.type == TOK_RPAREN) {
depth--;
if (depth == 0) {
break;
}
}
basLexerNext(&p->lex);
}
// Advance past the closing )
if (p->lex.token.type == TOK_RPAREN) {
basLexerNext(&p->lex);
}
bool dotFollows = (p->lex.token.type == TOK_DOT);
// Restore lexer state
p->lex = savedLex;
p->hasError = savedErr;
return dotFollows;
}
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';
}
// Snapshot global variables for the debugger at the end of compilation.
static void collectDebugGlobals(BasParserT *p) {
for (int32_t i = 0; i < p->sym.count; i++) {
BasSymbolT *s = p->sym.symbols[i];
if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE) {
basCodeGenAddDebugVar(&p->cg, s->name, SCOPE_GLOBAL, s->dataType, s->index, -1, NULL);
} else if (s->scope == SCOPE_FORM && s->kind == SYM_VARIABLE) {
basCodeGenAddDebugVar(&p->cg, s->name, SCOPE_FORM, s->dataType, s->index, -1, s->formName);
} else if (s->kind == SYM_TYPE_DEF && s->fields) {
// Collect UDT type definitions for watch window field access
BasDebugUdtDefT def;
memset(&def, 0, sizeof(def));
snprintf(def.name, BAS_MAX_PROC_NAME, "%s", s->name);
def.typeId = s->index;
def.fieldCount = (int32_t)arrlen(s->fields);
def.fields = (BasDebugFieldT *)malloc(def.fieldCount * sizeof(BasDebugFieldT));
if (def.fields) {
for (int32_t f = 0; f < def.fieldCount; f++) {
snprintf(def.fields[f].name, BAS_MAX_PROC_NAME, "%s", s->fields[f].name);
def.fields[f].dataType = s->fields[f].dataType;
}
}
arrput(p->cg.debugUdtDefs, def);
p->cg.debugUdtDefCount = (int32_t)arrlen(p->cg.debugUdtDefs);
}
}
}
// Snapshot local variables for the debugger before leaving local scope.
// procIndex is the index into the proc table for the current procedure.
// Also saves the local count on the proc symbol for BasProcEntryT.
static void collectDebugLocals(BasParserT *p, int32_t procIndex) {
// Save localCount on the proc symbol
if (p->currentProc[0]) {
BasSymbolT *procSym = basSymTabFind(&p->sym, p->currentProc);
if (procSym) {
procSym->localCount = p->sym.nextLocalIdx;
}
}
for (int32_t i = 0; i < p->sym.count; i++) {
BasSymbolT *s = p->sym.symbols[i];
if (s->scope == SCOPE_LOCAL && s->kind == SYM_VARIABLE) {
basCodeGenAddDebugVar(&p->cg, s->name, SCOPE_LOCAL, s->dataType, s->index, procIndex, NULL);
}
}
}
// Try to emit a ByRef argument (push address of variable).
// If the current token is a simple variable name not followed by
// '(' or '.', we emit PUSH_LOCAL_ADDR/PUSH_GLOBAL_ADDR.
// Otherwise, we fall back to parseExpression (effectively ByVal).
static void emitByRefArg(BasParserT *p) {
if (!check(p, TOK_IDENT)) {
parseExpression(p);
return;
}
// Save the identifier name before peeking ahead
char name[BAS_MAX_TOKEN_LEN];
strncpy(name, p->lex.token.text, sizeof(name) - 1);
name[sizeof(name) - 1] = '\0';
// Look up the symbol -- must be a variable (simple or array)
BasSymbolT *sym = basSymTabFind(&p->sym, name);
if (!sym || sym->kind != SYM_VARIABLE) {
parseExpression(p);
return;
}
// Save lexer state to peek at what follows the identifier
int32_t savedPos = p->lex.pos;
int32_t savedLine = p->lex.line;
int32_t savedCol = p->lex.col;
BasTokenT savedTok = p->lex.token;
advance(p); // consume the identifier
// Array element as BYREF: `arr(i)` or `arr(i, j)`. Emit LOAD of
// the array ref, then the indices, then OP_PUSH_ARR_ADDR which
// produces a BAS_TYPE_REF pointing at the element. Writes through
// that ref in the callee update the actual array element.
if (sym->isArray && check(p, TOK_LPAREN)) {
advance(p); // consume (
// Push the array reference
emitLoad(p, sym);
// Parse indices
int32_t dims = 0;
parseExpression(p);
dims++;
while (match(p, TOK_COMMA)) {
parseExpression(p);
dims++;
}
expect(p, TOK_RPAREN);
// If the next token isn't an argument delimiter, this wasn't
// a simple `arr(i)` -- rewind and fall back. (rare; e.g.
// `arr(i).field` not supported as BYREF.)
if (!check(p, TOK_COMMA) && !check(p, TOK_RPAREN) && !check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) {
// Too late to rewind cleanly -- we've already emitted code.
// Treat this as an error. Callers can restructure as a
// simple variable BYREF or BYVAL.
error(p, "Complex BYREF array expression not supported; use a temporary variable");
return;
}
basEmit8(&p->cg, OP_PUSH_ARR_ADDR);
basEmit8(&p->cg, (uint8_t)dims);
return;
}
// The token after the identifier must be an argument delimiter
// (comma, rparen, newline, colon, EOF, ELSE) for this to be a
// bare variable reference. Anything else (operator, dot, paren)
// means it's part of an expression -- fall back to parseExpression.
bool isDelim = check(p, TOK_COMMA) || check(p, TOK_RPAREN) || check(p, TOK_NEWLINE) || check(p, TOK_COLON) || check(p, TOK_EOF) || check(p, TOK_ELSE);
if (!isDelim) {
// Restore and let parseExpression handle the full expression
p->lex.pos = savedPos;
p->lex.line = savedLine;
p->lex.col = savedCol;
p->lex.token = savedTok;
parseExpression(p);
return;
}
// It's a bare variable reference -- push its address
if (sym->scope == SCOPE_LOCAL) {
basEmit8(&p->cg, OP_PUSH_LOCAL_ADDR);
} else if (sym->scope == SCOPE_FORM) {
basEmit8(&p->cg, OP_PUSH_FORM_ADDR);
} else {
basEmit8(&p->cg, OP_PUSH_GLOBAL_ADDR);
}
basEmitU16(&p->cg, (uint16_t)sym->index);
}
static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
// Parse argument list
expect(p, TOK_LPAREN);
int32_t argc = 0;
if (!check(p, TOK_RPAREN)) {
if (argc < sym->paramCount && !sym->paramByVal[argc]) {
emitByRefArg(p);
} else {
parseExpression(p);
}
argc++;
while (match(p, TOK_COMMA)) {
if (argc < sym->paramCount && !sym->paramByVal[argc]) {
emitByRefArg(p);
} else {
parseExpression(p);
}
argc++;
}
}
expect(p, TOK_RPAREN);
if (p->hasError) {
return;
}
// Determine minimum required arguments
int32_t minArgs = sym->requiredParams;
if (minArgs == 0 && sym->paramCount > 0) {
// No optional params declared -- all are required
bool hasOptional = false;
for (int32_t i = 0; i < sym->paramCount && i < BAS_MAX_PARAMS; i++) {
if (sym->paramOptional[i]) {
hasOptional = true;
break;
}
}
if (!hasOptional) {
minArgs = sym->paramCount;
}
}
if (argc < minArgs || argc > sym->paramCount) {
char buf[BAS_PARSE_ERR_SCRATCH];
if (minArgs == sym->paramCount) {
snprintf(buf, sizeof(buf), "Function '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc);
} else {
snprintf(buf, sizeof(buf), "Function '%s' expects %d to %d arguments, got %d", sym->name, (int)minArgs, (int)sym->paramCount, (int)argc);
}
error(p, buf);
return;
}
// Push default zero-values for omitted optional parameters
for (int32_t i = argc; i < sym->paramCount; i++) {
uint8_t pdt = sym->paramTypes[i];
if (pdt == BAS_TYPE_STRING) {
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, emptyIdx);
} else if (pdt == BAS_TYPE_OBJECT) {
// Nothing -- push NULL object
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
}
}
argc = sym->paramCount;
// External library function: emit OP_CALL_EXTERN
if (sym->isExtern) {
basEmit8(&p->cg, OP_CALL_EXTERN);
basEmitU16(&p->cg, sym->externLibIdx);
basEmitU16(&p->cg, sym->externFuncIdx);
basEmit8(&p->cg, (uint8_t)argc);
basEmit8(&p->cg, sym->dataType);
return;
}
// Internal BASIC function: emit OP_CALL
// 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 && true) {
arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
}
}
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
arrput(sym->patchAddrs, patchAddr);
sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
}
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 if (sym->scope == SCOPE_FORM) {
basEmit8(&p->cg, OP_LOAD_FORM_VAR);
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 if (sym->scope == SCOPE_FORM) {
basEmit8(&p->cg, OP_STORE_FORM_VAR);
basEmitU16(&p->cg, (uint16_t)sym->index);
} else {
basEmit8(&p->cg, OP_STORE_GLOBAL);
basEmitU16(&p->cg, (uint16_t)sym->index);
}
}
// emitUdtInit -- emit code to initialize nested UDT fields after a UDT
// has been created and is on top of the stack. For each field that is
// itself a UDT, we DUP the parent, allocate the child UDT, and store it
// into the field.
static void emitUdtInit(BasParserT *p, int32_t udtTypeId) {
BasSymbolT *typeSym = findTypeDefById(p, udtTypeId);
if (!typeSym) {
return;
}
for (int32_t i = 0; i < typeSym->fieldCount; i++) {
if (typeSym->fields[i].dataType != BAS_TYPE_UDT) {
continue;
}
int32_t childTypeId = typeSym->fields[i].udtTypeId;
BasSymbolT *childType = findTypeDefById(p, childTypeId);
if (!childType) {
continue;
}
// DUP parent, allocate child UDT, STORE_FIELD
basEmit8(&p->cg, OP_DUP);
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, (int16_t)childTypeId);
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, (int16_t)childType->fieldCount);
basEmit8(&p->cg, OP_DIM_ARRAY);
basEmit8(&p->cg, 0);
basEmit8(&p->cg, BAS_TYPE_UDT);
// Recursively init the child's nested UDT fields
emitUdtInit(p, childTypeId);
basEmit8(&p->cg, OP_STORE_FIELD);
basEmitU16(&p->cg, (uint16_t)i);
}
}
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 error(BasParserT *p, const char *msg) {
if (p->hasError) {
return;
}
p->hasError = true;
// If the current token is on a later line than the previous token,
// the error is about the previous line (e.g. missing token at EOL).
int32_t line = p->lex.token.line;
if (p->prevLine > 0 && line > p->prevLine) {
line = p->prevLine;
}
p->errorLine = line;
snprintf(p->error, sizeof(p->error), "Line %d: %s", (int)line, msg);
}
static void errorExpected(BasParserT *p, const char *what) {
char buf[BAS_PARSE_ERR_SCRATCH];
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) {
arrput(el->patchAddr, addr);
}
static void exitListInit(ExitListT *el) {
el->patchAddr = NULL;
}
static void exitListPatch(ExitListT *el, BasParserT *p) {
int32_t target = basCodePos(&p->cg);
int32_t n = (int32_t)arrlen(el->patchAddr);
for (int32_t i = 0; i < n; i++) {
int16_t offset = (int16_t)(target - (el->patchAddr[i] + 2));
basPatch16(&p->cg, el->patchAddr[i], offset);
}
arrfree(el->patchAddr);
el->patchAddr = NULL;
}
static void expect(BasParserT *p, BasTokenTypeE type) {
if (p->hasError) {
return;
}
if (p->lex.token.type != type) {
char buf[BAS_PARSE_ERR_SCRATCH];
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 BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId) {
for (int32_t i = 0; i < p->sym.count; i++) {
if (p->sym.symbols[i]->kind == SYM_TYPE_DEF && p->sym.symbols[i]->index == typeId) {
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 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 parseAndExpr(BasParserT *p) {
parseNotExpr(p);
while (!p->hasError && check(p, TOK_AND)) {
advance(p);
parseNotExpr(p);
basEmit8(&p->cg, OP_AND);
}
}
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);
// Dot member access: UDT field or control property/method
if (check(p, TOK_DOT)) {
// Check for UDT field access first
if (sym != NULL && sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) {
emitLoad(p, sym);
int32_t curTypeId = sym->udtTypeId;
// Walk the dot chain: a.b.c = expr
// For intermediate fields, emit LOAD_FIELD (navigate into nested UDT).
// For the final field, emit STORE_FIELD with the assigned value.
while (check(p, TOK_DOT) && curTypeId >= 0) {
advance(p); // consume DOT
if (!check(p, TOK_IDENT)) {
errorExpected(p, "field name");
return;
}
BasSymbolT *typeSym = findTypeDefById(p, curTypeId);
if (typeSym == NULL) {
error(p, "Unknown TYPE definition");
return;
}
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
if (fieldIdx < 0) {
char buf[BAS_PARSE_ERR_SCRATCH];
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
// Check if this field is a nested UDT with more dots coming
bool fieldIsUdt = (typeSym->fields[fieldIdx].dataType == BAS_TYPE_UDT);
if (fieldIsUdt && check(p, TOK_DOT)) {
// Intermediate level: load this field and continue
basEmit8(&p->cg, OP_LOAD_FIELD);
basEmitU16(&p->cg, (uint16_t)fieldIdx);
curTypeId = typeSym->fields[fieldIdx].udtTypeId;
} else {
// Final field: store value
expect(p, TOK_EQ);
parseExpression(p);
basEmit8(&p->cg, OP_STORE_FIELD);
basEmitU16(&p->cg, (uint16_t)fieldIdx);
return;
}
}
error(p, "Expected '=' in UDT field assignment");
return;
}
// Control property/method access: CtrlName.Member
// Emit: push current form ref, push ctrl name, FIND_CTRL
advance(p); // consume DOT
// Accept any identifier or keyword as a member name — keywords
// like Load, Show, Hide, Clear are valid method names on controls.
if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
errorExpected(p, "property or method name");
return;
}
char memberName[BAS_MAX_TOKEN_LEN];
strncpy(memberName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p); // consume member name
// If `name` is a regular variable, the user is dereferencing an
// object reference (typically a form or control returned by
// CreateForm / CreateControl). Use the variable's value as the
// ref directly instead of treating `name` as a literal control
// name.
bool isVarRef = (sym != NULL && sym->kind == SYM_VARIABLE);
// Special form methods: Show, Hide
if (strcasecmp(memberName, "Show") == 0) {
// name.Show [modal]
if (isVarRef) {
emitLoad(p, sym);
} else {
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, nameIdx);
basEmit8(&p->cg, OP_LOAD_FORM);
}
uint8_t modal = 0;
if (check(p, TOK_INT_LIT)) {
if (p->lex.token.intVal != 0) {
modal = 1;
}
advance(p);
} else if (check(p, TOK_IDENT)) {
BasSymbolT *modSym = basSymTabFind(&p->sym, p->lex.token.text);
if (modSym && modSym->kind == SYM_CONST && modSym->constInt != 0) {
modal = 1;
}
advance(p);
}
basEmit8(&p->cg, OP_SHOW_FORM);
basEmit8(&p->cg, modal);
return;
}
if (strcasecmp(memberName, "Hide") == 0) {
if (isVarRef) {
emitLoad(p, sym);
} else {
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, nameIdx);
basEmit8(&p->cg, OP_LOAD_FORM);
}
basEmit8(&p->cg, OP_HIDE_FORM);
return;
}
if (check(p, TOK_EQ)) {
// Property assignment: CtrlName.Property = expr
advance(p); // consume =
// Compile-time validation: if the host provided a validator
// (IDE), check that the property exists on the widget type.
// Skip when `name` is a variable reference (dynamic ctrl we
// can't statically type) or when the ctrl isn't in the map
// (e.g. created via CreateControl at runtime).
if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isPropValid) {
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
if (wgtType && !p->validator->isPropValid(p->validator->ctx, wgtType, memberName)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unknown property '%s.%s' (type '%s' has no such property)", name, memberName, wgtType);
error(p, buf);
return;
}
}
// Push ctrl/form ref
if (isVarRef) {
emitLoad(p, sym);
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx);
basEmit8(&p->cg, OP_FIND_CTRL);
}
// Push property name
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, propNameIdx);
// Parse value expression
parseExpression(p);
// Store property
basEmit8(&p->cg, OP_STORE_PROP);
return;
}
// Method call: CtrlName.Method [args]
// Same compile-time validation as above, but for methods.
if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isMethodValid) {
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
if (wgtType && !p->validator->isMethodValid(p->validator->ctx, wgtType, memberName)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unknown method '%s.%s' (type '%s' has no such method)", name, memberName, wgtType);
error(p, buf);
return;
}
}
// Push ctrl/form ref
if (isVarRef) {
emitLoad(p, sym);
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx);
basEmit8(&p->cg, OP_FIND_CTRL);
}
// Push method name
uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, methodNameIdx);
// Parse arguments (space-separated, like VB)
int32_t argc = 0;
while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) {
if (argc > 0) {
if (check(p, TOK_COMMA)) {
advance(p);
}
}
parseExpression(p);
argc++;
}
basEmit8(&p->cg, OP_CALL_METHOD);
basEmit8(&p->cg, (uint8_t)argc);
basEmit8(&p->cg, OP_POP); // discard return value (statement form)
return;
}
// Array assignment, sub/function call, or control array access: var(index)
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;
}
// Control array property/method: Name(idx).Prop = expr OR Name(idx).Method args
if (sym == NULL && checkCtrlArrayAccess(p)) {
expect(p, TOK_LPAREN);
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0); // NULL form ref = current form
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx);
parseExpression(p); // index expression
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_FIND_CTRL_IDX);
expect(p, TOK_DOT);
if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
errorExpected(p, "property or method name");
return;
}
char memberName[BAS_MAX_TOKEN_LEN];
strncpy(memberName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
if (check(p, TOK_EQ)) {
// Property assignment: Name(idx).Prop = expr
advance(p);
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, propNameIdx);
parseExpression(p);
basEmit8(&p->cg, OP_STORE_PROP);
} else {
// Method call: Name(idx).Method args
uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, methodNameIdx);
int32_t argc = 0;
while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) {
if (argc > 0 && check(p, TOK_COMMA)) {
advance(p);
}
parseExpression(p);
argc++;
}
basEmit8(&p->cg, OP_CALL_METHOD);
basEmit8(&p->cg, (uint8_t)argc);
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);
// Array-of-UDT field store: arr(i).field = expr
if (sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0 && check(p, TOK_DOT)) {
advance(p); // consume DOT
if (!check(p, TOK_IDENT)) {
errorExpected(p, "field name");
return;
}
BasSymbolT *typeSym = findTypeDefById(p, sym->udtTypeId);
if (typeSym == NULL) {
error(p, "Unknown TYPE definition");
return;
}
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
if (fieldIdx < 0) {
char buf[BAS_PARSE_ERR_SCRATCH];
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_ARRAY_FIELD);
basEmit8(&p->cg, (uint8_t)dims);
basEmitU16(&p->cg, (uint16_t)fieldIdx);
return;
}
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 the identifier is unknown, treat it as a forward-referenced sub.
if (sym == NULL) {
sym = basSymTabAdd(&p->sym, name, SYM_SUB, BAS_TYPE_INTEGER);
if (sym == NULL) {
error(p, "Symbol table full");
return;
}
sym->scope = SCOPE_GLOBAL;
sym->isDefined = false;
sym->codeAddr = 0;
}
if (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)) {
if (argc < sym->paramCount && !sym->paramByVal[argc]) {
emitByRefArg(p);
} else {
parseExpression(p);
}
argc++;
while (match(p, TOK_COMMA)) {
if (argc < sym->paramCount && !sym->paramByVal[argc]) {
emitByRefArg(p);
} else {
parseExpression(p);
}
argc++;
}
}
// Determine the minimum acceptable count (ignore trailing optionals).
int32_t minArgs = sym->requiredParams;
bool hasOptional = false;
for (int32_t i = 0; i < sym->paramCount && i < BAS_MAX_PARAMS; i++) {
if (sym->paramOptional[i]) {
hasOptional = true;
break;
}
}
if (!hasOptional) {
minArgs = sym->paramCount;
}
if (!p->hasError && (argc < minArgs || argc > sym->paramCount)) {
char buf[BAS_PARSE_ERR_SCRATCH];
if (minArgs == sym->paramCount) {
snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc);
} else {
snprintf(buf, sizeof(buf), "Sub '%s' expects %d to %d arguments, got %d", sym->name, (int)minArgs, (int)sym->paramCount, (int)argc);
}
error(p, buf);
return;
}
// Pad missing optional arguments with zero-valued defaults so
// the callee's OP_CALL receives a full parameter list.
while (argc < sym->paramCount) {
uint8_t pType = sym->paramTypes[argc];
if (pType == BAS_TYPE_STRING) {
uint16_t idx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, idx);
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
}
argc++;
}
// External library SUB: emit OP_CALL_EXTERN
if (sym->isExtern) {
basEmit8(&p->cg, OP_CALL_EXTERN);
basEmitU16(&p->cg, sym->externLibIdx);
basEmitU16(&p->cg, sym->externFuncIdx);
basEmit8(&p->cg, (uint8_t)argc);
basEmit8(&p->cg, sym->dataType);
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 && true) {
arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
}
}
return;
}
// If nothing else, it's an assignment missing the =
errorExpected(p, "'=' or '('");
}
// BEGINFORM "FormName" enters form scope: DIM at module level
// creates per-form variables. ENDFORM exits form scope.
static void parseBeginForm(BasParserT *p) {
advance(p); // consume BEGINFORM
if (!check(p, TOK_STRING_LIT)) {
errorExpected(p, "form name string");
return;
}
char formName[BAS_MAX_SYMBOL_NAME];
strncpy(formName, p->lex.token.text, BAS_MAX_SYMBOL_NAME - 1);
formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
advance(p);
if (p->sym.inFormScope) {
error(p, "Nested BEGINFORM is not allowed");
return;
}
if (p->sym.inLocalScope) {
error(p, "BEGINFORM inside SUB/FUNCTION is not allowed");
return;
}
basSymTabEnterFormScope(&p->sym, formName);
// Emit a forward JMP to skip over form init code. All module-level
// bytecode inside the form scope (array DIMs, UDT init, executable
// statements, JMPs over SUB bodies) goes into the init block that
// runs at form load time, not at program startup.
basEmit8(&p->cg, OP_JMP);
p->formInitJmpAddr = basCodePos(&p->cg);
basEmit16(&p->cg, 0); // placeholder — patched at ENDFORM
p->formInitCodeStart = basCodePos(&p->cg);
}
static void parseChDir(BasParserT *p) {
// CHDIR path
advance(p);
parseExpression(p);
basEmit8(&p->cg, OP_FS_CHDIR);
}
static void parseChDrive(BasParserT *p) {
// CHDRIVE drive
advance(p);
parseExpression(p);
basEmit8(&p->cg, OP_FS_CHDRIVE);
}
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 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 parseConst(BasParserT *p) {
// CONST name [AS type] = 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);
// Optional type annotation (declarative; value's literal type still
// determines runtime representation).
if (match(p, TOK_AS)) {
(void)resolveTypeName(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
// DECLARE LIBRARY "name" ... END DECLARE
advance(p); // consume DECLARE
// DECLARE LIBRARY block
if (checkKeyword(p, "LIBRARY")) {
parseDeclareLibrary(p);
return;
}
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, FUNCTION, or LIBRARY 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];
}
}
// Declares external native functions from a dynamically loaded
// library. The library name is stored in the constant pool.
// Each function inside the block is registered as an extern symbol.
// At runtime, OP_CALL_EXTERN resolves the function via the host's
// resolveExtern callback (typically dlsym).
static void parseDeclareLibrary(BasParserT *p) {
advance(p); // consume LIBRARY
if (!check(p, TOK_STRING_LIT)) {
errorExpected(p, "library name string");
return;
}
uint16_t libNameIdx = basAddConstant(&p->cg, p->lex.token.text, p->lex.token.textLen);
advance(p);
skipNewlines(p);
// Parse function declarations until END DECLARE
while (!p->hasError && !check(p, TOK_EOF)) {
skipNewlines(p);
// Check for END DECLARE
if (check(p, TOK_END)) {
advance(p);
if (check(p, TOK_DECLARE)) {
advance(p);
break;
}
error(p, "Expected DECLARE after END in DECLARE LIBRARY block");
return;
}
// Must be DECLARE SUB or DECLARE FUNCTION
if (!check(p, TOK_DECLARE)) {
errorExpected(p, "DECLARE or END DECLARE");
return;
}
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 in DECLARE LIBRARY block");
return;
}
if (!check(p, TOK_IDENT)) {
errorExpected(p, "function name");
return;
}
char funcName[BAS_MAX_TOKEN_LEN];
strncpy(funcName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
funcName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
// Strip type suffix ($%&!#) from extern name so dlsym finds the
// C function. The suffix is still used for return type via suffixToType.
char externName[BAS_MAX_TOKEN_LEN];
strncpy(externName, funcName, BAS_MAX_TOKEN_LEN - 1);
externName[BAS_MAX_TOKEN_LEN - 1] = '\0';
int32_t enLen = (int32_t)strlen(externName);
if (enLen > 0) {
char last = externName[enLen - 1];
if (last == '$' || last == '%' || last == '&' || last == '!' || last == '#') {
externName[enLen - 1] = '\0';
}
}
uint16_t funcNameIdx = basAddConstant(&p->cg, externName, (int32_t)strlen(externName));
// 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 = match(p, TOK_BYVAL);
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(funcName);
if (kind == SYM_FUNCTION && match(p, TOK_AS)) {
returnType = resolveTypeName(p);
}
if (p->hasError) {
return;
}
// Register as extern symbol
BasSymbolT *sym = basSymTabAdd(&p->sym, funcName, kind, returnType);
if (sym == NULL) {
sym = basSymTabFind(&p->sym, funcName);
if (sym == NULL) {
error(p, "Symbol table full");
return;
}
}
sym->scope = SCOPE_GLOBAL;
sym->isDefined = true;
sym->isExtern = true;
sym->externLibIdx = libNameIdx;
sym->externFuncIdx = funcNameIdx;
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);
int32_t pUdtTypeId = -1;
if (match(p, TOK_AS)) {
pdt = resolveTypeName(p);
if (pdt == BAS_TYPE_UDT) {
pUdtTypeId = p->lastUdtTypeId;
}
}
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;
paramSym->udtTypeId = pUdtTypeId;
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);
collectDebugLocals(p, p->cg.debugProcCount++);
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);
}
// 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 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[BAS_PARSE_ERR_SCRATCH];
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 if (p->sym.inFormScope) {
sym->scope = SCOPE_FORM;
} else {
sym->scope = SCOPE_GLOBAL;
}
if (isArray) {
if (dt == BAS_TYPE_UDT && udtTypeId >= 0) {
// For UDT arrays, push typeId and fieldCount so elements
// can be properly initialized
BasSymbolT *typeSym = findTypeDefById(p, udtTypeId);
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);
}
}
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 = findTypeDefById(p, udtTypeId);
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);
// Initialize nested UDT fields
emitUdtInit(p, udtTypeId);
emitStore(p, sym);
}
} else if (dt == BAS_TYPE_STRING) {
// STRING slots must start as an empty string, not numeric 0.
// Arithmetic falls through basValToNumber so numeric defaults
// stay harmless, but STRING concat checks actual slot type.
uint16_t idx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, idx);
emitStore(p, sym);
}
}
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
{
// Grow the array to make room for the insertion
for (int32_t pad = 0; pad < insertLen; pad++) {
arrput(p->cg.code, 0);
}
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 = (int32_t)arrlen(p->cg.code);
}
}
(*outDims)++;
if (!match(p, TOK_COMMA)) {
break;
}
}
}
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 = terminate program
// END IF / END SUB / END FUNCTION / END SELECT are handled by their parsers
advance(p); // consume END
basEmit8(&p->cg, OP_END);
}
static void parseEndForm(BasParserT *p) {
advance(p); // consume ENDFORM
if (!p->sym.inFormScope) {
error(p, "ENDFORM without BEGINFORM");
return;
}
// Capture form name before leaving scope
char formName[BAS_MAX_SYMBOL_NAME];
strncpy(formName, p->sym.formScopeName, sizeof(formName) - 1);
formName[sizeof(formName) - 1] = '\0';
int32_t varCount = basSymTabLeaveFormScope(&p->sym);
// Close the form init block: add OP_RET and patch the JMP
basEmit8(&p->cg, OP_RET);
int32_t initAddr = p->formInitCodeStart;
int32_t initLen = basCodePos(&p->cg) - p->formInitCodeStart;
// Patch the JMP to skip over the entire init block
int16_t offset = (int16_t)(basCodePos(&p->cg) - (p->formInitJmpAddr + 2));
basPatch16(&p->cg, p->formInitJmpAddr, offset);
p->formInitJmpAddr = -1;
p->formInitCodeStart = -1;
// Record form variable info (even if varCount is 0 but init code exists)
if (varCount > 0 || initAddr >= 0) {
BasFormVarInfoT info;
memset(&info, 0, sizeof(info));
snprintf(info.formName, sizeof(info.formName), "%s", formName);
info.varCount = varCount;
info.initCodeAddr = initAddr;
info.initCodeLen = initLen;
arrput(p->cg.formVarInfo, info);
p->cg.formVarInfoCount = (int32_t)arrlen(p->cg.formVarInfo);
}
}
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 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);
basEmit8(&p->cg, OP_FOR_POP);
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 parseExpression(BasParserT *p) {
parseImpExpr(p);
}
static void parseFileCopy(BasParserT *p) {
// FILECOPY source, dest
advance(p);
parseExpression(p);
expect(p, TOK_COMMA);
parseExpression(p);
basEmit8(&p->cg, OP_FS_FILECOPY);
}
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. The
// trailing int16 is a forward offset the VM uses to skip the body
// when the loop's range is already empty at entry (e.g. FOR i = 10
// TO 5). We patch it after the body is emitted.
basEmit8(&p->cg, OP_FOR_INIT);
basEmitU16(&p->cg, (uint16_t)loopVar->index);
basEmit8(&p->cg, (uint8_t)loopVar->scope);
int32_t skipOffsetPos = basCodePos(&p->cg);
basEmit16(&p->cg, 0); // placeholder
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, (uint8_t)loopVar->scope);
int16_t backOffset = (int16_t)(loopBody - (basCodePos(&p->cg) + 2));
basEmit16(&p->cg, backOffset);
// Patch FOR_INIT's forward skip offset to point past FOR_NEXT.
int32_t loopEnd = basCodePos(&p->cg);
int16_t skipOffset = (int16_t)(loopEnd - (skipOffsetPos + 2));
p->cg.code[skipOffsetPos] = (uint8_t)(skipOffset & 0xFF);
p->cg.code[skipOffsetPos + 1] = (uint8_t)((skipOffset >> 8) & 0xFF);
// 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;
int32_t requiredCount = 0;
bool seenOptional = false;
uint8_t paramTypes[BAS_MAX_PARAMS];
bool paramByVal[BAS_MAX_PARAMS];
bool paramOptional[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 optional = false;
if (match(p, TOK_OPTIONAL)) {
optional = true;
seenOptional = true;
} else if (seenOptional) {
error(p, "Required parameter cannot follow Optional parameter");
return;
}
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);
int32_t pUdtTypeId = -1;
if (match(p, TOK_AS)) {
pdt = resolveTypeName(p);
if (pdt == BAS_TYPE_UDT) {
pUdtTypeId = p->lastUdtTypeId;
}
}
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;
paramSym->udtTypeId = pUdtTypeId;
if (paramCount < BAS_MAX_PARAMS) {
paramTypes[paramCount] = pdt;
paramByVal[paramCount] = byVal;
paramOptional[paramCount] = optional;
}
if (!optional) {
requiredCount = paramCount + 1;
}
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->requiredParams = requiredCount;
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];
funcSym->paramOptional[i] = paramOptional[i];
}
// Record the owning form -- see parseSub for the rationale.
if (p->sym.inFormScope && p->sym.formScopeName[0]) {
strncpy(funcSym->formName, p->sym.formScopeName, BAS_MAX_SYMBOL_NAME - 1);
funcSym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
}
// 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
collectDebugLocals(p, p->cg.debugProcCount++);
basSymTabLeaveLocal(&p->sym);
p->currentProc[0] = '\0';
// Patch the skip jump
patchJump(p, skipJump);
}
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 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 (stb_ds dynamic array)
int32_t *endJumps = NULL;
// 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 < (int32_t)arrlen(endJumps); i++) {
patchJump(p, endJumps[i]);
}
arrfree(endJumps);
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
arrput(endJumps, 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 < (int32_t)arrlen(endJumps); i++) {
patchJump(p, endJumps[i]);
}
arrfree(endJumps);
return;
}
p->lex = savedLex;
}
parseStatement(p);
skipNewlines(p);
}
}
// ELSE block
if (!p->hasError && check(p, TOK_ELSE)) {
arrput(endJumps, 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 < (int32_t)arrlen(endJumps); i++) {
patchJump(p, endJumps[i]);
}
arrfree(endJumps);
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 < (int32_t)arrlen(endJumps); i++) {
patchJump(p, endJumps[i]);
}
arrfree(endJumps);
// If we got here without END IF, that's an error
if (!p->hasError) {
error(p, "Expected END IF");
}
}
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 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 parseKill(BasParserT *p) {
// KILL filename
advance(p);
parseExpression(p);
basEmit8(&p->cg, OP_FS_KILL);
}
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 parseMkDir(BasParserT *p) {
// MKDIR path
advance(p);
parseExpression(p);
basEmit8(&p->cg, OP_FS_MKDIR);
}
// Walk the token stream from current position, find every
// top-level SUB/FUNCTION declaration, extract the signature
// (name, params, return type), and register it in the symbol
// table. Does not emit code. Saves and restores lexer position
// so the main parse pass starts from the same point. This gives
// VB-style forward visibility: call sites that appear earlier in
// the source than the SUB definition still resolve to the right
// paramCount / types.
static void prescanSignatures(BasParserT *p) {
BasLexerT savedLex = p->lex;
bool savedErr = p->hasError;
int32_t savedErrLn = p->errorLine;
char savedErrMsg[BAS_PARSE_ERR_SCRATCH];
snprintf(savedErrMsg, sizeof(savedErrMsg), "%s", p->error);
while (!check(p, TOK_EOF)) {
// "END SUB" / "END FUNCTION" consume the END and the following
// keyword as separate tokens; skip END so the next-iteration
// SUB/FUNCTION check doesn't misinterpret it as a declaration.
if (check(p, TOK_END)) {
advance(p);
continue;
}
bool isFn = check(p, TOK_FUNCTION);
bool isSub = check(p, TOK_SUB);
if (!isFn && !isSub) {
advance(p);
continue;
}
advance(p); // consume SUB / FUNCTION
if (!check(p, TOK_IDENT)) {
continue;
}
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);
// Param list
int32_t paramCount = 0;
int32_t requiredCount = 0;
uint8_t paramTypes[BAS_MAX_PARAMS] = {0};
bool paramByVal[BAS_MAX_PARAMS] = {0};
bool paramOptional[BAS_MAX_PARAMS] = {0};
if (check(p, TOK_LPAREN)) {
advance(p);
while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !check(p, TOK_NEWLINE)) {
if (paramCount > 0) {
if (!check(p, TOK_COMMA)) {
break;
}
advance(p);
}
bool optional = false;
if (check(p, TOK_OPTIONAL)) {
optional = true;
advance(p);
}
bool byVal = false;
if (check(p, TOK_BYVAL)) {
byVal = true;
advance(p);
}
if (!check(p, TOK_IDENT)) {
break;
}
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 (check(p, TOK_AS)) {
advance(p);
// resolveTypeName sets hasError on miss; we clear
// at function end so one broken signature doesn't
// stop us from discovering the rest.
pdt = resolveTypeName(p);
if (p->hasError) {
break;
}
}
if (paramCount < BAS_MAX_PARAMS) {
paramTypes[paramCount] = pdt;
paramByVal[paramCount] = byVal;
paramOptional[paramCount] = optional;
}
if (!optional) {
requiredCount = paramCount + 1;
}
paramCount++;
}
if (check(p, TOK_RPAREN)) {
advance(p);
}
}
// FUNCTION return type (AS clause; or suffix on name)
uint8_t returnType = suffixToType(name);
if (isFn && check(p, TOK_AS)) {
advance(p);
returnType = resolveTypeName(p);
}
// Register / update the symbol. If a call site already
// created a forward-ref stub, update it in place.
BasSymbolT *sym = basSymTabFindGlobal(&p->sym, name);
if (sym == NULL) {
bool savedLocal = p->sym.inLocalScope;
p->sym.inLocalScope = false;
sym = basSymTabAdd(&p->sym, name,
isFn ? SYM_FUNCTION : SYM_SUB,
returnType);
p->sym.inLocalScope = savedLocal;
}
if (sym != NULL) {
sym->scope = SCOPE_GLOBAL;
sym->dataType = returnType;
sym->paramCount = paramCount;
sym->requiredParams = requiredCount;
for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) {
sym->paramTypes[i] = paramTypes[i];
sym->paramByVal[i] = paramByVal[i];
sym->paramOptional[i] = paramOptional[i];
}
// basSymTabAdd defaults isDefined=true; clear it so
// call sites that encounter this symbol before the real
// body is parsed register themselves as forward-refs
// (patchAddrs). The real parseSub/parseFunction pass
// sets isDefined=true and fills in codeAddr, at which
// point patchCallAddrs backpatches the forward refs.
sym->isDefined = false;
sym->codeAddr = 0;
}
// Best-effort: clear any scan error so we continue to the
// next declaration. The main parse pass will re-surface
// real errors with full location info.
if (p->hasError) {
p->hasError = false;
p->errorLine = 0;
p->error[0] = '\0';
}
}
p->lex = savedLex;
p->hasError = savedErr;
p->errorLine = savedErrLn;
snprintf(p->error, sizeof(p->error), "%s", savedErrMsg);
}
static void parseModule(BasParserT *p) {
// VB semantics: all SUB/FUNCTION declarations are visible from
// anywhere in the module regardless of source order. Do a
// pre-scan that walks the token stream, extracts every
// SUB/FUNCTION signature, and registers it in the symbol table.
// Call sites encountered later (including module-level code
// that precedes the SUB definition in source) can then validate
// argument count / types against the real signature instead of
// falling back to a paramCount=0 placeholder.
prescanSignatures(p);
skipNewlines(p);
while (!p->hasError && !check(p, TOK_EOF)) {
parseStatement(p);
skipNewlines(p);
}
// Check for unresolved forward references (skip externs from DECLARE LIBRARY)
if (!p->hasError) {
for (int32_t i = 0; i < p->sym.count; i++) {
BasSymbolT *sym = p->sym.symbols[i];
if ((sym->kind == SYM_SUB || sym->kind == SYM_FUNCTION) && !sym->isDefined && !sym->isExtern) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Undefined %s: %s",
sym->kind == SYM_SUB ? "Sub" : "Function", sym->name);
error(p, buf);
break;
}
}
}
// End of module -- emit HALT
basEmit8(&p->cg, OP_HALT);
}
// VB precedence (high to low): ^, unary -, *, /, \, MOD, +, -
// So parseMulExpr calls parseUnaryExpr, which calls parsePowExpr,
// which calls parsePrimary. This makes -2^2 = -(2^2) = -4.
static void parseMulExpr(BasParserT *p) {
parseUnaryExpr(p);
while (!p->hasError) {
if (check(p, TOK_STAR)) {
advance(p);
parseUnaryExpr(p);
basEmit8(&p->cg, OP_MUL_INT);
} else if (check(p, TOK_SLASH)) {
advance(p);
parseUnaryExpr(p);
basEmit8(&p->cg, OP_DIV_FLT);
} else if (check(p, TOK_BACKSLASH)) {
advance(p);
parseUnaryExpr(p);
basEmit8(&p->cg, OP_IDIV_INT);
} else if (check(p, TOK_MOD)) {
advance(p);
parseUnaryExpr(p);
basEmit8(&p->cg, OP_MOD_INT);
} else {
break;
}
}
}
static void parseName(BasParserT *p) {
// NAME oldname AS newname
advance(p);
parseExpression(p);
expect(p, TOK_AS);
parseExpression(p);
basEmit8(&p->cg, OP_FS_NAME);
}
static void parseNotExpr(BasParserT *p) {
if (check(p, TOK_NOT)) {
advance(p);
parseNotExpr(p);
basEmit8(&p->cg, OP_NOT);
return;
}
parseCompareExpr(p);
}
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 (stb_ds dynamic array)
int32_t *endJumps = NULL;
int32_t labelIdx = 1;
for (;;) {
if (p->hasError) {
arrfree(endJumps);
return;
}
if (!check(p, TOK_IDENT)) {
errorExpected(p, "label name");
arrfree(endJumps);
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
arrput(endJumps, 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);
int32_t n = (int32_t)arrlen(endJumps);
for (int32_t i = 0; i < n; i++) {
int16_t offset = (int16_t)(endTarget - (endJumps[i] + 2));
basPatch16(&p->cg, endJumps[i], offset);
}
arrfree(endJumps);
}
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);
arrput(sym->patchAddrs, patchAddr);
sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
}
}
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 = BAS_FILE_MODE_INPUT;
advance(p);
} else if (check(p, TOK_OUTPUT)) {
mode = BAS_FILE_MODE_OUTPUT;
advance(p);
} else if (check(p, TOK_APPEND)) {
mode = BAS_FILE_MODE_APPEND;
advance(p);
} else if (check(p, TOK_RANDOM)) {
mode = BAS_FILE_MODE_RANDOM;
advance(p);
} else if (check(p, TOK_BINARY)) {
mode = BAS_FILE_MODE_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 parseOrExpr(BasParserT *p) {
parseXorExpr(p);
while (!p->hasError && check(p, TOK_OR)) {
advance(p);
parseXorExpr(p);
basEmit8(&p->cg, OP_OR);
}
}
static void parsePowExpr(BasParserT *p) {
parsePrimary(p);
while (!p->hasError && check(p, TOK_CARET)) {
advance(p);
parsePrimary(p);
basEmit8(&p->cg, OP_POW);
}
}
static void parsePrimary(BasParserT *p) {
if (p->hasError) {
return;
}
BasTokenTypeE tt = p->lex.token.type;
// App.Path / App.Config / App.Data
if (tt == TOK_APP) {
advance(p);
expect(p, TOK_DOT);
if (checkKeyword(p,"Path")) {
advance(p);
basEmit8(&p->cg, OP_APP_PATH);
} else if (checkKeyword(p,"Config")) {
advance(p);
basEmit8(&p->cg, OP_APP_CONFIG);
} else if (checkKeyword(p,"Data") || p->lex.token.type == TOK_DATA) {
// "Data" tokenizes as TOK_DATA (the DATA/READ keyword) rather
// than TOK_IDENT, so accept it directly by token type too.
advance(p);
basEmit8(&p->cg, OP_APP_DATA);
} else {
error(p, "Expected 'Path', 'Config', or 'Data' after 'App.'");
}
return;
}
// Integer literal
if (tt == TOK_INT_LIT) {
int32_t val = p->lex.token.intVal;
if (val >= INT16_MIN && val <= INT16_MAX) {
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;
}
// Me -- reference to current form
if (tt == TOK_ME) {
advance(p);
basEmit8(&p->cg, OP_ME_REF);
return;
}
// Nothing -- null object reference
if (tt == TOK_NOTHING) {
advance(p);
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
return;
}
// CreateForm(name$, width%, height%) -- create a form in code
if (tt == TOK_CREATEFORM) {
advance(p);
expect(p, TOK_LPAREN);
parseExpression(p); // name
expect(p, TOK_COMMA);
parseExpression(p); // width
expect(p, TOK_COMMA);
parseExpression(p); // height
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_CREATE_FORM);
return;
}
// CreateControl(form, typeName$, ctrlName$ [, parent]) -- create a control
if (tt == TOK_CREATECONTROL) {
advance(p);
expect(p, TOK_LPAREN);
parseExpression(p); // form ref
expect(p, TOK_COMMA);
parseExpression(p); // type name
expect(p, TOK_COMMA);
parseExpression(p); // control name
if (match(p, TOK_COMMA)) {
// Optional parent parameter
parseExpression(p); // parent ctrl ref
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_CREATE_CTRL_EX);
} else {
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_CREATE_CTRL);
}
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;
}
// CurDir$ -- current directory (no args)
if (tt == TOK_CURDIR) {
advance(p);
if (check(p, TOK_LPAREN)) {
expect(p, TOK_LPAREN);
expect(p, TOK_RPAREN);
}
basEmit8(&p->cg, OP_FS_CURDIR);
return;
}
// Dir$(pattern) or Dir$() for next match
if (tt == TOK_DIR) {
advance(p);
if (check(p, TOK_LPAREN)) {
expect(p, TOK_LPAREN);
if (check(p, TOK_RPAREN)) {
// Dir$() -- no args, get next match
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_FS_DIR_NEXT);
} else {
// Dir$(pattern)
parseExpression(p);
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_FS_DIR);
}
} else {
// Dir with no parens -- next match
basEmit8(&p->cg, OP_FS_DIR_NEXT);
}
return;
}
// FileLen(filename) -- file size without opening
if (tt == TOK_FILELEN) {
advance(p);
expect(p, TOK_LPAREN);
parseExpression(p);
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_FS_FILELEN);
return;
}
// GetAttr(filename) -- file attributes
if (tt == TOK_GETATTR) {
advance(p);
expect(p, TOK_LPAREN);
parseExpression(p);
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_FS_GETATTR);
return;
}
// InputBox$(prompt [, title [, default]])
if (tt == TOK_INPUTBOX) {
advance(p);
expect(p, TOK_LPAREN);
parseExpression(p); // prompt
if (match(p, TOK_COMMA)) {
parseExpression(p); // title
} else {
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, basAddConstant(&p->cg, "", 0));
}
if (match(p, TOK_COMMA)) {
parseExpression(p); // default
} else {
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, basAddConstant(&p->cg, "", 0));
}
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_INPUTBOX);
return;
}
// MsgBox(message [, flags [, title]]) -- function form returning button ID
if (tt == TOK_MSGBOX) {
advance(p);
expect(p, TOK_LPAREN);
parseExpression(p); // message
if (match(p, TOK_COMMA)) {
parseExpression(p); // flags
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0); // default flags = vbOKOnly
}
if (match(p, TOK_COMMA)) {
parseExpression(p); // title
} else {
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, emptyIdx);
}
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_MSGBOX);
return;
}
// IniRead$(file, section, key, default)
if (tt == TOK_INIREAD) {
advance(p);
expect(p, TOK_LPAREN);
parseExpression(p); // file
expect(p, TOK_COMMA);
parseExpression(p); // section
expect(p, TOK_COMMA);
parseExpression(p); // key
expect(p, TOK_COMMA);
parseExpression(p); // default
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_INI_READ);
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[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Built-in '%s' expects %d-%d arguments, got %d", builtin->name, (int)builtin->minArgs, (int)builtin->maxArgs, (int)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);
// Array-of-UDT field access: arr(i).field
if (sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0 && check(p, TOK_DOT)) {
advance(p); // consume DOT
if (!check(p, TOK_IDENT)) {
errorExpected(p, "field name");
return;
}
BasSymbolT *typeSym = findTypeDefById(p, sym->udtTypeId);
if (typeSym == NULL) {
error(p, "Unknown TYPE definition");
return;
}
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
if (fieldIdx < 0) {
char buf[BAS_PARSE_ERR_SCRATCH];
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;
}
// Unknown identifier + '(' -- could be forward-ref function or
// control array access: Name(index).Property
if (sym == NULL) {
if (checkCtrlArrayAccess(p)) {
// Control array read: Name(idx).Property
expect(p, TOK_LPAREN);
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0); // NULL form ref = current form
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx);
parseExpression(p); // index expression
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_FIND_CTRL_IDX);
expect(p, TOK_DOT);
if (!check(p, TOK_IDENT)) {
errorExpected(p, "property name");
return;
}
char memberName[BAS_MAX_TOKEN_LEN];
strncpy(memberName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, propNameIdx);
basEmit8(&p->cg, OP_LOAD_PROP);
return;
}
// Not a control array -- forward-ref function call
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 dot access: UDT field or control property
if (check(p, TOK_DOT)) {
// If we already know this is a UDT variable, do field access
sym = basSymTabFind(&p->sym, name);
if (sym != NULL && sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) {
emitLoad(p, sym);
int32_t curTypeId = sym->udtTypeId;
// Loop to handle nested UDT field access: a.b.c
while (check(p, TOK_DOT) && curTypeId >= 0) {
advance(p); // consume DOT
if (!check(p, TOK_IDENT)) {
errorExpected(p, "field name");
return;
}
BasSymbolT *typeSym = findTypeDefById(p, curTypeId);
if (typeSym == NULL) {
error(p, "Unknown TYPE definition");
return;
}
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
if (fieldIdx < 0) {
char buf[BAS_PARSE_ERR_SCRATCH];
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);
// If this field is also a UDT, allow further dot access
if (typeSym->fields[fieldIdx].dataType == BAS_TYPE_UDT) {
curTypeId = typeSym->fields[fieldIdx].udtTypeId;
} else {
curTypeId = -1;
}
}
return;
}
// Not a UDT -- treat as control property/method: CtrlName.Member
advance(p); // consume DOT
if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
errorExpected(p, "property or method name");
return;
}
char memberName[BAS_MAX_TOKEN_LEN];
strncpy(memberName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
bool isVarRef2 = (sym != NULL && sym->kind == SYM_VARIABLE);
// Compile-time validation for expression-context reads /
// method returns. Peek ahead: if '(' follows, memberName
// is a method; otherwise it's a property read. Skip when
// the host didn't attach a validator or the ctrl is dynamic.
if (!isVarRef2 && p->validator && p->validator->lookupCtrlType) {
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
if (wgtType) {
bool isMethodCall = check(p, TOK_LPAREN);
bool valid = true;
if (isMethodCall && p->validator->isMethodValid) {
valid = p->validator->isMethodValid(p->validator->ctx, wgtType, memberName);
} else if (!isMethodCall && p->validator->isPropValid) {
valid = p->validator->isPropValid(p->validator->ctx, wgtType, memberName);
}
if (!valid) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unknown %s '%s.%s' (type '%s' has no such %s)",
isMethodCall ? "method" : "property",
name, memberName, wgtType,
isMethodCall ? "method" : "property");
error(p, buf);
return;
}
}
}
// If `name` is a regular variable holding an object reference
// (form/control returned by CreateForm/CreateControl), use its
// value directly instead of treating `name` as a literal name.
if (isVarRef2) {
emitLoad(p, sym);
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx);
basEmit8(&p->cg, OP_FIND_CTRL);
}
// If followed by '(', this is a method call with args
if (check(p, TOK_LPAREN)) {
advance(p); // consume '('
uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, methodNameIdx);
int32_t argc = 0;
if (!check(p, TOK_RPAREN)) {
parseExpression(p);
argc++;
while (match(p, TOK_COMMA)) {
parseExpression(p);
argc++;
}
}
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_CALL_METHOD);
basEmit8(&p->cg, (uint8_t)argc);
} else {
// Property read
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, propNameIdx);
basEmit8(&p->cg, OP_LOAD_PROP);
}
return;
}
// Plain variable reference
sym = ensureVariable(p, name);
if (sym != NULL) {
emitLoad(p, sym);
}
return;
}
// Nothing matched
errorExpected(p, "expression");
}
static void parsePrint(BasParserT *p) {
// PRINT [#channel, expr]
// PRINT [expr] [; expr] [, expr] [;]
// PRINT USING "fmt"; expr [; expr] ...
advance(p); // consume PRINT
// File I/O: PRINT #channel, expr [; expr | , expr ]* [;]
//
// Channel is pushed once and DUP'd per value. `;` means no separator
// between values; `,` is treated the same (tab-zone separator not
// supported for file output). Trailing `;` suppresses the final newline.
if (check(p, TOK_HASH)) {
advance(p); // consume #
// Channel number -- stays on stack as "keep" for the whole statement.
parseExpression(p);
expect(p, TOK_COMMA);
bool trailingSep = false;
for (;;) {
// Duplicate the channel for this OP_FILE_PRINT.
basEmit8(&p->cg, OP_DUP);
parseExpression(p);
basEmit8(&p->cg, OP_FILE_PRINT);
if (check(p, TOK_SEMICOLON) || check(p, TOK_COMMA)) {
advance(p);
trailingSep = true;
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
break;
}
trailingSep = false;
continue;
}
break;
}
// No trailing ; or , -> write newline. Otherwise, just drop the
// kept channel value.
if (trailingSep) {
basEmit8(&p->cg, OP_POP);
} else {
basEmit8(&p->cg, OP_FILE_WRITE_NL);
}
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 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 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 parseRemoveControl(BasParserT *p) {
// REMOVECONTROL formRef, ctrlName$
advance(p); // consume REMOVECONTROL
parseExpression(p); // form reference
expect(p, TOK_COMMA);
parseExpression(p); // control name string
basEmit8(&p->cg, OP_REMOVE_CTRL);
}
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 parseRmDir(BasParserT *p) {
// RMDIR path
advance(p);
parseExpression(p);
basEmit8(&p->cg, OP_FS_RMDIR);
}
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 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 = NULL; // stb_ds dynamic array
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 < (int32_t)arrlen(endJumps); i++) {
patchJump(p, endJumps[i]);
}
arrfree(endJumps);
return;
}
p->lex = savedLex;
}
if (!check(p, TOK_CASE)) {
error(p, "Expected CASE or END SELECT");
arrfree(endJumps);
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 < (int32_t)arrlen(endJumps); i++) {
patchJump(p, endJumps[i]);
}
arrfree(endJumps);
return;
}
p->lex = savedLex;
}
parseStatement(p);
skipNewlines(p);
}
continue;
}
// CASE val [, val | val TO val | IS op val] ...
//
// Strategy for multi-value CASE using JMP_TRUE chaining:
// For each item:
// Plain value: DUP, push val, CMP_EQ, JMP_TRUE -> body
// Range (val TO val): DUP, push lo, CMP_GE, JMP_FALSE -> skip,
// DUP, push hi, CMP_LE, JMP_TRUE -> body, skip:
// IS op val: DUP, push val, CMP_xx, JMP_TRUE -> body
// JMP -> next_case (none of the items matched)
// body:
// ...statements...
// JMP -> end_select
// next_case:
int32_t *bodyJumps = NULL; // stb_ds dynamic array
for (;;) {
if (check(p, TOK_IS)) {
// CASE IS <op> value
advance(p); // consume IS
uint8_t cmpOp;
if (check(p, TOK_LT)) { cmpOp = OP_CMP_LT; advance(p); }
else if (check(p, TOK_GT)) { cmpOp = OP_CMP_GT; advance(p); }
else if (check(p, TOK_LE)) { cmpOp = OP_CMP_LE; advance(p); }
else if (check(p, TOK_GE)) { cmpOp = OP_CMP_GE; advance(p); }
else if (check(p, TOK_EQ)) { cmpOp = OP_CMP_EQ; advance(p); }
else if (check(p, TOK_NE)) { cmpOp = OP_CMP_NE; advance(p); }
else {
error(p, "Expected comparison operator after IS");
arrfree(bodyJumps);
arrfree(endJumps);
return;
}
basEmit8(&p->cg, OP_DUP);
parseExpression(p);
basEmit8(&p->cg, cmpOp);
arrput(bodyJumps, emitJump(p, OP_JMP_TRUE));
} else {
// Parse first value -- could be plain or start of range
basEmit8(&p->cg, OP_DUP);
parseExpression(p);
if (check(p, TOK_TO)) {
// CASE low TO high
advance(p); // consume TO
// Stack: testval testval low
// Check testval >= low
basEmit8(&p->cg, OP_CMP_GE);
int32_t skipRange = emitJump(p, OP_JMP_FALSE);
// Check testval <= high
basEmit8(&p->cg, OP_DUP);
parseExpression(p);
basEmit8(&p->cg, OP_CMP_LE);
arrput(bodyJumps, emitJump(p, OP_JMP_TRUE));
patchJump(p, skipRange);
} else {
// Plain value -- equality test
basEmit8(&p->cg, OP_CMP_EQ);
arrput(bodyJumps, emitJump(p, OP_JMP_TRUE));
}
}
if (!match(p, TOK_COMMA)) {
break;
}
}
// 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 < (int32_t)arrlen(bodyJumps); i++) {
patchJump(p, bodyJumps[i]);
}
arrfree(bodyJumps);
// 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 < (int32_t)arrlen(endJumps); i++) {
patchJump(p, endJumps[i]);
}
arrfree(endJumps);
return;
}
p->lex = savedLex;
}
parseStatement(p);
skipNewlines(p);
}
// Jump to end of SELECT (skip remaining cases)
arrput(endJumps, emitJump(p, OP_JMP));
// Patch the next-case jump to here
patchJump(p, nextCaseJump);
}
// Reached if EOF hit without END SELECT -- patch pending end jumps
// and clean up.
basEmit8(&p->cg, OP_POP);
for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) {
patchJump(p, endJumps[i]);
}
arrfree(endJumps);
if (!p->hasError) {
error(p, "Expected END SELECT");
}
}
static void parseSetAttr(BasParserT *p) {
// SETATTR filename, attributes
advance(p);
parseExpression(p);
expect(p, TOK_COMMA);
parseExpression(p);
basEmit8(&p->cg, OP_FS_SETATTR);
}
static void parseSetEvent(BasParserT *p) {
// SETEVENT ctrlRef, eventName$, handlerName$
advance(p); // consume SETEVENT
parseExpression(p); // control reference
expect(p, TOK_COMMA);
parseExpression(p); // event name string
expect(p, TOK_COMMA);
parseExpression(p); // handler name string
basEmit8(&p->cg, OP_SET_EVENT);
}
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 parseStatement(BasParserT *p) {
if (p->hasError) {
return;
}
skipNewlines(p);
if (check(p, TOK_EOF)) {
return;
}
// Emit source line number for debugger (before statement code)
basEmit8(&p->cg, OP_LINE);
basEmitU16(&p->cg, (uint16_t)p->lex.token.line);
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_ERROR_KW:
// ERROR n -- raise a runtime error
advance(p);
parseExpression(p);
basEmit8(&p->cg, OP_RAISE_ERR);
break;
case TOK_ERASE:
parseErase(p);
break;
case TOK_TYPE:
parseType(p);
break;
case TOK_REDIM:
parseRedim(p);
break;
case TOK_FILECOPY:
parseFileCopy(p);
break;
case TOK_INPUT:
parseInput(p);
break;
case TOK_KILL:
parseKill(p);
break;
case TOK_MKDIR:
parseMkDir(p);
break;
case TOK_NAME:
parseName(p);
break;
case TOK_OPEN:
parseOpen(p);
break;
case TOK_CHDIR:
parseChDir(p);
break;
case TOK_CHDRIVE:
parseChDrive(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_RMDIR:
parseRmDir(p);
break;
case TOK_SETATTR:
parseSetAttr(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 && true) {
arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
}
}
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_CREATEFORM:
// CreateForm used as statement (discard return value)
parsePrimary(p);
basEmit8(&p->cg, OP_POP);
break;
case TOK_CREATECONTROL:
// CreateControl used as statement (discard return value)
parsePrimary(p);
basEmit8(&p->cg, OP_POP);
break;
case TOK_SETEVENT:
parseSetEvent(p);
break;
case TOK_REMOVECONTROL:
parseRemoveControl(p);
break;
case TOK_SET:
// SET var = expr (object assignment)
advance(p); // consume SET
if (!check(p, TOK_IDENT)) {
errorExpected(p, "variable name");
break;
}
{
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);
expect(p, TOK_EQ);
parseExpression(p);
BasSymbolT *varSym = ensureVariable(p, varName);
if (varSym) {
varSym->dataType = BAS_TYPE_OBJECT;
emitStore(p, varSym);
}
}
break;
case TOK_LOAD:
// Load FormName (identifier, not string)
advance(p);
if (!check(p, TOK_IDENT)) {
errorExpected(p, "form name");
break;
}
{
uint16_t nameIdx = basAddConstant(&p->cg, p->lex.token.text, (int32_t)strlen(p->lex.token.text));
advance(p);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, nameIdx);
basEmit8(&p->cg, OP_LOAD_FORM);
basEmit8(&p->cg, OP_POP);
}
break;
case TOK_UNLOAD:
// Unload FormName (identifier, not string)
advance(p);
if (!check(p, TOK_IDENT)) {
errorExpected(p, "form name");
break;
}
{
uint16_t nameIdx = basAddConstant(&p->cg, p->lex.token.text, (int32_t)strlen(p->lex.token.text));
advance(p);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, nameIdx);
basEmit8(&p->cg, OP_LOAD_FORM);
basEmit8(&p->cg, OP_UNLOAD_FORM);
}
break;
case TOK_INPUTBOX:
// InputBox$ prompt [, title [, default]] (statement form, discard result)
advance(p);
parseExpression(p); // prompt
if (match(p, TOK_COMMA)) {
parseExpression(p); // title
} else {
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, basAddConstant(&p->cg, "", 0));
}
if (match(p, TOK_COMMA)) {
parseExpression(p); // default
} else {
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, basAddConstant(&p->cg, "", 0));
}
basEmit8(&p->cg, OP_INPUTBOX);
basEmit8(&p->cg, OP_POP); // discard result
break;
case TOK_MSGBOX: {
// MsgBox message [, flags [, title]] (statement form, discards result)
advance(p);
parseExpression(p); // message
if (match(p, TOK_COMMA)) {
parseExpression(p); // flags
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0); // default flags = MB_OK
}
if (match(p, TOK_COMMA)) {
parseExpression(p); // title
} else {
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, emptyIdx);
}
basEmit8(&p->cg, OP_MSGBOX);
basEmit8(&p->cg, OP_POP); // discard result
break;
}
case TOK_INIWRITE:
// IniWrite file, section, key, value
advance(p);
parseExpression(p); // file
expect(p, TOK_COMMA);
parseExpression(p); // section
expect(p, TOK_COMMA);
parseExpression(p); // key
expect(p, TOK_COMMA);
parseExpression(p); // value
basEmit8(&p->cg, OP_INI_WRITE);
break;
case TOK_ME: {
// Me.Show / Me.Hide / Me.CtrlName.Property = expr
advance(p); // consume Me
if (!check(p, TOK_DOT)) {
errorExpected(p, "'.' after Me");
break;
}
advance(p); // consume DOT
if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
errorExpected(p, "method or member name after Me.");
break;
}
char meMember[BAS_MAX_TOKEN_LEN];
strncpy(meMember, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
meMember[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
if (strcasecmp(meMember, "Show") == 0) {
// Me.Show [modal]
basEmit8(&p->cg, OP_ME_REF);
uint8_t modal = 0;
if (check(p, TOK_INT_LIT)) {
if (p->lex.token.intVal != 0) {
modal = 1;
}
advance(p);
} else if (check(p, TOK_IDENT)) {
BasSymbolT *modSym = basSymTabFind(&p->sym, p->lex.token.text);
if (modSym && modSym->kind == SYM_CONST && modSym->constInt != 0) {
modal = 1;
}
advance(p);
}
basEmit8(&p->cg, OP_SHOW_FORM);
basEmit8(&p->cg, modal);
} else if (strcasecmp(meMember, "Hide") == 0) {
// Me.Hide
basEmit8(&p->cg, OP_ME_REF);
basEmit8(&p->cg, OP_HIDE_FORM);
} else if (check(p, TOK_LPAREN) || check(p, TOK_DOT)) {
// Me.CtrlName(idx).Property OR Me.CtrlName.Property
bool hasIndex = check(p, TOK_LPAREN);
// Push form ref (Me), ctrl name
basEmit8(&p->cg, OP_ME_REF);
uint16_t ctrlIdx = basAddConstant(&p->cg, meMember, (int32_t)strlen(meMember));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlIdx);
if (hasIndex) {
// Me.CtrlName(idx) -- parse index, use FIND_CTRL_IDX
expect(p, TOK_LPAREN);
parseExpression(p);
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_FIND_CTRL_IDX);
} else {
basEmit8(&p->cg, OP_FIND_CTRL);
}
expect(p, TOK_DOT);
if (!check(p, TOK_IDENT)) {
errorExpected(p, "property name");
break;
}
char propName[BAS_MAX_TOKEN_LEN];
strncpy(propName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
propName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
if (check(p, TOK_EQ)) {
// Property assignment
advance(p);
uint16_t propIdx = basAddConstant(&p->cg, propName, (int32_t)strlen(propName));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, propIdx);
parseExpression(p);
basEmit8(&p->cg, OP_STORE_PROP);
} else {
// Method call
uint16_t methodIdx = basAddConstant(&p->cg, propName, (int32_t)strlen(propName));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, methodIdx);
int32_t argc = 0;
while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) {
if (argc > 0 && check(p, TOK_COMMA)) {
advance(p);
}
parseExpression(p);
argc++;
}
basEmit8(&p->cg, OP_CALL_METHOD);
basEmit8(&p->cg, (uint8_t)argc);
basEmit8(&p->cg, OP_POP);
}
} else if (check(p, TOK_EQ)) {
// Me.Property = expr (form-level property set)
advance(p); // consume =
basEmit8(&p->cg, OP_ME_REF);
uint16_t propIdx = basAddConstant(&p->cg, meMember, (int32_t)strlen(meMember));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, propIdx);
parseExpression(p);
basEmit8(&p->cg, OP_STORE_PROP);
} else {
// Me.Method [args] (form-level method call)
basEmit8(&p->cg, OP_ME_REF);
uint16_t methodIdx = basAddConstant(&p->cg, meMember, (int32_t)strlen(meMember));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, methodIdx);
int32_t argc = 0;
while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) {
if (argc > 0 && check(p, TOK_COMMA)) {
advance(p);
}
parseExpression(p);
argc++;
}
basEmit8(&p->cg, OP_CALL_METHOD);
basEmit8(&p->cg, (uint8_t)argc);
basEmit8(&p->cg, OP_POP);
}
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 form scope directives (injected by IDE)
if (checkKeyword(p, "BEGINFORM")) {
parseBeginForm(p);
break;
}
if (checkKeyword(p, "ENDFORM")) {
parseEndForm(p);
break;
}
// 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[BAS_PARSE_ERR_SCRATCH];
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[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unexpected token: %s", basTokenName(tt));
error(p, buf);
break;
}
}
if (!p->hasError) {
expectEndOfStatement(p);
}
}
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"
// Truncation is intentional -- symbol names are clamped to BAS_MAX_SYMBOL_NAME.
char mangledName[BAS_MAX_SYMBOL_NAME * 2 + 1];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-truncation"
snprintf(mangledName, sizeof(mangledName), "%s$%s", p->currentProc, varName);
#pragma GCC diagnostic pop
// 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 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;
int32_t requiredCount = 0;
bool seenOptional = false;
uint8_t paramTypes[BAS_MAX_PARAMS];
bool paramByVal[BAS_MAX_PARAMS];
bool paramOptional[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 optional = false;
if (match(p, TOK_OPTIONAL)) {
optional = true;
seenOptional = true;
} else if (seenOptional) {
error(p, "Required parameter cannot follow Optional parameter");
return;
}
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);
int32_t pUdtTypeId = -1;
if (match(p, TOK_AS)) {
pdt = resolveTypeName(p);
if (pdt == BAS_TYPE_UDT) {
pUdtTypeId = p->lastUdtTypeId;
}
}
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;
paramSym->udtTypeId = pUdtTypeId;
if (paramCount < BAS_MAX_PARAMS) {
paramTypes[paramCount] = pdt;
paramByVal[paramCount] = byVal;
paramOptional[paramCount] = optional;
}
if (!optional) {
requiredCount = paramCount + 1;
}
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->requiredParams = requiredCount;
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];
subSym->paramOptional[i] = paramOptional[i];
}
// Record the owning form so fireCtrlEvent can bind the SUB's
// form-scope variables at call time. Prescan adds SUB symbols
// before the BEGINFORM directive is consumed, so the formName
// wasn't populated by basSymTabAdd; set it here once we know we
// are inside a form scope.
if (p->sym.inFormScope && p->sym.formScopeName[0]) {
strncpy(subSym->formName, p->sym.formScopeName, BAS_MAX_SYMBOL_NAME - 1);
subSym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
}
// 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
collectDebugLocals(p, p->cg.debugProcCount++);
basSymTabLeaveLocal(&p->sym);
p->currentProc[0] = '\0';
// Patch the skip jump
patchJump(p, skipJump);
}
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 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;
}
BasFieldDefT field;
memset(&field, 0, sizeof(field));
// Truncation is intentional -- field names are clamped to BAS_MAX_SYMBOL_NAME.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-truncation"
snprintf(field.name, BAS_MAX_SYMBOL_NAME, "%s", p->lex.token.text);
#pragma GCC diagnostic pop
advance(p);
expect(p, TOK_AS);
field.dataType = resolveTypeName(p);
if (field.dataType == BAS_TYPE_UDT) {
field.udtTypeId = p->lastUdtTypeId;
}
arrput(typeSym->fields, field);
typeSym->fieldCount = (int32_t)arrlen(typeSym->fields);
expectEndOfStatement(p);
skipNewlines(p);
}
}
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;
}
parsePowExpr(p);
}
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;
}
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 parseXorExpr(BasParserT *p) {
parseAndExpr(p);
while (!p->hasError && check(p, TOK_XOR)) {
advance(p);
parseAndExpr(p);
basEmit8(&p->cg, OP_XOR);
}
}
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;
}
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 (!p->hasError && 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
}
}