Many BASIC compiler and VM fixes. Other fixes. New BASIC apps.

This commit is contained in:
Scott Duensing 2026-04-16 18:18:04 -05:00
parent de60200f23
commit dcd120d769
74 changed files with 6091 additions and 505 deletions

View file

@ -11,11 +11,14 @@ BINDIR = ../bin/apps
DVXRES = ../bin/host/dvxres DVXRES = ../bin/host/dvxres
# App definitions: each is a subdir with a single .c file # App definitions: each is a subdir with a single .c file
APPS = progman notepad clock dvxdemo cpanel imgview dvxhelp APPS = progman clock dvxdemo cpanel dvxhelp
.PHONY: all clean $(APPS) dvxbasic BASCOMP = ../bin/host/bascomp
BASICAPPS = iconed notepad-bas imgview-bas helpedit resedit basicdemo
all: $(APPS) dvxbasic .PHONY: all clean $(APPS) dvxbasic $(BASICAPPS)
all: $(APPS) dvxbasic $(BASICAPPS)
dvxbasic: dvxbasic:
$(MAKE) -C dvxbasic $(MAKE) -C dvxbasic
@ -30,28 +33,28 @@ dvxhelp: $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
$(BINDIR)/kpunch/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/kpunch/cpanel $(BINDIR)/kpunch/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/kpunch/cpanel
$(DXE3GEN) -o $@ -U $< $(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ cpanel/cpanel.res cd cpanel && ../$(DVXRES) build ../$@ cpanel.res
$(BINDIR)/kpunch/imgview/imgview.app: $(OBJDIR)/imgview.o imgview/imgview.res imgview/icon32.bmp | $(BINDIR)/kpunch/imgview $(BINDIR)/kpunch/imgview/imgview.app: $(OBJDIR)/imgview.o imgview/imgview.res imgview/icon32.bmp | $(BINDIR)/kpunch/imgview
$(DXE3GEN) -o $@ -U $< $(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ imgview/imgview.res cd imgview && ../$(DVXRES) build ../$@ imgview.res
$(BINDIR)/kpunch/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/kpunch/progman $(BINDIR)/kpunch/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/kpunch/progman
$(DXE3GEN) -o $@ -U $< $(DXE3GEN) -o $@ -U $<
$(BINDIR)/kpunch/notepad/notepad.app: $(OBJDIR)/notepad.o notepad/notepad.res notepad/icon32.bmp | $(BINDIR)/kpunch/notepad $(BINDIR)/kpunch/notepad/notepad.app: $(OBJDIR)/notepad.o notepad/notepad.res notepad/icon32.bmp | $(BINDIR)/kpunch/notepad
$(DXE3GEN) -o $@ -U $< $(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ notepad/notepad.res cd notepad && ../$(DVXRES) build ../$@ notepad.res
$(BINDIR)/kpunch/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/kpunch/clock $(BINDIR)/kpunch/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/kpunch/clock
$(DXE3GEN) -o $@ -U $< $(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ clock/clock.res cd clock && ../$(DVXRES) build ../$@ clock.res
DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp
$(BINDIR)/kpunch/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/kpunch/dvxdemo $(BINDIR)/kpunch/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/kpunch/dvxdemo
$(DXE3GEN) -o $@ -U $< $(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ dvxdemo/dvxdemo.res cd dvxdemo && ../$(DVXRES) build ../$@ dvxdemo.res
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/ cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/
$(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR) $(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR)
@ -74,11 +77,52 @@ $(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
$(BINDIR)/kpunch/dvxhelp/dvxhelp.app: $(OBJDIR)/dvxhelp.o dvxhelp/dvxhelp.res dvxhelp/icon32.bmp | $(BINDIR)/kpunch/dvxhelp $(BINDIR)/kpunch/dvxhelp/dvxhelp.app: $(OBJDIR)/dvxhelp.o dvxhelp/dvxhelp.res dvxhelp/icon32.bmp | $(BINDIR)/kpunch/dvxhelp
$(DXE3GEN) -o $@ -U $< $(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ dvxhelp/dvxhelp.res cd dvxhelp && ../$(DVXRES) build ../$@ dvxhelp.res
$(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR) $(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<
# BASIC apps (compiled from .dbp projects via bascomp)
iconed: $(BINDIR)/kpunch/iconed/iconed.app
$(BINDIR)/kpunch/iconed/iconed.app: ../sdk/samples/basic/iconed/iconed.dbp ../sdk/samples/basic/iconed/iconed.frm ../sdk/samples/basic/iconed/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/iconed dvxbasic
$(BASCOMP) ../sdk/samples/basic/iconed/iconed.dbp -o $@ -release
notepad-bas: $(BINDIR)/kpunch/notepad/notepad.app
$(BINDIR)/kpunch/notepad/notepad.app: ../sdk/samples/basic/notepad/notepad.dbp ../sdk/samples/basic/notepad/notepad.frm ../sdk/samples/basic/notepad/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/notepad dvxbasic
$(BASCOMP) ../sdk/samples/basic/notepad/notepad.dbp -o $@ -release
imgview-bas: $(BINDIR)/kpunch/imgview/imgview.app
$(BINDIR)/kpunch/imgview/imgview.app: ../sdk/samples/basic/imgview/imgview.dbp ../sdk/samples/basic/imgview/imgview.frm ../sdk/samples/basic/imgview/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/imgview dvxbasic
$(BASCOMP) ../sdk/samples/basic/imgview/imgview.dbp -o $@ -release
helpedit: $(BINDIR)/kpunch/dvxhelp/helpedit.app
$(BINDIR)/kpunch/dvxhelp/helpedit.app: ../sdk/samples/basic/helpedit/helpedit.dbp ../sdk/samples/basic/helpedit/helpedit.frm ../sdk/samples/basic/helpedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/dvxhelp dvxbasic
$(BASCOMP) ../sdk/samples/basic/helpedit/helpedit.dbp -o $@ -release
$(DVXRES) add $@ helpfile text "dvxhelp.hlp"
resedit: $(BINDIR)/kpunch/resedit/resedit.app
$(BINDIR)/kpunch/resedit/resedit.app: ../sdk/samples/basic/resedit/resedit.dbp ../sdk/samples/basic/resedit/resedit.frm ../sdk/samples/basic/resedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/resedit dvxbasic
$(BASCOMP) ../sdk/samples/basic/resedit/resedit.dbp -o $@ -release
$(BINDIR)/kpunch/resedit:
mkdir -p $(BINDIR)/kpunch/resedit
basicdemo: $(BINDIR)/kpunch/basicdemo/basicdemo.app
$(BINDIR)/kpunch/basicdemo/basicdemo.app: ../sdk/samples/basic/basicdemo/basicdemo.dbp ../sdk/samples/basic/basicdemo/basicdemo.frm ../sdk/samples/basic/basicdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basicdemo dvxbasic
$(BASCOMP) ../sdk/samples/basic/basicdemo/basicdemo.dbp -o $@ -release
$(BINDIR)/kpunch/basicdemo:
mkdir -p $(BINDIR)/kpunch/basicdemo
$(BINDIR)/kpunch/iconed:
mkdir -p $(BINDIR)/kpunch/iconed
$(OBJDIR): $(OBJDIR):
mkdir -p $(OBJDIR) mkdir -p $(OBJDIR)

View file

@ -1,5 +1,5 @@
# clock.res -- Resource manifest for Clock # clock.res -- Resource manifest for Clock
icon32 icon clock/icon32.bmp icon32 icon icon32.bmp
name text "Clock" name text "Clock"
author text "DVX Project" author text "DVX Project"
description text "Digital clock with date display" description text "Digital clock with date display"

View file

@ -1,7 +1,7 @@
// ctrlpanel.c -- DVX Control Panel // ctrlpanel.c -- DVX Control Panel
// //
// System configuration app with four tabs: // System configuration app with four tabs:
// Mouse -- scroll direction, double-click speed, acceleration, cursor speed // Mouse -- scroll direction, wheel speed, double-click speed, acceleration, cursor speed
// Colors -- all 18 system colors, theme load/save // Colors -- all 18 system colors, theme load/save
// Desktop -- wallpaper image (stretch mode) // Desktop -- wallpaper image (stretch mode)
// Video -- resolution and color depth // Video -- resolution and color depth
@ -95,6 +95,7 @@ static int32_t sSavedWheelDir;
static int32_t sSavedDblClick; static int32_t sSavedDblClick;
static int32_t sSavedAccel; static int32_t sSavedAccel;
static int32_t sSavedSpeed; static int32_t sSavedSpeed;
static int32_t sSavedWheelStep;
static int32_t sSavedVideoW; static int32_t sSavedVideoW;
static int32_t sSavedVideoH; static int32_t sSavedVideoH;
static int32_t sSavedVideoBpp; static int32_t sSavedVideoBpp;
@ -107,6 +108,8 @@ static WidgetT *sDblClickLbl = NULL;
static WidgetT *sAccelDrop = NULL; static WidgetT *sAccelDrop = NULL;
static WidgetT *sSpeedSldr = NULL; static WidgetT *sSpeedSldr = NULL;
static WidgetT *sSpeedLbl = NULL; static WidgetT *sSpeedLbl = NULL;
static WidgetT *sWheelStepSldr = NULL;
static WidgetT *sWheelStepLbl = NULL;
static WidgetT *sDblTestLbl = NULL; static WidgetT *sDblTestLbl = NULL;
// Colors tab widgets // Colors tab widgets
@ -152,6 +155,7 @@ static const char *mapAccelValue(int32_t val);
static void onAccelChange(WidgetT *w); static void onAccelChange(WidgetT *w);
static void onSpeedSlider(WidgetT *w); static void onSpeedSlider(WidgetT *w);
static void updateSpeedLabel(void); static void updateSpeedLabel(void);
static void updateWheelStepLabel(void);
static void applyMouseConfig(void); static void applyMouseConfig(void);
static void onApplyTheme(WidgetT *w); static void onApplyTheme(WidgetT *w);
static void onBrowseTheme(WidgetT *w); static void onBrowseTheme(WidgetT *w);
@ -170,6 +174,7 @@ static void onOk(WidgetT *w);
static void onSaveTheme(WidgetT *w); static void onSaveTheme(WidgetT *w);
static void onVideoApply(WidgetT *w); static void onVideoApply(WidgetT *w);
static void onWheelChange(WidgetT *w); static void onWheelChange(WidgetT *w);
static void onWheelStepSlider(WidgetT *w);
static void saveSnapshot(void); static void saveSnapshot(void);
static void restoreSnapshot(void); static void restoreSnapshot(void);
static void scanWallpapers(void); static void scanWallpapers(void);
@ -334,6 +339,26 @@ static void buildMouseTab(WidgetT *page) {
wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL); wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);
// Wheel speed (lines per notch)
wgtLabel(page, "Scroll Speed:");
WidgetT *wheelStepRow = wgtHBox(page);
wheelStepRow->spacing = wgtPixels(CP_SPACING);
wgtLabel(wheelStepRow, "Slow");
sWheelStepSldr = wgtSlider(wheelStepRow, MOUSE_WHEEL_STEP_MIN, MOUSE_WHEEL_STEP_MAX);
sWheelStepSldr->weight = 100;
sWheelStepSldr->onChange = onWheelStepSlider;
int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT);
wgtSliderSetValue(sWheelStepSldr, wheelStep);
wgtLabel(wheelStepRow, "Fast ");
sWheelStepLbl = wgtLabel(wheelStepRow, "");
sWheelStepLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W);
updateWheelStepLabel();
wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);
// Double-click speed // Double-click speed
wgtLabel(page, "Double-Click Speed:"); wgtLabel(page, "Double-Click Speed:");
@ -500,6 +525,13 @@ static void onWheelChange(WidgetT *w) {
} }
static void onWheelStepSlider(WidgetT *w) {
(void)w;
updateWheelStepLabel();
applyMouseConfig();
}
static void onDblClickSlider(WidgetT *w) { static void onDblClickSlider(WidgetT *w) {
(void)w; (void)w;
updateDblClickLabel(); updateDblClickLabel();
@ -588,8 +620,8 @@ static void onBrowseTheme(WidgetT *w) {
(void)w; (void)w;
FileFilterT filters[] = { FileFilterT filters[] = {
{ "Theme Files (*.thm)", "*.thm" }, { "\1" },
{ "All Files (*.*)", "*.*" } { "\1" }
}; };
char path[DVX_MAX_PATH]; char path[DVX_MAX_PATH];
@ -604,8 +636,8 @@ static void onSaveTheme(WidgetT *w) {
(void)w; (void)w;
FileFilterT filters[] = { FileFilterT filters[] = {
{ "Theme Files (*.thm)", "*.thm" }, { "\1" },
{ "All Files (*.*)", "*.*" } { "\1" }
}; };
char path[DVX_MAX_PATH]; char path[DVX_MAX_PATH];
@ -650,8 +682,8 @@ static void onChooseWallpaper(WidgetT *w) {
(void)w; (void)w;
FileFilterT filters[] = { FileFilterT filters[] = {
{ "Images (*.bmp;*.jpg;*.png)", "*.bmp;*.jpg;*.png" }, { "\1" },
{ "All Files (*.*)", "*.*" } { "\1" }
}; };
char path[DVX_MAX_PATH]; char path[DVX_MAX_PATH];
@ -824,6 +856,7 @@ static void onOk(WidgetT *w) {
prefsSetInt(sPrefs, "mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr)); prefsSetInt(sPrefs, "mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr));
prefsSetString(sPrefs, "mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop))); prefsSetString(sPrefs, "mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop)));
prefsSetInt(sPrefs, "mouse", "speed", wgtSliderGetValue(sSpeedSldr)); prefsSetInt(sPrefs, "mouse", "speed", wgtSliderGetValue(sSpeedSldr));
prefsSetInt(sPrefs, "mouse", "wheelspeed", wgtSliderGetValue(sWheelStepSldr));
// Save colors to INI // Save colors to INI
for (int32_t i = 0; i < ColorCountE; i++) { for (int32_t i = 0; i < ColorCountE; i++) {
@ -898,6 +931,7 @@ static void saveSnapshot(void) {
sSavedDblClick = (int32_t)(sAc->dblClickTicks * 1000 / CLOCKS_PER_SEC); sSavedDblClick = (int32_t)(sAc->dblClickTicks * 1000 / CLOCKS_PER_SEC);
sSavedAccel = wgtDropdownGetSelected(sAccelDrop); sSavedAccel = wgtDropdownGetSelected(sAccelDrop);
sSavedSpeed = wgtSliderGetValue(sSpeedSldr); sSavedSpeed = wgtSliderGetValue(sSpeedSldr);
sSavedWheelStep = sAc->wheelStep;
sSavedVideoW = sAc->display.width; sSavedVideoW = sAc->display.width;
sSavedVideoH = sAc->display.height; sSavedVideoH = sAc->display.height;
sSavedVideoBpp = sAc->display.format.bitsPerPixel; sSavedVideoBpp = sAc->display.format.bitsPerPixel;
@ -918,7 +952,7 @@ static void restoreSnapshot(void) {
const char *accelName = mapAccelValue(sSavedAccel); const char *accelName = mapAccelValue(sSavedAccel);
int32_t accelVal = mapAccelName(accelName); int32_t accelVal = mapAccelName(accelName);
int32_t speedVal = 34 - sSavedSpeed; int32_t speedVal = 34 - sSavedSpeed;
dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, accelVal, speedVal); dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, accelVal, speedVal, sSavedWheelStep);
// Restore video mode if changed // Restore video mode if changed
if (sAc->display.width != sSavedVideoW || if (sAc->display.width != sSavedVideoW ||
@ -1097,6 +1131,18 @@ static void updateSpeedLabel(void) {
} }
// ============================================================
// updateWheelStepLabel
// ============================================================
static void updateWheelStepLabel(void) {
static char buf[32];
int32_t val = wgtSliderGetValue(sWheelStepSldr);
snprintf(buf, sizeof(buf), "%ld", (long)val);
wgtSetText(sWheelStepLbl, buf);
}
// ============================================================ // ============================================================
// applyMouseConfig -- gather current widget values and apply // applyMouseConfig -- gather current widget values and apply
// ============================================================ // ============================================================
@ -1114,8 +1160,9 @@ static void applyMouseConfig(void) {
// slider 8 -> 26 (near default) // slider 8 -> 26 (near default)
// slider 32 -> 2 mickeys/8px (fastest) // slider 32 -> 2 mickeys/8px (fastest)
int32_t speedVal = 34 - wgtSliderGetValue(sSpeedSldr); int32_t speedVal = 34 - wgtSliderGetValue(sSpeedSldr);
int32_t wheelStep = wgtSliderGetValue(sWheelStepSldr);
dvxSetMouseConfig(sAc, dir, dbl, accelVal, speedVal); dvxSetMouseConfig(sAc, dir, dbl, accelVal, speedVal, wheelStep);
} }

View file

@ -1,5 +1,5 @@
# cpanel.res -- Resource manifest for Control Panel # cpanel.res -- Resource manifest for Control Panel
icon32 icon cpanel/icon32.bmp icon32 icon icon32.bmp
name text "Control Panel" name text "Control Panel"
author text "DVX Project" author text "DVX Project"
description text "System settings and preferences" description text "System settings and preferences"

View file

@ -25,7 +25,7 @@ RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt
RT_TARGET = $(RT_TARGETDIR)/basrt.lib RT_TARGET = $(RT_TARGETDIR)/basrt.lib
# Compiler objects (only needed by the IDE) # Compiler objects (only needed by the IDE)
COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/symtab.o $(OBJDIR)/strip.o COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/symtab.o $(OBJDIR)/strip.o $(OBJDIR)/obfuscate.o $(OBJDIR)/compact.o
# IDE app objects # IDE app objects
IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideMenuEditor.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideMenuEditor.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o
@ -38,25 +38,39 @@ STUB_TARGET = $(OBJDIR)/basstub.app
# Native test programs (host gcc, not cross-compiled) # Native test programs (host gcc, not cross-compiled)
HOSTCC = gcc HOSTCC = gcc
HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -I. -I../../core HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -D_GNU_SOURCE -I. -I../../core -I../../core/platform -I../../core/thirdparty
BINDIR = ../../bin BINDIR = ../../bin
TEST_COMPILER = $(BINDIR)/test_compiler TEST_COMPILER = $(BINDIR)/test_compiler
TEST_VM = $(BINDIR)/test_vm TEST_VM = $(BINDIR)/test_vm
TEST_LEX = $(BINDIR)/test_lex TEST_LEX = $(BINDIR)/test_lex
TEST_QUICK = $(BINDIR)/test_quick TEST_QUICK = $(BINDIR)/test_quick
TEST_COMPACT = $(BINDIR)/test_compact
STB_DS_IMPL = ../../core/thirdparty/stb_ds_impl.c STB_DS_IMPL = ../../core/thirdparty/stb_ds_impl.c
TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c $(STB_DS_IMPL) TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(STB_DS_IMPL)
TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c $(STB_DS_IMPL) TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c runtime/serialize.c $(STB_DS_IMPL)
TEST_LEX_SRCS = test_lex.c compiler/lexer.c TEST_LEX_SRCS = test_lex.c compiler/lexer.c
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c $(STB_DS_IMPL) TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(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 $(STB_DS_IMPL)
# Command-line compiler (host tool)
HOSTDIR = ../../bin/host
BASCOMP_SRCS = stub/bascomp.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 ../../core/dvxPrefs.c ../../core/dvxResource.c $(STB_DS_IMPL)
BASCOMP_TARGET = $(HOSTDIR)/bascomp
# DOS command-line compiler
DOSCC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
DOSCFLAGS = -O2 -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -march=i486 -mtune=i586 -I../../core -I../../core/platform -I../../core/thirdparty -I.
EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff
CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE
SYSTEMDIR = ../../bin/system
.PHONY: all clean tests .PHONY: all clean tests
all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE $(SYSTEMDIR)/BASSTUB.APP
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT)
$(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(BINDIR) $(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(BINDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm $(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm
@ -70,6 +84,31 @@ $(TEST_LEX): $(TEST_LEX_SRCS) | $(BINDIR)
$(TEST_QUICK): $(TEST_QUICK_SRCS) | $(BINDIR) $(TEST_QUICK): $(TEST_QUICK_SRCS) | $(BINDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_QUICK_SRCS) -lm $(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_QUICK_SRCS) -lm
$(TEST_COMPACT): $(TEST_COMPACT_SRCS) | $(BINDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPACT_SRCS) -lm
# Host command-line compiler (stub deployed alongside)
$(BASCOMP_TARGET): $(BASCOMP_SRCS) $(STUB_TARGET) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -DBASCOMP_STANDALONE -I../../tools -o $@ $(BASCOMP_SRCS) -lm
cp $(STUB_TARGET) $(HOSTDIR)/BASSTUB.APP
# DOS command-line compiler
$(SYSTEMDIR)/BASCOMP.EXE: $(BASCOMP_SRCS) | $(SYSTEMDIR)
$(DOSCC) $(DOSCFLAGS) -DBASCOMP_STANDALONE -I../../tools -o $(SYSTEMDIR)/bascomp.exe $(BASCOMP_SRCS) -lm
$(EXE2COFF) $(SYSTEMDIR)/bascomp.exe
cat $(CWSDSTUB) $(SYSTEMDIR)/bascomp > $@
rm -f $(SYSTEMDIR)/bascomp $(SYSTEMDIR)/bascomp.exe
# Deploy stub alongside DOS compiler
$(SYSTEMDIR)/BASSTUB.APP: $(STUB_TARGET) | $(SYSTEMDIR)
cp $(STUB_TARGET) $@
$(HOSTDIR):
mkdir -p $(HOSTDIR)
$(SYSTEMDIR):
mkdir -p $(SYSTEMDIR)
# Runtime library DXE (exports symbols via dlregsym constructor) # Runtime library DXE (exports symbols via dlregsym constructor)
$(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR) $(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR)
$(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe -U $(RT_OBJS) $(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe -U $(RT_OBJS)
@ -105,6 +144,12 @@ $(OBJDIR)/serialize.o: runtime/serialize.c runtime/serialize.h runtime/vm.h | $(
$(OBJDIR)/strip.o: compiler/strip.c compiler/strip.h compiler/opcodes.h runtime/vm.h | $(OBJDIR) $(OBJDIR)/strip.o: compiler/strip.c compiler/strip.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/obfuscate.o: compiler/obfuscate.c compiler/obfuscate.h runtime/vm.h runtime/values.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/compact.o: compiler/compact.c compiler/compact.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/basstub.o: stub/basstub.c runtime/vm.h runtime/serialize.h formrt/formrt.h formrt/formcfm.h | $(OBJDIR) $(OBJDIR)/basstub.o: stub/basstub.c runtime/vm.h runtime/serialize.h formrt/formrt.h formrt/formcfm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<
@ -158,5 +203,5 @@ $(BINDIR):
mkdir -p $(BINDIR) mkdir -p $(BINDIR)
clean: clean:
rm -rf $(RT_OBJS) $(COMP_OBJS) $(IDE_OBJS) $(STUB_OBJS) $(RT_TARGET) $(APP_TARGET) $(STUB_TARGET) $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR) $(OBJDIR)/basrt_init.o rm -rf $(RT_OBJS) $(COMP_OBJS) $(IDE_OBJS) $(STUB_OBJS) $(RT_TARGET) $(APP_TARGET) $(STUB_TARGET) $(BASCOMP_TARGET) $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR) $(OBJDIR)/basrt_init.o
rm -f $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) rm -f $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK)

View file

@ -372,47 +372,6 @@ const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name)
} }
// ============================================================
// basModuleFree
// ============================================================
void basModuleFree(BasModuleT *mod) {
if (!mod) {
return;
}
free(mod->code);
if (mod->constants) {
for (int32_t i = 0; i < mod->constCount; i++) {
basStringUnref(mod->constants[i]);
}
free(mod->constants);
}
if (mod->dataPool) {
for (int32_t i = 0; i < mod->dataCount; i++) {
basValRelease(&mod->dataPool[i]);
}
free(mod->dataPool);
}
free(mod->procs);
free(mod->formVarInfo);
free(mod->debugVars);
if (mod->debugUdtDefs) {
for (int32_t i = 0; i < mod->debugUdtDefCount; i++) {
free(mod->debugUdtDefs[i].fields);
}
free(mod->debugUdtDefs);
}
free(mod);
}
// ============================================================ // ============================================================

View file

@ -0,0 +1,564 @@
// compact.c -- Release build bytecode compaction
//
// Walks the module's bytecode, removes OP_LINE instructions (3 bytes
// each), and rewrites all code-address references so control flow
// still lands on the correct instructions.
//
// Address references:
// - BasProcEntryT::codeAddr (absolute)
// - BasFormVarInfoT::initCodeAddr (absolute, 0 = no init)
// - OP_CALL operand (absolute uint16)
// - OP_JMP / OP_JMP_TRUE / OP_JMP_FALSE operand (relative int16)
// - OP_FOR_NEXT loopTop operand (relative int16)
// - OP_ON_ERROR handler operand (relative int16; 0 = disable, not remapped)
// - GOSUB return address (emitted as OP_PUSH_INT32 followed by OP_JMP,
// where the pushed value equals the PC immediately after the JMP)
//
// Safety: if any opcode cannot be sized, any jump overflows int16, or
// the walk doesn't reach codeLen exactly, the module is left untouched.
#include "compact.h"
#include "opcodes.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// Opcode operand size table
// ============================================================
// Returns operand byte count (excluding the 1-byte opcode), or -1 if unknown.
static int32_t opOperandSize(uint8_t op) {
switch (op) {
// No operand bytes
case OP_NOP:
case OP_PUSH_TRUE: case OP_PUSH_FALSE:
case OP_POP: case OP_DUP:
case OP_LOAD_REF: case OP_STORE_REF:
case OP_ADD_INT: case OP_SUB_INT: case OP_MUL_INT:
case OP_IDIV_INT: case OP_MOD_INT: case OP_NEG_INT:
case OP_ADD_FLT: case OP_SUB_FLT: case OP_MUL_FLT:
case OP_DIV_FLT: case OP_NEG_FLT: case OP_POW:
case OP_STR_CONCAT: case OP_STR_LEFT: case OP_STR_RIGHT:
case OP_STR_MID: case OP_STR_MID2: case OP_STR_LEN:
case OP_STR_INSTR: case OP_STR_INSTR3:
case OP_STR_UCASE: case OP_STR_LCASE:
case OP_STR_TRIM: case OP_STR_LTRIM: case OP_STR_RTRIM:
case OP_STR_CHR: case OP_STR_ASC: case OP_STR_SPACE:
case OP_CMP_EQ: case OP_CMP_NE: case OP_CMP_LT:
case OP_CMP_GT: case OP_CMP_LE: case OP_CMP_GE:
case OP_AND: case OP_OR: case OP_NOT:
case OP_XOR: case OP_EQV: case OP_IMP:
case OP_GOSUB_RET: case OP_RET: case OP_RET_VAL:
case OP_FOR_POP:
case OP_CONV_INT_FLT: case OP_CONV_FLT_INT:
case OP_CONV_INT_STR: case OP_CONV_STR_INT:
case OP_CONV_FLT_STR: case OP_CONV_STR_FLT:
case OP_CONV_INT_LONG: case OP_CONV_LONG_INT:
case OP_PRINT: case OP_PRINT_NL: case OP_PRINT_TAB:
case OP_INPUT:
case OP_FILE_CLOSE: case OP_FILE_PRINT: case OP_FILE_INPUT:
case OP_FILE_EOF: case OP_FILE_LINE_INPUT:
case OP_LOAD_PROP: case OP_STORE_PROP:
case OP_LOAD_FORM: case OP_UNLOAD_FORM:
case OP_HIDE_FORM: case OP_DO_EVENTS:
case OP_MSGBOX: case OP_INPUTBOX: case OP_ME_REF:
case OP_CREATE_CTRL: case OP_FIND_CTRL: case OP_FIND_CTRL_IDX:
case OP_CREATE_CTRL_EX:
case OP_ERASE:
case OP_RESUME: case OP_RESUME_NEXT:
case OP_RAISE_ERR: case OP_ERR_NUM: case OP_ERR_CLEAR:
case OP_MATH_ABS: case OP_MATH_INT: case OP_MATH_FIX:
case OP_MATH_SGN: case OP_MATH_SQR: case OP_MATH_SIN:
case OP_MATH_COS: case OP_MATH_TAN: case OP_MATH_ATN:
case OP_MATH_LOG: case OP_MATH_EXP: case OP_MATH_RND:
case OP_MATH_RANDOMIZE:
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_MATH_TIMER: case OP_DATE_STR: case OP_TIME_STR:
case OP_SLEEP: case OP_ENVIRON:
case OP_READ_DATA: case OP_RESTORE:
case OP_FILE_WRITE: case OP_FILE_WRITE_SEP: case OP_FILE_WRITE_NL:
case OP_FILE_GET: case OP_FILE_PUT: case OP_FILE_SEEK:
case OP_FILE_LOF: case OP_FILE_LOC: case OP_FILE_FREEFILE:
case OP_FILE_INPUT_N:
case OP_STR_MID_ASGN: case OP_PRINT_USING:
case OP_PRINT_TAB_N: case OP_PRINT_SPC_N:
case OP_FORMAT: case OP_SHELL:
case OP_APP_PATH: case OP_APP_CONFIG: case OP_APP_DATA:
case OP_INI_READ: case OP_INI_WRITE:
case OP_FS_KILL: case OP_FS_NAME: case OP_FS_FILECOPY:
case OP_FS_MKDIR: case OP_FS_RMDIR: case OP_FS_CHDIR:
case OP_FS_CHDRIVE: case OP_FS_CURDIR: case OP_FS_DIR:
case OP_FS_DIR_NEXT: case OP_FS_FILELEN:
case OP_FS_GETATTR: case OP_FS_SETATTR:
case OP_CREATE_FORM: case OP_SET_EVENT: case OP_REMOVE_CTRL:
case OP_END: case OP_HALT:
return 0;
case OP_LOAD_ARRAY: case OP_STORE_ARRAY:
case OP_PRINT_SPC: case OP_FILE_OPEN:
case OP_CALL_METHOD: case OP_SHOW_FORM:
case OP_LBOUND: case OP_UBOUND:
case OP_COMPARE_MODE:
return 1;
case OP_PUSH_INT16: case OP_PUSH_STR:
case OP_LOAD_LOCAL: case OP_STORE_LOCAL:
case OP_LOAD_GLOBAL: case OP_STORE_GLOBAL:
case OP_LOAD_FIELD: case OP_STORE_FIELD:
case OP_PUSH_LOCAL_ADDR: case OP_PUSH_GLOBAL_ADDR:
case OP_JMP: case OP_JMP_TRUE: case OP_JMP_FALSE:
case OP_CTRL_REF:
case OP_LOAD_FORM_VAR: case OP_STORE_FORM_VAR:
case OP_PUSH_FORM_ADDR:
case OP_DIM_ARRAY: case OP_REDIM:
case OP_ON_ERROR:
case OP_STR_FIXLEN:
case OP_LINE:
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_NEXT:
return 5;
case OP_CALL_EXTERN:
return 6;
case OP_PUSH_FLT64:
return 8;
default:
return -1;
}
}
// ============================================================
// Little-endian helpers (bytecode is always LE regardless of host)
// ============================================================
static int16_t readI16LE(const uint8_t *p) {
return (int16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static uint16_t readU16LE(const uint8_t *p) {
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
}
static int32_t readI32LE(const uint8_t *p) {
return (int32_t)((uint32_t)p[0] |
((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 24));
}
static void writeI16LE(uint8_t *p, int16_t v) {
uint16_t u = (uint16_t)v;
p[0] = (uint8_t)(u & 0xFF);
p[1] = (uint8_t)((u >> 8) & 0xFF);
}
static void writeU16LE(uint8_t *p, uint16_t v) {
p[0] = (uint8_t)(v & 0xFF);
p[1] = (uint8_t)((v >> 8) & 0xFF);
}
static void writeI32LE(uint8_t *p, int32_t v) {
uint32_t u = (uint32_t)v;
p[0] = (uint8_t)(u & 0xFF);
p[1] = (uint8_t)((u >> 8) & 0xFF);
p[2] = (uint8_t)((u >> 16) & 0xFF);
p[3] = (uint8_t)((u >> 24) & 0xFF);
}
// ============================================================
// Walk bytecode, build remap
// ============================================================
//
// remap[oldPos] = newPos for every byte position in [0, oldCodeLen].
// For OP_LINE bytes (removed): remap points at where the NEXT instruction
// starts in the new code.
// Final entry remap[oldCodeLen] = newCodeLen.
//
// Returns malloc'd array of size (oldCodeLen + 1), or NULL on failure.
static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, int32_t *outNewLen) {
int32_t *remap = (int32_t *)malloc((codeLen + 1) * sizeof(int32_t));
if (!remap) {
return NULL;
}
int32_t oldPc = 0;
int32_t newPc = 0;
while (oldPc < codeLen) {
uint8_t op = code[oldPc];
int32_t operand = opOperandSize(op);
if (operand < 0) {
free(remap);
return NULL;
}
int32_t instSize = 1 + operand;
if (oldPc + instSize > codeLen) {
free(remap);
return NULL;
}
if (op == OP_LINE) {
// These bytes are removed; they map to where the next instruction starts.
for (int32_t i = 0; i < instSize; i++) {
remap[oldPc + i] = newPc;
}
} else {
for (int32_t i = 0; i < instSize; i++) {
remap[oldPc + i] = newPc + i;
}
newPc += instSize;
}
oldPc += instSize;
}
if (oldPc != codeLen) {
free(remap);
return NULL;
}
remap[codeLen] = newPc;
*outNewLen = newPc;
return remap;
}
// ============================================================
// GOSUB pattern detection
// ============================================================
//
// GOSUB emits:
// oldPc: OP_PUSH_INT32 (1 byte)
// oldPc+1: int32 value V (4 bytes)
// oldPc+5: OP_JMP (1 byte)
// oldPc+6: int16 offset (2 bytes)
// oldPc+8: <next instruction>
// The invariant is V == oldPc + 8 (the pushed return address).
//
// Returns true if the given position is the start of such a pattern.
static bool isGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos) {
if (pos + 8 > codeLen) {
return false;
}
if (code[pos] != OP_PUSH_INT32) {
return false;
}
if (code[pos + 5] != OP_JMP) {
return false;
}
int32_t value = readI32LE(code + pos + 1);
return value == pos + 8;
}
// ============================================================
// Apply remap to a single instruction's operand
// ============================================================
//
// Returns true on success, false if an offset overflows int16 or a
// target doesn't land on a valid instruction in the old code.
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t newCodeLen) {
int32_t newAddr = remap[oldAddr];
if (newAddr < 0 || newAddr > newCodeLen) {
return false;
}
if (newAddr > 0xFFFF) {
return false;
}
writeU16LE(newCode + newOpPos + operandOffset, (uint16_t)newAddr);
return true;
}
// Rewrite a relative int16 offset.
// oldOpPos, oldPcAfter: position of opcode and PC after reading offset
// newOpPos, newPcAfter: same in the new code
// operandOffset: byte offset from opcode to the int16 operand
// oldOffset: the offset as stored in the old code
//
// Handles ON_ERROR's special case (offset == 0 means "disable").
// For ON_ERROR the caller passes allowZero=true; the zero is preserved as-is.
static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, int16_t oldOffset, int32_t oldPcAfter, int32_t newPcAfter, const int32_t *remap, int32_t codeLen, int32_t newCodeLen, bool allowZero) {
if (allowZero && oldOffset == 0) {
writeI16LE(newCode + newOpPos + operandOffset, 0);
return true;
}
int32_t oldTarget = oldPcAfter + oldOffset;
if (oldTarget < 0 || oldTarget > codeLen) {
return false;
}
int32_t newTarget = remap[oldTarget];
if (newTarget < 0 || newTarget > newCodeLen) {
return false;
}
int32_t newOffset = newTarget - newPcAfter;
if (newOffset < -32768 || newOffset > 32767) {
return false;
}
writeI16LE(newCode + newOpPos + operandOffset, (int16_t)newOffset);
return true;
}
// ============================================================
// basCompactBytecode
// ============================================================
int32_t basCompactBytecode(BasModuleT *mod) {
if (!mod || !mod->code || mod->codeLen <= 0) {
return 0;
}
const uint8_t *oldCode = mod->code;
int32_t oldCodeLen = mod->codeLen;
// Count OP_LINE occurrences. If none, nothing to do.
int32_t lineCount = 0;
{
int32_t pc = 0;
while (pc < oldCodeLen) {
uint8_t op = oldCode[pc];
int32_t operand = opOperandSize(op);
if (operand < 0 || pc + 1 + operand > oldCodeLen) {
return 0; // unknown opcode -- skip compaction
}
if (op == OP_LINE) {
lineCount++;
}
pc += 1 + operand;
}
if (pc != oldCodeLen) {
return 0;
}
if (lineCount == 0) {
return 0;
}
}
int32_t newCodeLen = 0;
int32_t *remap = buildRemap(oldCode, oldCodeLen, &newCodeLen);
if (!remap) {
return 0;
}
uint8_t *newCode = (uint8_t *)malloc(newCodeLen > 0 ? newCodeLen : 1);
if (!newCode) {
free(remap);
return 0;
}
// Copy bytes (skipping OP_LINE) and rewrite address operands.
bool ok = true;
int32_t oldPc = 0;
while (oldPc < oldCodeLen && ok) {
uint8_t op = oldCode[oldPc];
int32_t operand = opOperandSize(op);
int32_t instSize = 1 + operand;
if (op == OP_LINE) {
oldPc += instSize;
continue;
}
int32_t newPc = remap[oldPc];
// Copy the instruction verbatim first; we'll overwrite operands that
// need remapping below.
memcpy(newCode + newPc, oldCode + oldPc, instSize);
switch (op) {
case OP_CALL: {
uint16_t oldAddr = readU16LE(oldCode + oldPc + 1);
if (!remapAbsU16(newCode, newPc, 1, oldAddr, remap, newCodeLen)) {
ok = false;
}
break;
}
case OP_JMP:
case OP_JMP_TRUE:
case OP_JMP_FALSE: {
int16_t oldOff = readI16LE(oldCode + oldPc + 1);
if (!remapRelI16(newCode, newPc, 1, oldOff,
oldPc + 3, newPc + 3,
remap, oldCodeLen, newCodeLen, false)) {
ok = false;
}
break;
}
case OP_FOR_NEXT: {
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);
if (!remapRelI16(newCode, newPc, 1, oldOff,
oldPc + 3, newPc + 3,
remap, oldCodeLen, newCodeLen, true)) {
ok = false;
}
break;
}
case OP_PUSH_INT32: {
// Detect GOSUB return-address push and remap the absolute address.
if (isGosubPush(oldCode, oldCodeLen, oldPc)) {
int32_t oldAddr = readI32LE(oldCode + oldPc + 1);
if (oldAddr < 0 || oldAddr > oldCodeLen) {
ok = false;
break;
}
int32_t newAddr = remap[oldAddr];
if (newAddr < 0 || newAddr > newCodeLen) {
ok = false;
break;
}
writeI32LE(newCode + newPc + 1, newAddr);
}
break;
}
default:
break;
}
oldPc += instSize;
}
if (!ok) {
free(newCode);
free(remap);
return 0;
}
// Rewrite proc entry points
for (int32_t i = 0; i < mod->procCount; i++) {
int32_t oldAddr = mod->procs[i].codeAddr;
if (oldAddr < 0 || oldAddr > oldCodeLen) {
free(newCode);
free(remap);
return 0;
}
mod->procs[i].codeAddr = remap[oldAddr];
}
// Rewrite form-var init code addresses. Negative means "no init code".
for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
int32_t oldAddr = mod->formVarInfo[i].initCodeAddr;
if (oldAddr < 0) {
continue;
}
if (oldAddr > oldCodeLen) {
free(newCode);
free(remap);
return 0;
}
int32_t oldLen = mod->formVarInfo[i].initCodeLen;
int32_t oldEnd = oldAddr + oldLen;
if (oldEnd > oldCodeLen) {
oldEnd = oldCodeLen;
}
int32_t newAddr = remap[oldAddr];
int32_t newEnd = remap[oldEnd];
mod->formVarInfo[i].initCodeAddr = newAddr;
mod->formVarInfo[i].initCodeLen = newEnd - newAddr;
}
// Rewrite entry point
if (mod->entryPoint >= 0 && mod->entryPoint <= oldCodeLen) {
mod->entryPoint = remap[mod->entryPoint];
}
// Swap in the new code
free(mod->code);
mod->code = newCode;
mod->codeLen = newCodeLen;
int32_t removed = oldCodeLen - newCodeLen;
free(remap);
return removed;
}

View file

@ -0,0 +1,21 @@
// compact.h -- Release build bytecode compaction
//
// Removes OP_LINE instructions from the module's bytecode and
// rewrites all code-address references (proc entries, CALL operands,
// JMP/FOR_NEXT/ON_ERROR relative offsets, GOSUB return addresses,
// and formVarInfo init code addresses).
//
// Safe by construction: if any sanity check fails (unknown opcode,
// offset overflow, walk not landing on codeLen, etc.), the module
// is left unchanged and the function returns 0.
#ifndef DVXBASIC_COMPACT_H
#define DVXBASIC_COMPACT_H
#include "../runtime/vm.h"
#include <stdint.h>
// Returns the number of bytes removed, or 0 if compaction was skipped.
int32_t basCompactBytecode(BasModuleT *mod);
#endif // DVXBASIC_COMPACT_H

View file

@ -77,6 +77,7 @@ static const KeywordEntryT sKeywords[] = {
{ "IF", TOK_IF }, { "IF", TOK_IF },
{ "IMP", TOK_IMP }, { "IMP", TOK_IMP },
{ "INIREAD", TOK_INIREAD }, { "INIREAD", TOK_INIREAD },
{ "INIREAD$", TOK_INIREAD },
{ "INIWRITE", TOK_INIWRITE }, { "INIWRITE", TOK_INIWRITE },
{ "INPUT", TOK_INPUT }, { "INPUT", TOK_INPUT },
{ "INTEGER", TOK_INTEGER }, { "INTEGER", TOK_INTEGER },
@ -682,7 +683,16 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
} }
} }
BasTokenTypeE kwType = lookupKeyword(lex->token.text, baseLen); // Try the full text first (including any type suffix). Suffix-bearing
// keywords like CURDIR$, DIR$, INIREAD$, INPUTBOX$ are listed in the
// keyword table with their $ and will match here. If the full text
// isn't a keyword, fall back to the base name (without suffix).
BasTokenTypeE kwType = lookupKeyword(lex->token.text, idx);
bool matchedWithSuffix = (kwType != TOK_IDENT && baseLen != idx);
if (kwType == TOK_IDENT && baseLen != idx) {
kwType = lookupKeyword(lex->token.text, baseLen);
}
// REM is a comment -- skip to end of line // REM is a comment -- skip to end of line
if (kwType == TOK_REM) { if (kwType == TOK_REM) {
@ -694,9 +704,9 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
return TOK_NEWLINE; return TOK_NEWLINE;
} }
// If it's a keyword and has no suffix, return the keyword token. // Accept the keyword if it's a plain keyword (no suffix on source) or
// String-returning builtins (SQLError$, SQLField$) also match with $. // if it explicitly matched a $-suffixed entry in the keyword table.
if (kwType != TOK_IDENT && (baseLen == idx || kwType == TOK_INPUTBOX)) { if (kwType != TOK_IDENT && (baseLen == idx || matchedWithSuffix)) {
return kwType; return kwType;
} }

View file

@ -0,0 +1,590 @@
// obfuscate.c -- Release build name obfuscation
//
// See obfuscate.h for the high-level description.
#include "obfuscate.h"
#include "../runtime/values.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// Event suffix list (must match strip.c)
// ============================================================
static const char *sEventSuffixes[] = {
"Load", "Unload", "QueryUnload", "Resize", "Activate", "Deactivate",
"Click", "DblClick", "Change", "Timer",
"GotFocus", "LostFocus",
"KeyPress", "KeyDown", "KeyUp",
"MouseDown", "MouseUp", "MouseMove",
"Scroll", "Reposition", "Validate",
NULL
};
// ============================================================
// Name map
// ============================================================
typedef struct {
char *orig; // original name (strdup'd, case-preserved)
char *mapped; // new name (strdup'd, "C1" .. "Cn")
} NameEntryT;
typedef struct {
NameEntryT *entries;
int32_t count;
int32_t cap;
} NameMapT;
static void nameMapInit(NameMapT *m) {
m->entries = NULL;
m->count = 0;
m->cap = 0;
}
static void nameMapFree(NameMapT *m) {
for (int32_t i = 0; i < m->count; i++) {
free(m->entries[i].orig);
free(m->entries[i].mapped);
}
free(m->entries);
m->entries = NULL;
m->count = 0;
m->cap = 0;
}
// Look up an original name (case-insensitive). Returns mapped name or NULL.
static const char *nameMapLookup(const NameMapT *m, const char *name) {
for (int32_t i = 0; i < m->count; i++) {
if (strcasecmp(m->entries[i].orig, name) == 0) {
return m->entries[i].mapped;
}
}
return NULL;
}
// Add a name if not already present. Returns mapped name.
static const char *nameMapAdd(NameMapT *m, const char *name) {
const char *existing = nameMapLookup(m, name);
if (existing) {
return existing;
}
if (m->count >= m->cap) {
int32_t newCap = m->cap == 0 ? 16 : m->cap * 2;
NameEntryT *newEntries = realloc(m->entries, newCap * sizeof(NameEntryT));
if (!newEntries) {
return NULL;
}
m->entries = newEntries;
m->cap = newCap;
}
char mapped[16];
snprintf(mapped, sizeof(mapped), "C%ld", (long)(m->count + 1));
m->entries[m->count].orig = strdup(name);
m->entries[m->count].mapped = strdup(mapped);
m->count++;
return m->entries[m->count - 1].mapped;
}
// ============================================================
// .frm parsing helpers
// ============================================================
// Skip ASCII whitespace. Returns pointer past whitespace.
static const char *skipWhitespace(const char *p, const char *end) {
while (p < end && (*p == ' ' || *p == '\t')) {
p++;
}
return p;
}
// Copy next whitespace-delimited token into buf. Returns pointer after token.
static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize) {
int32_t len = 0;
while (p < end && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && len < bufSize - 1) {
buf[len++] = *p++;
}
buf[len] = '\0';
return p;
}
// Check if name is a valid identifier (letters, digits, underscore, starts non-digit)
static bool isValidIdent(const char *name) {
if (!name || !*name) {
return false;
}
if (!isalpha((unsigned char)name[0]) && name[0] != '_') {
return false;
}
for (const char *p = name; *p; p++) {
if (!isalnum((unsigned char)*p) && *p != '_') {
return false;
}
}
return true;
}
// ============================================================
// Pass 1: collect all form/control names from .frm texts
// ============================================================
// Scan a .frm text and add all "Begin <Type> <Name>" names to the map.
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *map) {
const char *p = text;
const char *end = text + len;
while (p < end) {
// Read one line
const char *lineStart = p;
while (p < end && *p != '\n' && *p != '\r') {
p++;
}
const char *lineEnd = p;
if (p < end && *p == '\r') {
p++;
}
if (p < end && *p == '\n') {
p++;
}
// Trim leading whitespace
const char *l = skipWhitespace(lineStart, lineEnd);
// Check "Begin "
if ((lineEnd - l) < 6 || strncasecmp(l, "Begin ", 6) != 0) {
continue;
}
l += 6;
l = skipWhitespace(l, lineEnd);
// Read type name
char typeName[64];
l = readToken(l, lineEnd, typeName, sizeof(typeName));
if (typeName[0] == '\0') {
continue;
}
// Read control name
l = skipWhitespace(l, lineEnd);
char ctrlName[64];
l = readToken(l, lineEnd, ctrlName, sizeof(ctrlName));
if (ctrlName[0] && isValidIdent(ctrlName)) {
nameMapAdd(map, ctrlName);
}
}
}
// ============================================================
// Pass 2: strip BASIC code from .frm text (everything after outer End)
// ============================================================
// Find the position just after the matching End of the outermost Begin Form.
// Returns len of the stripped .frm. If no Begin Form found, returns original len.
static int32_t findFormEndPos(const char *text, int32_t len) {
int32_t nesting = 0;
bool inForm = false;
const char *p = text;
const char *end = text + len;
while (p < end) {
const char *lineStart = p;
while (p < end && *p != '\n' && *p != '\r') {
p++;
}
const char *lineEnd = p;
if (p < end && *p == '\r') {
p++;
}
if (p < end && *p == '\n') {
p++;
}
const char *l = skipWhitespace(lineStart, lineEnd);
if ((lineEnd - l) >= 6 && strncasecmp(l, "Begin ", 6) == 0) {
// Check for "Begin Form ..." to set inForm on outer open
if (!inForm) {
const char *r = l + 6;
r = skipWhitespace(r, lineEnd);
if ((lineEnd - r) >= 5 && strncasecmp(r, "Form ", 5) == 0) {
inForm = true;
}
}
nesting++;
} else if ((lineEnd - l) >= 3 && strncasecmp(l, "End", 3) == 0 &&
(lineEnd - l == 3 || l[3] == ' ' || l[3] == '\t' || l[3] == '\r')) {
nesting--;
if (inForm && nesting == 0) {
return (int32_t)(p - text);
}
}
}
return len;
}
// ============================================================
// Pass 3: rewrite .frm text with mapped names
// ============================================================
// Returns true if c is a valid identifier character.
static bool isIdentChar(int c) {
return isalnum(c) || c == '_';
}
// Scan text; for each identifier found outside of strings, if it's in
// the map, emit the mapped name instead. Output to out (returns bytes written).
static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap) {
int32_t outLen = 0;
int32_t i = 0;
bool inStr = false;
while (i < srcLen) {
char c = src[i];
if (c == '"') {
inStr = !inStr;
if (outLen < outCap) {
out[outLen++] = (uint8_t)c;
}
i++;
continue;
}
// Read identifier
if (!inStr && (isalpha((unsigned char)c) || c == '_')) {
int32_t identStart = i;
while (i < srcLen && isIdentChar((unsigned char)src[i])) {
i++;
}
int32_t identLen = i - identStart;
char ident[128];
if (identLen >= (int32_t)sizeof(ident)) {
identLen = (int32_t)sizeof(ident) - 1;
}
memcpy(ident, src + identStart, identLen);
ident[identLen] = '\0';
const char *mapped = nameMapLookup(map, ident);
if (mapped) {
int32_t mLen = (int32_t)strlen(mapped);
for (int32_t k = 0; k < mLen && outLen < outCap; k++) {
out[outLen++] = (uint8_t)mapped[k];
}
} else {
for (int32_t k = 0; k < identLen && outLen < outCap; k++) {
out[outLen++] = (uint8_t)ident[k];
}
}
continue;
}
if (outLen < outCap) {
out[outLen++] = (uint8_t)c;
}
i++;
}
return outLen;
}
// ============================================================
// Module rewriting
// ============================================================
// Replace the contents of a constant pool entry with a new string.
static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText) {
BasStringT *newStr = basStringNew(newText, (int32_t)strlen(newText));
if (!newStr) {
return;
}
basStringUnref(mod->constants[idx]);
mod->constants[idx] = newStr;
}
static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) {
for (int32_t i = 0; i < mod->constCount; i++) {
const BasStringT *s = mod->constants[i];
if (!s) {
continue;
}
const char *mapped = nameMapLookup(map, s->data);
if (mapped) {
replaceConstant(mod, i, mapped);
}
}
}
// Check if suffix is a known event name.
static bool isEventSuffix(const char *suffix) {
for (int32_t i = 0; sEventSuffixes[i]; i++) {
if (strcasecmp(suffix, sEventSuffixes[i]) == 0) {
return true;
}
}
return false;
}
static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
for (int32_t i = 0; i < mod->procCount; i++) {
BasProcEntryT *proc = &mod->procs[i];
if (proc->name[0] == '\0') {
continue;
}
// Find last underscore
char *underscore = strrchr(proc->name, '_');
if (!underscore) {
continue;
}
const char *suffix = underscore + 1;
if (!isEventSuffix(suffix)) {
continue;
}
// Split on underscore
int32_t prefixLen = (int32_t)(underscore - proc->name);
char prefix[BAS_MAX_PROC_NAME];
if (prefixLen >= (int32_t)sizeof(prefix)) {
prefixLen = (int32_t)sizeof(prefix) - 1;
}
memcpy(prefix, proc->name, prefixLen);
prefix[prefixLen] = '\0';
const char *mapped = nameMapLookup(map, prefix);
if (mapped) {
char newName[BAS_MAX_PROC_NAME];
snprintf(newName, sizeof(newName), "%s_%s", mapped, suffix);
snprintf(proc->name, sizeof(proc->name), "%s", newName);
}
}
}
static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map) {
for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
BasFormVarInfoT *fv = &mod->formVarInfo[i];
const char *mapped = nameMapLookup(map, fv->formName);
if (mapped) {
snprintf(fv->formName, sizeof(fv->formName), "%s", mapped);
}
}
}
// ============================================================
// basStripFrmComments
// ============================================================
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap) {
if (!src || srcLen <= 0 || !outBuf || outCap <= 0) {
return 0;
}
int32_t outLen = 0;
int32_t i = 0;
while (i < srcLen) {
int32_t lineStart = i;
while (i < srcLen && src[i] != '\n' && src[i] != '\r') {
i++;
}
int32_t lineEnd = i;
if (i < srcLen && src[i] == '\r') {
i++;
}
if (i < srcLen && src[i] == '\n') {
i++;
}
// Scan for first unquoted ' (comment start).
bool inStr = false;
int32_t commentStart = -1;
for (int32_t j = lineStart; j < lineEnd; j++) {
char c = src[j];
if (c == '"') {
inStr = !inStr;
} else if (c == '\'' && !inStr) {
commentStart = j;
break;
}
}
int32_t contentEnd = (commentStart >= 0) ? commentStart : lineEnd;
// Check for whole-line REM. Find first non-whitespace position.
int32_t firstNonWs = lineStart;
while (firstNonWs < contentEnd && (src[firstNonWs] == ' ' || src[firstNonWs] == '\t')) {
firstNonWs++;
}
if (contentEnd - firstNonWs >= 3 &&
strncasecmp(src + firstNonWs, "REM", 3) == 0 &&
(contentEnd - firstNonWs == 3 ||
src[firstNonWs + 3] == ' ' ||
src[firstNonWs + 3] == '\t')) {
contentEnd = firstNonWs;
}
// Trim trailing whitespace.
while (contentEnd > lineStart && (src[contentEnd - 1] == ' ' || src[contentEnd - 1] == '\t')) {
contentEnd--;
}
// Drop lines that have no non-whitespace content.
if (contentEnd <= firstNonWs) {
continue;
}
int32_t writeLen = contentEnd - lineStart;
if (outLen + writeLen + 1 >= outCap) {
break;
}
memcpy(outBuf + outLen, src + lineStart, writeLen);
outLen += writeLen;
outBuf[outLen++] = '\n';
}
return outLen;
}
// ============================================================
// Top-level entry point
// ============================================================
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms) {
if (!mod || frmCount < 0) {
return;
}
NameMapT map;
nameMapInit(&map);
// Pass 1: collect all names from all .frm texts
for (int32_t i = 0; i < frmCount; i++) {
if (frmTexts[i] && frmLens[i] > 0) {
collectNamesFromFrm(frmTexts[i], frmLens[i], &map);
}
}
// Pass 2: rewrite each .frm
for (int32_t i = 0; i < frmCount; i++) {
outFrms[i].data = NULL;
outFrms[i].len = 0;
if (!frmTexts[i] || frmLens[i] <= 0) {
continue;
}
int32_t strippedLen = findFormEndPos(frmTexts[i], frmLens[i]);
// Allocate generous output buffer (mapped names are usually shorter
// than originals, but allow for growth and a trailing newline).
int32_t outCap = strippedLen + 1024;
uint8_t *outBuf = malloc(outCap);
if (!outBuf) {
continue;
}
int32_t outLen = rewriteFrmText(frmTexts[i], strippedLen, &map, outBuf, outCap);
// Ensure trailing newline
if (outLen > 0 && outBuf[outLen - 1] != '\n' && outLen < outCap) {
outBuf[outLen++] = '\n';
}
outFrms[i].data = outBuf;
outFrms[i].len = outLen;
}
// Pass 3: rewrite module
rewriteModuleConstants(mod, &map);
rewriteModuleProcs(mod, &map);
rewriteModuleFormVars(mod, &map);
nameMapFree(&map);
}

