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

View file

@ -3188,6 +3188,7 @@ End Sub</code></pre>
<h2>Type-Specific Methods</h2>
<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.
GetWordAtCursor() Returns the word under the cursor.
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>
<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 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.

View file

@ -39,7 +39,7 @@ C_APPS = progman clock dvxdemo cpanel dvxhelp
BASCOMP = ../../../bin/host/bascomp
# BASIC apps: each is a .dbp project in its own directory.
# BASIC-only notepad, imgview, etc. replace the old C versions.
BASIC_APPS = iconed notepad imgview helpedit resedit basicdemo
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo
.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS)
@ -115,10 +115,10 @@ resedit: $(BINDIR)/kpunch/resedit/resedit.app
$(BINDIR)/kpunch/resedit/resedit.app: resedit/resedit.dbp resedit/resedit.frm resedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/resedit dvxbasic
$(BASCOMP) resedit/resedit.dbp -o $@ -release
basicdemo: $(BINDIR)/kpunch/basicdemo/basicdemo.app
basdemo: $(BINDIR)/kpunch/basdemo/basdemo.app
$(BINDIR)/kpunch/basicdemo/basicdemo.app: basicdemo/basicdemo.dbp basicdemo/basicdemo.frm basicdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basicdemo dvxbasic
$(BASCOMP) basicdemo/basicdemo.dbp -o $@ -release
$(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic
$(BASCOMP) basdemo/basdemo.dbp -o $@ -release
$(OBJDIR):
mkdir -p $(OBJDIR)
@ -132,7 +132,7 @@ $(BINDIR)/kpunch/iconed: ; mkdir -p $@
$(BINDIR)/kpunch/notepad: ; mkdir -p $@
$(BINDIR)/kpunch/imgview: ; mkdir -p $@
$(BINDIR)/kpunch/resedit: ; mkdir -p $@
$(BINDIR)/kpunch/basicdemo: ; mkdir -p $@
$(BINDIR)/kpunch/basdemo: ; mkdir -p $@
# Header dependencies
COMMON_H = ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxDlg.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxWm.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h
@ -153,5 +153,5 @@ clean:
rm -f $(BINDIR)/kpunch/imgview/imgview.app
rm -f $(BINDIR)/kpunch/dvxhelp/helpedit.app
rm -f $(BINDIR)/kpunch/resedit/resedit.app
rm -f $(BINDIR)/kpunch/basicdemo/basicdemo.app
rm -f $(BINDIR)/kpunch/basdemo/basdemo.app
$(MAKE) -C dvxbasic clean

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

View file

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

View file

@ -170,6 +170,21 @@ TYPE PointT
y AS INTEGER
END TYPE
' Module-level state. Grouped here up front (rather than sprinkled
' between SUB definitions) so the compiler sees a simple top-level
' run-once block followed by a flat list of SUBs / FUNCTIONs.
DIM gfxWin AS LONG
DIM dynForm AS LONG
DIM dynCount AS INTEGER
DIM timerWin AS LONG
DIM tickCount AS LONG
gfxWin = 0
dynForm = 0
dynCount = 0
timerWin = 0
tickCount = 0
' ============================================================
' OutArea helpers
@ -247,7 +262,7 @@ SUB mnuAbout_Click
msg = "DVX BASIC Feature Tour" + CHR$(10) + CHR$(10)
msg = msg + "A visual catalog of DVX BASIC language"
msg = msg + " and runtime features." + CHR$(10) + CHR$(10)
msg = msg + "(c) 2026 DVX Project"
msg = msg + "Copyright 2026 Scott Duensing"
MsgBox msg, vbOKOnly, "About"
END SUB
@ -740,17 +755,13 @@ END SUB
' Graphics demo (opens a second form with Canvas)
' ============================================================
DIM gfxWin AS LONG
gfxWin = 0
SUB mnuGraphics_Click
IF gfxWin <> 0 THEN
EXIT SUB
END IF
DIM frm AS LONG
SET frm = CreateForm("GraphicsForm", 360, 320)
SET frm = CreateForm("GraphicsForm", 380, 360)
GraphicsForm.Caption = "Graphics Demo"
gfxWin = frm
@ -844,6 +855,13 @@ SUB GfxClearCanvas
END SUB
SUB BasicDemo_Unload
' Closing the main form shuts down the whole app, including any
' child forms (Graphics, Dynamic, Timer) the user left open.
END
END SUB
SUB GraphicsForm_Unload
gfxWin = 0
END SUB
@ -853,10 +871,6 @@ END SUB
' Dynamic form demo
' ============================================================
DIM dynForm AS LONG
dynForm = 0
SUB mnuDynamic_Click
IF dynForm <> 0 THEN
EXIT SUB
@ -892,10 +906,6 @@ SUB mnuDynamic_Click
END SUB
DIM dynCount AS INTEGER
dynCount = 0
SUB DynInc
dynCount = dynCount + 1
CountLabel.Caption = "Counter: " + STR$(dynCount)
@ -917,10 +927,6 @@ END SUB
' Timer demo
' ============================================================
DIM timerWin AS LONG
timerWin = 0
SUB mnuTimer_Click
IF timerWin <> 0 THEN
EXIT SUB
@ -944,10 +950,6 @@ SUB mnuTimer_Click
END SUB
DIM tickCount AS LONG
tickCount = 0
SUB TickHandler
tickCount = tickCount + 1
TickLabel.Caption = "Ticks: " + STR$(tickCount) + " Time: " + TIME$

View file

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

View file

@ -43,7 +43,7 @@ HOSTDIR = ../../../../bin/host
DVXRES = $(HOSTDIR)/dvxres
# Runtime library objects (VM + values + form runtime + serialization)
RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/serialize.o
RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/frmParser.o $(OBJDIR)/serialize.o
RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt
RT_TARGET = $(RT_TARGETDIR)/basrt.lib
@ -68,6 +68,7 @@ TEST_VM = $(HOSTDIR)/test_vm
TEST_LEX = $(HOSTDIR)/test_lex
TEST_QUICK = $(HOSTDIR)/test_quick
TEST_COMPACT = $(HOSTDIR)/test_compact
TEST_SUITE = $(HOSTDIR)/test_suite
STB_DS_IMPL = ../../../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c
PLATFORM_UTIL = ../../../libs/kpunch/libdvx/platform/dvxPlatformUtil.c
@ -76,6 +77,7 @@ TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c runtime/serialize.c
TEST_LEX_SRCS = test_lex.c compiler/lexer.c
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_COMPACT_SRCS = test_compact.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_SUITE_SRCS = test_suite.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
# Command-line compiler (host tool)
BASCOMP_SRCS = stub/bascomp.c basBuild.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/obfuscate.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c ../../../libs/kpunch/libdvx/dvxPrefs.c ../../../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
@ -92,11 +94,15 @@ SYSTEMDIR = ../../../../bin/system
all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT)
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(TEST_SUITE)
$(TEST_SUITE)
$(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm
$(TEST_SUITE): $(TEST_SUITE_SRCS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_SUITE_SRCS) -lm
$(TEST_VM): $(TEST_VM_SRCS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_VM_SRCS) -lm
@ -152,7 +158,10 @@ $(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) $(STUB_TARGET) dvxbasic.res | $(APPDIR)
$(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h formrt/frmParser.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/frmParser.o: formrt/frmParser.c formrt/frmParser.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/serialize.o: runtime/serialize.c runtime/serialize.h runtime/vm.h | $(OBJDIR)
@ -173,7 +182,7 @@ $(OBJDIR)/basBuild.o: basBuild.c basBuild.h basRes.h | $(OBJDIR)
$(OBJDIR)/basstub.o: stub/basstub.c runtime/vm.h runtime/serialize.h formrt/formrt.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h | $(OBJDIR)
$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideMenuEditor.o: ide/ideMenuEditor.c ide/ideMenuEditor.h ide/ideDesigner.h | $(OBJDIR)
@ -185,7 +194,7 @@ $(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideMenuEditor.h ide/ide
$(OBJDIR)/ideProject.o: ide/ideProject.c ide/ideProject.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h | $(OBJDIR)
$(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideToolbox.o: ide/ideToolbox.c ide/ideToolbox.h ide/ideDesigner.h | $(OBJDIR)

View file

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

View file

@ -208,12 +208,40 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
// Count SUB/FUNCTION entries
int32_t procCount = 0;
// Count globals that need runtime type init (currently: STRING).
// This list survives stripping, unlike debugVars.
int32_t globalInitCount = 0;
for (int32_t i = 0; i < tab->count; i++) {
BasSymbolT *s = tab->symbols[i];
if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) {
procCount++;
}
if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) {
globalInitCount++;
}
}
if (globalInitCount > 0) {
mod->globalInits = (BasGlobalInitT *)malloc(globalInitCount * sizeof(BasGlobalInitT));
if (mod->globalInits) {
int32_t gi = 0;
for (int32_t i = 0; i < tab->count; i++) {
BasSymbolT *s = tab->symbols[i];
if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) {
mod->globalInits[gi].index = s->index;
mod->globalInits[gi].dataType = s->dataType;
gi++;
}
}
mod->globalInitCount = gi;
}
}
if (procCount == 0) {
@ -235,6 +263,8 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
BasProcEntryT *p = &mod->procs[idx++];
strncpy(p->name, s->name, BAS_MAX_PROC_NAME - 1);
p->name[BAS_MAX_PROC_NAME - 1] = '\0';
strncpy(p->formName, s->formName, BAS_MAX_PROC_NAME - 1);
p->formName[BAS_MAX_PROC_NAME - 1] = '\0';
p->codeAddr = s->codeAddr;
p->paramCount = s->paramCount;
p->localCount = s->localCount;

View file

@ -169,6 +169,19 @@ int32_t basCompactBytecode(BasModuleT *mod) {
break;
}
case OP_FOR_INIT: {
// [uint16 varIdx] [uint8 scope] [int16 skipOffset]
int16_t oldOff = readI16LE(oldCode + oldPc + 4);
if (!remapRelI16(newCode, newPc, 4, oldOff,
oldPc + 6, newPc + 6,
remap, oldCodeLen, newCodeLen, false)) {
ok = false;
}
break;
}
case OP_ON_ERROR: {
int16_t oldOff = readI16LE(oldCode + oldPc + 1);
@ -421,7 +434,7 @@ static int32_t opOperandSize(uint8_t op) {
case OP_RGB:
case OP_GET_RED: case OP_GET_GREEN: case OP_GET_BLUE:
case OP_STR_VAL: case OP_STR_STRF: case OP_STR_HEX:
case OP_STR_STRING:
case OP_STR_STRING: case OP_STR_OCT: case OP_CONV_BOOL:
case OP_MATH_TIMER: case OP_DATE_STR: case OP_TIME_STR:
case OP_SLEEP: case OP_ENVIRON:
case OP_READ_DATA: case OP_RESTORE:
@ -444,6 +457,7 @@ static int32_t opOperandSize(uint8_t op) {
return 0;
case OP_LOAD_ARRAY: case OP_STORE_ARRAY:
case OP_PUSH_ARR_ADDR:
case OP_PRINT_SPC: case OP_FILE_OPEN:
case OP_CALL_METHOD: case OP_SHOW_FORM:
case OP_LBOUND: case OP_UBOUND:
@ -466,13 +480,13 @@ static int32_t opOperandSize(uint8_t op) {
return 2;
case OP_STORE_ARRAY_FIELD:
case OP_FOR_INIT:
return 3;
case OP_PUSH_INT32: case OP_PUSH_FLT32:
case OP_CALL:
return 4;
case OP_FOR_INIT:
case OP_FOR_NEXT:
return 5;

View file

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

View file

@ -566,6 +566,16 @@ static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
continue;
}
// Remap the owning form name (used at runtime to bind form-scope
// variables). The form itself gets renamed by the same pass.
if (proc->formName[0]) {
const char *mappedForm = nameMapLookup(map, proc->formName);
if (mappedForm) {
snprintf(proc->formName, sizeof(proc->formName), "%s", mappedForm);
}
}
// Find last underscore
char *underscore = strrchr(proc->name, '_');

View file

@ -180,7 +180,7 @@ typedef enum {
#define OP_GOSUB_RET 0x54 // pop PC from eval stack, jump (GOSUB return)
#define OP_RET 0x55 // return from subroutine
#define OP_RET_VAL 0x56 // return from function (value on stack)
#define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 isLocal] init FOR
#define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 scope] [int16 skipOffset] init FOR, skip body if range empty
#define OP_FOR_NEXT 0x58 // [uint16 varIdx] [uint8 isLocal] [int16 loopTop]
#define OP_FOR_POP 0x59 // pop top FOR stack entry (for EXIT FOR)
@ -297,6 +297,9 @@ typedef enum {
#define OP_STR_STRF 0xB1 // STR$(n) -> string
#define OP_STR_HEX 0xB2 // HEX$(n) -> string
#define OP_STR_STRING 0xB3 // STRING$(n, char) -> string
#define OP_STR_OCT 0xF3 // OCT$(n) -> string
#define OP_CONV_BOOL 0xF4 // CBOOL(n) -> -1 (true) or 0 (false)
#define OP_PUSH_ARR_ADDR 0xF5 // [uint8 dims] pop dims indices, pop array ref, push REF to element
// ============================================================
// Extended built-ins

View file

@ -69,6 +69,7 @@ static const BuiltinFuncT builtinFuncs[] = {
{"LEN", OP_STR_LEN, 1, 1, BAS_TYPE_INTEGER},
{"LTRIM$", OP_STR_LTRIM, 1, 1, BAS_TYPE_STRING},
{"MID$", OP_STR_MID2, 2, 3, BAS_TYPE_STRING},
{"OCT$", OP_STR_OCT, 1, 1, BAS_TYPE_STRING},
{"RIGHT$", OP_STR_RIGHT, 2, 2, BAS_TYPE_STRING},
{"RTRIM$", OP_STR_RTRIM, 1, 1, BAS_TYPE_STRING},
{"SPACE$", OP_STR_SPACE, 1, 1, BAS_TYPE_STRING},
@ -84,6 +85,7 @@ static const BuiltinFuncT builtinFuncs[] = {
{"LOF", OP_FILE_LOF, 1, 1, BAS_TYPE_LONG},
// Conversion functions
{"CBOOL", OP_CONV_BOOL, 1, 1, BAS_TYPE_BOOLEAN},
{"CDBL", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_DOUBLE},
{"CINT", OP_CONV_FLT_INT, 1, 1, BAS_TYPE_INTEGER},
{"CLNG", OP_CONV_INT_LONG, 1, 1, BAS_TYPE_LONG},
@ -355,6 +357,11 @@ void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) {
}
void basParserSetValidator(BasParserT *p, const BasCtrlValidatorT *v) {
p->validator = v;
}
static bool check(BasParserT *p, BasTokenTypeE type) {
return p->lex.token.type == type;
}
@ -501,10 +508,10 @@ static void emitByRefArg(BasParserT *p) {
strncpy(name, p->lex.token.text, sizeof(name) - 1);
name[sizeof(name) - 1] = '\0';
// Look up the symbol -- must be a simple variable (not array, not const)
// Look up the symbol -- must be a variable (simple or array)
BasSymbolT *sym = basSymTabFind(&p->sym, name);
if (!sym || sym->kind != SYM_VARIABLE || sym->isArray) {
if (!sym || sym->kind != SYM_VARIABLE) {
parseExpression(p);
return;
}
@ -517,6 +524,44 @@ static void emitByRefArg(BasParserT *p) {
advance(p); // consume the identifier
// Array element as BYREF: `arr(i)` or `arr(i, j)`. Emit LOAD of
// the array ref, then the indices, then OP_PUSH_ARR_ADDR which
// produces a BAS_TYPE_REF pointing at the element. Writes through
// that ref in the callee update the actual array element.
if (sym->isArray && check(p, TOK_LPAREN)) {
advance(p); // consume (
// Push the array reference
emitLoad(p, sym);
// Parse indices
int32_t dims = 0;
parseExpression(p);
dims++;
while (match(p, TOK_COMMA)) {
parseExpression(p);
dims++;
}
expect(p, TOK_RPAREN);
// If the next token isn't an argument delimiter, this wasn't
// a simple `arr(i)` -- rewind and fall back. (rare; e.g.
// `arr(i).field` not supported as BYREF.)
if (!check(p, TOK_COMMA) && !check(p, TOK_RPAREN) && !check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) {
// Too late to rewind cleanly -- we've already emitted code.
// Treat this as an error. Callers can restructure as a
// simple variable BYREF or BYVAL.
error(p, "Complex BYREF array expression not supported; use a temporary variable");
return;
}
basEmit8(&p->cg, OP_PUSH_ARR_ADDR);
basEmit8(&p->cg, (uint8_t)dims);
return;
}
// The token after the identifier must be an argument delimiter
// (comma, rparen, newline, colon, EOF, ELSE) for this to be a
// bare variable reference. Anything else (operator, dot, paren)
@ -1139,14 +1184,24 @@ static void parseAssignOrCall(BasParserT *p) {
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p); // consume member name
// If `name` is a regular variable, the user is dereferencing an
// object reference (typically a form or control returned by
// CreateForm / CreateControl). Use the variable's value as the
// ref directly instead of treating `name` as a literal control
// name.
bool isVarRef = (sym != NULL && sym->kind == SYM_VARIABLE);
// Special form methods: Show, Hide
if (strcasecmp(memberName, "Show") == 0) {
// name.Show [modal]
// Push form name, LOAD_FORM, SHOW_FORM
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, nameIdx);
basEmit8(&p->cg, OP_LOAD_FORM);
if (isVarRef) {
emitLoad(p, sym);
} else {
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, nameIdx);
basEmit8(&p->cg, OP_LOAD_FORM);
}
uint8_t modal = 0;
if (check(p, TOK_INT_LIT)) {
if (p->lex.token.intVal != 0) {
@ -1166,10 +1221,14 @@ static void parseAssignOrCall(BasParserT *p) {
}
if (strcasecmp(memberName, "Hide") == 0) {
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, nameIdx);
basEmit8(&p->cg, OP_LOAD_FORM);
if (isVarRef) {
emitLoad(p, sym);
} else {
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, nameIdx);
basEmit8(&p->cg, OP_LOAD_FORM);
}
basEmit8(&p->cg, OP_HIDE_FORM);
return;
}
@ -1178,16 +1237,33 @@ static void parseAssignOrCall(BasParserT *p) {
// Property assignment: CtrlName.Property = expr
advance(p); // consume =
// Push ctrl ref: push form ref (NULL = current), push ctrl name, FIND_CTRL
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
BasValueT formNull;
memset(&formNull, 0, sizeof(formNull));
// Use OP_PUSH_STR for ctrl name, then FIND_CTRL
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx);
basEmit8(&p->cg, OP_FIND_CTRL);
// Compile-time validation: if the host provided a validator
// (IDE), check that the property exists on the widget type.
// Skip when `name` is a variable reference (dynamic ctrl we
// can't statically type) or when the ctrl isn't in the map
// (e.g. created via CreateControl at runtime).
if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isPropValid) {
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
if (wgtType && !p->validator->isPropValid(p->validator->ctx, wgtType, memberName)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unknown property '%s.%s' (type '%s' has no such property)", name, memberName, wgtType);
error(p, buf);
return;
}
}
// Push ctrl/form ref
if (isVarRef) {
emitLoad(p, sym);
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx);
basEmit8(&p->cg, OP_FIND_CTRL);
}
// Push property name
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
@ -1203,13 +1279,29 @@ static void parseAssignOrCall(BasParserT *p) {
}
// Method call: CtrlName.Method [args]
// Push ctrl ref
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx);
basEmit8(&p->cg, OP_FIND_CTRL);
// Same compile-time validation as above, but for methods.
if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isMethodValid) {
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
if (wgtType && !p->validator->isMethodValid(p->validator->ctx, wgtType, memberName)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unknown method '%s.%s' (type '%s' has no such method)", name, memberName, wgtType);
error(p, buf);
return;
}
}
// Push ctrl/form ref
if (isVarRef) {
emitLoad(p, sym);
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx);
basEmit8(&p->cg, OP_FIND_CTRL);
}
// Push method name
uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
@ -1412,13 +1504,52 @@ static void parseAssignOrCall(BasParserT *p) {
argc++;
}
}
if (!p->hasError && argc != sym->paramCount) {
// Determine the minimum acceptable count (ignore trailing optionals).
int32_t minArgs = sym->requiredParams;
bool hasOptional = false;
for (int32_t i = 0; i < sym->paramCount && i < BAS_MAX_PARAMS; i++) {
if (sym->paramOptional[i]) {
hasOptional = true;
break;
}
}
if (!hasOptional) {
minArgs = sym->paramCount;
}
if (!p->hasError && (argc < minArgs || argc > sym->paramCount)) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc);
if (minArgs == sym->paramCount) {
snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc);
} else {
snprintf(buf, sizeof(buf), "Sub '%s' expects %d to %d arguments, got %d", sym->name, (int)minArgs, (int)sym->paramCount, (int)argc);
}
error(p, buf);
return;
}
// Pad missing optional arguments with zero-valued defaults so
// the callee's OP_CALL receives a full parameter list.
while (argc < sym->paramCount) {
uint8_t pType = sym->paramTypes[argc];
if (pType == BAS_TYPE_STRING) {
uint16_t idx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, idx);
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
}
argc++;
}
// External library SUB: emit OP_CALL_EXTERN
if (sym->isExtern) {
basEmit8(&p->cg, OP_CALL_EXTERN);
@ -2047,9 +2178,13 @@ static void parseDef(BasParserT *p) {
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
uint8_t pdt = suffixToType(paramName);
uint8_t pdt = suffixToType(paramName);
int32_t pUdtTypeId = -1;
if (match(p, TOK_AS)) {
pdt = resolveTypeName(p);
if (pdt == BAS_TYPE_UDT) {
pUdtTypeId = p->lastUdtTypeId;
}
}
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
@ -2060,6 +2195,7 @@ static void parseDef(BasParserT *p) {
paramSym->scope = SCOPE_LOCAL;
paramSym->index = basSymTabAllocSlot(&p->sym);
paramSym->isDefined = true;
paramSym->udtTypeId = pUdtTypeId;
if (paramCount < BAS_MAX_PARAMS) {
paramTypes[paramCount] = pdt;
@ -2283,6 +2419,14 @@ static void parseDim(BasParserT *p) {
emitUdtInit(p, udtTypeId);
emitStore(p, sym);
}
} else if (dt == BAS_TYPE_STRING) {
// STRING slots must start as an empty string, not numeric 0.
// Arithmetic falls through basValToNumber so numeric defaults
// stay harmless, but STRING concat checks actual slot type.
uint16_t idx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, idx);
emitStore(p, sym);
}
}
@ -2572,10 +2716,15 @@ static void parseFor(BasParserT *p) {
basEmit16(&p->cg, 1);
}
// Emit FOR_INIT -- sets up the for-loop state in the VM
// Emit FOR_INIT -- sets up the for-loop state in the VM. The
// trailing int16 is a forward offset the VM uses to skip the body
// when the loop's range is already empty at entry (e.g. FOR i = 10
// TO 5). We patch it after the body is emitted.
basEmit8(&p->cg, OP_FOR_INIT);
basEmitU16(&p->cg, (uint16_t)loopVar->index);
basEmit8(&p->cg, (uint8_t)loopVar->scope);
int32_t skipOffsetPos = basCodePos(&p->cg);
basEmit16(&p->cg, 0); // placeholder
int32_t loopBody = basCodePos(&p->cg);
@ -2606,6 +2755,12 @@ static void parseFor(BasParserT *p) {
int16_t backOffset = (int16_t)(loopBody - (basCodePos(&p->cg) + 2));
basEmit16(&p->cg, backOffset);
// Patch FOR_INIT's forward skip offset to point past FOR_NEXT.
int32_t loopEnd = basCodePos(&p->cg);
int16_t skipOffset = (int16_t)(loopEnd - (skipOffsetPos + 2));
p->cg.code[skipOffsetPos] = (uint8_t)(skipOffset & 0xFF);
p->cg.code[skipOffsetPos + 1] = (uint8_t)((skipOffset >> 8) & 0xFF);
// Patch all EXIT FOR jumps to here
exitListPatch(&exitForList, p);
exitForList = savedExitFor;
@ -2684,9 +2839,13 @@ static void parseFunction(BasParserT *p) {
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
uint8_t pdt = suffixToType(paramName);
uint8_t pdt = suffixToType(paramName);
int32_t pUdtTypeId = -1;
if (match(p, TOK_AS)) {
pdt = resolveTypeName(p);
if (pdt == BAS_TYPE_UDT) {
pUdtTypeId = p->lastUdtTypeId;
}
}
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
@ -2697,6 +2856,7 @@ static void parseFunction(BasParserT *p) {
paramSym->scope = SCOPE_LOCAL;
paramSym->index = basSymTabAllocSlot(&p->sym);
paramSym->isDefined = true;
paramSym->udtTypeId = pUdtTypeId;
if (paramCount < BAS_MAX_PARAMS) {
paramTypes[paramCount] = pdt;
@ -4172,13 +4332,50 @@ static void parsePrimary(BasParserT *p) {
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
// Emit: push NULL (current form), push ctrl name, FIND_CTRL
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx);
basEmit8(&p->cg, OP_FIND_CTRL);
bool isVarRef2 = (sym != NULL && sym->kind == SYM_VARIABLE);
// Compile-time validation for expression-context reads /
// method returns. Peek ahead: if '(' follows, memberName
// is a method; otherwise it's a property read. Skip when
// the host didn't attach a validator or the ctrl is dynamic.
if (!isVarRef2 && p->validator && p->validator->lookupCtrlType) {
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
if (wgtType) {
bool isMethodCall = check(p, TOK_LPAREN);
bool valid = true;
if (isMethodCall && p->validator->isMethodValid) {
valid = p->validator->isMethodValid(p->validator->ctx, wgtType, memberName);
} else if (!isMethodCall && p->validator->isPropValid) {
valid = p->validator->isPropValid(p->validator->ctx, wgtType, memberName);
}
if (!valid) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Unknown %s '%s.%s' (type '%s' has no such %s)",
isMethodCall ? "method" : "property",
name, memberName, wgtType,
isMethodCall ? "method" : "property");
error(p, buf);
return;
}
}
}
// If `name` is a regular variable holding an object reference
// (form/control returned by CreateForm/CreateControl), use its
// value directly instead of treating `name` as a literal name.
if (isVarRef2) {
emitLoad(p, sym);
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, ctrlNameIdx);
basEmit8(&p->cg, OP_FIND_CTRL);
}
// If followed by '(', this is a method call with args
if (check(p, TOK_LPAREN)) {
@ -5583,9 +5780,13 @@ static void parseSub(BasParserT *p) {
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
uint8_t pdt = suffixToType(paramName);
uint8_t pdt = suffixToType(paramName);
int32_t pUdtTypeId = -1;
if (match(p, TOK_AS)) {
pdt = resolveTypeName(p);
if (pdt == BAS_TYPE_UDT) {
pUdtTypeId = p->lastUdtTypeId;
}
}
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
@ -5596,6 +5797,7 @@ static void parseSub(BasParserT *p) {
paramSym->scope = SCOPE_LOCAL;
paramSym->index = basSymTabAllocSlot(&p->sym);
paramSym->isDefined = true;
paramSym->udtTypeId = pUdtTypeId;
if (paramCount < BAS_MAX_PARAMS) {
paramTypes[paramCount] = pdt;

View file

@ -49,6 +49,27 @@
#define BAS_PARSE_ERROR_LEN 1024
#define BAS_PARSE_ERR_SCRATCH 512
// Optional compile-time validator for CtrlName.Member references.
// The IDE populates this from the project's .frm files + widget DXE
// metadata so typos die at compile time instead of at event-click
// time. bascomp leaves it NULL (runs on the host, no widget DXEs)
// and falls back to the runtime error net. Dynamically-created
// controls aren't in the map, so lookupCtrlType returns NULL and
// validation is skipped for those -- no false positives.
typedef struct {
// Return the widget type name for a control declared in a .frm,
// or NULL if the control isn't statically known.
const char *(*lookupCtrlType)(void *ctx, const char *ctrlName);
// Return true if `methodName` is valid for widget type `wgtType`
// (or a common method like Refresh/SetFocus). Called with the
// type string returned by lookupCtrlType.
bool (*isMethodValid)(void *ctx, const char *wgtType, const char *methodName);
// Return true if `propName` is a valid property on wgtType.
bool (*isPropValid)(void *ctx, const char *wgtType, const char *propName);
void *ctx;
} BasCtrlValidatorT;
typedef struct {
BasLexerT lex;
BasCodeGenT cg;
@ -66,6 +87,8 @@ typedef struct {
// Per-form init block tracking
int32_t formInitJmpAddr; // code position of JMP to patch (-1 = none)
int32_t formInitCodeStart; // code position where init block starts (-1 = none)
// Optional compile-time CtrlName.Member validator (IDE-only).
const BasCtrlValidatorT *validator;
} BasParserT;
// ============================================================
@ -75,6 +98,11 @@ typedef struct {
// Initialize parser with source text.
void basParserInit(BasParserT *p, const char *source, int32_t sourceLen);
// Attach an optional compile-time validator for CtrlName.Member
// references. The parser borrows the pointer -- caller owns the
// underlying struct and must keep it alive until basParserFree.
void basParserSetValidator(BasParserT *p, const BasCtrlValidatorT *v);
// Parse the entire source and generate p-code.
// Returns true on success, false on error (check p->error).
bool basParse(BasParserT *p);

View file

@ -83,7 +83,13 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
sym->dataType = dataType;
sym->isDefined = true;
if (scope == SCOPE_FORM && tab->formScopeName[0]) {
// Record owning form for both SCOPE_FORM vars AND SUBs/FUNCTIONs
// declared inside BEGINFORM...ENDFORM. SUBs stay at SCOPE_GLOBAL
// (callable from anywhere) but carry the owning form so the VM can
// bind form-scope vars correctly when the SUB is dispatched as an
// event handler for a different form's control.
if (tab->inFormScope && tab->formScopeName[0] &&
(scope == SCOPE_FORM || kind == SYM_SUB || kind == SYM_FUNCTION)) {
strncpy(sym->formName, tab->formScopeName, BAS_MAX_SYMBOL_NAME - 1);
sym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
}

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)
BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES];
int32_t eventOverrideCount;
// Re-entrancy guard: set while an event handler for this control
// is running. runSubLoop pumps events during long-running handlers
// (painting, draw loops), and without this guard a lingering
// mouse-up or repaint cycle can re-fire the same Click on top of
// itself -- an unbounded recursion. firingEventName records which
// event is in flight so different-event delivery (e.g. LostFocus
// while Click is still running) is allowed through.
bool eventFiring;
char firingEventName[BAS_MAX_CTRL_NAME];
} BasControlT;
// ============================================================
@ -153,6 +162,10 @@ typedef struct {
int32_t frmCacheCount;
BasCfmCacheT *cfmCache; // stb_ds array of compiled form binaries
int32_t cfmCacheCount;
// Set true when a runtime error has halted the program; the event
// loop exits at the next pump so the app doesn't stumble forward
// with a halted VM.
bool terminated;
} BasFormRtT;
// ============================================================
@ -209,7 +222,28 @@ void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *d
// ---- Widget creation ----
WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent);
// Create a widget by resolved (DVX) type name. Returns NULL if the type
// is unknown. createWidgetByIface dispatches on the interface
// descriptor's createSig; pass allowData=true for an empty pixel-data
// widget (runtime path), or false to refuse (design-time path).
WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent);
WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *parent, bool allowData);
// Resolve a VB-style name ("CommandButton") to a DVX widget name
// ("button"). Falls back to the same name if already canonical. Returns
// NULL if the type is unknown.
const char *resolveTypeName(const char *typeName);
// Apply a "Key = Value" string pair to a widget property via the
// interface descriptor. Handles ENUM/INT/BOOL/STRING dispatch. Returns
// true if the property was set, false if unsupported type / no setter.
bool wgtApplyPropFromString(WidgetT *w, const WgtPropDescT *p, const char *val);
// Read a widget property via the interface getter and format it into
// out as a .frm-style value ("True", "False", enum name, or decimal
// string). Returns true if the property was read; false if the getter
// is NULL, the type is not serializable, or the enum value is unknown.
bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize);
// ---- Form window creation ----

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 "../formrt/formrt.h"
#include "../formrt/frmParser.h"
#include "dvxDraw.h"
#include "dvxVideo.h"
#include "dvxWm.h"
@ -61,16 +62,39 @@ static const char *FORM_DEFAULT_EVENT = "Load";
// ============================================================
// dsgnCreateDesignWidget is declared in ideDesigner.h (non-static)
static void dsgnLoad_onCtrlBegin(void *userData, const char *typeName, const char *name);
static void dsgnLoad_onCtrlEnd(void *userData);
static void dsgnLoad_onCtrlProp(void *userData, const char *key, const char *value);
static bool dsgnLoad_onFormBegin(void *userData, const char *name);
static void dsgnLoad_onFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen);
static void dsgnLoad_onFormProp(void *userData, const char *key, const char *value);
static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level);
static void dsgnLoad_onMenuEnd(void *userData);
static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *value);
static const char *getPropValue(const DsgnControlT *ctrl, const char *name);
static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y);
static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y);
static void rebuildWidgets(DsgnStateT *ds);
static const char *resolveTypeName(const char *typeName);
static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, int32_t pos, const char *parentName, int32_t indent);
static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value);
static void syncWidgetGeom(DsgnControlT *ctrl);
// ============================================================
// .frm parsing context (used by dsgnLoad_* callbacks)
// ============================================================
typedef struct {
DsgnFormT *form;
DsgnControlT *current;
int32_t curMenuItemIdx;
char parentStack[8][DSGN_MAX_NAME];
int32_t nestDepth;
bool containerStack[BAS_MAX_FRM_NESTING];
int32_t containerDepth;
} DsgnFrmLoadCtxT;
void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize) {
// Look up the name prefix from the widget interface descriptor.
// Falls back to the type name itself if no prefix is registered.
@ -156,8 +180,10 @@ WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout) {
}
// Create a real DVX widget for design-time display. Mirrors the
// logic in formrt.c createWidget().
// Create a real DVX widget for design-time display. Uses the shared
// createWidgetByIface switch from formrt; design-time refuses
// WGT_CREATE_PARENT_DATA widgets (Image/ImageButton) since we have no
// pixel data.
WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) {
const char *wgtName = resolveTypeName(vbTypeName);
@ -171,50 +197,7 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) {
return NULL;
}
const WgtIfaceT *iface = wgtGetIface(wgtName);
uint8_t sig = iface ? iface->createSig : WGT_CREATE_PARENT;
typedef WidgetT *(*CreateParentFnT)(WidgetT *);
typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *);
typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t);
typedef WidgetT *(*CreateParentIntIntFnT)(WidgetT *, int32_t, int32_t);
typedef WidgetT *(*CreateParentIntIntIntFnT)(WidgetT *, int32_t, int32_t, int32_t);
typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool);
typedef WidgetT *(*CreateParentBoolFnT)(WidgetT *, bool);
switch (sig) {
case WGT_CREATE_PARENT_TEXT: {
CreateParentTextFnT fn = *(CreateParentTextFnT *)api;
return fn(parent, "");
}
case WGT_CREATE_PARENT_INT: {
CreateParentIntFnT fn = *(CreateParentIntFnT *)api;
return fn(parent, iface->createArgs[0]);
}
case WGT_CREATE_PARENT_INT_INT: {
CreateParentIntIntFnT fn = *(CreateParentIntIntFnT *)api;
return fn(parent, iface->createArgs[0], iface->createArgs[1]);
}
case WGT_CREATE_PARENT_INT_INT_INT: {
CreateParentIntIntIntFnT fn = *(CreateParentIntIntIntFnT *)api;
return fn(parent, iface->createArgs[0], iface->createArgs[1], iface->createArgs[2]);
}
case WGT_CREATE_PARENT_INT_BOOL: {
CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api;
return fn(parent, iface->createArgs[0], (bool)iface->createArgs[1]);
}
case WGT_CREATE_PARENT_BOOL: {
CreateParentBoolFnT fn = *(CreateParentBoolFnT *)api;
return fn(parent, (bool)iface->createArgs[0]);
}
case WGT_CREATE_PARENT_DATA:
// Image/ImageButton -- cannot auto-create without pixel data
return NULL;
default: {
CreateParentFnT fn = *(CreateParentFnT *)api;
return fn(parent);
}
}
return createWidgetByIface(wgtGetIface(wgtName), api, parent, false);
}
@ -320,23 +303,8 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
const char *val = getPropValue(ctrl, p->name);
if (!val) {
continue;
}
if (p->type == WGT_IFACE_ENUM && p->enumNames) {
for (int32_t en = 0; p->enumNames[en]; en++) {
if (strcasecmp(p->enumNames[en], val) == 0) {
((void (*)(WidgetT *, int32_t))p->setFn)(w, en);
break;
}
}
} else if (p->type == WGT_IFACE_INT) {
((void (*)(WidgetT *, int32_t))p->setFn)(w, atoi(val));
} else if (p->type == WGT_IFACE_BOOL) {
((void (*)(WidgetT *, bool))p->setFn)(w, strcasecmp(val, "True") == 0);
} else if (p->type == WGT_IFACE_STRING) {
((void (*)(WidgetT *, const char *))p->setFn)(w, val);
if (val) {
wgtApplyPropFromString(w, p, val);
}
}
}
@ -427,259 +395,27 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
snprintf(form->name, DSGN_MAX_NAME, "Form1");
snprintf(form->caption, DSGN_MAX_TEXT, "Form1");
DsgnControlT *curCtrl = NULL;
DsgnMenuItemT *curMenuItem = NULL;
bool inForm = false;
bool inMenu = false;
int32_t menuNestDepth = 0;
int32_t blockDepth = 0; // Begin/End nesting depth (0 = form level)
bool blockIsContainer[BAS_MAX_FRM_NESTING]; // whether each block is a container
DsgnFrmLoadCtxT ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.form = form;
ctx.curMenuItemIdx = -1;
// Parent name stack for nesting (index 0 = form level)
char parentStack[8][DSGN_MAX_NAME];
int32_t nestDepth = 0;
parentStack[0][0] = '\0';
FrmParserCbsT cbs;
memset(&cbs, 0, sizeof(cbs));
cbs.userData = &ctx;
cbs.onFormBegin = dsgnLoad_onFormBegin;
cbs.onFormProp = dsgnLoad_onFormProp;
cbs.onFormEnd = dsgnLoad_onFormEnd;
cbs.onMenuBegin = dsgnLoad_onMenuBegin;
cbs.onMenuEnd = dsgnLoad_onMenuEnd;
cbs.onMenuProp = dsgnLoad_onMenuProp;
cbs.onCtrlBegin = dsgnLoad_onCtrlBegin;
cbs.onCtrlEnd = dsgnLoad_onCtrlEnd;
cbs.onCtrlProp = dsgnLoad_onCtrlProp;
const char *pos = source;
const char *end = source + sourceLen;
while (pos < end) {
const char *lineStart = pos;
while (pos < end && *pos != '\n' && *pos != '\r') {
pos++;
}
int32_t lineLen = (int32_t)(pos - lineStart);
if (pos < end && *pos == '\r') { pos++; }
if (pos < end && *pos == '\n') { pos++; }
char line[BAS_MAX_FRM_LINE_LEN];
if (lineLen >= BAS_MAX_FRM_LINE_LEN) {
lineLen = BAS_MAX_FRM_LINE_LEN - 1;
}
memcpy(line, lineStart, lineLen);
line[lineLen] = '\0';
const char *trimmed = dvxSkipWs(line);
if (*trimmed == '\0' || *trimmed == '\'') {
continue;
}
if (strncasecmp(trimmed, "VERSION ", 8) == 0) {
// Accept "VERSION DVX x.xx" (native) and "VERSION x.xx" (VB import).
// Reject VB forms with version > 1.xx (VB4+/VB6 use features we
// don't support like OLE controls and binary properties).
const char *ver = trimmed + 8;
if (strncasecmp(ver, "DVX ", 4) == 0) {
// Native DVX BASIC form -- always accepted
} else {
// VB form -- check version number
double vbVer = atof(ver);
if (vbVer > 2.0) {
return false; // VB4+ form, not compatible
}
}
continue;
}
// Begin TypeName CtrlName
if (strncasecmp(trimmed, "Begin ", 6) == 0) {
const char *rest = trimmed + 6;
char typeName[DSGN_MAX_NAME];
char ctrlName[DSGN_MAX_NAME];
int32_t ti = 0;
while (*rest && *rest != ' ' && *rest != '\t' && ti < DSGN_MAX_NAME - 1) {
typeName[ti++] = *rest++;
}
typeName[ti] = '\0';
rest = dvxSkipWs(rest);
int32_t ci = 0;
while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < DSGN_MAX_NAME - 1) {
ctrlName[ci++] = *rest++;
}
ctrlName[ci] = '\0';
if (strcasecmp(typeName, "Form") == 0) {
snprintf(form->name, DSGN_MAX_NAME, "%s", ctrlName);
snprintf(form->caption, DSGN_MAX_TEXT, "%s", ctrlName);
inForm = true;
nestDepth = 0;
curCtrl = NULL;
} else if (strcasecmp(typeName, "Menu") == 0 && inForm) {
DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi));
snprintf(mi.name, DSGN_MAX_NAME, "%s", ctrlName);
mi.level = menuNestDepth;
mi.enabled = true;
arrput(form->menuItems, mi);
curMenuItem = &form->menuItems[arrlen(form->menuItems) - 1];
curCtrl = NULL; // not a control
menuNestDepth++;
inMenu = true;
if (blockDepth < BAS_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = false; }
blockDepth++;
} else if (inForm) {
DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
cp->index = -1;
snprintf(cp->name, DSGN_MAX_NAME, "%s", ctrlName);
snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
// Set parent from current nesting
if (nestDepth > 0) {
snprintf(cp->parentName, DSGN_MAX_NAME, "%s", parentStack[nestDepth - 1]);
}
cp->width = DEFAULT_CTRL_W;
cp->height = DEFAULT_CTRL_H;
arrput(form->controls, cp);
curCtrl = form->controls[arrlen(form->controls) - 1];
bool isCtrl = dsgnIsContainer(typeName);
if (blockDepth < BAS_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = isCtrl; }
blockDepth++;
// If this is a container, push onto parent stack
if (isCtrl && nestDepth < 7) {
snprintf(parentStack[nestDepth], DSGN_MAX_NAME, "%s", ctrlName);
nestDepth++;
}
}
continue;
}
if (strcasecmp(trimmed, "End") == 0) {
if (blockDepth > 0) {
blockDepth--;
if (inMenu) {
menuNestDepth--;
curMenuItem = NULL;
if (menuNestDepth <= 0) {
menuNestDepth = 0;
inMenu = false;
}
} else {
// If we're closing a container, pop the parent stack
if (blockDepth < BAS_MAX_FRM_NESTING && blockIsContainer[blockDepth] && nestDepth > 0) {
nestDepth--;
}
curCtrl = NULL;
}
} else {
// blockDepth == 0: this is the form's closing End
inForm = false;
// Everything after the form's closing End is code
if (pos < end) {
// Skip leading blank lines
const char *codeStart = pos;
while (codeStart < end && (*codeStart == '\r' || *codeStart == '\n' || *codeStart == ' ' || *codeStart == '\t')) {
codeStart++;
}
if (codeStart < end) {
int32_t codeLen = (int32_t)(end - codeStart);
form->code = (char *)malloc(codeLen + 1);
if (form->code) {
memcpy(form->code, codeStart, codeLen);
form->code[codeLen] = '\0';
}
}
}
break; // done parsing
}
continue;
}
// Property = Value
const char *eq = strchr(trimmed, '=');
if (eq && inForm) {
char key[DSGN_MAX_NAME];
const char *kend = eq - 1;
while (kend > trimmed && (*kend == ' ' || *kend == '\t')) { kend--; }
int32_t klen = (int32_t)(kend - trimmed + 1);
if (klen >= DSGN_MAX_NAME) { klen = DSGN_MAX_NAME - 1; }
memcpy(key, trimmed, klen);
key[klen] = '\0';
const char *vstart = dvxSkipWs(eq + 1);
char val[DSGN_MAX_TEXT];
int32_t vi = 0;
if (*vstart == '"') {
vstart++;
while (*vstart && *vstart != '"' && vi < DSGN_MAX_TEXT - 1) {
val[vi++] = *vstart++;
}
} else {
while (*vstart && *vstart != '\r' && *vstart != '\n' && vi < DSGN_MAX_TEXT - 1) {
val[vi++] = *vstart++;
}
while (vi > 0 && (val[vi - 1] == ' ' || val[vi - 1] == '\t')) { vi--; }
}
val[vi] = '\0';
if (curMenuItem) {
if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, DSGN_MAX_TEXT, "%s", val); }
else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
else if (strcasecmp(key, "RadioCheck") == 0) { curMenuItem->radioCheck = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
} else if (curCtrl) {
if (strcasecmp(key, "Left") == 0) { curCtrl->left = atoi(val); }
else if (strcasecmp(key, "Top") == 0) { curCtrl->top = atoi(val); }
else if (strcasecmp(key, "MinWidth") == 0 ||
strcasecmp(key, "Width") == 0) { curCtrl->width = atoi(val); }
else if (strcasecmp(key, "MinHeight") == 0 ||
strcasecmp(key, "Height") == 0) { curCtrl->height = atoi(val); }
else if (strcasecmp(key, "MaxWidth") == 0) { curCtrl->maxWidth = atoi(val); }
else if (strcasecmp(key, "MaxHeight") == 0) { curCtrl->maxHeight = atoi(val); }
else if (strcasecmp(key, "Weight") == 0) { curCtrl->weight = atoi(val); }
else if (strcasecmp(key, "Index") == 0) { curCtrl->index = atoi(val); }
else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(curCtrl->helpTopic, DSGN_MAX_NAME, "%s", val); }
else if (strcasecmp(key, "TabIndex") == 0) { /* ignored -- DVX has no tab order */ }
else { setPropValue(curCtrl, key, val); }
} else {
if (strcasecmp(key, "Caption") == 0) { snprintf(form->caption, DSGN_MAX_TEXT, "%s", val); }
else if (strcasecmp(key, "Layout") == 0) { strncpy(form->layout, val, DSGN_MAX_NAME - 1); form->layout[DSGN_MAX_NAME - 1] = '\0'; }
else if (strcasecmp(key, "AutoSize") == 0) { form->autoSize = (strcasecmp(val, "True") == 0); }
else if (strcasecmp(key, "Resizable") == 0) { form->resizable = (strcasecmp(val, "True") == 0); }
else if (strcasecmp(key, "Centered") == 0) { form->centered = (strcasecmp(val, "True") == 0); }
else if (strcasecmp(key, "Left") == 0) { form->left = atoi(val); }
else if (strcasecmp(key, "Top") == 0) { form->top = atoi(val); }
else if (strcasecmp(key, "Width") == 0) { form->width = atoi(val); form->autoSize = false; }
else if (strcasecmp(key, "Height") == 0) { form->height = atoi(val); form->autoSize = false; }
else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(form->helpTopic, DSGN_MAX_NAME, "%s", val); }
}
}
if (!frmParse(source, sourceLen, &cbs)) {
free(form);
return false;
}
ds->form = form;
@ -688,6 +424,239 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
}
static void dsgnLoad_onCtrlBegin(void *userData, const char *typeName, const char *name) {
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
if (!ctx->form) {
return;
}
DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
if (!cp) {
if (ctx->containerDepth < BAS_MAX_FRM_NESTING) {
ctx->containerStack[ctx->containerDepth++] = false;
}
return;
}
cp->index = -1;
snprintf(cp->name, DSGN_MAX_NAME, "%s", name);
snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
if (ctx->nestDepth > 0) {
snprintf(cp->parentName, DSGN_MAX_NAME, "%s", ctx->parentStack[ctx->nestDepth - 1]);
}
cp->width = DEFAULT_CTRL_W;
cp->height = DEFAULT_CTRL_H;
arrput(ctx->form->controls, cp);
ctx->current = ctx->form->controls[arrlen(ctx->form->controls) - 1];
bool isCtrl = dsgnIsContainer(typeName);
if (ctx->containerDepth < BAS_MAX_FRM_NESTING) {
ctx->containerStack[ctx->containerDepth++] = isCtrl;
}
if (isCtrl && ctx->nestDepth < 7) {
snprintf(ctx->parentStack[ctx->nestDepth], DSGN_MAX_NAME, "%s", name);
ctx->nestDepth++;
}
}
static void dsgnLoad_onCtrlEnd(void *userData) {
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
if (ctx->containerDepth > 0) {
ctx->containerDepth--;
if (ctx->containerStack[ctx->containerDepth] && ctx->nestDepth > 0) {
ctx->nestDepth--;
}
}
ctx->current = NULL;
}
static void dsgnLoad_onCtrlProp(void *userData, const char *key, const char *value) {
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
if (!ctx->current) {
return;
}
char val[DSGN_MAX_TEXT];
snprintf(val, sizeof(val), "%s", value);
frmStripQuotes(val);
DsgnControlT *cc = ctx->current;
if (strcasecmp(key, "Left") == 0) {
cc->left = atoi(val);
} else if (strcasecmp(key, "Top") == 0) {
cc->top = atoi(val);
} else if (strcasecmp(key, "MinWidth") == 0 || strcasecmp(key, "Width") == 0) {
cc->width = atoi(val);
} else if (strcasecmp(key, "MinHeight") == 0 || strcasecmp(key, "Height") == 0) {
cc->height = atoi(val);
} else if (strcasecmp(key, "MaxWidth") == 0) {
cc->maxWidth = atoi(val);
} else if (strcasecmp(key, "MaxHeight") == 0) {
cc->maxHeight = atoi(val);
} else if (strcasecmp(key, "Weight") == 0) {
cc->weight = atoi(val);
} else if (strcasecmp(key, "Index") == 0) {
cc->index = atoi(val);
} else if (strcasecmp(key, "HelpTopic") == 0) {
snprintf(cc->helpTopic, DSGN_MAX_NAME, "%s", val);
} else if (strcasecmp(key, "TabIndex") == 0) {
// ignored -- DVX has no tab order
} else {
setPropValue(cc, key, val);
}
}
static bool dsgnLoad_onFormBegin(void *userData, const char *name) {
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
if (!ctx->form) {
return false;
}
snprintf(ctx->form->name, DSGN_MAX_NAME, "%s", name);
snprintf(ctx->form->caption, DSGN_MAX_TEXT, "%s", name);
ctx->current = NULL;
ctx->nestDepth = 0;
return true;
}
static void dsgnLoad_onFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen) {
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
if (!ctx->form || trailingLen <= 0) {
return;
}
// Skip leading whitespace/blank lines
const char *codeStart = trailingSrc;
const char *codeEnd = trailingSrc + trailingLen;
while (codeStart < codeEnd && (*codeStart == '\r' || *codeStart == '\n' || *codeStart == ' ' || *codeStart == '\t')) {
codeStart++;
}
if (codeStart >= codeEnd) {
return;
}
int32_t codeLen = (int32_t)(codeEnd - codeStart);
ctx->form->code = (char *)malloc(codeLen + 1);
if (ctx->form->code) {
memcpy(ctx->form->code, codeStart, codeLen);
ctx->form->code[codeLen] = '\0';
}
}
static void dsgnLoad_onFormProp(void *userData, const char *key, const char *value) {
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
if (!ctx->form) {
return;
}
char val[DSGN_MAX_TEXT];
snprintf(val, sizeof(val), "%s", value);
frmStripQuotes(val);
DsgnFormT *ff = ctx->form;
if (strcasecmp(key, "Caption") == 0) {
snprintf(ff->caption, DSGN_MAX_TEXT, "%s", val);
} else if (strcasecmp(key, "Layout") == 0) {
strncpy(ff->layout, val, DSGN_MAX_NAME - 1);
ff->layout[DSGN_MAX_NAME - 1] = '\0';
} else if (strcasecmp(key, "AutoSize") == 0) {
ff->autoSize = frmParseBool(val);
} else if (strcasecmp(key, "Resizable") == 0) {
ff->resizable = frmParseBool(val);
} else if (strcasecmp(key, "Centered") == 0) {
ff->centered = frmParseBool(val);
} else if (strcasecmp(key, "Left") == 0) {
ff->left = atoi(val);
} else if (strcasecmp(key, "Top") == 0) {
ff->top = atoi(val);
} else if (strcasecmp(key, "Width") == 0) {
ff->width = atoi(val);
ff->autoSize = false;
} else if (strcasecmp(key, "Height") == 0) {
ff->height = atoi(val);
ff->autoSize = false;
} else if (strcasecmp(key, "HelpTopic") == 0) {
snprintf(ff->helpTopic, DSGN_MAX_NAME, "%s", val);
}
}
static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level) {
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
if (!ctx->form) {
return;
}
DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi));
snprintf(mi.name, DSGN_MAX_NAME, "%s", name);
mi.level = level;
mi.enabled = true;
arrput(ctx->form->menuItems, mi);
ctx->curMenuItemIdx = (int32_t)arrlen(ctx->form->menuItems) - 1;
ctx->current = NULL;
}
static void dsgnLoad_onMenuEnd(void *userData) {
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
ctx->curMenuItemIdx = -1;
}
static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *value) {
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
if (!ctx->form ||
ctx->curMenuItemIdx < 0 ||
ctx->curMenuItemIdx >= (int32_t)arrlen(ctx->form->menuItems)) {
return;
}
// Resolve pointer fresh each write -- arrput on nested menus may
// have reallocated the array.
DsgnMenuItemT *mip = &ctx->form->menuItems[ctx->curMenuItemIdx];
char val[DSGN_MAX_TEXT];
snprintf(val, sizeof(val), "%s", value);
frmStripQuotes(val);
if (strcasecmp(key, "Caption") == 0) {
snprintf(mip->caption, DSGN_MAX_TEXT, "%s", val);
} else if (strcasecmp(key, "Checked") == 0) {
mip->checked = frmParseBool(val);
} else if (strcasecmp(key, "RadioCheck") == 0) {
mip->radioCheck = frmParseBool(val);
} else if (strcasecmp(key, "Enabled") == 0) {
mip->enabled = frmParseBool(val);
}
}
void dsgnNewForm(DsgnStateT *ds, const char *name) {
dsgnFree(ds);
@ -1218,21 +1187,6 @@ static void rebuildWidgets(DsgnStateT *ds) {
}
static const char *resolveTypeName(const char *typeName) {
const char *wgtName = wgtFindByBasName(typeName);
if (wgtName) {
return wgtName;
}
if (wgtGetApi(typeName)) {
return typeName;
}
return NULL;
}
// Write controls at a given nesting level with the specified parent name.
static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, int32_t pos, const char *parentName, int32_t indent) {
int32_t count = (int32_t)arrlen(form->controls);
@ -1318,26 +1272,17 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i
continue;
}
if (p->type == WGT_IFACE_ENUM && p->enumNames) {
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
const char *name = NULL;
// Skip STRING props here: saveControls only emits
// the iface-known scalar types. String values come
// through ctrl->props[] (custom props).
if (p->type == WGT_IFACE_STRING) {
continue;
}
for (int32_t en = 0; p->enumNames[en]; en++) {
if (en == v) {
name = p->enumNames[en];
break;
}
}
char valBuf[DSGN_MAX_TEXT];
if (name) {
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, name);
}
} else if (p->type == WGT_IFACE_INT) {
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %d\n", pad, p->name, (int)v);
} else if (p->type == WGT_IFACE_BOOL) {
bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget);
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, v ? "True" : "False");
if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, valBuf);
}
}
}

