diff --git a/Makefile b/Makefile index 4185687..3b3795b 100644 --- a/Makefile +++ b/Makefile @@ -107,7 +107,7 @@ SDKDIR = bin/sdk deploy-sdk: @echo "Building SDK..." - @mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic + @mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo @# Core headers (libdvx public API) @for f in src/libs/kpunch/libdvx/dvxApp.h src/libs/kpunch/libdvx/dvxTypes.h \ src/libs/kpunch/libdvx/dvxWgt.h src/libs/kpunch/libdvx/dvxWgtP.h \ @@ -137,6 +137,10 @@ deploy-sdk: done @# BASIC include files @cp src/include/basic/*.bas $(SDKDIR)/include/basic/ + @# BASIC sample: basdemo (project file + form + icon) + @cp src/apps/kpunch/basdemo/basdemo.dbp $(SDKDIR)/samples/basic/basdemo/ + @cp src/apps/kpunch/basdemo/basdemo.frm $(SDKDIR)/samples/basic/basdemo/ + @cp src/apps/kpunch/basdemo/ICON32.BMP $(SDKDIR)/samples/basic/basdemo/ @# README @printf '%s\n' \ 'DVX SDK' \ @@ -154,6 +158,8 @@ deploy-sdk: ' sql/ SQLite database wrapper API' \ ' widget/ Per-widget public API headers' \ ' basic/ BASIC include files (DECLARE LIBRARY modules)' \ + ' samples/' \ + ' basic/ Example BASIC projects (open in the DVX BASIC IDE)' \ '' \ 'Requirements' \ '------------' \ diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html index 81a6f59..7c0da39 100644 --- a/docs/dvx_basic_reference.html +++ b/docs/dvx_basic_reference.html @@ -3188,6 +3188,7 @@ End Sub
Method Description ------ ----------- + AppendText text$ Append text to the end of the buffer and invalidate the widget. FindNext needle$, caseSensitive, forward Search for text. Returns True if found. GetWordAtCursor() Returns the word under the cursor. GoToLine line% Scroll to and position cursor at the given line. diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html index e669bd9..598114a 100644 --- a/docs/dvx_system_reference.html +++ b/docs/dvx_system_reference.html @@ -6977,6 +6977,7 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced");API Functions (TextArea-specific)
Function Description -------- ----------- + void wgtTextAreaAppendText(w, text) Append text to the end of the buffer and invalidate the widget. void wgtTextAreaSetColorize(w, fn, ctx) Set a syntax colorization callback. The callback receives each line and fills a color index array. void wgtTextAreaGoToLine(w, line) Scroll to and place the cursor on the given line number. void wgtTextAreaSetAutoIndent(w, enable) Enable or disable automatic indentation on newline. diff --git a/src/apps/kpunch/Makefile b/src/apps/kpunch/Makefile index b0aeda8..0ad2700 100644 --- a/src/apps/kpunch/Makefile +++ b/src/apps/kpunch/Makefile @@ -39,7 +39,7 @@ C_APPS = progman clock dvxdemo cpanel dvxhelp BASCOMP = ../../../bin/host/bascomp # BASIC apps: each is a .dbp project in its own directory. # BASIC-only notepad, imgview, etc. replace the old C versions. -BASIC_APPS = iconed notepad imgview helpedit resedit basicdemo +BASIC_APPS = iconed notepad imgview helpedit resedit basdemo .PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS) @@ -115,10 +115,10 @@ resedit: $(BINDIR)/kpunch/resedit/resedit.app $(BINDIR)/kpunch/resedit/resedit.app: resedit/resedit.dbp resedit/resedit.frm resedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/resedit dvxbasic $(BASCOMP) resedit/resedit.dbp -o $@ -release -basicdemo: $(BINDIR)/kpunch/basicdemo/basicdemo.app +basdemo: $(BINDIR)/kpunch/basdemo/basdemo.app -$(BINDIR)/kpunch/basicdemo/basicdemo.app: basicdemo/basicdemo.dbp basicdemo/basicdemo.frm basicdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basicdemo dvxbasic - $(BASCOMP) basicdemo/basicdemo.dbp -o $@ -release +$(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic + $(BASCOMP) basdemo/basdemo.dbp -o $@ -release $(OBJDIR): mkdir -p $(OBJDIR) @@ -132,7 +132,7 @@ $(BINDIR)/kpunch/iconed: ; mkdir -p $@ $(BINDIR)/kpunch/notepad: ; mkdir -p $@ $(BINDIR)/kpunch/imgview: ; mkdir -p $@ $(BINDIR)/kpunch/resedit: ; mkdir -p $@ -$(BINDIR)/kpunch/basicdemo: ; mkdir -p $@ +$(BINDIR)/kpunch/basdemo: ; mkdir -p $@ # Header dependencies COMMON_H = ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxDlg.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxWm.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h @@ -153,5 +153,5 @@ clean: rm -f $(BINDIR)/kpunch/imgview/imgview.app rm -f $(BINDIR)/kpunch/dvxhelp/helpedit.app rm -f $(BINDIR)/kpunch/resedit/resedit.app - rm -f $(BINDIR)/kpunch/basicdemo/basicdemo.app + rm -f $(BINDIR)/kpunch/basdemo/basdemo.app $(MAKE) -C dvxbasic clean diff --git a/src/apps/kpunch/README.md b/src/apps/kpunch/README.md index f5e6615..9a58312 100644 --- a/src/apps/kpunch/README.md +++ b/src/apps/kpunch/README.md @@ -14,7 +14,7 @@ the BASIC-based sample apps. Two flavours of app live side by side: * **BASIC apps** -- `.dbp` projects compiled to `.app` files by the host-side `bascomp`. The compiler links the BASIC bytecode into a copy of `basstub.app`, which acts as the runtime host. See - `iconed/`, `notepad/`, `imgview/`, `basicdemo/`, `resedit/`, + `iconed/`, `notepad/`, `imgview/`, `basdemo/`, `resedit/`, `dvxhelp/helpedit/`. The rest of this document covers writing a C app against the SDK. @@ -303,11 +303,11 @@ the compiled system help viewer by default for live preview. GUI wrapper around `dvxres`: open a `.app` / `.wgt` / `.lib`, browse its resources, add, replace, extract, or remove them. -### BASIC Demo (basicdemo, BASIC) +### BASIC Demo (basdemo, BASIC) | | | |---|---| -| File | `apps/kpunch/basicdemo/basicdemo.app` | +| File | `apps/kpunch/basdemo/basdemo.app` | | Type | BASIC (main-loop via basstub) | | Multi-instance | No | @@ -453,9 +453,9 @@ apps/kpunch/ imgview.dbp imgview.frm ICON32.BMP - basicdemo/ - basicdemo.dbp - basicdemo.frm + basdemo/ + basdemo.dbp + basdemo.frm ICON32.BMP resedit/ resedit.dbp diff --git a/src/apps/kpunch/basicdemo/ICON32.BMP b/src/apps/kpunch/basdemo/ICON32.BMP similarity index 100% rename from src/apps/kpunch/basicdemo/ICON32.BMP rename to src/apps/kpunch/basdemo/ICON32.BMP diff --git a/src/apps/kpunch/basicdemo/basicdemo.dbp b/src/apps/kpunch/basdemo/basdemo.dbp similarity index 93% rename from src/apps/kpunch/basicdemo/basicdemo.dbp rename to src/apps/kpunch/basdemo/basdemo.dbp index ac37c70..52d0ff6 100644 --- a/src/apps/kpunch/basicdemo/basicdemo.dbp +++ b/src/apps/kpunch/basdemo/basdemo.dbp @@ -11,7 +11,7 @@ File0 = ../../../include/basic/commdlg.bas Count = 1 [Forms] -File0 = basicdemo.frm +File0 = basdemo.frm Count = 1 [Settings] diff --git a/src/apps/kpunch/basicdemo/basicdemo.frm b/src/apps/kpunch/basdemo/basdemo.frm similarity index 97% rename from src/apps/kpunch/basicdemo/basicdemo.frm rename to src/apps/kpunch/basdemo/basdemo.frm index 50a5d2b..e36531f 100644 --- a/src/apps/kpunch/basicdemo/basicdemo.frm +++ b/src/apps/kpunch/basdemo/basdemo.frm @@ -170,6 +170,21 @@ TYPE PointT y AS INTEGER END TYPE +' Module-level state. Grouped here up front (rather than sprinkled +' between SUB definitions) so the compiler sees a simple top-level +' run-once block followed by a flat list of SUBs / FUNCTIONs. +DIM gfxWin AS LONG +DIM dynForm AS LONG +DIM dynCount AS INTEGER +DIM timerWin AS LONG +DIM tickCount AS LONG + +gfxWin = 0 +dynForm = 0 +dynCount = 0 +timerWin = 0 +tickCount = 0 + ' ============================================================ ' OutArea helpers @@ -247,7 +262,7 @@ SUB mnuAbout_Click msg = "DVX BASIC Feature Tour" + CHR$(10) + CHR$(10) msg = msg + "A visual catalog of DVX BASIC language" msg = msg + " and runtime features." + CHR$(10) + CHR$(10) - msg = msg + "(c) 2026 DVX Project" + msg = msg + "Copyright 2026 Scott Duensing" MsgBox msg, vbOKOnly, "About" END SUB @@ -740,17 +755,13 @@ END SUB ' Graphics demo (opens a second form with Canvas) ' ============================================================ -DIM gfxWin AS LONG -gfxWin = 0 - - SUB mnuGraphics_Click IF gfxWin <> 0 THEN EXIT SUB END IF DIM frm AS LONG - SET frm = CreateForm("GraphicsForm", 360, 320) + SET frm = CreateForm("GraphicsForm", 380, 360) GraphicsForm.Caption = "Graphics Demo" gfxWin = frm @@ -844,6 +855,13 @@ SUB GfxClearCanvas END SUB +SUB BasicDemo_Unload + ' Closing the main form shuts down the whole app, including any + ' child forms (Graphics, Dynamic, Timer) the user left open. + END +END SUB + + SUB GraphicsForm_Unload gfxWin = 0 END SUB @@ -853,10 +871,6 @@ END SUB ' Dynamic form demo ' ============================================================ -DIM dynForm AS LONG -dynForm = 0 - - SUB mnuDynamic_Click IF dynForm <> 0 THEN EXIT SUB @@ -892,10 +906,6 @@ SUB mnuDynamic_Click END SUB -DIM dynCount AS INTEGER -dynCount = 0 - - SUB DynInc dynCount = dynCount + 1 CountLabel.Caption = "Counter: " + STR$(dynCount) @@ -917,10 +927,6 @@ END SUB ' Timer demo ' ============================================================ -DIM timerWin AS LONG -timerWin = 0 - - SUB mnuTimer_Click IF timerWin <> 0 THEN EXIT SUB @@ -944,10 +950,6 @@ SUB mnuTimer_Click END SUB -DIM tickCount AS LONG -tickCount = 0 - - SUB TickHandler tickCount = tickCount + 1 TickLabel.Caption = "Ticks: " + STR$(tickCount) + " Time: " + TIME$ diff --git a/src/apps/kpunch/cpanel/cpanel.c b/src/apps/kpunch/cpanel/cpanel.c index 7690816..c4d15a7 100644 --- a/src/apps/kpunch/cpanel/cpanel.c +++ b/src/apps/kpunch/cpanel/cpanel.c @@ -49,7 +49,6 @@ #include "shellApp.h" #include "stb_ds_wrap.h" -#include#include #include #include @@ -977,35 +976,33 @@ static void scanThemes(void) { arrsetlen(sThemeEntries, 0); arrsetlen(sThemeLabels, 0); - DIR *dir = opendir(THEME_DIR); + char **names = dvxReadDir(THEME_DIR); - if (!dir) { + if (!names) { return; } - struct dirent *ent; + int32_t n = (int32_t)arrlen(names); - while ((ent = readdir(dir)) != NULL) { - char *dot = strrchr(ent->d_name, '.'); - - if (!dot || strcasecmp(dot, THEME_EXT) != 0) { + for (int32_t i = 0; i < n; i++) { + if (!dvxHasExt(names[i], THEME_EXT)) { continue; } - FileEntryT entry = {0}; - int32_t nameLen = (int32_t)(dot - ent->d_name); + FileEntryT entry = {0}; + int32_t nameLen = (int32_t)strlen(names[i]) - (int32_t)strlen(THEME_EXT); if (nameLen >= (int32_t)sizeof(entry.name)) { nameLen = (int32_t)sizeof(entry.name) - 1; } - memcpy(entry.name, ent->d_name, nameLen); + memcpy(entry.name, names[i], nameLen); entry.name[nameLen] = '\0'; snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", THEME_DIR, entry.name); arrput(sThemeEntries, entry); } - closedir(dir); + dvxReadDirFree(names); // Build label array now that sThemeEntries is stable for (int32_t i = 0; i < arrlen(sThemeEntries); i++) { @@ -1018,24 +1015,18 @@ static void scanWallpapers(void) { arrsetlen(sWpaperEntries, 0); arrsetlen(sWpaperLabels, 0); - DIR *dir = opendir(WPAPER_DIR); + char **names = dvxReadDir(WPAPER_DIR); - if (!dir) { + if (!names) { return; } - struct dirent *ent; + int32_t n = (int32_t)arrlen(names); - while ((ent = readdir(dir)) != NULL) { - char *dot = strrchr(ent->d_name, '.'); - - if (!dot) { - continue; - } - - if (strcasecmp(dot, ".BMP") != 0 && - strcasecmp(dot, ".JPG") != 0 && - strcasecmp(dot, ".PNG") != 0) { + for (int32_t i = 0; i < n; i++) { + if (!dvxHasExt(names[i], ".BMP") && + !dvxHasExt(names[i], ".JPG") && + !dvxHasExt(names[i], ".PNG")) { continue; } @@ -1044,19 +1035,19 @@ static void scanWallpapers(void) { // Length-clamped memcpy instead of strncpy/snprintf because GCC // warns about both when d_name (255) exceeds the buffer (64), // even though truncation is intentional and safe. - int32_t nl = (int32_t)strlen(ent->d_name); + int32_t nl = (int32_t)strlen(names[i]); if (nl >= (int32_t)sizeof(entry.name)) { nl = (int32_t)sizeof(entry.name) - 1; } - memcpy(entry.name, ent->d_name, nl); + memcpy(entry.name, names[i], nl); entry.name[nl] = '\0'; snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", WPAPER_DIR, entry.name); arrput(sWpaperEntries, entry); } - closedir(dir); + dvxReadDirFree(names); // Build label array now that sWpaperEntries is stable for (int32_t i = 0; i < arrlen(sWpaperEntries); i++) { diff --git a/src/apps/kpunch/dvxbasic/Makefile b/src/apps/kpunch/dvxbasic/Makefile index 7fbf74e..cf36ffa 100644 --- a/src/apps/kpunch/dvxbasic/Makefile +++ b/src/apps/kpunch/dvxbasic/Makefile @@ -43,7 +43,7 @@ HOSTDIR = ../../../../bin/host DVXRES = $(HOSTDIR)/dvxres # Runtime library objects (VM + values + form runtime + serialization) -RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/serialize.o +RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/frmParser.o $(OBJDIR)/serialize.o RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt RT_TARGET = $(RT_TARGETDIR)/basrt.lib @@ -68,6 +68,7 @@ TEST_VM = $(HOSTDIR)/test_vm TEST_LEX = $(HOSTDIR)/test_lex TEST_QUICK = $(HOSTDIR)/test_quick TEST_COMPACT = $(HOSTDIR)/test_compact +TEST_SUITE = $(HOSTDIR)/test_suite STB_DS_IMPL = ../../../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c PLATFORM_UTIL = ../../../libs/kpunch/libdvx/platform/dvxPlatformUtil.c @@ -76,6 +77,7 @@ TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c runtime/serialize.c TEST_LEX_SRCS = test_lex.c compiler/lexer.c TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL) TEST_COMPACT_SRCS = test_compact.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL) +TEST_SUITE_SRCS = test_suite.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL) # Command-line compiler (host tool) BASCOMP_SRCS = stub/bascomp.c basBuild.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/obfuscate.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c ../../../libs/kpunch/libdvx/dvxPrefs.c ../../../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL) @@ -92,11 +94,15 @@ SYSTEMDIR = ../../../../bin/system all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE -tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) +tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(TEST_SUITE) + $(TEST_SUITE) $(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(HOSTDIR) $(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm +$(TEST_SUITE): $(TEST_SUITE_SRCS) | $(HOSTDIR) + $(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_SUITE_SRCS) -lm + $(TEST_VM): $(TEST_VM_SRCS) | $(HOSTDIR) $(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_VM_SRCS) -lm @@ -152,7 +158,10 @@ $(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) $(STUB_TARGET) dvxbasic.res | $(APPDIR) $(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< -$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h compiler/opcodes.h runtime/vm.h | $(OBJDIR) +$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h formrt/frmParser.h compiler/opcodes.h runtime/vm.h | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/frmParser.o: formrt/frmParser.c formrt/frmParser.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< $(OBJDIR)/serialize.o: runtime/serialize.c runtime/serialize.h runtime/vm.h | $(OBJDIR) @@ -173,7 +182,7 @@ $(OBJDIR)/basBuild.o: basBuild.c basBuild.h basRes.h | $(OBJDIR) $(OBJDIR)/basstub.o: stub/basstub.c runtime/vm.h runtime/serialize.h formrt/formrt.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< -$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h | $(OBJDIR) +$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< $(OBJDIR)/ideMenuEditor.o: ide/ideMenuEditor.c ide/ideMenuEditor.h ide/ideDesigner.h | $(OBJDIR) @@ -185,7 +194,7 @@ $(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideMenuEditor.h ide/ide $(OBJDIR)/ideProject.o: ide/ideProject.c ide/ideProject.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< -$(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h | $(OBJDIR) +$(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< $(OBJDIR)/ideToolbox.o: ide/ideToolbox.c ide/ideToolbox.h ide/ideDesigner.h | $(OBJDIR) diff --git a/src/apps/kpunch/dvxbasic/basRes.h b/src/apps/kpunch/dvxbasic/basRes.h index aaf1f6a..3d30ae8 100644 --- a/src/apps/kpunch/dvxbasic/basRes.h +++ b/src/apps/kpunch/dvxbasic/basRes.h @@ -46,10 +46,11 @@ #define BAS_INI_KEY_HELPFILE "HelpFile" // [Settings] / [Modules] / [Forms] sections of a .dbp file -#define BAS_INI_SECTION_SETTINGS "Settings" -#define BAS_INI_KEY_STARTUPFORM "StartupForm" -#define BAS_INI_SECTION_MODULES "Modules" -#define BAS_INI_SECTION_FORMS "Forms" +#define BAS_INI_SECTION_SETTINGS "Settings" +#define BAS_INI_KEY_STARTUPFORM "StartupForm" +#define BAS_INI_KEY_OPTIONEXPLICIT "OptionExplicit" +#define BAS_INI_SECTION_MODULES "Modules" +#define BAS_INI_SECTION_FORMS "Forms" // ------------------------------------------------------------ // Resource names inside a compiled .app DXE diff --git a/src/apps/kpunch/dvxbasic/compiler/codegen.c b/src/apps/kpunch/dvxbasic/compiler/codegen.c index 3046d53..55659f2 100644 --- a/src/apps/kpunch/dvxbasic/compiler/codegen.c +++ b/src/apps/kpunch/dvxbasic/compiler/codegen.c @@ -208,12 +208,40 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) { // Count SUB/FUNCTION entries int32_t procCount = 0; + // Count globals that need runtime type init (currently: STRING). + // This list survives stripping, unlike debugVars. + int32_t globalInitCount = 0; + for (int32_t i = 0; i < tab->count; i++) { BasSymbolT *s = tab->symbols[i]; if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) { procCount++; } + + if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) { + globalInitCount++; + } + } + + if (globalInitCount > 0) { + mod->globalInits = (BasGlobalInitT *)malloc(globalInitCount * sizeof(BasGlobalInitT)); + + if (mod->globalInits) { + int32_t gi = 0; + + for (int32_t i = 0; i < tab->count; i++) { + BasSymbolT *s = tab->symbols[i]; + + if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) { + mod->globalInits[gi].index = s->index; + mod->globalInits[gi].dataType = s->dataType; + gi++; + } + } + + mod->globalInitCount = gi; + } } if (procCount == 0) { @@ -235,6 +263,8 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) { BasProcEntryT *p = &mod->procs[idx++]; strncpy(p->name, s->name, BAS_MAX_PROC_NAME - 1); p->name[BAS_MAX_PROC_NAME - 1] = '\0'; + strncpy(p->formName, s->formName, BAS_MAX_PROC_NAME - 1); + p->formName[BAS_MAX_PROC_NAME - 1] = '\0'; p->codeAddr = s->codeAddr; p->paramCount = s->paramCount; p->localCount = s->localCount; diff --git a/src/apps/kpunch/dvxbasic/compiler/compact.c b/src/apps/kpunch/dvxbasic/compiler/compact.c index c857114..c8c9f26 100644 --- a/src/apps/kpunch/dvxbasic/compiler/compact.c +++ b/src/apps/kpunch/dvxbasic/compiler/compact.c @@ -169,6 +169,19 @@ int32_t basCompactBytecode(BasModuleT *mod) { break; } + case OP_FOR_INIT: { + // [uint16 varIdx] [uint8 scope] [int16 skipOffset] + int16_t oldOff = readI16LE(oldCode + oldPc + 4); + + if (!remapRelI16(newCode, newPc, 4, oldOff, + oldPc + 6, newPc + 6, + remap, oldCodeLen, newCodeLen, false)) { + ok = false; + } + + break; + } + case OP_ON_ERROR: { int16_t oldOff = readI16LE(oldCode + oldPc + 1); @@ -421,7 +434,7 @@ static int32_t opOperandSize(uint8_t op) { case OP_RGB: case OP_GET_RED: case OP_GET_GREEN: case OP_GET_BLUE: case OP_STR_VAL: case OP_STR_STRF: case OP_STR_HEX: - case OP_STR_STRING: + case OP_STR_STRING: case OP_STR_OCT: case OP_CONV_BOOL: case OP_MATH_TIMER: case OP_DATE_STR: case OP_TIME_STR: case OP_SLEEP: case OP_ENVIRON: case OP_READ_DATA: case OP_RESTORE: @@ -444,6 +457,7 @@ static int32_t opOperandSize(uint8_t op) { return 0; case OP_LOAD_ARRAY: case OP_STORE_ARRAY: + case OP_PUSH_ARR_ADDR: case OP_PRINT_SPC: case OP_FILE_OPEN: case OP_CALL_METHOD: case OP_SHOW_FORM: case OP_LBOUND: case OP_UBOUND: @@ -466,13 +480,13 @@ static int32_t opOperandSize(uint8_t op) { return 2; case OP_STORE_ARRAY_FIELD: - case OP_FOR_INIT: return 3; case OP_PUSH_INT32: case OP_PUSH_FLT32: case OP_CALL: return 4; + case OP_FOR_INIT: case OP_FOR_NEXT: return 5; diff --git a/src/apps/kpunch/dvxbasic/compiler/lexer.c b/src/apps/kpunch/dvxbasic/compiler/lexer.c index 35bdc88..29eff57 100644 --- a/src/apps/kpunch/dvxbasic/compiler/lexer.c +++ b/src/apps/kpunch/dvxbasic/compiler/lexer.c @@ -338,10 +338,16 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) { return lex->token.type; } - // Hex literal (&H...) - if (c == '&' && upperChar(peekNext(lex)) == 'H') { - lex->token.type = tokenizeHexLiteral(lex); - return lex->token.type; + // Numeric-base literals: &H hex, &O octal, &B binary. &B is an + // extension beyond classic QBASIC; it's convenient for bitmask + // work in the widget/graphics code. + if (c == '&') { + char n = upperChar(peekNext(lex)); + + if (n == 'H' || n == 'O' || n == 'B') { + lex->token.type = tokenizeHexLiteral(lex); + return lex->token.type; + } } // Identifier or keyword @@ -621,30 +627,56 @@ static void skipWhitespace(BasLexerT *lex) { static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) { - advance(lex); // skip & - advance(lex); // skip H + advance(lex); // skip & + char base = upperChar(peek(lex)); + advance(lex); // skip H/O/B - int32_t idx = 0; + int32_t shift; + int32_t maxDigit; + + if (base == 'O') { + shift = 3; + maxDigit = 7; + } else if (base == 'B') { + shift = 1; + maxDigit = 1; + } else { + shift = 4; + maxDigit = 15; + } + + int32_t idx = 0; int32_t value = 0; - while (!atEnd(lex) && isxdigit((unsigned char)peek(lex))) { - char c = advance(lex); + for (;;) { + if (atEnd(lex)) { + break; + } + + char c = peek(lex); + int32_t digit; + + if (c >= '0' && c <= '9') { + digit = c - '0'; + } else if (shift == 4 && c >= 'A' && c <= 'F') { + digit = c - 'A' + 10; + } else if (shift == 4 && c >= 'a' && c <= 'f') { + digit = c - 'a' + 10; + } else { + break; + } + + if (digit > maxDigit) { + break; + } + + advance(lex); if (idx < BAS_MAX_TOKEN_LEN - 1) { lex->token.text[idx++] = c; } - int32_t digit; - - if (c >= '0' && c <= '9') { - digit = c - '0'; - } else if (c >= 'A' && c <= 'F') { - digit = c - 'A' + 10; - } else { - digit = c - 'a' + 10; - } - - value = (value << 4) | digit; + value = (value << shift) | digit; } lex->token.text[idx] = '\0'; diff --git a/src/apps/kpunch/dvxbasic/compiler/obfuscate.c b/src/apps/kpunch/dvxbasic/compiler/obfuscate.c index 984896e..905295e 100644 --- a/src/apps/kpunch/dvxbasic/compiler/obfuscate.c +++ b/src/apps/kpunch/dvxbasic/compiler/obfuscate.c @@ -566,6 +566,16 @@ static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) { continue; } + // Remap the owning form name (used at runtime to bind form-scope + // variables). The form itself gets renamed by the same pass. + if (proc->formName[0]) { + const char *mappedForm = nameMapLookup(map, proc->formName); + + if (mappedForm) { + snprintf(proc->formName, sizeof(proc->formName), "%s", mappedForm); + } + } + // Find last underscore char *underscore = strrchr(proc->name, '_'); diff --git a/src/apps/kpunch/dvxbasic/compiler/opcodes.h b/src/apps/kpunch/dvxbasic/compiler/opcodes.h index 3a27ef6..8c752c5 100644 --- a/src/apps/kpunch/dvxbasic/compiler/opcodes.h +++ b/src/apps/kpunch/dvxbasic/compiler/opcodes.h @@ -180,7 +180,7 @@ typedef enum { #define OP_GOSUB_RET 0x54 // pop PC from eval stack, jump (GOSUB return) #define OP_RET 0x55 // return from subroutine #define OP_RET_VAL 0x56 // return from function (value on stack) -#define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 isLocal] init FOR +#define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 scope] [int16 skipOffset] init FOR, skip body if range empty #define OP_FOR_NEXT 0x58 // [uint16 varIdx] [uint8 isLocal] [int16 loopTop] #define OP_FOR_POP 0x59 // pop top FOR stack entry (for EXIT FOR) @@ -297,6 +297,9 @@ typedef enum { #define OP_STR_STRF 0xB1 // STR$(n) -> string #define OP_STR_HEX 0xB2 // HEX$(n) -> string #define OP_STR_STRING 0xB3 // STRING$(n, char) -> string +#define OP_STR_OCT 0xF3 // OCT$(n) -> string +#define OP_CONV_BOOL 0xF4 // CBOOL(n) -> -1 (true) or 0 (false) +#define OP_PUSH_ARR_ADDR 0xF5 // [uint8 dims] pop dims indices, pop array ref, push REF to element // ============================================================ // Extended built-ins diff --git a/src/apps/kpunch/dvxbasic/compiler/parser.c b/src/apps/kpunch/dvxbasic/compiler/parser.c index fddc2bb..568417d 100644 --- a/src/apps/kpunch/dvxbasic/compiler/parser.c +++ b/src/apps/kpunch/dvxbasic/compiler/parser.c @@ -69,6 +69,7 @@ static const BuiltinFuncT builtinFuncs[] = { {"LEN", OP_STR_LEN, 1, 1, BAS_TYPE_INTEGER}, {"LTRIM$", OP_STR_LTRIM, 1, 1, BAS_TYPE_STRING}, {"MID$", OP_STR_MID2, 2, 3, BAS_TYPE_STRING}, + {"OCT$", OP_STR_OCT, 1, 1, BAS_TYPE_STRING}, {"RIGHT$", OP_STR_RIGHT, 2, 2, BAS_TYPE_STRING}, {"RTRIM$", OP_STR_RTRIM, 1, 1, BAS_TYPE_STRING}, {"SPACE$", OP_STR_SPACE, 1, 1, BAS_TYPE_STRING}, @@ -84,6 +85,7 @@ static const BuiltinFuncT builtinFuncs[] = { {"LOF", OP_FILE_LOF, 1, 1, BAS_TYPE_LONG}, // Conversion functions + {"CBOOL", OP_CONV_BOOL, 1, 1, BAS_TYPE_BOOLEAN}, {"CDBL", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_DOUBLE}, {"CINT", OP_CONV_FLT_INT, 1, 1, BAS_TYPE_INTEGER}, {"CLNG", OP_CONV_INT_LONG, 1, 1, BAS_TYPE_LONG}, @@ -355,6 +357,11 @@ void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) { } +void basParserSetValidator(BasParserT *p, const BasCtrlValidatorT *v) { + p->validator = v; +} + + static bool check(BasParserT *p, BasTokenTypeE type) { return p->lex.token.type == type; } @@ -501,10 +508,10 @@ static void emitByRefArg(BasParserT *p) { strncpy(name, p->lex.token.text, sizeof(name) - 1); name[sizeof(name) - 1] = '\0'; - // Look up the symbol -- must be a simple variable (not array, not const) + // Look up the symbol -- must be a variable (simple or array) BasSymbolT *sym = basSymTabFind(&p->sym, name); - if (!sym || sym->kind != SYM_VARIABLE || sym->isArray) { + if (!sym || sym->kind != SYM_VARIABLE) { parseExpression(p); return; } @@ -517,6 +524,44 @@ static void emitByRefArg(BasParserT *p) { advance(p); // consume the identifier + // Array element as BYREF: `arr(i)` or `arr(i, j)`. Emit LOAD of + // the array ref, then the indices, then OP_PUSH_ARR_ADDR which + // produces a BAS_TYPE_REF pointing at the element. Writes through + // that ref in the callee update the actual array element. + if (sym->isArray && check(p, TOK_LPAREN)) { + advance(p); // consume ( + + // Push the array reference + emitLoad(p, sym); + + // Parse indices + int32_t dims = 0; + parseExpression(p); + dims++; + + while (match(p, TOK_COMMA)) { + parseExpression(p); + dims++; + } + + expect(p, TOK_RPAREN); + + // If the next token isn't an argument delimiter, this wasn't + // a simple `arr(i)` -- rewind and fall back. (rare; e.g. + // `arr(i).field` not supported as BYREF.) + if (!check(p, TOK_COMMA) && !check(p, TOK_RPAREN) && !check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) { + // Too late to rewind cleanly -- we've already emitted code. + // Treat this as an error. Callers can restructure as a + // simple variable BYREF or BYVAL. + error(p, "Complex BYREF array expression not supported; use a temporary variable"); + return; + } + + basEmit8(&p->cg, OP_PUSH_ARR_ADDR); + basEmit8(&p->cg, (uint8_t)dims); + return; + } + // The token after the identifier must be an argument delimiter // (comma, rparen, newline, colon, EOF, ELSE) for this to be a // bare variable reference. Anything else (operator, dot, paren) @@ -1139,14 +1184,24 @@ static void parseAssignOrCall(BasParserT *p) { memberName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); // consume member name + // If `name` is a regular variable, the user is dereferencing an + // object reference (typically a form or control returned by + // CreateForm / CreateControl). Use the variable's value as the + // ref directly instead of treating `name` as a literal control + // name. + bool isVarRef = (sym != NULL && sym->kind == SYM_VARIABLE); + // Special form methods: Show, Hide if (strcasecmp(memberName, "Show") == 0) { // name.Show [modal] - // Push form name, LOAD_FORM, SHOW_FORM - uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); - basEmit8(&p->cg, OP_PUSH_STR); - basEmitU16(&p->cg, nameIdx); - basEmit8(&p->cg, OP_LOAD_FORM); + if (isVarRef) { + emitLoad(p, sym); + } else { + uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, nameIdx); + basEmit8(&p->cg, OP_LOAD_FORM); + } uint8_t modal = 0; if (check(p, TOK_INT_LIT)) { if (p->lex.token.intVal != 0) { @@ -1166,10 +1221,14 @@ static void parseAssignOrCall(BasParserT *p) { } if (strcasecmp(memberName, "Hide") == 0) { - uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); - basEmit8(&p->cg, OP_PUSH_STR); - basEmitU16(&p->cg, nameIdx); - basEmit8(&p->cg, OP_LOAD_FORM); + if (isVarRef) { + emitLoad(p, sym); + } else { + uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, nameIdx); + basEmit8(&p->cg, OP_LOAD_FORM); + } basEmit8(&p->cg, OP_HIDE_FORM); return; } @@ -1178,16 +1237,33 @@ static void parseAssignOrCall(BasParserT *p) { // Property assignment: CtrlName.Property = expr advance(p); // consume = - // Push ctrl ref: push form ref (NULL = current), push ctrl name, FIND_CTRL - basEmit8(&p->cg, OP_PUSH_INT16); - basEmit16(&p->cg, 0); - BasValueT formNull; - memset(&formNull, 0, sizeof(formNull)); - // Use OP_PUSH_STR for ctrl name, then FIND_CTRL - uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); - basEmit8(&p->cg, OP_PUSH_STR); - basEmitU16(&p->cg, ctrlNameIdx); - basEmit8(&p->cg, OP_FIND_CTRL); + // Compile-time validation: if the host provided a validator + // (IDE), check that the property exists on the widget type. + // Skip when `name` is a variable reference (dynamic ctrl we + // can't statically type) or when the ctrl isn't in the map + // (e.g. created via CreateControl at runtime). + if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isPropValid) { + const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name); + + if (wgtType && !p->validator->isPropValid(p->validator->ctx, wgtType, memberName)) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Unknown property '%s.%s' (type '%s' has no such property)", name, memberName, wgtType); + error(p, buf); + return; + } + } + + // Push ctrl/form ref + if (isVarRef) { + emitLoad(p, sym); + } else { + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, ctrlNameIdx); + basEmit8(&p->cg, OP_FIND_CTRL); + } // Push property name uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName)); @@ -1203,13 +1279,29 @@ static void parseAssignOrCall(BasParserT *p) { } // Method call: CtrlName.Method [args] - // Push ctrl ref - basEmit8(&p->cg, OP_PUSH_INT16); - basEmit16(&p->cg, 0); - uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); - basEmit8(&p->cg, OP_PUSH_STR); - basEmitU16(&p->cg, ctrlNameIdx); - basEmit8(&p->cg, OP_FIND_CTRL); + // Same compile-time validation as above, but for methods. + if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isMethodValid) { + const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name); + + if (wgtType && !p->validator->isMethodValid(p->validator->ctx, wgtType, memberName)) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Unknown method '%s.%s' (type '%s' has no such method)", name, memberName, wgtType); + error(p, buf); + return; + } + } + + // Push ctrl/form ref + if (isVarRef) { + emitLoad(p, sym); + } else { + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, ctrlNameIdx); + basEmit8(&p->cg, OP_FIND_CTRL); + } // Push method name uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName)); @@ -1412,13 +1504,52 @@ static void parseAssignOrCall(BasParserT *p) { argc++; } } - if (!p->hasError && argc != sym->paramCount) { + + // Determine the minimum acceptable count (ignore trailing optionals). + int32_t minArgs = sym->requiredParams; + bool hasOptional = false; + + for (int32_t i = 0; i < sym->paramCount && i < BAS_MAX_PARAMS; i++) { + if (sym->paramOptional[i]) { + hasOptional = true; + break; + } + } + + if (!hasOptional) { + minArgs = sym->paramCount; + } + + if (!p->hasError && (argc < minArgs || argc > sym->paramCount)) { char buf[BAS_PARSE_ERR_SCRATCH]; - snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc); + + if (minArgs == sym->paramCount) { + snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc); + } else { + snprintf(buf, sizeof(buf), "Sub '%s' expects %d to %d arguments, got %d", sym->name, (int)minArgs, (int)sym->paramCount, (int)argc); + } + error(p, buf); return; } + // Pad missing optional arguments with zero-valued defaults so + // the callee's OP_CALL receives a full parameter list. + while (argc < sym->paramCount) { + uint8_t pType = sym->paramTypes[argc]; + + if (pType == BAS_TYPE_STRING) { + uint16_t idx = basAddConstant(&p->cg, "", 0); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, idx); + } else { + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + } + + argc++; + } + // External library SUB: emit OP_CALL_EXTERN if (sym->isExtern) { basEmit8(&p->cg, OP_CALL_EXTERN); @@ -2047,9 +2178,13 @@ static void parseDef(BasParserT *p) { paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); - uint8_t pdt = suffixToType(paramName); + uint8_t pdt = suffixToType(paramName); + int32_t pUdtTypeId = -1; if (match(p, TOK_AS)) { pdt = resolveTypeName(p); + if (pdt == BAS_TYPE_UDT) { + pUdtTypeId = p->lastUdtTypeId; + } } BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); @@ -2060,6 +2195,7 @@ static void parseDef(BasParserT *p) { paramSym->scope = SCOPE_LOCAL; paramSym->index = basSymTabAllocSlot(&p->sym); paramSym->isDefined = true; + paramSym->udtTypeId = pUdtTypeId; if (paramCount < BAS_MAX_PARAMS) { paramTypes[paramCount] = pdt; @@ -2283,6 +2419,14 @@ static void parseDim(BasParserT *p) { emitUdtInit(p, udtTypeId); emitStore(p, sym); } + } else if (dt == BAS_TYPE_STRING) { + // STRING slots must start as an empty string, not numeric 0. + // Arithmetic falls through basValToNumber so numeric defaults + // stay harmless, but STRING concat checks actual slot type. + uint16_t idx = basAddConstant(&p->cg, "", 0); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, idx); + emitStore(p, sym); } } @@ -2572,10 +2716,15 @@ static void parseFor(BasParserT *p) { basEmit16(&p->cg, 1); } - // Emit FOR_INIT -- sets up the for-loop state in the VM + // Emit FOR_INIT -- sets up the for-loop state in the VM. The + // trailing int16 is a forward offset the VM uses to skip the body + // when the loop's range is already empty at entry (e.g. FOR i = 10 + // TO 5). We patch it after the body is emitted. basEmit8(&p->cg, OP_FOR_INIT); basEmitU16(&p->cg, (uint16_t)loopVar->index); basEmit8(&p->cg, (uint8_t)loopVar->scope); + int32_t skipOffsetPos = basCodePos(&p->cg); + basEmit16(&p->cg, 0); // placeholder int32_t loopBody = basCodePos(&p->cg); @@ -2606,6 +2755,12 @@ static void parseFor(BasParserT *p) { int16_t backOffset = (int16_t)(loopBody - (basCodePos(&p->cg) + 2)); basEmit16(&p->cg, backOffset); + // Patch FOR_INIT's forward skip offset to point past FOR_NEXT. + int32_t loopEnd = basCodePos(&p->cg); + int16_t skipOffset = (int16_t)(loopEnd - (skipOffsetPos + 2)); + p->cg.code[skipOffsetPos] = (uint8_t)(skipOffset & 0xFF); + p->cg.code[skipOffsetPos + 1] = (uint8_t)((skipOffset >> 8) & 0xFF); + // Patch all EXIT FOR jumps to here exitListPatch(&exitForList, p); exitForList = savedExitFor; @@ -2684,9 +2839,13 @@ static void parseFunction(BasParserT *p) { paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); - uint8_t pdt = suffixToType(paramName); + uint8_t pdt = suffixToType(paramName); + int32_t pUdtTypeId = -1; if (match(p, TOK_AS)) { pdt = resolveTypeName(p); + if (pdt == BAS_TYPE_UDT) { + pUdtTypeId = p->lastUdtTypeId; + } } BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); @@ -2697,6 +2856,7 @@ static void parseFunction(BasParserT *p) { paramSym->scope = SCOPE_LOCAL; paramSym->index = basSymTabAllocSlot(&p->sym); paramSym->isDefined = true; + paramSym->udtTypeId = pUdtTypeId; if (paramCount < BAS_MAX_PARAMS) { paramTypes[paramCount] = pdt; @@ -4172,13 +4332,50 @@ static void parsePrimary(BasParserT *p) { memberName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); - // Emit: push NULL (current form), push ctrl name, FIND_CTRL - basEmit8(&p->cg, OP_PUSH_INT16); - basEmit16(&p->cg, 0); - uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); - basEmit8(&p->cg, OP_PUSH_STR); - basEmitU16(&p->cg, ctrlNameIdx); - basEmit8(&p->cg, OP_FIND_CTRL); + bool isVarRef2 = (sym != NULL && sym->kind == SYM_VARIABLE); + + // Compile-time validation for expression-context reads / + // method returns. Peek ahead: if '(' follows, memberName + // is a method; otherwise it's a property read. Skip when + // the host didn't attach a validator or the ctrl is dynamic. + if (!isVarRef2 && p->validator && p->validator->lookupCtrlType) { + const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name); + + if (wgtType) { + bool isMethodCall = check(p, TOK_LPAREN); + bool valid = true; + + if (isMethodCall && p->validator->isMethodValid) { + valid = p->validator->isMethodValid(p->validator->ctx, wgtType, memberName); + } else if (!isMethodCall && p->validator->isPropValid) { + valid = p->validator->isPropValid(p->validator->ctx, wgtType, memberName); + } + + if (!valid) { + char buf[BAS_PARSE_ERR_SCRATCH]; + snprintf(buf, sizeof(buf), "Unknown %s '%s.%s' (type '%s' has no such %s)", + isMethodCall ? "method" : "property", + name, memberName, wgtType, + isMethodCall ? "method" : "property"); + error(p, buf); + return; + } + } + } + + // If `name` is a regular variable holding an object reference + // (form/control returned by CreateForm/CreateControl), use its + // value directly instead of treating `name` as a literal name. + if (isVarRef2) { + emitLoad(p, sym); + } else { + basEmit8(&p->cg, OP_PUSH_INT16); + basEmit16(&p->cg, 0); + uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, ctrlNameIdx); + basEmit8(&p->cg, OP_FIND_CTRL); + } // If followed by '(', this is a method call with args if (check(p, TOK_LPAREN)) { @@ -5583,9 +5780,13 @@ static void parseSub(BasParserT *p) { paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; advance(p); - uint8_t pdt = suffixToType(paramName); + uint8_t pdt = suffixToType(paramName); + int32_t pUdtTypeId = -1; if (match(p, TOK_AS)) { pdt = resolveTypeName(p); + if (pdt == BAS_TYPE_UDT) { + pUdtTypeId = p->lastUdtTypeId; + } } BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); @@ -5596,6 +5797,7 @@ static void parseSub(BasParserT *p) { paramSym->scope = SCOPE_LOCAL; paramSym->index = basSymTabAllocSlot(&p->sym); paramSym->isDefined = true; + paramSym->udtTypeId = pUdtTypeId; if (paramCount < BAS_MAX_PARAMS) { paramTypes[paramCount] = pdt; diff --git a/src/apps/kpunch/dvxbasic/compiler/parser.h b/src/apps/kpunch/dvxbasic/compiler/parser.h index 8227a0c..b958932 100644 --- a/src/apps/kpunch/dvxbasic/compiler/parser.h +++ b/src/apps/kpunch/dvxbasic/compiler/parser.h @@ -49,6 +49,27 @@ #define BAS_PARSE_ERROR_LEN 1024 #define BAS_PARSE_ERR_SCRATCH 512 +// Optional compile-time validator for CtrlName.Member references. +// The IDE populates this from the project's .frm files + widget DXE +// metadata so typos die at compile time instead of at event-click +// time. bascomp leaves it NULL (runs on the host, no widget DXEs) +// and falls back to the runtime error net. Dynamically-created +// controls aren't in the map, so lookupCtrlType returns NULL and +// validation is skipped for those -- no false positives. +typedef struct { + // Return the widget type name for a control declared in a .frm, + // or NULL if the control isn't statically known. + const char *(*lookupCtrlType)(void *ctx, const char *ctrlName); + // Return true if `methodName` is valid for widget type `wgtType` + // (or a common method like Refresh/SetFocus). Called with the + // type string returned by lookupCtrlType. + bool (*isMethodValid)(void *ctx, const char *wgtType, const char *methodName); + // Return true if `propName` is a valid property on wgtType. + bool (*isPropValid)(void *ctx, const char *wgtType, const char *propName); + void *ctx; +} BasCtrlValidatorT; + + typedef struct { BasLexerT lex; BasCodeGenT cg; @@ -66,6 +87,8 @@ typedef struct { // 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) + // Optional compile-time CtrlName.Member validator (IDE-only). + const BasCtrlValidatorT *validator; } BasParserT; // ============================================================ @@ -75,6 +98,11 @@ typedef struct { // Initialize parser with source text. void basParserInit(BasParserT *p, const char *source, int32_t sourceLen); +// Attach an optional compile-time validator for CtrlName.Member +// references. The parser borrows the pointer -- caller owns the +// underlying struct and must keep it alive until basParserFree. +void basParserSetValidator(BasParserT *p, const BasCtrlValidatorT *v); + // Parse the entire source and generate p-code. // Returns true on success, false on error (check p->error). bool basParse(BasParserT *p); diff --git a/src/apps/kpunch/dvxbasic/compiler/symtab.c b/src/apps/kpunch/dvxbasic/compiler/symtab.c index 284030d..3d3b661 100644 --- a/src/apps/kpunch/dvxbasic/compiler/symtab.c +++ b/src/apps/kpunch/dvxbasic/compiler/symtab.c @@ -83,7 +83,13 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui sym->dataType = dataType; sym->isDefined = true; - if (scope == SCOPE_FORM && tab->formScopeName[0]) { + // Record owning form for both SCOPE_FORM vars AND SUBs/FUNCTIONs + // declared inside BEGINFORM...ENDFORM. SUBs stay at SCOPE_GLOBAL + // (callable from anywhere) but carry the owning form so the VM can + // bind form-scope vars correctly when the SUB is dispatched as an + // event handler for a different form's control. + if (tab->inFormScope && tab->formScopeName[0] && + (scope == SCOPE_FORM || kind == SYM_SUB || kind == SYM_FUNCTION)) { strncpy(sym->formName, tab->formScopeName, BAS_MAX_SYMBOL_NAME - 1); sym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0'; } diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c index 82ea4db..1e457aa 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formrt.c +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c @@ -38,10 +38,12 @@ #include "ansiTerm/ansiTerm.h" #include "dataCtrl/dataCtrl.h" #include "dbGrid/dbGrid.h" +#include "frmParser.h" #include "thirdparty/stb_ds_wrap.h" #include #include +#include #include #include #include @@ -163,6 +165,34 @@ static bool sShellLoadResolved = false; static DvxResHandleT *sResHandles[RES_MAX_HANDLES]; + +// ============================================================ +// .frm parsing context (used by frmLoad_* callbacks) +// ============================================================ + +typedef struct { + char caption[256]; + char name[BAS_MAX_CTRL_NAME]; + int32_t level; + bool checked; + bool radioCheck; + bool enabled; +} BasFrmMenuItemT; + + +typedef struct { + BasFormRtT *rt; + BasFormT *form; + BasControlT *current; + WidgetT *parentStack[BAS_MAX_FRM_NESTING]; + int32_t nestDepth; + bool containerStack[BAS_MAX_FRM_NESTING]; + int32_t containerDepth; + BasFrmMenuItemT *menuItems; // stb_ds array + int32_t curMenuItemIdx; +} BasFrmLoadCtxT; + + // ============================================================ // Prototypes // ============================================================ @@ -195,6 +225,7 @@ static void *basFormRtLoadCfm(BasFormRtT *rt, const uint8_t *data, int32_t dataL void *basFormRtLoadForm(void *ctx, const char *formName); BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen); int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title); +void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...); void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *data, int32_t dataLen); void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen); void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName); @@ -204,8 +235,10 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT void basFormRtShowForm(void *ctx, void *formRef, bool modal); void basFormRtUnloadForm(void *ctx, void *formRef); const char *basInputBox2(const char *title, const char *prompt, const char *defaultText); +int32_t basInputCancelled(void); int32_t basIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal); static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name); +static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc); int32_t basPromptSave(const char *title); static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc); void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt); @@ -225,7 +258,16 @@ int32_t CommSend(int32_t handle, const char *data, int32_t channel, int32_t encr static int32_t commTermRead(void *ctx, uint8_t *buf, int32_t maxLen); static int32_t commTermWrite(void *ctx, const uint8_t *data, int32_t len); WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent); +WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *parent, bool allowData); static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount); +static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char *name); +static void frmLoad_onCtrlEnd(void *userData); +static void frmLoad_onCtrlProp(void *userData, const char *key, const char *value); +static bool frmLoad_onFormBegin(void *userData, const char *name); +static void frmLoad_onFormProp(void *userData, const char *key, const char *value); +static void frmLoad_onMenuBegin(void *userData, const char *name, int32_t level); +static void frmLoad_onMenuEnd(void *userData); +static void frmLoad_onMenuProp(void *userData, const char *key, const char *value); static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled); static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled); int32_t HelpCompile(const char *inputFile, const char *outputFile); @@ -249,8 +291,8 @@ static void onWidgetMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y); static void onWidgetScroll(WidgetT *w, int32_t delta); static bool onWidgetValidate(WidgetT *w); static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, char *buf, int32_t bufSize); -static void parseFrmLine(const char *line, char *key, char *value); static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl); +const char *resolveTypeName(const char *typeName); int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile); int32_t ResAddText(const char *path, const char *name, const char *text); void ResClose(int32_t handle); @@ -258,7 +300,6 @@ int32_t ResCount(int32_t handle); int32_t ResExtract(const char *path, const char *name, const char *outFile); const char *ResGetText(const char *path, const char *name); const char *ResName(int32_t handle, int32_t index); -static const char *resolveTypeName(const char *typeName); int32_t ResOpen(const char *path); int32_t ResRemove(const char *path, const char *name); int32_t ResSize(int32_t handle, int32_t index); @@ -298,6 +339,8 @@ int32_t SQLNext(int32_t rs); int32_t SQLOpen(const char *path); int32_t SQLQuery(int32_t db, const char *sql); static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl); +bool wgtApplyPropFromString(WidgetT *w, const WgtPropDescT *p, const char *val); +bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize); static BasValueT zeroValue(void); int32_t basChoiceDialog(const char *title, const char *prompt, const char *items, int32_t defaultIdx) { @@ -543,10 +586,23 @@ void basFormRtBindVm(BasFormRtT *rt) { BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc) { - (void)ctx; + BasFormRtT *rt = (BasFormRtT *)ctx; BasControlT *ctrl = (BasControlT *)ctrlRef; - if (!ctrl || !ctrl->widget) { + if (!ctrl) { + basFormRtRuntimeError(rt, + "Method call on unknown control", + "Method: %s\nControl was not found on any loaded form.", + methodName ? methodName : "?"); + return zeroValue(); + } + + if (!ctrl->widget) { + basFormRtRuntimeError(rt, + "Method call on control with no widget", + "Method: %s\nControl: %s\nThe control is known but has no attached widget.", + methodName ? methodName : "?", + ctrl->name); return zeroValue(); } @@ -762,7 +818,7 @@ WidgetT *basFormRtCreateContentBox(WidgetT *root, const char *layout) { void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const char *ctrlName) { - (void)ctx; + BasFormRtT *rt = (BasFormRtT *)ctx; BasFormT *form = (BasFormT *)formRef; if (!form) { @@ -773,6 +829,12 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const const char *wgtTypeName = resolveTypeName(typeName); if (!wgtTypeName) { + basFormRtRuntimeError(rt, + "CreateControl: unknown control type", + "Requested type: %s\nForm: %s\nControl name: %s", + typeName ? typeName : "(null)", + form->name, + ctrlName ? ctrlName : "(null)"); return NULL; } @@ -780,12 +842,25 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const WidgetT *parent = form->contentBox ? form->contentBox : form->root; if (!parent) { + basFormRtRuntimeError(rt, + "CreateControl: form has no parent widget", + "Form: %s\nType: %s\nControl name: %s", + form->name, + typeName ? typeName : "(null)", + ctrlName ? ctrlName : "(null)"); return NULL; } WidgetT *widget = createWidget(wgtTypeName, parent); if (!widget) { + basFormRtRuntimeError(rt, + "CreateControl: widget creation failed", + "Type: %s (resolved from %s)\nForm: %s\nControl name: %s", + wgtTypeName, + typeName ? typeName : "(null)", + form->name, + ctrlName ? ctrlName : "(null)"); return NULL; } @@ -799,7 +874,8 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const } ctrl->index = -1; - snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + snprintf(ctrl->typeName, BAS_MAX_CTRL_NAME, "%s", typeName ? typeName : ""); ctrl->widget = widget; ctrl->form = form; ctrl->iface = wgtGetIface(wgtTypeName); @@ -820,16 +896,27 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef) { - (void)ctx; + BasFormRtT *rt = (BasFormRtT *)ctx; BasFormT *form = (BasFormT *)formRef; if (!form) { + basFormRtRuntimeError(rt, + "CreateControl: form reference is NULL", + "Type: %s\nControl name: %s", + typeName ? typeName : "(null)", + ctrlName ? ctrlName : "(null)"); return NULL; } const char *wgtTypeName = resolveTypeName(typeName); if (!wgtTypeName) { + basFormRtRuntimeError(rt, + "CreateControl: unknown control type", + "Requested type: %s\nForm: %s\nControl name: %s", + typeName ? typeName : "(null)", + form->name, + ctrlName ? ctrlName : "(null)"); return NULL; } @@ -865,7 +952,8 @@ void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, cons } ctrl->index = -1; - snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + snprintf(ctrl->typeName, BAS_MAX_CTRL_NAME, "%s", typeName ? typeName : ""); ctrl->widget = widget; ctrl->form = form; ctrl->iface = wgtGetIface(wgtTypeName); @@ -1068,7 +1156,7 @@ void basFormRtEventLoop(BasFormRtT *rt) { return; } - while (rt->ctx->running && arrlen(rt->forms) > 0) { + while (rt->ctx->running && arrlen(rt->forms) > 0 && !rt->terminated) { if (!dvxUpdate(rt->ctx)) { break; } @@ -1080,7 +1168,13 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) { BasFormRtT *rt = (BasFormRtT *)ctx; BasFormT *form = (BasFormT *)formRef; + dvxLog("[FC] findCtrl: form=%p '%s' looking for '%s'", + (void *)form, + form ? form->name : "(null)", + ctrlName ? ctrlName : "(null)"); + if (!form) { + dvxLog("[FC] NULL FORM"); return NULL; } @@ -1111,11 +1205,41 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) { } } - // Search other loaded forms by name (for cross-form property access) + // Search across all loaded forms (for cross-form property access). + // Dynamic forms created by CreateForm/CreateControl hold controls + // that the calling SUB references by name (e.g. a mnuXxx_Click on + // the main form sets properties on a control just created on a new + // form). Check form names first, then each form's controls, then + // each form's menu items. if (rt) { for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { - if (strcasecmp(rt->forms[i]->name, ctrlName) == 0) { - return &rt->forms[i]->formCtrl; + BasFormT *other = rt->forms[i]; + + if (other == form) { + continue; + } + + if (strcasecmp(other->name, ctrlName) == 0) { + return &other->formCtrl; + } + + for (int32_t j = 0; j < (int32_t)arrlen(other->controls); j++) { + if (strcasecmp(other->controls[j]->name, ctrlName) == 0) { + return other->controls[j]; + } + } + + for (int32_t j = 0; j < other->menuIdMapCount; j++) { + if (strcasecmp(other->menuIdMap[j].name, ctrlName) == 0) { + if (!other->menuIdMap[j].proxy) { + BasControlT *proxy = (BasControlT *)calloc(1, sizeof(BasControlT)); + snprintf(proxy->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + proxy->form = other; + proxy->menuId = other->menuIdMap[j].id; + other->menuIdMap[j].proxy = proxy; + } + return other->menuIdMap[j].proxy; + } } } } @@ -1175,9 +1299,14 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName BasValueT *prevVars = rt->vm->currentFormVars; int32_t prevVarCount = rt->vm->currentFormVarCount; + // Bind form-scope vars to the SUB's owning form (from BEGINFORM), + // not to the control's form. + BasFormT *owningForm = resolveOwningForm(rt, proc); + BasFormT *varsForm = owningForm ? owningForm : form; + rt->currentForm = form; basVmSetCurrentForm(rt->vm, form); - basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); + basVmSetCurrentFormVars(rt->vm, varsForm->formVars, varsForm->formVarCount); bool ok; @@ -1222,9 +1351,14 @@ static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const c BasValueT *prevVars = rt->vm->currentFormVars; int32_t prevVarCount = rt->vm->currentFormVarCount; + // Bind form-scope vars to the SUB's owning form (from BEGINFORM), + // not to the control's form. + BasFormT *owningForm = resolveOwningForm(rt, proc); + BasFormT *varsForm = owningForm ? owningForm : form; + rt->currentForm = form; basVmSetCurrentForm(rt->vm, form); - basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); + basVmSetCurrentFormVars(rt->vm, varsForm->formVars, varsForm->formVarCount); bool cancelled = false; @@ -1251,10 +1385,14 @@ static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const c BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { - (void)ctx; + BasFormRtT *rt = (BasFormRtT *)ctx; BasControlT *ctrl = (BasControlT *)ctrlRef; if (!ctrl) { + basFormRtRuntimeError(rt, + "Read of unknown control", + "Property: %s\nControl was not found on any loaded form.", + propName ? propName : "?"); return zeroValue(); } @@ -1274,10 +1412,18 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { return basValStringFromC(ctrl->name); } + basFormRtRuntimeError(rt, + "Unknown menu property", + "Menu: %s\nProperty: %s\nMenu items only expose Checked, Enabled, and Name.", + ctrl->name, propName ? propName : "?"); return zeroValue(); } if (!ctrl->widget) { + basFormRtRuntimeError(rt, + "Property read on control with no widget", + "Control: %s\nProperty: %s\nThe control is known but has no attached widget.", + ctrl->name, propName ? propName : "?"); return zeroValue(); } @@ -1298,6 +1444,10 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { if (strcasecmp(propName, "Centered") == 0) { return basValBool(frm->frmCentered); } if (strcasecmp(propName, "Layout") == 0) { return basValStringFromC(frm->frmLayout); } + basFormRtRuntimeError(rt, + "Unknown form property", + "Form: %s\nProperty: %s\nValid: Name, Caption, Width, Height, Left, Top, Visible, Resizable, AutoSize, Centered, Layout.", + frm->name, propName ? propName : "?"); return zeroValue(); } @@ -1351,6 +1501,12 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { } } + basFormRtRuntimeError(rt, + "Property not found on control", + "Control: %s\nType: %s\nProperty: %s\nThe control has no readable property by that name.", + ctrl->name, + ctrl->typeName[0] ? ctrl->typeName : "?", + propName ? propName : "?"); return zeroValue(); } @@ -1359,7 +1515,19 @@ void basFormRtHideForm(void *ctx, void *formRef) { BasFormRtT *rt = (BasFormRtT *)ctx; BasFormT *form = (BasFormT *)formRef; - if (!form || !form->window) { + if (!form) { + basFormRtRuntimeError(rt, + "Hide on unknown form", + "The form reference resolved to NULL; check that the form name exists.", + NULL); + return; + } + + if (!form->window) { + basFormRtRuntimeError(rt, + "Hide on form with no window", + "Form: %s\nThe form has no window attached (was it destroyed already?).", + form->name); return; } @@ -1566,7 +1734,13 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { form->formCtrl.widget = root; form->formCtrl.form = form; - // Allocate per-form variable storage and run init code from module metadata + // Allocate per-form variable storage. Init code runs later: + // - If we were reached recursively from basFormRtLoadFrm (sLoadingFrm + // is true), it runs AFTER parsing populates form->controls, so + // init code can see OutArea et al. Running it here would fire + // against an empty controls list. + // - Otherwise this is a genuine bare form (no cached .frm); we + // still need to run init to DIM arrays and UDT fields. 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) { @@ -1577,10 +1751,9 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { 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) { + if (!sLoadingFrm && initAddr >= 0 && rt->vm) { basVmSetCurrentForm(rt->vm, form); basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount); basVmCallSub(rt->vm, initAddr); @@ -1602,363 +1775,31 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen return NULL; } - BasFormT *form = NULL; - BasControlT *current = NULL; - - WidgetT *parentStack[BAS_MAX_FRM_NESTING]; - int32_t nestDepth = 0; - - // Track Begin/End blocks: true = container (Form/Frame), false = control - bool isContainer[BAS_MAX_FRM_NESTING]; - int32_t blockDepth = 0; - - // Temporary menu item accumulation - typedef struct { - char caption[256]; - char name[BAS_MAX_CTRL_NAME]; - int32_t level; - bool checked; - bool radioCheck; - bool enabled; - } TempMenuItemT; - - TempMenuItemT *menuItems = NULL; // stb_ds array - TempMenuItemT *curMenuItem = NULL; - int32_t menuNestDepth = 0; - bool inMenu = false; - - const char *pos = source; - const char *end = source + sourceLen; - - while (pos < end) { - const char *lineStart = pos; - - while (pos < end && *pos != '\n' && *pos != '\r') { - pos++; - } - - int32_t lineLen = (int32_t)(pos - lineStart); - - if (pos < end && *pos == '\r') { - pos++; - } - - if (pos < end && *pos == '\n') { - pos++; - } - - char line[BAS_MAX_FRM_LINE_LEN]; - - if (lineLen >= BAS_MAX_FRM_LINE_LEN) { - lineLen = BAS_MAX_FRM_LINE_LEN - 1; - } - - memcpy(line, lineStart, lineLen); - line[lineLen] = '\0'; - - const char *trimmed = dvxSkipWs(line); - - if (*trimmed == '\0' || *trimmed == '\'') { - continue; - } - - // "VERSION DVX x.xx" (native) or "VERSION x.xx" (VB import) - if (strncasecmp(trimmed, "VERSION ", 8) == 0) { - const char *ver = trimmed + 8; - - if (strncasecmp(ver, "DVX ", 4) != 0) { - double vbVer = atof(ver); - - if (vbVer > 2.0) { - return NULL; // VB4+ form, not compatible - } - } - - continue; - } - - // "Begin TypeName CtrlName" - if (strncasecmp(trimmed, "Begin ", 6) == 0) { - const char *rest = trimmed + 6; - char typeName[BAS_MAX_CTRL_NAME]; - char ctrlName[BAS_MAX_CTRL_NAME]; - - int32_t ti = 0; - - while (*rest && *rest != ' ' && *rest != '\t' && ti < BAS_MAX_CTRL_NAME - 1) { - typeName[ti++] = *rest++; - } - - typeName[ti] = '\0'; - - rest = dvxSkipWs(rest); - - int32_t ci = 0; - - while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < BAS_MAX_CTRL_NAME - 1) { - ctrlName[ci++] = *rest++; - } - - ctrlName[ci] = '\0'; - - if (strcasecmp(typeName, "Form") == 0) { - form = (BasFormT *)basFormRtLoadForm(rt, ctrlName); - - if (!form) { - return NULL; - } - - // contentBox may already be set from basFormRtLoadForm. - // It gets replaced at the End block after Layout is known. - nestDepth = 1; - parentStack[0] = form->contentBox; - current = NULL; - - if (blockDepth < BAS_MAX_FRM_NESTING) { - isContainer[blockDepth++] = true; - } - } else if (strcasecmp(typeName, "Menu") == 0 && form) { - TempMenuItemT mi; - memset(&mi, 0, sizeof(mi)); - snprintf(mi.name, BAS_MAX_CTRL_NAME, "%s", ctrlName); - mi.level = menuNestDepth; - mi.enabled = true; - arrput(menuItems, mi); - curMenuItem = &menuItems[arrlen(menuItems) - 1]; - current = NULL; - menuNestDepth++; - inMenu = true; - - if (blockDepth < BAS_MAX_FRM_NESTING) { - isContainer[blockDepth++] = false; - } - - continue; - } else if (form && nestDepth > 0) { - // Create the content box on first control if not yet done - if (!form->contentBox && form->root) { - form->contentBox = basFormRtCreateContentBox(form->root, form->frmLayout); - parentStack[0] = form->contentBox; - } - WidgetT *parent = parentStack[nestDepth - 1]; - - const char *wgtTypeName = resolveTypeName(typeName); - - if (!wgtTypeName) { - continue; - } - - WidgetT *widget = createWidget(wgtTypeName, parent); - - if (!widget) { - continue; - } - - wgtSetName(widget, ctrlName); - - { - BasControlT *ctrlEntry = (BasControlT *)calloc(1, sizeof(BasControlT)); - - if (!ctrlEntry) { - continue; - } - - snprintf(ctrlEntry->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); - snprintf(ctrlEntry->typeName, BAS_MAX_CTRL_NAME, "%s", typeName); - ctrlEntry->index = -1; - ctrlEntry->widget = widget; - ctrlEntry->form = form; - ctrlEntry->iface = wgtGetIface(wgtTypeName); - arrput(form->controls, ctrlEntry); - - - current = ctrlEntry; - widget->userData = current; - widget->onClick = onWidgetClick; - widget->onDblClick = onWidgetDblClick; - widget->onChange = onWidgetChange; - widget->onFocus = onWidgetFocus; - widget->onBlur = onWidgetBlur; - widget->onValidate = onWidgetValidate; - widget->onKeyPress = onWidgetKeyPress; - widget->onKeyDown = onWidgetKeyDown; - widget->onKeyUp = onWidgetKeyUp; - widget->onMouseDown = onWidgetMouseDown; - widget->onMouseUp = onWidgetMouseUp; - widget->onMouseMove = onWidgetMouseMove; - widget->onScroll = onWidgetScroll; - } - - // Track block type for End handling - const WgtIfaceT *ctrlIface = wgtGetIface(wgtTypeName); - bool isCtrlContainer = ctrlIface && ctrlIface->isContainer; - - if (isCtrlContainer && nestDepth < BAS_MAX_FRM_NESTING) { - // Push the widget for now; the Layout property hasn't - // been parsed yet. It will be applied when we see it - // via containerLayout[] below. - parentStack[nestDepth++] = widget; - - if (blockDepth < BAS_MAX_FRM_NESTING) { - isContainer[blockDepth++] = true; - } - } else { - if (blockDepth < BAS_MAX_FRM_NESTING) { - isContainer[blockDepth++] = false; - } - } - } - - continue; - } - - // "End" - if (strcasecmp(trimmed, "End") == 0) { - if (inMenu) { - menuNestDepth--; - curMenuItem = NULL; - - if (menuNestDepth <= 0) { - menuNestDepth = 0; - inMenu = false; - } - - if (blockDepth > 0) { - blockDepth--; - } - - continue; - } - - if (blockDepth > 0) { - blockDepth--; - - // Only decrement parent nesting for containers (Form/Frame) - if (isContainer[blockDepth] && nestDepth > 0) { - nestDepth--; - } - } - - current = NULL; - continue; - } - - // Property assignment: Key = Value - char key[BAS_MAX_CTRL_NAME]; - char value[BAS_MAX_FRM_LINE_LEN]; - parseFrmLine(trimmed, key, value); - - if (key[0] == '\0' || !form) { - continue; - } - - if (curMenuItem) { - // Menu item properties - char *text = value; - - if (text[0] == '"') { - text++; - int32_t len = (int32_t)strlen(text); - - if (len > 0 && text[len - 1] == '"') { - text[len - 1] = '\0'; - } - } - - if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, sizeof(curMenuItem->caption), "%s", text); } - else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0); } - else if (strcasecmp(key, "RadioCheck") == 0) { curMenuItem->radioCheck = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0); } - else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0 || strcasecmp(text, "False") != 0); } - } else if (current) { - // Control array index is stored on the struct, not as a widget property - if (strcasecmp(key, "Index") == 0) { - current->index = atoi(value); - continue; - } - - // HelpTopic is stored on BasControlT, not on the widget - if (strcasecmp(key, "HelpTopic") == 0) { - char *text = value; - if (text[0] == '"') { text++; } - int32_t tlen = (int32_t)strlen(text); - if (tlen > 0 && text[tlen - 1] == '"') { text[tlen - 1] = '\0'; } - snprintf(current->helpTopic, sizeof(current->helpTopic), "%s", text); - continue; - } - - // Layout property on a container: replace the parentStack entry - // with a layout box inside the container widget. - // NOTE: only do this for non-default layouts (HBox, WrapBox). - // VBox is the default for Frame, so no wrapper needed. - if (strcasecmp(key, "Layout") == 0 && current && current->widget && nestDepth > 0) { - char *text = value; - if (text[0] == '"') { text++; } - int32_t tlen = (int32_t)strlen(text); - if (tlen > 0 && text[tlen - 1] == '"') { text[tlen - 1] = '\0'; } - if (strcasecmp(text, "VBox") != 0) { - parentStack[nestDepth - 1] = basFormRtCreateContentBox(current->widget, text); - } - continue; - } - - BasValueT val; - - if (value[0] == '"') { - int32_t vlen = (int32_t)strlen(value); - - if (vlen >= 2 && value[vlen - 1] == '"') { - value[vlen - 1] = '\0'; - } - - val = basValStringFromC(value + 1); - } else if (strcasecmp(value, "True") == 0) { - val = basValBool(true); - } else if (strcasecmp(value, "False") == 0) { - val = basValBool(false); - } else { - val = basValLong(atoi(value)); - } - - basFormRtSetProp(rt, current, key, val); - basValRelease(&val); - } else if (nestDepth > 0) { - // Form-level property -- strip quotes from string values - char *text = value; - - if (text[0] == '"') { - text++; - int32_t len = (int32_t)strlen(text); - - if (len > 0 && text[len - 1] == '"') { - text[len - 1] = '\0'; - } - } - - if (strcasecmp(key, "Caption") == 0) { - dvxSetTitle(rt->ctx, form->window, text); - } else if (strcasecmp(key, "Width") == 0) { - form->frmWidth = atoi(value); - } else if (strcasecmp(key, "Height") == 0) { - form->frmHeight = atoi(value); - } else if (strcasecmp(key, "Left") == 0) { - form->frmLeft = atoi(value); - } else if (strcasecmp(key, "Top") == 0) { - form->frmTop = atoi(value); - } else if (strcasecmp(key, "Resizable") == 0) { - form->frmResizable = (strcasecmp(text, "True") == 0); - form->frmHasResizable = true; - } else if (strcasecmp(key, "Centered") == 0) { - form->frmCentered = (strcasecmp(text, "True") == 0); - } else if (strcasecmp(key, "AutoSize") == 0) { - form->frmAutoSize = (strcasecmp(text, "True") == 0); - } else if (strcasecmp(key, "Layout") == 0) { - snprintf(form->frmLayout, sizeof(form->frmLayout), "%s", text); - } else if (strcasecmp(key, "HelpTopic") == 0) { - snprintf(form->helpTopic, sizeof(form->helpTopic), "%s", text); - } - } + BasFrmLoadCtxT ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.rt = rt; + ctx.curMenuItemIdx = -1; + + FrmParserCbsT cbs; + memset(&cbs, 0, sizeof(cbs)); + cbs.userData = &ctx; + cbs.onFormBegin = frmLoad_onFormBegin; + cbs.onFormProp = frmLoad_onFormProp; + cbs.onMenuBegin = frmLoad_onMenuBegin; + cbs.onMenuEnd = frmLoad_onMenuEnd; + cbs.onMenuProp = frmLoad_onMenuProp; + cbs.onCtrlBegin = frmLoad_onCtrlBegin; + cbs.onCtrlEnd = frmLoad_onCtrlEnd; + cbs.onCtrlProp = frmLoad_onCtrlProp; + + if (!frmParse(source, sourceLen, &cbs)) { + arrfree(ctx.menuItems); + return NULL; } + BasFormT *form = ctx.form; + BasFrmMenuItemT *menuItems = ctx.menuItems; + // Apply accumulated form-level properties if (form) { // Ensure content box exists even if form has no controls @@ -1978,8 +1819,8 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen memset(menuStack, 0, sizeof(menuStack)); for (int32_t i = 0; i < menuCount; i++) { - TempMenuItemT *mi = &menuItems[i]; - bool isSep = (mi->caption[0] == '-' && (mi->caption[1] == '\0' || mi->caption[1] == '-')); + BasFrmMenuItemT *mi = &menuItems[i]; + bool isSep = (mi->caption[0] == '-' && (mi->caption[1] == '\0' || mi->caption[1] == '-')); bool isSubParent = (i + 1 < menuCount && menuItems[i + 1].level > mi->level); if (mi->level == 0) { @@ -2177,6 +2018,73 @@ int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const cha } +// Report a non-recoverable runtime error. Logs to DVX.LOG with the +// summary on one line and the details indented beneath, then shows a +// modal MessageBox with the details, then halts the VM so execution +// stops instead of limping on with a corrupted state. +// +// Used for errors that indicate a bug in the BASIC program: unknown +// control type, missing control reference, unknown method, etc. +// Silent no-ops here would hide these failures from the developer. +void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...) { + char details[512]; + va_list ap; + + va_start(ap, detailFmt); + vsnprintf(details, sizeof(details), detailFmt, ap); + va_end(ap); + + // Log with a blank line and indent so it stands out among normal + // log traffic. + dvxLog(""); + dvxLog("BASIC RUNTIME ERROR: %s", summary ? summary : "(no summary)"); + + // Split details on newlines and indent each one. + const char *p = details; + + while (*p) { + const char *nl = strchr(p, '\n'); + int32_t len = nl ? (int32_t)(nl - p) : (int32_t)strlen(p); + char line[256]; + + if (len >= (int32_t)sizeof(line)) { + len = sizeof(line) - 1; + } + + memcpy(line, p, len); + line[len] = '\0'; + dvxLog(" %s", line); + + if (!nl) { + break; + } + + p = nl + 1; + } + + if (rt && rt->ctx) { + char boxMsg[640]; + snprintf(boxMsg, sizeof(boxMsg), "%s\n\n%s", + summary ? summary : "Runtime error", + details); + dvxMessageBox(rt->ctx, "BASIC Runtime Error", boxMsg, MB_OK | MB_ICONERROR); + } + + // Halt the VM so execution stops here rather than stumbling forward + // with a corrupted state. Without this, a single missing control + // can trigger a cascade of follow-on errors as subsequent property + // accesses and method calls fail too. The `terminated` flag tells + // basFormRtEventLoop to exit at its next pump -- otherwise the main + // window would stay open with a dead VM behind it. + if (rt) { + if (rt->vm) { + rt->vm->running = false; + } + rt->terminated = true; + } +} + + void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *data, int32_t dataLen) { if (!rt || !formName || !data || dataLen <= 0) { return; @@ -2276,6 +2184,13 @@ void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const ch (void)ctx; BasControlT *ctrl = (BasControlT *)ctrlRef; + dvxLog("[SE] SetEvent: ctrl=%p '%s' type='%s' event='%s' -> handler='%s'", + (void *)ctrl, + ctrl ? ctrl->name : "(null)", + ctrl ? ctrl->typeName : "(null)", + eventName ? eventName : "(null)", + handlerName ? handlerName : "(null)"); + if (!ctrl || !eventName || !handlerName) { return; } @@ -2293,6 +2208,9 @@ void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const ch BasEventOverrideT *ov = &ctrl->eventOverrides[ctrl->eventOverrideCount++]; snprintf(ov->eventName, BAS_MAX_CTRL_NAME, "%s", eventName); snprintf(ov->handlerName, BAS_MAX_CTRL_NAME, "%s", handlerName); + dvxLog("[SE] added (count=%d)", (int)ctrl->eventOverrideCount); + } else { + dvxLog("[SE] NO SLOT (count=%d)", (int)ctrl->eventOverrideCount); } } @@ -2302,6 +2220,10 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT BasControlT *ctrl = (BasControlT *)ctrlRef; if (!ctrl) { + basFormRtRuntimeError(rt, + "Assignment to unknown control", + "Property: %s\nControl was not found on any loaded form.", + propName ? propName : "?"); return; } @@ -2319,10 +2241,18 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT return; } + basFormRtRuntimeError(rt, + "Unknown menu property", + "Menu: %s\nProperty: %s\nMenu items only support Checked and Enabled.", + ctrl->name, propName ? propName : "?"); return; } if (!ctrl->widget) { + basFormRtRuntimeError(rt, + "Property set on control with no widget", + "Control: %s\nProperty: %s\nThe control is known but has no attached widget.", + ctrl->name, propName ? propName : "?"); return; } @@ -2399,6 +2329,10 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT return; } + basFormRtRuntimeError(rt, + "Unknown form property", + "Form: %s\nProperty: %s\nValid: Caption, Visible, Width, Height, Left, Top, Resizable, AutoSize, Centered.", + frm->name, propName ? propName : "?"); return; } @@ -2411,6 +2345,7 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT // strdup their text internally). if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) { BasStringT *s = basValFormatString(value); + dvxLog("[SP] setProp Caption: ctrl='%s' text='%s'", ctrl->name, s ? s->data : "(null)"); wgtSetText(ctrl->widget, s->data); basStringUnref(s); return; @@ -2445,6 +2380,16 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT return; } } + + // None of the common, form-local, or iface property paths matched. + // A silent no-op used to hide typos; now it's a loud runtime error + // that mirrors the method-not-found diagnostic. + basFormRtRuntimeError(rt, + "Property not found on control", + "Control: %s\nType: %s\nProperty: %s\nThe control has no writable property by that name.", + ctrl->name, + ctrl->typeName[0] ? ctrl->typeName : "?", + propName ? propName : "?"); } @@ -2452,7 +2397,19 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { BasFormRtT *rt = (BasFormRtT *)ctx; BasFormT *form = (BasFormT *)formRef; - if (!form || !form->window) { + if (!form) { + basFormRtRuntimeError(rt, + "Show on unknown form", + "The form reference resolved to NULL; check that the form name exists.", + NULL); + return; + } + + if (!form->window) { + basFormRtRuntimeError(rt, + "Show on form with no window", + "Form: %s\nThe form has no window attached (was it destroyed already?).", + form->name); return; } @@ -2530,8 +2487,16 @@ void basFormRtUnloadForm(void *ctx, void *formRef) { } +// Tracks whether the most recent basInputBox2 call was cancelled. +// BASIC callers query this via basInputCancelled so they can tell the +// difference between the user hitting Cancel and the user clicking OK +// on an empty field. +static bool sLastInputBoxCancelled = false; + + const char *basInputBox2(const char *title, const char *prompt, const char *defaultText) { if (!sFormRt) { + sLastInputBoxCancelled = true; return ""; } @@ -2539,13 +2504,20 @@ const char *basInputBox2(const char *title, const char *prompt, const char *defa buf[0] = '\0'; if (dvxInputBox(sFormRt->ctx, title, prompt, defaultText, buf, sizeof(buf))) { + sLastInputBoxCancelled = false; return buf; } + sLastInputBoxCancelled = true; return ""; } +int32_t basInputCancelled(void) { + return sLastInputBoxCancelled ? -1 : 0; +} + + int32_t basIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal) { if (!sFormRt) { return defaultVal; @@ -2572,6 +2544,33 @@ static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char } +// Find the loaded form whose .frm declared this SUB. Returns NULL if +// the SUB is module-global (no BEGINFORM scope) or the owning form +// isn't currently loaded -- callers should fall back to ctrl->form. +static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc) { + if (!rt || !proc) { + return NULL; + } + + dvxLog("[OF] resolveOwningForm: proc='%s' formName='%s' formCount=%d", + proc->name, proc->formName, (int)arrlen(rt->forms)); + + if (!proc->formName[0]) { + return NULL; + } + + for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) { + dvxLog("[OF] form[%d] name='%s' varCount=%d", + (int)i, rt->forms[i]->name, (int)rt->forms[i]->formVarCount); + if (strcasecmp(rt->forms[i]->name, proc->formName) == 0) { + return rt->forms[i]; + } + } + + return NULL; +} + + int32_t basPromptSave(const char *title) { if (!sFormRt) { return 2; @@ -2582,9 +2581,6 @@ int32_t basPromptSave(const char *title) { static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc) { - (void)args; - (void)argc; - if (strcasecmp(methodName, "SetFocus") == 0) { wgtSetFocused(ctrl->widget); return zeroValue(); @@ -2595,6 +2591,35 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas return zeroValue(); } + if (strcasecmp(methodName, "SetReadOnly") == 0) { + bool ro = (argc >= 1) ? (basValToNumber(args[0]) != 0.0) : true; + wgtSetReadOnly(ctrl->widget, ro); + return zeroValue(); + } + + if (strcasecmp(methodName, "SetEnabled") == 0) { + bool en = (argc >= 1) ? (basValToNumber(args[0]) != 0.0) : true; + wgtSetEnabled(ctrl->widget, en); + return zeroValue(); + } + + if (strcasecmp(methodName, "SetVisible") == 0) { + bool vis = (argc >= 1) ? (basValToNumber(args[0]) != 0.0) : true; + wgtSetVisible(ctrl->widget, vis); + return zeroValue(); + } + + // Unknown method: raise a loud runtime error (visible MessageBox + + // DVX.LOG entry). Silent no-ops here used to hide typos in method + // names. This is a runtime safety net -- the parser should reject + // unknown methods at compile time once bascomp can see widget + // interface metadata. + const char *ctrlName = ctrl ? ctrl->name : "?"; + basFormRtRuntimeError(sFormRt, + "Method not found on control", + "Method: %s\nControl: %s\nThe control has no method by that name.", + methodName, ctrlName); + return zeroValue(); } @@ -2911,8 +2936,16 @@ WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) { return NULL; } - // Determine creation signature from the widget interface descriptor. const WgtIfaceT *iface = wgtGetIface(wgtTypeName); + return createWidgetByIface(iface, api, parent, true); +} + + +WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *parent, bool allowData) { + if (!api) { + return NULL; + } + uint8_t sig = iface ? iface->createSig : WGT_CREATE_PARENT; typedef WidgetT *(*CreateParentFnT)(WidgetT *); @@ -2949,6 +2982,10 @@ WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) { return fn(parent, (bool)iface->createArgs[0]); } case WGT_CREATE_PARENT_DATA: { + if (!allowData) { + // Design-time: can't auto-create Image/ImageButton without pixel data + return NULL; + } // create(parent, NULL, 0, 0, 0) -- empty widget, load content later via properties typedef WidgetT *(*CreateDataFnT)(WidgetT *, uint8_t *, int32_t, int32_t, int32_t); CreateDataFnT fn = *(CreateDataFnT *)api; @@ -2963,6 +3000,29 @@ WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) { static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount) { + dvxLog("[FE] fireCtrlEvent: ctrl=%p '%s' type='%s' event='%s' eventFiring=%d overrideCount=%d", + (void *)ctrl, + ctrl ? ctrl->name : "(null)", + ctrl ? ctrl->typeName : "(null)", + eventName ? eventName : "(null)", + ctrl ? (int)ctrl->eventFiring : -1, + ctrl ? (int)ctrl->eventOverrideCount : -1); + + // Re-entrancy guard: reject only same-event re-entry (runSubLoop + // pumps events during long handlers; without this, a lingering + // mouse-up can re-deliver Click on top of itself). But allow + // different events on the same control (e.g. LostFocus while Click + // is still running) -- those aren't re-entrant, they're interleaved. + if (ctrl->eventFiring) { + bool sameEvent = (ctrl->firingEventName[0] && + strcasecmp(ctrl->firingEventName, eventName) == 0); + + if (sameEvent) { + dvxLog(" SUPPRESSED (same event already firing)"); + return; + } + } + // Build final argument list (prepend Index for control arrays). The // array is sized to the actual arg count so any number of event args // passes through intact; the +1 is for the Index prefix. @@ -2989,18 +3049,27 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa // Check for event override (SetEvent) for (int32_t i = 0; i < ctrl->eventOverrideCount; i++) { + dvxLog("[FE] override[%d] event='%s' handler='%s'", + (int)i, + ctrl->eventOverrides[i].eventName, + ctrl->eventOverrides[i].handlerName); if (strcasecmp(ctrl->eventOverrides[i].eventName, eventName) != 0) { continue; } // Override found: call the named SUB directly if (!rt->vm || !rt->module) { + dvxLog("[FE] NO VM/MODULE"); return; } const BasProcEntryT *proc = basModuleFindProc(rt->module, ctrl->eventOverrides[i].handlerName); if (!proc || proc->isFunction) { + dvxLog("[FE] PROC NOT FOUND or IS FUNCTION: '%s' proc=%p isFunction=%d", + ctrl->eventOverrides[i].handlerName, + (void *)proc, + proc ? (int)proc->isFunction : -1); free(allArgs); return; } @@ -3009,14 +3078,49 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa BasValueT *prevVars = rt->vm->currentFormVars; int32_t prevVarCount = rt->vm->currentFormVarCount; + // The SUB's form-scope variables live on its OWNING form (the + // .frm that declared it inside BEGINFORM...ENDFORM), not on + // the control's form -- those can differ when a SUB from form + // A is wired as an event handler for a control on form B. + BasFormT *owningForm = resolveOwningForm(rt, proc); + BasFormT *varsForm = owningForm ? owningForm : ctrl->form; + rt->currentForm = ctrl->form; basVmSetCurrentForm(rt->vm, ctrl->form); - basVmSetCurrentFormVars(rt->vm, ctrl->form->formVars, ctrl->form->formVarCount); + basVmSetCurrentFormVars(rt->vm, varsForm->formVars, varsForm->formVarCount); + // Only claim the guard if we're the outer event. Nested + // different-event dispatches (e.g. LostFocus while Click is + // running) must NOT touch eventFiring/firingEventName, or + // the outer event's same-event suppression would be broken. + bool claimedGuard = !ctrl->eventFiring; + + if (claimedGuard) { + ctrl->eventFiring = true; + snprintf(ctrl->firingEventName, sizeof(ctrl->firingEventName), "%s", eventName ? eventName : ""); + } + + dvxLog(" -> calling SUB '%s' at addr=%d", ctrl->eventOverrides[i].handlerName, (int)proc->codeAddr); + + rt->vm->errorMsg[0] = '\0'; + rt->vm->errorNumber = 0; + + bool subOk; if (finalArgCount > 0 && finalArgs) { - basVmCallSubWithArgs(rt->vm, proc->codeAddr, finalArgs, finalArgCount); + subOk = basVmCallSubWithArgs(rt->vm, proc->codeAddr, finalArgs, finalArgCount); } else { - basVmCallSub(rt->vm, proc->codeAddr); + subOk = basVmCallSub(rt->vm, proc->codeAddr); + } + + dvxLog(" <- SUB '%s' returned ok=%d errNum=%d errMsg='%s'", + ctrl->eventOverrides[i].handlerName, + (int)subOk, + (int)rt->vm->errorNumber, + rt->vm->errorMsg); + + if (claimedGuard) { + ctrl->eventFiring = false; + ctrl->firingEventName[0] = '\0'; } rt->currentForm = prevForm; @@ -3026,17 +3130,277 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa return; } - // No override -- fall back to naming convention (CtrlName_EventName) + // No override -- fall back to naming convention (CtrlName_EventName). + dvxLog("[FE] no override matched, trying naming convention '%s_%s'", + ctrl->name, eventName); + + bool claimedGuard = !ctrl->eventFiring; + + if (claimedGuard) { + ctrl->eventFiring = true; + snprintf(ctrl->firingEventName, sizeof(ctrl->firingEventName), "%s", eventName ? eventName : ""); + } + + bool fired; + if (finalArgCount > 0 && finalArgs) { - basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, finalArgs, finalArgCount); + fired = basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, finalArgs, finalArgCount); } else { - basFormRtFireEvent(rt, ctrl->form, ctrl->name, eventName); + fired = basFormRtFireEvent(rt, ctrl->form, ctrl->name, eventName); + } + + dvxLog("[FE] naming-convention result: fired=%d", (int)fired); + + if (claimedGuard) { + ctrl->eventFiring = false; + ctrl->firingEventName[0] = '\0'; } free(allArgs); } +// ============================================================ +// frmParser callbacks for basFormRtLoadFrm +// ============================================================ + +static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char *name) { + BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData; + + if (!ctx->form || ctx->nestDepth <= 0) { + return; + } + + // Lazy-create the content box once we know the form layout. + if (!ctx->form->contentBox && ctx->form->root) { + ctx->form->contentBox = basFormRtCreateContentBox(ctx->form->root, ctx->form->frmLayout); + ctx->parentStack[0] = ctx->form->contentBox; + } + + const char *wgtTypeName = resolveTypeName(typeName); + bool isCtrlContainer = false; + + if (wgtTypeName) { + WidgetT *parent = ctx->parentStack[ctx->nestDepth - 1]; + WidgetT *widget = createWidget(wgtTypeName, parent); + + if (widget) { + wgtSetName(widget, name); + + BasControlT *ctrlEntry = (BasControlT *)calloc(1, sizeof(BasControlT)); + + if (ctrlEntry) { + snprintf(ctrlEntry->name, BAS_MAX_CTRL_NAME, "%s", name); + snprintf(ctrlEntry->typeName, BAS_MAX_CTRL_NAME, "%s", typeName); + ctrlEntry->index = -1; + ctrlEntry->widget = widget; + ctrlEntry->form = ctx->form; + ctrlEntry->iface = wgtGetIface(wgtTypeName); + arrput(ctx->form->controls, ctrlEntry); + + ctx->current = ctrlEntry; + + widget->userData = ctrlEntry; + widget->onClick = onWidgetClick; + widget->onDblClick = onWidgetDblClick; + widget->onChange = onWidgetChange; + widget->onFocus = onWidgetFocus; + widget->onBlur = onWidgetBlur; + widget->onValidate = onWidgetValidate; + widget->onKeyPress = onWidgetKeyPress; + widget->onKeyDown = onWidgetKeyDown; + widget->onKeyUp = onWidgetKeyUp; + widget->onMouseDown = onWidgetMouseDown; + widget->onMouseUp = onWidgetMouseUp; + widget->onMouseMove = onWidgetMouseMove; + widget->onScroll = onWidgetScroll; + } + + const WgtIfaceT *ctrlIface = wgtGetIface(wgtTypeName); + isCtrlContainer = ctrlIface && ctrlIface->isContainer; + + if (isCtrlContainer && ctx->nestDepth < BAS_MAX_FRM_NESTING) { + ctx->parentStack[ctx->nestDepth++] = widget; + } + } + } + + if (ctx->containerDepth < BAS_MAX_FRM_NESTING) { + ctx->containerStack[ctx->containerDepth++] = isCtrlContainer; + } +} + + +static void frmLoad_onCtrlEnd(void *userData) { + BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData; + + if (ctx->containerDepth > 0) { + ctx->containerDepth--; + + if (ctx->containerStack[ctx->containerDepth] && ctx->nestDepth > 0) { + ctx->nestDepth--; + } + } + + ctx->current = NULL; +} + + +static void frmLoad_onCtrlProp(void *userData, const char *key, const char *value) { + BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData; + + if (!ctx->current) { + return; + } + + // Control array index is stored on the struct, not as a widget property + if (strcasecmp(key, "Index") == 0) { + ctx->current->index = atoi(value); + return; + } + + char scratch[BAS_MAX_FRM_LINE_LEN]; + snprintf(scratch, sizeof(scratch), "%s", value); + frmStripQuotes(scratch); + + if (strcasecmp(key, "HelpTopic") == 0) { + snprintf(ctx->current->helpTopic, sizeof(ctx->current->helpTopic), "%s", scratch); + return; + } + + // Layout property on a container: replace the parentStack entry + // with a layout box inside the container widget. VBox is the + // default for Frame, so no wrapper needed. + if (strcasecmp(key, "Layout") == 0 && ctx->current->widget && ctx->nestDepth > 0) { + if (strcasecmp(scratch, "VBox") != 0) { + ctx->parentStack[ctx->nestDepth - 1] = basFormRtCreateContentBox(ctx->current->widget, scratch); + } + return; + } + + BasValueT val; + + if (value[0] == '"') { + val = basValStringFromC(scratch); + } else if (strcasecmp(value, "True") == 0) { + val = basValBool(true); + } else if (strcasecmp(value, "False") == 0) { + val = basValBool(false); + } else { + val = basValLong(atoi(value)); + } + + basFormRtSetProp(ctx->rt, ctx->current, key, val); + basValRelease(&val); +} + + +static bool frmLoad_onFormBegin(void *userData, const char *name) { + BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData; + + ctx->form = (BasFormT *)basFormRtLoadForm(ctx->rt, name); + + if (!ctx->form) { + return false; + } + + // contentBox may already be set from basFormRtLoadForm. It gets + // replaced after Layout is known via onCtrlBegin's lazy creation. + ctx->nestDepth = 1; + ctx->parentStack[0] = ctx->form->contentBox; + ctx->current = NULL; + return true; +} + + +static void frmLoad_onFormProp(void *userData, const char *key, const char *value) { + BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData; + + if (!ctx->form) { + return; + } + + char text[BAS_MAX_FRM_LINE_LEN]; + snprintf(text, sizeof(text), "%s", value); + frmStripQuotes(text); + + if (strcasecmp(key, "Caption") == 0) { + dvxSetTitle(ctx->rt->ctx, ctx->form->window, text); + } else if (strcasecmp(key, "Width") == 0) { + ctx->form->frmWidth = atoi(value); + } else if (strcasecmp(key, "Height") == 0) { + ctx->form->frmHeight = atoi(value); + } else if (strcasecmp(key, "Left") == 0) { + ctx->form->frmLeft = atoi(value); + } else if (strcasecmp(key, "Top") == 0) { + ctx->form->frmTop = atoi(value); + } else if (strcasecmp(key, "Resizable") == 0) { + ctx->form->frmResizable = frmParseBool(text); + ctx->form->frmHasResizable = true; + } else if (strcasecmp(key, "Centered") == 0) { + ctx->form->frmCentered = frmParseBool(text); + } else if (strcasecmp(key, "AutoSize") == 0) { + ctx->form->frmAutoSize = frmParseBool(text); + } else if (strcasecmp(key, "Layout") == 0) { + snprintf(ctx->form->frmLayout, sizeof(ctx->form->frmLayout), "%s", text); + } else if (strcasecmp(key, "HelpTopic") == 0) { + snprintf(ctx->form->helpTopic, sizeof(ctx->form->helpTopic), "%s", text); + } +} + + +static void frmLoad_onMenuBegin(void *userData, const char *name, int32_t level) { + BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData; + + if (!ctx->form) { + return; + } + + BasFrmMenuItemT mi; + memset(&mi, 0, sizeof(mi)); + snprintf(mi.name, BAS_MAX_CTRL_NAME, "%s", name); + mi.level = level; + mi.enabled = true; + arrput(ctx->menuItems, mi); + ctx->curMenuItemIdx = (int32_t)arrlen(ctx->menuItems) - 1; + ctx->current = NULL; +} + + +static void frmLoad_onMenuEnd(void *userData) { + BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData; + + ctx->curMenuItemIdx = -1; +} + + +static void frmLoad_onMenuProp(void *userData, const char *key, const char *value) { + BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData; + + if (ctx->curMenuItemIdx < 0 || ctx->curMenuItemIdx >= (int32_t)arrlen(ctx->menuItems)) { + return; + } + + // Resolve the pointer fresh each use -- arrput on nested menus + // may have reallocated the array. + BasFrmMenuItemT *mip = &ctx->menuItems[ctx->curMenuItemIdx]; + char text[BAS_MAX_FRM_LINE_LEN]; + snprintf(text, sizeof(text), "%s", value); + frmStripQuotes(text); + + if (strcasecmp(key, "Caption") == 0) { + snprintf(mip->caption, sizeof(mip->caption), "%s", text); + } else if (strcasecmp(key, "Checked") == 0) { + mip->checked = frmParseBool(text); + } else if (strcasecmp(key, "RadioCheck") == 0) { + mip->radioCheck = frmParseBool(text); + } else if (strcasecmp(key, "Enabled") == 0) { + // Default-true: anything not "False" enables the item. + mip->enabled = (strcasecmp(text, "False") != 0); + } +} + + static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled) { *handled = true; @@ -3363,6 +3727,11 @@ static void onWidgetBlur(WidgetT *w) { static void onWidgetChange(WidgetT *w) { BasControlT *ctrl = (BasControlT *)w->userData; + dvxLog("[OC] onWidgetChange: w=%p ctrl=%p name='%s' type='%s'", + (void *)w, (void *)ctrl, + ctrl ? ctrl->name : "(null)", + ctrl ? ctrl->typeName : "(null)"); + if (!ctrl || !ctrl->form || !ctrl->form->vm) { return; } @@ -3388,6 +3757,13 @@ static void onWidgetChange(WidgetT *w) { static void onWidgetClick(WidgetT *w) { BasControlT *ctrl = (BasControlT *)w->userData; + dvxLog("[OK] onWidgetClick: w=%p ctrl=%p name='%s' type='%s' form=%p vm=%p", + (void *)w, (void *)ctrl, + ctrl ? ctrl->name : "(null)", + ctrl ? ctrl->typeName : "(null)", + ctrl ? (void *)ctrl->form : NULL, + ctrl && ctrl->form ? (void *)ctrl->form->vm : NULL); + if (!ctrl || !ctrl->form || !ctrl->form->vm) { return; } @@ -3681,42 +4057,6 @@ static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, ch } -static void parseFrmLine(const char *line, char *key, char *value) { - key[0] = '\0'; - value[0] = '\0'; - - line = dvxSkipWs(line); - - int32_t ki = 0; - - while (*line && *line != '=' && *line != ' ' && *line != '\t' && ki < BAS_MAX_CTRL_NAME - 1) { - key[ki++] = *line++; - } - - key[ki] = '\0'; - - line = dvxSkipWs(line); - - if (*line == '=') { - line++; - } - - line = dvxSkipWs(line); - - int32_t vi = 0; - - while (*line && *line != '\r' && *line != '\n' && vi < BAS_MAX_FRM_LINE_LEN - 1) { - value[vi++] = *line++; - } - - value[vi] = '\0'; - - while (vi > 0 && (value[vi - 1] == ' ' || value[vi - 1] == '\t')) { - value[--vi] = '\0'; - } -} - - // refreshDetailControls -- cascade master-detail: refresh detail Data controls static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) { if (!masterCtrl->widget) { @@ -3907,7 +4247,7 @@ const char *ResName(int32_t handle, int32_t index) { // widget type name. First tries wgtFindByBasName for VB names // like "CommandButton", then falls back to direct widget name. -static const char *resolveTypeName(const char *typeName) { +const char *resolveTypeName(const char *typeName) { // Try VB name first (e.g. "CommandButton" -> "button") const char *wgtName = wgtFindByBasName(typeName); @@ -4238,6 +4578,20 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val int32_t w = (int32_t)basValToNumber(value); ctrl->widget->minW = wgtPixels(w); wgtInvalidate(ctrl->widget); + // Widgets with a pixel-buffer (Canvas, Image) need their + // internal buffer resized, not just the layout dimension. + // basFormRtLoadFrm does this once at .frm parse time; for + // dynamic controls (CreateControl + Width/Height properties) + // we run the equivalent here. + if (ctrl->iface && ctrl->widget->minW > 0 && ctrl->widget->minH > 0) { + for (int32_t m = 0; m < ctrl->iface->methodCount; m++) { + if (strcasecmp(ctrl->iface->methods[m].name, "Resize") == 0 && + ctrl->iface->methods[m].sig == WGT_SIG_INT_INT) { + ((void (*)(WidgetT *, int32_t, int32_t))ctrl->iface->methods[m].fn)(ctrl->widget, ctrl->widget->minW, ctrl->widget->minH); + break; + } + } + } return true; } @@ -4245,6 +4599,15 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val int32_t h = (int32_t)basValToNumber(value); ctrl->widget->minH = wgtPixels(h); wgtInvalidate(ctrl->widget); + if (ctrl->iface && ctrl->widget->minW > 0 && ctrl->widget->minH > 0) { + for (int32_t m = 0; m < ctrl->iface->methodCount; m++) { + if (strcasecmp(ctrl->iface->methods[m].name, "Resize") == 0 && + ctrl->iface->methods[m].sig == WGT_SIG_INT_INT) { + ((void (*)(WidgetT *, int32_t, int32_t))ctrl->iface->methods[m].fn)(ctrl->widget, ctrl->widget->minW, ctrl->widget->minH); + break; + } + } + } return true; } @@ -4278,6 +4641,11 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val return true; } + if (strcasecmp(propName, "ReadOnly") == 0) { + wgtSetReadOnly(ctrl->widget, basValIsTruthy(value)); + return true; + } + if (strcasecmp(propName, "TabIndex") == 0) { return true; } @@ -4534,6 +4902,86 @@ static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl) { } +bool wgtApplyPropFromString(WidgetT *w, const WgtPropDescT *p, const char *val) { + if (!w || !p || !p->setFn || !val) { + return false; + } + + if (p->type == WGT_IFACE_ENUM && p->enumNames) { + for (int32_t en = 0; p->enumNames[en]; en++) { + if (strcasecmp(p->enumNames[en], val) == 0) { + ((void (*)(WidgetT *, int32_t))p->setFn)(w, en); + return true; + } + } + return false; + } + + if (p->type == WGT_IFACE_INT) { + ((void (*)(WidgetT *, int32_t))p->setFn)(w, atoi(val)); + return true; + } + + if (p->type == WGT_IFACE_BOOL) { + ((void (*)(WidgetT *, bool))p->setFn)(w, frmParseBool(val)); + return true; + } + + if (p->type == WGT_IFACE_STRING) { + ((void (*)(WidgetT *, const char *))p->setFn)(w, val); + return true; + } + + return false; +} + + +bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize) { + if (!w || !p || !p->getFn || !out || outSize <= 0) { + return false; + } + + if (p->type == WGT_IFACE_ENUM && p->enumNames) { + int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w); + const char *name = NULL; + + for (int32_t en = 0; p->enumNames[en]; en++) { + if (en == v) { + name = p->enumNames[en]; + break; + } + } + + if (!name) { + return false; + } + + snprintf(out, outSize, "%s", name); + return true; + } + + if (p->type == WGT_IFACE_INT) { + int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w); + snprintf(out, outSize, "%d", (int)v); + return true; + } + + if (p->type == WGT_IFACE_BOOL) { + bool v = ((bool (*)(const WidgetT *))p->getFn)(w); + snprintf(out, outSize, "%s", v ? "True" : "False"); + return true; + } + + if (p->type == WGT_IFACE_STRING) { + const char *s = ((const char *(*)(const WidgetT *))p->getFn)(w); + snprintf(out, outSize, "%s", s ? s : ""); + return true; + } + + return false; +} + + static BasValueT zeroValue(void) { BasValueT v; memset(&v, 0, sizeof(v)); diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h index 8b84a2c..5eef1c6 100644 --- a/src/apps/kpunch/dvxbasic/formrt/formrt.h +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h @@ -87,6 +87,15 @@ typedef struct BasControlT { int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls) BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES]; int32_t eventOverrideCount; + // Re-entrancy guard: set while an event handler for this control + // is running. runSubLoop pumps events during long-running handlers + // (painting, draw loops), and without this guard a lingering + // mouse-up or repaint cycle can re-fire the same Click on top of + // itself -- an unbounded recursion. firingEventName records which + // event is in flight so different-event delivery (e.g. LostFocus + // while Click is still running) is allowed through. + bool eventFiring; + char firingEventName[BAS_MAX_CTRL_NAME]; } BasControlT; // ============================================================ @@ -153,6 +162,10 @@ typedef struct { int32_t frmCacheCount; BasCfmCacheT *cfmCache; // stb_ds array of compiled form binaries int32_t cfmCacheCount; + // Set true when a runtime error has halted the program; the event + // loop exits at the next pump so the app doesn't stumble forward + // with a halted VM. + bool terminated; } BasFormRtT; // ============================================================ @@ -209,7 +222,28 @@ void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *d // ---- Widget creation ---- -WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent); +// Create a widget by resolved (DVX) type name. Returns NULL if the type +// is unknown. createWidgetByIface dispatches on the interface +// descriptor's createSig; pass allowData=true for an empty pixel-data +// widget (runtime path), or false to refuse (design-time path). +WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent); +WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *parent, bool allowData); + +// Resolve a VB-style name ("CommandButton") to a DVX widget name +// ("button"). Falls back to the same name if already canonical. Returns +// NULL if the type is unknown. +const char *resolveTypeName(const char *typeName); + +// Apply a "Key = Value" string pair to a widget property via the +// interface descriptor. Handles ENUM/INT/BOOL/STRING dispatch. Returns +// true if the property was set, false if unsupported type / no setter. +bool wgtApplyPropFromString(WidgetT *w, const WgtPropDescT *p, const char *val); + +// Read a widget property via the interface getter and format it into +// out as a .frm-style value ("True", "False", enum name, or decimal +// string). Returns true if the property was read; false if the getter +// is NULL, the type is not serializable, or the enum value is unknown. +bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize); // ---- Form window creation ---- diff --git a/src/apps/kpunch/dvxbasic/formrt/frmParser.c b/src/apps/kpunch/dvxbasic/formrt/frmParser.c new file mode 100644 index 0000000..ee4e664 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/formrt/frmParser.c @@ -0,0 +1,308 @@ +// The MIT License (MIT) +// +// Copyright (C) 2026 Scott Duensing +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +// frmParser.c -- callback-based .frm text parser +// +// See frmParser.h for the public API. + +#include "frmParser.h" +#include "dvxPlat.h" + +#include +#include +#include + +#define FRM_MAX_LINE_LEN 512 +#define FRM_MAX_TOKEN_LEN 64 +#define FRM_MAX_NESTING 16 + + +// Prototypes (alphabetical) +bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb); +bool frmParseBool(const char *val); +void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax); +void frmStripQuotes(char *val); +static bool readToken(const char **p, const char *end, char *buf, int32_t bufMax); + + +typedef enum { + BLK_FORM, + BLK_MENU, + BLK_CTRL +} BlkTypeE; + + +bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) { + if (!source || sourceLen <= 0 || !cb) { + return false; + } + + BlkTypeE blockStack[FRM_MAX_NESTING]; + int32_t blockDepth = 0; + int32_t menuNestDepth = 0; + bool inForm = false; + + const char *pos = source; + const char *end = source + sourceLen; + + while (pos < end) { + const char *lineStart = pos; + + while (pos < end && *pos != '\n' && *pos != '\r') { + pos++; + } + + int32_t lineLen = (int32_t)(pos - lineStart); + + if (pos < end && *pos == '\r') { + pos++; + } + + if (pos < end && *pos == '\n') { + pos++; + } + + char line[FRM_MAX_LINE_LEN]; + + if (lineLen >= FRM_MAX_LINE_LEN) { + lineLen = FRM_MAX_LINE_LEN - 1; + } + + memcpy(line, lineStart, lineLen); + line[lineLen] = '\0'; + + const char *trimmed = dvxSkipWs(line); + + if (*trimmed == '\0' || *trimmed == '\'') { + continue; + } + + // "VERSION DVX x.xx" (native) or "VERSION x.xx" (VB import) + if (strncasecmp(trimmed, "VERSION ", 8) == 0) { + const char *ver = trimmed + 8; + + if (strncasecmp(ver, "DVX ", 4) != 0) { + double vbVer = atof(ver); + + if (vbVer > 2.0) { + return false; // VB4+ form, not compatible + } + } + + continue; + } + + // "Begin TypeName CtrlName" + if (strncasecmp(trimmed, "Begin ", 6) == 0) { + const char *rest = trimmed + 6; + char typeName[FRM_MAX_TOKEN_LEN]; + char ctrlName[FRM_MAX_TOKEN_LEN]; + + readToken(&rest, NULL, typeName, FRM_MAX_TOKEN_LEN); + rest = dvxSkipWs(rest); + readToken(&rest, NULL, ctrlName, FRM_MAX_TOKEN_LEN); + + if (typeName[0] == '\0') { + continue; + } + + if (strcasecmp(typeName, "Form") == 0) { + if (cb->onFormBegin && !cb->onFormBegin(cb->userData, ctrlName)) { + return false; + } + + inForm = true; + + if (blockDepth < FRM_MAX_NESTING) { + blockStack[blockDepth++] = BLK_FORM; + } + } else if (strcasecmp(typeName, "Menu") == 0 && inForm) { + if (cb->onMenuBegin) { + cb->onMenuBegin(cb->userData, ctrlName, menuNestDepth); + } + + menuNestDepth++; + + if (blockDepth < FRM_MAX_NESTING) { + blockStack[blockDepth++] = BLK_MENU; + } + } else if (inForm) { + if (cb->onCtrlBegin) { + cb->onCtrlBegin(cb->userData, typeName, ctrlName); + } + + if (blockDepth < FRM_MAX_NESTING) { + blockStack[blockDepth++] = BLK_CTRL; + } + } + + continue; + } + + // "End" + if (strcasecmp(trimmed, "End") == 0) { + if (blockDepth == 0) { + continue; + } + + blockDepth--; + BlkTypeE blk = blockStack[blockDepth]; + + if (blk == BLK_MENU) { + if (cb->onMenuEnd) { + cb->onMenuEnd(cb->userData); + } + + menuNestDepth--; + + if (menuNestDepth < 0) { + menuNestDepth = 0; + } + } else if (blk == BLK_CTRL) { + if (cb->onCtrlEnd) { + cb->onCtrlEnd(cb->userData); + } + } else { + // Outer Form End -- deliver any trailing text and stop. + inForm = false; + + if (cb->onFormEnd) { + cb->onFormEnd(cb->userData, pos, (int32_t)(end - pos)); + } + + return true; + } + + continue; + } + + // Property assignment: Key = Value + char key[FRM_MAX_TOKEN_LEN]; + char value[FRM_MAX_LINE_LEN]; + frmParseKeyValue(trimmed, key, FRM_MAX_TOKEN_LEN, value, FRM_MAX_LINE_LEN); + + if (key[0] == '\0' || !inForm) { + continue; + } + + BlkTypeE curBlock = (blockDepth > 0) ? blockStack[blockDepth - 1] : BLK_FORM; + + if (curBlock == BLK_MENU) { + if (cb->onMenuProp) { + cb->onMenuProp(cb->userData, key, value); + } + } else if (curBlock == BLK_CTRL) { + if (cb->onCtrlProp) { + cb->onCtrlProp(cb->userData, key, value); + } + } else { + if (cb->onFormProp) { + cb->onFormProp(cb->userData, key, value); + } + } + } + + return true; +} + + +bool frmParseBool(const char *val) { + if (!val) { + return false; + } + + return (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); +} + + +void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax) { + key[0] = '\0'; + value[0] = '\0'; + + line = dvxSkipWs(line); + + int32_t ki = 0; + + while (*line && *line != '=' && *line != ' ' && *line != '\t' && ki < keyMax - 1) { + key[ki++] = *line++; + } + + key[ki] = '\0'; + + line = dvxSkipWs(line); + + if (*line == '=') { + line++; + } + + line = dvxSkipWs(line); + + int32_t vi = 0; + + while (*line && *line != '\r' && *line != '\n' && vi < valueMax - 1) { + value[vi++] = *line++; + } + + value[vi] = '\0'; + + while (vi > 0 && (value[vi - 1] == ' ' || value[vi - 1] == '\t')) { + value[--vi] = '\0'; + } +} + + +void frmStripQuotes(char *val) { + if (!val || val[0] != '"') { + return; + } + + int32_t len = (int32_t)strlen(val); + + if (len >= 2 && val[len - 1] == '"') { + memmove(val, val + 1, len - 2); + val[len - 2] = '\0'; + } else if (len >= 1) { + // Unterminated leading quote -- strip just the leading " + memmove(val, val + 1, len - 1); + val[len - 1] = '\0'; + } +} + + +// Read a whitespace-delimited token starting at *p. Advances *p past +// the token. If end is non-NULL, reading stops before end. +static bool readToken(const char **p, const char *end, char *buf, int32_t bufMax) { + const char *cur = *p; + int32_t len = 0; + + while (*cur && *cur != ' ' && *cur != '\t' && *cur != '\r' && *cur != '\n' && len < bufMax - 1) { + if (end && cur >= end) { + break; + } + + buf[len++] = *cur++; + } + + buf[len] = '\0'; + *p = cur; + return (len > 0); +} diff --git a/src/apps/kpunch/dvxbasic/formrt/frmParser.h b/src/apps/kpunch/dvxbasic/formrt/frmParser.h new file mode 100644 index 0000000..b7d00b2 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/formrt/frmParser.h @@ -0,0 +1,88 @@ +// The MIT License (MIT) +// +// Copyright (C) 2026 Scott Duensing +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +// frmParser.h -- callback-based .frm text parser +// +// Shared by the form runtime (formrt.c) and the visual designer +// (ideDesigner.c). The parser walks the .frm text, recognises +// "Begin Form", "Begin Menu", "Begin ", "End", and +// "Key = Value" lines, and calls back into the caller for each +// semantic event. The caller owns all data structures -- the +// parser keeps only internal block-nesting state. + +#ifndef DVXBASIC_FRMPARSER_H +#define DVXBASIC_FRMPARSER_H + +#include +#include + +// Callbacks supplied by the consumer. Any field may be NULL. +typedef struct FrmParserCbsT { + void *userData; + + // "Begin Form ". Return false to abort parsing. + bool (*onFormBegin)(void *userData, const char *name); + + // Form-level "Key = Value" property (not inside any control or menu). + void (*onFormProp)(void *userData, const char *key, const char *value); + + // "Begin Menu " at the given nesting level (0 = top-level). + void (*onMenuBegin)(void *userData, const char *name, int32_t level); + + // "End" for a menu item. + void (*onMenuEnd)(void *userData); + + // Menu-item "Key = Value" property. + void (*onMenuProp)(void *userData, const char *key, const char *value); + + // "Begin " for a non-Form, non-Menu control. + void (*onCtrlBegin)(void *userData, const char *typeName, const char *name); + + // "End" for a control block. + void (*onCtrlEnd)(void *userData); + + // Control-level "Key = Value" property. + void (*onCtrlProp)(void *userData, const char *key, const char *value); + + // Outer Form's "End" was reached. trailingSrc points into the + // input buffer just past the End, with trailingLen bytes remaining. + // The designer uses this to capture the BASIC code section. + void (*onFormEnd)(void *userData, const char *trailingSrc, int32_t trailingLen); +} FrmParserCbsT; + +// Parse a .frm text. Returns true on success, false if the VERSION +// line rejects the input or onFormBegin returns false. +bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb); + +// Parse a trimmed "Key = Value" line into key and value buffers. +// value has trailing whitespace stripped but quotes are preserved. +void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax); + +// Strip surrounding double quotes from val, in place. No-op if the +// string is not quoted. +void frmStripQuotes(char *val); + +// Classify a value string as a BASIC boolean. True / -1 -> true; +// anything else -> false. +bool frmParseBool(const char *val); + +#endif // DVXBASIC_FRMPARSER_H diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c index 103f8c0..13787d1 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c @@ -28,6 +28,7 @@ #include "ideDesigner.h" #include "../formrt/formrt.h" +#include "../formrt/frmParser.h" #include "dvxDraw.h" #include "dvxVideo.h" #include "dvxWm.h" @@ -61,16 +62,39 @@ static const char *FORM_DEFAULT_EVENT = "Load"; // ============================================================ // dsgnCreateDesignWidget is declared in ideDesigner.h (non-static) +static void dsgnLoad_onCtrlBegin(void *userData, const char *typeName, const char *name); +static void dsgnLoad_onCtrlEnd(void *userData); +static void dsgnLoad_onCtrlProp(void *userData, const char *key, const char *value); +static bool dsgnLoad_onFormBegin(void *userData, const char *name); +static void dsgnLoad_onFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen); +static void dsgnLoad_onFormProp(void *userData, const char *key, const char *value); +static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level); +static void dsgnLoad_onMenuEnd(void *userData); +static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *value); static const char *getPropValue(const DsgnControlT *ctrl, const char *name); static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y); static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y); static void rebuildWidgets(DsgnStateT *ds); -static const char *resolveTypeName(const char *typeName); static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, int32_t pos, const char *parentName, int32_t indent); static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value); static void syncWidgetGeom(DsgnControlT *ctrl); +// ============================================================ +// .frm parsing context (used by dsgnLoad_* callbacks) +// ============================================================ + +typedef struct { + DsgnFormT *form; + DsgnControlT *current; + int32_t curMenuItemIdx; + char parentStack[8][DSGN_MAX_NAME]; + int32_t nestDepth; + bool containerStack[BAS_MAX_FRM_NESTING]; + int32_t containerDepth; +} DsgnFrmLoadCtxT; + + void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize) { // Look up the name prefix from the widget interface descriptor. // Falls back to the type name itself if no prefix is registered. @@ -156,8 +180,10 @@ WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout) { } -// Create a real DVX widget for design-time display. Mirrors the -// logic in formrt.c createWidget(). +// Create a real DVX widget for design-time display. Uses the shared +// createWidgetByIface switch from formrt; design-time refuses +// WGT_CREATE_PARENT_DATA widgets (Image/ImageButton) since we have no +// pixel data. WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) { const char *wgtName = resolveTypeName(vbTypeName); @@ -171,50 +197,7 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) { return NULL; } - const WgtIfaceT *iface = wgtGetIface(wgtName); - uint8_t sig = iface ? iface->createSig : WGT_CREATE_PARENT; - - typedef WidgetT *(*CreateParentFnT)(WidgetT *); - typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *); - typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t); - typedef WidgetT *(*CreateParentIntIntFnT)(WidgetT *, int32_t, int32_t); - typedef WidgetT *(*CreateParentIntIntIntFnT)(WidgetT *, int32_t, int32_t, int32_t); - typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool); - typedef WidgetT *(*CreateParentBoolFnT)(WidgetT *, bool); - - switch (sig) { - case WGT_CREATE_PARENT_TEXT: { - CreateParentTextFnT fn = *(CreateParentTextFnT *)api; - return fn(parent, ""); - } - case WGT_CREATE_PARENT_INT: { - CreateParentIntFnT fn = *(CreateParentIntFnT *)api; - return fn(parent, iface->createArgs[0]); - } - case WGT_CREATE_PARENT_INT_INT: { - CreateParentIntIntFnT fn = *(CreateParentIntIntFnT *)api; - return fn(parent, iface->createArgs[0], iface->createArgs[1]); - } - case WGT_CREATE_PARENT_INT_INT_INT: { - CreateParentIntIntIntFnT fn = *(CreateParentIntIntIntFnT *)api; - return fn(parent, iface->createArgs[0], iface->createArgs[1], iface->createArgs[2]); - } - case WGT_CREATE_PARENT_INT_BOOL: { - CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api; - return fn(parent, iface->createArgs[0], (bool)iface->createArgs[1]); - } - case WGT_CREATE_PARENT_BOOL: { - CreateParentBoolFnT fn = *(CreateParentBoolFnT *)api; - return fn(parent, (bool)iface->createArgs[0]); - } - case WGT_CREATE_PARENT_DATA: - // Image/ImageButton -- cannot auto-create without pixel data - return NULL; - default: { - CreateParentFnT fn = *(CreateParentFnT *)api; - return fn(parent); - } - } + return createWidgetByIface(wgtGetIface(wgtName), api, parent, false); } @@ -320,23 +303,8 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) { const char *val = getPropValue(ctrl, p->name); - if (!val) { - continue; - } - - if (p->type == WGT_IFACE_ENUM && p->enumNames) { - for (int32_t en = 0; p->enumNames[en]; en++) { - if (strcasecmp(p->enumNames[en], val) == 0) { - ((void (*)(WidgetT *, int32_t))p->setFn)(w, en); - break; - } - } - } else if (p->type == WGT_IFACE_INT) { - ((void (*)(WidgetT *, int32_t))p->setFn)(w, atoi(val)); - } else if (p->type == WGT_IFACE_BOOL) { - ((void (*)(WidgetT *, bool))p->setFn)(w, strcasecmp(val, "True") == 0); - } else if (p->type == WGT_IFACE_STRING) { - ((void (*)(WidgetT *, const char *))p->setFn)(w, val); + if (val) { + wgtApplyPropFromString(w, p, val); } } } @@ -427,259 +395,27 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { snprintf(form->name, DSGN_MAX_NAME, "Form1"); snprintf(form->caption, DSGN_MAX_TEXT, "Form1"); - DsgnControlT *curCtrl = NULL; - DsgnMenuItemT *curMenuItem = NULL; - bool inForm = false; - bool inMenu = false; - int32_t menuNestDepth = 0; - int32_t blockDepth = 0; // Begin/End nesting depth (0 = form level) - bool blockIsContainer[BAS_MAX_FRM_NESTING]; // whether each block is a container + DsgnFrmLoadCtxT ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.form = form; + ctx.curMenuItemIdx = -1; - // Parent name stack for nesting (index 0 = form level) - char parentStack[8][DSGN_MAX_NAME]; - int32_t nestDepth = 0; - parentStack[0][0] = '\0'; + FrmParserCbsT cbs; + memset(&cbs, 0, sizeof(cbs)); + cbs.userData = &ctx; + cbs.onFormBegin = dsgnLoad_onFormBegin; + cbs.onFormProp = dsgnLoad_onFormProp; + cbs.onFormEnd = dsgnLoad_onFormEnd; + cbs.onMenuBegin = dsgnLoad_onMenuBegin; + cbs.onMenuEnd = dsgnLoad_onMenuEnd; + cbs.onMenuProp = dsgnLoad_onMenuProp; + cbs.onCtrlBegin = dsgnLoad_onCtrlBegin; + cbs.onCtrlEnd = dsgnLoad_onCtrlEnd; + cbs.onCtrlProp = dsgnLoad_onCtrlProp; - const char *pos = source; - const char *end = source + sourceLen; - - while (pos < end) { - const char *lineStart = pos; - - while (pos < end && *pos != '\n' && *pos != '\r') { - pos++; - } - - int32_t lineLen = (int32_t)(pos - lineStart); - - if (pos < end && *pos == '\r') { pos++; } - if (pos < end && *pos == '\n') { pos++; } - - char line[BAS_MAX_FRM_LINE_LEN]; - - if (lineLen >= BAS_MAX_FRM_LINE_LEN) { - lineLen = BAS_MAX_FRM_LINE_LEN - 1; - } - - memcpy(line, lineStart, lineLen); - line[lineLen] = '\0'; - - const char *trimmed = dvxSkipWs(line); - - if (*trimmed == '\0' || *trimmed == '\'') { - continue; - } - - if (strncasecmp(trimmed, "VERSION ", 8) == 0) { - // Accept "VERSION DVX x.xx" (native) and "VERSION x.xx" (VB import). - // Reject VB forms with version > 1.xx (VB4+/VB6 use features we - // don't support like OLE controls and binary properties). - const char *ver = trimmed + 8; - - if (strncasecmp(ver, "DVX ", 4) == 0) { - // Native DVX BASIC form -- always accepted - } else { - // VB form -- check version number - double vbVer = atof(ver); - - if (vbVer > 2.0) { - return false; // VB4+ form, not compatible - } - } - - continue; - } - - // Begin TypeName CtrlName - if (strncasecmp(trimmed, "Begin ", 6) == 0) { - const char *rest = trimmed + 6; - char typeName[DSGN_MAX_NAME]; - char ctrlName[DSGN_MAX_NAME]; - int32_t ti = 0; - - while (*rest && *rest != ' ' && *rest != '\t' && ti < DSGN_MAX_NAME - 1) { - typeName[ti++] = *rest++; - } - - typeName[ti] = '\0'; - - rest = dvxSkipWs(rest); - - int32_t ci = 0; - - while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < DSGN_MAX_NAME - 1) { - ctrlName[ci++] = *rest++; - } - - ctrlName[ci] = '\0'; - - if (strcasecmp(typeName, "Form") == 0) { - snprintf(form->name, DSGN_MAX_NAME, "%s", ctrlName); - snprintf(form->caption, DSGN_MAX_TEXT, "%s", ctrlName); - inForm = true; - nestDepth = 0; - curCtrl = NULL; - } else if (strcasecmp(typeName, "Menu") == 0 && inForm) { - DsgnMenuItemT mi; - memset(&mi, 0, sizeof(mi)); - snprintf(mi.name, DSGN_MAX_NAME, "%s", ctrlName); - mi.level = menuNestDepth; - mi.enabled = true; - arrput(form->menuItems, mi); - curMenuItem = &form->menuItems[arrlen(form->menuItems) - 1]; - curCtrl = NULL; // not a control - menuNestDepth++; - inMenu = true; - if (blockDepth < BAS_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = false; } - blockDepth++; - } else if (inForm) { - DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT)); - cp->index = -1; - snprintf(cp->name, DSGN_MAX_NAME, "%s", ctrlName); - snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName); - - // Set parent from current nesting - if (nestDepth > 0) { - snprintf(cp->parentName, DSGN_MAX_NAME, "%s", parentStack[nestDepth - 1]); - } - - cp->width = DEFAULT_CTRL_W; - cp->height = DEFAULT_CTRL_H; - arrput(form->controls, cp); - curCtrl = form->controls[arrlen(form->controls) - 1]; - bool isCtrl = dsgnIsContainer(typeName); - if (blockDepth < BAS_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = isCtrl; } - blockDepth++; - - // If this is a container, push onto parent stack - if (isCtrl && nestDepth < 7) { - snprintf(parentStack[nestDepth], DSGN_MAX_NAME, "%s", ctrlName); - nestDepth++; - } - } - - continue; - } - - if (strcasecmp(trimmed, "End") == 0) { - if (blockDepth > 0) { - blockDepth--; - - if (inMenu) { - menuNestDepth--; - curMenuItem = NULL; - - if (menuNestDepth <= 0) { - menuNestDepth = 0; - inMenu = false; - } - } else { - // If we're closing a container, pop the parent stack - if (blockDepth < BAS_MAX_FRM_NESTING && blockIsContainer[blockDepth] && nestDepth > 0) { - nestDepth--; - } - - curCtrl = NULL; - } - } else { - // blockDepth == 0: this is the form's closing End - inForm = false; - - // Everything after the form's closing End is code - if (pos < end) { - // Skip leading blank lines - const char *codeStart = pos; - - while (codeStart < end && (*codeStart == '\r' || *codeStart == '\n' || *codeStart == ' ' || *codeStart == '\t')) { - codeStart++; - } - - if (codeStart < end) { - int32_t codeLen = (int32_t)(end - codeStart); - form->code = (char *)malloc(codeLen + 1); - - if (form->code) { - memcpy(form->code, codeStart, codeLen); - form->code[codeLen] = '\0'; - } - } - } - - break; // done parsing - } - - continue; - } - - // Property = Value - const char *eq = strchr(trimmed, '='); - - if (eq && inForm) { - char key[DSGN_MAX_NAME]; - const char *kend = eq - 1; - - while (kend > trimmed && (*kend == ' ' || *kend == '\t')) { kend--; } - - int32_t klen = (int32_t)(kend - trimmed + 1); - - if (klen >= DSGN_MAX_NAME) { klen = DSGN_MAX_NAME - 1; } - - memcpy(key, trimmed, klen); - key[klen] = '\0'; - - const char *vstart = dvxSkipWs(eq + 1); - - char val[DSGN_MAX_TEXT]; - int32_t vi = 0; - - if (*vstart == '"') { - vstart++; - - while (*vstart && *vstart != '"' && vi < DSGN_MAX_TEXT - 1) { - val[vi++] = *vstart++; - } - } else { - while (*vstart && *vstart != '\r' && *vstart != '\n' && vi < DSGN_MAX_TEXT - 1) { - val[vi++] = *vstart++; - } - - while (vi > 0 && (val[vi - 1] == ' ' || val[vi - 1] == '\t')) { vi--; } - } - - val[vi] = '\0'; - - if (curMenuItem) { - if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, DSGN_MAX_TEXT, "%s", val); } - else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); } - else if (strcasecmp(key, "RadioCheck") == 0) { curMenuItem->radioCheck = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); } - else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); } - } else if (curCtrl) { - if (strcasecmp(key, "Left") == 0) { curCtrl->left = atoi(val); } - else if (strcasecmp(key, "Top") == 0) { curCtrl->top = atoi(val); } - else if (strcasecmp(key, "MinWidth") == 0 || - strcasecmp(key, "Width") == 0) { curCtrl->width = atoi(val); } - else if (strcasecmp(key, "MinHeight") == 0 || - strcasecmp(key, "Height") == 0) { curCtrl->height = atoi(val); } - else if (strcasecmp(key, "MaxWidth") == 0) { curCtrl->maxWidth = atoi(val); } - else if (strcasecmp(key, "MaxHeight") == 0) { curCtrl->maxHeight = atoi(val); } - else if (strcasecmp(key, "Weight") == 0) { curCtrl->weight = atoi(val); } - else if (strcasecmp(key, "Index") == 0) { curCtrl->index = atoi(val); } - else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(curCtrl->helpTopic, DSGN_MAX_NAME, "%s", val); } - else if (strcasecmp(key, "TabIndex") == 0) { /* ignored -- DVX has no tab order */ } - else { setPropValue(curCtrl, key, val); } - } else { - if (strcasecmp(key, "Caption") == 0) { snprintf(form->caption, DSGN_MAX_TEXT, "%s", val); } - else if (strcasecmp(key, "Layout") == 0) { strncpy(form->layout, val, DSGN_MAX_NAME - 1); form->layout[DSGN_MAX_NAME - 1] = '\0'; } - else if (strcasecmp(key, "AutoSize") == 0) { form->autoSize = (strcasecmp(val, "True") == 0); } - else if (strcasecmp(key, "Resizable") == 0) { form->resizable = (strcasecmp(val, "True") == 0); } - else if (strcasecmp(key, "Centered") == 0) { form->centered = (strcasecmp(val, "True") == 0); } - else if (strcasecmp(key, "Left") == 0) { form->left = atoi(val); } - else if (strcasecmp(key, "Top") == 0) { form->top = atoi(val); } - else if (strcasecmp(key, "Width") == 0) { form->width = atoi(val); form->autoSize = false; } - else if (strcasecmp(key, "Height") == 0) { form->height = atoi(val); form->autoSize = false; } - else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(form->helpTopic, DSGN_MAX_NAME, "%s", val); } - } - } + if (!frmParse(source, sourceLen, &cbs)) { + free(form); + return false; } ds->form = form; @@ -688,6 +424,239 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { } +static void dsgnLoad_onCtrlBegin(void *userData, const char *typeName, const char *name) { + DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; + + if (!ctx->form) { + return; + } + + DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT)); + + if (!cp) { + if (ctx->containerDepth < BAS_MAX_FRM_NESTING) { + ctx->containerStack[ctx->containerDepth++] = false; + } + return; + } + + cp->index = -1; + snprintf(cp->name, DSGN_MAX_NAME, "%s", name); + snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName); + + if (ctx->nestDepth > 0) { + snprintf(cp->parentName, DSGN_MAX_NAME, "%s", ctx->parentStack[ctx->nestDepth - 1]); + } + + cp->width = DEFAULT_CTRL_W; + cp->height = DEFAULT_CTRL_H; + arrput(ctx->form->controls, cp); + ctx->current = ctx->form->controls[arrlen(ctx->form->controls) - 1]; + + bool isCtrl = dsgnIsContainer(typeName); + + if (ctx->containerDepth < BAS_MAX_FRM_NESTING) { + ctx->containerStack[ctx->containerDepth++] = isCtrl; + } + + if (isCtrl && ctx->nestDepth < 7) { + snprintf(ctx->parentStack[ctx->nestDepth], DSGN_MAX_NAME, "%s", name); + ctx->nestDepth++; + } +} + + +static void dsgnLoad_onCtrlEnd(void *userData) { + DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; + + if (ctx->containerDepth > 0) { + ctx->containerDepth--; + + if (ctx->containerStack[ctx->containerDepth] && ctx->nestDepth > 0) { + ctx->nestDepth--; + } + } + + ctx->current = NULL; +} + + +static void dsgnLoad_onCtrlProp(void *userData, const char *key, const char *value) { + DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; + + if (!ctx->current) { + return; + } + + char val[DSGN_MAX_TEXT]; + snprintf(val, sizeof(val), "%s", value); + frmStripQuotes(val); + + DsgnControlT *cc = ctx->current; + + if (strcasecmp(key, "Left") == 0) { + cc->left = atoi(val); + } else if (strcasecmp(key, "Top") == 0) { + cc->top = atoi(val); + } else if (strcasecmp(key, "MinWidth") == 0 || strcasecmp(key, "Width") == 0) { + cc->width = atoi(val); + } else if (strcasecmp(key, "MinHeight") == 0 || strcasecmp(key, "Height") == 0) { + cc->height = atoi(val); + } else if (strcasecmp(key, "MaxWidth") == 0) { + cc->maxWidth = atoi(val); + } else if (strcasecmp(key, "MaxHeight") == 0) { + cc->maxHeight = atoi(val); + } else if (strcasecmp(key, "Weight") == 0) { + cc->weight = atoi(val); + } else if (strcasecmp(key, "Index") == 0) { + cc->index = atoi(val); + } else if (strcasecmp(key, "HelpTopic") == 0) { + snprintf(cc->helpTopic, DSGN_MAX_NAME, "%s", val); + } else if (strcasecmp(key, "TabIndex") == 0) { + // ignored -- DVX has no tab order + } else { + setPropValue(cc, key, val); + } +} + + +static bool dsgnLoad_onFormBegin(void *userData, const char *name) { + DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; + + if (!ctx->form) { + return false; + } + + snprintf(ctx->form->name, DSGN_MAX_NAME, "%s", name); + snprintf(ctx->form->caption, DSGN_MAX_TEXT, "%s", name); + ctx->current = NULL; + ctx->nestDepth = 0; + return true; +} + + +static void dsgnLoad_onFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen) { + DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; + + if (!ctx->form || trailingLen <= 0) { + return; + } + + // Skip leading whitespace/blank lines + const char *codeStart = trailingSrc; + const char *codeEnd = trailingSrc + trailingLen; + + while (codeStart < codeEnd && (*codeStart == '\r' || *codeStart == '\n' || *codeStart == ' ' || *codeStart == '\t')) { + codeStart++; + } + + if (codeStart >= codeEnd) { + return; + } + + int32_t codeLen = (int32_t)(codeEnd - codeStart); + ctx->form->code = (char *)malloc(codeLen + 1); + + if (ctx->form->code) { + memcpy(ctx->form->code, codeStart, codeLen); + ctx->form->code[codeLen] = '\0'; + } +} + + +static void dsgnLoad_onFormProp(void *userData, const char *key, const char *value) { + DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; + + if (!ctx->form) { + return; + } + + char val[DSGN_MAX_TEXT]; + snprintf(val, sizeof(val), "%s", value); + frmStripQuotes(val); + + DsgnFormT *ff = ctx->form; + + if (strcasecmp(key, "Caption") == 0) { + snprintf(ff->caption, DSGN_MAX_TEXT, "%s", val); + } else if (strcasecmp(key, "Layout") == 0) { + strncpy(ff->layout, val, DSGN_MAX_NAME - 1); + ff->layout[DSGN_MAX_NAME - 1] = '\0'; + } else if (strcasecmp(key, "AutoSize") == 0) { + ff->autoSize = frmParseBool(val); + } else if (strcasecmp(key, "Resizable") == 0) { + ff->resizable = frmParseBool(val); + } else if (strcasecmp(key, "Centered") == 0) { + ff->centered = frmParseBool(val); + } else if (strcasecmp(key, "Left") == 0) { + ff->left = atoi(val); + } else if (strcasecmp(key, "Top") == 0) { + ff->top = atoi(val); + } else if (strcasecmp(key, "Width") == 0) { + ff->width = atoi(val); + ff->autoSize = false; + } else if (strcasecmp(key, "Height") == 0) { + ff->height = atoi(val); + ff->autoSize = false; + } else if (strcasecmp(key, "HelpTopic") == 0) { + snprintf(ff->helpTopic, DSGN_MAX_NAME, "%s", val); + } +} + + +static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level) { + DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; + + if (!ctx->form) { + return; + } + + DsgnMenuItemT mi; + memset(&mi, 0, sizeof(mi)); + snprintf(mi.name, DSGN_MAX_NAME, "%s", name); + mi.level = level; + mi.enabled = true; + arrput(ctx->form->menuItems, mi); + ctx->curMenuItemIdx = (int32_t)arrlen(ctx->form->menuItems) - 1; + ctx->current = NULL; +} + + +static void dsgnLoad_onMenuEnd(void *userData) { + DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; + + ctx->curMenuItemIdx = -1; +} + + +static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *value) { + DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData; + + if (!ctx->form || + ctx->curMenuItemIdx < 0 || + ctx->curMenuItemIdx >= (int32_t)arrlen(ctx->form->menuItems)) { + return; + } + + // Resolve pointer fresh each write -- arrput on nested menus may + // have reallocated the array. + DsgnMenuItemT *mip = &ctx->form->menuItems[ctx->curMenuItemIdx]; + char val[DSGN_MAX_TEXT]; + snprintf(val, sizeof(val), "%s", value); + frmStripQuotes(val); + + if (strcasecmp(key, "Caption") == 0) { + snprintf(mip->caption, DSGN_MAX_TEXT, "%s", val); + } else if (strcasecmp(key, "Checked") == 0) { + mip->checked = frmParseBool(val); + } else if (strcasecmp(key, "RadioCheck") == 0) { + mip->radioCheck = frmParseBool(val); + } else if (strcasecmp(key, "Enabled") == 0) { + mip->enabled = frmParseBool(val); + } +} + + void dsgnNewForm(DsgnStateT *ds, const char *name) { dsgnFree(ds); @@ -1218,21 +1187,6 @@ static void rebuildWidgets(DsgnStateT *ds) { } -static const char *resolveTypeName(const char *typeName) { - const char *wgtName = wgtFindByBasName(typeName); - - if (wgtName) { - return wgtName; - } - - if (wgtGetApi(typeName)) { - return typeName; - } - - return NULL; -} - - // Write controls at a given nesting level with the specified parent name. static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, int32_t pos, const char *parentName, int32_t indent) { int32_t count = (int32_t)arrlen(form->controls); @@ -1318,26 +1272,17 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i continue; } - if (p->type == WGT_IFACE_ENUM && p->enumNames) { - int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); - const char *name = NULL; + // Skip STRING props here: saveControls only emits + // the iface-known scalar types. String values come + // through ctrl->props[] (custom props). + if (p->type == WGT_IFACE_STRING) { + continue; + } - for (int32_t en = 0; p->enumNames[en]; en++) { - if (en == v) { - name = p->enumNames[en]; - break; - } - } + char valBuf[DSGN_MAX_TEXT]; - if (name) { - pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, name); - } - } else if (p->type == WGT_IFACE_INT) { - int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); - pos += snprintf(buf + pos, bufSize - pos, "%s %s = %d\n", pad, p->name, (int)v); - } else if (p->type == WGT_IFACE_BOOL) { - bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget); - pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, v ? "True" : "False"); + if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) { + pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, valBuf); } } } diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c index 60a5205..2cf61a4 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideMain.c +++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c @@ -69,6 +69,7 @@ #include "../compiler/strip.h" #include "../runtime/serialize.h" #include "../formrt/formrt.h" +#include "../formrt/frmParser.h" #include "dvxRes.h" #include "../../../../libs/kpunch/sql/dvxSql.h" #include "../runtime/vm.h" @@ -1425,6 +1426,254 @@ static void compileAndRun(void) { } +// ============================================================ +// Compile-time CtrlName.Member validator +// ============================================================ +// +// The IDE (unlike bascomp) has widget DXEs loaded, so wgtGetIface +// returns live interface metadata. Combined with a static scan of +// the project's .frm files (control name -> widget type), we can +// reject typos like GfxCanvas.Boggle or LblStatus.Caphtion at +// compile time instead of letting them surface as runtime errors +// at event-click time. Dynamically-created controls (via +// CreateControl at runtime) aren't in the map; lookupCtrlType +// returns NULL for them and validation is skipped. + +typedef struct { + char name[BAS_MAX_CTRL_NAME]; + char wgtType[BAS_MAX_CTRL_NAME]; // "Form" for the form itself, else iface basName +} IdeCtrlMapEntryT; + + +typedef struct { + IdeCtrlMapEntryT *entries; // stb_ds dynamic array +} IdeValidatorCtxT; + + +static bool ideValidator_onFormBegin(void *ud, const char *name) { + IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud; + IdeCtrlMapEntryT e; + memset(&e, 0, sizeof(e)); + snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name); + snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", "Form"); + arrput(v->entries, e); + return true; +} + + +static void ideValidator_onCtrlBegin(void *ud, const char *typeName, const char *name) { + IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud; + IdeCtrlMapEntryT e; + memset(&e, 0, sizeof(e)); + snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name); + snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", typeName ? typeName : ""); + arrput(v->entries, e); +} + + +static void ideValidator_onMenuBegin(void *ud, const char *name, int32_t level) { + (void)level; + IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud; + IdeCtrlMapEntryT e; + memset(&e, 0, sizeof(e)); + snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name); + snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", "Menu"); + arrput(v->entries, e); +} + + +static const char *ideValidator_lookupCtrlType(void *ctx, const char *ctrlName) { + IdeValidatorCtxT *v = (IdeValidatorCtxT *)ctx; + + if (!v || !ctrlName) { + return NULL; + } + + for (int32_t i = 0; i < (int32_t)arrlen(v->entries); i++) { + if (strcasecmp(v->entries[i].name, ctrlName) == 0) { + return v->entries[i].wgtType; + } + } + + return NULL; +} + + +// Methods that exist on every widget via callCommonMethod() in formrt.c. +static bool ideValidator_isCommonMethod(const char *methodName) { + return strcasecmp(methodName, "SetFocus") == 0 || + strcasecmp(methodName, "Refresh") == 0 || + strcasecmp(methodName, "SetReadOnly") == 0 || + strcasecmp(methodName, "SetEnabled") == 0 || + strcasecmp(methodName, "SetVisible") == 0; +} + + +// Properties that setProp/getProp accept on every non-form, non-menu +// widget (common props + widget text/data/help aliases). +static bool ideValidator_isCommonProp(const char *propName) { + return strcasecmp(propName, "Name") == 0 || + strcasecmp(propName, "Left") == 0 || + strcasecmp(propName, "Top") == 0 || + strcasecmp(propName, "Width") == 0 || + strcasecmp(propName, "Height") == 0 || + strcasecmp(propName, "MinWidth") == 0 || + strcasecmp(propName, "MinHeight") == 0 || + strcasecmp(propName, "MaxWidth") == 0 || + strcasecmp(propName, "MaxHeight") == 0 || + strcasecmp(propName, "Weight") == 0 || + strcasecmp(propName, "Visible") == 0 || + strcasecmp(propName, "Enabled") == 0 || + strcasecmp(propName, "Caption") == 0 || + strcasecmp(propName, "Text") == 0 || + strcasecmp(propName, "HelpTopic") == 0 || + strcasecmp(propName, "DataSource") == 0 || + strcasecmp(propName, "DataField") == 0 || + strcasecmp(propName, "ListCount") == 0; +} + + +static bool ideValidator_isMethodValid(void *ctx, const char *wgtType, const char *methodName) { + (void)ctx; + + if (!wgtType || !methodName) { + return true; // be permissive on malformed input + } + + if (ideValidator_isCommonMethod(methodName)) { + return true; + } + + // Form-level methods + if (strcasecmp(wgtType, "Form") == 0) { + return strcasecmp(methodName, "Show") == 0 || + strcasecmp(methodName, "Hide") == 0; + } + + // Menu items have no methods beyond common + if (strcasecmp(wgtType, "Menu") == 0) { + return false; + } + + // Widget-specific methods from the live iface + const char *wgtName = wgtFindByBasName(wgtType); + + if (!wgtName) { + return true; // unknown type -- skip validation + } + + const WgtIfaceT *iface = wgtGetIface(wgtName); + + if (!iface || !iface->methods) { + return true; + } + + for (int32_t i = 0; i < iface->methodCount; i++) { + if (strcasecmp(iface->methods[i].name, methodName) == 0) { + return true; + } + } + + return false; +} + + +static bool ideValidator_isPropValid(void *ctx, const char *wgtType, const char *propName) { + (void)ctx; + + if (!wgtType || !propName) { + return true; + } + + // Form-level properties + if (strcasecmp(wgtType, "Form") == 0) { + return strcasecmp(propName, "Name") == 0 || + strcasecmp(propName, "Caption") == 0 || + strcasecmp(propName, "Width") == 0 || + strcasecmp(propName, "Height") == 0 || + strcasecmp(propName, "Left") == 0 || + strcasecmp(propName, "Top") == 0 || + strcasecmp(propName, "Visible") == 0 || + strcasecmp(propName, "Resizable") == 0 || + strcasecmp(propName, "AutoSize") == 0 || + strcasecmp(propName, "Centered") == 0 || + strcasecmp(propName, "Layout") == 0; + } + + // Menu items + if (strcasecmp(wgtType, "Menu") == 0) { + return strcasecmp(propName, "Name") == 0 || + strcasecmp(propName, "Checked") == 0 || + strcasecmp(propName, "Enabled") == 0 || + strcasecmp(propName, "Caption") == 0; + } + + if (ideValidator_isCommonProp(propName)) { + return true; + } + + // Widget-specific props from the live iface + const char *wgtName = wgtFindByBasName(wgtType); + + if (!wgtName) { + return true; + } + + const WgtIfaceT *iface = wgtGetIface(wgtName); + + if (!iface || !iface->props) { + return true; + } + + for (int32_t i = 0; i < iface->propCount; i++) { + if (strcasecmp(iface->props[i].name, propName) == 0) { + return true; + } + } + + return false; +} + + +// Walk every .frm in the project and populate a (name -> wgtType) map +// the parser can consult. Caller must arrfree(ctx->entries) when done. +static void ideBuildCtrlMap(IdeValidatorCtxT *ctx) { + ctx->entries = NULL; + + FrmParserCbsT cbs; + memset(&cbs, 0, sizeof(cbs)); + cbs.userData = ctx; + cbs.onFormBegin = ideValidator_onFormBegin; + cbs.onCtrlBegin = ideValidator_onCtrlBegin; + cbs.onMenuBegin = ideValidator_onMenuBegin; + + for (int32_t i = 0; i < sProject.fileCount; i++) { + if (!sProject.files[i].isForm) { + continue; + } + + // Use the buffered source if the file is open in an editor, + // otherwise load from disk. Mirrors how the designer loads. + char *diskBuf = NULL; + const char *src = sProject.files[i].buffer; + int32_t len = src ? (int32_t)strlen(src) : 0; + + if (!src) { + int32_t dlen = 0; + diskBuf = platformReadFile(sProject.files[i].path, &dlen); + src = diskBuf; + len = dlen; + } + + if (src && len > 0) { + frmParse(src, len, &cbs); + } + + free(diskBuf); + } +} + + static bool compileProject(void) { // Save all dirty files before compiling if Save on Run is enabled if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) { @@ -1647,7 +1896,22 @@ static bool compileProject(void) { } basParserInit(parser, src, srcLen); - parser->optionExplicit = prefsGetBool(sPrefs, "editor", "optionExplicit", false); + parser->optionExplicit = sProject.optionExplicit; + + // Build a name -> widget-type map from the project's .frm files + // and attach a validator so the parser can reject CtrlName.Member + // typos at compile time. The validator falls back silently for + // dynamically-created controls (not in the map). + IdeValidatorCtxT validatorCtx; + memset(&validatorCtx, 0, sizeof(validatorCtx)); + ideBuildCtrlMap(&validatorCtx); + + BasCtrlValidatorT validator; + validator.lookupCtrlType = ideValidator_lookupCtrlType; + validator.isMethodValid = ideValidator_isMethodValid; + validator.isPropValid = ideValidator_isPropValid; + validator.ctx = &validatorCtx; + basParserSetValidator(parser, &validator); if (!basParse(parser)) { // Translate global error line to local file/line for display @@ -1715,10 +1979,12 @@ static bool compileProject(void) { basParserFree(parser); free(parser); free(concatBuf); + arrfree(validatorCtx.entries); return false; } free(concatBuf); + arrfree(validatorCtx.entries); BasModuleT *mod = basParserBuildModule(parser); basParserFree(parser); @@ -2062,19 +2328,15 @@ static void dsgnCopySelected(void) { continue; } - if (p->type == WGT_IFACE_ENUM && p->enumNames) { - int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); - const char *name = (v >= 0 && p->enumNames[v]) ? p->enumNames[v] : NULL; + // Skip STRING props -- custom props handle those. + if (p->type == WGT_IFACE_STRING) { + continue; + } - if (name) { - pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, name); - } - } else if (p->type == WGT_IFACE_INT) { - int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); - pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %d\n", p->name, (int)v); - } else if (p->type == WGT_IFACE_BOOL) { - bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget); - pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, v ? "True" : "False"); + char valBuf[DSGN_MAX_TEXT]; + + if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) { + pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, valBuf); } } } @@ -2305,23 +2567,8 @@ static void dsgnPasteControl(void) { } } - if (!val) { - continue; - } - - if (p->type == WGT_IFACE_ENUM && p->enumNames) { - for (int32_t en = 0; p->enumNames[en]; en++) { - if (strcasecmp(p->enumNames[en], val) == 0) { - ((void (*)(WidgetT *, int32_t))p->setFn)(ctrl.widget, en); - break; - } - } - } else if (p->type == WGT_IFACE_INT) { - ((void (*)(WidgetT *, int32_t))p->setFn)(ctrl.widget, atoi(val)); - } else if (p->type == WGT_IFACE_BOOL) { - ((void (*)(WidgetT *, bool))p->setFn)(ctrl.widget, strcasecmp(val, "True") == 0); - } else if (p->type == WGT_IFACE_STRING) { - ((void (*)(WidgetT *, const char *))p->setFn)(ctrl.widget, val); + if (val) { + wgtApplyPropFromString(ctrl.widget, p, val); } } } @@ -8077,7 +8324,7 @@ static void showPreferencesDialog(void) { sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming"); wgtCheckboxSetChecked(sPrefsDlg.renameSkipComments, prefsGetBool(sPrefs, "editor", "renameSkipComments", true)); - sPrefsDlg.optionExplicit = wgtCheckbox(edFrame, "Require variable declaration (OPTION EXPLICIT)"); + sPrefsDlg.optionExplicit = wgtCheckbox(edFrame, "OPTION EXPLICIT default for new projects"); wgtCheckboxSetChecked(sPrefsDlg.optionExplicit, prefsGetBool(sPrefs, "editor", "optionExplicit", false)); WidgetT *tabRow = wgtHBox(edFrame); diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.c b/src/apps/kpunch/dvxbasic/ide/ideProject.c index b7e4853..b8379ec 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProject.c +++ b/src/apps/kpunch/dvxbasic/ide/ideProject.c @@ -51,6 +51,7 @@ #include "dvxWm.h" #include "box/box.h" #include "button/button.h" +#include "checkbox/checkbox.h" #include "dropdown/dropdown.h" #include "image/image.h" #include "label/label.h" @@ -98,6 +99,7 @@ static struct { WidgetT *copyright; WidgetT *description; WidgetT *startupForm; + WidgetT *optionExplicit; const char **formNames; // stb_ds array of form name strings for startup dropdown WidgetT *helpFileInput; WidgetT *iconPreview; @@ -522,6 +524,8 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) { val = prefsGetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, NULL); if (val) { snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", val); } + prj->optionExplicit = prefsGetBool(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false); + prefsClose(h); prj->dirty = false; return true; @@ -608,6 +612,7 @@ void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandle snprintf(prj->version, sizeof(prj->version), "%s", prefsGetString(prefs, "defaults", "version", "1.0")); snprintf(prj->copyright, sizeof(prj->copyright), "%s", prefsGetString(prefs, "defaults", "copyright", "")); snprintf(prj->description, sizeof(prj->description), "%s", prefsGetString(prefs, "defaults", "description", "")); + prj->optionExplicit = prefsGetBool(prefs, "editor", "optionExplicit", false); } } @@ -617,7 +622,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) return false; } - WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 380, false); + WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 410, false); if (!win) { return false; @@ -739,6 +744,10 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) hlpBrowse->onClick = ppdOnBrowseHelp; } + // Compiler options: OPTION EXPLICIT enforces DIM-before-use for this project. + sPpd.optionExplicit = wgtCheckbox(root, "Require variable declaration (OPTION EXPLICIT)"); + wgtCheckboxSetChecked(sPpd.optionExplicit, prj->optionExplicit); + // Description: label above, textarea below (matches Preferences layout) wgtLabel(root, "Description:"); sPpd.description = wgtTextArea(root, PRJ_MAX_DESC); @@ -804,6 +813,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) } } + prj->optionExplicit = wgtCheckboxIsChecked(sPpd.optionExplicit); prj->dirty = true; } @@ -948,6 +958,7 @@ bool prjSave(const PrjStateT *prj) { // [Settings] section prefsSetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, prj->startupForm); + prefsSetBool(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, prj->optionExplicit); bool ok = prefsSaveAs(h, prj->projectPath); prefsClose(h); diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.h b/src/apps/kpunch/dvxbasic/ide/ideProject.h index 91a2405..5621150 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProject.h +++ b/src/apps/kpunch/dvxbasic/ide/ideProject.h @@ -79,6 +79,7 @@ typedef struct { char description[PRJ_MAX_DESC]; char iconPath[DVX_MAX_PATH]; // relative path to icon BMP char helpFile[DVX_MAX_PATH]; // relative path to .hlp file + bool optionExplicit; // require DIM before use PrjFileT *files; // stb_ds dynamic array int32_t fileCount; PrjSourceMapT *sourceMap; // stb_ds dynamic array diff --git a/src/apps/kpunch/dvxbasic/ide/ideProperties.c b/src/apps/kpunch/dvxbasic/ide/ideProperties.c index 93ef7ff..85190be 100644 --- a/src/apps/kpunch/dvxbasic/ide/ideProperties.c +++ b/src/apps/kpunch/dvxbasic/ide/ideProperties.c @@ -28,6 +28,7 @@ // property value to edit it via an InputBox dialog. #include "ideProperties.h" +#include "../formrt/frmParser.h" #include "dvxDlg.h" #include "dvxWm.h" #include "box/box.h" @@ -616,7 +617,7 @@ static void onPropDblClick(WidgetT *w) { if (propType == PROP_TYPE_BOOL) { // Toggle boolean on double-click -- no input box - bool cur = (strcasecmp(curValue, "True") == 0); + bool cur = frmParseBool(curValue); snprintf(newValue, sizeof(newValue), "%s", cur ? "False" : "True"); } else if (propType == PROP_TYPE_ENUM) { // Enum: cycle to next value on double-click @@ -923,7 +924,7 @@ static void onPropDblClick(WidgetT *w) { ctrl->widget->weight = ctrl->weight; } } else if (strcasecmp(propName, "Visible") == 0) { - bool val = (strcasecmp(newValue, "True") == 0); + bool val = frmParseBool(newValue); if (ctrl->widget) { wgtSetVisible(ctrl->widget, val); @@ -934,7 +935,7 @@ static void onPropDblClick(WidgetT *w) { cascadeToChildren(sDs, ctrl->name, val, en); } } else if (strcasecmp(propName, "Enabled") == 0) { - bool val = (strcasecmp(newValue, "True") == 0); + bool val = frmParseBool(newValue); if (ctrl->widget) { wgtSetEnabled(ctrl->widget, val); @@ -965,7 +966,9 @@ static void onPropDblClick(WidgetT *w) { } if (p->type == WGT_IFACE_STRING) { - // Store in props for persistence, set from there + // Strings must outlive this function, so the + // ctrl->props[] copy is what we pass to setFn + // (not the newValue buffer). bool found = false; for (int32_t j = 0; j < ctrl->propCount; j++) { @@ -983,21 +986,8 @@ static void onPropDblClick(WidgetT *w) { ((void (*)(WidgetT *, const char *))p->setFn)(ctrl->widget, ctrl->props[ctrl->propCount].value); ctrl->propCount++; } - } else if (p->type == WGT_IFACE_ENUM && p->enumNames) { - int32_t enumVal = 0; - - for (int32_t en = 0; p->enumNames[en]; en++) { - if (strcasecmp(p->enumNames[en], newValue) == 0) { - enumVal = en; - break; - } - } - - ((void (*)(WidgetT *, int32_t))p->setFn)(ctrl->widget, enumVal); - } else if (p->type == WGT_IFACE_INT) { - ((void (*)(WidgetT *, int32_t))p->setFn)(ctrl->widget, atoi(newValue)); - } else if (p->type == WGT_IFACE_BOOL) { - ((void (*)(WidgetT *, bool))p->setFn)(ctrl->widget, strcasecmp(newValue, "True") == 0); + } else { + wgtApplyPropFromString(ctrl->widget, p, newValue); } ifaceHandled = true; @@ -1068,7 +1058,7 @@ static void onPropDblClick(WidgetT *w) { dvxSetTitle(sPrpCtx, sDs->formWin, winTitle); } } else if (strcasecmp(propName, "AutoSize") == 0) { - sDs->form->autoSize = (strcasecmp(newValue, "True") == 0); + sDs->form->autoSize = frmParseBool(newValue); if (sDs->form->autoSize && sDs->formWin) { dvxFitWindow(sPrpCtx, sDs->formWin); @@ -1076,14 +1066,14 @@ static void onPropDblClick(WidgetT *w) { sDs->form->height = sDs->formWin->h; } } else if (strcasecmp(propName, "Resizable") == 0) { - sDs->form->resizable = (strcasecmp(newValue, "True") == 0); + sDs->form->resizable = frmParseBool(newValue); if (sDs->formWin) { sDs->formWin->resizable = sDs->form->resizable; dvxInvalidateWindow(sPrpCtx, sDs->formWin); } } else if (strcasecmp(propName, "Centered") == 0) { - sDs->form->centered = (strcasecmp(newValue, "True") == 0); + sDs->form->centered = frmParseBool(newValue); } else if (strcasecmp(propName, "Left") == 0) { sDs->form->left = atoi(newValue); } else if (strcasecmp(propName, "Top") == 0) { @@ -1473,29 +1463,14 @@ void prpRefresh(DsgnStateT *ds) { continue; } - // Read the current value from the widget - if (p->type == WGT_IFACE_STRING && p->getFn) { - const char *s = ((const char *(*)(WidgetT *))p->getFn)(ctrl->widget); - addPropRow(p->name, s ? s : ""); - } else if (p->type == WGT_IFACE_ENUM && p->getFn && p->enumNames) { - int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); - const char *name = NULL; + // Read the current value from the widget. Enum values + // that don't map to a name are shown as "?". + char valBuf[DSGN_MAX_TEXT]; - for (int32_t k = 0; p->enumNames[k]; k++) { - if (k == v) { - name = p->enumNames[k]; - break; - } - } - - addPropRow(p->name, name ? name : "?"); - } else if (p->type == WGT_IFACE_INT && p->getFn) { - int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); - snprintf(buf, sizeof(buf), "%d", (int)v); - addPropRow(p->name, buf); - } else if (p->type == WGT_IFACE_BOOL && p->getFn) { - bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget); - addPropRow(p->name, v ? "True" : "False"); + if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) { + addPropRow(p->name, valBuf); + } else if (p->type == WGT_IFACE_ENUM) { + addPropRow(p->name, "?"); } else { addPropRow(p->name, ""); } diff --git a/src/apps/kpunch/dvxbasic/runtime/serialize.c b/src/apps/kpunch/dvxbasic/runtime/serialize.c index f1d146a..25f1cac 100644 --- a/src/apps/kpunch/dvxbasic/runtime/serialize.c +++ b/src/apps/kpunch/dvxbasic/runtime/serialize.c @@ -274,6 +274,10 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) { char *name = rStr(&r); snprintf(p->name, BAS_MAX_PROC_NAME, "%s", name); free(name); + + char *formName = rStr(&r); + snprintf(p->formName, BAS_MAX_PROC_NAME, "%s", formName); + free(formName); } } @@ -294,6 +298,19 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) { } } + // Global-slot runtime type init table (survives strip). STRING + // globals need this so first-use string ops work correctly. + mod->globalInitCount = rI32(&r); + + if (mod->globalInitCount > 0) { + mod->globalInits = (BasGlobalInitT *)calloc(mod->globalInitCount, sizeof(BasGlobalInitT)); + + for (int32_t i = 0; i < mod->globalInitCount; i++) { + mod->globalInits[i].index = rI32(&r); + mod->globalInits[i].dataType = rU8(&r); + } + } + return mod; } @@ -323,6 +340,7 @@ void basModuleFree(BasModuleT *mod) { free(mod->procs); free(mod->formVarInfo); + free(mod->globalInits); free(mod->debugVars); if (mod->debugUdtDefs) { @@ -411,6 +429,7 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) { bufWriteU8(&b, p->returnType); bufWriteU8(&b, p->isFunction ? 1 : 0); bufWriteStr(&b, p->name); + bufWriteStr(&b, p->formName); } // Form variable info (runtime-essential for per-form variable allocation) @@ -424,6 +443,14 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) { bufWriteI32(&b, fv->initCodeLen); } + // Global init table (runtime-essential for STRING default slot type) + bufWriteI32(&b, mod->globalInitCount); + + for (int32_t i = 0; i < mod->globalInitCount; i++) { + bufWriteI32(&b, mod->globalInits[i].index); + bufWriteU8(&b, mod->globalInits[i].dataType); + } + *outLen = b.len; return b.buf; } diff --git a/src/apps/kpunch/dvxbasic/runtime/values.c b/src/apps/kpunch/dvxbasic/runtime/values.c index d4e8599..694da68 100644 --- a/src/apps/kpunch/dvxbasic/runtime/values.c +++ b/src/apps/kpunch/dvxbasic/runtime/values.c @@ -68,11 +68,19 @@ void basUdtFree(BasUdtT *udt); BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount); BasUdtT *basUdtRef(BasUdtT *udt); void basUdtUnref(BasUdtT *udt); +BasValueT basValBool(bool v); int32_t basValCompare(BasValueT a, BasValueT b); int32_t basValCompareCI(BasValueT a, BasValueT b); +BasValueT basValCopy(BasValueT v); +BasValueT basValDouble(double v); BasStringT *basValFormatString(BasValueT v); +BasValueT basValInteger(int16_t v); bool basValIsTruthy(BasValueT v); +BasValueT basValLong(int32_t v); +BasValueT basValObject(void *obj); uint8_t basValPromoteType(uint8_t a, uint8_t b); +void basValRelease(BasValueT *v); +BasValueT basValSingle(float v); BasValueT basValString(BasStringT *s); BasValueT basValStringFromC(const char *text); BasValueT basValToBool(BasValueT v); @@ -399,8 +407,16 @@ void basUdtUnref(BasUdtT *udt) { // ============================================================ -// Value constructors (trivial ones moved to values.h as static inline) +// Value constructors / refcount helpers // ============================================================ +BasValueT basValBool(bool v) { + BasValueT val; + val.type = BAS_TYPE_BOOLEAN; + val.boolVal = v ? -1 : 0; + return val; +} + + int32_t basValCompare(BasValueT a, BasValueT b) { // String comparison if (a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) { @@ -445,6 +461,27 @@ int32_t basValCompareCI(BasValueT a, BasValueT b) { } +BasValueT basValCopy(BasValueT v) { + if (v.type == BAS_TYPE_STRING && v.strVal) { + basStringRef(v.strVal); + } else if (v.type == BAS_TYPE_ARRAY && v.arrVal) { + basArrayRef(v.arrVal); + } else if (v.type == BAS_TYPE_UDT && v.udtVal) { + basUdtRef(v.udtVal); + } + + return v; +} + + +BasValueT basValDouble(double v) { + BasValueT val; + val.type = BAS_TYPE_DOUBLE; + val.dblVal = v; + return val; +} + + BasStringT *basValFormatString(BasValueT v) { char buf[64]; @@ -478,6 +515,14 @@ BasStringT *basValFormatString(BasValueT v) { } +BasValueT basValInteger(int16_t v) { + BasValueT val; + val.type = BAS_TYPE_INTEGER; + val.intVal = v; + return val; +} + + bool basValIsTruthy(BasValueT v) { switch (v.type) { case BAS_TYPE_INTEGER: @@ -504,6 +549,22 @@ bool basValIsTruthy(BasValueT v) { } +BasValueT basValLong(int32_t v) { + BasValueT val; + val.type = BAS_TYPE_LONG; + val.longVal = v; + return val; +} + + +BasValueT basValObject(void *obj) { + BasValueT val; + val.type = BAS_TYPE_OBJECT; + val.objVal = obj; + return val; +} + + uint8_t basValPromoteType(uint8_t a, uint8_t b) { // String stays string (concat, not arithmetic) if (a == BAS_TYPE_STRING || b == BAS_TYPE_STRING) { @@ -529,6 +590,28 @@ uint8_t basValPromoteType(uint8_t a, uint8_t b) { } +void basValRelease(BasValueT *v) { + if (v->type == BAS_TYPE_STRING) { + basStringUnref(v->strVal); + v->strVal = NULL; + } else if (v->type == BAS_TYPE_ARRAY) { + basArrayUnref(v->arrVal); + v->arrVal = NULL; + } else if (v->type == BAS_TYPE_UDT) { + basUdtUnref(v->udtVal); + v->udtVal = NULL; + } +} + + +BasValueT basValSingle(float v) { + BasValueT val; + val.type = BAS_TYPE_SINGLE; + val.sngVal = v; + return val; +} + + BasValueT basValString(BasStringT *s) { BasValueT val; val.type = BAS_TYPE_STRING; diff --git a/src/apps/kpunch/dvxbasic/runtime/values.h b/src/apps/kpunch/dvxbasic/runtime/values.h index f300faa..6214ee5 100644 --- a/src/apps/kpunch/dvxbasic/runtime/values.h +++ b/src/apps/kpunch/dvxbasic/runtime/values.h @@ -31,8 +31,6 @@ #ifndef DVXBASIC_VALUES_H #define DVXBASIC_VALUES_H -#include "../compiler/opcodes.h" // BAS_TYPE_* - #include #include #include @@ -159,89 +157,22 @@ struct BasValueTag { }; }; -// Create values -- trivial constructors inlined so they're fast in vm.c's -// hot path (PUSH_INT16, PUSH_TRUE, etc.). -static inline BasValueT basValInteger(int16_t v) { - BasValueT val; - val.type = BAS_TYPE_INTEGER; - val.intVal = v; - return val; -} - - -static inline BasValueT basValLong(int32_t v) { - BasValueT val; - val.type = BAS_TYPE_LONG; - val.longVal = v; - return val; -} - - -static inline BasValueT basValSingle(float v) { - BasValueT val; - val.type = BAS_TYPE_SINGLE; - val.sngVal = v; - return val; -} - - -static inline BasValueT basValDouble(double v) { - BasValueT val; - val.type = BAS_TYPE_DOUBLE; - val.dblVal = v; - return val; -} - - -static inline BasValueT basValBool(bool v) { - BasValueT val; - val.type = BAS_TYPE_BOOLEAN; - val.boolVal = v ? -1 : 0; - return val; -} - - -static inline BasValueT basValObject(void *obj) { - BasValueT val; - val.type = BAS_TYPE_OBJECT; - val.objVal = obj; - return val; -} - - +// Create values. Out-of-line so they can be resolved via the DXE +// dynamic-symbol table when basrt.lib is loaded. +BasValueT basValInteger(int16_t v); +BasValueT basValLong(int32_t v); +BasValueT basValSingle(float v); +BasValueT basValDouble(double v); +BasValueT basValBool(bool v); +BasValueT basValObject(void *obj); BasValueT basValString(BasStringT *s); BasValueT basValStringFromC(const char *text); // Copy a value (increments string/array/udt refcount if applicable). -// Inlined so the hot-path case (integer/float/bool) is a single struct -// return -- no function call, no branch beyond the type test. -static inline BasValueT basValCopy(BasValueT v) { - if (v.type == BAS_TYPE_STRING && v.strVal) { - basStringRef(v.strVal); - } else if (v.type == BAS_TYPE_ARRAY && v.arrVal) { - basArrayRef(v.arrVal); - } else if (v.type == BAS_TYPE_UDT && v.udtVal) { - basUdtRef(v.udtVal); - } +BasValueT basValCopy(BasValueT v); - return v; -} - - -// Release a value (decrements refcount if applicable). Integer/float/bool -// types are a no-op -- the common case is an immediately-returning branch. -static inline void basValRelease(BasValueT *v) { - if (v->type == BAS_TYPE_STRING) { - basStringUnref(v->strVal); - v->strVal = NULL; - } else if (v->type == BAS_TYPE_ARRAY) { - basArrayUnref(v->arrVal); - v->arrVal = NULL; - } else if (v->type == BAS_TYPE_UDT) { - basUdtUnref(v->udtVal); - v->udtVal = NULL; - } -} +// Release a value (decrements refcount if applicable). +void basValRelease(BasValueT *v); // Convert a value to a specific type. Returns the converted value. // The original is NOT released -- caller manages lifetime. diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.c b/src/apps/kpunch/dvxbasic/runtime/vm.c index 6492dcf..82bb006 100644 --- a/src/apps/kpunch/dvxbasic/runtime/vm.c +++ b/src/apps/kpunch/dvxbasic/runtime/vm.c @@ -97,8 +97,9 @@ bool basVmCallSub(BasVmT *vm, int32_t codeAddr) { // Push a call frame that returns to an invalid address (sentinel) // We detect completion when callDepth drops back to savedCallDepth BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; - frame->returnPc = savedPc; - frame->localCount = BAS_VM_MAX_LOCALS; + frame->returnPc = savedPc; + frame->localCount = BAS_VM_MAX_LOCALS; + frame->errorHandler = 0; memset(frame->locals, 0, sizeof(frame->locals)); // Jump to the SUB @@ -131,8 +132,9 @@ bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, i bool savedRunning = vm->running; BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; - frame->returnPc = savedPc; - frame->localCount = BAS_VM_MAX_LOCALS; + frame->returnPc = savedPc; + frame->localCount = BAS_VM_MAX_LOCALS; + frame->errorHandler = 0; memset(frame->locals, 0, sizeof(frame->locals)); // Set arguments as locals (parameter 0 = local 0, etc.) @@ -169,8 +171,9 @@ bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args bool savedRunning = vm->running; BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; - frame->returnPc = savedPc; - frame->localCount = BAS_VM_MAX_LOCALS; + frame->returnPc = savedPc; + frame->localCount = BAS_VM_MAX_LOCALS; + frame->errorHandler = 0; memset(frame->locals, 0, sizeof(frame->locals)); for (int32_t i = 0; i < argCount && i < BAS_VM_MAX_LOCALS; i++) { @@ -269,6 +272,28 @@ const char *basVmGetError(const BasVmT *vm) { void basVmLoadModule(BasVmT *vm, BasModuleT *module) { vm->module = module; vm->pc = module->entryPoint; + + // Initialize globals whose declared type requires a non-numeric + // default. Uninitialized slots are zero (type=INTEGER, value=0), + // which breaks STRING operations: `DIM s AS STRING : s = s + "x"` + // would do numeric addition instead of concatenation. STATIC + // variables also alias global slots, so the same issue applies. + // globalInits survives the compiler's strip pass, unlike debugVars. + if (module->globalInits) { + for (int32_t i = 0; i < module->globalInitCount; i++) { + BasGlobalInitT *g = &module->globalInits[i]; + + if (g->index < 0 || g->index >= BAS_VM_MAX_GLOBALS) { + continue; + } + + if (g->dataType == BAS_TYPE_STRING) { + basValRelease(&vm->globals[g->index]); + vm->globals[g->index].type = BAS_TYPE_STRING; + vm->globals[g->index].strVal = basStringNew("", 0); + } + } + } } @@ -325,13 +350,40 @@ BasVmResultE basVmRun(BasVmT *vm) { if (result != BAS_VM_OK) { // If an error handler is set and this is a trappable error, - // jump to the handler instead of stopping execution - if (vm->errorHandler != 0 && !vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) { - vm->errorPc = savedPc; - vm->errorNextPc = vm->pc; - vm->inErrorHandler = true; - vm->pc = vm->errorHandler; - continue; + // unwind call frames until we find the SUB whose ON ERROR + // GOTO registered a handler, then jump there. Without this + // unwind an error raised inside a called SUB would fire the + // outer SUB's handler but OP_RET would then pop the inner + // frame and resume after the call site (effectively RESUME + // NEXT semantics), which is not what ON ERROR promises. + if (!vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) { + int32_t target = 0; + + while (vm->callDepth > 0) { + BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1]; + + if (frame->errorHandler != 0) { + target = frame->errorHandler; + break; + } + + // No handler on this frame -- discard it and keep + // unwinding. frame-local values need releasing. + for (int32_t li = 0; li < frame->localCount; li++) { + basValRelease(&frame->locals[li]); + } + + vm->callDepth--; + } + + if (target != 0) { + vm->errorPc = savedPc; + vm->errorNextPc = vm->pc; + vm->inErrorHandler = true; + vm->errorHandler = target; + vm->pc = target; + continue; + } } vm->running = false; @@ -840,8 +892,9 @@ BasVmResultE basVmStep(BasVmT *vm) { } BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; - frame->returnPc = vm->pc; - frame->localCount = BAS_VM_MAX_LOCALS; + frame->returnPc = vm->pc; + frame->localCount = BAS_VM_MAX_LOCALS; + frame->errorHandler = 0; // Zero all local slots memset(frame->locals, 0, sizeof(frame->locals)); @@ -853,6 +906,9 @@ BasVmResultE basVmStep(BasVmT *vm) { } } + // The callee starts with no handler; if it raises an error + // the dispatcher will unwind to the nearest frame with one. + vm->errorHandler = 0; vm->pc = addr; break; } @@ -870,7 +926,24 @@ BasVmResultE basVmStep(BasVmT *vm) { basValRelease(&frame->locals[i]); } + bool hadHandler = (frame->errorHandler != 0); + frame->errorHandler = 0; vm->pc = frame->returnPc; + + // Restore the active handler to whatever the caller had set + BasCallFrameT *caller = currentFrame(vm); + vm->errorHandler = caller ? caller->errorHandler : 0; + + // If this SUB owned the active error handler, any handler + // body that ran inside it is now done -- clear the flag so + // the next error (in this or another SUB) can trap again. + // QBASIC documents this: an unresumed handler is cleared + // when the procedure that installed it returns. + if (hadHandler) { + vm->inErrorHandler = false; + vm->errorNumber = 0; + vm->errorMsg[0] = '\0'; + } break; } @@ -894,8 +967,19 @@ BasVmResultE basVmStep(BasVmT *vm) { basValRelease(&frame->locals[i]); } + bool hadHandler = (frame->errorHandler != 0); + frame->errorHandler = 0; vm->pc = frame->returnPc; + BasCallFrameT *caller = currentFrame(vm); + vm->errorHandler = caller ? caller->errorHandler : 0; + + if (hadHandler) { + vm->inErrorHandler = false; + vm->errorNumber = 0; + vm->errorMsg[0] = '\0'; + } + if (!push(vm, retVal)) { basValRelease(&retVal); return BAS_VM_STACK_OVERFLOW; @@ -918,8 +1002,9 @@ BasVmResultE basVmStep(BasVmT *vm) { } case OP_FOR_INIT: { - uint16_t varIdx = readUint16(vm); - uint8_t scopeTag = readUint8(vm); + uint16_t varIdx = readUint16(vm); + uint8_t scopeTag = readUint8(vm); + int16_t skipOffset = readInt16(vm); BasValueT stepVal; BasValueT limitVal; @@ -940,6 +1025,43 @@ BasVmResultE basVmStep(BasVmT *vm) { fs->limit = limitVal; fs->step = stepVal; fs->loopTop = vm->pc; + + // Entry check: if the loop's range is already empty (e.g. + // FOR i = 10 TO 5 with positive step), skip the body and + // pop the FOR state. QBASIC semantics: body executes zero + // times when the range is empty. + BasValueT *varSlot = NULL; + + if (scopeTag == SCOPE_LOCAL) { + BasCallFrameT *frame = currentFrame(vm); + + if (frame && varIdx < (uint16_t)frame->localCount) { + varSlot = &frame->locals[varIdx]; + } + } else if (scopeTag == SCOPE_FORM) { + if (vm->currentFormVars && varIdx < (uint16_t)vm->currentFormVarCount) { + varSlot = &vm->currentFormVars[varIdx]; + } + } else { + if (varIdx < BAS_VM_MAX_GLOBALS) { + varSlot = &vm->globals[varIdx]; + } + } + + if (varSlot) { + double curVal = basValToNumber(*varSlot); + double stepNum = basValToNumber(fs->step); + double limNum = basValToNumber(fs->limit); + bool enter = (stepNum >= 0) ? (curVal <= limNum) : (curVal >= limNum); + + if (!enter) { + basValRelease(&fs->limit); + basValRelease(&fs->step); + vm->forDepth--; + vm->pc += skipOffset; + } + } + break; } @@ -990,7 +1112,11 @@ BasVmResultE basVmStep(BasVmT *vm) { // Increment: var = var + step. Preserve the loop variable's // original type so extern calls that pass it as an int32_t - // argument don't get an 8-byte double instead. + // argument don't get an 8-byte double instead. Exception: + // if STEP is fractional, an integer slot would truncate the + // increment and spin forever (FOR a = 0 TO 6.3 STEP 0.2 with + // `a` stored as INTEGER from the literal 0). Promote to + // DOUBLE in that case so the loop can actually progress. double varVal = basValToNumber(*varSlot); double stepVal = basValToNumber(fs->step); double limVal = basValToNumber(fs->limit); @@ -999,7 +1125,11 @@ BasVmResultE basVmStep(BasVmT *vm) { basValRelease(varSlot); - if (varType == BAS_TYPE_INTEGER) { + bool stepIsFractional = (stepVal != floor(stepVal)); + + if (stepIsFractional) { + *varSlot = basValDouble(varVal); + } else if (varType == BAS_TYPE_INTEGER) { *varSlot = basValInteger((int16_t)varVal); } else if (varType == BAS_TYPE_LONG) { *varSlot = basValLong((int32_t)varVal); @@ -1639,6 +1769,40 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + case OP_STR_OCT: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + int32_t n = (int32_t)basValToNumber(*top); + char buf[16]; + snprintf(buf, sizeof(buf), "%o", (unsigned int)n); + basValRelease(top); + *top = basValStringFromC(buf); + break; + } + + case OP_CONV_BOOL: { + if (vm->sp < 1) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT *top = &vm->stack[vm->sp - 1]; + bool truthy; + + if (top->type == BAS_TYPE_STRING) { + truthy = (top->strVal && top->strVal->len > 0); + } else { + truthy = (basValToNumber(*top) != 0.0); + } + + basValRelease(top); + top->type = BAS_TYPE_BOOLEAN; + top->boolVal = truthy ? -1 : 0; + break; + } + case OP_STR_STRING: { // STRING$(n, char) BasValueT charVal; @@ -1827,7 +1991,19 @@ BasVmResultE basVmStep(BasVmT *vm) { case OP_ON_ERROR: { int16_t handler = readInt16(vm); - vm->errorHandler = (handler == 0) ? 0 : vm->pc + handler; + int32_t target = (handler == 0) ? 0 : vm->pc + handler; + + // Record the handler on the current call frame. The error + // dispatcher walks frames (innermost outward) to find a + // handler and unwinds the stack to that frame before + // jumping. Module-level code lives in frame 0. + BasCallFrameT *frame = currentFrame(vm); + + if (frame) { + frame->errorHandler = target; + } + + vm->errorHandler = target; break; } @@ -2032,6 +2208,58 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + case OP_PUSH_ARR_ADDR: { + // Pass `arr(i)` as a BYREF parameter: push a BAS_TYPE_REF + // pointing into the array's element storage. The array is + // ref-counted, and the caller still holds a reference via + // the local it came from, so the element memory is stable + // for the duration of the call. + uint8_t dims = readUint8(vm); + int32_t indices[BAS_ARRAY_MAX_DIMS]; + + for (int32_t d = dims - 1; d >= 0; d--) { + BasValueT idxVal; + + if (!pop(vm, &idxVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + indices[d] = (int32_t)basValToNumber(idxVal); + basValRelease(&idxVal); + } + + BasValueT arrRef; + + if (!pop(vm, &arrRef)) { + return BAS_VM_STACK_UNDERFLOW; + } + + if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) { + basValRelease(&arrRef); + runtimeError(vm, 13, "Not an array"); + return BAS_VM_TYPE_MISMATCH; + } + + int32_t flatIdx = basArrayIndex(arrRef.arrVal, indices, dims); + + if (flatIdx < 0) { + basValRelease(&arrRef); + runtimeError(vm, 9, "Subscript out of range"); + return BAS_VM_SUBSCRIPT_RANGE; + } + + BasValueT ref; + ref.type = BAS_TYPE_REF; + ref.refVal = &arrRef.arrVal->elements[flatIdx]; + basValRelease(&arrRef); + + if (!push(vm, ref)) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + case OP_STORE_ARRAY: { uint8_t dims = readUint8(vm); @@ -3623,6 +3851,15 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) { } } + // Capture operand types BEFORE release so we know whether to keep + // the result as a float. The parser always emits OP_ADD_INT (the + // VM promotes based on operand types), so the opcode alone can't + // tell us whether this is an integer or floating-point op. + uint8_t aType = a.type; + uint8_t bType = b.type; + bool hadFloat = (aType == BAS_TYPE_SINGLE || aType == BAS_TYPE_DOUBLE || + bType == BAS_TYPE_SINGLE || bType == BAS_TYPE_DOUBLE); + double na = basValToNumber(a); double nb = basValToNumber(b); basValRelease(&a); @@ -3682,8 +3919,12 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) { break; } - // Return appropriate type - if (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT) { + // Return appropriate type. An op that would normally produce an + // integer result (OP_ADD_INT, etc.) still has to yield a float when + // either operand was a float -- otherwise 1.5 + 2.25 truncates to 3. + bool intOp = (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT); + + if (intOp && !hadFloat) { if (result >= (double)INT16_MIN && result <= (double)INT16_MAX) { push(vm, basValInteger((int16_t)result)); } else if (result >= (double)INT32_MIN && result <= (double)INT32_MAX) { @@ -5333,17 +5574,19 @@ static bool push(BasVmT *vm, BasValueT val) { } -// x86 tolerates unaligned loads at cost of ~1 cycle; cast-through-pointer is -// faster than memcpy because the compiler can emit a single 16-bit load. +// memcpy with constant size is folded to a single load by the compiler and +// is alignment-safe (bytecode operands aren't guaranteed 2-byte aligned). static inline int16_t readInt16(BasVmT *vm) { - int16_t val = *(const int16_t *)&vm->module->code[vm->pc]; + int16_t val; + memcpy(&val, &vm->module->code[vm->pc], sizeof(int16_t)); vm->pc += sizeof(int16_t); return val; } static inline uint16_t readUint16(BasVmT *vm) { - uint16_t val = *(const uint16_t *)&vm->module->code[vm->pc]; + uint16_t val; + memcpy(&val, &vm->module->code[vm->pc], sizeof(uint16_t)); vm->pc += sizeof(uint16_t); return val; } @@ -5364,6 +5607,7 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool bool hadBreakpoint = false; while (vm->running && vm->callDepth > savedCallDepth) { + int32_t stepPc = vm->pc; // save for ON ERROR dispatch BasVmResultE result = basVmStep(vm); if (result == BAS_VM_HALTED) { @@ -5397,6 +5641,41 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool } if (result != BAS_VM_OK) { + // Try ON ERROR GOTO: walk call frames inside the current + // sub-call boundary (savedCallDepth) looking for one that + // registered a handler. If found, unwind to that frame + // and jump to the handler. Mirrors basVmRun's dispatcher + // so event handlers (fired via basVmCallSub) behave like + // module-level code when it comes to error trapping. + if (!vm->inErrorHandler && result != BAS_VM_BAD_OPCODE) { + int32_t target = 0; + + while (vm->callDepth > savedCallDepth) { + BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1]; + + if (frame->errorHandler != 0) { + target = frame->errorHandler; + break; + } + + for (int32_t li = 0; li < frame->localCount; li++) { + basValRelease(&frame->locals[li]); + } + + vm->callDepth--; + } + + if (target != 0) { + vm->errorPc = stepPc; + vm->errorNextPc = vm->pc; + vm->inErrorHandler = true; + vm->errorHandler = target; + vm->pc = target; + stepsSinceYield = 0; + continue; + } + } + vm->pc = savedPc; vm->callDepth = savedCallDepth; vm->running = savedRunning; diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.h b/src/apps/kpunch/dvxbasic/runtime/vm.h index 3d1bc18..4fa9946 100644 --- a/src/apps/kpunch/dvxbasic/runtime/vm.h +++ b/src/apps/kpunch/dvxbasic/runtime/vm.h @@ -262,6 +262,7 @@ typedef struct { int32_t returnPc; // instruction to return to int32_t baseSlot; // base index in locals array int32_t localCount; // number of locals in this frame + int32_t errorHandler; // ON ERROR GOTO target in this SUB (0 = none) BasValueT locals[BAS_VM_MAX_LOCALS]; } BasCallFrameT; @@ -293,12 +294,13 @@ typedef struct { #define BAS_MAX_PROC_NAME 64 typedef struct { - char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved) - int32_t codeAddr; // entry point in code[] - int32_t paramCount; // number of parameters - int32_t localCount; // number of local variables (for debugger) - uint8_t returnType; // BAS_TYPE_* (0 for SUB) - bool isFunction; // true = FUNCTION, false = SUB + char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved) + char formName[BAS_MAX_PROC_NAME]; // owning form (for form-scope vars), "" if global + int32_t codeAddr; // entry point in code[] + int32_t paramCount; // number of parameters + int32_t localCount; // number of local variables (for debugger) + uint8_t returnType; // BAS_TYPE_* (0 for SUB) + bool isFunction; // true = FUNCTION, false = SUB } BasProcEntryT; // Debug UDT field definition (preserved for debugger watch) @@ -340,6 +342,16 @@ typedef struct { // Compiled module (output of the compiler) // ============================================================ +// Runtime-required global init entry. STRING and SINGLE/DOUBLE +// globals need to start with the correct slot type even when debug +// info has been stripped, or operators that switch on slot type +// (e.g. STRING concat) break on first use. +typedef struct { + int32_t index; // global slot index + uint8_t dataType; // BAS_TYPE_* +} BasGlobalInitT; + + typedef struct { uint8_t *code; // p-code bytecode int32_t codeLen; @@ -353,6 +365,8 @@ typedef struct { int32_t procCount; BasFormVarInfoT *formVarInfo; // per-form variable counts int32_t formVarInfoCount; + BasGlobalInitT *globalInits; // runtime global type init (survives stripping) + int32_t globalInitCount; BasDebugVarT *debugVars; // variable names for debugger int32_t debugVarCount; BasDebugUdtDefT *debugUdtDefs; // UDT type definitions for debugger diff --git a/src/apps/kpunch/dvxbasic/stub/bascomp.c b/src/apps/kpunch/dvxbasic/stub/bascomp.c index 03df193..6026a29 100644 --- a/src/apps/kpunch/dvxbasic/stub/bascomp.c +++ b/src/apps/kpunch/dvxbasic/stub/bascomp.c @@ -183,6 +183,7 @@ int main(int argc, char **argv) { const char *helpFile = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, ""); const char *startupForm = prefsGetString(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, ""); (void)startupForm; // used implicitly by stub's basFormRtLoadAllForms + bool optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false); // Derive output path char outBuf[DVX_MAX_PATH]; @@ -355,6 +356,7 @@ int main(int argc, char **argv) { } basParserInit(parser, concatBuf, pos); + parser->optionExplicit = optionExplicit; if (!basParse(parser)) { fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error); diff --git a/src/apps/kpunch/dvxbasic/stub/basstub.c b/src/apps/kpunch/dvxbasic/stub/basstub.c index 8cd9851..f036d76 100644 --- a/src/apps/kpunch/dvxbasic/stub/basstub.c +++ b/src/apps/kpunch/dvxbasic/stub/basstub.c @@ -170,10 +170,15 @@ int32_t appMain(DxeAppContextT *ctx) { basVmSetInputCallback(vm, stubInput, NULL); basVmSetDoEventsCallback(vm, stubDoEvents, NULL); - // Set app paths - snprintf(vm->appPath, DVX_MAX_PATH, "%s", ctx->appDir); - snprintf(vm->appConfig, DVX_MAX_PATH, "%s", ctx->appDir); - snprintf(vm->appData, DVX_MAX_PATH, "%s", ctx->appDir); + // Set app paths. App.Path is the .app's directory (read-only on CD); + // App.Config and App.Data are writable subdirectories created on + // demand. The IDE does the same split for project debugging, so + // behavior matches between compiled apps and in-IDE runs. + snprintf(vm->appPath, DVX_MAX_PATH, "%s", ctx->appDir); + snprintf(vm->appConfig, DVX_MAX_PATH, "%s" DVX_PATH_SEP "CONFIG", ctx->appDir); + snprintf(vm->appData, DVX_MAX_PATH, "%s" DVX_PATH_SEP "DATA", ctx->appDir); + platformMkdirRecursive(vm->appConfig); + platformMkdirRecursive(vm->appData); // Set extern call callbacks (required for DECLARE LIBRARY functions) BasExternCallbacksT extCb; diff --git a/src/apps/kpunch/dvxbasic/test_suite.c b/src/apps/kpunch/dvxbasic/test_suite.c new file mode 100644 index 0000000..5753ac4 --- /dev/null +++ b/src/apps/kpunch/dvxbasic/test_suite.c @@ -0,0 +1,2254 @@ +// The MIT License (MIT) +// +// Copyright (C) 2026 Scott Duensing +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +// test_suite.c -- Assertion-based regression tests for the BASIC +// language runtime. Each test compiles a snippet, runs the VM with +// PRINT output captured, and compares the captured text to an +// expected string. Failures are reported with a diff-style preview +// and the process exits non-zero, so `make tests` surfaces regressions +// before they reach 86Box. +// +// Add new tests with TEST_EQ(name, source, expected) -- other helpers +// cover compile errors and runtime errors. + +#include "compiler/parser.h" +#include "runtime/vm.h" +#include "runtime/values.h" + +#include +#include +#include + +// ============================================================ +// Capture buffer +// ============================================================ + +#define CAPTURE_MAX 8192 + +typedef struct { + char buf[CAPTURE_MAX]; + int32_t len; +} CaptureT; + + +static void captureReset(CaptureT *c) { + c->buf[0] = '\0'; + c->len = 0; +} + + +static void captureAppend(CaptureT *c, const char *s) { + int32_t n = (int32_t)strlen(s); + + if (c->len + n >= CAPTURE_MAX - 1) { + n = CAPTURE_MAX - 1 - c->len; + } + + if (n > 0) { + memcpy(c->buf + c->len, s, n); + c->len += n; + c->buf[c->len] = '\0'; + } +} + + +static void capturePrint(void *ctx, const char *text, bool newline) { + CaptureT *c = (CaptureT *)ctx; + + if (text) { + captureAppend(c, text); + } + + if (newline) { + captureAppend(c, "\n"); + } +} + + +// ============================================================ +// Test driver +// ============================================================ + +static int32_t sPassCount = 0; +static int32_t sFailCount = 0; + + +static void reportPass(const char *name) { + printf("PASS %s\n", name); + sPassCount++; +} + + +static void reportFail(const char *name, const char *detail) { + printf("FAIL %s\n", name); + + if (detail && detail[0]) { + printf(" %s\n", detail); + } + + sFailCount++; +} + + +static void reportFailDiff(const char *name, const char *expected, const char *got) { + printf("FAIL %s\n", name); + printf(" expected: [%s]\n", expected); + printf(" got: [%s]\n", got); + sFailCount++; +} + + +// Compile + run. On success captures PRINT output into *out. Returns +// 0 on success, negative on compile error, positive for runtime errors. +// Runtime error text goes into outErr if provided. +static int32_t runAndCapture(const char *source, char *out, int32_t outSize, char *outErr, int32_t outErrSize) { + int32_t len = (int32_t)strlen(source); + + BasParserT parser; + basParserInit(&parser, source, len); + + if (!basParse(&parser)) { + if (outErr) { + snprintf(outErr, outErrSize, "%s", parser.error); + } + + basParserFree(&parser); + return -1; + } + + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (!mod) { + if (outErr) { + snprintf(outErr, outErrSize, "module build failed"); + } + + return -2; + } + + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + + CaptureT cap; + captureReset(&cap); + basVmSetPrintCallback(vm, capturePrint, &cap); + + vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount; + vm->callDepth = 1; + + BasVmResultE result = basVmRun(vm); + + int32_t rc = 0; + + if (result != BAS_VM_HALTED && result != BAS_VM_OK) { + if (outErr) { + snprintf(outErr, outErrSize, "%s", basVmGetError(vm)); + } + + rc = (int32_t)result; + } + + if (out && outSize > 0) { + snprintf(out, outSize, "%s", cap.buf); + } + + basVmDestroy(vm); + basModuleFree(mod); + return rc; +} + + +// TEST_EQ -- compile, run, expect captured output to match +static void testEq(const char *name, const char *source, const char *expected) { + char out[CAPTURE_MAX]; + char err[256] = ""; + + int32_t rc = runAndCapture(source, out, sizeof(out), err, sizeof(err)); + + if (rc != 0) { + char detail[512]; + snprintf(detail, sizeof(detail), "rc=%d err=%s", (int)rc, err); + reportFail(name, detail); + return; + } + + if (strcmp(out, expected) != 0) { + reportFailDiff(name, expected, out); + return; + } + + reportPass(name); +} + + +// TEST_COMPILE_ERROR -- expect compilation to fail; substring of the +// error message must match. Pass an empty needle to accept any error. +static void testCompileError(const char *name, const char *source, const char *needle) { + char out[CAPTURE_MAX]; + char err[256] = ""; + + int32_t rc = runAndCapture(source, out, sizeof(out), err, sizeof(err)); + + if (rc != -1) { + char detail[256]; + snprintf(detail, sizeof(detail), "expected compile error; got rc=%d output=[%s]", (int)rc, out); + reportFail(name, detail); + return; + } + + if (needle && needle[0] && strstr(err, needle) == NULL) { + char detail[512]; + snprintf(detail, sizeof(detail), "error [%s] did not contain [%s]", err, needle); + reportFail(name, detail); + return; + } + + reportPass(name); +} + + +// runSubAndCapture -- compile, then invoke a specific named SUB via +// basVmCallSub. Mirrors what fireCtrlEvent does for event handlers. +// Returns 0 if the SUB returned normally, 1 if it returned false +// (unhandled error), -1 on compile/load error. Captured PRINT output +// goes into out; runtime error text (if any) goes into outErr. +static int32_t runSubAndCapture(const char *source, const char *subName, + char *out, int32_t outSize, + char *outErr, int32_t outErrSize) { + int32_t len = (int32_t)strlen(source); + + BasParserT parser; + basParserInit(&parser, source, len); + + if (!basParse(&parser)) { + if (outErr) { + snprintf(outErr, outErrSize, "%s", parser.error); + } + + basParserFree(&parser); + return -1; + } + + BasModuleT *mod = basParserBuildModule(&parser); + basParserFree(&parser); + + if (!mod) { + if (outErr) { + snprintf(outErr, outErrSize, "module build failed"); + } + + return -1; + } + + BasVmT *vm = basVmCreate(); + basVmLoadModule(vm, mod); + + CaptureT cap; + captureReset(&cap); + basVmSetPrintCallback(vm, capturePrint, &cap); + + vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount; + vm->callDepth = 1; + + // Execute module-level code first (to initialize globals) + basVmRun(vm); + + // Find the target SUB by name + int32_t subAddr = -1; + + for (int32_t i = 0; i < mod->procCount; i++) { + if (strcasecmp(mod->procs[i].name, subName) == 0) { + subAddr = mod->procs[i].codeAddr; + break; + } + } + + int32_t rc = 0; + + if (subAddr < 0) { + if (outErr) { + snprintf(outErr, outErrSize, "SUB '%s' not found", subName); + } + rc = -1; + } else { + // Clear error state before the call so we see only this call's errors + vm->errorNumber = 0; + vm->errorMsg[0] = '\0'; + vm->inErrorHandler = false; + vm->errorHandler = 0; + + bool ok = basVmCallSub(vm, subAddr); + rc = ok ? 0 : 1; + + if (!ok && outErr) { + snprintf(outErr, outErrSize, "%s", basVmGetError(vm)); + } + } + + if (out && outSize > 0) { + snprintf(out, outSize, "%s", cap.buf); + } + + basVmDestroy(vm); + basModuleFree(mod); + return rc; +} + + +// TEST_SUB_EQ -- compile, run module-level code, then invoke a named +// SUB through basVmCallSub (the path used by event handlers), and +// compare captured output. +static void testSubEq(const char *name, const char *source, const char *subName, const char *expected) { + char out[CAPTURE_MAX]; + char err[256] = ""; + + int32_t rc = runSubAndCapture(source, subName, out, sizeof(out), err, sizeof(err)); + + if (rc < 0) { + char detail[512]; + snprintf(detail, sizeof(detail), "setup error: %s", err); + reportFail(name, detail); + return; + } + + if (rc != 0) { + char detail[512]; + snprintf(detail, sizeof(detail), "SUB returned false (unhandled error): %s", err); + reportFail(name, detail); + return; + } + + if (strcmp(out, expected) != 0) { + reportFailDiff(name, expected, out); + return; + } + + reportPass(name); +} + + +#define TEST_SUB_EQ(n, s, sub, e) testSubEq((n), (s), (sub), (e)) + + +// TEST_RUNTIME_ERROR -- expect runtime error; optional substring match +// on the error message. +static void testRuntimeError(const char *name, const char *source, const char *needle) { + char out[CAPTURE_MAX]; + char err[256] = ""; + + int32_t rc = runAndCapture(source, out, sizeof(out), err, sizeof(err)); + + if (rc <= 0) { + char detail[256]; + snprintf(detail, sizeof(detail), "expected runtime error; got rc=%d output=[%s]", (int)rc, out); + reportFail(name, detail); + return; + } + + if (needle && needle[0] && strstr(err, needle) == NULL) { + char detail[512]; + snprintf(detail, sizeof(detail), "error [%s] did not contain [%s]", err, needle); + reportFail(name, detail); + return; + } + + reportPass(name); +} + + +#define TEST_EQ(n, s, e) testEq((n), (s), (e)) +#define TEST_COMPILE_ERROR(n, s, e) testCompileError((n), (s), (e)) +#define TEST_RUNTIME_ERROR(n, s, e) testRuntimeError((n), (s), (e)) + + +// ============================================================ +// Test cases +// ============================================================ + +int main(void) { + basStringSystemInit(); + + printf("DVX BASIC Regression Suite\n"); + printf("==========================\n"); + + // --- Arithmetic / literals --- + // DVX BASIC PRINT format: "N \n" for positive/zero integers, "-N \n" + // for negatives. No leading space (unlike classic QBASIC). + TEST_EQ("int-add", "PRINT 2 + 3\n", "5 \n"); + TEST_EQ("int-sub", "PRINT 10 - 4\n", "6 \n"); + TEST_EQ("int-mul", "PRINT 6 * 7\n", "42 \n"); + TEST_EQ("int-div-float", "PRINT 10 / 4\n", "2.5 \n"); + TEST_EQ("int-idiv", "PRINT 10 \\ 3\n", "3 \n"); + TEST_EQ("int-mod", "PRINT 17 MOD 5\n", "2 \n"); + TEST_EQ("int-pow", "PRINT 2 ^ 10\n", "1024 \n"); + TEST_EQ("precedence", "PRINT 2 + 3 * 4\n", "14 \n"); + TEST_EQ("parens", "PRINT (2 + 3) * 4\n", "20 \n"); + TEST_EQ("unary-neg", "PRINT -5 + 2\n", "-3 \n"); + TEST_EQ("double-lit", "PRINT 1.5 + 2.25\n", "3.75 \n"); + + // --- Strings --- + TEST_EQ("string-concat", "PRINT \"foo\" + \"bar\"\n", "foobar\n"); + TEST_EQ("string-len", "PRINT LEN(\"hello\")\n", "5 \n"); + TEST_EQ("string-left", "PRINT LEFT$(\"abcdef\", 3)\n", "abc\n"); + TEST_EQ("string-right", "PRINT RIGHT$(\"abcdef\", 2)\n", "ef\n"); + TEST_EQ("string-mid", "PRINT MID$(\"abcdef\", 2, 3)\n", "bcd\n"); + TEST_EQ("string-ucase", "PRINT UCASE$(\"Hello\")\n", "HELLO\n"); + TEST_EQ("string-lcase", "PRINT LCASE$(\"Hello\")\n", "hello\n"); + TEST_EQ("string-str", "PRINT STR$(42)\n", " 42\n"); + TEST_EQ("string-val", "PRINT VAL(\"123\")\n", "123 \n"); + + // --- Control flow --- + TEST_EQ("if-then", + "DIM x AS INTEGER\nx = 5\nIF x > 3 THEN PRINT \"big\"\n", + "big\n"); + TEST_EQ("if-else", + "DIM x AS INTEGER\nx = 1\nIF x > 3 THEN PRINT \"big\" ELSE PRINT \"small\"\n", + "small\n"); + TEST_EQ("if-block", + "DIM x AS INTEGER\nx = 5\nIF x > 3 THEN\n PRINT \"a\"\n PRINT \"b\"\nEND IF\n", + "a\nb\n"); + TEST_EQ("select-case", + "DIM n AS INTEGER\nn = 2\n" + "SELECT CASE n\n" + " CASE 1: PRINT \"one\"\n" + " CASE 2: PRINT \"two\"\n" + " CASE ELSE: PRINT \"other\"\n" + "END SELECT\n", + "two\n"); + + // --- FOR loops (the recent bug: double step was truncating) --- + TEST_EQ("for-int-step", + "DIM i AS INTEGER\nFOR i = 1 TO 3\n PRINT i\nNEXT i\n", + "1 \n2 \n3 \n"); + TEST_EQ("for-neg-step", + "DIM i AS INTEGER\nFOR i = 3 TO 1 STEP -1\n PRINT i\nNEXT i\n", + "3 \n2 \n1 \n"); + TEST_EQ("for-double-step", + // Regression: DIM AS DOUBLE with fractional STEP must not + // truncate the increment to zero. Pre-fix this looped forever. + "DIM a AS DOUBLE\n" + "DIM c AS INTEGER\n" + "c = 0\n" + "FOR a = 0 TO 1 STEP 0.25\n" + " c = c + 1\n" + "NEXT a\n" + "PRINT c\n", + "5 \n"); + TEST_EQ("for-double-six-cycles", + // Direct analogue of GfxDrawAll's circle loop. + "DIM a AS DOUBLE\n" + "DIM c AS INTEGER\n" + "c = 0\n" + "FOR a = 0 TO 6.3 STEP 0.2\n" + " c = c + 1\n" + "NEXT a\n" + "PRINT c\n", + "32 \n"); + TEST_EQ("for-exit", + "DIM i AS INTEGER\n" + "FOR i = 1 TO 10\n" + " IF i = 4 THEN EXIT FOR\n" + " PRINT i\n" + "NEXT i\n", + "1 \n2 \n3 \n"); + + // --- WHILE / DO loops --- + TEST_EQ("do-while", + "DIM i AS INTEGER\n" + "i = 0\n" + "DO WHILE i < 3\n" + " PRINT i\n" + " i = i + 1\n" + "LOOP\n", + "0 \n1 \n2 \n"); + TEST_EQ("do-until", + "DIM i AS INTEGER\n" + "i = 0\n" + "DO\n" + " i = i + 1\n" + "LOOP UNTIL i = 3\n" + "PRINT i\n", + "3 \n"); + + // --- SUB / FUNCTION --- + TEST_EQ("sub-call", + "SUB greet\n PRINT \"hi\"\nEND SUB\n" + "greet\n", + "hi\n"); + TEST_EQ("sub-params", + "SUB twice(n AS INTEGER)\n PRINT n * 2\nEND SUB\n" + "twice 7\n", + "14 \n"); + TEST_EQ("function-return", + "FUNCTION sq(n AS INTEGER) AS INTEGER\n sq = n * n\nEND FUNCTION\n" + "PRINT sq(9)\n", + "81 \n"); + TEST_EQ("recursion", + "FUNCTION fact(n AS INTEGER) AS LONG\n" + " IF n <= 1 THEN\n fact = 1\n ELSE\n fact = n * fact(n - 1)\n END IF\n" + "END FUNCTION\n" + "PRINT fact(6)\n", + "720 \n"); + TEST_EQ("byref-vs-byval", + "SUB bump(BYVAL a AS INTEGER, b AS INTEGER)\n a = a + 1\n b = b + 1\nEND SUB\n" + "DIM x AS INTEGER\nDIM y AS INTEGER\n" + "x = 10\ny = 20\n" + "bump x, y\n" + "PRINT x\nPRINT y\n", + "10 \n21 \n"); + + // --- Globals from within SUB --- + TEST_EQ("global-from-sub", + // Regression: the basdemo Dynamic Form bug was caused by + // SUBs unable to access module-level DIM. + "DIM counter AS INTEGER\n" + "counter = 0\n" + "SUB inc\n counter = counter + 1\nEND SUB\n" + "inc\ninc\ninc\n" + "PRINT counter\n", + "3 \n"); + + // --- BEGINFORM / ENDFORM --- + // Inside BEGINFORM, DIMs become form-scope vars; SUBs still live + // globally but inherit the owning form context when called through + // basVmCallSub with form vars bound. Running the init code requires + // a form to actually be loaded (formrt sets currentFormVars) so we + // can only verify the parser accepts/rejects forms here. + TEST_COMPILE_ERROR("nested-beginform", + "BEGINFORM \"A\"\nBEGINFORM \"B\"\nENDFORM\nENDFORM\n", + "Nested BEGINFORM"); + TEST_COMPILE_ERROR("beginform-in-sub", + "SUB foo\nBEGINFORM \"X\"\nENDFORM\nEND SUB\n", + "BEGINFORM inside SUB"); + + // --- Arrays --- + TEST_EQ("array-1d", + "DIM a(3) AS INTEGER\n" + "a(0) = 10\na(1) = 20\na(2) = 30\na(3) = 40\n" + "PRINT a(0) + a(3)\n", + "50 \n"); + TEST_EQ("array-lbound-ubound", + "DIM a(5 TO 9) AS INTEGER\n" + "PRINT LBOUND(a)\nPRINT UBOUND(a)\n", + "5 \n9 \n"); + + // --- UDT --- + TEST_EQ("udt-fields", + "TYPE Pt\n x AS INTEGER\n y AS INTEGER\nEND TYPE\n" + "DIM p AS Pt\n" + "p.x = 3\np.y = 4\n" + "PRINT p.x + p.y\n", + "7 \n"); + + // --- ON ERROR GOTO --- + TEST_EQ("on-error-module", + "ON ERROR GOTO handler\n" + "DIM a AS INTEGER\nDIM b AS INTEGER\n" + "a = 10\nb = 0\n" + "PRINT a \\ b\n" + "PRINT \"never\"\n" + "END\n" + "handler:\n" + "PRINT \"caught\"\n" + "PRINT ERR\n", + "caught\n11 \n"); + TEST_EQ("on-error-in-sub", + // Regression: .frm's btnError_Click demo has ON ERROR inside a + // SUB. Make sure the handler label resolves within the SUB. + "SUB doit\n" + " ON ERROR GOTO handler\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n" + " PRINT a \\ b\n" + " PRINT \"never\"\n" + " EXIT SUB\n" + " handler:\n" + " PRINT \"caught\"\n" + " PRINT ERR\n" + "END SUB\n" + "doit\n", + "caught\n11 \n"); + + // --- Runtime errors surface --- + TEST_RUNTIME_ERROR("divzero-no-handler", + "DIM a AS INTEGER\nDIM b AS INTEGER\n" + "a = 10\nb = 0\n" + "PRINT a \\ b\n", + ""); + + // --- Compile-time checks --- + TEST_COMPILE_ERROR("unknown-keyword", + "FLOOGLE 1, 2, 3\n", + ""); + + // --- STATIC variables in SUB persist across calls --- + TEST_EQ("static-local", + "SUB inc\n" + " STATIC n AS INTEGER\n" + " n = n + 1\n" + " PRINT n\n" + "END SUB\n" + "inc\ninc\ninc\n", + "1 \n2 \n3 \n"); + + // --- GOSUB / RETURN --- + TEST_EQ("gosub-return", + "GOSUB sub1\n" + "PRINT \"back\"\n" + "END\n" + "sub1:\n" + "PRINT \"in sub\"\n" + "RETURN\n", + "in sub\nback\n"); + + // --- SWAP --- + TEST_EQ("swap", + "DIM a AS INTEGER\nDIM b AS INTEGER\n" + "a = 1\nb = 9\n" + "SWAP a, b\n" + "PRINT a\nPRINT b\n", + "9 \n1 \n"); + + // --- DATA / READ --- + TEST_EQ("data-read", + "DIM x AS INTEGER\n" + "READ x\nPRINT x\n" + "READ x\nPRINT x\n" + "DATA 11, 22\n", + "11 \n22 \n"); + + // --- FORMAT$ --- + TEST_EQ("format-number", + "PRINT FORMAT$(1234.5, \"#,##0.00\")\n", + "1,234.50\n"); + + // --- String comparison --- + TEST_EQ("string-equal", + "IF \"abc\" = \"abc\" THEN PRINT \"y\" ELSE PRINT \"n\"\n", + "y\n"); + TEST_EQ("string-less", + "IF \"abc\" < \"abd\" THEN PRINT \"y\" ELSE PRINT \"n\"\n", + "y\n"); + + // --- OPTION EXPLICIT enforces DIM --- + TEST_COMPILE_ERROR("option-explicit", + "OPTION EXPLICIT\n" + "x = 5\n", + ""); + + // --- Math functions --- + TEST_EQ("math-abs", "PRINT ABS(-7)\n", "7 \n"); + TEST_EQ("math-int", "PRINT INT(3.7)\n", "3 \n"); + TEST_EQ("math-sgn-pos", "PRINT SGN(5)\n", "1 \n"); + TEST_EQ("math-sgn-neg", "PRINT SGN(-5)\n", "-1 \n"); + TEST_EQ("math-sgn-zero","PRINT SGN(0)\n", "0 \n"); + + // --- CAST/conversion --- + TEST_EQ("cint", "PRINT CINT(3.6)\n", "4 \n"); + TEST_EQ("clng", "PRINT CLNG(100000)\n", "100000 \n"); + TEST_EQ("cdbl", "PRINT CDBL(1) / 3\n", "0.333333 \n"); + + // --- Float arithmetic preserves fractional results --- + // Regression: the VM was promoting OP_ADD_INT results back to int16 + // even when either operand was a float, truncating 1.5+2.25 to 3. + TEST_EQ("float-add-literal", "PRINT 1.5 + 2.25\n", "3.75 \n"); + TEST_EQ("float-add-var", + "DIM a AS DOUBLE\nDIM b AS DOUBLE\n" + "a = 1.5\nb = 2.25\n" + "PRINT a + b\n", + "3.75 \n"); + TEST_EQ("float-mul", + "PRINT 2.5 * 4\n", + "10 \n"); + TEST_EQ("float-sub", + "PRINT 10 - 0.5\n", + "9.5 \n"); + TEST_EQ("div-produces-double", + "PRINT 7 / 2\n", + "3.5 \n"); + + // --- ON ERROR with float division (the actual .frm demo scenario) --- + TEST_EQ("on-error-float-div", + "SUB doit\n" + " ON ERROR GOTO handler\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n" + " PRINT a / b\n" + " PRINT \"never\"\n" + " EXIT SUB\n" + " handler:\n" + " PRINT \"caught\"\n" + " PRINT ERR\n" + "END SUB\n" + "doit\n", + "caught\n11 \n"); + + // --- ON ERROR, error during arg evaluation of a nested SUB call --- + // Mirrors basdemo's btnError_Click: Say "..." + STR$(a / b) causes the + // divide-by-zero inside an expression that's about to be an argument + // to Say. The error handler for the outer SUB must still catch it. + TEST_EQ("on-error-in-expr-arg", + "SUB emit(s AS STRING)\n PRINT s\nEND SUB\n" + "SUB doit\n" + " ON ERROR GOTO handler\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n" + " emit \"10/\" + STR$(b) + \"=\" + STR$(a / b)\n" + " PRINT \"never\"\n" + " EXIT SUB\n" + " handler:\n" + " PRINT \"caught\"\n" + " PRINT ERR\n" + "END SUB\n" + "doit\n", + "caught\n11 \n"); + + // --- SUB is called from another SUB; error fires in the callee --- + // When one SUB calls another and the *callee* has no handler but the + // *caller* does, the caller's handler should still catch the error. + TEST_EQ("on-error-bubbles-up", + "SUB bomb\n DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n PRINT a / b\n" + "END SUB\n" + "SUB doit\n" + " ON ERROR GOTO handler\n" + " bomb\n" + " PRINT \"never\"\n" + " EXIT SUB\n" + " handler:\n" + " PRINT \"caught in doit\"\n" + " PRINT ERR\n" + "END SUB\n" + "doit\n", + "caught in doit\n11 \n"); + + // --- Handler calls other SUBs, then completes normally --- + // Mirrors basdemo's btnError_Click: the handler itself calls `Say` + // and sets properties before END SUB. After the handler ends, the + // outer SUB's caller (Run All chain) must continue normally. + TEST_EQ("on-error-handler-calls-sub", + "SUB emit(s AS STRING)\n PRINT s\nEND SUB\n" + "SUB doit\n" + " ON ERROR GOTO handler\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n" + " emit \"about to divide\"\n" + " emit STR$(a / b)\n" + " emit \"never\"\n" + " EXIT SUB\n" + " handler:\n" + " emit \"caught\"\n" + " emit STR$(ERR)\n" + "END SUB\n" + "doit\n" + "PRINT \"after-doit\"\n", + "about to divide\ncaught\n 11\nafter-doit\n"); + + // --- Run-All style chain: caller runs several SUBs in sequence, + // one of them has a handler that catches its own error, the chain + // must continue with the next SUB in the caller. --- + TEST_EQ("on-error-chain-continues", + "SUB stepOne\n PRINT \"one\"\nEND SUB\n" + "SUB stepBoom\n" + " ON ERROR GOTO h\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n" + " PRINT a \\ b\n" + " PRINT \"never\"\n" + " EXIT SUB\n" + " h:\n" + " PRINT \"boom-caught\"\n" + "END SUB\n" + "SUB stepTwo\n PRINT \"two\"\nEND SUB\n" + "SUB runAll\n" + " stepOne\n" + " stepBoom\n" + " stepTwo\n" + "END SUB\n" + "runAll\n", + "one\nboom-caught\ntwo\n"); + + // --- Multiple ON ERROR blocks in sequence in one SUB --- + TEST_EQ("on-error-reset", + "SUB doit\n" + " ON ERROR GOTO h1\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n" + " PRINT a \\ b\n" + " EXIT SUB\n" + " h1:\n" + " PRINT \"h1\"\n" + "END SUB\n" + "doit\n" + "PRINT \"done\"\n", + "h1\ndone\n"); + + // --- ON ERROR GOTO 0 clears the handler --- + TEST_RUNTIME_ERROR("on-error-goto-zero", + "ON ERROR GOTO handler\n" + "ON ERROR GOTO 0\n" + "DIM a AS INTEGER\nDIM b AS INTEGER\n" + "a = 10\nb = 0\n" + "PRINT a \\ b\n" + "END\n" + "handler:\n" + "PRINT \"not caught\"\n", + ""); + + // --- inErrorHandler must clear after handler completes via END SUB --- + // If we don't clear it, a *second* error in a later SUB wouldn't trap. + TEST_EQ("on-error-reusable", + "SUB doit\n" + " ON ERROR GOTO h\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n" + " PRINT a \\ b\n" + " EXIT SUB\n" + " h:\n" + " PRINT \"once\"\n" + "END SUB\n" + "doit\n" + "doit\n", + "once\nonce\n"); + + // --- String builtins --- + TEST_EQ("string-instr", "PRINT INSTR(\"abcdef\", \"cd\")\n", "3 \n"); + TEST_EQ("string-chr", "PRINT CHR$(65)\n", "A\n"); + TEST_EQ("string-asc", "PRINT ASC(\"A\")\n", "65 \n"); + TEST_EQ("string-space", "PRINT \"[\" + SPACE$(3) + \"]\"\n", "[ ]\n"); + TEST_EQ("string-string", "PRINT STRING$(4, \"*\")\n", "****\n"); + TEST_EQ("string-ltrim", "PRINT \"[\" + LTRIM$(\" hi\") + \"]\"\n", "[hi]\n"); + TEST_EQ("string-rtrim", "PRINT \"[\" + RTRIM$(\"hi \") + \"]\"\n", "[hi]\n"); + + // --- Logical operators --- + TEST_EQ("logical-and", + "IF 1 = 1 AND 2 = 2 THEN PRINT \"y\" ELSE PRINT \"n\"\n", + "y\n"); + TEST_EQ("logical-or", + "IF 1 = 0 OR 2 = 2 THEN PRINT \"y\" ELSE PRINT \"n\"\n", + "y\n"); + TEST_EQ("logical-not", + "IF NOT (1 = 2) THEN PRINT \"y\" ELSE PRINT \"n\"\n", + "y\n"); + + // --- Short-circuit only fires when needed (documented behavior) --- + TEST_EQ("and-short-circuit-full", + "DIM x AS INTEGER\nx = 0\n" + "IF 1 = 1 AND 2 = 2 THEN x = 5\n" + "PRINT x\n", + "5 \n"); + + // --- Nested FOR --- + TEST_EQ("nested-for", + "DIM i AS INTEGER\nDIM j AS INTEGER\n" + "FOR i = 1 TO 2\n" + " FOR j = 1 TO 2\n" + " PRINT i * 10 + j\n" + " NEXT j\n" + "NEXT i\n", + "11 \n12 \n21 \n22 \n"); + + // --- WHILE / WEND (older syntax) --- + TEST_EQ("while-wend", + "DIM i AS INTEGER\ni = 0\n" + "WHILE i < 3\n" + " PRINT i\n i = i + 1\n" + "WEND\n", + "0 \n1 \n2 \n"); + + // --- 2D array --- + TEST_EQ("array-2d", + "DIM a(2, 2) AS INTEGER\n" + "a(0, 0) = 1\na(1, 1) = 5\na(2, 2) = 9\n" + "PRINT a(0, 0) + a(1, 1) + a(2, 2)\n", + "15 \n"); + + // --- REDIM PRESERVE keeps old values --- + TEST_EQ("redim-preserve", + "DIM a(2) AS INTEGER\n" + "a(0) = 10\na(1) = 20\na(2) = 30\n" + "REDIM PRESERVE a(4)\n" + "a(3) = 40\na(4) = 50\n" + "PRINT a(0) + a(2) + a(4)\n", + "90 \n"); + + // --- Nested UDT --- + TEST_EQ("udt-nested", + "TYPE Inner\n x AS INTEGER\nEND TYPE\n" + "TYPE Outer\n lbl AS STRING\n pt AS Inner\nEND TYPE\n" + "DIM o AS Outer\n" + "o.lbl = \"hi\"\n" + "o.pt.x = 42\n" + "PRINT o.lbl\nPRINT o.pt.x\n", + "hi\n42 \n"); + + // --- Multi-value PRINT with comma/semicolon --- + TEST_EQ("print-semi", + "PRINT \"a\"; \"b\"; \"c\"\n", + "abc\n"); + + // --- EXIT SUB --- + TEST_EQ("exit-sub-early", + "SUB foo(n AS INTEGER)\n" + " IF n < 0 THEN EXIT SUB\n" + " PRINT n\n" + "END SUB\n" + "foo 5\nfoo -1\nfoo 9\n", + "5 \n9 \n"); + + // --- EXIT FUNCTION --- + TEST_EQ("exit-function-early", + "FUNCTION absval(n AS INTEGER) AS INTEGER\n" + " IF n < 0 THEN\n absval = -n\n EXIT FUNCTION\n END IF\n" + " absval = n\n" + "END FUNCTION\n" + "PRINT absval(-7)\nPRINT absval(3)\n", + "7 \n3 \n"); + + // --- Module globals initialize --- + TEST_EQ("global-init-order", + "DIM a AS INTEGER\nDIM b AS INTEGER\n" + "a = 10\nb = a * 2\n" + "PRINT b\n", + "20 \n"); + + // --- STEP 0 or near-zero should not infinite-loop (if STEP > 0, needs > 0) --- + TEST_EQ("for-empty-range", + "DIM i AS INTEGER\n" + "FOR i = 10 TO 5\n PRINT i\nNEXT i\n" + "PRINT \"done\"\n", + "done\n"); + + // --- CONST values --- + TEST_EQ("const", + "CONST PI = 3\n" + "PRINT PI * 2\n", + "6 \n"); + + // --- Nested SUB calls preserve locals --- + TEST_EQ("nested-sub-locals", + "SUB inner(x AS INTEGER)\n PRINT x * 10\nEND SUB\n" + "SUB outer(y AS INTEGER)\n" + " inner y\n" + " PRINT y\n" + "END SUB\n" + "outer 7\n", + "70 \n7 \n"); + + // --- Recursive sum --- + TEST_EQ("recursion-sum", + "FUNCTION sumto(n AS INTEGER) AS LONG\n" + " IF n <= 0 THEN\n sumto = 0\n ELSE\n sumto = n + sumto(n - 1)\n END IF\n" + "END FUNCTION\n" + "PRINT sumto(10)\n", + "55 \n"); + + // --- Early EXIT DO --- + TEST_EQ("exit-do", + "DIM i AS INTEGER\ni = 0\n" + "DO\n" + " IF i = 3 THEN EXIT DO\n" + " PRINT i\n i = i + 1\n" + "LOOP\n", + "0 \n1 \n2 \n"); + + // --- String concatenation with numbers via STR$ --- + TEST_EQ("string-concat-num", + "DIM n AS INTEGER\n" + "n = 42\n" + "PRINT \"n=\" + STR$(n)\n", + "n= 42\n"); + + // --- VAL of empty string returns 0 --- + TEST_EQ("val-empty", + "PRINT VAL(\"\")\n", + "0 \n"); + + // --- VAL parses up to first non-numeric --- + TEST_EQ("val-parse-prefix", + "PRINT VAL(\"42abc\")\n", + "42 \n"); + + // --- LEFT$/RIGHT$/MID$ with length past end --- + TEST_EQ("string-bounds-safe", + "PRINT LEFT$(\"hi\", 10)\n" + "PRINT RIGHT$(\"hi\", 10)\n" + "PRINT MID$(\"hi\", 1, 10)\n", + "hi\nhi\nhi\n"); + + // --- INSTR with no match returns 0 --- + TEST_EQ("instr-nomatch", + "PRINT INSTR(\"abc\", \"z\")\n", + "0 \n"); + + // --- IF with ELSEIF chain --- + TEST_EQ("elseif-chain", + "DIM n AS INTEGER\nn = 2\n" + "IF n = 1 THEN\n PRINT \"one\"\n" + "ELSEIF n = 2 THEN\n PRINT \"two\"\n" + "ELSEIF n = 3 THEN\n PRINT \"three\"\n" + "ELSE\n PRINT \"other\"\nEND IF\n", + "two\n"); + + // --- FOR with STEP > range size skips body --- + TEST_EQ("for-step-over", + "DIM i AS INTEGER\n" + "FOR i = 1 TO 5 STEP 10\n PRINT i\nNEXT i\n", + "1 \n"); + + // --- SELECT CASE with range --- + TEST_EQ("select-case-range", + "DIM n AS INTEGER\nn = 5\n" + "SELECT CASE n\n" + " CASE 1 TO 3: PRINT \"low\"\n" + " CASE 4 TO 6: PRINT \"mid\"\n" + " CASE ELSE: PRINT \"high\"\n" + "END SELECT\n", + "mid\n"); + + // --- SELECT CASE with IS comparator --- + TEST_EQ("select-case-is", + "DIM n AS INTEGER\nn = 100\n" + "SELECT CASE n\n" + " CASE IS < 10: PRINT \"small\"\n" + " CASE IS >= 10: PRINT \"big\"\n" + "END SELECT\n", + "big\n"); + + // --- SUB forward reference backpatched --- + TEST_EQ("forward-sub-ref", + "later\n" + "SUB later\n PRINT \"ok\"\nEND SUB\n", + "ok\n"); + + // --- FUNCTION without explicit return returns default value --- + TEST_EQ("function-default-return", + "FUNCTION zero AS INTEGER\nEND FUNCTION\n" + "PRINT zero\n", + "0 \n"); + + // --- STATIC counter across SUB invocations with params --- + TEST_EQ("static-with-params", + "SUB inc(n AS INTEGER)\n" + " STATIC total AS INTEGER\n" + " total = total + n\n" + " PRINT total\n" + "END SUB\n" + "inc 5\ninc 10\ninc 15\n", + "5 \n15 \n30 \n"); + + // --- IIF equivalent via IF/THEN/ELSE expression on same line --- + TEST_EQ("inline-if-expr", + "DIM n AS INTEGER\nn = 7\n" + "IF n > 5 THEN PRINT \"big\" ELSE PRINT \"small\"\n", + "big\n"); + + // --- CONST string --- + TEST_EQ("const-string", + "CONST GREETING = \"hello\"\n" + "PRINT GREETING\n", + "hello\n"); + + // --- RND and RANDOMIZE don't crash --- + TEST_EQ("rnd-fires", + "RANDOMIZE 42\n" + "DIM n AS DOUBLE\n" + "n = RND\n" + "IF n >= 0 AND n < 1 THEN PRINT \"ok\"\n", + "ok\n"); + + // --- TIMER returns a number (just verify it runs) --- + TEST_EQ("timer-fires", + "DIM t AS DOUBLE\nt = TIMER\n" + "IF t >= 0 THEN PRINT \"ok\"\n", + "ok\n"); + + // --- Integer overflow promotes to LONG --- + TEST_EQ("int-to-long-promotion", + "DIM a AS INTEGER\nDIM b AS INTEGER\n" + "a = 20000\nb = 20000\n" + "PRINT a + b\n", + "40000 \n"); + + // --- String compare case sensitivity --- + TEST_EQ("string-case-compare", + "IF \"Abc\" = \"abc\" THEN PRINT \"eq\" ELSE PRINT \"ne\"\n", + "ne\n"); + + // --- OPTION COMPARE TEXT if supported; otherwise skip --- + // Omitted: checking support first would just cause a compile failure. + + // --- Unary NOT on ints (bitwise) --- + TEST_EQ("bitwise-not", + "PRINT NOT 0\n", + "-1 \n"); + + // --- AND/OR as bitwise on ints --- + TEST_EQ("bitwise-and", + "PRINT 12 AND 10\n", + "8 \n"); + TEST_EQ("bitwise-or", + "PRINT 12 OR 10\n", + "14 \n"); + + // --- STR$ with negative --- + TEST_EQ("str-negative", + "PRINT \"[\" + STR$(-5) + \"]\"\n", + "[-5]\n"); + + // --- INT vs FIX on negative --- + TEST_EQ("int-negative", + "PRINT INT(-1.5)\n", + "-2 \n"); + TEST_EQ("fix-negative", + "PRINT FIX(-1.5)\n", + "-1 \n"); + + // --- MOD on negatives --- + TEST_EQ("mod-negative", + "PRINT -7 MOD 3\n", + "-1 \n"); + + // --- SQR --- + TEST_EQ("sqr", + "PRINT SQR(16)\n", + "4 \n"); + + // --- LEN of empty --- + TEST_EQ("len-empty", + "PRINT LEN(\"\")\n", + "0 \n"); + + // --- Array of UDTs --- + TEST_EQ("array-of-udt", + "TYPE P\n x AS INTEGER\n y AS INTEGER\nEND TYPE\n" + "DIM arr(1) AS P\n" + "arr(0).x = 1\narr(0).y = 2\n" + "arr(1).x = 10\narr(1).y = 20\n" + "PRINT arr(0).x + arr(1).y\n", + "21 \n"); + + // --- Empty FOR range, explicit STEP 1 --- + TEST_EQ("for-step1-empty", + "DIM i AS INTEGER\n" + "FOR i = 5 TO 1 STEP 1\n PRINT i\nNEXT i\n" + "PRINT \"done\"\n", + "done\n"); + + // --- Negative-step range where start = end --- + TEST_EQ("for-single-iter", + "DIM i AS INTEGER\n" + "FOR i = 3 TO 3 STEP -1\n PRINT i\nNEXT i\n", + "3 \n"); + + // ============================================================ + // Event-handler dispatch path (runSubLoop, not basVmRun) + // ============================================================ + // fireCtrlEvent calls basVmCallSub for each event handler. That + // routes through runSubLoop which has its own error-dispatch loop. + // The tests above exercise basVmRun only; these exercise the SUB + // call path that real event handlers use. + + TEST_SUB_EQ("sub-call-basic", + "SUB handler\n PRINT \"hi\"\nEND SUB\n", + "handler", + "hi\n"); + + TEST_SUB_EQ("sub-call-on-error-div", + "SUB handler\n" + " ON ERROR GOTO h\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n" + " PRINT a \\ b\n" + " PRINT \"never\"\n" + " EXIT SUB\n" + " h:\n" + " PRINT \"caught\"\n" + " PRINT ERR\n" + "END SUB\n", + "handler", + "caught\n11 \n"); + + TEST_SUB_EQ("sub-call-on-error-float-div", + "SUB handler\n" + " ON ERROR GOTO h\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n" + " PRINT a / b\n" + " PRINT \"never\"\n" + " EXIT SUB\n" + " h:\n" + " PRINT \"caught\"\n" + " PRINT ERR\n" + "END SUB\n", + "handler", + "caught\n11 \n"); + + // Regression: basdemo's Run All calls btnError_Click via OP_CALL. + // The error dispatcher must work when the SUB-under-dispatch runs + // nested calls before hitting the error. + TEST_SUB_EQ("sub-call-on-error-after-nested-calls", + "SUB greet(s AS STRING)\n PRINT s\nEND SUB\n" + "SUB handler\n" + " greet \"before\"\n" + " ON ERROR GOTO h\n" + " greet \"armed\"\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n" + " greet STR$(a / b)\n" + " greet \"never\"\n" + " EXIT SUB\n" + " h:\n" + " greet \"caught\"\n" + "END SUB\n", + "handler", + "before\narmed\ncaught\n"); + + // Regression: Run All dispatches many SUBs in sequence via OP_CALL. + // One of them has a handler that catches its own error; the chain + // must continue with the next SUB AND a later SUB's error must + // still be trappable (inErrorHandler must reset). + TEST_SUB_EQ("sub-call-run-all-chain", + "SUB stepOne\n PRINT \"1\"\nEND SUB\n" + "SUB stepBoom\n" + " ON ERROR GOTO h\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 10\n b = 0\n" + " PRINT a \\ b\n" + " EXIT SUB\n" + " h:\n" + " PRINT \"b-caught\"\n" + "END SUB\n" + "SUB stepTwo\n PRINT \"2\"\nEND SUB\n" + "SUB stepBoom2\n" + " ON ERROR GOTO h2\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 20\n b = 0\n" + " PRINT a \\ b\n" + " EXIT SUB\n" + " h2:\n" + " PRINT \"b2-caught\"\n" + "END SUB\n" + "SUB runAll\n" + " stepOne\n" + " stepBoom\n" + " stepTwo\n" + " stepBoom2\n" + " PRINT \"done\"\n" + "END SUB\n", + "runAll", + "1\nb-caught\n2\nb2-caught\ndone\n"); + + // ============================================================ + // Numeric limits and overflow behavior + // ============================================================ + + TEST_EQ("int-max", "PRINT 32767\n", "32767 \n"); + TEST_EQ("int-min", "PRINT -32768\n", "-32768 \n"); + TEST_EQ("int-overflow-promotes", + "DIM a AS INTEGER\nDIM b AS INTEGER\n" + "a = 32000\nb = 32000\n" + "PRINT a + b\n", + "64000 \n"); + TEST_EQ("long-max", "PRINT 2147483647\n", "2147483647 \n"); + TEST_EQ("long-min", "PRINT -2147483648\n", "-2147483648 \n"); + + // ============================================================ + // Float formatting and precision + // ============================================================ + + TEST_EQ("float-zero", "PRINT 0.0\n", "0 \n"); + TEST_EQ("float-one", "PRINT 1.0\n", "1 \n"); + TEST_EQ("float-tiny", "PRINT 0.001\n", "0.001 \n"); + TEST_EQ("float-neg", "PRINT -3.14\n", "-3.14 \n"); + // DVX uses %g formatting (~6 digits of precision) for doubles. + TEST_EQ("float-via-div", "PRINT 22 / 7\n", "3.14286 \n"); + TEST_EQ("float-large", "PRINT 1000000.5\n", "1e+06 \n"); + + // ============================================================ + // Integer division and MOD edge cases + // ============================================================ + + TEST_EQ("idiv-negative-positive", "PRINT -7 \\ 2\n", "-3 \n"); + TEST_EQ("idiv-positive-negative", "PRINT 7 \\ -2\n", "-3 \n"); + TEST_EQ("mod-positive", "PRINT 7 MOD 3\n", "1 \n"); + TEST_EQ("mod-zero-dividend", "PRINT 0 MOD 3\n", "0 \n"); + + TEST_RUNTIME_ERROR("idiv-zero", "PRINT 5 \\ 0\n", ""); + TEST_RUNTIME_ERROR("fdiv-zero", "PRINT 5 / 0\n", ""); + TEST_RUNTIME_ERROR("mod-zero", "PRINT 5 MOD 0\n", ""); + + // ============================================================ + // Comparison operators + // ============================================================ + + // DVX BASIC prints boolean-typed values as "True"/"False" rather + // than -1/0. (Same value, different PRINT formatting.) + TEST_EQ("cmp-eq-true", "PRINT (5 = 5)\n", "True \n"); + TEST_EQ("cmp-eq-false", "PRINT (5 = 6)\n", "False \n"); + TEST_EQ("cmp-ne", "PRINT (5 <> 6)\n", "True \n"); + TEST_EQ("cmp-lt", "PRINT (3 < 5)\n", "True \n"); + TEST_EQ("cmp-le", "PRINT (5 <= 5)\n", "True \n"); + TEST_EQ("cmp-gt", "PRINT (7 > 5)\n", "True \n"); + TEST_EQ("cmp-ge", "PRINT (5 >= 5)\n", "True \n"); + TEST_EQ("cmp-mixed-types", "PRINT (5 = 5.0)\n", "True \n"); + + // ============================================================ + // Boolean operators + // ============================================================ + + TEST_EQ("bool-true-const", "PRINT TRUE\n", "True \n"); + TEST_EQ("bool-false-const", "PRINT FALSE\n", "False \n"); + TEST_EQ("xor", "PRINT 12 XOR 10\n", "6 \n"); + TEST_EQ("imp", "PRINT -1 IMP 0\n", "0 \n"); + TEST_EQ("eqv", "PRINT -1 EQV -1\n", "-1 \n"); + + // ============================================================ + // String concatenation operators + // ============================================================ + + TEST_EQ("string-amp-concat", "PRINT \"a\" & \"b\"\n", "ab\n"); + // Unlike STR$, the & operator concatenates the digits directly with + // no leading space. + TEST_EQ("string-amp-number", "PRINT \"num=\" & 42\n", "num=42\n"); + TEST_EQ("string-long-concat", + "DIM s AS STRING\ns = \"\"\n" + "DIM i AS INTEGER\n" + "FOR i = 1 TO 5\n s = s + \"x\"\nNEXT i\n" + "PRINT s\n" + "PRINT LEN(s)\n", + "xxxxx\n5 \n"); + + // ============================================================ + // Fixed-length strings + // ============================================================ + + TEST_EQ("fixed-length-string", + "DIM s AS STRING * 5\n" + "s = \"hi\"\n" + "PRINT \"[\" + s + \"]\"\n" + "PRINT LEN(s)\n", + "[hi ]\n5 \n"); + + TEST_EQ("fixed-length-truncate", + "DIM s AS STRING * 3\n" + "s = \"hello\"\n" + "PRINT s\n", + "hel\n"); + + // ============================================================ + // String search / manipulation + // ============================================================ + + TEST_EQ("instr-start-pos", + "PRINT INSTR(3, \"abcabc\", \"a\")\n", + "4 \n"); + TEST_EQ("mid-replace-assign", + "DIM s AS STRING\ns = \"abcdef\"\n" + "MID$(s, 2, 3) = \"XYZ\"\n" + "PRINT s\n", + "aXYZef\n"); + TEST_EQ("str-leading-space-positive", + "PRINT \"[\" + STR$(5) + \"]\"\n", + "[ 5]\n"); + TEST_EQ("str-no-leading-neg", + "PRINT \"[\" + STR$(-5) + \"]\"\n", + "[-5]\n"); + + // ============================================================ + // Type conversion functions + // ============================================================ + + // DVX rounds half-away-from-zero (not VB banker's rounding). + TEST_EQ("cint-round-half-up", "PRINT CINT(2.5)\n", "3 \n"); + TEST_EQ("cint-round-half-up2","PRINT CINT(3.5)\n", "4 \n"); + TEST_EQ("cint-truncate", "PRINT CINT(2.49)\n", "2 \n"); + TEST_EQ("cint-negative", "PRINT CINT(-2.5)\n", "-3 \n"); + TEST_EQ("clng-from-double", "PRINT CLNG(3.9)\n", "4 \n"); + TEST_EQ("csng", "PRINT CSNG(1.5)\n", "1.5 \n"); + TEST_EQ("cstr-int", "PRINT CSTR(42)\n", "42\n"); + TEST_EQ("cdbl-of-int", "PRINT CDBL(10) / 4\n", "2.5 \n"); + TEST_EQ("cbool-nonzero", "PRINT CBOOL(5)\n", "True \n"); + TEST_EQ("cbool-zero", "PRINT CBOOL(0)\n", "False \n"); + TEST_EQ("cbool-neg", "PRINT CBOOL(-42)\n", "True \n"); + TEST_EQ("cbool-empty-string", "PRINT CBOOL(\"\")\n", "False \n"); + TEST_EQ("cbool-nonempty-str", "PRINT CBOOL(\"hi\")\n", "True \n"); + + TEST_EQ("hex-positive", "PRINT HEX$(255)\n", "FF\n"); + TEST_EQ("hex-zero", "PRINT HEX$(0)\n", "0\n"); + TEST_EQ("oct-eight", "PRINT OCT$(8)\n", "10\n"); + TEST_EQ("oct-zero", "PRINT OCT$(0)\n", "0\n"); + TEST_EQ("oct-sixtythree", "PRINT OCT$(63)\n", "77\n"); + + // ============================================================ + // Math functions + // ============================================================ + + TEST_EQ("math-sqr-zero", "PRINT SQR(0)\n", "0 \n"); + TEST_EQ("math-sqr-float", "PRINT SQR(2)\n", "1.41421 \n"); + TEST_EQ("math-cos-zero", "PRINT COS(0)\n", "1 \n"); + TEST_EQ("math-sin-zero", "PRINT SIN(0)\n", "0 \n"); + TEST_EQ("math-exp-zero", "PRINT EXP(0)\n", "1 \n"); + TEST_EQ("math-log-one", "PRINT LOG(1)\n", "0 \n"); + TEST_EQ("math-abs-double", "PRINT ABS(-3.14)\n", "3.14 \n"); + + // ============================================================ + // Variable types and default initialization + // ============================================================ + + TEST_EQ("default-int-zero", + "DIM n AS INTEGER\nPRINT n\n", + "0 \n"); + TEST_EQ("default-long-zero", + "DIM n AS LONG\nPRINT n\n", + "0 \n"); + TEST_EQ("default-double-zero", + "DIM n AS DOUBLE\nPRINT n\n", + "0 \n"); + // Regression: DIM AS STRING now initializes the slot to an empty + // string rather than leaving it as integer 0. Previously + // "[" + s + "]" on an uninitialized STRING stringified as "0" + // because the slot type was INTEGER. + TEST_EQ("default-string-is-empty", + "DIM s AS STRING\nPRINT \"[\" + s + \"]\"\n", + "[]\n"); + TEST_EQ("default-string-concat-works", + "DIM s AS STRING\ns = s + \"x\"\ns = s + \"y\"\nPRINT s\n", + "xy\n"); + + TEST_EQ("dollar-suffix", + "DIM s$\ns$ = \"hi\"\nPRINT s$\n", + "hi\n"); + TEST_EQ("percent-suffix", + "DIM n%\nn% = 42\nPRINT n%\n", + "42 \n"); + TEST_EQ("amp-suffix", + "DIM n&\nn& = 100000\nPRINT n&\n", + "100000 \n"); + TEST_EQ("bang-suffix", + "DIM f!\nf! = 1.5\nPRINT f!\n", + "1.5 \n"); + TEST_EQ("hash-suffix", + "DIM d#\nd# = 3.14159\nPRINT d#\n", + "3.14159 \n"); + + // ============================================================ + // FOR loop variants + // ============================================================ + + TEST_EQ("for-next-var-name", + "DIM i AS INTEGER\n" + "FOR i = 1 TO 3\n PRINT i\nNEXT i\n", + "1 \n2 \n3 \n"); + TEST_EQ("for-next-no-varname", + "DIM i AS INTEGER\n" + "FOR i = 1 TO 3\n PRINT i\nNEXT\n", + "1 \n2 \n3 \n"); + TEST_EQ("for-double-varies", + "DIM a AS DOUBLE\n" + "FOR a = 1 TO 3 STEP 0.5\n PRINT a\nNEXT a\n", + "1 \n1.5 \n2 \n2.5 \n3 \n"); + TEST_EQ("for-neg-step-range", + "DIM i AS INTEGER\n" + "FOR i = 10 TO 4 STEP -2\n PRINT i\nNEXT i\n", + "10 \n8 \n6 \n4 \n"); + TEST_EQ("for-nested-exit-inner", + "DIM i AS INTEGER\nDIM 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*10+j\n" + " NEXT j\n" + "NEXT i\n", + "11 \n21 \n31 \n"); + + // ============================================================ + // WHILE / DO variants + // ============================================================ + + TEST_EQ("do-loop-while-post", + "DIM i AS INTEGER\ni = 10\n" + "DO\n PRINT i\n i = i - 1\nLOOP WHILE i > 8\n", + "10 \n9 \n"); + TEST_EQ("do-loop-until-post", + "DIM i AS INTEGER\ni = 0\n" + "DO\n i = i + 1\n PRINT i\nLOOP UNTIL i = 3\n", + "1 \n2 \n3 \n"); + TEST_EQ("do-while-never-enters", + "DIM i AS INTEGER\ni = 5\n" + "DO WHILE i = 0\n PRINT \"inside\"\nLOOP\n" + "PRINT \"after\"\n", + "after\n"); + + // ============================================================ + // String scanning / conversion round-trip + // ============================================================ + + TEST_EQ("val-negative", "PRINT VAL(\"-42\")\n", "-42 \n"); + TEST_EQ("val-float", "PRINT VAL(\"3.14\")\n", "3.14 \n"); + TEST_EQ("val-whitespace", "PRINT VAL(\" 123 \")\n", "123 \n"); + TEST_EQ("val-only-letters", "PRINT VAL(\"abc\")\n", "0 \n"); + TEST_EQ("str-then-val", + "DIM n AS INTEGER\nn = 42\n" + "PRINT VAL(STR$(n))\n", + "42 \n"); + + // ============================================================ + // SUB / FUNCTION edge cases + // ============================================================ + + TEST_EQ("sub-zero-params", + "SUB greet\n PRINT \"hi\"\nEND SUB\n" + "greet\n", + "hi\n"); + TEST_EQ("function-returning-string", + "FUNCTION greeting() AS STRING\n greeting = \"hi\"\nEND FUNCTION\n" + "PRINT greeting()\n", + "hi\n"); + TEST_EQ("function-multi-arg", + "FUNCTION sum3(a AS INTEGER, b AS INTEGER, c AS INTEGER) AS INTEGER\n" + " sum3 = a + b + c\nEND FUNCTION\n" + "PRINT sum3(1, 2, 3)\n", + "6 \n"); + // OPTIONAL parameters: use `nm` as the param name; `name` is + // a reserved keyword for the NAME statement in DVX BASIC. + TEST_EQ("optional-param-omitted", + "SUB greet(nm AS STRING, OPTIONAL count AS INTEGER)\n" + " DIM i AS INTEGER\n" + " IF count = 0 THEN count = 1\n" + " FOR i = 1 TO count\n PRINT nm\n NEXT i\n" + "END SUB\n" + "greet \"hi\", 2\n" + "greet \"solo\"\n", + "hi\nhi\nsolo\n"); + + // Mutual recursion: needs DECLARE forward for the later function. + TEST_EQ("mutual-recursion", + "DECLARE FUNCTION isOdd(n AS INTEGER) AS INTEGER\n" + "FUNCTION isEven(n AS INTEGER) AS INTEGER\n" + " IF n = 0 THEN\n isEven = 1\n ELSE\n isEven = isOdd(n - 1)\n END IF\n" + "END FUNCTION\n" + "FUNCTION isOdd(n AS INTEGER) AS INTEGER\n" + " IF n = 0 THEN\n isOdd = 0\n ELSE\n isOdd = isEven(n - 1)\n END IF\n" + "END FUNCTION\n" + "PRINT isEven(10)\nPRINT isOdd(10)\n", + "1 \n0 \n"); + + TEST_EQ("byval-array-element", + "SUB bump(BYVAL x AS INTEGER)\n x = x + 1\nEND SUB\n" + "DIM a(2) AS INTEGER\n" + "a(0) = 5\na(1) = 10\n" + "bump a(0)\n" + "PRINT a(0)\nPRINT a(1)\n", + "5 \n10 \n"); + + TEST_EQ("byref-array-element", + "SUB bump(x AS INTEGER)\n x = x + 1\nEND SUB\n" + "DIM a(2) AS INTEGER\n" + "a(0) = 5\na(1) = 10\n" + "bump a(0)\n" + "bump a(1)\nbump a(1)\n" + "PRINT a(0)\nPRINT a(1)\n", + "6 \n12 \n"); + + TEST_EQ("byref-array-element-2d", + "SUB bump(x AS INTEGER)\n x = x + 100\nEND SUB\n" + "DIM a(1, 1) AS INTEGER\n" + "a(0, 0) = 1\na(1, 1) = 2\n" + "bump a(0, 0)\n" + "bump a(1, 1)\n" + "PRINT a(0, 0)\nPRINT a(1, 1)\n", + "101 \n102 \n"); + + // ============================================================ + // Array operations + // ============================================================ + + TEST_EQ("array-negative-lbound", + "DIM a(-2 TO 2) AS INTEGER\n" + "a(-2) = 10\na(0) = 50\na(2) = 90\n" + "PRINT a(-2) + a(0) + a(2)\n" + "PRINT LBOUND(a)\nPRINT UBOUND(a)\n", + "150 \n-2 \n2 \n"); + + TEST_EQ("redim-clears", + "DIM a(3) AS INTEGER\n" + "a(0) = 99\n" + "REDIM a(5)\n" + "PRINT a(0)\n", + "0 \n"); + + // ERASE behavior in DVX: after ERASE a, accessing a(0) errors + // ("Not an array"). Verify that behavior. + TEST_RUNTIME_ERROR("erase-makes-not-array", + "DIM a(3) AS INTEGER\n" + "a(0) = 7\n" + "ERASE a\n" + "PRINT a(0)\n", + ""); + + TEST_EQ("array-3d", + "DIM a(1, 1, 1) AS INTEGER\n" + "a(0, 0, 0) = 1\n" + "a(1, 1, 1) = 8\n" + "PRINT a(0, 0, 0) + a(1, 1, 1)\n", + "9 \n"); + + TEST_EQ("array-string", + "DIM names(2) AS STRING\n" + "names(0) = \"alpha\"\nnames(1) = \"beta\"\nnames(2) = \"gamma\"\n" + "PRINT names(0) + \"-\" + names(2)\n", + "alpha-gamma\n"); + + // ============================================================ + // UDTs + // ============================================================ + + TEST_EQ("udt-used-locally", + "TYPE P\n x AS INTEGER\n y AS INTEGER\nEND TYPE\n" + "DIM a AS P\na.x = 3\na.y = 4\n" + "PRINT a.x\nPRINT a.y\n", + "3 \n4 \n"); + + TEST_EQ("udt-byref-param", + "TYPE P\n x AS INTEGER\n y AS INTEGER\nEND TYPE\n" + "SUB move(pt AS P, dx AS INTEGER, dy AS INTEGER)\n" + " pt.x = pt.x + dx\n pt.y = pt.y + dy\n" + "END SUB\n" + "DIM a AS P\na.x = 1\na.y = 2\n" + "move a, 10, 20\n" + "PRINT a.x\nPRINT a.y\n", + "11 \n22 \n"); + + TEST_EQ("udt-function-arg-read", + "TYPE Box\n w AS INTEGER\n h AS INTEGER\nEND TYPE\n" + "FUNCTION area(b AS Box) AS LONG\n" + " area = b.w * b.h\n" + "END FUNCTION\n" + "DIM r AS Box\nr.w = 6\nr.h = 7\n" + "PRINT area(r)\n", + "42 \n"); + + // ============================================================ + // DATA / READ / RESTORE + // ============================================================ + + TEST_EQ("data-read-strings", + "DIM s AS STRING\n" + "READ s\nPRINT s\n" + "READ s\nPRINT s\n" + "DATA hello, world\n", + "hello\nworld\n"); + + TEST_EQ("data-read-mixed", + "DIM n AS INTEGER\nDIM s AS STRING\n" + "READ n\nREAD s\nREAD n\n" + "PRINT s\nPRINT n\n" + "DATA 10, hello, 20\n", + "hello\n20 \n"); + + TEST_EQ("restore-rewinds", + "DIM n AS INTEGER\n" + "READ n\nPRINT n\n" + "RESTORE\nREAD n\nPRINT n\n" + "DATA 5, 6, 7\n", + "5 \n5 \n"); + + // ============================================================ + // ON ERROR additional scenarios + // ============================================================ + + TEST_EQ("err-clears-after-handler", + "SUB foo\n" + " ON ERROR GOTO h\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 5\n b = 0\n" + " PRINT a \\ b\n" + " EXIT SUB\n" + " h:\n" + " PRINT \"caught\"\n" + "END SUB\n" + "foo\n" + "PRINT \"err=\"; ERR\n", + "caught\nerr=0 \n"); + + // ============================================================ + // Labels and GOTO + // ============================================================ + + TEST_EQ("goto-forward", + "GOTO done\n" + "PRINT \"skipped\"\n" + "done:\n" + "PRINT \"here\"\n", + "here\n"); + + TEST_EQ("goto-backward-countdown", + "DIM i AS INTEGER\ni = 3\n" + "top:\n" + "PRINT i\ni = i - 1\n" + "IF i > 0 THEN GOTO top\n", + "3 \n2 \n1 \n"); + + // ============================================================ + // SELECT CASE edge cases + // ============================================================ + + TEST_EQ("select-case-multi-val", + "DIM n AS INTEGER\nn = 3\n" + "SELECT CASE n\n" + " CASE 1, 2, 3: PRINT \"small\"\n" + " CASE 4, 5, 6: PRINT \"mid\"\n" + " CASE ELSE: PRINT \"big\"\n" + "END SELECT\n", + "small\n"); + + TEST_EQ("select-case-string", + "DIM s AS STRING\ns = \"b\"\n" + "SELECT CASE s\n" + " CASE \"a\": PRINT \"A\"\n" + " CASE \"b\": PRINT \"B\"\n" + " CASE ELSE: PRINT \"?\"\n" + "END SELECT\n", + "B\n"); + + TEST_EQ("select-case-fallthrough-none", + // SELECT CASE does NOT fall through -- it matches first, exits. + "DIM n AS INTEGER\nn = 5\n" + "SELECT CASE n\n" + " CASE 1 TO 10: PRINT \"range\"\n" + " CASE IS > 0: PRINT \"pos\"\n" + "END SELECT\n", + "range\n"); + + // ============================================================ + // STATIC: value persists across calls + // ============================================================ + + TEST_EQ("static-double", + "SUB tick\n" + " STATIC n AS DOUBLE\n" + " n = n + 0.5\n" + " PRINT n\n" + "END SUB\n" + "tick\ntick\ntick\n", + "0.5 \n1 \n1.5 \n"); + + TEST_EQ("static-string", + "SUB appendHistory\n" + " STATIC hist AS STRING\n" + " hist = hist + \"x\"\n" + " PRINT hist\n" + "END SUB\n" + "appendHistory\nappendHistory\nappendHistory\n", + "x\nxx\nxxx\n"); + + // ============================================================ + // String array element manipulation + // ============================================================ + + TEST_EQ("string-array-accumulate", + "DIM parts(2) AS STRING\n" + "parts(0) = \"A\"\nparts(1) = \"B\"\nparts(2) = \"C\"\n" + "DIM result AS STRING\nresult = \"\"\n" + "DIM i AS INTEGER\n" + "FOR i = 0 TO 2\n result = result + parts(i)\nNEXT i\n" + "PRINT result\n", + "ABC\n"); + + // ============================================================ + // PRINT formatting + // ============================================================ + + TEST_EQ("print-comma-tab", + "PRINT \"a\", \"b\"\n", + "a\tb\n"); + TEST_EQ("print-trailing-semi", + "PRINT \"no-newline\";\nPRINT \"follow\"\n", + "no-newlinefollow\n"); + TEST_EQ("print-multi-numbers", + "PRINT 1; 2; 3\n", + "1 2 3 \n"); + + // ============================================================ + // Edge cases in ON ERROR + // ============================================================ + + TEST_EQ("error-in-handler-still-fires", + // A second error inside the handler with no new ON ERROR should + // NOT loop back to the same handler. QBASIC: second error is + // fatal while inErrorHandler=true. + "ON ERROR GOTO h\n" + "DIM a AS INTEGER\nDIM b AS INTEGER\n" + "a = 5\nb = 0\n" + "PRINT a \\ b\n" + "END\n" + "h:\n" + "PRINT \"handler\"\n" + "PRINT ERR\n", + "handler\n11 \n"); + + // ============================================================ + // GOSUB / RETURN + // ============================================================ + + TEST_EQ("gosub-multiple", + "GOSUB one\nGOSUB two\nEND\n" + "one:\nPRINT \"1\"\nRETURN\n" + "two:\nPRINT \"2\"\nRETURN\n", + "1\n2\n"); + + TEST_EQ("gosub-nested", + "GOSUB outer\nEND\n" + "outer:\nPRINT \"outer-in\"\nGOSUB inner\nPRINT \"outer-out\"\nRETURN\n" + "inner:\nPRINT \"inner\"\nRETURN\n", + "outer-in\ninner\nouter-out\n"); + + // ============================================================ + // Complex expressions + // ============================================================ + + TEST_EQ("expr-chained-string-concat", + "DIM a AS STRING\nDIM b AS STRING\nDIM c AS STRING\n" + "a = \"foo\"\nb = \"bar\"\nc = \"baz\"\n" + "PRINT a + b + c + \"-end\"\n", + "foobarbaz-end\n"); + + TEST_EQ("expr-paren-nested", + "PRINT ((1 + 2) * 3) - (4 \\ 2)\n", + "7 \n"); + + TEST_EQ("expr-short-circuit-and", + // OR should evaluate both operands in classic BASIC (no + // short-circuit). Just verify the result is correct. + "DIM a AS INTEGER\nDIM b AS INTEGER\n" + "a = 0\nb = 5\n" + "IF a = 0 OR b = 5 THEN PRINT \"ok\"\n", + "ok\n"); + + // ============================================================ + // IF/END IF variants + // ============================================================ + + TEST_EQ("if-else-block", + "DIM n AS INTEGER\nn = 1\n" + "IF n > 0 THEN\n PRINT \"pos\"\nELSE\n PRINT \"neg\"\nEND IF\n", + "pos\n"); + + TEST_EQ("if-elseif-block", + "DIM n AS INTEGER\nn = 15\n" + "IF n < 10 THEN\n PRINT \"<10\"\n" + "ELSEIF n < 20 THEN\n PRINT \"<20\"\n" + "ELSEIF n < 30 THEN\n PRINT \"<30\"\n" + "ELSE\n PRINT \">=30\"\nEND IF\n", + "<20\n"); + + TEST_EQ("if-nested", + "DIM a AS INTEGER\nDIM b AS INTEGER\n" + "a = 5\nb = 10\n" + "IF a > 0 THEN\n" + " IF b > 0 THEN\n PRINT \"both\"\n END IF\n" + "END IF\n", + "both\n"); + + // ============================================================ + // Number parsing in source + // ============================================================ + + TEST_EQ("num-hex-literal", "PRINT &H10\n", "16 \n"); + TEST_EQ("num-hex-ff", "PRINT &HFF\n", "255 \n"); + TEST_EQ("num-octal-literal", "PRINT &O10\n", "8 \n"); + TEST_EQ("num-octal-77", "PRINT &O77\n", "63 \n"); + TEST_EQ("num-binary-literal","PRINT &B1010\n", "10 \n"); + TEST_EQ("num-binary-all", "PRINT &B11111111\n", "255 \n"); + TEST_EQ("num-scientific", "PRINT 1E3\n", "1000 \n"); + TEST_EQ("num-scientific-neg","PRINT 1E-2\n", "0.01 \n"); + + // ============================================================ + // Comments + // ============================================================ + + TEST_EQ("apostrophe-comment", + "PRINT 1 ' this is a comment\n" + "PRINT 2\n", + "1 \n2 \n"); + TEST_EQ("rem-comment", + "REM this is a comment\n" + "PRINT 1\n", + "1 \n"); + + // ============================================================ + // Empty PRINT and blank lines + // ============================================================ + + TEST_EQ("print-empty", + "PRINT\n" + "PRINT \"after\"\n", + "\nafter\n"); + + // ============================================================ + // String literal with embedded quotes (via CHR$) + // ============================================================ + + TEST_EQ("chr-quote", + "PRINT \"say \" + CHR$(34) + \"hi\" + CHR$(34)\n", + "say \"hi\"\n"); + TEST_EQ("chr-newline-embeds", + "PRINT \"a\" + CHR$(10) + \"b\"\n", + "a\nb\n"); + + // ============================================================ + // END statement halts execution + // ============================================================ + + TEST_EQ("end-halts", + "PRINT \"before\"\nEND\nPRINT \"after\"\n", + "before\n"); + + // ============================================================ + // Declaration ordering: globals must be DIM'd before SUBs that + // reference them (the DVX compiler is single-pass). + // ============================================================ + + TEST_EQ("global-dim-then-sub", + "DIM counter AS INTEGER\n" + "SUB showIt\n PRINT counter\nEND SUB\n" + "counter = 7\n" + "showIt\n", + "7 \n"); + + // ============================================================ + // Type conversion through assignment + // ============================================================ + + // DVX uses dynamic typing: assigning a float to a DIM AS INTEGER + // slot stores the float value (the DIM only sets the INITIAL type). + // This differs from QBASIC but is consistent across the language. + TEST_EQ("dim-int-assign-float-preserves-type", + "DIM n AS INTEGER\n" + "n = 3.7\n" + "PRINT n\n", + "3.7 \n"); + + TEST_EQ("assign-string-from-num", + "DIM s AS STRING\n" + "s = STR$(42)\n" + "PRINT s\n", + " 42\n"); + + // ============================================================ + // Arrays of doubles + // ============================================================ + + TEST_EQ("double-array", + "DIM d(3) AS DOUBLE\n" + "d(0) = 0.25\nd(1) = 0.5\nd(2) = 0.75\nd(3) = 1.0\n" + "PRINT d(0) + d(1) + d(2) + d(3)\n", + "2.5 \n"); + + // ============================================================ + // Very deep recursion smoke test + // ============================================================ + + TEST_EQ("deep-recursion-50", + "FUNCTION countdown(n AS INTEGER) AS INTEGER\n" + " IF n <= 0 THEN\n countdown = 0\n ELSE\n countdown = 1 + countdown(n - 1)\n END IF\n" + "END FUNCTION\n" + "PRINT countdown(50)\n", + "50 \n"); + + // ============================================================ + // String in SELECT CASE with mixed case (default: case sensitive) + // ============================================================ + + TEST_EQ("select-case-str-case-sensitive", + "DIM s AS STRING\ns = \"Apple\"\n" + "SELECT CASE s\n" + " CASE \"apple\": PRINT \"lower\"\n" + " CASE \"Apple\": PRINT \"title\"\n" + " CASE ELSE: PRINT \"?\"\n" + "END SELECT\n", + "title\n"); + + // ============================================================ + // SUB-call tests through basVmCallSub (event handler path) + // ============================================================ + + TEST_SUB_EQ("subcall-globals-shared", + "DIM g AS INTEGER\ng = 5\n" + "SUB showG\n PRINT g\n g = g + 1\nEND SUB\n", + "showG", + "5 \n"); + + TEST_SUB_EQ("subcall-calls-other-sub", + "SUB a\n PRINT \"a\"\n b\nEND SUB\n" + "SUB b\n PRINT \"b\"\nEND SUB\n", + "a", + "a\nb\n"); + + TEST_SUB_EQ("subcall-for-loop-fractional", + // Event handler running GfxDrawAll-style code + "SUB drawCircle\n" + " DIM a AS DOUBLE\n" + " DIM c AS INTEGER\n" + " c = 0\n" + " FOR a = 0 TO 6.3 STEP 0.2\n" + " c = c + 1\n" + " NEXT a\n" + " PRINT c\n" + "END SUB\n", + "drawCircle", + "32 \n"); + + TEST_SUB_EQ("subcall-on-error-then-normal", + // After handler runs, a subsequent call should work normally. + "SUB maybeFail(shouldFail AS INTEGER)\n" + " ON ERROR GOTO h\n" + " IF shouldFail THEN\n" + " DIM a AS INTEGER\n DIM b AS INTEGER\n" + " a = 1\n b = 0\n" + " PRINT a \\ b\n" + " ELSE\n" + " PRINT \"ok\"\n" + " END IF\n" + " EXIT SUB\n" + " h:\n" + " PRINT \"caught\"\n" + "END SUB\n" + "SUB driver\n" + " maybeFail 0\n" + " maybeFail 1\n" + " maybeFail 0\n" + "END SUB\n", + "driver", + "ok\ncaught\nok\n"); + + // ============================================================ + // Operator precedence edge cases + // ============================================================ + + TEST_EQ("prec-neg-then-pow", "PRINT -2 ^ 2\n", "-4 \n"); // -(2^2) = -4 + // DVX ^ is left-associative: 2^3^2 == (2^3)^2 = 64. QBASIC doc + // says right-assoc but DVX chose left. Matches actual behavior. + TEST_EQ("prec-pow-left-assoc", "PRINT 2 ^ 3 ^ 2\n", "64 \n"); + TEST_EQ("prec-mul-add", "PRINT 2 + 3 * 4\n", "14 \n"); + TEST_EQ("prec-eq-chain", "PRINT 1 + 2 = 3\n", "True \n"); + TEST_EQ("prec-string-and-num", "PRINT \"n=\" + STR$(1 + 2)\n", "n= 3\n"); + + // ============================================================ + // More string operations + // ============================================================ + + TEST_EQ("trim-both-sides", + "DIM s AS STRING\ns = \" hello \"\n" + "PRINT \"[\" + LTRIM$(RTRIM$(s)) + \"]\"\n", + "[hello]\n"); + TEST_EQ("chr-special-chars", + "PRINT CHR$(9) + \"tab\"\n", + "\ttab\n"); + + // ============================================================ + // LEN edge cases + // ============================================================ + + TEST_EQ("len-unicode-is-bytes", + "PRINT LEN(\"abc\")\n", + "3 \n"); + + // ============================================================ + // Integer overflow detection + // ============================================================ + + TEST_EQ("int-max-plus-one", + "PRINT 32767 + 1\n", + "32768 \n"); + + // ============================================================ + // String length tests + // ============================================================ + + TEST_EQ("string-len-after-build", + "DIM s AS STRING\ns = \"abc\"\ns = s + s\n" + "PRINT LEN(s)\n", + "6 \n"); + + // ============================================================ + // Array bounds: writing past UBOUND is an error + // ============================================================ + + TEST_RUNTIME_ERROR("array-out-of-bounds-write", + "DIM a(2) AS INTEGER\na(5) = 99\n", + ""); + TEST_RUNTIME_ERROR("array-out-of-bounds-read", + "DIM a(2) AS INTEGER\nPRINT a(5)\n", + ""); + TEST_RUNTIME_ERROR("array-negative-index-no-lbound", + "DIM a(2) AS INTEGER\nPRINT a(-1)\n", + ""); + + // ============================================================ + // More UDT tests + // ============================================================ + + TEST_EQ("udt-multiple-instances", + "TYPE P\n x AS INTEGER\n y AS INTEGER\nEND TYPE\n" + "DIM a AS P\nDIM b AS P\n" + "a.x = 1\na.y = 2\n" + "b.x = 10\nb.y = 20\n" + "PRINT a.x + b.x\nPRINT a.y + b.y\n", + "11 \n22 \n"); + + TEST_EQ("udt-return-from-function-via-byref", + "TYPE P\n x AS INTEGER\n y AS INTEGER\nEND TYPE\n" + "SUB setPt(pt AS P, xv AS INTEGER, yv AS INTEGER)\n" + " pt.x = xv\n pt.y = yv\n" + "END SUB\n" + "DIM a AS P\n" + "setPt a, 7, 8\n" + "PRINT a.x\nPRINT a.y\n", + "7 \n8 \n"); + + // ============================================================ + // More FOR/loop edge cases + // ============================================================ + + TEST_EQ("for-zero-to-zero", + "DIM i AS INTEGER\nFOR i = 0 TO 0\n PRINT i\nNEXT i\n", + "0 \n"); + + TEST_EQ("for-neg-to-neg", + "DIM i AS INTEGER\nFOR i = -3 TO -1\n PRINT i\nNEXT i\n", + "-3 \n-2 \n-1 \n"); + + // ============================================================ + // More SELECT CASE + // ============================================================ + + TEST_EQ("select-case-no-match-no-else", + "DIM n AS INTEGER\nn = 99\n" + "SELECT CASE n\n" + " CASE 1: PRINT \"one\"\n" + " CASE 2: PRINT \"two\"\n" + "END SELECT\n" + "PRINT \"done\"\n", + "done\n"); + + // ============================================================ + // Global String and STATIC STRING from event-dispatch path + // ============================================================ + + TEST_SUB_EQ("subcall-global-string", + "DIM g AS STRING\ng = \"init\"\n" + "SUB appendX\n" + " g = g + \"x\"\n" + " PRINT g\n" + "END SUB\n", + "appendX", + "initx\n"); + + TEST_SUB_EQ("subcall-static-accumulates", + "SUB count\n" + " STATIC n AS INTEGER\n" + " n = n + 1\n" + " PRINT n\n" + "END SUB\n" + "SUB run3\n" + " count\n count\n count\n" + "END SUB\n", + "run3", + "1 \n2 \n3 \n"); + + // ============================================================ + // FUNCTION returning string works in expressions + // ============================================================ + + TEST_EQ("function-string-in-expr", + "FUNCTION tag(s AS STRING) AS STRING\n" + " tag = \"<\" + s + \">\"\nEND FUNCTION\n" + "PRINT tag(\"foo\") + tag(\"bar\")\n", + " \n"); + + // ============================================================ + // Dictionary-like pattern using parallel arrays + // ============================================================ + + TEST_EQ("parallel-arrays", + "DIM keys(2) AS STRING\nDIM vals(2) AS INTEGER\n" + "keys(0) = \"a\"\nkeys(1) = \"b\"\nkeys(2) = \"c\"\n" + "vals(0) = 10\nvals(1) = 20\nvals(2) = 30\n" + "DIM i AS INTEGER\n" + "FOR i = 0 TO 2\n" + " PRINT keys(i) + \"=\" + STR$(vals(i))\n" + "NEXT i\n", + "a= 10\nb= 20\nc= 30\n"); + + // ============================================================ + // Recursive algorithms + // ============================================================ + + TEST_EQ("fib-recursive", + "FUNCTION fib(n AS INTEGER) AS LONG\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" + "PRINT fib(10)\n", + "55 \n"); + + // ============================================================ + // String-valued DIM inside a SUB + // ============================================================ + + TEST_EQ("local-string-dim-in-sub", + "SUB build\n" + " DIM s AS STRING\n" + " s = s + \"x\"\n" + " s = s + \"y\"\n" + " PRINT s\n" + "END SUB\n" + "build\n" + "build\n", + "xy\nxy\n"); + + // ============================================================ + // Boolean short-circuit edge case (no crash on null side) + // ============================================================ + + TEST_EQ("bool-and-normal", + "DIM a AS INTEGER\nDIM b AS INTEGER\n" + "a = 5\nb = 10\n" + "IF a > 0 AND b > 0 THEN PRINT \"both-pos\"\n", + "both-pos\n"); + + // ============================================================ + // IF with compound conditions using NOT / AND / OR + // ============================================================ + + TEST_EQ("if-not-or", + "DIM x AS INTEGER\nx = 5\n" + "IF NOT (x < 0 OR x > 100) THEN PRINT \"in-range\"\n", + "in-range\n"); + + printf("\n--------------------------\n"); + printf("PASS: %d FAIL: %d\n", (int)sPassCount, (int)sFailCount); + + return sFailCount == 0 ? 0 : 1; +} diff --git a/src/apps/kpunch/dvxbasic/test_vm.c b/src/apps/kpunch/dvxbasic/test_vm.c index 36a27c6..280060d 100644 --- a/src/apps/kpunch/dvxbasic/test_vm.c +++ b/src/apps/kpunch/dvxbasic/test_vm.c @@ -190,7 +190,7 @@ static void test4(void) { // PUSH 5 (limit); PUSH 1 (step) emit8(OP_PUSH_INT16); emit16(5); emit8(OP_PUSH_INT16); emit16(1); - emit8(OP_FOR_INIT); emitU16(0); emit8(1); // isLocal=1 + emit8(OP_FOR_INIT); emitU16(0); emit8(1); emit16(0); // scope=local, skipOffset patched below // Loop body start (record PC for FOR_NEXT offset) int32_t loopBody = sCodeLen; diff --git a/src/apps/kpunch/progman/progman.c b/src/apps/kpunch/progman/progman.c index 2944712..2457551 100644 --- a/src/apps/kpunch/progman/progman.c +++ b/src/apps/kpunch/progman/progman.c @@ -63,7 +63,6 @@ #include #include #include -#include #include #include "dvxMem.h" #include "stb_ds_wrap.h" @@ -448,9 +447,9 @@ static void scanAppsDirRecurse(const char *dirPath) { // Collect all entries first, close the handle, then process. // DOS has limited file handles; keeping a DIR open while // opening .app files for resource loading causes failures. - DIR *dir = opendir(dirPath); + char **names = dvxReadDir(dirPath); - if (!dir) { + if (!names) { if (sAppCount == 0) { dvxLog("Progman: %s directory not found", dirPath); } @@ -458,19 +457,6 @@ static void scanAppsDirRecurse(const char *dirPath) { return; } - char **names = NULL; - struct dirent *ent; - - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) { - continue; - } - - arrput(names, strdup(ent->d_name)); - } - - closedir(dir); - int32_t nEntries = (int32_t)arrlen(names); for (int32_t i = 0; i < nEntries; i++) { @@ -481,14 +467,10 @@ static void scanAppsDirRecurse(const char *dirPath) { if (stat(fullPath, &st) == 0 && S_ISDIR(st.st_mode)) { scanAppsDirRecurse(fullPath); - free(names[i]); continue; } - int32_t len = (int32_t)strlen(names[i]); - - if (len < 5 || strcasecmp(names[i] + len - 4, ".app") != 0 || strcasecmp(names[i], "progman.app") == 0) { - free(names[i]); + if (!dvxHasExt(names[i], ".app") || strcasecmp(names[i], "progman.app") == 0) { continue; } @@ -497,6 +479,7 @@ static void scanAppsDirRecurse(const char *dirPath) { snprintf(newEntry.path, sizeof(newEntry.path), "%s", fullPath); // Default name from filename (without .app extension) + int32_t len = (int32_t)strlen(names[i]); int32_t nameLen = len - 4; if (nameLen >= SHELL_APP_NAME_MAX) { @@ -524,11 +507,10 @@ static void scanAppsDirRecurse(const char *dirPath) { arrput(sAppFiles, newEntry); sAppCount = (int32_t)arrlen(sAppFiles); - free(names[i]); dvxUpdate(sAc); } - arrfree(names); + dvxReadDirFree(names); } diff --git a/src/apps/kpunch/resedit/resedit.frm b/src/apps/kpunch/resedit/resedit.frm index fc5431f..f71226b 100644 --- a/src/apps/kpunch/resedit/resedit.frm +++ b/src/apps/kpunch/resedit/resedit.frm @@ -270,12 +270,15 @@ SUB mnuAddText_Click DIM rName AS STRING rName = basInputBox2("Add Text Resource", "Resource name:", "") - IF rName = "" THEN + IF basInputCancelled OR rName = "" THEN EXIT SUB END IF DIM text AS STRING text = basInputBox2("Add Text Resource", "Text value:", "") + IF basInputCancelled THEN + EXIT SUB + END IF IF ResAddText(filePath, rName, text) THEN ReopenAndRefresh @@ -293,7 +296,7 @@ SUB mnuAddFile_Click DIM rName AS STRING rName = basInputBox2("Add File Resource", "Resource name:", "") - IF rName = "" THEN + IF basInputCancelled OR rName = "" THEN EXIT SUB END IF @@ -352,6 +355,10 @@ SUB mnuEditText_Click DIM newText AS STRING newText = basInputBox2("Edit Text Resource", "Value for '" + rName + "':", oldText) + IF basInputCancelled THEN + EXIT SUB + END IF + IF ResAddText(filePath, rName, newText) THEN ReopenAndRefresh LblStatus.Caption = "Updated: " + rName diff --git a/src/include/basic/commdlg.bas b/src/include/basic/commdlg.bas index c01eb6a..0c28a25 100644 --- a/src/include/basic/commdlg.bas +++ b/src/include/basic/commdlg.bas @@ -46,6 +46,10 @@ DECLARE LIBRARY "basrt" ' Show a modal text input box. Returns entered text, or "" if cancelled. DECLARE FUNCTION basInputBox2(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL defaultText AS STRING) AS STRING + ' Returns True if the most recent basInputBox2 call was cancelled, + ' False if the user clicked OK (even with an empty field). + DECLARE FUNCTION basInputCancelled() AS INTEGER + ' Show a choice dialog with a listbox. items$ is pipe-delimited ' (e.g. "Red|Green|Blue"). Returns chosen index (0-based), or -1. DECLARE FUNCTION basChoiceDialog(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL items AS STRING, BYVAL defaultIdx AS INTEGER) AS INTEGER diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c index 26a27fe..4b9525c 100644 --- a/src/libs/kpunch/libdvx/dvxApp.c +++ b/src/libs/kpunch/libdvx/dvxApp.c @@ -1193,6 +1193,14 @@ static void dispatchEvents(AppContextT *ctx) { int32_t menuId = item->id; WindowT *win = findWindowById(ctx, ctx->popup.windowId); closeAllPopups(ctx); + // Consume the press so that if the menu handler + // re-enters the event loop (CreateForm/Show, modal + // dialog) the still-held button isn't re-dispatched + // as a fresh MouseDown to whatever widget is under + // the (now-closed) menu dropdown. The matching + // release is also swallowed via suppressNextMouseUp. + ctx->prevMouseButtons |= MOUSE_LEFT; + ctx->suppressNextMouseUp = true; if (win && win->onMenu) { WIN_CALLBACK(ctx, win, win->onMenu(win, menuId)); @@ -1363,9 +1371,14 @@ static void dispatchEvents(AppContextT *ctx) { } } - // Handle button release on content -- send to focused window + // Handle button release on content -- send to focused window. + // Skip if a menu click consumed this press (prevents the stray + // release from firing as a click on whatever widget sits under + // where the menu dropdown was). if (!(buttons & MOUSE_LEFT) && (prevBtn & MOUSE_LEFT)) { - if (ctx->stack.focusedIdx >= 0) { + if (ctx->suppressNextMouseUp) { + ctx->suppressNextMouseUp = false; + } else if (ctx->stack.focusedIdx >= 0) { WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; if (win->onMouse) { diff --git a/src/libs/kpunch/libdvx/dvxApp.h b/src/libs/kpunch/libdvx/dvxApp.h index f446175..805511f 100644 --- a/src/libs/kpunch/libdvx/dvxApp.h +++ b/src/libs/kpunch/libdvx/dvxApp.h @@ -82,6 +82,10 @@ typedef struct AppContextT { int32_t prevMouseX; int32_t prevMouseY; int32_t prevMouseButtons; + // Set true when a menu/popup click consumed a mouse press; the + // matching release is then swallowed so widgets underneath the + // (now-closed) menu don't receive a stray click. + bool suppressNextMouseUp; // Double-click detection for minimized window icons: timestamps and // window IDs track whether two clicks land on the same icon within // the system double-click interval. diff --git a/src/libs/kpunch/libdvx/dvxDraw.c b/src/libs/kpunch/libdvx/dvxDraw.c index 24747b4..7ee5ce2 100644 --- a/src/libs/kpunch/libdvx/dvxDraw.c +++ b/src/libs/kpunch/libdvx/dvxDraw.c @@ -588,6 +588,65 @@ void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w } +// ============================================================ +// calcCenteredText +// ============================================================ +// +// Shared by every widget that centers a text label in its own rect +// (buttons, checkbox labels, etc.). Centralising the arithmetic +// avoids tiny off-by-one drift from copy-paste variations. + +void calcCenteredText(const BitmapFontT *font, int32_t rectX, int32_t rectY, int32_t rectW, int32_t rectH, const char *text, int32_t *outX, int32_t *outY) { + int32_t textW = textWidthAccel(font, text); + + if (outX) { + *outX = rectX + (rectW - textW) / 2; + } + + if (outY) { + *outY = rectY + (rectH - font->charHeight) / 2; + } +} + + +// ============================================================ +// drawPressableBevel +// ============================================================ +// +// Standard 2px button / toggle bevel. Swaps highlight and shadow when +// pressed to create the sunken look. face fills the interior (pass 0 +// to leave the underlying content alone, e.g. for transparent toggle +// buttons). + +void drawPressableBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, bool pressed, uint32_t face, const ColorSchemeT *colors) { + BevelStyleT bevel; + bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight; + bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow; + bevel.face = face; + bevel.width = 2; + drawBevel(d, ops, x, y, w, h, &bevel); +} + + +// ============================================================ +// drawWidgetTextAccel +// ============================================================ +// +// Every text-bearing widget paints this same enabled/disabled branch. +// Centralising it keeps the embossed-disabled appearance consistent +// and removes ~5 lines of boilerplate per widget. + +void drawWidgetTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque, bool enabled, const ColorSchemeT *colors) { + if (!enabled) { + drawTextAccel(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false); + drawTextAccel(d, ops, font, x, y, text, colors->windowShadow, 0, false); + return; + } + + drawTextAccel(d, ops, font, x, y, text, fg, bg, opaque); +} + + // ============================================================ // drawInit // ============================================================ diff --git a/src/libs/kpunch/libdvx/dvxDraw.h b/src/libs/kpunch/libdvx/dvxDraw.h index 0037136..95ffede 100644 --- a/src/libs/kpunch/libdvx/dvxDraw.h +++ b/src/libs/kpunch/libdvx/dvxDraw.h @@ -116,6 +116,24 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3 // Windows 3.x focus rectangle convention. void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color); +// Compute the (x, y) origin for rendering `text` centered both +// horizontally and vertically inside the rect (rectX, rectY, rectW, +// rectH). Measures `text` with textWidthAccel (ignores & markers) and +// the font's charHeight. Writes results to *outX / *outY. +void calcCenteredText(const BitmapFontT *font, int32_t rectX, int32_t rectY, int32_t rectW, int32_t rectH, const char *text, int32_t *outX, int32_t *outY); + +// Draw `text` with &-accelerator markers, automatically choosing +// between the enabled and disabled (embossed) rendering paths. Saves +// the if/else block every widget paints around drawTextAccel / +// drawTextAccelEmbossed. +void drawWidgetTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque, bool enabled, const ColorSchemeT *colors); + +// Draw a pressable 2px bevel. When pressed is true, highlight/shadow +// colors swap to produce the "sunken" look (button pressed, toggle +// checked). face is the fill color; pass 0 to leave the interior +// alone. Used by buttons, checkboxes, toggle bars, etc. +void drawPressableBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, bool pressed, uint32_t face, const ColorSchemeT *colors); + // Horizontal line (1px tall rectangle fill, but with simpler clipping). void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color); diff --git a/src/libs/kpunch/libdvx/dvxWgtP.h b/src/libs/kpunch/libdvx/dvxWgtP.h index 1516be6..e5df10b 100644 --- a/src/libs/kpunch/libdvx/dvxWgtP.h +++ b/src/libs/kpunch/libdvx/dvxWgtP.h @@ -142,6 +142,13 @@ void widgetDestroyChildren(WidgetT *w); // Allocation WidgetT *widgetAlloc(WidgetT *parent, int32_t type); +// Allocate a widget of the given type PLUS a data struct of dataSize +// bytes. The data struct must begin with a `const char *text` field +// (WCLASS_HAS_TEXT semantics); this field is set to strdup(text) and +// the widget's accelKey is parsed from text. Other fields in the data +// struct remain zeroed. Returns NULL on allocation failure. +WidgetT *widgetAllocWithText(WidgetT *parent, int32_t type, size_t dataSize, const char *text); + // Focus management WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after); WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before); diff --git a/src/libs/kpunch/libdvx/platform/dvxPlat.h b/src/libs/kpunch/libdvx/platform/dvxPlat.h index 9e2066a..0118e16 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlat.h +++ b/src/libs/kpunch/libdvx/platform/dvxPlat.h @@ -312,6 +312,22 @@ const char *dvxSkipWs(const char *s); // new length. buf may be NULL or empty. int32_t dvxTrimRight(char *buf); +// Case-insensitive check that `name` ends with `ext` (which should +// include the leading dot, e.g. ".app"). Returns false if either +// argument is NULL or name is shorter than ext. +bool dvxHasExt(const char *name, const char *ext); + +// Read all entries from `dirPath` (except "." and "..") into a +// heap-allocated stb_ds array of malloc'd strings. Returns NULL on +// open failure. An empty directory returns a valid empty array; +// check arrlen() for count. Caller must pass the result to +// dvxReadDirFree when done. +char **dvxReadDir(const char *dirPath); + +// Free an array returned by dvxReadDir: frees every entry and the +// stb_ds array header. Safe to call with NULL. +void dvxReadDirFree(char **entries); + // The platform's native directory separator as a string literal. Use // with string-literal concatenation in format strings or path constants: // snprintf(buf, sz, "%s" DVX_PATH_SEP "%s", dir, name); diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c index 2b4f31a..f19289e 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c @@ -2312,12 +2312,15 @@ DXE_EXPORT_TABLE(sDxeExportTable) // --- dvx helpers (lives in dvx.exe, used by all modules) --- DXE_EXPORT(dvxCalloc) DXE_EXPORT(dvxFree) + DXE_EXPORT(dvxHasExt) DXE_EXPORT(dvxLog) DXE_EXPORT(dvxMalloc) DXE_EXPORT(dvxMemAppIdPtr) DXE_EXPORT(dvxMemGetAppUsage) DXE_EXPORT(dvxMemResetApp) DXE_EXPORT(dvxMemSnapshotLoad) + DXE_EXPORT(dvxReadDir) + DXE_EXPORT(dvxReadDirFree) DXE_EXPORT(dvxRealloc) DXE_EXPORT(dvxSkipWs) DXE_EXPORT(dvxStrdup) diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c index f41d762..6ef6acf 100644 --- a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c @@ -29,11 +29,14 @@ // can still use them. #include "dvxPlat.h" +#include "thirdparty/stb_ds_wrap.h" #include +#include #include #include #include +#include #include #ifdef __DJGPP__ @@ -70,6 +73,67 @@ int32_t dvxTrimRight(char *buf) { } +bool dvxHasExt(const char *name, const char *ext) { + if (!name || !ext) { + return false; + } + + size_t nameLen = strlen(name); + size_t extLen = strlen(ext); + + if (nameLen < extLen) { + return false; + } + + return strcasecmp(name + nameLen - extLen, ext) == 0; +} + + +char **dvxReadDir(const char *dirPath) { + if (!dirPath) { + return NULL; + } + + DIR *dir = opendir(dirPath); + + if (!dir) { + return NULL; + } + + char **entries = NULL; + struct dirent *ent; + + while ((ent = readdir(dir)) != NULL) { + // Skip "." and ".." + if (ent->d_name[0] == '.' && + (ent->d_name[1] == '\0' || + (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) { + continue; + } + + arrput(entries, strdup(ent->d_name)); + } + + closedir(dir); + return entries; +} + + +void dvxReadDirFree(char **entries) { + if (!entries) { + return; + } + + int32_t n = (int32_t)arrlen(entries); + + for (int32_t i = 0; i < n; i++) { + free(entries[i]); + } + + arrfree(entries); +} + + int32_t platformChdir(const char *path) { #ifdef __DJGPP__ if (path[0] && path[1] == ':') { diff --git a/src/libs/kpunch/libdvx/widgetCore.c b/src/libs/kpunch/libdvx/widgetCore.c index 54ec140..119b2a1 100644 --- a/src/libs/kpunch/libdvx/widgetCore.c +++ b/src/libs/kpunch/libdvx/widgetCore.c @@ -41,9 +41,12 @@ // which doesn't map cleanly to an arena pattern. #include "dvxWgtP.h" +#include "dvxDraw.h" #include "dvxPlat.h" #include "stb_ds_wrap.h" +#include +#include #include // ============================================================ @@ -254,6 +257,31 @@ WidgetT *widgetAlloc(WidgetT *parent, int32_t type) { } +WidgetT *widgetAllocWithText(WidgetT *parent, int32_t type, size_t dataSize, const char *text) { + WidgetT *w = widgetAlloc(parent, type); + + if (!w) { + return NULL; + } + + void *data = calloc(1, dataSize); + + if (!data) { + // Widget itself is already in the tree; leave it rather than + // attempting a partial rollback (widget destroy is idempotent + // via the parent teardown). + dvxLog("Widget: failed to allocate %u-byte data for type %d", (unsigned)dataSize, type); + return w; + } + + // The data struct must begin with a `const char *text` field. + *(const char **)data = text ? strdup(text) : NULL; + w->data = data; + w->accelKey = accelParse(text); + return w; +} + + int32_t widgetCountVisibleChildren(const WidgetT *w) { int32_t count = 0; diff --git a/src/loader/loaderMain.c b/src/loader/loaderMain.c index 1f7113d..ea9d954 100644 --- a/src/loader/loaderMain.c +++ b/src/loader/loaderMain.c @@ -38,7 +38,6 @@ #include "../tools/hlpcCompile.h" #include -#include #include #include #include @@ -175,49 +174,41 @@ static void collectGlobFiles(char ***outFiles, const char *pattern, const char * dirPart[1] = '\0'; } - DIR *d = opendir(dirPart); + char **names = dvxReadDir(dirPart); - if (!d) { + if (!names) { return; } - char **names = NULL; - struct dirent *ent; - - while ((ent = readdir(d)) != NULL) { - if (ent->d_name[0] == '.') { - continue; - } - - arrput(names, strdup(ent->d_name)); - } - - closedir(d); - int32_t nEntries = (int32_t)arrlen(names); for (int32_t i = 0; i < nEntries; i++) { + // Skip hidden files (dvxReadDir already strips "." and "..") + if (names[i][0] == '.') { + continue; + } + char fullPath[DVX_MAX_PATH]; snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPart, names[i]); struct stat st; - if (stat(fullPath, &st) == 0) { - if (S_ISDIR(st.st_mode)) { - char subPattern[DVX_MAX_PATH]; - snprintf(subPattern, sizeof(subPattern), "%s" DVX_PATH_SEP "%s", fullPath, globPart); - collectGlobFiles(outFiles, subPattern, excludePattern); - } else if (platformGlobMatch(globPart, names[i])) { - if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) { - arrput(*outFiles, strdup(fullPath)); - } - } + if (stat(fullPath, &st) != 0) { + continue; } - free(names[i]); + if (S_ISDIR(st.st_mode)) { + char subPattern[DVX_MAX_PATH]; + snprintf(subPattern, sizeof(subPattern), "%s" DVX_PATH_SEP "%s", fullPath, globPart); + collectGlobFiles(outFiles, subPattern, excludePattern); + } else if (platformGlobMatch(globPart, names[i])) { + if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) { + arrput(*outFiles, strdup(fullPath)); + } + } } - arrfree(names); + dvxReadDirFree(names); } @@ -247,51 +238,38 @@ static int32_t countHcfInputFiles(const char *hcfPath) { // Count total progress steps across all .hcf files under a directory. static int32_t countTotalHelpSteps(const char *dirPath) { - DIR *dir = opendir(dirPath); + char **names = dvxReadDir(dirPath); - if (!dir) { + if (!names) { return 0; } - char **names = NULL; - struct dirent *ent; - - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == '.') { - continue; - } - - arrput(names, strdup(ent->d_name)); - } - - closedir(dir); - int32_t total = 0; int32_t nEntries = (int32_t)arrlen(names); for (int32_t i = 0; i < nEntries; i++) { + if (names[i][0] == '.') { + continue; + } + char fullPath[DVX_MAX_PATH]; snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]); struct stat st; - if (stat(fullPath, &st) == 0) { - if (S_ISDIR(st.st_mode)) { - total += countTotalHelpSteps(fullPath); - } else { - int32_t nameLen = (int32_t)strlen(names[i]); - - if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) { - int32_t fileCount = countHcfInputFiles(fullPath); - total += hlpcProgressTotal(fileCount); - } - } + if (stat(fullPath, &st) != 0) { + continue; } - free(names[i]); + if (S_ISDIR(st.st_mode)) { + total += countTotalHelpSteps(fullPath); + } else if (dvxHasExt(names[i], ".hcf")) { + int32_t fileCount = countHcfInputFiles(fullPath); + total += hlpcProgressTotal(fileCount); + } } - arrfree(names); + dvxReadDirFree(names); return total; } @@ -764,49 +742,36 @@ static void processHcf(const char *hcfPath, const char *hcfDir) { // Recursively scan a directory for .hcf files and process each one. static void processHcfDir(const char *dirPath) { - DIR *dir = opendir(dirPath); + char **names = dvxReadDir(dirPath); - if (!dir) { + if (!names) { return; } - char **names = NULL; - struct dirent *ent; - - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == '.') { - continue; - } - - arrput(names, strdup(ent->d_name)); - } - - closedir(dir); - int32_t nEntries = (int32_t)arrlen(names); for (int32_t i = 0; i < nEntries; i++) { + if (names[i][0] == '.') { + continue; + } + char fullPath[DVX_MAX_PATH]; snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]); struct stat st; - if (stat(fullPath, &st) == 0) { - if (S_ISDIR(st.st_mode)) { - processHcfDir(fullPath); - } else { - int32_t nameLen = (int32_t)strlen(names[i]); - - if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) { - processHcf(fullPath, dirPath); - } - } + if (stat(fullPath, &st) != 0) { + continue; } - free(names[i]); + if (S_ISDIR(st.st_mode)) { + processHcfDir(fullPath); + } else if (dvxHasExt(names[i], ".hcf")) { + processHcf(fullPath, dirPath); + } } - arrfree(names); + dvxReadDirFree(names); } @@ -864,35 +829,19 @@ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) { // Collect all entries first, close the handle, then process. // DOS has limited file handles; keeping a DIR open during // recursion or stat() causes intermittent failures. - DIR *dir = opendir(dirPath); + char **names = dvxReadDir(dirPath); - if (!dir) { + if (!names) { return; } - char **names = NULL; - struct dirent *ent; - - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) { - continue; - } - - arrput(names, strdup(ent->d_name)); - } - - closedir(dir); - - int32_t count = (int32_t)arrlen(names); - int32_t extLen = (int32_t)strlen(ext); + int32_t count = (int32_t)arrlen(names); for (int32_t i = 0; i < count; i++) { char path[DVX_MAX_PATH]; snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dirPath, names[i]); - int32_t nameLen = (int32_t)strlen(names[i]); - - if (nameLen > extLen && strcasecmp(names[i] + nameLen - extLen, ext) == 0) { + if (dvxHasExt(names[i], ext)) { ModuleT mod; memset(&mod, 0, sizeof(mod)); snprintf(mod.path, sizeof(mod.path), "%s", path); @@ -905,11 +854,9 @@ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) { scanDir(path, ext, mods); } } - - free(names[i]); } - arrfree(names); + dvxReadDirFree(names); } diff --git a/src/tools/Makefile b/src/tools/Makefile index b8e4328..cdafc37 100644 --- a/src/tools/Makefile +++ b/src/tools/Makefile @@ -43,9 +43,10 @@ SYSTEMDIR = ../../bin/system all: $(HOSTDIR)/dvxres $(HOSTDIR)/mkicon $(HOSTDIR)/mktbicon $(HOSTDIR)/mkwgticon $(HOSTDIR)/bmp2raw $(HOSTDIR)/dvxhlpc $(SYSTEMDIR)/SPLASH.RAW $(SYSTEMDIR)/DVXHLPC.EXE $(SYSTEMDIR)/DVXRES.EXE PLATFORM_UTIL = ../libs/kpunch/libdvx/platform/dvxPlatformUtil.c +STB_DS_IMPL = ../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c -$(HOSTDIR)/dvxres: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) | $(HOSTDIR) - $(CC) $(CFLAGS) -o $@ dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) +$(HOSTDIR)/dvxres: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(HOSTDIR) + $(CC) $(CFLAGS) -o $@ dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL) $(HOSTDIR)/mkicon: mkicon.c bmpDraw.c bmpDraw.h | $(HOSTDIR) $(CC) $(CFLAGS) -o $@ mkicon.c bmpDraw.c -lm @@ -59,8 +60,8 @@ $(HOSTDIR)/mkwgticon: mkwgticon.c bmpDraw.c bmpDraw.h | $(HOSTDIR) $(HOSTDIR)/bmp2raw: bmp2raw.c | $(HOSTDIR) $(CC) $(CFLAGS) -o $@ bmp2raw.c -$(HOSTDIR)/dvxhlpc: dvxhlpc.c ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) | $(HOSTDIR) - $(CC) $(CFLAGS) -o $@ dvxhlpc.c $(PLATFORM_UTIL) +$(HOSTDIR)/dvxhlpc: dvxhlpc.c ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(HOSTDIR) + $(CC) $(CFLAGS) -o $@ dvxhlpc.c $(PLATFORM_UTIL) $(STB_DS_IMPL) $(HOSTDIR): mkdir -p $(HOSTDIR) @@ -74,8 +75,8 @@ $(BINDIR): $(CONFIGDIR): mkdir -p $(CONFIGDIR) -$(SYSTEMDIR)/DVXHLPC.EXE: dvxhlpc.c hlpcCompile.h ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) | $(SYSTEMDIR) - $(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxhlpc.exe dvxhlpc.c $(PLATFORM_UTIL) +$(SYSTEMDIR)/DVXHLPC.EXE: dvxhlpc.c hlpcCompile.h ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(SYSTEMDIR) + $(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxhlpc.exe dvxhlpc.c $(PLATFORM_UTIL) $(STB_DS_IMPL) $(EXE2COFF) $(SYSTEMDIR)/dvxhlpc.exe cat $(CWSDSTUB) $(SYSTEMDIR)/dvxhlpc > $@ rm -f $(SYSTEMDIR)/dvxhlpc $(SYSTEMDIR)/dvxhlpc.exe @@ -87,8 +88,8 @@ $(SYSTEMDIR)/DVXHLPC.EXE: dvxhlpc.c hlpcCompile.h ../apps/kpunch/dvxhelp/hlpform ../../obj/loader: mkdir -p ../../obj/loader -$(SYSTEMDIR)/DVXRES.EXE: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) | $(SYSTEMDIR) - $(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxres.exe dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) +$(SYSTEMDIR)/DVXRES.EXE: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(SYSTEMDIR) + $(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxres.exe dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL) $(EXE2COFF) $(SYSTEMDIR)/dvxres.exe cat $(CWSDSTUB) $(SYSTEMDIR)/dvxres > $@ rm -f $(SYSTEMDIR)/dvxres $(SYSTEMDIR)/dvxres.exe diff --git a/src/widgets/kpunch/box/widgetBox.c b/src/widgets/kpunch/box/widgetBox.c index 6be7e17..4a8b4ef 100644 --- a/src/widgets/kpunch/box/widgetBox.c +++ b/src/widgets/kpunch/box/widgetBox.c @@ -73,15 +73,11 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap WidgetT *wgtFrame(WidgetT *parent, const char *title) { - WidgetT *w = widgetAlloc(parent, sFrameTypeId); + WidgetT *w = widgetAllocWithText(parent, sFrameTypeId, sizeof(FrameDataT), title); - if (w) { - FrameDataT *fd = calloc(1, sizeof(FrameDataT)); - fd->title = title ? strdup(title) : NULL; - fd->style = FrameInE; - fd->color = 0; - w->data = fd; - w->accelKey = accelParse(title); + if (w && w->data) { + FrameDataT *fd = (FrameDataT *)w->data; + fd->style = FrameInE; } return w; @@ -168,11 +164,7 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap rectFill(d, ops, titleX - 2, titleY, titleW + 4, font->charHeight, bg); - if (!w->enabled) { - drawTextAccelEmbossed(d, ops, font, titleX, titleY, fd->title, colors); - } else { - drawTextAccel(d, ops, font, titleX, titleY, fd->title, fg, bg, true); - } + drawWidgetTextAccel(d, ops, font, titleX, titleY, fd->title, fg, bg, true, w->enabled, colors); } } diff --git a/src/widgets/kpunch/button/widgetButton.c b/src/widgets/kpunch/button/widgetButton.c index 033ac88..3956b5f 100644 --- a/src/widgets/kpunch/button/widgetButton.c +++ b/src/widgets/kpunch/button/widgetButton.c @@ -70,16 +70,7 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma WidgetT *wgtButton(WidgetT *parent, const char *text) { - WidgetT *w = widgetAlloc(parent, sTypeId); - - if (w) { - ButtonDataT *d = calloc(1, sizeof(ButtonDataT)); - w->data = d; - d->text = text ? strdup(text) : NULL; - w->accelKey = accelParse(text); - } - - return w; + return widgetAllocWithText(parent, sTypeId, sizeof(ButtonDataT), text); } @@ -104,27 +95,18 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace; - BevelStyleT bevel; - bevel.highlight = w->pressed ? colors->windowShadow : colors->windowHighlight; - bevel.shadow = w->pressed ? colors->windowHighlight : colors->windowShadow; - bevel.face = bgFace; - bevel.width = 2; - drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); + drawPressableBevel(d, ops, w->x, w->y, w->w, w->h, w->pressed, bgFace, colors); - int32_t textW = textWidthAccel(font, bd->text); - int32_t textX = w->x + (w->w - textW) / 2; - int32_t textY = w->y + (w->h - font->charHeight) / 2; + int32_t textX; + int32_t textY; + calcCenteredText(font, w->x, w->y, w->w, w->h, bd->text, &textX, &textY); if (w->pressed) { textX += BUTTON_PRESS_OFFSET; textY += BUTTON_PRESS_OFFSET; } - if (!w->enabled) { - drawTextAccelEmbossed(d, ops, font, textX, textY, bd->text, colors); - } else { - drawTextAccel(d, ops, font, textX, textY, bd->text, fg, bgFace, true); - } + drawWidgetTextAccel(d, ops, font, textX, textY, bd->text, fg, bgFace, true, w->enabled, colors); if (w == sFocusedWidget) { int32_t off = w->pressed ? BUTTON_PRESS_OFFSET : 0; diff --git a/src/widgets/kpunch/canvas/widgetCanvas.c b/src/widgets/kpunch/canvas/widgetCanvas.c index 16f9c68..7c12bb4 100644 --- a/src/widgets/kpunch/canvas/widgetCanvas.c +++ b/src/widgets/kpunch/canvas/widgetCanvas.c @@ -1045,7 +1045,7 @@ static const WgtMethodDescT sMethods[] = { }; static const WgtIfaceT sIface = { - .basName = "PictureBox", + .basName = "Canvas", .props = NULL, .propCount = 0, .methods = sMethods, diff --git a/src/widgets/kpunch/checkbox/widgetCheckbox.c b/src/widgets/kpunch/checkbox/widgetCheckbox.c index 35d884e..157675a 100644 --- a/src/widgets/kpunch/checkbox/widgetCheckbox.c +++ b/src/widgets/kpunch/checkbox/widgetCheckbox.c @@ -67,16 +67,7 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit WidgetT *wgtCheckbox(WidgetT *parent, const char *text) { - WidgetT *w = widgetAlloc(parent, sTypeId); - - if (w) { - CheckboxDataT *d = calloc(1, sizeof(CheckboxDataT)); - w->data = d; - d->text = text ? strdup(text) : NULL; - w->accelKey = accelParse(text); - } - - return w; + return widgetAllocWithText(parent, sTypeId, sizeof(CheckboxDataT), text); } @@ -183,11 +174,7 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit int32_t labelY = w->y + (w->h - font->charHeight) / 2; int32_t labelW = textWidthAccel(font, cd->text); - if (!w->enabled) { - drawTextAccelEmbossed(d, ops, font, labelX, labelY, cd->text, colors); - } else { - drawTextAccel(d, ops, font, labelX, labelY, cd->text, fg, bg, false); - } + drawWidgetTextAccel(d, ops, font, labelX, labelY, cd->text, fg, bg, false, w->enabled, colors); if (w == sFocusedWidget) { drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg); diff --git a/src/widgets/kpunch/imageButton/widgetImageButton.c b/src/widgets/kpunch/imageButton/widgetImageButton.c index 8284351..a7f3dca 100644 --- a/src/widgets/kpunch/imageButton/widgetImageButton.c +++ b/src/widgets/kpunch/imageButton/widgetImageButton.c @@ -211,12 +211,7 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, con uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace; bool pressed = w->pressed && w->enabled; - BevelStyleT bevel; - bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight; - bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow; - bevel.face = bgFace; - bevel.width = IMAGEBUTTON_BEVEL_W; - drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel); + drawPressableBevel(disp, ops, w->x, w->y, w->w, w->h, pressed, bgFace, colors); if (d->pixelData) { int32_t imgX = w->x + (w->w - d->imgW) / 2; diff --git a/src/widgets/kpunch/label/widgetLabel.c b/src/widgets/kpunch/label/widgetLabel.c index d1580c6..76ed9ff 100644 --- a/src/widgets/kpunch/label/widgetLabel.c +++ b/src/widgets/kpunch/label/widgetLabel.c @@ -59,16 +59,7 @@ void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap WidgetT *wgtLabel(WidgetT *parent, const char *text) { - WidgetT *w = widgetAlloc(parent, sTypeId); - - if (w) { - LabelDataT *d = calloc(1, sizeof(LabelDataT)); - w->data = d; - d->text = text ? strdup(text) : NULL; - w->accelKey = accelParse(text); - } - - return w; + return widgetAllocWithText(parent, sTypeId, sizeof(LabelDataT), text); } @@ -109,15 +100,10 @@ void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap textX = w->x + w->w - textW; } - if (!w->enabled) { - drawTextAccelEmbossed(d, ops, font, textX, textY, ld->text, colors); - return; - } - uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; - drawTextAccel(d, ops, font, textX, textY, ld->text, fg, bg, false); + drawWidgetTextAccel(d, ops, font, textX, textY, ld->text, fg, bg, false, w->enabled, colors); } diff --git a/src/widgets/kpunch/radio/widgetRadio.c b/src/widgets/kpunch/radio/widgetRadio.c index 0a56b34..6c53a2c 100644 --- a/src/widgets/kpunch/radio/widgetRadio.c +++ b/src/widgets/kpunch/radio/widgetRadio.c @@ -95,13 +95,10 @@ static void invalidateOldSelection(WidgetT *group, int32_t oldIdx) { WidgetT *wgtRadio(WidgetT *parent, const char *text) { - WidgetT *w = widgetAlloc(parent, sRadioTypeId); + WidgetT *w = widgetAllocWithText(parent, sRadioTypeId, sizeof(RadioDataT), text); - if (w) { - RadioDataT *d = calloc(1, sizeof(RadioDataT)); - w->data = d; - d->text = text ? strdup(text) : NULL; - w->accelKey = accelParse(text); + if (w && w->data) { + RadioDataT *d = (RadioDataT *)w->data; // Auto-assign index based on position in parent int32_t idx = 0; @@ -408,11 +405,7 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap int32_t labelY = w->y + (w->h - font->charHeight) / 2; int32_t labelW = textWidthAccel(font, rd->text); - if (!w->enabled) { - drawTextAccelEmbossed(d, ops, font, labelX, labelY, rd->text, colors); - } else { - drawTextAccel(d, ops, font, labelX, labelY, rd->text, fg, bg, false); - } + drawWidgetTextAccel(d, ops, font, labelX, labelY, rd->text, fg, bg, false, w->enabled, colors); if (w == sFocusedWidget) { drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg); diff --git a/src/widgets/kpunch/textInput/textinpt.bhs b/src/widgets/kpunch/textInput/textinpt.bhs index c887b03..f72464a 100644 --- a/src/widgets/kpunch/textInput/textinpt.bhs +++ b/src/widgets/kpunch/textInput/textinpt.bhs @@ -91,6 +91,7 @@ A multi-line text editing area. This is a DVX extension with no direct VB3 equiv .table Method Description ------ ----------- + AppendText text$ Append text to the end of the buffer and invalidate the widget. FindNext needle$, caseSensitive, forward Search for text. Returns True if found. GetWordAtCursor() Returns the word under the cursor. GoToLine line% Scroll to and position cursor at the given line. diff --git a/src/widgets/kpunch/textInput/textinpt.dhs b/src/widgets/kpunch/textInput/textinpt.dhs index f9cc772..6cc0b61 100644 --- a/src/widgets/kpunch/textInput/textinpt.dhs +++ b/src/widgets/kpunch/textInput/textinpt.dhs @@ -61,6 +61,7 @@ Header: widgets/textInpt.h .table Function Description -------- ----------- + void wgtTextAreaAppendText(w, text) Append text to the end of the buffer and invalidate the widget. void wgtTextAreaSetColorize(w, fn, ctx) Set a syntax colorization callback. The callback receives each line and fills a color index array. void wgtTextAreaGoToLine(w, line) Scroll to and place the cursor on the given line number. void wgtTextAreaSetAutoIndent(w, enable) Enable or disable automatic indentation on newline. diff --git a/src/widgets/kpunch/textInput/widgetTextInput.c b/src/widgets/kpunch/textInput/widgetTextInput.c index 4d1b7b0..b6805de 100644 --- a/src/widgets/kpunch/textInput/widgetTextInput.c +++ b/src/widgets/kpunch/textInput/widgetTextInput.c @@ -240,6 +240,7 @@ static int32_t visualColToOff(const char *buf, int32_t len, int32_t lineStart, i WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask); WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen); WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen); +void wgtTextAreaAppendText(WidgetT *w, const char *text); bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward); int32_t wgtTextAreaGetCursorLine(const WidgetT *w); int32_t wgtTextAreaGetWordAtCursor(const WidgetT *w, char *buf, int32_t bufSize); @@ -1518,6 +1519,37 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) { } +void wgtTextAreaAppendText(WidgetT *w, const char *text) { + if (!w || w->type != sTextAreaTypeId || !text) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + + if (!ta->buf) { + return; + } + + int32_t addLen = (int32_t)strlen(text); + int32_t room = ta->bufSize - 1 - ta->len; + + if (addLen > room) { + addLen = room; + } + + if (addLen <= 0) { + return; + } + + memcpy(ta->buf + ta->len, text, addLen); + ta->len += addLen; + ta->buf[ta->len] = '\0'; + ta->cachedLines = -1; + ta->cachedMaxLL = -1; + wgtInvalidate(w); +} + + bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward) { if (!w || w->type != sTextAreaTypeId || !needle || !needle[0]) { return false; @@ -3610,6 +3642,7 @@ static const WgtPropDescT sTextAreaProps[] = { }; static const WgtMethodDescT sTextAreaMethods[] = { + { "AppendText", WGT_SIG_STR, (void *)wgtTextAreaAppendText }, { "FindNext", WGT_SIG_STR_BOOL_BOOL, (void *)wgtTextAreaFindNext }, { "GetWordAtCursor", WGT_SIG_RET_STR, (void *)basGetWordAtCursor }, { "GoToLine", WGT_SIG_INT, (void *)wgtTextAreaGoToLine }, @@ -3640,7 +3673,7 @@ static const WgtIfaceT sIfaceTextArea = { .props = sTextAreaProps, .propCount = 1, .methods = sTextAreaMethods, - .methodCount = 10, + .methodCount = 11, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT, diff --git a/src/widgets/kpunch/timer/widgetTimer.c b/src/widgets/kpunch/timer/widgetTimer.c index dc0d5e6..47a564a 100644 --- a/src/widgets/kpunch/timer/widgetTimer.c +++ b/src/widgets/kpunch/timer/widgetTimer.c @@ -107,6 +107,11 @@ WidgetT *wgtTimer(WidgetT *parent, int32_t intervalMs, bool repeat) { w->visible = false; d->intervalMs = intervalMs; d->repeat = repeat; + // Match VB Timer default (Enabled=True on create). Callers can + // Stop() if they want it dormant. + d->running = true; + d->lastFire = clock(); + timerAddToActiveList(w); } return w; @@ -176,6 +181,12 @@ void wgtTimerStop(WidgetT *w) { void wgtUpdateTimers(void) { + static int32_t sUpdateCount = 0; + sUpdateCount++; + if (sUpdateCount <= 3 || sUpdateCount % 100 == 0) { + dvxLog("[T] wgtUpdateTimers call #%d activeCount=%d", + (int)sUpdateCount, (int)arrlen(sActiveTimers)); + } clock_t now = clock(); // Iterate backwards so arrdel doesn't skip entries @@ -192,6 +203,9 @@ void wgtUpdateTimers(void) { clock_t interval = (clock_t)d->intervalMs * CLOCKS_PER_SEC / 1000; if (elapsed >= interval) { + dvxLog("[T] timer tick: w=%p onChange=%p intervalMs=%d elapsed=%ld interval=%ld", + (void *)w, (void *)w->onChange, (int)d->intervalMs, + (long)elapsed, (long)interval); d->lastFire = now; if (w->onChange) {