View file

@ -0,0 +1,46 @@
// obfuscate.h -- Release build name obfuscation
//
// Replaces form and control names with generated tokens (C1, C2, ...)
// across the compiled module AND the raw .frm text resources. This
// hinders casual decompilation by removing meaningful identifiers
// from event handler names (e.g., BtnHello_Click -> C3_Click).
#ifndef DVXBASIC_OBFUSCATE_H
#define DVXBASIC_OBFUSCATE_H
#include "../runtime/vm.h"
// One .frm text after obfuscation (data is newly-allocated).
typedef struct {
uint8_t *data;
int32_t len;
} BasObfFrmT;
// Obfuscate form/control names in the module and all .frm texts.
//
// Reads original names from the Begin declarations in each .frm,
// generates C1..Cn, then rewrites:
// - The .frm text (form/control name declarations, stripping the
// trailing BASIC code section after the outer form closes)
// - Module string constants matching any original name
// - Procedure names matching <OrigName>_<Event>
// - formVarInfo entries keyed by form name
//
// frmTexts / frmLens: input .frm buffers (one per form).
// outFrms: caller-allocated array of frmCount entries; function fills
// in .data (malloc'd, caller frees) and .len for each.
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms);
// Strip comments from .frm text.
//
// Removes both whole-line comments (lines whose first non-whitespace
// token is `'` or `REM`) and trailing comments (everything from the
// first unquoted `'` to end of line). Pure-whitespace lines are
// dropped. Quoted string literals are preserved as-is.
//
// srcLen: input length; outBuf: caller-allocated buffer of outCap bytes.
// Returns the number of bytes written.
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap);
#endif // DVXBASIC_OBFUSCATE_H

View file

@ -235,6 +235,9 @@
#define OP_MATH_EXP 0xAA #define OP_MATH_EXP 0xAA
#define OP_MATH_RND 0xAB #define OP_MATH_RND 0xAB
#define OP_MATH_RANDOMIZE 0xAC // seed on stack (or TIMER if -1) #define OP_MATH_RANDOMIZE 0xAC // seed on stack (or TIMER if -1)
#define OP_RGB 0xAD // pop b, g, r; push LONG = (r<<16)|(g<<8)|b
#define OP_GET_RED 0xAE // pop LONG color; push (color>>16) & 0xFF
#define OP_GET_GREEN 0xAF // pop LONG color; push (color>>8) & 0xFF
// ============================================================ // ============================================================
// Conversion built-ins // Conversion built-ins
@ -317,6 +320,8 @@
#define OP_CALL_EXTERN 0xCD // [uint16 libNameIdx] [uint16 funcNameIdx] [uint8 argc] [uint8 retType] #define OP_CALL_EXTERN 0xCD // [uint16 libNameIdx] [uint16 funcNameIdx] [uint8 argc] [uint8 retType]
#define OP_GET_BLUE 0xD0 // pop LONG color; push color & 0xFF
// App object // App object
#define OP_APP_PATH 0xDD // push App.Path string #define OP_APP_PATH 0xDD // push App.Path string
#define OP_APP_CONFIG 0xDE // push App.Config string #define OP_APP_CONFIG 0xDE // push App.Config string

View file

