// 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 #include #include // ============================================================ // Forward jump list (for EXIT FOR / EXIT DO backpatching) // ============================================================ #define MAX_EXITS 32 typedef struct { int32_t patchAddr[MAX_EXITS]; int32_t count; } ExitListT; // ============================================================ // Built-in function table // ============================================================ typedef struct { const char *name; uint8_t opcode; int32_t minArgs; int32_t maxArgs; uint8_t resultType; } BuiltinFuncT; static const BuiltinFuncT builtinFuncs[] = { // String functions {"ASC", OP_STR_ASC, 1, 1, BAS_TYPE_INTEGER}, {"CHR$", OP_STR_CHR, 1, 1, BAS_TYPE_STRING}, {"DATE$", OP_DATE_STR, 0, 0, BAS_TYPE_STRING}, {"ENVIRON$", OP_ENVIRON, 1, 1, BAS_TYPE_STRING}, {"FORMAT$", OP_FORMAT, 2, 2, BAS_TYPE_STRING}, {"HEX$", OP_STR_HEX, 1, 1, BAS_TYPE_STRING}, {"INSTR", OP_STR_INSTR, 2, 3, BAS_TYPE_INTEGER}, {"LCASE$", OP_STR_LCASE, 1, 1, BAS_TYPE_STRING}, {"LEFT$", OP_STR_LEFT, 2, 2, BAS_TYPE_STRING}, {"LEN", OP_STR_LEN, 1, 1, BAS_TYPE_INTEGER}, {"LTRIM$", OP_STR_LTRIM, 1, 1, BAS_TYPE_STRING}, {"MID$", OP_STR_MID2, 2, 3, BAS_TYPE_STRING}, {"RIGHT$", OP_STR_RIGHT, 2, 2, BAS_TYPE_STRING}, {"RTRIM$", OP_STR_RTRIM, 1, 1, BAS_TYPE_STRING}, {"SPACE$", OP_STR_SPACE, 1, 1, BAS_TYPE_STRING}, {"STR$", OP_STR_STRF, 1, 1, BAS_TYPE_STRING}, {"STRING$", OP_STR_STRING, 2, 2, BAS_TYPE_STRING}, {"TRIM$", OP_STR_TRIM, 1, 1, BAS_TYPE_STRING}, {"UCASE$", OP_STR_UCASE, 1, 1, BAS_TYPE_STRING}, {"VAL", OP_STR_VAL, 1, 1, BAS_TYPE_DOUBLE}, // File I/O functions {"FREEFILE", OP_FILE_FREEFILE, 0, 0, BAS_TYPE_INTEGER}, {"LOC", OP_FILE_LOC, 1, 1, BAS_TYPE_LONG}, {"LOF", OP_FILE_LOF, 1, 1, BAS_TYPE_LONG}, // Conversion functions {"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}, {"INT", OP_MATH_INT, 1, 1, BAS_TYPE_INTEGER}, {"LOG", OP_MATH_LOG, 1, 1, BAS_TYPE_DOUBLE}, {"RND", OP_MATH_RND, 0, 1, BAS_TYPE_DOUBLE}, {"SGN", OP_MATH_SGN, 1, 1, BAS_TYPE_INTEGER}, {"SIN", OP_MATH_SIN, 1, 1, BAS_TYPE_DOUBLE}, {"SQR", OP_MATH_SQR, 1, 1, BAS_TYPE_DOUBLE}, {"TAN", OP_MATH_TAN, 1, 1, BAS_TYPE_DOUBLE}, {"TIME$", OP_TIME_STR, 0, 0, BAS_TYPE_STRING}, {"TIMER", OP_MATH_TIMER, 0, 0, BAS_TYPE_DOUBLE}, {NULL, 0, 0, 0, 0} }; // ============================================================ // Helper prototypes (alphabetized) // ============================================================ static void addPredefConst(BasParserT *p, const char *name, int32_t val); static void addPredefConsts(BasParserT *p); static void advance(BasParserT *p); static bool checkCtrlArrayAccess(BasParserT *p); static bool check(BasParserT *p, BasTokenTypeE type); static bool checkKeyword(BasParserT *p, const char *kw); static bool checkKeywordText(const char *text, const char *kw); static void emitByRefArg(BasParserT *p); static void error(BasParserT *p, const char *msg); static void errorExpected(BasParserT *p, const char *what); static void expect(BasParserT *p, BasTokenTypeE type); static void expectEndOfStatement(BasParserT *p); static const BuiltinFuncT *findBuiltin(const char *name); static void emitUdtInit(BasParserT *p, int32_t udtTypeId); static BasSymbolT *findTypeDef(BasParserT *p, const char *name); static BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId); static bool match(BasParserT *p, BasTokenTypeE type); static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName); static uint8_t resolveTypeName(BasParserT *p); static uint8_t suffixToType(const char *name); static void skipNewlines(BasParserT *p); // ============================================================ // Expression parser prototypes (by precedence, lowest first) // ============================================================ static void parseExpression(BasParserT *p); static void parseImpExpr(BasParserT *p); static void parseEqvExpr(BasParserT *p); static void parseOrExpr(BasParserT *p); static void parseXorExpr(BasParserT *p); static void parseAndExpr(BasParserT *p); static void parseNotExpr(BasParserT *p); static void parseCompareExpr(BasParserT *p); static void parseConcatExpr(BasParserT *p); static void parseAddExpr(BasParserT *p); static void parseMulExpr(BasParserT *p); static void parsePowExpr(BasParserT *p); static void parseUnaryExpr(BasParserT *p); static void parsePrimary(BasParserT *p); // ============================================================ // Statement parser prototypes (alphabetized) // ============================================================ static void parseAssignOrCall(BasParserT *p); static void parseBeginForm(BasParserT *p); static void parseClose(BasParserT *p); static void parseConst(BasParserT *p); static void parseData(BasParserT *p); static void parseDeclare(BasParserT *p); static void parseDef(BasParserT *p); static void parseDefType(BasParserT *p, uint8_t dataType); static void parseDim(BasParserT *p); static void parseDimBounds(BasParserT *p, int32_t *outDims); static void parseDo(BasParserT *p); static void parseEnd(BasParserT *p); static void parseEndForm(BasParserT *p); static void parseErase(BasParserT *p); static void parseExit(BasParserT *p); static void parseFor(BasParserT *p); static void parseFunction(BasParserT *p); static void parseGet(BasParserT *p); static void parseGosub(BasParserT *p); static void parseGoto(BasParserT *p); static void parseIf(BasParserT *p); static void parseInput(BasParserT *p); static void parseLineInput(BasParserT *p); static void parseModule(BasParserT *p); static void parseOn(BasParserT *p); static void parseOnError(BasParserT *p); static void parseOpen(BasParserT *p); static void parseOption(BasParserT *p); static void parsePrint(BasParserT *p); static void parsePut(BasParserT *p); static void parseRead(BasParserT *p); static void parseRedim(BasParserT *p); static void parseRestore(BasParserT *p); static void parseResume(BasParserT *p); static void parseSeek(BasParserT *p); static void parseSelectCase(BasParserT *p); static void parseShell(BasParserT *p); static void parseSleep(BasParserT *p); static void parseStatement(BasParserT *p); static void parseStatic(BasParserT *p); static void parseSub(BasParserT *p); static void parseSwap(BasParserT *p); static void parseType(BasParserT *p); static void parseWhile(BasParserT *p); static void parseWrite(BasParserT *p); // ============================================================ // Variable / code emit helper prototypes (alphabetized) // ============================================================ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym); static int32_t emitJump(BasParserT *p, uint8_t opcode); static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName); static void emitLoad(BasParserT *p, BasSymbolT *sym); static void emitStore(BasParserT *p, BasSymbolT *sym); static BasSymbolT *ensureVariable(BasParserT *p, const char *name); static void patchCallAddrs(BasParserT *p, BasSymbolT *sym); static void patchJump(BasParserT *p, int32_t addr); static void patchLabelRefs(BasParserT *p, BasSymbolT *sym); // ============================================================ // Exit list helpers // ============================================================ static ExitListT exitForList; static ExitListT exitDoList; static ExitListT exitSubList; static ExitListT exitFuncList; static void exitListInit(ExitListT *el); static void exitListAdd(ExitListT *el, int32_t addr); static void exitListPatch(ExitListT *el, BasParserT *p); // ============================================================ // Helper implementations // ============================================================ static void 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); } // 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 void advance(BasParserT *p) { if (p->hasError) { return; } basLexerNext(&p->lex); if (p->lex.token.type == TOK_ERROR) { error(p, p->lex.error); } } static bool check(BasParserT *p, BasTokenTypeE type) { return p->lex.token.type == type; } static bool checkKeyword(BasParserT *p, const char *kw) { if (p->lex.token.type != TOK_IDENT) { return false; } // Case-insensitive comparison const char *a = p->lex.token.text; const char *b = kw; while (*a && *b) { if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) { return false; } a++; b++; } return *a == '\0' && *b == '\0'; } static bool checkKeywordText(const char *text, const char *kw) { const char *a = text; const char *b = kw; while (*a && *b) { if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) { return false; } a++; b++; } return *a == '\0' && *b == '\0'; } static void error(BasParserT *p, const char *msg) { if (p->hasError) { return; } p->hasError = true; p->errorLine = p->lex.token.line; snprintf(p->error, sizeof(p->error), "Line %d: %s", (int)p->lex.token.line, msg); } static void errorExpected(BasParserT *p, const char *what) { char buf[512]; snprintf(buf, sizeof(buf), "Expected %s, got %s", what, basTokenName(p->lex.token.type)); error(p, buf); } static void exitListAdd(ExitListT *el, int32_t addr) { if (el->count < MAX_EXITS) { el->patchAddr[el->count++] = addr; } } static void exitListInit(ExitListT *el) { el->count = 0; } static void exitListPatch(ExitListT *el, BasParserT *p) { int32_t target = basCodePos(&p->cg); for (int32_t i = 0; i < el->count; i++) { int16_t offset = (int16_t)(target - (el->patchAddr[i] + 2)); basPatch16(&p->cg, el->patchAddr[i], offset); } el->count = 0; } static void expect(BasParserT *p, BasTokenTypeE type) { if (p->hasError) { return; } if (p->lex.token.type != type) { char buf[512]; snprintf(buf, sizeof(buf), "Expected %s, got %s", basTokenName(type), basTokenName(p->lex.token.type)); error(p, buf); return; } advance(p); } static void expectEndOfStatement(BasParserT *p) { if (p->hasError) { return; } // Statement must end with newline, colon, EOF, or ELSE (single-line IF) if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_ELSE)) { return; } if (check(p, TOK_COLON)) { advance(p); return; } errorExpected(p, "end of statement"); } static const BuiltinFuncT *findBuiltin(const char *name) { for (int32_t i = 0; builtinFuncs[i].name != NULL; i++) { // Case-insensitive comparison const char *a = name; const char *b = builtinFuncs[i].name; bool match = true; while (*a && *b) { if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) { match = false; break; } a++; b++; } if (match && *a == '\0' && *b == '\0') { return &builtinFuncs[i]; } } return NULL; } static BasSymbolT *findTypeDef(BasParserT *p, const char *name) { for (int32_t i = 0; i < p->sym.count; i++) { if (p->sym.symbols[i].kind == SYM_TYPE_DEF) { // Case-insensitive comparison const char *a = p->sym.symbols[i].name; const char *b = name; bool eq = true; while (*a && *b) { if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) { eq = false; break; } a++; b++; } if (eq && *a == '\0' && *b == '\0') { return &p->sym.symbols[i]; } } } return NULL; } static 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; } // 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 bool match(BasParserT *p, BasTokenTypeE type) { if (p->lex.token.type == type) { advance(p); return true; } return false; } static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName) { for (int32_t i = 0; i < typeSym->fieldCount; i++) { const char *a = typeSym->fields[i].name; const char *b = fieldName; bool eq = true; while (*a && *b) { if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) { eq = false; break; } a++; b++; } if (eq && *a == '\0' && *b == '\0') { return i; } } return -1; } static uint8_t resolveTypeName(BasParserT *p) { // Expect a type keyword after AS if (check(p, TOK_INTEGER)) { advance(p); return BAS_TYPE_INTEGER; } if (check(p, TOK_LONG)) { advance(p); return BAS_TYPE_LONG; } if (check(p, TOK_SINGLE)) { advance(p); return BAS_TYPE_SINGLE; } if (check(p, TOK_DOUBLE)) { advance(p); return BAS_TYPE_DOUBLE; } if (check(p, TOK_STRING_KW)) { advance(p); return BAS_TYPE_STRING; } if (check(p, TOK_BOOLEAN)) { advance(p); return BAS_TYPE_BOOLEAN; } // Check for user-defined TYPE name if (check(p, TOK_IDENT)) { BasSymbolT *typeSym = findTypeDef(p, p->lex.token.text); if (typeSym != NULL) { p->lastUdtTypeId = typeSym->index; advance(p); return BAS_TYPE_UDT; } } error(p, "Expected type name (Integer, Long, Single, Double, String, Boolean, or TYPE name)"); return BAS_TYPE_INTEGER; } // 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); } } } // 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); } } } 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 } } // ============================================================ // Variable / code emit helpers // ============================================================ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) { // Parse argument list expect(p, TOK_LPAREN); int32_t argc = 0; if (!check(p, TOK_RPAREN)) { 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; } if (argc != sym->paramCount) { char buf[256]; snprintf(buf, sizeof(buf), "Function '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc); error(p, buf); return; } // 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); } } // 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 simple variable (not array, not const) BasSymbolT *sym = basSymTabFind(&p->sym, name); if (!sym || sym->kind != SYM_VARIABLE || sym->isArray) { 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 // 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 BasSymbolT *ensureVariable(BasParserT *p, const char *name) { BasSymbolT *sym = basSymTabFind(&p->sym, name); if (sym != NULL) { return sym; } // When in local scope, check if a shared global exists before auto-declaring if (p->sym.inLocalScope) { BasSymbolT *globalSym = basSymTabFindGlobal(&p->sym, name); if (globalSym != NULL && globalSym->isShared) { return globalSym; } } // OPTION EXPLICIT: require explicit DIM if (p->optionExplicit) { char buf[320]; snprintf(buf, sizeof(buf), "Variable not declared: %s (OPTION EXPLICIT is on)", name); error(p, buf); return NULL; } // Auto-declare (QB implicit declaration) // Use suffix type if present, otherwise defType for the first letter uint8_t dt = suffixToType(name); if (dt == BAS_TYPE_SINGLE && name[0] != '\0') { // suffixToType returns SINGLE as the default when no suffix. // Check if defType overrides it. char firstLetter = name[0]; if (firstLetter >= 'a' && firstLetter <= 'z') { firstLetter -= 32; } if (firstLetter >= 'A' && firstLetter <= 'Z') { uint8_t defDt = p->defType[firstLetter - 'A']; if (defDt != 0) { dt = defDt; } } } sym = basSymTabAdd(&p->sym, name, SYM_VARIABLE, dt); if (sym == NULL) { error(p, "Symbol table full"); return NULL; } sym->scope = SCOPE_GLOBAL; sym->index = basSymTabAllocSlot(&p->sym); sym->isDefined = true; return sym; } static void patchCallAddrs(BasParserT *p, BasSymbolT *sym) { // Backpatch all forward-reference CALL addresses uint16_t addr = (uint16_t)sym->codeAddr; for (int32_t i = 0; i < sym->patchCount; i++) { int32_t pos = sym->patchAddrs[i]; if (pos >= 0 && pos + 2 <= p->cg.codeLen) { memcpy(&p->cg.code[pos], &addr, sizeof(uint16_t)); } } sym->patchCount = 0; } static void patchJump(BasParserT *p, int32_t addr) { int32_t target = basCodePos(&p->cg); int16_t offset = (int16_t)(target - (addr + 2)); basPatch16(&p->cg, addr, offset); } static void patchLabelRefs(BasParserT *p, BasSymbolT *sym) { // Backpatch all forward-reference jumps to this label int32_t target = sym->codeAddr; for (int32_t i = 0; i < sym->patchCount; i++) { int32_t patchAddr = sym->patchAddrs[i]; int16_t offset = (int16_t)(target - (patchAddr + 2)); basPatch16(&p->cg, patchAddr, offset); } sym->patchCount = 0; } // ============================================================ // Expression parsing // ============================================================ static void parseExpression(BasParserT *p) { parseImpExpr(p); } static void parseImpExpr(BasParserT *p) { parseEqvExpr(p); while (!p->hasError && check(p, TOK_IMP)) { advance(p); parseEqvExpr(p); basEmit8(&p->cg, OP_IMP); } } static void parseEqvExpr(BasParserT *p) { parseOrExpr(p); while (!p->hasError && check(p, TOK_EQV)) { advance(p); parseOrExpr(p); basEmit8(&p->cg, OP_EQV); } } static void parseOrExpr(BasParserT *p) { parseXorExpr(p); while (!p->hasError && check(p, TOK_OR)) { advance(p); parseXorExpr(p); basEmit8(&p->cg, OP_OR); } } static void parseXorExpr(BasParserT *p) { parseAndExpr(p); while (!p->hasError && check(p, TOK_XOR)) { advance(p); parseAndExpr(p); basEmit8(&p->cg, OP_XOR); } } static void parseAndExpr(BasParserT *p) { parseNotExpr(p); while (!p->hasError && check(p, TOK_AND)) { advance(p); parseNotExpr(p); basEmit8(&p->cg, OP_AND); } } static void parseNotExpr(BasParserT *p) { if (check(p, TOK_NOT)) { advance(p); parseNotExpr(p); basEmit8(&p->cg, OP_NOT); return; } parseCompareExpr(p); } static void parseCompareExpr(BasParserT *p) { parseConcatExpr(p); while (!p->hasError) { if (check(p, TOK_EQ)) { advance(p); parseConcatExpr(p); basEmit8(&p->cg, OP_CMP_EQ); } else if (check(p, TOK_NE)) { advance(p); parseConcatExpr(p); basEmit8(&p->cg, OP_CMP_NE); } else if (check(p, TOK_LT)) { advance(p); parseConcatExpr(p); basEmit8(&p->cg, OP_CMP_LT); } else if (check(p, TOK_GT)) { advance(p); parseConcatExpr(p); basEmit8(&p->cg, OP_CMP_GT); } else if (check(p, TOK_LE)) { advance(p); parseConcatExpr(p); basEmit8(&p->cg, OP_CMP_LE); } else if (check(p, TOK_GE)) { advance(p); parseConcatExpr(p); basEmit8(&p->cg, OP_CMP_GE); } else { break; } } } static void parseConcatExpr(BasParserT *p) { parseAddExpr(p); while (!p->hasError && check(p, TOK_AMPERSAND)) { advance(p); parseAddExpr(p); basEmit8(&p->cg, OP_STR_CONCAT); } } static void parseAddExpr(BasParserT *p) { parseMulExpr(p); while (!p->hasError) { if (check(p, TOK_PLUS)) { advance(p); parseMulExpr(p); basEmit8(&p->cg, OP_ADD_INT); // VM handles type promotion } else if (check(p, TOK_MINUS)) { advance(p); parseMulExpr(p); basEmit8(&p->cg, OP_SUB_INT); } else { break; } } } // 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 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 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")) { 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 >= -32768 && val <= 32767) { basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, (int16_t)val); } else { basEmit8(&p->cg, OP_PUSH_INT32); basEmit16(&p->cg, (int16_t)(val & 0xFFFF)); basEmit16(&p->cg, (int16_t)((val >> 16) & 0xFFFF)); } advance(p); return; } // Long literal if (tt == TOK_LONG_LIT) { int32_t val = (int32_t)p->lex.token.longVal; basEmit8(&p->cg, OP_PUSH_INT32); basEmit16(&p->cg, (int16_t)(val & 0xFFFF)); basEmit16(&p->cg, (int16_t)((val >> 16) & 0xFFFF)); advance(p); return; } // Float literal if (tt == TOK_FLOAT_LIT) { basEmit8(&p->cg, OP_PUSH_FLT64); basEmitDouble(&p->cg, p->lex.token.dblVal); advance(p); return; } // String literal if (tt == TOK_STRING_LIT) { uint16_t idx = basAddConstant(&p->cg, p->lex.token.text, p->lex.token.textLen); basEmit8(&p->cg, OP_PUSH_STR); basEmitU16(&p->cg, idx); advance(p); return; } // Boolean literals if (tt == TOK_TRUE_KW) { basEmit8(&p->cg, OP_PUSH_TRUE); advance(p); return; } if (tt == TOK_FALSE_KW) { basEmit8(&p->cg, OP_PUSH_FALSE); advance(p); return; } // Me -- reference to current form if (tt == TOK_ME) { advance(p); basEmit8(&p->cg, OP_ME_REF); 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; } // 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]) -- as function expression 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 } expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_MSGBOX); return; } // SQL expression functions -- all require parentheses if (tt == TOK_SQLOPEN) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_SQL_OPEN); 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; } if (tt == TOK_SQLERROR) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_SQL_ERROR); return; } if (tt == TOK_SQLQUERY) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); // db expect(p, TOK_COMMA); parseExpression(p); // sql expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_SQL_QUERY); return; } if (tt == TOK_SQLEOF) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_SQL_EOF); return; } if (tt == TOK_SQLFIELDCOUNT) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_SQL_FIELD_COUNT); return; } if (tt == TOK_SQLFIELD) { // SQLField$(rs, col) or SQLField$(rs, "name") // If second arg is a string, use OP_SQL_FIELD_BYNAME; else OP_SQL_FIELD_TEXT advance(p); expect(p, TOK_LPAREN); parseExpression(p); // rs expect(p, TOK_COMMA); parseExpression(p); // col or name expect(p, TOK_RPAREN); // Runtime will determine type — use BYNAME if string, TEXT if int. // For simplicity, always use BYNAME (works with both int index and string name // since the C function handles both patterns). Actually we need separate opcodes. // Use a simple heuristic: if the arg is a string literal, use BYNAME. // For now, default to BYNAME which is more flexible. basEmit8(&p->cg, OP_SQL_FIELD_BYNAME); return; } if (tt == TOK_SQLFIELDINT) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); // rs expect(p, TOK_COMMA); parseExpression(p); // col expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_SQL_FIELD_INT); return; } if (tt == TOK_SQLFIELDDBL) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); // rs expect(p, TOK_COMMA); parseExpression(p); // col expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_SQL_FIELD_DBL); return; } if (tt == TOK_SQLAFFECTED) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_SQL_AFFECTED); return; } if (tt == TOK_SQLEXEC) { // SQLExec as expression: SQLExec(db, sql) returns bool advance(p); expect(p, TOK_LPAREN); parseExpression(p); // db expect(p, TOK_COMMA); parseExpression(p); // sql expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_SQL_EXEC); return; } if (tt == TOK_SQLNEXT) { // SQLNext as expression: SQLNext(rs) returns bool advance(p); expect(p, TOK_LPAREN); parseExpression(p); expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_SQL_NEXT); return; } // SHELL("command") -- as function expression if (tt == TOK_SHELL) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_SHELL); return; } // LBOUND(array [, dim]) if (tt == TOK_LBOUND) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); uint8_t dim = 1; if (match(p, TOK_COMMA)) { if (check(p, TOK_INT_LIT)) { dim = (uint8_t)p->lex.token.intVal; advance(p); } else { error(p, "LBOUND dimension must be a constant integer"); } } expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_LBOUND); basEmit8(&p->cg, dim); return; } // UBOUND(array [, dim]) if (tt == TOK_UBOUND) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); uint8_t dim = 1; if (match(p, TOK_COMMA)) { if (check(p, TOK_INT_LIT)) { dim = (uint8_t)p->lex.token.intVal; advance(p); } else { error(p, "UBOUND dimension must be a constant integer"); } } expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_UBOUND); basEmit8(&p->cg, dim); return; } // Parenthesized expression if (tt == TOK_LPAREN) { advance(p); parseExpression(p); expect(p, TOK_RPAREN); return; } // Identifier: variable, function call, or built-in if (tt == TOK_IDENT) { char name[BAS_MAX_TOKEN_LEN]; strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); name[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); // INPUT$(n, #channel) -- special handling for optional # in second arg if (checkKeywordText(name, "INPUT$") && check(p, TOK_LPAREN)) { expect(p, TOK_LPAREN); parseExpression(p); // n (number of chars) expect(p, TOK_COMMA); match(p, TOK_HASH); // optional # parseExpression(p); // channel number expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_FILE_INPUT_N); return; } // Check for built-in function const BuiltinFuncT *builtin = findBuiltin(name); if (builtin != NULL) { int32_t argc = 0; // Zero-arg builtins can be used without parens if (builtin->minArgs == 0 && builtin->maxArgs == 0 && !check(p, TOK_LPAREN)) { basEmit8(&p->cg, builtin->opcode); return; } if (check(p, TOK_LPAREN)) { expect(p, TOK_LPAREN); // RND/zero-arg builtins can be called with empty parens if (!check(p, TOK_RPAREN)) { parseExpression(p); argc++; while (match(p, TOK_COMMA)) { parseExpression(p); argc++; } } expect(p, TOK_RPAREN); } if (p->hasError) { return; } if (argc < builtin->minArgs || argc > builtin->maxArgs) { char buf[256]; snprintf(buf, sizeof(buf), "Built-in '%s' expects %d-%d arguments, got %d", builtin->name, (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[512]; 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[512]; 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 read: CtrlName.Property advance(p); // consume 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); // Emit: push NULL (current form), push ctrl name, FIND_CTRL 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, LOAD_PROP 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"); } // ============================================================ // Statement parsing // ============================================================ static void parseAssignOrCall(BasParserT *p) { char name[BAS_MAX_TOKEN_LEN]; strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); name[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); // MID$ statement: MID$(var$, start [, len]) = replacement$ if (checkKeywordText(name, "MID$") && check(p, TOK_LPAREN)) { expect(p, TOK_LPAREN); // First arg: target string variable if (!check(p, TOK_IDENT)) { errorExpected(p, "string variable name"); return; } char varName[BAS_MAX_TOKEN_LEN]; strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); BasSymbolT *varSym = ensureVariable(p, varName); if (varSym == NULL) { return; } // Load the original string emitLoad(p, varSym); expect(p, TOK_COMMA); parseExpression(p); // start position // Optional length if (match(p, TOK_COMMA)) { parseExpression(p); // length } else { // Push 0 as sentinel meaning "use replacement length" basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, 0); } expect(p, TOK_RPAREN); expect(p, TOK_EQ); // Parse replacement expression parseExpression(p); // Emit MID$ assignment: pops replacement, len, start, str; pushes result basEmit8(&p->cg, OP_STR_MID_ASGN); // Store back to the variable emitStore(p, varSym); return; } BasSymbolT *sym = basSymTabFind(&p->sym, name); // 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[512]; 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 if (!check(p, TOK_IDENT) && !check(p, TOK_SHOW) && !check(p, TOK_HIDE)) { 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 // Special form methods: Show, Hide if (strcasecmp(memberName, "Show") == 0) { // name.Show [modal] // Push form name, LOAD_FORM, SHOW_FORM 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) { 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 = // Push ctrl ref: push form ref (NULL = current), push ctrl name, FIND_CTRL basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, 0); BasValueT formNull; memset(&formNull, 0, sizeof(formNull)); // Use OP_PUSH_STR for ctrl name, then FIND_CTRL 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] // Push ctrl ref 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 (!check(p, TOK_IDENT) && !check(p, TOK_SHOW) && !check(p, TOK_HIDE)) { 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[512]; 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++; } } if (!p->hasError && argc != sym->paramCount) { char buf[256]; snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc); error(p, buf); return; } { uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0; basEmit8(&p->cg, OP_CALL); int32_t addrPos = basCodePos(&p->cg); basEmitU16(&p->cg, (uint16_t)sym->codeAddr); basEmit8(&p->cg, (uint8_t)argc); basEmit8(&p->cg, baseSlot); if (!sym->isDefined && 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 '('"); } // ============================================================ // parseBeginForm / parseEndForm -- form scope directives // ============================================================ // // 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 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 parseClose(BasParserT *p) { // CLOSE #channel advance(p); // consume CLOSE // Optional # prefix match(p, TOK_HASH); // Channel number parseExpression(p); basEmit8(&p->cg, OP_FILE_CLOSE); } static void parseConst(BasParserT *p) { // CONST name = value advance(p); // consume CONST if (!check(p, TOK_IDENT)) { errorExpected(p, "constant name"); return; } char name[BAS_MAX_TOKEN_LEN]; strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); name[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); expect(p, TOK_EQ); // Parse the constant value (must be a literal) bool isNeg = false; if (check(p, TOK_MINUS)) { isNeg = true; advance(p); } BasSymbolT *sym = NULL; if (check(p, TOK_INT_LIT) || check(p, TOK_LONG_LIT)) { int32_t val = check(p, TOK_INT_LIT) ? p->lex.token.intVal : (int32_t)p->lex.token.longVal; if (isNeg) { val = -val; } sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_LONG); if (sym != NULL) { sym->constInt = val; sym->isDefined = true; sym->scope = SCOPE_GLOBAL; } advance(p); } else if (check(p, TOK_FLOAT_LIT)) { double val = p->lex.token.dblVal; if (isNeg) { val = -val; } sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_DOUBLE); if (sym != NULL) { sym->constDbl = val; sym->isDefined = true; sym->scope = SCOPE_GLOBAL; } advance(p); } else if (check(p, TOK_STRING_LIT) && !isNeg) { sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_STRING); if (sym != NULL) { strncpy(sym->constStr, p->lex.token.text, sizeof(sym->constStr) - 1); sym->constStr[sizeof(sym->constStr) - 1] = '\0'; sym->isDefined = true; sym->scope = SCOPE_GLOBAL; } advance(p); } else if (check(p, TOK_TRUE_KW) && !isNeg) { sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_BOOLEAN); if (sym != NULL) { sym->constInt = -1; sym->isDefined = true; sym->scope = SCOPE_GLOBAL; } advance(p); } else if (check(p, TOK_FALSE_KW) && !isNeg) { sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_BOOLEAN); if (sym != NULL) { sym->constInt = 0; sym->isDefined = true; sym->scope = SCOPE_GLOBAL; } advance(p); } else { error(p, "Constant value must be a literal"); } if (sym == NULL && !p->hasError) { error(p, "Duplicate constant or symbol table full"); } } static void parseData(BasParserT *p) { // DATA val1, val2, "string", ... // Collect all values into the data pool. No runtime code is emitted. advance(p); // consume DATA for (;;) { if (p->hasError) { return; } bool isNeg = false; if (check(p, TOK_MINUS)) { isNeg = true; advance(p); } if (check(p, TOK_INT_LIT)) { int32_t val = p->lex.token.intVal; if (isNeg) { val = -val; } BasValueT v = basValInteger((int16_t)val); basAddData(&p->cg, v); advance(p); } else if (check(p, TOK_LONG_LIT)) { int32_t val = (int32_t)p->lex.token.longVal; if (isNeg) { val = -val; } BasValueT v = basValLong(val); basAddData(&p->cg, v); advance(p); } else if (check(p, TOK_FLOAT_LIT)) { double val = p->lex.token.dblVal; if (isNeg) { val = -val; } BasValueT v = basValDouble(val); basAddData(&p->cg, v); advance(p); } else if (check(p, TOK_STRING_LIT) && !isNeg) { BasValueT v = basValStringFromC(p->lex.token.text); basAddData(&p->cg, v); basValRelease(&v); advance(p); } else { // Unquoted text -- read as string up to comma/newline/EOF // In QB, unquoted DATA values are treated as strings if (isNeg) { // Negative sign without a number -- treat "-" as string data BasValueT v = basValStringFromC("-"); basAddData(&p->cg, v); basValRelease(&v); } else if (check(p, TOK_IDENT)) { BasValueT v = basValStringFromC(p->lex.token.text); basAddData(&p->cg, v); basValRelease(&v); advance(p); } else { error(p, "Expected DATA value"); return; } } if (!match(p, TOK_COMMA)) { break; } } } static void parseDeclareLibrary(BasParserT *p); 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]; } } // ============================================================ // parseDeclareLibrary -- DECLARE LIBRARY "name" / END DECLARE // ============================================================ // // 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'; uint16_t funcNameIdx = basAddConstant(&p->cg, funcName, (int32_t)strlen(funcName)); 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 = 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); if (match(p, TOK_AS)) { pdt = resolveTypeName(p); } BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); if (paramSym == NULL) { error(p, "Symbol table full"); return; } paramSym->scope = SCOPE_LOCAL; paramSym->index = basSymTabAllocSlot(&p->sym); paramSym->isDefined = true; if (paramCount < BAS_MAX_PARAMS) { paramTypes[paramCount] = pdt; paramByVal[paramCount] = true; } paramCount++; } expect(p, TOK_RPAREN); } expect(p, TOK_EQ); parseExpression(p); basEmit8(&p->cg, OP_RET_VAL); 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); } // ============================================================ // parseDefType -- DEFINT, DEFLNG, DEFSNG, DEFDBL, DEFSTR // ============================================================ // // Sets the default type for variables whose names start with // letters in the given range. Example: DEFINT A-Z makes all // untyped variables default to INTEGER. static void parseDefType(BasParserT *p, uint8_t dataType) { advance(p); // consume DEFxxx keyword while (!p->hasError) { if (!check(p, TOK_IDENT)) { errorExpected(p, "letter or letter range"); return; } char startLetter = p->lex.token.text[0]; if (startLetter >= 'a' && startLetter <= 'z') { startLetter -= 32; } if (startLetter < 'A' || startLetter > 'Z') { error(p, "Expected letter A-Z"); return; } advance(p); char endLetter = startLetter; if (match(p, TOK_MINUS)) { if (!check(p, TOK_IDENT)) { errorExpected(p, "letter after '-'"); return; } endLetter = p->lex.token.text[0]; if (endLetter >= 'a' && endLetter <= 'z') { endLetter -= 32; } if (endLetter < 'A' || endLetter > 'Z') { error(p, "Expected letter A-Z"); return; } advance(p); } // Set default type for the range for (char c = startLetter; c <= endLetter; c++) { p->defType[c - 'A'] = dataType; } if (!match(p, TOK_COMMA)) { break; } } } static void parseDimBounds(BasParserT *p, int32_t *outDims) { // Parse each dimension bound, pushing (lbound, ubound) pairs onto the stack. // Supports both "ubound" (lbound=optionBase) and "lbound TO ubound" syntax. *outDims = 0; for (;;) { // Save code position before parsing the first expression int32_t exprStart = basCodePos(&p->cg); parseExpression(p); if (match(p, TOK_TO)) { // "lbound TO ubound" -- first expr is lbound, parse ubound next parseExpression(p); } else { // Single value = ubound, lbound defaults to optionBase. // Ubound expression already emitted. Insert PUSH_INT16 before it. int32_t exprLen = basCodePos(&p->cg) - exprStart; int32_t insertLen = 3; // OP_PUSH_INT16 + 2 bytes { // 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 parseDim(BasParserT *p) { // DIM [SHARED] var AS type // DIM var(ubound) AS type // DIM var(lbound TO ubound) AS type // DIM var AS UdtType advance(p); // consume DIM // Check for SHARED keyword bool isShared = false; if (check(p, TOK_SHARED)) { isShared = true; advance(p); } if (!check(p, TOK_IDENT)) { errorExpected(p, "variable name"); return; } char name[BAS_MAX_TOKEN_LEN]; strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); name[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); bool isArray = false; int32_t dims = 0; // Check for array bounds if (check(p, TOK_LPAREN)) { isArray = true; advance(p); parseDimBounds(p, &dims); expect(p, TOK_RPAREN); } // Optional AS type uint8_t dt = suffixToType(name); int32_t udtTypeId = -1; int32_t fixedLen = 0; if (match(p, TOK_AS)) { dt = resolveTypeName(p); if (dt == BAS_TYPE_UDT) { udtTypeId = p->lastUdtTypeId; } // Check for STRING * n (fixed-length string) if (dt == BAS_TYPE_STRING && check(p, TOK_STAR)) { advance(p); if (check(p, TOK_INT_LIT)) { fixedLen = p->lex.token.intVal; advance(p); } else { error(p, "Expected integer after STRING *"); } } } if (p->hasError) { return; } // Check for duplicate BasSymbolT *existing = basSymTabFind(&p->sym, name); if (existing != NULL && existing->isDefined) { char buf[256]; snprintf(buf, sizeof(buf), "Variable '%s' already declared", name); error(p, buf); return; } BasSymbolT *sym = basSymTabAdd(&p->sym, name, SYM_VARIABLE, dt); if (sym == NULL) { error(p, "Symbol table full or duplicate name"); return; } sym->index = basSymTabAllocSlot(&p->sym); sym->isDefined = true; sym->isArray = isArray; sym->isShared = isShared; sym->udtTypeId = udtTypeId; sym->fixedLen = fixedLen; if (p->sym.inLocalScope) { sym->scope = SCOPE_LOCAL; } else 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); } } } 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 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 parseFor(BasParserT *p) { // FOR var = start TO limit [STEP step] // ... // NEXT [var] advance(p); // consume FOR ExitListT savedExitFor = exitForList; exitListInit(&exitForList); // Loop variable if (!check(p, TOK_IDENT)) { errorExpected(p, "loop variable"); return; } char varName[BAS_MAX_TOKEN_LEN]; strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); BasSymbolT *loopVar = ensureVariable(p, varName); if (loopVar == NULL) { return; } // = start expect(p, TOK_EQ); parseExpression(p); emitStore(p, loopVar); // TO limit expect(p, TOK_TO); parseExpression(p); // limit is on stack // STEP step (optional, default 1) if (match(p, TOK_STEP)) { parseExpression(p); // step is on stack } else { // Default step = 1 basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, 1); } // Emit FOR_INIT -- sets up the for-loop state in the VM basEmit8(&p->cg, OP_FOR_INIT); basEmitU16(&p->cg, (uint16_t)loopVar->index); basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : (loopVar->scope == SCOPE_FORM ? 2 : 0)); int32_t loopBody = basCodePos(&p->cg); expectEndOfStatement(p); skipNewlines(p); // Loop body while (!p->hasError && !check(p, TOK_NEXT) && !check(p, TOK_EOF)) { parseStatement(p); skipNewlines(p); } if (p->hasError) { return; } expect(p, TOK_NEXT); // Optional variable name after NEXT (we just skip it) if (check(p, TOK_IDENT)) { advance(p); } // Emit FOR_NEXT with backward jump to loop body basEmit8(&p->cg, OP_FOR_NEXT); basEmitU16(&p->cg, (uint16_t)loopVar->index); basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : (loopVar->scope == SCOPE_FORM ? 2 : 0)); int16_t backOffset = (int16_t)(loopBody - (basCodePos(&p->cg) + 2)); basEmit16(&p->cg, backOffset); // Patch all EXIT FOR jumps to here exitListPatch(&exitForList, p); exitForList = savedExitFor; } static void parseFunction(BasParserT *p) { // FUNCTION name(params) AS type // ... // END FUNCTION advance(p); // consume FUNCTION if (!check(p, TOK_IDENT)) { errorExpected(p, "function name"); return; } char name[BAS_MAX_TOKEN_LEN]; strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); name[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); // Save current proc name for STATIC variable mangling strncpy(p->currentProc, name, BAS_MAX_TOKEN_LEN - 1); p->currentProc[BAS_MAX_TOKEN_LEN - 1] = '\0'; // Jump over the function body in module-level code int32_t skipJump = emitJump(p, OP_JMP); int32_t funcAddr = basCodePos(&p->cg); // Enter local scope basSymTabEnterLocal(&p->sym); ExitListT savedExitFunc = exitFuncList; exitListInit(&exitFuncList); // Allocate slot 0 for return value basSymTabAllocSlot(&p->sym); // Parse parameter list int32_t paramCount = 0; uint8_t paramTypes[BAS_MAX_PARAMS]; bool paramByVal[BAS_MAX_PARAMS]; if (match(p, TOK_LPAREN)) { while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) { if (paramCount > 0) { expect(p, TOK_COMMA); } bool byVal = false; if (match(p, TOK_BYVAL)) { byVal = true; } if (!check(p, TOK_IDENT)) { errorExpected(p, "parameter name"); return; } char paramName[BAS_MAX_TOKEN_LEN]; strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); uint8_t pdt = suffixToType(paramName); if (match(p, TOK_AS)) { pdt = resolveTypeName(p); } BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); if (paramSym == NULL) { error(p, "Symbol table full"); return; } paramSym->scope = SCOPE_LOCAL; paramSym->index = basSymTabAllocSlot(&p->sym); paramSym->isDefined = true; if (paramCount < BAS_MAX_PARAMS) { paramTypes[paramCount] = pdt; paramByVal[paramCount] = byVal; } paramCount++; } expect(p, TOK_RPAREN); } // Return type uint8_t returnType = suffixToType(name); if (match(p, TOK_AS)) { returnType = resolveTypeName(p); } // Register the function in the symbol table (global scope entry) // We need to temporarily leave local scope to add to global BasSymbolT *existing = basSymTabFindGlobal(&p->sym, name); BasSymbolT *funcSym = NULL; if (existing != NULL && existing->kind == SYM_FUNCTION) { // Forward-declared, now define it funcSym = existing; } else { // Temporarily store the local state, add globally bool savedLocal = p->sym.inLocalScope; p->sym.inLocalScope = false; funcSym = basSymTabAdd(&p->sym, name, SYM_FUNCTION, returnType); p->sym.inLocalScope = savedLocal; } if (funcSym == NULL) { error(p, "Could not register function"); return; } funcSym->codeAddr = funcAddr; funcSym->isDefined = true; funcSym->paramCount = paramCount; funcSym->scope = SCOPE_GLOBAL; for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { funcSym->paramTypes[i] = paramTypes[i]; funcSym->paramByVal[i] = paramByVal[i]; } // Backpatch any forward-reference calls to this function patchCallAddrs(p, funcSym); expectEndOfStatement(p); skipNewlines(p); // Parse function body while (!p->hasError && !check(p, TOK_EOF)) { // Check for END FUNCTION if (check(p, TOK_END)) { // Peek ahead -- we need to see if it's END FUNCTION BasLexerT savedLex = p->lex; advance(p); if (check(p, TOK_FUNCTION)) { advance(p); break; } // Not END FUNCTION, restore and parse as statement p->lex = savedLex; } parseStatement(p); skipNewlines(p); } // Patch EXIT FUNCTION jumps exitListPatch(&exitFuncList, p); exitFuncList = savedExitFunc; // Load return value from slot 0 and return basEmit8(&p->cg, OP_LOAD_LOCAL); basEmitU16(&p->cg, 0); basEmit8(&p->cg, OP_RET_VAL); // Leave local scope collectDebugLocals(p, p->cg.debugProcCount++); basSymTabLeaveLocal(&p->sym); p->currentProc[0] = '\0'; // Patch the skip jump patchJump(p, skipJump); } static void parseGosub(BasParserT *p) { // GOSUB label -- push return PC, then JMP to label advance(p); // consume GOSUB if (!check(p, TOK_IDENT)) { errorExpected(p, "label name"); return; } char labelName[BAS_MAX_TOKEN_LEN]; strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); labelName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); // Push the return PC (address after the JMP instruction) // OP_PUSH_INT32 = 1 + 4 bytes, OP_JMP = 1 + 2 bytes int32_t pushPos = basCodePos(&p->cg); basEmit8(&p->cg, OP_PUSH_INT32); basEmit16(&p->cg, 0); // placeholder lo basEmit16(&p->cg, 0); // placeholder hi // Emit the jump to the label emitJumpToLabel(p, OP_JMP, labelName); // Backpatch the return address (PC is now right after the JMP) int32_t returnPc = basCodePos(&p->cg); int16_t lo = (int16_t)(returnPc & 0xFFFF); int16_t hi = (int16_t)((returnPc >> 16) & 0xFFFF); basPatch16(&p->cg, pushPos + 1, lo); basPatch16(&p->cg, pushPos + 3, hi); } static void parseGoto(BasParserT *p) { // GOTO label advance(p); // consume GOTO if (!check(p, TOK_IDENT)) { errorExpected(p, "label name"); return; } char labelName[BAS_MAX_TOKEN_LEN]; strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); labelName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); emitJumpToLabel(p, OP_JMP, labelName); } static void parseIf(BasParserT *p) { // IF expr THEN // ... // [ELSEIF expr THEN] // ... // [ELSE] // ... // END IF advance(p); // consume IF parseExpression(p); expect(p, TOK_THEN); if (p->hasError) { return; } // Check for single-line IF: IF cond THEN stmt if (!check(p, TOK_NEWLINE) && !check(p, TOK_EOF)) { // Single-line IF int32_t falseJump = emitJump(p, OP_JMP_FALSE); parseStatement(p); if (check(p, TOK_ELSE)) { advance(p); int32_t endJump = emitJump(p, OP_JMP); patchJump(p, falseJump); parseStatement(p); patchJump(p, endJump); } else { patchJump(p, falseJump); } return; } // Multi-line IF expectEndOfStatement(p); skipNewlines(p); int32_t falseJump = emitJump(p, OP_JMP_FALSE); // Collect end-of-chain jumps for backpatching int32_t endJumps[MAX_EXITS]; int32_t endJumpCount = 0; // Parse THEN block while (!p->hasError && !check(p, TOK_ELSEIF) && !check(p, TOK_ELSE) && !check(p, TOK_EOF)) { // Check for END IF if (check(p, TOK_END)) { BasLexerT savedLex = p->lex; advance(p); if (check(p, TOK_IF)) { advance(p); patchJump(p, falseJump); // Patch all end jumps for (int32_t i = 0; i < endJumpCount; i++) { patchJump(p, endJumps[i]); } return; } p->lex = savedLex; } parseStatement(p); skipNewlines(p); } // ELSEIF chain while (!p->hasError && check(p, TOK_ELSEIF)) { // Jump from previous true-block to end of chain if (endJumpCount < MAX_EXITS) { endJumps[endJumpCount++] = emitJump(p, OP_JMP); } // Patch the previous false jump to here patchJump(p, falseJump); advance(p); // consume ELSEIF parseExpression(p); expect(p, TOK_THEN); falseJump = emitJump(p, OP_JMP_FALSE); expectEndOfStatement(p); skipNewlines(p); while (!p->hasError && !check(p, TOK_ELSEIF) && !check(p, TOK_ELSE) && !check(p, TOK_EOF)) { if (check(p, TOK_END)) { BasLexerT savedLex = p->lex; advance(p); if (check(p, TOK_IF)) { advance(p); patchJump(p, falseJump); for (int32_t i = 0; i < endJumpCount; i++) { patchJump(p, endJumps[i]); } return; } p->lex = savedLex; } parseStatement(p); skipNewlines(p); } } // ELSE block if (!p->hasError && check(p, TOK_ELSE)) { if (endJumpCount < MAX_EXITS) { endJumps[endJumpCount++] = emitJump(p, OP_JMP); } patchJump(p, falseJump); falseJump = -1; // no more false jump needed advance(p); // consume ELSE expectEndOfStatement(p); skipNewlines(p); while (!p->hasError && !check(p, TOK_EOF)) { if (check(p, TOK_END)) { BasLexerT savedLex = p->lex; advance(p); if (check(p, TOK_IF)) { advance(p); for (int32_t i = 0; i < endJumpCount; i++) { patchJump(p, endJumps[i]); } return; } p->lex = savedLex; } parseStatement(p); skipNewlines(p); } } // Patch the last false jump if no ELSE block if (falseJump >= 0) { patchJump(p, falseJump); } // Patch all end-of-chain jumps for (int32_t i = 0; i < endJumpCount; i++) { patchJump(p, endJumps[i]); } // If we got here without END IF, that's an error if (!p->hasError) { error(p, "Expected END IF"); } } static void parseInput(BasParserT *p) { // INPUT #channel, var // INPUT [prompt;] var advance(p); // consume INPUT // Check for file I/O: INPUT #channel, var if (check(p, TOK_HASH)) { advance(p); // consume # // Channel number parseExpression(p); // Comma separator expect(p, TOK_COMMA); // Target variable if (!check(p, TOK_IDENT)) { errorExpected(p, "variable name"); return; } char varName[BAS_MAX_TOKEN_LEN]; strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); basEmit8(&p->cg, OP_FILE_INPUT); BasSymbolT *sym = ensureVariable(p, varName); if (sym != NULL) { // If the variable is numeric, convert the input string if (sym->dataType != BAS_TYPE_STRING) { if (sym->dataType == BAS_TYPE_INTEGER || sym->dataType == BAS_TYPE_LONG) { basEmit8(&p->cg, OP_CONV_STR_INT); } else { basEmit8(&p->cg, OP_CONV_STR_FLT); } } emitStore(p, sym); } return; } // Check for optional prompt string if (check(p, TOK_STRING_LIT)) { uint16_t idx = basAddConstant(&p->cg, p->lex.token.text, p->lex.token.textLen); basEmit8(&p->cg, OP_PUSH_STR); basEmitU16(&p->cg, idx); advance(p); // Semicolon after prompt if (match(p, TOK_SEMICOLON)) { // nothing extra } else if (match(p, TOK_COMMA)) { // comma -- no question mark (just prompt) } } else { // No prompt -- push empty string uint16_t idx = basAddConstant(&p->cg, "", 0); basEmit8(&p->cg, OP_PUSH_STR); basEmitU16(&p->cg, idx); } // Emit INPUT opcode -- pops prompt, pushes input string basEmit8(&p->cg, OP_INPUT); // Target variable if (!check(p, TOK_IDENT)) { errorExpected(p, "variable name"); return; } char varName[BAS_MAX_TOKEN_LEN]; strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); BasSymbolT *sym = ensureVariable(p, varName); if (sym == NULL) { return; } // If the variable is numeric, we need to convert the input string if (sym->dataType != BAS_TYPE_STRING) { if (sym->dataType == BAS_TYPE_INTEGER || sym->dataType == BAS_TYPE_LONG) { basEmit8(&p->cg, OP_CONV_STR_INT); } else { basEmit8(&p->cg, OP_CONV_STR_FLT); } } emitStore(p, sym); } static void parseLineInput(BasParserT *p) { // LINE INPUT #channel, var advance(p); // consume LINE if (!check(p, TOK_INPUT)) { error(p, "Expected INPUT after LINE"); return; } advance(p); // consume INPUT // Must have # for file I/O if (!match(p, TOK_HASH)) { error(p, "Expected # for file channel in LINE INPUT"); return; } // Channel expression parseExpression(p); // Comma separator expect(p, TOK_COMMA); // Target variable if (!check(p, TOK_IDENT)) { errorExpected(p, "variable name"); return; } char varName[BAS_MAX_TOKEN_LEN]; strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); basEmit8(&p->cg, OP_FILE_LINE_INPUT); BasSymbolT *sym = ensureVariable(p, varName); if (sym != NULL) { emitStore(p, sym); } } static void parseModule(BasParserT *p) { skipNewlines(p); while (!p->hasError && !check(p, TOK_EOF)) { parseStatement(p); skipNewlines(p); } // 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[256]; 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); } #define MAX_ON_LABELS 32 static void parseOn(BasParserT *p) { // ON ERROR GOTO label -- error handler // ON expr GOTO label1, label2, ... -- computed goto // ON expr GOSUB label1, label2, ... -- computed gosub advance(p); // consume ON // ON ERROR GOTO is a special form if (check(p, TOK_ERROR_KW)) { parseOnError(p); return; } // ON expr GOTO/GOSUB label1, label2, ... parseExpression(p); bool isGosub; if (check(p, TOK_GOTO)) { isGosub = false; advance(p); } else if (check(p, TOK_GOSUB)) { isGosub = true; advance(p); } else { error(p, "Expected GOTO or GOSUB after ON expression"); return; } // Track end-of-gosub jumps for patching int32_t endJumps[MAX_ON_LABELS]; int32_t endJumpCount = 0; int32_t labelIdx = 1; for (;;) { if (p->hasError) { return; } if (!check(p, TOK_IDENT)) { errorExpected(p, "label name"); return; } char labelName[BAS_MAX_TOKEN_LEN]; strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); labelName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); // DUP the selector basEmit8(&p->cg, OP_DUP); // PUSH the 1-based index basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, (int16_t)labelIdx); // Compare basEmit8(&p->cg, OP_CMP_EQ); // JMP_FALSE to skip this branch int32_t skipAddr = emitJump(p, OP_JMP_FALSE); // Match: POP the selector value basEmit8(&p->cg, OP_POP); if (isGosub) { // Push return PC before jumping int32_t pushPos = basCodePos(&p->cg); basEmit8(&p->cg, OP_PUSH_INT32); basEmit16(&p->cg, 0); // placeholder lo basEmit16(&p->cg, 0); // placeholder hi emitJumpToLabel(p, OP_JMP, labelName); // Backpatch the return address int32_t returnPc = basCodePos(&p->cg); int16_t lo = (int16_t)(returnPc & 0xFFFF); int16_t hi = (int16_t)((returnPc >> 16) & 0xFFFF); basPatch16(&p->cg, pushPos + 1, lo); basPatch16(&p->cg, pushPos + 3, hi); // After GOSUB returns, jump to end of ON...GOSUB if (endJumpCount < MAX_ON_LABELS) { endJumps[endJumpCount++] = emitJump(p, OP_JMP); } } else { // GOTO: just jump to the label emitJumpToLabel(p, OP_JMP, labelName); } // Patch the skip (no-match continues to next branch) patchJump(p, skipAddr); labelIdx++; if (!match(p, TOK_COMMA)) { break; } } // No match: POP the selector and fall through basEmit8(&p->cg, OP_POP); // Patch all end-of-gosub jumps to here int32_t endTarget = basCodePos(&p->cg); for (int32_t i = 0; i < endJumpCount; i++) { int16_t offset = (int16_t)(endTarget - (endJumps[i] + 2)); basPatch16(&p->cg, endJumps[i], offset); } } static void parseOnError(BasParserT *p) { // ON ERROR GOTO label // ON ERROR GOTO 0 (disable) // Note: ON and ERROR already consumed by parseOn dispatcher advance(p); // consume ERROR if (!check(p, TOK_GOTO)) { error(p, "Expected GOTO after ON ERROR"); return; } advance(p); // consume GOTO // ON ERROR GOTO 0 -- disable error handler if (check(p, TOK_INT_LIT) && p->lex.token.intVal == 0) { advance(p); basEmit8(&p->cg, OP_ON_ERROR); basEmit16(&p->cg, 0); return; } // ON ERROR GOTO label if (!check(p, TOK_IDENT)) { errorExpected(p, "label name or 0"); return; } char labelName[BAS_MAX_TOKEN_LEN]; strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); labelName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); // Look up the label BasSymbolT *sym = basSymTabFind(&p->sym, labelName); if (sym != NULL && sym->kind == SYM_LABEL && sym->isDefined) { // Label already defined -- emit ON_ERROR with offset to handler basEmit8(&p->cg, OP_ON_ERROR); int32_t here = basCodePos(&p->cg); int16_t offset = (int16_t)(sym->codeAddr - (here + 2)); basEmit16(&p->cg, offset); } else { // Forward reference if (sym == NULL) { sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0); if (sym == NULL) { error(p, "Symbol table full"); return; } sym->scope = SCOPE_GLOBAL; sym->isDefined = false; sym->codeAddr = 0; } basEmit8(&p->cg, OP_ON_ERROR); int32_t patchAddr = basCodePos(&p->cg); basEmit16(&p->cg, 0); 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 = 1; // INPUT advance(p); } else if (check(p, TOK_OUTPUT)) { mode = 2; // OUTPUT advance(p); } else if (check(p, TOK_APPEND)) { mode = 3; // APPEND advance(p); } else if (check(p, TOK_RANDOM)) { mode = 4; // RANDOM advance(p); } else if (check(p, TOK_BINARY)) { mode = 5; // BINARY advance(p); } else { error(p, "Expected INPUT, OUTPUT, APPEND, RANDOM, or BINARY after FOR"); return; } // AS keyword expect(p, TOK_AS); // Optional # prefix match(p, TOK_HASH); // Channel number expression parseExpression(p); // Optional LEN = recordsize (for RANDOM mode) if (checkKeyword(p, "LEN")) { advance(p); // consume LEN expect(p, TOK_EQ); // For now we just parse and discard -- record length is not // enforced at the VM level (GET/PUT use variable type size) parseExpression(p); basEmit8(&p->cg, OP_POP); } // Emit: stack has [filename, channel] -- OP_FILE_OPEN reads mode byte basEmit8(&p->cg, OP_FILE_OPEN); basEmit8(&p->cg, mode); } static void parseOption(BasParserT *p) { // OPTION BASE 0 | OPTION BASE 1 // OPTION COMPARE BINARY | OPTION COMPARE TEXT advance(p); // consume OPTION if (check(p, TOK_BASE)) { advance(p); // consume BASE if (!check(p, TOK_INT_LIT)) { error(p, "Expected 0 or 1 after OPTION BASE"); return; } int32_t base = p->lex.token.intVal; if (base != 0 && base != 1) { error(p, "OPTION BASE must be 0 or 1"); return; } p->optionBase = base; advance(p); return; } if (checkKeyword(p, "COMPARE")) { advance(p); // consume COMPARE if (check(p, TOK_BINARY)) { p->optionCompareText = false; advance(p); basEmit8(&p->cg, OP_COMPARE_MODE); basEmit8(&p->cg, 0); } else if (checkKeyword(p, "TEXT")) { p->optionCompareText = true; advance(p); basEmit8(&p->cg, OP_COMPARE_MODE); basEmit8(&p->cg, 1); } else { error(p, "Expected BINARY or TEXT after OPTION COMPARE"); } return; } if (check(p, TOK_EXPLICIT)) { advance(p); p->optionExplicit = true; return; } error(p, "Expected BASE, COMPARE, or EXPLICIT after OPTION"); } static void parsePrint(BasParserT *p) { // PRINT [#channel, expr] // PRINT [expr] [; expr] [, expr] [;] // PRINT USING "fmt"; expr [; expr] ... advance(p); // consume PRINT // Check for file I/O: PRINT #channel, expr if (check(p, TOK_HASH)) { advance(p); // consume # // Channel number parseExpression(p); // Comma separator expect(p, TOK_COMMA); // Value to print parseExpression(p); basEmit8(&p->cg, OP_FILE_PRINT); return; } // Check for PRINT USING if (checkKeyword(p, "USING")) { advance(p); // consume USING // Parse format string expression parseExpression(p); // Semicolon separates format from values expect(p, TOK_SEMICOLON); // Parse values, each one gets formatted with PRINT_USING for (;;) { parseExpression(p); basEmit8(&p->cg, OP_PRINT_USING); basEmit8(&p->cg, OP_PRINT); if (check(p, TOK_SEMICOLON)) { advance(p); if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { break; } continue; } break; } basEmit8(&p->cg, OP_PRINT_NL); return; } bool trailingSemicolon = false; // Empty PRINT = just newline if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { basEmit8(&p->cg, OP_PRINT_NL); return; } while (!p->hasError) { trailingSemicolon = false; if (check(p, TOK_SEMICOLON)) { // Just a semicolon -- no space trailingSemicolon = true; advance(p); if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { break; } continue; } if (check(p, TOK_COMMA)) { // Comma -- print tab basEmit8(&p->cg, OP_PRINT_TAB); advance(p); if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { trailingSemicolon = true; // comma at end suppresses newline too break; } continue; } if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { break; } // Check for SPC(n) and TAB(n) inside PRINT if (checkKeyword(p, "SPC")) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_PRINT_SPC_N); continue; } if (checkKeyword(p, "TAB")) { advance(p); expect(p, TOK_LPAREN); parseExpression(p); expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_PRINT_TAB_N); continue; } // Expression parseExpression(p); basEmit8(&p->cg, OP_PRINT); } // Print newline unless suppressed by trailing semicolon/comma if (!trailingSemicolon) { basEmit8(&p->cg, OP_PRINT_NL); } } static void parseRead(BasParserT *p) { // READ var1, var2, ... advance(p); // consume READ for (;;) { if (p->hasError) { return; } if (!check(p, TOK_IDENT)) { errorExpected(p, "variable name"); return; } char name[BAS_MAX_TOKEN_LEN]; strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); name[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); BasSymbolT *sym = ensureVariable(p, name); if (sym == NULL) { return; } basEmit8(&p->cg, OP_READ_DATA); emitStore(p, sym); if (!match(p, TOK_COMMA)) { break; } } } static void parseRedim(BasParserT *p) { // REDIM [PRESERVE] var(bounds) AS type advance(p); // consume REDIM uint8_t preserve = 0; if (check(p, TOK_PRESERVE)) { preserve = 1; advance(p); } if (!check(p, TOK_IDENT)) { errorExpected(p, "array variable name"); return; } char name[BAS_MAX_TOKEN_LEN]; strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); name[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); BasSymbolT *sym = basSymTabFind(&p->sym, name); if (sym == NULL) { sym = ensureVariable(p, name); } if (sym == NULL) { return; } sym->isArray = true; // Load the old array reference emitLoad(p, sym); // Parse new bounds int32_t dims = 0; expect(p, TOK_LPAREN); parseDimBounds(p, &dims); expect(p, TOK_RPAREN); // Optional AS type if (match(p, TOK_AS)) { resolveTypeName(p); } if (p->hasError) { return; } basEmit8(&p->cg, OP_REDIM); basEmit8(&p->cg, (uint8_t)dims); basEmit8(&p->cg, preserve); emitStore(p, sym); } static void parseRestore(BasParserT *p) { // RESTORE -- reset the DATA read pointer to the beginning advance(p); // consume RESTORE basEmit8(&p->cg, OP_RESTORE); } static void parseResume(BasParserT *p) { // RESUME -- re-execute the statement that caused the error // RESUME NEXT -- continue at the next statement after the error advance(p); // consume RESUME if (check(p, TOK_NEXT)) { advance(p); basEmit8(&p->cg, OP_RESUME_NEXT); } else { basEmit8(&p->cg, OP_RESUME); } } static void parseSelectCase(BasParserT *p) { // SELECT CASE expr // CASE val [, val] ... // ... // [CASE ELSE] // ... // END SELECT advance(p); // consume SELECT expect(p, TOK_CASE); // Evaluate the test expression -- stays on stack throughout parseExpression(p); expectEndOfStatement(p); skipNewlines(p); int32_t endJumps[MAX_EXITS]; int32_t endJumpCount = 0; while (!p->hasError && !check(p, TOK_EOF)) { // Check for END SELECT if (check(p, TOK_END)) { BasLexerT savedLex = p->lex; advance(p); if (check(p, TOK_SELECT)) { advance(p); basEmit8(&p->cg, OP_POP); // pop test expression for (int32_t i = 0; i < endJumpCount; i++) { patchJump(p, endJumps[i]); } return; } p->lex = savedLex; } if (!check(p, TOK_CASE)) { error(p, "Expected CASE or END SELECT"); return; } advance(p); // consume CASE // CASE ELSE -- always matches, no comparison needed if (check(p, TOK_ELSE)) { advance(p); expectEndOfStatement(p); skipNewlines(p); // Parse body until END SELECT while (!p->hasError && !check(p, TOK_EOF)) { if (check(p, TOK_END)) { BasLexerT savedLex = p->lex; advance(p); if (check(p, TOK_SELECT)) { advance(p); basEmit8(&p->cg, OP_POP); for (int32_t i = 0; i < endJumpCount; i++) { patchJump(p, endJumps[i]); } return; } p->lex = savedLex; } parseStatement(p); skipNewlines(p); } continue; } // CASE val [, val | 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[MAX_EXITS]; int32_t bodyJumpCount = 0; for (;;) { if (check(p, TOK_IS)) { // CASE IS 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"); return; } basEmit8(&p->cg, OP_DUP); parseExpression(p); basEmit8(&p->cg, cmpOp); if (bodyJumpCount < MAX_EXITS) { bodyJumps[bodyJumpCount++] = 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); if (bodyJumpCount < MAX_EXITS) { bodyJumps[bodyJumpCount++] = emitJump(p, OP_JMP_TRUE); } patchJump(p, skipRange); } else { // Plain value -- equality test basEmit8(&p->cg, OP_CMP_EQ); if (bodyJumpCount < MAX_EXITS) { bodyJumps[bodyJumpCount++] = 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 < bodyJumpCount; i++) { patchJump(p, bodyJumps[i]); } // Parse the CASE body expectEndOfStatement(p); skipNewlines(p); while (!p->hasError && !check(p, TOK_CASE) && !check(p, TOK_EOF)) { if (check(p, TOK_END)) { BasLexerT savedLex = p->lex; advance(p); if (check(p, TOK_SELECT)) { advance(p); basEmit8(&p->cg, OP_POP); patchJump(p, nextCaseJump); for (int32_t i = 0; i < endJumpCount; i++) { patchJump(p, endJumps[i]); } return; } p->lex = savedLex; } parseStatement(p); skipNewlines(p); } // Jump to end of SELECT (skip remaining cases) if (endJumpCount < MAX_EXITS) { endJumps[endJumpCount++] = emitJump(p, OP_JMP); } // Patch the next-case jump to here patchJump(p, nextCaseJump); } // Pop test value (reached if no END SELECT but hit EOF) basEmit8(&p->cg, OP_POP); for (int32_t i = 0; i < endJumpCount; i++) { patchJump(p, endJumps[i]); } if (!p->hasError) { error(p, "Expected END SELECT"); } } static void parseShell(BasParserT *p) { // SHELL "command" -- execute an OS command (discard return value) // SHELL -- no argument, no-op in embedded context advance(p); // consume SHELL if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { // No argument -- push empty string and call SHELL (no-op) uint16_t idx = basAddConstant(&p->cg, "", 0); basEmit8(&p->cg, OP_PUSH_STR); basEmitU16(&p->cg, idx); } else { parseExpression(p); } basEmit8(&p->cg, OP_SHELL); basEmit8(&p->cg, OP_POP); // discard return value in statement form } static void parseSleep(BasParserT *p) { // SLEEP [seconds] // If no argument, default to 1 second advance(p); // consume SLEEP if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) { // No argument -- push 1 second basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, 1); } else { parseExpression(p); } basEmit8(&p->cg, OP_SLEEP); } static void parseStatic(BasParserT *p) { // STATIC var AS type // Only valid inside SUB/FUNCTION. Creates a global variable with a // mangled name (procName$varName) that persists across calls. advance(p); // consume STATIC if (!p->sym.inLocalScope) { error(p, "STATIC is only valid inside SUB or FUNCTION"); return; } if (!check(p, TOK_IDENT)) { errorExpected(p, "variable name"); return; } char varName[BAS_MAX_TOKEN_LEN]; strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); // Optional AS type uint8_t dt = suffixToType(varName); if (match(p, TOK_AS)) { dt = resolveTypeName(p); } if (p->hasError) { return; } // Create a mangled global name: "procName$varName" // 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 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_INPUT: parseInput(p); break; case TOK_OPEN: parseOpen(p); break; case TOK_CLOSE: parseClose(p); break; case TOK_GET: parseGet(p); break; case TOK_PUT: parsePut(p); break; case TOK_SEEK: parseSeek(p); break; case TOK_WRITE: parseWrite(p); break; case TOK_LINE: parseLineInput(p); break; case TOK_GOTO: parseGoto(p); break; case TOK_GOSUB: parseGosub(p); break; case TOK_ON: parseOn(p); break; case TOK_OPTION: parseOption(p); break; case TOK_SHELL: parseShell(p); break; case TOK_RESUME: parseResume(p); break; case TOK_RETURN: advance(p); if (p->sym.inLocalScope) { // Inside SUB/FUNCTION: return from subroutine basEmit8(&p->cg, OP_RET); } else { // Module level: GOSUB return (pop PC from eval stack) basEmit8(&p->cg, OP_GOSUB_RET); } break; case TOK_SLEEP: parseSleep(p); break; case TOK_SWAP: parseSwap(p); break; case TOK_CALL: { advance(p); // consume CALL if (!check(p, TOK_IDENT)) { errorExpected(p, "subroutine name"); break; } char name[BAS_MAX_TOKEN_LEN]; strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); name[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); BasSymbolT *sym = basSymTabFind(&p->sym, name); if (sym == NULL) { // Forward reference sym = basSymTabAdd(&p->sym, name, SYM_SUB, BAS_TYPE_INTEGER); if (sym == NULL) { error(p, "Symbol table full"); break; } sym->scope = SCOPE_GLOBAL; sym->isDefined = false; sym->codeAddr = 0; } if (check(p, TOK_LPAREN)) { emitFunctionCall(p, sym); } else { // CALL with no arguments uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0; basEmit8(&p->cg, OP_CALL); int32_t addrPos = basCodePos(&p->cg); basEmitU16(&p->cg, (uint16_t)sym->codeAddr); basEmit8(&p->cg, 0); basEmit8(&p->cg, baseSlot); if (!sym->isDefined && 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_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] (statement form, discard 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 } basEmit8(&p->cg, OP_MSGBOX); basEmit8(&p->cg, OP_POP); // discard result break; // SQL statement forms (no return value) case TOK_SQLCLOSE: advance(p); parseExpression(p); // db basEmit8(&p->cg, OP_SQL_CLOSE); 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_SQLEXEC: // SQLExec db, sql (statement form, discard result) advance(p); parseExpression(p); // db expect(p, TOK_COMMA); parseExpression(p); // sql basEmit8(&p->cg, OP_SQL_EXEC); basEmit8(&p->cg, OP_POP); // discard bool result break; case TOK_SQLNEXT: // SQLNext rs (statement form, discard result) advance(p); parseExpression(p); // rs basEmit8(&p->cg, OP_SQL_NEXT); basEmit8(&p->cg, OP_POP); // discard bool result break; case TOK_SQLFREERESULT: advance(p); parseExpression(p); // rs basEmit8(&p->cg, OP_SQL_FREE_RESULT); 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 (!check(p, TOK_IDENT) && !check(p, TOK_SHOW) && !check(p, TOK_HIDE)) { 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[512]; snprintf(buf, sizeof(buf), "Name '%s' already used", labelName); error(p, buf); } // After the label, there may be a statement on the same line // which will be parsed on the next iteration break; } // Not a label -- restore and parse as assignment/call p->lex = savedLex; parseAssignOrCall(p); break; } case TOK_REM: // Comment -- skip to end of line advance(p); break; default: { char buf[256]; snprintf(buf, sizeof(buf), "Unexpected token: %s", basTokenName(tt)); error(p, buf); break; } } if (!p->hasError) { expectEndOfStatement(p); } } static void parseSub(BasParserT *p) { // SUB name(params) // ... // END SUB advance(p); // consume SUB if (!check(p, TOK_IDENT)) { errorExpected(p, "subroutine name"); return; } char name[BAS_MAX_TOKEN_LEN]; strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); name[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); // Save current proc name for STATIC variable mangling strncpy(p->currentProc, name, BAS_MAX_TOKEN_LEN - 1); p->currentProc[BAS_MAX_TOKEN_LEN - 1] = '\0'; // Jump over the sub body in module-level code int32_t skipJump = emitJump(p, OP_JMP); int32_t subAddr = basCodePos(&p->cg); // Enter local scope basSymTabEnterLocal(&p->sym); ExitListT savedExitSub = exitSubList; exitListInit(&exitSubList); // Parse parameter list int32_t paramCount = 0; uint8_t paramTypes[BAS_MAX_PARAMS]; bool paramByVal[BAS_MAX_PARAMS]; if (match(p, TOK_LPAREN)) { while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) { if (paramCount > 0) { expect(p, TOK_COMMA); } bool byVal = false; if (match(p, TOK_BYVAL)) { byVal = true; } if (!check(p, TOK_IDENT)) { errorExpected(p, "parameter name"); return; } char paramName[BAS_MAX_TOKEN_LEN]; strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); uint8_t pdt = suffixToType(paramName); if (match(p, TOK_AS)) { pdt = resolveTypeName(p); } BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); if (paramSym == NULL) { error(p, "Symbol table full"); return; } paramSym->scope = SCOPE_LOCAL; paramSym->index = basSymTabAllocSlot(&p->sym); paramSym->isDefined = true; if (paramCount < BAS_MAX_PARAMS) { paramTypes[paramCount] = pdt; paramByVal[paramCount] = byVal; } paramCount++; } expect(p, TOK_RPAREN); } // Register the sub in the symbol table (global scope) BasSymbolT *existing = basSymTabFindGlobal(&p->sym, name); BasSymbolT *subSym = NULL; if (existing != NULL && existing->kind == SYM_SUB) { subSym = existing; } else { bool savedLocal = p->sym.inLocalScope; p->sym.inLocalScope = false; subSym = basSymTabAdd(&p->sym, name, SYM_SUB, BAS_TYPE_INTEGER); p->sym.inLocalScope = savedLocal; } if (subSym == NULL) { error(p, "Could not register subroutine"); return; } subSym->codeAddr = subAddr; subSym->isDefined = true; subSym->paramCount = paramCount; subSym->scope = SCOPE_GLOBAL; for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) { subSym->paramTypes[i] = paramTypes[i]; subSym->paramByVal[i] = paramByVal[i]; } // Backpatch any forward-reference calls to this sub patchCallAddrs(p, subSym); expectEndOfStatement(p); skipNewlines(p); // Parse sub body while (!p->hasError && !check(p, TOK_EOF)) { if (check(p, TOK_END)) { BasLexerT savedLex = p->lex; advance(p); if (check(p, TOK_SUB)) { advance(p); break; } p->lex = savedLex; } parseStatement(p); skipNewlines(p); } // Patch EXIT SUB jumps exitListPatch(&exitSubList, p); exitSubList = savedExitSub; basEmit8(&p->cg, OP_RET); // Leave local scope collectDebugLocals(p, p->cg.debugProcCount++); basSymTabLeaveLocal(&p->sym); p->currentProc[0] = '\0'; // Patch the skip jump patchJump(p, skipJump); } static void parseType(BasParserT *p) { // TYPE name // field AS type // ... // END TYPE advance(p); // consume TYPE if (!check(p, TOK_IDENT)) { errorExpected(p, "type name"); return; } char typeName[BAS_MAX_TOKEN_LEN]; strncpy(typeName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); typeName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); // Add TYPE_DEF symbol bool savedLocal = p->sym.inLocalScope; p->sym.inLocalScope = false; BasSymbolT *typeSym = basSymTabAdd(&p->sym, typeName, SYM_TYPE_DEF, BAS_TYPE_UDT); p->sym.inLocalScope = savedLocal; if (typeSym == NULL) { error(p, "Symbol table full or duplicate TYPE name"); return; } typeSym->scope = SCOPE_GLOBAL; typeSym->isDefined = true; typeSym->index = p->sym.count - 1; typeSym->fieldCount = 0; expectEndOfStatement(p); skipNewlines(p); // Parse fields until END TYPE while (!p->hasError && !check(p, TOK_EOF)) { if (check(p, TOK_END)) { BasLexerT savedLex = p->lex; advance(p); if (check(p, TOK_TYPE)) { advance(p); break; } p->lex = savedLex; } if (!check(p, TOK_IDENT)) { errorExpected(p, "field name or END TYPE"); return; } 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 parseSwap(BasParserT *p) { // SWAP a, b -- swap the values of two variables advance(p); // consume SWAP // First variable if (!check(p, TOK_IDENT)) { errorExpected(p, "variable name"); return; } char nameA[BAS_MAX_TOKEN_LEN]; strncpy(nameA, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); nameA[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); BasSymbolT *symA = ensureVariable(p, nameA); if (symA == NULL) { return; } expect(p, TOK_COMMA); // Second variable if (!check(p, TOK_IDENT)) { errorExpected(p, "variable name"); return; } char nameB[BAS_MAX_TOKEN_LEN]; strncpy(nameB, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); nameB[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); BasSymbolT *symB = ensureVariable(p, nameB); if (symB == NULL) { return; } // Emit: load a, load b, store a, store b emitLoad(p, symA); emitLoad(p, symB); emitStore(p, symA); emitStore(p, symB); } static void parseGet(BasParserT *p) { // GET #channel, [recno], var advance(p); // consume GET match(p, TOK_HASH); // optional # // Channel number parseExpression(p); expect(p, TOK_COMMA); // Optional record number if (check(p, TOK_COMMA)) { // No record number specified -- push 0 (current position) basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, 0); } else { parseExpression(p); } expect(p, TOK_COMMA); // Target variable if (!check(p, TOK_IDENT)) { errorExpected(p, "variable name"); return; } char varName[BAS_MAX_TOKEN_LEN]; strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); varName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); BasSymbolT *sym = ensureVariable(p, varName); if (sym == NULL) { return; } // Push variable type so VM knows how many bytes to read basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, (int16_t)sym->dataType); basEmit8(&p->cg, OP_FILE_GET); emitStore(p, sym); } static void parsePut(BasParserT *p) { // PUT #channel, [recno], var advance(p); // consume PUT match(p, TOK_HASH); // optional # // Channel number parseExpression(p); expect(p, TOK_COMMA); // Optional record number if (check(p, TOK_COMMA)) { // No record number specified -- push 0 (current position) basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, 0); } else { parseExpression(p); } expect(p, TOK_COMMA); // Value expression parseExpression(p); basEmit8(&p->cg, OP_FILE_PUT); } static void parseSeek(BasParserT *p) { // SEEK #channel, position advance(p); // consume SEEK match(p, TOK_HASH); // optional # // Channel number parseExpression(p); expect(p, TOK_COMMA); // Position parseExpression(p); basEmit8(&p->cg, OP_FILE_SEEK); } static void parseWrite(BasParserT *p) { // WRITE #channel, expr1, expr2, ... // Values are comma-delimited. Strings are quoted. Numbers undecorated. // Each WRITE statement ends with a newline. advance(p); // consume WRITE if (!check(p, TOK_HASH)) { error(p, "Expected # after WRITE"); return; } advance(p); // consume # // Channel number expression parseExpression(p); // Comma separator between channel and first value expect(p, TOK_COMMA); // Parse each value bool first = true; while (!p->hasError) { if (!first) { // Emit comma separator to file basEmit8(&p->cg, OP_DUP); // dup channel for separator basEmit8(&p->cg, OP_FILE_WRITE_SEP); } first = false; // Duplicate channel for the write operation basEmit8(&p->cg, OP_DUP); // Parse value expression parseExpression(p); // Write value in WRITE format (strings quoted, numbers undecorated) basEmit8(&p->cg, OP_FILE_WRITE); if (!match(p, TOK_COMMA)) { break; } } // Write newline to file (channel still on stack) basEmit8(&p->cg, OP_FILE_WRITE_NL); } static void parseWhile(BasParserT *p) { // WHILE cond // ... // WEND advance(p); // consume WHILE ExitListT savedExitDo = exitDoList; exitListInit(&exitDoList); int32_t loopTop = basCodePos(&p->cg); parseExpression(p); int32_t falseJump = emitJump(p, OP_JMP_FALSE); expectEndOfStatement(p); skipNewlines(p); while (!p->hasError && !check(p, TOK_WEND) && !check(p, TOK_EOF)) { parseStatement(p); skipNewlines(p); } if (p->hasError) { return; } expect(p, TOK_WEND); // Jump back to loop top basEmit8(&p->cg, OP_JMP); int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2)); basEmit16(&p->cg, backOffset); // Patch the false jump to exit patchJump(p, falseJump); // Patch EXIT DO jumps (WHILE/WEND uses the DO exit list) exitListPatch(&exitDoList, p); exitDoList = savedExitDo; } // ============================================================ // Public API // ============================================================ void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) { memset(p, 0, sizeof(BasParserT)); basLexerInit(&p->lex, source, sourceLen); basCodeGenInit(&p->cg); basSymTabInit(&p->sym); p->hasError = false; p->errorLine = 0; p->error[0] = '\0'; exitListInit(&exitForList); exitListInit(&exitDoList); exitListInit(&exitSubList); exitListInit(&exitFuncList); p->formInitJmpAddr = -1; p->formInitCodeStart = -1; addPredefConsts(p); // basLexerInit already primes the first token -- no advance needed } bool basParse(BasParserT *p) { parseModule(p); return !p->hasError; } BasModuleT *basParserBuildModule(BasParserT *p) { if (p->hasError) { return NULL; } // 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 for (int32_t i = 0; i < p->sym.count; i++) { arrfree(p->sym.symbols[i].patchAddrs); arrfree(p->sym.symbols[i].fields); } arrfree(p->sym.symbols); p->sym.symbols = NULL; p->sym.count = 0; }