View file

@ -69,6 +69,7 @@
#include "../compiler/strip.h"
#include "../runtime/serialize.h"
#include "../formrt/formrt.h"
#include "../formrt/frmParser.h"
#include "dvxRes.h"
#include "../../../../libs/kpunch/sql/dvxSql.h"
#include "../runtime/vm.h"
@ -1425,6 +1426,254 @@ static void compileAndRun(void) {
}
// ============================================================
// Compile-time CtrlName.Member validator
// ============================================================
//
// The IDE (unlike bascomp) has widget DXEs loaded, so wgtGetIface
// returns live interface metadata. Combined with a static scan of
// the project's .frm files (control name -> widget type), we can
// reject typos like GfxCanvas.Boggle or LblStatus.Caphtion at
// compile time instead of letting them surface as runtime errors
// at event-click time. Dynamically-created controls (via
// CreateControl at runtime) aren't in the map; lookupCtrlType
// returns NULL for them and validation is skipped.
typedef struct {
char name[BAS_MAX_CTRL_NAME];
char wgtType[BAS_MAX_CTRL_NAME]; // "Form" for the form itself, else iface basName
} IdeCtrlMapEntryT;
typedef struct {
IdeCtrlMapEntryT *entries; // stb_ds dynamic array
} IdeValidatorCtxT;
static bool ideValidator_onFormBegin(void *ud, const char *name) {
IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
IdeCtrlMapEntryT e;
memset(&e, 0, sizeof(e));
snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name);
snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", "Form");
arrput(v->entries, e);
return true;
}
static void ideValidator_onCtrlBegin(void *ud, const char *typeName, const char *name) {
IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
IdeCtrlMapEntryT e;
memset(&e, 0, sizeof(e));
snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name);
snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", typeName ? typeName : "");
arrput(v->entries, e);
}
static void ideValidator_onMenuBegin(void *ud, const char *name, int32_t level) {
(void)level;
IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
IdeCtrlMapEntryT e;
memset(&e, 0, sizeof(e));
snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name);
snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", "Menu");
arrput(v->entries, e);
}
static const char *ideValidator_lookupCtrlType(void *ctx, const char *ctrlName) {
IdeValidatorCtxT *v = (IdeValidatorCtxT *)ctx;
if (!v || !ctrlName) {
return NULL;
}
for (int32_t i = 0; i < (int32_t)arrlen(v->entries); i++) {
if (strcasecmp(v->entries[i].name, ctrlName) == 0) {
return v->entries[i].wgtType;
}
}
return NULL;
}
// Methods that exist on every widget via callCommonMethod() in formrt.c.
static bool ideValidator_isCommonMethod(const char *methodName) {
return strcasecmp(methodName, "SetFocus") == 0 ||
strcasecmp(methodName, "Refresh") == 0 ||
strcasecmp(methodName, "SetReadOnly") == 0 ||
strcasecmp(methodName, "SetEnabled") == 0 ||
strcasecmp(methodName, "SetVisible") == 0;
}
// Properties that setProp/getProp accept on every non-form, non-menu
// widget (common props + widget text/data/help aliases).
static bool ideValidator_isCommonProp(const char *propName) {
return strcasecmp(propName, "Name") == 0 ||
strcasecmp(propName, "Left") == 0 ||
strcasecmp(propName, "Top") == 0 ||
strcasecmp(propName, "Width") == 0 ||
strcasecmp(propName, "Height") == 0 ||
strcasecmp(propName, "MinWidth") == 0 ||
strcasecmp(propName, "MinHeight") == 0 ||
strcasecmp(propName, "MaxWidth") == 0 ||
strcasecmp(propName, "MaxHeight") == 0 ||
strcasecmp(propName, "Weight") == 0 ||
strcasecmp(propName, "Visible") == 0 ||
strcasecmp(propName, "Enabled") == 0 ||
strcasecmp(propName, "Caption") == 0 ||
strcasecmp(propName, "Text") == 0 ||
strcasecmp(propName, "HelpTopic") == 0 ||
strcasecmp(propName, "DataSource") == 0 ||
strcasecmp(propName, "DataField") == 0 ||
strcasecmp(propName, "ListCount") == 0;
}
static bool ideValidator_isMethodValid(void *ctx, const char *wgtType, const char *methodName) {
(void)ctx;
if (!wgtType || !methodName) {
return true; // be permissive on malformed input
}
if (ideValidator_isCommonMethod(methodName)) {
return true;
}
// Form-level methods
if (strcasecmp(wgtType, "Form") == 0) {
return strcasecmp(methodName, "Show") == 0 ||
strcasecmp(methodName, "Hide") == 0;
}
// Menu items have no methods beyond common
if (strcasecmp(wgtType, "Menu") == 0) {
return false;
}
// Widget-specific methods from the live iface
const char *wgtName = wgtFindByBasName(wgtType);
if (!wgtName) {
return true; // unknown type -- skip validation
}
const WgtIfaceT *iface = wgtGetIface(wgtName);
if (!iface || !iface->methods) {
return true;
}
for (int32_t i = 0; i < iface->methodCount; i++) {
if (strcasecmp(iface->methods[i].name, methodName) == 0) {
return true;
}
}
return false;
}
static bool ideValidator_isPropValid(void *ctx, const char *wgtType, const char *propName) {
(void)ctx;
if (!wgtType || !propName) {
return true;
}
// Form-level properties
if (strcasecmp(wgtType, "Form") == 0) {
return strcasecmp(propName, "Name") == 0 ||
strcasecmp(propName, "Caption") == 0 ||
strcasecmp(propName, "Width") == 0 ||
strcasecmp(propName, "Height") == 0 ||
strcasecmp(propName, "Left") == 0 ||
strcasecmp(propName, "Top") == 0 ||
strcasecmp(propName, "Visible") == 0 ||
strcasecmp(propName, "Resizable") == 0 ||
strcasecmp(propName, "AutoSize") == 0 ||
strcasecmp(propName, "Centered") == 0 ||
strcasecmp(propName, "Layout") == 0;
}
// Menu items
if (strcasecmp(wgtType, "Menu") == 0) {
return strcasecmp(propName, "Name") == 0 ||
strcasecmp(propName, "Checked") == 0 ||
strcasecmp(propName, "Enabled") == 0 ||
strcasecmp(propName, "Caption") == 0;
}
if (ideValidator_isCommonProp(propName)) {
return true;
}
// Widget-specific props from the live iface
const char *wgtName = wgtFindByBasName(wgtType);
if (!wgtName) {
return true;
}
const WgtIfaceT *iface = wgtGetIface(wgtName);
if (!iface || !iface->props) {
return true;
}
for (int32_t i = 0; i < iface->propCount; i++) {
if (strcasecmp(iface->props[i].name, propName) == 0) {
return true;
}
}
return false;
}
// Walk every .frm in the project and populate a (name -> wgtType) map
// the parser can consult. Caller must arrfree(ctx->entries) when done.
static void ideBuildCtrlMap(IdeValidatorCtxT *ctx) {
ctx->entries = NULL;
FrmParserCbsT cbs;
memset(&cbs, 0, sizeof(cbs));
cbs.userData = ctx;
cbs.onFormBegin = ideValidator_onFormBegin;
cbs.onCtrlBegin = ideValidator_onCtrlBegin;
cbs.onMenuBegin = ideValidator_onMenuBegin;
for (int32_t i = 0; i < sProject.fileCount; i++) {
if (!sProject.files[i].isForm) {
continue;
}
// Use the buffered source if the file is open in an editor,
// otherwise load from disk. Mirrors how the designer loads.
char *diskBuf = NULL;
const char *src = sProject.files[i].buffer;
int32_t len = src ? (int32_t)strlen(src) : 0;
if (!src) {
int32_t dlen = 0;
diskBuf = platformReadFile(sProject.files[i].path, &dlen);
src = diskBuf;
len = dlen;
}
if (src && len > 0) {
frmParse(src, len, &cbs);
}
free(diskBuf);
}
}
static bool compileProject(void) {
// Save all dirty files before compiling if Save on Run is enabled
if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) {
@ -1647,7 +1896,22 @@ static bool compileProject(void) {
}
basParserInit(parser, src, srcLen);
parser->optionExplicit = prefsGetBool(sPrefs, "editor", "optionExplicit", false);
parser->optionExplicit = sProject.optionExplicit;
// Build a name -> widget-type map from the project's .frm files
// and attach a validator so the parser can reject CtrlName.Member
// typos at compile time. The validator falls back silently for
// dynamically-created controls (not in the map).
IdeValidatorCtxT validatorCtx;
memset(&validatorCtx, 0, sizeof(validatorCtx));
ideBuildCtrlMap(&validatorCtx);
BasCtrlValidatorT validator;
validator.lookupCtrlType = ideValidator_lookupCtrlType;
validator.isMethodValid = ideValidator_isMethodValid;
validator.isPropValid = ideValidator_isPropValid;
validator.ctx = &validatorCtx;
basParserSetValidator(parser, &validator);
if (!basParse(parser)) {
// Translate global error line to local file/line for display
@ -1715,10 +1979,12 @@ static bool compileProject(void) {
basParserFree(parser);
free(parser);
free(concatBuf);
arrfree(validatorCtx.entries);
return false;
}
free(concatBuf);
arrfree(validatorCtx.entries);
BasModuleT *mod = basParserBuildModule(parser);
basParserFree(parser);
@ -2062,19 +2328,15 @@ static void dsgnCopySelected(void) {
continue;
}
if (p->type == WGT_IFACE_ENUM && p->enumNames) {
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
const char *name = (v >= 0 && p->enumNames[v]) ? p->enumNames[v] : NULL;
// Skip STRING props -- custom props handle those.
if (p->type == WGT_IFACE_STRING) {
continue;
}
if (name) {
pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, name);
}
} else if (p->type == WGT_IFACE_INT) {
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %d\n", p->name, (int)v);
} else if (p->type == WGT_IFACE_BOOL) {
bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget);
pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, v ? "True" : "False");
char valBuf[DSGN_MAX_TEXT];
if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, valBuf);
}
}
}
@ -2305,23 +2567,8 @@ static void dsgnPasteControl(void) {
}
}
if (!val) {
continue;
}
if (p->type == WGT_IFACE_ENUM && p->enumNames) {
for (int32_t en = 0; p->enumNames[en]; en++) {
if (strcasecmp(p->enumNames[en], val) == 0) {
((void (*)(WidgetT *, int32_t))p->setFn)(ctrl.widget, en);
break;
}
}
} else if (p->type == WGT_IFACE_INT) {
((void (*)(WidgetT *, int32_t))p->setFn)(ctrl.widget, atoi(val));
} else if (p->type == WGT_IFACE_BOOL) {
((void (*)(WidgetT *, bool))p->setFn)(ctrl.widget, strcasecmp(val, "True") == 0);
} else if (p->type == WGT_IFACE_STRING) {
((void (*)(WidgetT *, const char *))p->setFn)(ctrl.widget, val);
if (val) {
wgtApplyPropFromString(ctrl.widget, p, val);
}
}
}
@ -8077,7 +8324,7 @@ static void showPreferencesDialog(void) {
sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming");
wgtCheckboxSetChecked(sPrefsDlg.renameSkipComments, prefsGetBool(sPrefs, "editor", "renameSkipComments", true));
sPrefsDlg.optionExplicit = wgtCheckbox(edFrame, "Require variable declaration (OPTION EXPLICIT)");
sPrefsDlg.optionExplicit = wgtCheckbox(edFrame, "OPTION EXPLICIT default for new projects");
wgtCheckboxSetChecked(sPrefsDlg.optionExplicit, prefsGetBool(sPrefs, "editor", "optionExplicit", false));
WidgetT *tabRow = wgtHBox(edFrame);

View file

@ -51,6 +51,7 @@
#include "dvxWm.h"
#include "box/box.h"
#include "button/button.h"
#include "checkbox/checkbox.h"
#include "dropdown/dropdown.h"
#include "image/image.h"
#include "label/label.h"
@ -98,6 +99,7 @@ static struct {
WidgetT *copyright;
WidgetT *description;
WidgetT *startupForm;
WidgetT *optionExplicit;
const char **formNames; // stb_ds array of form name strings for startup dropdown
WidgetT *helpFileInput;
WidgetT *iconPreview;
@ -522,6 +524,8 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
val = prefsGetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, NULL);
if (val) { snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", val); }
prj->optionExplicit = prefsGetBool(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
prefsClose(h);
prj->dirty = false;
return true;
@ -608,6 +612,7 @@ void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandle
snprintf(prj->version, sizeof(prj->version), "%s", prefsGetString(prefs, "defaults", "version", "1.0"));
snprintf(prj->copyright, sizeof(prj->copyright), "%s", prefsGetString(prefs, "defaults", "copyright", ""));
snprintf(prj->description, sizeof(prj->description), "%s", prefsGetString(prefs, "defaults", "description", ""));
prj->optionExplicit = prefsGetBool(prefs, "editor", "optionExplicit", false);
}
}
@ -617,7 +622,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
return false;
}
WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 380, false);
WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 410, false);
if (!win) {
return false;
@ -739,6 +744,10 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
hlpBrowse->onClick = ppdOnBrowseHelp;
}
// Compiler options: OPTION EXPLICIT enforces DIM-before-use for this project.
sPpd.optionExplicit = wgtCheckbox(root, "Require variable declaration (OPTION EXPLICIT)");
wgtCheckboxSetChecked(sPpd.optionExplicit, prj->optionExplicit);
// Description: label above, textarea below (matches Preferences layout)
wgtLabel(root, "Description:");
sPpd.description = wgtTextArea(root, PRJ_MAX_DESC);
@ -804,6 +813,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
}
}
prj->optionExplicit = wgtCheckboxIsChecked(sPpd.optionExplicit);
prj->dirty = true;
}
@ -948,6 +958,7 @@ bool prjSave(const PrjStateT *prj) {
// [Settings] section
prefsSetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, prj->startupForm);
prefsSetBool(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, prj->optionExplicit);
bool ok = prefsSaveAs(h, prj->projectPath);
prefsClose(h);