@ -77,8 +77,12 @@ static const BuiltinFuncT builtinFuncs[] = {
{"COS", OP_MATH_COS, 1, 1, BAS_TYPE_DOUBLE}, {"COS", OP_MATH_COS, 1, 1, BAS_TYPE_DOUBLE},
{"EXP", OP_MATH_EXP, 1, 1, BAS_TYPE_DOUBLE}, {"EXP", OP_MATH_EXP, 1, 1, BAS_TYPE_DOUBLE},
{"FIX", OP_MATH_FIX, 1, 1, BAS_TYPE_INTEGER}, {"FIX", OP_MATH_FIX, 1, 1, BAS_TYPE_INTEGER},
{"GETBLUE", OP_GET_BLUE, 1, 1, BAS_TYPE_INTEGER},
{"GETGREEN", OP_GET_GREEN, 1, 1, BAS_TYPE_INTEGER},
{"GETRED", OP_GET_RED, 1, 1, BAS_TYPE_INTEGER},
{"INT", OP_MATH_INT, 1, 1, BAS_TYPE_INTEGER}, {"INT", OP_MATH_INT, 1, 1, BAS_TYPE_INTEGER},
{"LOG", OP_MATH_LOG, 1, 1, BAS_TYPE_DOUBLE}, {"LOG", OP_MATH_LOG, 1, 1, BAS_TYPE_DOUBLE},
{"RGB", OP_RGB, 3, 3, BAS_TYPE_LONG},
{"RND", OP_MATH_RND, 0, 1, BAS_TYPE_DOUBLE}, {"RND", OP_MATH_RND, 0, 1, BAS_TYPE_DOUBLE},
{"SGN", OP_MATH_SGN, 1, 1, BAS_TYPE_INTEGER}, {"SGN", OP_MATH_SGN, 1, 1, BAS_TYPE_INTEGER},
{"SIN", OP_MATH_SIN, 1, 1, BAS_TYPE_DOUBLE}, {"SIN", OP_MATH_SIN, 1, 1, BAS_TYPE_DOUBLE},
@ -1250,7 +1254,9 @@ static void parsePrimary(BasParserT *p) {
} else if (checkKeyword(p,"Config")) { } else if (checkKeyword(p,"Config")) {
advance(p); advance(p);
basEmit8(&p->cg, OP_APP_CONFIG); basEmit8(&p->cg, OP_APP_CONFIG);
} else if (checkKeyword(p,"Data")) { } else if (checkKeyword(p,"Data") || p->lex.token.type == TOK_DATA) {
// "Data" tokenizes as TOK_DATA (the DATA/READ keyword) rather
// than TOK_IDENT, so accept it directly by token type too.
advance(p); advance(p);
basEmit8(&p->cg, OP_APP_DATA); basEmit8(&p->cg, OP_APP_DATA);
} else { } else {
@ -1482,23 +1488,32 @@ static void parsePrimary(BasParserT *p) {
return; return;
} }
// MsgBox(message [, flags]) -- as function expression returning button ID // MsgBox(message [, flags [, title]]) -- function form returning button ID
if (tt == TOK_MSGBOX) { if (tt == TOK_MSGBOX) {
advance(p); advance(p);
expect(p, TOK_LPAREN); expect(p, TOK_LPAREN);
parseExpression(p); // message parseExpression(p); // message
if (match(p, TOK_COMMA)) { if (match(p, TOK_COMMA)) {
parseExpression(p); // flags parseExpression(p); // flags
} else { } else {
basEmit8(&p->cg, OP_PUSH_INT16); basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0); // default flags = vbOKOnly basEmit16(&p->cg, 0); // default flags = vbOKOnly
} }
if (match(p, TOK_COMMA)) {
parseExpression(p); // title
} else {
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, emptyIdx);
}
expect(p, TOK_RPAREN); expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_MSGBOX); basEmit8(&p->cg, OP_MSGBOX);
return; return;
} }
// SQL expression functions -- all require parentheses
// IniRead$(file, section, key, default) // IniRead$(file, section, key, default)
if (tt == TOK_INIREAD) { if (tt == TOK_INIREAD) {
advance(p); advance(p);
@ -2251,6 +2266,17 @@ static void parseAssignOrCall(BasParserT *p) {
error(p, buf); error(p, buf);
return; return;
} }
// External library SUB: emit OP_CALL_EXTERN
if (sym->isExtern) {
basEmit8(&p->cg, OP_CALL_EXTERN);
basEmitU16(&p->cg, sym->externLibIdx);
basEmitU16(&p->cg, sym->externFuncIdx);
basEmit8(&p->cg, (uint8_t)argc);
basEmit8(&p->cg, sym->dataType);
return;
}
{ {
uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0; uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0;
basEmit8(&p->cg, OP_CALL); basEmit8(&p->cg, OP_CALL);
@ -2371,7 +2397,7 @@ static void parseClose(BasParserT *p) {
static void parseConst(BasParserT *p) { static void parseConst(BasParserT *p) {
// CONST name = value // CONST name [AS type] = value
advance(p); // consume CONST advance(p); // consume CONST
if (!check(p, TOK_IDENT)) { if (!check(p, TOK_IDENT)) {
@ -2384,6 +2410,12 @@ static void parseConst(BasParserT *p) {
name[BAS_MAX_TOKEN_LEN - 1] = '\0'; name[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p); advance(p);
// Optional type annotation (declarative; value's literal type still
// determines runtime representation).
if (match(p, TOK_AS)) {
(void)resolveTypeName(p);
}
expect(p, TOK_EQ); expect(p, TOK_EQ);
// Parse the constant value (must be a literal) // Parse the constant value (must be a literal)
@ -2711,9 +2743,25 @@ static void parseDeclareLibrary(BasParserT *p) {
char funcName[BAS_MAX_TOKEN_LEN]; char funcName[BAS_MAX_TOKEN_LEN];
strncpy(funcName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1); strncpy(funcName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
funcName[BAS_MAX_TOKEN_LEN - 1] = '\0'; funcName[BAS_MAX_TOKEN_LEN - 1] = '\0';
uint16_t funcNameIdx = basAddConstant(&p->cg, funcName, (int32_t)strlen(funcName));
advance(p); advance(p);
// Strip type suffix ($%&!#) from extern name so dlsym finds the
// C function. The suffix is still used for return type via suffixToType.
char externName[BAS_MAX_TOKEN_LEN];
strncpy(externName, funcName, BAS_MAX_TOKEN_LEN - 1);
externName[BAS_MAX_TOKEN_LEN - 1] = '\0';
int32_t enLen = (int32_t)strlen(externName);
if (enLen > 0) {
char last = externName[enLen - 1];
if (last == '$' || last == '%' || last == '&' || last == '!' || last == '#') {
externName[enLen - 1] = '\0';
}
}
uint16_t funcNameIdx = basAddConstant(&p->cg, externName, (int32_t)strlen(externName));
// Parse parameter list // Parse parameter list
int32_t paramCount = 0; int32_t paramCount = 0;
uint8_t paramTypes[BAS_MAX_PARAMS]; uint8_t paramTypes[BAS_MAX_PARAMS];
@ -4191,20 +4239,49 @@ static void parsePrint(BasParserT *p) {
// PRINT USING "fmt"; expr [; expr] ... // PRINT USING "fmt"; expr [; expr] ...
advance(p); // consume PRINT advance(p); // consume PRINT
// Check for file I/O: PRINT #channel, expr // File I/O: PRINT #channel, expr [; expr | , expr ]* [;]
//
// Channel is pushed once and DUP'd per value. `;` means no separator
// between values; `,` is treated the same (tab-zone separator not
// supported for file output). Trailing `;` suppresses the final newline.
if (check(p, TOK_HASH)) { if (check(p, TOK_HASH)) {
advance(p); // consume # advance(p); // consume #
// Channel number // Channel number -- stays on stack as "keep" for the whole statement.
parseExpression(p); parseExpression(p);
// Comma separator
expect(p, TOK_COMMA); expect(p, TOK_COMMA);
// Value to print bool trailingSep = false;
parseExpression(p);
for (;;) {
// Duplicate the channel for this OP_FILE_PRINT.
basEmit8(&p->cg, OP_DUP);
parseExpression(p);
basEmit8(&p->cg, OP_FILE_PRINT); basEmit8(&p->cg, OP_FILE_PRINT);
if (check(p, TOK_SEMICOLON) || check(p, TOK_COMMA)) {
advance(p);
trailingSep = true;
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
break;
}
trailingSep = false;
continue;
}
break;
}
// No trailing ; or , -> write newline. Otherwise, just drop the
// kept channel value.
if (trailingSep) {
basEmit8(&p->cg, OP_POP);
} else {
basEmit8(&p->cg, OP_FILE_WRITE_NL);
}
return; return;
} }
@ -5187,21 +5264,31 @@ static void parseStatement(BasParserT *p) {
basEmit8(&p->cg, OP_POP); // discard result basEmit8(&p->cg, OP_POP); // discard result
break; break;
case TOK_MSGBOX: case TOK_MSGBOX: {
// MsgBox message [, flags] (statement form, discard result) // MsgBox message [, flags [, title]] (statement form, discards result)
advance(p); advance(p);
parseExpression(p); // message parseExpression(p); // message
if (match(p, TOK_COMMA)) { if (match(p, TOK_COMMA)) {
parseExpression(p); // flags parseExpression(p); // flags
} else { } else {
basEmit8(&p->cg, OP_PUSH_INT16); basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0); // default flags = MB_OK basEmit16(&p->cg, 0); // default flags = MB_OK
} }
if (match(p, TOK_COMMA)) {
parseExpression(p); // title
} else {
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, emptyIdx);
}
basEmit8(&p->cg, OP_MSGBOX); basEmit8(&p->cg, OP_MSGBOX);
basEmit8(&p->cg, OP_POP); // discard result basEmit8(&p->cg, OP_POP); // discard result
break; break;
}
// SQL statement forms (no return value)
case TOK_INIWRITE: case TOK_INIWRITE:
// IniWrite file, section, key, value // IniWrite file, section, key, value
advance(p); advance(p);

View file

@ -3,19 +3,68 @@
// Removes debug information from a compiled module: // Removes debug information from a compiled module:
// - Clears debug variable info (names, scopes, types) // - Clears debug variable info (names, scopes, types)
// - Clears debug UDT definitions // - Clears debug UDT definitions
// // - Mangles procedure names that aren't needed for runtime dispatch.
// Procedure names are preserved because the form runtime uses // The form runtime dispatches events by name (Control_Event pattern)
// them for event dispatch (ControlName_EventName convention). // and SetEvent looks up handlers by name at runtime, so those proc
// names must be preserved. Everything else becomes F1, F2, F3...
// //
// OP_LINE removal is deferred to a future version (requires // OP_LINE removal is deferred to a future version (requires
// bytecode compaction and offset rewriting). // bytecode compaction and offset rewriting).
#include "strip.h" #include "strip.h"
#include "../runtime/values.h"
#include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
// Events fired by name via basFormRtFireEvent* in formrt.c. Any proc
// ending in "_<EventName>" must keep its name so the dispatcher can
// find it.
static const char *sEventSuffixes[] = {
"Load", "Unload", "QueryUnload", "Resize", "Activate", "Deactivate",
"Click", "DblClick", "Change", "Timer",
"GotFocus", "LostFocus",
"KeyPress", "KeyDown", "KeyUp",
"MouseDown", "MouseUp", "MouseMove",
"Scroll", "Reposition", "Validate",
NULL
};
static bool nameEndsWithEventSuffix(const char *name) {
const char *underscore = strrchr(name, '_');
if (!underscore) {
return false;
}
const char *suffix = underscore + 1;
for (int32_t i = 0; sEventSuffixes[i]; i++) {
if (strcasecmp(suffix, sEventSuffixes[i]) == 0) {
return true;
}
}
return false;
}
static bool nameInConstantPool(const BasModuleT *mod, const char *name) {
for (int32_t i = 0; i < mod->constCount; i++) {
const BasStringT *s = mod->constants[i];
if (s && strcasecmp(s->data, name) == 0) {
return true;
}
}
return false;
}
void basStripModule(BasModuleT *mod) { void basStripModule(BasModuleT *mod) {
if (!mod) { if (!mod) {
return; return;
@ -36,4 +85,27 @@ void basStripModule(BasModuleT *mod) {
mod->debugUdtDefs = NULL; mod->debugUdtDefs = NULL;
mod->debugUdtDefCount = 0; mod->debugUdtDefCount = 0;
} }
// Mangle proc names. Keep names that are needed for runtime name
// lookup: event handlers (Control_Event pattern) and any name
// referenced as a string constant (e.g. SetEvent's target name).
int32_t nextMangled = 1;
for (int32_t i = 0; i < mod->procCount; i++) {
BasProcEntryT *proc = &mod->procs[i];
if (proc->name[0] == '\0') {
continue;
}
if (nameEndsWithEventSuffix(proc->name)) {
continue;
}
if (nameInConstantPool(mod, proc->name)) {
continue;
}
snprintf(proc->name, sizeof(proc->name), "F%ld", (long)nextMangled++);
}
} }

View file

@ -1,8 +1,9 @@
// strip.h -- Release build stripping // strip.h -- Release build stripping
// //
// Removes debug information from a compiled module to prevent // Removes debug information from a compiled module to hinder
// decompilation. Clears procedure names, debug variable info, // decompilation. Clears debug variable info and debug UDT
// and debug UDT definitions. // definitions, and mangles proc names that aren't needed for
// runtime name-based dispatch.
#ifndef DVXBASIC_STRIP_H #ifndef DVXBASIC_STRIP_H
#define DVXBASIC_STRIP_H #define DVXBASIC_STRIP_H
@ -10,9 +11,11 @@
#include "../runtime/vm.h" #include "../runtime/vm.h"
// Strip debug info from a module for release builds: // Strip debug info from a module for release builds:
// - Clear all procedure names
// - Clear debug variable info // - Clear debug variable info
// - Clear debug UDT definitions // - Clear debug UDT definitions
// - Mangle proc names to F1, F2, ... except for event handlers
// (matched by Control_Event suffix) and names referenced as
// string constants (SetEvent dispatch targets).
void basStripModule(BasModuleT *mod); void basStripModule(BasModuleT *mod);
#endif // DVXBASIC_STRIP_H #endif // DVXBASIC_STRIP_H

View file

@ -8,6 +8,7 @@
#include "formcfm.h" #include "formcfm.h"
#include "../compiler/opcodes.h" #include "../compiler/opcodes.h"
#include "dvxDlg.h" #include "dvxDlg.h"
#include "dvxRes.h"
#include "dvxWm.h" #include "dvxWm.h"
#include "box/box.h" #include "box/box.h"
#include "ansiTerm/ansiTerm.h" #include "ansiTerm/ansiTerm.h"
@ -1291,22 +1292,29 @@ void *basFormRtLoadForm(void *ctx, const char *formName) {
} }
} }
// Check the .frm cache for reload after unload // Check the .frm cache for reload after unload.
// Use a static guard to prevent recursion: basFormRtLoadFrm calls
// basFormRtLoadForm for the "Begin Form" line, which would re-enter
// this function. The guard lets the recursive call fall through to
// bare form creation, which basFormRtLoadFrm then populates.
static bool sLoadingFrm = false;
if (!sLoadingFrm) {
for (int32_t i = 0; i < rt->frmCacheCount; i++) { for (int32_t i = 0; i < rt->frmCacheCount; i++) {
if (strcasecmp(rt->frmCache[i].formName, formName) == 0) { if (strcasecmp(rt->frmCache[i].formName, formName) == 0) {
// Save source and remove from cache BEFORE calling basFormRtLoadFrm,
// because basFormRtLoadFrm internally calls basFormRtLoadForm (for the
// "Begin Form" line) which would find this cache entry again and recurse.
char *src = rt->frmCache[i].frmSource; char *src = rt->frmCache[i].frmSource;
int32_t srcLen = rt->frmCache[i].frmSourceLen; int32_t srcLen = rt->frmCache[i].frmSourceLen;
arrdel(rt->frmCache, i); arrdel(rt->frmCache, i);
rt->frmCacheCount = (int32_t)arrlen(rt->frmCache); rt->frmCacheCount = (int32_t)arrlen(rt->frmCache);
sLoadingFrm = true;
BasFormT *form = basFormRtLoadFrm(rt, src, srcLen); BasFormT *form = basFormRtLoadFrm(rt, src, srcLen);
sLoadingFrm = false;
free(src); free(src);
return form; return form;
} }
} }
}
// Check the compiled form cache (standalone apps) // Check the compiled form cache (standalone apps)
for (int32_t i = 0; i < rt->cfmCacheCount; i++) { for (int32_t i = 0; i < rt->cfmCacheCount; i++) {
@ -1701,6 +1709,10 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
} }
val = basValStringFromC(value + 1); val = basValStringFromC(value + 1);
} else if (strcasecmp(value, "True") == 0) {
val = basValBool(true);
} else if (strcasecmp(value, "False") == 0) {
val = basValBool(false);
} else { } else {
val = basValLong(atoi(value)); val = basValLong(atoi(value));
} }
@ -1884,6 +1896,38 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
} }
} }
// Allocate per-form variable storage and run init code.
// This must happen AFTER controls are created (so init code can
// reference controls by name) but BEFORE Form_Load fires.
if (form && rt->module && rt->module->formVarInfo) {
for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) {
if (strcasecmp(rt->module->formVarInfo[j].formName, form->name) != 0) {
continue;
}
if (!form->formVars) {
int32_t vc = rt->module->formVarInfo[j].varCount;
if (vc > 0) {
form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT));
form->formVarCount = vc;
}
}
int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr;
if (initAddr >= 0 && rt->vm) {
basVmSetCurrentForm(rt->vm, form);
basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount);
basVmCallSub(rt->vm, initAddr);
basVmSetCurrentForm(rt->vm, NULL);
basVmSetCurrentFormVars(rt->vm, NULL, 0);
}
break;
}
}
// Fire the Load event now that the form and controls are ready // Fire the Load event now that the form and controls are ready
if (form) { if (form) {
basFormRtFireEvent(rt, form, form->name, "Load"); basFormRtFireEvent(rt, form, form->name, "Load");
@ -1928,10 +1972,10 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
// basFormRtMsgBox // basFormRtMsgBox
// ============================================================ // ============================================================
int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags) { int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title) {
BasFormRtT *rt = (BasFormRtT *)ctx; BasFormRtT *rt = (BasFormRtT *)ctx;
return dvxMessageBox(rt->ctx, "DVX BASIC", message, flags); return dvxMessageBox(rt->ctx, (title && title[0]) ? title : "DVX BASIC", message, flags);
} }
@ -2049,14 +2093,12 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT
return; return;
} }
// "Caption" and "Text": save to the persistent textBuf and apply // "Caption" and "Text": pass directly to the widget (all widgets
// immediately. Controls are heap-allocated so textBuf addresses // strdup their text internally).
// are stable across arrput calls.
if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) { if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) {
BasStringT *s = basValFormatString(value); BasStringT *s = basValFormatString(value);
snprintf(ctrl->textBuf, BAS_MAX_TEXT_BUF, "%s", s->data); wgtSetText(ctrl->widget, s->data);
basStringUnref(s); basStringUnref(s);
wgtSetText(ctrl->widget, ctrl->textBuf);
return; return;
} }
@ -2129,6 +2171,11 @@ void basFormRtUnloadForm(void *ctx, void *formRef) {
return; return;
} }
// QueryUnload: give the form a chance to cancel
if (basFormRtFireEventWithCancel(rt, form, form->name, "QueryUnload")) {
return;
}
basFormRtFireEvent(rt, form, form->name, "Unload"); basFormRtFireEvent(rt, form, form->name, "Unload");
// Release per-form variables // Release per-form variables
@ -2253,8 +2300,12 @@ WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) {
CreateParentBoolFnT fn = *(CreateParentBoolFnT *)api; CreateParentBoolFnT fn = *(CreateParentBoolFnT *)api;
return fn(parent, (bool)iface->createArgs[0]); return fn(parent, (bool)iface->createArgs[0]);
} }
case WGT_CREATE_PARENT_DATA: case WGT_CREATE_PARENT_DATA: {
return NULL; // create(parent, NULL, 0, 0, 0) -- empty widget, load content later via properties
typedef WidgetT *(*CreateDataFnT)(WidgetT *, uint8_t *, int32_t, int32_t, int32_t);
CreateDataFnT fn = *(CreateDataFnT *)api;
return fn(parent, NULL, 0, 0, 0);
}
default: { default: {
CreateParentFnT fn = *(CreateParentFnT *)api; CreateParentFnT fn = *(CreateParentFnT *)api;
return fn(parent); return fn(parent);
@ -2769,8 +2820,7 @@ static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl) {
const char *val = wgtDataCtrlGetField(dataCtrl->widget, ctrl->dataField); const char *val = wgtDataCtrlGetField(dataCtrl->widget, ctrl->dataField);
if (val) { if (val) {
snprintf(ctrl->textBuf, BAS_MAX_TEXT_BUF, "%s", val); wgtSetText(ctrl->widget, val);
wgtSetText(ctrl->widget, ctrl->textBuf);
} }
} }
} }
@ -3214,21 +3264,64 @@ static BasValueT zeroValue(void) {
// need C-side storage since BASIC strings can't be used as // need C-side storage since BASIC strings can't be used as
// writable char pointers. // writable char pointers.
// Parse a filter string into FileFilterT entries.
//
// Format: "Label (pattern)|Label (pattern)|..."
// The pattern is extracted from inside the parentheses.
// Example: "Images (*.bmp;*.png;*.jpg)|Text (*.txt)|All Files (*.*)"
//
// If no parentheses, the entire entry is used as both label and pattern.
// If no pipe, treat as a single pattern for backward compatibility.
#define BAS_MAX_FILE_FILTERS 16
static int32_t parseFileFilters(const char *filter, FileFilterT *out, char *buf, int32_t bufSize) {
if (!filter || !filter[0]) {
out[0].label = "All Files (*.*)";
return 1;
}
// No pipe and no parentheses = old-style single pattern, wrap it
if (!strchr(filter, '|') && !strchr(filter, '(')) {
snprintf(buf, bufSize, "%s (%s)", filter, filter);
out[0].label = buf;
out[1].label = "All Files (*.*)";
return 2;
}
snprintf(buf, bufSize, "%s", filter);
int32_t count = 0;
char *p = buf;
while (*p && count < BAS_MAX_FILE_FILTERS) {
char *pipe = strchr(p, '|');
if (pipe) {
*pipe = '\0';
}
out[count].label = p;
count++;
p = pipe ? pipe + 1 : p + strlen(p);
}
return count > 0 ? count : 1;
}
const char *basFileOpen(const char *title, const char *filter) { const char *basFileOpen(const char *title, const char *filter) {
if (!sFormRt) { if (!sFormRt) {
return ""; return "";
} }
FileFilterT filters[2]; FileFilterT filters[BAS_MAX_FILE_FILTERS];
filters[0].label = filter; char filterBuf[1024];
filters[0].pattern = filter; int32_t count = parseFileFilters(filter, filters, filterBuf, sizeof(filterBuf));
filters[1].label = "All Files (*.*)";
filters[1].pattern = "*.*";
static char path[260]; static char path[DVX_MAX_PATH];
path[0] = '\0'; path[0] = '\0';
if (dvxFileDialog(sFormRt->ctx, title, FD_OPEN, NULL, filters, 2, path, sizeof(path))) { if (dvxFileDialog(sFormRt->ctx, title, FD_OPEN, NULL, filters, count, path, sizeof(path))) {
return path; return path;
} }
@ -3241,16 +3334,14 @@ const char *basFileSave(const char *title, const char *filter) {
return ""; return "";
} }
FileFilterT filters[2]; FileFilterT filters[BAS_MAX_FILE_FILTERS];
filters[0].label = filter; char filterBuf[1024];
filters[0].pattern = filter; int32_t count = parseFileFilters(filter, filters, filterBuf, sizeof(filterBuf));
filters[1].label = "All Files (*.*)";
filters[1].pattern = "*.*";
static char path[260]; static char path[DVX_MAX_PATH];
path[0] = '\0'; path[0] = '\0';
if (dvxFileDialog(sFormRt->ctx, title, FD_SAVE, NULL, filters, 2, path, sizeof(path))) { if (dvxFileDialog(sFormRt->ctx, title, FD_SAVE, NULL, filters, count, path, sizeof(path))) {
return path; return path;
} }
@ -4277,10 +4368,244 @@ void HelpView(const char *hlpFile) {
return; return;
} }
char viewerPath[260]; // If hlpFile has no directory component, resolve it against the calling
// app's directory. This matches the convention where apps bundle their
// help file alongside the .app.
char resolved[DVX_MAX_PATH];
bool hasDir = strchr(hlpFile, '/') != NULL || strchr(hlpFile, '\\') != NULL || (hlpFile[0] && hlpFile[1] == ':');
if (!hasDir && sFormRt->vm && sFormRt->vm->appPath[0]) {
snprintf(resolved, sizeof(resolved), "%s%c%s", sFormRt->vm->appPath, DVX_PATH_SEP, hlpFile);
} else {
snprintf(resolved, sizeof(resolved), "%s", hlpFile);
}
char viewerPath[DVX_MAX_PATH];
snprintf(viewerPath, sizeof(viewerPath), "APPS%cKPUNCH%cDVXHELP%cDVXHELP.APP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP); snprintf(viewerPath, sizeof(viewerPath), "APPS%cKPUNCH%cDVXHELP%cDVXHELP.APP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP);
sShellLoadApp(sFormRt->ctx, viewerPath, hlpFile); sShellLoadApp(sFormRt->ctx, viewerPath, resolved);
}
// ============================================================
// Resource file extern wrappers (DECLARE LIBRARY "basrt")
//
// Expose the DVX resource API to BASIC programs. Each function
// is a thin wrapper around the C resource API.
// ============================================================
// Maximum number of simultaneously open resource handles
#define RES_MAX_HANDLES 8
static DvxResHandleT *sResHandles[RES_MAX_HANDLES];
int32_t ResOpen(const char *path) {
if (!path) {
return 0;
}
for (int32_t i = 0; i < RES_MAX_HANDLES; i++) {
if (!sResHandles[i]) {
sResHandles[i] = dvxResOpen(path);
if (sResHandles[i]) {
return i + 1;
}
return 0;
}
}
return 0;
}
void ResClose(int32_t handle) {
int32_t idx = handle - 1;
if (idx < 0 || idx >= RES_MAX_HANDLES || !sResHandles[idx]) {
return;
}
dvxResClose(sResHandles[idx]);
sResHandles[idx] = NULL;
}
int32_t ResCount(int32_t handle) {
int32_t idx = handle - 1;
if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx]) {
return (int32_t)sResHandles[idx]->entryCount;
}
return 0;
}
const char *ResName(int32_t handle, int32_t index) {
int32_t idx = handle - 1;
if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
return sResHandles[idx]->entries[index].name;
}
return "";
}
int32_t ResType(int32_t handle, int32_t index) {
int32_t idx = handle - 1;
if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
return (int32_t)sResHandles[idx]->entries[index].type;
}
return 0;
}
int32_t ResSize(int32_t handle, int32_t index) {
int32_t idx = handle - 1;
if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
return (int32_t)sResHandles[idx]->entries[index].size;
}
return 0;
}
const char *ResGetText(const char *path, const char *name) {
static char sBuf[1024];
sBuf[0] = '\0';
if (!path || !name) {
return sBuf;
}
DvxResHandleT *h = dvxResOpen(path);
if (!h) {
return sBuf;
}
uint32_t size = 0;
void *data = dvxResRead(h, name, &size);
dvxResClose(h);
if (data) {
uint32_t copyLen = size;
if (copyLen >= sizeof(sBuf)) {
copyLen = sizeof(sBuf) - 1;
}
memcpy(sBuf, data, copyLen);
sBuf[copyLen] = '\0';
free(data);
}
return sBuf;
}
int32_t ResAddText(const char *path, const char *name, const char *text) {
if (!path || !name) {
return 0;
}
if (!text) {
text = "";
}
return dvxResAppend(path, name, DVX_RES_TEXT, text, (uint32_t)strlen(text) + 1) == 0 ? -1 : 0;
}
int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile) {
if (!path || !name || !srcFile) {
return 0;
}
FILE *f = fopen(srcFile, "rb");
if (!f) {
return 0;
}
fseek(f, 0, SEEK_END);
long fileSize = ftell(f);
fseek(f, 0, SEEK_SET);
if (fileSize <= 0) {
fclose(f);
return 0;
}
uint8_t *buf = (uint8_t *)malloc((size_t)fileSize);
if (!buf) {
fclose(f);
return 0;
}
if (fread(buf, 1, (size_t)fileSize, f) != (size_t)fileSize) {
free(buf);
fclose(f);
return 0;
}
fclose(f);
int32_t result = dvxResAppend(path, name, (uint32_t)type, buf, (uint32_t)fileSize) == 0 ? -1 : 0;
free(buf);
return result;
}
int32_t ResRemove(const char *path, const char *name) {
if (!path || !name) {
return 0;
}
return dvxResRemove(path, name) == 0 ? -1 : 0;
}
int32_t ResExtract(const char *path, const char *name, const char *outFile) {
if (!path || !name || !outFile) {
return 0;
}
DvxResHandleT *h = dvxResOpen(path);
if (!h) {
return 0;
}
uint32_t size = 0;
void *data = dvxResRead(h, name, &size);
dvxResClose(h);
if (!data) {
return 0;
}
FILE *f = fopen(outFile, "wb");
if (!f) {
free(data);
return 0;
}
int32_t result = (fwrite(data, 1, size, f) == size) ? -1 : 0;
fclose(f);
free(data);
return result;
} }

View file

@ -43,7 +43,6 @@ typedef struct {
// Control instance (a widget on a form) // Control instance (a widget on a form)
// ============================================================ // ============================================================
#define BAS_MAX_TEXT_BUF 256
#define BAS_MAX_EVENT_OVERRIDES 16 #define BAS_MAX_EVENT_OVERRIDES 16
// Event handler override (SetEvent) // Event handler override (SetEvent)
@ -59,7 +58,6 @@ typedef struct BasControlT {
WidgetT *widget; // the DVX widget WidgetT *widget; // the DVX widget
BasFormT *form; // owning form BasFormT *form; // owning form
const WgtIfaceT *iface; // interface descriptor (from .wgt) const WgtIfaceT *iface; // interface descriptor (from .wgt)
char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text
char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding
char dataField[BAS_MAX_CTRL_NAME]; // column name for binding char dataField[BAS_MAX_CTRL_NAME]; // column name for binding
char helpTopic[BAS_MAX_CTRL_NAME]; // help topic ID for F1 char helpTopic[BAS_MAX_CTRL_NAME]; // help topic ID for F1
@ -171,7 +169,7 @@ void *basFormRtLoadForm(void *ctx, const char *formName);
void basFormRtUnloadForm(void *ctx, void *formRef); void basFormRtUnloadForm(void *ctx, void *formRef);
void basFormRtShowForm(void *ctx, void *formRef, bool modal); void basFormRtShowForm(void *ctx, void *formRef, bool modal);
void basFormRtHideForm(void *ctx, void *formRef); void basFormRtHideForm(void *ctx, void *formRef);
int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags); int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title);
// ---- Extern call callbacks (shared by IDE and stub) ---- // ---- Extern call callbacks (shared by IDE and stub) ----

View file

