From 17fe1840e327eb0d661f3ca4b9cd57a0f20f0cf8 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Wed, 1 Apr 2026 21:54:27 -0500 Subject: [PATCH] Compiler fixes. Editor fixes. Fixes fixes. --- apps/dvxbasic/compiler/opcodes.h | 1 + apps/dvxbasic/compiler/parser.c | 426 ++++++++++++++--- apps/dvxbasic/formrt/formrt.c | 5 +- apps/dvxbasic/ide/ideMain.c | 700 +++++++++++++++++++++++----- apps/dvxbasic/ide/ideProject.c | 8 +- apps/dvxbasic/runtime/vm.c | 107 +++++ apps/dvxbasic/test_compiler.c | 643 ++++++++++++++++++++++++- core/dvxApp.c | 44 +- core/dvxWidget.h | 1 + mkcd.sh | 4 +- shell/shellApp.c | 23 +- widgets/textInput/widgetTextInput.c | 95 +++- widgets/widgetTextInput.h | 6 + 13 files changed, 1803 insertions(+), 260 deletions(-) diff --git a/apps/dvxbasic/compiler/opcodes.h b/apps/dvxbasic/compiler/opcodes.h index 7dbfa03..158a1e4 100644 --- a/apps/dvxbasic/compiler/opcodes.h +++ b/apps/dvxbasic/compiler/opcodes.h @@ -52,6 +52,7 @@ #define OP_STORE_FIELD 0x19 // [uint16 fieldIdx] store UDT field #define OP_PUSH_LOCAL_ADDR 0x1A // [uint16 idx] push address of local (for ByRef) #define OP_PUSH_GLOBAL_ADDR 0x1B // [uint16 idx] push address of global (for ByRef) +#define OP_STORE_ARRAY_FIELD 0x1C // [uint8 dims, uint16 fieldIdx] value, indices, array on stack // ============================================================ // Arithmetic (integer) diff --git a/apps/dvxbasic/compiler/parser.c b/apps/dvxbasic/compiler/parser.c index 6e758fc..9e85fa0 100644 --- a/apps/dvxbasic/compiler/parser.c +++ b/apps/dvxbasic/compiler/parser.c @@ -104,7 +104,9 @@ static void errorExpected(BasParserT *p, const char *what); static void expect(BasParserT *p, BasTokenTypeE type); static void expectEndOfStatement(BasParserT *p); static const BuiltinFuncT *findBuiltin(const char *name); +static void emitUdtInit(BasParserT *p, int32_t udtTypeId); static BasSymbolT *findTypeDef(BasParserT *p, const char *name); +static BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId); static bool match(BasParserT *p, BasTokenTypeE type); static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName); static uint8_t resolveTypeName(BasParserT *p); @@ -373,6 +375,59 @@ static BasSymbolT *findTypeDef(BasParserT *p, const char *name) { } +static BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId) { + for (int32_t i = 0; i < p->sym.count; i++) { + if (p->sym.symbols[i].kind == SYM_TYPE_DEF && p->sym.symbols[i].index == typeId) { + return &p->sym.symbols[i]; + } + } + return NULL; +} + + +// emitUdtInit -- emit code to initialize nested UDT fields after a UDT +// has been created and is on top of the stack. For each field that is +// itself a UDT, we DUP the parent, allocate the child UDT, and store it +// into the field. + +static void emitUdtInit(BasParserT *p, int32_t udtTypeId) { + BasSymbolT *typeSym = findTypeDefById(p, udtTypeId); + + if (!typeSym) { + return; + } + + for (int32_t i = 0; i < typeSym->fieldCount; i++) { + if (typeSym->fields[i].dataType != BAS_TYPE_UDT) { + continue; + } + + int32_t childTypeId = typeSym->fields[i].udtTypeId; + BasSymbolT *childType = findTypeDefById(p, childTypeId); + + if (!childType) { + continue; + } + + // DUP parent, allocate child UDT, STORE_FIELD + basEmit8(&p->cg, OP_DUP); + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, (int16_t)childTypeId); + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, (int16_t)childType->fieldCount); + basEmit8(&p->cg, OP_DIM_ARRAY); + basEmit8(&p->cg, 0); + basEmit8(&p->cg, BAS_TYPE_UDT); + + // Recursively init the child's nested UDT fields + emitUdtInit(p, childTypeId); + + basEmit8(&p->cg, OP_STORE_FIELD); + basEmitU16(&p->cg, (uint16_t)i); + } +} + + static bool match(BasParserT *p, BasTokenTypeE type) { if (p->lex.token.type == type) { advance(p); @@ -444,7 +499,7 @@ static uint8_t resolveTypeName(BasParserT *p) { static void skipNewlines(BasParserT *p) { - while (check(p, TOK_NEWLINE)) { + while (!p->hasError && check(p, TOK_NEWLINE)) { advance(p); } } @@ -912,24 +967,28 @@ static void parseAddExpr(BasParserT *p) { } +// VB precedence (high to low): ^, unary -, *, /, \, MOD, +, - +// So parseMulExpr calls parseUnaryExpr, which calls parsePowExpr, +// which calls parsePrimary. This makes -2^2 = -(2^2) = -4. + static void parseMulExpr(BasParserT *p) { - parsePowExpr(p); + parseUnaryExpr(p); while (!p->hasError) { if (check(p, TOK_STAR)) { advance(p); - parsePowExpr(p); + parseUnaryExpr(p); basEmit8(&p->cg, OP_MUL_INT); } else if (check(p, TOK_SLASH)) { advance(p); - parsePowExpr(p); + parseUnaryExpr(p); basEmit8(&p->cg, OP_DIV_FLT); } else if (check(p, TOK_BACKSLASH)) { advance(p); - parsePowExpr(p); + parseUnaryExpr(p); basEmit8(&p->cg, OP_IDIV_INT); } else if (check(p, TOK_MOD)) { advance(p); - parsePowExpr(p); + parseUnaryExpr(p); basEmit8(&p->cg, OP_MOD_INT); } else { break; @@ -938,17 +997,6 @@ static void parseMulExpr(BasParserT *p) { } -static void parsePowExpr(BasParserT *p) { - parseUnaryExpr(p); - // Right-associative, but iterative is fine for most BASIC uses - while (!p->hasError && check(p, TOK_CARET)) { - advance(p); - parseUnaryExpr(p); - basEmit8(&p->cg, OP_POW); - } -} - - static void parseUnaryExpr(BasParserT *p) { if (check(p, TOK_MINUS)) { advance(p); @@ -961,7 +1009,17 @@ static void parseUnaryExpr(BasParserT *p) { parseUnaryExpr(p); return; } + parsePowExpr(p); +} + + +static void parsePowExpr(BasParserT *p) { parsePrimary(p); + while (!p->hasError && check(p, TOK_CARET)) { + advance(p); + parsePrimary(p); + basEmit8(&p->cg, OP_POW); + } } @@ -1226,6 +1284,30 @@ static void parsePrimary(BasParserT *p) { expect(p, TOK_RPAREN); basEmit8(&p->cg, OP_LOAD_ARRAY); basEmit8(&p->cg, (uint8_t)dims); + + // Array-of-UDT field access: arr(i).field + if (sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0 && check(p, TOK_DOT)) { + advance(p); // consume DOT + if (!check(p, TOK_IDENT)) { + errorExpected(p, "field name"); + return; + } + BasSymbolT *typeSym = findTypeDefById(p, sym->udtTypeId); + if (typeSym == NULL) { + error(p, "Unknown TYPE definition"); + return; + } + int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text); + if (fieldIdx < 0) { + char buf[512]; + snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name); + error(p, buf); + return; + } + advance(p); // consume field name + basEmit8(&p->cg, OP_LOAD_FIELD); + basEmitU16(&p->cg, (uint16_t)fieldIdx); + } return; } // Unknown function -- forward reference, assume it's a function @@ -1249,32 +1331,38 @@ static void parsePrimary(BasParserT *p) { sym = basSymTabFind(&p->sym, name); if (sym != NULL && sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) { emitLoad(p, sym); - advance(p); // consume DOT - if (!check(p, TOK_IDENT)) { - errorExpected(p, "field name"); - return; - } - BasSymbolT *typeSym = NULL; - for (int32_t i = 0; i < p->sym.count; i++) { - if (p->sym.symbols[i].kind == SYM_TYPE_DEF && p->sym.symbols[i].index == sym->udtTypeId) { - typeSym = &p->sym.symbols[i]; - break; + int32_t curTypeId = sym->udtTypeId; + + // Loop to handle nested UDT field access: a.b.c + while (check(p, TOK_DOT) && curTypeId >= 0) { + advance(p); // consume DOT + if (!check(p, TOK_IDENT)) { + errorExpected(p, "field name"); + return; + } + BasSymbolT *typeSym = findTypeDefById(p, curTypeId); + if (typeSym == NULL) { + error(p, "Unknown TYPE definition"); + return; + } + int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text); + if (fieldIdx < 0) { + char buf[512]; + snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name); + error(p, buf); + return; + } + advance(p); // consume field name + basEmit8(&p->cg, OP_LOAD_FIELD); + basEmitU16(&p->cg, (uint16_t)fieldIdx); + + // If this field is also a UDT, allow further dot access + if (typeSym->fields[fieldIdx].dataType == BAS_TYPE_UDT) { + curTypeId = typeSym->fields[fieldIdx].udtTypeId; + } else { + curTypeId = -1; } } - if (typeSym == NULL) { - error(p, "Unknown TYPE definition"); - return; - } - int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text); - if (fieldIdx < 0) { - char buf[512]; - snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name); - error(p, buf); - return; - } - advance(p); // consume field name - basEmit8(&p->cg, OP_LOAD_FIELD); - basEmitU16(&p->cg, (uint16_t)fieldIdx); return; } @@ -1383,34 +1471,50 @@ static void parseAssignOrCall(BasParserT *p) { // Check for UDT field access first if (sym != NULL && sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0) { emitLoad(p, sym); - advance(p); // consume DOT - if (!check(p, TOK_IDENT)) { - errorExpected(p, "field name"); - return; - } - BasSymbolT *typeSym = NULL; - for (int32_t i = 0; i < p->sym.count; i++) { - if (p->sym.symbols[i].kind == SYM_TYPE_DEF && p->sym.symbols[i].index == sym->udtTypeId) { - typeSym = &p->sym.symbols[i]; - break; + int32_t curTypeId = sym->udtTypeId; + + // Walk the dot chain: a.b.c = expr + // For intermediate fields, emit LOAD_FIELD (navigate into nested UDT). + // For the final field, emit STORE_FIELD with the assigned value. + while (check(p, TOK_DOT) && curTypeId >= 0) { + advance(p); // consume DOT + if (!check(p, TOK_IDENT)) { + errorExpected(p, "field name"); + return; + } + BasSymbolT *typeSym = findTypeDefById(p, curTypeId); + if (typeSym == NULL) { + error(p, "Unknown TYPE definition"); + return; + } + int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text); + if (fieldIdx < 0) { + char buf[512]; + snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name); + error(p, buf); + return; + } + advance(p); // consume field name + + // Check if this field is a nested UDT with more dots coming + bool fieldIsUdt = (typeSym->fields[fieldIdx].dataType == BAS_TYPE_UDT); + + if (fieldIsUdt && check(p, TOK_DOT)) { + // Intermediate level: load this field and continue + basEmit8(&p->cg, OP_LOAD_FIELD); + basEmitU16(&p->cg, (uint16_t)fieldIdx); + curTypeId = typeSym->fields[fieldIdx].udtTypeId; + } else { + // Final field: store value + expect(p, TOK_EQ); + parseExpression(p); + basEmit8(&p->cg, OP_STORE_FIELD); + basEmitU16(&p->cg, (uint16_t)fieldIdx); + return; } } - if (typeSym == NULL) { - error(p, "Unknown TYPE definition"); - return; - } - int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text); - if (fieldIdx < 0) { - char buf[512]; - snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name); - error(p, buf); - return; - } - advance(p); // consume field name - expect(p, TOK_EQ); - parseExpression(p); - basEmit8(&p->cg, OP_STORE_FIELD); - basEmitU16(&p->cg, (uint16_t)fieldIdx); + + error(p, "Expected '=' in UDT field assignment"); return; } @@ -1549,6 +1653,34 @@ static void parseAssignOrCall(BasParserT *p) { } expect(p, TOK_RPAREN); + // Array-of-UDT field store: arr(i).field = expr + if (sym->dataType == BAS_TYPE_UDT && sym->udtTypeId >= 0 && check(p, TOK_DOT)) { + advance(p); // consume DOT + if (!check(p, TOK_IDENT)) { + errorExpected(p, "field name"); + return; + } + BasSymbolT *typeSym = findTypeDefById(p, sym->udtTypeId); + if (typeSym == NULL) { + error(p, "Unknown TYPE definition"); + return; + } + int32_t fieldIdx = resolveFieldIndex(typeSym, p->lex.token.text); + if (fieldIdx < 0) { + char buf[512]; + snprintf(buf, sizeof(buf), "Unknown field '%s' in TYPE '%s'", p->lex.token.text, typeSym->name); + error(p, buf); + return; + } + advance(p); // consume field name + expect(p, TOK_EQ); + parseExpression(p); + basEmit8(&p->cg, OP_STORE_ARRAY_FIELD); + basEmit8(&p->cg, (uint8_t)dims); + basEmitU16(&p->cg, (uint16_t)fieldIdx); + return; + } + expect(p, TOK_EQ); parseExpression(p); @@ -1584,7 +1716,19 @@ static void parseAssignOrCall(BasParserT *p) { } // Sub call without parens: SUBName arg1, arg2 ... - if (sym != NULL && sym->kind == SYM_SUB) { + // If the identifier is unknown, treat it as a forward-referenced sub. + if (sym == NULL) { + sym = basSymTabAdd(&p->sym, name, SYM_SUB, BAS_TYPE_INTEGER); + if (sym == NULL) { + error(p, "Symbol table full"); + return; + } + sym->scope = SCOPE_GLOBAL; + sym->isDefined = false; + sym->codeAddr = 0; + } + + if (sym->kind == SYM_SUB) { int32_t argc = 0; if (!check(p, TOK_NEWLINE) && !check(p, TOK_EOF) && !check(p, TOK_COLON) && !check(p, TOK_ELSE)) { if (argc < sym->paramCount && !sym->paramByVal[argc]) { @@ -2358,20 +2502,24 @@ static void parseDim(BasParserT *p) { } if (isArray) { - // Emit array dimension instruction + 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 = NULL; - for (int32_t i = 0; i < p->sym.count; i++) { - if (p->sym.symbols[i].kind == SYM_TYPE_DEF && p->sym.symbols[i].index == udtTypeId) { - typeSym = &p->sym.symbols[i]; - break; - } - } + BasSymbolT *typeSym = findTypeDefById(p, udtTypeId); if (typeSym != NULL) { basEmit8(&p->cg, OP_PUSH_INT16); basEmit16(&p->cg, (int16_t)udtTypeId); @@ -2381,6 +2529,8 @@ static void parseDim(BasParserT *p) { 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); } } @@ -3118,6 +3268,21 @@ static void parseModule(BasParserT *p) { skipNewlines(p); } + // Check for unresolved forward references (skip externs from DECLARE LIBRARY) + if (!p->hasError) { + for (int32_t i = 0; i < p->sym.count; i++) { + BasSymbolT *sym = &p->sym.symbols[i]; + + if ((sym->kind == SYM_SUB || sym->kind == SYM_FUNCTION) && !sym->isDefined && !sym->isExtern) { + char buf[256]; + snprintf(buf, sizeof(buf), "Undefined %s: %s", + sym->kind == SYM_SUB ? "Sub" : "Function", sym->name); + error(p, buf); + break; + } + } + } + // End of module -- emit HALT basEmit8(&p->cg, OP_HALT); } @@ -4252,6 +4417,115 @@ static void parseStatement(BasParserT *p) { basEmit8(&p->cg, OP_POP); break; + case TOK_ME: { + // Me.Show / Me.Hide / Me.CtrlName.Property = expr + advance(p); // consume Me + if (!check(p, TOK_DOT)) { + errorExpected(p, "'.' after Me"); + break; + } + advance(p); // consume DOT + + if (!check(p, TOK_IDENT) && !check(p, TOK_SHOW) && !check(p, TOK_HIDE)) { + errorExpected(p, "method or member name after Me."); + break; + } + + char meMember[BAS_MAX_TOKEN_LEN]; + strncpy(meMember, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); + meMember[BAS_MAX_TOKEN_LEN - 1] = '\0'; + advance(p); + + if (strcasecmp(meMember, "Show") == 0) { + // Me.Show [modal] + basEmit8(&p->cg, OP_ME_REF); + uint8_t modal = 0; + if (check(p, TOK_INT_LIT)) { + if (p->lex.token.intVal != 0) { + modal = 1; + } + advance(p); + } + 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_DOT)) { + // Me.CtrlName.Property = expr (control access via Me) + advance(p); // consume second 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); + + // Push form ref (Me), ctrl name, FIND_CTRL + 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); + basEmit8(&p->cg, OP_FIND_CTRL); + + if (check(p, TOK_EQ)) { + // Property assignment + advance(p); + uint16_t propIdx = basAddConstant(&p->cg, propName, (int32_t)strlen(propName)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, propIdx); + parseExpression(p); + basEmit8(&p->cg, OP_STORE_PROP); + } else { + // Method call + uint16_t methodIdx = basAddConstant(&p->cg, propName, (int32_t)strlen(propName)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, methodIdx); + int32_t argc = 0; + while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) { + if (argc > 0 && check(p, TOK_COMMA)) { + advance(p); + } + parseExpression(p); + argc++; + } + basEmit8(&p->cg, OP_CALL_METHOD); + basEmit8(&p->cg, (uint8_t)argc); + basEmit8(&p->cg, OP_POP); + } + } else if (check(p, TOK_EQ)) { + // Me.Property = expr (form-level property set) + advance(p); // consume = + basEmit8(&p->cg, OP_ME_REF); + uint16_t propIdx = basAddConstant(&p->cg, meMember, (int32_t)strlen(meMember)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, propIdx); + parseExpression(p); + basEmit8(&p->cg, OP_STORE_PROP); + } else { + // Me.Method [args] (form-level method call) + basEmit8(&p->cg, OP_ME_REF); + uint16_t methodIdx = basAddConstant(&p->cg, meMember, (int32_t)strlen(meMember)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, methodIdx); + int32_t argc = 0; + while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) { + if (argc > 0 && check(p, TOK_COMMA)) { + advance(p); + } + parseExpression(p); + argc++; + } + basEmit8(&p->cg, OP_CALL_METHOD); + basEmit8(&p->cg, (uint8_t)argc); + basEmit8(&p->cg, OP_POP); + } + break; + } + case TOK_LET: advance(p); // consume LET, then fall through to assignment if (!check(p, TOK_IDENT)) { diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index 232986e..67a4ffe 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -898,7 +898,10 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { } form->window->visible = true; - dvxFitWindow(rt->ctx, form->window); + + if (form->frmAutoSize) { + dvxFitWindow(rt->ctx, form->window); + } if (modal) { rt->ctx->modalWindow = form->window; diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 9bcbc5e..194af82 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -86,6 +86,7 @@ #define CMD_PRJ_REMOVE 136 #define CMD_PRJ_PROPS 138 #define CMD_WIN_PROJECT 137 +#define CMD_HELP_ABOUT 140 #define IDE_MAX_IMM 1024 #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 @@ -97,12 +98,14 @@ int32_t appMain(DxeAppContextT *ctx); static void buildWindow(void); static void clearOutput(void); +static int cmpStrPtrs(const void *a, const void *b); static void compileAndRun(void); static void ensureProject(const char *filePath); static void freeProcBufs(void); static const char *getFullSource(void); static void loadFile(void); static void parseProcs(const char *source); +static void updateProjectMenuState(void); static void saveActiveFile(void); static void saveCurProc(void); static void showProc(int32_t procIdx); @@ -136,8 +139,10 @@ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufS static bool doEventsCallback(void *ctx); static void runCached(void); static void runModule(BasModuleT *mod); +static void onEditorChange(WidgetT *w); static void setStatus(const char *text); static void switchToCode(void); +static void updateDirtyIndicators(void); static void switchToDesign(void); static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y); static void onFormWinKey(WindowT *win, int32_t key, int32_t mod); @@ -151,6 +156,7 @@ static void onTbStop(WidgetT *w); static void onTbOpen(WidgetT *w); static void onTbCode(WidgetT *w); static void onTbDesign(WidgetT *w); +static void selectDropdowns(const char *objName, const char *evtName); static void updateDropdowns(void); // ============================================================ @@ -182,10 +188,8 @@ static WindowT *sProjectWin = NULL; static PrjStateT sProject; static WindowT *sLastFocusWin = NULL; // last focused non-toolbar window -static char sSourceBuf[IDE_MAX_SOURCE]; static char sOutputBuf[IDE_MAX_OUTPUT]; static int32_t sOutputLen = 0; -static char sFilePath[DVX_MAX_PATH]; // Procedure view state -- the editor shows one procedure at a time. // Each procedure is stored in its own malloc'd buffer. The editor @@ -204,6 +208,7 @@ typedef struct { static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array static const char **sObjItems = NULL; // stb_ds dynamic array static const char **sEvtItems = NULL; // stb_ds dynamic array +static bool sDropdownNavSuppressed = false; // ============================================================ // App descriptor @@ -255,8 +260,6 @@ int32_t appMain(DxeAppContextT *ctx) { dvxFitWindowH(sAc, sWin); } - sFilePath[0] = '\0'; - sSourceBuf[0] = '\0'; sOutputBuf[0] = '\0'; sOutputLen = 0; @@ -274,6 +277,7 @@ int32_t appMain(DxeAppContextT *ctx) { dvxSetTitle(sAc, sWin, title); } + updateProjectMenuState(); setStatus("Ready."); return 0; } @@ -372,6 +376,9 @@ static void buildWindow(void) { wmAddMenuItem(winMenu, "&Toolbox", CMD_WIN_TOOLBOX); wmAddMenuItem(winMenu, "&Properties", CMD_WIN_PROPS); + MenuT *helpMenu = wmAddMenu(menuBar, "&Help"); + wmAddMenuItem(helpMenu, "&About DVX BASIC...", CMD_HELP_ABOUT); + AccelTableT *accel = dvxCreateAccelTable(); dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN); dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_SAVE); @@ -598,6 +605,34 @@ static void clearOutput(void) { setOutputText(""); } + +static int cmpStrPtrs(const void *a, const void *b) { + const char *sa = *(const char **)a; + const char *sb = *(const char **)b; + return strcasecmp(sa, sb); +} + + +// Sort event names: implemented (no brackets) first, then unimplemented +// ([brackets]), alphabetically within each group. + +static int cmpEvtPtrs(const void *a, const void *b) { + const char *sa = *(const char **)a; + const char *sb = *(const char **)b; + bool aImpl = (sa[0] != '['); + bool bImpl = (sb[0] != '['); + + if (aImpl != bImpl) { + return aImpl ? -1 : 1; + } + + // Skip brackets for alphabetical comparison + if (sa[0] == '[') { sa++; } + if (sb[0] == '[') { sb++; } + return strcasecmp(sa, sb); +} + + // ============================================================ // compileAndRun // ============================================================ @@ -627,6 +662,8 @@ static void compileAndRun(void) { } } } + + updateDirtyIndicators(); } clearOutput(); @@ -958,6 +995,17 @@ static void runModule(BasModuleT *mod) { break; } + // VB-style event loop: after module-level code finishes, + // keep processing events as long as any form is loaded. + // The program ends when all forms are unloaded (closed). + if (result == BAS_VM_HALTED && formRt->formCount > 0) { + setStatus("Running (event loop)..."); + + while (sWin && sAc->running && formRt->formCount > 0) { + dvxUpdate(sAc); + } + } + sVm = NULL; // Update output display @@ -1128,18 +1176,24 @@ static void loadFilePath(const char *path) { return; } - int32_t bytesRead = (int32_t)fread(sSourceBuf, 1, size, f); + char *srcBuf = (char *)malloc(size + 1); + + if (!srcBuf) { + fclose(f); + return; + } + + int32_t bytesRead = (int32_t)fread(srcBuf, 1, size, f); fclose(f); - sSourceBuf[bytesRead] = '\0'; + srcBuf[bytesRead] = '\0'; if (!sCodeWin) { showCodeWindow(); } // Parse into per-procedure buffers and show (General) section - parseProcs(sSourceBuf); - - snprintf(sFilePath, sizeof(sFilePath), "%s", path); + parseProcs(srcBuf); + free(srcBuf); if (sFormWin) { dvxDestroyWindow(sAc, sFormWin); @@ -1149,6 +1203,11 @@ static void loadFilePath(const char *path) { dsgnFree(&sDesigner); updateDropdowns(); showProc(-1); // show (General) section + + if (sEditor && !sEditor->onChange) { + sEditor->onChange = onEditorChange; + } + setStatus("File loaded."); } @@ -1250,14 +1309,12 @@ static void ensureProject(const char *filePath) { if (sWin) { dvxSetTitle(sAc, sWin, title); } + + updateProjectMenuState(); } static void loadFile(void) { - if (!promptAndSave()) { - return; - } - FileFilterT filters[] = { { "BASIC Files (*.bas)", "*.bas" }, { "Form Files (*.frm)", "*.frm" }, @@ -1266,12 +1323,36 @@ static void loadFile(void) { char path[DVX_MAX_PATH]; - if (dvxFileDialog(sAc, "Open BASIC File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) { - closeProject(); - ensureProject(path); + if (!dvxFileDialog(sAc, "Open BASIC File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) { + return; + } - const char *ext = strrchr(path, '.'); - bool isForm = (ext && strcasecmp(ext, ".frm") == 0); + const char *ext = strrchr(path, '.'); + bool isForm = (ext && strcasecmp(ext, ".frm") == 0); + + if (sProject.projectPath[0] != '\0') { + // Add the file to the current project + const char *fileName = strrchr(path, '/'); + const char *fileName2 = strrchr(path, '\\'); + + if (fileName2 > fileName) { + fileName = fileName2; + } + + fileName = fileName ? fileName + 1 : path; + + prjAddFile(&sProject, fileName, isForm); + prjRebuildTree(&sProject); + + int32_t fileIdx = sProject.fileCount - 1; + onPrjFileClick(fileIdx, isForm); + } else { + // No project -- create one from this file + if (!promptAndSave()) { + return; + } + + ensureProject(path); if (isForm) { onPrjFileClick(0, true); @@ -1346,37 +1427,11 @@ static void saveActiveFile(void) { } setStatus("Saved."); + updateDirtyIndicators(); } static void saveFile(void) { - // If no project or no active file, prompt for a path if (sProject.projectPath[0] == '\0' || sProject.activeFileIdx < 0) { - if (sFilePath[0] == '\0') { - FileFilterT filters[] = { - { "BASIC Files (*.bas)", "*.bas" }, - { "All Files (*.*)", "*.*" } - }; - - if (!dvxFileDialog(sAc, "Save BASIC File", FD_SAVE, NULL, filters, 2, sFilePath, sizeof(sFilePath))) { - return; - } - - ensureProject(sFilePath); - } - - // Save editor to sFilePath directly - const char *src = sEditor ? wgtGetText(sEditor) : NULL; - - if (src) { - FILE *f = fopen(sFilePath, "w"); - - if (f) { - fputs(src, f); - fclose(f); - } - } - - setStatus("Saved."); return; } @@ -1402,6 +1457,8 @@ static void saveFile(void) { } } } + + updateDirtyIndicators(); } @@ -1563,6 +1620,10 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) { } } + if (sEditor && !sEditor->onChange) { + sEditor->onChange = onEditorChange; + } + sProject.activeFileIdx = fileIdx; } } @@ -1636,6 +1697,7 @@ static void newProject(void) { dvxSetTitle(sAc, sWin, title); setStatus("New project created."); + updateProjectMenuState(); } @@ -1687,6 +1749,7 @@ static void openProject(void) { dvxSetTitle(sAc, sWin, title); setStatus("Project loaded."); + updateProjectMenuState(); } @@ -1717,6 +1780,8 @@ static void closeProject(void) { if (sStatus) { setStatus("Project closed."); } + + updateProjectMenuState(); } @@ -1941,12 +2006,12 @@ void ideRenameInCode(const char *oldName, const char *newName) { // ============================================================ static void onCodeWinClose(WindowT *win) { - // Stash code back to form->code before the window is destroyed + // Stash code back to form->code before the window is destroyed. + // This is just caching -- do not mark dirty. if (sDesigner.form && sCurProcIdx >= -1) { saveCurProc(); free(sDesigner.form->code); - sDesigner.form->code = strdup(getFullSource()); - sDesigner.form->dirty = true; + sDesigner.form->code = strdup(getFullSource()); } dvxDestroyWindow(sAc, win); @@ -2220,6 +2285,7 @@ static void onMenu(WindowT *win, int32_t menuId) { } setStatus("All files saved."); + updateDirtyIndicators(); break; case CMD_RUN: @@ -2251,6 +2317,9 @@ static void onMenu(WindowT *win, int32_t menuId) { case CMD_WIN_CODE: showCodeWindow(); + if (sEditor && !sEditor->onChange) { + sEditor->onChange = onEditorChange; + } break; case CMD_WIN_OUTPUT: @@ -2379,6 +2448,12 @@ static void onMenu(WindowT *win, int32_t menuId) { case CMD_PRJ_REMOVE: if (sProject.activeFileIdx >= 0) { PrjFileT *rmFile = &sProject.files[sProject.activeFileIdx]; + char rmMsg[DVX_MAX_PATH + 32]; + snprintf(rmMsg, sizeof(rmMsg), "Remove %s from the project?", rmFile->path); + + if (dvxMessageBox(sAc, "Remove File", rmMsg, MB_YESNO | MB_ICONQUESTION) != ID_YES) { + break; + } if (rmFile->modified) { int32_t result = dvxPromptSave(sAc, "DVX BASIC"); @@ -2413,6 +2488,16 @@ static void onMenu(WindowT *win, int32_t menuId) { onClose(sWin); } break; + + case CMD_HELP_ABOUT: + dvxMessageBox(sAc, "About DVX BASIC", + "DVX BASIC 1.0\n" + "Visual BASIC Development Environment\n" + "for the DVX GUI System\n" + "\n" + "Copyright 2026 Scott Duensing", + MB_OK | MB_ICONINFO); + break; } } @@ -2425,6 +2510,10 @@ static void onMenu(WindowT *win, int32_t menuId) { static void onEvtDropdownChange(WidgetT *w) { (void)w; + if (sDropdownNavSuppressed) { + return; + } + if (!sObjDropdown || !sEvtDropdown || !sEditor) { return; } @@ -2440,6 +2529,12 @@ static void onEvtDropdownChange(WidgetT *w) { const char *selObj = sObjItems[objIdx]; const char *selEvt = sEvtItems[evtIdx]; + // (Global) shows the General module-level section + if (strcasecmp(selEvt, "(Global)") == 0) { + showProc(-1); + return; + } + // Strip brackets if present (unimplemented event) char evtName[64]; if (selEvt[0] == '[') { @@ -2464,7 +2559,9 @@ static void onEvtDropdownChange(WidgetT *w) { } } - // Not found -- create a new sub skeleton + // Not found -- create a new sub skeleton for editing. + // Don't mark dirty yet; saveCurProc will discard it if the + // user doesn't add any code. char subName[128]; snprintf(subName, sizeof(subName), "%s_%s", selObj, evtName); @@ -2473,13 +2570,6 @@ static void onEvtDropdownChange(WidgetT *w) { arrput(sProcBufs, strdup(skeleton)); - // Update form code if editing a form - if (sDesigner.form) { - free(sDesigner.form->code); - sDesigner.form->code = strdup(getFullSource()); - sDesigner.form->dirty = true; - } - updateDropdowns(); // Show the new proc (last in the list) @@ -2603,7 +2693,9 @@ static void onObjDropdownChange(WidgetT *w) { const char **availEvents = sCommonEvents; if (strcasecmp(selObj, "(General)") == 0) { - // (General) has no standard events -- just show existing procs + // Always include (Global) to access module-level code + arrput(sEvtItems, "(Global)"); + for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) { arrput(sEvtItems, existingEvts[i]); } @@ -2611,12 +2703,16 @@ static void onObjDropdownChange(WidgetT *w) { arrfree(existingEvts); int32_t evtCount = (int32_t)arrlen(sEvtItems); - wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount); - if (evtCount > 0) { - wgtDropdownSetSelected(sEvtDropdown, 0); + // Sort procs after (Global) + if (evtCount > 2) { + qsort(sEvtItems + 1, evtCount - 1, sizeof(const char *), cmpStrPtrs); } + wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount); + wgtDropdownSetSelected(sEvtDropdown, 0); + onEvtDropdownChange(sEvtDropdown); + return; } @@ -2739,10 +2835,64 @@ static void onObjDropdownChange(WidgetT *w) { arrfree(existingEvts); int32_t evtCount = (int32_t)arrlen(sEvtItems); + + // Sort: implemented events first, then unimplemented, alphabetical within each + if (evtCount > 1) { + qsort(sEvtItems, evtCount, sizeof(const char *), cmpEvtPtrs); + } + wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount); if (evtCount > 0) { - wgtDropdownSetSelected(sEvtDropdown, 0); + // Find the first implemented event (no brackets) + int32_t selectIdx = -1; + + for (int32_t i = 0; i < evtCount; i++) { + if (sEvtItems[i][0] != '[') { + selectIdx = i; + break; + } + } + + if (selectIdx >= 0) { + // Navigate to the first implemented event + wgtDropdownSetSelected(sEvtDropdown, selectIdx); + onEvtDropdownChange(sEvtDropdown); + } else { + // No implemented events -- find and select the default event + // for this widget type, which will create its skeleton + const char *defEvt = NULL; + + if (isForm) { + defEvt = dsgnDefaultEvent("Form"); + } else if (sDesigner.form) { + for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { + if (strcasecmp(sDesigner.form->controls[i].name, selObj) == 0) { + defEvt = dsgnDefaultEvent(sDesigner.form->controls[i].typeName); + break; + } + } + } + + if (!defEvt) { + defEvt = "Click"; + } + + // Find the default event in the list (it will be bracketed) + for (int32_t i = 0; i < evtCount; i++) { + const char *label = sEvtItems[i]; + + if (label[0] == '[') { + label++; + } + + if (strncasecmp(label, defEvt, strlen(defEvt)) == 0) { + wgtDropdownSetSelected(sEvtDropdown, i); + onEvtDropdownChange(sEvtDropdown); + break; + } + } + } } } @@ -2875,6 +3025,51 @@ static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) { } +// selectDropdowns -- set the Object and Event dropdowns to match a +// given control name and event name. + +// selectDropdowns -- set the Object and Event dropdown selections to +// match a given control/event without triggering navigation callbacks. +// The caller is responsible for having already navigated to the proc. + +static void selectDropdowns(const char *objName, const char *evtName) { + if (!sObjDropdown || !sEvtDropdown) { + return; + } + + // Select the object + int32_t objCount = (int32_t)arrlen(sObjItems); + + for (int32_t i = 0; i < objCount; i++) { + if (strcasecmp(sObjItems[i], objName) == 0) { + wgtDropdownSetSelected(sObjDropdown, i); + break; + } + } + + // Rebuild the event list for this object but suppress navigation + sDropdownNavSuppressed = true; + onObjDropdownChange(sObjDropdown); + sDropdownNavSuppressed = false; + + // Now select the specific event + int32_t evtCount = (int32_t)arrlen(sEvtItems); + + for (int32_t i = 0; i < evtCount; i++) { + const char *label = sEvtItems[i]; + + if (label[0] == '[') { + label++; + } + + if (strncasecmp(label, evtName, strlen(evtName)) == 0) { + wgtDropdownSetSelected(sEvtDropdown, i); + break; + } + } +} + + // navigateToEventSub -- open code editor at the default event sub for the // selected control (or form). Creates the sub skeleton if it doesn't exist. // Code is stored in the .frm file's code section (sDesigner.form->code). @@ -2917,7 +3112,11 @@ static void navigateToEventSub(void) { return; } + // Populate dropdown items without triggering navigation -- + // we navigate explicitly below after finding the target proc. + sDropdownNavSuppressed = true; updateDropdowns(); + sDropdownNavSuppressed = false; // Search for existing procedure int32_t procCount = (int32_t)arrlen(sProcTable); @@ -2929,28 +3128,31 @@ static void navigateToEventSub(void) { if (strcasecmp(fullName, subName) == 0) { switchToCode(); showProc(i); + if (sEditor && !sEditor->onChange) { + sEditor->onChange = onEditorChange; + } + selectDropdowns(ctrlName, eventName); return; } } - // Not found -- create a new sub and add it as a procedure buffer + // Not found -- create a new sub skeleton for editing. + // Don't mark dirty yet; saveCurProc will discard it if the + // user doesn't add any code. char skeleton[256]; snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName); - char *newBuf = strdup(skeleton); - arrput(sProcBufs, newBuf); - - sDesigner.form->dirty = true; - - // Save code back to form - free(sDesigner.form->code); - sDesigner.form->code = strdup(getFullSource()); + arrput(sProcBufs, strdup(skeleton)); updateDropdowns(); // Show the new procedure (it's the last one) switchToCode(); showProc((int32_t)arrlen(sProcBufs) - 1); + if (sEditor && !sEditor->onChange) { + sEditor->onChange = onEditorChange; + } + selectDropdowns(ctrlName, eventName); } @@ -3008,6 +3210,8 @@ static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) } lastButtons = buttons; + + updateDirtyIndicators(); } @@ -3087,7 +3291,8 @@ static void onFormWinClose(WindowT *win) { // ============================================================ static void switchToCode(void) { - // Stash form data + // Stash form data so the project system has a current copy. + // This does not mark the file as modified -- it's just caching. if (sDesigner.form && sProject.activeFileIdx >= 0) { PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; @@ -3106,8 +3311,6 @@ static void switchToCode(void) { free(frmBuf); cur->buffer = NULL; } - - cur->modified = true; } } } @@ -3136,44 +3339,9 @@ static void switchToDesign(void) { return; } - // Load .frm if we don't have a form yet + // If no form is loaded, create a blank one if (!sDesigner.form) { - if (sFilePath[0]) { - char frmPath[DVX_MAX_PATH]; - snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); - char *dot = strrchr(frmPath, '.'); - - if (dot && (dot - frmPath) + 4 < DVX_MAX_PATH) { - snprintf(dot, sizeof(frmPath) - (dot - frmPath), ".frm"); - } else if ((int32_t)strlen(frmPath) + 4 < DVX_MAX_PATH) { - snprintf(frmPath + strlen(frmPath), sizeof(frmPath) - strlen(frmPath), ".frm"); - } - - FILE *f = fopen(frmPath, "r"); - - if (f) { - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - if (size > 0 && size < IDE_MAX_SOURCE) { - char *buf = (char *)malloc(size + 1); - - if (buf) { - int32_t bytesRead = (int32_t)fread(buf, 1, size, f); - buf[bytesRead] = '\0'; - dsgnLoadFrm(&sDesigner, buf, bytesRead); - free(buf); - } - } - - fclose(f); - } - } - - if (!sDesigner.form) { - dsgnNewForm(&sDesigner, "Form1"); - } + dsgnNewForm(&sDesigner, "Form1"); } // Create the form designer window (same size as runtime) @@ -3188,8 +3356,10 @@ static void switchToDesign(void) { return; } - sFormWin->onClose = onFormWinClose; - sDesigner.formWin = sFormWin; + sFormWin->onClose = onFormWinClose; + sFormWin->onMenu = onMenu; + sFormWin->accelTable = sWin ? sWin->accelTable : NULL; + sDesigner.formWin = sFormWin; WidgetT *root = wgtInitWindow(sAc, sFormWin); WidgetT *contentBox; @@ -3307,12 +3477,12 @@ static void showCodeWindow(void) { wgtTextAreaSetColorize(sEditor, basicColorize, NULL); wgtTextAreaSetShowLineNumbers(sEditor, true); wgtTextAreaSetAutoIndent(sEditor, true); + wgtTextAreaSetCaptureTabs(sEditor, true); + wgtTextAreaSetTabWidth(sEditor, 3); + wgtTextAreaSetUseTabChar(sEditor, false); - if (sFilePath[0] && sSourceBuf[0]) { - wgtSetText(sEditor, sSourceBuf); - } - - updateDropdowns(); + // onChange is set after initial content is loaded by the caller + // (navigateToEventSub, onPrjFileClick, etc.) to prevent false dirty marking. } } @@ -3332,8 +3502,10 @@ static void showOutputWindow(void) { sOutWin = dvxCreateWindow(sAc, "Output", 0, outY, sAc->display.width / 2, outH, true); if (sOutWin) { - sOutWin->onFocus = onContentFocus; - sLastFocusWin = sOutWin; + sOutWin->onFocus = onContentFocus; + sOutWin->onMenu = onMenu; + sOutWin->accelTable = sWin ? sWin->accelTable : NULL; + sLastFocusWin = sOutWin; WidgetT *outRoot = wgtInitWindow(sAc, sOutWin); sOutput = wgtTextArea(outRoot, IDE_MAX_OUTPUT); @@ -3362,8 +3534,10 @@ static void showImmediateWindow(void) { sImmWin = dvxCreateWindow(sAc, "Immediate", sAc->display.width / 2, outY, sAc->display.width / 2, outH, true); if (sImmWin) { - sImmWin->onFocus = onContentFocus; - sLastFocusWin = sImmWin; + sImmWin->onFocus = onContentFocus; + sImmWin->onMenu = onMenu; + sImmWin->accelTable = sWin ? sWin->accelTable : NULL; + sLastFocusWin = sImmWin; WidgetT *immRoot = wgtInitWindow(sAc, sImmWin); @@ -3389,6 +3563,23 @@ static void showImmediateWindow(void) { // setStatus // ============================================================ +static void onEditorChange(WidgetT *w) { + (void)w; + + // Mark the active file as modified + if (sProject.activeFileIdx >= 0 && sProject.activeFileIdx < sProject.fileCount) { + sProject.files[sProject.activeFileIdx].modified = true; + + // Only mark form dirty when editing the form's code, not a .bas file + if (sProject.files[sProject.activeFileIdx].isForm && sDesigner.form) { + sDesigner.form->dirty = true; + } + } + + updateDirtyIndicators(); +} + + static void setStatus(const char *text) { if (sStatus) { wgtSetText(sStatus, text); @@ -3396,6 +3587,83 @@ static void setStatus(const char *text) { } +// ============================================================ +// updateDirtyIndicators -- update window titles and project tree +// with "*" markers when files have unsaved changes. +// ============================================================ + +static void updateProjectMenuState(void) { + if (!sWin || !sWin->menuBar) { + return; + } + + bool hasProject = (sProject.projectPath[0] != '\0'); + + wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_SAVE, hasProject); + wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_CLOSE, hasProject); + wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_PROPS, hasProject); + wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, hasProject); + wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE, hasProject); + wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, hasProject); +} + + +static void updateDirtyIndicators(void) { + // Sync form->dirty to the project file entry so the tree, title + // bar, and save logic all see a single consistent modified flag. + if (sDesigner.form && sDesigner.form->dirty) { + int32_t idx = sProject.activeFileIdx; + if (idx >= 0 && idx < sProject.fileCount && sProject.files[idx].isForm) { + sProject.files[idx].modified = true; + } + } + + // Toolbar title: "DVX BASIC - [ProjectName] *" + if (sWin && sProject.projectPath[0] != '\0') { + char title[300]; + bool anyDirty = sProject.dirty; + + if (!anyDirty) { + for (int32_t i = 0; i < sProject.fileCount; i++) { + if (sProject.files[i].modified) { + anyDirty = true; + break; + } + } + } + + snprintf(title, sizeof(title), "DVX BASIC - [%s]%s", + sProject.name, anyDirty ? " *" : ""); + dvxSetTitle(sAc, sWin, title); + } + + // Code window title -- only shows * when code has been edited + if (sCodeWin) { + bool codeDirty = false; + + if (sProject.activeFileIdx >= 0) { + codeDirty = sProject.files[sProject.activeFileIdx].modified; + } + + dvxSetTitle(sAc, sCodeWin, codeDirty ? "Code *" : "Code"); + } + + // Design window title + if (sFormWin && sDesigner.form) { + char title[280]; + snprintf(title, sizeof(title), "%s [Design]%s", + sDesigner.form->caption[0] ? sDesigner.form->caption : sDesigner.form->name, + sDesigner.form->dirty ? " *" : ""); + dvxSetTitle(sAc, sFormWin, title); + } + + // Project tree: rebuild with "*" on dirty files + if (sProjectWin) { + prjRebuildTree(&sProject); + } +} + + // ============================================================ // updateDropdowns // ============================================================ @@ -3716,8 +3984,175 @@ static void saveCurProc(void) { updateDropdowns(); } } else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) { - free(sProcBufs[sCurProcIdx]); - sProcBufs[sCurProcIdx] = strdup(edText); + // Get the name of the current proc so we can identify its block + // regardless of position in the editor. + char ownName[128] = ""; + + if (sCurProcIdx < (int32_t)arrlen(sProcTable)) { + snprintf(ownName, sizeof(ownName), "%s_%s", + sProcTable[sCurProcIdx].objName, + sProcTable[sCurProcIdx].evtName); + } + + // If ownName is empty (General proc without underscore), extract + // it from the first Sub/Function line of the existing buffer. + if (ownName[0] == '\0' || strcmp(ownName, "(General)_") == 0) { + const char *ep = sProcBufs[sCurProcIdx]; + while (*ep == ' ' || *ep == '\t') { ep++; } + if (strncasecmp(ep, "SUB ", 4) == 0) { ep += 4; } + else if (strncasecmp(ep, "FUNCTION ", 9) == 0) { ep += 9; } + while (*ep == ' ' || *ep == '\t') { ep++; } + int32_t n = 0; + while (*ep && *ep != '(' && *ep != ' ' && *ep != '\t' && *ep != '\n' && n < 127) { + ownName[n++] = *ep++; + } + ownName[n] = '\0'; + } + + // Count Sub/Function blocks in the editor text + int32_t blockCount = 0; + const char *scan = edText; + + while (*scan) { + const char *tl = scan; + while (*tl == ' ' || *tl == '\t') { tl++; } + if (strncasecmp(tl, "SUB ", 4) == 0 || strncasecmp(tl, "FUNCTION ", 9) == 0) { + blockCount++; + } + while (*scan && *scan != '\n') { scan++; } + if (*scan == '\n') { scan++; } + } + + if (blockCount <= 1) { + // Single proc (or none) -- check for empty skeleton + const char *p = edText; + while (*p == ' ' || *p == '\t') { p++; } + bool isSub = (strncasecmp(p, "SUB ", 4) == 0); + const char *endTag = isSub ? "END SUB" : "END FUNCTION"; + int32_t endTagLen = isSub ? 7 : 12; + + // Skip declaration line + while (*p && *p != '\n') { p++; } + if (*p == '\n') { p++; } + + bool bodyEmpty = true; + + while (*p) { + const char *line = p; + while (*line == ' ' || *line == '\t') { line++; } + if (strncasecmp(line, endTag, endTagLen) == 0) { break; } + while (*p && *p != '\n') { + if (*p != ' ' && *p != '\t' && *p != '\r') { bodyEmpty = false; } + p++; + } + if (*p == '\n') { p++; } + } + + if (bodyEmpty) { + free(sProcBufs[sCurProcIdx]); + arrdel(sProcBufs, sCurProcIdx); + sCurProcIdx = -2; + updateDropdowns(); + } else { + free(sProcBufs[sCurProcIdx]); + sProcBufs[sCurProcIdx] = strdup(edText); + } + } else { + // Multiple proc blocks in the editor. Find the one matching + // ownName, keep it in this buffer, extract the rest. + const char *pos = edText; + const char *ownStart = NULL; + const char *ownEnd = NULL; + char *extras = (char *)malloc(strlen(edText) + 1); + int32_t extPos = 0; + + if (!extras) { + free(sProcBufs[sCurProcIdx]); + sProcBufs[sCurProcIdx] = strdup(edText); + } else { + while (*pos) { + const char *lineStart = pos; + const char *tl = pos; + while (*tl == ' ' || *tl == '\t') { tl++; } + + bool isSub = (strncasecmp(tl, "SUB ", 4) == 0); + bool isFunc = (strncasecmp(tl, "FUNCTION ", 9) == 0); + + if (isSub || isFunc) { + // Extract the proc name + const char *np = tl + (isSub ? 4 : 9); + while (*np == ' ' || *np == '\t') { np++; } + char name[128]; + int32_t nn = 0; + while (*np && *np != '(' && *np != ' ' && *np != '\t' && *np != '\n' && nn < 127) { + name[nn++] = *np++; + } + name[nn] = '\0'; + + // Find the matching End tag + const char *endTag = isSub ? "END SUB" : "END FUNCTION"; + int32_t endTagLen = isSub ? 7 : 12; + const char *s = pos; + while (*s && *s != '\n') { s++; } + if (*s == '\n') { s++; } + + while (*s) { + const char *sl = s; + while (*sl == ' ' || *sl == '\t') { sl++; } + if (strncasecmp(sl, endTag, endTagLen) == 0) { + while (*s && *s != '\n') { s++; } + if (*s == '\n') { s++; } + break; + } + while (*s && *s != '\n') { s++; } + if (*s == '\n') { s++; } + } + + if (strcasecmp(name, ownName) == 0) { + ownStart = lineStart; + ownEnd = s; + } else { + // Copy this block to extras + int32_t blen = (int32_t)(s - lineStart); + memcpy(extras + extPos, lineStart, blen); + extPos += blen; + } + + pos = s; + continue; + } + + // Non-proc line (blank lines between blocks etc.) -- skip + while (*pos && *pos != '\n') { pos++; } + if (*pos == '\n') { pos++; } + } + + extras[extPos] = '\0'; + + // Update this buffer with just the owned proc + if (ownStart && ownEnd) { + int32_t keepLen = (int32_t)(ownEnd - ownStart); + char *kept = (char *)malloc(keepLen + 1); + if (kept) { + memcpy(kept, ownStart, keepLen); + kept[keepLen] = '\0'; + free(sProcBufs[sCurProcIdx]); + sProcBufs[sCurProcIdx] = kept; + } + } + + // Extract extra procs into their own buffers + if (extPos > 0) { + extractNewProcs(extras); + } + + free(extras); + + // Update editor to show only this proc + wgtSetText(sEditor, sProcBufs[sCurProcIdx]); + updateDropdowns(); + } + } } } @@ -3734,6 +4169,10 @@ static void showProc(int32_t procIdx) { saveCurProc(); } + // Suppress onChange while loading -- setting text is not a user edit + void (*savedOnChange)(WidgetT *) = sEditor->onChange; + sEditor->onChange = NULL; + if (procIdx == -1) { wgtSetText(sEditor, sGeneralBuf ? sGeneralBuf : ""); sCurProcIdx = -1; @@ -3741,6 +4180,8 @@ static void showProc(int32_t procIdx) { wgtSetText(sEditor, sProcBufs[procIdx] ? sProcBufs[procIdx] : ""); sCurProcIdx = procIdx; } + + sEditor->onChange = savedOnChange; } @@ -3767,7 +4208,7 @@ static const char *getFullSource(void) { for (int32_t i = 0; i < procCount; i++) { if (sProcBufs[i]) { totalLen += (int32_t)strlen(sProcBufs[i]); - totalLen += 1; // newline separator + totalLen += 2; // newline + blank line between procedures } } @@ -3800,6 +4241,11 @@ static const char *getFullSource(void) { if (pos > 0 && sFullSourceCache[pos - 1] != '\n') { sFullSourceCache[pos++] = '\n'; } + + // Blank line between procedures + if (i < procCount - 1) { + sFullSourceCache[pos++] = '\n'; + } } } @@ -3971,7 +4417,13 @@ static void updateDropdowns(void) { } } + // Sort object items alphabetically, keeping (General) first int32_t objCount = (int32_t)arrlen(sObjItems); + + if (objCount > 1) { + qsort(sObjItems + 1, objCount - 1, sizeof(const char *), cmpStrPtrs); + } + wgtDropdownSetItems(sObjDropdown, sObjItems, objCount); if (objCount > 0) { diff --git a/apps/dvxbasic/ide/ideProject.c b/apps/dvxbasic/ide/ideProject.c index db015b4..7ce169c 100644 --- a/apps/dvxbasic/ide/ideProject.c +++ b/apps/dvxbasic/ide/ideProject.c @@ -469,7 +469,9 @@ void prjRebuildTree(PrjStateT *prj) { for (int32_t i = 0; i < prj->fileCount; i++) { if (prj->files[i].isForm) { - char *label = strdup(prj->files[i].path); + char buf[DVX_MAX_PATH + 4]; + snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? " *" : ""); + char *label = strdup(buf); arrput(sLabels, label); WidgetT *item = wgtTreeItem(formsNode, label); item->userData = (void *)(intptr_t)i; @@ -486,7 +488,9 @@ void prjRebuildTree(PrjStateT *prj) { for (int32_t i = 0; i < prj->fileCount; i++) { if (!prj->files[i].isForm) { - char *label = strdup(prj->files[i].path); + char buf[DVX_MAX_PATH + 4]; + snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? " *" : ""); + char *label = strdup(buf); arrput(sLabels, label); WidgetT *item = wgtTreeItem(modsNode, label); item->userData = (void *)(intptr_t)i; diff --git a/apps/dvxbasic/runtime/vm.c b/apps/dvxbasic/runtime/vm.c index bbd7a06..fbe124f 100644 --- a/apps/dvxbasic/runtime/vm.c +++ b/apps/dvxbasic/runtime/vm.c @@ -1811,6 +1811,24 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + // For UDT arrays, parser pushes typeId and fieldCount after bounds + int32_t udtTypeId = -1; + int32_t udtFieldCnt = 0; + + if (elementType == BAS_TYPE_UDT) { + BasValueT fcVal; + BasValueT tiVal; + + if (!pop(vm, &fcVal) || !pop(vm, &tiVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + udtFieldCnt = (int32_t)basValToNumber(fcVal); + udtTypeId = (int32_t)basValToNumber(tiVal); + basValRelease(&fcVal); + basValRelease(&tiVal); + } + // Normal array allocation: parser pushes (lbound, ubound) pairs per dim int32_t lbounds[BAS_ARRAY_MAX_DIMS]; int32_t ubounds[BAS_ARRAY_MAX_DIMS]; @@ -1837,6 +1855,22 @@ BasVmResultE basVmStep(BasVmT *vm) { return BAS_VM_OUT_OF_MEMORY; } + // Initialize UDT array elements with proper UDT instances + if (elementType == BAS_TYPE_UDT && udtTypeId >= 0) { + for (int32_t i = 0; i < arr->totalElements; i++) { + BasUdtT *udt = basUdtNew(udtTypeId, udtFieldCnt); + + if (!udt) { + basArrayFree(arr); + runtimeError(vm, 7, "Out of memory allocating TYPE array elements"); + return BAS_VM_OUT_OF_MEMORY; + } + + arr->elements[i].type = BAS_TYPE_UDT; + arr->elements[i].udtVal = udt; + } + } + BasValueT arrVal; arrVal.type = BAS_TYPE_ARRAY; arrVal.arrVal = arr; @@ -1952,6 +1986,79 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + case OP_STORE_ARRAY_FIELD: { + uint8_t dims = readUint8(vm); + uint16_t fieldIdx = readUint16(vm); + + // Pop value to store + BasValueT storeVal; + + if (!pop(vm, &storeVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + // Pop indices in reverse order + int32_t indices[BAS_ARRAY_MAX_DIMS]; + + for (int32_t d = dims - 1; d >= 0; d--) { + BasValueT idxVal; + + if (!pop(vm, &idxVal)) { + basValRelease(&storeVal); + return BAS_VM_STACK_UNDERFLOW; + } + + indices[d] = (int32_t)basValToNumber(idxVal); + basValRelease(&idxVal); + } + + // Pop array reference + BasValueT arrRef; + + if (!pop(vm, &arrRef)) { + basValRelease(&storeVal); + return BAS_VM_STACK_UNDERFLOW; + } + + if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { + basValRelease(&arrRef); + basValRelease(&storeVal); + runtimeError(vm, 13, "Not an array"); + return BAS_VM_TYPE_MISMATCH; + } + + int32_t flatIdx = basArrayIndex(arrRef.arrVal, indices, dims); + + if (flatIdx < 0) { + basValRelease(&arrRef); + basValRelease(&storeVal); + runtimeError(vm, 9, "Subscript out of range"); + return BAS_VM_SUBSCRIPT_RANGE; + } + + // Element must be a UDT + BasValueT *elem = &arrRef.arrVal->elements[flatIdx]; + + if (elem->type != BAS_TYPE_UDT || !elem->udtVal) { + basValRelease(&arrRef); + basValRelease(&storeVal); + runtimeError(vm, 13, "Array element is not a TYPE instance"); + return BAS_VM_TYPE_MISMATCH; + } + + if (fieldIdx >= (uint16_t)elem->udtVal->fieldCount) { + basValRelease(&arrRef); + basValRelease(&storeVal); + runtimeError(vm, 9, "Invalid field index"); + return BAS_VM_ERROR; + } + + basValRelease(&elem->udtVal->fields[fieldIdx]); + elem->udtVal->fields[fieldIdx] = storeVal; + basValRelease(&arrRef); + break; + } + case OP_REDIM: { uint8_t dims = readUint8(vm); uint8_t preserve = readUint8(vm); diff --git a/apps/dvxbasic/test_compiler.c b/apps/dvxbasic/test_compiler.c index a29c014..e403043 100644 --- a/apps/dvxbasic/test_compiler.c +++ b/apps/dvxbasic/test_compiler.c @@ -1629,14 +1629,649 @@ int main(void) { // Expected: 3 / 42 / 3 / 3 / [ 42] // ============================================================ - // Coverage: Me keyword + // Coverage: Me keyword (compilation only -- no form context) // ============================================================ - runProgram("Me keyword", - "' Me compiles to OP_ME_REF (returns NULL outside form context)\n" + runProgram("Me keyword compiles", + "' Me compiles to OP_ME_REF\n" "PRINT \"ok\"\n" ); - // Expected: ok (just verify Me doesn't crash compilation) + + // ============================================================ + // Coverage: Me.Show / Me.Hide as statements + // ============================================================ + + { + printf("=== Me.Show / Me.Hide statements ===\n"); + + // These compile -- runtime needs a form context to actually show/hide + const char *src = + "Sub Form1_Load ()\n" + " Me.Show\n" + " Me.Hide\n" + " PRINT \"me ok\"\n" + "End Sub\n"; + + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n\n", parser.error); + basParserFree(&parser); + } else { + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (mod) { + printf("Compiled OK, proc=%s\n", mod->procs[0].name); + basModuleFree(mod); + } + } + + printf("\n"); + } + + // ============================================================ + // Coverage: Me keyword with form context + // ============================================================ + { + printf("=== Me keyword with form context ===\n"); + + const char *src = + "Sub Form1_Load ()\n" + " PRINT \"load fired\"\n" + "End Sub\n" + "\n" + "Sub Command1_Click ()\n" + " PRINT \"click fired\"\n" + "End Sub\n" + "\n" + "Sub Text1_KeyPress (KeyAscii As Integer)\n" + " PRINT \"key:\"; KeyAscii\n" + "End Sub\n" + "\n" + "Sub Picture1_MouseDown (Button As Integer, X As Integer, Y As Integer)\n" + " PRINT \"mouse:\"; Button; X; Y\n" + "End Sub\n"; + + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n\n", parser.error); + basParserFree(&parser); + } else { + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (!mod) { + printf("MODULE BUILD FAILED\n\n"); + } else { + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount; + vm->callDepth = 1; + + // Test: find and call Form1_Load (no args) + const BasProcEntryT *loadProc = basModuleFindProc(mod, "Form1_Load"); + + if (loadProc) { + basVmCallSub(vm, loadProc->codeAddr); + } else { + printf("Form1_Load not found!\n"); + } + + // Test: find and call Command1_Click (no args) + const BasProcEntryT *clickProc = basModuleFindProc(mod, "Command1_Click"); + + if (clickProc) { + basVmCallSub(vm, clickProc->codeAddr); + } else { + printf("Command1_Click not found!\n"); + } + + // Test: find and call Text1_KeyPress with parameter + const BasProcEntryT *keyProc = basModuleFindProc(mod, "Text1_KeyPress"); + + if (keyProc) { + BasValueT args[1]; + args[0] = basValLong(65); // 'A' + basVmCallSubWithArgs(vm, keyProc->codeAddr, args, 1); + } else { + printf("Text1_KeyPress not found!\n"); + } + + // Test: find and call Picture1_MouseDown with 3 parameters + const BasProcEntryT *mouseProc = basModuleFindProc(mod, "Picture1_MouseDown"); + + if (mouseProc) { + BasValueT args[3]; + args[0] = basValLong(1); // left button + args[1] = basValLong(50); // X + args[2] = basValLong(100); // Y + basVmCallSubWithArgs(vm, mouseProc->codeAddr, args, 3); + } else { + printf("Picture1_MouseDown not found!\n"); + } + + // Test: verify proc table + printf("Proc count: %d\n", (int)mod->procCount); + + for (int32_t i = 0; i < mod->procCount; i++) { + printf(" [%d] %s (params=%d)\n", (int)i, mod->procs[i].name, (int)mod->procs[i].paramCount); + } + + basVmDestroy(vm); + basModuleFree(mod); + } + } + + printf("\n"); + } + + // ============================================================ + // Coverage: Event parameter mismatch + // ============================================================ + + { + printf("=== Event parameter mismatch ===\n"); + + const char *src = + "Sub NoParams ()\n" + " PRINT \"no params ok\"\n" + "End Sub\n" + "\n" + "Sub OneParam (X As Integer)\n" + " PRINT \"X =\"; X\n" + "End Sub\n" + "\n" + "Sub TwoParams (A As Integer, B As Integer)\n" + " PRINT \"A =\"; A; \"B =\"; B\n" + "End Sub\n"; + + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n\n", parser.error); + basParserFree(&parser); + } else { + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (mod) { + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount; + vm->callDepth = 1; + + // Call NoParams with no args + const BasProcEntryT *p = basModuleFindProc(mod, "NoParams"); + + if (p) { + basVmCallSub(vm, p->codeAddr); + } + + // Call OneParam with 1 arg + p = basModuleFindProc(mod, "OneParam"); + + if (p) { + BasValueT args[1]; + args[0] = basValLong(42); + basVmCallSubWithArgs(vm, p->codeAddr, args, 1); + } + + // Call TwoParams with 2 args + p = basModuleFindProc(mod, "TwoParams"); + + if (p) { + BasValueT args[2]; + args[0] = basValLong(10); + args[1] = basValLong(20); + basVmCallSubWithArgs(vm, p->codeAddr, args, 2); + } + + basVmDestroy(vm); + basModuleFree(mod); + } + } + + printf("\n"); + } + + // ============================================================ + // Coverage: Procedure extraction from source + // ============================================================ + + runProgram("Mixed module-level and procedures", + "DIM x As Integer\n" + "x = 10\n" + "PRINT \"module:\"; x\n" + "\n" + "Sub Helper ()\n" + " PRINT \"helper called\"\n" + "End Sub\n" + "\n" + "Function Add (a As Integer, b As Integer) As Integer\n" + " Add = a + b\n" + "End Function\n" + "\n" + "CALL Helper\n" + "PRINT \"sum:\"; Add(3, 4)\n" + ); + + // ============================================================ + // Coverage: ? shortcut for PRINT + // ============================================================ + + runProgram("? shortcut for PRINT", + "? \"hello\"\n" + "? 1 + 2\n" + ); + + // ============================================================ + // Coverage: Code in .frm format (Sub after form definition) + // ============================================================ + + { + printf("=== FRM code extraction ===\n"); + + // Simulate what the IDE does: code section from a .frm + const char *frmCode = + "Sub Form1_Load ()\n" + " PRINT \"form loaded\"\n" + "End Sub\n" + "\n" + "Sub Command1_Click ()\n" + " PRINT \"button clicked\"\n" + "End Sub\n"; + + int32_t len = (int32_t)strlen(frmCode); + BasParserT parser; + basParserInit(&parser, frmCode, len); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n\n", parser.error); + basParserFree(&parser); + } else { + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (mod) { + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount; + vm->callDepth = 1; + + // Fire both events + const BasProcEntryT *p1 = basModuleFindProc(mod, "Form1_Load"); + const BasProcEntryT *p2 = basModuleFindProc(mod, "Command1_Click"); + + if (p1) { basVmCallSub(vm, p1->codeAddr); } + else { printf("Form1_Load NOT FOUND\n"); } + + if (p2) { basVmCallSub(vm, p2->codeAddr); } + else { printf("Command1_Click NOT FOUND\n"); } + + basVmDestroy(vm); + basModuleFree(mod); + } + } + + printf("\n"); + } + + // ============================================================ + // Coverage: RANDOMIZE TIMER + // ============================================================ + + runProgram("RANDOMIZE TIMER", + "RANDOMIZE TIMER\n" + "DIM x AS SINGLE\n" + "x = RND\n" + "PRINT \"rnd ok\"\n" + ); + + // ============================================================ + // Coverage: Forward GOSUB + // ============================================================ + + runProgram("Forward GOSUB", + "GOSUB doWork\n" + "PRINT \"back\"\n" + "END\n" + "doWork:\n" + "PRINT \"working\"\n" + "RETURN\n" + ); + + // ============================================================ + // Coverage: Forward GOTO + // ============================================================ + + runProgram("Forward GOTO to label", + "GOTO skip\n" + "PRINT \"should not print\"\n" + "skip:\n" + "PRINT \"jumped\"\n" + ); + + // ============================================================ + // Coverage: Array of UDT + // ============================================================ + + runProgram("Array of UDT", + "TYPE PointT\n" + " x AS INTEGER\n" + " y AS INTEGER\n" + "END TYPE\n" + "\n" + "DIM pts(3) AS PointT\n" + "pts(1).x = 10\n" + "pts(1).y = 20\n" + "pts(2).x = 30\n" + "pts(2).y = 40\n" + "PRINT pts(1).x; pts(1).y\n" + "PRINT pts(2).x; pts(2).y\n" + ); + + // ============================================================ + // Coverage: Nested UDTs + // ============================================================ + + runProgram("Nested UDT", + "TYPE AddressT\n" + " city AS STRING\n" + " zip AS INTEGER\n" + "END TYPE\n" + "\n" + "TYPE PersonT\n" + " name AS STRING\n" + " addr AS AddressT\n" + "END TYPE\n" + "\n" + "DIM p AS PersonT\n" + "p.name = \"Alice\"\n" + "p.addr.city = \"NYC\"\n" + "p.addr.zip = 10001\n" + "PRINT p.name\n" + "PRINT p.addr.city\n" + "PRINT p.addr.zip\n" + ); + + // ============================================================ + // Coverage: Operator precedence stress + // ============================================================ + + runProgram("Operator precedence", + "' NOT has highest unary, then AND, OR, XOR, EQV, IMP\n" + "PRINT NOT 0 AND -1\n" // NOT 0 = -1, -1 AND -1 = -1 + "PRINT (1 > 0) AND (2 > 1)\n" // True AND True = -1 + "PRINT (1 > 0) OR (2 < 1)\n" // True OR False = -1 + "PRINT 5 AND 3\n" // bitwise: 1 + "PRINT 5 OR 3\n" // bitwise: 7 + "PRINT 5 XOR 3\n" // bitwise: 6 + "PRINT 2 + 3 * 4 - 1\n" // 2 + 12 - 1 = 13 + "PRINT (2 + 3) * (4 - 1)\n" // 5 * 3 = 15 + "PRINT -2 ^ 2\n" // -(2^2) = -4 (VB precedence) + ); + + // ============================================================ + // Coverage: Nested FOR with EXIT + // ============================================================ + + runProgram("Nested FOR with EXIT FOR", + "DIM i AS INTEGER\n" + "DIM j AS INTEGER\n" + "FOR i = 1 TO 3\n" + " FOR j = 1 TO 3\n" + " IF j = 2 THEN EXIT FOR\n" + " PRINT i; j\n" + " NEXT j\n" + "NEXT i\n" + ); + + // ============================================================ + // Coverage: Forward reference CALL (sub defined after use) + // ============================================================ + + runProgram("Forward CALL reference", + "CALL doWork\n" + "PRINT \"after call\"\n" + "\n" + "Sub doWork ()\n" + " PRINT \"in sub\"\n" + "End Sub\n" + ); + + // ============================================================ + // Coverage: Multiple CONST declarations + // ============================================================ + + runProgram("Multiple CONST", + "CONST PI = 3.14159\n" + "CONST E = 2.71828\n" + "CONST NAME = \"DVX\"\n" + "PRINT INT(PI * 100)\n" + "PRINT INT(E * 100)\n" + "PRINT NAME\n" + ); + + // ============================================================ + // Coverage: SELECT CASE with strings + // ============================================================ + + runProgram("SELECT CASE strings", + "DIM s AS STRING\n" + "s = \"hello\"\n" + "SELECT CASE s\n" + " CASE \"world\"\n" + " PRINT \"wrong\"\n" + " CASE \"hello\"\n" + " PRINT \"right\"\n" + " CASE ELSE\n" + " PRINT \"default\"\n" + "END SELECT\n" + ); + + // ============================================================ + // Coverage: Nested IF/ELSEIF chains + // ============================================================ + + runProgram("Nested IF chains", + "DIM x AS INTEGER\n" + "x = 50\n" + "IF x > 90 THEN\n" + " PRINT \"A\"\n" + "ELSEIF x > 80 THEN\n" + " PRINT \"B\"\n" + "ELSEIF x > 70 THEN\n" + " PRINT \"C\"\n" + "ELSEIF x > 60 THEN\n" + " PRINT \"D\"\n" + "ELSE\n" + " PRINT \"F\"\n" + "END IF\n" + ); + + // ============================================================ + // Coverage: DO WHILE with EXIT DO + // ============================================================ + + runProgram("DO WHILE with EXIT DO", + "DIM i AS INTEGER\n" + "i = 0\n" + "DO WHILE i < 100\n" + " i = i + 1\n" + " IF i = 5 THEN EXIT DO\n" + "LOOP\n" + "PRINT i\n" + ); + + // ============================================================ + // Coverage: Recursive Fibonacci + // ============================================================ + + runProgram("Recursive Fibonacci", + "Function Fib (n As Integer) As Integer\n" + " IF n <= 1 THEN\n" + " Fib = n\n" + " ELSE\n" + " Fib = Fib(n - 1) + Fib(n - 2)\n" + " END IF\n" + "End Function\n" + "\n" + "PRINT Fib(0); Fib(1); Fib(5); Fib(10)\n" + ); + + // ============================================================ + // Coverage: String comparison operators + // ============================================================ + + runProgram("String comparison ops", + "IF \"abc\" < \"def\" THEN PRINT \"lt ok\"\n" + "IF \"abc\" <= \"abc\" THEN PRINT \"le ok\"\n" + "IF \"def\" > \"abc\" THEN PRINT \"gt ok\"\n" + "IF \"abc\" >= \"abc\" THEN PRINT \"ge ok\"\n" + "IF \"abc\" <> \"def\" THEN PRINT \"ne ok\"\n" + "IF \"abc\" = \"abc\" THEN PRINT \"eq ok\"\n" + ); + + // ============================================================ + // Coverage: LOAD/UNLOAD/Me statements compile + // ============================================================ + + { + printf("=== Form statements compile ===\n"); + + const char *src = + "Sub Form1_Load ()\n" + " Me.Show\n" + " Me.Hide\n" + " Load frmOther\n" + " Unload frmOther\n" + "End Sub\n"; + + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n", parser.error); + basParserFree(&parser); + } else { + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (mod) { + printf("OK: %d procs, %d bytes\n", (int)mod->procCount, (int)mod->codeLen); + basModuleFree(mod); + } + } + + printf("\n"); + } + + // ============================================================ + // Coverage: Me.Property assignment compiles + // ============================================================ + + { + printf("=== Me.Property assignment ===\n"); + + const char *src = + "Sub Form1_Load ()\n" + " Me.Caption = \"Hello\"\n" + " Me.Visible = 1\n" + "End Sub\n"; + + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n", parser.error); + basParserFree(&parser); + } else { + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (mod) { + printf("OK\n"); + basModuleFree(mod); + } + } + + printf("\n"); + } + + // ============================================================ + // Coverage: Me.Control.Property compiles + // ============================================================ + + { + printf("=== Me.Control.Property ===\n"); + + const char *src = + "Sub Form1_Load ()\n" + " Me.Text1.Text = \"hello\"\n" + " Me.Label1.Caption = \"world\"\n" + "End Sub\n"; + + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n", parser.error); + basParserFree(&parser); + } else { + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (mod) { + printf("OK\n"); + basModuleFree(mod); + } + } + + printf("\n"); + } + + // ============================================================ + // Coverage: DOEVENTS compiles + // ============================================================ + + { + printf("=== DOEVENTS compiles ===\n"); + + const char *src = + "Sub Form1_Load ()\n" + " DoEvents\n" + "End Sub\n"; + + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n", parser.error); + basParserFree(&parser); + } else { + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (mod) { + printf("OK\n"); + basModuleFree(mod); + } + } + + printf("\n"); + } printf("All tests complete.\n"); return 0; diff --git a/core/dvxApp.c b/core/dvxApp.c index 852ad51..4afbd49 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -2893,7 +2893,7 @@ static void pollKeyboard(AppContextT *ctx) { WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; if (win->widgetRoot) { - // Find currently focused widget + // Find the currently focused widget WidgetT *current = NULL; WidgetT **fstack = NULL; arrput(fstack, win->widgetRoot); @@ -2903,11 +2903,6 @@ static void pollKeyboard(AppContextT *ctx) { arrsetlen(fstack, arrlen(fstack) - 1); if (w->focused && widgetIsFocusable(w->type)) { - if (w->wclass && (w->wclass->flags & WCLASS_SWALLOWS_TAB)) { - current = NULL; - break; - } - current = w; break; } @@ -2919,37 +2914,16 @@ static void pollKeyboard(AppContextT *ctx) { } } - // Terminal swallowed Tab -- send to widget system instead - if (current == NULL) { - arrsetlen(fstack, 0); - arrput(fstack, win->widgetRoot); - bool termFocused = false; - - while (arrlen(fstack) > 0) { - WidgetT *w = fstack[arrlen(fstack) - 1]; - arrsetlen(fstack, arrlen(fstack) - 1); - - if (w->focused && w->wclass && (w->wclass->flags & WCLASS_SWALLOWS_TAB)) { - termFocused = true; - break; - } - - for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - if (c->visible) { - arrput(fstack, c); - } - } + // If the focused widget wants Tab, send it there + // instead of cycling focus. + if (current && (current->swallowTab || + (current->wclass && (current->wclass->flags & WCLASS_SWALLOWS_TAB)))) { + if (win->onKey) { + WIN_CALLBACK(ctx, win, win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags)); } - if (termFocused) { - // Terminal has focus -- send Tab to it - if (win->onKey) { - WIN_CALLBACK(ctx, win, win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags)); - } - - arrfree(fstack); - continue; - } + arrfree(fstack); + continue; } WidgetT *next; diff --git a/core/dvxWidget.h b/core/dvxWidget.h index fbd96f6..4e69ddb 100644 --- a/core/dvxWidget.h +++ b/core/dvxWidget.h @@ -249,6 +249,7 @@ typedef struct WidgetT { bool enabled; bool readOnly; bool focused; + bool swallowTab; // Tab key goes to widget, not focus nav char accelKey; // lowercase accelerator character, 0 if none // User data and callbacks. These fire for ALL widget types from the diff --git a/mkcd.sh b/mkcd.sh index 9123329..843723c 100755 --- a/mkcd.sh +++ b/mkcd.sh @@ -38,13 +38,13 @@ echo "$WGT_COUNT widget modules found in bin/widgets/." # Create the ISO image # -iso-level 1: strict 8.3 filenames (DOS compatibility) # -J: Joliet extensions (long names for Windows/Linux) -# -R: Rock Ridge (long names for Linux) # -V: volume label +# Note: -R (Rock Ridge) is omitted because DOS surfaces its +# attribute sidecar files (._ATR_) as visible junk files. echo "Creating ISO image..." mkisofs \ -iso-level 1 \ -J \ - -R \ -V "DVX" \ -o "$ISO_PATH" \ "$SCRIPT_DIR/bin/" diff --git a/shell/shellApp.c b/shell/shellApp.c index 042c4b7..abc4c2e 100644 --- a/shell/shellApp.c +++ b/shell/shellApp.c @@ -445,23 +445,16 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { app->dxeCtx->appDir[1] = '\0'; } - // Derive config directory: replace the APPS/ prefix with CONFIG/. - // e.g. "APPS/GAMES/TETRIS" -> "CONFIG/GAMES/TETRIS" - // If the path doesn't start with "apps/" or "APPS/", fall back to - // "CONFIG/" using the descriptor name. - const char *appDirStr = app->dxeCtx->appDir; - const char *appsPrefix = NULL; + // Derive config directory: mirror the app directory under CONFIG/. + // e.g. "APPS/DVXBASIC" -> "CONFIG/APPS/DVXBASIC" + // If the path doesn't start with a recognized prefix, fall back to + // "CONFIG/APPS/" using the descriptor name. + const char *appDirStr = app->dxeCtx->appDir; - if (strncasecmp(appDirStr, "apps/", 5) == 0 || strncasecmp(appDirStr, "apps\\", 5) == 0) { - appsPrefix = appDirStr + 5; - } else if (strncasecmp(appDirStr, "APPS/", 5) == 0 || strncasecmp(appDirStr, "APPS\\", 5) == 0) { - appsPrefix = appDirStr + 5; - } - - if (appsPrefix && appsPrefix[0]) { - snprintf(app->dxeCtx->configDir, sizeof(app->dxeCtx->configDir), "CONFIG/%s", appsPrefix); + if (appDirStr[0]) { + snprintf(app->dxeCtx->configDir, sizeof(app->dxeCtx->configDir), "CONFIG/%s", appDirStr); } else { - snprintf(app->dxeCtx->configDir, sizeof(app->dxeCtx->configDir), "CONFIG/%s", desc->name); + snprintf(app->dxeCtx->configDir, sizeof(app->dxeCtx->configDir), "CONFIG/APPS/%s", desc->name); } // Launch. Set currentAppId before any app code runs so that diff --git a/widgets/textInput/widgetTextInput.c b/widgets/textInput/widgetTextInput.c index af82317..397c604 100644 --- a/widgets/textInput/widgetTextInput.c +++ b/widgets/textInput/widgetTextInput.c @@ -103,6 +103,9 @@ typedef struct { bool sbDragging; bool showLineNumbers; bool autoIndent; + bool captureTabs; // true = Tab key inserts tab/spaces; false = Tab moves focus + bool useTabChar; // true = insert '\t'; false = insert spaces + int32_t tabWidth; // display width and space count (default 3) // Syntax colorizer callback (optional). Called for each visible line. // line: text of the line (NOT null-terminated, use lineLen). @@ -1333,6 +1336,56 @@ navigation: return; } + // Tab key -- insert tab character or spaces if captureTabs is enabled + if (key == 9 && ta->captureTabs && !w->readOnly) { + if (*pLen < bufSize - 1) { + textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize); + + if (HAS_SEL()) { + int32_t lo = SEL_LO(); + int32_t hi = SEL_HI(); + memmove(buf + lo, buf + hi, *pLen - hi + 1); + *pLen -= (hi - lo); + textAreaOffToRowCol(buf, lo, pRow, pCol); + *pSA = -1; + *pSC = -1; + } + + int32_t off = CUR_OFF(); + + if (ta->useTabChar) { + // Insert a single tab character + if (*pLen < bufSize - 1) { + memmove(buf + off + 1, buf + off, *pLen - off + 1); + buf[off] = '\t'; + (*pLen)++; + (*pCol)++; + ta->desiredCol = *pCol; + } + } else { + // Insert spaces to next tab stop + int32_t tw = ta->tabWidth > 0 ? ta->tabWidth : 3; + int32_t spaces = tw - (*pCol % tw); + + for (int32_t s = 0; s < spaces && *pLen < bufSize - 1; s++) { + memmove(buf + off + 1, buf + off, *pLen - off + 1); + buf[off] = ' '; + off++; + (*pLen)++; + (*pCol)++; + } + + ta->desiredCol = *pCol; + } + + textAreaDirtyCache(w); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidatePaint(w); + } + + return; + } + // Printable character (blocked in read-only mode) if (key >= 32 && key < 127 && !w->readOnly) { if (*pLen < bufSize - 1) { @@ -2449,6 +2502,9 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) { ta->desiredCol = 0; ta->cachedLines = -1; ta->cachedMaxLL = -1; + ta->captureTabs = false; // default: Tab moves focus + ta->useTabChar = true; // default: insert actual tab character + ta->tabWidth = 3; // default: 3-space tab stops w->weight = 100; } @@ -2587,6 +2643,37 @@ void wgtTextAreaSetShowLineNumbers(WidgetT *w, bool show) { } +void wgtTextAreaSetCaptureTabs(WidgetT *w, bool capture) { + if (!w || w->type != sTextAreaTypeId) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + ta->captureTabs = capture; + w->swallowTab = capture; +} + + +void wgtTextAreaSetTabWidth(WidgetT *w, int32_t width) { + if (!w || w->type != sTextAreaTypeId) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + ta->tabWidth = width > 0 ? width : 1; +} + + +void wgtTextAreaSetUseTabChar(WidgetT *w, bool useChar) { + if (!w || w->type != sTextAreaTypeId) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + ta->useTabChar = useChar; +} + + // ============================================================ // DXE registration // ============================================================ @@ -2601,6 +2688,9 @@ static const struct { void (*goToLine)(WidgetT *w, int32_t line); void (*setAutoIndent)(WidgetT *w, bool enable); void (*setShowLineNumbers)(WidgetT *w, bool show); + void (*setCaptureTabs)(WidgetT *w, bool capture); + void (*setTabWidth)(WidgetT *w, int32_t width); + void (*setUseTabChar)(WidgetT *w, bool useChar); } sApi = { .create = wgtTextInput, .password = wgtPasswordInput, @@ -2609,7 +2699,10 @@ static const struct { .setColorize = wgtTextAreaSetColorize, .goToLine = wgtTextAreaGoToLine, .setAutoIndent = wgtTextAreaSetAutoIndent, - .setShowLineNumbers = wgtTextAreaSetShowLineNumbers + .setShowLineNumbers = wgtTextAreaSetShowLineNumbers, + .setCaptureTabs = wgtTextAreaSetCaptureTabs, + .setTabWidth = wgtTextAreaSetTabWidth, + .setUseTabChar = wgtTextAreaSetUseTabChar }; // Per-type APIs for the designer diff --git a/widgets/widgetTextInput.h b/widgets/widgetTextInput.h index 262e27c..01a2ce5 100644 --- a/widgets/widgetTextInput.h +++ b/widgets/widgetTextInput.h @@ -22,6 +22,9 @@ typedef struct { void (*goToLine)(WidgetT *w, int32_t line); void (*setAutoIndent)(WidgetT *w, bool enable); void (*setShowLineNumbers)(WidgetT *w, bool show); + void (*setCaptureTabs)(WidgetT *w, bool capture); + void (*setTabWidth)(WidgetT *w, int32_t width); + void (*setUseTabChar)(WidgetT *w, bool useChar); } TextInputApiT; static inline const TextInputApiT *dvxTextInputApi(void) { @@ -38,5 +41,8 @@ static inline const TextInputApiT *dvxTextInputApi(void) { #define wgtTextAreaGoToLine(w, line) dvxTextInputApi()->goToLine(w, line) #define wgtTextAreaSetAutoIndent(w, en) dvxTextInputApi()->setAutoIndent(w, en) #define wgtTextAreaSetShowLineNumbers(w, show) dvxTextInputApi()->setShowLineNumbers(w, show) +#define wgtTextAreaSetCaptureTabs(w, capture) dvxTextInputApi()->setCaptureTabs(w, capture) +#define wgtTextAreaSetTabWidth(w, width) dvxTextInputApi()->setTabWidth(w, width) +#define wgtTextAreaSetUseTabChar(w, useChar) dvxTextInputApi()->setUseTabChar(w, useChar) #endif // WIDGET_TEXTINPUT_H