View file

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

View file

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

View file

@ -274,6 +274,10 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
char *name = rStr(&r);
snprintf(p->name, BAS_MAX_PROC_NAME, "%s", name);
free(name);
char *formName = rStr(&r);
snprintf(p->formName, BAS_MAX_PROC_NAME, "%s", formName);
free(formName);
}
}
@ -294,6 +298,19 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
}
}
// Global-slot runtime type init table (survives strip). STRING
// globals need this so first-use string ops work correctly.
mod->globalInitCount = rI32(&r);
if (mod->globalInitCount > 0) {
mod->globalInits = (BasGlobalInitT *)calloc(mod->globalInitCount, sizeof(BasGlobalInitT));
for (int32_t i = 0; i < mod->globalInitCount; i++) {
mod->globalInits[i].index = rI32(&r);
mod->globalInits[i].dataType = rU8(&r);
}
}
return mod;
}
@ -323,6 +340,7 @@ void basModuleFree(BasModuleT *mod) {
free(mod->procs);
free(mod->formVarInfo);
free(mod->globalInits);
free(mod->debugVars);
if (mod->debugUdtDefs) {
@ -411,6 +429,7 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
bufWriteU8(&b, p->returnType);
bufWriteU8(&b, p->isFunction ? 1 : 0);
bufWriteStr(&b, p->name);
bufWriteStr(&b, p->formName);
}
// Form variable info (runtime-essential for per-form variable allocation)
@ -424,6 +443,14 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
bufWriteI32(&b, fv->initCodeLen);
}
// Global init table (runtime-essential for STRING default slot type)
bufWriteI32(&b, mod->globalInitCount);
for (int32_t i = 0; i < mod->globalInitCount; i++) {
bufWriteI32(&b, mod->globalInits[i].index);
bufWriteU8(&b, mod->globalInits[i].dataType);
}
*outLen = b.len;
return b.buf;
}