@ -3095,9 +3095,9 @@ static void recentOpen(int32_t index) {
static void loadFile(void) { static void loadFile(void) {
FileFilterT filters[] = { FileFilterT filters[] = {
{ "BASIC Files (*.bas)", "*.bas" }, { "BASIC Files (*.bas)" },
{ "Form Files (*.frm)", "*.frm" }, { "Form Files (*.frm)" },
{ "All Files (*.*)", "*.*" } { "All Files (*.*)" }
}; };
char path[DVX_MAX_PATH]; char path[DVX_MAX_PATH];
@ -3247,7 +3247,7 @@ static void newProject(void) {
// Ask for directory via save dialog (file = name.dbp) // Ask for directory via save dialog (file = name.dbp)
FileFilterT filters[] = { FileFilterT filters[] = {
{ "Project Files (*.dbp)", "*.dbp" } { "Project Files (*.dbp)" }
}; };
char dbpPath[DVX_MAX_PATH]; char dbpPath[DVX_MAX_PATH];
@ -3310,8 +3310,8 @@ static void newProject(void) {
static void openProject(void) { static void openProject(void) {
FileFilterT filters[] = { FileFilterT filters[] = {
{ "Project Files (*.dbp)", "*.dbp" }, { "Project Files (*.dbp)" },
{ "All Files (*.*)", "*.*" } { "All Files (*.*)" }
}; };
char path[DVX_MAX_PATH]; char path[DVX_MAX_PATH];
@ -3964,8 +3964,8 @@ static void makeExecutable(void) {
// Ask for output path // Ask for output path
FileFilterT filters[] = { FileFilterT filters[] = {
{ "DVX Applications (*.app)", "*.app" }, { "DVX Applications (*.app)" },
{ "All Files (*.*)", "*.*" } { "All Files (*.*)" }
}; };
char outPath[DVX_MAX_PATH]; char outPath[DVX_MAX_PATH];
outPath[0] = '\0'; outPath[0] = '\0';
@ -3975,7 +3975,7 @@ static void makeExecutable(void) {
} }
// Ask debug or release // Ask debug or release
const char *modeItems[] = { "Debug (include error info)", "Release (stripped)" }; const char *modeItems[] = { "Debug (include error info)" };
int32_t modeChoice = 0; int32_t modeChoice = 0;
if (!dvxChoiceDialog(sAc, "Build Mode", "Select build mode:", modeItems, 2, 0, &modeChoice)) { if (!dvxChoiceDialog(sAc, "Build Mode", "Select build mode:", modeItems, 2, 0, &modeChoice)) {
@ -4067,9 +4067,116 @@ static void makeExecutable(void) {
fclose(outFile); fclose(outFile);
free(stubData); free(stubData);
// Attach app name from project properties // Attach project property resources
const char *projName = sProject.name[0] ? sProject.name : "BASIC App"; const char *projName = sProject.name[0] ? sProject.name : "BASIC App";
dvxResAppend(outPath, "APPNAME", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1); dvxResAppend(outPath, "name", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1);
if (sProject.author[0]) {
dvxResAppend(outPath, "author", DVX_RES_TEXT, sProject.author, (uint32_t)strlen(sProject.author) + 1);
}
if (sProject.company[0]) {
dvxResAppend(outPath, "company", DVX_RES_TEXT, sProject.company, (uint32_t)strlen(sProject.company) + 1);
}
if (sProject.version[0]) {
dvxResAppend(outPath, "version", DVX_RES_TEXT, sProject.version, (uint32_t)strlen(sProject.version) + 1);
}
if (sProject.copyright[0]) {
dvxResAppend(outPath, "copyright", DVX_RES_TEXT, sProject.copyright, (uint32_t)strlen(sProject.copyright) + 1);
}
if (sProject.description[0]) {
dvxResAppend(outPath, "description", DVX_RES_TEXT, sProject.description, (uint32_t)strlen(sProject.description) + 1);
}
// Attach icon: project icon or fallback to IDE's noicon resource
if (sProject.iconPath[0]) {
char iconFullPath[DVX_MAX_PATH];
snprintf(iconFullPath, sizeof(iconFullPath), "%s%c%s", sProject.projectDir, DVX_PATH_SEP, sProject.iconPath);
FILE *iconFile = fopen(iconFullPath, "rb");
if (iconFile) {
fseek(iconFile, 0, SEEK_END);
long iconSize = ftell(iconFile);
fseek(iconFile, 0, SEEK_SET);
void *iconData = malloc(iconSize);
if (iconData) {
if (fread(iconData, 1, iconSize, iconFile) == (size_t)iconSize) {
dvxResAppend(outPath, "icon32", DVX_RES_ICON, iconData, (uint32_t)iconSize);
}
free(iconData);
}
fclose(iconFile);
}
} else {
// Use stock noicon from IDE resources
DvxResHandleT *ideRes = dvxResOpen(sCtx->appPath);
if (ideRes) {
uint32_t noiconSize = 0;
void *noiconData = dvxResRead(ideRes, "noicon", &noiconSize);
if (noiconData) {
dvxResAppend(outPath, "icon32", DVX_RES_ICON, noiconData, noiconSize);
free(noiconData);
}
dvxResClose(ideRes);
}
}
// Copy help file alongside the output app (if specified in project)
if (sProject.helpFile[0]) {
// Store just the filename as a text resource so the stub can find it
const char *helpBase = sProject.helpFile;
const char *sep = strrchr(helpBase, DVX_PATH_SEP);
if (sep) {
helpBase = sep + 1;
}
dvxResAppend(outPath, "helpfile", DVX_RES_TEXT, helpBase, (uint32_t)strlen(helpBase) + 1);
// Copy the .hlp file to sit next to the output .app
char helpSrc[DVX_MAX_PATH];
snprintf(helpSrc, sizeof(helpSrc), "%s%c%s", sProject.projectDir, DVX_PATH_SEP, sProject.helpFile);
char outDir[DVX_MAX_PATH];
snprintf(outDir, sizeof(outDir), "%s", outPath);
char *lastSep = strrchr(outDir, DVX_PATH_SEP);
if (lastSep) {
*lastSep = '\0';
}
char helpDst[DVX_MAX_PATH];
snprintf(helpDst, sizeof(helpDst), "%s%c%s", outDir, DVX_PATH_SEP, helpBase);
FILE *hSrc = fopen(helpSrc, "rb");
if (hSrc) {
FILE *hDst = fopen(helpDst, "wb");
if (hDst) {
char cpBuf[4096];
size_t n;
while ((n = fread(cpBuf, 1, sizeof(cpBuf), hSrc)) > 0) {
fwrite(cpBuf, 1, n, hDst);
}
fclose(hDst);
}
fclose(hSrc);
}
}
// Attach MODULE resource // Attach MODULE resource
dvxResAppend(outPath, "MODULE", DVX_RES_BINARY, modData, (uint32_t)modLen); dvxResAppend(outPath, "MODULE", DVX_RES_BINARY, modData, (uint32_t)modLen);
@ -8519,6 +8626,7 @@ static void updateProjectMenuState(void) {
} }
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, anyDirty); wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, anyDirty);
wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_EXE, hasProject && isIdle);
// Edit menu // Edit menu
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject); wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject);

View file

@ -441,6 +441,7 @@ void basModuleFree(BasModuleT *mod) {
} }
free(mod->procs); free(mod->procs);
free(mod->formVarInfo);
free(mod->debugVars); free(mod->debugVars);
if (mod->debugUdtDefs) { if (mod->debugUdtDefs) {
@ -451,10 +452,6 @@ void basModuleFree(BasModuleT *mod) {
free(mod->debugUdtDefs); free(mod->debugUdtDefs);
} }
if (mod->formVarInfo) {
free(mod->formVarInfo);
}
free(mod); free(mod);
} }

View file

@ -1144,14 +1144,26 @@ BasVmResultE basVmStep(BasVmT *vm) {
varSlot = &vm->globals[varIdx]; varSlot = &vm->globals[varIdx];
} }
// Increment: var = var + step // 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.
double varVal = basValToNumber(*varSlot); double varVal = basValToNumber(*varSlot);
double stepVal = basValToNumber(fs->step); double stepVal = basValToNumber(fs->step);
double limVal = basValToNumber(fs->limit); double limVal = basValToNumber(fs->limit);
uint8_t varType = varSlot->type;
varVal += stepVal; varVal += stepVal;
basValRelease(varSlot); basValRelease(varSlot);
if (varType == BAS_TYPE_INTEGER) {
*varSlot = basValInteger((int16_t)varVal);
} else if (varType == BAS_TYPE_LONG) {
*varSlot = basValLong((int32_t)varVal);
} else if (varType == BAS_TYPE_SINGLE) {
*varSlot = basValSingle((float)varVal);
} else {
*varSlot = basValDouble(varVal); *varSlot = basValDouble(varVal);
}
// Test: if step > 0 then continue while var <= limit // Test: if step > 0 then continue while var <= limit
// if step < 0 then continue while var >= limit // if step < 0 then continue while var >= limit
@ -1669,6 +1681,67 @@ BasVmResultE basVmStep(BasVmT *vm) {
case OP_MATH_RANDOMIZE: case OP_MATH_RANDOMIZE:
return execMath(vm, op); return execMath(vm, op);
case OP_RGB: {
// RGB(r, g, b) -> long color 0x00RRGGBB
BasValueT vb;
BasValueT vg;
BasValueT vr;
if (!pop(vm, &vb) || !pop(vm, &vg) || !pop(vm, &vr)) {
return BAS_VM_STACK_UNDERFLOW;
}
int32_t r = (int32_t)basValToNumber(vr);
int32_t g = (int32_t)basValToNumber(vg);
int32_t b = (int32_t)basValToNumber(vb);
basValRelease(&vr);
basValRelease(&vg);
basValRelease(&vb);
if (r < 0) { r = 0; }
if (r > 255) { r = 255; }
if (g < 0) { g = 0; }
if (g > 255) { g = 255; }
if (b < 0) { b = 0; }
if (b > 255) { b = 255; }
if (!push(vm, basValLong((r << 16) | (g << 8) | b))) {
return BAS_VM_STACK_OVERFLOW;
}
break;
}
case OP_GET_RED:
case OP_GET_GREEN:
case OP_GET_BLUE: {
BasValueT vc;
if (!pop(vm, &vc)) {
return BAS_VM_STACK_UNDERFLOW;
}
int32_t color = (int32_t)basValToNumber(vc);
basValRelease(&vc);
int32_t component;
if (op == OP_GET_RED) {
component = (color >> 16) & 0xFF;
} else if (op == OP_GET_GREEN) {
component = (color >> 8) & 0xFF;
} else {
component = color & 0xFF;
}
if (!push(vm, basValInteger((int16_t)component))) {
return BAS_VM_STACK_OVERFLOW;
}
break;
}
// ============================================================ // ============================================================
// Conversion built-ins // Conversion built-ins
// ============================================================ // ============================================================
@ -2887,11 +2960,12 @@ BasVmResultE basVmStep(BasVmT *vm) {
} }
case OP_MSGBOX: { case OP_MSGBOX: {
// Stack: [message, flags] — flags on top // Stack: [message, flags, title] -- title on top
BasValueT titleVal;
BasValueT flagsVal; BasValueT flagsVal;
BasValueT msgVal; BasValueT msgVal;
if (!pop(vm, &flagsVal) || !pop(vm, &msgVal)) { if (!pop(vm, &titleVal) || !pop(vm, &flagsVal) || !pop(vm, &msgVal)) {
return BAS_VM_STACK_UNDERFLOW; return BAS_VM_STACK_UNDERFLOW;
} }
@ -2900,10 +2974,13 @@ BasVmResultE basVmStep(BasVmT *vm) {
if (vm->ui.msgBox) { if (vm->ui.msgBox) {
BasValueT sv = basValToString(msgVal); BasValueT sv = basValToString(msgVal);
result = vm->ui.msgBox(vm->ui.ctx, sv.strVal->data, flags); BasValueT tv = basValToString(titleVal);
result = vm->ui.msgBox(vm->ui.ctx, sv.strVal->data, flags, tv.strVal->data);
basValRelease(&sv); basValRelease(&sv);
basValRelease(&tv);
} }
basValRelease(&titleVal);
basValRelease(&flagsVal); basValRelease(&flagsVal);
basValRelease(&msgVal); basValRelease(&msgVal);
push(vm, basValInteger((int16_t)result)); push(vm, basValInteger((int16_t)result));
@ -3766,13 +3843,13 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
switch (mode) { switch (mode) {
case FILE_MODE_INPUT: case FILE_MODE_INPUT:
modeStr = "r"; modeStr = "rb";
break; break;
case FILE_MODE_OUTPUT: case FILE_MODE_OUTPUT:
modeStr = "w"; modeStr = "wb";
break; break;
case FILE_MODE_APPEND: case FILE_MODE_APPEND:
modeStr = "a"; modeStr = "ab";
break; break;
case FILE_MODE_RANDOM: case FILE_MODE_RANDOM:
case FILE_MODE_BINARY: case FILE_MODE_BINARY:
@ -3836,6 +3913,10 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
} }
case OP_FILE_PRINT: { case OP_FILE_PRINT: {
// Writes one value without a trailing newline. The parser emits
// OP_FILE_WRITE_NL separately when the PRINT statement finishes
// without a trailing ; so multi-value PRINT with `;` separator
// works correctly.
BasValueT val; BasValueT val;
BasValueT channelVal; BasValueT channelVal;
@ -3857,7 +3938,6 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
if (s) { if (s) {
fputs(s->data, (FILE *)vm->files[channel].handle); fputs(s->data, (FILE *)vm->files[channel].handle);
fputc('\n', (FILE *)vm->files[channel].handle);
basStringUnref(s); basStringUnref(s);
} }
@ -3913,17 +3993,12 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
return BAS_VM_FILE_ERROR; return BAS_VM_FILE_ERROR;
} }
// Peek ahead to detect EOF before the next read
FILE *fp = (FILE *)vm->files[channel].handle; FILE *fp = (FILE *)vm->files[channel].handle;
int ch = fgetc(fp); long curPos = ftell(fp);
bool isEof; fseek(fp, 0, SEEK_END);
long endPos = ftell(fp);
if (ch == EOF) { fseek(fp, curPos, SEEK_SET);
isEof = true; bool isEof = (curPos >= endPos);
} else {
ungetc(ch, fp);
isEof = false;
}
if (!push(vm, basValBool(isEof))) { if (!push(vm, basValBool(isEof))) {
return BAS_VM_STACK_OVERFLOW; return BAS_VM_STACK_OVERFLOW;

View file

@ -124,7 +124,7 @@ typedef void (*BasUiShowFormFnT)(void *ctx, void *formRef, bool modal);
typedef void (*BasUiHideFormFnT)(void *ctx, void *formRef); typedef void (*BasUiHideFormFnT)(void *ctx, void *formRef);
// Display a message box. Returns the button clicked (1=OK, 6=Yes, 7=No, 2=Cancel). // Display a message box. Returns the button clicked (1=OK, 6=Yes, 7=No, 2=Cancel).
typedef int32_t (*BasUiMsgBoxFnT)(void *ctx, const char *message, int32_t flags); typedef int32_t (*BasUiMsgBoxFnT)(void *ctx, const char *message, int32_t flags, const char *title);
// Display an input box. Returns the entered string (empty on cancel). // Display an input box. Returns the entered string (empty on cancel).
typedef BasStringT *(*BasUiInputBoxFnT)(void *ctx, const char *prompt, const char *title, const char *defaultText); typedef BasStringT *(*BasUiInputBoxFnT)(void *ctx, const char *prompt, const char *title, const char *defaultText);

View file

@ -0,0 +1,667 @@
// bascomp.c -- DVX BASIC command-line compiler
//
// Compiles a .dbp project into a standalone .app file.
//
// Usage: BASCOMP project.dbp [-o output.app] [-release]
//
// The project file and all referenced source files (.bas, .frm)
// are loaded relative to the directory containing the .dbp file.
// The stub (basstub.app) is read from the same directory as the
// compiler executable.
#include "../compiler/compact.h"
#include "../compiler/lexer.h"
#include "../compiler/obfuscate.h"
#include "../compiler/parser.h"
#include "../compiler/strip.h"
#include "../compiler/opcodes.h"
#include "../runtime/vm.h"
#include "../runtime/values.h"
#include "../runtime/serialize.h"
#include "../../core/dvxRes.h"
#include "../../core/dvxPrefs.h"
#include "../../core/dvxTypes.h"
#include "../../tools/dvxResWrite.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// Limits
// ============================================================
#define MAX_FILES 64
#define MAX_PATH_LEN 260
#define MAX_NAME 64
// ============================================================
// Prototypes
// ============================================================
static void concatGrow(char **buf, int32_t *cap, int32_t need);
static char *readFile(const char *path, int32_t *outLen);
static const char *extractFormCode(const char *frmText);
static void usage(void);
// ============================================================
// extractFormCode -- skip past the form layout to the code section
// ============================================================
static void concatGrow(char **buf, int32_t *cap, int32_t need) {
while (*cap < need) {
*cap *= 2;
}
*buf = (char *)realloc(*buf, *cap);
}
static const char *extractFormCode(const char *frmText) {
if (!frmText) {
return NULL;
}
const char *p = frmText;
int32_t depth = 0;
bool inForm = false;
while (*p) {
// Skip leading whitespace
while (*p == ' ' || *p == '\t') { p++; }
if (strncasecmp(p, "Begin ", 6) == 0) {
if (!inForm && strncasecmp(p + 6, "Form ", 5) == 0) {
inForm = true;
}
depth++;
} else if (strncasecmp(p, "End", 3) == 0 && (p[3] == '\0' || p[3] == '\r' || p[3] == '\n' || p[3] == ' ')) {
depth--;
if (depth <= 0 && inForm) {
// Skip past this line
while (*p && *p != '\n') { p++; }
if (*p == '\n') { p++; }
return p;
}
}
// Skip to next line
while (*p && *p != '\n') { p++; }
if (*p == '\n') { p++; }
}
return NULL;
}
// ============================================================
// readFile
// ============================================================
static char *readFile(const char *path, int32_t *outLen) {
FILE *f = fopen(path, "rb");
if (!f) {
return NULL;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
char *buf = (char *)malloc(size + 1);
if (!buf) {
fclose(f);
return NULL;
}
int32_t n = (int32_t)fread(buf, 1, size, f);
fclose(f);
buf[n] = '\0';
if (outLen) {
*outLen = n;
}
return buf;
}
// ============================================================
// usage
// ============================================================
static void usage(void) {
fprintf(stderr, "DVX BASIC Compiler\n\n");
fprintf(stderr, "Usage: BASCOMP project.dbp [-o output.app] [-release]\n\n");
fprintf(stderr, " project.dbp DVX BASIC project file\n");
fprintf(stderr, " -o output.app Output file (default: project name + .app)\n");
fprintf(stderr, " -release Strip debug information\n");
}
// ============================================================
// main
// ============================================================
int main(int argc, char **argv) {
if (argc < 2) {
usage();
return 1;
}
const char *dbpPath = NULL;
const char *outputPath = NULL;
bool release = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
outputPath = argv[++i];
} else if (strcmp(argv[i], "-release") == 0) {
release = true;
} else if (argv[i][0] != '-') {
dbpPath = argv[i];
} else {
fprintf(stderr, "Unknown option: %s\n", argv[i]);
usage();
return 1;
}
}
if (!dbpPath) {
fprintf(stderr, "Error: no project file specified.\n");
usage();
return 1;
}
// Load the project file
PrefsHandleT *prefs = prefsLoad(dbpPath);
if (!prefs) {
fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath);
return 1;
}
// Derive project directory
char projectDir[MAX_PATH_LEN];
snprintf(projectDir, sizeof(projectDir), "%s", dbpPath);
char *sep = strrchr(projectDir, '/');
char *sep2 = strrchr(projectDir, '\\');
if (sep2 > sep) {
sep = sep2;
}
if (sep) {
*sep = '\0';
} else {
projectDir[0] = '.';
projectDir[1] = '\0';
}
// Read project metadata
const char *projName = prefsGetString(prefs, "Project", "Name", "App");
const char *author = prefsGetString(prefs, "Project", "Author", "");
const char *company = prefsGetString(prefs, "Project", "Company", "");
const char *version = prefsGetString(prefs, "Project", "Version", "");
const char *copyright = prefsGetString(prefs, "Project", "Copyright", "");
const char *description = prefsGetString(prefs, "Project", "Description", "");
const char *iconPath = prefsGetString(prefs, "Project", "Icon", "");
const char *helpFile = prefsGetString(prefs, "Project", "HelpFile", "");
const char *startupForm = prefsGetString(prefs, "Settings", "StartupForm", "");
(void)startupForm; // used implicitly by stub's basFormRtLoadAllForms
// Derive output path
char outBuf[MAX_PATH_LEN];
if (!outputPath) {
snprintf(outBuf, sizeof(outBuf), "%s/%s.app", projectDir, projName);
outputPath = outBuf;
}
printf("Project: %s\n", projName);
printf("Output: %s (%s)\n", outputPath, release ? "release" : "debug");
// Collect source files
typedef struct {
char path[MAX_PATH_LEN];
bool isForm;
} SrcFileT;
SrcFileT files[MAX_FILES];
int32_t fileCount = 0;
// Modules
for (int32_t i = 0; i < MAX_FILES; i++) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)i);
const char *val = prefsGetString(prefs, "Modules", key, NULL);
if (!val) {
break;
}
snprintf(files[fileCount].path, MAX_PATH_LEN, "%s/%s", projectDir, val);
files[fileCount].isForm = false;
fileCount++;
}
// Forms
for (int32_t i = 0; i < MAX_FILES; i++) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)i);
const char *val = prefsGetString(prefs, "Forms", key, NULL);
if (!val) {
break;
}
snprintf(files[fileCount].path, MAX_PATH_LEN, "%s/%s", projectDir, val);
files[fileCount].isForm = true;
fileCount++;
}
if (fileCount == 0) {
fprintf(stderr, "Error: project has no source files.\n");
prefsClose(prefs);
return 1;
}
// Concatenate sources (modules first, then form code)
int32_t concatCap = 8192;
char *concatBuf = (char *)malloc(concatCap);
if (!concatBuf) {
fprintf(stderr, "Error: out of memory.\n");
prefsClose(prefs);
return 1;
}
int32_t pos = 0;
// Pass 0: .bas modules, Pass 1: .frm code sections
for (int32_t pass = 0; pass < 2; pass++) {
for (int32_t i = 0; i < fileCount; i++) {
if (pass == 0 && files[i].isForm) { continue; }
if (pass == 1 && !files[i].isForm) { continue; }
int32_t srcLen = 0;
char *srcBuf = readFile(files[i].path, &srcLen);
if (!srcBuf) {
fprintf(stderr, "Error: cannot read %s\n", files[i].path);
free(concatBuf);
prefsClose(prefs);
return 1;
}
const char *code = srcBuf;
if (files[i].isForm) {
// Extract form name from "Begin Form <name>"
char formName[MAX_NAME] = "";
const char *bp = srcBuf;
while (*bp) {
while (*bp == ' ' || *bp == '\t') { bp++; }
if (strncasecmp(bp, "Begin Form ", 11) == 0) {
bp += 11;
while (*bp == ' ' || *bp == '\t') { bp++; }
int32_t ni = 0;
while (*bp && *bp != ' ' && *bp != '\t' && *bp != '\r' && *bp != '\n' && ni < MAX_NAME - 1) {
formName[ni++] = *bp++;
}
formName[ni] = '\0';
break;
}
while (*bp && *bp != '\n') { bp++; }
if (*bp == '\n') { bp++; }
}
code = extractFormCode(srcBuf);
if (!code) {
code = "";
}
int32_t codeLen = (int32_t)strlen(code);
concatGrow(&concatBuf, &concatCap, pos + codeLen + 128);
// Inject BEGINFORM directive before form code
if (formName[0]) {
pos += snprintf(concatBuf + pos, concatCap - pos, "BEGINFORM \"%s\"\n", formName);
}
memcpy(concatBuf + pos, code, codeLen);
pos += codeLen;
if (pos > 0 && concatBuf[pos - 1] != '\n') {
concatBuf[pos++] = '\n';
}
// Inject ENDFORM directive after form code
if (formName[0]) {
pos += snprintf(concatBuf + pos, concatCap - pos, "ENDFORM\n");
}
} else {
int32_t codeLen = (int32_t)strlen(code);
concatGrow(&concatBuf, &concatCap, pos + codeLen + 2);
memcpy(concatBuf + pos, code, codeLen);
pos += codeLen;
if (pos > 0 && concatBuf[pos - 1] != '\n') {
concatBuf[pos++] = '\n';
}
}
free(srcBuf);
}
}
concatBuf[pos] = '\0';
// Compile
printf("Compiling %d file(s)...\n", (int)fileCount);
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
if (!parser) {
fprintf(stderr, "Error: out of memory.\n");
free(concatBuf);
prefsClose(prefs);
return 1;
}
basParserInit(parser, concatBuf, pos);
if (!basParse(parser)) {
fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error);
basParserFree(parser);
free(parser);
free(concatBuf);
prefsClose(prefs);
return 1;
}
free(concatBuf);
BasModuleT *mod = basParserBuildModule(parser);
basParserFree(parser);
free(parser);
if (!mod) {
fprintf(stderr, "Error: failed to build module.\n");
prefsClose(prefs);
return 1;
}
printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount);
// Strip for release
if (release) {
basStripModule(mod);
printf(" stripped debug info\n");
}
// Read all .frm texts up front; they're used for obfuscation and
// then embedded as FORM0, FORM1, ... resources below.
int32_t frmCount = 0;
char *frmData[MAX_FILES];
int32_t frmLens[MAX_FILES];
int32_t frmFileIdx[MAX_FILES]; // index into files[] for this frm
for (int32_t i = 0; i < fileCount; i++) {
if (!files[i].isForm) {
continue;
}
int32_t flen = 0;
char *fdata = readFile(files[i].path, &flen);
if (!fdata) {
continue;
}
// Strip comments from the .frm text unconditionally. Comments
// are source-only; they shouldn't ship in the embedded resource
// for either debug or release builds.
int32_t stripCap = flen + 16;
uint8_t *stripped = (uint8_t *)malloc(stripCap);
if (!stripped) {
free(fdata);
continue;
}
int32_t strippedLen = basStripFrmComments(fdata, flen, stripped, stripCap);
free(fdata);
frmData[frmCount] = (char *)stripped;
frmLens[frmCount] = strippedLen;
frmFileIdx[frmCount] = i;
frmCount++;
}
// Obfuscate form/control names in release mode
BasObfFrmT obfFrms[MAX_FILES];
for (int32_t i = 0; i < MAX_FILES; i++) {
obfFrms[i].data = NULL;
obfFrms[i].len = 0;
}
if (release && frmCount > 0) {
const char *frmTexts[MAX_FILES];
for (int32_t i = 0; i < frmCount; i++) {
frmTexts[i] = frmData[i];
}
basObfuscateNames(mod, frmTexts, frmLens, frmCount, obfFrms);
printf(" obfuscated %d form(s)\n", (int)frmCount);
}
// Remove OP_LINE instructions and compact the bytecode.
if (release) {
int32_t removed = basCompactBytecode(mod);
if (removed > 0) {
printf(" compacted bytecode (-%d bytes)\n", (int)removed);
}
}
// Serialize module
int32_t modLen = 0;
uint8_t *modData = basModuleSerialize(mod, &modLen);
if (!modData) {
fprintf(stderr, "Error: failed to serialize module.\n");
basModuleFree(mod);
prefsClose(prefs);
return 1;
}
// Serialize debug info
int32_t dbgLen = 0;
uint8_t *dbgData = NULL;
if (!release) {
dbgData = basDebugSerialize(mod, &dbgLen);
}
basModuleFree(mod);
// Find the stub -- look in the compiler's own directory
char compilerDir[MAX_PATH_LEN];
snprintf(compilerDir, sizeof(compilerDir), "%s", argv[0]);
sep = strrchr(compilerDir, '/');
sep2 = strrchr(compilerDir, '\\');
if (sep2 > sep) {
sep = sep2;
}
if (sep) {
*sep = '\0';
} else {
compilerDir[0] = '.';
compilerDir[1] = '\0';
}
char stubPath[MAX_PATH_LEN];
snprintf(stubPath, sizeof(stubPath), "%s/BASSTUB.APP", compilerDir);
int32_t stubLen = 0;
char *stubData = readFile(stubPath, &stubLen);
if (!stubData) {
fprintf(stderr, "Error: cannot find stub at %s\n", stubPath);
free(modData);
free(dbgData);
prefsClose(prefs);
return 1;
}
// Write stub to output file
FILE *out = fopen(outputPath, "wb");
if (!out) {
fprintf(stderr, "Error: cannot create %s\n", outputPath);
free(stubData);
free(modData);
free(dbgData);
prefsClose(prefs);
return 1;
}
fwrite(stubData, 1, stubLen, out);
fclose(out);
free(stubData);
// Attach resources
dvxResAppendEntry(outputPath, "name", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1);
if (author[0]) { dvxResAppendEntry(outputPath, "author", DVX_RES_TEXT, author, (uint32_t)strlen(author) + 1); }
if (company[0]) { dvxResAppendEntry(outputPath, "company", DVX_RES_TEXT, company, (uint32_t)strlen(company) + 1); }
if (version[0]) { dvxResAppendEntry(outputPath, "version", DVX_RES_TEXT, version, (uint32_t)strlen(version) + 1); }
if (copyright[0]) { dvxResAppendEntry(outputPath, "copyright", DVX_RES_TEXT, copyright, (uint32_t)strlen(copyright) + 1); }
if (description[0]) { dvxResAppendEntry(outputPath, "description", DVX_RES_TEXT, description, (uint32_t)strlen(description) + 1); }
// Icon
if (iconPath[0]) {
char iconFullPath[MAX_PATH_LEN];
snprintf(iconFullPath, sizeof(iconFullPath), "%s/%s", projectDir, iconPath);
int32_t iconLen = 0;
char *iconData = readFile(iconFullPath, &iconLen);
if (iconData) {
dvxResAppendEntry(outputPath, "icon32", DVX_RES_ICON, iconData, (uint32_t)iconLen);
free(iconData);
}
}
// Help file name (file is expected alongside the .app)
if (helpFile[0]) {
const char *helpBase = helpFile;
const char *hs = strrchr(helpBase, '/');
const char *hs2 = strrchr(helpBase, '\\');
if (hs2 > hs) {
hs = hs2;
}
if (hs) {
helpBase = hs + 1;
}
dvxResAppendEntry(outputPath, "helpfile", DVX_RES_TEXT, helpBase, (uint32_t)strlen(helpBase) + 1);
// Copy help file to output directory
char helpSrc[MAX_PATH_LEN];
snprintf(helpSrc, sizeof(helpSrc), "%s/%s", projectDir, helpFile);
char outDir[MAX_PATH_LEN];
snprintf(outDir, sizeof(outDir), "%s", outputPath);
char *outSep = strrchr(outDir, '/');
char *outSep2 = strrchr(outDir, '\\');
if (outSep2 > outSep) {
outSep = outSep2;
}
if (outSep) {
*outSep = '\0';
} else {
outDir[0] = '.';
outDir[1] = '\0';
}
char helpDst[MAX_PATH_LEN];
snprintf(helpDst, sizeof(helpDst), "%s/%s", outDir, helpBase);
int32_t hLen = 0;
char *hData = readFile(helpSrc, &hLen);
if (hData) {
FILE *hf = fopen(helpDst, "wb");
if (hf) {
fwrite(hData, 1, hLen, hf);
fclose(hf);
}
free(hData);
}
}
// MODULE resource
dvxResAppendEntry(outputPath, "MODULE", DVX_RES_BINARY, modData, (uint32_t)modLen);
free(modData);
// DEBUG resource
if (dbgData) {
dvxResAppendEntry(outputPath, "DEBUG", DVX_RES_BINARY, dbgData, (uint32_t)dbgLen);
free(dbgData);
}
// Form resources -- use obfuscated bytes in release mode, raw otherwise
for (int32_t fi = 0; fi < frmCount; fi++) {
char resName[16];
snprintf(resName, sizeof(resName), "FORM%d", (int)fi);
if (release && obfFrms[fi].data) {
dvxResAppendEntry(outputPath, resName, DVX_RES_BINARY, obfFrms[fi].data, (uint32_t)obfFrms[fi].len);
} else {
dvxResAppendEntry(outputPath, resName, DVX_RES_BINARY, frmData[fi], (uint32_t)frmLens[fi]);
}
}
// Free .frm buffers
for (int32_t i = 0; i < frmCount; i++) {
free(frmData[i]);
free(obfFrms[i].data);
}
(void)frmFileIdx; // unused (kept in case of future per-file metadata)
prefsClose(prefs);
printf("Created %s (%d bytes)\n", outputPath, (int)stubLen + (int)modLen);
return 0;
}

