Form level variable scope. Proper Load/Unload.

This commit is contained in:
Scott Duensing 2026-04-03 18:09:50 -05:00
parent dd68a19b5b
commit bf5bf9bb1d
15 changed files with 611 additions and 50 deletions

View file

@ -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

View file

@ -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);
}

View file

@ -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;
// ============================================================

View file

@ -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;

View file

@ -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 &currentFormVars[idx] (ByRef)
// ============================================================
// Array / misc

View file

@ -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

View file

@ -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;
// ============================================================

View file

@ -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;
}

View file

@ -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

View file

@ -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]);

View file

@ -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;
// ============================================================

View file

@ -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;

View file

@ -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
// ============================================================

View file

@ -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.

View file

@ -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;
}