View file

@ -68,11 +68,19 @@ void basUdtFree(BasUdtT *udt);
BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount);
BasUdtT *basUdtRef(BasUdtT *udt);
void basUdtUnref(BasUdtT *udt);
BasValueT basValBool(bool v);
int32_t basValCompare(BasValueT a, BasValueT b);
int32_t basValCompareCI(BasValueT a, BasValueT b);
BasValueT basValCopy(BasValueT v);
BasValueT basValDouble(double v);
BasStringT *basValFormatString(BasValueT v);
BasValueT basValInteger(int16_t v);
bool basValIsTruthy(BasValueT v);
BasValueT basValLong(int32_t v);
BasValueT basValObject(void *obj);
uint8_t basValPromoteType(uint8_t a, uint8_t b);
void basValRelease(BasValueT *v);
BasValueT basValSingle(float v);
BasValueT basValString(BasStringT *s);
BasValueT basValStringFromC(const char *text);
BasValueT basValToBool(BasValueT v);
@ -399,8 +407,16 @@ void basUdtUnref(BasUdtT *udt) {
// ============================================================
// Value constructors (trivial ones moved to values.h as static inline)
// Value constructors / refcount helpers
// ============================================================
BasValueT basValBool(bool v) {
BasValueT val;
val.type = BAS_TYPE_BOOLEAN;
val.boolVal = v ? -1 : 0;
return val;
}
int32_t basValCompare(BasValueT a, BasValueT b) {
// String comparison
if (a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) {
@ -445,6 +461,27 @@ int32_t basValCompareCI(BasValueT a, BasValueT b) {
}
BasValueT basValCopy(BasValueT v) {
if (v.type == BAS_TYPE_STRING && v.strVal) {
basStringRef(v.strVal);
} else if (v.type == BAS_TYPE_ARRAY && v.arrVal) {
basArrayRef(v.arrVal);
} else if (v.type == BAS_TYPE_UDT && v.udtVal) {
basUdtRef(v.udtVal);
}
return v;
}
BasValueT basValDouble(double v) {
BasValueT val;
val.type = BAS_TYPE_DOUBLE;
val.dblVal = v;
return val;
}
BasStringT *basValFormatString(BasValueT v) {
char buf[64];
@ -478,6 +515,14 @@ BasStringT *basValFormatString(BasValueT v) {
}
BasValueT basValInteger(int16_t v) {
BasValueT val;
val.type = BAS_TYPE_INTEGER;
val.intVal = v;
return val;
}
bool basValIsTruthy(BasValueT v) {
switch (v.type) {
case BAS_TYPE_INTEGER:
@ -504,6 +549,22 @@ bool basValIsTruthy(BasValueT v) {
}
BasValueT basValLong(int32_t v) {
BasValueT val;
val.type = BAS_TYPE_LONG;
val.longVal = v;
return val;
}
BasValueT basValObject(void *obj) {
BasValueT val;
val.type = BAS_TYPE_OBJECT;
val.objVal = obj;
return val;
}
uint8_t basValPromoteType(uint8_t a, uint8_t b) {
// String stays string (concat, not arithmetic)
if (a == BAS_TYPE_STRING || b == BAS_TYPE_STRING) {
@ -529,6 +590,28 @@ uint8_t basValPromoteType(uint8_t a, uint8_t b) {
}
void basValRelease(BasValueT *v) {
if (v->type == BAS_TYPE_STRING) {
basStringUnref(v->strVal);
v->strVal = NULL;
} else if (v->type == BAS_TYPE_ARRAY) {
basArrayUnref(v->arrVal);
v->arrVal = NULL;
} else if (v->type == BAS_TYPE_UDT) {
basUdtUnref(v->udtVal);
v->udtVal = NULL;
}
}
BasValueT basValSingle(float v) {
BasValueT val;
val.type = BAS_TYPE_SINGLE;
val.sngVal = v;
return val;
}
BasValueT basValString(BasStringT *s) {
BasValueT val;
val.type = BAS_TYPE_STRING;

View file

@ -31,8 +31,6 @@
#ifndef DVXBASIC_VALUES_H
#define DVXBASIC_VALUES_H
#include "../compiler/opcodes.h" // BAS_TYPE_*
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
@ -159,89 +157,22 @@ struct BasValueTag {
};
};
// Create values -- trivial constructors inlined so they're fast in vm.c's
// hot path (PUSH_INT16, PUSH_TRUE, etc.).
static inline BasValueT basValInteger(int16_t v) {
BasValueT val;
val.type = BAS_TYPE_INTEGER;
val.intVal = v;
return val;
}
static inline BasValueT basValLong(int32_t v) {
BasValueT val;
val.type = BAS_TYPE_LONG;
val.longVal = v;
return val;
}
static inline BasValueT basValSingle(float v) {
BasValueT val;
val.type = BAS_TYPE_SINGLE;
val.sngVal = v;
return val;
}
static inline BasValueT basValDouble(double v) {
BasValueT val;
val.type = BAS_TYPE_DOUBLE;
val.dblVal = v;
return val;
}
static inline BasValueT basValBool(bool v) {
BasValueT val;
val.type = BAS_TYPE_BOOLEAN;
val.boolVal = v ? -1 : 0;
return val;
}
static inline BasValueT basValObject(void *obj) {
BasValueT val;
val.type = BAS_TYPE_OBJECT;
val.objVal = obj;
return val;
}
// Create values. Out-of-line so they can be resolved via the DXE
// dynamic-symbol table when basrt.lib is loaded.
BasValueT basValInteger(int16_t v);
BasValueT basValLong(int32_t v);
BasValueT basValSingle(float v);
BasValueT basValDouble(double v);
BasValueT basValBool(bool v);
BasValueT basValObject(void *obj);
BasValueT basValString(BasStringT *s);
BasValueT basValStringFromC(const char *text);
// Copy a value (increments string/array/udt refcount if applicable).
// Inlined so the hot-path case (integer/float/bool) is a single struct
// return -- no function call, no branch beyond the type test.
static inline BasValueT basValCopy(BasValueT v) {
if (v.type == BAS_TYPE_STRING && v.strVal) {
basStringRef(v.strVal);
} else if (v.type == BAS_TYPE_ARRAY && v.arrVal) {
basArrayRef(v.arrVal);
} else if (v.type == BAS_TYPE_UDT && v.udtVal) {
basUdtRef(v.udtVal);
}
BasValueT basValCopy(BasValueT v);
return v;
}
// Release a value (decrements refcount if applicable). Integer/float/bool
// types are a no-op -- the common case is an immediately-returning branch.
static inline void basValRelease(BasValueT *v) {
if (v->type == BAS_TYPE_STRING) {
basStringUnref(v->strVal);
v->strVal = NULL;
} else if (v->type == BAS_TYPE_ARRAY) {
basArrayUnref(v->arrVal);
v->arrVal = NULL;
} else if (v->type == BAS_TYPE_UDT) {
basUdtUnref(v->udtVal);
v->udtVal = NULL;
}
}
// Release a value (decrements refcount if applicable).
void basValRelease(BasValueT *v);
// Convert a value to a specific type. Returns the converted value.
// The original is NOT released -- caller manages lifetime.

View file

@ -97,8 +97,9 @@ bool basVmCallSub(BasVmT *vm, int32_t codeAddr) {
// Push a call frame that returns to an invalid address (sentinel)
// We detect completion when callDepth drops back to savedCallDepth
BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
frame->returnPc = savedPc;
frame->localCount = BAS_VM_MAX_LOCALS;
frame->returnPc = savedPc;
frame->localCount = BAS_VM_MAX_LOCALS;
frame->errorHandler = 0;
memset(frame->locals, 0, sizeof(frame->locals));
// Jump to the SUB
@ -131,8 +132,9 @@ bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, i
bool savedRunning = vm->running;
BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
frame->returnPc = savedPc;
frame->localCount = BAS_VM_MAX_LOCALS;
frame->returnPc = savedPc;
frame->localCount = BAS_VM_MAX_LOCALS;
frame->errorHandler = 0;
memset(frame->locals, 0, sizeof(frame->locals));
// Set arguments as locals (parameter 0 = local 0, etc.)
@ -169,8 +171,9 @@ bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args
bool savedRunning = vm->running;
BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
frame->returnPc = savedPc;
frame->localCount = BAS_VM_MAX_LOCALS;
frame->returnPc = savedPc;
frame->localCount = BAS_VM_MAX_LOCALS;
frame->errorHandler = 0;
memset(frame->locals, 0, sizeof(frame->locals));
for (int32_t i = 0; i < argCount && i < BAS_VM_MAX_LOCALS; i++) {
@ -269,6 +272,28 @@ const char *basVmGetError(const BasVmT *vm) {
void basVmLoadModule(BasVmT *vm, BasModuleT *module) {
vm->module = module;
vm->pc = module->entryPoint;
// Initialize globals whose declared type requires a non-numeric
// default. Uninitialized slots are zero (type=INTEGER, value=0),
// which breaks STRING operations: `DIM s AS STRING : s = s + "x"`
// would do numeric addition instead of concatenation. STATIC
// variables also alias global slots, so the same issue applies.
// globalInits survives the compiler's strip pass, unlike debugVars.
if (module->globalInits) {
for (int32_t i = 0; i < module->globalInitCount; i++) {
BasGlobalInitT *g = &module->globalInits[i];
if (g->index < 0 || g->index >= BAS_VM_MAX_GLOBALS) {
continue;
}
if (g->dataType == BAS_TYPE_STRING) {
basValRelease(&vm->globals[g->index]);
vm->globals[g->index].type = BAS_TYPE_STRING;
vm->globals[g->index].strVal = basStringNew("", 0);
}
}
}
}
@ -325,13 +350,40 @@ BasVmResultE basVmRun(BasVmT *vm) {
if (result != BAS_VM_OK) {
// If an error handler is set and this is a trappable error,
// jump to the handler instead of stopping execution
if (vm->errorHandler != 0 && !vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) {
vm->errorPc = savedPc;
vm->errorNextPc = vm->pc;
vm->inErrorHandler = true;
vm->pc = vm->errorHandler;
continue;
// unwind call frames until we find the SUB whose ON ERROR
// GOTO registered a handler, then jump there. Without this
// unwind an error raised inside a called SUB would fire the
// outer SUB's handler but OP_RET would then pop the inner
// frame and resume after the call site (effectively RESUME
// NEXT semantics), which is not what ON ERROR promises.
if (!vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) {
int32_t target = 0;
while (vm->callDepth > 0) {
BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1];
if (frame->errorHandler != 0) {
target = frame->errorHandler;
break;
}
// No handler on this frame -- discard it and keep
// unwinding. frame-local values need releasing.
for (int32_t li = 0; li < frame->localCount; li++) {
basValRelease(&frame->locals[li]);
}
vm->callDepth--;
}
if (target != 0) {
vm->errorPc = savedPc;
vm->errorNextPc = vm->pc;
vm->inErrorHandler = true;
vm->errorHandler = target;
vm->pc = target;
continue;
}
}
vm->running = false;
@ -840,8 +892,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
frame->returnPc = vm->pc;
frame->localCount = BAS_VM_MAX_LOCALS;
frame->returnPc = vm->pc;
frame->localCount = BAS_VM_MAX_LOCALS;
frame->errorHandler = 0;
// Zero all local slots
memset(frame->locals, 0, sizeof(frame->locals));
@ -853,6 +906,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
}
// The callee starts with no handler; if it raises an error
// the dispatcher will unwind to the nearest frame with one.
vm->errorHandler = 0;
vm->pc = addr;
break;
}
@ -870,7 +926,24 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&frame->locals[i]);
}
bool hadHandler = (frame->errorHandler != 0);
frame->errorHandler = 0;
vm->pc = frame->returnPc;
// Restore the active handler to whatever the caller had set
BasCallFrameT *caller = currentFrame(vm);
vm->errorHandler = caller ? caller->errorHandler : 0;
// If this SUB owned the active error handler, any handler
// body that ran inside it is now done -- clear the flag so
// the next error (in this or another SUB) can trap again.
// QBASIC documents this: an unresumed handler is cleared
// when the procedure that installed it returns.
if (hadHandler) {
vm->inErrorHandler = false;
vm->errorNumber = 0;
vm->errorMsg[0] = '\0';
}
break;
}
@ -894,8 +967,19 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&frame->locals[i]);
}
bool hadHandler = (frame->errorHandler != 0);
frame->errorHandler = 0;
vm->pc = frame->returnPc;
BasCallFrameT *caller = currentFrame(vm);
vm->errorHandler = caller ? caller->errorHandler : 0;
if (hadHandler) {
vm->inErrorHandler = false;
vm->errorNumber = 0;
vm->errorMsg[0] = '\0';
}
if (!push(vm, retVal)) {
basValRelease(&retVal);
return BAS_VM_STACK_OVERFLOW;
@ -918,8 +1002,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
case OP_FOR_INIT: {
uint16_t varIdx = readUint16(vm);
uint8_t scopeTag = readUint8(vm);
uint16_t varIdx = readUint16(vm);
uint8_t scopeTag = readUint8(vm);
int16_t skipOffset = readInt16(vm);
BasValueT stepVal;
BasValueT limitVal;
@ -940,6 +1025,43 @@ BasVmResultE basVmStep(BasVmT *vm) {
fs->limit = limitVal;
fs->step = stepVal;
fs->loopTop = vm->pc;
// Entry check: if the loop's range is already empty (e.g.
// FOR i = 10 TO 5 with positive step), skip the body and
// pop the FOR state. QBASIC semantics: body executes zero
// times when the range is empty.
BasValueT *varSlot = NULL;
if (scopeTag == SCOPE_LOCAL) {
BasCallFrameT *frame = currentFrame(vm);
if (frame && varIdx < (uint16_t)frame->localCount) {
varSlot = &frame->locals[varIdx];
}
} else if (scopeTag == SCOPE_FORM) {
if (vm->currentFormVars && varIdx < (uint16_t)vm->currentFormVarCount) {
varSlot = &vm->currentFormVars[varIdx];
}
} else {
if (varIdx < BAS_VM_MAX_GLOBALS) {
varSlot = &vm->globals[varIdx];
}
}
if (varSlot) {
double curVal = basValToNumber(*varSlot);
double stepNum = basValToNumber(fs->step);
double limNum = basValToNumber(fs->limit);
bool enter = (stepNum >= 0) ? (curVal <= limNum) : (curVal >= limNum);
if (!enter) {
basValRelease(&fs->limit);
basValRelease(&fs->step);
vm->forDepth--;
vm->pc += skipOffset;
}
}
break;
}
@ -990,7 +1112,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
// Increment: var = var + step. Preserve the loop variable's
// original type so extern calls that pass it as an int32_t
// argument don't get an 8-byte double instead.
// argument don't get an 8-byte double instead. Exception:
// if STEP is fractional, an integer slot would truncate the
// increment and spin forever (FOR a = 0 TO 6.3 STEP 0.2 with
// `a` stored as INTEGER from the literal 0). Promote to
// DOUBLE in that case so the loop can actually progress.
double varVal = basValToNumber(*varSlot);
double stepVal = basValToNumber(fs->step);
double limVal = basValToNumber(fs->limit);
@ -999,7 +1125,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(varSlot);
if (varType == BAS_TYPE_INTEGER) {
bool stepIsFractional = (stepVal != floor(stepVal));
if (stepIsFractional) {
*varSlot = basValDouble(varVal);
} else if (varType == BAS_TYPE_INTEGER) {
*varSlot = basValInteger((int16_t)varVal);
} else if (varType == BAS_TYPE_LONG) {
*varSlot = basValLong((int32_t)varVal);
@ -1639,6 +1769,40 @@ BasVmResultE basVmStep(BasVmT *vm) {
break;
}
case OP_STR_OCT: {
if (vm->sp < 1) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT *top = &vm->stack[vm->sp - 1];
int32_t n = (int32_t)basValToNumber(*top);
char buf[16];
snprintf(buf, sizeof(buf), "%o", (unsigned int)n);
basValRelease(top);
*top = basValStringFromC(buf);
break;
}
case OP_CONV_BOOL: {
if (vm->sp < 1) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT *top = &vm->stack[vm->sp - 1];
bool truthy;
if (top->type == BAS_TYPE_STRING) {
truthy = (top->strVal && top->strVal->len > 0);
} else {
truthy = (basValToNumber(*top) != 0.0);
}
basValRelease(top);
top->type = BAS_TYPE_BOOLEAN;
top->boolVal = truthy ? -1 : 0;
break;
}
case OP_STR_STRING: {
// STRING$(n, char)
BasValueT charVal;
@ -1827,7 +1991,19 @@ BasVmResultE basVmStep(BasVmT *vm) {
case OP_ON_ERROR: {
int16_t handler = readInt16(vm);
vm->errorHandler = (handler == 0) ? 0 : vm->pc + handler;
int32_t target = (handler == 0) ? 0 : vm->pc + handler;
// Record the handler on the current call frame. The error
// dispatcher walks frames (innermost outward) to find a
// handler and unwinds the stack to that frame before
// jumping. Module-level code lives in frame 0.
BasCallFrameT *frame = currentFrame(vm);
if (frame) {
frame->errorHandler = target;
}
vm->errorHandler = target;
break;
}
@ -2032,6 +2208,58 @@ BasVmResultE basVmStep(BasVmT *vm) {
break;
}
case OP_PUSH_ARR_ADDR: {
// Pass `arr(i)` as a BYREF parameter: push a BAS_TYPE_REF
// pointing into the array's element storage. The array is
// ref-counted, and the caller still holds a reference via
// the local it came from, so the element memory is stable
// for the duration of the call.
uint8_t dims = readUint8(vm);
int32_t indices[BAS_ARRAY_MAX_DIMS];
for (int32_t d = dims - 1; d >= 0; d--) {
BasValueT idxVal;
if (!pop(vm, &idxVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
indices[d] = (int32_t)basValToNumber(idxVal);
basValRelease(&idxVal);
}
BasValueT arrRef;
if (!pop(vm, &arrRef)) {
return BAS_VM_STACK_UNDERFLOW;
}
if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) {
basValRelease(&arrRef);
runtimeError(vm, 13, "Not an array");
return BAS_VM_TYPE_MISMATCH;
}
int32_t flatIdx = basArrayIndex(arrRef.arrVal, indices, dims);
if (flatIdx < 0) {
basValRelease(&arrRef);
runtimeError(vm, 9, "Subscript out of range");
return BAS_VM_SUBSCRIPT_RANGE;
}
BasValueT ref;
ref.type = BAS_TYPE_REF;
ref.refVal = &arrRef.arrVal->elements[flatIdx];
basValRelease(&arrRef);
if (!push(vm, ref)) {
return BAS_VM_STACK_OVERFLOW;
}
break;
}
case OP_STORE_ARRAY: {
uint8_t dims = readUint8(vm);
@ -3623,6 +3851,15 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
}
}
// Capture operand types BEFORE release so we know whether to keep
// the result as a float. The parser always emits OP_ADD_INT (the
// VM promotes based on operand types), so the opcode alone can't
// tell us whether this is an integer or floating-point op.
uint8_t aType = a.type;
uint8_t bType = b.type;
bool hadFloat = (aType == BAS_TYPE_SINGLE || aType == BAS_TYPE_DOUBLE ||
bType == BAS_TYPE_SINGLE || bType == BAS_TYPE_DOUBLE);
double na = basValToNumber(a);
double nb = basValToNumber(b);
basValRelease(&a);
@ -3682,8 +3919,12 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
break;
}
// Return appropriate type
if (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT) {
// Return appropriate type. An op that would normally produce an
// integer result (OP_ADD_INT, etc.) still has to yield a float when
// either operand was a float -- otherwise 1.5 + 2.25 truncates to 3.
bool intOp = (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT);
if (intOp && !hadFloat) {
if (result >= (double)INT16_MIN && result <= (double)INT16_MAX) {
push(vm, basValInteger((int16_t)result));
} else if (result >= (double)INT32_MIN && result <= (double)INT32_MAX) {
@ -5333,17 +5574,19 @@ static bool push(BasVmT *vm, BasValueT val) {
}
// x86 tolerates unaligned loads at cost of ~1 cycle; cast-through-pointer is
// faster than memcpy because the compiler can emit a single 16-bit load.
// memcpy with constant size is folded to a single load by the compiler and
// is alignment-safe (bytecode operands aren't guaranteed 2-byte aligned).
static inline int16_t readInt16(BasVmT *vm) {
int16_t val = *(const int16_t *)&vm->module->code[vm->pc];
int16_t val;
memcpy(&val, &vm->module->code[vm->pc], sizeof(int16_t));
vm->pc += sizeof(int16_t);
return val;
}
static inline uint16_t readUint16(BasVmT *vm) {
uint16_t val = *(const uint16_t *)&vm->module->code[vm->pc];
uint16_t val;
memcpy(&val, &vm->module->code[vm->pc], sizeof(uint16_t));
vm->pc += sizeof(uint16_t);
return val;
}
@ -5364,6 +5607,7 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
bool hadBreakpoint = false;
while (vm->running && vm->callDepth > savedCallDepth) {
int32_t stepPc = vm->pc; // save for ON ERROR dispatch
BasVmResultE result = basVmStep(vm);
if (result == BAS_VM_HALTED) {
@ -5397,6 +5641,41 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
}
if (result != BAS_VM_OK) {
// Try ON ERROR GOTO: walk call frames inside the current
// sub-call boundary (savedCallDepth) looking for one that
// registered a handler. If found, unwind to that frame
// and jump to the handler. Mirrors basVmRun's dispatcher
// so event handlers (fired via basVmCallSub) behave like
// module-level code when it comes to error trapping.
if (!vm->inErrorHandler && result != BAS_VM_BAD_OPCODE) {
int32_t target = 0;
while (vm->callDepth > savedCallDepth) {
BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1];
if (frame->errorHandler != 0) {
target = frame->errorHandler;
break;
}
for (int32_t li = 0; li < frame->localCount; li++) {
basValRelease(&frame->locals[li]);
}
vm->callDepth--;
}
if (target != 0) {
vm->errorPc = stepPc;
vm->errorNextPc = vm->pc;
vm->inErrorHandler = true;
vm->errorHandler = target;
vm->pc = target;
stepsSinceYield = 0;
continue;
}
}
vm->pc = savedPc;
vm->callDepth = savedCallDepth;
vm->running = savedRunning;

View file

@ -262,6 +262,7 @@ typedef struct {
int32_t returnPc; // instruction to return to
int32_t baseSlot; // base index in locals array
int32_t localCount; // number of locals in this frame
int32_t errorHandler; // ON ERROR GOTO target in this SUB (0 = none)
BasValueT locals[BAS_VM_MAX_LOCALS];
} BasCallFrameT;
@ -293,12 +294,13 @@ typedef struct {
#define BAS_MAX_PROC_NAME 64
typedef struct {
char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved)
int32_t codeAddr; // entry point in code[]
int32_t paramCount; // number of parameters
int32_t localCount; // number of local variables (for debugger)
uint8_t returnType; // BAS_TYPE_* (0 for SUB)
bool isFunction; // true = FUNCTION, false = SUB
char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved)
char formName[BAS_MAX_PROC_NAME]; // owning form (for form-scope vars), "" if global
int32_t codeAddr; // entry point in code[]
int32_t paramCount; // number of parameters
int32_t localCount; // number of local variables (for debugger)
uint8_t returnType; // BAS_TYPE_* (0 for SUB)
bool isFunction; // true = FUNCTION, false = SUB
} BasProcEntryT;
// Debug UDT field definition (preserved for debugger watch)
@ -340,6 +342,16 @@ typedef struct {
// Compiled module (output of the compiler)
// ============================================================
// Runtime-required global init entry. STRING and SINGLE/DOUBLE
// globals need to start with the correct slot type even when debug
// info has been stripped, or operators that switch on slot type
// (e.g. STRING concat) break on first use.
typedef struct {
int32_t index; // global slot index
uint8_t dataType; // BAS_TYPE_*
} BasGlobalInitT;
typedef struct {
uint8_t *code; // p-code bytecode
int32_t codeLen;
@ -353,6 +365,8 @@ typedef struct {
int32_t procCount;
BasFormVarInfoT *formVarInfo; // per-form variable counts
int32_t formVarInfoCount;
BasGlobalInitT *globalInits; // runtime global type init (survives stripping)
int32_t globalInitCount;
BasDebugVarT *debugVars; // variable names for debugger
int32_t debugVarCount;
BasDebugUdtDefT *debugUdtDefs; // UDT type definitions for debugger

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

View file

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

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)
emit8(OP_PUSH_INT16); emit16(5);
emit8(OP_PUSH_INT16); emit16(1);
emit8(OP_FOR_INIT); emitU16(0); emit8(1); // isLocal=1
emit8(OP_FOR_INIT); emitU16(0); emit8(1); emit16(0); // scope=local, skipOffset patched below
// Loop body start (record PC for FOR_NEXT offset)
int32_t loopBody = sCodeLen;

View file

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

View file

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

View file

@ -46,6 +46,10 @@ DECLARE LIBRARY "basrt"
' Show a modal text input box. Returns entered text, or "" if cancelled.
DECLARE FUNCTION basInputBox2(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL defaultText AS STRING) AS STRING
' Returns True if the most recent basInputBox2 call was cancelled,
' False if the user clicked OK (even with an empty field).
DECLARE FUNCTION basInputCancelled() AS INTEGER
' Show a choice dialog with a listbox. items$ is pipe-delimited
' (e.g. "Red|Green|Blue"). Returns chosen index (0-based), or -1.
DECLARE FUNCTION basChoiceDialog(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL items AS STRING, BYVAL defaultIdx AS INTEGER) AS INTEGER

View file

@ -1193,6 +1193,14 @@ static void dispatchEvents(AppContextT *ctx) {
int32_t menuId = item->id;
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
closeAllPopups(ctx);
// Consume the press so that if the menu handler
// re-enters the event loop (CreateForm/Show, modal
// dialog) the still-held button isn't re-dispatched
// as a fresh MouseDown to whatever widget is under
// the (now-closed) menu dropdown. The matching
// release is also swallowed via suppressNextMouseUp.
ctx->prevMouseButtons |= MOUSE_LEFT;
ctx->suppressNextMouseUp = true;
if (win && win->onMenu) {
WIN_CALLBACK(ctx, win, win->onMenu(win, menuId));
@ -1363,9 +1371,14 @@ static void dispatchEvents(AppContextT *ctx) {
}
}
// Handle button release on content -- send to focused window
// Handle button release on content -- send to focused window.
// Skip if a menu click consumed this press (prevents the stray
// release from firing as a click on whatever widget sits under
// where the menu dropdown was).
if (!(buttons & MOUSE_LEFT) && (prevBtn & MOUSE_LEFT)) {
if (ctx->stack.focusedIdx >= 0) {
if (ctx->suppressNextMouseUp) {
ctx->suppressNextMouseUp = false;
} else if (ctx->stack.focusedIdx >= 0) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
if (win->onMouse) {

View file

@ -82,6 +82,10 @@ typedef struct AppContextT {
int32_t prevMouseX;
int32_t prevMouseY;
int32_t prevMouseButtons;
// Set true when a menu/popup click consumed a mouse press; the
// matching release is then swallowed so widgets underneath the
// (now-closed) menu don't receive a stray click.
bool suppressNextMouseUp;
// Double-click detection for minimized window icons: timestamps and
// window IDs track whether two clicks land on the same icon within
// the system double-click interval.

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

View file

@ -116,6 +116,24 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
// Windows 3.x focus rectangle convention.
void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color);
// Compute the (x, y) origin for rendering `text` centered both
// horizontally and vertically inside the rect (rectX, rectY, rectW,
// rectH). Measures `text` with textWidthAccel (ignores & markers) and
// the font's charHeight. Writes results to *outX / *outY.
void calcCenteredText(const BitmapFontT *font, int32_t rectX, int32_t rectY, int32_t rectW, int32_t rectH, const char *text, int32_t *outX, int32_t *outY);
// Draw `text` with &-accelerator markers, automatically choosing
// between the enabled and disabled (embossed) rendering paths. Saves
// the if/else block every widget paints around drawTextAccel /
// drawTextAccelEmbossed.
void drawWidgetTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque, bool enabled, const ColorSchemeT *colors);
// Draw a pressable 2px bevel. When pressed is true, highlight/shadow
// colors swap to produce the "sunken" look (button pressed, toggle
// checked). face is the fill color; pass 0 to leave the interior
// alone. Used by buttons, checkboxes, toggle bars, etc.
void drawPressableBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, bool pressed, uint32_t face, const ColorSchemeT *colors);
// Horizontal line (1px tall rectangle fill, but with simpler clipping).
void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color);

View file

@ -142,6 +142,13 @@ void widgetDestroyChildren(WidgetT *w);
// Allocation
WidgetT *widgetAlloc(WidgetT *parent, int32_t type);
// Allocate a widget of the given type PLUS a data struct of dataSize
// bytes. The data struct must begin with a `const char *text` field
// (WCLASS_HAS_TEXT semantics); this field is set to strdup(text) and
// the widget's accelKey is parsed from text. Other fields in the data
// struct remain zeroed. Returns NULL on allocation failure.
WidgetT *widgetAllocWithText(WidgetT *parent, int32_t type, size_t dataSize, const char *text);
// Focus management
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after);
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before);

View file

@ -312,6 +312,22 @@ const char *dvxSkipWs(const char *s);
// new length. buf may be NULL or empty.
int32_t dvxTrimRight(char *buf);
// Case-insensitive check that `name` ends with `ext` (which should
// include the leading dot, e.g. ".app"). Returns false if either
// argument is NULL or name is shorter than ext.
bool dvxHasExt(const char *name, const char *ext);
// Read all entries from `dirPath` (except "." and "..") into a
// heap-allocated stb_ds array of malloc'd strings. Returns NULL on
// open failure. An empty directory returns a valid empty array;
// check arrlen() for count. Caller must pass the result to
// dvxReadDirFree when done.
char **dvxReadDir(const char *dirPath);
// Free an array returned by dvxReadDir: frees every entry and the
// stb_ds array header. Safe to call with NULL.
void dvxReadDirFree(char **entries);
// The platform's native directory separator as a string literal. Use
// with string-literal concatenation in format strings or path constants:
// snprintf(buf, sz, "%s" DVX_PATH_SEP "%s", dir, name);

View file

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

View file

@ -29,11 +29,14 @@
// can still use them.
#include "dvxPlat.h"
#include "thirdparty/stb_ds_wrap.h"
#include <ctype.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#ifdef __DJGPP__
@ -70,6 +73,67 @@ int32_t dvxTrimRight(char *buf) {
}
bool dvxHasExt(const char *name, const char *ext) {
if (!name || !ext) {
return false;
}
size_t nameLen = strlen(name);
size_t extLen = strlen(ext);
if (nameLen < extLen) {
return false;
}
return strcasecmp(name + nameLen - extLen, ext) == 0;
}
char **dvxReadDir(const char *dirPath) {
if (!dirPath) {
return NULL;
}
DIR *dir = opendir(dirPath);
if (!dir) {
return NULL;
}
char **entries = NULL;
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
// Skip "." and ".."
if (ent->d_name[0] == '.' &&
(ent->d_name[1] == '\0' ||
(ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
continue;
}
arrput(entries, strdup(ent->d_name));
}
closedir(dir);
return entries;
}
void dvxReadDirFree(char **entries) {
if (!entries) {
return;
}
int32_t n = (int32_t)arrlen(entries);
for (int32_t i = 0; i < n; i++) {
free(entries[i]);
}
arrfree(entries);
}
int32_t platformChdir(const char *path) {
#ifdef __DJGPP__
if (path[0] && path[1] == ':') {

View file

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

View file

@ -38,7 +38,6 @@
#include "../tools/hlpcCompile.h"
#include <ctype.h>
#include <dirent.h>
#include <dlfcn.h>
#include <stdarg.h>
#include <stdio.h>
@ -175,49 +174,41 @@ static void collectGlobFiles(char ***outFiles, const char *pattern, const char *
dirPart[1] = '\0';
}
DIR *d = opendir(dirPart);
char **names = dvxReadDir(dirPart);
if (!d) {
if (!names) {
return;
}
char **names = NULL;
struct dirent *ent;
while ((ent = readdir(d)) != NULL) {
if (ent->d_name[0] == '.') {
continue;
}
arrput(names, strdup(ent->d_name));
}
closedir(d);
int32_t nEntries = (int32_t)arrlen(names);
for (int32_t i = 0; i < nEntries; i++) {
// Skip hidden files (dvxReadDir already strips "." and "..")
if (names[i][0] == '.') {
continue;
}
char fullPath[DVX_MAX_PATH];
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPart, names[i]);
struct stat st;
if (stat(fullPath, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
char subPattern[DVX_MAX_PATH];
snprintf(subPattern, sizeof(subPattern), "%s" DVX_PATH_SEP "%s", fullPath, globPart);
collectGlobFiles(outFiles, subPattern, excludePattern);
} else if (platformGlobMatch(globPart, names[i])) {
if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) {
arrput(*outFiles, strdup(fullPath));
}
}
if (stat(fullPath, &st) != 0) {
continue;
}
free(names[i]);
if (S_ISDIR(st.st_mode)) {
char subPattern[DVX_MAX_PATH];
snprintf(subPattern, sizeof(subPattern), "%s" DVX_PATH_SEP "%s", fullPath, globPart);
collectGlobFiles(outFiles, subPattern, excludePattern);
} else if (platformGlobMatch(globPart, names[i])) {
if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) {
arrput(*outFiles, strdup(fullPath));
}
}
}
arrfree(names);
dvxReadDirFree(names);
}
@ -247,51 +238,38 @@ static int32_t countHcfInputFiles(const char *hcfPath) {
// Count total progress steps across all .hcf files under a directory.
static int32_t countTotalHelpSteps(const char *dirPath) {
DIR *dir = opendir(dirPath);
char **names = dvxReadDir(dirPath);
if (!dir) {
if (!names) {
return 0;
}
char **names = NULL;
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
if (ent->d_name[0] == '.') {
continue;
}
arrput(names, strdup(ent->d_name));
}
closedir(dir);
int32_t total = 0;
int32_t nEntries = (int32_t)arrlen(names);
for (int32_t i = 0; i < nEntries; i++) {
if (names[i][0] == '.') {
continue;
}
char fullPath[DVX_MAX_PATH];
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
struct stat st;
if (stat(fullPath, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
total += countTotalHelpSteps(fullPath);
} else {
int32_t nameLen = (int32_t)strlen(names[i]);
if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) {
int32_t fileCount = countHcfInputFiles(fullPath);
total += hlpcProgressTotal(fileCount);
}
}
if (stat(fullPath, &st) != 0) {
continue;
}
free(names[i]);
if (S_ISDIR(st.st_mode)) {
total += countTotalHelpSteps(fullPath);
} else if (dvxHasExt(names[i], ".hcf")) {
int32_t fileCount = countHcfInputFiles(fullPath);
total += hlpcProgressTotal(fileCount);
}
}
arrfree(names);
dvxReadDirFree(names);
return total;
}
@ -764,49 +742,36 @@ static void processHcf(const char *hcfPath, const char *hcfDir) {
// Recursively scan a directory for .hcf files and process each one.
static void processHcfDir(const char *dirPath) {
DIR *dir = opendir(dirPath);
char **names = dvxReadDir(dirPath);
if (!dir) {
if (!names) {
return;
}
char **names = NULL;
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
if (ent->d_name[0] == '.') {
continue;
}
arrput(names, strdup(ent->d_name));
}
closedir(dir);
int32_t nEntries = (int32_t)arrlen(names);
for (int32_t i = 0; i < nEntries; i++) {
if (names[i][0] == '.') {
continue;
}
char fullPath[DVX_MAX_PATH];
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
struct stat st;
if (stat(fullPath, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
processHcfDir(fullPath);
} else {
int32_t nameLen = (int32_t)strlen(names[i]);
if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) {
processHcf(fullPath, dirPath);
}
}
if (stat(fullPath, &st) != 0) {
continue;
}
free(names[i]);
if (S_ISDIR(st.st_mode)) {
processHcfDir(fullPath);
} else if (dvxHasExt(names[i], ".hcf")) {
processHcf(fullPath, dirPath);
}
}
arrfree(names);
dvxReadDirFree(names);
}
@ -864,35 +829,19 @@ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
// Collect all entries first, close the handle, then process.
// DOS has limited file handles; keeping a DIR open during
// recursion or stat() causes intermittent failures.
DIR *dir = opendir(dirPath);
char **names = dvxReadDir(dirPath);
if (!dir) {
if (!names) {
return;
}
char **names = NULL;
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
continue;
}
arrput(names, strdup(ent->d_name));
}
closedir(dir);
int32_t count = (int32_t)arrlen(names);
int32_t extLen = (int32_t)strlen(ext);
int32_t count = (int32_t)arrlen(names);
for (int32_t i = 0; i < count; i++) {
char path[DVX_MAX_PATH];
snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
int32_t nameLen = (int32_t)strlen(names[i]);
if (nameLen > extLen && strcasecmp(names[i] + nameLen - extLen, ext) == 0) {
if (dvxHasExt(names[i], ext)) {
ModuleT mod;
memset(&mod, 0, sizeof(mod));
snprintf(mod.path, sizeof(mod.path), "%s", path);
@ -905,11 +854,9 @@ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
scanDir(path, ext, mods);
}
}
free(names[i]);
}
arrfree(names);
dvxReadDirFree(names);
}

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

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

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

View file

@ -1045,7 +1045,7 @@ static const WgtMethodDescT sMethods[] = {
};
static const WgtIfaceT sIface = {
.basName = "PictureBox",
.basName = "Canvas",
.props = NULL,
.propCount = 0,
.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 *w = widgetAlloc(parent, sTypeId);
if (w) {
CheckboxDataT *d = calloc(1, sizeof(CheckboxDataT));
w->data = d;
d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text);
}
return w;
return widgetAllocWithText(parent, sTypeId, sizeof(CheckboxDataT), text);
}
@ -183,11 +174,7 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
int32_t labelW = textWidthAccel(font, cd->text);
if (!w->enabled) {
drawTextAccelEmbossed(d, ops, font, labelX, labelY, cd->text, colors);
} else {
drawTextAccel(d, ops, font, labelX, labelY, cd->text, fg, bg, false);
}
drawWidgetTextAccel(d, ops, font, labelX, labelY, cd->text, fg, bg, false, w->enabled, colors);
if (w == sFocusedWidget) {
drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg);

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;
bool pressed = w->pressed && w->enabled;
BevelStyleT bevel;
bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight;
bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow;
bevel.face = bgFace;
bevel.width = IMAGEBUTTON_BEVEL_W;
drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel);
drawPressableBevel(disp, ops, w->x, w->y, w->w, w->h, pressed, bgFace, colors);
if (d->pixelData) {
int32_t imgX = w->x + (w->w - d->imgW) / 2;

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

View file

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

View file

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

View file

@ -61,6 +61,7 @@ Header: widgets/textInpt.h
.table
Function Description
-------- -----------
void wgtTextAreaAppendText(w, text) Append text to the end of the buffer and invalidate the widget.
void wgtTextAreaSetColorize(w, fn, ctx) Set a syntax colorization callback. The callback receives each line and fills a color index array.
void wgtTextAreaGoToLine(w, line) Scroll to and place the cursor on the given line number.
void wgtTextAreaSetAutoIndent(w, enable) Enable or disable automatic indentation on newline.

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

View file

@ -107,6 +107,11 @@ WidgetT *wgtTimer(WidgetT *parent, int32_t intervalMs, bool repeat) {
w->visible = false;
d->intervalMs = intervalMs;
d->repeat = repeat;
// Match VB Timer default (Enabled=True on create). Callers can
// Stop() if they want it dormant.
d->running = true;
d->lastFire = clock();
timerAddToActiveList(w);
}
return w;
@ -176,6 +181,12 @@ void wgtTimerStop(WidgetT *w) {
void wgtUpdateTimers(void) {
static int32_t sUpdateCount = 0;
sUpdateCount++;
if (sUpdateCount <= 3 || sUpdateCount % 100 == 0) {
dvxLog("[T] wgtUpdateTimers call #%d activeCount=%d",
(int)sUpdateCount, (int)arrlen(sActiveTimers));
}
clock_t now = clock();
// Iterate backwards so arrdel doesn't skip entries
@ -192,6 +203,9 @@ void wgtUpdateTimers(void) {
clock_t interval = (clock_t)d->intervalMs * CLOCKS_PER_SEC / 1000;
if (elapsed >= interval) {
dvxLog("[T] timer tick: w=%p onChange=%p intervalMs=%d elapsed=%ld interval=%ld",
(void *)w, (void *)w->onChange, (int)d->intervalMs,
(long)elapsed, (long)interval);
d->lastFire = now;
if (w->onChange) {