Major BASIC runtime work.

This commit is contained in:
Scott Duensing 2026-04-20 20:20:05 -05:00
parent a8c38267bc
commit 1affec7e8c
62 changed files with 5458 additions and 1333 deletions

View file

@ -107,7 +107,7 @@ SDKDIR = bin/sdk
deploy-sdk: deploy-sdk:
@echo "Building 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) @# Core headers (libdvx public API)
@for f in src/libs/kpunch/libdvx/dvxApp.h src/libs/kpunch/libdvx/dvxTypes.h \ @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 \ src/libs/kpunch/libdvx/dvxWgt.h src/libs/kpunch/libdvx/dvxWgtP.h \
@ -137,6 +137,10 @@ deploy-sdk:
done done
@# BASIC include files @# BASIC include files
@cp src/include/basic/*.bas $(SDKDIR)/include/basic/ @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 @# README
@printf '%s\n' \ @printf '%s\n' \
'DVX SDK' \ 'DVX SDK' \
@ -154,6 +158,8 @@ deploy-sdk:
' sql/ SQLite database wrapper API' \ ' sql/ SQLite database wrapper API' \
' widget/ Per-widget public API headers' \ ' widget/ Per-widget public API headers' \
' basic/ BASIC include files (DECLARE LIBRARY modules)' \ ' basic/ BASIC include files (DECLARE LIBRARY modules)' \
' samples/' \
' basic/ Example BASIC projects (open in the DVX BASIC IDE)' \
'' \ '' \
'Requirements' \ 'Requirements' \
'------------' \ '------------' \

View file

@ -3188,6 +3188,7 @@ End Sub</code></pre>
<h2>Type-Specific Methods</h2> <h2>Type-Specific Methods</h2>
<pre> Method Description <pre> 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. FindNext needle$, caseSensitive, forward Search for text. Returns True if found.
GetWordAtCursor() Returns the word under the cursor. GetWordAtCursor() Returns the word under the cursor.
GoToLine line% Scroll to and position cursor at the given line. GoToLine line% Scroll to and position cursor at the given line.

View file

@ -6977,6 +6977,7 @@ WidgetT *page2 = wgtTabPage(tabs, &quot;Advanced&quot;);
<h3>API Functions (TextArea-specific)</h3> <h3>API Functions (TextArea-specific)</h3>
<pre> Function Description <pre> 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 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 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. void wgtTextAreaSetAutoIndent(w, enable) Enable or disable automatic indentation on newline.

View file

@ -39,7 +39,7 @@ C_APPS = progman clock dvxdemo cpanel dvxhelp
BASCOMP = ../../../bin/host/bascomp BASCOMP = ../../../bin/host/bascomp
# BASIC apps: each is a .dbp project in its own directory. # BASIC apps: each is a .dbp project in its own directory.
# BASIC-only notepad, imgview, etc. replace the old C versions. # 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) .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 $(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 $(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 $(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic
$(BASCOMP) basicdemo/basicdemo.dbp -o $@ -release $(BASCOMP) basdemo/basdemo.dbp -o $@ -release
$(OBJDIR): $(OBJDIR):
mkdir -p $(OBJDIR) mkdir -p $(OBJDIR)
@ -132,7 +132,7 @@ $(BINDIR)/kpunch/iconed: ; mkdir -p $@
$(BINDIR)/kpunch/notepad: ; mkdir -p $@ $(BINDIR)/kpunch/notepad: ; mkdir -p $@
$(BINDIR)/kpunch/imgview: ; mkdir -p $@ $(BINDIR)/kpunch/imgview: ; mkdir -p $@
$(BINDIR)/kpunch/resedit: ; mkdir -p $@ $(BINDIR)/kpunch/resedit: ; mkdir -p $@
$(BINDIR)/kpunch/basicdemo: ; mkdir -p $@ $(BINDIR)/kpunch/basdemo: ; mkdir -p $@
# Header dependencies # 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 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/imgview/imgview.app
rm -f $(BINDIR)/kpunch/dvxhelp/helpedit.app rm -f $(BINDIR)/kpunch/dvxhelp/helpedit.app
rm -f $(BINDIR)/kpunch/resedit/resedit.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 $(MAKE) -C dvxbasic clean

View file

@ -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 * **BASIC apps** -- `.dbp` projects compiled to `.app` files by the
host-side `bascomp`. The compiler links the BASIC bytecode into a host-side `bascomp`. The compiler links the BASIC bytecode into a
copy of `basstub.app`, which acts as the runtime host. See copy of `basstub.app`, which acts as the runtime host. See
`iconed/`, `notepad/`, `imgview/`, `basicdemo/`, `resedit/`, `iconed/`, `notepad/`, `imgview/`, `basdemo/`, `resedit/`,
`dvxhelp/helpedit/`. `dvxhelp/helpedit/`.
The rest of this document covers writing a C app against the SDK. 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`, GUI wrapper around `dvxres`: open a `.app` / `.wgt` / `.lib`,
browse its resources, add, replace, extract, or remove them. 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) | | Type | BASIC (main-loop via basstub) |
| Multi-instance | No | | Multi-instance | No |
@ -453,9 +453,9 @@ apps/kpunch/
imgview.dbp imgview.dbp
imgview.frm imgview.frm
ICON32.BMP ICON32.BMP
basicdemo/ basdemo/
basicdemo.dbp basdemo.dbp
basicdemo.frm basdemo.frm
ICON32.BMP ICON32.BMP
resedit/ resedit/
resedit.dbp resedit.dbp

View file

@ -11,7 +11,7 @@ File0 = ../../../include/basic/commdlg.bas
Count = 1 Count = 1
[Forms] [Forms]
File0 = basicdemo.frm File0 = basdemo.frm
Count = 1 Count = 1
[Settings] [Settings]

View file

@ -170,6 +170,21 @@ TYPE PointT
y AS INTEGER y AS INTEGER
END TYPE 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 ' OutArea helpers
@ -247,7 +262,7 @@ SUB mnuAbout_Click
msg = "DVX BASIC Feature Tour" + CHR$(10) + CHR$(10) msg = "DVX BASIC Feature Tour" + CHR$(10) + CHR$(10)
msg = msg + "A visual catalog of DVX BASIC language" msg = msg + "A visual catalog of DVX BASIC language"
msg = msg + " and runtime features." + CHR$(10) + CHR$(10) 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" MsgBox msg, vbOKOnly, "About"
END SUB END SUB
@ -740,17 +755,13 @@ END SUB
' Graphics demo (opens a second form with Canvas) ' Graphics demo (opens a second form with Canvas)
' ============================================================ ' ============================================================
DIM gfxWin AS LONG
gfxWin = 0
SUB mnuGraphics_Click SUB mnuGraphics_Click
IF gfxWin <> 0 THEN IF gfxWin <> 0 THEN
EXIT SUB EXIT SUB
END IF END IF
DIM frm AS LONG DIM frm AS LONG
SET frm = CreateForm("GraphicsForm", 360, 320) SET frm = CreateForm("GraphicsForm", 380, 360)
GraphicsForm.Caption = "Graphics Demo" GraphicsForm.Caption = "Graphics Demo"
gfxWin = frm gfxWin = frm
@ -844,6 +855,13 @@ SUB GfxClearCanvas
END SUB 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 SUB GraphicsForm_Unload
gfxWin = 0 gfxWin = 0
END SUB END SUB
@ -853,10 +871,6 @@ END SUB
' Dynamic form demo ' Dynamic form demo
' ============================================================ ' ============================================================
DIM dynForm AS LONG
dynForm = 0
SUB mnuDynamic_Click SUB mnuDynamic_Click
IF dynForm <> 0 THEN IF dynForm <> 0 THEN
EXIT SUB EXIT SUB
@ -892,10 +906,6 @@ SUB mnuDynamic_Click
END SUB END SUB
DIM dynCount AS INTEGER
dynCount = 0
SUB DynInc SUB DynInc
dynCount = dynCount + 1 dynCount = dynCount + 1
CountLabel.Caption = "Counter: " + STR$(dynCount) CountLabel.Caption = "Counter: " + STR$(dynCount)
@ -917,10 +927,6 @@ END SUB
' Timer demo ' Timer demo
' ============================================================ ' ============================================================
DIM timerWin AS LONG
timerWin = 0
SUB mnuTimer_Click SUB mnuTimer_Click
IF timerWin <> 0 THEN IF timerWin <> 0 THEN
EXIT SUB EXIT SUB
@ -944,10 +950,6 @@ SUB mnuTimer_Click
END SUB END SUB
DIM tickCount AS LONG
tickCount = 0
SUB TickHandler SUB TickHandler
tickCount = tickCount + 1 tickCount = tickCount + 1
TickLabel.Caption = "Ticks: " + STR$(tickCount) + " Time: " + TIME$ TickLabel.Caption = "Ticks: " + STR$(tickCount) + " Time: " + TIME$

View file

@ -49,7 +49,6 @@
#include "shellApp.h" #include "shellApp.h"
#include "stb_ds_wrap.h" #include "stb_ds_wrap.h"
#include <dirent.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -977,35 +976,33 @@ static void scanThemes(void) {
arrsetlen(sThemeEntries, 0); arrsetlen(sThemeEntries, 0);
arrsetlen(sThemeLabels, 0); arrsetlen(sThemeLabels, 0);
DIR *dir = opendir(THEME_DIR); char **names = dvxReadDir(THEME_DIR);
if (!dir) { if (!names) {
return; return;
} }
struct dirent *ent; int32_t n = (int32_t)arrlen(names);
while ((ent = readdir(dir)) != NULL) { for (int32_t i = 0; i < n; i++) {
char *dot = strrchr(ent->d_name, '.'); if (!dvxHasExt(names[i], THEME_EXT)) {
if (!dot || strcasecmp(dot, THEME_EXT) != 0) {
continue; continue;
} }
FileEntryT entry = {0}; FileEntryT entry = {0};
int32_t nameLen = (int32_t)(dot - ent->d_name); int32_t nameLen = (int32_t)strlen(names[i]) - (int32_t)strlen(THEME_EXT);
if (nameLen >= (int32_t)sizeof(entry.name)) { if (nameLen >= (int32_t)sizeof(entry.name)) {
nameLen = (int32_t)sizeof(entry.name) - 1; nameLen = (int32_t)sizeof(entry.name) - 1;
} }
memcpy(entry.name, ent->d_name, nameLen); memcpy(entry.name, names[i], nameLen);
entry.name[nameLen] = '\0'; entry.name[nameLen] = '\0';
snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", THEME_DIR, entry.name); snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", THEME_DIR, entry.name);
arrput(sThemeEntries, entry); arrput(sThemeEntries, entry);
} }
closedir(dir); dvxReadDirFree(names);
// Build label array now that sThemeEntries is stable // Build label array now that sThemeEntries is stable
for (int32_t i = 0; i < arrlen(sThemeEntries); i++) { for (int32_t i = 0; i < arrlen(sThemeEntries); i++) {
@ -1018,24 +1015,18 @@ static void scanWallpapers(void) {
arrsetlen(sWpaperEntries, 0); arrsetlen(sWpaperEntries, 0);
arrsetlen(sWpaperLabels, 0); arrsetlen(sWpaperLabels, 0);
DIR *dir = opendir(WPAPER_DIR); char **names = dvxReadDir(WPAPER_DIR);
if (!dir) { if (!names) {
return; return;
} }
struct dirent *ent; int32_t n = (int32_t)arrlen(names);
while ((ent = readdir(dir)) != NULL) { for (int32_t i = 0; i < n; i++) {
char *dot = strrchr(ent->d_name, '.'); if (!dvxHasExt(names[i], ".BMP") &&
!dvxHasExt(names[i], ".JPG") &&
if (!dot) { !dvxHasExt(names[i], ".PNG")) {
continue;
}
if (strcasecmp(dot, ".BMP") != 0 &&
strcasecmp(dot, ".JPG") != 0 &&
strcasecmp(dot, ".PNG") != 0) {
continue; continue;
} }
@ -1044,19 +1035,19 @@ static void scanWallpapers(void) {
// Length-clamped memcpy instead of strncpy/snprintf because GCC // Length-clamped memcpy instead of strncpy/snprintf because GCC
// warns about both when d_name (255) exceeds the buffer (64), // warns about both when d_name (255) exceeds the buffer (64),
// even though truncation is intentional and safe. // 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)) { if (nl >= (int32_t)sizeof(entry.name)) {
nl = (int32_t)sizeof(entry.name) - 1; nl = (int32_t)sizeof(entry.name) - 1;
} }
memcpy(entry.name, ent->d_name, nl); memcpy(entry.name, names[i], nl);
entry.name[nl] = '\0'; entry.name[nl] = '\0';
snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", WPAPER_DIR, entry.name); snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", WPAPER_DIR, entry.name);
arrput(sWpaperEntries, entry); arrput(sWpaperEntries, entry);
} }
closedir(dir); dvxReadDirFree(names);
// Build label array now that sWpaperEntries is stable // Build label array now that sWpaperEntries is stable
for (int32_t i = 0; i < arrlen(sWpaperEntries); i++) { for (int32_t i = 0; i < arrlen(sWpaperEntries); i++) {

View file

@ -43,7 +43,7 @@ HOSTDIR = ../../../../bin/host
DVXRES = $(HOSTDIR)/dvxres DVXRES = $(HOSTDIR)/dvxres
# Runtime library objects (VM + values + form runtime + serialization) # 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_TARGETDIR = $(LIBSDIR)/kpunch/basrt
RT_TARGET = $(RT_TARGETDIR)/basrt.lib RT_TARGET = $(RT_TARGETDIR)/basrt.lib
@ -68,6 +68,7 @@ TEST_VM = $(HOSTDIR)/test_vm
TEST_LEX = $(HOSTDIR)/test_lex TEST_LEX = $(HOSTDIR)/test_lex
TEST_QUICK = $(HOSTDIR)/test_quick TEST_QUICK = $(HOSTDIR)/test_quick
TEST_COMPACT = $(HOSTDIR)/test_compact TEST_COMPACT = $(HOSTDIR)/test_compact
TEST_SUITE = $(HOSTDIR)/test_suite
STB_DS_IMPL = ../../../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c STB_DS_IMPL = ../../../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c
PLATFORM_UTIL = ../../../libs/kpunch/libdvx/platform/dvxPlatformUtil.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_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_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_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) # 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) 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 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) $(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm $(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) $(TEST_VM): $(TEST_VM_SRCS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_VM_SRCS) -lm $(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) $(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(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 $@ $< $(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/serialize.o: runtime/serialize.c runtime/serialize.h runtime/vm.h | $(OBJDIR) $(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) $(OBJDIR)/basstub.o: stub/basstub.c runtime/vm.h runtime/serialize.h formrt/formrt.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(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 $@ $< $(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideMenuEditor.o: ide/ideMenuEditor.c ide/ideMenuEditor.h ide/ideDesigner.h | $(OBJDIR) $(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) $(OBJDIR)/ideProject.o: ide/ideProject.c ide/ideProject.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(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 $@ $< $(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideToolbox.o: ide/ideToolbox.c ide/ideToolbox.h ide/ideDesigner.h | $(OBJDIR) $(OBJDIR)/ideToolbox.o: ide/ideToolbox.c ide/ideToolbox.h ide/ideDesigner.h | $(OBJDIR)

View file

@ -46,10 +46,11 @@
#define BAS_INI_KEY_HELPFILE "HelpFile" #define BAS_INI_KEY_HELPFILE "HelpFile"
// [Settings] / [Modules] / [Forms] sections of a .dbp file // [Settings] / [Modules] / [Forms] sections of a .dbp file
#define BAS_INI_SECTION_SETTINGS "Settings" #define BAS_INI_SECTION_SETTINGS "Settings"
#define BAS_INI_KEY_STARTUPFORM "StartupForm" #define BAS_INI_KEY_STARTUPFORM "StartupForm"
#define BAS_INI_SECTION_MODULES "Modules" #define BAS_INI_KEY_OPTIONEXPLICIT "OptionExplicit"
#define BAS_INI_SECTION_FORMS "Forms" #define BAS_INI_SECTION_MODULES "Modules"
#define BAS_INI_SECTION_FORMS "Forms"
// ------------------------------------------------------------ // ------------------------------------------------------------
// Resource names inside a compiled .app DXE // Resource names inside a compiled .app DXE

View file

@ -208,12 +208,40 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
// Count SUB/FUNCTION entries // Count SUB/FUNCTION entries
int32_t procCount = 0; 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++) { for (int32_t i = 0; i < tab->count; i++) {
BasSymbolT *s = tab->symbols[i]; BasSymbolT *s = tab->symbols[i];
if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) { if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) {
procCount++; 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) { if (procCount == 0) {
@ -235,6 +263,8 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
BasProcEntryT *p = &mod->procs[idx++]; BasProcEntryT *p = &mod->procs[idx++];
strncpy(p->name, s->name, BAS_MAX_PROC_NAME - 1); strncpy(p->name, s->name, BAS_MAX_PROC_NAME - 1);
p->name[BAS_MAX_PROC_NAME - 1] = '\0'; 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->codeAddr = s->codeAddr;
p->paramCount = s->paramCount; p->paramCount = s->paramCount;
p->localCount = s->localCount; p->localCount = s->localCount;

View file

@ -169,6 +169,19 @@ int32_t basCompactBytecode(BasModuleT *mod) {
break; 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: { case OP_ON_ERROR: {
int16_t oldOff = readI16LE(oldCode + oldPc + 1); int16_t oldOff = readI16LE(oldCode + oldPc + 1);
@ -421,7 +434,7 @@ static int32_t opOperandSize(uint8_t op) {
case OP_RGB: case OP_RGB:
case OP_GET_RED: case OP_GET_GREEN: case OP_GET_BLUE: 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_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_MATH_TIMER: case OP_DATE_STR: case OP_TIME_STR:
case OP_SLEEP: case OP_ENVIRON: case OP_SLEEP: case OP_ENVIRON:
case OP_READ_DATA: case OP_RESTORE: case OP_READ_DATA: case OP_RESTORE:
@ -444,6 +457,7 @@ static int32_t opOperandSize(uint8_t op) {
return 0; return 0;
case OP_LOAD_ARRAY: case OP_STORE_ARRAY: case OP_LOAD_ARRAY: case OP_STORE_ARRAY:
case OP_PUSH_ARR_ADDR:
case OP_PRINT_SPC: case OP_FILE_OPEN: case OP_PRINT_SPC: case OP_FILE_OPEN:
case OP_CALL_METHOD: case OP_SHOW_FORM: case OP_CALL_METHOD: case OP_SHOW_FORM:
case OP_LBOUND: case OP_UBOUND: case OP_LBOUND: case OP_UBOUND:
@ -466,13 +480,13 @@ static int32_t opOperandSize(uint8_t op) {
return 2; return 2;
case OP_STORE_ARRAY_FIELD: case OP_STORE_ARRAY_FIELD:
case OP_FOR_INIT:
return 3; return 3;
case OP_PUSH_INT32: case OP_PUSH_FLT32: case OP_PUSH_INT32: case OP_PUSH_FLT32:
case OP_CALL: case OP_CALL:
return 4; return 4;
case OP_FOR_INIT:
case OP_FOR_NEXT: case OP_FOR_NEXT:
return 5; return 5;

View file

@ -338,10 +338,16 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) {
return lex->token.type; return lex->token.type;
} }
// Hex literal (&H...) // Numeric-base literals: &H hex, &O octal, &B binary. &B is an
if (c == '&' && upperChar(peekNext(lex)) == 'H') { // extension beyond classic QBASIC; it's convenient for bitmask
lex->token.type = tokenizeHexLiteral(lex); // work in the widget/graphics code.
return lex->token.type; 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 // Identifier or keyword
@ -621,30 +627,56 @@ static void skipWhitespace(BasLexerT *lex) {
static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) { static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
advance(lex); // skip & advance(lex); // skip &
advance(lex); // skip H 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; int32_t value = 0;
while (!atEnd(lex) && isxdigit((unsigned char)peek(lex))) { for (;;) {
char c = advance(lex); 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) { if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = c; lex->token.text[idx++] = c;
} }
int32_t digit; value = (value << shift) | 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;
} }
lex->token.text[idx] = '\0'; lex->token.text[idx] = '\0';

View file

@ -566,6 +566,16 @@ static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
continue; 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 // Find last underscore
char *underscore = strrchr(proc->name, '_'); char *underscore = strrchr(proc->name, '_');

View file

@ -180,7 +180,7 @@ typedef enum {
#define OP_GOSUB_RET 0x54 // pop PC from eval stack, jump (GOSUB return) #define OP_GOSUB_RET 0x54 // pop PC from eval stack, jump (GOSUB return)
#define OP_RET 0x55 // return from subroutine #define OP_RET 0x55 // return from subroutine
#define OP_RET_VAL 0x56 // return from function (value on stack) #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_NEXT 0x58 // [uint16 varIdx] [uint8 isLocal] [int16 loopTop]
#define OP_FOR_POP 0x59 // pop top FOR stack entry (for EXIT FOR) #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_STRF 0xB1 // STR$(n) -> string
#define OP_STR_HEX 0xB2 // HEX$(n) -> string #define OP_STR_HEX 0xB2 // HEX$(n) -> string
#define OP_STR_STRING 0xB3 // STRING$(n, char) -> 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 // Extended built-ins

View file

@ -69,6 +69,7 @@ static const BuiltinFuncT builtinFuncs[] = {
{"LEN", OP_STR_LEN, 1, 1, BAS_TYPE_INTEGER}, {"LEN", OP_STR_LEN, 1, 1, BAS_TYPE_INTEGER},
{"LTRIM$", OP_STR_LTRIM, 1, 1, BAS_TYPE_STRING}, {"LTRIM$", OP_STR_LTRIM, 1, 1, BAS_TYPE_STRING},
{"MID$", OP_STR_MID2, 2, 3, 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}, {"RIGHT$", OP_STR_RIGHT, 2, 2, BAS_TYPE_STRING},
{"RTRIM$", OP_STR_RTRIM, 1, 1, BAS_TYPE_STRING}, {"RTRIM$", OP_STR_RTRIM, 1, 1, BAS_TYPE_STRING},
{"SPACE$", OP_STR_SPACE, 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}, {"LOF", OP_FILE_LOF, 1, 1, BAS_TYPE_LONG},
// Conversion functions // Conversion functions
{"CBOOL", OP_CONV_BOOL, 1, 1, BAS_TYPE_BOOLEAN},
{"CDBL", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_DOUBLE}, {"CDBL", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_DOUBLE},
{"CINT", OP_CONV_FLT_INT, 1, 1, BAS_TYPE_INTEGER}, {"CINT", OP_CONV_FLT_INT, 1, 1, BAS_TYPE_INTEGER},
{"CLNG", OP_CONV_INT_LONG, 1, 1, BAS_TYPE_LONG}, {"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) { static bool check(BasParserT *p, BasTokenTypeE type) {
return p->lex.token.type == 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); strncpy(name, p->lex.token.text, sizeof(name) - 1);
name[sizeof(name) - 1] = '\0'; 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); BasSymbolT *sym = basSymTabFind(&p->sym, name);
if (!sym || sym->kind != SYM_VARIABLE || sym->isArray) { if (!sym || sym->kind != SYM_VARIABLE) {
parseExpression(p); parseExpression(p);
return; return;
} }
@ -517,6 +524,44 @@ static void emitByRefArg(BasParserT *p) {
advance(p); // consume the identifier 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 // The token after the identifier must be an argument delimiter
// (comma, rparen, newline, colon, EOF, ELSE) for this to be a // (comma, rparen, newline, colon, EOF, ELSE) for this to be a
// bare variable reference. Anything else (operator, dot, paren) // bare variable reference. Anything else (operator, dot, paren)
@ -1139,14 +1184,24 @@ static void parseAssignOrCall(BasParserT *p) {
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0'; memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p); // consume member name 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 // Special form methods: Show, Hide
if (strcasecmp(memberName, "Show") == 0) { if (strcasecmp(memberName, "Show") == 0) {
// name.Show [modal] // name.Show [modal]
// Push form name, LOAD_FORM, SHOW_FORM if (isVarRef) {
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); emitLoad(p, sym);
basEmit8(&p->cg, OP_PUSH_STR); } else {
basEmitU16(&p->cg, nameIdx); uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_LOAD_FORM); basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, nameIdx);
basEmit8(&p->cg, OP_LOAD_FORM);
}
uint8_t modal = 0; uint8_t modal = 0;
if (check(p, TOK_INT_LIT)) { if (check(p, TOK_INT_LIT)) {
if (p->lex.token.intVal != 0) { if (p->lex.token.intVal != 0) {
@ -1166,10 +1221,14 @@ static void parseAssignOrCall(BasParserT *p) {
} }
if (strcasecmp(memberName, "Hide") == 0) { if (strcasecmp(memberName, "Hide") == 0) {
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); if (isVarRef) {
basEmit8(&p->cg, OP_PUSH_STR); emitLoad(p, sym);
basEmitU16(&p->cg, nameIdx); } else {
basEmit8(&p->cg, OP_LOAD_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);
}
basEmit8(&p->cg, OP_HIDE_FORM); basEmit8(&p->cg, OP_HIDE_FORM);
return; return;
} }
@ -1178,16 +1237,33 @@ static void parseAssignOrCall(BasParserT *p) {
// Property assignment: CtrlName.Property = expr // Property assignment: CtrlName.Property = expr
advance(p); // consume = advance(p); // consume =
// Push ctrl ref: push form ref (NULL = current), push ctrl name, FIND_CTRL // Compile-time validation: if the host provided a validator
basEmit8(&p->cg, OP_PUSH_INT16); // (IDE), check that the property exists on the widget type.
basEmit16(&p->cg, 0); // Skip when `name` is a variable reference (dynamic ctrl we
BasValueT formNull; // can't statically type) or when the ctrl isn't in the map
memset(&formNull, 0, sizeof(formNull)); // (e.g. created via CreateControl at runtime).
// Use OP_PUSH_STR for ctrl name, then FIND_CTRL if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isPropValid) {
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx); if (wgtType && !p->validator->isPropValid(p->validator->ctx, wgtType, memberName)) {
basEmit8(&p->cg, OP_FIND_CTRL); 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 // Push property name
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName)); 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] // Method call: CtrlName.Method [args]
// Push ctrl ref // Same compile-time validation as above, but for methods.
basEmit8(&p->cg, OP_PUSH_INT16); if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isMethodValid) {
basEmit16(&p->cg, 0); const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR); if (wgtType && !p->validator->isMethodValid(p->validator->ctx, wgtType, memberName)) {
basEmitU16(&p->cg, ctrlNameIdx); char buf[BAS_PARSE_ERR_SCRATCH];
basEmit8(&p->cg, OP_FIND_CTRL); 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 // Push method name
uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName)); uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
@ -1412,13 +1504,52 @@ static void parseAssignOrCall(BasParserT *p) {
argc++; 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]; 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); error(p, buf);
return; 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 // External library SUB: emit OP_CALL_EXTERN
if (sym->isExtern) { if (sym->isExtern) {
basEmit8(&p->cg, OP_CALL_EXTERN); basEmit8(&p->cg, OP_CALL_EXTERN);
@ -2047,9 +2178,13 @@ static void parseDef(BasParserT *p) {
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p); advance(p);
uint8_t pdt = suffixToType(paramName); uint8_t pdt = suffixToType(paramName);
int32_t pUdtTypeId = -1;
if (match(p, TOK_AS)) { if (match(p, TOK_AS)) {
pdt = resolveTypeName(p); pdt = resolveTypeName(p);
if (pdt == BAS_TYPE_UDT) {
pUdtTypeId = p->lastUdtTypeId;
}
} }
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
@ -2060,6 +2195,7 @@ static void parseDef(BasParserT *p) {
paramSym->scope = SCOPE_LOCAL; paramSym->scope = SCOPE_LOCAL;
paramSym->index = basSymTabAllocSlot(&p->sym); paramSym->index = basSymTabAllocSlot(&p->sym);
paramSym->isDefined = true; paramSym->isDefined = true;
paramSym->udtTypeId = pUdtTypeId;
if (paramCount < BAS_MAX_PARAMS) { if (paramCount < BAS_MAX_PARAMS) {
paramTypes[paramCount] = pdt; paramTypes[paramCount] = pdt;
@ -2283,6 +2419,14 @@ static void parseDim(BasParserT *p) {
emitUdtInit(p, udtTypeId); emitUdtInit(p, udtTypeId);
emitStore(p, sym); 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); 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); basEmit8(&p->cg, OP_FOR_INIT);
basEmitU16(&p->cg, (uint16_t)loopVar->index); basEmitU16(&p->cg, (uint16_t)loopVar->index);
basEmit8(&p->cg, (uint8_t)loopVar->scope); basEmit8(&p->cg, (uint8_t)loopVar->scope);
int32_t skipOffsetPos = basCodePos(&p->cg);
basEmit16(&p->cg, 0); // placeholder
int32_t loopBody = basCodePos(&p->cg); 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)); int16_t backOffset = (int16_t)(loopBody - (basCodePos(&p->cg) + 2));
basEmit16(&p->cg, backOffset); 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 // Patch all EXIT FOR jumps to here
exitListPatch(&exitForList, p); exitListPatch(&exitForList, p);
exitForList = savedExitFor; exitForList = savedExitFor;
@ -2684,9 +2839,13 @@ static void parseFunction(BasParserT *p) {
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p); advance(p);
uint8_t pdt = suffixToType(paramName); uint8_t pdt = suffixToType(paramName);
int32_t pUdtTypeId = -1;
if (match(p, TOK_AS)) { if (match(p, TOK_AS)) {
pdt = resolveTypeName(p); pdt = resolveTypeName(p);
if (pdt == BAS_TYPE_UDT) {
pUdtTypeId = p->lastUdtTypeId;
}
} }
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
@ -2697,6 +2856,7 @@ static void parseFunction(BasParserT *p) {
paramSym->scope = SCOPE_LOCAL; paramSym->scope = SCOPE_LOCAL;
paramSym->index = basSymTabAllocSlot(&p->sym); paramSym->index = basSymTabAllocSlot(&p->sym);
paramSym->isDefined = true; paramSym->isDefined = true;
paramSym->udtTypeId = pUdtTypeId;
if (paramCount < BAS_MAX_PARAMS) { if (paramCount < BAS_MAX_PARAMS) {
paramTypes[paramCount] = pdt; paramTypes[paramCount] = pdt;
@ -4172,13 +4332,50 @@ static void parsePrimary(BasParserT *p) {
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0'; memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p); advance(p);
// Emit: push NULL (current form), push ctrl name, FIND_CTRL bool isVarRef2 = (sym != NULL && sym->kind == SYM_VARIABLE);
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0); // Compile-time validation for expression-context reads /
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name)); // method returns. Peek ahead: if '(' follows, memberName
basEmit8(&p->cg, OP_PUSH_STR); // is a method; otherwise it's a property read. Skip when
basEmitU16(&p->cg, ctrlNameIdx); // the host didn't attach a validator or the ctrl is dynamic.
basEmit8(&p->cg, OP_FIND_CTRL); 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 followed by '(', this is a method call with args
if (check(p, TOK_LPAREN)) { if (check(p, TOK_LPAREN)) {
@ -5583,9 +5780,13 @@ static void parseSub(BasParserT *p) {
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0'; paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p); advance(p);
uint8_t pdt = suffixToType(paramName); uint8_t pdt = suffixToType(paramName);
int32_t pUdtTypeId = -1;
if (match(p, TOK_AS)) { if (match(p, TOK_AS)) {
pdt = resolveTypeName(p); pdt = resolveTypeName(p);
if (pdt == BAS_TYPE_UDT) {
pUdtTypeId = p->lastUdtTypeId;
}
} }
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt); BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
@ -5596,6 +5797,7 @@ static void parseSub(BasParserT *p) {
paramSym->scope = SCOPE_LOCAL; paramSym->scope = SCOPE_LOCAL;
paramSym->index = basSymTabAllocSlot(&p->sym); paramSym->index = basSymTabAllocSlot(&p->sym);
paramSym->isDefined = true; paramSym->isDefined = true;
paramSym->udtTypeId = pUdtTypeId;
if (paramCount < BAS_MAX_PARAMS) { if (paramCount < BAS_MAX_PARAMS) {
paramTypes[paramCount] = pdt; paramTypes[paramCount] = pdt;

View file

@ -49,6 +49,27 @@
#define BAS_PARSE_ERROR_LEN 1024 #define BAS_PARSE_ERROR_LEN 1024
#define BAS_PARSE_ERR_SCRATCH 512 #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 { typedef struct {
BasLexerT lex; BasLexerT lex;
BasCodeGenT cg; BasCodeGenT cg;
@ -66,6 +87,8 @@ typedef struct {
// Per-form init block tracking // Per-form init block tracking
int32_t formInitJmpAddr; // code position of JMP to patch (-1 = none) int32_t formInitJmpAddr; // code position of JMP to patch (-1 = none)
int32_t formInitCodeStart; // code position where init block starts (-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; } BasParserT;
// ============================================================ // ============================================================
@ -75,6 +98,11 @@ typedef struct {
// Initialize parser with source text. // Initialize parser with source text.
void basParserInit(BasParserT *p, const char *source, int32_t sourceLen); 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. // Parse the entire source and generate p-code.
// Returns true on success, false on error (check p->error). // Returns true on success, false on error (check p->error).
bool basParse(BasParserT *p); bool basParse(BasParserT *p);

View file

@ -83,7 +83,13 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
sym->dataType = dataType; sym->dataType = dataType;
sym->isDefined = true; 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); strncpy(sym->formName, tab->formScopeName, BAS_MAX_SYMBOL_NAME - 1);
sym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0'; sym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
} }

File diff suppressed because it is too large Load diff

View file

@ -87,6 +87,15 @@ typedef struct BasControlT {
int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls) int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls)
BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES]; BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES];
int32_t eventOverrideCount; 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; } BasControlT;
// ============================================================ // ============================================================
@ -153,6 +162,10 @@ typedef struct {
int32_t frmCacheCount; int32_t frmCacheCount;
BasCfmCacheT *cfmCache; // stb_ds array of compiled form binaries BasCfmCacheT *cfmCache; // stb_ds array of compiled form binaries
int32_t cfmCacheCount; 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; } BasFormRtT;
// ============================================================ // ============================================================
@ -209,7 +222,28 @@ void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *d
// ---- Widget creation ---- // ---- 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 ---- // ---- Form window creation ----

View file

@ -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 <stdlib.h>
#include <string.h>
#include <strings.h>
#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);
}

View file

@ -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 <Control>", "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 <stdbool.h>
#include <stdint.h>
// Callbacks supplied by the consumer. Any field may be NULL.
typedef struct FrmParserCbsT {
void *userData;
// "Begin Form <name>". 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 <name>" 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 <Type> <Name>" 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

View file

@ -28,6 +28,7 @@
#include "ideDesigner.h" #include "ideDesigner.h"
#include "../formrt/formrt.h" #include "../formrt/formrt.h"
#include "../formrt/frmParser.h"
#include "dvxDraw.h" #include "dvxDraw.h"
#include "dvxVideo.h" #include "dvxVideo.h"
#include "dvxWm.h" #include "dvxWm.h"
@ -61,16 +62,39 @@ static const char *FORM_DEFAULT_EVENT = "Load";
// ============================================================ // ============================================================
// dsgnCreateDesignWidget is declared in ideDesigner.h (non-static) // 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 const char *getPropValue(const DsgnControlT *ctrl, const char *name);
static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y); 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 DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y);
static void rebuildWidgets(DsgnStateT *ds); 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 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 setPropValue(DsgnControlT *ctrl, const char *name, const char *value);
static void syncWidgetGeom(DsgnControlT *ctrl); 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) { void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize) {
// Look up the name prefix from the widget interface descriptor. // Look up the name prefix from the widget interface descriptor.
// Falls back to the type name itself if no prefix is registered. // 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 // Create a real DVX widget for design-time display. Uses the shared
// logic in formrt.c createWidget(). // 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) { WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) {
const char *wgtName = resolveTypeName(vbTypeName); const char *wgtName = resolveTypeName(vbTypeName);
@ -171,50 +197,7 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) {
return NULL; return NULL;
} }
const WgtIfaceT *iface = wgtGetIface(wgtName); return createWidgetByIface(wgtGetIface(wgtName), api, parent, false);
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);
}
}
} }
@ -320,23 +303,8 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
const char *val = getPropValue(ctrl, p->name); const char *val = getPropValue(ctrl, p->name);
if (!val) { if (val) {
continue; wgtApplyPropFromString(w, p, val);
}
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);
} }
} }
} }
@ -427,259 +395,27 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
snprintf(form->name, DSGN_MAX_NAME, "Form1"); snprintf(form->name, DSGN_MAX_NAME, "Form1");
snprintf(form->caption, DSGN_MAX_TEXT, "Form1"); snprintf(form->caption, DSGN_MAX_TEXT, "Form1");
DsgnControlT *curCtrl = NULL; DsgnFrmLoadCtxT ctx;
DsgnMenuItemT *curMenuItem = NULL; memset(&ctx, 0, sizeof(ctx));
bool inForm = false; ctx.form = form;
bool inMenu = false; ctx.curMenuItemIdx = -1;
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
// Parent name stack for nesting (index 0 = form level) FrmParserCbsT cbs;
char parentStack[8][DSGN_MAX_NAME]; memset(&cbs, 0, sizeof(cbs));
int32_t nestDepth = 0; cbs.userData = &ctx;
parentStack[0][0] = '\0'; 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; if (!frmParse(source, sourceLen, &cbs)) {
const char *end = source + sourceLen; free(form);
return false;
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); }
}
}
} }
ds->form = form; 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) { void dsgnNewForm(DsgnStateT *ds, const char *name) {
dsgnFree(ds); 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. // 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) { 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); 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; continue;
} }
if (p->type == WGT_IFACE_ENUM && p->enumNames) { // Skip STRING props here: saveControls only emits
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); // the iface-known scalar types. String values come
const char *name = NULL; // through ctrl->props[] (custom props).
if (p->type == WGT_IFACE_STRING) {
continue;
}
for (int32_t en = 0; p->enumNames[en]; en++) { char valBuf[DSGN_MAX_TEXT];
if (en == v) {
name = p->enumNames[en];
break;
}
}
if (name) { if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, name); pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, valBuf);
}
} 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");
} }
} }
} }

View file

@ -69,6 +69,7 @@
#include "../compiler/strip.h" #include "../compiler/strip.h"
#include "../runtime/serialize.h" #include "../runtime/serialize.h"
#include "../formrt/formrt.h" #include "../formrt/formrt.h"
#include "../formrt/frmParser.h"
#include "dvxRes.h" #include "dvxRes.h"
#include "../../../../libs/kpunch/sql/dvxSql.h" #include "../../../../libs/kpunch/sql/dvxSql.h"
#include "../runtime/vm.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) { static bool compileProject(void) {
// Save all dirty files before compiling if Save on Run is enabled // Save all dirty files before compiling if Save on Run is enabled
if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) { if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) {
@ -1647,7 +1896,22 @@ static bool compileProject(void) {
} }
basParserInit(parser, src, srcLen); 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)) { if (!basParse(parser)) {
// Translate global error line to local file/line for display // Translate global error line to local file/line for display
@ -1715,10 +1979,12 @@ static bool compileProject(void) {
basParserFree(parser); basParserFree(parser);
free(parser); free(parser);
free(concatBuf); free(concatBuf);
arrfree(validatorCtx.entries);
return false; return false;
} }
free(concatBuf); free(concatBuf);
arrfree(validatorCtx.entries);
BasModuleT *mod = basParserBuildModule(parser); BasModuleT *mod = basParserBuildModule(parser);
basParserFree(parser); basParserFree(parser);
@ -2062,19 +2328,15 @@ static void dsgnCopySelected(void) {
continue; continue;
} }
if (p->type == WGT_IFACE_ENUM && p->enumNames) { // Skip STRING props -- custom props handle those.
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); if (p->type == WGT_IFACE_STRING) {
const char *name = (v >= 0 && p->enumNames[v]) ? p->enumNames[v] : NULL; continue;
}
if (name) { char valBuf[DSGN_MAX_TEXT];
pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, name);
} if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
} else if (p->type == WGT_IFACE_INT) { pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, valBuf);
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");
} }
} }
} }
@ -2305,23 +2567,8 @@ static void dsgnPasteControl(void) {
} }
} }
if (!val) { if (val) {
continue; wgtApplyPropFromString(ctrl.widget, p, val);
}
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);
} }
} }
} }
@ -8077,7 +8324,7 @@ static void showPreferencesDialog(void) {
sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming"); sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming");
wgtCheckboxSetChecked(sPrefsDlg.renameSkipComments, prefsGetBool(sPrefs, "editor", "renameSkipComments", true)); 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)); wgtCheckboxSetChecked(sPrefsDlg.optionExplicit, prefsGetBool(sPrefs, "editor", "optionExplicit", false));
WidgetT *tabRow = wgtHBox(edFrame); WidgetT *tabRow = wgtHBox(edFrame);

View file

@ -51,6 +51,7 @@
#include "dvxWm.h" #include "dvxWm.h"
#include "box/box.h" #include "box/box.h"
#include "button/button.h" #include "button/button.h"
#include "checkbox/checkbox.h"
#include "dropdown/dropdown.h" #include "dropdown/dropdown.h"
#include "image/image.h" #include "image/image.h"
#include "label/label.h" #include "label/label.h"
@ -98,6 +99,7 @@ static struct {
WidgetT *copyright; WidgetT *copyright;
WidgetT *description; WidgetT *description;
WidgetT *startupForm; WidgetT *startupForm;
WidgetT *optionExplicit;
const char **formNames; // stb_ds array of form name strings for startup dropdown const char **formNames; // stb_ds array of form name strings for startup dropdown
WidgetT *helpFileInput; WidgetT *helpFileInput;
WidgetT *iconPreview; 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); val = prefsGetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, NULL);
if (val) { snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", val); } 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); prefsClose(h);
prj->dirty = false; prj->dirty = false;
return true; 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->version, sizeof(prj->version), "%s", prefsGetString(prefs, "defaults", "version", "1.0"));
snprintf(prj->copyright, sizeof(prj->copyright), "%s", prefsGetString(prefs, "defaults", "copyright", "")); snprintf(prj->copyright, sizeof(prj->copyright), "%s", prefsGetString(prefs, "defaults", "copyright", ""));
snprintf(prj->description, sizeof(prj->description), "%s", prefsGetString(prefs, "defaults", "description", "")); 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; return false;
} }
WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 380, false); WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 410, false);
if (!win) { if (!win) {
return false; return false;
@ -739,6 +744,10 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
hlpBrowse->onClick = ppdOnBrowseHelp; 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) // Description: label above, textarea below (matches Preferences layout)
wgtLabel(root, "Description:"); wgtLabel(root, "Description:");
sPpd.description = wgtTextArea(root, PRJ_MAX_DESC); 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; prj->dirty = true;
} }
@ -948,6 +958,7 @@ bool prjSave(const PrjStateT *prj) {
// [Settings] section // [Settings] section
prefsSetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, prj->startupForm); 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); bool ok = prefsSaveAs(h, prj->projectPath);
prefsClose(h); prefsClose(h);

View file

@ -79,6 +79,7 @@ typedef struct {
char description[PRJ_MAX_DESC]; char description[PRJ_MAX_DESC];
char iconPath[DVX_MAX_PATH]; // relative path to icon BMP char iconPath[DVX_MAX_PATH]; // relative path to icon BMP
char helpFile[DVX_MAX_PATH]; // relative path to .hlp file char helpFile[DVX_MAX_PATH]; // relative path to .hlp file
bool optionExplicit; // require DIM before use
PrjFileT *files; // stb_ds dynamic array PrjFileT *files; // stb_ds dynamic array
int32_t fileCount; int32_t fileCount;
PrjSourceMapT *sourceMap; // stb_ds dynamic array PrjSourceMapT *sourceMap; // stb_ds dynamic array

View file

@ -28,6 +28,7 @@
// property value to edit it via an InputBox dialog. // property value to edit it via an InputBox dialog.
#include "ideProperties.h" #include "ideProperties.h"
#include "../formrt/frmParser.h"
#include "dvxDlg.h" #include "dvxDlg.h"
#include "dvxWm.h" #include "dvxWm.h"
#include "box/box.h" #include "box/box.h"
@ -616,7 +617,7 @@ static void onPropDblClick(WidgetT *w) {
if (propType == PROP_TYPE_BOOL) { if (propType == PROP_TYPE_BOOL) {
// Toggle boolean on double-click -- no input box // 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"); snprintf(newValue, sizeof(newValue), "%s", cur ? "False" : "True");
} else if (propType == PROP_TYPE_ENUM) { } else if (propType == PROP_TYPE_ENUM) {
// Enum: cycle to next value on double-click // Enum: cycle to next value on double-click
@ -923,7 +924,7 @@ static void onPropDblClick(WidgetT *w) {
ctrl->widget->weight = ctrl->weight; ctrl->widget->weight = ctrl->weight;
} }
} else if (strcasecmp(propName, "Visible") == 0) { } else if (strcasecmp(propName, "Visible") == 0) {
bool val = (strcasecmp(newValue, "True") == 0); bool val = frmParseBool(newValue);
if (ctrl->widget) { if (ctrl->widget) {
wgtSetVisible(ctrl->widget, val); wgtSetVisible(ctrl->widget, val);
@ -934,7 +935,7 @@ static void onPropDblClick(WidgetT *w) {
cascadeToChildren(sDs, ctrl->name, val, en); cascadeToChildren(sDs, ctrl->name, val, en);
} }
} else if (strcasecmp(propName, "Enabled") == 0) { } else if (strcasecmp(propName, "Enabled") == 0) {
bool val = (strcasecmp(newValue, "True") == 0); bool val = frmParseBool(newValue);
if (ctrl->widget) { if (ctrl->widget) {
wgtSetEnabled(ctrl->widget, val); wgtSetEnabled(ctrl->widget, val);
@ -965,7 +966,9 @@ static void onPropDblClick(WidgetT *w) {
} }
if (p->type == WGT_IFACE_STRING) { 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; bool found = false;
for (int32_t j = 0; j < ctrl->propCount; j++) { 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); ((void (*)(WidgetT *, const char *))p->setFn)(ctrl->widget, ctrl->props[ctrl->propCount].value);
ctrl->propCount++; ctrl->propCount++;
} }
} else if (p->type == WGT_IFACE_ENUM && p->enumNames) { } else {
int32_t enumVal = 0; wgtApplyPropFromString(ctrl->widget, p, newValue);
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);
} }
ifaceHandled = true; ifaceHandled = true;
@ -1068,7 +1058,7 @@ static void onPropDblClick(WidgetT *w) {
dvxSetTitle(sPrpCtx, sDs->formWin, winTitle); dvxSetTitle(sPrpCtx, sDs->formWin, winTitle);
} }
} else if (strcasecmp(propName, "AutoSize") == 0) { } else if (strcasecmp(propName, "AutoSize") == 0) {
sDs->form->autoSize = (strcasecmp(newValue, "True") == 0); sDs->form->autoSize = frmParseBool(newValue);
if (sDs->form->autoSize && sDs->formWin) { if (sDs->form->autoSize && sDs->formWin) {
dvxFitWindow(sPrpCtx, sDs->formWin); dvxFitWindow(sPrpCtx, sDs->formWin);
@ -1076,14 +1066,14 @@ static void onPropDblClick(WidgetT *w) {
sDs->form->height = sDs->formWin->h; sDs->form->height = sDs->formWin->h;
} }
} else if (strcasecmp(propName, "Resizable") == 0) { } else if (strcasecmp(propName, "Resizable") == 0) {
sDs->form->resizable = (strcasecmp(newValue, "True") == 0); sDs->form->resizable = frmParseBool(newValue);
if (sDs->formWin) { if (sDs->formWin) {
sDs->formWin->resizable = sDs->form->resizable; sDs->formWin->resizable = sDs->form->resizable;
dvxInvalidateWindow(sPrpCtx, sDs->formWin); dvxInvalidateWindow(sPrpCtx, sDs->formWin);
} }
} else if (strcasecmp(propName, "Centered") == 0) { } else if (strcasecmp(propName, "Centered") == 0) {
sDs->form->centered = (strcasecmp(newValue, "True") == 0); sDs->form->centered = frmParseBool(newValue);
} else if (strcasecmp(propName, "Left") == 0) { } else if (strcasecmp(propName, "Left") == 0) {
sDs->form->left = atoi(newValue); sDs->form->left = atoi(newValue);
} else if (strcasecmp(propName, "Top") == 0) { } else if (strcasecmp(propName, "Top") == 0) {
@ -1473,29 +1463,14 @@ void prpRefresh(DsgnStateT *ds) {
continue; continue;
} }
// Read the current value from the widget // Read the current value from the widget. Enum values
if (p->type == WGT_IFACE_STRING && p->getFn) { // that don't map to a name are shown as "?".
const char *s = ((const char *(*)(WidgetT *))p->getFn)(ctrl->widget); char valBuf[DSGN_MAX_TEXT];
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;
for (int32_t k = 0; p->enumNames[k]; k++) { if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
if (k == v) { addPropRow(p->name, valBuf);
name = p->enumNames[k]; } else if (p->type == WGT_IFACE_ENUM) {
break; addPropRow(p->name, "?");
}
}
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");
} else { } else {
addPropRow(p->name, ""); addPropRow(p->name, "");
} }

View file

@ -274,6 +274,10 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
char *name = rStr(&r); char *name = rStr(&r);
snprintf(p->name, BAS_MAX_PROC_NAME, "%s", name); snprintf(p->name, BAS_MAX_PROC_NAME, "%s", name);
free(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; return mod;
} }
@ -323,6 +340,7 @@ void basModuleFree(BasModuleT *mod) {
free(mod->procs); free(mod->procs);
free(mod->formVarInfo); free(mod->formVarInfo);
free(mod->globalInits);
free(mod->debugVars); free(mod->debugVars);
if (mod->debugUdtDefs) { if (mod->debugUdtDefs) {
@ -411,6 +429,7 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
bufWriteU8(&b, p->returnType); bufWriteU8(&b, p->returnType);
bufWriteU8(&b, p->isFunction ? 1 : 0); bufWriteU8(&b, p->isFunction ? 1 : 0);
bufWriteStr(&b, p->name); bufWriteStr(&b, p->name);
bufWriteStr(&b, p->formName);
} }
// Form variable info (runtime-essential for per-form variable allocation) // 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); 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; *outLen = b.len;
return b.buf; return b.buf;
} }

View file

@ -68,11 +68,19 @@ void basUdtFree(BasUdtT *udt);
BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount); BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount);
BasUdtT *basUdtRef(BasUdtT *udt); BasUdtT *basUdtRef(BasUdtT *udt);
void basUdtUnref(BasUdtT *udt); void basUdtUnref(BasUdtT *udt);
BasValueT basValBool(bool v);
int32_t basValCompare(BasValueT a, BasValueT b); int32_t basValCompare(BasValueT a, BasValueT b);
int32_t basValCompareCI(BasValueT a, BasValueT b); int32_t basValCompareCI(BasValueT a, BasValueT b);
BasValueT basValCopy(BasValueT v);
BasValueT basValDouble(double v);
BasStringT *basValFormatString(BasValueT v); BasStringT *basValFormatString(BasValueT v);
BasValueT basValInteger(int16_t v);
bool basValIsTruthy(BasValueT v); bool basValIsTruthy(BasValueT v);
BasValueT basValLong(int32_t v);
BasValueT basValObject(void *obj);
uint8_t basValPromoteType(uint8_t a, uint8_t b); uint8_t basValPromoteType(uint8_t a, uint8_t b);
void basValRelease(BasValueT *v);
BasValueT basValSingle(float v);
BasValueT basValString(BasStringT *s); BasValueT basValString(BasStringT *s);
BasValueT basValStringFromC(const char *text); BasValueT basValStringFromC(const char *text);
BasValueT basValToBool(BasValueT v); 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) { int32_t basValCompare(BasValueT a, BasValueT b) {
// String comparison // String comparison
if (a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) { 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) { BasStringT *basValFormatString(BasValueT v) {
char buf[64]; 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) { bool basValIsTruthy(BasValueT v) {
switch (v.type) { switch (v.type) {
case BAS_TYPE_INTEGER: 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) { uint8_t basValPromoteType(uint8_t a, uint8_t b) {
// String stays string (concat, not arithmetic) // String stays string (concat, not arithmetic)
if (a == BAS_TYPE_STRING || b == BAS_TYPE_STRING) { 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 basValString(BasStringT *s) {
BasValueT val; BasValueT val;
val.type = BAS_TYPE_STRING; val.type = BAS_TYPE_STRING;

View file

@ -31,8 +31,6 @@
#ifndef DVXBASIC_VALUES_H #ifndef DVXBASIC_VALUES_H
#define DVXBASIC_VALUES_H #define DVXBASIC_VALUES_H
#include "../compiler/opcodes.h" // BAS_TYPE_*
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
@ -159,89 +157,22 @@ struct BasValueTag {
}; };
}; };
// Create values -- trivial constructors inlined so they're fast in vm.c's // Create values. Out-of-line so they can be resolved via the DXE
// hot path (PUSH_INT16, PUSH_TRUE, etc.). // dynamic-symbol table when basrt.lib is loaded.
static inline BasValueT basValInteger(int16_t v) { BasValueT basValInteger(int16_t v);
BasValueT val; BasValueT basValLong(int32_t v);
val.type = BAS_TYPE_INTEGER; BasValueT basValSingle(float v);
val.intVal = v; BasValueT basValDouble(double v);
return val; BasValueT basValBool(bool v);
} BasValueT basValObject(void *obj);
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;
}
BasValueT basValString(BasStringT *s); BasValueT basValString(BasStringT *s);
BasValueT basValStringFromC(const char *text); BasValueT basValStringFromC(const char *text);
// Copy a value (increments string/array/udt refcount if applicable). // Copy a value (increments string/array/udt refcount if applicable).
// Inlined so the hot-path case (integer/float/bool) is a single struct BasValueT basValCopy(BasValueT v);
// 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);
}
return v; // Release a value (decrements refcount if applicable).
} void basValRelease(BasValueT *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;
}
}
// Convert a value to a specific type. Returns the converted value. // Convert a value to a specific type. Returns the converted value.
// The original is NOT released -- caller manages lifetime. // The original is NOT released -- caller manages lifetime.

View file

@ -97,8 +97,9 @@ bool basVmCallSub(BasVmT *vm, int32_t codeAddr) {
// Push a call frame that returns to an invalid address (sentinel) // Push a call frame that returns to an invalid address (sentinel)
// We detect completion when callDepth drops back to savedCallDepth // We detect completion when callDepth drops back to savedCallDepth
BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
frame->returnPc = savedPc; frame->returnPc = savedPc;
frame->localCount = BAS_VM_MAX_LOCALS; frame->localCount = BAS_VM_MAX_LOCALS;
frame->errorHandler = 0;
memset(frame->locals, 0, sizeof(frame->locals)); memset(frame->locals, 0, sizeof(frame->locals));
// Jump to the SUB // Jump to the SUB
@ -131,8 +132,9 @@ bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, i
bool savedRunning = vm->running; bool savedRunning = vm->running;
BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
frame->returnPc = savedPc; frame->returnPc = savedPc;
frame->localCount = BAS_VM_MAX_LOCALS; frame->localCount = BAS_VM_MAX_LOCALS;
frame->errorHandler = 0;
memset(frame->locals, 0, sizeof(frame->locals)); memset(frame->locals, 0, sizeof(frame->locals));
// Set arguments as locals (parameter 0 = local 0, etc.) // 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; bool savedRunning = vm->running;
BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
frame->returnPc = savedPc; frame->returnPc = savedPc;
frame->localCount = BAS_VM_MAX_LOCALS; frame->localCount = BAS_VM_MAX_LOCALS;
frame->errorHandler = 0;
memset(frame->locals, 0, sizeof(frame->locals)); memset(frame->locals, 0, sizeof(frame->locals));
for (int32_t i = 0; i < argCount && i < BAS_VM_MAX_LOCALS; i++) { 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) { void basVmLoadModule(BasVmT *vm, BasModuleT *module) {
vm->module = module; vm->module = module;
vm->pc = module->entryPoint; 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 (result != BAS_VM_OK) {
// If an error handler is set and this is a trappable error, // If an error handler is set and this is a trappable error,
// jump to the handler instead of stopping execution // unwind call frames until we find the SUB whose ON ERROR
if (vm->errorHandler != 0 && !vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) { // GOTO registered a handler, then jump there. Without this
vm->errorPc = savedPc; // unwind an error raised inside a called SUB would fire the
vm->errorNextPc = vm->pc; // outer SUB's handler but OP_RET would then pop the inner
vm->inErrorHandler = true; // frame and resume after the call site (effectively RESUME
vm->pc = vm->errorHandler; // NEXT semantics), which is not what ON ERROR promises.
continue; 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; vm->running = false;
@ -840,8 +892,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
} }
BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
frame->returnPc = vm->pc; frame->returnPc = vm->pc;
frame->localCount = BAS_VM_MAX_LOCALS; frame->localCount = BAS_VM_MAX_LOCALS;
frame->errorHandler = 0;
// Zero all local slots // Zero all local slots
memset(frame->locals, 0, sizeof(frame->locals)); 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; vm->pc = addr;
break; break;
} }
@ -870,7 +926,24 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&frame->locals[i]); basValRelease(&frame->locals[i]);
} }
bool hadHandler = (frame->errorHandler != 0);
frame->errorHandler = 0;
vm->pc = frame->returnPc; 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; break;
} }
@ -894,8 +967,19 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&frame->locals[i]); basValRelease(&frame->locals[i]);
} }
bool hadHandler = (frame->errorHandler != 0);
frame->errorHandler = 0;
vm->pc = frame->returnPc; 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)) { if (!push(vm, retVal)) {
basValRelease(&retVal); basValRelease(&retVal);
return BAS_VM_STACK_OVERFLOW; return BAS_VM_STACK_OVERFLOW;
@ -918,8 +1002,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
} }
case OP_FOR_INIT: { case OP_FOR_INIT: {
uint16_t varIdx = readUint16(vm); uint16_t varIdx = readUint16(vm);
uint8_t scopeTag = readUint8(vm); uint8_t scopeTag = readUint8(vm);
int16_t skipOffset = readInt16(vm);
BasValueT stepVal; BasValueT stepVal;
BasValueT limitVal; BasValueT limitVal;
@ -940,6 +1025,43 @@ BasVmResultE basVmStep(BasVmT *vm) {
fs->limit = limitVal; fs->limit = limitVal;
fs->step = stepVal; fs->step = stepVal;
fs->loopTop = vm->pc; 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; break;
} }
@ -990,7 +1112,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
// Increment: var = var + step. Preserve the loop variable's // Increment: var = var + step. Preserve the loop variable's
// original type so extern calls that pass it as an int32_t // 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 varVal = basValToNumber(*varSlot);
double stepVal = basValToNumber(fs->step); double stepVal = basValToNumber(fs->step);
double limVal = basValToNumber(fs->limit); double limVal = basValToNumber(fs->limit);
@ -999,7 +1125,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(varSlot); 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); *varSlot = basValInteger((int16_t)varVal);
} else if (varType == BAS_TYPE_LONG) { } else if (varType == BAS_TYPE_LONG) {
*varSlot = basValLong((int32_t)varVal); *varSlot = basValLong((int32_t)varVal);
@ -1639,6 +1769,40 @@ BasVmResultE basVmStep(BasVmT *vm) {
break; 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: { case OP_STR_STRING: {
// STRING$(n, char) // STRING$(n, char)
BasValueT charVal; BasValueT charVal;
@ -1827,7 +1991,19 @@ BasVmResultE basVmStep(BasVmT *vm) {
case OP_ON_ERROR: { case OP_ON_ERROR: {
int16_t handler = readInt16(vm); 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; break;
} }
@ -2032,6 +2208,58 @@ BasVmResultE basVmStep(BasVmT *vm) {
break; 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: { case OP_STORE_ARRAY: {
uint8_t dims = readUint8(vm); 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 na = basValToNumber(a);
double nb = basValToNumber(b); double nb = basValToNumber(b);
basValRelease(&a); basValRelease(&a);
@ -3682,8 +3919,12 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
break; break;
} }
// Return appropriate type // Return appropriate type. An op that would normally produce an
if (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT) { // 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) { if (result >= (double)INT16_MIN && result <= (double)INT16_MAX) {
push(vm, basValInteger((int16_t)result)); push(vm, basValInteger((int16_t)result));
} else if (result >= (double)INT32_MIN && result <= (double)INT32_MAX) { } 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 // memcpy with constant size is folded to a single load by the compiler and
// faster than memcpy because the compiler can emit a single 16-bit load. // is alignment-safe (bytecode operands aren't guaranteed 2-byte aligned).
static inline int16_t readInt16(BasVmT *vm) { 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); vm->pc += sizeof(int16_t);
return val; return val;
} }
static inline uint16_t readUint16(BasVmT *vm) { 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); vm->pc += sizeof(uint16_t);
return val; return val;
} }
@ -5364,6 +5607,7 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
bool hadBreakpoint = false; bool hadBreakpoint = false;
while (vm->running && vm->callDepth > savedCallDepth) { while (vm->running && vm->callDepth > savedCallDepth) {
int32_t stepPc = vm->pc; // save for ON ERROR dispatch
BasVmResultE result = basVmStep(vm); BasVmResultE result = basVmStep(vm);
if (result == BAS_VM_HALTED) { 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) { 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->pc = savedPc;
vm->callDepth = savedCallDepth; vm->callDepth = savedCallDepth;
vm->running = savedRunning; vm->running = savedRunning;

View file

@ -262,6 +262,7 @@ typedef struct {
int32_t returnPc; // instruction to return to int32_t returnPc; // instruction to return to
int32_t baseSlot; // base index in locals array int32_t baseSlot; // base index in locals array
int32_t localCount; // number of locals in this frame 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]; BasValueT locals[BAS_VM_MAX_LOCALS];
} BasCallFrameT; } BasCallFrameT;
@ -293,12 +294,13 @@ typedef struct {
#define BAS_MAX_PROC_NAME 64 #define BAS_MAX_PROC_NAME 64
typedef struct { typedef struct {
char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved) char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved)
int32_t codeAddr; // entry point in code[] char formName[BAS_MAX_PROC_NAME]; // owning form (for form-scope vars), "" if global
int32_t paramCount; // number of parameters int32_t codeAddr; // entry point in code[]
int32_t localCount; // number of local variables (for debugger) int32_t paramCount; // number of parameters
uint8_t returnType; // BAS_TYPE_* (0 for SUB) int32_t localCount; // number of local variables (for debugger)
bool isFunction; // true = FUNCTION, false = SUB uint8_t returnType; // BAS_TYPE_* (0 for SUB)
bool isFunction; // true = FUNCTION, false = SUB
} BasProcEntryT; } BasProcEntryT;
// Debug UDT field definition (preserved for debugger watch) // Debug UDT field definition (preserved for debugger watch)
@ -340,6 +342,16 @@ typedef struct {
// Compiled module (output of the compiler) // 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 { typedef struct {
uint8_t *code; // p-code bytecode uint8_t *code; // p-code bytecode
int32_t codeLen; int32_t codeLen;
@ -353,6 +365,8 @@ typedef struct {
int32_t procCount; int32_t procCount;
BasFormVarInfoT *formVarInfo; // per-form variable counts BasFormVarInfoT *formVarInfo; // per-form variable counts
int32_t formVarInfoCount; int32_t formVarInfoCount;
BasGlobalInitT *globalInits; // runtime global type init (survives stripping)
int32_t globalInitCount;
BasDebugVarT *debugVars; // variable names for debugger BasDebugVarT *debugVars; // variable names for debugger
int32_t debugVarCount; int32_t debugVarCount;
BasDebugUdtDefT *debugUdtDefs; // UDT type definitions for debugger BasDebugUdtDefT *debugUdtDefs; // UDT type definitions for debugger

View file

@ -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 *helpFile = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, "");
const char *startupForm = prefsGetString(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, ""); const char *startupForm = prefsGetString(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, "");
(void)startupForm; // used implicitly by stub's basFormRtLoadAllForms (void)startupForm; // used implicitly by stub's basFormRtLoadAllForms
bool optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
// Derive output path // Derive output path
char outBuf[DVX_MAX_PATH]; char outBuf[DVX_MAX_PATH];
@ -355,6 +356,7 @@ int main(int argc, char **argv) {
} }
basParserInit(parser, concatBuf, pos); basParserInit(parser, concatBuf, pos);
parser->optionExplicit = optionExplicit;
if (!basParse(parser)) { if (!basParse(parser)) {
fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error); fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error);

View file

@ -170,10 +170,15 @@ int32_t appMain(DxeAppContextT *ctx) {
basVmSetInputCallback(vm, stubInput, NULL); basVmSetInputCallback(vm, stubInput, NULL);
basVmSetDoEventsCallback(vm, stubDoEvents, NULL); basVmSetDoEventsCallback(vm, stubDoEvents, NULL);
// Set app paths // Set app paths. App.Path is the .app's directory (read-only on CD);
snprintf(vm->appPath, DVX_MAX_PATH, "%s", ctx->appDir); // App.Config and App.Data are writable subdirectories created on
snprintf(vm->appConfig, DVX_MAX_PATH, "%s", ctx->appDir); // demand. The IDE does the same split for project debugging, so
snprintf(vm->appData, DVX_MAX_PATH, "%s", ctx->appDir); // 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) // Set extern call callbacks (required for DECLARE LIBRARY functions)
BasExternCallbacksT extCb; BasExternCallbacksT extCb;

File diff suppressed because it is too large Load diff

View file

@ -190,7 +190,7 @@ static void test4(void) {
// PUSH 5 (limit); PUSH 1 (step) // PUSH 5 (limit); PUSH 1 (step)
emit8(OP_PUSH_INT16); emit16(5); emit8(OP_PUSH_INT16); emit16(5);
emit8(OP_PUSH_INT16); emit16(1); 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) // Loop body start (record PC for FOR_NEXT offset)
int32_t loopBody = sCodeLen; int32_t loopBody = sCodeLen;

View file

@ -63,7 +63,6 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
#include <dirent.h>
#include <sys/stat.h> #include <sys/stat.h>
#include "dvxMem.h" #include "dvxMem.h"
#include "stb_ds_wrap.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. // Collect all entries first, close the handle, then process.
// DOS has limited file handles; keeping a DIR open while // DOS has limited file handles; keeping a DIR open while
// opening .app files for resource loading causes failures. // opening .app files for resource loading causes failures.
DIR *dir = opendir(dirPath); char **names = dvxReadDir(dirPath);
if (!dir) { if (!names) {
if (sAppCount == 0) { if (sAppCount == 0) {
dvxLog("Progman: %s directory not found", dirPath); dvxLog("Progman: %s directory not found", dirPath);
} }
@ -458,19 +457,6 @@ static void scanAppsDirRecurse(const char *dirPath) {
return; 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); int32_t nEntries = (int32_t)arrlen(names);
for (int32_t i = 0; i < nEntries; i++) { 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)) { if (stat(fullPath, &st) == 0 && S_ISDIR(st.st_mode)) {
scanAppsDirRecurse(fullPath); scanAppsDirRecurse(fullPath);
free(names[i]);
continue; continue;
} }
int32_t len = (int32_t)strlen(names[i]); if (!dvxHasExt(names[i], ".app") || strcasecmp(names[i], "progman.app") == 0) {
if (len < 5 || strcasecmp(names[i] + len - 4, ".app") != 0 || strcasecmp(names[i], "progman.app") == 0) {
free(names[i]);
continue; continue;
} }
@ -497,6 +479,7 @@ static void scanAppsDirRecurse(const char *dirPath) {
snprintf(newEntry.path, sizeof(newEntry.path), "%s", fullPath); snprintf(newEntry.path, sizeof(newEntry.path), "%s", fullPath);
// Default name from filename (without .app extension) // Default name from filename (without .app extension)
int32_t len = (int32_t)strlen(names[i]);
int32_t nameLen = len - 4; int32_t nameLen = len - 4;
if (nameLen >= SHELL_APP_NAME_MAX) { if (nameLen >= SHELL_APP_NAME_MAX) {
@ -524,11 +507,10 @@ static void scanAppsDirRecurse(const char *dirPath) {
arrput(sAppFiles, newEntry); arrput(sAppFiles, newEntry);
sAppCount = (int32_t)arrlen(sAppFiles); sAppCount = (int32_t)arrlen(sAppFiles);
free(names[i]);
dvxUpdate(sAc); dvxUpdate(sAc);
} }
arrfree(names); dvxReadDirFree(names);
} }

View file

@ -270,12 +270,15 @@ SUB mnuAddText_Click
DIM rName AS STRING DIM rName AS STRING
rName = basInputBox2("Add Text Resource", "Resource name:", "") rName = basInputBox2("Add Text Resource", "Resource name:", "")
IF rName = "" THEN IF basInputCancelled OR rName = "" THEN
EXIT SUB EXIT SUB
END IF END IF
DIM text AS STRING DIM text AS STRING
text = basInputBox2("Add Text Resource", "Text value:", "") text = basInputBox2("Add Text Resource", "Text value:", "")
IF basInputCancelled THEN
EXIT SUB
END IF
IF ResAddText(filePath, rName, text) THEN IF ResAddText(filePath, rName, text) THEN
ReopenAndRefresh ReopenAndRefresh
@ -293,7 +296,7 @@ SUB mnuAddFile_Click
DIM rName AS STRING DIM rName AS STRING
rName = basInputBox2("Add File Resource", "Resource name:", "") rName = basInputBox2("Add File Resource", "Resource name:", "")
IF rName = "" THEN IF basInputCancelled OR rName = "" THEN
EXIT SUB EXIT SUB
END IF END IF
@ -352,6 +355,10 @@ SUB mnuEditText_Click
DIM newText AS STRING DIM newText AS STRING
newText = basInputBox2("Edit Text Resource", "Value for '" + rName + "':", oldText) newText = basInputBox2("Edit Text Resource", "Value for '" + rName + "':", oldText)
IF basInputCancelled THEN
EXIT SUB
END IF
IF ResAddText(filePath, rName, newText) THEN IF ResAddText(filePath, rName, newText) THEN
ReopenAndRefresh ReopenAndRefresh
LblStatus.Caption = "Updated: " + rName LblStatus.Caption = "Updated: " + rName

View file

@ -46,6 +46,10 @@ DECLARE LIBRARY "basrt"
' Show a modal text input box. Returns entered text, or "" if cancelled. ' 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 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 ' Show a choice dialog with a listbox. items$ is pipe-delimited
' (e.g. "Red|Green|Blue"). Returns chosen index (0-based), or -1. ' (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 DECLARE FUNCTION basChoiceDialog(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL items AS STRING, BYVAL defaultIdx AS INTEGER) AS INTEGER

View file

@ -1193,6 +1193,14 @@ static void dispatchEvents(AppContextT *ctx) {
int32_t menuId = item->id; int32_t menuId = item->id;
WindowT *win = findWindowById(ctx, ctx->popup.windowId); WindowT *win = findWindowById(ctx, ctx->popup.windowId);
closeAllPopups(ctx); 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) { if (win && win->onMenu) {
WIN_CALLBACK(ctx, win, win->onMenu(win, menuId)); 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 (!(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]; WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
if (win->onMouse) { if (win->onMouse) {

View file

@ -82,6 +82,10 @@ typedef struct AppContextT {
int32_t prevMouseX; int32_t prevMouseX;
int32_t prevMouseY; int32_t prevMouseY;
int32_t prevMouseButtons; 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 // Double-click detection for minimized window icons: timestamps and
// window IDs track whether two clicks land on the same icon within // window IDs track whether two clicks land on the same icon within
// the system double-click interval. // the system double-click interval.

View file

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

View file

@ -116,6 +116,24 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
// Windows 3.x focus rectangle convention. // 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); 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). // 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); void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color);

View file

@ -142,6 +142,13 @@ void widgetDestroyChildren(WidgetT *w);
// Allocation // Allocation
WidgetT *widgetAlloc(WidgetT *parent, int32_t type); 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 // Focus management
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after); WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after);
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before); WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before);

View file

@ -312,6 +312,22 @@ const char *dvxSkipWs(const char *s);
// new length. buf may be NULL or empty. // new length. buf may be NULL or empty.
int32_t dvxTrimRight(char *buf); 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 // The platform's native directory separator as a string literal. Use
// with string-literal concatenation in format strings or path constants: // with string-literal concatenation in format strings or path constants:
// snprintf(buf, sz, "%s" DVX_PATH_SEP "%s", dir, name); // snprintf(buf, sz, "%s" DVX_PATH_SEP "%s", dir, name);

View file

@ -2312,12 +2312,15 @@ DXE_EXPORT_TABLE(sDxeExportTable)
// --- dvx helpers (lives in dvx.exe, used by all modules) --- // --- dvx helpers (lives in dvx.exe, used by all modules) ---
DXE_EXPORT(dvxCalloc) DXE_EXPORT(dvxCalloc)
DXE_EXPORT(dvxFree) DXE_EXPORT(dvxFree)
DXE_EXPORT(dvxHasExt)
DXE_EXPORT(dvxLog) DXE_EXPORT(dvxLog)
DXE_EXPORT(dvxMalloc) DXE_EXPORT(dvxMalloc)
DXE_EXPORT(dvxMemAppIdPtr) DXE_EXPORT(dvxMemAppIdPtr)
DXE_EXPORT(dvxMemGetAppUsage) DXE_EXPORT(dvxMemGetAppUsage)
DXE_EXPORT(dvxMemResetApp) DXE_EXPORT(dvxMemResetApp)
DXE_EXPORT(dvxMemSnapshotLoad) DXE_EXPORT(dvxMemSnapshotLoad)
DXE_EXPORT(dvxReadDir)
DXE_EXPORT(dvxReadDirFree)
DXE_EXPORT(dvxRealloc) DXE_EXPORT(dvxRealloc)
DXE_EXPORT(dvxSkipWs) DXE_EXPORT(dvxSkipWs)
DXE_EXPORT(dvxStrdup) DXE_EXPORT(dvxStrdup)

View file

@ -29,11 +29,14 @@
// can still use them. // can still use them.
#include "dvxPlat.h" #include "dvxPlat.h"
#include "thirdparty/stb_ds_wrap.h"
#include <ctype.h> #include <ctype.h>
#include <dirent.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <strings.h>
#include <unistd.h> #include <unistd.h>
#ifdef __DJGPP__ #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) { int32_t platformChdir(const char *path) {
#ifdef __DJGPP__ #ifdef __DJGPP__
if (path[0] && path[1] == ':') { if (path[0] && path[1] == ':') {

View file

@ -41,9 +41,12 @@
// which doesn't map cleanly to an arena pattern. // which doesn't map cleanly to an arena pattern.
#include "dvxWgtP.h" #include "dvxWgtP.h"
#include "dvxDraw.h"
#include "dvxPlat.h" #include "dvxPlat.h"
#include "stb_ds_wrap.h" #include "stb_ds_wrap.h"
#include <stdlib.h>
#include <string.h>
#include <time.h> #include <time.h>
// ============================================================ // ============================================================
@ -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 widgetCountVisibleChildren(const WidgetT *w) {
int32_t count = 0; int32_t count = 0;

View file

@ -38,7 +38,6 @@
#include "../tools/hlpcCompile.h" #include "../tools/hlpcCompile.h"
#include <ctype.h> #include <ctype.h>
#include <dirent.h>
#include <dlfcn.h> #include <dlfcn.h>
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
@ -175,49 +174,41 @@ static void collectGlobFiles(char ***outFiles, const char *pattern, const char *
dirPart[1] = '\0'; dirPart[1] = '\0';
} }
DIR *d = opendir(dirPart); char **names = dvxReadDir(dirPart);
if (!d) { if (!names) {
return; 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); int32_t nEntries = (int32_t)arrlen(names);
for (int32_t i = 0; i < nEntries; i++) { 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]; char fullPath[DVX_MAX_PATH];
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPart, names[i]); snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPart, names[i]);
struct stat st; struct stat st;
if (stat(fullPath, &st) == 0) { if (stat(fullPath, &st) != 0) {
if (S_ISDIR(st.st_mode)) { continue;
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));
}
}
} }
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. // Count total progress steps across all .hcf files under a directory.
static int32_t countTotalHelpSteps(const char *dirPath) { static int32_t countTotalHelpSteps(const char *dirPath) {
DIR *dir = opendir(dirPath); char **names = dvxReadDir(dirPath);
if (!dir) { if (!names) {
return 0; 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 total = 0;
int32_t nEntries = (int32_t)arrlen(names); int32_t nEntries = (int32_t)arrlen(names);
for (int32_t i = 0; i < nEntries; i++) { for (int32_t i = 0; i < nEntries; i++) {
if (names[i][0] == '.') {
continue;
}
char fullPath[DVX_MAX_PATH]; char fullPath[DVX_MAX_PATH];
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]); snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
struct stat st; struct stat st;
if (stat(fullPath, &st) == 0) { if (stat(fullPath, &st) != 0) {
if (S_ISDIR(st.st_mode)) { continue;
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);
}
}
} }
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; 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. // Recursively scan a directory for .hcf files and process each one.
static void processHcfDir(const char *dirPath) { static void processHcfDir(const char *dirPath) {
DIR *dir = opendir(dirPath); char **names = dvxReadDir(dirPath);
if (!dir) { if (!names) {
return; 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); int32_t nEntries = (int32_t)arrlen(names);
for (int32_t i = 0; i < nEntries; i++) { for (int32_t i = 0; i < nEntries; i++) {
if (names[i][0] == '.') {
continue;
}
char fullPath[DVX_MAX_PATH]; char fullPath[DVX_MAX_PATH];
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]); snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
struct stat st; struct stat st;
if (stat(fullPath, &st) == 0) { if (stat(fullPath, &st) != 0) {
if (S_ISDIR(st.st_mode)) { continue;
processHcfDir(fullPath);
} else {
int32_t nameLen = (int32_t)strlen(names[i]);
if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) {
processHcf(fullPath, dirPath);
}
}
} }
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. // Collect all entries first, close the handle, then process.
// DOS has limited file handles; keeping a DIR open during // DOS has limited file handles; keeping a DIR open during
// recursion or stat() causes intermittent failures. // recursion or stat() causes intermittent failures.
DIR *dir = opendir(dirPath); char **names = dvxReadDir(dirPath);
if (!dir) { if (!names) {
return; return;
} }
char **names = NULL; int32_t count = (int32_t)arrlen(names);
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);
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
char path[DVX_MAX_PATH]; char path[DVX_MAX_PATH];
snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dirPath, names[i]); snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
int32_t nameLen = (int32_t)strlen(names[i]); if (dvxHasExt(names[i], ext)) {
if (nameLen > extLen && strcasecmp(names[i] + nameLen - extLen, ext) == 0) {
ModuleT mod; ModuleT mod;
memset(&mod, 0, sizeof(mod)); memset(&mod, 0, sizeof(mod));
snprintf(mod.path, sizeof(mod.path), "%s", path); 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); scanDir(path, ext, mods);
} }
} }
free(names[i]);
} }
arrfree(names); dvxReadDirFree(names);
} }

View file

@ -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 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 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) $(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) $(CC) $(CFLAGS) -o $@ dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
$(HOSTDIR)/mkicon: mkicon.c bmpDraw.c bmpDraw.h | $(HOSTDIR) $(HOSTDIR)/mkicon: mkicon.c bmpDraw.c bmpDraw.h | $(HOSTDIR)
$(CC) $(CFLAGS) -o $@ mkicon.c bmpDraw.c -lm $(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) $(HOSTDIR)/bmp2raw: bmp2raw.c | $(HOSTDIR)
$(CC) $(CFLAGS) -o $@ bmp2raw.c $(CC) $(CFLAGS) -o $@ bmp2raw.c
$(HOSTDIR)/dvxhlpc: dvxhlpc.c ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) | $(HOSTDIR) $(HOSTDIR)/dvxhlpc: dvxhlpc.c ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(HOSTDIR)
$(CC) $(CFLAGS) -o $@ dvxhlpc.c $(PLATFORM_UTIL) $(CC) $(CFLAGS) -o $@ dvxhlpc.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
$(HOSTDIR): $(HOSTDIR):
mkdir -p $(HOSTDIR) mkdir -p $(HOSTDIR)
@ -74,8 +75,8 @@ $(BINDIR):
$(CONFIGDIR): $(CONFIGDIR):
mkdir -p $(CONFIGDIR) mkdir -p $(CONFIGDIR)
$(SYSTEMDIR)/DVXHLPC.EXE: dvxhlpc.c hlpcCompile.h ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) | $(SYSTEMDIR) $(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) $(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxhlpc.exe dvxhlpc.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
$(EXE2COFF) $(SYSTEMDIR)/dvxhlpc.exe $(EXE2COFF) $(SYSTEMDIR)/dvxhlpc.exe
cat $(CWSDSTUB) $(SYSTEMDIR)/dvxhlpc > $@ cat $(CWSDSTUB) $(SYSTEMDIR)/dvxhlpc > $@
rm -f $(SYSTEMDIR)/dvxhlpc $(SYSTEMDIR)/dvxhlpc.exe rm -f $(SYSTEMDIR)/dvxhlpc $(SYSTEMDIR)/dvxhlpc.exe
@ -87,8 +88,8 @@ $(SYSTEMDIR)/DVXHLPC.EXE: dvxhlpc.c hlpcCompile.h ../apps/kpunch/dvxhelp/hlpform
../../obj/loader: ../../obj/loader:
mkdir -p ../../obj/loader mkdir -p ../../obj/loader
$(SYSTEMDIR)/DVXRES.EXE: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) | $(SYSTEMDIR) $(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) $(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxres.exe dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
$(EXE2COFF) $(SYSTEMDIR)/dvxres.exe $(EXE2COFF) $(SYSTEMDIR)/dvxres.exe
cat $(CWSDSTUB) $(SYSTEMDIR)/dvxres > $@ cat $(CWSDSTUB) $(SYSTEMDIR)/dvxres > $@
rm -f $(SYSTEMDIR)/dvxres $(SYSTEMDIR)/dvxres.exe rm -f $(SYSTEMDIR)/dvxres $(SYSTEMDIR)/dvxres.exe

View file

@ -73,15 +73,11 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
WidgetT *wgtFrame(WidgetT *parent, const char *title) { WidgetT *wgtFrame(WidgetT *parent, const char *title) {
WidgetT *w = widgetAlloc(parent, sFrameTypeId); WidgetT *w = widgetAllocWithText(parent, sFrameTypeId, sizeof(FrameDataT), title);
if (w) { if (w && w->data) {
FrameDataT *fd = calloc(1, sizeof(FrameDataT)); FrameDataT *fd = (FrameDataT *)w->data;
fd->title = title ? strdup(title) : NULL; fd->style = FrameInE;
fd->style = FrameInE;
fd->color = 0;
w->data = fd;
w->accelKey = accelParse(title);
} }
return w; return w;
@ -168,11 +164,7 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
rectFill(d, ops, titleX - 2, titleY, rectFill(d, ops, titleX - 2, titleY,
titleW + 4, font->charHeight, bg); titleW + 4, font->charHeight, bg);
if (!w->enabled) { drawWidgetTextAccel(d, ops, font, titleX, titleY, fd->title, fg, bg, true, w->enabled, colors);
drawTextAccelEmbossed(d, ops, font, titleX, titleY, fd->title, colors);
} else {
drawTextAccel(d, ops, font, titleX, titleY, fd->title, fg, bg, true);
}
} }
} }

View file

@ -70,16 +70,7 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
WidgetT *wgtButton(WidgetT *parent, const char *text) { WidgetT *wgtButton(WidgetT *parent, const char *text) {
WidgetT *w = widgetAlloc(parent, sTypeId); return widgetAllocWithText(parent, sTypeId, sizeof(ButtonDataT), text);
if (w) {
ButtonDataT *d = calloc(1, sizeof(ButtonDataT));
w->data = d;
d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text);
}
return w;
} }
@ -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 fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace; uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
BevelStyleT bevel; drawPressableBevel(d, ops, w->x, w->y, w->w, w->h, w->pressed, bgFace, colors);
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);
int32_t textW = textWidthAccel(font, bd->text); int32_t textX;
int32_t textX = w->x + (w->w - textW) / 2; int32_t textY;
int32_t textY = w->y + (w->h - font->charHeight) / 2; calcCenteredText(font, w->x, w->y, w->w, w->h, bd->text, &textX, &textY);
if (w->pressed) { if (w->pressed) {
textX += BUTTON_PRESS_OFFSET; textX += BUTTON_PRESS_OFFSET;
textY += BUTTON_PRESS_OFFSET; textY += BUTTON_PRESS_OFFSET;
} }
if (!w->enabled) { drawWidgetTextAccel(d, ops, font, textX, textY, bd->text, fg, bgFace, true, w->enabled, colors);
drawTextAccelEmbossed(d, ops, font, textX, textY, bd->text, colors);
} else {
drawTextAccel(d, ops, font, textX, textY, bd->text, fg, bgFace, true);
}
if (w == sFocusedWidget) { if (w == sFocusedWidget) {
int32_t off = w->pressed ? BUTTON_PRESS_OFFSET : 0; int32_t off = w->pressed ? BUTTON_PRESS_OFFSET : 0;

View file

@ -1045,7 +1045,7 @@ static const WgtMethodDescT sMethods[] = {
}; };
static const WgtIfaceT sIface = { static const WgtIfaceT sIface = {
.basName = "PictureBox", .basName = "Canvas",
.props = NULL, .props = NULL,
.propCount = 0, .propCount = 0,
.methods = sMethods, .methods = sMethods,

View file

@ -67,16 +67,7 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
WidgetT *wgtCheckbox(WidgetT *parent, const char *text) { WidgetT *wgtCheckbox(WidgetT *parent, const char *text) {
WidgetT *w = widgetAlloc(parent, sTypeId); return widgetAllocWithText(parent, sTypeId, sizeof(CheckboxDataT), text);
if (w) {
CheckboxDataT *d = calloc(1, sizeof(CheckboxDataT));
w->data = d;
d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text);
}
return w;
} }
@ -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 labelY = w->y + (w->h - font->charHeight) / 2;
int32_t labelW = textWidthAccel(font, cd->text); int32_t labelW = textWidthAccel(font, cd->text);
if (!w->enabled) { drawWidgetTextAccel(d, ops, font, labelX, labelY, cd->text, fg, bg, false, w->enabled, colors);
drawTextAccelEmbossed(d, ops, font, labelX, labelY, cd->text, colors);
} else {
drawTextAccel(d, ops, font, labelX, labelY, cd->text, fg, bg, false);
}
if (w == sFocusedWidget) { if (w == sFocusedWidget) {
drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg); drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg);

View file

@ -211,12 +211,7 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, con
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace; uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
bool pressed = w->pressed && w->enabled; bool pressed = w->pressed && w->enabled;
BevelStyleT bevel; drawPressableBevel(disp, ops, w->x, w->y, w->w, w->h, pressed, bgFace, colors);
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);
if (d->pixelData) { if (d->pixelData) {
int32_t imgX = w->x + (w->w - d->imgW) / 2; int32_t imgX = w->x + (w->w - d->imgW) / 2;

View file

@ -59,16 +59,7 @@ void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
WidgetT *wgtLabel(WidgetT *parent, const char *text) { WidgetT *wgtLabel(WidgetT *parent, const char *text) {
WidgetT *w = widgetAlloc(parent, sTypeId); return widgetAllocWithText(parent, sTypeId, sizeof(LabelDataT), text);
if (w) {
LabelDataT *d = calloc(1, sizeof(LabelDataT));
w->data = d;
d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text);
}
return w;
} }
@ -109,15 +100,10 @@ void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
textX = w->x + w->w - textW; 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 fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; 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);
} }

View file

@ -95,13 +95,10 @@ static void invalidateOldSelection(WidgetT *group, int32_t oldIdx) {
WidgetT *wgtRadio(WidgetT *parent, const char *text) { WidgetT *wgtRadio(WidgetT *parent, const char *text) {
WidgetT *w = widgetAlloc(parent, sRadioTypeId); WidgetT *w = widgetAllocWithText(parent, sRadioTypeId, sizeof(RadioDataT), text);
if (w) { if (w && w->data) {
RadioDataT *d = calloc(1, sizeof(RadioDataT)); RadioDataT *d = (RadioDataT *)w->data;
w->data = d;
d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text);
// Auto-assign index based on position in parent // Auto-assign index based on position in parent
int32_t idx = 0; 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 labelY = w->y + (w->h - font->charHeight) / 2;
int32_t labelW = textWidthAccel(font, rd->text); int32_t labelW = textWidthAccel(font, rd->text);
if (!w->enabled) { drawWidgetTextAccel(d, ops, font, labelX, labelY, rd->text, fg, bg, false, w->enabled, colors);
drawTextAccelEmbossed(d, ops, font, labelX, labelY, rd->text, colors);
} else {
drawTextAccel(d, ops, font, labelX, labelY, rd->text, fg, bg, false);
}
if (w == sFocusedWidget) { if (w == sFocusedWidget) {
drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg); drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg);

View file

@ -91,6 +91,7 @@ A multi-line text editing area. This is a DVX extension with no direct VB3 equiv
.table .table
Method Description 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. FindNext needle$, caseSensitive, forward Search for text. Returns True if found.
GetWordAtCursor() Returns the word under the cursor. GetWordAtCursor() Returns the word under the cursor.
GoToLine line% Scroll to and position cursor at the given line. GoToLine line% Scroll to and position cursor at the given line.

View file

@ -61,6 +61,7 @@ Header: widgets/textInpt.h
.table .table
Function Description 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 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 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. void wgtTextAreaSetAutoIndent(w, enable) Enable or disable automatic indentation on newline.

View file

@ -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 *wgtMaskedInput(WidgetT *parent, const char *mask);
WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen); WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen);
WidgetT *wgtTextArea(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); bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward);
int32_t wgtTextAreaGetCursorLine(const WidgetT *w); int32_t wgtTextAreaGetCursorLine(const WidgetT *w);
int32_t wgtTextAreaGetWordAtCursor(const WidgetT *w, char *buf, int32_t bufSize); 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) { bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward) {
if (!w || w->type != sTextAreaTypeId || !needle || !needle[0]) { if (!w || w->type != sTextAreaTypeId || !needle || !needle[0]) {
return false; return false;
@ -3610,6 +3642,7 @@ static const WgtPropDescT sTextAreaProps[] = {
}; };
static const WgtMethodDescT sTextAreaMethods[] = { static const WgtMethodDescT sTextAreaMethods[] = {
{ "AppendText", WGT_SIG_STR, (void *)wgtTextAreaAppendText },
{ "FindNext", WGT_SIG_STR_BOOL_BOOL, (void *)wgtTextAreaFindNext }, { "FindNext", WGT_SIG_STR_BOOL_BOOL, (void *)wgtTextAreaFindNext },
{ "GetWordAtCursor", WGT_SIG_RET_STR, (void *)basGetWordAtCursor }, { "GetWordAtCursor", WGT_SIG_RET_STR, (void *)basGetWordAtCursor },
{ "GoToLine", WGT_SIG_INT, (void *)wgtTextAreaGoToLine }, { "GoToLine", WGT_SIG_INT, (void *)wgtTextAreaGoToLine },
@ -3640,7 +3673,7 @@ static const WgtIfaceT sIfaceTextArea = {
.props = sTextAreaProps, .props = sTextAreaProps,
.propCount = 1, .propCount = 1,
.methods = sTextAreaMethods, .methods = sTextAreaMethods,
.methodCount = 10, .methodCount = 11,
.events = NULL, .events = NULL,
.eventCount = 0, .eventCount = 0,
.createSig = WGT_CREATE_PARENT_INT, .createSig = WGT_CREATE_PARENT_INT,

View file

@ -107,6 +107,11 @@ WidgetT *wgtTimer(WidgetT *parent, int32_t intervalMs, bool repeat) {
w->visible = false; w->visible = false;
d->intervalMs = intervalMs; d->intervalMs = intervalMs;
d->repeat = repeat; 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; return w;
@ -176,6 +181,12 @@ void wgtTimerStop(WidgetT *w) {
void wgtUpdateTimers(void) { 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(); clock_t now = clock();
// Iterate backwards so arrdel doesn't skip entries // 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; clock_t interval = (clock_t)d->intervalMs * CLOCKS_PER_SEC / 1000;
if (elapsed >= interval) { 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; d->lastFire = now;
if (w->onChange) { if (w->onChange) {