From bf5bf9bb1d00a9bbb21f7f7598d2b0400eae7d86 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Fri, 3 Apr 2026 18:09:50 -0500 Subject: [PATCH] Form level variable scope. Proper Load/Unload. --- apps/dvxbasic/Makefile | 2 +- apps/dvxbasic/compiler/codegen.c | 27 +++++-- apps/dvxbasic/compiler/codegen.h | 16 ++-- apps/dvxbasic/compiler/lexer.c | 2 +- apps/dvxbasic/compiler/opcodes.h | 3 + apps/dvxbasic/compiler/parser.c | 122 ++++++++++++++++++++++++++++++- apps/dvxbasic/compiler/parser.h | 3 + apps/dvxbasic/compiler/symtab.c | 73 +++++++++++++++++- apps/dvxbasic/compiler/symtab.h | 23 ++++-- apps/dvxbasic/formrt/formrt.c | 118 +++++++++++++++++++++++++++++- apps/dvxbasic/formrt/formrt.h | 12 +++ apps/dvxbasic/ide/ideMain.c | 24 +++++- apps/dvxbasic/runtime/vm.c | 79 ++++++++++++++++++-- apps/dvxbasic/runtime/vm.h | 40 +++++++--- apps/dvxbasic/test_compiler.c | 117 +++++++++++++++++++++++++++++ 15 files changed, 611 insertions(+), 50 deletions(-) diff --git a/apps/dvxbasic/Makefile b/apps/dvxbasic/Makefile index 98b1374..0414d60 100644 --- a/apps/dvxbasic/Makefile +++ b/apps/dvxbasic/Makefile @@ -38,7 +38,7 @@ APP_TARGET = $(APPDIR)/dvxbasic.app # Native test programs (host gcc, not cross-compiled) HOSTCC = gcc HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -I. -I../../core -BINDIR = ../bin +BINDIR = ../../bin TEST_COMPILER = $(BINDIR)/test_compiler TEST_VM = $(BINDIR)/test_vm diff --git a/apps/dvxbasic/compiler/codegen.c b/apps/dvxbasic/compiler/codegen.c index c804621..e5c36ea 100644 --- a/apps/dvxbasic/compiler/codegen.c +++ b/apps/dvxbasic/compiler/codegen.c @@ -100,6 +100,17 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) { mod->dataCount = cg->dataCount; + // Copy form variable info + if (cg->formVarInfoCount > 0) { + mod->formVarInfo = (BasFormVarInfoT *)malloc(cg->formVarInfoCount * sizeof(BasFormVarInfoT)); + + if (mod->formVarInfo) { + memcpy(mod->formVarInfo, cg->formVarInfo, cg->formVarInfoCount * sizeof(BasFormVarInfoT)); + } + + mod->formVarInfoCount = cg->formVarInfoCount; + } + return mod; } @@ -175,12 +186,15 @@ void basCodeGenFree(BasCodeGenT *cg) { arrfree(cg->code); arrfree(cg->constants); arrfree(cg->dataPool); - cg->code = NULL; - cg->constants = NULL; - cg->dataPool = NULL; - cg->constCount = 0; - cg->dataCount = 0; - cg->codeLen = 0; + arrfree(cg->formVarInfo); + cg->code = NULL; + cg->constants = NULL; + cg->dataPool = NULL; + cg->formVarInfo = NULL; + cg->constCount = 0; + cg->dataCount = 0; + cg->codeLen = 0; + cg->formVarInfoCount = 0; } @@ -317,6 +331,7 @@ void basModuleFree(BasModuleT *mod) { } free(mod->procs); + free(mod->formVarInfo); free(mod); } diff --git a/apps/dvxbasic/compiler/codegen.h b/apps/dvxbasic/compiler/codegen.h index 6efc575..988ac5e 100644 --- a/apps/dvxbasic/compiler/codegen.h +++ b/apps/dvxbasic/compiler/codegen.h @@ -20,13 +20,15 @@ // ============================================================ typedef struct { - uint8_t *code; // stb_ds dynamic array - int32_t codeLen; - BasStringT **constants; // stb_ds dynamic array - int32_t constCount; - int32_t globalCount; - BasValueT *dataPool; // stb_ds dynamic array - int32_t dataCount; + uint8_t *code; // stb_ds dynamic array + int32_t codeLen; + BasStringT **constants; // stb_ds dynamic array + int32_t constCount; + int32_t globalCount; + BasValueT *dataPool; // stb_ds dynamic array + int32_t dataCount; + BasFormVarInfoT *formVarInfo; // stb_ds dynamic array + int32_t formVarInfoCount; } BasCodeGenT; // ============================================================ diff --git a/apps/dvxbasic/compiler/lexer.c b/apps/dvxbasic/compiler/lexer.c index 1712b5d..afbf76f 100644 --- a/apps/dvxbasic/compiler/lexer.c +++ b/apps/dvxbasic/compiler/lexer.c @@ -182,7 +182,7 @@ static bool atEnd(const BasLexerT *lex) { void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen) { memset(lex, 0, sizeof(*lex)); lex->source = source; - lex->sourceLen = sourceLen; + lex->sourceLen = (sourceLen < 0) ? (int32_t)strlen(source) : sourceLen; lex->pos = 0; lex->line = 1; lex->col = 1; diff --git a/apps/dvxbasic/compiler/opcodes.h b/apps/dvxbasic/compiler/opcodes.h index 78b4c8e..9645554 100644 --- a/apps/dvxbasic/compiler/opcodes.h +++ b/apps/dvxbasic/compiler/opcodes.h @@ -194,6 +194,9 @@ #define OP_FIND_CTRL 0x8C // pop ctrlName, pop formRef, push controlRef #define OP_CTRL_REF 0x8D // [uint16 nameConstIdx] push named control on current form #define OP_FIND_CTRL_IDX 0x8E // pop index, pop ctrlName, pop formRef, push ctrlRef +#define OP_LOAD_FORM_VAR 0x8F // [uint16 idx] push currentFormVars[idx] +#define OP_STORE_FORM_VAR 0x9B // [uint16 idx] pop, store to currentFormVars[idx] +#define OP_PUSH_FORM_ADDR 0x9C // [uint16 idx] push ¤tFormVars[idx] (ByRef) // ============================================================ // Array / misc diff --git a/apps/dvxbasic/compiler/parser.c b/apps/dvxbasic/compiler/parser.c index 9c5633b..8598cda 100644 --- a/apps/dvxbasic/compiler/parser.c +++ b/apps/dvxbasic/compiler/parser.c @@ -140,6 +140,7 @@ static void parsePrimary(BasParserT *p); // ============================================================ static void parseAssignOrCall(BasParserT *p); +static void parseBeginForm(BasParserT *p); static void parseClose(BasParserT *p); static void parseConst(BasParserT *p); static void parseData(BasParserT *p); @@ -150,6 +151,7 @@ static void parseDim(BasParserT *p); static void parseDimBounds(BasParserT *p, int32_t *outDims); static void parseDo(BasParserT *p); static void parseEnd(BasParserT *p); +static void parseEndForm(BasParserT *p); static void parseErase(BasParserT *p); static void parseExit(BasParserT *p); static void parseFor(BasParserT *p); @@ -734,6 +736,9 @@ static void emitLoad(BasParserT *p, BasSymbolT *sym) { 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); @@ -750,6 +755,9 @@ static void emitStore(BasParserT *p, BasSymbolT *sym) { 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); @@ -807,6 +815,8 @@ static void emitByRefArg(BasParserT *p) { // 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); } @@ -1951,6 +1961,98 @@ static void parseAssignOrCall(BasParserT *p) { } +// ============================================================ +// parseBeginForm / parseEndForm -- form scope directives +// ============================================================ +// +// BEGINFORM "FormName" enters form scope: DIM at module level +// creates per-form variables. ENDFORM exits form scope. + +static void parseBeginForm(BasParserT *p) { + advance(p); // consume BEGINFORM + + if (!check(p, TOK_STRING_LIT)) { + errorExpected(p, "form name string"); + return; + } + + char formName[BAS_MAX_SYMBOL_NAME]; + strncpy(formName, p->lex.token.text, BAS_MAX_SYMBOL_NAME - 1); + formName[BAS_MAX_SYMBOL_NAME - 1] = '\0'; + advance(p); + + if (p->sym.inFormScope) { + error(p, "Nested BEGINFORM is not allowed"); + return; + } + + if (p->sym.inLocalScope) { + error(p, "BEGINFORM inside SUB/FUNCTION is not allowed"); + return; + } + + basSymTabEnterFormScope(&p->sym, formName); + + // Emit a forward JMP that will skip form init code (patched at ENDFORM). + // If no init code is emitted, the JMP is patched to jump to right here (noop). + basEmit8(&p->cg, OP_JMP); + p->formInitJmpAddr = basCodePos(&p->cg); + basEmit16(&p->cg, 0); // placeholder — patched at ENDFORM + p->formInitCodeStart = basCodePos(&p->cg); +} + + +static void parseEndForm(BasParserT *p) { + advance(p); // consume ENDFORM + + if (!p->sym.inFormScope) { + error(p, "ENDFORM without BEGINFORM"); + return; + } + + // Capture form name before leaving scope + char formName[BAS_MAX_SYMBOL_NAME]; + strncpy(formName, p->sym.formScopeName, sizeof(formName) - 1); + formName[sizeof(formName) - 1] = '\0'; + + int32_t varCount = basSymTabLeaveFormScope(&p->sym); + + // Determine if any form init code was emitted + int32_t initStart = p->formInitCodeStart; + int32_t initAddr = -1; + int32_t initLen = 0; + int32_t curPos = basCodePos(&p->cg); + + if (curPos > initStart) { + // Init code was emitted — add OP_RET to end the init block + basEmit8(&p->cg, OP_RET); + initAddr = initStart; + initLen = basCodePos(&p->cg) - initStart; + } + + // Patch the JMP to skip over init code (land here) + if (p->formInitJmpAddr >= 0) { + int16_t offset = (int16_t)(basCodePos(&p->cg) - (p->formInitJmpAddr + 2)); + basPatch16(&p->cg, p->formInitJmpAddr, offset); + } + + p->formInitJmpAddr = -1; + p->formInitCodeStart = -1; + + // Record form variable info (even if varCount is 0 but init code exists) + if (varCount > 0 || initAddr >= 0) { + BasFormVarInfoT info; + memset(&info, 0, sizeof(info)); + snprintf(info.formName, sizeof(info.formName), "%s", formName); + info.varCount = varCount; + info.initCodeAddr = initAddr; + info.initCodeLen = initLen; + arrput(p->cg.formVarInfo, info); + p->cg.formVarInfoCount = (int32_t)arrlen(p->cg.formVarInfo); + } +} + + static void parseClose(BasParserT *p) { // CLOSE #channel advance(p); // consume CLOSE @@ -2675,6 +2777,8 @@ static void parseDim(BasParserT *p) { if (p->sym.inLocalScope) { sym->scope = SCOPE_LOCAL; + } else if (p->sym.inFormScope) { + sym->scope = SCOPE_FORM; } else { sym->scope = SCOPE_GLOBAL; } @@ -2896,7 +3000,7 @@ static void parseFor(BasParserT *p) { // Emit FOR_INIT -- sets up the for-loop state in the VM basEmit8(&p->cg, OP_FOR_INIT); basEmitU16(&p->cg, (uint16_t)loopVar->index); - basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : 0); + basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : (loopVar->scope == SCOPE_FORM ? 2 : 0)); int32_t loopBody = basCodePos(&p->cg); @@ -2923,7 +3027,7 @@ static void parseFor(BasParserT *p) { // Emit FOR_NEXT with backward jump to loop body basEmit8(&p->cg, OP_FOR_NEXT); basEmitU16(&p->cg, (uint16_t)loopVar->index); - basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : 0); + basEmit8(&p->cg, loopVar->scope == SCOPE_LOCAL ? 1 : (loopVar->scope == SCOPE_FORM ? 2 : 0)); int16_t backOffset = (int16_t)(loopBody - (basCodePos(&p->cg) + 2)); basEmit16(&p->cg, backOffset); @@ -4731,6 +4835,17 @@ static void parseStatement(BasParserT *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]; @@ -5254,6 +5369,9 @@ void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) { exitListInit(&exitSubList); exitListInit(&exitFuncList); + p->formInitJmpAddr = -1; + p->formInitCodeStart = -1; + addPredefConsts(p); // basLexerInit already primes the first token -- no advance needed diff --git a/apps/dvxbasic/compiler/parser.h b/apps/dvxbasic/compiler/parser.h index c52cfd3..488481d 100644 --- a/apps/dvxbasic/compiler/parser.h +++ b/apps/dvxbasic/compiler/parser.h @@ -34,6 +34,9 @@ typedef struct { bool optionExplicit; // true = variables must be declared with DIM uint8_t defType[26]; // default type per letter (A-Z), set by DEFINT etc. char currentProc[BAS_MAX_TOKEN_LEN]; // name of current SUB/FUNCTION + // Per-form init block tracking + int32_t formInitJmpAddr; // code position of JMP to patch (-1 = none) + int32_t formInitCodeStart; // code position where init block starts (-1 = none) } BasParserT; // ============================================================ diff --git a/apps/dvxbasic/compiler/symtab.c b/apps/dvxbasic/compiler/symtab.c index a0d75ed..23999cf 100644 --- a/apps/dvxbasic/compiler/symtab.c +++ b/apps/dvxbasic/compiler/symtab.c @@ -33,10 +33,24 @@ static bool namesEqual(const char *a, const char *b) { // ============================================================ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) { - // Check for duplicate in current scope - BasScopeE scope = tab->inLocalScope ? SCOPE_LOCAL : SCOPE_GLOBAL; + // Determine scope: local > form > global. + // Only variables get SCOPE_FORM; SUBs/FUNCTIONs/CONSTs remain global. + BasScopeE scope; + if (tab->inLocalScope) { + scope = SCOPE_LOCAL; + } else if (tab->inFormScope && kind == SYM_VARIABLE) { + scope = SCOPE_FORM; + } else { + scope = SCOPE_GLOBAL; + } + + // Check for duplicate in current scope (skip ended form symbols) for (int32_t i = 0; i < tab->count; i++) { + if (tab->symbols[i].formScopeEnded) { + continue; + } + if (tab->symbols[i].scope == scope && namesEqual(tab->symbols[i].name, name)) { return NULL; // duplicate } @@ -67,6 +81,10 @@ int32_t basSymTabAllocSlot(BasSymTabT *tab) { return tab->nextLocalIdx++; } + if (tab->inFormScope) { + return tab->nextFormVarIdx++; + } + return tab->nextGlobalIdx++; } @@ -95,6 +113,17 @@ BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) { } } + // Search form scope (active form-scoped symbols shadow globals) + for (int32_t i = tab->count - 1; i >= 0; i--) { + if (tab->symbols[i].formScopeEnded) { + continue; + } + + if (tab->symbols[i].scope == SCOPE_FORM && namesEqual(tab->symbols[i].name, name)) { + return &tab->symbols[i]; + } + } + // Search global scope return basSymTabFindGlobal(tab, name); } @@ -106,6 +135,10 @@ BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) { BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name) { for (int32_t i = 0; i < tab->count; i++) { + if (tab->symbols[i].formScopeEnded) { + continue; + } + if (tab->symbols[i].scope == SCOPE_GLOBAL && namesEqual(tab->symbols[i].name, name)) { return &tab->symbols[i]; } @@ -150,3 +183,39 @@ void basSymTabLeaveLocal(BasSymTabT *tab) { tab->inLocalScope = false; tab->nextLocalIdx = 0; } + + +// ============================================================ +// basSymTabEnterFormScope +// ============================================================ + +void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName) { + tab->inFormScope = true; + strncpy(tab->formScopeName, formName, BAS_MAX_SYMBOL_NAME - 1); + tab->formScopeName[BAS_MAX_SYMBOL_NAME - 1] = '\0'; + tab->nextFormVarIdx = 0; + tab->formScopeSymStart = tab->count; +} + + +// ============================================================ +// basSymTabLeaveFormScope +// ============================================================ + +int32_t basSymTabLeaveFormScope(BasSymTabT *tab) { + int32_t varCount = tab->nextFormVarIdx; + + // Mark all form-scope symbols added since BEGINFORM as ended + for (int32_t i = tab->formScopeSymStart; i < tab->count; i++) { + if (tab->symbols[i].scope == SCOPE_FORM) { + tab->symbols[i].formScopeEnded = true; + } + } + + tab->inFormScope = false; + tab->formScopeName[0] = '\0'; + tab->nextFormVarIdx = 0; + tab->formScopeSymStart = 0; + + return varCount; +} diff --git a/apps/dvxbasic/compiler/symtab.h b/apps/dvxbasic/compiler/symtab.h index 5259d28..c4c41f5 100644 --- a/apps/dvxbasic/compiler/symtab.h +++ b/apps/dvxbasic/compiler/symtab.h @@ -33,7 +33,8 @@ typedef enum { typedef enum { SCOPE_GLOBAL, - SCOPE_LOCAL + SCOPE_LOCAL, + SCOPE_FORM // per-form variable (persists while form is loaded) } BasScopeE; // ============================================================ @@ -61,6 +62,7 @@ typedef struct { bool isArray; bool isShared; bool isExtern; // true = external library function (DECLARE LIBRARY) + bool formScopeEnded; // true = form scope ended, invisible to lookups int32_t udtTypeId; // for variables of BAS_TYPE_UDT: index of TYPE_DEF symbol int32_t fixedLen; // for STRING * n: fixed length (0 = variable-length) uint16_t externLibIdx; // constant pool index for library name (if isExtern) @@ -94,9 +96,13 @@ typedef struct { typedef struct { BasSymbolT *symbols; // stb_ds dynamic array int32_t count; - int32_t nextGlobalIdx; // next global variable slot - int32_t nextLocalIdx; // next local variable slot (reset per SUB/FUNCTION) - bool inLocalScope; // true when inside SUB/FUNCTION + int32_t nextGlobalIdx; // next global variable slot + int32_t nextLocalIdx; // next local variable slot (reset per SUB/FUNCTION) + bool inLocalScope; // true when inside SUB/FUNCTION + bool inFormScope; // true inside BEGINFORM...ENDFORM + char formScopeName[BAS_MAX_SYMBOL_NAME]; // current form name + int32_t nextFormVarIdx; // next form-level variable slot + int32_t formScopeSymStart; // symbol count at BEGINFORM (for marking ended) } BasSymTabT; // ============================================================ @@ -122,7 +128,14 @@ void basSymTabEnterLocal(BasSymTabT *tab); // Leave local scope (called at END SUB/FUNCTION). Removes local symbols. void basSymTabLeaveLocal(BasSymTabT *tab); -// Allocate the next variable slot (global or local depending on scope). +// Allocate the next variable slot (global, local, or form depending on scope). int32_t basSymTabAllocSlot(BasSymTabT *tab); +// Enter form scope (called at BEGINFORM). Form-level DIMs create SCOPE_FORM variables. +void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName); + +// Leave form scope (called at ENDFORM). Marks form-scope symbols as ended. +// Returns the number of form variables allocated. +int32_t basSymTabLeaveFormScope(BasSymTabT *tab); + #endif // DVXBASIC_SYMTAB_H diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index 19028c1..8438a2a 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -308,6 +308,14 @@ void basFormRtDestroy(BasFormRtT *rt) { for (int32_t i = 0; i < rt->formCount; i++) { BasFormT *form = &rt->forms[i]; + if (form->formVars) { + for (int32_t j = 0; j < form->formVarCount; j++) { + basValRelease(&form->formVars[j]); + } + + free(form->formVars); + } + for (int32_t j = 0; j < form->controlCount; j++) { freeListBoxItems(&form->controls[j]); } @@ -321,6 +329,13 @@ void basFormRtDestroy(BasFormRtT *rt) { } arrfree(rt->forms); + + // Free .frm source cache + for (int32_t i = 0; i < rt->frmCacheCount; i++) { + free(rt->frmCache[i].frmSource); + } + + arrfree(rt->frmCache); free(rt); } @@ -401,9 +416,13 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName return false; } - BasFormT *prevForm = rt->currentForm; + BasFormT *prevForm = rt->currentForm; + BasValueT *prevVars = rt->vm->currentFormVars; + int32_t prevVarCount = rt->vm->currentFormVarCount; + rt->currentForm = form; basVmSetCurrentForm(rt->vm, form); + basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); bool ok; @@ -415,6 +434,7 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName rt->currentForm = prevForm; basVmSetCurrentForm(rt->vm, prevForm); + basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount); return ok; } @@ -443,9 +463,13 @@ static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const c return false; } - BasFormT *prevForm = rt->currentForm; + BasFormT *prevForm = rt->currentForm; + BasValueT *prevVars = rt->vm->currentFormVars; + int32_t prevVarCount = rt->vm->currentFormVarCount; + rt->currentForm = form; basVmSetCurrentForm(rt->vm, form); + basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); bool cancelled = false; @@ -466,6 +490,7 @@ static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const c rt->currentForm = prevForm; basVmSetCurrentForm(rt->vm, prevForm); + basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount); return cancelled; } @@ -569,7 +594,15 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { } } - // Forms start hidden; code must call Show to make them visible + // Check the .frm cache for reload after unload + for (int32_t i = 0; i < rt->frmCacheCount; i++) { + if (strcasecmp(rt->frmCache[i].formName, formName) == 0) { + // Re-parse the cached .frm source (creates window, controls, fires Load) + return basFormRtLoadFrm(rt, rt->frmCache[i].frmSource, rt->frmCache[i].frmSourceLen); + } + } + + // No cache entry — create a bare form (first-time load without .frm file) WindowT *win = dvxCreateWindowCentered(rt->ctx, formName, DEFAULT_FORM_W, DEFAULT_FORM_H, true); if (!win) { @@ -597,11 +630,38 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { win->onBlur = onFormDeactivate; form->window = win; form->root = root; - form->contentBox = NULL; // created lazily after Layout property is known + form->contentBox = NULL; form->ctx = rt->ctx; form->vm = rt->vm; form->module = rt->module; + // Allocate per-form variable storage and run init code from module metadata + if (rt->module && rt->module->formVarInfo) { + for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) { + if (strcasecmp(rt->module->formVarInfo[j].formName, formName) == 0) { + int32_t vc = rt->module->formVarInfo[j].varCount; + + if (vc > 0) { + form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT)); + form->formVarCount = vc; + } + + // Execute per-form init block (DIM arrays, UDT init) + int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr; + + if (initAddr >= 0 && rt->vm) { + basVmSetCurrentForm(rt->vm, form); + basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); + basVmCallSub(rt->vm, initAddr); + basVmSetCurrentForm(rt->vm, NULL); + basVmSetCurrentFormVars(rt->vm, NULL, 0); + } + + break; + } + } + } + return form; } @@ -919,6 +979,34 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen } } + // Cache the .frm source for reload after unload + if (form) { + bool cached = false; + + for (int32_t i = 0; i < rt->frmCacheCount; i++) { + if (strcasecmp(rt->frmCache[i].formName, form->name) == 0) { + cached = true; + break; + } + } + + if (!cached) { + BasFrmCacheT entry; + memset(&entry, 0, sizeof(entry)); + snprintf(entry.formName, BAS_MAX_CTRL_NAME, "%s", form->name); + entry.frmSource = (char *)malloc(sourceLen + 1); + entry.frmSourceLen = sourceLen; + + if (entry.frmSource) { + memcpy(entry.frmSource, source, sourceLen); + entry.frmSource[sourceLen] = '\0'; + } + + arrput(rt->frmCache, entry); + rt->frmCacheCount = (int32_t)arrlen(rt->frmCache); + } + } + // Fire the Load event now that the form and controls are ready if (form) { basFormRtFireEvent(rt, form, form->name, "Load"); @@ -1015,6 +1103,17 @@ void basFormRtUnloadForm(void *ctx, void *formRef) { basFormRtFireEvent(rt, form, form->name, "Unload"); + // Release per-form variables + if (form->formVars) { + for (int32_t i = 0; i < form->formVarCount; i++) { + basValRelease(&form->formVars[i]); + } + + free(form->formVars); + form->formVars = NULL; + form->formVarCount = 0; + } + for (int32_t i = 0; i < form->controlCount; i++) { freeListBoxItems(&form->controls[i]); } @@ -1399,6 +1498,17 @@ static void onFormClose(WindowT *win) { basFormRtFireEvent(sFormRt, form, form->name, "Unload"); + // Release per-form variables + if (form->formVars) { + for (int32_t j = 0; j < form->formVarCount; j++) { + basValRelease(&form->formVars[j]); + } + + free(form->formVars); + form->formVars = NULL; + form->formVarCount = 0; + } + // Free control resources for (int32_t j = 0; j < form->controlCount; j++) { freeListBoxItems(&form->controls[j]); diff --git a/apps/dvxbasic/formrt/formrt.h b/apps/dvxbasic/formrt/formrt.h index 1417f6d..6758787 100644 --- a/apps/dvxbasic/formrt/formrt.h +++ b/apps/dvxbasic/formrt/formrt.h @@ -68,12 +68,22 @@ typedef struct BasFormT { bool frmCentered; bool frmAutoSize; bool frmHBox; // true if Layout = "HBox" + // Per-form variable storage (allocated at load, freed at unload) + BasValueT *formVars; + int32_t formVarCount; } BasFormT; // ============================================================ // Form runtime context // ============================================================ +// Cached .frm source for reload after unload +typedef struct { + char formName[BAS_MAX_CTRL_NAME]; + char *frmSource; // malloc'd copy of .frm text + int32_t frmSourceLen; +} BasFrmCacheT; + typedef struct { AppContextT *ctx; // DVX app context BasVmT *vm; // shared VM instance @@ -81,6 +91,8 @@ typedef struct { BasFormT *forms; // stb_ds dynamic array int32_t formCount; BasFormT *currentForm; // form currently dispatching events + BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources + int32_t frmCacheCount; } BasFormRtT; // ============================================================ diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 65f112e..61cbadb 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -1027,11 +1027,20 @@ static void compileAndRun(void) { } int32_t startLine = line; - int32_t fileLen = (int32_t)strlen(fileSrc); - int32_t copyLen = fileLen; - if (pos + copyLen >= IDE_MAX_SOURCE - 1) { - copyLen = IDE_MAX_SOURCE - 1 - pos; + // Inject BEGINFORM directive for .frm code sections + if (sProject.files[i].isForm && sProject.files[i].formName[0]) { + int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos, + "BEGINFORM \"%s\"\n", sProject.files[i].formName); + pos += dirLen; + line++; + } + + int32_t fileLen = (int32_t)strlen(fileSrc); + int32_t copyLen = fileLen; + + if (pos + copyLen >= IDE_MAX_SOURCE - 64) { + copyLen = IDE_MAX_SOURCE - 64 - pos; } memcpy(concatBuf + pos, fileSrc, copyLen); @@ -1052,6 +1061,13 @@ static void compileAndRun(void) { line++; } + // Inject ENDFORM directive + if (sProject.files[i].isForm && sProject.files[i].formName[0]) { + int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos, "ENDFORM\n"); + pos += dirLen; + line++; + } + { PrjSourceMapT mapEntry; mapEntry.fileIdx = i; diff --git a/apps/dvxbasic/runtime/vm.c b/apps/dvxbasic/runtime/vm.c index 71e30f4..fe9fc95 100644 --- a/apps/dvxbasic/runtime/vm.c +++ b/apps/dvxbasic/runtime/vm.c @@ -399,6 +399,16 @@ void basVmSetCurrentForm(BasVmT *vm, void *formRef) { } +// ============================================================ +// basVmSetCurrentFormVars +// ============================================================ + +void basVmSetCurrentFormVars(BasVmT *vm, BasValueT *vars, int32_t count) { + vm->currentFormVars = vars; + vm->currentFormVarCount = count; +} + + // ============================================================ // basVmSetStepLimit // ============================================================ @@ -955,8 +965,8 @@ BasVmResultE basVmStep(BasVmT *vm) { } case OP_FOR_INIT: { - uint16_t varIdx = readUint16(vm); - uint8_t isLocal = readUint8(vm); + uint16_t varIdx = readUint16(vm); + uint8_t scopeTag = readUint8(vm); BasValueT stepVal; BasValueT limitVal; @@ -973,7 +983,7 @@ BasVmResultE basVmStep(BasVmT *vm) { BasForStateT *fs = &vm->forStack[vm->forDepth++]; fs->varIdx = varIdx; - fs->isLocal = (isLocal != 0); + fs->scopeTag = scopeTag; fs->limit = limitVal; fs->step = stepVal; fs->loopTop = vm->pc; @@ -982,7 +992,7 @@ BasVmResultE basVmStep(BasVmT *vm) { case OP_FOR_NEXT: { uint16_t varIdx = readUint16(vm); - uint8_t isLocal = readUint8(vm); + uint8_t scopeTag = readUint8(vm); int16_t loopTopOffset = readInt16(vm); if (vm->forDepth <= 0) { @@ -997,10 +1007,10 @@ BasVmResultE basVmStep(BasVmT *vm) { return BAS_VM_ERROR; } - // Get pointer to the loop variable (global or local) + // Get pointer to the loop variable BasValueT *varSlot; - if (isLocal) { + if (scopeTag == 1) { BasCallFrameT *frame = currentFrame(vm); if (!frame || varIdx >= (uint16_t)frame->localCount) { @@ -1009,6 +1019,13 @@ BasVmResultE basVmStep(BasVmT *vm) { } varSlot = &frame->locals[varIdx]; + } else if (scopeTag == 2) { + if (!vm->currentFormVars || varIdx >= (uint16_t)vm->currentFormVarCount) { + runtimeError(vm, 9, "Invalid form variable index"); + return BAS_VM_ERROR; + } + + varSlot = &vm->currentFormVars[varIdx]; } else { if (varIdx >= BAS_VM_MAX_GLOBALS) { runtimeError(vm, 9, "Invalid global variable index"); @@ -2905,6 +2922,56 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + // ============================================================ + // Form-level variables + // ============================================================ + + case OP_LOAD_FORM_VAR: { + uint16_t idx = readUint16(vm); + + if (!vm->currentFormVars || idx >= (uint16_t)vm->currentFormVarCount) { + runtimeError(vm, 9, "Form variable access outside form context"); + return BAS_VM_ERROR; + } + + push(vm, basValCopy(vm->currentFormVars[idx])); + break; + } + + case OP_STORE_FORM_VAR: { + uint16_t idx = readUint16(vm); + BasValueT val; + + if (!pop(vm, &val)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (!vm->currentFormVars || idx >= (uint16_t)vm->currentFormVarCount) { + basValRelease(&val); + runtimeError(vm, 9, "Form variable access outside form context"); + return BAS_VM_ERROR; + } + + basValRelease(&vm->currentFormVars[idx]); + vm->currentFormVars[idx] = val; + break; + } + + case OP_PUSH_FORM_ADDR: { + uint16_t idx = readUint16(vm); + + if (!vm->currentFormVars || idx >= (uint16_t)vm->currentFormVarCount) { + runtimeError(vm, 9, "Form variable address outside form context"); + return BAS_VM_ERROR; + } + + BasValueT ref; + ref.type = BAS_TYPE_REF; + ref.refVal = &vm->currentFormVars[idx]; + push(vm, ref); + break; + } + // ============================================================ // External library calls // ============================================================ diff --git a/apps/dvxbasic/runtime/vm.h b/apps/dvxbasic/runtime/vm.h index 4386331..8683eca 100644 --- a/apps/dvxbasic/runtime/vm.h +++ b/apps/dvxbasic/runtime/vm.h @@ -195,7 +195,7 @@ typedef struct { typedef struct { int32_t varIdx; // loop variable slot index - bool isLocal; // true = local, false = global + uint8_t scopeTag; // 0 = global, 1 = local, 2 = form BasValueT limit; // upper bound BasValueT step; // step value int32_t loopTop; // PC of the loop body start @@ -224,21 +224,34 @@ typedef struct { bool isFunction; // true = FUNCTION, false = SUB } BasProcEntryT; +// ============================================================ +// Per-form variable info (how many form-scoped vars each form needs) +// ============================================================ + +typedef struct { + char formName[BAS_MAX_PROC_NAME]; + int32_t varCount; + int32_t initCodeAddr; // offset in module->code for per-form init (-1 = none) + int32_t initCodeLen; // length of init bytecode +} BasFormVarInfoT; + // ============================================================ // Compiled module (output of the compiler) // ============================================================ typedef struct { - uint8_t *code; // p-code bytecode - int32_t codeLen; - BasStringT **constants; // string constant pool - int32_t constCount; - int32_t globalCount; // number of global variable slots needed - int32_t entryPoint; // PC of the first instruction (module-level code) - BasValueT *dataPool; // DATA statement value pool - int32_t dataCount; // number of values in the data pool - BasProcEntryT *procs; // procedure table (SUBs and FUNCTIONs) - int32_t procCount; + uint8_t *code; // p-code bytecode + int32_t codeLen; + BasStringT **constants; // string constant pool + int32_t constCount; + int32_t globalCount; // number of global variable slots needed + int32_t entryPoint; // PC of the first instruction (module-level code) + BasValueT *dataPool; // DATA statement value pool + int32_t dataCount; // number of values in the data pool + BasProcEntryT *procs; // procedure table (SUBs and FUNCTIONs) + int32_t procCount; + BasFormVarInfoT *formVarInfo; // per-form variable counts + int32_t formVarInfoCount; } BasModuleT; // ============================================================ @@ -308,7 +321,9 @@ typedef struct { int32_t externCacheCount; // Current form reference (set during event dispatch) - void *currentForm; + void *currentForm; + BasValueT *currentFormVars; // points to current form's variable storage + int32_t currentFormVarCount; // number of form variables } BasVmT; // ============================================================ @@ -348,6 +363,7 @@ void basVmSetExternCallbacks(BasVmT *vm, const BasExternCallbacksT *ext); // Set the current form context (called by host during event dispatch). void basVmSetCurrentForm(BasVmT *vm, void *formRef); +void basVmSetCurrentFormVars(BasVmT *vm, BasValueT *vars, int32_t count); // Set the step limit for basVmRun. 0 = unlimited (default). // When the limit is reached, basVmRun returns BAS_VM_STEP_LIMIT. diff --git a/apps/dvxbasic/test_compiler.c b/apps/dvxbasic/test_compiler.c index cfa3081..6752eb3 100644 --- a/apps/dvxbasic/test_compiler.c +++ b/apps/dvxbasic/test_compiler.c @@ -2630,6 +2630,123 @@ int main(void) { printf("\n"); } + // ============================================================ + // Coverage: BEGINFORM/ENDFORM with form-level DIM + // ============================================================ + + { + printf("=== Form-level variables ===\n"); + BasParserT parser; + basParserInit(&parser, + "BEGINFORM \"Form1\"\n" + "Dim counter As Integer\n" + "Dim msg As String\n" + "\n" + "Sub Form1_Load ()\n" + " counter = 0\n" + " msg = \"hello\"\n" + "End Sub\n" + "\n" + "Sub Timer1_Timer ()\n" + " counter = counter + 1\n" + "End Sub\n" + "ENDFORM\n", + -1); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n", parser.error); + } else { + printf("OK\n"); + } + + basParserFree(&parser); + printf("\n"); + } + + // ============================================================ + // Coverage: Form-scope arrays compile (init deferred to load) + // ============================================================ + + { + printf("=== Form-scope array ===\n"); + BasParserT parser; + basParserInit(&parser, + "BEGINFORM \"Form1\"\n" + "Dim arr(10) As Integer\n" + "\n" + "Sub Form1_Load ()\n" + " arr(0) = 42\n" + "End Sub\n" + "ENDFORM\n", + -1); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n", parser.error); + } else { + printf("OK\n"); + } + + basParserFree(&parser); + printf("\n"); + } + + // ============================================================ + // Coverage: Two forms with same-named form vars (independent) + // ============================================================ + + { + printf("=== Two forms same var name ===\n"); + BasParserT parser; + basParserInit(&parser, + "BEGINFORM \"Form1\"\n" + "Dim counter As Integer\n" + "Sub Form1_Load ()\n" + " counter = 1\n" + "End Sub\n" + "ENDFORM\n" + "\n" + "BEGINFORM \"Form2\"\n" + "Dim counter As Integer\n" + "Sub Form2_Load ()\n" + " counter = 2\n" + "End Sub\n" + "ENDFORM\n", + -1); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n", parser.error); + } else { + printf("OK\n"); + } + + basParserFree(&parser); + printf("\n"); + } + + // ============================================================ + // Coverage: Nested BEGINFORM rejected + // ============================================================ + + { + printf("=== Nested BEGINFORM rejected ===\n"); + BasParserT parser; + basParserInit(&parser, + "BEGINFORM \"Form1\"\n" + "BEGINFORM \"Form2\"\n" + "ENDFORM\n" + "ENDFORM\n", + -1); + + if (!basParse(&parser)) { + printf("OK (expected error: %s)\n", parser.error); + } else { + printf("FAIL: should have rejected nested BEGINFORM\n"); + } + + basParserFree(&parser); + printf("\n"); + } + printf("All tests complete.\n"); return 0; }