6990 lines
216 KiB
C
6990 lines
216 KiB
C
// The MIT License (MIT)
|
|
//
|
|
// Copyright (C) 2026 Scott Duensing
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to
|
|
// deal in the Software without restriction, including without limitation the
|
|
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
// sell copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
// IN THE SOFTWARE.
|
|
|
|
// parser.c -- DVX BASIC recursive descent parser
|
|
//
|
|
// Single-pass compiler: reads tokens from the lexer and emits
|
|
// p-code directly via the code generator. No AST.
|
|
//
|
|
// Embeddable: no DVX dependencies, pure C.
|
|
|
|
#include "parser.h"
|
|
#include "opcodes.h"
|
|
#include "thirdparty/stb_ds_wrap.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
// ============================================================
|
|
// Built-in function table
|
|
// ============================================================
|
|
|
|
typedef struct {
|
|
const char *name;
|
|
uint8_t opcode;
|
|
int32_t minArgs;
|
|
int32_t maxArgs;
|
|
uint8_t resultType;
|
|
} BuiltinFuncT;
|
|
|
|
static const BuiltinFuncT builtinFuncs[] = {
|
|
// String functions
|
|
{"ASC", OP_STR_ASC, 1, 1, BAS_TYPE_INTEGER},
|
|
{"CHR$", OP_STR_CHR, 1, 1, BAS_TYPE_STRING},
|
|
{"DATE$", OP_DATE_STR, 0, 0, BAS_TYPE_STRING},
|
|
{"ENVIRON$", OP_ENVIRON, 1, 1, BAS_TYPE_STRING},
|
|
{"FORMAT$", OP_FORMAT, 2, 2, BAS_TYPE_STRING},
|
|
{"HEX$", OP_STR_HEX, 1, 1, BAS_TYPE_STRING},
|
|
{"INSTR", OP_STR_INSTR, 2, 3, BAS_TYPE_INTEGER},
|
|
{"LCASE$", OP_STR_LCASE, 1, 1, BAS_TYPE_STRING},
|
|
{"LEFT$", OP_STR_LEFT, 2, 2, BAS_TYPE_STRING},
|
|
{"LEN", OP_STR_LEN, 1, 1, BAS_TYPE_INTEGER},
|
|
{"LTRIM$", OP_STR_LTRIM, 1, 1, BAS_TYPE_STRING},
|
|
{"MID$", OP_STR_MID2, 2, 3, BAS_TYPE_STRING},
|
|
{"OCT$", OP_STR_OCT, 1, 1, BAS_TYPE_STRING},
|
|
{"RIGHT$", OP_STR_RIGHT, 2, 2, BAS_TYPE_STRING},
|
|
{"RTRIM$", OP_STR_RTRIM, 1, 1, BAS_TYPE_STRING},
|
|
{"SPACE$", OP_STR_SPACE, 1, 1, BAS_TYPE_STRING},
|
|
{"STR$", OP_STR_STRF, 1, 1, BAS_TYPE_STRING},
|
|
{"STRING$", OP_STR_STRING, 2, 2, BAS_TYPE_STRING},
|
|
{"TRIM$", OP_STR_TRIM, 1, 1, BAS_TYPE_STRING},
|
|
{"UCASE$", OP_STR_UCASE, 1, 1, BAS_TYPE_STRING},
|
|
{"VAL", OP_STR_VAL, 1, 1, BAS_TYPE_DOUBLE},
|
|
|
|
// File I/O functions
|
|
{"FREEFILE", OP_FILE_FREEFILE, 0, 0, BAS_TYPE_INTEGER},
|
|
{"LOC", OP_FILE_LOC, 1, 1, BAS_TYPE_LONG},
|
|
{"LOF", OP_FILE_LOF, 1, 1, BAS_TYPE_LONG},
|
|
|
|
// Conversion functions
|
|
{"CBOOL", OP_CONV_BOOL, 1, 1, BAS_TYPE_BOOLEAN},
|
|
{"CDBL", OP_CONV_STR_FLT, 1, 1, BAS_TYPE_DOUBLE}, // any value -> DOUBLE
|
|
{"CINT", OP_CONV_FLT_INT, 1, 1, BAS_TYPE_INTEGER},
|
|
{"CLNG", OP_CONV_INT_LONG, 1, 1, BAS_TYPE_LONG},
|
|
{"CSNG", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_SINGLE},
|
|
{"CSTR", OP_CONV_INT_STR, 1, 1, BAS_TYPE_STRING},
|
|
|
|
// Math functions
|
|
{"ABS", OP_MATH_ABS, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"ATN", OP_MATH_ATN, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"COS", OP_MATH_COS, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"EXP", OP_MATH_EXP, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"FIX", OP_MATH_FIX, 1, 1, BAS_TYPE_INTEGER},
|
|
{"GETBLUE", OP_GET_BLUE, 1, 1, BAS_TYPE_INTEGER},
|
|
{"GETGREEN", OP_GET_GREEN, 1, 1, BAS_TYPE_INTEGER},
|
|
{"GETRED", OP_GET_RED, 1, 1, BAS_TYPE_INTEGER},
|
|
{"INT", OP_MATH_INT, 1, 1, BAS_TYPE_INTEGER},
|
|
{"LOG", OP_MATH_LOG, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"RGB", OP_RGB, 3, 3, BAS_TYPE_LONG},
|
|
{"RND", OP_MATH_RND, 0, 1, BAS_TYPE_DOUBLE},
|
|
{"SGN", OP_MATH_SGN, 1, 1, BAS_TYPE_INTEGER},
|
|
{"SIN", OP_MATH_SIN, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"SQR", OP_MATH_SQR, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"TAN", OP_MATH_TAN, 1, 1, BAS_TYPE_DOUBLE},
|
|
{"TIME$", OP_TIME_STR, 0, 0, BAS_TYPE_STRING},
|
|
{"TIMER", OP_MATH_TIMER, 0, 0, BAS_TYPE_DOUBLE},
|
|
|
|
{NULL, 0, 0, 0, 0}
|
|
};
|
|
|
|
// ============================================================
|
|
// Module-scope declarations
|
|
// ============================================================
|
|
|
|
// Relative jump operands are 2 bytes; the VM takes the jump from the
|
|
// PC positioned just past the operand.
|
|
#define BAS_JUMP_OPERAND_SIZE 2
|
|
|
|
// Parsed SUB/FUNCTION parameter list. Every declaration form (SUB,
|
|
// FUNCTION, DEF FN, DECLARE, DECLARE LIBRARY, prescan) goes through
|
|
// parseParamList so the grammar exists exactly once.
|
|
typedef struct {
|
|
int32_t count;
|
|
int32_t required; // index of the last non-OPTIONAL parameter + 1
|
|
uint8_t types[BAS_MAX_PARAMS];
|
|
bool byVal[BAS_MAX_PARAMS];
|
|
bool optional[BAS_MAX_PARAMS];
|
|
} ParamListT;
|
|
|
|
// parseParamList behaviour flags
|
|
#define PARAM_REGISTER (1u << 0) // add each parameter as a SCOPE_LOCAL variable symbol
|
|
#define PARAM_PRESCAN (1u << 1) // accept AS <name> for a TYPE not parsed yet
|
|
#define PARAM_FORCE_BYVAL (1u << 2) // DEF FN: parameters are always by value
|
|
|
|
// State saved around a FOR / DO / WHILE loop body.
|
|
typedef struct {
|
|
ExitListT list;
|
|
int32_t selectBase;
|
|
} LoopSaveT;
|
|
|
|
// State saved around a SUB / FUNCTION body: EXIT lists, loop bookkeeping
|
|
// and SELECT depth all restart at zero inside the procedure so an EXIT
|
|
// or RETURN in the body can never be patched against the caller's
|
|
// constructs.
|
|
typedef struct {
|
|
ExitListT exitFor;
|
|
ExitListT exitDo;
|
|
ExitListT exitSub;
|
|
ExitListT exitFunc;
|
|
int32_t forSelectBase;
|
|
int32_t doSelectBase;
|
|
int32_t selectDepth;
|
|
int32_t forDepth;
|
|
int32_t doDepth;
|
|
int32_t lastLabelSelectDepth;
|
|
int32_t prologueJmp; // operand address of the JMP at the procedure entry
|
|
int32_t bodyAddr; // first byte of the body (prologue jumps back here)
|
|
bool isFunction;
|
|
} ProcSaveT;
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void addPatchAddr(BasSymbolT *sym, int32_t addr);
|
|
static void addPredefConst(BasParserT *p, const char *name, int32_t val);
|
|
static void addPredefConsts(BasParserT *p);
|
|
static void advance(BasParserT *p);
|
|
static void applyParamList(BasSymbolT *sym, const ParamListT *pl);
|
|
bool basParse(BasParserT *p);
|
|
BasModuleT *basParserBuildModule(BasParserT *p);
|
|
void basParserFree(BasParserT *p);
|
|
void basParserInit(BasParserT *p, const char *source, int32_t sourceLen);
|
|
static bool check(BasParserT *p, BasTokenTypeE type);
|
|
static bool checkCtrlArrayAccess(BasParserT *p);
|
|
static void checkIdentLength(BasParserT *p);
|
|
static bool checkKeyword(BasParserT *p, const char *kw);
|
|
static bool clampArgCount(BasParserT *p, int32_t argc);
|
|
static bool clampParamCount(BasParserT *p, int32_t paramCount);
|
|
static void closeSelectCase(BasParserT *p, int32_t **endJumps);
|
|
static void collectDebugGlobals(BasParserT *p);
|
|
static void collectDebugLocals(BasParserT *p, int32_t procIndex);
|
|
static void emitByRefArg(BasParserT *p);
|
|
static void emitCallWithArgs(BasParserT *p, BasSymbolT *sym, int32_t argc);
|
|
static void emitFunctionCall(BasParserT *p, BasSymbolT *sym);
|
|
static void emitInputConv(BasParserT *p, const BasSymbolT *sym);
|
|
static void emitGotoWithSelectPops(BasParserT *p, const char *labelName);
|
|
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 emitMethodCallStatement(BasParserT *p);
|
|
static void emitSelectPops(BasParserT *p, int32_t count);
|
|
static void emitStore(BasParserT *p, BasSymbolT *sym);
|
|
static void emitStoreConv(BasParserT *p, uint8_t dataType);
|
|
static void emitUdtInit(BasParserT *p, int32_t udtTypeId);
|
|
static BasSymbolT *ensureVariable(BasParserT *p, const char *name);
|
|
static void error(BasParserT *p, const char *msg);
|
|
static void errorExpected(BasParserT *p, const char *what);
|
|
static void exitListAdd(ExitListT *el, int32_t addr);
|
|
static void exitListInit(ExitListT *el);
|
|
static void exitListPatch(ExitListT *el, BasParserT *p);
|
|
static void expect(BasParserT *p, BasTokenTypeE type);
|
|
static void expectEndOfStatement(BasParserT *p);
|
|
static bool exprEnter(BasParserT *p);
|
|
static void exprLeave(BasParserT *p);
|
|
static const BuiltinFuncT *findBuiltin(const char *name);
|
|
static BasSymbolT *findTypeDef(BasParserT *p, const char *name);
|
|
static BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId);
|
|
static void loopBegin(BasParserT *p, bool isFor, LoopSaveT *save);
|
|
static void loopEnd(BasParserT *p, bool isFor, LoopSaveT *save);
|
|
static bool match(BasParserT *p, BasTokenTypeE type);
|
|
static bool nameHasTypeSuffix(const char *name);
|
|
static void parseAddExpr(BasParserT *p);
|
|
static void parseAndExpr(BasParserT *p);
|
|
static void parseAssignOrCall(BasParserT *p);
|
|
static void parseBeginForm(BasParserT *p);
|
|
static int32_t parseCallArgs(BasParserT *p, BasSymbolT *sym, bool parens);
|
|
static void parseChDir(BasParserT *p);
|
|
static void parseChDrive(BasParserT *p);
|
|
static void parseClose(BasParserT *p);
|
|
static void parseCompareExpr(BasParserT *p);
|
|
static void parseConcatExpr(BasParserT *p);
|
|
static void parseConst(BasParserT *p);
|
|
static void parseData(BasParserT *p);
|
|
static void parseDeclare(BasParserT *p);
|
|
static void parseDeclareLibrary(BasParserT *p);
|
|
static void parseDef(BasParserT *p);
|
|
static void parseDefType(BasParserT *p, uint8_t dataType);
|
|
static void parseDim(BasParserT *p);
|
|
static void parseDimBounds(BasParserT *p, int32_t *outDims);
|
|
static void parseDo(BasParserT *p);
|
|
static void parseEnd(BasParserT *p);
|
|
static void parseEndForm(BasParserT *p);
|
|
static void parseEqvExpr(BasParserT *p);
|
|
static void parseErase(BasParserT *p);
|
|
static void parseExit(BasParserT *p);
|
|
static void parseExpression(BasParserT *p);
|
|
static void parseFileCopy(BasParserT *p);
|
|
static void parseFor(BasParserT *p);
|
|
static void parseFunction(BasParserT *p);
|
|
static void parseGet(BasParserT *p);
|
|
static void parseGosub(BasParserT *p);
|
|
static void parseGoto(BasParserT *p);
|
|
static void parseIdivExpr(BasParserT *p);
|
|
static void parseIf(BasParserT *p);
|
|
static void parseImpExpr(BasParserT *p);
|
|
static void parseInput(BasParserT *p);
|
|
static void parseKill(BasParserT *p);
|
|
static void parseLineInput(BasParserT *p);
|
|
static void parseMkDir(BasParserT *p);
|
|
static void parseModule(BasParserT *p);
|
|
static void parseMulDivExpr(BasParserT *p);
|
|
static void parseMulExpr(BasParserT *p);
|
|
static void parseName(BasParserT *p);
|
|
static void parseNotExpr(BasParserT *p);
|
|
static void parseOn(BasParserT *p);
|
|
static void parseOnError(BasParserT *p);
|
|
static void parseOpen(BasParserT *p);
|
|
static void parseOption(BasParserT *p);
|
|
static void parseOrExpr(BasParserT *p);
|
|
static bool parseParamList(BasParserT *p, ParamListT *pl, uint32_t flags);
|
|
static void parsePowExpr(BasParserT *p);
|
|
static void parsePowOperand(BasParserT *p);
|
|
static void parsePrimary(BasParserT *p);
|
|
static void parsePrint(BasParserT *p);
|
|
static void parsePut(BasParserT *p);
|
|
static void parseRead(BasParserT *p);
|
|
static void parseRedim(BasParserT *p);
|
|
static void parseRemoveControl(BasParserT *p);
|
|
static void parseRestore(BasParserT *p);
|
|
static void parseResume(BasParserT *p);
|
|
static void parseRmDir(BasParserT *p);
|
|
static void parseSeek(BasParserT *p);
|
|
static void parseSelectCase(BasParserT *p);
|
|
static void parseSetAttr(BasParserT *p);
|
|
static void parseSetEvent(BasParserT *p);
|
|
static void parseShell(BasParserT *p);
|
|
static uint8_t parseShowModalFlag(BasParserT *p);
|
|
static void parseSleep(BasParserT *p);
|
|
static void parseStatement(BasParserT *p);
|
|
static void parseStatic(BasParserT *p);
|
|
static void parseSub(BasParserT *p);
|
|
static void parseSwap(BasParserT *p);
|
|
static void parseType(BasParserT *p);
|
|
static void parseUnaryExpr(BasParserT *p);
|
|
static void parseWhile(BasParserT *p);
|
|
static void parseWrite(BasParserT *p);
|
|
static void parseXorExpr(BasParserT *p);
|
|
static void patchCallAddrs(BasParserT *p, BasSymbolT *sym);
|
|
static void patchJump(BasParserT *p, int32_t addr);
|
|
static void patchLabelRefs(BasParserT *p, BasSymbolT *sym);
|
|
static void prescanSignatures(BasParserT *p);
|
|
static void procBegin(BasParserT *p, ProcSaveT *save, bool isFunction);
|
|
static void procEnd(BasParserT *p, ProcSaveT *save);
|
|
static int16_t relJumpOffset(BasParserT *p, int32_t target, int32_t operandAddr);
|
|
static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName);
|
|
static uint8_t resolveParamType(BasParserT *p, bool prescan, int32_t *outUdtTypeId);
|
|
static uint8_t resolveTypeName(BasParserT *p);
|
|
static void shiftBackpatchAddrs(BasParserT *p, int32_t from, int32_t delta);
|
|
static void skipNewlines(BasParserT *p);
|
|
static uint8_t suffixToType(const char *name);
|
|
|
|
// Record a code position that must be patched once sym (a forward-
|
|
// referenced SUB/FUNCTION/label) is defined.
|
|
static void addPatchAddr(BasSymbolT *sym, int32_t addr) {
|
|
arrput(sym->patchAddrs, addr);
|
|
sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
|
|
}
|
|
|
|
|
|
static void addPredefConst(BasParserT *p, const char *name, int32_t val) {
|
|
BasSymbolT *sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_LONG);
|
|
if (sym) {
|
|
sym->constInt = val;
|
|
sym->isDefined = true;
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
}
|
|
|
|
|
|
static void addPredefConsts(BasParserT *p) {
|
|
// MsgBox button flags (VB3 compatible)
|
|
addPredefConst(p, "vbOKOnly", 0x0000);
|
|
addPredefConst(p, "vbOKCancel", 0x0001);
|
|
addPredefConst(p, "vbYesNo", 0x0002);
|
|
addPredefConst(p, "vbYesNoCancel", 0x0003);
|
|
addPredefConst(p, "vbRetryCancel", 0x0004);
|
|
|
|
// MsgBox icon flags
|
|
addPredefConst(p, "vbInformation", 0x0010);
|
|
addPredefConst(p, "vbExclamation", 0x0020);
|
|
addPredefConst(p, "vbCritical", 0x0030);
|
|
addPredefConst(p, "vbQuestion", 0x0040);
|
|
|
|
// MsgBox return values
|
|
addPredefConst(p, "vbOK", 1);
|
|
addPredefConst(p, "vbCancel", 2);
|
|
addPredefConst(p, "vbYes", 3);
|
|
addPredefConst(p, "vbNo", 4);
|
|
addPredefConst(p, "vbRetry", 5);
|
|
|
|
// Show mode flags
|
|
addPredefConst(p, "vbModal", 1);
|
|
|
|
// File attribute constants
|
|
addPredefConst(p, "vbNormal", 0);
|
|
addPredefConst(p, "vbReadOnly", 1);
|
|
addPredefConst(p, "vbHidden", 2);
|
|
addPredefConst(p, "vbSystem", 4);
|
|
addPredefConst(p, "vbDirectory", 16);
|
|
addPredefConst(p, "vbArchive", 32);
|
|
}
|
|
|
|
|
|
static void advance(BasParserT *p) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
p->prevLine = p->lex.token.line;
|
|
basLexerNext(&p->lex);
|
|
if (p->lex.token.type == TOK_ERROR) {
|
|
error(p, p->lex.error);
|
|
return;
|
|
}
|
|
checkIdentLength(p);
|
|
}
|
|
|
|
|
|
// Copy a parsed parameter list onto a SUB/FUNCTION symbol and mark the
|
|
// signature as known (call-site arity checks trust hasSignature).
|
|
static void applyParamList(BasSymbolT *sym, const ParamListT *pl) {
|
|
sym->paramCount = pl->count;
|
|
sym->requiredParams = pl->required;
|
|
sym->hasSignature = true;
|
|
|
|
for (int32_t i = 0; i < pl->count; i++) {
|
|
sym->paramTypes[i] = pl->types[i];
|
|
sym->paramByVal[i] = pl->byVal[i];
|
|
sym->paramOptional[i] = pl->optional[i];
|
|
}
|
|
}
|
|
|
|
|
|
bool basParse(BasParserT *p) {
|
|
parseModule(p);
|
|
|
|
// The code generator silently caps the constant pool at 16-bit
|
|
// indices; if it overflowed, surface it as a compile error so we
|
|
// never emit a module with truncated constant references.
|
|
if (p->cg.overflow) {
|
|
error(p, "Too many string constants (constant pool index exceeds 16 bits)");
|
|
}
|
|
|
|
// OP_CALL operands are absolute 16-bit unsigned addresses, so code
|
|
// beyond the uint16 range is unreachable by calls; reject cleanly
|
|
// instead of silently miscompiling. Relative jump spans are signed
|
|
// 16-bit and are range-checked individually in relJumpOffset, so
|
|
// modules between 32K and 64K stay compilable.
|
|
if (p->cg.codeLen > UINT16_MAX) {
|
|
error(p, "Module too large (code size exceeds 16-bit address range)");
|
|
}
|
|
|
|
// The VM's global slot table is fixed-size; a module needing more
|
|
// would be refused at load time with no source position.
|
|
if (p->sym.nextGlobalIdx > BAS_VM_MAX_GLOBALS) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Too many module-level variables (%d, limit is %d)", (int)p->sym.nextGlobalIdx, (int)BAS_VM_MAX_GLOBALS);
|
|
error(p, buf);
|
|
}
|
|
|
|
return !p->hasError;
|
|
}
|
|
|
|
|
|
BasModuleT *basParserBuildModule(BasParserT *p) {
|
|
if (p->hasError) {
|
|
return NULL;
|
|
}
|
|
|
|
// Collect global and form-scope variables for the debugger
|
|
collectDebugGlobals(p);
|
|
|
|
p->cg.globalCount = p->sym.nextGlobalIdx;
|
|
return basCodeGenBuildModuleWithProcs(&p->cg, &p->sym);
|
|
}
|
|
|
|
|
|
void basParserFree(BasParserT *p) {
|
|
basCodeGenFree(&p->cg);
|
|
|
|
// Free all symbols and the symbol array.
|
|
basSymTabFree(&p->sym);
|
|
|
|
// EXIT-target backpatch lists are normally arrfree'd by exitListPatch at
|
|
// the close of each loop/proc; an error early-return can skip that, so
|
|
// release the top-level lists here too.
|
|
arrfree(p->exitForList.patchAddr);
|
|
arrfree(p->exitDoList.patchAddr);
|
|
arrfree(p->exitSubList.patchAddr);
|
|
arrfree(p->exitFuncList.patchAddr);
|
|
p->exitForList.patchAddr = NULL;
|
|
p->exitDoList.patchAddr = NULL;
|
|
p->exitSubList.patchAddr = NULL;
|
|
p->exitFuncList.patchAddr = NULL;
|
|
}
|
|
|
|
|
|
void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) {
|
|
memset(p, 0, sizeof(BasParserT));
|
|
memset(p->defType, BAS_DEFTYPE_NONE, sizeof(p->defType));
|
|
basLexerInit(&p->lex, source, sourceLen);
|
|
basCodeGenInit(&p->cg);
|
|
basSymTabInit(&p->sym);
|
|
|
|
p->formInitJmpAddr = -1;
|
|
p->formInitCodeStart = -1;
|
|
|
|
addPredefConsts(p);
|
|
|
|
// basLexerInit already primes the first token -- no advance needed,
|
|
// but the primed token bypassed advance(), so vet it here.
|
|
if (p->lex.token.type == TOK_ERROR) {
|
|
error(p, p->lex.error);
|
|
} else {
|
|
checkIdentLength(p);
|
|
}
|
|
}
|
|
|
|
|
|
void basParserSetValidator(BasParserT *p, const BasCtrlValidatorT *v) {
|
|
p->validator = v;
|
|
}
|
|
|
|
|
|
static bool check(BasParserT *p, BasTokenTypeE type) {
|
|
return p->lex.token.type == type;
|
|
}
|
|
|
|
|
|
// Check if current token '(' is followed by a matching ')' then '.'.
|
|
// This disambiguates control array access Name(idx).Property from
|
|
// function calls Name(args). Saves and restores lexer state.
|
|
// Must be called when current token is TOK_LPAREN.
|
|
static bool checkCtrlArrayAccess(BasParserT *p) {
|
|
BasLexerT savedLex = p->lex;
|
|
bool savedErr = p->hasError;
|
|
|
|
basLexerNext(&p->lex); // consume (
|
|
|
|
int32_t depth = 1;
|
|
|
|
while (depth > 0 && p->lex.token.type != TOK_EOF && !p->hasError) {
|
|
if (p->lex.token.type == TOK_LPAREN) {
|
|
depth++;
|
|
} else if (p->lex.token.type == TOK_RPAREN) {
|
|
depth--;
|
|
if (depth == 0) {
|
|
break;
|
|
}
|
|
}
|
|
basLexerNext(&p->lex);
|
|
}
|
|
|
|
// Advance past the closing )
|
|
if (p->lex.token.type == TOK_RPAREN) {
|
|
basLexerNext(&p->lex);
|
|
}
|
|
|
|
bool dotFollows = (p->lex.token.type == TOK_DOT);
|
|
|
|
// Restore lexer state
|
|
p->lex = savedLex;
|
|
p->hasError = savedErr;
|
|
|
|
return dotFollows;
|
|
}
|
|
|
|
|
|
// Identifiers must fit BasSymbolT.name. The lexer accepts names up to
|
|
// BAS_MAX_TOKEN_LEN, so reject longer ones here (the one place every
|
|
// token passes through) rather than truncating them into a name that no
|
|
// later reference would match.
|
|
static void checkIdentLength(BasParserT *p) {
|
|
if (p->lex.token.type == TOK_IDENT && p->lex.token.textLen >= BAS_MAX_IDENT) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Identifier too long (maximum is %d characters)", (int)(BAS_MAX_IDENT - 1));
|
|
error(p, buf);
|
|
}
|
|
}
|
|
|
|
|
|
static bool checkKeyword(BasParserT *p, const char *kw) {
|
|
return p->lex.token.type == TOK_IDENT && strcasecmp(p->lex.token.text, kw) == 0;
|
|
}
|
|
|
|
|
|
// Guard against emitting a call with more than BAS_VM_MAX_CALL_ARGS
|
|
// arguments. The VM encodes the argument count in a single byte, so a
|
|
// call may not exceed that bound. Returns true (and reports an error)
|
|
// when argc exceeds the limit, signalling the caller to abort the
|
|
// current call emission.
|
|
static bool clampArgCount(BasParserT *p, int32_t argc) {
|
|
if (argc > BAS_VM_MAX_CALL_ARGS) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Too many arguments (max %d)", (int)BAS_VM_MAX_CALL_ARGS);
|
|
error(p, buf);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Guard against declaring more than BAS_MAX_PARAMS parameters. The
|
|
// typed param arrays on a symbol are fixed at BAS_MAX_PARAMS, so a
|
|
// declaration may not exceed that bound. Returns true (and reports an
|
|
// error) when paramCount has reached the limit, signalling the caller
|
|
// to stop the parameter loop.
|
|
static bool clampParamCount(BasParserT *p, int32_t paramCount) {
|
|
if (paramCount >= BAS_MAX_PARAMS) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Too many parameters (maximum is %d)", (int)BAS_MAX_PARAMS);
|
|
error(p, buf);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Emit the shared END SELECT epilogue: land every pending end-of-case jump on
|
|
// the OP_POP that discards the SELECT test value, free the jump list, and drop
|
|
// the select-depth marker. Every exit from parseSelectCase runs this so the
|
|
// test value is popped exactly once and selectDepth stays balanced.
|
|
static void closeSelectCase(BasParserT *p, int32_t **endJumps) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(*endJumps); i++) {
|
|
patchJump(p, (*endJumps)[i]);
|
|
}
|
|
basEmit8(&p->cg, OP_POP);
|
|
arrfree(*endJumps);
|
|
p->selectDepth--;
|
|
}
|
|
|
|
|
|
// Snapshot global variables for the debugger at the end of compilation.
|
|
static void collectDebugGlobals(BasParserT *p) {
|
|
for (int32_t i = 0; i < p->sym.count; i++) {
|
|
BasSymbolT *s = p->sym.symbols[i];
|
|
|
|
if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE) {
|
|
basCodeGenAddDebugVar(&p->cg, s->name, SCOPE_GLOBAL, s->dataType, s->index, -1, NULL);
|
|
} else if (s->scope == SCOPE_FORM && s->kind == SYM_VARIABLE) {
|
|
basCodeGenAddDebugVar(&p->cg, s->name, SCOPE_FORM, s->dataType, s->index, -1, s->formName);
|
|
} else if (s->kind == SYM_TYPE_DEF && s->fields) {
|
|
// Collect UDT type definitions for watch window field access
|
|
BasDebugUdtDefT def;
|
|
memset(&def, 0, sizeof(def));
|
|
snprintf(def.name, BAS_MAX_IDENT, "%s", s->name);
|
|
def.typeId = s->index;
|
|
def.fieldCount = (int32_t)arrlen(s->fields);
|
|
def.fields = (BasDebugFieldT *)malloc(def.fieldCount * sizeof(BasDebugFieldT));
|
|
|
|
if (def.fields) {
|
|
for (int32_t f = 0; f < def.fieldCount; f++) {
|
|
snprintf(def.fields[f].name, BAS_MAX_IDENT, "%s", s->fields[f].name);
|
|
def.fields[f].dataType = s->fields[f].dataType;
|
|
}
|
|
} else {
|
|
// Allocation failed: record the type name but no fields so
|
|
// the debugger never walks a NULL field array.
|
|
def.fieldCount = 0;
|
|
}
|
|
|
|
arrput(p->cg.debugUdtDefs, def);
|
|
p->cg.debugUdtDefCount = (int32_t)arrlen(p->cg.debugUdtDefs);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Snapshot local variables for the debugger before leaving local scope.
|
|
// procIndex is the index into the proc table for the current procedure.
|
|
// Also saves the local count on the proc symbol for BasProcEntryT.
|
|
static void collectDebugLocals(BasParserT *p, int32_t procIndex) {
|
|
// Save localCount on the proc symbol
|
|
if (p->currentProc[0]) {
|
|
BasSymbolT *procSym = basSymTabFind(&p->sym, p->currentProc);
|
|
|
|
if (procSym) {
|
|
procSym->localCount = p->sym.nextLocalIdx;
|
|
}
|
|
}
|
|
|
|
for (int32_t i = 0; i < p->sym.count; i++) {
|
|
BasSymbolT *s = p->sym.symbols[i];
|
|
|
|
if (s->scope == SCOPE_LOCAL && s->kind == SYM_VARIABLE) {
|
|
basCodeGenAddDebugVar(&p->cg, s->name, SCOPE_LOCAL, s->dataType, s->index, procIndex, NULL);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Try to emit a ByRef argument (push address of variable).
|
|
// If the current token is a simple variable name not followed by
|
|
// '(' or '.', we emit PUSH_LOCAL_ADDR/PUSH_GLOBAL_ADDR.
|
|
// Otherwise, we fall back to parseExpression (effectively ByVal).
|
|
static void emitByRefArg(BasParserT *p) {
|
|
if (!check(p, TOK_IDENT)) {
|
|
parseExpression(p);
|
|
return;
|
|
}
|
|
|
|
// Save the identifier name before peeking ahead
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, sizeof(name) - 1);
|
|
name[sizeof(name) - 1] = '\0';
|
|
|
|
// Look up the symbol -- must be a variable (simple or array)
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
|
|
|
if (!sym || sym->kind != SYM_VARIABLE) {
|
|
parseExpression(p);
|
|
return;
|
|
}
|
|
|
|
// Save lexer state to peek at what follows the identifier
|
|
int32_t savedPos = p->lex.pos;
|
|
int32_t savedLine = p->lex.line;
|
|
int32_t savedCol = p->lex.col;
|
|
BasTokenT savedTok = p->lex.token;
|
|
|
|
advance(p); // consume the identifier
|
|
|
|
// Array element as BYREF: `arr(i)` or `arr(i, j)`. Emit LOAD of
|
|
// the array ref, then the indices, then OP_PUSH_ARR_ADDR which
|
|
// produces a BAS_TYPE_REF pointing at the element. Writes through
|
|
// that ref in the callee update the actual array element.
|
|
if (sym->isArray && check(p, TOK_LPAREN)) {
|
|
advance(p); // consume (
|
|
|
|
// Push the array reference
|
|
emitLoad(p, sym);
|
|
|
|
// Parse indices
|
|
int32_t dims = 0;
|
|
parseExpression(p);
|
|
dims++;
|
|
|
|
while (match(p, TOK_COMMA)) {
|
|
parseExpression(p);
|
|
dims++;
|
|
}
|
|
|
|
expect(p, TOK_RPAREN);
|
|
|
|
// If the next token isn't an argument delimiter, this wasn't
|
|
// a simple `arr(i)` -- rewind and fall back. (rare; e.g.
|
|
// `arr(i).field` not supported as BYREF.)
|
|
if (!check(p, TOK_COMMA) && !check(p, TOK_RPAREN) && !check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) {
|
|
// Too late to rewind cleanly -- we've already emitted code.
|
|
// Treat this as an error. Callers can restructure as a
|
|
// simple variable BYREF or BYVAL.
|
|
error(p, "Complex BYREF array expression not supported; use a temporary variable");
|
|
return;
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_PUSH_ARR_ADDR);
|
|
basEmit8(&p->cg, (uint8_t)dims);
|
|
return;
|
|
}
|
|
|
|
// The token after the identifier must be an argument delimiter
|
|
// (comma, rparen, newline, colon, EOF, ELSE) for this to be a
|
|
// bare variable reference. Anything else (operator, dot, paren)
|
|
// means it's part of an expression -- fall back to parseExpression.
|
|
bool isDelim = check(p, TOK_COMMA) || check(p, TOK_RPAREN) || check(p, TOK_NEWLINE) || check(p, TOK_COLON) || check(p, TOK_EOF) || check(p, TOK_ELSE);
|
|
|
|
if (!isDelim) {
|
|
// Restore and let parseExpression handle the full expression
|
|
p->lex.pos = savedPos;
|
|
p->lex.line = savedLine;
|
|
p->lex.col = savedCol;
|
|
p->lex.token = savedTok;
|
|
parseExpression(p);
|
|
return;
|
|
}
|
|
|
|
// It's a bare variable reference -- push its address
|
|
if (sym->scope == SCOPE_LOCAL) {
|
|
basEmit8(&p->cg, OP_PUSH_LOCAL_ADDR);
|
|
} else if (sym->scope == SCOPE_FORM) {
|
|
basEmit8(&p->cg, OP_PUSH_FORM_ADDR);
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_GLOBAL_ADDR);
|
|
}
|
|
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
}
|
|
|
|
|
|
// Emit the call of a SUB/FUNCTION whose argc arguments are already on
|
|
// the stack: validate the count against the signature, pad omitted
|
|
// OPTIONAL parameters with zero-values, then emit OP_CALL_EXTERN or
|
|
// OP_CALL (recording a forward reference when the body is not yet
|
|
// defined). Shared by every call form: name(args), bare "name args",
|
|
// CALL name(args) and CALL name.
|
|
static void emitCallWithArgs(BasParserT *p, BasSymbolT *sym, int32_t argc) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
// Minimum acceptable count: everything up to the last required
|
|
// parameter when OPTIONALs are declared, otherwise the full list.
|
|
int32_t minArgs = sym->paramCount;
|
|
bool hasOptional = false;
|
|
|
|
for (int32_t i = 0; i < sym->paramCount; i++) {
|
|
if (sym->paramOptional[i]) {
|
|
hasOptional = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasOptional) {
|
|
minArgs = sym->requiredParams;
|
|
}
|
|
|
|
if (argc < minArgs || argc > sym->paramCount) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
const char *what = (sym->kind == SYM_FUNCTION) ? "Function" : "Sub";
|
|
|
|
if (minArgs == sym->paramCount) {
|
|
snprintf(buf, sizeof(buf), "%s '%s' expects %d arguments, got %d", what, sym->name, (int)sym->paramCount, (int)argc);
|
|
} else {
|
|
snprintf(buf, sizeof(buf), "%s '%s' expects %d to %d arguments, got %d", what, sym->name, (int)minArgs, (int)sym->paramCount, (int)argc);
|
|
}
|
|
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
|
|
// Push default zero-values for omitted optional parameters
|
|
for (int32_t i = argc; i < sym->paramCount; i++) {
|
|
if (sym->paramTypes[i] == BAS_TYPE_STRING) {
|
|
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, emptyIdx);
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0);
|
|
}
|
|
}
|
|
|
|
argc = sym->paramCount;
|
|
|
|
if (clampArgCount(p, argc)) {
|
|
return;
|
|
}
|
|
|
|
// External library function: emit OP_CALL_EXTERN
|
|
if (sym->isExtern) {
|
|
basEmit8(&p->cg, OP_CALL_EXTERN);
|
|
basEmitU16(&p->cg, sym->externLibIdx);
|
|
basEmitU16(&p->cg, sym->externFuncIdx);
|
|
basEmit8(&p->cg, (uint8_t)argc);
|
|
basEmit8(&p->cg, sym->dataType);
|
|
return;
|
|
}
|
|
|
|
// Internal BASIC procedure: emit OP_CALL
|
|
// baseSlot: functions reserve slot 0 for the return value
|
|
uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0;
|
|
|
|
basEmit8(&p->cg, OP_CALL);
|
|
int32_t addrPos = basCodePos(&p->cg);
|
|
basEmitU16(&p->cg, (uint16_t)sym->codeAddr);
|
|
basEmit8(&p->cg, (uint8_t)argc);
|
|
basEmit8(&p->cg, baseSlot);
|
|
|
|
// If not yet defined, record the address for backpatching
|
|
if (!sym->isDefined) {
|
|
addPatchAddr(sym, addrPos);
|
|
}
|
|
}
|
|
|
|
|
|
// name(arg, ...) in either statement or expression context.
|
|
static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
|
|
int32_t argc = parseCallArgs(p, sym, true);
|
|
emitCallWithArgs(p, sym, argc);
|
|
}
|
|
|
|
|
|
// Emit a jump to a GOTO target, discarding the live test values of any
|
|
// SELECT CASE blocks the jump leaves. Shared by GOTO and the ON expr
|
|
// GOTO arms so both discard the same way.
|
|
static void emitGotoWithSelectPops(BasParserT *p, const char *labelName) {
|
|
// A GOTO out of one or more SELECT CASE blocks must discard their
|
|
// live test values -- but only for the blocks actually being left.
|
|
// A label may sit inside the same SELECT, so the number of pops is
|
|
// the GOTO site's depth minus the label's depth.
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, labelName);
|
|
|
|
if (sym != NULL && sym->kind == SYM_LABEL && sym->isDefined) {
|
|
// Backward GOTO: the label's SELECT depth was recorded in
|
|
// sym->localCount when it was defined.
|
|
int32_t depthDiff = p->selectDepth - sym->localCount;
|
|
|
|
if (depthDiff > 0) {
|
|
emitSelectPops(p, depthDiff);
|
|
}
|
|
|
|
emitJumpToLabel(p, OP_JMP, labelName);
|
|
return;
|
|
}
|
|
|
|
// Forward GOTO: the label's depth is unknown until it is defined.
|
|
// Reserve one OP_NOP per currently-open SELECT ahead of the jump and
|
|
// encode this site's depth in the jump's placeholder operand;
|
|
// patchLabelRefs converts the needed number of NOPs to OP_POP once
|
|
// the label's depth is known. Unconverted NOPs execute harmlessly.
|
|
for (int32_t i = 0; i < p->selectDepth; i++) {
|
|
basEmit8(&p->cg, OP_NOP);
|
|
}
|
|
|
|
emitJumpToLabel(p, OP_JMP, labelName);
|
|
|
|
if (p->selectDepth > 0 && !p->hasError) {
|
|
sym = basSymTabFind(&p->sym, labelName);
|
|
|
|
if (sym != NULL && sym->patchCount > 0) {
|
|
basPatch16(&p->cg, sym->patchAddrs[sym->patchCount - 1], (int16_t)p->selectDepth);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// INPUT / INPUT # read a string; convert it for a numeric target. The
|
|
// INTEGER conversion range-checks to 16 bits itself; every other numeric
|
|
// type goes through the double conversion and is narrowed by emitStore.
|
|
static void emitInputConv(BasParserT *p, const BasSymbolT *sym) {
|
|
if (sym->dataType == BAS_TYPE_STRING) {
|
|
return;
|
|
}
|
|
if (sym->dataType == BAS_TYPE_INTEGER) {
|
|
basEmit8(&p->cg, OP_CONV_STR_INT);
|
|
} else {
|
|
basEmit8(&p->cg, OP_CONV_STR_FLT);
|
|
}
|
|
}
|
|
|
|
|
|
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 = relJumpOffset(p, sym->codeAddr, here);
|
|
basEmit16(&p->cg, offset);
|
|
return;
|
|
}
|
|
|
|
// Forward reference -- create label symbol if needed
|
|
if (sym == NULL) {
|
|
sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0);
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = false;
|
|
sym->codeAddr = 0;
|
|
}
|
|
|
|
// Emit jump with placeholder offset
|
|
basEmit8(&p->cg, opcode);
|
|
int32_t patchAddr = basCodePos(&p->cg);
|
|
basEmit16(&p->cg, 0);
|
|
|
|
// Record patch address for backpatching when label is defined
|
|
arrput(sym->patchAddrs, patchAddr);
|
|
sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
|
|
}
|
|
|
|
|
|
static void emitLoad(BasParserT *p, BasSymbolT *sym) {
|
|
// Only variables and constants have a value to load; a SUB, label or
|
|
// TYPE name used as an operand would otherwise read slot `index` of
|
|
// the globals as if it were a variable.
|
|
if (sym->kind != SYM_VARIABLE && sym->kind != SYM_CONST) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "'%s' is not a variable", sym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
|
|
if (sym->kind == SYM_CONST) {
|
|
// Emit the constant value directly
|
|
if (sym->dataType == BAS_TYPE_STRING) {
|
|
uint16_t idx = basAddConstant(&p->cg, sym->constStr, (int32_t)strlen(sym->constStr));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, idx);
|
|
} else if (sym->dataType == BAS_TYPE_INTEGER || sym->dataType == BAS_TYPE_LONG) {
|
|
basEmit8(&p->cg, OP_PUSH_INT32);
|
|
basEmit16(&p->cg, (int16_t)(sym->constInt & 0xFFFF));
|
|
basEmit16(&p->cg, (int16_t)((sym->constInt >> 16) & 0xFFFF));
|
|
} else if (sym->dataType == BAS_TYPE_BOOLEAN) {
|
|
basEmit8(&p->cg, sym->constInt ? OP_PUSH_TRUE : OP_PUSH_FALSE);
|
|
} else {
|
|
// Float constant
|
|
basEmit8(&p->cg, OP_PUSH_FLT64);
|
|
basEmitDouble(&p->cg, sym->constDbl);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (sym->scope == SCOPE_LOCAL) {
|
|
basEmit8(&p->cg, OP_LOAD_LOCAL);
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
} else if (sym->scope == SCOPE_FORM) {
|
|
basEmit8(&p->cg, OP_LOAD_FORM_VAR);
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
} else {
|
|
basEmit8(&p->cg, OP_LOAD_GLOBAL);
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
}
|
|
}
|
|
|
|
|
|
// Statement-form method call "obj.Method arg, arg": the object and
|
|
// method name are already on the stack. Arguments are comma-separated
|
|
// (VB style, no parentheses); the return value is discarded.
|
|
static void emitMethodCallStatement(BasParserT *p) {
|
|
int32_t argc = 0;
|
|
|
|
while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE) && !p->hasError) {
|
|
if (argc > 0) {
|
|
expect(p, TOK_COMMA);
|
|
}
|
|
parseExpression(p);
|
|
argc++;
|
|
}
|
|
|
|
if (clampArgCount(p, argc)) {
|
|
return;
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_CALL_METHOD);
|
|
basEmit8(&p->cg, (uint8_t)argc);
|
|
basEmit8(&p->cg, OP_POP); // discard return value (statement form)
|
|
}
|
|
|
|
|
|
// Emit OP_POP `count` times. Used to discard the live SELECT CASE test
|
|
// values from the eval stack when EXIT/GOTO jumps out of one or more
|
|
// enclosing SELECT constructs (see selectDepth).
|
|
static void emitSelectPops(BasParserT *p, int32_t count) {
|
|
for (int32_t i = 0; i < count; i++) {
|
|
basEmit8(&p->cg, OP_POP);
|
|
}
|
|
}
|
|
|
|
|
|
static void emitStore(BasParserT *p, BasSymbolT *sym) {
|
|
// Storing into a CONST would emit OP_STORE_GLOBAL at its index 0
|
|
// (CONSTs never allocate a slot), corrupting the first real global.
|
|
// Reject here so every store path (FOR/READ/INPUT/SWAP/MID$/GET/SET)
|
|
// is covered, not just the plain-assignment path.
|
|
if (sym->kind == SYM_CONST) {
|
|
error(p, "Cannot assign to a constant");
|
|
return;
|
|
}
|
|
// Likewise a SUB/FUNCTION/label/TYPE name is not a store target; the
|
|
// FUNCTION return-value assignment is handled before reaching here.
|
|
if (sym->kind != SYM_VARIABLE) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Cannot assign to '%s'", sym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
// Fixed-length string: pad/truncate before storing. Skip for arrays:
|
|
// OP_STR_FIXLEN would convert the array reference being stored at DIM
|
|
// time into a fixed-width string, destroying the array (per-element
|
|
// fixed length is not modelled, so the '* n' is simply ignored here).
|
|
if (sym->fixedLen > 0 && !sym->isArray) {
|
|
basEmit8(&p->cg, OP_STR_FIXLEN);
|
|
basEmitU16(&p->cg, (uint16_t)sym->fixedLen);
|
|
}
|
|
// Explicitly typed scalar: coerce the value to the declared type so
|
|
// i% = 1.7 stores 2 and i% = 40000 raises Overflow. Arrays store a
|
|
// reference here (DIM/REDIM); their elements convert in the
|
|
// OP_STORE_ARRAY path.
|
|
if (sym->isTyped && !sym->isArray) {
|
|
emitStoreConv(p, sym->dataType);
|
|
}
|
|
if (sym->scope == SCOPE_LOCAL) {
|
|
basEmit8(&p->cg, OP_STORE_LOCAL);
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
} else if (sym->scope == SCOPE_FORM) {
|
|
basEmit8(&p->cg, OP_STORE_FORM_VAR);
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
} else {
|
|
basEmit8(&p->cg, OP_STORE_GLOBAL);
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
}
|
|
}
|
|
|
|
|
|
// Emit the conversion that coerces the value on top of the stack to a
|
|
// declared numeric type before it is stored. Shared by scalar, array
|
|
// element and TYPE field stores. STRING, DOUBLE, BOOLEAN, OBJECT and
|
|
// user types store the value as it is.
|
|
static void emitStoreConv(BasParserT *p, uint8_t dataType) {
|
|
switch (dataType) {
|
|
case BAS_TYPE_INTEGER:
|
|
basEmit8(&p->cg, OP_CONV_FLT_INT);
|
|
break;
|
|
case BAS_TYPE_LONG:
|
|
basEmit8(&p->cg, OP_CONV_INT_LONG);
|
|
break;
|
|
case BAS_TYPE_SINGLE:
|
|
basEmit8(&p->cg, OP_CONV_INT_FLT);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// emitUdtInit -- emit code to initialize nested UDT fields after a UDT
|
|
// has been created and is on top of the stack. For each field that is
|
|
// itself a UDT, we DUP the parent, allocate the child UDT, and store it
|
|
// into the field.
|
|
|
|
static void emitUdtInit(BasParserT *p, int32_t udtTypeId) {
|
|
BasSymbolT *typeSym = findTypeDefById(p, udtTypeId);
|
|
|
|
if (!typeSym) {
|
|
return;
|
|
}
|
|
|
|
for (int32_t i = 0; i < typeSym->fieldCount; i++) {
|
|
if (typeSym->fields[i].dataType != BAS_TYPE_UDT) {
|
|
continue;
|
|
}
|
|
|
|
int32_t childTypeId = typeSym->fields[i].udtTypeId;
|
|
BasSymbolT *childType = findTypeDefById(p, childTypeId);
|
|
|
|
if (!childType) {
|
|
continue;
|
|
}
|
|
|
|
// DUP parent, allocate child UDT, STORE_FIELD
|
|
basEmit8(&p->cg, OP_DUP);
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)childTypeId);
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)childType->fieldCount);
|
|
basEmit8(&p->cg, OP_DIM_ARRAY);
|
|
basEmit8(&p->cg, 0);
|
|
basEmit8(&p->cg, BAS_TYPE_UDT);
|
|
|
|
// Recursively init the child's nested UDT fields
|
|
emitUdtInit(p, childTypeId);
|
|
|
|
basEmit8(&p->cg, OP_STORE_FIELD);
|
|
basEmitU16(&p->cg, (uint16_t)i);
|
|
}
|
|
}
|
|
|
|
|
|
static BasSymbolT *ensureVariable(BasParserT *p, const char *name) {
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
|
if (sym != NULL) {
|
|
return sym;
|
|
}
|
|
|
|
// When in local scope, check if a shared global exists before auto-declaring
|
|
if (p->sym.inLocalScope) {
|
|
BasSymbolT *globalSym = basSymTabFindGlobal(&p->sym, name);
|
|
if (globalSym != NULL && globalSym->isShared) {
|
|
return globalSym;
|
|
}
|
|
}
|
|
|
|
// OPTION EXPLICIT: require explicit DIM
|
|
if (p->optionExplicit) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
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);
|
|
bool isTyped = nameHasTypeSuffix(name);
|
|
|
|
if (!isTyped && name[0] != '\0') {
|
|
// suffixToType returns SINGLE both for an explicit '!' suffix and
|
|
// for no suffix at all. Only apply the DEFxxx override when there
|
|
// is NO explicit suffix; an explicit '!' must stay SINGLE.
|
|
char firstLetter = (char)toupper((unsigned char)name[0]);
|
|
|
|
if (firstLetter >= 'A' && firstLetter <= 'Z') {
|
|
uint8_t defDt = p->defType[firstLetter - 'A'];
|
|
|
|
if (defDt != BAS_DEFTYPE_NONE) {
|
|
dt = defDt;
|
|
isTyped = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
sym = basSymTabAdd(&p->sym, name, SYM_VARIABLE, dt);
|
|
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return NULL;
|
|
}
|
|
|
|
sym->isTyped = isTyped;
|
|
|
|
// QB semantics: a variable first used inside a SUB/FUNCTION is local
|
|
// to that procedure. Anywhere else (module level, form scope) it is
|
|
// a module global. Implicit STRING locals are initialised to "" by
|
|
// the procedure prologue (see procEnd) since there is no DIM to emit
|
|
// the init at.
|
|
if (p->sym.inLocalScope) {
|
|
sym->scope = SCOPE_LOCAL;
|
|
sym->index = basSymTabAllocSlot(&p->sym);
|
|
sym->isImplicit = true;
|
|
} else {
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->index = basSymTabAllocGlobalSlot(&p->sym);
|
|
}
|
|
|
|
sym->isDefined = true;
|
|
return sym;
|
|
}
|
|
|
|
|
|
static void error(BasParserT *p, const char *msg) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
p->hasError = true;
|
|
|
|
// If the current token is on a later line than the previous token,
|
|
// the error is about the previous line (e.g. missing token at EOL).
|
|
int32_t line = p->lex.token.line;
|
|
if (p->prevLine > 0 && line > p->prevLine) {
|
|
line = p->prevLine;
|
|
}
|
|
|
|
p->errorLine = line;
|
|
snprintf(p->error, sizeof(p->error), "Line %d: %s", (int)line, msg);
|
|
}
|
|
|
|
|
|
static void errorExpected(BasParserT *p, const char *what) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Expected %s, got %s", what, basTokenName(p->lex.token.type));
|
|
error(p, buf);
|
|
}
|
|
|
|
|
|
static void exitListAdd(ExitListT *el, int32_t addr) {
|
|
arrput(el->patchAddr, addr);
|
|
}
|
|
|
|
|
|
static void exitListInit(ExitListT *el) {
|
|
el->patchAddr = NULL;
|
|
}
|
|
|
|
|
|
static void exitListPatch(ExitListT *el, BasParserT *p) {
|
|
int32_t target = basCodePos(&p->cg);
|
|
int32_t n = (int32_t)arrlen(el->patchAddr);
|
|
|
|
for (int32_t i = 0; i < n; i++) {
|
|
int16_t offset = relJumpOffset(p, target, el->patchAddr[i]);
|
|
basPatch16(&p->cg, el->patchAddr[i], offset);
|
|
}
|
|
|
|
arrfree(el->patchAddr);
|
|
el->patchAddr = NULL;
|
|
}
|
|
|
|
|
|
static void expect(BasParserT *p, BasTokenTypeE type) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
if (p->lex.token.type != type) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Expected %s, got %s", basTokenName(type), basTokenName(p->lex.token.type));
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
advance(p);
|
|
}
|
|
|
|
|
|
static void expectEndOfStatement(BasParserT *p) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
// Statement must end with newline, colon, EOF, or ELSE (single-line IF)
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_ELSE)) {
|
|
return;
|
|
}
|
|
if (check(p, TOK_COLON)) {
|
|
advance(p);
|
|
return;
|
|
}
|
|
errorExpected(p, "end of statement");
|
|
}
|
|
|
|
|
|
// Recursion guard for expression parsing. Returns false (with an error
|
|
// set) once the nesting limit is hit; callers return immediately.
|
|
static bool exprEnter(BasParserT *p) {
|
|
if (p->exprDepth >= BAS_MAX_EXPR_DEPTH) {
|
|
error(p, "Expression nested too deeply");
|
|
return false;
|
|
}
|
|
p->exprDepth++;
|
|
return true;
|
|
}
|
|
|
|
|
|
static void exprLeave(BasParserT *p) {
|
|
p->exprDepth--;
|
|
}
|
|
|
|
|
|
static const BuiltinFuncT *findBuiltin(const char *name) {
|
|
for (int32_t i = 0; builtinFuncs[i].name != NULL; i++) {
|
|
if (strcasecmp(name, builtinFuncs[i].name) == 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 && strcasecmp(p->sym.symbols[i]->name, name) == 0) {
|
|
return p->sym.symbols[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
static BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId) {
|
|
for (int32_t i = 0; i < p->sym.count; i++) {
|
|
if (p->sym.symbols[i]->kind == SYM_TYPE_DEF && p->sym.symbols[i]->index == typeId) {
|
|
return p->sym.symbols[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// Open a FOR (isFor) or DO/WHILE loop: start a fresh EXIT list, record
|
|
// the SELECT depth EXIT must unwind to, and bump the loop depth that
|
|
// makes a stray EXIT FOR / EXIT DO a compile error.
|
|
static void loopBegin(BasParserT *p, bool isFor, LoopSaveT *save) {
|
|
ExitListT *list = isFor ? &p->exitForList : &p->exitDoList;
|
|
int32_t *base = isFor ? &p->forSelectBase : &p->doSelectBase;
|
|
|
|
save->list = *list;
|
|
save->selectBase = *base;
|
|
|
|
exitListInit(list);
|
|
*base = p->selectDepth;
|
|
|
|
if (isFor) {
|
|
p->forDepth++;
|
|
} else {
|
|
p->doDepth++;
|
|
}
|
|
}
|
|
|
|
|
|
// Close the loop opened by loopBegin: land every EXIT on the current
|
|
// position (freeing the list) and restore the enclosing loop's state.
|
|
// Runs on the error path too, so no EXIT list is ever leaked.
|
|
static void loopEnd(BasParserT *p, bool isFor, LoopSaveT *save) {
|
|
ExitListT *list = isFor ? &p->exitForList : &p->exitDoList;
|
|
int32_t *base = isFor ? &p->forSelectBase : &p->doSelectBase;
|
|
|
|
exitListPatch(list, p);
|
|
|
|
*list = save->list;
|
|
*base = save->selectBase;
|
|
|
|
if (isFor) {
|
|
p->forDepth--;
|
|
} else {
|
|
p->doDepth--;
|
|
}
|
|
}
|
|
|
|
|
|
static bool match(BasParserT *p, BasTokenTypeE type) {
|
|
// Once an error is set, advance() is a no-op (the token is frozen),
|
|
// so a true return here would spin any 'while (match(...))' loop
|
|
// forever. Report no match so error unwinding terminates the loop.
|
|
if (p->hasError) {
|
|
return false;
|
|
}
|
|
|
|
if (p->lex.token.type == type) {
|
|
advance(p);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Report whether a name carries an explicit BASIC type-suffix character.
|
|
// Lets callers distinguish an explicitly typed name like "a!" from a bare
|
|
// "a", which suffixToType cannot do alone because it returns
|
|
// BAS_TYPE_SINGLE for both. Shares the lexer's basIsTypeSuffixChar so the
|
|
// suffix character set has a single source of truth.
|
|
static bool nameHasTypeSuffix(const char *name) {
|
|
int32_t len = (int32_t)strlen(name);
|
|
if (len == 0) {
|
|
return false;
|
|
}
|
|
return basIsTypeSuffixChar(name[len - 1]);
|
|
}
|
|
|
|
|
|
static void parseAddExpr(BasParserT *p) {
|
|
parseMulExpr(p);
|
|
while (!p->hasError) {
|
|
if (check(p, TOK_PLUS)) {
|
|
advance(p);
|
|
parseMulExpr(p);
|
|
basEmit8(&p->cg, OP_ADD_INT); // VM handles type promotion
|
|
} else if (check(p, TOK_MINUS)) {
|
|
advance(p);
|
|
parseMulExpr(p);
|
|
basEmit8(&p->cg, OP_SUB_INT);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseAndExpr(BasParserT *p) {
|
|
parseNotExpr(p);
|
|
while (!p->hasError && check(p, TOK_AND)) {
|
|
advance(p);
|
|
parseNotExpr(p);
|
|
basEmit8(&p->cg, OP_AND);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseAssignOrCall(BasParserT *p) {
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// MID$ statement: MID$(var$, start [, len]) = replacement$
|
|
if (strcasecmp(name, "MID$") == 0 && check(p, TOK_LPAREN)) {
|
|
expect(p, TOK_LPAREN);
|
|
|
|
// First arg: target string variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "string variable name");
|
|
return;
|
|
}
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *varSym = ensureVariable(p, varName);
|
|
if (varSym == NULL) {
|
|
return;
|
|
}
|
|
|
|
// Load the original string
|
|
emitLoad(p, varSym);
|
|
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // start position
|
|
|
|
// Optional length
|
|
if (match(p, TOK_COMMA)) {
|
|
parseExpression(p); // length
|
|
} else {
|
|
// Push 0 as sentinel meaning "use replacement length"
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0);
|
|
}
|
|
|
|
expect(p, TOK_RPAREN);
|
|
expect(p, TOK_EQ);
|
|
|
|
// Parse replacement expression
|
|
parseExpression(p);
|
|
|
|
// Emit MID$ assignment: pops replacement, len, start, str; pushes result
|
|
basEmit8(&p->cg, OP_STR_MID_ASGN);
|
|
|
|
// Store back to the variable
|
|
emitStore(p, varSym);
|
|
return;
|
|
}
|
|
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
|
|
|
// Dot member access: UDT field or control property/method
|
|
if (check(p, TOK_DOT)) {
|
|
// Check for UDT field access first
|
|
if (sym != NULL && sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) {
|
|
emitLoad(p, sym);
|
|
int32_t curTypeId = sym->udtTypeId;
|
|
|
|
// Walk the dot chain: a.b.c = expr
|
|
// For intermediate fields, emit LOAD_FIELD (navigate into nested UDT).
|
|
// For the final field, emit STORE_FIELD with the assigned value.
|
|
while (check(p, TOK_DOT) && curTypeId >= 0) {
|
|
advance(p); // consume DOT
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "field name");
|
|
return;
|
|
}
|
|
BasSymbolT *typeSym = findTypeDefById(p, curTypeId);
|
|
if (typeSym == NULL) {
|
|
error(p, "Unknown TYPE definition");
|
|
return;
|
|
}
|
|
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
|
|
if (fieldIdx < 0) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
advance(p); // consume field name
|
|
|
|
// Check if this field is a nested UDT with more dots coming
|
|
bool fieldIsUdt = (typeSym->fields[fieldIdx].dataType == BAS_TYPE_UDT);
|
|
|
|
if (fieldIsUdt && check(p, TOK_DOT)) {
|
|
// Intermediate level: load this field and continue
|
|
basEmit8(&p->cg, OP_LOAD_FIELD);
|
|
basEmitU16(&p->cg, (uint16_t)fieldIdx);
|
|
curTypeId = typeSym->fields[fieldIdx].udtTypeId;
|
|
} else {
|
|
// Final field: store value
|
|
expect(p, TOK_EQ);
|
|
parseExpression(p);
|
|
emitStoreConv(p, typeSym->fields[fieldIdx].dataType);
|
|
basEmit8(&p->cg, OP_STORE_FIELD);
|
|
basEmitU16(&p->cg, (uint16_t)fieldIdx);
|
|
return;
|
|
}
|
|
}
|
|
|
|
error(p, "Expected '=' in UDT field assignment");
|
|
return;
|
|
}
|
|
|
|
// Control property/method access: CtrlName.Member
|
|
// Emit: push current form ref, push ctrl name, FIND_CTRL
|
|
advance(p); // consume DOT
|
|
|
|
// Accept any identifier or keyword as a member name -- keywords
|
|
// like Load, Show, Hide, Clear are valid method names on controls.
|
|
if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
|
|
errorExpected(p, "property or method name");
|
|
return;
|
|
}
|
|
|
|
char memberName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(memberName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p); // consume member name
|
|
|
|
// If `name` is a regular variable, the user is dereferencing an
|
|
// object reference (typically a form or control returned by
|
|
// CreateForm / CreateControl). Use the variable's value as the
|
|
// ref directly instead of treating `name` as a literal control
|
|
// name.
|
|
bool isVarRef = (sym != NULL && sym->kind == SYM_VARIABLE);
|
|
|
|
// Special form methods: Show, Hide
|
|
if (strcasecmp(memberName, "Show") == 0) {
|
|
// name.Show [modal]
|
|
if (isVarRef) {
|
|
emitLoad(p, sym);
|
|
} else {
|
|
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, nameIdx);
|
|
basEmit8(&p->cg, OP_LOAD_FORM);
|
|
}
|
|
uint8_t modal = parseShowModalFlag(p);
|
|
basEmit8(&p->cg, OP_SHOW_FORM);
|
|
basEmit8(&p->cg, modal);
|
|
return;
|
|
}
|
|
|
|
if (strcasecmp(memberName, "Hide") == 0) {
|
|
if (isVarRef) {
|
|
emitLoad(p, sym);
|
|
} else {
|
|
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, nameIdx);
|
|
basEmit8(&p->cg, OP_LOAD_FORM);
|
|
}
|
|
basEmit8(&p->cg, OP_HIDE_FORM);
|
|
return;
|
|
}
|
|
|
|
if (check(p, TOK_EQ)) {
|
|
// Property assignment: CtrlName.Property = expr
|
|
advance(p); // consume =
|
|
|
|
// Compile-time validation: if the host provided a validator
|
|
// (IDE), check that the property exists on the widget type.
|
|
// Skip when `name` is a variable reference (dynamic ctrl we
|
|
// can't statically type) or when the ctrl isn't in the map
|
|
// (e.g. created via CreateControl at runtime).
|
|
if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isPropValid) {
|
|
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
|
|
|
|
if (wgtType && !p->validator->isPropValid(p->validator->ctx, wgtType, memberName)) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Unknown property '%s.%s' (type '%s' has no such property)", name, memberName, wgtType);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Push ctrl/form ref
|
|
if (isVarRef) {
|
|
emitLoad(p, sym);
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0);
|
|
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, ctrlNameIdx);
|
|
basEmit8(&p->cg, OP_FIND_CTRL);
|
|
}
|
|
|
|
// Push property name
|
|
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, propNameIdx);
|
|
|
|
// Parse value expression
|
|
parseExpression(p);
|
|
|
|
// Store property
|
|
basEmit8(&p->cg, OP_STORE_PROP);
|
|
return;
|
|
}
|
|
|
|
// Method call: CtrlName.Method [args]
|
|
// Same compile-time validation as above, but for methods.
|
|
if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isMethodValid) {
|
|
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
|
|
|
|
if (wgtType && !p->validator->isMethodValid(p->validator->ctx, wgtType, memberName)) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Unknown method '%s.%s' (type '%s' has no such method)", name, memberName, wgtType);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Push ctrl/form ref
|
|
if (isVarRef) {
|
|
emitLoad(p, sym);
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0);
|
|
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, ctrlNameIdx);
|
|
basEmit8(&p->cg, OP_FIND_CTRL);
|
|
}
|
|
|
|
// Push method name
|
|
uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, methodNameIdx);
|
|
|
|
emitMethodCallStatement(p);
|
|
return;
|
|
}
|
|
|
|
// Array assignment, sub/function call, or control array access: var(index)
|
|
if (check(p, TOK_LPAREN)) {
|
|
// Could be a function call as a statement (discard result)
|
|
// or array assignment
|
|
if (sym != NULL && (sym->kind == SYM_SUB || sym->kind == SYM_FUNCTION)) {
|
|
emitFunctionCall(p, sym);
|
|
if (sym->kind == SYM_FUNCTION) {
|
|
basEmit8(&p->cg, OP_POP); // discard return value
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Control array property/method: Name(idx).Prop = expr OR Name(idx).Method args
|
|
if (sym == NULL && checkCtrlArrayAccess(p)) {
|
|
expect(p, TOK_LPAREN);
|
|
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0); // NULL form ref = current form
|
|
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, ctrlNameIdx);
|
|
|
|
parseExpression(p); // index expression
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FIND_CTRL_IDX);
|
|
|
|
expect(p, TOK_DOT);
|
|
if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
|
|
errorExpected(p, "property or method name");
|
|
return;
|
|
}
|
|
char memberName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(memberName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
if (check(p, TOK_EQ)) {
|
|
// Property assignment: Name(idx).Prop = expr
|
|
advance(p);
|
|
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, propNameIdx);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_STORE_PROP);
|
|
} else {
|
|
// Method call: Name(idx).Method args
|
|
uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, methodNameIdx);
|
|
|
|
emitMethodCallStatement(p);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Array element assignment
|
|
if (sym == NULL) {
|
|
sym = ensureVariable(p, name);
|
|
}
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
|
|
emitLoad(p, sym);
|
|
expect(p, TOK_LPAREN);
|
|
int32_t dims = 0;
|
|
parseExpression(p);
|
|
dims++;
|
|
while (match(p, TOK_COMMA)) {
|
|
parseExpression(p);
|
|
dims++;
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
|
|
// Array-of-UDT field store: arr(i).field = expr
|
|
if (sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0 && check(p, TOK_DOT)) {
|
|
advance(p); // consume DOT
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "field name");
|
|
return;
|
|
}
|
|
BasSymbolT *typeSym = findTypeDefById(p, sym->udtTypeId);
|
|
if (typeSym == NULL) {
|
|
error(p, "Unknown TYPE definition");
|
|
return;
|
|
}
|
|
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
|
|
if (fieldIdx < 0) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
advance(p); // consume field name
|
|
expect(p, TOK_EQ);
|
|
parseExpression(p);
|
|
emitStoreConv(p, typeSym->fields[fieldIdx].dataType);
|
|
basEmit8(&p->cg, OP_STORE_ARRAY_FIELD);
|
|
basEmit8(&p->cg, (uint8_t)dims);
|
|
basEmitU16(&p->cg, (uint16_t)fieldIdx);
|
|
return;
|
|
}
|
|
|
|
expect(p, TOK_EQ);
|
|
parseExpression(p);
|
|
|
|
if (sym->isTyped) {
|
|
emitStoreConv(p, sym->dataType);
|
|
}
|
|
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;
|
|
}
|
|
// Assigning to the enclosing FUNCTION's own name sets its return
|
|
// value. Any other procedure name is not a store target (a
|
|
// STORE_LOCAL 0 here would clobber the caller's first slot).
|
|
if (sym->kind == SYM_FUNCTION) {
|
|
if (!p->currentProcIsFunction || strcasecmp(name, p->currentProc) != 0) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Cannot assign to function '%s' outside its own body", sym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
parseExpression(p);
|
|
// Store to the implicit return-value local slot (index 0 in function scope)
|
|
basEmit8(&p->cg, OP_STORE_LOCAL);
|
|
basEmitU16(&p->cg, 0);
|
|
return;
|
|
}
|
|
parseExpression(p);
|
|
emitStore(p, sym);
|
|
return;
|
|
}
|
|
|
|
// Sub call without parens: SUBName arg1, arg2 ...
|
|
// If the identifier is unknown, treat it as a forward-referenced sub.
|
|
if (sym == NULL) {
|
|
sym = basSymTabAdd(&p->sym, name, SYM_SUB, BAS_TYPE_INTEGER);
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = false;
|
|
sym->codeAddr = 0;
|
|
}
|
|
|
|
if (sym->kind == SYM_SUB) {
|
|
int32_t argc = parseCallArgs(p, sym, false);
|
|
emitCallWithArgs(p, sym, argc);
|
|
return;
|
|
}
|
|
|
|
// If nothing else, it's an assignment missing the =
|
|
errorExpected(p, "'=' or '('");
|
|
}
|
|
|
|
|
|
// BEGINFORM "FormName" enters form scope: DIM at module level
|
|
// creates per-form variables. ENDFORM exits form scope.
|
|
|
|
static void parseBeginForm(BasParserT *p) {
|
|
advance(p); // consume BEGINFORM
|
|
|
|
if (!check(p, TOK_STRING_LIT)) {
|
|
errorExpected(p, "form name string");
|
|
return;
|
|
}
|
|
|
|
if (p->lex.token.textLen >= BAS_MAX_IDENT) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Form name too long (maximum is %d characters)", (int)(BAS_MAX_IDENT - 1));
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
|
|
char formName[BAS_MAX_IDENT];
|
|
strcpy(formName, p->lex.token.text);
|
|
advance(p);
|
|
|
|
if (p->sym.inFormScope) {
|
|
error(p, "Nested BEGINFORM is not allowed");
|
|
return;
|
|
}
|
|
|
|
if (p->sym.inLocalScope) {
|
|
error(p, "BEGINFORM inside SUB/FUNCTION is not allowed");
|
|
return;
|
|
}
|
|
|
|
basSymTabEnterFormScope(&p->sym, formName);
|
|
|
|
// Emit a forward JMP to skip over form init code. All module-level
|
|
// bytecode inside the form scope (array DIMs, UDT init, executable
|
|
// statements, JMPs over SUB bodies) goes into the init block that
|
|
// runs at form load time, not at program startup.
|
|
basEmit8(&p->cg, OP_JMP);
|
|
p->formInitJmpAddr = basCodePos(&p->cg);
|
|
basEmit16(&p->cg, 0); // placeholder -- patched at ENDFORM
|
|
p->formInitCodeStart = basCodePos(&p->cg);
|
|
}
|
|
|
|
|
|
// Parse the argument list of a SUB/FUNCTION call and push each value (or
|
|
// address, for BYREF parameters). parens=true expects "(args)"; false
|
|
// parses the bare "name arg, arg" statement form up to end of statement.
|
|
// Returns the number of arguments parsed.
|
|
static int32_t parseCallArgs(BasParserT *p, BasSymbolT *sym, bool parens) {
|
|
int32_t argc = 0;
|
|
bool more;
|
|
|
|
if (parens) {
|
|
expect(p, TOK_LPAREN);
|
|
more = !check(p, TOK_RPAREN);
|
|
} else {
|
|
more = !check(p, TOK_NEWLINE) && !check(p, TOK_EOF) && !check(p, TOK_COLON) && !check(p, TOK_ELSE);
|
|
}
|
|
|
|
while (more && !p->hasError) {
|
|
if (argc < sym->paramCount && !sym->paramByVal[argc]) {
|
|
emitByRefArg(p);
|
|
} else {
|
|
parseExpression(p);
|
|
}
|
|
argc++;
|
|
more = match(p, TOK_COMMA);
|
|
}
|
|
|
|
if (parens) {
|
|
expect(p, TOK_RPAREN);
|
|
}
|
|
|
|
return argc;
|
|
}
|
|
|
|
|
|
static void parseChDir(BasParserT *p) {
|
|
// CHDIR path
|
|
advance(p);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_FS_CHDIR);
|
|
}
|
|
|
|
|
|
static void parseChDrive(BasParserT *p) {
|
|
// CHDRIVE drive
|
|
advance(p);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_FS_CHDRIVE);
|
|
}
|
|
|
|
|
|
static void parseClose(BasParserT *p) {
|
|
// CLOSE #channel
|
|
advance(p); // consume CLOSE
|
|
|
|
// Optional # prefix
|
|
match(p, TOK_HASH);
|
|
|
|
// Channel number
|
|
parseExpression(p);
|
|
|
|
basEmit8(&p->cg, OP_FILE_CLOSE);
|
|
}
|
|
|
|
|
|
static void parseCompareExpr(BasParserT *p) {
|
|
parseConcatExpr(p);
|
|
while (!p->hasError) {
|
|
if (check(p, TOK_EQ)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_EQ);
|
|
} else if (check(p, TOK_NE)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_NE);
|
|
} else if (check(p, TOK_LT)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_LT);
|
|
} else if (check(p, TOK_GT)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_GT);
|
|
} else if (check(p, TOK_LE)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_LE);
|
|
} else if (check(p, TOK_GE)) {
|
|
advance(p);
|
|
parseConcatExpr(p);
|
|
basEmit8(&p->cg, OP_CMP_GE);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseConcatExpr(BasParserT *p) {
|
|
parseAddExpr(p);
|
|
while (!p->hasError && check(p, TOK_AMPERSAND)) {
|
|
advance(p);
|
|
parseAddExpr(p);
|
|
basEmit8(&p->cg, OP_STR_CONCAT);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseConst(BasParserT *p) {
|
|
// CONST name [AS type] = value
|
|
advance(p); // consume CONST
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "constant name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Optional type annotation (declarative; value's literal type still
|
|
// determines runtime representation).
|
|
if (match(p, TOK_AS)) {
|
|
(void)resolveTypeName(p);
|
|
}
|
|
|
|
expect(p, TOK_EQ);
|
|
|
|
// Parse the constant value (must be a literal)
|
|
bool isNeg = false;
|
|
if (check(p, TOK_MINUS)) {
|
|
isNeg = true;
|
|
advance(p);
|
|
}
|
|
|
|
BasSymbolT *sym = NULL;
|
|
|
|
if (check(p, TOK_INT_LIT) || check(p, TOK_LONG_LIT)) {
|
|
int32_t val = check(p, TOK_INT_LIT) ? p->lex.token.intVal : (int32_t)p->lex.token.longVal;
|
|
if (isNeg) {
|
|
val = -val;
|
|
}
|
|
sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_LONG);
|
|
if (sym != NULL) {
|
|
sym->constInt = val;
|
|
sym->isDefined = true;
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
advance(p);
|
|
} else if (check(p, TOK_FLOAT_LIT)) {
|
|
double val = p->lex.token.dblVal;
|
|
if (isNeg) {
|
|
val = -val;
|
|
}
|
|
sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_DOUBLE);
|
|
if (sym != NULL) {
|
|
sym->constDbl = val;
|
|
sym->isDefined = true;
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
advance(p);
|
|
} else if (check(p, TOK_STRING_LIT) && !isNeg) {
|
|
sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_STRING);
|
|
if (sym != NULL) {
|
|
strncpy(sym->constStr, p->lex.token.text, sizeof(sym->constStr) - 1);
|
|
sym->constStr[sizeof(sym->constStr) - 1] = '\0';
|
|
sym->isDefined = true;
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
advance(p);
|
|
} else if (check(p, TOK_TRUE_KW) && !isNeg) {
|
|
sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_BOOLEAN);
|
|
if (sym != NULL) {
|
|
sym->constInt = -1;
|
|
sym->isDefined = true;
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
advance(p);
|
|
} else if (check(p, TOK_FALSE_KW) && !isNeg) {
|
|
sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_BOOLEAN);
|
|
if (sym != NULL) {
|
|
sym->constInt = 0;
|
|
sym->isDefined = true;
|
|
sym->scope = SCOPE_GLOBAL;
|
|
}
|
|
advance(p);
|
|
} else {
|
|
error(p, "Constant value must be a literal");
|
|
}
|
|
|
|
if (sym == NULL && !p->hasError) {
|
|
error(p, "Duplicate constant or symbol table full");
|
|
}
|
|
}
|
|
|
|
|
|
static void parseData(BasParserT *p) {
|
|
// DATA val1, val2, "string", ...
|
|
// Collect all values into the data pool. No runtime code is emitted.
|
|
advance(p); // consume DATA
|
|
|
|
for (;;) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
bool isNeg = false;
|
|
if (check(p, TOK_MINUS)) {
|
|
isNeg = true;
|
|
advance(p);
|
|
}
|
|
|
|
if (check(p, TOK_INT_LIT)) {
|
|
// Hex/octal/binary literals arrive as TOK_INT_LIT with up to
|
|
// 32 significant bits; keep the wide ones as LONG.
|
|
int32_t val = p->lex.token.intVal;
|
|
if (isNeg) {
|
|
val = -val;
|
|
}
|
|
BasValueT v = (val >= INT16_MIN && val <= INT16_MAX) ? basValInteger((int16_t)val) : basValLong(val);
|
|
basAddData(&p->cg, v);
|
|
advance(p);
|
|
} else if (check(p, TOK_LONG_LIT)) {
|
|
int32_t val = (int32_t)p->lex.token.longVal;
|
|
if (isNeg) {
|
|
val = -val;
|
|
}
|
|
BasValueT v = basValLong(val);
|
|
basAddData(&p->cg, v);
|
|
advance(p);
|
|
} else if (check(p, TOK_FLOAT_LIT)) {
|
|
double val = p->lex.token.dblVal;
|
|
if (isNeg) {
|
|
val = -val;
|
|
}
|
|
BasValueT v = basValDouble(val);
|
|
basAddData(&p->cg, v);
|
|
advance(p);
|
|
} else if (check(p, TOK_STRING_LIT) && !isNeg) {
|
|
BasValueT v = basValStringFromC(p->lex.token.text);
|
|
basAddData(&p->cg, v);
|
|
basValRelease(&v);
|
|
advance(p);
|
|
} else {
|
|
// Unquoted text -- read as string up to comma/newline/EOF
|
|
// In QB, unquoted DATA values are treated as strings
|
|
if (isNeg) {
|
|
// Negative sign without a number -- treat "-" as string data
|
|
BasValueT v = basValStringFromC("-");
|
|
basAddData(&p->cg, v);
|
|
basValRelease(&v);
|
|
} else if (check(p, TOK_IDENT)) {
|
|
BasValueT v = basValStringFromC(p->lex.token.text);
|
|
basAddData(&p->cg, v);
|
|
basValRelease(&v);
|
|
advance(p);
|
|
} else {
|
|
error(p, "Expected DATA value");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseDeclare(BasParserT *p) {
|
|
// DECLARE SUB name(params)
|
|
// DECLARE FUNCTION name(params) AS type
|
|
// DECLARE LIBRARY "name" ... END DECLARE
|
|
advance(p); // consume DECLARE
|
|
|
|
// DECLARE LIBRARY block
|
|
if (checkKeyword(p, "LIBRARY")) {
|
|
parseDeclareLibrary(p);
|
|
return;
|
|
}
|
|
|
|
BasSymKindE kind;
|
|
|
|
if (check(p, TOK_SUB)) {
|
|
kind = SYM_SUB;
|
|
advance(p);
|
|
} else if (check(p, TOK_FUNCTION)) {
|
|
kind = SYM_FUNCTION;
|
|
advance(p);
|
|
} else {
|
|
error(p, "Expected SUB, FUNCTION, or LIBRARY after DECLARE");
|
|
return;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "subroutine/function name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
ParamListT pl;
|
|
|
|
if (!parseParamList(p, &pl, 0)) {
|
|
return;
|
|
}
|
|
|
|
// 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 = basSymTabFindGlobal(&p->sym, name);
|
|
|
|
if (sym == NULL) {
|
|
sym = basSymTabAdd(&p->sym, name, kind, returnType);
|
|
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = false;
|
|
sym->codeAddr = 0;
|
|
} else if (sym->kind != kind) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "'%s' is already declared as something else", sym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
|
|
// The prescan (or an earlier definition/DECLARE) may already have
|
|
// recorded the real signature; a DECLARE that disagrees with it is an
|
|
// error rather than a silent partial overwrite. A call-site stub has
|
|
// no signature yet and simply takes this one.
|
|
if (sym->hasSignature) {
|
|
bool matches = (sym->paramCount == pl.count);
|
|
|
|
for (int32_t i = 0; matches && i < pl.count; i++) {
|
|
matches = (sym->paramTypes[i] == pl.types[i]);
|
|
}
|
|
|
|
if (!matches) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "DECLARE for '%s' does not match its definition", sym->name);
|
|
error(p, buf);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
applyParamList(sym, &pl);
|
|
}
|
|
|
|
|
|
// Declares external native functions from a dynamically loaded
|
|
// library. The library name is stored in the constant pool.
|
|
// Each function inside the block is registered as an extern symbol.
|
|
// At runtime, OP_CALL_EXTERN resolves the function via the host's
|
|
// resolveExtern callback (typically dlsym).
|
|
|
|
static void parseDeclareLibrary(BasParserT *p) {
|
|
advance(p); // consume LIBRARY
|
|
|
|
if (!check(p, TOK_STRING_LIT)) {
|
|
errorExpected(p, "library name string");
|
|
return;
|
|
}
|
|
|
|
uint16_t libNameIdx = basAddConstant(&p->cg, p->lex.token.text, p->lex.token.textLen);
|
|
advance(p);
|
|
|
|
skipNewlines(p);
|
|
|
|
// Parse function declarations until END DECLARE
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
skipNewlines(p);
|
|
|
|
// Check for END DECLARE
|
|
if (check(p, TOK_END)) {
|
|
advance(p);
|
|
|
|
if (check(p, TOK_DECLARE)) {
|
|
advance(p);
|
|
break;
|
|
}
|
|
|
|
error(p, "Expected DECLARE after END in DECLARE LIBRARY block");
|
|
return;
|
|
}
|
|
|
|
// Must be DECLARE SUB or DECLARE FUNCTION
|
|
if (!check(p, TOK_DECLARE)) {
|
|
errorExpected(p, "DECLARE or END DECLARE");
|
|
return;
|
|
}
|
|
|
|
advance(p); // consume DECLARE
|
|
|
|
BasSymKindE kind;
|
|
|
|
if (check(p, TOK_SUB)) {
|
|
kind = SYM_SUB;
|
|
advance(p);
|
|
} else if (check(p, TOK_FUNCTION)) {
|
|
kind = SYM_FUNCTION;
|
|
advance(p);
|
|
} else {
|
|
error(p, "Expected SUB or FUNCTION in DECLARE LIBRARY block");
|
|
return;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "function name");
|
|
return;
|
|
}
|
|
|
|
char funcName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(funcName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
funcName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Strip type suffix ($%&!#) from extern name so dlsym finds the
|
|
// C function. The suffix is still used for return type via suffixToType.
|
|
char externName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(externName, funcName, BAS_MAX_TOKEN_LEN - 1);
|
|
externName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
int32_t enLen = (int32_t)strlen(externName);
|
|
|
|
if (enLen > 0) {
|
|
char last = externName[enLen - 1];
|
|
|
|
if (basIsTypeSuffixChar(last)) {
|
|
externName[enLen - 1] = '\0';
|
|
}
|
|
}
|
|
|
|
uint16_t funcNameIdx = basAddConstant(&p->cg, externName, (int32_t)strlen(externName));
|
|
|
|
ParamListT pl;
|
|
|
|
if (!parseParamList(p, &pl, 0)) {
|
|
return;
|
|
}
|
|
|
|
// Return type for FUNCTION
|
|
uint8_t returnType = suffixToType(funcName);
|
|
|
|
if (kind == SYM_FUNCTION && match(p, TOK_AS)) {
|
|
returnType = resolveTypeName(p);
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
// Register as extern symbol (the prescan may already have added
|
|
// it from this same DECLARE line; reuse that entry).
|
|
BasSymbolT *sym = basSymTabFindGlobal(&p->sym, funcName);
|
|
|
|
if (sym == NULL) {
|
|
sym = basSymTabAdd(&p->sym, funcName, kind, returnType);
|
|
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
} else if (sym->kind != kind) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "'%s' is already declared as something else", sym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->dataType = returnType;
|
|
sym->isDefined = true;
|
|
sym->isExtern = true;
|
|
sym->externLibIdx = libNameIdx;
|
|
sym->externFuncIdx = funcNameIdx;
|
|
applyParamList(sym, &pl);
|
|
}
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
// A DEF FN body gets its own local scope; opening one inside a
|
|
// SUB/FUNCTION would reset that procedure's slot numbering and free
|
|
// its locals on exit. QB forbids it too.
|
|
if (p->sym.inLocalScope) {
|
|
error(p, "DEF FN is not allowed inside SUB or FUNCTION");
|
|
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
|
|
|
|
ParamListT pl;
|
|
|
|
if (!parseParamList(p, &pl, PARAM_REGISTER | PARAM_FORCE_BYVAL)) {
|
|
return;
|
|
}
|
|
|
|
expect(p, TOK_EQ);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_RET_VAL);
|
|
|
|
// A DEF FN occupies a proc-table slot (it is a defined FUNCTION
|
|
// symbol), and the proc table is sorted by code address to match the
|
|
// source-order debugProcCount indices -- so consume a debug proc index
|
|
// here even though no debug locals are emitted for it. DEF FN params
|
|
// are tiny/transient, so skipping their debug info is safe.
|
|
p->cg.debugProcCount++;
|
|
basSymTabLeaveLocal(&p->sym);
|
|
|
|
uint8_t returnType = suffixToType(name);
|
|
BasSymbolT *funcSym = basSymTabAdd(&p->sym, name, SYM_FUNCTION, returnType);
|
|
|
|
if (funcSym == NULL) {
|
|
error(p, "Could not register DEF function");
|
|
return;
|
|
}
|
|
|
|
funcSym->codeAddr = funcAddr;
|
|
funcSym->isDefined = true;
|
|
funcSym->scope = SCOPE_GLOBAL;
|
|
applyParamList(funcSym, &pl);
|
|
|
|
patchCallAddrs(p, funcSym);
|
|
patchJump(p, skipJump);
|
|
}
|
|
|
|
|
|
// Sets the default type for variables whose names start with
|
|
// letters in the given range. Example: DEFINT A-Z makes all
|
|
// untyped variables default to INTEGER.
|
|
|
|
static void parseDefType(BasParserT *p, uint8_t dataType) {
|
|
advance(p); // consume DEFxxx keyword
|
|
|
|
while (!p->hasError) {
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "letter or letter range");
|
|
return;
|
|
}
|
|
|
|
char startLetter = (char)toupper((unsigned char)p->lex.token.text[0]);
|
|
|
|
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 = (char)toupper((unsigned char)p->lex.token.text[0]);
|
|
|
|
if (endLetter < 'A' || endLetter > 'Z') {
|
|
error(p, "Expected letter A-Z");
|
|
return;
|
|
}
|
|
|
|
advance(p);
|
|
}
|
|
|
|
if (endLetter < startLetter) {
|
|
error(p, "Letter range must be in ascending order (e.g. A-Z)");
|
|
return;
|
|
}
|
|
|
|
// Set default type for the range
|
|
for (char c = startLetter; c <= endLetter; c++) {
|
|
p->defType[c - 'A'] = dataType;
|
|
}
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseDim(BasParserT *p) {
|
|
// DIM [SHARED] var AS type
|
|
// DIM var(ubound) AS type
|
|
// DIM var(lbound TO ubound) AS type
|
|
// DIM var AS UdtType
|
|
advance(p); // consume DIM
|
|
|
|
// Check for SHARED keyword
|
|
bool isShared = false;
|
|
if (check(p, TOK_SHARED)) {
|
|
isShared = true;
|
|
advance(p);
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
bool isArray = false;
|
|
int32_t dims = 0;
|
|
|
|
// Check for array bounds
|
|
if (check(p, TOK_LPAREN)) {
|
|
isArray = true;
|
|
advance(p);
|
|
parseDimBounds(p, &dims);
|
|
expect(p, TOK_RPAREN);
|
|
}
|
|
|
|
// Optional AS type
|
|
uint8_t dt = suffixToType(name);
|
|
bool isTyped = nameHasTypeSuffix(name);
|
|
int32_t udtTypeId = -1;
|
|
int32_t fixedLen = 0;
|
|
if (match(p, TOK_AS)) {
|
|
dt = resolveTypeName(p);
|
|
isTyped = true;
|
|
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)) {
|
|
error(p, "Expected integer after STRING *");
|
|
} else if (p->lex.token.intVal < 1 || p->lex.token.intVal > UINT16_MAX) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "STRING * length must be 1 to %d", (int)UINT16_MAX);
|
|
error(p, buf);
|
|
} else {
|
|
fixedLen = p->lex.token.intVal;
|
|
advance(p);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
// Determine the scope this DIM would create (local > form > global)
|
|
// so duplicate detection only rejects a name already declared in the
|
|
// SAME scope. basSymTabFind falls back to form/global from a SUB, so a
|
|
// plain isDefined check wrongly blocked a local DIM from shadowing a
|
|
// module-level variable (standard QB semantics, see dvx_basic_reference).
|
|
BasScopeE newScope;
|
|
if (p->sym.inLocalScope) {
|
|
newScope = SCOPE_LOCAL;
|
|
} else if (p->sym.inFormScope) {
|
|
newScope = SCOPE_FORM;
|
|
} else {
|
|
newScope = SCOPE_GLOBAL;
|
|
}
|
|
|
|
// Check for duplicates. Only a VARIABLE from an outer scope may be
|
|
// shadowed; a CONST/label/type of the same name must be rejected
|
|
// regardless of scope, because local-first lookup would make every
|
|
// later use of that name silently bind to the new variable. A
|
|
// SUB/FUNCTION is rejected even when not yet defined: prescan
|
|
// registers every signature before bodies parse, so the name is
|
|
// already committed to a procedure (a shadowed FUNCTION call would
|
|
// otherwise compile as an array load on a scalar).
|
|
BasSymbolT *existing = basSymTabFind(&p->sym, name);
|
|
bool isDupSymbol = false;
|
|
|
|
if (existing != NULL) {
|
|
if (existing->kind == SYM_SUB || existing->kind == SYM_FUNCTION) {
|
|
isDupSymbol = true;
|
|
} else if (existing->isDefined && (existing->kind != SYM_VARIABLE || existing->scope == newScope)) {
|
|
isDupSymbol = true;
|
|
}
|
|
}
|
|
|
|
if (isDupSymbol) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "'%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;
|
|
sym->isTyped = isTyped;
|
|
|
|
sym->scope = newScope;
|
|
|
|
if (isArray) {
|
|
if (dt == BAS_TYPE_UDT && udtTypeId >= 0) {
|
|
// For UDT arrays, push typeId and fieldCount so elements
|
|
// can be properly initialized
|
|
BasSymbolT *typeSym = findTypeDefById(p, udtTypeId);
|
|
if (typeSym != NULL) {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)udtTypeId);
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)typeSym->fieldCount);
|
|
}
|
|
}
|
|
basEmit8(&p->cg, OP_DIM_ARRAY);
|
|
basEmit8(&p->cg, (uint8_t)dims);
|
|
basEmit8(&p->cg, dt);
|
|
emitStore(p, sym);
|
|
} else if (dt == BAS_TYPE_UDT && udtTypeId >= 0) {
|
|
// Allocate a UDT instance
|
|
BasSymbolT *typeSym = findTypeDefById(p, udtTypeId);
|
|
if (typeSym != NULL) {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)udtTypeId);
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)typeSym->fieldCount);
|
|
// OP_DIM_ARRAY with dims=0 signals UDT allocation
|
|
basEmit8(&p->cg, OP_DIM_ARRAY);
|
|
basEmit8(&p->cg, 0);
|
|
basEmit8(&p->cg, BAS_TYPE_UDT);
|
|
// Initialize nested UDT fields
|
|
emitUdtInit(p, udtTypeId);
|
|
emitStore(p, sym);
|
|
}
|
|
} else if (dt == BAS_TYPE_STRING) {
|
|
// STRING slots must start as an empty string, not numeric 0.
|
|
// Arithmetic falls through basValToNumber so numeric defaults
|
|
// stay harmless, but STRING concat checks actual slot type.
|
|
uint16_t idx = basAddConstant(&p->cg, "", 0);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, idx);
|
|
emitStore(p, sym);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseDimBounds(BasParserT *p, int32_t *outDims) {
|
|
// Parse each dimension bound, pushing (lbound, ubound) pairs onto the stack.
|
|
// Supports both "ubound" (lbound=optionBase) and "lbound TO ubound" syntax.
|
|
*outDims = 0;
|
|
|
|
for (;;) {
|
|
// Save code position before parsing the first expression
|
|
int32_t exprStart = basCodePos(&p->cg);
|
|
parseExpression(p);
|
|
|
|
if (match(p, TOK_TO)) {
|
|
// "lbound TO ubound" -- first expr is lbound, parse ubound next
|
|
parseExpression(p);
|
|
} else {
|
|
// Single value = ubound, lbound defaults to optionBase.
|
|
// Ubound expression already emitted. Insert PUSH_INT16 before it.
|
|
int16_t lbound = (int16_t)p->optionBase;
|
|
int32_t exprLen = basCodePos(&p->cg) - exprStart;
|
|
int32_t insertLen = (int32_t)(1 + sizeof(lbound)); // opcode byte + int16 operand
|
|
|
|
{
|
|
// Grow the array to make room for the insertion
|
|
for (int32_t pad = 0; pad < insertLen; pad++) {
|
|
arrput(p->cg.code, 0);
|
|
}
|
|
|
|
memmove(&p->cg.code[exprStart + insertLen], &p->cg.code[exprStart], exprLen);
|
|
p->cg.code[exprStart] = OP_PUSH_INT16;
|
|
memcpy(&p->cg.code[exprStart + 1], &lbound, sizeof(lbound));
|
|
p->cg.codeLen = (int32_t)arrlen(p->cg.code);
|
|
|
|
// The ubound expression was just moved forward by insertLen.
|
|
// Any forward-CALL or label-ref patch address that landed
|
|
// inside it now points at stale bytes; shift them so they
|
|
// patch the relocated operand instead of corrupting code.
|
|
shiftBackpatchAddrs(p, exprStart, insertLen);
|
|
}
|
|
}
|
|
|
|
(*outDims)++;
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseDo(BasParserT *p) {
|
|
// DO [WHILE|UNTIL cond]
|
|
// ...
|
|
// LOOP [WHILE|UNTIL cond]
|
|
advance(p); // consume DO
|
|
|
|
LoopSaveT save;
|
|
loopBegin(p, false, &save);
|
|
|
|
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) {
|
|
loopEnd(p, false, &save);
|
|
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 = relJumpOffset(p, loopTop, basCodePos(&p->cg));
|
|
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 = relJumpOffset(p, loopTop, basCodePos(&p->cg));
|
|
basEmit16(&p->cg, backOffset);
|
|
} else {
|
|
// Plain LOOP -- unconditional jump back
|
|
basEmit8(&p->cg, OP_JMP);
|
|
int16_t backOffset = relJumpOffset(p, loopTop, basCodePos(&p->cg));
|
|
basEmit16(&p->cg, backOffset);
|
|
}
|
|
|
|
// Backpatch pre-condition jump (exits the loop)
|
|
if (hasPreCondition) {
|
|
patchJump(p, preCondJump);
|
|
}
|
|
|
|
// Patch all EXIT DO jumps to here
|
|
loopEnd(p, false, &save);
|
|
}
|
|
|
|
|
|
static void parseEnd(BasParserT *p) {
|
|
// END -- by itself = terminate program
|
|
// END IF / END SUB / END FUNCTION / END SELECT are handled by their parsers
|
|
advance(p); // consume END
|
|
basEmit8(&p->cg, OP_END);
|
|
}
|
|
|
|
|
|
static void parseEndForm(BasParserT *p) {
|
|
advance(p); // consume ENDFORM
|
|
|
|
if (!p->sym.inFormScope) {
|
|
error(p, "ENDFORM without BEGINFORM");
|
|
return;
|
|
}
|
|
|
|
// Capture form name before leaving scope
|
|
char formName[BAS_MAX_IDENT];
|
|
strncpy(formName, p->sym.formScopeName, sizeof(formName) - 1);
|
|
formName[sizeof(formName) - 1] = '\0';
|
|
|
|
int32_t varCount = basSymTabLeaveFormScope(&p->sym);
|
|
|
|
// Close the form init block: add OP_RET and patch the JMP
|
|
basEmit8(&p->cg, OP_RET);
|
|
int32_t initAddr = p->formInitCodeStart;
|
|
int32_t initLen = basCodePos(&p->cg) - p->formInitCodeStart;
|
|
|
|
// Patch the JMP to skip over the entire init block
|
|
int16_t offset = relJumpOffset(p, basCodePos(&p->cg), p->formInitJmpAddr);
|
|
basPatch16(&p->cg, p->formInitJmpAddr, offset);
|
|
|
|
p->formInitJmpAddr = -1;
|
|
p->formInitCodeStart = -1;
|
|
|
|
// Record form variable info (init code always exists once BEGINFORM ran,
|
|
// so initAddr >= 0 here -- the entry is recorded unconditionally).
|
|
BasFormVarInfoT info;
|
|
memset(&info, 0, sizeof(info));
|
|
snprintf(info.formName, sizeof(info.formName), "%s", formName);
|
|
info.varCount = varCount;
|
|
info.initCodeAddr = initAddr;
|
|
info.initCodeLen = initLen;
|
|
arrput(p->cg.formVarInfo, info);
|
|
p->cg.formVarInfoCount = (int32_t)arrlen(p->cg.formVarInfo);
|
|
}
|
|
|
|
|
|
static void parseEqvExpr(BasParserT *p) {
|
|
parseOrExpr(p);
|
|
while (!p->hasError && check(p, TOK_EQV)) {
|
|
advance(p);
|
|
parseOrExpr(p);
|
|
basEmit8(&p->cg, OP_EQV);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseErase(BasParserT *p) {
|
|
// ERASE arrayVar
|
|
advance(p); // consume ERASE
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "array variable name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
|
if (sym == NULL || !sym->isArray) {
|
|
error(p, "ERASE requires an array variable");
|
|
return;
|
|
}
|
|
|
|
emitLoad(p, sym);
|
|
basEmit8(&p->cg, OP_ERASE);
|
|
emitStore(p, sym);
|
|
}
|
|
|
|
|
|
static void parseExit(BasParserT *p) {
|
|
advance(p); // consume EXIT
|
|
|
|
// Each form is only legal inside its construct; the depth counters
|
|
// and currentProc flags are reset per procedure (procBegin), so an
|
|
// EXIT can never be patched against a loop or procedure of the caller.
|
|
if (check(p, TOK_FOR)) {
|
|
advance(p);
|
|
if (p->forDepth == 0) {
|
|
error(p, "EXIT FOR outside FOR loop");
|
|
return;
|
|
}
|
|
// Discard the test values of any SELECT CASE bodies this EXIT
|
|
// jumps out of, then drop the VM for-frame and jump.
|
|
emitSelectPops(p, p->selectDepth - p->forSelectBase);
|
|
basEmit8(&p->cg, OP_FOR_POP);
|
|
int32_t addr = emitJump(p, OP_JMP);
|
|
exitListAdd(&p->exitForList, addr);
|
|
} else if (check(p, TOK_DO)) {
|
|
advance(p);
|
|
if (p->doDepth == 0) {
|
|
error(p, "EXIT DO outside DO or WHILE loop");
|
|
return;
|
|
}
|
|
emitSelectPops(p, p->selectDepth - p->doSelectBase);
|
|
int32_t addr = emitJump(p, OP_JMP);
|
|
exitListAdd(&p->exitDoList, addr);
|
|
} else if (check(p, TOK_SUB)) {
|
|
advance(p);
|
|
if (!p->sym.inLocalScope || p->currentProcIsFunction) {
|
|
error(p, "EXIT SUB outside SUB");
|
|
return;
|
|
}
|
|
// Leaving the procedure entirely: pop every open SELECT value.
|
|
emitSelectPops(p, p->selectDepth);
|
|
int32_t addr = emitJump(p, OP_JMP);
|
|
exitListAdd(&p->exitSubList, addr);
|
|
} else if (check(p, TOK_FUNCTION)) {
|
|
advance(p);
|
|
if (!p->sym.inLocalScope || !p->currentProcIsFunction) {
|
|
error(p, "EXIT FUNCTION outside FUNCTION");
|
|
return;
|
|
}
|
|
emitSelectPops(p, p->selectDepth);
|
|
int32_t addr = emitJump(p, OP_JMP);
|
|
exitListAdd(&p->exitFuncList, addr);
|
|
} else {
|
|
error(p, "Expected FOR, DO, SUB, or FUNCTION after EXIT");
|
|
}
|
|
}
|
|
|
|
|
|
static void parseExpression(BasParserT *p) {
|
|
if (!exprEnter(p)) {
|
|
return;
|
|
}
|
|
parseImpExpr(p);
|
|
exprLeave(p);
|
|
}
|
|
|
|
|
|
static void parseFileCopy(BasParserT *p) {
|
|
// FILECOPY source, dest
|
|
advance(p);
|
|
parseExpression(p);
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_FS_FILECOPY);
|
|
}
|
|
|
|
|
|
static void parseFor(BasParserT *p) {
|
|
// FOR var = start TO limit [STEP step]
|
|
// ...
|
|
// NEXT [var]
|
|
advance(p); // consume FOR
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Open the loop (fresh EXIT FOR list, SELECT depth for EXIT to unwind
|
|
// to) only once the header is known to be well-formed.
|
|
LoopSaveT save;
|
|
loopBegin(p, true, &save);
|
|
|
|
// = start
|
|
expect(p, TOK_EQ);
|
|
parseExpression(p);
|
|
emitStore(p, loopVar);
|
|
|
|
// TO limit
|
|
expect(p, TOK_TO);
|
|
parseExpression(p); // limit is on stack
|
|
|
|
// STEP step (optional, default 1)
|
|
if (match(p, TOK_STEP)) {
|
|
parseExpression(p); // step is on stack
|
|
} else {
|
|
// Default step = 1
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 1);
|
|
}
|
|
|
|
// Emit FOR_INIT -- sets up the for-loop state in the VM. The
|
|
// trailing int16 is a forward offset the VM uses to skip the body
|
|
// when the loop's range is already empty at entry (e.g. FOR i = 10
|
|
// TO 5). We patch it after the body is emitted.
|
|
basEmit8(&p->cg, OP_FOR_INIT);
|
|
basEmitU16(&p->cg, (uint16_t)loopVar->index);
|
|
basEmit8(&p->cg, (uint8_t)loopVar->scope);
|
|
int32_t skipOffsetPos = basCodePos(&p->cg);
|
|
basEmit16(&p->cg, 0); // placeholder
|
|
|
|
int32_t loopBody = basCodePos(&p->cg);
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
// Loop body
|
|
while (!p->hasError && !check(p, TOK_NEXT) && !check(p, TOK_EOF)) {
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
if (p->hasError) {
|
|
loopEnd(p, true, &save);
|
|
return;
|
|
}
|
|
|
|
expect(p, TOK_NEXT);
|
|
|
|
// Optional variable name after NEXT must name this loop's variable
|
|
if (check(p, TOK_IDENT)) {
|
|
if (strcasecmp(p->lex.token.text, loopVar->name) != 0) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "NEXT %s does not match FOR %s", p->lex.token.text, loopVar->name);
|
|
error(p, buf);
|
|
loopEnd(p, true, &save);
|
|
return;
|
|
}
|
|
advance(p);
|
|
}
|
|
|
|
// Emit FOR_NEXT with backward jump to loop body
|
|
basEmit8(&p->cg, OP_FOR_NEXT);
|
|
basEmitU16(&p->cg, (uint16_t)loopVar->index);
|
|
basEmit8(&p->cg, (uint8_t)loopVar->scope);
|
|
int16_t backOffset = relJumpOffset(p, loopBody, basCodePos(&p->cg));
|
|
basEmit16(&p->cg, backOffset);
|
|
|
|
// Patch FOR_INIT's forward skip offset to point past FOR_NEXT.
|
|
patchJump(p, skipOffsetPos);
|
|
|
|
// Patch all EXIT FOR jumps to here
|
|
loopEnd(p, true, &save);
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
if (p->sym.inLocalScope) {
|
|
error(p, "FUNCTION cannot be defined inside SUB or FUNCTION");
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
|
|
ProcSaveT save;
|
|
procBegin(p, &save, true);
|
|
|
|
// Allocate slot 0 for return value
|
|
basSymTabAllocSlot(&p->sym);
|
|
|
|
ParamListT pl;
|
|
|
|
if (!parseParamList(p, &pl, PARAM_REGISTER)) {
|
|
procEnd(p, &save);
|
|
return;
|
|
}
|
|
|
|
// 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).
|
|
// The prescan / a DECLARE / a call-site stub may already have added
|
|
// it; a symbol that is already defined is a duplicate body.
|
|
BasSymbolT *existing = basSymTabFindGlobal(&p->sym, name);
|
|
BasSymbolT *funcSym = NULL;
|
|
|
|
if (existing != NULL && existing->kind == SYM_FUNCTION) {
|
|
if (existing->isDefined) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Function '%s' is already defined", existing->name);
|
|
error(p, buf);
|
|
procEnd(p, &save);
|
|
return;
|
|
}
|
|
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");
|
|
procEnd(p, &save);
|
|
return;
|
|
}
|
|
|
|
funcSym->codeAddr = funcAddr;
|
|
funcSym->isDefined = true;
|
|
funcSym->scope = SCOPE_GLOBAL;
|
|
funcSym->dataType = returnType;
|
|
applyParamList(funcSym, &pl);
|
|
|
|
// Record the owning form -- see parseSub for the rationale.
|
|
if (p->sym.inFormScope && p->sym.formScopeName[0]) {
|
|
strncpy(funcSym->formName, p->sym.formScopeName, BAS_MAX_IDENT - 1);
|
|
funcSym->formName[BAS_MAX_IDENT - 1] = '\0';
|
|
}
|
|
|
|
// Backpatch any forward-reference calls to this function
|
|
patchCallAddrs(p, funcSym);
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
// Parse function body
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
// Check for END FUNCTION
|
|
if (check(p, TOK_END)) {
|
|
// Peek ahead -- we need to see if it's END FUNCTION
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_FUNCTION)) {
|
|
advance(p);
|
|
break;
|
|
}
|
|
// Not END FUNCTION, restore and parse as statement
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
// Patch EXIT FUNCTION jumps, emit the epilogue and prologue, and
|
|
// restore the enclosing state
|
|
procEnd(p, &save);
|
|
|
|
// Leave local scope
|
|
collectDebugLocals(p, p->cg.debugProcCount++);
|
|
basSymTabLeaveLocal(&p->sym);
|
|
|
|
// Patch the skip jump
|
|
patchJump(p, skipJump);
|
|
}
|
|
|
|
|
|
static void parseGet(BasParserT *p) {
|
|
// GET #channel, [recno], var
|
|
advance(p); // consume GET
|
|
|
|
match(p, TOK_HASH); // optional #
|
|
|
|
// Channel number
|
|
parseExpression(p);
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Optional record number
|
|
if (check(p, TOK_COMMA)) {
|
|
// No record number specified -- push 0 (current position)
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0);
|
|
} else {
|
|
parseExpression(p);
|
|
}
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Target variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *sym = ensureVariable(p, varName);
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
|
|
// A STRING target reads as many bytes as it already holds in BINARY
|
|
// mode, so its current value goes on the stack ahead of the type.
|
|
if (sym->dataType == BAS_TYPE_STRING) {
|
|
emitLoad(p, sym);
|
|
}
|
|
|
|
// Push variable type so VM knows how many bytes to read
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)sym->dataType);
|
|
|
|
basEmit8(&p->cg, OP_FILE_GET);
|
|
|
|
emitStore(p, sym);
|
|
}
|
|
|
|
|
|
static void parseGosub(BasParserT *p) {
|
|
// GOSUB label -- push return PC, then JMP to label
|
|
advance(p); // consume GOSUB
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "label name");
|
|
return;
|
|
}
|
|
|
|
char labelName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
labelName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Push the return PC (address after the JMP instruction)
|
|
// OP_PUSH_INT32 = 1 + 4 bytes, OP_JMP = 1 + 2 bytes
|
|
int32_t pushPos = basCodePos(&p->cg);
|
|
basEmit8(&p->cg, OP_PUSH_INT32);
|
|
basEmit16(&p->cg, 0); // placeholder lo
|
|
basEmit16(&p->cg, 0); // placeholder hi
|
|
|
|
// Emit the jump to the label
|
|
emitJumpToLabel(p, OP_JMP, labelName);
|
|
|
|
// Backpatch the return address (PC is now right after the JMP)
|
|
int32_t returnPc = basCodePos(&p->cg);
|
|
int16_t lo = (int16_t)(returnPc & 0xFFFF);
|
|
int16_t hi = (int16_t)((returnPc >> 16) & 0xFFFF);
|
|
basPatch16(&p->cg, pushPos + 1, lo);
|
|
basPatch16(&p->cg, pushPos + 3, hi);
|
|
}
|
|
|
|
|
|
static void parseGoto(BasParserT *p) {
|
|
// GOTO label
|
|
advance(p); // consume GOTO
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "label name");
|
|
return;
|
|
}
|
|
|
|
char labelName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
labelName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
emitGotoWithSelectPops(p, labelName);
|
|
}
|
|
|
|
|
|
static void parseIdivExpr(BasParserT *p) {
|
|
parseMulDivExpr(p);
|
|
while (!p->hasError && check(p, TOK_BACKSLASH)) {
|
|
advance(p);
|
|
parseMulDivExpr(p);
|
|
basEmit8(&p->cg, OP_IDIV_INT);
|
|
}
|
|
}
|
|
|
|
|
|
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 [: stmt]...
|
|
if (!check(p, TOK_NEWLINE) && !check(p, TOK_EOF)) {
|
|
// Single-line IF. QuickBASIC semantics: ALL colon-separated
|
|
// statements after THEN (and after ELSE) belong to that branch.
|
|
// parseStatement consumes a trailing colon, so loop until the
|
|
// branch terminator (ELSE/NEWLINE/EOF) is reached.
|
|
int32_t falseJump = emitJump(p, OP_JMP_FALSE);
|
|
|
|
while (!p->hasError && !check(p, TOK_ELSE) && !check(p, TOK_NEWLINE) && !check(p, TOK_EOF)) {
|
|
parseStatement(p);
|
|
}
|
|
|
|
if (check(p, TOK_ELSE)) {
|
|
advance(p);
|
|
int32_t endJump = emitJump(p, OP_JMP);
|
|
patchJump(p, falseJump);
|
|
|
|
while (!p->hasError && !check(p, TOK_NEWLINE) && !check(p, TOK_EOF)) {
|
|
parseStatement(p);
|
|
}
|
|
|
|
patchJump(p, endJump);
|
|
} else {
|
|
patchJump(p, falseJump);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Multi-line IF
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
int32_t falseJump = emitJump(p, OP_JMP_FALSE);
|
|
|
|
// Collect end-of-chain jumps for backpatching (stb_ds dynamic array)
|
|
int32_t *endJumps = NULL;
|
|
|
|
// Parse THEN block
|
|
while (!p->hasError && !check(p, TOK_ELSEIF) && !check(p, TOK_ELSE) && !check(p, TOK_EOF)) {
|
|
// Check for END IF
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_IF)) {
|
|
advance(p);
|
|
patchJump(p, falseJump);
|
|
// Patch all end jumps
|
|
for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
arrfree(endJumps);
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
// ELSEIF chain
|
|
while (!p->hasError && check(p, TOK_ELSEIF)) {
|
|
// Jump from previous true-block to end of chain
|
|
arrput(endJumps, emitJump(p, OP_JMP));
|
|
|
|
// Patch the previous false jump to here
|
|
patchJump(p, falseJump);
|
|
|
|
advance(p); // consume ELSEIF
|
|
parseExpression(p);
|
|
expect(p, TOK_THEN);
|
|
|
|
falseJump = emitJump(p, OP_JMP_FALSE);
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
while (!p->hasError && !check(p, TOK_ELSEIF) && !check(p, TOK_ELSE) && !check(p, TOK_EOF)) {
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_IF)) {
|
|
advance(p);
|
|
patchJump(p, falseJump);
|
|
for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
arrfree(endJumps);
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
}
|
|
|
|
// ELSE block
|
|
if (!p->hasError && check(p, TOK_ELSE)) {
|
|
arrput(endJumps, emitJump(p, OP_JMP));
|
|
patchJump(p, falseJump);
|
|
falseJump = -1; // no more false jump needed
|
|
|
|
advance(p); // consume ELSE
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_IF)) {
|
|
advance(p);
|
|
for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
arrfree(endJumps);
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
}
|
|
|
|
// Patch the last false jump if no ELSE block
|
|
if (falseJump >= 0) {
|
|
patchJump(p, falseJump);
|
|
}
|
|
|
|
// Patch all end-of-chain jumps
|
|
for (int32_t i = 0; i < (int32_t)arrlen(endJumps); i++) {
|
|
patchJump(p, endJumps[i]);
|
|
}
|
|
|
|
arrfree(endJumps);
|
|
|
|
// If we got here without END IF, that's an error
|
|
if (!p->hasError) {
|
|
error(p, "Expected END IF");
|
|
}
|
|
}
|
|
|
|
|
|
static void parseImpExpr(BasParserT *p) {
|
|
parseEqvExpr(p);
|
|
while (!p->hasError && check(p, TOK_IMP)) {
|
|
advance(p);
|
|
parseEqvExpr(p);
|
|
basEmit8(&p->cg, OP_IMP);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseInput(BasParserT *p) {
|
|
// INPUT #channel, var
|
|
// INPUT [prompt;] var
|
|
advance(p); // consume INPUT
|
|
|
|
// Check for file I/O: INPUT #channel, var
|
|
if (check(p, TOK_HASH)) {
|
|
advance(p); // consume #
|
|
|
|
// Channel number
|
|
parseExpression(p);
|
|
|
|
// Comma separator
|
|
expect(p, TOK_COMMA);
|
|
|
|
// One OP_FILE_INPUT per comma-separated target. Each consumes
|
|
// the channel, so it is duplicated ahead of every field but the
|
|
// last.
|
|
for (;;) {
|
|
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);
|
|
|
|
bool more = check(p, TOK_COMMA);
|
|
|
|
if (more) {
|
|
basEmit8(&p->cg, OP_DUP);
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_FILE_INPUT);
|
|
|
|
BasSymbolT *sym = ensureVariable(p, varName);
|
|
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
|
|
emitInputConv(p, sym);
|
|
emitStore(p, sym);
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
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;
|
|
}
|
|
|
|
emitInputConv(p, sym);
|
|
emitStore(p, sym);
|
|
}
|
|
|
|
|
|
static void parseKill(BasParserT *p) {
|
|
// KILL filename
|
|
advance(p);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_FS_KILL);
|
|
}
|
|
|
|
|
|
static void parseLineInput(BasParserT *p) {
|
|
// LINE INPUT #channel, var
|
|
advance(p); // consume LINE
|
|
|
|
if (!check(p, TOK_INPUT)) {
|
|
error(p, "Expected INPUT after LINE");
|
|
return;
|
|
}
|
|
|
|
advance(p); // consume INPUT
|
|
|
|
// Must have # for file I/O
|
|
if (!match(p, TOK_HASH)) {
|
|
error(p, "Expected # for file channel in LINE INPUT");
|
|
return;
|
|
}
|
|
|
|
// Channel expression
|
|
parseExpression(p);
|
|
|
|
// Comma separator
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Target variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
basEmit8(&p->cg, OP_FILE_LINE_INPUT);
|
|
|
|
BasSymbolT *sym = ensureVariable(p, varName);
|
|
|
|
if (sym != NULL) {
|
|
emitStore(p, sym);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseMkDir(BasParserT *p) {
|
|
// MKDIR path
|
|
advance(p);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_FS_MKDIR);
|
|
}
|
|
|
|
|
|
static void parseModule(BasParserT *p) {
|
|
// VB semantics: all SUB/FUNCTION declarations are visible from
|
|
// anywhere in the module regardless of source order. Do a
|
|
// pre-scan that walks the token stream, extracts every
|
|
// SUB/FUNCTION signature, and registers it in the symbol table.
|
|
// Call sites encountered later (including module-level code
|
|
// that precedes the SUB definition in source) can then validate
|
|
// argument count / types against the real signature instead of
|
|
// falling back to a paramCount=0 placeholder.
|
|
prescanSignatures(p);
|
|
|
|
skipNewlines(p);
|
|
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
// Check for unresolved forward references (skip externs from DECLARE LIBRARY)
|
|
if (!p->hasError) {
|
|
for (int32_t i = 0; i < p->sym.count; i++) {
|
|
BasSymbolT *sym = p->sym.symbols[i];
|
|
|
|
if ((sym->kind == SYM_SUB || sym->kind == SYM_FUNCTION) && !sym->isDefined && !sym->isExtern) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Undefined %s: %s",
|
|
sym->kind == SYM_SUB ? "Sub" : "Function", sym->name);
|
|
error(p, buf);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// End of module -- emit HALT
|
|
basEmit8(&p->cg, OP_HALT);
|
|
}
|
|
|
|
|
|
// QuickBASIC precedence (high to low): ^, unary -, (* /), \, MOD, +, -.
|
|
// The multiplicative operators occupy THREE distinct levels, so the chain is
|
|
// parseAddExpr -> parseMulExpr (MOD) -> parseIdivExpr (\) ->
|
|
// parseMulDivExpr (* /) -> parseUnaryExpr -> parsePowExpr -> parsePrimary.
|
|
// This makes 8 MOD 3 * 2 == 2 and 10 \ 2 * 3 == 1 (matching QB), and keeps
|
|
// -2^2 = -(2^2) = -4.
|
|
|
|
static void parseMulDivExpr(BasParserT *p) {
|
|
parseUnaryExpr(p);
|
|
while (!p->hasError) {
|
|
if (check(p, TOK_STAR)) {
|
|
advance(p);
|
|
parseUnaryExpr(p);
|
|
basEmit8(&p->cg, OP_MUL_INT);
|
|
} else if (check(p, TOK_SLASH)) {
|
|
advance(p);
|
|
parseUnaryExpr(p);
|
|
basEmit8(&p->cg, OP_DIV_FLT);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseMulExpr(BasParserT *p) {
|
|
parseIdivExpr(p);
|
|
while (!p->hasError && check(p, TOK_MOD)) {
|
|
advance(p);
|
|
parseIdivExpr(p);
|
|
basEmit8(&p->cg, OP_MOD_INT);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseName(BasParserT *p) {
|
|
// NAME oldname AS newname
|
|
advance(p);
|
|
parseExpression(p);
|
|
expect(p, TOK_AS);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_FS_NAME);
|
|
}
|
|
|
|
|
|
static void parseNotExpr(BasParserT *p) {
|
|
if (check(p, TOK_NOT)) {
|
|
advance(p);
|
|
if (!exprEnter(p)) {
|
|
return;
|
|
}
|
|
parseNotExpr(p);
|
|
exprLeave(p);
|
|
basEmit8(&p->cg, OP_NOT);
|
|
return;
|
|
}
|
|
parseCompareExpr(p);
|
|
}
|
|
|
|
|
|
static void parseOn(BasParserT *p) {
|
|
// ON ERROR GOTO label -- error handler
|
|
// ON expr GOTO label1, label2, ... -- computed goto
|
|
// ON expr GOSUB label1, label2, ... -- computed gosub
|
|
advance(p); // consume ON
|
|
|
|
// ON ERROR GOTO is a special form
|
|
if (check(p, TOK_ERROR_KW)) {
|
|
parseOnError(p);
|
|
return;
|
|
}
|
|
|
|
// ON expr GOTO/GOSUB label1, label2, ...
|
|
parseExpression(p);
|
|
|
|
bool isGosub;
|
|
|
|
if (check(p, TOK_GOTO)) {
|
|
isGosub = false;
|
|
advance(p);
|
|
} else if (check(p, TOK_GOSUB)) {
|
|
isGosub = true;
|
|
advance(p);
|
|
} else {
|
|
error(p, "Expected GOTO or GOSUB after ON expression");
|
|
return;
|
|
}
|
|
|
|
// Track end-of-gosub jumps for patching (stb_ds dynamic array)
|
|
int32_t *endJumps = NULL;
|
|
int32_t labelIdx = 1;
|
|
|
|
for (;;) {
|
|
if (p->hasError) {
|
|
arrfree(endJumps);
|
|
return;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "label name");
|
|
arrfree(endJumps);
|
|
return;
|
|
}
|
|
|
|
char labelName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
labelName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// DUP the selector
|
|
basEmit8(&p->cg, OP_DUP);
|
|
|
|
// PUSH the 1-based index
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)labelIdx);
|
|
|
|
// Compare
|
|
basEmit8(&p->cg, OP_CMP_EQ);
|
|
|
|
// JMP_FALSE to skip this branch
|
|
int32_t skipAddr = emitJump(p, OP_JMP_FALSE);
|
|
|
|
// Match: POP the selector value
|
|
basEmit8(&p->cg, OP_POP);
|
|
|
|
if (isGosub) {
|
|
// Push return PC before jumping
|
|
int32_t pushPos = basCodePos(&p->cg);
|
|
basEmit8(&p->cg, OP_PUSH_INT32);
|
|
basEmit16(&p->cg, 0); // placeholder lo
|
|
basEmit16(&p->cg, 0); // placeholder hi
|
|
|
|
emitJumpToLabel(p, OP_JMP, labelName);
|
|
|
|
// Backpatch the return address
|
|
int32_t returnPc = basCodePos(&p->cg);
|
|
int16_t lo = (int16_t)(returnPc & 0xFFFF);
|
|
int16_t hi = (int16_t)((returnPc >> 16) & 0xFFFF);
|
|
basPatch16(&p->cg, pushPos + 1, lo);
|
|
basPatch16(&p->cg, pushPos + 3, hi);
|
|
|
|
// After GOSUB returns, jump to end of ON...GOSUB
|
|
arrput(endJumps, emitJump(p, OP_JMP));
|
|
} else {
|
|
// GOTO: jump to the label, discarding the live test values
|
|
// of any SELECT CASE blocks being left (same as plain GOTO).
|
|
emitGotoWithSelectPops(p, labelName);
|
|
}
|
|
|
|
// Patch the skip (no-match continues to next branch)
|
|
patchJump(p, skipAddr);
|
|
|
|
labelIdx++;
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// No match: POP the selector and fall through
|
|
basEmit8(&p->cg, OP_POP);
|
|
|
|
// Patch all end-of-gosub jumps to here
|
|
int32_t endTarget = basCodePos(&p->cg);
|
|
int32_t n = (int32_t)arrlen(endJumps);
|
|
|
|
for (int32_t i = 0; i < n; i++) {
|
|
int16_t offset = relJumpOffset(p, endTarget, endJumps[i]);
|
|
basPatch16(&p->cg, endJumps[i], offset);
|
|
}
|
|
|
|
arrfree(endJumps);
|
|
}
|
|
|
|
|
|
static void parseOnError(BasParserT *p) {
|
|
// ON ERROR GOTO label
|
|
// ON ERROR GOTO 0 (disable)
|
|
// Note: ON and ERROR already consumed by parseOn dispatcher
|
|
advance(p); // consume ERROR
|
|
|
|
if (!check(p, TOK_GOTO)) {
|
|
error(p, "Expected GOTO after ON ERROR");
|
|
return;
|
|
}
|
|
advance(p); // consume GOTO
|
|
|
|
// ON ERROR GOTO 0 -- disable error handler
|
|
if (check(p, TOK_INT_LIT) && p->lex.token.intVal == 0) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_ON_ERROR);
|
|
basEmit16(&p->cg, 0);
|
|
return;
|
|
}
|
|
|
|
// ON ERROR GOTO label
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "label name or 0");
|
|
return;
|
|
}
|
|
|
|
char labelName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
labelName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Look up the label
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, labelName);
|
|
|
|
if (sym != NULL && sym->kind == SYM_LABEL && sym->isDefined) {
|
|
// Label already defined -- emit ON_ERROR with offset to handler
|
|
basEmit8(&p->cg, OP_ON_ERROR);
|
|
int32_t here = basCodePos(&p->cg);
|
|
int16_t offset = relJumpOffset(p, sym->codeAddr, here);
|
|
basEmit16(&p->cg, offset);
|
|
} else {
|
|
// Forward reference
|
|
if (sym == NULL) {
|
|
sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0);
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = false;
|
|
sym->codeAddr = 0;
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_ON_ERROR);
|
|
int32_t patchAddr = basCodePos(&p->cg);
|
|
basEmit16(&p->cg, 0);
|
|
|
|
arrput(sym->patchAddrs, patchAddr);
|
|
sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseOpen(BasParserT *p) {
|
|
// OPEN filename FOR mode AS #channel
|
|
advance(p); // consume OPEN
|
|
|
|
// Filename expression
|
|
parseExpression(p);
|
|
|
|
// FOR keyword
|
|
expect(p, TOK_FOR);
|
|
|
|
// Mode: INPUT, OUTPUT, APPEND
|
|
uint8_t mode;
|
|
|
|
if (check(p, TOK_INPUT)) {
|
|
mode = BAS_FILE_MODE_INPUT;
|
|
advance(p);
|
|
} else if (check(p, TOK_OUTPUT)) {
|
|
mode = BAS_FILE_MODE_OUTPUT;
|
|
advance(p);
|
|
} else if (check(p, TOK_APPEND)) {
|
|
mode = BAS_FILE_MODE_APPEND;
|
|
advance(p);
|
|
} else if (check(p, TOK_RANDOM)) {
|
|
mode = BAS_FILE_MODE_RANDOM;
|
|
advance(p);
|
|
} else if (check(p, TOK_BINARY)) {
|
|
mode = BAS_FILE_MODE_BINARY;
|
|
advance(p);
|
|
} else {
|
|
error(p, "Expected INPUT, OUTPUT, APPEND, RANDOM, or BINARY after FOR");
|
|
return;
|
|
}
|
|
|
|
// AS keyword
|
|
expect(p, TOK_AS);
|
|
|
|
// Optional # prefix
|
|
match(p, TOK_HASH);
|
|
|
|
// Channel number expression
|
|
parseExpression(p);
|
|
|
|
// Optional LEN = recordsize: the RANDOM-mode record length used by
|
|
// GET/PUT record numbers. Defaults when omitted.
|
|
if (checkKeyword(p, "LEN")) {
|
|
advance(p); // consume LEN
|
|
expect(p, TOK_EQ);
|
|
parseExpression(p);
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, BAS_VM_RANDOM_RECORD_SIZE);
|
|
}
|
|
|
|
// Emit: stack has [filename, channel, recLen] -- 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)) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_COMPARE_MODE);
|
|
basEmit8(&p->cg, BAS_COMPARE_MODE_BINARY);
|
|
} else if (checkKeyword(p, "TEXT")) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_COMPARE_MODE);
|
|
basEmit8(&p->cg, BAS_COMPARE_MODE_TEXT);
|
|
} else {
|
|
error(p, "Expected BINARY or TEXT after OPTION COMPARE");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (check(p, TOK_EXPLICIT)) {
|
|
advance(p);
|
|
p->optionExplicit = true;
|
|
return;
|
|
}
|
|
|
|
error(p, "Expected BASE, COMPARE, or EXPLICIT after OPTION");
|
|
}
|
|
|
|
|
|
static void parseOrExpr(BasParserT *p) {
|
|
parseXorExpr(p);
|
|
while (!p->hasError && check(p, TOK_OR)) {
|
|
advance(p);
|
|
parseXorExpr(p);
|
|
basEmit8(&p->cg, OP_OR);
|
|
}
|
|
}
|
|
|
|
|
|
// Parse an optional "(param, ...)" list for any SUB/FUNCTION
|
|
// declaration form. Each parameter is [OPTIONAL] [BYVAL] name [AS type].
|
|
// With PARAM_REGISTER the parameters are also added as SCOPE_LOCAL
|
|
// variables in slot order (the caller has already entered local scope).
|
|
// Returns false with an error set on any problem.
|
|
static bool parseParamList(BasParserT *p, ParamListT *pl, uint32_t flags) {
|
|
memset(pl, 0, sizeof(*pl));
|
|
|
|
if (!match(p, TOK_LPAREN)) {
|
|
return !p->hasError;
|
|
}
|
|
|
|
bool seenOptional = false;
|
|
|
|
while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) {
|
|
if (pl->count > 0) {
|
|
expect(p, TOK_COMMA);
|
|
}
|
|
|
|
bool optional = match(p, TOK_OPTIONAL);
|
|
|
|
if (optional) {
|
|
seenOptional = true;
|
|
} else if (seenOptional) {
|
|
error(p, "Required parameter cannot follow Optional parameter");
|
|
return false;
|
|
}
|
|
|
|
bool byVal = match(p, TOK_BYVAL) || (flags & PARAM_FORCE_BYVAL) != 0;
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "parameter name");
|
|
return false;
|
|
}
|
|
|
|
if (clampParamCount(p, pl->count)) {
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
bool pTyped = nameHasTypeSuffix(paramName);
|
|
int32_t pUdtTypeId = -1;
|
|
|
|
if (match(p, TOK_AS)) {
|
|
pdt = resolveParamType(p, (flags & PARAM_PRESCAN) != 0, &pUdtTypeId);
|
|
pTyped = true;
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return false;
|
|
}
|
|
|
|
if (flags & PARAM_REGISTER) {
|
|
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
|
|
|
|
if (paramSym == NULL) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Duplicate parameter name '%s'", paramName);
|
|
error(p, buf);
|
|
return false;
|
|
}
|
|
|
|
paramSym->scope = SCOPE_LOCAL;
|
|
paramSym->index = basSymTabAllocSlot(&p->sym);
|
|
paramSym->isDefined = true;
|
|
paramSym->udtTypeId = pUdtTypeId;
|
|
paramSym->isTyped = pTyped;
|
|
}
|
|
|
|
pl->types[pl->count] = pdt;
|
|
pl->byVal[pl->count] = byVal;
|
|
pl->optional[pl->count] = optional;
|
|
|
|
if (!optional) {
|
|
pl->required = pl->count + 1;
|
|
}
|
|
|
|
pl->count++;
|
|
}
|
|
|
|
expect(p, TOK_RPAREN);
|
|
return !p->hasError;
|
|
}
|
|
|
|
|
|
static void parsePowExpr(BasParserT *p) {
|
|
parsePrimary(p);
|
|
while (!p->hasError && check(p, TOK_CARET)) {
|
|
advance(p);
|
|
// The exponent operand may carry a leading sign (QB: 2 ^ -3 == 0.125).
|
|
// A leading sign on the BASE is handled higher up by parseUnaryExpr,
|
|
// so only the RHS needs parsePowOperand.
|
|
parsePowOperand(p);
|
|
basEmit8(&p->cg, OP_POW);
|
|
}
|
|
}
|
|
|
|
|
|
static void parsePowOperand(BasParserT *p) {
|
|
if (check(p, TOK_MINUS) || check(p, TOK_PLUS)) {
|
|
bool negate = check(p, TOK_MINUS);
|
|
advance(p); // unary plus is a no-op
|
|
if (!exprEnter(p)) {
|
|
return;
|
|
}
|
|
parsePowOperand(p);
|
|
exprLeave(p);
|
|
if (negate) {
|
|
basEmit8(&p->cg, OP_NEG_INT);
|
|
}
|
|
return;
|
|
}
|
|
parsePrimary(p);
|
|
}
|
|
|
|
|
|
static void parsePrimary(BasParserT *p) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
BasTokenTypeE tt = p->lex.token.type;
|
|
|
|
// App.Path / App.Config / App.Data
|
|
if (tt == TOK_APP) {
|
|
advance(p);
|
|
expect(p, TOK_DOT);
|
|
|
|
if (checkKeyword(p,"Path")) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_APP_PATH);
|
|
} else if (checkKeyword(p,"Config")) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_APP_CONFIG);
|
|
} else if (checkKeyword(p,"Data") || p->lex.token.type == TOK_DATA) {
|
|
// "Data" tokenizes as TOK_DATA (the DATA/READ keyword) rather
|
|
// than TOK_IDENT, so accept it directly by token type too.
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_APP_DATA);
|
|
} else {
|
|
error(p, "Expected 'Path', 'Config', or 'Data' after 'App.'");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Integer literal
|
|
if (tt == TOK_INT_LIT) {
|
|
int32_t val = p->lex.token.intVal;
|
|
if (val >= INT16_MIN && val <= INT16_MAX) {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)val);
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_INT32);
|
|
basEmit16(&p->cg, (int16_t)(val & 0xFFFF));
|
|
basEmit16(&p->cg, (int16_t)(((uint32_t)val >> 16) & 0xFFFF));
|
|
}
|
|
advance(p);
|
|
return;
|
|
}
|
|
|
|
// Long literal
|
|
if (tt == TOK_LONG_LIT) {
|
|
int32_t val = (int32_t)p->lex.token.longVal;
|
|
basEmit8(&p->cg, OP_PUSH_INT32);
|
|
basEmit16(&p->cg, (int16_t)(val & 0xFFFF));
|
|
basEmit16(&p->cg, (int16_t)((val >> 16) & 0xFFFF));
|
|
advance(p);
|
|
return;
|
|
}
|
|
|
|
// Float literal
|
|
if (tt == TOK_FLOAT_LIT) {
|
|
basEmit8(&p->cg, OP_PUSH_FLT64);
|
|
basEmitDouble(&p->cg, p->lex.token.dblVal);
|
|
advance(p);
|
|
return;
|
|
}
|
|
|
|
// String literal
|
|
if (tt == TOK_STRING_LIT) {
|
|
uint16_t idx = basAddConstant(&p->cg, p->lex.token.text, p->lex.token.textLen);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, idx);
|
|
advance(p);
|
|
return;
|
|
}
|
|
|
|
// Boolean literals
|
|
if (tt == TOK_TRUE_KW) {
|
|
basEmit8(&p->cg, OP_PUSH_TRUE);
|
|
advance(p);
|
|
return;
|
|
}
|
|
if (tt == TOK_FALSE_KW) {
|
|
basEmit8(&p->cg, OP_PUSH_FALSE);
|
|
advance(p);
|
|
return;
|
|
}
|
|
|
|
// Me -- reference to current form
|
|
if (tt == TOK_ME) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_ME_REF);
|
|
return;
|
|
}
|
|
|
|
// Nothing -- null object reference
|
|
if (tt == TOK_NOTHING) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0);
|
|
return;
|
|
}
|
|
|
|
// CreateForm(name$, width%, height%) -- create a form in code
|
|
if (tt == TOK_CREATEFORM) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p); // name
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // width
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // height
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_CREATE_FORM);
|
|
return;
|
|
}
|
|
|
|
// CreateControl(form, typeName$, ctrlName$ [, parent]) -- create a control
|
|
if (tt == TOK_CREATECONTROL) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p); // form ref
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // type name
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // control name
|
|
if (match(p, TOK_COMMA)) {
|
|
// Optional parent parameter
|
|
parseExpression(p); // parent ctrl ref
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_CREATE_CTRL_EX);
|
|
} else {
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_CREATE_CTRL);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// EOF(#channel) -- file end-of-file test
|
|
if (tt == TOK_EOF_KW) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
match(p, TOK_HASH); // optional #
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FILE_EOF);
|
|
return;
|
|
}
|
|
|
|
// SEEK(n) -- return current file position (function form)
|
|
if (tt == TOK_SEEK) {
|
|
advance(p);
|
|
if (check(p, TOK_LPAREN)) {
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FILE_LOC);
|
|
return;
|
|
}
|
|
// Not a function call -- error (SEEK as statement is handled elsewhere)
|
|
error(p, "SEEK requires parentheses when used as a function");
|
|
return;
|
|
}
|
|
|
|
// TIMER -- seconds since midnight (no args needed)
|
|
if (tt == TOK_TIMER) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_MATH_TIMER);
|
|
return;
|
|
}
|
|
|
|
// ERR -- current error number
|
|
if (tt == TOK_ERR) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_ERR_NUM);
|
|
return;
|
|
}
|
|
|
|
// CurDir$ -- current directory (no args)
|
|
if (tt == TOK_CURDIR) {
|
|
advance(p);
|
|
if (check(p, TOK_LPAREN)) {
|
|
expect(p, TOK_LPAREN);
|
|
expect(p, TOK_RPAREN);
|
|
}
|
|
basEmit8(&p->cg, OP_FS_CURDIR);
|
|
return;
|
|
}
|
|
|
|
// Dir$(pattern) or Dir$() for next match
|
|
if (tt == TOK_DIR) {
|
|
advance(p);
|
|
if (check(p, TOK_LPAREN)) {
|
|
expect(p, TOK_LPAREN);
|
|
if (check(p, TOK_RPAREN)) {
|
|
// Dir$() -- no args, get next match
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FS_DIR_NEXT);
|
|
} else {
|
|
// Dir$(pattern)
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FS_DIR);
|
|
}
|
|
} else {
|
|
// Dir with no parens -- next match
|
|
basEmit8(&p->cg, OP_FS_DIR_NEXT);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// FileLen(filename) -- file size without opening
|
|
if (tt == TOK_FILELEN) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FS_FILELEN);
|
|
return;
|
|
}
|
|
|
|
// GetAttr(filename) -- file attributes
|
|
if (tt == TOK_GETATTR) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FS_GETATTR);
|
|
return;
|
|
}
|
|
|
|
// InputBox$(prompt [, title [, default]])
|
|
if (tt == TOK_INPUTBOX) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p); // prompt
|
|
|
|
if (match(p, TOK_COMMA)) {
|
|
parseExpression(p); // title
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, basAddConstant(&p->cg, "", 0));
|
|
}
|
|
|
|
if (match(p, TOK_COMMA)) {
|
|
parseExpression(p); // default
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, basAddConstant(&p->cg, "", 0));
|
|
}
|
|
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_INPUTBOX);
|
|
return;
|
|
}
|
|
|
|
// MsgBox(message [, flags [, title]]) -- function form returning button ID
|
|
if (tt == TOK_MSGBOX) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p); // message
|
|
|
|
if (match(p, TOK_COMMA)) {
|
|
parseExpression(p); // flags
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0); // default flags = vbOKOnly
|
|
}
|
|
|
|
if (match(p, TOK_COMMA)) {
|
|
parseExpression(p); // title
|
|
} else {
|
|
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, emptyIdx);
|
|
}
|
|
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_MSGBOX);
|
|
return;
|
|
}
|
|
|
|
// IniRead$(file, section, key, default)
|
|
if (tt == TOK_INIREAD) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p); // file
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // section
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // key
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // default
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_INI_READ);
|
|
return;
|
|
}
|
|
|
|
// SHELL("command") -- as function expression
|
|
if (tt == TOK_SHELL) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_SHELL);
|
|
return;
|
|
}
|
|
|
|
// LBOUND(array [, dim])
|
|
if (tt == TOK_LBOUND) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
uint8_t dim = 1;
|
|
if (match(p, TOK_COMMA)) {
|
|
if (check(p, TOK_INT_LIT)) {
|
|
dim = (uint8_t)p->lex.token.intVal;
|
|
advance(p);
|
|
} else {
|
|
error(p, "LBOUND dimension must be a constant integer");
|
|
}
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_LBOUND);
|
|
basEmit8(&p->cg, dim);
|
|
return;
|
|
}
|
|
|
|
// UBOUND(array [, dim])
|
|
if (tt == TOK_UBOUND) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
uint8_t dim = 1;
|
|
if (match(p, TOK_COMMA)) {
|
|
if (check(p, TOK_INT_LIT)) {
|
|
dim = (uint8_t)p->lex.token.intVal;
|
|
advance(p);
|
|
} else {
|
|
error(p, "UBOUND dimension must be a constant integer");
|
|
}
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_UBOUND);
|
|
basEmit8(&p->cg, dim);
|
|
return;
|
|
}
|
|
|
|
// Parenthesized expression
|
|
if (tt == TOK_LPAREN) {
|
|
advance(p);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
return;
|
|
}
|
|
|
|
// Identifier: variable, function call, or built-in
|
|
if (tt == TOK_IDENT) {
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// INPUT$(n, #channel) -- special handling for optional # in second arg
|
|
if (strcasecmp(name, "INPUT$") == 0 && check(p, TOK_LPAREN)) {
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p); // n (number of chars)
|
|
expect(p, TOK_COMMA);
|
|
match(p, TOK_HASH); // optional #
|
|
parseExpression(p); // channel number
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FILE_INPUT_N);
|
|
return;
|
|
}
|
|
|
|
// Check for built-in function
|
|
const BuiltinFuncT *builtin = findBuiltin(name);
|
|
if (builtin != NULL) {
|
|
int32_t argc = 0;
|
|
|
|
// Zero-arg builtins can be used without parens
|
|
if (builtin->minArgs == 0 && builtin->maxArgs == 0 && !check(p, TOK_LPAREN)) {
|
|
basEmit8(&p->cg, builtin->opcode);
|
|
return;
|
|
}
|
|
|
|
if (check(p, TOK_LPAREN)) {
|
|
expect(p, TOK_LPAREN);
|
|
|
|
// RND/zero-arg builtins can be called with empty parens
|
|
if (!check(p, TOK_RPAREN)) {
|
|
parseExpression(p);
|
|
argc++;
|
|
while (match(p, TOK_COMMA)) {
|
|
parseExpression(p);
|
|
argc++;
|
|
}
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
if (argc < builtin->minArgs || argc > builtin->maxArgs) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Built-in '%s' expects %d-%d arguments, got %d", builtin->name, (int)builtin->minArgs, (int)builtin->maxArgs, (int)argc);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
|
|
// MID$ with 3 args uses a different opcode than 2 args
|
|
if (builtin->opcode == OP_STR_MID2 && argc == 3) {
|
|
basEmit8(&p->cg, OP_STR_MID);
|
|
} else if (builtin->opcode == OP_STR_INSTR && argc == 3) {
|
|
basEmit8(&p->cg, OP_STR_INSTR3);
|
|
} else if (builtin->opcode == OP_MATH_RND && argc == 0) {
|
|
// Push -1 as dummy arg for RND()
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, -1);
|
|
basEmit8(&p->cg, OP_MATH_RND);
|
|
} else {
|
|
basEmit8(&p->cg, builtin->opcode);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check symbol table for user-defined function or variable
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
|
|
|
// Function call with parens
|
|
if (check(p, TOK_LPAREN)) {
|
|
if (sym != NULL && (sym->kind == SYM_FUNCTION || sym->kind == SYM_SUB)) {
|
|
emitFunctionCall(p, sym);
|
|
return;
|
|
}
|
|
// Could be an array access -- treat as load + array index
|
|
if (sym != NULL && sym->isArray) {
|
|
emitLoad(p, sym);
|
|
expect(p, TOK_LPAREN);
|
|
int32_t dims = 0;
|
|
parseExpression(p);
|
|
dims++;
|
|
while (match(p, TOK_COMMA)) {
|
|
parseExpression(p);
|
|
dims++;
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_LOAD_ARRAY);
|
|
basEmit8(&p->cg, (uint8_t)dims);
|
|
|
|
// Array-of-UDT field access: arr(i).field
|
|
if (sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0 && check(p, TOK_DOT)) {
|
|
advance(p); // consume DOT
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "field name");
|
|
return;
|
|
}
|
|
BasSymbolT *typeSym = findTypeDefById(p, sym->udtTypeId);
|
|
if (typeSym == NULL) {
|
|
error(p, "Unknown TYPE definition");
|
|
return;
|
|
}
|
|
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
|
|
if (fieldIdx < 0) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
advance(p); // consume field name
|
|
basEmit8(&p->cg, OP_LOAD_FIELD);
|
|
basEmitU16(&p->cg, (uint16_t)fieldIdx);
|
|
}
|
|
return;
|
|
}
|
|
// Unknown identifier + '(' -- could be forward-ref function or
|
|
// control array access: Name(index).Property
|
|
if (sym == NULL) {
|
|
if (checkCtrlArrayAccess(p)) {
|
|
// Control array read: Name(idx).Property
|
|
expect(p, TOK_LPAREN);
|
|
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0); // NULL form ref = current form
|
|
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, ctrlNameIdx);
|
|
|
|
parseExpression(p); // index expression
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FIND_CTRL_IDX);
|
|
|
|
expect(p, TOK_DOT);
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "property name");
|
|
return;
|
|
}
|
|
char memberName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(memberName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, propNameIdx);
|
|
basEmit8(&p->cg, OP_LOAD_PROP);
|
|
return;
|
|
}
|
|
|
|
// Not a control array -- forward-ref function call
|
|
sym = basSymTabAdd(&p->sym, name, SYM_FUNCTION, suffixToType(name));
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
return;
|
|
}
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = false;
|
|
sym->codeAddr = 0;
|
|
}
|
|
emitFunctionCall(p, sym);
|
|
return;
|
|
}
|
|
|
|
// Check for dot access: UDT field or control property
|
|
if (check(p, TOK_DOT)) {
|
|
// If we already know this is a UDT variable, do field access
|
|
sym = basSymTabFind(&p->sym, name);
|
|
if (sym != NULL && sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) {
|
|
emitLoad(p, sym);
|
|
int32_t curTypeId = sym->udtTypeId;
|
|
|
|
// Loop to handle nested UDT field access: a.b.c
|
|
while (check(p, TOK_DOT) && curTypeId >= 0) {
|
|
advance(p); // consume DOT
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "field name");
|
|
return;
|
|
}
|
|
BasSymbolT *typeSym = findTypeDefById(p, curTypeId);
|
|
if (typeSym == NULL) {
|
|
error(p, "Unknown TYPE definition");
|
|
return;
|
|
}
|
|
int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text);
|
|
if (fieldIdx < 0) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
advance(p); // consume field name
|
|
basEmit8(&p->cg, OP_LOAD_FIELD);
|
|
basEmitU16(&p->cg, (uint16_t)fieldIdx);
|
|
|
|
// If this field is also a UDT, allow further dot access
|
|
if (typeSym->fields[fieldIdx].dataType == BAS_TYPE_UDT) {
|
|
curTypeId = typeSym->fields[fieldIdx].udtTypeId;
|
|
} else {
|
|
curTypeId = -1;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Not a UDT -- treat as control property/method: CtrlName.Member
|
|
advance(p); // consume DOT
|
|
if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
|
|
errorExpected(p, "property or method name");
|
|
return;
|
|
}
|
|
char memberName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(memberName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
bool isVarRef2 = (sym != NULL && sym->kind == SYM_VARIABLE);
|
|
|
|
// Compile-time validation for expression-context reads /
|
|
// method returns. Peek ahead: if '(' follows, memberName
|
|
// is a method; otherwise it's a property read. Skip when
|
|
// the host didn't attach a validator or the ctrl is dynamic.
|
|
if (!isVarRef2 && p->validator && p->validator->lookupCtrlType) {
|
|
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
|
|
|
|
if (wgtType) {
|
|
bool isMethodCall = check(p, TOK_LPAREN);
|
|
bool valid = true;
|
|
|
|
if (isMethodCall && p->validator->isMethodValid) {
|
|
valid = p->validator->isMethodValid(p->validator->ctx, wgtType, memberName);
|
|
} else if (!isMethodCall && p->validator->isPropValid) {
|
|
valid = p->validator->isPropValid(p->validator->ctx, wgtType, memberName);
|
|
}
|
|
|
|
if (!valid) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Unknown %s '%s.%s' (type '%s' has no such %s)",
|
|
isMethodCall ? "method" : "property",
|
|
name, memberName, wgtType,
|
|
isMethodCall ? "method" : "property");
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If `name` is a regular variable holding an object reference
|
|
// (form/control returned by CreateForm/CreateControl), use its
|
|
// value directly instead of treating `name` as a literal name.
|
|
if (isVarRef2) {
|
|
emitLoad(p, sym);
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0);
|
|
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, ctrlNameIdx);
|
|
basEmit8(&p->cg, OP_FIND_CTRL);
|
|
}
|
|
|
|
// If followed by '(', this is a method call with args
|
|
if (check(p, TOK_LPAREN)) {
|
|
advance(p); // consume '('
|
|
uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, methodNameIdx);
|
|
|
|
int32_t argc = 0;
|
|
if (!check(p, TOK_RPAREN)) {
|
|
parseExpression(p);
|
|
argc++;
|
|
while (match(p, TOK_COMMA)) {
|
|
parseExpression(p);
|
|
argc++;
|
|
}
|
|
}
|
|
expect(p, TOK_RPAREN);
|
|
if (clampArgCount(p, argc)) {
|
|
return;
|
|
}
|
|
basEmit8(&p->cg, OP_CALL_METHOD);
|
|
basEmit8(&p->cg, (uint8_t)argc);
|
|
} else {
|
|
// Property read
|
|
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, propNameIdx);
|
|
basEmit8(&p->cg, OP_LOAD_PROP);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// A FUNCTION named without an argument list is a call with no
|
|
// arguments (QB allows "x = f" for a parameterless FUNCTION).
|
|
if (sym != NULL && sym->kind == SYM_FUNCTION) {
|
|
emitCallWithArgs(p, sym, 0);
|
|
return;
|
|
}
|
|
|
|
// Plain variable reference
|
|
sym = ensureVariable(p, name);
|
|
if (sym != NULL) {
|
|
emitLoad(p, sym);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Nothing matched
|
|
errorExpected(p, "expression");
|
|
}
|
|
|
|
|
|
static void parsePrint(BasParserT *p) {
|
|
// PRINT [#channel, expr]
|
|
// PRINT [expr] [; expr] [, expr] [;]
|
|
// PRINT USING "fmt"; expr [; expr] ...
|
|
advance(p); // consume PRINT
|
|
|
|
// File I/O: PRINT #channel, expr [; expr | , expr ]* [;]
|
|
//
|
|
// Channel is pushed once and DUP'd per value. `;` means no separator
|
|
// between values; `,` is treated the same (tab-zone separator not
|
|
// supported for file output). Trailing `;` suppresses the final newline.
|
|
if (check(p, TOK_HASH)) {
|
|
advance(p); // consume #
|
|
|
|
// Channel number -- stays on stack as "keep" for the whole statement.
|
|
parseExpression(p);
|
|
expect(p, TOK_COMMA);
|
|
|
|
bool trailingSep = false;
|
|
|
|
for (;;) {
|
|
// Bail on error: parseExpression leaves the token frozen, and
|
|
// a stuck ';'/',' would otherwise re-satisfy the separator
|
|
// check below and spin (advance() is a no-op under error).
|
|
if (p->hasError) {
|
|
break;
|
|
}
|
|
|
|
// Duplicate the channel for this OP_FILE_PRINT.
|
|
basEmit8(&p->cg, OP_DUP);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_FILE_PRINT);
|
|
|
|
if (check(p, TOK_SEMICOLON) || check(p, TOK_COMMA)) {
|
|
advance(p);
|
|
trailingSep = true;
|
|
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
break;
|
|
}
|
|
|
|
trailingSep = false;
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// No trailing ; or , -> write newline. Otherwise, just drop the
|
|
// kept channel value.
|
|
if (trailingSep) {
|
|
basEmit8(&p->cg, OP_POP);
|
|
} else {
|
|
basEmit8(&p->cg, OP_FILE_WRITE_NL);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Check for PRINT USING
|
|
if (checkKeyword(p, "USING")) {
|
|
advance(p); // consume USING
|
|
|
|
// Parse format string expression
|
|
parseExpression(p);
|
|
|
|
// Semicolon separates format from values
|
|
expect(p, TOK_SEMICOLON);
|
|
|
|
// Parse values, each one gets formatted with PRINT_USING
|
|
for (;;) {
|
|
// Bail on error: a stuck ';' would re-satisfy the separator
|
|
// check below and spin (advance() is a no-op under error).
|
|
if (p->hasError) {
|
|
break;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// The format string pushed at the top of this branch is re-pushed by
|
|
// each OP_PRINT_USING and left on the stack after the final OP_PRINT;
|
|
// pop it so PRINT USING doesn't leak one eval slot per execution.
|
|
basEmit8(&p->cg, OP_POP);
|
|
basEmit8(&p->cg, OP_PRINT_NL);
|
|
return;
|
|
}
|
|
|
|
bool trailingSemicolon = false;
|
|
|
|
// Empty PRINT = just newline
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
basEmit8(&p->cg, OP_PRINT_NL);
|
|
return;
|
|
}
|
|
|
|
while (!p->hasError) {
|
|
trailingSemicolon = false;
|
|
|
|
if (check(p, TOK_SEMICOLON)) {
|
|
// Just a semicolon -- no space
|
|
trailingSemicolon = true;
|
|
advance(p);
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (check(p, TOK_COMMA)) {
|
|
// Comma -- print tab
|
|
basEmit8(&p->cg, OP_PRINT_TAB);
|
|
advance(p);
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
trailingSemicolon = true; // comma at end suppresses newline too
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
break;
|
|
}
|
|
|
|
// Check for SPC(n) and TAB(n) inside PRINT
|
|
if (checkKeyword(p, "SPC")) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_PRINT_SPC_N);
|
|
continue;
|
|
}
|
|
|
|
if (checkKeyword(p, "TAB")) {
|
|
advance(p);
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_PRINT_TAB_N);
|
|
continue;
|
|
}
|
|
|
|
// Expression
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_PRINT);
|
|
}
|
|
|
|
// Print newline unless suppressed by trailing semicolon/comma
|
|
if (!trailingSemicolon) {
|
|
basEmit8(&p->cg, OP_PRINT_NL);
|
|
}
|
|
}
|
|
|
|
|
|
static void parsePut(BasParserT *p) {
|
|
// PUT #channel, [recno], var
|
|
advance(p); // consume PUT
|
|
|
|
match(p, TOK_HASH); // optional #
|
|
|
|
// Channel number
|
|
parseExpression(p);
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Optional record number
|
|
if (check(p, TOK_COMMA)) {
|
|
// No record number specified -- push 0 (current position)
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0);
|
|
} else {
|
|
parseExpression(p);
|
|
}
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Value expression
|
|
parseExpression(p);
|
|
|
|
basEmit8(&p->cg, OP_FILE_PUT);
|
|
}
|
|
|
|
|
|
static void parseRead(BasParserT *p) {
|
|
// READ var1, var2, ...
|
|
advance(p); // consume READ
|
|
|
|
for (;;) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *sym = ensureVariable(p, name);
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_READ_DATA);
|
|
emitStore(p, sym);
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void parseRedim(BasParserT *p) {
|
|
// REDIM [PRESERVE] var(bounds) [AS type]
|
|
advance(p); // consume REDIM
|
|
|
|
uint8_t preserve = 0;
|
|
if (check(p, TOK_PRESERVE)) {
|
|
preserve = 1;
|
|
advance(p);
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "array variable name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// ensureVariable finds an existing symbol or auto-declares one; a
|
|
// SUB/FUNCTION/label/CONST of that name is not a REDIM target.
|
|
BasSymbolT *sym = ensureVariable(p, name);
|
|
if (sym == NULL) {
|
|
return;
|
|
}
|
|
if (sym->kind != SYM_VARIABLE) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "'%s' is not a variable", name);
|
|
error(p, buf);
|
|
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);
|
|
|
|
// Element type: the AS clause when given, else the variable's own
|
|
// declared type (a first REDIM adopts the AS type so later element
|
|
// and field accesses resolve against it).
|
|
uint8_t dt = sym->dataType;
|
|
int32_t udtTypeId = sym->udtTypeId;
|
|
if (match(p, TOK_AS)) {
|
|
dt = resolveTypeName(p);
|
|
udtTypeId = (dt == BAS_TYPE_UDT) ? p->lastUdtTypeId : -1;
|
|
sym->dataType = dt;
|
|
sym->udtTypeId = udtTypeId;
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
// UDT element arrays carry typeId and fieldCount after the bounds,
|
|
// exactly as OP_DIM_ARRAY does, so REDIM can seed every element.
|
|
if (dt == BAS_TYPE_UDT) {
|
|
BasSymbolT *typeSym = (udtTypeId >= 0) ? findTypeDefById(p, udtTypeId) : NULL;
|
|
if (typeSym == NULL) {
|
|
error(p, "Unknown TYPE definition");
|
|
return;
|
|
}
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)udtTypeId);
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, (int16_t)typeSym->fieldCount);
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_REDIM);
|
|
basEmit8(&p->cg, (uint8_t)dims);
|
|
basEmit8(&p->cg, preserve);
|
|
basEmit8(&p->cg, dt);
|
|
emitStore(p, sym);
|
|
}
|
|
|
|
|
|
static void parseRemoveControl(BasParserT *p) {
|
|
// REMOVECONTROL formRef, ctrlName$
|
|
advance(p); // consume REMOVECONTROL
|
|
parseExpression(p); // form reference
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // control name string
|
|
basEmit8(&p->cg, OP_REMOVE_CTRL);
|
|
}
|
|
|
|
|
|
static void parseRestore(BasParserT *p) {
|
|
// RESTORE -- reset the DATA read pointer to the beginning
|
|
advance(p); // consume RESTORE
|
|
basEmit8(&p->cg, OP_RESTORE);
|
|
}
|
|
|
|
|
|
static void parseResume(BasParserT *p) {
|
|
// RESUME -- re-execute the statement that caused the error
|
|
// RESUME NEXT -- continue at the next statement after the error
|
|
advance(p); // consume RESUME
|
|
|
|
if (check(p, TOK_NEXT)) {
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_RESUME_NEXT);
|
|
} else {
|
|
basEmit8(&p->cg, OP_RESUME);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseRmDir(BasParserT *p) {
|
|
// RMDIR path
|
|
advance(p);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_FS_RMDIR);
|
|
}
|
|
|
|
|
|
static void parseSeek(BasParserT *p) {
|
|
// SEEK #channel, position
|
|
advance(p); // consume SEEK
|
|
|
|
match(p, TOK_HASH); // optional #
|
|
|
|
// Channel number
|
|
parseExpression(p);
|
|
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Position
|
|
parseExpression(p);
|
|
|
|
basEmit8(&p->cg, OP_FILE_SEEK);
|
|
}
|
|
|
|
|
|
static void parseSelectCase(BasParserT *p) {
|
|
// SELECT CASE expr
|
|
// CASE val [, val] ...
|
|
// ...
|
|
// [CASE ELSE]
|
|
// ...
|
|
// END SELECT
|
|
advance(p); // consume SELECT
|
|
expect(p, TOK_CASE);
|
|
|
|
// Evaluate the test expression -- stays on stack throughout
|
|
parseExpression(p);
|
|
|
|
// The test value is now live on the eval stack for the whole
|
|
// construct; track it so EXIT/GOTO can pop it on the way out.
|
|
p->selectDepth++;
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
int32_t *endJumps = NULL; // stb_ds dynamic array
|
|
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
// Check for END SELECT
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_SELECT)) {
|
|
advance(p);
|
|
closeSelectCase(p, &endJumps);
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
|
|
if (!check(p, TOK_CASE)) {
|
|
error(p, "Expected CASE or END SELECT");
|
|
arrfree(endJumps);
|
|
p->selectDepth--;
|
|
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);
|
|
closeSelectCase(p, &endJumps);
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// CASE val [, val | val TO val | IS op val] ...
|
|
//
|
|
// Strategy for multi-value CASE using JMP_TRUE chaining:
|
|
// For each item:
|
|
// Plain value: DUP, push val, CMP_EQ, JMP_TRUE -> body
|
|
// Range (val TO val): DUP, push lo, CMP_GE, JMP_FALSE -> skip,
|
|
// DUP, push hi, CMP_LE, JMP_TRUE -> body, skip:
|
|
// IS op val: DUP, push val, CMP_xx, JMP_TRUE -> body
|
|
// JMP -> next_case (none of the items matched)
|
|
// body:
|
|
// ...statements...
|
|
// JMP -> end_select
|
|
// next_case:
|
|
|
|
int32_t *bodyJumps = NULL; // stb_ds dynamic array
|
|
|
|
for (;;) {
|
|
if (check(p, TOK_IS)) {
|
|
// CASE IS <op> value
|
|
advance(p); // consume IS
|
|
|
|
uint8_t cmpOp;
|
|
|
|
switch (p->lex.token.type) {
|
|
case TOK_LT:
|
|
cmpOp = OP_CMP_LT;
|
|
break;
|
|
|
|
case TOK_GT:
|
|
cmpOp = OP_CMP_GT;
|
|
break;
|
|
|
|
case TOK_LE:
|
|
cmpOp = OP_CMP_LE;
|
|
break;
|
|
|
|
case TOK_GE:
|
|
cmpOp = OP_CMP_GE;
|
|
break;
|
|
|
|
case TOK_EQ:
|
|
cmpOp = OP_CMP_EQ;
|
|
break;
|
|
|
|
case TOK_NE:
|
|
cmpOp = OP_CMP_NE;
|
|
break;
|
|
|
|
default:
|
|
error(p, "Expected comparison operator after IS");
|
|
arrfree(bodyJumps);
|
|
arrfree(endJumps);
|
|
p->selectDepth--;
|
|
return;
|
|
}
|
|
|
|
advance(p);
|
|
|
|
basEmit8(&p->cg, OP_DUP);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, cmpOp);
|
|
|
|
arrput(bodyJumps, emitJump(p, OP_JMP_TRUE));
|
|
} else {
|
|
// Parse first value -- could be plain or start of range
|
|
basEmit8(&p->cg, OP_DUP);
|
|
parseExpression(p);
|
|
|
|
if (check(p, TOK_TO)) {
|
|
// CASE low TO high
|
|
advance(p); // consume TO
|
|
|
|
// Stack: testval testval low
|
|
// Check testval >= low
|
|
basEmit8(&p->cg, OP_CMP_GE);
|
|
int32_t skipRange = emitJump(p, OP_JMP_FALSE);
|
|
|
|
// Check testval <= high
|
|
basEmit8(&p->cg, OP_DUP);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_CMP_LE);
|
|
|
|
arrput(bodyJumps, emitJump(p, OP_JMP_TRUE));
|
|
|
|
patchJump(p, skipRange);
|
|
} else {
|
|
// Plain value -- equality test
|
|
basEmit8(&p->cg, OP_CMP_EQ);
|
|
|
|
arrput(bodyJumps, emitJump(p, OP_JMP_TRUE));
|
|
}
|
|
}
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// None matched -- jump to next case
|
|
int32_t nextCaseJump = emitJump(p, OP_JMP);
|
|
|
|
// Patch all body jumps to here (start of body)
|
|
for (int32_t i = 0; i < (int32_t)arrlen(bodyJumps); i++) {
|
|
patchJump(p, bodyJumps[i]);
|
|
}
|
|
|
|
arrfree(bodyJumps);
|
|
|
|
// Parse the CASE body
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
while (!p->hasError && !check(p, TOK_CASE) && !check(p, TOK_EOF)) {
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_SELECT)) {
|
|
advance(p);
|
|
// Both the no-match fall-through (nextCaseJump) and
|
|
// earlier matched bodies (endJumps) land ON the pop.
|
|
patchJump(p, nextCaseJump);
|
|
closeSelectCase(p, &endJumps);
|
|
return;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
// Jump to end of SELECT (skip remaining cases)
|
|
arrput(endJumps, emitJump(p, OP_JMP));
|
|
|
|
// Patch the next-case jump to here
|
|
patchJump(p, nextCaseJump);
|
|
}
|
|
|
|
// Reached if EOF hit without END SELECT -- patch pending end jumps
|
|
// and clean up. End jumps land on the pop, then fall through to it.
|
|
closeSelectCase(p, &endJumps);
|
|
|
|
if (!p->hasError) {
|
|
error(p, "Expected END SELECT");
|
|
}
|
|
}
|
|
|
|
|
|
static void parseSetAttr(BasParserT *p) {
|
|
// SETATTR filename, attributes
|
|
advance(p);
|
|
parseExpression(p);
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_FS_SETATTR);
|
|
}
|
|
|
|
|
|
static void parseSetEvent(BasParserT *p) {
|
|
// SETEVENT ctrlRef, eventName$, handlerName$
|
|
advance(p); // consume SETEVENT
|
|
parseExpression(p); // control reference
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // event name string
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // handler name string
|
|
basEmit8(&p->cg, OP_SET_EVENT);
|
|
}
|
|
|
|
|
|
static void parseShell(BasParserT *p) {
|
|
// SHELL "command" -- execute an OS command (discard return value)
|
|
// SHELL -- no argument, no-op in embedded context
|
|
advance(p); // consume SHELL
|
|
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
// No argument -- push empty string and call SHELL (no-op)
|
|
uint16_t idx = basAddConstant(&p->cg, "", 0);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, idx);
|
|
} else {
|
|
parseExpression(p);
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_SHELL);
|
|
basEmit8(&p->cg, OP_POP); // discard return value in statement form
|
|
}
|
|
|
|
|
|
// Optional modal flag after form.Show / Me.Show: a literal, TRUE/FALSE
|
|
// or a CONST. OP_SHOW_FORM takes the flag as an immediate operand, so a
|
|
// variable cannot be accepted here.
|
|
static uint8_t parseShowModalFlag(BasParserT *p) {
|
|
uint8_t modal = 0;
|
|
|
|
if (check(p, TOK_INT_LIT)) {
|
|
modal = (p->lex.token.intVal != 0);
|
|
advance(p);
|
|
} else if (check(p, TOK_TRUE_KW)) {
|
|
modal = 1;
|
|
advance(p);
|
|
} else if (check(p, TOK_FALSE_KW)) {
|
|
advance(p);
|
|
} else if (check(p, TOK_IDENT)) {
|
|
BasSymbolT *modSym = basSymTabFind(&p->sym, p->lex.token.text);
|
|
|
|
if (modSym == NULL || modSym->kind != SYM_CONST) {
|
|
error(p, "Show modal flag must be a literal or CONST");
|
|
return 0;
|
|
}
|
|
|
|
modal = (modSym->constInt != 0);
|
|
advance(p);
|
|
}
|
|
|
|
return modal;
|
|
}
|
|
|
|
|
|
static void parseSleep(BasParserT *p) {
|
|
// SLEEP [seconds]
|
|
// If no argument, default to 1 second
|
|
advance(p); // consume SLEEP
|
|
|
|
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
|
|
// No argument -- push 1 second
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 1);
|
|
} else {
|
|
parseExpression(p);
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_SLEEP);
|
|
}
|
|
|
|
|
|
static void parseStatement(BasParserT *p) {
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
skipNewlines(p);
|
|
|
|
if (check(p, TOK_EOF)) {
|
|
return;
|
|
}
|
|
|
|
// Every nested block (IF/FOR/DO/SELECT/SUB body) recurses through
|
|
// here; bound the depth so a pathological input is an error, not a
|
|
// stack overflow.
|
|
if (p->blockDepth >= BAS_MAX_BLOCK_DEPTH) {
|
|
error(p, "Statements nested too deeply");
|
|
return;
|
|
}
|
|
|
|
p->blockDepth++;
|
|
|
|
// Emit source line number for debugger (before statement code)
|
|
basEmit8(&p->cg, OP_LINE);
|
|
basEmitU16(&p->cg, (uint16_t)p->lex.token.line);
|
|
|
|
// A label "Name:" consumes its own colon and may be followed by a
|
|
// statement on the same line, so it skips the end-of-statement check.
|
|
bool endOfStatement = true;
|
|
BasTokenTypeE tt = p->lex.token.type;
|
|
|
|
switch (tt) {
|
|
case TOK_PRINT:
|
|
parsePrint(p);
|
|
break;
|
|
|
|
case TOK_DIM:
|
|
parseDim(p);
|
|
break;
|
|
|
|
case TOK_DATA:
|
|
parseData(p);
|
|
break;
|
|
|
|
case TOK_READ:
|
|
parseRead(p);
|
|
break;
|
|
|
|
case TOK_RESTORE:
|
|
parseRestore(p);
|
|
break;
|
|
|
|
case TOK_STATIC:
|
|
parseStatic(p);
|
|
break;
|
|
|
|
case TOK_DEF:
|
|
parseDef(p);
|
|
break;
|
|
|
|
case TOK_DEFINT:
|
|
parseDefType(p, BAS_TYPE_INTEGER);
|
|
break;
|
|
|
|
case TOK_DEFLNG:
|
|
parseDefType(p, BAS_TYPE_LONG);
|
|
break;
|
|
|
|
case TOK_DEFSNG:
|
|
parseDefType(p, BAS_TYPE_SINGLE);
|
|
break;
|
|
|
|
case TOK_DEFDBL:
|
|
parseDefType(p, BAS_TYPE_DOUBLE);
|
|
break;
|
|
|
|
case TOK_DEFSTR:
|
|
parseDefType(p, BAS_TYPE_STRING);
|
|
break;
|
|
|
|
case TOK_DECLARE:
|
|
parseDeclare(p);
|
|
break;
|
|
|
|
case TOK_IF:
|
|
parseIf(p);
|
|
break;
|
|
|
|
case TOK_FOR:
|
|
parseFor(p);
|
|
break;
|
|
|
|
case TOK_DO:
|
|
parseDo(p);
|
|
break;
|
|
|
|
case TOK_WHILE:
|
|
parseWhile(p);
|
|
break;
|
|
|
|
case TOK_SELECT:
|
|
parseSelectCase(p);
|
|
break;
|
|
|
|
case TOK_SUB:
|
|
parseSub(p);
|
|
break;
|
|
|
|
case TOK_FUNCTION:
|
|
parseFunction(p);
|
|
break;
|
|
|
|
case TOK_EXIT:
|
|
parseExit(p);
|
|
break;
|
|
|
|
case TOK_CONST:
|
|
parseConst(p);
|
|
break;
|
|
|
|
case TOK_END:
|
|
parseEnd(p);
|
|
break;
|
|
|
|
case TOK_ERROR_KW:
|
|
// ERROR n -- raise a runtime error
|
|
advance(p);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_RAISE_ERR);
|
|
break;
|
|
|
|
case TOK_ERASE:
|
|
parseErase(p);
|
|
break;
|
|
|
|
case TOK_TYPE:
|
|
parseType(p);
|
|
break;
|
|
|
|
case TOK_REDIM:
|
|
parseRedim(p);
|
|
break;
|
|
|
|
case TOK_FILECOPY:
|
|
parseFileCopy(p);
|
|
break;
|
|
|
|
case TOK_INPUT:
|
|
parseInput(p);
|
|
break;
|
|
|
|
case TOK_KILL:
|
|
parseKill(p);
|
|
break;
|
|
|
|
case TOK_MKDIR:
|
|
parseMkDir(p);
|
|
break;
|
|
|
|
case TOK_NAME:
|
|
parseName(p);
|
|
break;
|
|
|
|
case TOK_OPEN:
|
|
parseOpen(p);
|
|
break;
|
|
|
|
case TOK_CHDIR:
|
|
parseChDir(p);
|
|
break;
|
|
|
|
case TOK_CHDRIVE:
|
|
parseChDrive(p);
|
|
break;
|
|
|
|
case TOK_CLOSE:
|
|
parseClose(p);
|
|
break;
|
|
|
|
case TOK_GET:
|
|
parseGet(p);
|
|
break;
|
|
|
|
case TOK_PUT:
|
|
parsePut(p);
|
|
break;
|
|
|
|
case TOK_SEEK:
|
|
parseSeek(p);
|
|
break;
|
|
|
|
case TOK_WRITE:
|
|
parseWrite(p);
|
|
break;
|
|
|
|
case TOK_LINE:
|
|
parseLineInput(p);
|
|
break;
|
|
|
|
case TOK_GOTO:
|
|
parseGoto(p);
|
|
break;
|
|
|
|
case TOK_GOSUB:
|
|
parseGosub(p);
|
|
break;
|
|
|
|
case TOK_ON:
|
|
parseOn(p);
|
|
break;
|
|
|
|
case TOK_OPTION:
|
|
parseOption(p);
|
|
break;
|
|
|
|
case TOK_SHELL:
|
|
parseShell(p);
|
|
break;
|
|
|
|
case TOK_RESUME:
|
|
parseResume(p);
|
|
break;
|
|
|
|
case TOK_RMDIR:
|
|
parseRmDir(p);
|
|
break;
|
|
|
|
case TOK_SETATTR:
|
|
parseSetAttr(p);
|
|
break;
|
|
|
|
case TOK_RETURN:
|
|
advance(p);
|
|
if (p->sym.inLocalScope) {
|
|
// Inside SUB/FUNCTION: return from subroutine
|
|
basEmit8(&p->cg, OP_RET);
|
|
} else {
|
|
// Module level: GOSUB return (pop PC from eval stack).
|
|
// Any SELECT CASE opened since the handler's label still
|
|
// has its test value above the return address; discard
|
|
// those first or OP_GOSUB_RET would pop a test value.
|
|
emitSelectPops(p, p->selectDepth - p->lastLabelSelectDepth);
|
|
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;
|
|
} else if (sym->kind != SYM_SUB && sym->kind != SYM_FUNCTION) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "'%s' is not a SUB or FUNCTION", sym->name);
|
|
error(p, buf);
|
|
break;
|
|
}
|
|
|
|
// CALL name(args) or bare CALL name (no arguments)
|
|
int32_t argc = 0;
|
|
|
|
if (check(p, TOK_LPAREN)) {
|
|
argc = parseCallArgs(p, sym, true);
|
|
}
|
|
|
|
emitCallWithArgs(p, sym, argc);
|
|
|
|
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, BAS_RANDOMIZE_TIMER_SEED);
|
|
} else {
|
|
parseExpression(p);
|
|
}
|
|
basEmit8(&p->cg, OP_MATH_RANDOMIZE);
|
|
break;
|
|
|
|
case TOK_DOEVENTS:
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_DO_EVENTS);
|
|
break;
|
|
|
|
case TOK_CREATEFORM:
|
|
// CreateForm used as statement (discard return value)
|
|
parsePrimary(p);
|
|
basEmit8(&p->cg, OP_POP);
|
|
break;
|
|
|
|
case TOK_CREATECONTROL:
|
|
// CreateControl used as statement (discard return value)
|
|
parsePrimary(p);
|
|
basEmit8(&p->cg, OP_POP);
|
|
break;
|
|
|
|
case TOK_SETEVENT:
|
|
parseSetEvent(p);
|
|
break;
|
|
|
|
case TOK_REMOVECONTROL:
|
|
parseRemoveControl(p);
|
|
break;
|
|
|
|
case TOK_SET:
|
|
// SET var = expr (object assignment)
|
|
advance(p); // consume SET
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
break;
|
|
}
|
|
{
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
expect(p, TOK_EQ);
|
|
parseExpression(p);
|
|
BasSymbolT *varSym = ensureVariable(p, varName);
|
|
if (varSym) {
|
|
varSym->dataType = BAS_TYPE_OBJECT;
|
|
emitStore(p, varSym);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case TOK_LOAD:
|
|
// Load FormName (identifier, not string)
|
|
advance(p);
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "form name");
|
|
break;
|
|
}
|
|
{
|
|
uint16_t nameIdx = basAddConstant(&p->cg, p->lex.token.text, (int32_t)strlen(p->lex.token.text));
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, nameIdx);
|
|
basEmit8(&p->cg, OP_LOAD_FORM);
|
|
basEmit8(&p->cg, OP_POP);
|
|
}
|
|
break;
|
|
|
|
case TOK_UNLOAD:
|
|
// Unload FormName (identifier, not string)
|
|
advance(p);
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "form name");
|
|
break;
|
|
}
|
|
{
|
|
uint16_t nameIdx = basAddConstant(&p->cg, p->lex.token.text, (int32_t)strlen(p->lex.token.text));
|
|
advance(p);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, nameIdx);
|
|
basEmit8(&p->cg, OP_LOAD_FORM);
|
|
basEmit8(&p->cg, OP_UNLOAD_FORM);
|
|
}
|
|
break;
|
|
|
|
case TOK_INPUTBOX:
|
|
// InputBox$ prompt [, title [, default]] (statement form, discard result)
|
|
advance(p);
|
|
parseExpression(p); // prompt
|
|
|
|
if (match(p, TOK_COMMA)) {
|
|
parseExpression(p); // title
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, basAddConstant(&p->cg, "", 0));
|
|
}
|
|
|
|
if (match(p, TOK_COMMA)) {
|
|
parseExpression(p); // default
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, basAddConstant(&p->cg, "", 0));
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_INPUTBOX);
|
|
basEmit8(&p->cg, OP_POP); // discard result
|
|
break;
|
|
|
|
case TOK_MSGBOX: {
|
|
// MsgBox message [, flags [, title]] (statement form, discards result)
|
|
advance(p);
|
|
parseExpression(p); // message
|
|
|
|
if (match(p, TOK_COMMA)) {
|
|
parseExpression(p); // flags
|
|
} else {
|
|
basEmit8(&p->cg, OP_PUSH_INT16);
|
|
basEmit16(&p->cg, 0); // default flags = MB_OK
|
|
}
|
|
|
|
if (match(p, TOK_COMMA)) {
|
|
parseExpression(p); // title
|
|
} else {
|
|
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, emptyIdx);
|
|
}
|
|
|
|
basEmit8(&p->cg, OP_MSGBOX);
|
|
basEmit8(&p->cg, OP_POP); // discard result
|
|
break;
|
|
}
|
|
|
|
case TOK_INIWRITE:
|
|
// IniWrite file, section, key, value
|
|
advance(p);
|
|
parseExpression(p); // file
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // section
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // key
|
|
expect(p, TOK_COMMA);
|
|
parseExpression(p); // value
|
|
basEmit8(&p->cg, OP_INI_WRITE);
|
|
break;
|
|
|
|
case TOK_ME: {
|
|
// Me.Show / Me.Hide / Me.CtrlName.Property = expr
|
|
advance(p); // consume Me
|
|
if (!check(p, TOK_DOT)) {
|
|
errorExpected(p, "'.' after Me");
|
|
break;
|
|
}
|
|
advance(p); // consume DOT
|
|
|
|
if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
|
|
errorExpected(p, "method or member name after Me.");
|
|
break;
|
|
}
|
|
|
|
char meMember[BAS_MAX_TOKEN_LEN];
|
|
strncpy(meMember, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
meMember[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
if (strcasecmp(meMember, "Show") == 0) {
|
|
// Me.Show [modal]
|
|
basEmit8(&p->cg, OP_ME_REF);
|
|
uint8_t modal = parseShowModalFlag(p);
|
|
basEmit8(&p->cg, OP_SHOW_FORM);
|
|
basEmit8(&p->cg, modal);
|
|
} else if (strcasecmp(meMember, "Hide") == 0) {
|
|
// Me.Hide
|
|
basEmit8(&p->cg, OP_ME_REF);
|
|
basEmit8(&p->cg, OP_HIDE_FORM);
|
|
} else if (check(p, TOK_LPAREN) || check(p, TOK_DOT)) {
|
|
// Me.CtrlName(idx).Property OR Me.CtrlName.Property
|
|
bool hasIndex = check(p, TOK_LPAREN);
|
|
|
|
// Push form ref (Me), ctrl name
|
|
basEmit8(&p->cg, OP_ME_REF);
|
|
uint16_t ctrlIdx = basAddConstant(&p->cg, meMember, (int32_t)strlen(meMember));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, ctrlIdx);
|
|
|
|
if (hasIndex) {
|
|
// Me.CtrlName(idx) -- parse index, use FIND_CTRL_IDX
|
|
expect(p, TOK_LPAREN);
|
|
parseExpression(p);
|
|
expect(p, TOK_RPAREN);
|
|
basEmit8(&p->cg, OP_FIND_CTRL_IDX);
|
|
} else {
|
|
basEmit8(&p->cg, OP_FIND_CTRL);
|
|
}
|
|
|
|
expect(p, TOK_DOT);
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "property name");
|
|
break;
|
|
}
|
|
char propName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(propName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
propName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
if (check(p, TOK_EQ)) {
|
|
// Property assignment
|
|
advance(p);
|
|
uint16_t propIdx = basAddConstant(&p->cg, propName, (int32_t)strlen(propName));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, propIdx);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_STORE_PROP);
|
|
} else {
|
|
// Method call
|
|
uint16_t methodIdx = basAddConstant(&p->cg, propName, (int32_t)strlen(propName));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, methodIdx);
|
|
emitMethodCallStatement(p);
|
|
}
|
|
} else if (check(p, TOK_EQ)) {
|
|
// Me.Property = expr (form-level property set)
|
|
advance(p); // consume =
|
|
basEmit8(&p->cg, OP_ME_REF);
|
|
uint16_t propIdx = basAddConstant(&p->cg, meMember, (int32_t)strlen(meMember));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, propIdx);
|
|
parseExpression(p);
|
|
basEmit8(&p->cg, OP_STORE_PROP);
|
|
} else {
|
|
// Me.Method [args] (form-level method call)
|
|
basEmit8(&p->cg, OP_ME_REF);
|
|
uint16_t methodIdx = basAddConstant(&p->cg, meMember, (int32_t)strlen(meMember));
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, methodIdx);
|
|
emitMethodCallStatement(p);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TOK_LET:
|
|
advance(p); // consume LET, then fall through to assignment
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name after LET");
|
|
break;
|
|
}
|
|
parseAssignOrCall(p);
|
|
break;
|
|
|
|
case TOK_IDENT: {
|
|
// Check for form scope directives (injected by IDE)
|
|
if (checkKeyword(p, "BEGINFORM")) {
|
|
parseBeginForm(p);
|
|
break;
|
|
}
|
|
|
|
if (checkKeyword(p, "ENDFORM")) {
|
|
parseEndForm(p);
|
|
break;
|
|
}
|
|
|
|
// Check for label: identifier followed by colon
|
|
BasLexerT savedLex = p->lex;
|
|
char labelName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(labelName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
labelName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
if (check(p, TOK_COLON)) {
|
|
advance(p); // consume colon
|
|
// Record the label at the current code position. For
|
|
// SYM_LABEL, localCount is repurposed to hold the SELECT
|
|
// depth at the definition, so GOTO can pop only the test
|
|
// values of the SELECT blocks it actually leaves.
|
|
BasSymbolT *sym = basSymTabFind(&p->sym, labelName);
|
|
if (sym != NULL && sym->kind == SYM_LABEL) {
|
|
if (sym->isDefined) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Duplicate label '%s'", labelName);
|
|
error(p, buf);
|
|
break;
|
|
}
|
|
// Forward-declared label -- now define it
|
|
sym->codeAddr = basCodePos(&p->cg);
|
|
sym->isDefined = true;
|
|
sym->localCount = p->selectDepth;
|
|
patchLabelRefs(p, sym);
|
|
} else if (sym == NULL) {
|
|
bool savedLocal = p->sym.inLocalScope;
|
|
p->sym.inLocalScope = false;
|
|
sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0);
|
|
p->sym.inLocalScope = savedLocal;
|
|
if (sym == NULL) {
|
|
error(p, "Symbol table full");
|
|
break;
|
|
}
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->isDefined = true;
|
|
sym->codeAddr = basCodePos(&p->cg);
|
|
sym->localCount = p->selectDepth;
|
|
} else {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Name '%s' already used", labelName);
|
|
error(p, buf);
|
|
break;
|
|
}
|
|
p->lastLabelSelectDepth = p->selectDepth;
|
|
// After the label, there may be a statement on the same line
|
|
// which will be parsed on the next iteration
|
|
endOfStatement = false;
|
|
break;
|
|
}
|
|
|
|
// Not a label -- restore and parse as assignment/call
|
|
p->lex = savedLex;
|
|
parseAssignOrCall(p);
|
|
break;
|
|
}
|
|
|
|
case TOK_REM:
|
|
// Comment -- skip to end of line
|
|
advance(p);
|
|
break;
|
|
|
|
default: {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Unexpected token: %s", basTokenName(tt));
|
|
error(p, buf);
|
|
break;
|
|
}
|
|
}
|
|
|
|
p->blockDepth--;
|
|
|
|
if (endOfStatement && !p->hasError) {
|
|
expectEndOfStatement(p);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseStatic(BasParserT *p) {
|
|
// STATIC var AS type
|
|
// Only valid inside SUB/FUNCTION. Creates a global variable with a
|
|
// mangled name (procName$varName) that persists across calls.
|
|
advance(p); // consume STATIC
|
|
|
|
if (!p->sym.inLocalScope) {
|
|
error(p, "STATIC is only valid inside SUB or FUNCTION");
|
|
return;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char varName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Optional AS type
|
|
uint8_t dt = suffixToType(varName);
|
|
bool isTyped = nameHasTypeSuffix(varName);
|
|
if (match(p, TOK_AS)) {
|
|
dt = resolveTypeName(p);
|
|
isTyped = true;
|
|
}
|
|
|
|
if (p->hasError) {
|
|
return;
|
|
}
|
|
|
|
// STATIC of a user-defined type is not supported: parseStatic neither
|
|
// records the UDT typeId nor emits the one-time allocation, so field
|
|
// access would fault at runtime. Reject it cleanly instead of silently
|
|
// mis-compiling.
|
|
if (dt == BAS_TYPE_UDT) {
|
|
error(p, "STATIC of a user type is not supported");
|
|
return;
|
|
}
|
|
|
|
// Create a mangled global name: "procName$varName". It must fit a
|
|
// symbol name like any other; the symbol table never truncates.
|
|
char mangledName[BAS_MAX_IDENT * 2 + 1];
|
|
int32_t mangledLen = snprintf(mangledName, sizeof(mangledName), "%s$%s", p->currentProc, varName);
|
|
|
|
if (mangledLen >= BAS_MAX_IDENT) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "STATIC name '%s' is too long (procedure plus variable name must be under %d characters)", varName, (int)BAS_MAX_IDENT);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
|
|
// 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 = basSymTabAllocGlobalSlot(&p->sym);
|
|
globalSym->isDefined = true;
|
|
globalSym->isTyped = isTyped;
|
|
|
|
// Create a local alias that maps to this global's index
|
|
BasSymbolT *localSym = basSymTabAdd(&p->sym, varName, SYM_VARIABLE, dt);
|
|
if (localSym == NULL) {
|
|
error(p, "Symbol table full or duplicate variable name");
|
|
return;
|
|
}
|
|
localSym->scope = SCOPE_GLOBAL; // accessed as global
|
|
localSym->index = globalSym->index;
|
|
localSym->isDefined = true;
|
|
}
|
|
|
|
|
|
static void parseSub(BasParserT *p) {
|
|
// SUB name(params)
|
|
// ...
|
|
// END SUB
|
|
advance(p); // consume SUB
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "subroutine name");
|
|
return;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
if (p->sym.inLocalScope) {
|
|
error(p, "SUB cannot be defined inside SUB or FUNCTION");
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
|
|
ProcSaveT save;
|
|
procBegin(p, &save, false);
|
|
|
|
ParamListT pl;
|
|
|
|
if (!parseParamList(p, &pl, PARAM_REGISTER)) {
|
|
procEnd(p, &save);
|
|
return;
|
|
}
|
|
|
|
// Register the sub in the symbol table (global scope). See
|
|
// parseFunction for the duplicate-body rule.
|
|
BasSymbolT *existing = basSymTabFindGlobal(&p->sym, name);
|
|
BasSymbolT *subSym = NULL;
|
|
|
|
if (existing != NULL && existing->kind == SYM_SUB) {
|
|
if (existing->isDefined) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Sub '%s' is already defined", existing->name);
|
|
error(p, buf);
|
|
procEnd(p, &save);
|
|
return;
|
|
}
|
|
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");
|
|
procEnd(p, &save);
|
|
return;
|
|
}
|
|
|
|
subSym->codeAddr = subAddr;
|
|
subSym->isDefined = true;
|
|
subSym->scope = SCOPE_GLOBAL;
|
|
applyParamList(subSym, &pl);
|
|
|
|
// Record the owning form so fireCtrlEvent can bind the SUB's
|
|
// form-scope variables at call time. Prescan adds SUB symbols
|
|
// before the BEGINFORM directive is consumed, so the formName
|
|
// wasn't populated by basSymTabAdd; set it here once we know we
|
|
// are inside a form scope.
|
|
if (p->sym.inFormScope && p->sym.formScopeName[0]) {
|
|
strncpy(subSym->formName, p->sym.formScopeName, BAS_MAX_IDENT - 1);
|
|
subSym->formName[BAS_MAX_IDENT - 1] = '\0';
|
|
}
|
|
|
|
// Backpatch any forward-reference calls to this sub
|
|
patchCallAddrs(p, subSym);
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
// Parse sub body
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_SUB)) {
|
|
advance(p);
|
|
break;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
parseStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
|
|
// Patch EXIT SUB jumps, emit the epilogue and prologue, and restore
|
|
// the enclosing state
|
|
procEnd(p, &save);
|
|
|
|
// Leave local scope
|
|
collectDebugLocals(p, p->cg.debugProcCount++);
|
|
basSymTabLeaveLocal(&p->sym);
|
|
|
|
// Patch the skip jump
|
|
patchJump(p, skipJump);
|
|
}
|
|
|
|
|
|
static void parseSwap(BasParserT *p) {
|
|
// SWAP a, b -- swap the values of two variables
|
|
advance(p); // consume SWAP
|
|
|
|
// First variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char nameA[BAS_MAX_TOKEN_LEN];
|
|
strncpy(nameA, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
nameA[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *symA = ensureVariable(p, nameA);
|
|
if (symA == NULL) {
|
|
return;
|
|
}
|
|
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Second variable
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "variable name");
|
|
return;
|
|
}
|
|
|
|
char nameB[BAS_MAX_TOKEN_LEN];
|
|
strncpy(nameB, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
nameB[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
BasSymbolT *symB = ensureVariable(p, nameB);
|
|
if (symB == NULL) {
|
|
return;
|
|
}
|
|
|
|
// Emit: load a, load b, store a, store b
|
|
emitLoad(p, symA);
|
|
emitLoad(p, symB);
|
|
emitStore(p, symA);
|
|
emitStore(p, symB);
|
|
}
|
|
|
|
|
|
static void parseType(BasParserT *p) {
|
|
// TYPE name
|
|
// field AS type
|
|
// ...
|
|
// END TYPE
|
|
advance(p); // consume TYPE
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "type name");
|
|
return;
|
|
}
|
|
|
|
char typeName[BAS_MAX_TOKEN_LEN];
|
|
strncpy(typeName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
typeName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
// Add TYPE_DEF symbol
|
|
bool savedLocal = p->sym.inLocalScope;
|
|
p->sym.inLocalScope = false;
|
|
BasSymbolT *typeSym = basSymTabAdd(&p->sym, typeName, SYM_TYPE_DEF, BAS_TYPE_UDT);
|
|
p->sym.inLocalScope = savedLocal;
|
|
|
|
if (typeSym == NULL) {
|
|
error(p, "Symbol table full or duplicate TYPE name");
|
|
return;
|
|
}
|
|
typeSym->scope = SCOPE_GLOBAL;
|
|
typeSym->isDefined = true;
|
|
typeSym->index = p->sym.count - 1;
|
|
typeSym->fieldCount = 0;
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
|
|
// Parse fields until END TYPE
|
|
while (!p->hasError && !check(p, TOK_EOF)) {
|
|
if (check(p, TOK_END)) {
|
|
BasLexerT savedLex = p->lex;
|
|
advance(p);
|
|
if (check(p, TOK_TYPE)) {
|
|
advance(p);
|
|
break;
|
|
}
|
|
p->lex = savedLex;
|
|
}
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
errorExpected(p, "field name or END TYPE");
|
|
return;
|
|
}
|
|
|
|
BasFieldDefT field;
|
|
memset(&field, 0, sizeof(field));
|
|
// Identifiers are already bounded by checkIdentLength.
|
|
snprintf(field.name, sizeof(field.name), "%s", p->lex.token.text);
|
|
advance(p);
|
|
|
|
if (resolveFieldIndex(typeSym, field.name) >= 0) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "Duplicate field '%s' in TYPE '%s'", field.name, typeSym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
|
|
expect(p, TOK_AS);
|
|
field.dataType = resolveTypeName(p);
|
|
if (field.dataType == BAS_TYPE_UDT) {
|
|
field.udtTypeId = p->lastUdtTypeId;
|
|
|
|
// The TYPE symbol is registered before its fields so nested
|
|
// types resolve; a field of the type being defined would make
|
|
// emitUdtInit recurse without end.
|
|
if (field.udtTypeId == typeSym->index) {
|
|
char buf[BAS_PARSE_ERR_SCRATCH];
|
|
snprintf(buf, sizeof(buf), "TYPE '%s' cannot contain a field of its own type", typeSym->name);
|
|
error(p, buf);
|
|
return;
|
|
}
|
|
}
|
|
|
|
arrput(typeSym->fields, field);
|
|
typeSym->fieldCount = (int32_t)arrlen(typeSym->fields);
|
|
|
|
expectEndOfStatement(p);
|
|
skipNewlines(p);
|
|
}
|
|
}
|
|
|
|
|
|
static void parseUnaryExpr(BasParserT *p) {
|
|
if (check(p, TOK_MINUS) || check(p, TOK_PLUS)) {
|
|
bool negate = check(p, TOK_MINUS);
|
|
// Let the lexer accept the magnitude of LONG's most negative
|
|
// value so "-2147483648" is a literal rather than an overflow.
|
|
p->lex.allowNegMagnitude = negate;
|
|
advance(p); // unary plus is a no-op
|
|
p->lex.allowNegMagnitude = false;
|
|
if (!exprEnter(p)) {
|
|
return;
|
|
}
|
|
if (negate && check(p, TOK_LONG_LIT) && p->lex.token.longVal == BAS_LONG_NEG_MAGNITUDE) {
|
|
// Fold the sign into the literal: the positive magnitude does
|
|
// not fit LONG, its negation does.
|
|
basEmit8(&p->cg, OP_PUSH_INT32);
|
|
basEmit16(&p->cg, (int16_t)(INT32_MIN & 0xFFFF));
|
|
basEmit16(&p->cg, (int16_t)((INT32_MIN >> 16) & 0xFFFF));
|
|
advance(p);
|
|
exprLeave(p);
|
|
return;
|
|
}
|
|
parseUnaryExpr(p);
|
|
exprLeave(p);
|
|
if (negate) {
|
|
basEmit8(&p->cg, OP_NEG_INT);
|
|
}
|
|
return;
|
|
}
|
|
parsePowExpr(p);
|
|
}
|
|
|
|
|
|
static void parseWhile(BasParserT *p) {
|
|
// WHILE cond
|
|
// ...
|
|
// WEND
|
|
advance(p); // consume WHILE
|
|
|
|
// WHILE/WEND shares the DO exit list: EXIT DO leaves it.
|
|
LoopSaveT save;
|
|
loopBegin(p, false, &save);
|
|
|
|
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) {
|
|
loopEnd(p, false, &save);
|
|
return;
|
|
}
|
|
|
|
expect(p, TOK_WEND);
|
|
|
|
// Jump back to loop top
|
|
basEmit8(&p->cg, OP_JMP);
|
|
int16_t backOffset = relJumpOffset(p, loopTop, basCodePos(&p->cg));
|
|
basEmit16(&p->cg, backOffset);
|
|
|
|
// Patch the false jump to exit
|
|
patchJump(p, falseJump);
|
|
|
|
// Patch EXIT DO jumps (WHILE/WEND uses the DO exit list)
|
|
loopEnd(p, false, &save);
|
|
}
|
|
|
|
|
|
static void parseWrite(BasParserT *p) {
|
|
// WRITE #channel, expr1, expr2, ...
|
|
// Values are comma-delimited. Strings are quoted. Numbers undecorated.
|
|
// Each WRITE statement ends with a newline.
|
|
advance(p); // consume WRITE
|
|
|
|
if (!check(p, TOK_HASH)) {
|
|
error(p, "Expected # after WRITE");
|
|
return;
|
|
}
|
|
advance(p); // consume #
|
|
|
|
// Channel number expression
|
|
parseExpression(p);
|
|
|
|
// Comma separator between channel and first value
|
|
expect(p, TOK_COMMA);
|
|
|
|
// Parse each value
|
|
bool first = true;
|
|
while (!p->hasError) {
|
|
if (!first) {
|
|
// Emit comma separator to file
|
|
basEmit8(&p->cg, OP_DUP); // dup channel for separator
|
|
basEmit8(&p->cg, OP_FILE_WRITE_SEP);
|
|
}
|
|
first = false;
|
|
|
|
// Duplicate channel for the write operation
|
|
basEmit8(&p->cg, OP_DUP);
|
|
|
|
// Parse value expression
|
|
parseExpression(p);
|
|
|
|
// Write value in WRITE format (strings quoted, numbers undecorated)
|
|
basEmit8(&p->cg, OP_FILE_WRITE);
|
|
|
|
if (!match(p, TOK_COMMA)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Write newline to file (channel still on stack)
|
|
basEmit8(&p->cg, OP_FILE_WRITE_NL);
|
|
}
|
|
|
|
|
|
static void parseXorExpr(BasParserT *p) {
|
|
parseAndExpr(p);
|
|
while (!p->hasError && check(p, TOK_XOR)) {
|
|
advance(p);
|
|
parseAndExpr(p);
|
|
basEmit8(&p->cg, OP_XOR);
|
|
}
|
|
}
|
|
|
|
|
|
static void patchCallAddrs(BasParserT *p, BasSymbolT *sym) {
|
|
// Backpatch all forward-reference CALL addresses
|
|
uint16_t addr = (uint16_t)sym->codeAddr;
|
|
|
|
for (int32_t i = 0; i < sym->patchCount; i++) {
|
|
int32_t pos = sym->patchAddrs[i];
|
|
|
|
if (pos >= 0 && pos + 2 <= p->cg.codeLen) {
|
|
memcpy(&p->cg.code[pos], &addr, sizeof(uint16_t));
|
|
}
|
|
}
|
|
|
|
sym->patchCount = 0;
|
|
}
|
|
|
|
|
|
static void patchJump(BasParserT *p, int32_t addr) {
|
|
int32_t target = basCodePos(&p->cg);
|
|
int16_t offset = relJumpOffset(p, target, addr);
|
|
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];
|
|
|
|
// A forward GOTO reserves one OP_NOP per SELECT open at its site
|
|
// directly before the jump opcode and stores that count in the
|
|
// placeholder operand (all other label refs leave it at 0). Now
|
|
// that the label's own SELECT depth is known (sym->localCount),
|
|
// convert one NOP to OP_POP per block the jump actually leaves.
|
|
if (patchAddr + 2 <= p->cg.codeLen) {
|
|
int16_t gotoDepth = 0;
|
|
memcpy(&gotoDepth, &p->cg.code[patchAddr], sizeof(gotoDepth));
|
|
|
|
if (gotoDepth > 0) {
|
|
int32_t pops = gotoDepth - sym->localCount;
|
|
|
|
if (pops > gotoDepth) {
|
|
pops = gotoDepth;
|
|
}
|
|
|
|
int32_t nopStart = patchAddr - 1 - gotoDepth;
|
|
|
|
if (nopStart >= 0) {
|
|
for (int32_t j = 0; j < pops; j++) {
|
|
if (p->cg.code[nopStart + j] == OP_NOP) {
|
|
p->cg.code[nopStart + j] = OP_POP;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int16_t offset = relJumpOffset(p, target, patchAddr);
|
|
basPatch16(&p->cg, patchAddr, offset);
|
|
}
|
|
|
|
sym->patchCount = 0;
|
|
}
|
|
|
|
|
|
// Walk the token stream from current position, find every
|
|
// top-level SUB/FUNCTION declaration, extract the signature
|
|
// (name, params, return type), and register it in the symbol
|
|
// table. Does not emit code. Saves and restores lexer position
|
|
// so the main parse pass starts from the same point. This gives
|
|
// VB-style forward visibility: call sites that appear earlier in
|
|
// the source than the SUB definition still resolve to the right
|
|
// paramCount / types.
|
|
//
|
|
// Lexer errors are skipped here (the main pass reports them with full
|
|
// context); a malformed signature is a real syntax error and is
|
|
// reported immediately, before a call site can misreport it as an
|
|
// arity mismatch against a half-registered signature.
|
|
static void prescanSignatures(BasParserT *p) {
|
|
BasLexerT savedLex = p->lex;
|
|
|
|
while (!check(p, TOK_EOF)) {
|
|
// Clear a latched lexer error so the walk continues; advance()
|
|
// refuses to move while hasError is set, so leaving it latched
|
|
// would spin this loop forever.
|
|
if (p->hasError) {
|
|
p->hasError = false;
|
|
p->errorLine = 0;
|
|
p->error[0] = '\0';
|
|
}
|
|
|
|
// "END SUB" / "END FUNCTION" consume the END and the following
|
|
// keyword as separate tokens; skip END so the next-iteration
|
|
// SUB/FUNCTION check doesn't misinterpret it as a declaration.
|
|
if (check(p, TOK_END)) {
|
|
advance(p);
|
|
continue;
|
|
}
|
|
|
|
bool isFn = check(p, TOK_FUNCTION);
|
|
bool isSub = check(p, TOK_SUB);
|
|
|
|
if (!isFn && !isSub) {
|
|
advance(p);
|
|
continue;
|
|
}
|
|
|
|
advance(p); // consume SUB / FUNCTION
|
|
|
|
if (!check(p, TOK_IDENT)) {
|
|
continue;
|
|
}
|
|
|
|
char name[BAS_MAX_TOKEN_LEN];
|
|
strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
|
|
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
|
advance(p);
|
|
|
|
ParamListT pl;
|
|
|
|
if (!parseParamList(p, &pl, PARAM_PRESCAN)) {
|
|
break;
|
|
}
|
|
|
|
// FUNCTION return type (AS clause; or suffix on name)
|
|
uint8_t returnType = suffixToType(name);
|
|
|
|
if (isFn && match(p, TOK_AS)) {
|
|
int32_t udtTypeId;
|
|
returnType = resolveParamType(p, true, &udtTypeId);
|
|
}
|
|
|
|
if (p->hasError) {
|
|
break;
|
|
}
|
|
|
|
// Register / update the symbol. If a call site already
|
|
// created a forward-ref stub, update it in place.
|
|
BasSymbolT *sym = basSymTabFindGlobal(&p->sym, name);
|
|
|
|
if (sym == NULL) {
|
|
bool savedLocal = p->sym.inLocalScope;
|
|
p->sym.inLocalScope = false;
|
|
sym = basSymTabAdd(&p->sym, name, isFn ? SYM_FUNCTION : SYM_SUB, returnType);
|
|
p->sym.inLocalScope = savedLocal;
|
|
}
|
|
|
|
if (sym != NULL) {
|
|
sym->scope = SCOPE_GLOBAL;
|
|
sym->dataType = returnType;
|
|
applyParamList(sym, &pl);
|
|
|
|
// basSymTabAdd defaults isDefined=true; clear it so
|
|
// call sites that encounter this symbol before the real
|
|
// body is parsed register themselves as forward-refs
|
|
// (patchAddrs). The real parseSub/parseFunction pass
|
|
// sets isDefined=true and fills in codeAddr, at which
|
|
// point patchCallAddrs backpatches the forward refs.
|
|
sym->isDefined = false;
|
|
sym->codeAddr = 0;
|
|
}
|
|
}
|
|
|
|
p->lex = savedLex;
|
|
}
|
|
|
|
|
|
// Enter a SUB/FUNCTION body: save the enclosing EXIT lists and loop /
|
|
// SELECT bookkeeping, start them fresh, and emit the entry jump the
|
|
// prologue (procEnd) will patch.
|
|
static void procBegin(BasParserT *p, ProcSaveT *save, bool isFunction) {
|
|
save->exitFor = p->exitForList;
|
|
save->exitDo = p->exitDoList;
|
|
save->exitSub = p->exitSubList;
|
|
save->exitFunc = p->exitFuncList;
|
|
save->forSelectBase = p->forSelectBase;
|
|
save->doSelectBase = p->doSelectBase;
|
|
save->selectDepth = p->selectDepth;
|
|
save->forDepth = p->forDepth;
|
|
save->doDepth = p->doDepth;
|
|
save->lastLabelSelectDepth = p->lastLabelSelectDepth;
|
|
save->isFunction = isFunction;
|
|
|
|
exitListInit(&p->exitForList);
|
|
exitListInit(&p->exitDoList);
|
|
exitListInit(&p->exitSubList);
|
|
exitListInit(&p->exitFuncList);
|
|
p->forSelectBase = 0;
|
|
p->doSelectBase = 0;
|
|
p->selectDepth = 0;
|
|
p->forDepth = 0;
|
|
p->doDepth = 0;
|
|
p->lastLabelSelectDepth = 0;
|
|
p->currentProcIsFunction = isFunction;
|
|
|
|
// Entry jump to the prologue. Implicit STRING locals only become
|
|
// known while the body is parsed, so their "" initialisation is
|
|
// emitted after the body and reached through this jump.
|
|
save->prologueJmp = emitJump(p, OP_JMP);
|
|
save->bodyAddr = basCodePos(&p->cg);
|
|
}
|
|
|
|
|
|
// Leave a SUB/FUNCTION body: land EXIT SUB/FUNCTION on the epilogue,
|
|
// emit the return, emit the prologue for implicit STRING locals, and
|
|
// restore the enclosing state. Runs on the error path too so the EXIT
|
|
// lists opened by procBegin are never leaked.
|
|
static void procEnd(BasParserT *p, ProcSaveT *save) {
|
|
exitListPatch(&p->exitSubList, p);
|
|
exitListPatch(&p->exitFuncList, p);
|
|
|
|
if (save->isFunction) {
|
|
// Load return value from slot 0 and return
|
|
basEmit8(&p->cg, OP_LOAD_LOCAL);
|
|
basEmitU16(&p->cg, 0);
|
|
basEmit8(&p->cg, OP_RET_VAL);
|
|
} else {
|
|
basEmit8(&p->cg, OP_RET);
|
|
}
|
|
|
|
// Prologue: "" into every implicit STRING local, then back to the
|
|
// body. With nothing to initialise the entry jump falls through.
|
|
int32_t prologueStart = basCodePos(&p->cg);
|
|
|
|
for (int32_t i = 0; i < p->sym.count; i++) {
|
|
BasSymbolT *sym = p->sym.symbols[i];
|
|
|
|
if (sym->scope == SCOPE_LOCAL && sym->kind == SYM_VARIABLE && sym->isImplicit && sym->dataType == BAS_TYPE_STRING) {
|
|
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
|
|
basEmit8(&p->cg, OP_PUSH_STR);
|
|
basEmitU16(&p->cg, emptyIdx);
|
|
basEmit8(&p->cg, OP_STORE_LOCAL);
|
|
basEmitU16(&p->cg, (uint16_t)sym->index);
|
|
}
|
|
}
|
|
|
|
if (basCodePos(&p->cg) != prologueStart) {
|
|
basEmit8(&p->cg, OP_JMP);
|
|
basEmit16(&p->cg, relJumpOffset(p, save->bodyAddr, basCodePos(&p->cg)));
|
|
basPatch16(&p->cg, save->prologueJmp, relJumpOffset(p, prologueStart, save->prologueJmp));
|
|
} else {
|
|
basPatch16(&p->cg, save->prologueJmp, relJumpOffset(p, save->bodyAddr, save->prologueJmp));
|
|
}
|
|
|
|
// A loop left open by an error still owns its EXIT list.
|
|
arrfree(p->exitForList.patchAddr);
|
|
arrfree(p->exitDoList.patchAddr);
|
|
|
|
p->exitForList = save->exitFor;
|
|
p->exitDoList = save->exitDo;
|
|
p->exitSubList = save->exitSub;
|
|
p->exitFuncList = save->exitFunc;
|
|
p->forSelectBase = save->forSelectBase;
|
|
p->doSelectBase = save->doSelectBase;
|
|
p->selectDepth = save->selectDepth;
|
|
p->forDepth = save->forDepth;
|
|
p->doDepth = save->doDepth;
|
|
p->lastLabelSelectDepth = save->lastLabelSelectDepth;
|
|
p->currentProcIsFunction = false;
|
|
p->currentProc[0] = '\0';
|
|
}
|
|
|
|
|
|
// Compute the operand for a relative jump whose 2-byte operand lives at
|
|
// operandAddr. A span outside the signed 16-bit range would silently
|
|
// wrap and jump somewhere wild, so surface it as a compile error; the
|
|
// module-size check in basParse only bounds absolute call addresses.
|
|
static int16_t relJumpOffset(BasParserT *p, int32_t target, int32_t operandAddr) {
|
|
int32_t span = target - (operandAddr + BAS_JUMP_OPERAND_SIZE);
|
|
|
|
if (span < INT16_MIN || span > INT16_MAX) {
|
|
error(p, "Jump distance exceeds 16-bit limit");
|
|
}
|
|
|
|
return (int16_t)span;
|
|
}
|
|
|
|
|
|
static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName) {
|
|
for (int32_t i = 0; i < typeSym->fieldCount; i++) {
|
|
if (strcasecmp(typeSym->fields[i].name, fieldName) == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
// Type after AS in a parameter list or FUNCTION return clause. During
|
|
// the prescan the TYPE statements have not been parsed yet, so an
|
|
// unknown identifier is accepted as a user type there (the main pass
|
|
// validates it); otherwise this is resolveTypeName plus the UDT id.
|
|
static uint8_t resolveParamType(BasParserT *p, bool prescan, int32_t *outUdtTypeId) {
|
|
*outUdtTypeId = -1;
|
|
|
|
if (prescan && check(p, TOK_IDENT) && findTypeDef(p, p->lex.token.text) == NULL) {
|
|
advance(p);
|
|
return BAS_TYPE_UDT;
|
|
}
|
|
|
|
uint8_t dt = resolveTypeName(p);
|
|
|
|
if (dt == BAS_TYPE_UDT) {
|
|
*outUdtTypeId = p->lastUdtTypeId;
|
|
}
|
|
|
|
return dt;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// Add delta to every recorded backpatch ADDRESS >= from. Used when
|
|
// emitted code is moved (e.g. parseDimBounds prepending a default
|
|
// lbound), so that pending forward-CALL and label-reference patch
|
|
// positions still point at the operand bytes after the shift. The
|
|
// operands themselves are computed at patch time from the (already
|
|
// shifted) stored address, so only the stored address needs adjusting.
|
|
static void shiftBackpatchAddrs(BasParserT *p, int32_t from, int32_t delta) {
|
|
// Per-symbol forward-CALL and label-reference patch addresses.
|
|
// Only the live patch set (patchCount) matters: entries already
|
|
// consumed by patchCallAddrs/patchLabelRefs leave patchCount at 0
|
|
// and are never re-applied, so they need no shifting.
|
|
for (int32_t i = 0; i < p->sym.count; i++) {
|
|
BasSymbolT *s = p->sym.symbols[i];
|
|
|
|
for (int32_t j = 0; j < s->patchCount; j++) {
|
|
if (s->patchAddrs[j] >= from) {
|
|
s->patchAddrs[j] += delta;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Active EXIT FOR / EXIT DO / EXIT SUB / EXIT FUNCTION lists.
|
|
ExitListT *exitLists[] = {&p->exitForList, &p->exitDoList, &p->exitSubList, &p->exitFuncList};
|
|
int32_t exitListCount = (int32_t)(sizeof(exitLists) / sizeof(exitLists[0]));
|
|
|
|
for (int32_t i = 0; i < exitListCount; i++) {
|
|
int32_t n = (int32_t)arrlen(exitLists[i]->patchAddr);
|
|
|
|
for (int32_t j = 0; j < n; j++) {
|
|
if (exitLists[i]->patchAddr[j] >= from) {
|
|
exitLists[i]->patchAddr[j] += delta;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pending JMP over a form's init block.
|
|
if (p->formInitJmpAddr >= from) {
|
|
p->formInitJmpAddr += delta;
|
|
}
|
|
}
|
|
|
|
|
|
static void skipNewlines(BasParserT *p) {
|
|
while (!p->hasError && check(p, TOK_NEWLINE)) {
|
|
advance(p);
|
|
}
|
|
}
|
|
|
|
|
|
static uint8_t suffixToType(const char *name) {
|
|
int32_t len = (int32_t)strlen(name);
|
|
if (len == 0) {
|
|
return BAS_TYPE_SINGLE; // QB default
|
|
}
|
|
int32_t dt = basTypeSuffixType(name[len - 1]);
|
|
if (dt < 0) {
|
|
return BAS_TYPE_SINGLE; // QB default
|
|
}
|
|
return (uint8_t)dt;
|
|
}
|
|
|
|
|