4324 lines
118 KiB
C
4324 lines
118 KiB
C
// 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 <ctype.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
// ============================================================
|
|
// Forward jump list (for EXIT FOR / EXIT DO backpatching)
|
|
// ============================================================
|
|
|
|
#define MAX_EXITS 32
|
|
|
|
typedef struct {
|
|
int32_t patchAddr[MAX_EXITS];
|
|
int32_t count;
|
|
} ExitListT;
|
|
|
|
// ============================================================
|
|
// Built-in function table
|
|
// ============================================================
|
|
|
|
typedef struct {
|
|
const char *name;
|
|
uint8_t opcode;
|
|
int32_t minArgs;
|
|
int32_t maxArgs;
|
|
uint8_t resultType;
|
|
} BuiltinFuncT;
|
|
|
|
static const BuiltinFuncT builtinFuncs[] = {
|
|
// String functions
|
|
{"ASC", OP_STR_ASC, 1, 1, BAS_TYPE_INTEGER},
|
|
{"CHR$", OP_STR_CHR, 1, 1, BAS_TYPE_STRING},
|
|
{"DATE$", OP_DATE_STR, 0, 0, BAS_TYPE_STRING},
|
|
{"ENVIRON$", OP_ENVIRON, 1, 1, BAS_TYPE_STRING},
|
|
{"FORMAT$", OP_FORMAT, 2, 2, BAS_TYPE_STRING},
|
|
{"HEX$", OP_STR_HEX, 1, 1, BAS_TYPE_STRING},
|
|
{"INSTR", OP_STR_INSTR, 2, 3, BAS_TYPE_INTEGER},
|
|
{"LCASE$", OP_STR_LCASE, 1, 1, BAS_TYPE_STRING},
|
|
{"LEFT$", OP_STR_LEFT, 2, 2, BAS_TYPE_STRING},
|
|
{"LEN", OP_STR_LEN, 1, 1, BAS_TYPE_INTEGER},
|
|
{"LTRIM$", OP_STR_LTRIM, 1, 1, BAS_TYPE_STRING},
|
|
{"MID$", OP_STR_MID2, 2, 3, BAS_TYPE_STRING},
|
|
{"RIGHT$", OP_STR_RIGHT, 2, 2, BAS_TYPE_STRING},
|
|
{"RTRIM$", OP_STR_RTRIM, 1, 1, BAS_TYPE_STRING},
|
|
{"SPACE$", OP_STR_SPACE, 1, 1, BAS_TYPE_STRING},
|
|
{"STR$", OP_STR_STRF, 1, 1, BAS_TYPE_STRING},
|
|
{"STRING$", OP_STR_STRING, 2, 2, BAS_TYPE_STRING},
|
|
{"TRIM$", OP_STR_TRIM, 1, 1, BAS_TYPE_STRING},
|
|
{"UCASE$", OP_STR_UCASE, 1, 1, BAS_TYPE_STRING},
|
|
{"VAL", OP_STR_VAL, 1, 1, BAS_TYPE_DOUBLE},
|
|
|
|
// File I/O functions
|
|
{"FREEFILE", OP_FILE_FREEFILE, 0, 0, BAS_TYPE_INTEGER},
|
|
{"LOC", OP_FILE_LOC, 1, 1, BAS_TYPE_LONG},
|
|
{"LOF", OP_FILE_LOF, 1, 1, BAS_TYPE_LONG},
|
|
|
|
// Math functions
|
|
{"ABS", OP_MATH_ABS, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"ATN", OP_MATH_ATN, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"COS", OP_MATH_COS, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"EXP", OP_MATH_EXP, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"FIX", OP_MATH_FIX, 1, 1, BAS_TYPE_INTEGER},
|
|
{"INT", OP_MATH_INT, 1, 1, BAS_TYPE_INTEGER},
|
|
{"LOG", OP_MATH_LOG, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"RND", OP_MATH_RND, 0, 1, BAS_TYPE_DOUBLE},
|
|
{"SGN", OP_MATH_SGN, 1, 1, BAS_TYPE_INTEGER},
|
|
{"SIN", OP_MATH_SIN, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"SQR", OP_MATH_SQR, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"TAN", OP_MATH_TAN, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"TIME$", OP_TIME_STR, 0, 0, BAS_TYPE_STRING},
|
|
{"TIMER", OP_MATH_TIMER, 0, 0, BAS_TYPE_DOUBLE},
|
|
|
|
{NULL, 0, 0, 0, 0}
|
|
};
|
|
|
|
// ============================================================
|
|
// Helper prototypes (alphabetized)
|
|
// ============================================================
|
|
|
|
static void advance(BasParserT *p);
|
|
static bool check(BasParserT *p, BasTokenTypeE type);
|
|
static bool checkKeyword(BasParserT *p, const char *kw);
|
|
static bool checkKeywordText(const char *text, const char *kw);
|
|
static void error(BasParserT *p, const char *msg);
|
|
static void errorExpected(BasParserT *p, const char *what);
|
|
static void expect(BasParserT *p, BasTokenTypeE type);
|
|
static void expectEndOfStatement(BasParserT *p);
|
|
static const BuiltinFuncT *findBuiltin(const char *name);
|
|
static BasSymbolT *findTypeDef(BasParserT *p, const char *name);
|
|
static bool match(BasParserT *p, BasTokenTypeE type);
|
|
static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName);
|
|
static uint8_t resolveTypeName(BasParserT *p);
|
|
static uint8_t suffixToType(const char *name);
|
|
static void skipNewlines(BasParserT *p);
|
|
|
|
// ============================================================
|
|
// Expression parser prototypes (by precedence, lowest first)
|
|
// ============================================================
|
|
|
|
static void parseExpression(BasParserT *p);
|
|
static void parseImpExpr(BasParserT *p);
|
|
static void parseEqvExpr(BasParserT *p);
|
|
static void parseOrExpr(BasParserT *p);
|
|
static void parseXorExpr(BasParserT *p);
|
|
static void parseAndExpr(BasParserT *p);
|
|
static void parseNotExpr(BasParserT *p);
|
|
static void parseCompareExpr(BasParserT *p);
|
|
static void parseConcatExpr(BasParserT *p);
|
|
static void parseAddExpr(BasParserT *p);
|
|
static void parseMulExpr(BasParserT *p);
|
|
static void parsePowExpr(BasParserT *p);
|
|
static void parseUnaryExpr(BasParserT *p);
|
|
static void parsePrimary(BasParserT *p);
|
|
|
|
// ============================================================
|
|
// Statement parser prototypes (alphabetized)
|
|
// ============================================================
|
|
|
|
static void parseAssignOrCall(BasParserT *p);
|
|
static void parseClose(BasParserT *p);
|
|
static void parseConst(BasParserT *p);
|
|
static void parseData(BasParserT *p);
|
|
static void parseDeclare(BasParserT *p);
|
|
static void parseDef(BasParserT *p);
|
|
static void parseDefType(BasParserT *p, uint8_t dataType);
|
|
static void parseDim(BasParserT *p);
|
|
static void parseDimBounds(BasParserT *p, int32_t *outDims);
|
|
static void parseDo(BasParserT *p);
|
|
static void parseEnd(BasParserT *p);
|
|
static void parseErase(BasParserT *p);
|
|
static void parseExit(BasParserT *p);
|
|
static void parseFor(BasParserT *p);
|
|
static void parseFunction(BasParserT *p);
|
|
static void parseGet(BasParserT *p);
|
|
static void parseGosub(BasParserT *p);
|
|
static void parseGoto(BasParserT *p);
|
|
static void parseIf(BasParserT *p);
|
|
static void parseInput(BasParserT *p);
|
|
static void parseLineInput(BasParserT *p);
|
|
static void parseModule(BasParserT *p);
|
|
static void parseOn(BasParserT *p);
|
|
static void parseOnError(BasParserT *p);
|
|
static void parseOpen(BasParserT *p);
|
|
static void parseOption(BasParserT *p);
|
|
static void parsePrint(BasParserT *p);
|
|
static void parsePut(BasParserT *p);
|
|
static void parseRead(BasParserT *p);
|
|
static void parseRedim(BasParserT *p);
|
|
static void parseRestore(BasParserT *p);
|
|
static void parseResume(BasParserT *p);
|
|
static void parseSeek(BasParserT *p);
|
|
static void parseSelectCase(BasParserT *p);
|
|
static void parseShell(BasParserT *p);
|
|
static void parseSleep(BasParserT *p);
|
|
static void parseStatement(BasParserT *p);
|
|
static void parseStatic(BasParserT *p);
|
|
static void parseSub(BasParserT *p);
|
|
static void parseSwap(BasParserT *p);
|
|
static void parseType(BasParserT *p);
|
|
static void parseWhile(BasParserT *p);
|
|
static void parseWrite(BasParserT *p);
|
|
|
|
// ============================================================
|
|
// Variable / code emit helper prototypes (alphabetized)
|
|
// ============================================================
|
|
|
|
static void emitFunctionCall(BasParserT *p, BasSymbolT *sym);
|
|
static int32_t emitJump(BasParserT *p, uint8_t opcode);
|
|
static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName);
|
|
static void emitLoad(BasParserT *p, BasSymbolT *sym);
|
|
static void emitStore(BasParserT *p, BasSymbolT *sym);
|
|
static BasSymbolT *ensureVariable(BasParserT *p, const char *name);
|
|
static void patchCallAddrs(BasParserT *p, BasSymbolT *sym);
|
|
static void patchJump(BasParserT *p, int32_t addr);
|
|
static void patchLabelRefs(BasParserT *p, BasSymbolT *sym);
|
|
|
|
// ============================================================
|
|
// Exit list helpers
|
|
// ============================================================
|
|
|
|
static ExitListT exitForList;
|
|
static ExitListT exitDoList;
|
|
static ExitListT exitSubList;
|
|
static ExitListT exitFuncList;
|
|
|
|
static void exitListInit(ExitListT *el);
|
|
static void exitListAdd(ExitListT *el, int32_t addr);
|
|
static void exitListPatch(ExitListT *el, BasParserT *p);
|
|
|
|
|
|
// ============================================================
|
|
// Helper implementations
|
|
// ============================================================
|
|
|
|
static void advance(BasParserT *p) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
basLexerNext(&p->lex);
|
|
if (p->lex.token.type == TOK_ERROR) {
|
|
error(p, p->lex.error);
|
|
}
|
|
}
|
|
|
|
|
|
static bool check(BasParserT *p, BasTokenTypeE type) {
|
|
return p->lex.token.type == type;
|
|
}
|
|
|
|
|
|
static bool checkKeyword(BasParserT *p, const char *kw) {
|
|
if (p->lex.token.type != TOK_IDENT) {
|
|
return false;
|
|
}
|
|
// Case-insensitive comparison
|
|
const char *a = p->lex.token.text;
|
|
const char *b = kw;
|
|
while (*a && *b) {
|
|
if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) {
|
|
return false;
|
|
}
|
|
a++;
|
|
b++;
|
|
}
|
|
return *a == '\0' && *b == '\0';
|
|
}
|
|
|
|
|
|
static bool checkKeywordText(const char *text, const char *kw) {
|
|
const char *a = text;
|
|
const char *b = kw;
|
|
while (*a && *b) {
|
|
if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) {
|
|
return false;
|
|
}
|
|
a++;
|
|
b++;
|
|
}
|
|
return *a == '\0' && *b == '\0';
|
|
}
|
|
|
|
|
|
static void error(BasParserT *p, const char *msg) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
p->hasError = true;
|
|
p->errorLine = p->lex.token.line;
|
|
snprintf(p->error, sizeof(p->error), "Line %d: %s", p->lex.token.line, msg);
|
|
}
|
|
|
|
|
|
static void errorExpected(BasParserT *p, const char *what) {
|
|
char buf[512];
|
|
snprintf(buf, sizeof(buf), "Expected %s, got %s", what, basTokenName(p->lex.token.type));
|
|
error(p, buf);
|
|
}
|
|
|
|
|
|
static void exitListAdd(ExitListT *el, int32_t addr) {
|
|
if (el->count < MAX_EXITS) {
|
|
el->patchAddr[el->count++] = addr;
|
|
}
|
|
}
|
|
|
|
|
|
static void exitListInit(ExitListT *el) {
|
|
el->count = 0;
|
|
}
|
|
|
|
|
|
static void exitListPatch(ExitListT *el, BasParserT *p) {
|
|
int32_t target = basCodePos(&p->cg);
|
|
for (int32_t i = 0; i < el->count; i++) {
|
|
int16_t offset = (int16_t)(target - (el->patchAddr[i] + 2));
|
|
basPatch16(&p->cg, el->patchAddr[i], offset);
|
|
}
|
|
el->count = 0;
|
|
}
|
|
|
|
|
|
static void expect(BasParserT *p, BasTokenTypeE type) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
if (p->lex.token.type != type) {
|
|
char buf[512];
|
|
snprintf(buf, sizeof(buf), "Expected %s, got %s", basTokenName(type), basTokenName(p->lex.token.type));
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
advance(p);
|
|
}
|
|
|
|
|
|
static void expectEndOfStatement(BasParserT *p) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
// Statement must end with newline, colon, EOF, or ELSE (single-line IF)
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_ELSE)) {
|
|
return;
|
|
}
|
|
if (check(p, TOK_COLON)) {
|
|
advance(p);
|
|
return;
|
|
}
|
|
errorExpected(p, "end of statement");
|
|
}
|
|
|
|
|
|
static const BuiltinFuncT *findBuiltin(const char *name) {
|
|
for (int32_t i = 0; builtinFuncs[i].name != NULL; i++) {
|
|
// Case-insensitive comparison
|
|
const char *a = name;
|
|
const char *b = builtinFuncs[i].name;
|
|
bool match = true;
|
|
while (*a && *b) {
|
|
if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) {
|
|
match = false;
|
|
break;
|
|
}
|
|
a++;
|
|
b++;
|
|
}
|
|
if (match && *a == '\0' && *b == '\0') {
|
|
return &builtinFuncs[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
static BasSymbolT *findTypeDef(BasParserT *p, const char *name) {
|
|
for (int32_t i = 0; i < p->sym.count; i++) {
|
|
if (p->sym.symbols[i].kind == SYM_TYPE_DEF) {
|
|
// Case-insensitive comparison
|
|
const char *a = p->sym.symbols[i].name;
|
|
const char *b = name;
|
|
bool eq = true;
|
|
while (*a && *b) {
|
|
if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) {
|
|
eq = false;
|
|
break;
|
|
}
|
|
a++;
|
|
b++;
|
|
}
|
|
if (eq && *a == '\0' && *b == '\0') {
|
|
return &p->sym.symbols[i];
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
static bool match(BasParserT *p, BasTokenTypeE type) {
|
|
if (p->lex.token.type == type) {
|
|
advance(p);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName) {
|
|
for (int32_t i = 0; i < typeSym->fieldCount; i++) {
|
|
const char *a = typeSym->fields[i].name;
|
|
const char *b = fieldName;
|
|
bool eq = true;
|
|
while (*a && *b) {
|
|
if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) {
|
|
eq = false;
|
|
break;
|
|
}
|
|
a++;
|
|
b++;
|
|
}
|
|
if (eq && *a == '\0' && *b == '\0') {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
static uint8_t resolveTypeName(BasParserT *p) {
|
|
// Expect a type keyword after AS
|
|
if (check(p, TOK_INTEGER)) {
|
|
advance(p);
|
|
return BAS_TYPE_INTEGER;
|
|
}
|
|
if (check(p, TOK_LONG)) {
|
|
advance(p);
|
|
return BAS_TYPE_LONG;
|
|
}
|
|
if (check(p, TOK_SINGLE)) {
|
|
advance(p);
|
|
return BAS_TYPE_SINGLE;
|
|
}
|
|
if (check(p, TOK_DOUBLE)) {
|
|
advance(p);
|
|
return BAS_TYPE_DOUBLE;
|
|
}
|
|
if (check(p, TOK_STRING_KW)) {
|
|
advance(p);
|
|
return BAS_TYPE_STRING;
|
|
}
|
|
if (check(p, TOK_BOOLEAN)) {
|
|
advance(p);
|
|
return BAS_TYPE_BOOLEAN;
|
|
}
|
|
// Check for user-defined TYPE name
|
|
if (check(p, TOK_IDENT)) {
|
|
BasSymbolT *typeSym = findTypeDef(p, p->lex.token.text);
|
|
if (typeSym != NULL) {
|
|
p->lastUdtTypeId = typeSym->index;
|
|
advance(p);
|
|
return BAS_TYPE_UDT;
|
|
}
|
|
}
|
|
error(p, "Expected type name (Integer, Long, Single, Double, String, Boolean, or TYPE name)");
|
|
return BAS_TYPE_INTEGER;
|
|
}
|
|
|
|
|
|
static void skipNewlines(BasParserT *p) {
|
|
while (check(p, TOK_NEWLINE)) {
|
|
advance(p);
|
|
}
|
|
}
|
|
|
|
|
|
static uint8_t suffixToType(const char *name) {
|
|
int32_t len = (int32_t)strlen(name);
|
|
if (len == 0) {
|
|
return BAS_TYPE_SINGLE; // QB default
|
|
}
|
|
switch (name[len - 1]) {
|
|
case '%':
|
|
return BAS_TYPE_INTEGER;
|
|
case '&':
|
|
return BAS_TYPE_LONG;
|
|
case '!':
|
|
return BAS_TYPE_SINGLE;
|
|
case '#':
|
|
return BAS_TYPE_DOUBLE;
|
|
case '$':
|
|
return BAS_TYPE_STRING;
|
|
default:
|
|
return BAS_TYPE_SINGLE; // QB default
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Variable / code emit helpers
|
|
// ============================================================
|
|
|
|
static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
|
|
// Parse argument list
|
|
expect(p, TOK_LPAREN);
|
|
int32_t argc = 0;
|
|
if (!check(p, TOK_RPAREN)) {
|
|
parseExpression(p);
|
|
argc++;
|
|
while (match(p, TOK_COMMA)) {
|
|
parseExpression(p);
|
|
argc++;
|
|
}
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
if (argc != sym->paramCount) {
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf), "Function '%s' expects %d arguments, got %d", sym->name, sym->paramCount, argc);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
|
|
// baseSlot: functions reserve slot 0 for the return value
|
|
uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0;
|
|
|
|
basEmit8(&p->cg, OP_CALL);
|
|
int32_t addrPos = basCodePos(&p->cg);
|
|
basEmitU16(&p->cg, (uint16_t)sym->codeAddr);
|
|
basEmit8(&p->cg, (uint8_t)argc);
|
|
basEmit8(&p->cg, baseSlot);
|
|
|
|
// If not yet defined, record the address for backpatching
|
|
if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) {
|
|
sym->patchAddrs[sym->patchCount++] = addrPos;
|
|
}
|
|
}
|
|
|
|
|
|
static int32_t emitJump(BasParserT *p, uint8_t opcode) {
|
|
basEmit8(&p->cg, opcode);
|
|
int32_t addr = basCodePos(&p->cg);
|
|
basEmit16(&p->cg, 0); // placeholder
|
|
return addr;
|
|
}
|
|
|
|
|
|
static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName) {
|
|
// Look up label; if defined, emit direct jump; if not, create forward ref
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, labelName);
|
|
|
|
if (sym != NULL && sym->kind == SYM_LABEL && sym->isDefined) {
|
|
// Label already defined -- emit jump to known address
|
|
basEmit8(&p->cg, opcode);
|
|
int32_t here = basCodePos(&p->cg);
|
|
int16_t offset = (int16_t)(sym->codeAddr - (here + 2));
|
|
basEmit16(&p->cg, offset);
|
|
return;
|
|
}
|
|
|
|
// Forward reference -- create label symbol if needed
|
|
if (sym == NULL) {
|
|
sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0);
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = false;
|
|
sym->codeAddr = 0;
|
|
}
|
|
|
|
// Emit jump with placeholder offset
|
|
basEmit8(&p->cg, opcode);
|
|
int32_t patchAddr = basCodePos(&p->cg);
|
|
basEmit16(&p->cg, 0);
|
|
|
|
// Record patch address for backpatching when label is defined
|
|
if (sym->patchCount < BAS_MAX_CALL_PATCHES) {
|
|
sym->patchAddrs[sym->patchCount++] = patchAddr;
|
|
}
|
|
}
|
|
|
|
|
|
static void emitLoad(BasParserT *p, BasSymbolT *sym) {
|
|
if (sym->kind == SYM_CONST) {
|
|
// Emit the constant value directly
|
|
if (sym->dataType == BAS_TYPE_STRING) {
|
|
uint16_t idx = basAddConstant(&p->cg, sym->constStr, (int32_t)strlen(sym->constStr));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, idx);
|
|
} else if (sym->dataType == BAS_TYPE_INTEGER || sym->dataType == BAS_TYPE_LONG) {
|
|
basEmit8(&p->cg, OP_PUSH_INT32);
|
|
basEmit16(&p->cg, (int16_t)(sym->constInt & 0xFFFF));
|
|
basEmit16(&p->cg, (int16_t)((sym->constInt >> 16) & 0xFFFF));
|
|
} else if (sym->dataType == BAS_TYPE_BOOLEAN) {
|
|
basEmit8(&p->cg, sym->constInt ? OP_PUSH_TRUE : OP_PUSH_FALSE);
|
|
} else {
|
|
// Float constant
|
|
basEmit8(&p->cg, OP_PUSH_FLT64);
|
|
basEmitDouble(&p->cg, sym->constDbl);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (sym->scope == SCOPE_LOCAL) {
|
|
basEmit8(&p->cg, OP_LOAD_LOCAL);
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
} else {
|
|
basEmit8(&p->cg, OP_LOAD_GLOBAL);
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
}
|
|
}
|
|
|
|
|
|
static void emitStore(BasParserT *p, BasSymbolT *sym) {
|
|
// Fixed-length string: pad/truncate before storing
|
|
if (sym->fixedLen > 0) {
|
|
basEmit8(&p->cg, OP_STR_FIXLEN);
|
|
basEmitU16(&p->cg, (uint16_t)sym->fixedLen);
|
|
}
|
|
if (sym->scope == SCOPE_LOCAL) {
|
|
basEmit8(&p->cg, OP_STORE_LOCAL);
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
} else {
|
|
basEmit8(&p->cg, OP_STORE_GLOBAL);
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
}
|
|
}
|
|
|
|
|
|
static BasSymbolT *ensureVariable(BasParserT *p, const char *name) {
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
|
if (sym != NULL) {
|
|
return sym;
|
|
}
|
|
|
|
// When in local scope, check if a shared global exists before auto-declaring
|
|
if (p->sym.inLocalScope) {
|
|
BasSymbolT *globalSym = basSymTabFindGlobal(&p->sym, name);
|
|
if (globalSym != NULL && globalSym->isShared) {
|
|
return globalSym;
|
|
}
|
|
}
|
|
|
|
// OPTION EXPLICIT: require explicit DIM
|
|
if (p->optionExplicit) {
|
|
char buf[320];
|
|
snprintf(buf, sizeof(buf), "Variable not declared: %s (OPTION EXPLICIT is on)", name);
|
|
error(p, buf);
|
|
return NULL;
|
|
}
|
|
|
|
// Auto-declare (QB implicit declaration)
|
|
// Use suffix type if present, otherwise defType for the first letter
|
|
uint8_t dt = suffixToType(name);
|
|
|
|
if (dt == BAS_TYPE_SINGLE && name[0] != '\0') {
|
|
// suffixToType returns SINGLE as the default when no suffix.
|
|
// Check if defType overrides it.
|
|
char firstLetter = name[0];
|
|
|
|
if (firstLetter >= 'a' && firstLetter <= 'z') {
|
|
firstLetter -= 32;
|
|
}
|
|
|
|
if (firstLetter >= 'A' && firstLetter <= 'Z') {
|
|
uint8_t defDt = p->defType[firstLetter - 'A'];
|
|
|
|
if (defDt != 0) {
|
|
dt = defDt;
|
|
}
|
|
}
|
|
}
|
|
|
|
sym = basSymTabAdd(&p->sym, name, SYM_VARIABLE, dt);
|
|
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return NULL;
|
|
}
|
|
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->index = basSymTabAllocSlot(&p->sym);
|
|
sym->isDefined = true;
|
|
return sym;
|
|
}
|
|
|
|
|
|
static void patchCallAddrs(BasParserT *p, BasSymbolT *sym) {
|
|
// Backpatch all forward-reference CALL addresses
|
|
uint16_t addr = (uint16_t)sym->codeAddr;
|
|
|
|
for (int32_t i = 0; i < sym->patchCount; i++) {
|
|
int32_t pos = sym->patchAddrs[i];
|
|
|
|
if (pos >= 0 && pos + 2 <= p->cg.codeLen) {
|
|
memcpy(&p->cg.code[pos], &addr, sizeof(uint16_t));
|
|
}
|
|
}
|
|
|
|
sym->patchCount = 0;
|
|
}
|
|
|
|
|
|
static void patchJump(BasParserT *p, int32_t addr) {
|
|
int32_t target = basCodePos(&p->cg);
|
|
int16_t offset = (int16_t)(target - (addr + 2));
|
|
basPatch16(&p->cg, addr, offset);
|
|
}
|
|
|
|
|
|
static void patchLabelRefs(BasParserT *p, BasSymbolT *sym) {
|
|
// Backpatch all forward-reference jumps to this label
|
|
int32_t target = sym->codeAddr;
|
|
|
|
for (int32_t i = 0; i < sym->patchCount; i++) {
|
|
int32_t patchAddr = sym->patchAddrs[i];
|
|
int16_t offset = (int16_t)(target - (patchAddr + 2));
|
|
basPatch16(&p->cg, patchAddr, offset);
|
|
}
|
|
|
|
sym->patchCount = 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Expression parsing
|
|
// ============================================================
|
|
|
|
static void parseExpression(BasParserT *p) {
|
|
parseImpExpr(p);
|
|
}
|
|
|
|
|
|
static void parseImpExpr(BasParserT *p) {
|
|
parseEqvExpr(p);
|
|
while (!p->hasError && check(p, TOK_IMP)) {
|
|
advance(p);
|
|
parseEqvExpr(p);
|
|
basEmit8(&p->cg, OP_IMP);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseEqvExpr(BasParserT *p) {
|
|
parseOrExpr(p);
|
|
while (!p->hasError && check(p, TOK_EQV)) {
|
|
advance(p);
|
|
parseOrExpr(p);
|
|
basEmit8(&p->cg, OP_EQV);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseOrExpr(BasParserT *p) {
|
|
parseXorExpr(p);
|
|
while (!p->hasError && check(p, TOK_OR)) {
|
|
advance(p);
|
|
parseXorExpr(p);
|
|
basEmit8(&p->cg, OP_OR);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseXorExpr(BasParserT *p) {
|
|
parseAndExpr(p);
|
|
while (!p->hasError && check(p, TOK_XOR)) {
|
|
advance(p);
|
|
parseAndExpr(p);
|
|
basEmit8(&p->cg, OP_XOR);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseAndExpr(BasParserT *p) {
|
|
parseNotExpr(p);
|
|
while (!p->hasError && check(p, TOK_AND)) {
|
|
advance(p);
|
|
parseNotExpr(p);
|
|
basEmit8(&p->cg, OP_AND);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseNotExpr(BasParserT *p) {
|
|
if (check(p, TOK_NOT)) {
|
|
advance(p);
|
|
parseNotExpr(p);
|
|
basEmit8(&p->cg, OP_NOT);
|
|
return;
|
|
}
|
|
parseCompareExpr(p);
|
|
}
|
|
|
|
|
|
static void parseCompareExpr(BasParserT *p) {
|
|
parseConcatExpr(p);
|
|
while (!p->hasError) {
|
|
if (check(p, TOK_EQ)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_EQ);
|
|
} else if (check(p, TOK_NE)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_NE);
|
|
} else if (check(p, TOK_LT)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_LT);
|
|
} else if (check(p, TOK_GT)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_GT);
|
|
} else if (check(p, TOK_LE)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_LE);
|
|
} else if (check(p, TOK_GE)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_GE);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseConcatExpr(BasParserT *p) {
|
|
parseAddExpr(p);
|
|
while (!p->hasError && check(p, TOK_AMPERSAND)) {
|
|
advance(p);
|
|
parseAddExpr(p);
|
|
basEmit8(&p->cg, OP_STR_CONCAT);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseAddExpr(BasParserT *p) {
|
|
parseMulExpr(p);
|
|
while (!p->hasError) {
|
|
if (check(p, TOK_PLUS)) {
|
|
advance(p);
|
|
parseMulExpr(p);
|
|
basEmit8(&p->cg, OP_ADD_INT); // VM handles type promotion
|
|
} else if (check(p, TOK_MINUS)) {
|
|
advance(p);
|
|
parseMulExpr(p);
|
|
basEmit8(&p->cg, OP_SUB_INT);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseMulExpr(BasParserT *p) {
|
|
parsePowExpr(p);
|
|
while (!p->hasError) {
|
|
if (check(p, TOK_STAR)) {
|
|
advance(p);
|
|
parsePowExpr(p);
|
|
basEmit8(&p->cg, OP_MUL_INT);
|
|
} else if (check(p, TOK_SLASH)) {
|
|
advance(p);
|
|
parsePowExpr(p);
|
|
basEmit8(&p->cg, OP_DIV_FLT);
|
|
} else if (check(p, TOK_BACKSLASH)) {
|
|
advance(p);
|
|
parsePowExpr(p);
|
|
basEmit8(&p->cg, OP_IDIV_INT);
|
|
} else if (check(p, TOK_MOD)) {
|
|
advance(p);
|
|
parsePowExpr(p);
|
|
basEmit8(&p->cg, OP_MOD_INT);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parsePowExpr(BasParserT *p) {
|
|
parseUnaryExpr(p);
|
|
// Right-associative, but iterative is fine for most BASIC uses
|
|
while (!p->hasError && check(p, TOK_CARET)) {
|
|
advance(p);
|
|
parseUnaryExpr(p);
|
|
basEmit8(&p->cg, OP_POW);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseUnaryExpr(BasParserT *p) {
|
|
if (check(p, TOK_MINUS)) {
|
|
advance(p);
|
|
parseUnaryExpr(p);
|
|
basEmit8(&p->cg, OP_NEG_INT);
|
|
return;
|
|
}
|
|
if (check(p, TOK_PLUS)) {
|
|
advance(p); // unary plus is a no-op
|
|
parseUnaryExpr(p);
|
|
return;
|
|
}
|
|
parsePrimary(p);
|
|
}
|
|
|
|
|
|
static void parsePrimary(BasParserT *p) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
BasTokenTypeE tt = p->lex.token.type;
|
|
|
|
// Integer literal
|
|
if (tt == TOK_INT_LIT) {
|
|
int32_t val = p->lex.token.intVal;
|
|
if (val >= -32768 && val <= 32767) {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)val);
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_INT32);
|
|
basEmit16(&p->cg, (int16_t)(val & 0xFFFF));
|
|
basEmit16(&p->cg, (int16_t)((val >> 16) & 0xFFFF));
|
|
}
|
|
advance(p);
|
|
return;
|
|
}
|
|
|
|
// Long literal
|
|
if (tt == TOK_LONG_LIT) {
|
|
int32_t val = (int32_t)p->lex.token.longVal;
|
|
basEmit8(&p->cg, OP_PUSH_INT32);
|
|
basEmit16(&p->cg, (int16_t)(val & 0xFFFF));
|
|
basEmit16(&p->cg, (int16_t)((val >> 16) & 0xFFFF));
|
|
advance(p);
|
|
return;
|
|
}
|
|
|
|
// Float literal
|
|
if (tt == TOK_FLOAT_LIT) {
|
|
basEmit8(&p->cg, OP_PUSH_FLT64);
|
|
basEmitDouble(&p->cg, p->lex.token.dblVal);
|
|
advance(p);
|
|
return;
|
|
}
|
|
|
|
// String literal
|
|
if (tt == TOK_STRING_LIT) {
|
|
uint16_t idx = basAddConstant(&p->cg, p->lex.token.text, p->lex.token.textLen);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, idx);
|
|
advance(p);
|
|
return;
|
|
}
|
|
|
|
// Boolean literals
|
|
if (tt == TOK_TRUE_KW) {
|
|
basEmit8(&p->cg, OP_PUSH_TRUE);
|
|
advance(p);
|
|
return;
|
|
}
|
|
if (tt == TOK_FALSE_KW) {
|
|
basEmit8(&p->cg, OP_PUSH_FALSE);
|
|
advance(p);
|
|
return;
|
|
}
|
|
|
|
// EOF(#channel) -- file end-of-file test
|
|
if (tt == TOK_EOF_KW) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
match(p, TOK_HASH); // optional #
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FILE_EOF);
|
|
return;
|
|
}
|
|
|
|
// SEEK(n) -- return current file position (function form)
|
|
if (tt == TOK_SEEK) {
|
|
advance(p);
|
|
if (check(p, TOK_LPAREN)) {
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FILE_LOC);
|
|
return;
|
|
}
|
|
// Not a function call -- error (SEEK as statement is handled elsewhere)
|
|
error(p, "SEEK requires parentheses when used as a function");
|
|
return;
|
|
}
|
|
|
|
// TIMER -- seconds since midnight (no args needed)
|
|
if (tt == TOK_TIMER) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_MATH_TIMER);
|
|
return;
|
|
}
|
|
|
|
// ERR -- current error number
|
|
if (tt == TOK_ERR) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_ERR_NUM);
|
|
return;
|
|
}
|
|
|
|
// SHELL("command") -- as function expression
|
|
if (tt == TOK_SHELL) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_SHELL);
|
|
return;
|
|
}
|
|
|
|
// LBOUND(array [, dim])
|
|
if (tt == TOK_LBOUND) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
uint8_t dim = 1;
|
|
if (match(p, TOK_COMMA)) {
|
|
if (check(p, TOK_INT_LIT)) {
|
|
dim = (uint8_t)p->lex.token.intVal;
|
|
advance(p);
|
|
} else {
|
|
error(p, "LBOUND dimension must be a constant integer");
|
|
}
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_LBOUND);
|
|
basEmit8(&p->cg, dim);
|
|
return;
|
|
}
|
|
|
|
// UBOUND(array [, dim])
|
|
if (tt == TOK_UBOUND) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
uint8_t dim = 1;
|
|
if (match(p, TOK_COMMA)) {
|
|
if (check(p, TOK_INT_LIT)) {
|
|
dim = (uint8_t)p->lex.token.intVal;
|
|
advance(p);
|
|
} else {
|
|
error(p, "UBOUND dimension must be a constant integer");
|
|
}
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_UBOUND);
|
|
basEmit8(&p->cg, dim);
|
|
return;
|
|
}
|
|
|
|
// Parenthesized expression
|
|
if (tt == TOK_LPAREN) {
|
|
advance(p);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
return;
|
|
}
|
|
|
|
// Identifier: variable, function call, or built-in
|
|
if (tt == TOK_IDENT) {
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// INPUT$(n, #channel) -- special handling for optional # in second arg
|
|
if (checkKeywordText(name, "INPUT$") && check(p, TOK_LPAREN)) {
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p); // n (number of chars)
|
|
expect(p, TOK_COMMA);
|
|
match(p, TOK_HASH); // optional #
|
|
parseExpression(p); // channel number
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FILE_INPUT_N);
|
|
return;
|
|
}
|
|
|
|
// Check for built-in function
|
|
const BuiltinFuncT *builtin = findBuiltin(name);
|
|
if (builtin != NULL) {
|
|
int32_t argc = 0;
|
|
|
|
// Zero-arg builtins can be used without parens
|
|
if (builtin->minArgs == 0 && builtin->maxArgs == 0 && !check(p, TOK_LPAREN)) {
|
|
basEmit8(&p->cg, builtin->opcode);
|
|
return;
|
|
}
|
|
|
|
if (check(p, TOK_LPAREN)) {
|
|
expect(p, TOK_LPAREN);
|
|
|
|
// RND/zero-arg builtins can be called with empty parens
|
|
if (!check(p, TOK_RPAREN)) {
|
|
parseExpression(p);
|
|
argc++;
|
|
while (match(p, TOK_COMMA)) {
|
|
parseExpression(p);
|
|
argc++;
|
|
}
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
if (argc < builtin->minArgs || argc > builtin->maxArgs) {
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf), "Built-in '%s' expects %d-%d arguments, got %d", builtin->name, builtin->minArgs, builtin->maxArgs, argc);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
|
|
// MID$ with 3 args uses a different opcode than 2 args
|
|
if (builtin->opcode == OP_STR_MID2 && argc == 3) {
|
|
basEmit8(&p->cg, OP_STR_MID);
|
|
} else if (builtin->opcode == OP_STR_INSTR && argc == 3) {
|
|
basEmit8(&p->cg, OP_STR_INSTR3);
|
|
} else if (builtin->opcode == OP_MATH_RND && argc == 0) {
|
|
// Push -1 as dummy arg for RND()
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, -1);
|
|
basEmit8(&p->cg, OP_MATH_RND);
|
|
} else {
|
|
basEmit8(&p->cg, builtin->opcode);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check symbol table for user-defined function or variable
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
|
|
|
// Function call with parens
|
|
if (check(p, TOK_LPAREN)) {
|
|
if (sym != NULL && (sym->kind == SYM_FUNCTION || sym->kind == SYM_SUB)) {
|
|
emitFunctionCall(p, sym);
|
|
return;
|
|
}
|
|
// Could be an array access -- treat as load + array index
|
|
if (sym != NULL && sym->isArray) {
|
|
emitLoad(p, sym);
|
|
expect(p, TOK_LPAREN);
|
|
int32_t dims = 0;
|
|
parseExpression(p);
|
|
dims++;
|
|
while (match(p, TOK_COMMA)) {
|
|
parseExpression(p);
|
|
dims++;
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_LOAD_ARRAY);
|
|
basEmit8(&p->cg, (uint8_t)dims);
|
|
return;
|
|
}
|
|
// Unknown function -- forward reference, assume it's a function
|
|
if (sym == NULL) {
|
|
sym = basSymTabAdd(&p->sym, name, SYM_FUNCTION, suffixToType(name));
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = false;
|
|
sym->codeAddr = 0;
|
|
}
|
|
emitFunctionCall(p, sym);
|
|
return;
|
|
}
|
|
|
|
// Check for UDT field access: var.field
|
|
if (check(p, TOK_DOT)) {
|
|
sym = ensureVariable(p, name);
|
|
if (sym != NULL && sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) {
|
|
emitLoad(p, sym);
|
|
advance(p); // consume DOT
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "field name");
|
|
return;
|
|
}
|
|
// Find the TYPE_DEF symbol
|
|
BasSymbolT *typeSym = NULL;
|
|
for (int32_t i = 0; i < p->sym.count; i++) {
|
|
if (p->sym.symbols[i].kind == SYM_TYPE_DEF && p->sym.symbols[i].index == sym->udtTypeId) {
|
|
typeSym = &p->sym.symbols[i];
|
|
break;
|
|
}
|
|
}
|
|
if (typeSym == NULL) {
|
|
error(p, "Unknown TYPE definition");
|
|
return;
|
|
}
|
|
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
|
|
if (fieldIdx < 0) {
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
advance(p); // consume field name
|
|
basEmit8(&p->cg, OP_LOAD_FIELD);
|
|
basEmitU16(&p->cg, (uint16_t)fieldIdx);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Plain variable reference
|
|
sym = ensureVariable(p, name);
|
|
if (sym != NULL) {
|
|
emitLoad(p, sym);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Nothing matched
|
|
errorExpected(p, "expression");
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Statement parsing
|
|
// ============================================================
|
|
|
|
static void parseAssignOrCall(BasParserT *p) {
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// MID$ statement: MID$(var$, start [, len]) = replacement$
|
|
if (checkKeywordText(name, "MID$") && check(p, TOK_LPAREN)) {
|
|
expect(p, TOK_LPAREN);
|
|
|
|
// First arg: target string variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "string variable name");
|
|
return;
|
|
}
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *varSym = ensureVariable(p, varName);
|
|
if (varSym == NULL) {
|
|
return;
|
|
}
|
|
|
|
// Load the original string
|
|
emitLoad(p, varSym);
|
|
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // start position
|
|
|
|
// Optional length
|
|
if (match(p, TOK_COMMA)) {
|
|
parseExpression(p); // length
|
|
} else {
|
|
// Push 0 as sentinel meaning "use replacement length"
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0);
|
|
}
|
|
|
|
expect(p, TOK_RPAREN);
|
|
expect(p, TOK_EQ);
|
|
|
|
// Parse replacement expression
|
|
parseExpression(p);
|
|
|
|
// Emit MID$ assignment: pops replacement, len, start, str; pushes result
|
|
basEmit8(&p->cg, OP_STR_MID_ASGN);
|
|
|
|
// Store back to the variable
|
|
emitStore(p, varSym);
|
|
return;
|
|
}
|
|
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
|
|
|
// UDT field assignment: var.field = expr
|
|
if (check(p, TOK_DOT)) {
|
|
if (sym == NULL) {
|
|
sym = ensureVariable(p, name);
|
|
}
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
if (sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) {
|
|
emitLoad(p, sym);
|
|
advance(p); // consume DOT
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "field name");
|
|
return;
|
|
}
|
|
BasSymbolT *typeSym = NULL;
|
|
for (int32_t i = 0; i < p->sym.count; i++) {
|
|
if (p->sym.symbols[i].kind == SYM_TYPE_DEF && p->sym.symbols[i].index == sym->udtTypeId) {
|
|
typeSym = &p->sym.symbols[i];
|
|
break;
|
|
}
|
|
}
|
|
if (typeSym == NULL) {
|
|
error(p, "Unknown TYPE definition");
|
|
return;
|
|
}
|
|
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
|
|
if (fieldIdx < 0) {
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
advance(p); // consume field name
|
|
expect(p, TOK_EQ);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_STORE_FIELD);
|
|
basEmitU16(&p->cg, (uint16_t)fieldIdx);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Array assignment: var(index) = expr
|
|
if (check(p, TOK_LPAREN)) {
|
|
// Could be a function call as a statement (discard result)
|
|
// or array assignment
|
|
if (sym != NULL && (sym->kind == SYM_SUB || sym->kind == SYM_FUNCTION)) {
|
|
emitFunctionCall(p, sym);
|
|
if (sym->kind == SYM_FUNCTION) {
|
|
basEmit8(&p->cg, OP_POP); // discard return value
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Array element assignment
|
|
if (sym == NULL) {
|
|
sym = ensureVariable(p, name);
|
|
}
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
|
|
emitLoad(p, sym);
|
|
expect(p, TOK_LPAREN);
|
|
int32_t dims = 0;
|
|
parseExpression(p);
|
|
dims++;
|
|
while (match(p, TOK_COMMA)) {
|
|
parseExpression(p);
|
|
dims++;
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
|
|
expect(p, TOK_EQ);
|
|
parseExpression(p);
|
|
|
|
basEmit8(&p->cg, OP_STORE_ARRAY);
|
|
basEmit8(&p->cg, (uint8_t)dims);
|
|
return;
|
|
}
|
|
|
|
// Simple assignment: var = expr
|
|
if (check(p, TOK_EQ)) {
|
|
advance(p);
|
|
if (sym == NULL) {
|
|
sym = ensureVariable(p, name);
|
|
}
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
if (sym->kind == SYM_CONST) {
|
|
error(p, "Cannot assign to a constant");
|
|
return;
|
|
}
|
|
// Check if this is a function name (assigning return value)
|
|
if (sym->kind == SYM_FUNCTION) {
|
|
parseExpression(p);
|
|
// Store to the implicit return-value local slot (index 0 in function scope)
|
|
basEmit8(&p->cg, OP_STORE_LOCAL);
|
|
basEmitU16(&p->cg, 0);
|
|
return;
|
|
}
|
|
parseExpression(p);
|
|
emitStore(p, sym);
|
|
return;
|
|
}
|
|
|
|
// Sub call without parens: SUBName arg1, arg2 ...
|
|
if (sym != NULL && sym->kind == SYM_SUB) {
|
|
int32_t argc = 0;
|
|
if (!check(p, TOK_NEWLINE) && !check(p, TOK_EOF) && !check(p, TOK_COLON) && !check(p, TOK_ELSE)) {
|
|
parseExpression(p);
|
|
argc++;
|
|
while (match(p, TOK_COMMA)) {
|
|
parseExpression(p);
|
|
argc++;
|
|
}
|
|
}
|
|
if (!p->hasError && argc != sym->paramCount) {
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, sym->paramCount, argc);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
{
|
|
uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0;
|
|
basEmit8(&p->cg, OP_CALL);
|
|
int32_t addrPos = basCodePos(&p->cg);
|
|
basEmitU16(&p->cg, (uint16_t)sym->codeAddr);
|
|
basEmit8(&p->cg, (uint8_t)argc);
|
|
basEmit8(&p->cg, baseSlot);
|
|
|
|
if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) {
|
|
sym->patchAddrs[sym->patchCount++] = addrPos;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// If nothing else, it's an assignment missing the =
|
|
errorExpected(p, "'=' or '('");
|
|
}
|
|
|
|
|
|
static void parseClose(BasParserT *p) {
|
|
// CLOSE #channel
|
|
advance(p); // consume CLOSE
|
|
|
|
// Optional # prefix
|
|
match(p, TOK_HASH);
|
|
|
|
// Channel number
|
|
parseExpression(p);
|
|
|
|
basEmit8(&p->cg, OP_FILE_CLOSE);
|
|
}
|
|
|
|
|
|
static void parseConst(BasParserT *p) {
|
|
// CONST name = value
|
|
advance(p); // consume CONST
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "constant name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
expect(p, TOK_EQ);
|
|
|
|
// Parse the constant value (must be a literal)
|
|
bool isNeg = false;
|
|
if (check(p, TOK_MINUS)) {
|
|
isNeg = true;
|
|
advance(p);
|
|
}
|
|
|
|
BasSymbolT *sym = NULL;
|
|
|
|
if (check(p, TOK_INT_LIT) || check(p, TOK_LONG_LIT)) {
|
|
int32_t val = check(p, TOK_INT_LIT) ? p->lex.token.intVal : (int32_t)p->lex.token.longVal;
|
|
if (isNeg) {
|
|
val = -val;
|
|
}
|
|
sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_LONG);
|
|
if (sym != NULL) {
|
|
sym->constInt = val;
|
|
sym->isDefined = true;
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
advance(p);
|
|
} else if (check(p, TOK_FLOAT_LIT)) {
|
|
double val = p->lex.token.dblVal;
|
|
if (isNeg) {
|
|
val = -val;
|
|
}
|
|
sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_DOUBLE);
|
|
if (sym != NULL) {
|
|
sym->constDbl = val;
|
|
sym->isDefined = true;
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
advance(p);
|
|
} else if (check(p, TOK_STRING_LIT) && !isNeg) {
|
|
sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_STRING);
|
|
if (sym != NULL) {
|
|
strncpy(sym->constStr, p->lex.token.text, sizeof(sym->constStr) - 1);
|
|
sym->constStr[sizeof(sym->constStr) - 1] = '\0';
|
|
sym->isDefined = true;
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
advance(p);
|
|
} else if (check(p, TOK_TRUE_KW) && !isNeg) {
|
|
sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_BOOLEAN);
|
|
if (sym != NULL) {
|
|
sym->constInt = -1;
|
|
sym->isDefined = true;
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
advance(p);
|
|
} else if (check(p, TOK_FALSE_KW) && !isNeg) {
|
|
sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_BOOLEAN);
|
|
if (sym != NULL) {
|
|
sym->constInt = 0;
|
|
sym->isDefined = true;
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
advance(p);
|
|
} else {
|
|
error(p, "Constant value must be a literal");
|
|
}
|
|
|
|
if (sym == NULL && !p->hasError) {
|
|
error(p, "Duplicate constant or symbol table full");
|
|
}
|
|
}
|
|
|
|
|
|
static void parseData(BasParserT *p) {
|
|
// DATA val1, val2, "string", ...
|
|
// Collect all values into the data pool. No runtime code is emitted.
|
|
advance(p); // consume DATA
|
|
|
|
for (;;) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
bool isNeg = false;
|
|
if (check(p, TOK_MINUS)) {
|
|
isNeg = true;
|
|
advance(p);
|
|
}
|
|
|
|
if (check(p, TOK_INT_LIT)) {
|
|
int32_t val = p->lex.token.intVal;
|
|
if (isNeg) {
|
|
val = -val;
|
|
}
|
|
BasValueT v = basValInteger((int16_t)val);
|
|
basAddData(&p->cg, v);
|
|
advance(p);
|
|
} else if (check(p, TOK_LONG_LIT)) {
|
|
int32_t val = (int32_t)p->lex.token.longVal;
|
|
if (isNeg) {
|
|
val = -val;
|
|
}
|
|
BasValueT v = basValLong(val);
|
|
basAddData(&p->cg, v);
|
|
advance(p);
|
|
} else if (check(p, TOK_FLOAT_LIT)) {
|
|
double val = p->lex.token.dblVal;
|
|
if (isNeg) {
|
|
val = -val;
|
|
}
|
|
BasValueT v = basValDouble(val);
|
|
basAddData(&p->cg, v);
|
|
advance(p);
|
|
} else if (check(p, TOK_STRING_LIT) && !isNeg) {
|
|
BasValueT v = basValStringFromC(p->lex.token.text);
|
|
basAddData(&p->cg, v);
|
|
basValRelease(&v);
|
|
advance(p);
|
|
} else {
|
|
// Unquoted text -- read as string up to comma/newline/EOF
|
|
// In QB, unquoted DATA values are treated as strings
|
|
if (isNeg) {
|
|
// Negative sign without a number -- treat "-" as string data
|
|
BasValueT v = basValStringFromC("-");
|
|
basAddData(&p->cg, v);
|
|
basValRelease(&v);
|
|
} else if (check(p, TOK_IDENT)) {
|
|
BasValueT v = basValStringFromC(p->lex.token.text);
|
|
basAddData(&p->cg, v);
|
|
basValRelease(&v);
|
|
advance(p);
|
|
} else {
|
|
error(p, "Expected DATA value");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseDeclare(BasParserT *p) {
|
|
// DECLARE SUB name(params)
|
|
// DECLARE FUNCTION name(params) AS type
|
|
advance(p); // consume DECLARE
|
|
|
|
BasSymKindE kind;
|
|
|
|
if (check(p, TOK_SUB)) {
|
|
kind = SYM_SUB;
|
|
advance(p);
|
|
} else if (check(p, TOK_FUNCTION)) {
|
|
kind = SYM_FUNCTION;
|
|
advance(p);
|
|
} else {
|
|
error(p, "Expected SUB or FUNCTION after DECLARE");
|
|
return;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "subroutine/function name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Parse parameter list
|
|
int32_t paramCount = 0;
|
|
uint8_t paramTypes[BAS_MAX_PARAMS];
|
|
bool paramByVal[BAS_MAX_PARAMS];
|
|
|
|
if (match(p, TOK_LPAREN)) {
|
|
while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) {
|
|
if (paramCount > 0) {
|
|
expect(p, TOK_COMMA);
|
|
}
|
|
|
|
bool byVal = false;
|
|
|
|
if (match(p, TOK_BYVAL)) {
|
|
byVal = true;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "parameter name");
|
|
return;
|
|
}
|
|
|
|
char paramName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
uint8_t pdt = suffixToType(paramName);
|
|
|
|
if (match(p, TOK_AS)) {
|
|
pdt = resolveTypeName(p);
|
|
}
|
|
|
|
if (paramCount < BAS_MAX_PARAMS) {
|
|
paramTypes[paramCount] = pdt;
|
|
paramByVal[paramCount] = byVal;
|
|
}
|
|
|
|
paramCount++;
|
|
}
|
|
|
|
expect(p, TOK_RPAREN);
|
|
}
|
|
|
|
// Return type for FUNCTION
|
|
uint8_t returnType = suffixToType(name);
|
|
|
|
if (kind == SYM_FUNCTION && match(p, TOK_AS)) {
|
|
returnType = resolveTypeName(p);
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
// Add to symbol table as forward declaration
|
|
BasSymbolT *sym = basSymTabAdd(&p->sym, name, kind, returnType);
|
|
|
|
if (sym == NULL) {
|
|
// Might already be declared -- look it up
|
|
sym = basSymTabFind(&p->sym, name);
|
|
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
}
|
|
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = false;
|
|
sym->codeAddr = 0;
|
|
sym->paramCount = paramCount;
|
|
|
|
for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) {
|
|
sym->paramTypes[i] = paramTypes[i];
|
|
sym->paramByVal[i] = paramByVal[i];
|
|
}
|
|
}
|
|
|
|
|
|
static void parseDef(BasParserT *p) {
|
|
// DEF FNname(params) = expression
|
|
advance(p); // consume DEF
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "function name (FNname)");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
|
|
if ((name[0] != 'F' && name[0] != 'f') || (name[1] != 'N' && name[1] != 'n')) {
|
|
error(p, "DEF function name must start with FN");
|
|
return;
|
|
}
|
|
|
|
advance(p);
|
|
|
|
int32_t skipJump = emitJump(p, OP_JMP);
|
|
int32_t funcAddr = basCodePos(&p->cg);
|
|
|
|
basSymTabEnterLocal(&p->sym);
|
|
basSymTabAllocSlot(&p->sym); // slot 0 for return value
|
|
|
|
int32_t paramCount = 0;
|
|
uint8_t paramTypes[BAS_MAX_PARAMS];
|
|
bool paramByVal[BAS_MAX_PARAMS];
|
|
|
|
if (match(p, TOK_LPAREN)) {
|
|
while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) {
|
|
if (paramCount > 0) {
|
|
expect(p, TOK_COMMA);
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "parameter name");
|
|
return;
|
|
}
|
|
|
|
char paramName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
uint8_t pdt = suffixToType(paramName);
|
|
if (match(p, TOK_AS)) {
|
|
pdt = resolveTypeName(p);
|
|
}
|
|
|
|
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
|
|
if (paramSym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
paramSym->scope = SCOPE_LOCAL;
|
|
paramSym->index = basSymTabAllocSlot(&p->sym);
|
|
paramSym->isDefined = true;
|
|
|
|
if (paramCount < BAS_MAX_PARAMS) {
|
|
paramTypes[paramCount] = pdt;
|
|
paramByVal[paramCount] = true;
|
|
}
|
|
paramCount++;
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
}
|
|
|
|
expect(p, TOK_EQ);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_RET_VAL);
|
|
|
|
basSymTabLeaveLocal(&p->sym);
|
|
|
|
uint8_t returnType = suffixToType(name);
|
|
bool savedLocal = p->sym.inLocalScope;
|
|
p->sym.inLocalScope = false;
|
|
BasSymbolT *funcSym = basSymTabAdd(&p->sym, name, SYM_FUNCTION, returnType);
|
|
p->sym.inLocalScope = savedLocal;
|
|
|
|
if (funcSym == NULL) {
|
|
error(p, "Could not register DEF function");
|
|
return;
|
|
}
|
|
|
|
funcSym->codeAddr = funcAddr;
|
|
funcSym->isDefined = true;
|
|
funcSym->paramCount = paramCount;
|
|
funcSym->scope = SCOPE_GLOBAL;
|
|
for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) {
|
|
funcSym->paramTypes[i] = paramTypes[i];
|
|
funcSym->paramByVal[i] = paramByVal[i];
|
|
}
|
|
|
|
patchCallAddrs(p, funcSym);
|
|
patchJump(p, skipJump);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// parseDefType -- DEFINT, DEFLNG, DEFSNG, DEFDBL, DEFSTR
|
|
// ============================================================
|
|
//
|
|
// Sets the default type for variables whose names start with
|
|
// letters in the given range. Example: DEFINT A-Z makes all
|
|
// untyped variables default to INTEGER.
|
|
|
|
static void parseDefType(BasParserT *p, uint8_t dataType) {
|
|
advance(p); // consume DEFxxx keyword
|
|
|
|
while (!p->hasError) {
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "letter or letter range");
|
|
return;
|
|
}
|
|
|
|
char startLetter = p->lex.token.text[0];
|
|
|
|
if (startLetter >= 'a' && startLetter <= 'z') {
|
|
startLetter -= 32;
|
|
}
|
|
|
|
if (startLetter < 'A' || startLetter > 'Z') {
|
|
error(p, "Expected letter A-Z");
|
|
return;
|
|
}
|
|
|
|
advance(p);
|
|
|
|
char endLetter = startLetter;
|
|
|
|
if (match(p, TOK_MINUS)) {
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "letter after '-'");
|
|
return;
|
|
}
|
|
|
|
endLetter = p->lex.token.text[0];
|
|
|
|
if (endLetter >= 'a' && endLetter <= 'z') {
|
|
endLetter -= 32;
|
|
}
|
|
|
|
if (endLetter < 'A' || endLetter > 'Z') {
|
|
error(p, "Expected letter A-Z");
|
|
return;
|
|
}
|
|
|
|
advance(p);
|
|
}
|
|
|
|
// Set default type for the range
|
|
for (char c = startLetter; c <= endLetter; c++) {
|
|
p->defType[c - 'A'] = dataType;
|
|
}
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseDimBounds(BasParserT *p, int32_t *outDims) {
|
|
// Parse each dimension bound, pushing (lbound, ubound) pairs onto the stack.
|
|
// Supports both "ubound" (lbound=optionBase) and "lbound TO ubound" syntax.
|
|
*outDims = 0;
|
|
|
|
for (;;) {
|
|
// Save code position before parsing the first expression
|
|
int32_t exprStart = basCodePos(&p->cg);
|
|
parseExpression(p);
|
|
|
|
if (match(p, TOK_TO)) {
|
|
// "lbound TO ubound" -- first expr is lbound, parse ubound next
|
|
parseExpression(p);
|
|
} else {
|
|
// Single value = ubound, lbound defaults to optionBase.
|
|
// Ubound expression already emitted. Insert PUSH_INT16 before it.
|
|
int32_t exprLen = basCodePos(&p->cg) - exprStart;
|
|
int32_t insertLen = 3; // OP_PUSH_INT16 + 2 bytes
|
|
|
|
if (basCodePos(&p->cg) + insertLen <= BAS_MAX_CODE) {
|
|
memmove(&p->cg.code[exprStart + insertLen], &p->cg.code[exprStart], exprLen);
|
|
p->cg.code[exprStart] = OP_PUSH_INT16;
|
|
int16_t lbound = (int16_t)p->optionBase;
|
|
memcpy(&p->cg.code[exprStart + 1], &lbound, 2);
|
|
p->cg.codeLen += insertLen;
|
|
}
|
|
}
|
|
|
|
(*outDims)++;
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseDim(BasParserT *p) {
|
|
// DIM [SHARED] var AS type
|
|
// DIM var(ubound) AS type
|
|
// DIM var(lbound TO ubound) AS type
|
|
// DIM var AS UdtType
|
|
advance(p); // consume DIM
|
|
|
|
// Check for SHARED keyword
|
|
bool isShared = false;
|
|
if (check(p, TOK_SHARED)) {
|
|
isShared = true;
|
|
advance(p);
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
bool isArray = false;
|
|
int32_t dims = 0;
|
|
|
|
// Check for array bounds
|
|
if (check(p, TOK_LPAREN)) {
|
|
isArray = true;
|
|
advance(p);
|
|
parseDimBounds(p, &dims);
|
|
expect(p, TOK_RPAREN);
|
|
}
|
|
|
|
// Optional AS type
|
|
uint8_t dt = suffixToType(name);
|
|
int32_t udtTypeId = -1;
|
|
int32_t fixedLen = 0;
|
|
if (match(p, TOK_AS)) {
|
|
dt = resolveTypeName(p);
|
|
if (dt == BAS_TYPE_UDT) {
|
|
udtTypeId = p->lastUdtTypeId;
|
|
}
|
|
// Check for STRING * n (fixed-length string)
|
|
if (dt == BAS_TYPE_STRING && check(p, TOK_STAR)) {
|
|
advance(p);
|
|
if (check(p, TOK_INT_LIT)) {
|
|
fixedLen = p->lex.token.intVal;
|
|
advance(p);
|
|
} else {
|
|
error(p, "Expected integer after STRING *");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
// Check for duplicate
|
|
BasSymbolT *existing = basSymTabFind(&p->sym, name);
|
|
if (existing != NULL && existing->isDefined) {
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf), "Variable '%s' already declared", name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
|
|
BasSymbolT *sym = basSymTabAdd(&p->sym, name, SYM_VARIABLE, dt);
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full or duplicate name");
|
|
return;
|
|
}
|
|
sym->index = basSymTabAllocSlot(&p->sym);
|
|
sym->isDefined = true;
|
|
sym->isArray = isArray;
|
|
sym->isShared = isShared;
|
|
sym->udtTypeId = udtTypeId;
|
|
sym->fixedLen = fixedLen;
|
|
|
|
if (p->sym.inLocalScope) {
|
|
sym->scope = SCOPE_LOCAL;
|
|
} else {
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
|
|
if (isArray) {
|
|
// Emit array dimension instruction
|
|
basEmit8(&p->cg, OP_DIM_ARRAY);
|
|
basEmit8(&p->cg, (uint8_t)dims);
|
|
basEmit8(&p->cg, dt);
|
|
emitStore(p, sym);
|
|
} else if (dt == BAS_TYPE_UDT && udtTypeId >= 0) {
|
|
// Allocate a UDT instance
|
|
BasSymbolT *typeSym = NULL;
|
|
for (int32_t i = 0; i < p->sym.count; i++) {
|
|
if (p->sym.symbols[i].kind == SYM_TYPE_DEF && p->sym.symbols[i].index == udtTypeId) {
|
|
typeSym = &p->sym.symbols[i];
|
|
break;
|
|
}
|
|
}
|
|
if (typeSym != NULL) {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)udtTypeId);
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)typeSym->fieldCount);
|
|
// OP_DIM_ARRAY with dims=0 signals UDT allocation
|
|
basEmit8(&p->cg, OP_DIM_ARRAY);
|
|
basEmit8(&p->cg, 0);
|
|
basEmit8(&p->cg, BAS_TYPE_UDT);
|
|
emitStore(p, sym);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseDo(BasParserT *p) {
|
|
// DO [WHILE|UNTIL cond]
|
|
// ...
|
|
// LOOP [WHILE|UNTIL cond]
|
|
advance(p); // consume DO
|
|
|
|
ExitListT savedExitDo = exitDoList;
|
|
exitListInit(&exitDoList);
|
|
|
|
int32_t loopTop = basCodePos(&p->cg);
|
|
|
|
bool hasPreCondition = false;
|
|
int32_t preCondJump = 0;
|
|
|
|
// DO WHILE cond / DO UNTIL cond
|
|
if (check(p, TOK_WHILE)) {
|
|
hasPreCondition = true;
|
|
advance(p);
|
|
parseExpression(p);
|
|
preCondJump = emitJump(p, OP_JMP_FALSE);
|
|
} else if (check(p, TOK_UNTIL)) {
|
|
hasPreCondition = true;
|
|
advance(p);
|
|
parseExpression(p);
|
|
preCondJump = emitJump(p, OP_JMP_TRUE);
|
|
}
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
// Loop body
|
|
while (!p->hasError && !check(p, TOK_LOOP) && !check(p, TOK_EOF)) {
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
expect(p, TOK_LOOP);
|
|
|
|
// LOOP WHILE cond / LOOP UNTIL cond
|
|
if (check(p, TOK_WHILE)) {
|
|
advance(p);
|
|
parseExpression(p);
|
|
// Jump back to loopTop if condition is true
|
|
basEmit8(&p->cg, OP_JMP_TRUE);
|
|
int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2));
|
|
basEmit16(&p->cg, backOffset);
|
|
} else if (check(p, TOK_UNTIL)) {
|
|
advance(p);
|
|
parseExpression(p);
|
|
// Jump back to loopTop if condition is false
|
|
basEmit8(&p->cg, OP_JMP_FALSE);
|
|
int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2));
|
|
basEmit16(&p->cg, backOffset);
|
|
} else {
|
|
// Plain LOOP -- unconditional jump back
|
|
basEmit8(&p->cg, OP_JMP);
|
|
int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2));
|
|
basEmit16(&p->cg, backOffset);
|
|
}
|
|
|
|
// Backpatch pre-condition jump (exits the loop)
|
|
if (hasPreCondition) {
|
|
patchJump(p, preCondJump);
|
|
}
|
|
|
|
// Patch all EXIT DO jumps to here
|
|
exitListPatch(&exitDoList, p);
|
|
exitDoList = savedExitDo;
|
|
}
|
|
|
|
|
|
static void parseEnd(BasParserT *p) {
|
|
// END -- by itself = halt
|
|
// END IF / END SUB / END FUNCTION / END SELECT are handled by their parsers
|
|
advance(p); // consume END
|
|
basEmit8(&p->cg, OP_HALT);
|
|
}
|
|
|
|
|
|
static void parseErase(BasParserT *p) {
|
|
// ERASE arrayVar
|
|
advance(p); // consume ERASE
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "array variable name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
|
if (sym == NULL || !sym->isArray) {
|
|
error(p, "ERASE requires an array variable");
|
|
return;
|
|
}
|
|
|
|
emitLoad(p, sym);
|
|
basEmit8(&p->cg, OP_ERASE);
|
|
emitStore(p, sym);
|
|
}
|
|
|
|
|
|
static void parseExit(BasParserT *p) {
|
|
advance(p); // consume EXIT
|
|
|
|
if (check(p, TOK_FOR)) {
|
|
advance(p);
|
|
int32_t addr = emitJump(p, OP_JMP);
|
|
exitListAdd(&exitForList, addr);
|
|
} else if (check(p, TOK_DO)) {
|
|
advance(p);
|
|
int32_t addr = emitJump(p, OP_JMP);
|
|
exitListAdd(&exitDoList, addr);
|
|
} else if (check(p, TOK_SUB)) {
|
|
advance(p);
|
|
int32_t addr = emitJump(p, OP_JMP);
|
|
exitListAdd(&exitSubList, addr);
|
|
} else if (check(p, TOK_FUNCTION)) {
|
|
advance(p);
|
|
int32_t addr = emitJump(p, OP_JMP);
|
|
exitListAdd(&exitFuncList, addr);
|
|
} else {
|
|
error(p, "Expected FOR, DO, SUB, or FUNCTION after EXIT");
|
|
}
|
|
}
|
|
|
|
|
|
static void parseFor(BasParserT *p) {
|
|
// FOR var = start TO limit [STEP step]
|
|
// ...
|
|
// NEXT [var]
|
|
advance(p); // consume FOR
|
|
|
|
ExitListT savedExitFor = exitForList;
|
|
exitListInit(&exitForList);
|
|
|
|
// Loop variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "loop variable");
|
|
return;
|
|
}
|
|
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *loopVar = ensureVariable(p, varName);
|
|
if (loopVar == NULL) {
|
|
return;
|
|
}
|
|
|
|
// = start
|
|
expect(p, TOK_EQ);
|
|
parseExpression(p);
|
|
emitStore(p, loopVar);
|
|
|
|
// TO limit
|
|
expect(p, TOK_TO);
|
|
parseExpression(p); // limit is on stack
|
|
|
|
// STEP step (optional, default 1)
|
|
if (match(p, TOK_STEP)) {
|
|
parseExpression(p); // step is on stack
|
|
} else {
|
|
// Default step = 1
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 1);
|
|
}
|
|
|
|
// Emit FOR_INIT -- sets up the for-loop state in the VM
|
|
basEmit8(&p->cg, OP_FOR_INIT);
|
|
basEmitU16(&p->cg, (uint16_t)loopVar->index);
|
|
basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : 0);
|
|
|
|
int32_t loopBody = basCodePos(&p->cg);
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
// Loop body
|
|
while (!p->hasError && !check(p, TOK_NEXT) && !check(p, TOK_EOF)) {
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
expect(p, TOK_NEXT);
|
|
|
|
// Optional variable name after NEXT (we just skip it)
|
|
if (check(p, TOK_IDENT)) {
|
|
advance(p);
|
|
}
|
|
|
|
// Emit FOR_NEXT with backward jump to loop body
|
|
basEmit8(&p->cg, OP_FOR_NEXT);
|
|
basEmitU16(&p->cg, (uint16_t)loopVar->index);
|
|
basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : 0);
|
|
int16_t backOffset = (int16_t)(loopBody - (basCodePos(&p->cg) + 2));
|
|
basEmit16(&p->cg, backOffset);
|
|
|
|
// Patch all EXIT FOR jumps to here
|
|
exitListPatch(&exitForList, p);
|
|
exitForList = savedExitFor;
|
|
}
|
|
|
|
|
|
static void parseFunction(BasParserT *p) {
|
|
// FUNCTION name(params) AS type
|
|
// ...
|
|
// END FUNCTION
|
|
advance(p); // consume FUNCTION
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "function name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Save current proc name for STATIC variable mangling
|
|
strncpy(p->currentProc, name, BAS_MAX_TOKEN_LEN - 1);
|
|
p->currentProc[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
|
|
// Jump over the function body in module-level code
|
|
int32_t skipJump = emitJump(p, OP_JMP);
|
|
|
|
int32_t funcAddr = basCodePos(&p->cg);
|
|
|
|
// Enter local scope
|
|
basSymTabEnterLocal(&p->sym);
|
|
|
|
ExitListT savedExitFunc = exitFuncList;
|
|
exitListInit(&exitFuncList);
|
|
|
|
// Allocate slot 0 for return value
|
|
basSymTabAllocSlot(&p->sym);
|
|
|
|
// Parse parameter list
|
|
int32_t paramCount = 0;
|
|
uint8_t paramTypes[BAS_MAX_PARAMS];
|
|
bool paramByVal[BAS_MAX_PARAMS];
|
|
|
|
if (match(p, TOK_LPAREN)) {
|
|
while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) {
|
|
if (paramCount > 0) {
|
|
expect(p, TOK_COMMA);
|
|
}
|
|
|
|
bool byVal = false;
|
|
if (match(p, TOK_BYVAL)) {
|
|
byVal = true;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "parameter name");
|
|
return;
|
|
}
|
|
|
|
char paramName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
uint8_t pdt = suffixToType(paramName);
|
|
if (match(p, TOK_AS)) {
|
|
pdt = resolveTypeName(p);
|
|
}
|
|
|
|
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
|
|
if (paramSym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
paramSym->scope = SCOPE_LOCAL;
|
|
paramSym->index = basSymTabAllocSlot(&p->sym);
|
|
paramSym->isDefined = true;
|
|
|
|
if (paramCount < BAS_MAX_PARAMS) {
|
|
paramTypes[paramCount] = pdt;
|
|
paramByVal[paramCount] = byVal;
|
|
}
|
|
paramCount++;
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
}
|
|
|
|
// Return type
|
|
uint8_t returnType = suffixToType(name);
|
|
if (match(p, TOK_AS)) {
|
|
returnType = resolveTypeName(p);
|
|
}
|
|
|
|
// Register the function in the symbol table (global scope entry)
|
|
// We need to temporarily leave local scope to add to global
|
|
BasSymbolT *existing = basSymTabFindGlobal(&p->sym, name);
|
|
BasSymbolT *funcSym = NULL;
|
|
|
|
if (existing != NULL && existing->kind == SYM_FUNCTION) {
|
|
// Forward-declared, now define it
|
|
funcSym = existing;
|
|
} else {
|
|
// Temporarily store the local state, add globally
|
|
bool savedLocal = p->sym.inLocalScope;
|
|
p->sym.inLocalScope = false;
|
|
funcSym = basSymTabAdd(&p->sym, name, SYM_FUNCTION, returnType);
|
|
p->sym.inLocalScope = savedLocal;
|
|
}
|
|
|
|
if (funcSym == NULL) {
|
|
error(p, "Could not register function");
|
|
return;
|
|
}
|
|
|
|
funcSym->codeAddr = funcAddr;
|
|
funcSym->isDefined = true;
|
|
funcSym->paramCount = paramCount;
|
|
funcSym->scope = SCOPE_GLOBAL;
|
|
for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) {
|
|
funcSym->paramTypes[i] = paramTypes[i];
|
|
funcSym->paramByVal[i] = paramByVal[i];
|
|
}
|
|
|
|
// Backpatch any forward-reference calls to this function
|
|
patchCallAddrs(p, funcSym);
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
// Parse function body
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
// Check for END FUNCTION
|
|
if (check(p, TOK_END)) {
|
|
// Peek ahead -- we need to see if it's END FUNCTION
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_FUNCTION)) {
|
|
advance(p);
|
|
break;
|
|
}
|
|
// Not END FUNCTION, restore and parse as statement
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
// Patch EXIT FUNCTION jumps
|
|
exitListPatch(&exitFuncList, p);
|
|
exitFuncList = savedExitFunc;
|
|
|
|
// Load return value from slot 0 and return
|
|
basEmit8(&p->cg, OP_LOAD_LOCAL);
|
|
basEmitU16(&p->cg, 0);
|
|
basEmit8(&p->cg, OP_RET_VAL);
|
|
|
|
// Leave local scope
|
|
basSymTabLeaveLocal(&p->sym);
|
|
p->currentProc[0] = '\0';
|
|
|
|
// Patch the skip jump
|
|
patchJump(p, skipJump);
|
|
}
|
|
|
|
|
|
static void parseGosub(BasParserT *p) {
|
|
// GOSUB label -- push return PC, then JMP to label
|
|
advance(p); // consume GOSUB
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "label name");
|
|
return;
|
|
}
|
|
|
|
char labelName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
labelName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Push the return PC (address after the JMP instruction)
|
|
// OP_PUSH_INT32 = 1 + 4 bytes, OP_JMP = 1 + 2 bytes
|
|
int32_t pushPos = basCodePos(&p->cg);
|
|
basEmit8(&p->cg, OP_PUSH_INT32);
|
|
basEmit16(&p->cg, 0); // placeholder lo
|
|
basEmit16(&p->cg, 0); // placeholder hi
|
|
|
|
// Emit the jump to the label
|
|
emitJumpToLabel(p, OP_JMP, labelName);
|
|
|
|
// Backpatch the return address (PC is now right after the JMP)
|
|
int32_t returnPc = basCodePos(&p->cg);
|
|
int16_t lo = (int16_t)(returnPc & 0xFFFF);
|
|
int16_t hi = (int16_t)((returnPc >> 16) & 0xFFFF);
|
|
basPatch16(&p->cg, pushPos + 1, lo);
|
|
basPatch16(&p->cg, pushPos + 3, hi);
|
|
}
|
|
|
|
|
|
static void parseGoto(BasParserT *p) {
|
|
// GOTO label
|
|
advance(p); // consume GOTO
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "label name");
|
|
return;
|
|
}
|
|
|
|
char labelName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
labelName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
emitJumpToLabel(p, OP_JMP, labelName);
|
|
}
|
|
|
|
|
|
static void parseIf(BasParserT *p) {
|
|
// IF expr THEN
|
|
// ...
|
|
// [ELSEIF expr THEN]
|
|
// ...
|
|
// [ELSE]
|
|
// ...
|
|
// END IF
|
|
advance(p); // consume IF
|
|
|
|
parseExpression(p);
|
|
|
|
expect(p, TOK_THEN);
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
// Check for single-line IF: IF cond THEN stmt
|
|
if (!check(p, TOK_NEWLINE) && !check(p, TOK_EOF)) {
|
|
// Single-line IF
|
|
int32_t falseJump = emitJump(p, OP_JMP_FALSE);
|
|
|
|
parseStatement(p);
|
|
|
|
if (check(p, TOK_ELSE)) {
|
|
advance(p);
|
|
int32_t endJump = emitJump(p, OP_JMP);
|
|
patchJump(p, falseJump);
|
|
parseStatement(p);
|
|
patchJump(p, endJump);
|
|
} else {
|
|
patchJump(p, falseJump);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Multi-line IF
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
int32_t falseJump = emitJump(p, OP_JMP_FALSE);
|
|
|
|
// Collect end-of-chain jumps for backpatching
|
|
int32_t endJumps[MAX_EXITS];
|
|
int32_t endJumpCount = 0;
|
|
|
|
// Parse THEN block
|
|
while (!p->hasError && !check(p, TOK_ELSEIF) && !check(p, TOK_ELSE) && !check(p, TOK_EOF)) {
|
|
// Check for END IF
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_IF)) {
|
|
advance(p);
|
|
patchJump(p, falseJump);
|
|
// Patch all end jumps
|
|
for (int32_t i = 0; i < endJumpCount; i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
// ELSEIF chain
|
|
while (!p->hasError && check(p, TOK_ELSEIF)) {
|
|
// Jump from previous true-block to end of chain
|
|
if (endJumpCount < MAX_EXITS) {
|
|
endJumps[endJumpCount++] = emitJump(p, OP_JMP);
|
|
}
|
|
|
|
// Patch the previous false jump to here
|
|
patchJump(p, falseJump);
|
|
|
|
advance(p); // consume ELSEIF
|
|
parseExpression(p);
|
|
expect(p, TOK_THEN);
|
|
|
|
falseJump = emitJump(p, OP_JMP_FALSE);
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
while (!p->hasError && !check(p, TOK_ELSEIF) && !check(p, TOK_ELSE) && !check(p, TOK_EOF)) {
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_IF)) {
|
|
advance(p);
|
|
patchJump(p, falseJump);
|
|
for (int32_t i = 0; i < endJumpCount; i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
}
|
|
|
|
// ELSE block
|
|
if (!p->hasError && check(p, TOK_ELSE)) {
|
|
if (endJumpCount < MAX_EXITS) {
|
|
endJumps[endJumpCount++] = emitJump(p, OP_JMP);
|
|
}
|
|
patchJump(p, falseJump);
|
|
falseJump = -1; // no more false jump needed
|
|
|
|
advance(p); // consume ELSE
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_IF)) {
|
|
advance(p);
|
|
for (int32_t i = 0; i < endJumpCount; i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
}
|
|
|
|
// Patch the last false jump if no ELSE block
|
|
if (falseJump >= 0) {
|
|
patchJump(p, falseJump);
|
|
}
|
|
|
|
// Patch all end-of-chain jumps
|
|
for (int32_t i = 0; i < endJumpCount; i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
|
|
// If we got here without END IF, that's an error
|
|
if (!p->hasError) {
|
|
error(p, "Expected END IF");
|
|
}
|
|
}
|
|
|
|
|
|
static void parseInput(BasParserT *p) {
|
|
// INPUT #channel, var
|
|
// INPUT [prompt;] var
|
|
advance(p); // consume INPUT
|
|
|
|
// Check for file I/O: INPUT #channel, var
|
|
if (check(p, TOK_HASH)) {
|
|
advance(p); // consume #
|
|
|
|
// Channel number
|
|
parseExpression(p);
|
|
|
|
// Comma separator
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Target variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
basEmit8(&p->cg, OP_FILE_INPUT);
|
|
|
|
BasSymbolT *sym = ensureVariable(p, varName);
|
|
|
|
if (sym != NULL) {
|
|
// If the variable is numeric, convert the input string
|
|
if (sym->dataType != BAS_TYPE_STRING) {
|
|
if (sym->dataType == BAS_TYPE_INTEGER || sym->dataType == BAS_TYPE_LONG) {
|
|
basEmit8(&p->cg, OP_CONV_STR_INT);
|
|
} else {
|
|
basEmit8(&p->cg, OP_CONV_STR_FLT);
|
|
}
|
|
}
|
|
|
|
emitStore(p, sym);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Check for optional prompt string
|
|
if (check(p, TOK_STRING_LIT)) {
|
|
uint16_t idx = basAddConstant(&p->cg, p->lex.token.text, p->lex.token.textLen);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, idx);
|
|
advance(p);
|
|
// Semicolon after prompt
|
|
if (match(p, TOK_SEMICOLON)) {
|
|
// nothing extra
|
|
} else if (match(p, TOK_COMMA)) {
|
|
// comma -- no question mark (just prompt)
|
|
}
|
|
} else {
|
|
// No prompt -- push empty string
|
|
uint16_t idx = basAddConstant(&p->cg, "", 0);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, idx);
|
|
}
|
|
|
|
// Emit INPUT opcode -- pops prompt, pushes input string
|
|
basEmit8(&p->cg, OP_INPUT);
|
|
|
|
// Target variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *sym = ensureVariable(p, varName);
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
|
|
// If the variable is numeric, we need to convert the input string
|
|
if (sym->dataType != BAS_TYPE_STRING) {
|
|
if (sym->dataType == BAS_TYPE_INTEGER || sym->dataType == BAS_TYPE_LONG) {
|
|
basEmit8(&p->cg, OP_CONV_STR_INT);
|
|
} else {
|
|
basEmit8(&p->cg, OP_CONV_STR_FLT);
|
|
}
|
|
}
|
|
|
|
emitStore(p, sym);
|
|
}
|
|
|
|
|
|
static void parseLineInput(BasParserT *p) {
|
|
// LINE INPUT #channel, var
|
|
advance(p); // consume LINE
|
|
|
|
if (!check(p, TOK_INPUT)) {
|
|
error(p, "Expected INPUT after LINE");
|
|
return;
|
|
}
|
|
|
|
advance(p); // consume INPUT
|
|
|
|
// Must have # for file I/O
|
|
if (!match(p, TOK_HASH)) {
|
|
error(p, "Expected # for file channel in LINE INPUT");
|
|
return;
|
|
}
|
|
|
|
// Channel expression
|
|
parseExpression(p);
|
|
|
|
// Comma separator
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Target variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
basEmit8(&p->cg, OP_FILE_LINE_INPUT);
|
|
|
|
BasSymbolT *sym = ensureVariable(p, varName);
|
|
|
|
if (sym != NULL) {
|
|
emitStore(p, sym);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseModule(BasParserT *p) {
|
|
skipNewlines(p);
|
|
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
// End of module -- emit HALT
|
|
basEmit8(&p->cg, OP_HALT);
|
|
}
|
|
|
|
|
|
#define MAX_ON_LABELS 32
|
|
|
|
static void parseOn(BasParserT *p) {
|
|
// ON ERROR GOTO label -- error handler
|
|
// ON expr GOTO label1, label2, ... -- computed goto
|
|
// ON expr GOSUB label1, label2, ... -- computed gosub
|
|
advance(p); // consume ON
|
|
|
|
// ON ERROR GOTO is a special form
|
|
if (check(p, TOK_ERROR_KW)) {
|
|
parseOnError(p);
|
|
return;
|
|
}
|
|
|
|
// ON expr GOTO/GOSUB label1, label2, ...
|
|
parseExpression(p);
|
|
|
|
bool isGosub;
|
|
|
|
if (check(p, TOK_GOTO)) {
|
|
isGosub = false;
|
|
advance(p);
|
|
} else if (check(p, TOK_GOSUB)) {
|
|
isGosub = true;
|
|
advance(p);
|
|
} else {
|
|
error(p, "Expected GOTO or GOSUB after ON expression");
|
|
return;
|
|
}
|
|
|
|
// Track end-of-gosub jumps for patching
|
|
int32_t endJumps[MAX_ON_LABELS];
|
|
int32_t endJumpCount = 0;
|
|
int32_t labelIdx = 1;
|
|
|
|
for (;;) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "label name");
|
|
return;
|
|
}
|
|
|
|
char labelName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
labelName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// DUP the selector
|
|
basEmit8(&p->cg, OP_DUP);
|
|
|
|
// PUSH the 1-based index
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)labelIdx);
|
|
|
|
// Compare
|
|
basEmit8(&p->cg, OP_CMP_EQ);
|
|
|
|
// JMP_FALSE to skip this branch
|
|
int32_t skipAddr = emitJump(p, OP_JMP_FALSE);
|
|
|
|
// Match: POP the selector value
|
|
basEmit8(&p->cg, OP_POP);
|
|
|
|
if (isGosub) {
|
|
// Push return PC before jumping
|
|
int32_t pushPos = basCodePos(&p->cg);
|
|
basEmit8(&p->cg, OP_PUSH_INT32);
|
|
basEmit16(&p->cg, 0); // placeholder lo
|
|
basEmit16(&p->cg, 0); // placeholder hi
|
|
|
|
emitJumpToLabel(p, OP_JMP, labelName);
|
|
|
|
// Backpatch the return address
|
|
int32_t returnPc = basCodePos(&p->cg);
|
|
int16_t lo = (int16_t)(returnPc & 0xFFFF);
|
|
int16_t hi = (int16_t)((returnPc >> 16) & 0xFFFF);
|
|
basPatch16(&p->cg, pushPos + 1, lo);
|
|
basPatch16(&p->cg, pushPos + 3, hi);
|
|
|
|
// After GOSUB returns, jump to end of ON...GOSUB
|
|
if (endJumpCount < MAX_ON_LABELS) {
|
|
endJumps[endJumpCount++] = emitJump(p, OP_JMP);
|
|
}
|
|
} else {
|
|
// GOTO: just jump to the label
|
|
emitJumpToLabel(p, OP_JMP, labelName);
|
|
}
|
|
|
|
// Patch the skip (no-match continues to next branch)
|
|
patchJump(p, skipAddr);
|
|
|
|
labelIdx++;
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// No match: POP the selector and fall through
|
|
basEmit8(&p->cg, OP_POP);
|
|
|
|
// Patch all end-of-gosub jumps to here
|
|
int32_t endTarget = basCodePos(&p->cg);
|
|
|
|
for (int32_t i = 0; i < endJumpCount; i++) {
|
|
int16_t offset = (int16_t)(endTarget - (endJumps[i] + 2));
|
|
basPatch16(&p->cg, endJumps[i], offset);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseOnError(BasParserT *p) {
|
|
// ON ERROR GOTO label
|
|
// ON ERROR GOTO 0 (disable)
|
|
// Note: ON and ERROR already consumed by parseOn dispatcher
|
|
advance(p); // consume ERROR
|
|
|
|
if (!check(p, TOK_GOTO)) {
|
|
error(p, "Expected GOTO after ON ERROR");
|
|
return;
|
|
}
|
|
advance(p); // consume GOTO
|
|
|
|
// ON ERROR GOTO 0 -- disable error handler
|
|
if (check(p, TOK_INT_LIT) && p->lex.token.intVal == 0) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_ON_ERROR);
|
|
basEmit16(&p->cg, 0);
|
|
return;
|
|
}
|
|
|
|
// ON ERROR GOTO label
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "label name or 0");
|
|
return;
|
|
}
|
|
|
|
char labelName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
labelName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Look up the label
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, labelName);
|
|
|
|
if (sym != NULL && sym->kind == SYM_LABEL && sym->isDefined) {
|
|
// Label already defined -- emit ON_ERROR with offset to handler
|
|
basEmit8(&p->cg, OP_ON_ERROR);
|
|
int32_t here = basCodePos(&p->cg);
|
|
int16_t offset = (int16_t)(sym->codeAddr - (here + 2));
|
|
basEmit16(&p->cg, offset);
|
|
} else {
|
|
// Forward reference
|
|
if (sym == NULL) {
|
|
sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0);
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = false;
|
|
sym->codeAddr = 0;
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_ON_ERROR);
|
|
int32_t patchAddr = basCodePos(&p->cg);
|
|
basEmit16(&p->cg, 0);
|
|
|
|
if (sym->patchCount < BAS_MAX_CALL_PATCHES) {
|
|
sym->patchAddrs[sym->patchCount++] = patchAddr;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseOpen(BasParserT *p) {
|
|
// OPEN filename FOR mode AS #channel
|
|
advance(p); // consume OPEN
|
|
|
|
// Filename expression
|
|
parseExpression(p);
|
|
|
|
// FOR keyword
|
|
expect(p, TOK_FOR);
|
|
|
|
// Mode: INPUT, OUTPUT, APPEND
|
|
uint8_t mode;
|
|
|
|
if (check(p, TOK_INPUT)) {
|
|
mode = 1; // INPUT
|
|
advance(p);
|
|
} else if (check(p, TOK_OUTPUT)) {
|
|
mode = 2; // OUTPUT
|
|
advance(p);
|
|
} else if (check(p, TOK_APPEND)) {
|
|
mode = 3; // APPEND
|
|
advance(p);
|
|
} else if (check(p, TOK_RANDOM)) {
|
|
mode = 4; // RANDOM
|
|
advance(p);
|
|
} else if (check(p, TOK_BINARY)) {
|
|
mode = 5; // BINARY
|
|
advance(p);
|
|
} else {
|
|
error(p, "Expected INPUT, OUTPUT, APPEND, RANDOM, or BINARY after FOR");
|
|
return;
|
|
}
|
|
|
|
// AS keyword
|
|
expect(p, TOK_AS);
|
|
|
|
// Optional # prefix
|
|
match(p, TOK_HASH);
|
|
|
|
// Channel number expression
|
|
parseExpression(p);
|
|
|
|
// Optional LEN = recordsize (for RANDOM mode)
|
|
if (checkKeyword(p, "LEN")) {
|
|
advance(p); // consume LEN
|
|
expect(p, TOK_EQ);
|
|
// For now we just parse and discard -- record length is not
|
|
// enforced at the VM level (GET/PUT use variable type size)
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_POP);
|
|
}
|
|
|
|
// Emit: stack has [filename, channel] -- OP_FILE_OPEN reads mode byte
|
|
basEmit8(&p->cg, OP_FILE_OPEN);
|
|
basEmit8(&p->cg, mode);
|
|
}
|
|
|
|
|
|
static void parseOption(BasParserT *p) {
|
|
// OPTION BASE 0 | OPTION BASE 1
|
|
// OPTION COMPARE BINARY | OPTION COMPARE TEXT
|
|
advance(p); // consume OPTION
|
|
|
|
if (check(p, TOK_BASE)) {
|
|
advance(p); // consume BASE
|
|
|
|
if (!check(p, TOK_INT_LIT)) {
|
|
error(p, "Expected 0 or 1 after OPTION BASE");
|
|
return;
|
|
}
|
|
|
|
int32_t base = p->lex.token.intVal;
|
|
|
|
if (base != 0 && base != 1) {
|
|
error(p, "OPTION BASE must be 0 or 1");
|
|
return;
|
|
}
|
|
|
|
p->optionBase = base;
|
|
advance(p);
|
|
return;
|
|
}
|
|
|
|
if (checkKeyword(p, "COMPARE")) {
|
|
advance(p); // consume COMPARE
|
|
|
|
if (check(p, TOK_BINARY)) {
|
|
p->optionCompareText = false;
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_COMPARE_MODE);
|
|
basEmit8(&p->cg, 0);
|
|
} else if (checkKeyword(p, "TEXT")) {
|
|
p->optionCompareText = true;
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_COMPARE_MODE);
|
|
basEmit8(&p->cg, 1);
|
|
} else {
|
|
error(p, "Expected BINARY or TEXT after OPTION COMPARE");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (check(p, TOK_EXPLICIT)) {
|
|
advance(p);
|
|
p->optionExplicit = true;
|
|
return;
|
|
}
|
|
|
|
error(p, "Expected BASE, COMPARE, or EXPLICIT after OPTION");
|
|
}
|
|
|
|
|
|
static void parsePrint(BasParserT *p) {
|
|
// PRINT [#channel, expr]
|
|
// PRINT [expr] [; expr] [, expr] [;]
|
|
// PRINT USING "fmt"; expr [; expr] ...
|
|
advance(p); // consume PRINT
|
|
|
|
// Check for file I/O: PRINT #channel, expr
|
|
if (check(p, TOK_HASH)) {
|
|
advance(p); // consume #
|
|
|
|
// Channel number
|
|
parseExpression(p);
|
|
|
|
// Comma separator
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Value to print
|
|
parseExpression(p);
|
|
|
|
basEmit8(&p->cg, OP_FILE_PRINT);
|
|
return;
|
|
}
|
|
|
|
// Check for PRINT USING
|
|
if (checkKeyword(p, "USING")) {
|
|
advance(p); // consume USING
|
|
|
|
// Parse format string expression
|
|
parseExpression(p);
|
|
|
|
// Semicolon separates format from values
|
|
expect(p, TOK_SEMICOLON);
|
|
|
|
// Parse values, each one gets formatted with PRINT_USING
|
|
for (;;) {
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_PRINT_USING);
|
|
basEmit8(&p->cg, OP_PRINT);
|
|
|
|
if (check(p, TOK_SEMICOLON)) {
|
|
advance(p);
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_PRINT_NL);
|
|
return;
|
|
}
|
|
|
|
bool trailingSemicolon = false;
|
|
|
|
// Empty PRINT = just newline
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
basEmit8(&p->cg, OP_PRINT_NL);
|
|
return;
|
|
}
|
|
|
|
while (!p->hasError) {
|
|
trailingSemicolon = false;
|
|
|
|
if (check(p, TOK_SEMICOLON)) {
|
|
// Just a semicolon -- no space
|
|
trailingSemicolon = true;
|
|
advance(p);
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (check(p, TOK_COMMA)) {
|
|
// Comma -- print tab
|
|
basEmit8(&p->cg, OP_PRINT_TAB);
|
|
advance(p);
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
trailingSemicolon = true; // comma at end suppresses newline too
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
break;
|
|
}
|
|
|
|
// Check for SPC(n) and TAB(n) inside PRINT
|
|
if (checkKeyword(p, "SPC")) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_PRINT_SPC_N);
|
|
continue;
|
|
}
|
|
|
|
if (checkKeyword(p, "TAB")) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_PRINT_TAB_N);
|
|
continue;
|
|
}
|
|
|
|
// Expression
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_PRINT);
|
|
}
|
|
|
|
// Print newline unless suppressed by trailing semicolon/comma
|
|
if (!trailingSemicolon) {
|
|
basEmit8(&p->cg, OP_PRINT_NL);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseRead(BasParserT *p) {
|
|
// READ var1, var2, ...
|
|
advance(p); // consume READ
|
|
|
|
for (;;) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *sym = ensureVariable(p, name);
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_READ_DATA);
|
|
emitStore(p, sym);
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseRedim(BasParserT *p) {
|
|
// REDIM [PRESERVE] var(bounds) AS type
|
|
advance(p); // consume REDIM
|
|
|
|
uint8_t preserve = 0;
|
|
if (check(p, TOK_PRESERVE)) {
|
|
preserve = 1;
|
|
advance(p);
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "array variable name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
|
if (sym == NULL) {
|
|
sym = ensureVariable(p, name);
|
|
}
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
sym->isArray = true;
|
|
|
|
// Load the old array reference
|
|
emitLoad(p, sym);
|
|
|
|
// Parse new bounds
|
|
int32_t dims = 0;
|
|
expect(p, TOK_LPAREN);
|
|
parseDimBounds(p, &dims);
|
|
expect(p, TOK_RPAREN);
|
|
|
|
// Optional AS type
|
|
if (match(p, TOK_AS)) {
|
|
resolveTypeName(p);
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_REDIM);
|
|
basEmit8(&p->cg, (uint8_t)dims);
|
|
basEmit8(&p->cg, preserve);
|
|
emitStore(p, sym);
|
|
}
|
|
|
|
|
|
static void parseRestore(BasParserT *p) {
|
|
// RESTORE -- reset the DATA read pointer to the beginning
|
|
advance(p); // consume RESTORE
|
|
basEmit8(&p->cg, OP_RESTORE);
|
|
}
|
|
|
|
|
|
static void parseResume(BasParserT *p) {
|
|
// RESUME -- re-execute the statement that caused the error
|
|
// RESUME NEXT -- continue at the next statement after the error
|
|
advance(p); // consume RESUME
|
|
|
|
if (check(p, TOK_NEXT)) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_RESUME_NEXT);
|
|
} else {
|
|
basEmit8(&p->cg, OP_RESUME);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseSelectCase(BasParserT *p) {
|
|
// SELECT CASE expr
|
|
// CASE val [, val] ...
|
|
// ...
|
|
// [CASE ELSE]
|
|
// ...
|
|
// END SELECT
|
|
advance(p); // consume SELECT
|
|
expect(p, TOK_CASE);
|
|
|
|
// Evaluate the test expression -- stays on stack throughout
|
|
parseExpression(p);
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
int32_t endJumps[MAX_EXITS];
|
|
int32_t endJumpCount = 0;
|
|
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
// Check for END SELECT
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_SELECT)) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_POP); // pop test expression
|
|
for (int32_t i = 0; i < endJumpCount; i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
|
|
if (!check(p, TOK_CASE)) {
|
|
error(p, "Expected CASE or END SELECT");
|
|
return;
|
|
}
|
|
|
|
advance(p); // consume CASE
|
|
|
|
// CASE ELSE -- always matches, no comparison needed
|
|
if (check(p, TOK_ELSE)) {
|
|
advance(p);
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
// Parse body until END SELECT
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_SELECT)) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_POP);
|
|
for (int32_t i = 0; i < endJumpCount; i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// CASE val [, val] ...
|
|
//
|
|
// Strategy for multi-value CASE using JMP_TRUE chaining:
|
|
// For each value:
|
|
// DUP testval
|
|
// push value
|
|
// CMP_EQ
|
|
// JMP_TRUE -> body
|
|
// JMP -> next_case (none of the values matched)
|
|
// body:
|
|
// ...statements...
|
|
// JMP -> end_select
|
|
// next_case:
|
|
|
|
int32_t bodyJumps[MAX_EXITS];
|
|
int32_t bodyJumpCount = 0;
|
|
|
|
// First value
|
|
basEmit8(&p->cg, OP_DUP);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_CMP_EQ);
|
|
|
|
if (bodyJumpCount < MAX_EXITS) {
|
|
bodyJumps[bodyJumpCount++] = emitJump(p, OP_JMP_TRUE);
|
|
}
|
|
|
|
// Additional comma-separated values
|
|
while (!p->hasError && match(p, TOK_COMMA)) {
|
|
basEmit8(&p->cg, OP_DUP);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_CMP_EQ);
|
|
|
|
if (bodyJumpCount < MAX_EXITS) {
|
|
bodyJumps[bodyJumpCount++] = emitJump(p, OP_JMP_TRUE);
|
|
}
|
|
}
|
|
|
|
// None matched -- jump to next case
|
|
int32_t nextCaseJump = emitJump(p, OP_JMP);
|
|
|
|
// Patch all body jumps to here (start of body)
|
|
for (int32_t i = 0; i < bodyJumpCount; i++) {
|
|
patchJump(p, bodyJumps[i]);
|
|
}
|
|
|
|
// Parse the CASE body
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
while (!p->hasError && !check(p, TOK_CASE) && !check(p, TOK_EOF)) {
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_SELECT)) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_POP);
|
|
patchJump(p, nextCaseJump);
|
|
for (int32_t i = 0; i < endJumpCount; i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
// Jump to end of SELECT (skip remaining cases)
|
|
if (endJumpCount < MAX_EXITS) {
|
|
endJumps[endJumpCount++] = emitJump(p, OP_JMP);
|
|
}
|
|
|
|
// Patch the next-case jump to here
|
|
patchJump(p, nextCaseJump);
|
|
}
|
|
|
|
// Pop test value (reached if no END SELECT but hit EOF)
|
|
basEmit8(&p->cg, OP_POP);
|
|
|
|
for (int32_t i = 0; i < endJumpCount; i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
|
|
if (!p->hasError) {
|
|
error(p, "Expected END SELECT");
|
|
}
|
|
}
|
|
|
|
|
|
static void parseShell(BasParserT *p) {
|
|
// SHELL "command" -- execute an OS command (discard return value)
|
|
// SHELL -- no argument, no-op in embedded context
|
|
advance(p); // consume SHELL
|
|
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
// No argument -- push empty string and call SHELL (no-op)
|
|
uint16_t idx = basAddConstant(&p->cg, "", 0);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, idx);
|
|
} else {
|
|
parseExpression(p);
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_SHELL);
|
|
basEmit8(&p->cg, OP_POP); // discard return value in statement form
|
|
}
|
|
|
|
|
|
static void parseSleep(BasParserT *p) {
|
|
// SLEEP [seconds]
|
|
// If no argument, default to 1 second
|
|
advance(p); // consume SLEEP
|
|
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
// No argument -- push 1 second
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 1);
|
|
} else {
|
|
parseExpression(p);
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_SLEEP);
|
|
}
|
|
|
|
|
|
static void parseStatic(BasParserT *p) {
|
|
// STATIC var AS type
|
|
// Only valid inside SUB/FUNCTION. Creates a global variable with a
|
|
// mangled name (procName$varName) that persists across calls.
|
|
advance(p); // consume STATIC
|
|
|
|
if (!p->sym.inLocalScope) {
|
|
error(p, "STATIC is only valid inside SUB or FUNCTION");
|
|
return;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Optional AS type
|
|
uint8_t dt = suffixToType(varName);
|
|
if (match(p, TOK_AS)) {
|
|
dt = resolveTypeName(p);
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
// Create a mangled global name: "procName$varName"
|
|
char mangledName[BAS_MAX_SYMBOL_NAME];
|
|
snprintf(mangledName, sizeof(mangledName), "%s$%s", p->currentProc, varName);
|
|
|
|
// Create the global variable with the mangled name
|
|
bool savedLocal = p->sym.inLocalScope;
|
|
p->sym.inLocalScope = false;
|
|
BasSymbolT *globalSym = basSymTabAdd(&p->sym, mangledName, SYM_VARIABLE, dt);
|
|
p->sym.inLocalScope = savedLocal;
|
|
|
|
if (globalSym == NULL) {
|
|
error(p, "Symbol table full or duplicate STATIC variable");
|
|
return;
|
|
}
|
|
globalSym->scope = SCOPE_GLOBAL;
|
|
globalSym->index = p->sym.nextGlobalIdx++;
|
|
globalSym->isDefined = true;
|
|
|
|
// Create a local alias that maps to this global's index
|
|
BasSymbolT *localSym = basSymTabAdd(&p->sym, varName, SYM_VARIABLE, dt);
|
|
if (localSym == NULL) {
|
|
error(p, "Symbol table full or duplicate variable name");
|
|
return;
|
|
}
|
|
localSym->scope = SCOPE_GLOBAL; // accessed as global
|
|
localSym->index = globalSym->index;
|
|
localSym->isDefined = true;
|
|
}
|
|
|
|
|
|
static void parseStatement(BasParserT *p) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
skipNewlines(p);
|
|
|
|
if (check(p, TOK_EOF)) {
|
|
return;
|
|
}
|
|
|
|
BasTokenTypeE tt = p->lex.token.type;
|
|
|
|
switch (tt) {
|
|
case TOK_PRINT:
|
|
parsePrint(p);
|
|
break;
|
|
|
|
case TOK_DIM:
|
|
parseDim(p);
|
|
break;
|
|
|
|
case TOK_DATA:
|
|
parseData(p);
|
|
break;
|
|
|
|
case TOK_READ:
|
|
parseRead(p);
|
|
break;
|
|
|
|
case TOK_RESTORE:
|
|
parseRestore(p);
|
|
break;
|
|
|
|
case TOK_STATIC:
|
|
parseStatic(p);
|
|
break;
|
|
|
|
case TOK_DEF:
|
|
parseDef(p);
|
|
break;
|
|
|
|
case TOK_DEFINT:
|
|
parseDefType(p, BAS_TYPE_INTEGER);
|
|
break;
|
|
|
|
case TOK_DEFLNG:
|
|
parseDefType(p, BAS_TYPE_LONG);
|
|
break;
|
|
|
|
case TOK_DEFSNG:
|
|
parseDefType(p, BAS_TYPE_SINGLE);
|
|
break;
|
|
|
|
case TOK_DEFDBL:
|
|
parseDefType(p, BAS_TYPE_DOUBLE);
|
|
break;
|
|
|
|
case TOK_DEFSTR:
|
|
parseDefType(p, BAS_TYPE_STRING);
|
|
break;
|
|
|
|
case TOK_DECLARE:
|
|
parseDeclare(p);
|
|
break;
|
|
|
|
case TOK_IF:
|
|
parseIf(p);
|
|
break;
|
|
|
|
case TOK_FOR:
|
|
parseFor(p);
|
|
break;
|
|
|
|
case TOK_DO:
|
|
parseDo(p);
|
|
break;
|
|
|
|
case TOK_WHILE:
|
|
parseWhile(p);
|
|
break;
|
|
|
|
case TOK_SELECT:
|
|
parseSelectCase(p);
|
|
break;
|
|
|
|
case TOK_SUB:
|
|
parseSub(p);
|
|
break;
|
|
|
|
case TOK_FUNCTION:
|
|
parseFunction(p);
|
|
break;
|
|
|
|
case TOK_EXIT:
|
|
parseExit(p);
|
|
break;
|
|
|
|
case TOK_CONST:
|
|
parseConst(p);
|
|
break;
|
|
|
|
case TOK_END:
|
|
parseEnd(p);
|
|
break;
|
|
|
|
case TOK_ERASE:
|
|
parseErase(p);
|
|
break;
|
|
|
|
case TOK_TYPE:
|
|
parseType(p);
|
|
break;
|
|
|
|
case TOK_REDIM:
|
|
parseRedim(p);
|
|
break;
|
|
|
|
case TOK_INPUT:
|
|
parseInput(p);
|
|
break;
|
|
|
|
case TOK_OPEN:
|
|
parseOpen(p);
|
|
break;
|
|
|
|
case TOK_CLOSE:
|
|
parseClose(p);
|
|
break;
|
|
|
|
case TOK_GET:
|
|
parseGet(p);
|
|
break;
|
|
|
|
case TOK_PUT:
|
|
parsePut(p);
|
|
break;
|
|
|
|
case TOK_SEEK:
|
|
parseSeek(p);
|
|
break;
|
|
|
|
case TOK_WRITE:
|
|
parseWrite(p);
|
|
break;
|
|
|
|
case TOK_LINE:
|
|
parseLineInput(p);
|
|
break;
|
|
|
|
case TOK_GOTO:
|
|
parseGoto(p);
|
|
break;
|
|
|
|
case TOK_GOSUB:
|
|
parseGosub(p);
|
|
break;
|
|
|
|
case TOK_ON:
|
|
parseOn(p);
|
|
break;
|
|
|
|
case TOK_OPTION:
|
|
parseOption(p);
|
|
break;
|
|
|
|
case TOK_SHELL:
|
|
parseShell(p);
|
|
break;
|
|
|
|
case TOK_RESUME:
|
|
parseResume(p);
|
|
break;
|
|
|
|
case TOK_RETURN:
|
|
advance(p);
|
|
if (p->sym.inLocalScope) {
|
|
// Inside SUB/FUNCTION: return from subroutine
|
|
basEmit8(&p->cg, OP_RET);
|
|
} else {
|
|
// Module level: GOSUB return (pop PC from eval stack)
|
|
basEmit8(&p->cg, OP_GOSUB_RET);
|
|
}
|
|
break;
|
|
|
|
case TOK_SLEEP:
|
|
parseSleep(p);
|
|
break;
|
|
|
|
case TOK_SWAP:
|
|
parseSwap(p);
|
|
break;
|
|
|
|
case TOK_CALL: {
|
|
advance(p); // consume CALL
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "subroutine name");
|
|
break;
|
|
}
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
|
if (sym == NULL) {
|
|
// Forward reference
|
|
sym = basSymTabAdd(&p->sym, name, SYM_SUB, BAS_TYPE_INTEGER);
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
break;
|
|
}
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = false;
|
|
sym->codeAddr = 0;
|
|
}
|
|
|
|
if (check(p, TOK_LPAREN)) {
|
|
emitFunctionCall(p, sym);
|
|
} else {
|
|
// CALL with no arguments
|
|
uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0;
|
|
basEmit8(&p->cg, OP_CALL);
|
|
int32_t addrPos = basCodePos(&p->cg);
|
|
basEmitU16(&p->cg, (uint16_t)sym->codeAddr);
|
|
basEmit8(&p->cg, 0);
|
|
basEmit8(&p->cg, baseSlot);
|
|
|
|
if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) {
|
|
sym->patchAddrs[sym->patchCount++] = addrPos;
|
|
}
|
|
}
|
|
|
|
if (sym->kind == SYM_FUNCTION) {
|
|
basEmit8(&p->cg, OP_POP); // discard return value
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TOK_RANDOMIZE:
|
|
advance(p);
|
|
if (check(p, TOK_TIMER)) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, -1);
|
|
} else {
|
|
parseExpression(p);
|
|
}
|
|
basEmit8(&p->cg, OP_MATH_RANDOMIZE);
|
|
break;
|
|
|
|
case TOK_DOEVENTS:
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_DO_EVENTS);
|
|
break;
|
|
|
|
case TOK_LET:
|
|
advance(p); // consume LET, then fall through to assignment
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name after LET");
|
|
break;
|
|
}
|
|
parseAssignOrCall(p);
|
|
break;
|
|
|
|
case TOK_IDENT: {
|
|
// Check for label: identifier followed by colon
|
|
BasLexerT savedLex = p->lex;
|
|
char labelName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
labelName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
if (check(p, TOK_COLON)) {
|
|
advance(p); // consume colon
|
|
// Record the label at the current code position
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, labelName);
|
|
if (sym != NULL && sym->kind == SYM_LABEL) {
|
|
// Forward-declared label -- now define it
|
|
sym->codeAddr = basCodePos(&p->cg);
|
|
sym->isDefined = true;
|
|
patchLabelRefs(p, sym);
|
|
} else if (sym == NULL) {
|
|
sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0);
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
break;
|
|
}
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = true;
|
|
sym->codeAddr = basCodePos(&p->cg);
|
|
} else {
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf), "Name '%s' already used", labelName);
|
|
error(p, buf);
|
|
}
|
|
// After the label, there may be a statement on the same line
|
|
// which will be parsed on the next iteration
|
|
break;
|
|
}
|
|
|
|
// Not a label -- restore and parse as assignment/call
|
|
p->lex = savedLex;
|
|
parseAssignOrCall(p);
|
|
break;
|
|
}
|
|
|
|
case TOK_REM:
|
|
// Comment -- skip to end of line
|
|
advance(p);
|
|
break;
|
|
|
|
default: {
|
|
char buf[256];
|
|
snprintf(buf, sizeof(buf), "Unexpected token: %s", basTokenName(tt));
|
|
error(p, buf);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!p->hasError) {
|
|
expectEndOfStatement(p);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseSub(BasParserT *p) {
|
|
// SUB name(params)
|
|
// ...
|
|
// END SUB
|
|
advance(p); // consume SUB
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "subroutine name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Save current proc name for STATIC variable mangling
|
|
strncpy(p->currentProc, name, BAS_MAX_TOKEN_LEN - 1);
|
|
p->currentProc[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
|
|
// Jump over the sub body in module-level code
|
|
int32_t skipJump = emitJump(p, OP_JMP);
|
|
|
|
int32_t subAddr = basCodePos(&p->cg);
|
|
|
|
// Enter local scope
|
|
basSymTabEnterLocal(&p->sym);
|
|
|
|
ExitListT savedExitSub = exitSubList;
|
|
exitListInit(&exitSubList);
|
|
|
|
// Parse parameter list
|
|
int32_t paramCount = 0;
|
|
uint8_t paramTypes[BAS_MAX_PARAMS];
|
|
bool paramByVal[BAS_MAX_PARAMS];
|
|
|
|
if (match(p, TOK_LPAREN)) {
|
|
while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) {
|
|
if (paramCount > 0) {
|
|
expect(p, TOK_COMMA);
|
|
}
|
|
|
|
bool byVal = false;
|
|
if (match(p, TOK_BYVAL)) {
|
|
byVal = true;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "parameter name");
|
|
return;
|
|
}
|
|
|
|
char paramName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
uint8_t pdt = suffixToType(paramName);
|
|
if (match(p, TOK_AS)) {
|
|
pdt = resolveTypeName(p);
|
|
}
|
|
|
|
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
|
|
if (paramSym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
paramSym->scope = SCOPE_LOCAL;
|
|
paramSym->index = basSymTabAllocSlot(&p->sym);
|
|
paramSym->isDefined = true;
|
|
|
|
if (paramCount < BAS_MAX_PARAMS) {
|
|
paramTypes[paramCount] = pdt;
|
|
paramByVal[paramCount] = byVal;
|
|
}
|
|
paramCount++;
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
}
|
|
|
|
// Register the sub in the symbol table (global scope)
|
|
BasSymbolT *existing = basSymTabFindGlobal(&p->sym, name);
|
|
BasSymbolT *subSym = NULL;
|
|
|
|
if (existing != NULL && existing->kind == SYM_SUB) {
|
|
subSym = existing;
|
|
} else {
|
|
bool savedLocal = p->sym.inLocalScope;
|
|
p->sym.inLocalScope = false;
|
|
subSym = basSymTabAdd(&p->sym, name, SYM_SUB, BAS_TYPE_INTEGER);
|
|
p->sym.inLocalScope = savedLocal;
|
|
}
|
|
|
|
if (subSym == NULL) {
|
|
error(p, "Could not register subroutine");
|
|
return;
|
|
}
|
|
|
|
subSym->codeAddr = subAddr;
|
|
subSym->isDefined = true;
|
|
subSym->paramCount = paramCount;
|
|
subSym->scope = SCOPE_GLOBAL;
|
|
for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) {
|
|
subSym->paramTypes[i] = paramTypes[i];
|
|
subSym->paramByVal[i] = paramByVal[i];
|
|
}
|
|
|
|
// Backpatch any forward-reference calls to this sub
|
|
patchCallAddrs(p, subSym);
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
// Parse sub body
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_SUB)) {
|
|
advance(p);
|
|
break;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
// Patch EXIT SUB jumps
|
|
exitListPatch(&exitSubList, p);
|
|
exitSubList = savedExitSub;
|
|
|
|
basEmit8(&p->cg, OP_RET);
|
|
|
|
// Leave local scope
|
|
basSymTabLeaveLocal(&p->sym);
|
|
p->currentProc[0] = '\0';
|
|
|
|
// Patch the skip jump
|
|
patchJump(p, skipJump);
|
|
}
|
|
|
|
|
|
static void parseType(BasParserT *p) {
|
|
// TYPE name
|
|
// field AS type
|
|
// ...
|
|
// END TYPE
|
|
advance(p); // consume TYPE
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "type name");
|
|
return;
|
|
}
|
|
|
|
char typeName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(typeName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
typeName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Add TYPE_DEF symbol
|
|
bool savedLocal = p->sym.inLocalScope;
|
|
p->sym.inLocalScope = false;
|
|
BasSymbolT *typeSym = basSymTabAdd(&p->sym, typeName, SYM_TYPE_DEF, BAS_TYPE_UDT);
|
|
p->sym.inLocalScope = savedLocal;
|
|
|
|
if (typeSym == NULL) {
|
|
error(p, "Symbol table full or duplicate TYPE name");
|
|
return;
|
|
}
|
|
typeSym->scope = SCOPE_GLOBAL;
|
|
typeSym->isDefined = true;
|
|
typeSym->index = p->sym.count - 1;
|
|
typeSym->fieldCount = 0;
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
// Parse fields until END TYPE
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_TYPE)) {
|
|
advance(p);
|
|
break;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "field name or END TYPE");
|
|
return;
|
|
}
|
|
|
|
if (typeSym->fieldCount >= BAS_MAX_UDT_FIELDS) {
|
|
error(p, "Too many fields in TYPE");
|
|
return;
|
|
}
|
|
|
|
BasFieldDefT *field = &typeSym->fields[typeSym->fieldCount];
|
|
strncpy(field->name, p->lex.token.text, BAS_MAX_SYMBOL_NAME - 1);
|
|
field->name[BAS_MAX_SYMBOL_NAME - 1] = '\0';
|
|
advance(p);
|
|
|
|
expect(p, TOK_AS);
|
|
field->dataType = resolveTypeName(p);
|
|
if (field->dataType == BAS_TYPE_UDT) {
|
|
field->udtTypeId = p->lastUdtTypeId;
|
|
}
|
|
|
|
typeSym->fieldCount++;
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseSwap(BasParserT *p) {
|
|
// SWAP a, b -- swap the values of two variables
|
|
advance(p); // consume SWAP
|
|
|
|
// First variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char nameA[BAS_MAX_TOKEN_LEN];
|
|
strncpy(nameA, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
nameA[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *symA = ensureVariable(p, nameA);
|
|
if (symA == NULL) {
|
|
return;
|
|
}
|
|
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Second variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char nameB[BAS_MAX_TOKEN_LEN];
|
|
strncpy(nameB, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
nameB[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *symB = ensureVariable(p, nameB);
|
|
if (symB == NULL) {
|
|
return;
|
|
}
|
|
|
|
// Emit: load a, load b, store a, store b
|
|
emitLoad(p, symA);
|
|
emitLoad(p, symB);
|
|
emitStore(p, symA);
|
|
emitStore(p, symB);
|
|
}
|
|
static void parseGet(BasParserT *p) {
|
|
// GET #channel, [recno], var
|
|
advance(p); // consume GET
|
|
|
|
match(p, TOK_HASH); // optional #
|
|
|
|
// Channel number
|
|
parseExpression(p);
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Optional record number
|
|
if (check(p, TOK_COMMA)) {
|
|
// No record number specified -- push 0 (current position)
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0);
|
|
} else {
|
|
parseExpression(p);
|
|
}
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Target variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *sym = ensureVariable(p, varName);
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
|
|
// Push variable type so VM knows how many bytes to read
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)sym->dataType);
|
|
|
|
basEmit8(&p->cg, OP_FILE_GET);
|
|
|
|
emitStore(p, sym);
|
|
}
|
|
|
|
|
|
static void parsePut(BasParserT *p) {
|
|
// PUT #channel, [recno], var
|
|
advance(p); // consume PUT
|
|
|
|
match(p, TOK_HASH); // optional #
|
|
|
|
// Channel number
|
|
parseExpression(p);
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Optional record number
|
|
if (check(p, TOK_COMMA)) {
|
|
// No record number specified -- push 0 (current position)
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0);
|
|
} else {
|
|
parseExpression(p);
|
|
}
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Value expression
|
|
parseExpression(p);
|
|
|
|
basEmit8(&p->cg, OP_FILE_PUT);
|
|
}
|
|
|
|
|
|
static void parseSeek(BasParserT *p) {
|
|
// SEEK #channel, position
|
|
advance(p); // consume SEEK
|
|
|
|
match(p, TOK_HASH); // optional #
|
|
|
|
// Channel number
|
|
parseExpression(p);
|
|
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Position
|
|
parseExpression(p);
|
|
|
|
basEmit8(&p->cg, OP_FILE_SEEK);
|
|
}
|
|
|
|
|
|
static void parseWrite(BasParserT *p) {
|
|
// WRITE #channel, expr1, expr2, ...
|
|
// Values are comma-delimited. Strings are quoted. Numbers undecorated.
|
|
// Each WRITE statement ends with a newline.
|
|
advance(p); // consume WRITE
|
|
|
|
if (!check(p, TOK_HASH)) {
|
|
error(p, "Expected # after WRITE");
|
|
return;
|
|
}
|
|
advance(p); // consume #
|
|
|
|
// Channel number expression
|
|
parseExpression(p);
|
|
|
|
// Comma separator between channel and first value
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Parse each value
|
|
bool first = true;
|
|
while (!p->hasError) {
|
|
if (!first) {
|
|
// Emit comma separator to file
|
|
basEmit8(&p->cg, OP_DUP); // dup channel for separator
|
|
basEmit8(&p->cg, OP_FILE_WRITE_SEP);
|
|
}
|
|
first = false;
|
|
|
|
// Duplicate channel for the write operation
|
|
basEmit8(&p->cg, OP_DUP);
|
|
|
|
// Parse value expression
|
|
parseExpression(p);
|
|
|
|
// Write value in WRITE format (strings quoted, numbers undecorated)
|
|
basEmit8(&p->cg, OP_FILE_WRITE);
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Write newline to file (channel still on stack)
|
|
basEmit8(&p->cg, OP_FILE_WRITE_NL);
|
|
}
|
|
|
|
|
|
|
|
|
|
static void parseWhile(BasParserT *p) {
|
|
// WHILE cond
|
|
// ...
|
|
// WEND
|
|
advance(p); // consume WHILE
|
|
|
|
ExitListT savedExitDo = exitDoList;
|
|
exitListInit(&exitDoList);
|
|
|
|
int32_t loopTop = basCodePos(&p->cg);
|
|
|
|
parseExpression(p);
|
|
int32_t falseJump = emitJump(p, OP_JMP_FALSE);
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
while (!p->hasError && !check(p, TOK_WEND) && !check(p, TOK_EOF)) {
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
expect(p, TOK_WEND);
|
|
|
|
// Jump back to loop top
|
|
basEmit8(&p->cg, OP_JMP);
|
|
int16_t backOffset = (int16_t)(loopTop - (basCodePos(&p->cg) + 2));
|
|
basEmit16(&p->cg, backOffset);
|
|
|
|
// Patch the false jump to exit
|
|
patchJump(p, falseJump);
|
|
|
|
// Patch EXIT DO jumps (WHILE/WEND uses the DO exit list)
|
|
exitListPatch(&exitDoList, p);
|
|
exitDoList = savedExitDo;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Public API
|
|
// ============================================================
|
|
|
|
void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) {
|
|
memset(p, 0, sizeof(BasParserT));
|
|
basLexerInit(&p->lex, source, sourceLen);
|
|
basCodeGenInit(&p->cg);
|
|
basSymTabInit(&p->sym);
|
|
p->hasError = false;
|
|
p->errorLine = 0;
|
|
p->error[0] = '\0';
|
|
|
|
exitListInit(&exitForList);
|
|
exitListInit(&exitDoList);
|
|
exitListInit(&exitSubList);
|
|
exitListInit(&exitFuncList);
|
|
|
|
// basLexerInit already primes the first token -- no advance needed
|
|
}
|
|
|
|
|
|
bool basParse(BasParserT *p) {
|
|
parseModule(p);
|
|
return !p->hasError;
|
|
}
|
|
|
|
|
|
BasModuleT *basParserBuildModule(BasParserT *p) {
|
|
if (p->hasError) {
|
|
return NULL;
|
|
}
|
|
p->cg.globalCount = p->sym.nextGlobalIdx;
|
|
return basCodeGenBuildModule(&p->cg);
|
|
}
|
|
|
|
|
|
void basParserFree(BasParserT *p) {
|
|
basCodeGenFree(&p->cg);
|
|
}
|