View file

@ -92,7 +92,7 @@ int32_t appMain(DxeAppContextT *ctx) {
// Read app name and update the shell's app record // Read app name and update the shell's app record
uint32_t nameSize = 0; uint32_t nameSize = 0;
char *appName = (char *)dvxResRead(res, "APPNAME", &nameSize); char *appName = (char *)dvxResRead(res, "name", &nameSize);
if (appName) { if (appName) {
ShellAppT *app = shellGetApp(ctx->appId); ShellAppT *app = shellGetApp(ctx->appId);
@ -104,6 +104,15 @@ int32_t appMain(DxeAppContextT *ctx) {
free(appName); free(appName);
} }
// Set help file path if present
uint32_t helpNameSize = 0;
char *helpName = (char *)dvxResRead(res, "helpfile", &helpNameSize);
if (helpName) {
snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s%c%s", ctx->appDir, DVX_PATH_SEP, helpName);
free(helpName);
}
// Load MODULE resource // Load MODULE resource
uint32_t modSize = 0; uint32_t modSize = 0;
uint8_t *modData = (uint8_t *)dvxResRead(res, "MODULE", &modSize); uint8_t *modData = (uint8_t *)dvxResRead(res, "MODULE", &modSize);

View file

@ -0,0 +1,326 @@
// test_compact.c -- verify that strip + compact produces identical output
//
// Compiles each test program, runs it once without compaction and once
// with strip + compact applied, and compares captured PRINT output.
// Exits nonzero if any mismatch is found.
#include "compiler/parser.h"
#include "compiler/strip.h"
#include "compiler/compact.h"
#include "runtime/vm.h"
#include "runtime/values.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_OUT 65536
typedef struct {
char *buf;
int32_t len;
int32_t cap;
} OutBufT;
static void captureCallback(void *ctx, const char *text, bool newline) {
OutBufT *ob = (OutBufT *)ctx;
int32_t tlen = (int32_t)strlen(text);
if (ob->len + tlen + 2 >= ob->cap) {
return;
}
memcpy(ob->buf + ob->len, text, tlen);
ob->len += tlen;
if (newline) {
ob->buf[ob->len++] = '\n';
}
}
static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int32_t outCap) {
BasParserT parser;
basParserInit(&parser, source, (int32_t)strlen(source));
if (!basParse(&parser)) {
fprintf(stderr, "compile error: %s\n", parser.error);
basParserFree(&parser);
return -1;
}
BasModuleT *mod = basParserBuildModule(&parser);
basParserFree(&parser);
if (!mod) {
return -1;
}
if (compact) {
basStripModule(mod);
basCompactBytecode(mod);
}
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
OutBufT ob;
ob.buf = outBuf;
ob.len = 0;
ob.cap = outCap;
basVmSetPrintCallback(vm, captureCallback, &ob);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
BasVmResultE result = basVmRun(vm);
if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
fprintf(stderr, "vm error %d: %s\n", result, basVmGetError(vm));
}
basVmDestroy(vm);
basModuleFree(mod);
outBuf[ob.len] = '\0';
return ob.len;
}
static int32_t sTotal = 0;
static int32_t sFailed = 0;
static void testCompact(const char *name, const char *source) {
sTotal++;
static char orig[MAX_OUT];
static char comp[MAX_OUT];
int32_t origLen = runAndCapture(source, false, orig, MAX_OUT);
int32_t compLen = runAndCapture(source, true, comp, MAX_OUT);
if (origLen < 0 || compLen < 0) {
printf("FAIL: %s (runtime error)\n", name);
sFailed++;
return;
}
if (origLen != compLen || memcmp(orig, comp, origLen) != 0) {
printf("FAIL: %s\n", name);
printf(" uncompacted (%d bytes):\n%s\n", (int)origLen, orig);
printf(" compacted (%d bytes):\n%s\n", (int)compLen, comp);
sFailed++;
return;
}
printf("PASS: %s\n", name);
}
int main(void) {
printf("DVX BASIC Bytecode Compaction Tests\n");
printf("====================================\n\n");
basStringSystemInit();
// ---- Basic control flow ----
testCompact("FOR loop",
"DIM i AS INTEGER\n"
"FOR i = 1 TO 5\n"
" PRINT i;\n"
"NEXT i\n"
"PRINT\n"
);
testCompact("Nested FOR",
"DIM i AS INTEGER\n"
"DIM j AS INTEGER\n"
"FOR i = 1 TO 3\n"
" FOR j = 1 TO 2\n"
" PRINT i * 10 + j;\n"
" NEXT j\n"
"NEXT i\n"
"PRINT\n"
);
testCompact("FOR with negative STEP",
"DIM i AS INTEGER\n"
"FOR i = 5 TO 1 STEP -1\n"
" PRINT i;\n"
"NEXT i\n"
"PRINT\n"
);
testCompact("EXIT FOR",
"DIM i AS INTEGER\n"
"FOR i = 1 TO 100\n"
" IF i = 4 THEN EXIT FOR\n"
" PRINT i;\n"
"NEXT i\n"
"PRINT\n"
);
testCompact("DO WHILE",
"DIM n AS INTEGER\n"
"n = 0\n"
"DO WHILE n < 3\n"
" PRINT n;\n"
" n = n + 1\n"
"LOOP\n"
"PRINT\n"
);
testCompact("IF THEN ELSE",
"DIM x AS INTEGER\n"
"x = 5\n"
"IF x > 3 THEN\n"
" PRINT \"big\"\n"
"ELSE\n"
" PRINT \"small\"\n"
"END IF\n"
);
testCompact("SELECT CASE",
"DIM g AS STRING\n"
"g = \"B\"\n"
"SELECT CASE g\n"
" CASE \"A\"\n"
" PRINT \"A\"\n"
" CASE \"B\", \"C\"\n"
" PRINT \"BC\"\n"
" CASE ELSE\n"
" PRINT \"?\"\n"
"END SELECT\n"
);
// ---- GOSUB: exercises the absolute address in OP_PUSH_INT32 pattern ----
testCompact("GOSUB",
"DIM n AS INTEGER\n"
"n = 10\n"
"GOSUB doubler\n"
"PRINT n\n"
"GOSUB doubler\n"
"PRINT n\n"
"END\n"
"doubler:\n"
"n = n * 2\n"
"RETURN\n"
);
testCompact("Multiple GOSUBs",
"DIM x AS INTEGER\n"
"x = 1\n"
"GOSUB a\n"
"GOSUB b\n"
"GOSUB c\n"
"PRINT x\n"
"END\n"
"a:\n"
"x = x + 10\n"
"RETURN\n"
"b:\n"
"x = x + 100\n"
"RETURN\n"
"c:\n"
"x = x + 1000\n"
"RETURN\n"
);
// ---- SUB / FUNCTION: exercises absolute CALL addresses + proc table ----
testCompact("SUB with CALL",
"CALL Greet\n"
"CALL Greet\n"
"CALL Greet\n"
"SUB Greet\n"
" PRINT \"Hello\"\n"
"END SUB\n"
);
testCompact("FUNCTION return value",
"DECLARE FUNCTION Square(x AS INTEGER) AS INTEGER\n"
"DIM r AS INTEGER\n"
"r = Square(5)\n"
"PRINT r\n"
"r = Square(7)\n"
"PRINT r\n"
"FUNCTION Square(x AS INTEGER) AS INTEGER\n"
" Square = x * x\n"
"END FUNCTION\n"
);
testCompact("Recursive FUNCTION",
"DECLARE FUNCTION Fact(n AS INTEGER) AS INTEGER\n"
"PRINT Fact(5)\n"
"FUNCTION Fact(n AS INTEGER) AS INTEGER\n"
" IF n <= 1 THEN\n"
" Fact = 1\n"
" ELSE\n"
" Fact = n * Fact(n - 1)\n"
" END IF\n"
"END FUNCTION\n"
);
// ---- ON ERROR: exercises the relative int16 offset path with non-zero ----
testCompact("ON ERROR GOTO",
"ON ERROR GOTO handler\n"
"PRINT 10 / 0\n"
"PRINT \"unreached\"\n"
"END\n"
"handler:\n"
"PRINT \"caught\"\n"
);
// ---- Mixed: many OP_LINE between jumps ----
testCompact("Long function with many lines",
"DIM total AS INTEGER\n"
"total = 0\n"
"DIM i AS INTEGER\n"
"FOR i = 1 TO 10\n"
" IF i MOD 2 = 0 THEN\n"
" total = total + i\n"
" ELSE\n"
" total = total + i * 2\n"
" END IF\n"
"NEXT i\n"
"PRINT total\n"
);
testCompact("GOSUB inside FOR",
"DIM i AS INTEGER\n"
"DIM sum AS INTEGER\n"
"sum = 0\n"
"FOR i = 1 TO 5\n"
" GOSUB addit\n"
"NEXT i\n"
"PRINT sum\n"
"END\n"
"addit:\n"
"sum = sum + i\n"
"RETURN\n"
);
testCompact("FOR inside SUB",
"CALL Loop5\n"
"SUB Loop5\n"
" DIM k AS INTEGER\n"
" FOR k = 0 TO 4\n"
" PRINT k;\n"
" NEXT k\n"
" PRINT\n"
"END SUB\n"
);
printf("\n%d/%d tests passed\n", (int)(sTotal - sFailed), (int)sTotal);
return sFailed > 0 ? 1 : 0;
}

View file

@ -900,6 +900,107 @@ int main(void) {
printf("\n"); printf("\n");
} }
// Regression test: App.Data must parse even though Data is a BASIC keyword
runProgram("App.Data parses",
"DIM p AS STRING\n"
"p = App.Data\n"
"PRINT p\n"
);
// Regression test: RGB(r,g,b) packs into 0x00RRGGBB
runProgram("RGB packing",
"PRINT RGB(255, 128, 0)\n"
"PRINT RGB(0, 255, 0)\n"
"PRINT RGB(0, 0, 255)\n"
);
// Expected: 16744448 (0xFF8000), 65280 (0x00FF00), 255 (0x0000FF)
// Regression test: GetRed/Green/Blue round-trip
runProgram("RGB round-trip",
"DIM c AS LONG\n"
"c = RGB(17, 99, 200)\n"
"PRINT GetRed(c); GetGreen(c); GetBlue(c)\n"
);
// Expected: 17 99 200
// Regression test: MsgBox function accepts optional title (3rd arg)
// The test harness's default msgBox callback returns 1; we just verify
// the program compiles and runs.
runProgram("MsgBox 3-arg compile",
"DIM r AS INTEGER\n"
"r = MsgBox(\"hi\", 0, \"My Title\")\n"
"PRINT \"ok\"\n"
);
// Regression test: CONST accepts AS type
runProgram("CONST AS type",
"CONST PI AS DOUBLE = 3.14159\n"
"CONST N AS INTEGER = 42\n"
"PRINT PI\n"
"PRINT N\n"
);
// Regression test: PRINT #ch with ; separator
runProgram("PRINT # with semicolon",
"OPEN \"/tmp/dvxbasic_psemi.txt\" FOR OUTPUT AS #1\n"
"PRINT #1, \"Line one\"\n"
"PRINT #1, \"ans = \"; 42\n"
"PRINT #1, \"x\"; \"y\"; \"z\"\n"
"CLOSE #1\n"
"DIM s AS STRING\n"
"OPEN \"/tmp/dvxbasic_psemi.txt\" FOR INPUT AS #1\n"
"DO WHILE NOT EOF(1)\n"
" LINE INPUT #1, s\n"
" PRINT s\n"
"LOOP\n"
"CLOSE #1\n"
);
// Expected: Line one / ans = 42 / xyz
// Regression test: IniRead$ with $ suffix must tokenize as keyword
runProgram("IniRead$ tokenizes",
"DIM v AS STRING\n"
"v = IniRead$(\"/tmp/nofile.ini\", \"S\", \"K\", \"default\")\n"
"PRINT v\n"
);
// Expected: default (file doesn't exist)
// Regression test: SUB extern call without parens must emit OP_CALL_EXTERN
// (previously emitted OP_CALL which called nothing, because no internal
// proc at address 0 existed for the extern.)
{
printf("=== DECLARE LIBRARY SUB no-parens ===\n");
const char *src =
"DECLARE LIBRARY \"basrt\"\n"
" DECLARE SUB DoIt(BYVAL s AS STRING)\n"
"END DECLARE\n"
"DoIt \"hello\"\n";
int32_t len = (int32_t)strlen(src);
BasParserT parser;
basParserInit(&parser, src, len);
bool ok = basParse(&parser);
if (!ok) {
printf("COMPILE ERROR: %s\n", parser.error);
} else {
BasModuleT *mod = basParserBuildModule(&parser);
bool found = false;
for (int32_t i = 0; i < mod->codeLen; i++) {
if (mod->code[i] == OP_CALL_EXTERN) {
found = true;
break;
}
}
printf(found ? "PASS: OP_CALL_EXTERN emitted\n" : "FAIL: OP_CALL_EXTERN not emitted\n");
basModuleFree(mod);
}
basParserFree(&parser);
printf("\n");
}
// Test: Procedure table populated for SUBs and FUNCTIONs // Test: Procedure table populated for SUBs and FUNCTIONs
{ {
printf("=== Procedure table ===\n"); printf("=== Procedure table ===\n");
@ -1213,6 +1314,56 @@ int main(void) {
); );
// Expected: 1 2 / after // Expected: 1 2 / after
// Regression test: FOR loop inside a SUB with local loop variable
runProgram("FOR inside SUB with local",
"CALL MySub\n"
"SUB MySub\n"
" DIM i AS LONG\n"
" FOR i = 0 TO 3\n"
" PRINT i;\n"
" NEXT i\n"
" PRINT\n"
"END SUB\n"
);
// Expected: 0 1 2 3
// Regression test: resedit pattern -- FOR in SUB with n = FUNC(...)
runProgram("FOR with func-returned limit",
"CALL DoIt\n"
"SUB DoIt\n"
" DIM n AS LONG\n"
" n = GetCount()\n"
" DIM i AS LONG\n"
" FOR i = 0 TO n - 1\n"
" PRINT i;\n"
" NEXT i\n"
" PRINT\n"
"END SUB\n"
"FUNCTION GetCount() AS LONG\n"
" GetCount = 4\n"
"END FUNCTION\n"
);
// Expected: 0 1 2 3
// Regression test: resedit pattern -- FOR in SUB inside a FORM
runProgram("FOR in SUB inside FORM",
"BEGINFORM \"Form1\"\n"
"DIM formVar AS LONG\n"
"formVar = 42\n"
"CALL DoIt\n"
"SUB DoIt\n"
" DIM n AS LONG\n"
" n = 4\n"
" DIM ix AS LONG\n"
" FOR ix = 0 TO n - 1\n"
" PRINT ix;\n"
" NEXT ix\n"
" PRINT\n"
"END SUB\n"
"ENDFORM\n"
);
// Expected: 0 1 2 3
runProgram("EXIT DO", runProgram("EXIT DO",
"DIM n AS INTEGER\n" "DIM n AS INTEGER\n"
"n = 0\n" "n = 0\n"

View file

@ -153,11 +153,11 @@ static void onCloseMainCb(WindowT *win) {
static const FileFilterT sFileFilters[] = { static const FileFilterT sFileFilters[] = {
{"All Files (*.*)", "*.*"}, {"All Files (*.*)"},
{"Text Files (*.txt)", "*.txt"}, {"Text Files (*.txt)"},
{"Batch Files (*.bat)", "*.bat"}, {"Batch Files (*.bat)"},
{"Executables (*.exe)", "*.exe"}, {"Executables (*.exe)"},
{"Bitmap Files (*.bmp)", "*.bmp"} {"Bitmap Files (*.bmp)"}
}; };
static void onMenuCb(WindowT *win, int32_t menuId) { static void onMenuCb(WindowT *win, int32_t menuId) {

View file

@ -1,5 +1,5 @@
# dvxdemo.res -- Resource manifest for DVX Demo # dvxdemo.res -- Resource manifest for DVX Demo
icon32 icon dvxdemo/icon32.bmp icon32 icon icon32.bmp
name text "DVX Demo" name text "DVX Demo"
author text "DVX Project" author text "DVX Project"
description text "Widget toolkit demonstration" description text "Widget toolkit demonstration"

View file

@ -1,5 +1,5 @@
# dvxhelp.res -- Resource manifest for DVX Help Viewer # dvxhelp.res -- Resource manifest for DVX Help Viewer
icon32 icon dvxhelp/icon32.bmp icon32 icon icon32.bmp
name text "DVX Help" name text "DVX Help"
author text "DVX Project" author text "DVX Project"
description text "Help file viewer" description text "Help file viewer"

View file

@ -328,8 +328,8 @@ static void onResize(WindowT *win, int32_t contentW, int32_t contentH) {
static void openFile(void) { static void openFile(void) {
FileFilterT filters[] = { FileFilterT filters[] = {
{ "Images (*.bmp;*.jpg;*.png;*.gif)", "*.bmp;*.jpg;*.png;*.gif" }, { "Images (*.bmp;*.jpg;*.png;*.gif)" },
{ "All Files (*.*)", "*.*" } { "All Files (*.*)" }
}; };
char path[DVX_MAX_PATH]; char path[DVX_MAX_PATH];

View file

@ -1,5 +1,5 @@
# imgview.res -- Resource manifest for Image Viewer # imgview.res -- Resource manifest for Image Viewer
icon32 icon imgview/icon32.bmp icon32 icon icon32.bmp
name text "Image Viewer" name text "Image Viewer"
author text "DVX Project" author text "DVX Project"
description text "BMP, PNG, JPEG, and GIF viewer" description text "BMP, PNG, JPEG, and GIF viewer"

View file

@ -150,8 +150,8 @@ static void doOpen(void) {
} }
FileFilterT filters[] = { FileFilterT filters[] = {
{ "Text Files (*.txt)", "*.txt" }, { "Text Files (*.txt)" },
{ "All Files (*.*)", "*.*" } { "All Files (*.*)" }
}; };
char path[DVX_MAX_PATH]; char path[DVX_MAX_PATH];
@ -236,8 +236,8 @@ static void doSave(void) {
static void doSaveAs(void) { static void doSaveAs(void) {
FileFilterT filters[] = { FileFilterT filters[] = {
{ "Text Files (*.txt)", "*.txt" }, { "Text Files (*.txt)" },
{ "All Files (*.*)", "*.*" } { "All Files (*.*)" }
}; };
char path[DVX_MAX_PATH]; char path[DVX_MAX_PATH];

View file

@ -1,5 +1,5 @@
# notepad.res -- Resource manifest for Notepad # notepad.res -- Resource manifest for Notepad
icon32 icon notepad/icon32.bmp icon32 icon icon32.bmp
name text "Notepad" name text "Notepad"
author text "DVX Project" author text "DVX Project"
description text "Simple text editor" description text "Simple text editor"

View file

@ -73,6 +73,7 @@
#define CMD_TILE_V 203 #define CMD_TILE_V 203
#define CMD_MIN_ON_RUN 104 #define CMD_MIN_ON_RUN 104
#define CMD_RESTORE_ALONE 105 #define CMD_RESTORE_ALONE 105
#define CMD_RELOAD 106
#define CMD_ABOUT 300 #define CMD_ABOUT 300
#define CMD_TASK_MGR 301 #define CMD_TASK_MGR 301
#define CMD_SYSINFO 302 #define CMD_SYSINFO 302
@ -166,6 +167,7 @@ static void buildPmWindow(void) {
MenuBarT *menuBar = wmAddMenuBar(sPmWindow); MenuBarT *menuBar = wmAddMenuBar(sPmWindow);
MenuT *fileMenu = wmAddMenu(menuBar, "&File"); MenuT *fileMenu = wmAddMenu(menuBar, "&File");
wmAddMenuItem(fileMenu, "&Run...", CMD_RUN); wmAddMenuItem(fileMenu, "&Run...", CMD_RUN);
wmAddMenuItem(fileMenu, "Re&load", CMD_RELOAD);
wmAddMenuSeparator(fileMenu); wmAddMenuSeparator(fileMenu);
wmAddMenuItem(fileMenu, "E&xit DVX", CMD_EXIT); wmAddMenuItem(fileMenu, "E&xit DVX", CMD_EXIT);
@ -313,8 +315,8 @@ static void onPmMenu(WindowT *win, int32_t menuId) {
case CMD_RUN: case CMD_RUN:
{ {
FileFilterT filters[] = { FileFilterT filters[] = {
{ "Applications (*.app)", "*.app" }, { "Applications (*.app)" },
{ "All Files (*.*)", "*.*" } { "All Files (*.*)" }
}; };
char path[MAX_PATH_LEN]; char path[MAX_PATH_LEN];
@ -329,6 +331,14 @@ static void onPmMenu(WindowT *win, int32_t menuId) {
} }
break; break;
case CMD_RELOAD:
dvxDestroyWindow(sAc, sPmWindow);
sPmWindow = NULL;
sStatusLabel = NULL;
scanAppsDir();
buildPmWindow();
break;
case CMD_EXIT: case CMD_EXIT:
onPmClose(sPmWindow); onPmClose(sPmWindow);
break; break;

View file

@ -11,12 +11,14 @@ bpp = 16
; Mouse settings. ; Mouse settings.
; wheel: normal or reversed ; wheel: normal or reversed
; wheelspeed: lines per wheel notch (1-10, default 3)
; doubleclick: double-click speed in milliseconds (200-900, default 500) ; doubleclick: double-click speed in milliseconds (200-900, default 500)
; acceleration: off, low, medium, high (default medium) ; acceleration: off, low, medium, high (default medium)
; speed: cursor speed (2-32, default 8; higher = faster) ; speed: cursor speed (2-32, default 8; higher = faster)
[mouse] [mouse]
wheel = normal wheel = normal
wheelspeed = 3
doubleclick = 500 doubleclick = 500
acceleration = medium acceleration = medium
speed = 8 speed = 8

View file

@ -1277,8 +1277,12 @@ static void dispatchEvents(AppContextT *ctx) {
return; return;
} }
// Handle left button press // Handle left button press. Consume the edge immediately so that if
// the click handler calls back into the event loop (e.g., BASIC's
// DoEvents yielding while a QueryUnload handler runs), the same
// physical click isn't re-seen as a fresh press.
if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) { if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) {
ctx->prevMouseButtons |= MOUSE_LEFT;
handleMouseButton(ctx, mx, my, buttons); handleMouseButton(ctx, mx, my, buttons);
} }
@ -1289,6 +1293,7 @@ static void dispatchEvents(AppContextT *ctx) {
// that apply to all their children without requiring each child to have // that apply to all their children without requiring each child to have
// its own menu, while still allowing per-widget overrides. // its own menu, while still allowing per-widget overrides.
if ((buttons & MOUSE_RIGHT) && !(prevBtn & MOUSE_RIGHT)) { if ((buttons & MOUSE_RIGHT) && !(prevBtn & MOUSE_RIGHT)) {
ctx->prevMouseButtons |= MOUSE_RIGHT;
int32_t hitPart; int32_t hitPart;
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart); int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
@ -1396,7 +1401,7 @@ static void dispatchEvents(AppContextT *ctx) {
if (target && wclsHas(target, WGT_METHOD_ON_KEY)) { if (target && wclsHas(target, WGT_METHOD_ON_KEY)) {
int32_t delta = ctx->mouseWheel * ctx->wheelDirection; int32_t delta = ctx->mouseWheel * ctx->wheelDirection;
int32_t arrowKey = (delta > 0) ? (0x50 | 0x100) : (0x48 | 0x100); int32_t arrowKey = (delta > 0) ? (0x50 | 0x100) : (0x48 | 0x100);
int32_t steps = abs(delta) * MOUSE_WHEEL_STEP; int32_t steps = abs(delta) * ctx->wheelStep;
for (int32_t s = 0; s < steps; s++) { for (int32_t s = 0; s < steps; s++) {
wclsOnKey(target, arrowKey, 0); wclsOnKey(target, arrowKey, 0);
@ -1423,7 +1428,7 @@ static void dispatchEvents(AppContextT *ctx) {
if (sb) { if (sb) {
int32_t oldValue = sb->value; int32_t oldValue = sb->value;
sb->value += ctx->mouseWheel * ctx->wheelDirection * MOUSE_WHEEL_STEP; sb->value += ctx->mouseWheel * ctx->wheelDirection * ctx->wheelStep;
if (sb->value < sb->min) { if (sb->value < sb->min) {
sb->value = sb->min; sb->value = sb->min;
@ -1721,11 +1726,18 @@ static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) {
break; break;
case SysMenuCloseE: case SysMenuCloseE:
// Sys menu is already closed. Composite a clean frame first
// so the menu's pixels are erased before onClose runs -- the
// close handler may open a modal dialog and we don't want the
// sys menu popup peeking out from under it.
compositeAndFlush(ctx);
if (win->onClose) { if (win->onClose) {
WIN_CALLBACK(ctx, win, win->onClose(win)); WIN_CALLBACK(ctx, win, win->onClose(win));
} else { } else {
dvxDestroyWindow(ctx, win); dvxDestroyWindow(ctx, win);
} }
break; break;
} }
} }
@ -1872,11 +1884,20 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t
ctx->lastCloseClickId = -1; ctx->lastCloseClickId = -1;
closeSysMenu(ctx); closeSysMenu(ctx);
// Composite a clean frame first so the sys menu is
// fully gone before onClose runs -- the close handler
// may open a modal dialog and we don't want the menu
// peeking out from under it.
compositeAndFlush(ctx);
if (win->onClose) { if (win->onClose) {
WIN_CALLBACK(ctx, win, win->onClose(win)); WIN_CALLBACK(ctx, win, win->onClose(win));
} else { } else {
dvxDestroyWindow(ctx, win); dvxDestroyWindow(ctx, win);
} }
// Ensure sys menu is closed even if onClose re-opened it
closeSysMenu(ctx);
} else { } else {
ctx->lastCloseClickTime = now; ctx->lastCloseClickTime = now;
ctx->lastCloseClickId = win->id; ctx->lastCloseClickId = win->id;
@ -1965,8 +1986,8 @@ static void initColorScheme(AppContextT *ctx) {
static void interactiveScreenshot(AppContextT *ctx) { static void interactiveScreenshot(AppContextT *ctx) {
FileFilterT filters[] = { FileFilterT filters[] = {
{ "PNG Images (*.png)", "*.png" }, { "PNG Images (*.png)" },
{ "BMP Images (*.bmp)", "*.bmp" } { "BMP Images (*.bmp)" }
}; };
char path[DVX_MAX_PATH]; char path[DVX_MAX_PATH];
@ -2000,8 +2021,8 @@ static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win) {
} }
FileFilterT filters[] = { FileFilterT filters[] = {
{ "PNG Images (*.png)", "*.png" }, { "PNG Images (*.png)" },
{ "BMP Images (*.bmp)", "*.bmp" } { "BMP Images (*.bmp)" }
}; };
char path[DVX_MAX_PATH]; char path[DVX_MAX_PATH];
@ -4309,6 +4330,7 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_
ctx->lastTitleClickId = -1; ctx->lastTitleClickId = -1;
ctx->lastTitleClickTime = 0; ctx->lastTitleClickTime = 0;
ctx->wheelDirection = 1; ctx->wheelDirection = 1;
ctx->wheelStep = MOUSE_WHEEL_STEP_DEFAULT;
ctx->dblClickTicks = DBLCLICK_THRESHOLD; ctx->dblClickTicks = DBLCLICK_THRESHOLD;
sDblClickTicks = DBLCLICK_THRESHOLD; sDblClickTicks = DBLCLICK_THRESHOLD;
@ -4833,11 +4855,21 @@ void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b)
// dvxSetMouseConfig // dvxSetMouseConfig
// ============================================================ // ============================================================
void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio) { void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio, int32_t wheelStep) {
ctx->wheelDirection = (wheelDir < 0) ? -1 : 1; ctx->wheelDirection = (wheelDir < 0) ? -1 : 1;
ctx->dblClickTicks = (clock_t)dblClickMs * CLOCKS_PER_SEC / 1000; ctx->dblClickTicks = (clock_t)dblClickMs * CLOCKS_PER_SEC / 1000;
sDblClickTicks = ctx->dblClickTicks; sDblClickTicks = ctx->dblClickTicks;
if (wheelStep < MOUSE_WHEEL_STEP_MIN) {
wheelStep = MOUSE_WHEEL_STEP_MIN;
}
if (wheelStep > MOUSE_WHEEL_STEP_MAX) {
wheelStep = MOUSE_WHEEL_STEP_MAX;
}
ctx->wheelStep = wheelStep;
if (accelThreshold > 0) { if (accelThreshold > 0) {
platformMouseSetAccel(accelThreshold); platformMouseSetAccel(accelThreshold);
} }

View file

@ -104,6 +104,7 @@ typedef struct AppContextT {
uint32_t charHeightRecip; // fixed-point 16.16 reciprocal of font.charHeight uint32_t charHeightRecip; // fixed-point 16.16 reciprocal of font.charHeight
// Mouse configuration (loaded from preferences) // Mouse configuration (loaded from preferences)
int32_t wheelDirection; // 1 = normal, -1 = reversed int32_t wheelDirection; // 1 = normal, -1 = reversed
int32_t wheelStep; // lines per wheel notch (1-10, default 3)
clock_t dblClickTicks; // double-click speed in clock() ticks clock_t dblClickTicks; // double-click speed in clock() ticks
// Color scheme source RGB values (unpacked, for theme save/get) // Color scheme source RGB values (unpacked, for theme save/get)
uint8_t colorRgb[ColorCountE][3]; uint8_t colorRgb[ColorCountE][3];
@ -134,7 +135,8 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request
// dblClickMs: double-click speed in milliseconds (e.g. 500). // dblClickMs: double-click speed in milliseconds (e.g. 500).
// accelThreshold: double-speed threshold in mickeys/sec (0 = don't change). // accelThreshold: double-speed threshold in mickeys/sec (0 = don't change).
// mickeyRatio: mickeys per 8 pixels (0 = don't change, 8 = default speed). // mickeyRatio: mickeys per 8 pixels (0 = don't change, 8 = default speed).
void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio); // wheelStep: lines per wheel notch (1-10, default 3).
void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio, int32_t wheelStep);
// ============================================================ // ============================================================
// Color scheme // Color scheme

View file

@ -1071,6 +1071,32 @@ typedef struct {
static FileDialogStateT sFd; static FileDialogStateT sFd;
// ============================================================
// fdExtractPattern -- extract glob pattern from inside parentheses in label
// ============================================================
static const char *fdExtractPattern(const FileFilterT *f, char *buf, int32_t bufSize) {
const char *open = strchr(f->label, '(');
const char *close = open ? strchr(open, ')') : NULL;
if (open && close && close > open + 1) {
int32_t len = (int32_t)(close - open - 1);
if (len >= bufSize) {
len = bufSize - 1;
}
memcpy(buf, open + 1, len);
buf[len] = '\0';
return buf;
}
// No parens -- use the whole label as the pattern
snprintf(buf, bufSize, "%s", f->label);
return buf;
}
// ============================================================ // ============================================================
// fdFilterMatch -- check if filename matches a glob pattern // fdFilterMatch -- check if filename matches a glob pattern
// ============================================================ // ============================================================
@ -1210,9 +1236,10 @@ static void fdLoadDir(void) {
fdFreeEntries(); fdFreeEntries();
const char *pattern = NULL; const char *pattern = NULL;
char patBuf[128];
if (sFd.filters && sFd.activeFilter >= 0 && sFd.activeFilter < sFd.filterCount) { if (sFd.filters && sFd.activeFilter >= 0 && sFd.activeFilter < sFd.filterCount) {
pattern = sFd.filters[sFd.activeFilter].pattern; pattern = fdExtractPattern(&sFd.filters[sFd.activeFilter], patBuf, sizeof(patBuf));
} }
DIR *dir = opendir(sFd.curDir); DIR *dir = opendir(sFd.curDir);
@ -1561,6 +1588,59 @@ static void fdOnOk(WidgetT *w) {
return; return;
} }
// Save dialog: if the filename has no extension, append the first
// extension from the active filter pattern. Wildcards like "*.*" or
// "*" don't add an extension.
char nameWithExt[FD_MAX_PATH];
if ((sFd.flags & FD_SAVE) && !strchr(name, '.')) {
char extPatBuf[128];
const char *pattern = fdExtractPattern(&sFd.filters[sFd.activeFilter], extPatBuf, sizeof(extPatBuf));
const char *ext = NULL;
// Find first non-wildcard extension in pattern (may be "*.txt;*.doc")
if (pattern) {
const char *p = pattern;
while (*p) {
if (p[0] == '*' && p[1] == '.') {
const char *e = p + 2;
// Skip wildcards like "*.*"
if (*e != '*' && *e != '\0') {
ext = p + 1; // points to ".ext"
break;
}
}
// Skip to next semicolon-delimited pattern
const char *semi = strchr(p, ';');
if (semi) {
p = semi + 1;
} else {
break;
}
}
}
if (ext) {
// Extract just the extension (up to ; or end)
char extBuf[16];
int32_t ei = 0;
while (ext[ei] && ext[ei] != ';' && ei < 15) {
extBuf[ei] = ext[ei];
ei++;
}
extBuf[ei] = '\0';
snprintf(nameWithExt, sizeof(nameWithExt), "%s%s", name, extBuf);
name = nameWithExt;
wgtSetText(sFd.nameInput, name);
}
}
// Accept the file (with confirmation if needed) // Accept the file (with confirmation if needed)
fdAcceptFile(name); fdAcceptFile(name);
} }

View file

@ -64,9 +64,11 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message,
// patterns (no semicolon-separated lists). This keeps the matching code // patterns (no semicolon-separated lists). This keeps the matching code
// trivial for a DOS filesystem where filenames are short and simple. // trivial for a DOS filesystem where filenames are short and simple.
// File filter for the file dialog. Pattern is extracted from inside
// parentheses in the label. Example: "Text Files (*.txt)"
// Multiple extensions: "Images (*.bmp;*.png;*.jpg;*.gif)"
typedef struct { typedef struct {
const char *label; // e.g. "Text Files (*.txt)" const char *label; // e.g. "Text Files (*.txt)" -- pattern extracted from parens
const char *pattern; // e.g. "*.txt" (case-insensitive, single pattern)
} FileFilterT; } FileFilterT;
// Display a modal file open/save dialog. The dialog shows a directory // Display a modal file open/save dialog. The dialog shows a directory

View file

@ -77,4 +77,8 @@ void dvxResClose(DvxResHandleT *h);
// Returns 0 on success, -1 on error. // Returns 0 on success, -1 on error.
int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize); int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize);
// Remove a resource by name from a DXE file.
// Returns 0 on success, -1 on error (not found or I/O failure).
int32_t dvxResRemove(const char *path, const char *name);
#endif // DVX_RES_H #endif // DVX_RES_H

View file

@ -144,3 +144,105 @@ void dvxResClose(DvxResHandleT *h) {
int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize) { int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize) {
return dvxResAppendEntry(path, name, type, data, dataSize); return dvxResAppendEntry(path, name, type, data, dataSize);
} }
int32_t dvxResRemove(const char *path, const char *name) {
if (!path || !name) {
return -1;
}
long dxeSize = dvxResDxeContentSize(path);
if (dxeSize < 0) {
return -1;
}
DvxResDirEntryT *entries = NULL;
uint32_t count = 0;
uint8_t **data = NULL;
dvxResReadExisting(path, dxeSize, &entries, &count, &data);
if (!entries || count == 0) {
return -1;
}
bool found = false;
for (uint32_t i = 0; i < count; i++) {
if (strcmp(entries[i].name, name) == 0) {
free(data[i]);
for (uint32_t j = i; j < count - 1; j++) {
entries[j] = entries[j + 1];
data[j] = data[j + 1];
}
count--;
found = true;
break;
}
}
if (!found) {
for (uint32_t i = 0; i < count; i++) {
free(data[i]);
}
free(data);
free(entries);
return -1;
}
int32_t result;
if (count == 0) {
// No resources left -- truncate to DXE content only
FILE *f = fopen(path, "rb");
if (!f) {
free(data);
free(entries);
return -1;
}
uint8_t *dxeBuf = (uint8_t *)malloc((size_t)dxeSize);
if (!dxeBuf) {
fclose(f);
free(data);
free(entries);
return -1;
}
if (fread(dxeBuf, 1, (size_t)dxeSize, f) != (size_t)dxeSize) {
free(dxeBuf);
fclose(f);
free(data);
free(entries);
return -1;
}
fclose(f);
f = fopen(path, "wb");
if (f) {
fwrite(dxeBuf, 1, (size_t)dxeSize, f);
fclose(f);
result = 0;
} else {
result = -1;
}
free(dxeBuf);
} else {
result = dvxResWriteBlock(path, dxeSize, entries, count, data);
}
for (uint32_t i = 0; i < count; i++) {
free(data[i]);
}
free(data);
free(entries);
return result;
}

View file

@ -611,8 +611,10 @@ typedef struct {
#define MOUSE_RIGHT 2 #define MOUSE_RIGHT 2
#define MOUSE_MIDDLE 4 #define MOUSE_MIDDLE 4
// Scrollbar lines to scroll per mouse wheel notch // Default scrollbar lines to scroll per mouse wheel notch
#define MOUSE_WHEEL_STEP 3 #define MOUSE_WHEEL_STEP_DEFAULT 3
#define MOUSE_WHEEL_STEP_MIN 1
#define MOUSE_WHEEL_STEP_MAX 10
// ============================================================ // ============================================================
// Mouse cursor // Mouse cursor

1
run.sh
View file

@ -1,5 +1,4 @@
#!/bin/bash #!/bin/bash
flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x-overrides.conf flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x-overrides.conf
#SDL_VIDEO_X11_VISUALID= ~/bin/dosbox-staging/dosbox -conf dosbox-staging-overrides.conf #SDL_VIDEO_X11_VISUALID= ~/bin/dosbox-staging/dosbox -conf dosbox-staging-overrides.conf

View file

@ -8,10 +8,17 @@
DECLARE LIBRARY "basrt" DECLARE LIBRARY "basrt"
' Show a file Open dialog. Returns selected path, or "" if cancelled. ' Show a file Open dialog. Returns selected path, or "" if cancelled.
' filter$ is a DOS wildcard (e.g. "*.bmp", "*.txt"). ' filter$ specifies file type filters, pipe-delimited:
' "Label (pattern)|Label (pattern)|..."
' The pattern is extracted from inside the parentheses.
' Examples:
' "*.txt" (single pattern, legacy)
' "Text Files (*.txt)|All Files (*.*)" (two filters)
' "Images (*.bmp;*.png;*.jpg)|All Files (*.*)" (multi-extension)
DECLARE FUNCTION basFileOpen(BYVAL title AS STRING, BYVAL filter AS STRING) AS STRING DECLARE FUNCTION basFileOpen(BYVAL title AS STRING, BYVAL filter AS STRING) AS STRING
' Show a file Save dialog. Returns selected path, or "" if cancelled. ' Show a file Save dialog. Returns selected path, or "" if cancelled.
' filter$ uses the same format as basFileOpen.
DECLARE FUNCTION basFileSave(BYVAL title AS STRING, BYVAL filter AS STRING) AS STRING DECLARE FUNCTION basFileSave(BYVAL title AS STRING, BYVAL filter AS STRING) AS STRING
' Show a modal text input box. Returns entered text, or "" if cancelled. ' Show a modal text input box. Returns entered text, or "" if cancelled.
@ -29,6 +36,6 @@ DECLARE LIBRARY "basrt"
END DECLARE END DECLARE
' Return value constants for basPromptSave ' Return value constants for basPromptSave
CONST DVX_SAVE_YES = 0 CONST DVX_SAVE_YES = 1
CONST DVX_SAVE_NO = 1 CONST DVX_SAVE_NO = 2
CONST DVX_SAVE_CANCEL = 2 CONST DVX_SAVE_CANCEL = 3

View file

@ -0,0 +1,50 @@
' resource.bas -- DVX Resource File Library for DVX BASIC
'
' Provides access to DVX resource blocks appended to DXE3
' files (.app, .wgt, .lib). Resources are named data entries
' of type icon, text, or binary.
'
' Usage: Add this file to your project.
CONST RES_TYPE_ICON = 1
CONST RES_TYPE_TEXT = 2
CONST RES_TYPE_BINARY = 3
DECLARE LIBRARY "basrt"
' Open a resource file for reading. Returns a handle (> 0)
' or 0 on failure. Close with ResClose when done.
DECLARE FUNCTION ResOpen(BYVAL path AS STRING) AS LONG
' Close a resource handle.
DECLARE SUB ResClose(BYVAL handle AS LONG)
' Return the number of resources in an open handle.
DECLARE FUNCTION ResCount(BYVAL handle AS LONG) AS LONG
' Return the name of a resource by zero-based index.
DECLARE FUNCTION ResName$(BYVAL handle AS LONG, BYVAL idx AS LONG)
' Return the type of a resource by zero-based index.
' Returns RES_TYPE_ICON (1), RES_TYPE_TEXT (2), or RES_TYPE_BINARY (3).
DECLARE FUNCTION ResType(BYVAL handle AS LONG, BYVAL idx AS LONG) AS LONG
' Return the size in bytes of a resource by zero-based index.
DECLARE FUNCTION ResSize(BYVAL handle AS LONG, BYVAL idx AS LONG) AS LONG
' Read a text resource by name. Opens and closes the file
' internally. Returns "" if not found.
DECLARE FUNCTION ResGetText$(BYVAL path AS STRING, BYVAL resName AS STRING)
' Add or replace a text resource. Returns True on success.
DECLARE FUNCTION ResAddText(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL text AS STRING) AS LONG
' Add or replace a resource from a file. Type should be
' RES_TYPE_ICON or RES_TYPE_BINARY. Returns True on success.
DECLARE FUNCTION ResAddFile(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL resType AS LONG, BYVAL srcFile AS STRING) AS LONG
' Remove a resource by name. Returns True on success.
DECLARE FUNCTION ResRemove(BYVAL path AS STRING, BYVAL resName AS STRING) AS LONG
' Extract a resource to a file. Returns True on success.
DECLARE FUNCTION ResExtract(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL outFile AS STRING) AS LONG
END DECLARE

BIN
sdk/samples/basic/basicdemo/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,16 @@
[Project]
Name = BASIC Demo
Author = DVX Project
Description = Comprehensive tour of DVX BASIC features
Icon = ICON32.BMP
[Modules]
File0 = ../../../include/basic/commdlg.bas
Count = 1
[Forms]
File0 = basicdemo.frm
Count = 1
[Settings]
StartupForm = BasicDemo

View file

@ -0,0 +1,937 @@
VERSION DVX 1.00
Begin Form BasicDemo
Caption = "DVX BASIC Feature Tour"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 600
Height = 440
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuClear
Caption = "&Clear OutArea"
End
Begin Menu mnuSaveOut
Caption = "&Save OutArea..."
End
Begin Menu mnuSepF1
Caption = "-"
End
Begin Menu mnuExit
Caption = "E&xit"
End
End
Begin Menu mnuRun
Caption = "&Demos"
Begin Menu mnuRunAll
Caption = "Run &All Text Demos"
End
Begin Menu mnuSepR1
Caption = "-"
End
Begin Menu mnuGraphics
Caption = "&Graphics..."
End
Begin Menu mnuDynamic
Caption = "&Dynamic Form..."
End
Begin Menu mnuTimer
Caption = "&Timer..."
End
End
Begin Menu mnuHelp
Caption = "&Help"
Begin Menu mnuAbout
Caption = "&About..."
End
End
Begin Frame fraButtons
Caption = "Language Demonstrations"
Layout = VBox
Weight = 0
Begin HBox rowA
Weight = 0
Begin CommandButton btnTypes
Caption = "&Types"
Weight = 1
End
Begin CommandButton btnMath
Caption = "&Math"
Weight = 1
End
Begin CommandButton btnStrings
Caption = "&Strings"
Weight = 1
End
Begin CommandButton btnArrays
Caption = "&Arrays"
Weight = 1
End
Begin CommandButton btnData
Caption = "&DATA/READ"
Weight = 1
End
End
Begin HBox rowB
Weight = 0
Begin CommandButton btnFlow
Caption = "Control &Flow"
Weight = 1
End
Begin CommandButton btnUdt
Caption = "&UDT"
Weight = 1
End
Begin CommandButton btnOpt
Caption = "&Optional"
Weight = 1
End
Begin CommandButton btnError
Caption = "&Errors"
Weight = 1
End
Begin CommandButton btnFormat
Caption = "F&ormat"
Weight = 1
End
End
Begin HBox rowC
Weight = 0
Begin CommandButton btnFileIO
Caption = "File &I/O"
Weight = 1
End
Begin CommandButton btnSystem
Caption = "S&ystem"
Weight = 1
End
Begin CommandButton btnIni
Caption = "I&NI"
Weight = 1
End
Begin CommandButton btnDialogs
Caption = "Di&alogs"
Weight = 1
End
Begin CommandButton btnClear
Caption = "Clea&r"
Weight = 1
End
End
End
Begin TextArea OutArea
Weight = 1
End
Begin Label LblStatus
Caption = "Ready. Click any button to run a demo."
Weight = 0
End
End
OPTION EXPLICIT
TYPE PointT
x AS INTEGER
y AS INTEGER
END TYPE
' ============================================================
' OutArea helpers
' ============================================================
SUB Say(s AS STRING)
OutArea.AppendText s + CHR$(10)
END SUB
SUB Header(title AS STRING)
Say ""
Say "--- " + title + " ---"
END SUB
Load BasicDemo
BasicDemo.Show
OutArea.SetShowLineNumbers False
OutArea.SetReadOnly True
Say "Welcome to the DVX BASIC Feature Tour!"
Say "Each button below runs a self-contained example."
Say "Check the Demos menu for graphics, dynamic UI, and timer demos."
Say ""
' ============================================================
' Menu handlers
' ============================================================
SUB mnuClear_Click
OutArea.Text = ""
LblStatus.Caption = "OutArea cleared."
END SUB
SUB mnuSaveOut_Click
DIM path AS STRING
path = basFileSave("Save OutArea", "Text Files (*.txt)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
OPEN path FOR OUTPUT AS #1
PRINT #1, OutArea.Text
CLOSE #1
LblStatus.Caption = "Saved: " + path
END SUB
SUB mnuExit_Click
Unload BasicDemo
END SUB
SUB mnuRunAll_Click
btnTypes_Click
btnMath_Click
btnStrings_Click
btnArrays_Click
btnData_Click
btnFlow_Click
btnUdt_Click
btnOpt_Click
btnError_Click
btnFormat_Click
btnFileIO_Click
btnSystem_Click
LblStatus.Caption = "All text demos complete."
END SUB
SUB mnuAbout_Click
DIM msg AS STRING
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"
MsgBox msg, vbOKOnly, "About"
END SUB
' ============================================================
' Types: INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
' ============================================================
SUB btnTypes_Click
Header "Types"
DIM i AS INTEGER
DIM l AS LONG
DIM s AS SINGLE
DIM d AS DOUBLE
DIM t AS STRING
DIM b AS BOOLEAN
i = 32767
l = 2147483647
s = 3.14159
d = 2.718281828459045
t = "Hello, DVX!"
b = True
Say "INTEGER (16-bit): " + STR$(i)
Say "LONG (32-bit): " + STR$(l)
Say "SINGLE (float): " + STR$(s)
Say "DOUBLE (double): " + STR$(d)
Say "STRING: " + CHR$(34) + t + CHR$(34)
Say "BOOLEAN True: " + STR$(b)
' CONST with AS type annotation
CONST PI AS DOUBLE = 3.1415926535
Say "CONST PI = " + STR$(PI)
LblStatus.Caption = "Types demo complete."
END SUB
' ============================================================
' Math: integer + float operators, built-in functions
' ============================================================
SUB btnMath_Click
Header "Math"
DIM a AS INTEGER
DIM b AS INTEGER
a = 17
b = 5
Say "a = 17, b = 5"
Say "a + b = " + STR$(a + b)
Say "a - b = " + STR$(a - b)
Say "a * b = " + STR$(a * b)
Say "a \ b = " + STR$(a \ b) + " (integer divide)"
Say "a MOD b = " + STR$(a MOD b)
Say "a / b = " + STR$(a / b) + " (float divide)"
Say ""
Say "SQR(144) = " + STR$(SQR(144))
Say "ABS(-42) = " + STR$(ABS(-42))
Say "INT(3.7) = " + STR$(INT(3.7))
Say "FIX(-3.7) = " + STR$(FIX(-3.7))
Say "SGN(-9) = " + STR$(SGN(-9))
Say "SIN(0) = " + STR$(SIN(0))
Say "COS(0) = " + STR$(COS(0))
Say "2 ^ 10 = " + STR$(2 ^ 10)
RANDOMIZE TIMER
DIM r AS INTEGER
r = INT(RND * 100)
Say "RND (0-99) = " + STR$(r)
Say "TIMER = " + STR$(TIMER) + " (seconds since midnight)"
LblStatus.Caption = "Math demo complete."
END SUB
' ============================================================
' Strings: concatenation, LEFT$/RIGHT$/MID$/LEN/INSTR/UCASE$/LCASE$
' ============================================================
SUB btnStrings_Click
Header "Strings"
DIM s AS STRING
s = "The quick brown fox"
Say "Source: " + CHR$(34) + s + CHR$(34)
Say "LEN = " + STR$(LEN(s))
Say "LEFT$(s, 3) = " + LEFT$(s, 3)
Say "RIGHT$(s, 3) = " + RIGHT$(s, 3)
Say "MID$(s, 5, 5) = " + MID$(s, 5, 5)
Say "UCASE$ = " + UCASE$(s)
Say "LCASE$('HELLO') = " + LCASE$("HELLO")
Say "INSTR(s, 'brown')= " + STR$(INSTR(s, "brown"))
Say "TRIM$(' hi ') = " + CHR$(34) + TRIM$(" hi ") + CHR$(34)
Say "STRING$(5, 42) = " + STRING$(5, 42)
Say "CHR$(65) = " + CHR$(65)
Say "ASC('A') = " + STR$(ASC("A"))
Say "HEX$(255) = " + HEX$(255)
Say "VAL('42.5xyz') = " + STR$(VAL("42.5xyz"))
LblStatus.Caption = "Strings demo complete."
END SUB
' ============================================================
' Arrays: 1D, 2D, LBOUND/UBOUND, REDIM PRESERVE
' ============================================================
SUB btnArrays_Click
Header "Arrays"
' 1D array
DIM squares(9) AS INTEGER
DIM i AS INTEGER
FOR i = 0 TO 9
squares(i) = i * i
NEXT i
DIM lineS AS STRING
lineS = "squares(0..9) = "
FOR i = 0 TO 9
lineS = lineS + STR$(squares(i)) + " "
NEXT i
Say lineS
' Bounds: DIM a(lo TO hi)
DIM prices(1 TO 3) AS SINGLE
prices(1) = 9.99
prices(2) = 14.99
prices(3) = 29.99
Say "LBOUND(prices) = " + STR$(LBOUND(prices)) + ", UBOUND = " + STR$(UBOUND(prices))
Say "prices(2) = " + STR$(prices(2))
' 2D array
DIM matrix(2, 2) AS INTEGER
matrix(0, 0) = 1 : matrix(0, 1) = 2 : matrix(0, 2) = 3
matrix(1, 0) = 4 : matrix(1, 1) = 5 : matrix(1, 2) = 6
matrix(2, 0) = 7 : matrix(2, 1) = 8 : matrix(2, 2) = 9
Say "3x3 matrix:"
DIM r AS INTEGER
DIM c AS INTEGER
FOR r = 0 TO 2
lineS = " "
FOR c = 0 TO 2
lineS = lineS + STR$(matrix(r, c))
NEXT c
Say lineS
NEXT r
' REDIM PRESERVE
DIM nums(2) AS INTEGER
nums(0) = 10
nums(1) = 20
nums(2) = 30
REDIM PRESERVE nums(4) AS INTEGER
nums(3) = 40
nums(4) = 50
Say "After REDIM PRESERVE: " + STR$(nums(0)) + " " + STR$(nums(1)) + " " + STR$(nums(2)) + " " + STR$(nums(3)) + " " + STR$(nums(4))
LblStatus.Caption = "Arrays demo complete."
END SUB
' ============================================================
' DATA / READ / RESTORE
' ============================================================
SUB btnData_Click
Header "DATA / READ / RESTORE"
DATA "Red", 255, 0, 0
DATA "Green", 0, 255, 0
DATA "Blue", 0, 0, 255
DIM colorName AS STRING
DIM r AS INTEGER
DIM g AS INTEGER
DIM b AS INTEGER
DIM i AS INTEGER
FOR i = 1 TO 3
READ colorName
READ r
READ g
READ b
Say colorName + ": (" + STR$(r) + "," + STR$(g) + "," + STR$(b) + ")"
NEXT i
Say ""
Say "RESTORE resets pointer. Reading first entry again:"
RESTORE
READ colorName
Say " first = " + colorName
LblStatus.Caption = "DATA/READ demo complete."
END SUB
' ============================================================
' Control flow: IF, SELECT CASE, FOR, DO WHILE, GOSUB
' ============================================================
SUB btnFlow_Click
Header "Control Flow"
' FOR with STEP
Say "FOR i = 10 TO 0 STEP -2:"
DIM i AS INTEGER
DIM lineS AS STRING
lineS = " "
FOR i = 10 TO 0 STEP -2
lineS = lineS + STR$(i)
NEXT i
Say lineS
' DO WHILE
Say ""
Say "DO WHILE n < 32 (doubling):"
DIM n AS LONG
n = 1
lineS = " "
DO WHILE n < 32
lineS = lineS + STR$(n)
n = n * 2
LOOP
Say lineS
' IF / ELSEIF / ELSE
Say ""
DIM score AS INTEGER
score = 78
IF score >= 90 THEN
Say "score " + STR$(score) + " -> A"
ELSEIF score >= 80 THEN
Say "score " + STR$(score) + " -> B"
ELSEIF score >= 70 THEN
Say "score " + STR$(score) + " -> C"
ELSE
Say "score " + STR$(score) + " -> F"
END IF
' SELECT CASE
Say ""
DIM day AS INTEGER
day = 3
SELECT CASE day
CASE 1
Say "day 1 = Monday"
CASE 2, 3
Say "day " + STR$(day) + " = midweek"
CASE 4 TO 5
Say "day " + STR$(day) + " = late week"
CASE ELSE
Say "day " + STR$(day) + " = weekend"
END SELECT
' GOSUB / RETURN
Say ""
Say "GOSUB to a local label:"
GOSUB labelHello
Say "back from subroutine"
LblStatus.Caption = "Control flow demo complete."
EXIT SUB
labelHello:
Say " inside GOSUB"
RETURN
END SUB
' ============================================================
' User-Defined Type
' ============================================================
SUB btnUdt_Click
Header "User-Defined Type"
DIM p AS PointT
p.x = 10
p.y = 20
Say "PointT p = (" + STR$(p.x) + "," + STR$(p.y) + ")"
' Array of UDT
DIM corners(3) AS PointT
corners(0).x = 0 : corners(0).y = 0
corners(1).x = 10 : corners(1).y = 0
corners(2).x = 10 : corners(2).y = 10
corners(3).x = 0 : corners(3).y = 10
Say "Rectangle corners:"
DIM i AS INTEGER
FOR i = 0 TO 3
Say " (" + STR$(corners(i).x) + "," + STR$(corners(i).y) + ")"
NEXT i
LblStatus.Caption = "UDT demo complete."
END SUB
' ============================================================
' Optional parameters (DVX extension)
' ============================================================
FUNCTION Greet(who AS STRING, OPTIONAL greeting AS STRING) AS STRING
IF greeting = "" THEN
greeting = "Hello"
END IF
Greet = greeting + ", " + who + "!"
END FUNCTION
SUB btnOpt_Click
Header "Optional Parameters"
Say Greet("World")
Say Greet("Scott", "Howdy")
Say Greet("DVX", "Greetings from")
LblStatus.Caption = "Optional params demo complete."
END SUB
' ============================================================
' ON ERROR GOTO
' ============================================================
SUB btnError_Click
Header "ON ERROR GOTO"
ON ERROR GOTO handler
DIM a AS INTEGER
DIM b AS INTEGER
a = 10
b = 0
Say "Attempting 10/0 ..."
Say " 10 / 0 = " + STR$(a / b)
Say "(should not reach here)"
EXIT SUB
handler:
Say " caught! ERR = " + STR$(ERR)
LblStatus.Caption = "Error handler ran successfully."
END SUB
' ============================================================
' PRINT USING / FORMAT$
' ============================================================
SUB btnFormat_Click
Header "Formatting"
Say "FORMAT$(1234.5, '#,##0.00') = " + FORMAT$(1234.5, "#,##0.00")
Say "FORMAT$(0.075, 'percent') = " + FORMAT$(0.075, "percent")
Say "FORMAT$(-42, '+#0') = " + FORMAT$(-42, "+#0")
Say "FORMAT$(3.14159, '0.00') = " + FORMAT$(3.14159, "0.00")
LblStatus.Caption = "Format demo complete."
END SUB
' ============================================================
' File I/O
' ============================================================
SUB btnFileIO_Click
Header "File I/O"
DIM path AS STRING
path = App.Data + "/demo.txt"
Say "Writing: " + path
OPEN path FOR OUTPUT AS #1
PRINT #1, "Line one"
PRINT #1, "Line two"
PRINT #1, "The answer is "; 42
CLOSE #1
Say "LOF = " + STR$(FILELEN(path))
Say "Reading back:"
OPEN path FOR INPUT AS #1
DIM ln AS STRING
DO WHILE NOT EOF(1)
LINE INPUT #1, ln
Say " " + ln
LOOP
CLOSE #1
KILL path
Say "Deleted."
LblStatus.Caption = "File I/O demo complete."
END SUB
' ============================================================
' System: App object, environment, current directory
' ============================================================
SUB btnSystem_Click
Header "System / App"
Say "App.Path = " + App.Path
Say "App.Config = " + App.Config
Say "App.Data = " + App.Data
Say "CurDir = " + CurDir()
Say "Date = " + Date$
Say "Time = " + Time$
Say "PATH env = " + LEFT$(Environ$("PATH"), 40) + "..."
LblStatus.Caption = "System demo complete."
END SUB
' ============================================================
' INI read/write
' ============================================================
SUB btnIni_Click
Header "INI Read/Write"
DIM path AS STRING
path = App.Data + "/demo.ini"
Say "Writing: " + path
IniWrite path, "General", "UserName", "Scott"
IniWrite path, "General", "Version", "1.00"
IniWrite path, "Options", "AutoSave", "True"
Say "Reading back:"
Say " UserName = " + IniRead$(path, "General", "UserName", "(missing)")
Say " Version = " + IniRead$(path, "General", "Version", "(missing)")
Say " AutoSave = " + IniRead$(path, "Options", "AutoSave", "(missing)")
Say " Missing = " + IniRead$(path, "General", "NotThere", "(default)")
KILL path
LblStatus.Caption = "INI demo complete."
END SUB
' ============================================================
' Dialog demos (spawns the real dialogs)
' ============================================================
SUB btnDialogs_Click
Header "Dialogs"
DIM response AS INTEGER
response = MsgBox("MessageBox demo." + CHR$(10) + "Are you enjoying the demo?", vbYesNo + vbQuestion, "Feedback")
IF response = vbYes THEN
Say "MsgBox: user said yes"
ELSE
Say "MsgBox: user said no"
END IF
DIM text AS STRING
text = basInputBox2("Input", "What is your name?", "Anonymous")
Say "InputBox returned: " + text
DIM choice AS INTEGER
choice = basChoiceDialog("Favorite", "Pick a color:", "Red|Green|Blue|Yellow", 1)
IF choice >= 0 THEN
Say "Choice index: " + STR$(choice)
ELSE
Say "Choice cancelled"
END IF
DIM n AS INTEGER
n = basIntInput("Number", "Pick a number (1-100):", 42, 1, 100)
Say "IntInput: " + STR$(n)
LblStatus.Caption = "Dialog demo complete."
END SUB
SUB btnClear_Click
mnuClear_Click
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)
GraphicsForm.Caption = "Graphics Demo"
gfxWin = frm
DIM cv AS LONG
SET cv = CreateControl(frm, "Canvas", "GfxCanvas")
GfxCanvas.Width = 340
GfxCanvas.Height = 260
GfxCanvas.Weight = 1
DIM btnRow AS LONG
SET btnRow = CreateControl(frm, "HBox", "GfxRow")
DIM bDraw AS LONG
SET bDraw = CreateControl(frm, "CommandButton", "GfxDraw", btnRow)
GfxDraw.Caption = "Draw"
SetEvent bDraw, "Click", "GfxDrawAll"
DIM bClear AS LONG
SET bClear = CreateControl(frm, "CommandButton", "GfxClear", btnRow)
GfxClear.Caption = "Clear"
SetEvent bClear, "Click", "GfxClearCanvas"
frm.Show
GfxDrawAll
END SUB
SUB GfxDrawAll
DIM w AS LONG
DIM h AS LONG
w = 340
h = 260
' Gradient background bars
DIM y AS INTEGER
DIM shade AS LONG
FOR y = 0 TO h - 1 STEP 4
shade = (y * 255) \ h
GfxCanvas.FillRect 0, y, w, 4, RGB(shade, shade \ 2, 128)
NEXT y
' Star field
RANDOMIZE TIMER
DIM s AS INTEGER
DIM sx AS INTEGER
DIM sy AS INTEGER
FOR s = 1 TO 40
sx = INT(RND * w)
sy = INT(RND * h)
GfxCanvas.SetPixel sx, sy, RGB(255, 255, 255)
NEXT s
' Rectangle + outline
GfxCanvas.FillRect 20, 20, 80, 50, RGB(240, 240, 0)
GfxCanvas.DrawRect 20, 20, 80, 50, RGB(0, 0, 0)
' Circle approximated by line segments
DIM cx AS INTEGER
DIM cy AS INTEGER
DIM r AS INTEGER
cx = 250
cy = 80
r = 40
DIM a AS DOUBLE
DIM px AS INTEGER
DIM py AS INTEGER
DIM qx AS INTEGER
DIM qy AS INTEGER
px = cx + r
py = cy
FOR a = 0 TO 6.3 STEP 0.2
qx = cx + INT(r * COS(a))
qy = cy + INT(r * SIN(a))
GfxCanvas.DrawLine px, py, qx, qy, RGB(255, 128, 0)
px = qx
py = qy
NEXT a
' Text
GfxCanvas.DrawText 60, 200, "Canvas + math + colors", RGB(255, 255, 255)
GfxCanvas.DrawText 60, 220, "DVX BASIC graphics", RGB(255, 255, 0)
GfxCanvas.Refresh
END SUB
SUB GfxClearCanvas
GfxCanvas.Clear RGB(0, 0, 0)
GfxCanvas.Refresh
END SUB
SUB GraphicsForm_Unload
gfxWin = 0
END SUB
' ============================================================
' Dynamic form demo
' ============================================================
DIM dynForm AS LONG
dynForm = 0
SUB mnuDynamic_Click
IF dynForm <> 0 THEN
EXIT SUB
END IF
DIM frm AS LONG
SET frm = CreateForm("DynForm", 320, 200)
DynForm.Caption = "Dynamic Form (built in code)"
dynForm = frm
DIM lbl AS LONG
SET lbl = CreateControl(frm, "Label", "DynLabel")
DynLabel.Caption = "This form was created 100% in code."
DIM lbl2 AS LONG
SET lbl2 = CreateControl(frm, "Label", "CountLabel")
CountLabel.Caption = "Counter: 0"
DIM btns AS LONG
SET btns = CreateControl(frm, "HBox", "DynBtns")
DIM bInc AS LONG
SET bInc = CreateControl(frm, "CommandButton", "BInc", btns)
BInc.Caption = "Count Up"
SetEvent bInc, "Click", "DynInc"
DIM bBye AS LONG
SET bBye = CreateControl(frm, "CommandButton", "BBye", btns)
BBye.Caption = "Close"
SetEvent bBye, "Click", "DynBye"
frm.Show
END SUB
DIM dynCount AS INTEGER
dynCount = 0
SUB DynInc
dynCount = dynCount + 1
CountLabel.Caption = "Counter: " + STR$(dynCount)
END SUB
SUB DynBye
Unload DynForm
END SUB
SUB DynForm_Unload
dynForm = 0
dynCount = 0
END SUB
' ============================================================
' Timer demo
' ============================================================
DIM timerWin AS LONG
timerWin = 0
SUB mnuTimer_Click
IF timerWin <> 0 THEN
EXIT SUB
END IF
DIM frm AS LONG
SET frm = CreateForm("TimerForm", 260, 140)
TimerForm.Caption = "Timer Demo"
timerWin = frm
DIM lbl AS LONG
SET lbl = CreateControl(frm, "Label", "TickLabel")
TickLabel.Caption = "Ticks: 0"
DIM t AS LONG
SET t = CreateControl(frm, "Timer", "Ticker")
Ticker.Interval = 500
SetEvent t, "Timer", "TickHandler"
frm.Show
END SUB
DIM tickCount AS LONG
tickCount = 0
SUB TickHandler
tickCount = tickCount + 1
TickLabel.Caption = "Ticks: " + STR$(tickCount) + " Time: " + TIME$
END SUB
SUB TimerForm_Unload
timerWin = 0
tickCount = 0
END SUB

View file

@ -62,14 +62,10 @@ CommDlgDemo.Show
PRINT "Common Dialog Demo started." PRINT "Common Dialog Demo started."
DO
DoEvents
LOOP
SUB BtnFileOpen_Click SUB BtnFileOpen_Click
DIM path AS STRING DIM path AS STRING
path = basFileOpen("Open a File", "*.bas") path = basFileOpen("Open a File", "BASIC Files (*.bas;*.frm)|All Files (*.*)")
IF path <> "" THEN IF path <> "" THEN
LblResult.Caption = "Opened: " + path LblResult.Caption = "Opened: " + path
PRINT "File Open: " + path PRINT "File Open: " + path
@ -82,7 +78,7 @@ END SUB
SUB BtnFileSave_Click SUB BtnFileSave_Click
DIM path AS STRING DIM path AS STRING
path = basFileSave("Save a File", "*.txt") path = basFileSave("Save a File", "Text Files (*.txt)|All Files (*.*)")
IF path <> "" THEN IF path <> "" THEN
LblResult.Caption = "Save to: " + path LblResult.Caption = "Save to: " + path
PRINT "File Save: " + path PRINT "File Save: " + path

View file

@ -47,10 +47,6 @@ frm.Show
PRINT "Dynamic form created. Try the buttons!" PRINT "Dynamic form created. Try the buttons!"
DO
DoEvents
LOOP
SUB OnHelloClick SUB OnHelloClick
DIM name AS STRING DIM name AS STRING
@ -64,7 +60,7 @@ END SUB
SUB OnFileClick SUB OnFileClick
DIM path AS STRING DIM path AS STRING
path = basFileOpen("Open a File", "*.*") path = basFileOpen("Open a File", "All Files (*.*)")
IF path <> "" THEN IF path <> "" THEN
StatusLabel.Caption = "Selected: " + path StatusLabel.Caption = "Selected: " + path
PRINT "File: " + path PRINT "File: " + path

View file

@ -1,205 +0,0 @@
VERSION DVX 1.00
' helpedit.frm -- DVX Help Editor
'
' A .dhs help source editor with syntax highlighting and
' live preview via the DVX Help Viewer.
'
' Add commdlg.bas and help.bas to your project, then click Run.
Begin Form HelpEdit
Caption = "DVX Help Editor"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 640
Height = 440
Begin HBox ToolBar
Weight = 0
Begin CommandButton BtnNew
Caption = "New"
MinWidth = 60
End
Begin CommandButton BtnOpen
Caption = "Open"
MinWidth = 60
End
Begin CommandButton BtnSave
Caption = "Save"
MinWidth = 60
End
Begin Spacer Spc1
MinWidth = 16
End
Begin CommandButton BtnCompile
Caption = "Compile"
MinWidth = 80
End
Begin CommandButton BtnPreview
Caption = "Preview"
MinWidth = 80
End
Begin Spacer Spc2
Weight = 1
End
Begin Label LblStatus
Caption = "Ready."
MinWidth = 150
End
End
Begin TextArea Editor
Weight = 1
End
End
DIM currentFile AS STRING
DIM hlpFile AS STRING
currentFile = ""
hlpFile = ""
Load HelpEdit
HelpEdit.Show
' Configure the editor
Editor.SetSyntaxMode "dhs"
Editor.SetShowLineNumbers True
Editor.SetAutoIndent True
Editor.SetCaptureTabs True
Editor.SetTabWidth 2
' Start with a template
Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + "This is your help file. Edit the .dhs source here," + CHR$(10) + "then click Compile and Preview to see the result." + CHR$(10)
LblStatus.Caption = "New file."
DO
DoEvents
LOOP
SUB BtnNew_Click
currentFile = ""
hlpFile = ""
Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10)
LblStatus.Caption = "New file."
HelpEdit.Caption = "DVX Help Editor"
END SUB
SUB BtnOpen_Click
DIM path AS STRING
path = basFileOpen("Open Help Source", "*.dhs")
IF path = "" THEN
EXIT SUB
END IF
' Read the entire file
DIM text AS STRING
DIM line AS STRING
text = ""
OPEN path FOR INPUT AS #1
DO WHILE NOT EOF(1)
LINE INPUT #1, line
IF text <> "" THEN
text = text + CHR$(10)
END IF
text = text + line
LOOP
CLOSE #1
Editor.Text = text
currentFile = path
hlpFile = ""
LblStatus.Caption = path
HelpEdit.Caption = "DVX Help Editor - " + path
END SUB
SUB BtnSave_Click
DIM path AS STRING
IF currentFile <> "" THEN
path = currentFile
ELSE
path = basFileSave("Save Help Source", "*.dhs")
IF path = "" THEN
EXIT SUB
END IF
END IF
' Write the editor contents to file
OPEN path FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
currentFile = path
LblStatus.Caption = "Saved: " + path
HelpEdit.Caption = "DVX Help Editor - " + path
END SUB
SUB BtnCompile_Click
' Save first if we have a file
IF currentFile = "" THEN
DIM path AS STRING
path = basFileSave("Save Help Source", "*.dhs")
IF path = "" THEN
LblStatus.Caption = "Save cancelled."
EXIT SUB
END IF
OPEN path FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
currentFile = path
ELSE
OPEN currentFile FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
END IF
' Derive .hlp filename from .dhs filename
DIM baseName AS STRING
baseName = currentFile
' Strip .dhs extension if present
IF LEN(baseName) > 4 THEN
IF UCASE$(RIGHT$(baseName, 4)) = ".DHS" THEN
baseName = LEFT$(baseName, LEN(baseName) - 4)
END IF
END IF
hlpFile = baseName + ".hlp"
LblStatus.Caption = "Compiling..."
DoEvents
IF HelpCompile(currentFile, hlpFile) THEN
LblStatus.Caption = "Compiled: " + hlpFile
ELSE
LblStatus.Caption = "Compile failed!"
hlpFile = ""
END IF
END SUB
SUB BtnPreview_Click
IF hlpFile = "" THEN
' Try compiling first
BtnCompile_Click
END IF
IF hlpFile = "" THEN
LblStatus.Caption = "Nothing to preview. Compile first."
EXIT SUB
END IF
LblStatus.Caption = "Opening viewer..."
HelpView hlpFile
END SUB
SUB HelpEdit_QueryUnload(Cancel AS INTEGER)
' Could prompt to save here
END SUB

BIN
sdk/samples/basic/helpedit/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,17 @@
[Project]
Name = Help Editor
Author = DVX Project
Description = DVX Help source editor with syntax highlighting and preview
Icon = ICON32.BMP
[Modules]
File0 = ../../../include/basic/commdlg.bas
File1 = ../../../include/basic/help.bas
Count = 2
[Forms]
File0 = helpedit.frm
Count = 1
[Settings]
StartupForm = HelpEdit

View file

@ -0,0 +1,310 @@
VERSION DVX 1.00
' helpedit.frm -- DVX Help Editor
'
' A .dhs help source editor with syntax highlighting and
' live preview via the DVX Help Viewer.
'
' Add commdlg.bas and help.bas to your project, then click Run.
Begin Form HelpEdit
Caption = "DVX Help Editor"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 640
Height = 440
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuNew
Caption = "&New"
End
Begin Menu mnuOpen
Caption = "&Open..."
End
Begin Menu mnuSep1
Caption = "-"
End
Begin Menu mnuSave
Caption = "&Save"
End
Begin Menu mnuSaveAs
Caption = "Save &As..."
End
Begin Menu mnuSep2
Caption = "-"
End
Begin Menu mnuExit
Caption = "E&xit"
End
End
Begin Menu mnuBuild
Caption = "&Build"
Begin Menu mnuCompile
Caption = "&Compile"
End
Begin Menu mnuPreview
Caption = "&Preview"
End
End
Begin Menu mnuHelp
Caption = "&Help"
Begin Menu mnuHelpTopic
Caption = "Help &Topics"
End
End
Begin TextArea Editor
Weight = 1
End
Begin Label LblStatus
Weight = 0
End
End
DIM currentFile AS STRING
DIM hlpFile AS STRING
DIM savedHash AS LONG
DIM dirty AS INTEGER
currentFile = ""
hlpFile = ""
savedHash = 0
dirty = 0
Load HelpEdit
HelpEdit.Show
' Configure the editor
Editor.SetSyntaxMode "dhs"
Editor.SetShowLineNumbers True
Editor.SetAutoIndent True
Editor.SetCaptureTabs True
Editor.SetTabWidth 2
' Start with a template
Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + "This is your help file. Edit the .dhs source here," + CHR$(10) + "then click Compile and Preview to see the result." + CHR$(10)
MarkClean
LblStatus.Caption = "Ready."
FUNCTION TextHash() AS LONG
DIM h AS LONG
DIM t AS STRING
t = Editor.Text
h = 0
DIM i AS INTEGER
FOR i = 1 TO LEN(t)
h = h * 31 + ASC(MID$(t, i, 1))
NEXT i
TextHash = h
END FUNCTION
FUNCTION IsDirty() AS INTEGER
IsDirty = (TextHash() <> savedHash)
END FUNCTION
FUNCTION AskSave() AS INTEGER
IF NOT IsDirty() THEN
AskSave = 0
EXIT FUNCTION
END IF
DIM result AS INTEGER
result = basPromptSave("Help Editor")
IF result = DVX_SAVE_YES THEN
FileSave
AskSave = 0
ELSEIF result = DVX_SAVE_NO THEN
AskSave = 0
ELSE
AskSave = 1
END IF
END FUNCTION
SUB UpdateTitle
DIM title AS STRING
IF currentFile <> "" THEN
title = currentFile + " - DVX Help Editor"
ELSE
title = "DVX Help Editor"
END IF
IF dirty THEN
title = "* " + title
END IF
HelpEdit.Caption = title
END SUB
SUB MarkClean
dirty = 0
savedHash = TextHash()
UpdateTitle
END SUB
SUB Editor_Change
IF NOT dirty THEN
dirty = -1
UpdateTitle
END IF
END SUB
SUB mnuNew_Click
IF AskSave() THEN
EXIT SUB
END IF
currentFile = ""
hlpFile = ""
Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10)
MarkClean
LblStatus.Caption = "New file."
UpdateTitle
END SUB
SUB mnuOpen_Click
IF AskSave() THEN
EXIT SUB
END IF
DIM path AS STRING
path = basFileOpen("Open Help Source", "Help Source (*.dhs)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
DIM text AS STRING
DIM ln AS STRING
text = ""
OPEN path FOR INPUT AS #1
DO WHILE NOT EOF(1)
LINE INPUT #1, ln
IF text <> "" THEN
text = text + CHR$(10)
END IF
text = text + ln
LOOP
CLOSE #1
Editor.Text = text
currentFile = path
hlpFile = ""
MarkClean
LblStatus.Caption = path
UpdateTitle
END SUB
SUB FileSave
IF currentFile = "" THEN
FileSaveAs
EXIT SUB
END IF
OPEN currentFile FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
MarkClean
LblStatus.Caption = "Saved: " + currentFile
UpdateTitle
END SUB
SUB FileSaveAs
DIM path AS STRING
path = basFileSave("Save Help Source", "Help Source (*.dhs)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
currentFile = path
FileSave
END SUB
SUB mnuSave_Click
FileSave
END SUB
SUB mnuSaveAs_Click
FileSaveAs
END SUB
SUB mnuExit_Click
Unload HelpEdit
END SUB
SUB DoCompile
' Save first
IF currentFile = "" THEN
FileSaveAs
IF currentFile = "" THEN
LblStatus.Caption = "Save cancelled."
EXIT SUB
END IF
ELSE
OPEN currentFile FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
MarkClean
END IF
' Derive .hlp filename from .dhs filename
DIM baseName AS STRING
baseName = currentFile
IF LEN(baseName) > 4 THEN
IF UCASE$(RIGHT$(baseName, 4)) = ".DHS" THEN
baseName = LEFT$(baseName, LEN(baseName) - 4)
END IF
END IF
hlpFile = baseName + ".hlp"
LblStatus.Caption = "Compiling..."
DoEvents
IF HelpCompile(currentFile, hlpFile) THEN
LblStatus.Caption = "Compiled: " + hlpFile
ELSE
LblStatus.Caption = "Compile failed!"
hlpFile = ""
END IF
END SUB
SUB mnuCompile_Click
DoCompile
END SUB
SUB mnuPreview_Click
IF hlpFile = "" THEN
DoCompile
END IF
IF hlpFile = "" THEN
EXIT SUB
END IF
LblStatus.Caption = "Opening viewer..."
HelpView hlpFile
END SUB
SUB mnuHelpTopic_Click
HelpView "dvxhelp.hlp"
END SUB
SUB HelpEdit_QueryUnload(Cancel AS INTEGER)
Cancel = AskSave()
END SUB

BIN
sdk/samples/basic/iconed/ICON32.BMP (Stored with Git LFS)

Binary file not shown.

View file

@ -5,9 +5,11 @@ Company = Kangaroo Punch Studios
Version = 1.00 Version = 1.00
Copyright = Copyright 2026 Scott Duensing Copyright = Copyright 2026 Scott Duensing
Description = Icon editor for DVX. Description = Icon editor for DVX.
Icon = ICON32.BMP
[Modules] [Modules]
Count = 0 File0 = ../../../include/basic/commdlg.bas
Count = 1
[Forms] [Forms]
File0 = iconed.frm File0 = iconed.frm

View file

@ -459,6 +459,23 @@ SUB cvEditor_MouseUp(button AS INTEGER, x AS INTEGER, y AS INTEGER)
drawing = False drawing = False
END SUB END SUB
SUB IconEd_QueryUnload(Cancel AS INTEGER)
IF NOT dirty THEN
EXIT SUB
END IF
DIM result AS INTEGER
result = basPromptSave("Icon Editor")
IF result = DVX_SAVE_YES THEN
mnuSave_Click
IF dirty THEN
Cancel = 1
END IF
ELSEIF result = DVX_SAVE_CANCEL THEN
Cancel = 1
END IF
END SUB
SUB cvPalette_MouseDown(button AS INTEGER, x AS INTEGER, y AS INTEGER) SUB cvPalette_MouseDown(button AS INTEGER, x AS INTEGER, y AS INTEGER)
DIM col AS INTEGER DIM col AS INTEGER
DIM row AS INTEGER DIM row AS INTEGER

BIN
sdk/samples/basic/imgview/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,16 @@
[Project]
Name = Image Viewer
Author = DVX Project
Description = BMP, PNG, JPEG, and GIF viewer
Icon = ICON32.BMP
[Modules]
File0 = ../../../include/basic/commdlg.bas
Count = 1
[Forms]
File0 = imgview.frm
Count = 1
[Settings]
StartupForm = ImgViewForm

View file

@ -0,0 +1,49 @@
VERSION DVX 1.00
Begin Form ImgViewForm
Caption = "Image Viewer"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 400
Height = 320
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuOpen
Caption = "&Open..."
End
Begin Menu mnuSep1
Caption = "-"
End
Begin Menu mnuClose
Caption = "&Close"
End
End
Begin Image ImgDisplay
Weight = 1
Stretch = True
End
End
'$INCLUDE: 'commdlg.bas'
SUB mnuOpen_Click
DIM path AS STRING
path = basFileOpen("Open Image", "Images (*.bmp;*.png;*.jpg;*.gif)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
ImgDisplay.Picture = path
ImgViewForm.Caption = path + " - Image Viewer"
END SUB
SUB mnuClose_Click
Unload ImgViewForm
END SUB
SUB ImgDisplay_DblClick
mnuOpen_Click
END SUB

BIN
sdk/samples/basic/notepad/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,16 @@
[Project]
Name = Notepad
Author = DVX Project
Description = Simple text editor
Icon = ICON32.BMP
[Modules]
File0 = ../../../include/basic/commdlg.bas
Count = 1
[Forms]
File0 = notepad.frm
Count = 1
[Settings]
StartupForm = NotepadForm

View file

@ -0,0 +1,237 @@
VERSION DVX 1.00
Begin Form NotepadForm
Caption = "Untitled - Notepad"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 480
Height = 320
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuNew
Caption = "&New"
End
Begin Menu mnuOpen
Caption = "&Open..."
End
Begin Menu mnuSep1
Caption = "-"
End
Begin Menu mnuSave
Caption = "&Save"
End
Begin Menu mnuSaveAs
Caption = "Save &As..."
End
Begin Menu mnuSep2
Caption = "-"
End
Begin Menu mnuExit
Caption = "E&xit"
End
End
Begin Menu mnuEdit
Caption = "&Edit"
Begin Menu mnuCut
Caption = "Cu&t"
End
Begin Menu mnuCopy
Caption = "&Copy"
End
Begin Menu mnuPaste
Caption = "&Paste"
End
Begin Menu mnuSep3
Caption = "-"
End
Begin Menu mnuSelAll
Caption = "Select &All"
End
End
Begin TextArea Editor
Weight = 1
End
End
'$INCLUDE: 'commdlg.bas'
DIM currentFile AS STRING
DIM savedHash AS LONG
DIM dirty AS INTEGER
currentFile = ""
savedHash = 0
dirty = 0
SUB NotepadForm_Load
Editor.SetCaptureTabs True
Editor.SetTabWidth 4
END SUB
FUNCTION TextHash() AS LONG
DIM h AS LONG
DIM t AS STRING
t = Editor.Text
h = 0
DIM i AS INTEGER
FOR i = 1 TO LEN(t)
h = h * 31 + ASC(MID$(t, i, 1))
NEXT i
TextHash = h
END FUNCTION
FUNCTION IsDirty() AS INTEGER
IsDirty = (TextHash() <> savedHash)
END FUNCTION
SUB UpdateTitle
DIM title AS STRING
IF currentFile <> "" THEN
title = currentFile + " - Notepad"
ELSE
title = "Untitled - Notepad"
END IF
IF dirty THEN
title = "* " + title
END IF
NotepadForm.Caption = title
END SUB
SUB MarkClean
dirty = 0
MarkClean
END SUB
SUB Editor_Change
IF NOT dirty THEN
dirty = -1
UpdateTitle
END IF
END SUB
FUNCTION AskSave() AS INTEGER
IF NOT IsDirty() THEN
AskSave = 0
EXIT FUNCTION
END IF
DIM result AS INTEGER
result = basPromptSave("Notepad")
IF result = DVX_SAVE_YES THEN
FileSave
AskSave = 0
ELSEIF result = DVX_SAVE_NO THEN
AskSave = 0
ELSE
AskSave = 1
END IF
END FUNCTION
SUB FileSave
IF currentFile = "" THEN
FileSaveAs
EXIT SUB
END IF
OPEN currentFile FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
MarkClean
END SUB
SUB FileSaveAs
DIM path AS STRING
path = basFileSave("Save As", "Text Files (*.txt)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
currentFile = path
FileSave
UpdateTitle
END SUB
SUB mnuNew_Click
IF AskSave() THEN
EXIT SUB
END IF
Editor.Text = ""
currentFile = ""
MarkClean
UpdateTitle
END SUB
SUB mnuOpen_Click
IF AskSave() THEN
EXIT SUB
END IF
DIM path AS STRING
path = basFileOpen("Open", "Text Files (*.txt)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
DIM text AS STRING
DIM ln AS STRING
text = ""
OPEN path FOR INPUT AS #1
DO WHILE NOT EOF(1)
LINE INPUT #1, ln
IF text <> "" THEN
text = text + CHR$(10)
END IF
text = text + ln
LOOP
CLOSE #1
Editor.Text = text
currentFile = path
MarkClean
UpdateTitle
END SUB
SUB mnuSave_Click
FileSave
END SUB
SUB mnuSaveAs_Click
FileSaveAs
END SUB
SUB mnuExit_Click
Unload NotepadForm
END SUB
SUB mnuCut_Click
Editor.Cut
END SUB
SUB mnuCopy_Click
Editor.Copy
END SUB
SUB mnuPaste_Click
Editor.Paste
END SUB
SUB mnuSelAll_Click
Editor.SelectAll
END SUB
SUB NotepadForm_QueryUnload(Cancel AS Integer)
Cancel = AskSave()
END SUB

BIN
sdk/samples/basic/resedit/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,17 @@
[Project]
Name = Resource Editor
Author = DVX Project
Description = DVX resource file editor
Icon = ICON32.BMP
[Modules]
File0 = ../../../include/basic/commdlg.bas
File1 = ../../../include/basic/resource.bas
Count = 2
[Forms]
File0 = resedit.frm
Count = 1
[Settings]
StartupForm = ResEdit

View file

@ -0,0 +1,431 @@
VERSION DVX 1.00
' resedit.frm -- DVX Resource Editor
'
' Graphical editor for the resource blocks appended to DXE3
' files (.app, .wgt, .lib). View, add, remove, and extract
' resources of type icon, text, or binary.
'
' Add commdlg.bas and resource.bas to your project, then click Run.
Begin Form ResEdit
Caption = "DVX Resource Editor"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 500
Height = 340
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuOpen
Caption = "&Open..."
End
Begin Menu mnuClose
Caption = "&Close"
Enabled = False
End
Begin Menu mnuSep1
Caption = "-"
End
Begin Menu mnuExit
Caption = "E&xit"
End
End
Begin Menu mnuResource
Caption = "&Resource"
Begin Menu mnuAddText
Caption = "Add &Text..."
Enabled = False
End
Begin Menu mnuAddFile
Caption = "Add &File..."
Enabled = False
End
Begin Menu mnuEditText
Caption = "&Edit Text..."
Enabled = False
End
Begin Menu mnuSep2
Caption = "-"
End
Begin Menu mnuExtract
Caption = "E&xtract..."
Enabled = False
End
Begin Menu mnuRemove
Caption = "&Remove"
Enabled = False
End
End
Begin ListView ResList
Weight = 1
End
Begin Label LblStatus
Caption = "No file loaded."
Weight = 0
End
End
OPTION EXPLICIT
DIM filePath AS STRING
DIM resHandle AS LONG
filePath = ""
resHandle = 0
Load ResEdit
ResEdit.Show
ResList.SetColumns "Name,20|Type,8|Size,12"
LblStatus.Caption = "Ready. Use File > Open to load a DXE file."
' ============================================================
' Type name helper
' ============================================================
FUNCTION TypeName$(t AS LONG)
IF t = RES_TYPE_ICON THEN
TypeName$ = "Icon"
ELSEIF t = RES_TYPE_TEXT THEN
TypeName$ = "Text"
ELSEIF t = RES_TYPE_BINARY THEN
TypeName$ = "Binary"
ELSE
TypeName$ = "Unknown"
END IF
END FUNCTION
' ============================================================
' Format a byte size for display
' ============================================================
FUNCTION FormatSize$(sz AS LONG)
IF sz < 1024 THEN
FormatSize$ = STR$(sz) + " B"
ELSE
FormatSize$ = STR$(sz \ 1024) + " KB"
END IF
END FUNCTION
' ============================================================
' Refresh the resource list from the open handle
' ============================================================
SUB RefreshList
ResList.Clear
IF resHandle = 0 THEN
EXIT SUB
END IF
DIM n AS LONG
n = ResCount(resHandle)
DIM ix AS LONG
FOR ix = 0 TO n - 1
ResList.AddItem ResName$(resHandle, ix)
ResList.SetCell ix, 1, TypeName$(ResType(resHandle, ix))
ResList.SetCell ix, 2, FormatSize$(ResSize(resHandle, ix))
NEXT ix
LblStatus.Caption = filePath + " - " + STR$(n) + " resource(s)"
END SUB
' ============================================================
' Close the current file
' ============================================================
SUB CloseFile
IF resHandle <> 0 THEN
ResClose resHandle
resHandle = 0
END IF
filePath = ""
ResList.Clear
ResEdit.Caption = "DVX Resource Editor"
mnuClose.Enabled = False
mnuAddText.Enabled = False
mnuAddFile.Enabled = False
mnuEditText.Enabled = False
mnuExtract.Enabled = False
mnuRemove.Enabled = False
LblStatus.Caption = "No file loaded."
END SUB
' ============================================================
' Reopen the file (after modification) and refresh
' ============================================================
SUB ReopenAndRefresh
DIM path AS STRING
path = filePath
IF resHandle <> 0 THEN
ResClose resHandle
resHandle = 0
END IF
resHandle = ResOpen(path)
IF resHandle = 0 THEN
' File may have had all resources stripped
filePath = path
ResList.Clear
LblStatus.Caption = path + " - 0 resource(s)"
ELSE
filePath = path
RefreshList
END IF
END SUB
' ============================================================
' Enable/disable selection-dependent menus
' ============================================================
SUB UpdateMenuState
DIM hasSel AS INTEGER
hasSel = (ResList.ListIndex >= 0)
mnuExtract.Enabled = hasSel
mnuRemove.Enabled = hasSel
mnuEditText.Enabled = hasSel
END SUB
' ============================================================
' Menu handlers
' ============================================================
SUB mnuOpen_Click
DIM path AS STRING
path = basFileOpen("Open DXE File", "Applications (*.app)|Widget Modules (*.wgt)|Libraries (*.lib)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
CloseFile
filePath = path
ResEdit.Caption = path + " - DVX Resource Editor"
mnuClose.Enabled = True
mnuAddText.Enabled = True
mnuAddFile.Enabled = True
resHandle = ResOpen(path)
IF resHandle = 0 THEN
LblStatus.Caption = "No resources in " + path
EXIT SUB
END IF
RefreshList
END SUB
SUB mnuClose_Click
CloseFile
END SUB
SUB mnuExit_Click
Unload ResEdit
END SUB
SUB mnuAddText_Click
IF filePath = "" THEN
EXIT SUB
END IF
DIM rName AS STRING
rName = basInputBox2("Add Text Resource", "Resource name:", "")
IF rName = "" THEN
EXIT SUB
END IF
DIM text AS STRING
text = basInputBox2("Add Text Resource", "Text value:", "")
IF ResAddText(filePath, rName, text) THEN
ReopenAndRefresh
LblStatus.Caption = "Added text resource: " + rName
ELSE
LblStatus.Caption = "Failed to add resource."
END IF
END SUB
SUB mnuAddFile_Click
IF filePath = "" THEN
EXIT SUB
END IF
DIM rName AS STRING
rName = basInputBox2("Add File Resource", "Resource name:", "")
IF rName = "" THEN
EXIT SUB
END IF
DIM typeChoice AS LONG
typeChoice = basChoiceDialog("Resource Type", "Select resource type:", "Icon|Binary", 0)
IF typeChoice < 0 THEN
EXIT SUB
END IF
DIM typeVal AS LONG
IF typeChoice = 0 THEN
typeVal = RES_TYPE_ICON
ELSE
typeVal = RES_TYPE_BINARY
END IF
DIM srcPath AS STRING
srcPath = basFileOpen("Select Source File", "All Files (*.*)")
IF srcPath = "" THEN
EXIT SUB
END IF
IF ResAddFile(filePath, rName, typeVal, srcPath) THEN
ReopenAndRefresh
LblStatus.Caption = "Added resource: " + rName
ELSE
LblStatus.Caption = "Failed to add resource."
END IF
END SUB
SUB mnuEditText_Click
IF filePath = "" THEN
EXIT SUB
END IF
DIM sel AS LONG
sel = ResList.ListIndex
IF sel < 0 THEN
EXIT SUB
END IF
DIM rName AS STRING
rName = ResName$(resHandle, sel)
DIM t AS LONG
t = ResType(resHandle, sel)
IF t <> RES_TYPE_TEXT THEN
LblStatus.Caption = "Only text resources can be edited inline."
EXIT SUB
END IF
DIM oldText AS STRING
oldText = ResGetText$(filePath, rName)
DIM newText AS STRING
newText = basInputBox2("Edit Text Resource", "Value for '" + rName + "':", oldText)
IF ResAddText(filePath, rName, newText) THEN
ReopenAndRefresh
LblStatus.Caption = "Updated: " + rName
ELSE
LblStatus.Caption = "Failed to update resource."
END IF
END SUB
SUB mnuExtract_Click
IF filePath = "" OR resHandle = 0 THEN
EXIT SUB
END IF
DIM sel AS LONG
sel = ResList.ListIndex
IF sel < 0 THEN
EXIT SUB
END IF
DIM rName AS STRING
rName = ResName$(resHandle, sel)
DIM outPath AS STRING
outPath = basFileSave("Extract Resource", "All Files (*.*)")
IF outPath = "" THEN
EXIT SUB
END IF
IF ResExtract(filePath, rName, outPath) THEN
LblStatus.Caption = "Extracted '" + rName + "' to " + outPath
ELSE
LblStatus.Caption = "Failed to extract resource."
END IF
END SUB
SUB mnuRemove_Click
IF filePath = "" THEN
EXIT SUB
END IF
DIM sel AS LONG
sel = ResList.ListIndex
IF sel < 0 THEN
EXIT SUB
END IF
DIM rName AS STRING
rName = ResName$(resHandle, sel)
DIM ans AS INTEGER
ans = MsgBox("Remove resource '" + rName + "'?", vbYesNo)
IF ans = vbNo THEN
EXIT SUB
END IF
' Close handle before modifying
ResClose resHandle
resHandle = 0
IF ResRemove(filePath, rName) THEN
ReopenAndRefresh
LblStatus.Caption = "Removed: " + rName
ELSE
ReopenAndRefresh
LblStatus.Caption = "Failed to remove resource."
END IF
END SUB
' ============================================================
' ListView selection change
' ============================================================
SUB ResList_Click
UpdateMenuState
END SUB
SUB ResList_DblClick
IF filePath = "" OR resHandle = 0 THEN
EXIT SUB
END IF
DIM sel AS LONG
sel = ResList.ListIndex
IF sel < 0 THEN
EXIT SUB
END IF
DIM t AS LONG
t = ResType(resHandle, sel)
IF t = RES_TYPE_TEXT THEN
mnuEditText_Click
ELSE
mnuExtract_Click
END IF
END SUB

View file

@ -284,9 +284,10 @@ int shellMain(int argc, char *argv[]) {
else if (strcmp(accelStr, "high") == 0) { accelVal = 32; } else if (strcmp(accelStr, "high") == 0) { accelVal = 32; }
int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", 8); int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", 8);
int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT);
dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal, speed); dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal, speed, wheelStep);
dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s speed=%ld", wheelStr, (long)dblClick, accelStr, (long)speed); dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s speed=%ld wheelspeed=%ld", wheelStr, (long)dblClick, accelStr, (long)speed, (long)wheelStep);
// Apply saved color scheme // Apply saved color scheme
bool colorsLoaded = false; bool colorsLoaded = false;

View file

@ -137,8 +137,8 @@ static void onTmRun(WidgetT *w) {
(void)w; (void)w;
FileFilterT filters[] = { FileFilterT filters[] = {
{ "Applications (*.app)", "*.app" }, { "Applications (*.app)" },
{ "All Files (*.*)", "*.*" } { "All Files (*.*)" }
}; };
char path[TM_MAX_PATH]; char path[TM_MAX_PATH];

View file

@ -352,10 +352,103 @@ static void writeBmp(const char *path) {
fclose(f); fclose(f);
} }
// Icon editor: pixel grid with pencil drawing a pixel
static void iconIconEd(void) {
clear(220, 220, 220);
// Draw a 5x5 grid of colored pixels (the "canvas")
int gx = 3;
int gy = 3;
int cs = 5;
// Grid lines
for (int i = 0; i <= 5; i++) {
hline(gx, gy + i * cs, 5 * cs + 1, 160, 160, 160);
vline(gx + i * cs, gy, 5 * cs + 1, 160, 160, 160);
}
// Some colored pixels in the grid
rect(gx + 1, gy + 1, cs - 1, cs - 1, 255, 0, 0);
rect(gx + cs + 1, gy + 1, cs - 1, cs - 1, 0, 0, 255);
rect(gx + 1, gy + cs + 1, cs - 1, cs - 1, 0, 180, 0);
rect(gx + cs + 1, gy + cs + 1, cs - 1, cs - 1, 255, 255, 0);
rect(gx + 2*cs+1, gy + 2*cs+1, cs - 1, cs - 1, 255, 128, 0);
rect(gx + 3*cs+1, gy + 1, cs - 1, cs - 1, 128, 0, 255);
// Pencil (diagonal, bottom-right area)
for (int i = 0; i < 12; i++) {
pixel(18 + i, 28 - i, 230, 200, 80);
pixel(19 + i, 28 - i, 230, 200, 80);
pixel(18 + i, 29 - i, 230, 200, 80);
}
// Pencil tip
pixel(17, 29, 40, 40, 40);
pixel(17, 30, 40, 40, 40);
pixel(18, 30, 40, 40, 40);
// Eraser end
pixel(29, 17, 240, 150, 150);
pixel(30, 16, 240, 150, 150);
pixel(30, 17, 240, 150, 150);
pixel(29, 16, 240, 150, 150);
// Border
rect(0, 0, 32, 1, 128, 128, 128);
rect(0, 31, 32, 1, 128, 128, 128);
rect(0, 0, 1, 32, 128, 128, 128);
rect(31, 0, 1, 32, 128, 128, 128);
}
// Resource editor: box with stacked resource entries
static void iconResedit(void) {
clear(220, 220, 220);
// Outer box (document frame)
rect(3, 2, 26, 28, 255, 255, 255);
rect(3, 2, 26, 1, 80, 80, 80);
rect(3, 29, 26, 1, 80, 80, 80);
rect(3, 2, 1, 28, 80, 80, 80);
rect(28, 2, 1, 28, 80, 80, 80);
// Title bar
rect(4, 3, 24, 4, 0, 0, 128);
// Resource entry rows (alternating light shading)
rect(4, 8, 24, 4, 240, 240, 255);
rect(4, 12, 24, 1, 180, 180, 200);
rect(4, 13, 24, 4, 255, 255, 255);
rect(4, 17, 24, 1, 180, 180, 200);
rect(4, 18, 24, 4, 240, 240, 255);
rect(4, 22, 24, 1, 180, 180, 200);
rect(4, 23, 24, 4, 255, 255, 255);
// Type indicator colored dots in each row
circle(8, 10, 1, 0, 160, 0); // icon (green)
circle(8, 15, 1, 0, 0, 200); // text (blue)
circle(8, 20, 1, 200, 0, 0); // binary (red)
circle(8, 25, 1, 0, 0, 200); // text (blue)
// Short "name" bars in each row
rect(11, 9, 10, 2, 60, 60, 60);
rect(11, 14, 14, 2, 60, 60, 60);
rect(11, 19, 8, 2, 60, 60, 60);
rect(11, 24, 12, 2, 60, 60, 60);
// Border
rect(0, 0, 32, 1, 128, 128, 128);
rect(0, 31, 32, 1, 128, 128, 128);
rect(0, 0, 1, 32, 128, 128, 128);
rect(31, 0, 1, 32, 128, 128, 128);
}
int main(int argc, char **argv) { int main(int argc, char **argv) {
if (argc < 3) { if (argc < 3) {
fprintf(stderr, "Usage: mkicon <output.bmp> <type>\n"); fprintf(stderr, "Usage: mkicon <output.bmp> <type>\n");
fprintf(stderr, "Types: noicon, clock, notepad, cpanel, dvxdemo, imgview, basic, help\n"); fprintf(stderr, "Types: noicon, clock, notepad, cpanel, dvxdemo, imgview, basic, help, iconed, resedit\n");
return 1; return 1;
} }
@ -378,6 +471,10 @@ int main(int argc, char **argv) {
iconImgview(); iconImgview();
} else if (strcmp(type, "help") == 0) { } else if (strcmp(type, "help") == 0) {
iconHelp(); iconHelp();
} else if (strcmp(type, "iconed") == 0) {
iconIconEd();
} else if (strcmp(type, "resedit") == 0) {
iconResedit();
} else { } else {
fprintf(stderr, "Unknown icon type: %s\n", type); fprintf(stderr, "Unknown icon type: %s\n", type);
return 1; return 1;

View file

@ -1024,6 +1024,13 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) {
} }
static int32_t wgtAnsiTermGetScrollback(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, 0);
AnsiTermDataT *at = (AnsiTermDataT *)w->data;
return at->scrollbackMax;
}
// ============================================================ // ============================================================
// BASIC-facing accessors // BASIC-facing accessors
// ============================================================ // ============================================================
@ -1072,7 +1079,7 @@ static const struct {
static const WgtPropDescT sProps[] = { static const WgtPropDescT sProps[] = {
{ "Cols", WGT_IFACE_INT, (void *)wgtAnsiTermGetCols, NULL, NULL }, { "Cols", WGT_IFACE_INT, (void *)wgtAnsiTermGetCols, NULL, NULL },
{ "Rows", WGT_IFACE_INT, (void *)wgtAnsiTermGetRows, NULL, NULL }, { "Rows", WGT_IFACE_INT, (void *)wgtAnsiTermGetRows, NULL, NULL },
{ "Scrollback", WGT_IFACE_INT, NULL, (void *)wgtAnsiTermSetScrollback, NULL } { "Scrollback", WGT_IFACE_INT, (void *)wgtAnsiTermGetScrollback, (void *)wgtAnsiTermSetScrollback, NULL }
}; };
static const WgtMethodDescT sMethods[] = { static const WgtMethodDescT sMethods[] = {

View file

@ -30,6 +30,8 @@ typedef struct {
bool pressed; bool pressed;
bool hasTransparency; bool hasTransparency;
uint32_t keyColor; uint32_t keyColor;
char picturePath[DVX_MAX_PATH];
bool stretch; // true = scale to fit widget bounds
} ImageDataT; } ImageDataT;
@ -55,8 +57,17 @@ void widgetImageDestroy(WidgetT *w) {
void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) { void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font; (void)font;
ImageDataT *d = (ImageDataT *)w->data; ImageDataT *d = (ImageDataT *)w->data;
if (d->stretch) {
// Stretch mode: don't force the widget to the image size.
// Let the layout engine determine the size; the paint method
// will scale the image to fit.
w->calcMinW = 0;
w->calcMinH = 0;
} else {
w->calcMinW = d->imgW; w->calcMinW = d->imgW;
w->calcMinH = d->imgH; w->calcMinH = d->imgH;
}
} }
@ -94,61 +105,108 @@ void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const Bit
(void)colors; (void)colors;
ImageDataT *d = (ImageDataT *)w->data; ImageDataT *d = (ImageDataT *)w->data;
if (!d->pixelData) { if (!d->pixelData || d->imgW <= 0 || d->imgH <= 0) {
return; return;
} }
// Center the image within the widget bounds
int32_t imgW = d->imgW; int32_t imgW = d->imgW;
int32_t imgH = d->imgH; int32_t imgH = d->imgH;
int32_t dx = w->x + (w->w - imgW) / 2; int32_t wgtW = w->w;
int32_t dy = w->y + (w->h - imgH) / 2; int32_t wgtH = w->h;
// Scale to fit (if stretch enabled) or use native size
int32_t fitW;
int32_t fitH;
if (d->stretch && (imgW > wgtW || imgH > wgtH)) {
fitW = wgtW;
fitH = (imgH * wgtW) / imgW;
if (fitH > wgtH) {
fitH = wgtH;
fitW = (imgW * wgtH) / imgH;
}
if (fitW <= 0) { fitW = 1; }
if (fitH <= 0) { fitH = 1; }
} else {
fitW = imgW;
fitH = imgH;
}
// Center in widget bounds
int32_t dx = w->x + (wgtW - fitW) / 2;
int32_t dy = w->y + (wgtH - fitH) / 2;
// Offset by 1px when pressed (button-press effect)
if (d->pressed) { if (d->pressed) {
dx++; dx++;
dy++; dy++;
} }
if (w->enabled) { // If image fits at 1:1, blit directly (no scaling needed)
if (d->hasTransparency) { if (fitW == imgW && fitH == imgH) {
uint8_t *src = w->enabled ? d->pixelData : d->grayData;
if (!src) {
src = d->pixelData;
}
if (d->hasTransparency && w->enabled) {
rectCopyTransparent(disp, ops, dx, dy, rectCopyTransparent(disp, ops, dx, dy,
d->pixelData, d->imgPitch, src, d->imgPitch,
0, 0, imgW, imgH, d->keyColor); 0, 0, imgW, imgH, d->keyColor);
} else { } else {
rectCopy(disp, ops, dx, dy, rectCopy(disp, ops, dx, dy,
d->pixelData, d->imgPitch, src, d->imgPitch,
0, 0, imgW, imgH); 0, 0, imgW, imgH);
} }
} else {
if (!d->grayData) {
int32_t bufSize = d->imgPitch * d->imgH;
d->grayData = (uint8_t *)malloc(bufSize);
if (d->grayData) {
DisplayT tmp = *disp;
tmp.backBuf = d->grayData;
tmp.width = d->imgW;
tmp.height = d->imgH;
tmp.pitch = d->imgPitch;
tmp.clipX = 0;
tmp.clipY = 0;
tmp.clipW = d->imgW;
tmp.clipH = d->imgH;
rectCopyGrayscale(&tmp, ops, 0, 0,
d->pixelData, d->imgPitch,
0, 0, d->imgW, d->imgH);
}
}
if (d->grayData) { return;
rectCopy(disp, ops, dx, dy, }
d->grayData, d->imgPitch,
0, 0, imgW, imgH); // Nearest-neighbor scale: sample source pixels directly into the
} else { // display backbuffer. This avoids allocating a scaled copy.
rectCopy(disp, ops, dx, dy, int32_t bpp = disp->format.bitsPerPixel;
d->pixelData, d->imgPitch,
0, 0, imgW, imgH); for (int32_t y = 0; y < fitH; y++) {
int32_t screenY = dy + y;
if (screenY < disp->clipY || screenY >= disp->clipY + disp->clipH) {
continue;
}
int32_t srcY = (y * imgH) / fitH;
if (srcY >= imgH) {
srcY = imgH - 1;
}
uint8_t *srcRow = d->pixelData + srcY * d->imgPitch;
for (int32_t x = 0; x < fitW; x++) {
int32_t screenX = dx + x;
if (screenX < disp->clipX || screenX >= disp->clipX + disp->clipW) {
continue;
}
int32_t srcX = (x * imgW) / fitW;
if (srcX >= imgW) {
srcX = imgW - 1;
}
// Copy one pixel from source to display
if (bpp == 16 || bpp == 15) {
uint16_t px = ((uint16_t *)srcRow)[srcX];
((uint16_t *)(disp->backBuf + screenY * disp->pitch))[screenX] = px;
} else if (bpp == 32) {
uint32_t px = ((uint32_t *)srcRow)[srcX];
((uint32_t *)(disp->backBuf + screenY * disp->pitch))[screenX] = px;
} else if (bpp == 8) {
uint8_t px = srcRow[srcX];
(disp->backBuf + screenY * disp->pitch)[screenX] = px;
}
} }
} }
} }
@ -261,19 +319,48 @@ static void wgtImageLoadFile(WidgetT *w, const char *path) {
return; return;
} }
dvxSetBusy(ctx, true);
int32_t imgW; int32_t imgW;
int32_t imgH; int32_t imgH;
int32_t pitch; int32_t pitch;
uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch);
dvxSetBusy(ctx, false);
if (!buf) { if (!buf) {
return; return;
} }
ImageDataT *d = (ImageDataT *)w->data;
snprintf(d->picturePath, sizeof(d->picturePath), "%s", path);
wgtImageSetData(w, buf, imgW, imgH, pitch); wgtImageSetData(w, buf, imgW, imgH, pitch);
} }
static bool wgtImageGetStretch(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, false);
ImageDataT *d = (ImageDataT *)w->data;
return d->stretch;
}
static void wgtImageSetStretch(WidgetT *w, bool stretch) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ImageDataT *d = (ImageDataT *)w->data;
d->stretch = stretch;
wgtInvalidatePaint(w);
}
static const char *wgtImageGetPicture(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, "");
ImageDataT *d = (ImageDataT *)w->data;
return d->picturePath;
}
static int32_t wgtImageGetWidth(const WidgetT *w) { static int32_t wgtImageGetWidth(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, 0); VALIDATE_WIDGET(w, sTypeId, 0);
ImageDataT *d = (ImageDataT *)w->data; ImageDataT *d = (ImageDataT *)w->data;
@ -308,15 +395,16 @@ static const struct {
}; };
static const WgtPropDescT sProps[] = { static const WgtPropDescT sProps[] = {
{ "Picture", WGT_IFACE_STRING, NULL, (void *)wgtImageLoadFile, NULL }, { "ImageHeight", WGT_IFACE_INT, (void *)wgtImageGetHeight, NULL, NULL },
{ "ImageWidth", WGT_IFACE_INT, (void *)wgtImageGetWidth, NULL, NULL }, { "ImageWidth", WGT_IFACE_INT, (void *)wgtImageGetWidth, NULL, NULL },
{ "ImageHeight", WGT_IFACE_INT, (void *)wgtImageGetHeight, NULL, NULL } { "Picture", WGT_IFACE_STRING, (void *)wgtImageGetPicture, (void *)wgtImageLoadFile, NULL },
{ "Stretch", WGT_IFACE_BOOL, (void *)wgtImageGetStretch, (void *)wgtImageSetStretch, NULL }
}; };
static const WgtIfaceT sIface = { static const WgtIfaceT sIface = {
.basName = "Image", .basName = "Image",
.props = sProps, .props = sProps,
.propCount = 3, .propCount = 4,
.methods = NULL, .methods = NULL,
.methodCount = 0, .methodCount = 0,
.events = NULL, .events = NULL,

View file

@ -28,6 +28,7 @@ typedef struct {
int32_t imgH; int32_t imgH;
int32_t imgPitch; int32_t imgPitch;
bool pressed; bool pressed;
char picturePath[DVX_MAX_PATH];
} ImageButtonDataT; } ImageButtonDataT;
@ -324,10 +325,20 @@ static void wgtImageButtonLoadFile(WidgetT *w, const char *path) {
return; return;
} }
ImageButtonDataT *d = (ImageButtonDataT *)w->data;
snprintf(d->picturePath, sizeof(d->picturePath), "%s", path);
wgtImageButtonSetData(w, buf, imgW, imgH, pitch); wgtImageButtonSetData(w, buf, imgW, imgH, pitch);
} }
static const char *wgtImageButtonGetPicture(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, "");
ImageButtonDataT *d = (ImageButtonDataT *)w->data;
return d->picturePath;
}
static int32_t wgtImageButtonGetWidth(const WidgetT *w) { static int32_t wgtImageButtonGetWidth(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, 0); VALIDATE_WIDGET(w, sTypeId, 0);
ImageButtonDataT *d = (ImageButtonDataT *)w->data; ImageButtonDataT *d = (ImageButtonDataT *)w->data;
@ -360,7 +371,7 @@ static const struct {
}; };
static const WgtPropDescT sImgBtnProps[] = { static const WgtPropDescT sImgBtnProps[] = {
{ "Picture", WGT_IFACE_STRING, NULL, (void *)wgtImageButtonLoadFile, NULL }, { "Picture", WGT_IFACE_STRING, (void *)wgtImageButtonGetPicture, (void *)wgtImageButtonLoadFile, NULL },
{ "ImageWidth", WGT_IFACE_INT, (void *)wgtImageButtonGetWidth, NULL, NULL }, { "ImageWidth", WGT_IFACE_INT, (void *)wgtImageButtonGetWidth, NULL, NULL },
{ "ImageHeight", WGT_IFACE_INT, (void *)wgtImageButtonGetHeight, NULL, NULL } { "ImageHeight", WGT_IFACE_INT, (void *)wgtImageButtonGetHeight, NULL, NULL }
}; };

View file

@ -101,6 +101,10 @@ typedef struct {
char **ownedCells; // stb_ds dynamic array of strdup'd strings char **ownedCells; // stb_ds dynamic array of strdup'd strings
bool ownsCells; // true if cellData points to ownedCells bool ownsCells; // true if cellData points to ownedCells
int32_t nextCell; // next cell position for AddItem (row-major index) int32_t nextCell; // next cell position for AddItem (row-major index)
// Owned column definitions (deep copies for BASIC callers)
ListViewColT *ownedColDefs; // heap array of column defs (NULL if not owned)
char **ownedColTitles; // heap array of strdup'd title strings
int32_t ownedColCount;
} ListViewDataT; } ListViewDataT;
@ -369,6 +373,13 @@ void widgetListViewDestroy(WidgetT *w) {
free(lv->ownedCells[i]); free(lv->ownedCells[i]);
} }
arrfree(lv->ownedCells); arrfree(lv->ownedCells);
for (int32_t i = 0; i < lv->ownedColCount; i++) {
free(lv->ownedColTitles[i]);
}
free(lv->ownedColTitles);
free(lv->ownedColDefs);
free(lv->selBits); free(lv->selBits);
free(lv->sortIndex); free(lv->sortIndex);
free(lv); free(lv);
@ -1673,14 +1684,20 @@ void wgtListViewAddItem(WidgetT *w, const char *text) {
return; return;
} }
// If at a row boundary, add colCount empty cells to start a new row // Always align to the next row boundary so AddItem always starts a new row,
if (lv->nextCell % lv->colCount == 0) { // even if SetCell was used to fill remaining columns of the previous row.
int32_t rem = lv->nextCell % lv->colCount;
if (rem != 0) {
lv->nextCell += lv->colCount - rem;
}
// Add colCount empty cells for the new row
for (int32_t c = 0; c < lv->colCount; c++) { for (int32_t c = 0; c < lv->colCount; c++) {
arrput(lv->ownedCells, strdup("")); arrput(lv->ownedCells, strdup(""));
} }
}
// Replace the next empty cell with the provided text // Set the first cell of the new row to the provided text
int32_t idx = lv->nextCell; int32_t idx = lv->nextCell;
free(lv->ownedCells[idx]); free(lv->ownedCells[idx]);
lv->ownedCells[idx] = strdup(text ? text : ""); lv->ownedCells[idx] = strdup(text ? text : "");
@ -1945,15 +1962,61 @@ static void basSetColumns(WidgetT *w, const char *spec) {
return; return;
} }
static char buf[1024]; VALIDATE_WIDGET_VOID(w, sTypeId);
ListViewDataT *lv = (ListViewDataT *)w->data;
// Free previous owned columns
for (int32_t i = 0; i < lv->ownedColCount; i++) {
free(lv->ownedColTitles[i]);
}
free(lv->ownedColTitles);
free(lv->ownedColDefs);
lv->ownedColTitles = NULL;
lv->ownedColDefs = NULL;
lv->ownedColCount = 0;
// Parse spec into a temporary buffer
char buf[1024];
snprintf(buf, sizeof(buf), "%s", spec); snprintf(buf, sizeof(buf), "%s", spec);
ListViewColT cols[BAS_MAX_LISTVIEW_COLS]; // First pass: count columns
int32_t count = 0; int32_t count = 0;
char *p = buf;
while (*p) {
count++;
char *sep = strchr(p, '|');
if (sep) {
p = sep + 1;
} else {
break;
}
}
if (count <= 0 || count > BAS_MAX_LISTVIEW_COLS) {
return;
}
// Allocate owned storage
lv->ownedColDefs = (ListViewColT *)calloc(count, sizeof(ListViewColT));
lv->ownedColTitles = (char **)calloc(count, sizeof(char *));
if (!lv->ownedColDefs || !lv->ownedColTitles) {
free(lv->ownedColDefs);
free(lv->ownedColTitles);
lv->ownedColDefs = NULL;
lv->ownedColTitles = NULL;
return;
}
// Second pass: parse and deep-copy
snprintf(buf, sizeof(buf), "%s", spec);
int32_t idx = 0;
char *tok = buf; char *tok = buf;
while (*tok && count < BAS_MAX_LISTVIEW_COLS) { while (*tok && idx < count) {
// Parse "Title,Width" or just "Title"
char *sep = strchr(tok, '|'); char *sep = strchr(tok, '|');
if (sep) { if (sep) {
@ -1964,16 +2027,18 @@ static void basSetColumns(WidgetT *w, const char *spec) {
if (comma) { if (comma) {
*comma = '\0'; *comma = '\0';
cols[count].title = tok; lv->ownedColTitles[idx] = strdup(tok);
cols[count].width = wgtChars(atoi(comma + 1)); lv->ownedColDefs[idx].title = lv->ownedColTitles[idx];
cols[count].align = ListViewAlignLeftE; lv->ownedColDefs[idx].width = wgtChars(atoi(comma + 1));
lv->ownedColDefs[idx].align = ListViewAlignLeftE;
} else { } else {
cols[count].title = tok; lv->ownedColTitles[idx] = strdup(tok);
cols[count].width = 0; lv->ownedColDefs[idx].title = lv->ownedColTitles[idx];
cols[count].align = ListViewAlignLeftE; lv->ownedColDefs[idx].width = 0;
lv->ownedColDefs[idx].align = ListViewAlignLeftE;
} }
count++; idx++;
if (sep) { if (sep) {
tok = sep + 1; tok = sep + 1;
@ -1982,9 +2047,8 @@ static void basSetColumns(WidgetT *w, const char *spec) {
} }
} }
if (count > 0) { lv->ownedColCount = idx;
wgtListViewSetColumns(w, cols, count); wgtListViewSetColumns(w, lv->ownedColDefs, idx);
}
} }

View file

@ -3760,7 +3760,7 @@ static const WgtIfaceT sIfaceTextArea = {
.events = NULL, .events = NULL,
.eventCount = 0, .eventCount = 0,
.createSig = WGT_CREATE_PARENT_INT, .createSig = WGT_CREATE_PARENT_INT,
.createArgs = { 4096 }, .createArgs = { 65536 },
.defaultEvent = "Change" .defaultEvent = "Change"
}; };

View file

@ -133,6 +133,16 @@ bool wgtTimerIsRunning(const WidgetT *w) {
} }
int32_t wgtTimerGetInterval(const WidgetT *w) {
if (!w || w->type != sTypeId) {
return 0;
}
TimerDataT *d = (TimerDataT *)w->data;
return d->intervalMs;
}
void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs) { void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs) {
if (!w || w->type != sTypeId) { if (!w || w->type != sTypeId) {
return; return;
@ -230,7 +240,7 @@ static const struct {
static const WgtPropDescT sProps[] = { static const WgtPropDescT sProps[] = {
{ "Enabled", WGT_IFACE_BOOL, (void *)wgtTimerIsRunning, (void *)wgtTimerSetEnabled, NULL }, { "Enabled", WGT_IFACE_BOOL, (void *)wgtTimerIsRunning, (void *)wgtTimerSetEnabled, NULL },
{ "Interval", WGT_IFACE_INT, NULL, (void *)wgtTimerSetInterval, NULL } { "Interval", WGT_IFACE_INT, (void *)wgtTimerGetInterval, (void *)wgtTimerSetInterval, NULL }
}; };
static const WgtMethodDescT sMethods[] = { static const WgtMethodDescT sMethods[] = {