Compare commits

..

68 commits

Author SHA1 Message Date
36c6eeaf81 Fixes for issues found by GCC analyzer. 2026-04-22 21:26:49 -05:00
d9889b2fbb Fixes for issues found by cppcheck. 2026-04-22 21:05:31 -05:00
60d24c8c33 Bug fixes. 2026-04-22 20:33:49 -05:00
4cdcfe6b8c More BASIC widget debugging. 2026-04-20 22:12:23 -05:00
4600f3e631 Minor platform cleanup. 2026-04-20 20:30:46 -05:00
1affec7e8c Major BASIC runtime work. 2026-04-20 20:20:05 -05:00
a8c38267bc More cleanup. Major docs update. 2026-04-18 17:00:18 -05:00
05e69f60a7 License added to source files. 2026-04-17 23:23:44 -05:00
217f138d07 License assigned. 2026-04-17 19:42:18 -05:00
48fb1c30ae Major code cleanup. 2026-04-17 19:36:05 -05:00
e6305db3b5 Major project layout change. Source locations now more match where binaries end up. 2026-04-16 19:33:17 -05:00
dcd120d769 Many BASIC compiler and VM fixes. Other fixes. New BASIC apps. 2026-04-16 18:18:04 -05:00
de60200f23 We can make binaries from BASIC! 2026-04-15 15:15:09 -05:00
7157f97c28 Help compiler integrated with DVX. 2026-04-14 00:43:17 -05:00
66952306df Some HTML help cleanup. 2026-04-13 22:34:15 -05:00
8abe947b8b Transparent image support. Image alignment in help. 2026-04-13 22:04:09 -05:00
3db721fdc0 Accelerator keys cleaned up. 2026-04-13 21:00:38 -05:00
7c1eb495e1 Several more minor widget fixes. 2026-04-13 20:57:05 -05:00
094b263c36 Several widget fixes after optimizing repainting. 2026-04-13 20:39:59 -05:00
5f305dd14c Bug and performance fixes. Lots of crap I forgot. 2026-04-13 20:03:24 -05:00
454a3620f7 An insane number of performance, logic, and feature enhancements; bug fixess; and other things. 2026-04-12 22:26:18 -05:00
3c886a97f6 Fixed merge order of help files. 2026-04-09 19:03:01 -05:00
fe68899020 Cleaning up HTML help. 2026-04-09 01:32:03 -05:00
5ece172ad0 Reorganizing things preparing for next testing release. SDK added! 2026-04-09 01:27:40 -05:00
c43c586f2f Dynamic help recompilation. Reorganization for third-party developers. 2026-04-08 23:35:59 -05:00
10ba408465 Help system and viewer seem to be working. 2026-04-08 20:16:16 -05:00
2a641f42c3 Now using -Wall -Werror (for real). All warnings fixed. Help system added. 2026-04-07 18:28:39 -05:00
0f5da09d1f Docs added. 2026-04-06 22:25:06 -05:00
7ec2aead8f TextArea now supports actual tab characters. Added additional data cached to improve performance. 2026-04-06 21:37:16 -05:00
2a2a386592 Added InputBox 2026-04-06 21:26:24 -05:00
1923886d42 Immediate window wired to running VM. 2026-04-06 20:31:07 -05:00
af0ad3091f Debug Layout option added. 2026-04-06 19:31:39 -05:00
5f2358fcf2 Breakpoint window added. 2026-04-06 19:06:49 -05:00
35f877d3e1 Debugger is coming together! 2026-04-06 18:38:56 -05:00
85010d17dc Breakpoints seem to be working. 2026-04-06 15:36:47 -05:00
de7027c44e New splash screen. Debugger. Starting to debug the debugger. 2026-04-05 19:01:44 -05:00
626befa664 VM will no longer block cooperative multitasking. 2026-04-05 00:09:45 -05:00
eb5e4e567e External library support. DBGrid widget. More data binding. App path properties. 2026-04-05 00:04:11 -05:00
827d73fbd1 Data binding! 2026-04-04 20:00:25 -05:00
d3898707f9 SQL support added! 2026-04-04 18:35:04 -05:00
93b912d932 Layout logic moved from core into widgets. 2026-04-04 15:50:58 -05:00
7cd7388607 WrapBox added. 2026-04-04 15:42:27 -05:00
7bc92549f7 Menu editor. Focus logic cleanup. Several bugs fixed. 2026-04-03 21:22:08 -05:00
e36d4b9cec New form scope bug fixing. 2026-04-03 19:29:09 -05:00
bf5bf9bb1d Form level variable scope. Proper Load/Unload. 2026-04-03 18:09:50 -05:00
dd68a19b5b Control arrays added. 2026-04-03 17:14:05 -05:00
657b44eb25 MsgBox() 2026-04-03 16:41:02 -05:00
0ef46ff6a0 Find/Replace 2026-04-03 16:11:24 -05:00
289adb8c47 Readmes updated. 2026-04-02 21:44:06 -05:00
6d75e4996a More event work, code cleanup. 2026-04-02 21:13:42 -05:00
88746ec2ba ProgMan now requires double clicks to launch. Enum type added to widgets. Huge amount of BASIC work. 2026-04-02 20:06:48 -05:00
17fe1840e3 Compiler fixes. Editor fixes. Fixes fixes. 2026-04-01 21:54:27 -05:00
4d4aedbc43 More events added and wired to BASIC. 2026-04-01 17:53:16 -05:00
1fb8e2a387 Working on BASIC code editor. 2026-03-31 21:58:06 -05:00
82939c3f27 Adding project support and breaking a lot of stuff. 2026-03-30 22:19:38 -05:00
5dd632a862 Removed some DOS-only format specifiers. 2026-03-29 22:16:41 -05:00
344ab4794d More IDE changes and a few core changes to help support easier app development. 2026-03-29 22:06:01 -05:00
d094205ed0 Tons of work on the form designer and widget system to support it. 2026-03-29 21:06:29 -05:00
59bc2b5ed3 More IDE work. 2026-03-28 21:14:38 -05:00
7d74d76985 Start of form designer. 2026-03-27 23:02:49 -05:00
5171753250 IDE fixes. 2026-03-27 19:40:28 -05:00
51ade9f119 All warnings fixed/silenced. 2026-03-27 19:21:11 -05:00
0043e06c82 Working on removing accidentally committed binaries. 2026-03-27 19:08:12 -05:00
65d7a252ca BASIC is getting to be pretty stable. 2026-03-27 18:59:58 -05:00
b3cc66be4b BASIC is starting to work. 2026-03-27 01:42:41 -05:00
f62b89fc02 Dynamic library support added to BASIC. 2026-03-27 01:01:38 -05:00
aa961425c9 Initial DVX BASIC Compiler and VM. 2026-03-27 00:40:17 -05:00
89690ca97c Added Alpha Release 3. 2026-03-26 21:46:14 -05:00
850 changed files with 328650 additions and 20816 deletions

4
.gitattributes vendored
View file

@ -2,5 +2,9 @@
*.BMP filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.JPG filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.PNG filter=lfs diff=lfs merge=lfs -text
*.xcf filter=lfs diff=lfs merge=lfs -text
*.XCF filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.ZIP filter=lfs diff=lfs merge=lfs -text

3
.gitignore vendored
View file

@ -2,8 +2,11 @@ dosbench/
bin/
obj/
lib/
*~
*.~
.gitignore~
.gitattributes~
*.SWP
.claude/
capture/
just-stuff/

21
LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2026 Scott Duensing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

212
Makefile
View file

@ -1,57 +1,197 @@
# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# DVX GUI -- Top-level Makefile
#
# Builds the full DVX stack: core library, task switcher,
# bootstrap loader, text help library, widgets, shell, and apps.
#
# Source tree mirrors bin/ layout:
# src/libs/kpunch/<libname>/ -> bin/libs/kpunch/<libname>/<libname>.lib
# src/widgets/kpunch/<widget>/ -> bin/widgets/kpunch/<widget>/<widget>.wgt
# src/apps/kpunch/<app>/ -> bin/apps/kpunch/<app>/<app>.app
# src/loader/ -> bin/DVX.EXE
# src/tools/ -> bin/host/<tool>
.PHONY: all clean core tasks loader texthelp listhelp widgets shell taskmgr serial apps tools
.PHONY: all clean libdvx libtasks loader texthelp listhelp widgets dvxshell taskmgr serial sql apps tools deploy-helpsrc compile-help deploy-sdk
all: core tasks loader texthelp listhelp widgets shell taskmgr serial apps tools
all: libdvx libtasks loader texthelp listhelp tools widgets dvxshell taskmgr serial sql apps deploy-helpsrc compile-help deploy-sdk
core:
$(MAKE) -C core
libdvx:
$(MAKE) -C src/libs/kpunch/libdvx
tasks:
$(MAKE) -C tasks
libtasks:
$(MAKE) -C src/libs/kpunch/libtasks
loader: core tasks
$(MAKE) -C loader
loader: libdvx libtasks
$(MAKE) -C src/loader
texthelp: core tasks
$(MAKE) -C texthelp
texthelp: libdvx libtasks
$(MAKE) -C src/libs/kpunch/texthelp
listhelp: core tasks
$(MAKE) -C listhelp
listhelp: libdvx libtasks
$(MAKE) -C src/libs/kpunch/listhelp
widgets: core tasks texthelp listhelp
$(MAKE) -C widgets
widgets: libdvx libtasks texthelp listhelp
$(MAKE) -C src/widgets/kpunch
shell: core tasks
$(MAKE) -C shell
dvxshell: libdvx libtasks
$(MAKE) -C src/libs/kpunch/dvxshell
taskmgr: shell
$(MAKE) -C taskmgr
taskmgr: dvxshell
$(MAKE) -C src/libs/kpunch/taskmgr
serial: core tasks
$(MAKE) -C serial
serial: libdvx libtasks
$(MAKE) -C src/libs/kpunch/serial
sql: libdvx libtasks
$(MAKE) -C src/libs/kpunch/sql
tools:
$(MAKE) -C tools
$(MAKE) -C src/tools
apps: core tasks shell tools
$(MAKE) -C apps
apps: libdvx libtasks dvxshell tools
$(MAKE) -C src/apps/kpunch
deploy-helpsrc:
$(MAKE) -C src/tools deploy-helpsrc
HLPC = bin/host/dvxhlpc
SYSTEM_DHS = src/libs/kpunch/libdvx/sysdoc.dhs \
src/libs/kpunch/libdvx/arch.dhs src/libs/kpunch/libdvx/apiref.dhs \
src/libs/kpunch/libtasks/libtasks.dhs src/libs/kpunch/dvxshell/dvxshell.dhs src/libs/kpunch/sql/dvxsql.dhs \
src/libs/kpunch/texthelp/texthelp.dhs src/libs/kpunch/listhelp/listhelp.dhs \
src/libs/kpunch/taskmgr/taskmgr.dhs src/libs/kpunch/serial/serial.dhs \
src/apps/kpunch/dvxbasic/basrt.dhs \
src/widgets/kpunch/wgtsys.dhs
BASIC_DHS = src/apps/kpunch/dvxbasic/ideguide.dhs src/apps/kpunch/dvxbasic/langref.dhs \
src/apps/kpunch/dvxbasic/ctrlover.dhs src/apps/kpunch/dvxbasic/form.dhs
compile-help:
@mkdir -p docs
$(HLPC) -o bin/apps/kpunch/dvxhelp/dvxhelp.hlp \
--html docs/dvx_help_viewer.html \
src/apps/kpunch/dvxhelp/help.dhs
$(HLPC) -o bin/apps/kpunch/progman/dvxhelp.hlp \
--html docs/dvx_system_reference.html \
-i src/libs/kpunch/libdvx \
$(SYSTEM_DHS) \
$$(find src/widgets/kpunch -name "*.dhs" ! -path "*/wgtsys.dhs" | sort)
$(HLPC) -o bin/apps/kpunch/dvxbasic/dvxbasic.hlp \
--html docs/dvx_basic_reference.html \
$(BASIC_DHS) \
$$(find src/widgets/kpunch -name "*.bhs" | sort)
SDKDIR = bin/sdk
deploy-sdk:
@echo "Building SDK..."
@mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow
@# Core headers (libdvx public API)
@for f in src/libs/kpunch/libdvx/dvxApp.h src/libs/kpunch/libdvx/dvxTypes.h \
src/libs/kpunch/libdvx/dvxWgt.h src/libs/kpunch/libdvx/dvxWgtP.h \
src/libs/kpunch/libdvx/dvxWm.h src/libs/kpunch/libdvx/dvxDraw.h \
src/libs/kpunch/libdvx/dvxVideo.h src/libs/kpunch/libdvx/dvxComp.h \
src/libs/kpunch/libdvx/dvxPrefs.h src/libs/kpunch/libdvx/dvxDlg.h \
src/libs/kpunch/libdvx/dvxRes.h src/libs/kpunch/libdvx/dvxFont.h \
src/libs/kpunch/libdvx/dvxCur.h src/libs/kpunch/libdvx/dvxPal.h \
src/libs/kpunch/libdvx/dvxMem.h src/libs/kpunch/libdvx/platform/dvxPlat.h; do \
[ -f "$$f" ] && cp "$$f" $(SDKDIR)/include/core/; \
done
@# Shell header
@cp src/libs/kpunch/dvxshell/shellApp.h $(SDKDIR)/include/shell/
@cp src/libs/kpunch/dvxshell/shellInf.h $(SDKDIR)/include/shell/
@# Tasks header
@cp src/libs/kpunch/libtasks/taskSwch.h $(SDKDIR)/include/tasks/
@# SQL header
@cp src/libs/kpunch/sql/dvxSql.h $(SDKDIR)/include/sql/
@# Widget headers -- one subdir per widget
@for d in src/widgets/kpunch/*/; do \
for h in "$$d"*.h; do \
[ -f "$$h" ] || continue; \
wgt=$$(basename "$$d"); \
mkdir -p $(SDKDIR)/include/widget/"$$wgt"; \
cp "$$h" $(SDKDIR)/include/widget/"$$wgt"/; \
done; \
done
@# BASIC include files
@cp src/include/basic/*.bas $(SDKDIR)/include/basic/
@# BASIC sample: basdemo (project file + form + icon)
@cp src/apps/kpunch/basdemo/basdemo.dbp $(SDKDIR)/samples/basic/basdemo/
@cp src/apps/kpunch/basdemo/basdemo.frm $(SDKDIR)/samples/basic/basdemo/
@cp src/apps/kpunch/basdemo/ICON32.BMP $(SDKDIR)/samples/basic/basdemo/
@# BASIC sample: widshow (widget showcase)
@cp src/apps/kpunch/widshow/widshow.dbp $(SDKDIR)/samples/basic/widshow/
@cp src/apps/kpunch/widshow/widshow.frm $(SDKDIR)/samples/basic/widshow/
@cp src/apps/kpunch/widshow/ICON32.BMP $(SDKDIR)/samples/basic/widshow/
@# README
@printf '%s\n' \
'DVX SDK' \
'=======' \
'' \
'Headers for developing applications and widgets against the DVX GUI.' \
'' \
'Directory Structure' \
'-------------------' \
'' \
' include/' \
' core/ DVX core headers (types, drawing, windows, widgets)' \
' shell/ Shell/app loading API' \
' tasks/ Cooperative task switching API' \
' sql/ SQLite database wrapper API' \
' widget/ Per-widget public API headers' \
' basic/ BASIC include files (DECLARE LIBRARY modules)' \
' samples/' \
' basic/ Example BASIC projects (open in the DVX BASIC IDE)' \
'' \
'Requirements' \
'------------' \
'' \
' - DJGPP cross-compiler (i586-pc-msdosdjgpp-gcc)' \
' - dxe3gen (included with DJGPP)' \
' - DVX resource compiler (SYSTEM/DVXRES.EXE on the target,' \
' or bin/host/dvxres on the host)' \
' - DVX help compiler (SYSTEM/DVXHLPC.EXE on the target,' \
' or bin/host/dvxhlpc on the host)' \
'' \
'For complete working examples, see the app sources under' \
'src/apps/kpunch/ in the DVX repository.' \
> $(SDKDIR)/README.TXT
clean:
$(MAKE) -C core clean
$(MAKE) -C tasks clean
$(MAKE) -C loader clean
$(MAKE) -C texthelp clean
$(MAKE) -C listhelp clean
$(MAKE) -C widgets clean
$(MAKE) -C shell clean
$(MAKE) -C taskmgr clean
$(MAKE) -C serial clean
$(MAKE) -C apps clean
$(MAKE) -C tools clean
$(MAKE) -C src/libs/kpunch/libdvx clean
$(MAKE) -C src/libs/kpunch/libtasks clean
$(MAKE) -C src/loader clean
$(MAKE) -C src/libs/kpunch/texthelp clean
$(MAKE) -C src/libs/kpunch/listhelp clean
$(MAKE) -C src/widgets/kpunch clean
$(MAKE) -C src/libs/kpunch/dvxshell clean
$(MAKE) -C src/libs/kpunch/taskmgr clean
$(MAKE) -C src/libs/kpunch/serial clean
$(MAKE) -C src/libs/kpunch/sql clean
$(MAKE) -C src/apps/kpunch clean
$(MAKE) -C src/tools clean
-rmdir obj 2>/dev/null
-rm -rf bin/config bin/widgets bin/libs
-rmdir bin/apps/cpanel bin/apps/imgview bin/apps/progman bin/apps/notepad bin/apps/clock bin/apps/dvxdemo bin/apps bin 2>/dev/null
-rm -rf bin/config bin/widgets bin/libs bin/sdk
-rm -f docs/*.html

309
README.md
View file

@ -1,219 +1,110 @@
# DVX -- DOS Visual eXecutive
A windowed GUI compositor and widget toolkit targeting DJGPP/DPMI on
486+ hardware. VESA VBE 2.0+ LFB only. Motif-inspired visual style
with 2px bevels, fixed bitmap fonts, and dirty-rect compositing.
DVX is a windowed GUI environment and application platform for DOS.
It provides a Motif-inspired desktop, a cooperative multitasking kernel,
a widget toolkit, a compiled BASIC language with a visual form designer,
and a small set of bundled applications.
The system runs on 86Box (primary target) and real DOS hardware.
The system is a compositor built on VESA VBE 2.0+ linear framebuffer
video modes. A dirty-rectangle compositor keeps redraw cost proportional
to what actually changes on screen, so the desktop is responsive even at
1024x768 on modest hardware.
## Architecture
## What's Included
DVX is built as a set of DXE3 dynamic modules loaded by a bootstrap
executable. The loader resolves dependencies and loads modules in
topological order, then hands control to the shell.
```
dvx.exe (loader)
|
+-- libs/libtasks.lib cooperative task switcher
+-- libs/libdvx.lib core GUI (draw, comp, wm, app, widget infra)
+-- libs/texthelp.lib shared text editing helpers
+-- libs/listhelp.lib shared list/dropdown helpers
+-- libs/dvxshell.lib shell (app lifecycle, desktop)
+-- libs/taskmgr.lib task manager (Ctrl+Esc, separate DXE)
|
+-- widgets/*.wgt 26 widget type plugins
|
+-- apps/*/*.app DXE applications
```
## Directory Structure
| Directory | Output | Description |
|--------------|-------------------------|--------------------------------------------|
| `loader/` | `bin/dvx.exe` | Bootstrap loader, platform layer, stb_ds |
| `core/` | `bin/libs/libdvx.lib` | Core GUI library (5 layers + widget infra) |
| `tasks/` | `bin/libs/libtasks.lib` | Cooperative task switching |
| `texthelp/` | `bin/libs/texthelp.lib` | Shared text editing helpers |
| `listhelp/` | `bin/libs/listhelp.lib` | Shared list/dropdown helpers |
| `shell/` | `bin/libs/dvxshell.lib` | DVX Shell (app lifecycle, desktop) |
| `taskmgr/` | `bin/libs/taskmgr.lib` | Task Manager (separate DXE, Ctrl+Esc) |
| `widgets/` | `bin/widgets/*.wgt` | 26 individual widget DXE modules |
| `apps/` | `bin/apps/*/*.app` | Application DXE modules |
| `tools/` | `bin/dvxres` | Resource tool (host native, not DXE) |
| `config/` | `bin/config/`, `bin/libs/`, `bin/widgets/` | INI config, themes, wallpapers, dep files |
| `rs232/` | `lib/librs232.a` | ISR-driven UART serial driver |
| `packet/` | `lib/libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
| `security/` | `lib/libsecurity.a` | DH key exchange, XTEA-CTR cipher, RNG |
| `seclink/` | `lib/libseclink.a` | Secure serial link (channels, encryption) |
| `proxy/` | `bin/secproxy` | Linux proxy: 86Box <-> telnet BBS |
| `termdemo/` | `bin/termdemo.exe` | Standalone encrypted terminal demo |
## Build
Requires the DJGPP cross-compiler at `~/djgpp/djgpp`.
```
make # build everything
make clean # remove all build artifacts
./mkcd.sh # build + create ISO for 86Box
```
The top-level Makefile builds in dependency order:
```
core -> tasks -> loader -> texthelp -> listhelp -> widgets -> shell -> taskmgr -> apps
tools (host native, parallel)
```
Build output goes to `bin/` (executables, DXE modules, config) and
`obj/` (intermediate object files).
## Runtime Directory Layout (bin/)
```
bin/
dvx.exe bootstrap loader (entry point)
libs/
libtasks.lib task switching library
libdvx.lib core GUI library
texthelp.lib text editing helpers
listhelp.lib list/dropdown helpers
dvxshell.lib DVX shell
taskmgr.lib task manager (Ctrl+Esc)
*.dep dependency files for load ordering
widgets/
box.wgt VBox/HBox/Frame containers
button.wgt push button
... (26 widget modules total)
*.dep dependency files
apps/
progman/progman.app Program Manager (desktop)
notepad/notepad.app text editor
clock/clock.app clock display
dvxdemo/dvxdemo.app widget showcase
cpanel/cpanel.app control panel
imgview/imgview.app image viewer
config/
dvx.ini system configuration
themes/ color theme files (.thm)
wpaper/ wallpaper images
```
## Core Architecture (5 Layers)
1. **dvxVideo** -- VESA VBE init, LFB mapping, backbuffer, pixel format, color packing
2. **dvxDraw** -- Rectangle fills, bevels, text rendering, cursor/icon drawing
3. **dvxComp** -- Dirty rectangle tracking, merge, compositor, LFB flush
4. **dvxWm** -- Window stack, chrome, drag/resize, menus, scrollbars, hit testing
5. **dvxApp** -- Event loop, input polling, public API, color schemes, wallpaper
## Widget System
Widgets are isolated DXE modules. Core knows nothing about individual
widget types -- no compile-time enum, no union, no per-widget structs in
dvxWidget.h.
* **Dynamic type IDs**: `wgtRegisterClass()` assigns IDs at load time
* **void *data**: Each widget allocates its own private data struct
* **ABI-stable dispatch**: `WidgetClassT.handlers[]` is an array of function
pointers indexed by `WGT_METHOD_*` constants (0-20, room for 32). Core
dispatches via `w->wclass->handlers[WGT_METHOD_PAINT]` etc., so adding
new methods does not break existing widget DXE binaries
* **Generic drag**: `WGT_METHOD_ON_DRAG_UPDATE` and `WGT_METHOD_ON_DRAG_END`
provide widget-level drag support without per-widget hacks in core
* **Per-widget API registry**: `wgtRegisterApi("name", &api)` replaces the monolithic API
* **Per-widget headers**: `widgets/widgetButton.h` etc. provide typed macros
* **Shared helpers**: texthelp.lib (text editing) and listhelp.lib (dropdown/list)
* **All limits dynamic**: widget child arrays, app slots, and desktop callbacks
are stb_ds dynamic arrays with no fixed maximums
## DXE Module System
All modules are DXE3 dynamic libraries loaded by dvx.exe at startup.
The loader recursively scans `libs/` for `.lib` files and `widgets/` for `.wgt`
files, reads `.dep` files for dependencies, and loads in topological
order. Widget modules that export `wgtRegister()` have it called after
loading.
Each `.dep` file lists base names of modules that must load first, one
per line. Comments start with `#`.
## App Types
**Callback-only** (`hasMainLoop = false`): `appMain` creates windows,
registers callbacks, and returns. The app lives through event callbacks
in the shell's main loop. No dedicated task or stack needed.
**Main-loop** (`hasMainLoop = true`): A cooperative task is created for
the app. `appMain` runs its own loop calling `tsYield()` to share CPU.
Used for apps that need continuous processing (clocks, terminal
emulators, games).
## Crash Recovery
The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL
before loading any apps, so even crashes during app initialization are
caught. If an app crashes, the handler `longjmp`s back to the shell's
main loop, the crashed app is force-killed, and the shell continues
running. Diagnostic information (registers, faulting EIP) is logged to
`dvx.log`.
## Bundled Applications
| App | File | Type | Description |
|-----------------|---------------|-----------|------------------------------------------------------------|
| Program Manager | `progman.app` | Callback | App launcher grid, system info dialog |
| Notepad | `notepad.app` | Callback | Text editor with File/Edit menus, open/save, clipboard |
| Clock | `clock.app` | Main-loop | Digital clock display; multi-instance capable |
| DVX Demo | `dvxdemo.app` | Callback | Widget system showcase demonstrating 31 widget types |
| Control Panel | `cpanel.app` | Callback | Themes, wallpaper, video mode, mouse configuration |
| Image Viewer | `imgview.app` | Callback | Displays BMP, PNG, JPEG, GIF images with aspect-ratio zoom |
## Serial / Networking Stack
A layered encrypted serial communications stack for connecting DVX to
remote systems (BBS, etc.) through 86Box's emulated UART:
| Layer | Library | Description |
|----------|------------------|---------------------------------------------------|
| rs232 | `librs232.a` | ISR-driven UART driver, ring buffers, flow control |
| packet | `libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
| security | `libsecurity.a` | 1024-bit DH, XTEA-CTR cipher, DRBG RNG |
| seclink | `libseclink.a` | Channel multiplexing, per-packet encryption |
| proxy | `secproxy` | Linux bridge: 86Box serial <-> telnet BBS |
## Third-Party Dependencies
All third-party code is vendored as single-header libraries:
| Library | Location | Purpose |
|-------------------|--------------------|---------------------------------|
| stb_image.h | `core/thirdparty/` | Image loading (BMP, PNG, JPEG, GIF) |
| stb_image_write.h | `core/thirdparty/` | PNG export for screenshots |
| stb_ds.h | `core/thirdparty/` | Dynamic arrays and hash maps |
stb_ds implementation is compiled into dvx.exe (the loader) with
`STBDS_REALLOC`/`STBDS_FREE` overridden to use `dvxRealloc`/`dvxFree`,
so all `arrput`/`arrfree` calls in DXE code are tracked per-app. The
functions are exported via `dlregsym` to all DXE modules.
* **DVX Shell** -- desktop, app lifecycle manager, Task Manager, crash
recovery. The shell is the program the user interacts with and the
host for all running applications.
* **Widget toolkit** -- 31 widget types as plug-in modules: buttons,
labels, text inputs, list boxes, tree views, tab controls, sliders,
spinners, progress bars, canvases, scroll panes, splitters, an ANSI
terminal, and more.
* **DVX BASIC** -- a Visual Basic 3 compatible language with a visual
form designer, per-procedure code editor, project management, and a
bytecode compiler and VM. BASIC programs compile to standalone DVX
applications.
* **Help system** -- a full hypertext help viewer with table of contents,
keyword index, full-text search, and inline images. Help files are
compiled from a plain-text source format.
* **Serial / networking stack** -- an ISR-driven UART driver, HDLC
packet framing with CRC-16 and Go-Back-N reliable delivery, 1024-bit
Diffie-Hellman key exchange, and XTEA-CTR encryption, plus a Linux
bridge that connects a virtual serial port to a telnet BBS.
* **Control Panel** -- themes, wallpapers, video mode, mouse
configuration, all with live preview.
* **Bundled applications** -- Program Manager (desktop launcher),
Notepad, Clock, Image Viewer, DVX Demo, Control Panel, DVX BASIC
IDE, Help Viewer, Resource Editor, Icon Editor, Help Editor, BASIC
Demo.
## Target Hardware
* CPU: 486 baseline, Pentium-optimized paths where significant
* Video: VESA VBE 2.0+ with LFB (no bank switching)
* Platform: 86Box emulator (trusted reference), real DOS hardware
* Resolutions: 640x480, 800x600, 1024x768 at 8/15/16/32 bpp
* DOS with a 386-class CPU or newer (486 recommended, Pentium for best
performance)
* VESA BIOS Extensions 2.0 or newer with linear framebuffer support
* 4 MB of extended memory minimum; 8 MB or more recommended
* Any resolution the video card reports at 8, 15, 16, 24, or 32 bits
per pixel. 640x480, 800x600, and 1024x768 are the common cases.
* Two-button mouse; scroll wheel supported if a compatible driver is
loaded
## Building and Deploying
```
make # build everything
make clean # remove all build artifacts
./mkcd.sh # build and produce a CD-ROM ISO image
```
`make` builds in dependency order: core, tasks, loader, helpers,
host tools, widgets, shell, task manager, apps. Build output goes to
`bin/` and intermediate objects to `obj/`.
`./mkcd.sh` builds everything, then wraps `bin/` into an ISO 9660
image for use with an emulator or for burning to physical media.
## Directory Layout
```
dvxgui/
assets/ logo and splash artwork
bin/ build output (runtime tree; mirrors what ships)
config/ shipped INI, themes, wallpapers
docs/ HTML exports of the built-in help content
LICENSE.txt MIT license
Makefile top-level build
mkcd.sh build-and-package script
src/
apps/ bundled applications
include/ shared include files (BASIC library declarations)
libs/ core libraries (GUI, tasks, shell, helpers, serial, SQL)
loader/ bootstrap executable
tools/ host-native development tools and the BASIC compiler
widgets/ widget plug-in modules
```
See `src/tools/README.md` for the host-side development tools
(resource tool, icon generators, help compiler, proxy), and
`src/apps/kpunch/README.md` for the SDK contract applications must
follow.
## Documentation
Full reference documentation ships with the system as browsable help:
* `docs/dvx_system_reference.html` -- system API, widget catalog
* `docs/dvx_basic_reference.html` -- DVX BASIC language reference
* `docs/dvx_help_viewer.html` -- help viewer guide
The same content is available from inside DVX via the Help Viewer and
Program Manager.
## License
MIT. See `LICENSE.txt`.

54
analyze.sh Executable file
View file

@ -0,0 +1,54 @@
#!/bin/bash
#
# analyze.sh -- run the DJGPP build under gcc's -fanalyzer.
#
# Usage:
# ./analyze.sh # analyse the whole build (slow: 2-5x)
# ./analyze.sh 2>&1 | tee analyze.log
#
# How it works:
#
# The project Makefiles hardcode -Werror in CFLAGS. gcc evaluates
# diagnostic flags in order, so a trailing -Wno-error (appended
# AFTER CFLAGS at compile time) demotes analyzer findings back to
# warnings -- otherwise the first analyzer hit aborts the build.
#
# We do that by setting CC to a wrapper that execs the real compiler
# with -fanalyzer prepended and -Wno-error appended. make -k keeps
# building after individual failures so we see every hit in one run.
#
# Runs the existing top-level Makefile with an override CC; no
# Makefile edits required. Output goes to stderr; redirect with
# '2>&1 | tee analyze.log' to capture.
set -u
cd "$(dirname "$0")"
REAL_CC="$HOME/djgpp/djgpp/bin/i586-pc-msdosdjgpp-gcc"
if [ ! -x "$REAL_CC" ]; then
echo "analyze.sh: $REAL_CC not executable" >&2
exit 1
fi
# Build the wrapper in a temp dir that the spawned makes can see.
WRAP_DIR="$(mktemp -d)"
trap 'rm -rf "$WRAP_DIR"' EXIT
WRAP="$WRAP_DIR/analyze-cc"
cat > "$WRAP" <<WRAPEOF
#!/bin/bash
exec "$REAL_CC" -fanalyzer "\$@" -Wno-error
WRAPEOF
chmod +x "$WRAP"
echo "analyze.sh: running full build under gcc -fanalyzer..." >&2
echo "analyze.sh: this will be slow (2-5x normal build time)." >&2
# -k = keep going after errors so we collect every analyzer hit
# CC=... overrides the assignment in each submakefile
make -k CC="$WRAP" 2>&1
echo "analyze.sh: done. Grep for 'Wanalyzer' to see findings." >&2

View file

@ -1,107 +0,0 @@
# DVX Shell Applications Makefile -- builds DXE3 modules
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../core/thirdparty -I../widgets -I../tasks -I../core/thirdparty -I../shell
OBJDIR = ../obj/apps
BINDIR = ../bin/apps
DVXRES = ../bin/dvxres
# App definitions: each is a subdir with a single .c file
APPS = progman notepad clock dvxdemo cpanel imgview
.PHONY: all clean $(APPS)
all: $(APPS)
cpanel: $(BINDIR)/cpanel/cpanel.app
imgview: $(BINDIR)/imgview/imgview.app
progman: $(BINDIR)/progman/progman.app
notepad: $(BINDIR)/notepad/notepad.app
clock: $(BINDIR)/clock/clock.app
dvxdemo: $(BINDIR)/dvxdemo/dvxdemo.app
$(BINDIR)/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/cpanel
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
$(DVXRES) build $@ cpanel/cpanel.res
$(BINDIR)/imgview/imgview.app: $(OBJDIR)/imgview.o imgview/imgview.res imgview/icon32.bmp | $(BINDIR)/imgview
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
$(DVXRES) build $@ imgview/imgview.res
$(BINDIR)/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/progman
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
$(BINDIR)/notepad/notepad.app: $(OBJDIR)/notepad.o notepad/notepad.res notepad/icon32.bmp | $(BINDIR)/notepad
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
$(DVXRES) build $@ notepad/notepad.res
$(BINDIR)/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/clock
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -E _appShutdown -U $<
$(DVXRES) build $@ clock/clock.res
DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp
$(BINDIR)/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/dvxdemo
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
$(DVXRES) build $@ dvxdemo/dvxdemo.res
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/dvxdemo/
$(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/imgview.o: imgview/imgview.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/progman.o: progman/progman.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/notepad.o: notepad/notepad.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/clock.o: clock/clock.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(BINDIR)/cpanel:
mkdir -p $(BINDIR)/cpanel
$(BINDIR)/imgview:
mkdir -p $(BINDIR)/imgview
$(BINDIR)/progman:
mkdir -p $(BINDIR)/progman
$(BINDIR)/notepad:
mkdir -p $(BINDIR)/notepad
$(BINDIR)/clock:
mkdir -p $(BINDIR)/clock
$(BINDIR)/dvxdemo:
mkdir -p $(BINDIR)/dvxdemo
# Dependencies
$(OBJDIR)/imgview.o: imgview/imgview.c ../core/dvxApp.h ../core/dvxDialog.h ../core/dvxWidget.h ../core/dvxWm.h ../core/dvxVideo.h ../shell/shellApp.h
$(OBJDIR)/cpanel.o: cpanel/cpanel.c ../core/dvxApp.h ../core/dvxDialog.h ../core/dvxPrefs.h ../core/dvxWidget.h ../core/dvxWm.h ../core/platform/dvxPlatform.h ../shell/shellApp.h
$(OBJDIR)/progman.o: progman/progman.c ../core/dvxApp.h ../core/dvxDialog.h ../core/dvxWidget.h ../core/dvxWm.h ../shell/shellApp.h ../shell/shellInfo.h
$(OBJDIR)/notepad.o: notepad/notepad.c ../core/dvxApp.h ../core/dvxDialog.h ../core/dvxWidget.h ../core/dvxWm.h ../shell/shellApp.h
$(OBJDIR)/clock.o: clock/clock.c ../core/dvxApp.h ../core/dvxWidget.h ../core/dvxDraw.h ../core/dvxVideo.h ../shell/shellApp.h ../tasks/taskswitch.h
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c ../core/dvxApp.h ../core/dvxDialog.h ../core/dvxWidget.h ../core/dvxWm.h ../core/dvxVideo.h ../shell/shellApp.h
clean:
rm -f $(OBJDIR)/cpanel.o $(OBJDIR)/imgview.o $(OBJDIR)/progman.o $(OBJDIR)/notepad.o $(OBJDIR)/clock.o $(OBJDIR)/dvxdemo.o
rm -f $(BINDIR)/cpanel/cpanel.app
rm -f $(BINDIR)/imgview/imgview.app
rm -f $(BINDIR)/progman/progman.app
rm -f $(BINDIR)/notepad/notepad.app
rm -f $(BINDIR)/clock/clock.app
rm -f $(BINDIR)/dvxdemo/dvxdemo.app $(addprefix $(BINDIR)/dvxdemo/,$(DVXDEMO_BMPS))

View file

@ -1,178 +0,0 @@
# DVX Shell Applications
DXE3 application modules for the DVX Shell. Each app is a `.app` file
(DXE3 shared object) in a subdirectory under `apps/`. The Program
Manager scans this directory at startup and displays all discovered
apps as launchable icons.
## DXE App Contract
Every app exports two symbols:
* `appDescriptor` (`AppDescriptorT`) -- name, hasMainLoop, multiInstance, stackSize, priority
* `appMain` (`int appMain(DxeAppContextT *)`) -- entry point
Optional: `appShutdown` (`void appShutdown(void)`) -- called during
graceful shutdown.
### Callback-Only Apps (hasMainLoop = false)
`appMain()` creates windows, registers callbacks, and returns 0. The
shell drives everything through event callbacks. No dedicated task or
stack is allocated. Lifecycle ends when the last window closes.
### Main-Loop Apps (hasMainLoop = true)
A cooperative task is created for the app. `appMain()` runs its own
loop calling `tsYield()` to share CPU. Lifecycle ends when `appMain()`
returns.
## Applications
### Program Manager (progman)
| | |
|---|---|
| File | `apps/progman/progman.app` |
| Type | Callback-only |
| Multi-instance | No |
The desktop app. Scans `apps/` recursively for `.app` files and
displays them in a grid. Double-click or Enter launches an app.
Includes a Help menu with system information dialog and About box.
Registers with `shellRegisterDesktopUpdate()` to refresh when apps
are loaded, crash, or terminate.
Widget headers used: `widgetBox.h`, `widgetButton.h`, `widgetLabel.h`,
`widgetStatusBar.h`, `widgetTextInput.h`.
### Notepad (notepad)
| | |
|---|---|
| File | `apps/notepad/notepad.app` |
| Type | Callback-only |
| Multi-instance | Yes |
Text editor with File menu (New, Open, Save, Save As) and Edit menu
(Cut, Copy, Paste, Select All). Uses the TextArea widget for all
editing. 32KB text buffer limit. Tracks dirty state for save prompts.
Keyboard accelerators for all menu commands.
Widget headers used: `widgetTextInput.h`.
### Clock (clock)
| | |
|---|---|
| File | `apps/clock/clock.app` |
| Type | Main-loop |
| Multi-instance | Yes |
Digital clock display showing time and date. Demonstrates the main-loop
app pattern -- polls the system clock every second and invalidates the
window to trigger a repaint. Uses raw `onPaint` callbacks (no widgets)
to draw centered text.
Widget headers used: none (raw paint callbacks).
### DVX Demo (dvxdemo)
| | |
|---|---|
| File | `apps/dvxdemo/dvxdemo.app` |
| Type | Callback-only |
| Multi-instance | No |
Comprehensive widget system showcase. Opens multiple windows
demonstrating 31 of the 32 widget types (all except Timer):
* Main window: raw paint callbacks (gradients, patterns, text)
* Widget demo: form widgets (TextInput, Checkbox, Radio, ListBox)
* Controls window: TabControl with tabs for advanced widgets
(Dropdown, ProgressBar, Slider, Spinner, TreeView, ListView,
ScrollPane, Toolbar, Canvas, Splitter, Image)
* Terminal window: AnsiTerm widget
Widget headers used: 25 of the 26 widget headers (all except widgetTimer.h).
Resources: `logo.bmp`, `new.bmp`, `open.bmp`, `sample.bmp`, `save.bmp`.
### Control Panel (cpanel)
| | |
|---|---|
| File | `apps/cpanel/cpanel.app` |
| Type | Callback-only |
| Multi-instance | No |
System configuration with four tabs:
* **Mouse** -- scroll direction, double-click speed, acceleration
* **Colors** -- all 20 system colors with live preview, theme load/save
* **Desktop** -- wallpaper image selection and display mode
* **Video** -- resolution and color depth switching
Changes preview live. OK saves to `DVX.INI`, Cancel reverts to the
state captured when the control panel was opened.
Widget headers used: `widgetBox.h`, `widgetButton.h`, `widgetCanvas.h`,
`widgetDropdown.h`, `widgetLabel.h`, `widgetListBox.h`, `widgetSlider.h`,
`widgetSpacer.h`, `widgetTabControl.h`.
### Image Viewer (imgview)
| | |
|---|---|
| File | `apps/imgview/imgview.app` |
| Type | Callback-only |
| Multi-instance | Yes |
Displays BMP, PNG, JPEG, and GIF images. The image is scaled to fit
the window while preserving aspect ratio. Resize the window to zoom.
Open files via the File menu or by launching with Run in the Task
Manager.
Widget headers used: none (raw paint callbacks with `dvxLoadImage()`).
## Build
```
make # builds all 6 app DXE modules
make clean # removes objects and app files
```
Each app compiles to a single `.o`, then is packaged via `dxe3gen`
into a `.app` DXE exporting `appDescriptor` and `appMain` (plus
`appShutdown` for clock).
Output goes to `bin/apps/<name>/<name>.app`.
## Files
```
apps/
Makefile top-level build for all apps
progman/
progman.c Program Manager
notepad/
notepad.c text editor
clock/
clock.c digital clock
dvxdemo/
dvxdemo.c widget demo
logo.bmp DVX logo bitmap
new.bmp toolbar icon
open.bmp toolbar icon
sample.bmp sample image
save.bmp toolbar icon
cpanel/
cpanel.c control panel
imgview/
imgview.c image viewer
```

View file

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

View file

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

View file

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

View file

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

View file

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

BIN
assets/DVX Help Logo.xcf (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/DVX Logo.xcf (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/DVX Text.xcf (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/help.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/splash.bmp (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -29,6 +29,8 @@ build. Text files are converted to DOS line endings (CR+LF) via sed.
| `listbox.dep` | `bin/widgets/listbox.dep` | ListBox widget dep file |
| `listview.dep` | `bin/widgets/listview.dep` | ListView widget dep file |
| `treeview.dep` | `bin/widgets/treeview.dep` | TreeView widget dep file |
| `basrt.dep` | `bin/libs/basrt.dep` | BASIC runtime dep file |
| `serial.dep` | `bin/libs/serial.dep` | Serial communications dep file |
## dvx.ini Format
@ -126,6 +128,8 @@ ignored. Names are case-insensitive.
| `texthelp.dep` | texthelp.lib | libtasks, libdvx |
| `listhelp.dep` | listhelp.lib | libtasks, libdvx |
| `dvxshell.dep` | dvxshell.lib | libtasks, libdvx, texthelp, listhelp |
| `basrt.dep` | basrt.lib | libtasks, libdvx |
| `serial.dep` | serial.lib | libtasks, libdvx |
### Widget Dependencies

View file

@ -5,22 +5,26 @@
; Supported color depths: 8, 15, 16, 24, 32
[video]
width = 640
height = 480
width = 1024
height = 768
bpp = 16
; Mouse settings.
; wheel: normal or reversed
; wheelspeed: lines per wheel notch (1-10, default 3)
; doubleclick: double-click speed in milliseconds (200-900, default 500)
; acceleration: off, low, medium, high (default medium)
; speed: cursor speed (2-32, default 8; higher = faster)
[mouse]
wheel = normal
wheelspeed = 3
doubleclick = 500
acceleration = medium
speed = 8
; Shell settings.
; desktop: path to the desktop app loaded at startup
[shell]
desktop = apps/progman/progman.app
desktop = apps/kpunch/progman/progman.app

25
config/themes/hotdog.thm Normal file
View file

@ -0,0 +1,25 @@
; DVX Color Theme - Hot Dog Stand
; The infamous Windows 3.1 color scheme. Yellow, red, black.
; Included here for historical authenticity and eye strain.
[colors]
desktop = 255,255,0
windowFace = 255,255,0
windowHighlight = 255,255,255
windowShadow = 0,0,0
activeTitleBg = 255,0,0
activeTitleFg = 255,255,255
inactiveTitleBg = 255,255,255
inactiveTitleFg = 0,0,0
contentBg = 255,255,255
contentFg = 0,0,0
menuBg = 255,255,255
menuFg = 0,0,0
menuHighlightBg = 255,0,0
menuHighlightFg = 255,255,255
buttonFace = 255,255,0
scrollbarBg = 255,255,0
scrollbarFg = 0,0,0
scrollbarTrough = 255,255,255
cursorColor = 255,255,0
cursorOutline = 0,0,0

View file

@ -1,74 +0,0 @@
# DVX Core Library Makefile for DJGPP cross-compilation
#
# Builds libdvx.lib -- core GUI infrastructure (draw, compositor,
# window manager, event dispatch, layout engine, widget infrastructure).
# Zero widget implementations -- those are in ../widgets/ as .wgt modules.
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I. -Iplatform -I../tasks -Ithirdparty
OBJDIR = ../obj/core
LIBSDIR = ../bin/libs
# Core sources
SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxImageWrite.c \
dvxApp.c dvxDialog.c dvxPrefs.c dvxResource.c \
widgetClass.c widgetCore.c widgetScrollbar.c \
widgetLayout.c widgetEvent.c widgetOps.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGET = $(LIBSDIR)/libdvx.lib
# libdvx.lib export prefixes
DVX_EXPORTS = -E _dvx -E _wgt -E _wm -E _prefs -E _rect -E _draw -E _pack -E _text \
-E _setClip -E _resetClip -E _stbi_ -E _stbi_write -E _dirtyList \
-E _widget \
-E _sCursor -E _sDbl -E _sDebug -E _sClosed -E _sFocused -E _sKey \
-E _sOpen -E _sPressed -E _sDrag -E _sDrawing -E _sResize \
-E _sListView -E _sSplitter -E _sTreeView \
-E _accelParse -E _clipboard -E _multiClick
.PHONY: all clean
all: $(TARGET) $(LIBSDIR)/libdvx.dep
$(LIBSDIR)/libdvx.dep: ../config/libdvx.dep | $(LIBSDIR)
sed 's/$$/\r/' $< > $@
$(TARGET): $(OBJS) | $(LIBSDIR)
$(DXE3GEN) -o $(LIBSDIR)/libdvx.dxe $(DVX_EXPORTS) -U $(OBJS)
mv $(LIBSDIR)/libdvx.dxe $@
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(LIBSDIR):
mkdir -p $(LIBSDIR)
# Dependencies
CORE_HDRS = dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h dvxWidget.h platform/dvxPlatform.h
$(OBJDIR)/dvxVideo.o: dvxVideo.c dvxVideo.h platform/dvxPlatform.h dvxTypes.h dvxPalette.h
$(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h platform/dvxPlatform.h dvxTypes.h
$(OBJDIR)/dvxComp.o: dvxComp.c dvxComp.h platform/dvxPlatform.h dvxTypes.h
$(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h dvxWidget.h thirdparty/stb_image.h
$(OBJDIR)/dvxImage.o: dvxImage.c thirdparty/stb_image.h
$(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h
$(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h platform/dvxPlatform.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h
$(OBJDIR)/dvxDialog.o: dvxDialog.c dvxDialog.h platform/dvxPlatform.h dvxApp.h dvxWidget.h dvxWidgetPlugin.h dvxTypes.h dvxDraw.h
$(OBJDIR)/dvxPrefs.o: dvxPrefs.c dvxPrefs.h
WIDGET_DEPS = dvxWidgetPlugin.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h
$(OBJDIR)/widgetClass.o: widgetClass.c $(WIDGET_DEPS)
$(OBJDIR)/widgetCore.o: widgetCore.c $(WIDGET_DEPS)
$(OBJDIR)/widgetScrollbar.o: widgetScrollbar.c $(WIDGET_DEPS)
$(OBJDIR)/widgetLayout.o: widgetLayout.c $(WIDGET_DEPS)
$(OBJDIR)/widgetEvent.o: widgetEvent.c $(WIDGET_DEPS)
$(OBJDIR)/widgetOps.o: widgetOps.c $(WIDGET_DEPS)
clean:
rm -f $(OBJS) $(TARGET) $(LIBSDIR)/libdvx.dep

View file

@ -1,449 +0,0 @@
# DVX Core Library (libdvx.lib)
The core GUI infrastructure for DVX, built as a DXE3 module. Provides
VESA video setup, 2D drawing primitives, dirty-rectangle compositing,
a window manager with Motif-style chrome, and the widget infrastructure
(layout engine, event dispatch, class registration). Individual widget
type implementations live in `../widgets/` as separate `.wgt` DXE
modules that register themselves at runtime via `wgtRegisterClass()`.
Core knows nothing about individual widget types. There is no
WidgetTypeE enum, no widget union, and no per-widget structs in
dvxWidget.h. All widget-specific behavior is dispatched through the
WidgetClassT dispatch table.
## 5-Layer Architecture
| Layer | Header | Source | Description |
|-------|--------|--------|-------------|
| 1. Video | `dvxVideo.h` | `dvxVideo.c` | VESA VBE init, LFB mapping, backbuffer, pixel format, `packColor()` |
| 2. Draw | `dvxDraw.h` | `dvxDraw.c` | Rect fills, bevels, text, bitmap cursors, focus rects, lines |
| 3. Compositor | `dvxComp.h` | `dvxComp.c` | Dirty rect tracking, merge, clip, LFB flush |
| 4. Window Manager | `dvxWm.h` | `dvxWm.c` | Window stack, chrome, drag/resize, menus, scrollbars, hit test |
| 5. Application | `dvxApp.h` | `dvxApp.c` | Event loop, input polling, color schemes, wallpaper, public API |
Additional modules built into libdvx.lib:
| Header | Source | Description |
|--------|--------|-------------|
| `dvxDialog.h` | `dvxDialog.c` | Modal message box and file open/save dialogs |
| `dvxPrefs.h` | `dvxPrefs.c` | INI-based preferences (read/write with typed accessors) |
| `dvxResource.h` | `dvxResource.c` | Resource system -- icons, text, and binary data appended to DXE files |
| `dvxMem.h` | (header only) | Per-app memory tracking API declarations |
| `dvxWidget.h` | `widgetClass.c`, `widgetCore.c`, `widgetEvent.c`, `widgetLayout.c`, `widgetOps.c`, `widgetScrollbar.c` | Widget infrastructure |
| `dvxWidgetPlugin.h` | (header only) | Plugin API for widget DXE modules |
| -- | `dvxImage.c` | Image loading via stb_image (BMP, PNG, JPEG, GIF) |
| -- | `dvxImageWrite.c` | PNG export via stb_image_write |
## Source Files
| File | Description |
|------|-------------|
| `dvxVideo.c` | VESA mode negotiation, LFB mapping via DPMI, backbuffer alloc, `packColor()` |
| `dvxDraw.c` | `rectFill()`, `rectCopy()`, `drawBevel()`, `drawText()`, `drawTextN()`, `drawTermRow()`, cursor rendering |
| `dvxComp.c` | `dirtyListAdd()`, `dirtyListMerge()`, `flushRect()`, `rectIntersect()` |
| `dvxWm.c` | Window create/destroy, Z-order, chrome drawing, drag/resize, menu bar, scrollbars, minimize/maximize |
| `dvxApp.c` | `dvxInit()`, `dvxRun()`, `dvxUpdate()`, `dvxCreateWindow()`, color schemes, wallpaper, screenshots |
| `dvxDialog.c` | `dvxMessageBox()`, `dvxFileDialog()` -- modal dialogs with own event loops |
| `dvxPrefs.c` | `prefsLoad()`, `prefsSave()`, typed get/set for string/int/bool |
| `dvxResource.c` | `dvxResOpen()`, `dvxResRead()`, `dvxResFind()`, `dvxResClose()` -- resource system |
| `dvxImage.c` | `dvxLoadImage()`, `dvxLoadImageFromMemory()` -- stb_image loader, converts to native pixel format |
| `dvxImageWrite.c` | `dvxSaveImage()` -- PNG writer for screenshots |
| `widgetClass.c` | `wgtRegisterClass()`, `wgtRegisterApi()`, `wgtGetApi()`, class table |
| `widgetCore.c` | Widget allocation, tree ops, focus management, clipboard, hit testing, cursor blink |
| `widgetEvent.c` | `widgetOnMouse()`, `widgetOnKey()`, `widgetOnPaint()`, `widgetOnResize()`, scrollbar management |
| `widgetLayout.c` | Two-pass flexbox layout: bottom-up `calcMinSize`, top-down space allocation with weights |
| `widgetOps.c` | `wgtPaint()`, `wgtLayout()`, `wgtInitWindow()`, text get/set, invalidation |
| `widgetScrollbar.c` | Scrollbar drawing (H/V), thumb calculation, hit testing, drag update |
## Public Headers
| Header | Purpose |
|--------|---------|
| `dvxTypes.h` | All shared types: DisplayT, RectT, BlitOpsT, BevelStyleT, BitmapFontT, ColorSchemeT, WindowT, MenuT, ScrollbarT, CursorT, PopupStateT |
| `dvxVideo.h` | `videoInit()`, `videoShutdown()`, `packColor()`, `setClipRect()`, `resetClipRect()` |
| `dvxDraw.h` | All drawing functions: `rectFill()`, `drawBevel()`, `drawText()`, `drawTextN()`, `drawTermRow()`, etc. |
| `dvxComp.h` | Dirty list operations: `dirtyListAdd()`, `dirtyListMerge()`, `flushRect()`, `rectIntersect()` |
| `dvxWm.h` | Window management: `wmCreateWindow()`, `wmDestroyWindow()`, `wmRaiseWindow()`, menus, scrollbars, chrome |
| `dvxApp.h` | Application API: `dvxInit()`, `dvxRun()`, `dvxUpdate()`, `dvxCreateWindow()`, color schemes, wallpaper, image I/O |
| `dvxDialog.h` | Modal dialogs: `dvxMessageBox()`, `dvxFileDialog()` |
| `dvxPrefs.h` | INI preferences: `prefsLoad()`, `prefsSave()`, typed accessors |
| `dvxResource.h` | Resource system: `dvxResOpen()`, `dvxResRead()`, `dvxResFind()`, `dvxResClose()` |
| `dvxMem.h` | Per-app memory tracking: `dvxMalloc()`, `dvxFree()`, `dvxMemGetAppUsage()`, etc. |
| `dvxWidget.h` | Widget system public API: WidgetT, WidgetClassT, size tags, layout, API registry, `wclsFoo()` dispatch helpers |
| `dvxWidgetPlugin.h` | Plugin API for widget DXE authors: tree ops, focus, scrollbar helpers, shared state |
| `dvxFont.h` | Embedded 8x14 and 8x16 bitmap font data (CP437) |
| `dvxCursor.h` | Mouse cursor AND/XOR mask data (arrow, resize H/V/diag, busy) |
| `dvxPalette.h` | Default 256-color VGA palette for 8-bit mode |
## Platform Layer
| File | Description |
|------|-------------|
| `platform/dvxPlatform.h` | Platform abstraction API (video, input, spans, DXE, crash recovery, memory tracking) |
| `platform/dvxPlatformDos.c` | DJGPP/DPMI implementation (VESA VBE, INT 33h mouse, INT 16h keyboard, asm spans) |
The platform layer is compiled into dvx.exe (the loader), not into
libdvx.lib. Platform functions are exported to all DXE modules via
`platformRegisterDxeExports()`.
## Third-Party Libraries
| File | Description |
|------|-------------|
| `thirdparty/stb_image.h` | Image loading (implementation compiled into dvxImage.c) |
| `thirdparty/stb_image_write.h` | PNG writing (implementation compiled into dvxImageWrite.c) |
| `thirdparty/stb_ds.h` | Dynamic arrays/hash maps (implementation in loader, exported to all DXEs) |
## Dynamic Limits
All major data structures grow dynamically via realloc. There are no
fixed-size limits for:
- **Windows** -- `WindowStackT.windows` is a dynamic array
- **Menus** -- `MenuBarT.menus` and `MenuT.items` are dynamic arrays
- **Accelerator entries** -- `AccelTableT.entries` is a dynamic array
- **Dirty rectangles** -- `DirtyListT.rects` is a dynamic array
- **Submenu depth** -- `PopupStateT.parentStack` is a dynamic array
The only fixed-size buffers remaining are per-element string fields
(`MAX_TITLE_LEN = 128`, `MAX_MENU_LABEL = 32`, `MAX_WIDGET_NAME = 32`)
and the system menu (`SYS_MENU_MAX_ITEMS = 10`).
## Resource System
Resources are appended to DXE3 files (.app, .wgt, .lib) after the
normal DXE content. The DXE loader never reads past the DXE header,
so appended data is invisible to dlopen.
File layout:
[DXE3 content]
[resource data entries] -- sequential, variable length
[resource directory] -- fixed-size entries (48 bytes each)
[footer] -- magic + directory offset + count (16 bytes)
### Resource Types
| Define | Value | Description |
|--------|-------|-------------|
| `DVX_RES_ICON` | 1 | Image data (BMP icon: 16x16, 32x32, etc.) |
| `DVX_RES_TEXT` | 2 | Null-terminated string (author, copyright, etc.) |
| `DVX_RES_BINARY` | 3 | Arbitrary binary data (app-specific) |
### Resource API
| Function | Description |
|----------|-------------|
| `dvxResOpen(path)` | Open a resource handle by reading the footer and directory. Returns NULL if no resources. |
| `dvxResRead(h, name, outSize)` | Find a resource by name and read its data into a malloc'd buffer. Caller frees. |
| `dvxResFind(h, name)` | Find a resource by name and return its directory entry pointer. |
| `dvxResClose(h)` | Close the handle and free associated memory. |
### Key Types
| Type | Description |
|------|-------------|
| `DvxResDirEntryT` | Directory entry: name[32], type, offset, size, reserved (48 bytes) |
| `DvxResFooterT` | Footer: magic (`0x52585644` = "DVXR"), dirOffset, entryCount, reserved (16 bytes) |
| `DvxResHandleT` | Runtime handle: path, entries array, entry count |
## Memory Tracking (dvxMem.h)
Per-app memory tracking wraps malloc/free/calloc/realloc/strdup with a
small header per allocation that records the owning app ID and size.
DXE code does not need to include dvxMem.h -- the DXE export table maps
the standard allocator names to these wrappers transparently.
| Function | Description |
|----------|-------------|
| `dvxMalloc(size)` | Tracked malloc |
| `dvxCalloc(nmemb, size)` | Tracked calloc |
| `dvxRealloc(ptr, size)` | Tracked realloc |
| `dvxFree(ptr)` | Tracked free (falls through to real free on non-tracked pointers) |
| `dvxStrdup(s)` | Tracked strdup |
| `dvxMemSnapshotLoad(appId)` | Record baseline memory for leak detection |
| `dvxMemGetAppUsage(appId)` | Query current memory usage for an app (bytes) |
| `dvxMemResetApp(appId)` | Free all allocations charged to an app |
The global `dvxMemAppIdPtr` pointer is set by the shell to
`&ctx->currentAppId` so the allocator knows which app to charge.
## WidgetT Structure
The WidgetT struct is generic -- no widget-specific fields or union:
```c
typedef struct WidgetT {
int32_t type; // assigned by wgtRegisterClass()
const struct WidgetClassT *wclass; // dispatch table pointer
char name[MAX_WIDGET_NAME];
// Tree linkage
struct WidgetT *parent, *firstChild, *lastChild, *nextSibling;
WindowT *window;
// Geometry (relative to window content area)
int32_t x, y, w, h;
int32_t calcMinW, calcMinH; // computed minimum size
// Size hints (tagged: wgtPixels/wgtChars/wgtPercent, 0 = auto)
int32_t minW, minH, maxW, maxH;
int32_t prefW, prefH;
int32_t weight; // extra-space distribution (0 = fixed)
// Container properties
WidgetAlignE align;
int32_t spacing, padding; // tagged sizes
// Colors (0 = use color scheme defaults)
uint32_t fgColor, bgColor;
// State
bool visible, enabled, readOnly, focused;
char accelKey;
// User data and callbacks
void *userData;
void *data; // widget-private data (allocated by widget DXE)
const char *tooltip;
MenuT *contextMenu;
void (*onClick)(struct WidgetT *w);
void (*onDblClick)(struct WidgetT *w);
void (*onChange)(struct WidgetT *w);
void (*onFocus)(struct WidgetT *w);
void (*onBlur)(struct WidgetT *w);
} WidgetT;
```
## WidgetClassT Dispatch Table
WidgetClassT is an ABI-stable dispatch table. Method IDs are fixed
constants that never change -- adding new methods appends new IDs
without shifting existing ones. Widget DXEs compiled against an older
DVX version continue to work unmodified.
```c
#define WGT_CLASS_VERSION 1 // bump on breaking ABI change
#define WGT_METHOD_MAX 32 // room for future methods
typedef struct WidgetClassT {
uint32_t version;
uint32_t flags;
void *handlers[WGT_METHOD_MAX];
} WidgetClassT;
```
### Method ID Table
21 methods are currently defined (IDs 0--20). WGT_METHOD_MAX is 32,
leaving room for 11 future methods without a version bump.
| ID | Method ID | Signature | Purpose |
|----|-----------|-----------|---------|
| 0 | `WGT_METHOD_PAINT` | `void (w, d, ops, font, colors)` | Render the widget |
| 1 | `WGT_METHOD_PAINT_OVERLAY` | `void (w, d, ops, font, colors)` | Render overlay (dropdown popup) |
| 2 | `WGT_METHOD_CALC_MIN_SIZE` | `void (w, font)` | Compute minimum size (bottom-up pass) |
| 3 | `WGT_METHOD_LAYOUT` | `void (w, font)` | Position children (top-down pass) |
| 4 | `WGT_METHOD_GET_LAYOUT_METRICS` | `void (w, font, pad, gap, extraTop, borderW)` | Return padding/gap for box layout |
| 5 | `WGT_METHOD_ON_MOUSE` | `void (w, root, vx, vy)` | Handle mouse click |
| 6 | `WGT_METHOD_ON_KEY` | `void (w, key, mod)` | Handle keyboard input |
| 7 | `WGT_METHOD_ON_ACCEL_ACTIVATE` | `void (w, root)` | Handle accelerator key match |
| 8 | `WGT_METHOD_DESTROY` | `void (w)` | Free widget-private data |
| 9 | `WGT_METHOD_ON_CHILD_CHANGED` | `void (parent, child)` | Notification when a child changes |
| 10 | `WGT_METHOD_GET_TEXT` | `const char *(w)` | Return widget text |
| 11 | `WGT_METHOD_SET_TEXT` | `void (w, text)` | Set widget text |
| 12 | `WGT_METHOD_CLEAR_SELECTION` | `bool (w)` | Clear text/item selection |
| 13 | `WGT_METHOD_CLOSE_POPUP` | `void (w)` | Close dropdown popup |
| 14 | `WGT_METHOD_GET_POPUP_RECT` | `void (w, font, contentH, popX, popY, popW, popH)` | Compute popup rectangle |
| 15 | `WGT_METHOD_ON_DRAG_UPDATE` | `void (w, root, x, y)` | Mouse move during drag |
| 16 | `WGT_METHOD_ON_DRAG_END` | `void (w, root, x, y)` | Mouse release after drag |
| 17 | `WGT_METHOD_GET_CURSOR_SHAPE` | `int32_t (w, vx, vy)` | Return cursor ID for this position |
| 18 | `WGT_METHOD_POLL` | `void (w, win)` | Periodic polling (AnsiTerm comms) |
| 19 | `WGT_METHOD_QUICK_REPAINT` | `int32_t (w, outY, outH)` | Fast incremental repaint |
| 20 | `WGT_METHOD_SCROLL_CHILD_INTO_VIEW` | `void (parent, child)` | Scroll to make a child visible |
### Typed Dispatch Helpers
Each `wclsFoo()` inline function extracts a handler by stable method
ID, casts it to the correct function pointer type, and calls it with
a NULL check. This gives callers type-safe dispatch with the same
codegen as a direct struct field call.
| Helper | Wraps Method ID |
|--------|-----------------|
| `wclsHas(w, methodId)` | Check if a method is implemented (non-NULL) |
| `wclsPaint(w, d, ops, font, colors)` | `WGT_METHOD_PAINT` |
| `wclsPaintOverlay(w, d, ops, font, colors)` | `WGT_METHOD_PAINT_OVERLAY` |
| `wclsCalcMinSize(w, font)` | `WGT_METHOD_CALC_MIN_SIZE` |
| `wclsLayout(w, font)` | `WGT_METHOD_LAYOUT` |
| `wclsGetLayoutMetrics(w, font, pad, gap, extraTop, borderW)` | `WGT_METHOD_GET_LAYOUT_METRICS` |
| `wclsOnMouse(w, root, vx, vy)` | `WGT_METHOD_ON_MOUSE` |
| `wclsOnKey(w, key, mod)` | `WGT_METHOD_ON_KEY` |
| `wclsOnAccelActivate(w, root)` | `WGT_METHOD_ON_ACCEL_ACTIVATE` |
| `wclsDestroy(w)` | `WGT_METHOD_DESTROY` |
| `wclsOnChildChanged(parent, child)` | `WGT_METHOD_ON_CHILD_CHANGED` |
| `wclsGetText(w)` | `WGT_METHOD_GET_TEXT` |
| `wclsSetText(w, text)` | `WGT_METHOD_SET_TEXT` |
| `wclsClearSelection(w)` | `WGT_METHOD_CLEAR_SELECTION` |
| `wclsClosePopup(w)` | `WGT_METHOD_CLOSE_POPUP` |
| `wclsGetPopupRect(w, font, contentH, popX, popY, popW, popH)` | `WGT_METHOD_GET_POPUP_RECT` |
| `wclsOnDragUpdate(w, root, x, y)` | `WGT_METHOD_ON_DRAG_UPDATE` |
| `wclsOnDragEnd(w, root, x, y)` | `WGT_METHOD_ON_DRAG_END` |
| `wclsGetCursorShape(w, vx, vy)` | `WGT_METHOD_GET_CURSOR_SHAPE` |
| `wclsPoll(w, win)` | `WGT_METHOD_POLL` |
| `wclsQuickRepaint(w, outY, outH)` | `WGT_METHOD_QUICK_REPAINT` |
| `wclsScrollChildIntoView(parent, child)` | `WGT_METHOD_SCROLL_CHILD_INTO_VIEW` |
### WidgetClassT Flags
| Flag | Value | Description |
|------|-------|-------------|
| `WCLASS_FOCUSABLE` | 0x0001 | Can receive keyboard focus |
| `WCLASS_BOX_CONTAINER` | 0x0002 | Uses VBox/HBox layout algorithm |
| `WCLASS_HORIZ_CONTAINER` | 0x0004 | Lays out children horizontally |
| `WCLASS_PAINTS_CHILDREN` | 0x0008 | Widget handles child rendering |
| `WCLASS_NO_HIT_RECURSE` | 0x0010 | Hit testing stops here |
| `WCLASS_FOCUS_FORWARD` | 0x0020 | Accel hit forwards focus to next focusable |
| `WCLASS_HAS_POPUP` | 0x0040 | Has dropdown popup overlay |
| `WCLASS_SCROLLABLE` | 0x0080 | Accepts mouse wheel events |
| `WCLASS_SCROLL_CONTAINER` | 0x0100 | Scroll container (ScrollPane) |
| `WCLASS_NEEDS_POLL` | 0x0200 | Needs periodic polling |
| `WCLASS_SWALLOWS_TAB` | 0x0400 | Tab key goes to widget, not focus nav |
| `WCLASS_RELAYOUT_ON_SCROLL` | 0x0800 | Full relayout on scrollbar drag |
| `WCLASS_PRESS_RELEASE` | 0x1000 | Click = press+release (buttons) |
| `WCLASS_ACCEL_WHEN_HIDDEN` | 0x2000 | Accel matching works when invisible |
## Widget Registration
Each widget DXE exports `wgtRegister()`, called by the loader after
`dlopen`. The WidgetClassT uses the `handlers[]` array indexed by
method IDs:
```c
static int32_t sButtonType;
static const WidgetClassT sButtonClass = {
.version = WGT_CLASS_VERSION,
.flags = WCLASS_FOCUSABLE | WCLASS_PRESS_RELEASE,
.handlers = {
[WGT_METHOD_PAINT] = buttonPaint,
[WGT_METHOD_CALC_MIN_SIZE] = buttonCalcMinSize,
[WGT_METHOD_ON_MOUSE] = buttonOnMouse,
[WGT_METHOD_ON_KEY] = buttonOnKey,
[WGT_METHOD_DESTROY] = buttonDestroy,
[WGT_METHOD_GET_TEXT] = buttonGetText,
[WGT_METHOD_SET_TEXT] = buttonSetText,
[WGT_METHOD_ON_ACCEL_ACTIVATE] = buttonAccelActivate,
}
};
static const ButtonApiT sApi = { .create = buttonCreate };
void wgtRegister(void) {
sButtonType = wgtRegisterClass(&sButtonClass);
wgtRegisterApi("button", &sApi);
}
```
## Per-Widget API Registry
The monolithic WidgetApiT is gone. Each widget registers a small API
struct under a name via `wgtRegisterApi()`. Callers retrieve it via
`wgtGetApi()` and cast to the widget-specific type. Per-widget headers
(e.g. `widgetButton.h`) provide typed accessors and convenience macros:
```c
// widgetButton.h
typedef struct {
WidgetT *(*create)(WidgetT *parent, const char *text);
} ButtonApiT;
static inline const ButtonApiT *dvxButtonApi(void) {
static const ButtonApiT *sApi;
if (!sApi) { sApi = (const ButtonApiT *)wgtGetApi("button"); }
return sApi;
}
#define wgtButton(parent, text) dvxButtonApi()->create(parent, text)
```
## Tagged Size Values
Size hints encode both unit type and numeric value in a single int32_t:
| Macro | Encoding | Example |
|-------|----------|---------|
| `wgtPixels(v)` | Bits 31:30 = 00 | `w->minW = wgtPixels(200);` |
| `wgtChars(v)` | Bits 31:30 = 01 | `w->minW = wgtChars(40);` |
| `wgtPercent(v)` | Bits 31:30 = 10 | `w->minW = wgtPercent(50);` |
| `0` | -- | Auto (use computed minimum) |
## Layout Algorithm
Two-pass flexbox-like layout:
1. **Bottom-up** (`calcMinSize`): Each widget computes its minimum size.
Containers sum children along the main axis, max across the cross axis.
2. **Top-down** (`layout`): Available space is allocated to children.
Extra space beyond minimum is distributed proportionally to weights.
`weight=0` means fixed size, `weight=100` is the default flexible weight.
### Cross-Axis Centering
When a child widget has a `maxW` (in a VBox) or `maxH` (in an HBox)
that constrains it smaller than the available cross-axis space, the
layout engine automatically centers the child on the cross axis. This
means setting `maxW` or `maxH` on a child inside a container will both
cap its size and center it within the remaining space.
## Image Loading
Two image loading functions are available:
| Function | Description |
|----------|-------------|
| `dvxLoadImage(ctx, path, outW, outH, outPitch)` | Load from a file path |
| `dvxLoadImageFromMemory(ctx, data, dataLen, outW, outH, outPitch)` | Load from a memory buffer (e.g. resource data) |
Both convert to the display's native pixel format. Caller frees the
returned buffer with `dvxFreeImage()`. Supported formats: BMP, PNG,
JPEG, GIF (via stb_image).
## Exported Symbols
libdvx.lib exports symbols matching these prefixes:
```
dvx*, wgt*, wm*, prefs*, rect*, draw*, pack*, text*, setClip*,
resetClip*, stbi*, stbi_write*, dirtyList*, widget*,
accelParse*, clipboard*, multiClick*,
sCursor*, sDbl*, sDebug*, sClosed*, sFocused*, sKey*,
sOpen*, sDrag*, sClosed*, sKey*
```
## Build
```
make # builds bin/libs/libdvx.lib + bin/libs/libdvx.dep
make clean # removes objects and library
```
Depends on: `libtasks.lib` (via libdvx.dep).

View file

@ -1,80 +0,0 @@
// dvxDialog.h -- Modal dialogs for DVX GUI
//
// Provides pre-built modal dialog boxes (message box, file dialog) that
// block the caller and run their own event loop via dvxUpdate() until the
// user dismisses them. Modal dialogs set ctx->modalWindow to prevent input
// from reaching other windows during the dialog's lifetime.
//
// The flag encoding uses separate bit fields for button configuration
// (low nibble) and icon type (high nibble) so they can be OR'd together
// in a single int32_t argument, matching the Win16 MessageBox() convention.
#ifndef DVX_DIALOG_H
#define DVX_DIALOG_H
#include "dvxApp.h"
// ============================================================
// Message box button flags (low nibble)
// ============================================================
#define MB_OK 0x0000
#define MB_OKCANCEL 0x0001
#define MB_YESNO 0x0002
#define MB_YESNOCANCEL 0x0003
#define MB_RETRYCANCEL 0x0004
// ============================================================
// Message box icon flags (high nibble, OR with button flags)
// ============================================================
#define MB_ICONINFO 0x0010
#define MB_ICONWARNING 0x0020
#define MB_ICONERROR 0x0030
#define MB_ICONQUESTION 0x0040
// ============================================================
// Message box return values
// ============================================================
#define ID_OK 1
#define ID_CANCEL 2
#define ID_YES 3
#define ID_NO 4
#define ID_RETRY 5
// Display a modal message box with the specified button and icon combination.
// Blocks the caller by running dvxUpdate() in a loop until a button is
// pressed or the dialog is closed. Returns the ID_xxx value of the button
// that was pressed. The dialog window is automatically destroyed on return.
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags);
// ============================================================
// File dialog flags
// ============================================================
#define FD_OPEN 0x0000 // Open file (default)
#define FD_SAVE 0x0001 // Save file
// ============================================================
// File dialog filter
// ============================================================
//
// Filters are displayed in a dropdown at the bottom of the file dialog.
// Pattern matching is case-insensitive and supports only single glob
// patterns (no semicolon-separated lists). This keeps the matching code
// trivial for a DOS filesystem where filenames are short and simple.
typedef struct {
const char *label; // e.g. "Text Files (*.txt)"
const char *pattern; // e.g. "*.txt" (case-insensitive, single pattern)
} FileFilterT;
// Display a modal file open/save dialog. The dialog shows a directory
// listing with navigation (parent directory, drive letters on DOS), a
// filename text input, and an optional filter dropdown. Blocks the caller
// via dvxUpdate() loop. Returns true if the user selected a file (path
// written to outPath), false if cancelled or closed. initialDir may be
// NULL to start in the current working directory.
bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const char *initialDir, const FileFilterT *filters, int32_t filterCount, char *outPath, int32_t outPathSize);
#endif // DVX_DIALOG_H

View file

@ -1,23 +0,0 @@
// dvxImageWrite.c -- stb_image_write implementation for DVX GUI
//
// Companion to dvxIcon.c: instantiates stb_image_write for PNG output
// (used by dvxScreenshot and dvxWindowScreenshot). Same rationale as
// dvxIcon.c for using stb -- zero external dependencies, single header,
// public domain. Kept in a separate translation unit from the read side
// so projects that don't need screenshot support can omit this file and
// save the code size.
//
// STBI_WRITE_NO_SIMD disables SSE codepaths for the same reason as
// STBI_NO_SIMD in dvxIcon.c: the DOS target lacks SSE support.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"
#define STBI_WRITE_NO_SIMD
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "thirdparty/stb_image_write.h"
#pragma GCC diagnostic pop
#include <stdlib.h>
#include "dvxMem.h"

View file

@ -1,28 +0,0 @@
// dvxMem.h -- Per-app memory tracking API for DVX
//
// Declares the tracked allocation functions. DXE code does NOT need
// to include this header for tracking to work -- the DXE export table
// maps malloc/free/calloc/realloc/strdup to these wrappers transparently.
//
// This header is provided for code that needs to call the tracking
// functions by name (e.g. dvxMemGetAppUsage in the Task Manager) or
// for the dvxMemAppIdPtr declaration.
#ifndef DVX_MEM_H
#define DVX_MEM_H
#include <stdint.h>
#include <stdlib.h>
extern int32_t *dvxMemAppIdPtr;
void *dvxMalloc(size_t size);
void *dvxCalloc(size_t nmemb, size_t size);
void *dvxRealloc(void *ptr, size_t size);
void dvxFree(void *ptr);
char *dvxStrdup(const char *s);
void dvxMemSnapshotLoad(int32_t appId);
uint32_t dvxMemGetAppUsage(int32_t appId);
void dvxMemResetApp(int32_t appId);
#endif // DVX_MEM_H

View file

@ -1,443 +0,0 @@
// dvxPrefs.c -- INI-based preferences system (read/write)
//
// Custom INI parser and writer. Stores entries as a dynamic array of
// section/key/value triples using stb_ds. Preserves insertion order
// on save so the file remains human-readable.
#include "dvxPrefs.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dvxMem.h"
// stb_ds dynamic arrays (implementation lives in libtasks.a)
#include "thirdparty/stb_ds.h"
// ============================================================
// Internal types
// ============================================================
typedef struct {
char *section;
char *key;
char *value;
} PrefsEntryT;
// Comment lines are stored to preserve them on save. A comment has
// key=NULL and value=the full line text (including the ; prefix).
// Section headers have key=NULL and value=NULL.
static PrefsEntryT *sEntries = NULL; // stb_ds dynamic array
static char *sFilePath = NULL; // path used by prefsLoad (for prefsSave)
// ============================================================
// Helpers
// ============================================================
static char *dupStr(const char *s) {
if (!s) {
return NULL;
}
size_t len = strlen(s);
char *d = (char *)malloc(len + 1);
if (d) {
memcpy(d, s, len + 1);
}
return d;
}
static void freeEntry(PrefsEntryT *e) {
free(e->section);
free(e->key);
free(e->value);
e->section = NULL;
e->key = NULL;
e->value = NULL;
}
// Case-insensitive string compare
static int strcmpci(const char *a, const char *b) {
for (;;) {
int d = tolower((unsigned char)*a) - tolower((unsigned char)*b);
if (d != 0 || !*a) {
return d;
}
a++;
b++;
}
}
// Find an entry by section+key (case-insensitive). Returns index or -1.
static int32_t findEntry(const char *section, const char *key) {
for (int32_t i = 0; i < arrlen(sEntries); i++) {
PrefsEntryT *e = &sEntries[i];
if (e->key && e->section &&
strcmpci(e->section, section) == 0 &&
strcmpci(e->key, key) == 0) {
return i;
}
}
return -1;
}
// Find the index of a section header entry. Returns -1 if not found.
static int32_t findSection(const char *section) {
for (int32_t i = 0; i < arrlen(sEntries); i++) {
PrefsEntryT *e = &sEntries[i];
if (!e->key && !e->value && e->section &&
strcmpci(e->section, section) == 0) {
return i;
}
}
return -1;
}
// Trim leading/trailing whitespace in place. Returns pointer into buf.
static char *trimInPlace(char *buf) {
while (*buf == ' ' || *buf == '\t') {
buf++;
}
char *end = buf + strlen(buf) - 1;
while (end >= buf && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
*end-- = '\0';
}
return buf;
}
// ============================================================
// prefsFree
// ============================================================
void prefsFree(void) {
for (int32_t i = 0; i < arrlen(sEntries); i++) {
freeEntry(&sEntries[i]);
}
arrfree(sEntries);
sEntries = NULL;
free(sFilePath);
sFilePath = NULL;
}
// ============================================================
// prefsGetBool
// ============================================================
bool prefsGetBool(const char *section, const char *key, bool defaultVal) {
const char *val = prefsGetString(section, key, NULL);
if (!val) {
return defaultVal;
}
char c = (char)tolower((unsigned char)val[0]);
if (c == 't' || c == 'y' || c == '1') {
return true;
}
if (c == 'f' || c == 'n' || c == '0') {
return false;
}
return defaultVal;
}
// ============================================================
// prefsGetInt
// ============================================================
int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal) {
const char *val = prefsGetString(section, key, NULL);
if (!val) {
return defaultVal;
}
char *end = NULL;
long n = strtol(val, &end, 10);
if (end == val) {
return defaultVal;
}
return (int32_t)n;
}
// ============================================================
// prefsGetString
// ============================================================
const char *prefsGetString(const char *section, const char *key, const char *defaultVal) {
int32_t idx = findEntry(section, key);
if (idx < 0) {
return defaultVal;
}
return sEntries[idx].value;
}
// ============================================================
// prefsLoad
// ============================================================
bool prefsLoad(const char *filename) {
prefsFree();
// Always store the path so prefsSave can create the file
// even if it doesn't exist yet.
sFilePath = dupStr(filename);
FILE *fp = fopen(filename, "rb");
if (!fp) {
return false;
}
char line[512];
char *currentSection = dupStr("");
while (fgets(line, sizeof(line), fp)) {
// Strip trailing whitespace/newline
char *end = line + strlen(line) - 1;
while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ' || *end == '\t')) {
*end-- = '\0';
}
char *p = line;
// Skip leading whitespace
while (*p == ' ' || *p == '\t') {
p++;
}
// Blank line -- store as comment to preserve formatting
if (*p == '\0') {
PrefsEntryT e = {0};
e.section = dupStr(currentSection);
e.value = dupStr("");
arrput(sEntries, e);
continue;
}
// Comment line
if (*p == ';' || *p == '#') {
PrefsEntryT e = {0};
e.section = dupStr(currentSection);
e.value = dupStr(line);
arrput(sEntries, e);
continue;
}
// Section header
if (*p == '[') {
char *close = strchr(p, ']');
if (close) {
*close = '\0';
free(currentSection);
currentSection = dupStr(trimInPlace(p + 1));
PrefsEntryT e = {0};
e.section = dupStr(currentSection);
arrput(sEntries, e);
}
continue;
}
// Key=value
char *eq = strchr(p, '=');
if (eq) {
*eq = '\0';
PrefsEntryT e = {0};
e.section = dupStr(currentSection);
e.key = dupStr(trimInPlace(p));
e.value = dupStr(trimInPlace(eq + 1));
arrput(sEntries, e);
}
}
free(currentSection);
fclose(fp);
return true;
}
// ============================================================
// prefsRemove
// ============================================================
void prefsRemove(const char *section, const char *key) {
int32_t idx = findEntry(section, key);
if (idx >= 0) {
freeEntry(&sEntries[idx]);
arrdel(sEntries, idx);
}
}
// ============================================================
// prefsSave
// ============================================================
bool prefsSave(void) {
if (!sFilePath) {
return false;
}
return prefsSaveAs(sFilePath);
}
// ============================================================
// prefsSaveAs
// ============================================================
bool prefsSaveAs(const char *filename) {
FILE *fp = fopen(filename, "wb");
if (!fp) {
return false;
}
const char *lastSection = "";
for (int32_t i = 0; i < arrlen(sEntries); i++) {
PrefsEntryT *e = &sEntries[i];
// Comment or blank line (key=NULL, value=text or empty)
if (!e->key && e->value) {
fprintf(fp, "%s\r\n", e->value);
continue;
}
// Section header (key=NULL, value=NULL)
if (!e->key && !e->value) {
fprintf(fp, "[%s]\r\n", e->section);
lastSection = e->section;
continue;
}
// Key=value
if (e->key && e->value) {
fprintf(fp, "%s = %s\r\n", e->key, e->value);
}
}
fclose(fp);
return true;
}
// ============================================================
// prefsSetBool
// ============================================================
void prefsSetBool(const char *section, const char *key, bool value) {
prefsSetString(section, key, value ? "true" : "false");
}
// ============================================================
// prefsSetInt
// ============================================================
void prefsSetInt(const char *section, const char *key, int32_t value) {
char buf[32];
snprintf(buf, sizeof(buf), "%ld", (long)value);
prefsSetString(section, key, buf);
}
// ============================================================
// prefsSetString
// ============================================================
void prefsSetString(const char *section, const char *key, const char *value) {
int32_t idx = findEntry(section, key);
if (idx >= 0) {
// Update existing entry
free(sEntries[idx].value);
sEntries[idx].value = dupStr(value);
return;
}
// Find or create section header
int32_t secIdx = findSection(section);
if (secIdx < 0) {
// Add blank line before new section (unless file is empty)
if (arrlen(sEntries) > 0) {
PrefsEntryT blank = {0};
blank.section = dupStr(section);
blank.value = dupStr("");
arrput(sEntries, blank);
}
// Add section header
PrefsEntryT secEntry = {0};
secEntry.section = dupStr(section);
arrput(sEntries, secEntry);
secIdx = arrlen(sEntries) - 1;
}
// Find insertion point: after last entry in this section
int32_t insertAt = secIdx + 1;
while (insertAt < arrlen(sEntries)) {
PrefsEntryT *e = &sEntries[insertAt];
// Stop if we've hit a different section header
if (!e->key && !e->value && e->section &&
strcmpci(e->section, section) != 0) {
break;
}
// Stop if we've hit an entry from a different section
if (e->section && strcmpci(e->section, section) != 0) {
break;
}
insertAt++;
}
// Insert new entry
PrefsEntryT newEntry = {0};
newEntry.section = dupStr(section);
newEntry.key = dupStr(key);
newEntry.value = dupStr(value);
arrins(sEntries, insertAt, newEntry);
}

View file

@ -1,51 +0,0 @@
// dvxPrefs.h -- INI-based preferences system (read/write)
//
// Loads a configuration file at startup and provides typed accessors
// with caller-supplied defaults. Values can be modified at runtime
// and saved back to disk. If the file is missing or a key is absent,
// getters return the default silently.
#ifndef DVX_PREFS_H
#define DVX_PREFS_H
#include <stdbool.h>
#include <stdint.h>
// Load an INI file into memory. Returns true on success, false if the
// file could not be opened (all getters will return their defaults).
// Only one file may be loaded at a time; calling again frees the previous.
bool prefsLoad(const char *filename);
// Save the current in-memory state back to the file that was loaded.
// Returns true on success.
bool prefsSave(void);
// Save the current in-memory state to a specific file.
bool prefsSaveAs(const char *filename);
// Release all memory held by the preferences.
void prefsFree(void);
// Retrieve a string value. Returns defaultVal if the key is not present.
// The returned pointer is valid until the key is modified or prefsFree().
const char *prefsGetString(const char *section, const char *key, const char *defaultVal);
// Retrieve an integer value.
int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal);
// Retrieve a boolean value. Recognises "true"/"yes"/"1" and "false"/"no"/"0".
bool prefsGetBool(const char *section, const char *key, bool defaultVal);
// Set a string value. Creates the section and key if they don't exist.
void prefsSetString(const char *section, const char *key, const char *value);
// Set an integer value.
void prefsSetInt(const char *section, const char *key, int32_t value);
// Set a boolean value (stored as "true"/"false").
void prefsSetBool(const char *section, const char *key, bool value);
// Remove a key from a section. No-op if not found.
void prefsRemove(const char *section, const char *key);
#endif

View file

@ -1,140 +0,0 @@
// dvxResource.c -- DVX resource runtime API
//
// Reads the resource block appended to DXE3 files. The resource
// block is located by reading the footer at the end of the file.
#include "dvxResource.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
DvxResHandleT *dvxResOpen(const char *path) {
if (!path) {
return NULL;
}
FILE *f = fopen(path, "rb");
if (!f) {
return NULL;
}
// Read footer from end of file
if (fseek(f, -(int32_t)sizeof(DvxResFooterT), SEEK_END) != 0) {
fclose(f);
return NULL;
}
DvxResFooterT footer;
if (fread(&footer, sizeof(footer), 1, f) != 1) {
fclose(f);
return NULL;
}
if (footer.magic != DVX_RES_MAGIC || footer.entryCount == 0) {
fclose(f);
return NULL;
}
// Read directory
if (fseek(f, footer.dirOffset, SEEK_SET) != 0) {
fclose(f);
return NULL;
}
DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT));
if (!entries) {
fclose(f);
return NULL;
}
if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) {
free(entries);
fclose(f);
return NULL;
}
fclose(f);
DvxResHandleT *h = (DvxResHandleT *)malloc(sizeof(DvxResHandleT));
if (!h) {
free(entries);
return NULL;
}
strncpy(h->path, path, sizeof(h->path) - 1);
h->path[sizeof(h->path) - 1] = '\0';
h->entries = entries;
h->entryCount = footer.entryCount;
return h;
}
const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name) {
if (!h || !name) {
return NULL;
}
for (uint32_t i = 0; i < h->entryCount; i++) {
if (strcmp(h->entries[i].name, name) == 0) {
return &h->entries[i];
}
}
return NULL;
}
void *dvxResRead(DvxResHandleT *h, const char *name, uint32_t *outSize) {
const DvxResDirEntryT *entry = dvxResFind(h, name);
if (!entry) {
return NULL;
}
FILE *f = fopen(h->path, "rb");
if (!f) {
return NULL;
}
if (fseek(f, entry->offset, SEEK_SET) != 0) {
fclose(f);
return NULL;
}
void *buf = malloc(entry->size);
if (!buf) {
fclose(f);
return NULL;
}
if (fread(buf, 1, entry->size, f) != entry->size) {
free(buf);
fclose(f);
return NULL;
}
fclose(f);
if (outSize) {
*outSize = entry->size;
}
return buf;
}
void dvxResClose(DvxResHandleT *h) {
if (h) {
free(h->entries);
free(h);
}
}

View file

@ -1,86 +0,0 @@
#define DVX_WIDGET_IMPL
// widgetClass.c -- Widget class table, API registry, and dynamic registration
//
// widgetClassTable is a stb_ds dynamic array. Widget DXEs call
// wgtRegisterClass() during wgtRegister() to append their class
// definition and receive a runtime type ID. No widget types are
// known at compile time.
//
// The API registry uses an stb_ds string hashmap for O(1) lookup.
// Each widget DXE calls wgtRegisterApi("name", &sApi) during
// wgtRegister(). App/core code calls wgtGetApi("name") to get
// the API pointer, then casts and calls through it.
#include "dvxWidgetPlugin.h"
#include "stb_ds.h"
#include <string.h>
// stb_ds dynamic array of class pointers. Grows on each
// wgtRegisterClass() call. Index = type ID.
const WidgetClassT **widgetClassTable = NULL;
// stb_ds string hashmap: key = widget name, value = API pointer
typedef struct {
char *key; // stb_ds string key (heap-allocated by shput)
const void *value;
} ApiMapEntryT;
static ApiMapEntryT *sApiMap = NULL;
// ============================================================
// wgtGetApi
// ============================================================
//
// Look up a widget API by name. O(1) via stb_ds string hashmap.
// Returns NULL if the widget is not loaded. Callers should still
// cache the result in a static local to skip even the hash.
const void *wgtGetApi(const char *name) {
if (!name) {
return NULL;
}
int32_t idx = shgeti(sApiMap, name);
if (idx < 0) {
return NULL;
}
return sApiMap[idx].value;
}
// ============================================================
// wgtRegisterApi
// ============================================================
//
// Register a widget's public API struct under a name. Called by
// each widget DXE during wgtRegister().
void wgtRegisterApi(const char *name, const void *api) {
if (!name || !api) {
return;
}
shput(sApiMap, name, api);
}
// ============================================================
// wgtRegisterClass
// ============================================================
//
// Appends a class to the table and returns the assigned type ID.
// The ID is simply the array index.
int32_t wgtRegisterClass(const WidgetClassT *wclass) {
if (!wclass) {
return -1;
}
int32_t id = arrlen(widgetClassTable);
arrput(widgetClassTable, wclass);
return id;
}

76
cppcheck.sh Executable file
View file

@ -0,0 +1,76 @@
#!/bin/bash
#
# cppcheck.sh -- static analysis for the DVX source tree.
#
# Usage:
# ./cppcheck.sh # whole tree
# ./cppcheck.sh src/widgets # one subtree
# ./cppcheck.sh src/libs/kpunch/texthelp/textHelp.c # one file
#
# Exits 0 even when warnings are found -- cppcheck's exit status is
# not useful as a gate for this codebase (too many DJGPP-isms it can't
# resolve). Read the output.
set -e
cd "$(dirname "$0")"
TARGETS="${@:-src}"
INCLUDES=(
-Isrc/libs/kpunch/libdvx
-Isrc/libs/kpunch/libdvx/platform
-Isrc/libs/kpunch/libdvx/thirdparty
-Isrc/libs/kpunch/libtasks
-Isrc/libs/kpunch/texthelp
-Isrc/libs/kpunch/listhelp
-Isrc/libs/kpunch/dvxshell
-Isrc/libs/kpunch/taskmgr
-Isrc/libs/kpunch/serial
-Isrc/widgets/kpunch
-Isrc/apps/kpunch/dvxbasic
-Isrc/apps/kpunch/dvxbasic/compiler
-Isrc/apps/kpunch/dvxbasic/runtime
-Isrc/apps/kpunch/dvxbasic/formrt
)
# DJGPP / platform macros cppcheck can't find. Expand them to nothing
# so the preprocessor leaves the code visible to the analyser instead
# of eliding whole functions behind unknown macro calls.
DEFINES=(
-DDXE_EXPORT=
-DDVX_WIDGET_IMPL
-D__DJGPP__=2
)
# Suppressions: things cppcheck flags that aren't actually wrong for
# this codebase. Keep the list short so real issues stay visible.
SUPPRESS=(
--suppress=missingIncludeSystem # system headers aren't on host
--suppress=unusedFunction # many public exports look "unused" to a .c-only scan
--suppress=constParameterPointer # noisy on this style
--suppress=constVariablePointer # ditto
--suppress=normalCheckLevelMaxBranches
# stb_ds arrput is a macro -- cppcheck can't see that the value
# (including heap pointers inside it) is stored, so it reports
# spurious null-derefs on the stb_ds dynamic-array handle.
--suppress=nullPointerRedundantCheck
--suppress=nullPointerArithmeticRedundantCheck
# Thirdparty headers -- their own style isn't our problem.
--suppress='*:src/libs/kpunch/libdvx/thirdparty/*'
--suppress='*:src/libs/kpunch/sql/thirdparty/*'
)
exec cppcheck \
--enable=warning,style,performance,portability \
--inline-suppr \
--std=c99 \
--platform=unix32 \
--quiet \
-j"$(nproc)" \
-i src/libs/kpunch/sql/thirdparty \
-i src/libs/kpunch/libdvx/thirdparty \
"${INCLUDES[@]}" \
"${DEFINES[@]}" \
"${SUPPRESS[@]}" \
$TARGETS

File diff suppressed because it is too large Load diff

171
docs/dvx_help_viewer.html Normal file
View file

@ -0,0 +1,171 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>DVX Help Viewer</title>
<style>
body { font-family: sans-serif; margin: 0; padding: 0; display: flex; }
nav { width: 250px; min-width: 250px; background: #f0f0f0; padding: 16px;
border-right: 1px solid #ccc; height: 100vh; overflow-y: auto;
position: sticky; top: 0; box-sizing: border-box; }
nav ul { list-style: none; padding-left: 16px; margin: 4px 0; }
nav > ul { padding-left: 0; }
nav a { text-decoration: none; color: #0066cc; }
nav a:hover { text-decoration: underline; }
main { flex: 1; padding: 24px 32px; max-width: 800px; }
h1 { border-bottom: 2px solid #333; padding-bottom: 4px; }
h2 { border-bottom: 1px solid #999; padding-bottom: 2px; margin-top: 32px; }
h3 { margin-top: 24px; }
pre { background: #f8f8f8; border: 1px solid #ddd; padding: 8px;
overflow-x: auto; font-size: 14px; }
blockquote { background: #fffde7; border-left: 4px solid #ffc107;
padding: 8px 12px; margin: 12px 0; }
hr { border: none; border-top: 1px solid #ccc; margin: 24px 0; }
img { max-width: 100%; }
.topic { margin-bottom: 48px; }
</style>
</head>
<body>
<nav>
<h3>Contents</h3>
<ul>
<li><a href="#help.overview">DVX Help Viewer</a></li>
<li><a href="#help.format">Help Source Format</a></li>
<li><a href="#help.compiler">Help Compiler</a></li>
<li><a href="#help.integration">Application Integration</a></li>
</ul>
<h3>Index</h3>
<ul>
<li><a href="#help.format">.dhs</a></li>
<li><a href="#help.compiler">Compiler</a></li>
<li><a href="#help.integration">Context Help</a></li>
<li><a href="#help.format">Directives</a></li>
<li><a href="#help.overview">DVX Help</a></li>
<li><a href="#help.compiler">dvxhlpc</a></li>
<li><a href="#help.integration">F1</a></li>
<li><a href="#help.overview">Help Viewer</a></li>
<li><a href="#help.integration">helpFile</a></li>
<li><a href="#help.integration">helpTopic</a></li>
<li><a href="#help.integration">shellLoadAppWithArgs</a></li>
<li><a href="#help.format">Source Format</a></li>
</ul>
</nav>
<main>
<div class="topic" id="help.overview">
<h1>DVX Help Viewer</h1>
<p>The DVX Help Viewer displays .hlp help files compiled from .dhs source documents. It provides a tree-based table of contents, scrollable content with word-wrapped text, clickable hyperlinks, full-text search, and a keyword index.</p>
<h2>Opening Help</h2>
<p>Press F1 from any DVX application to open context-sensitive help. Applications can register their own help file and topic so F1 opens the relevant page.</p>
<p>You can also launch the help viewer from an application's Help menu, or by clicking the DVX Help icon in the Program Manager.</p>
<h2>Navigation</h2>
<ul>
<li>Click a topic in the tree on the left to display it</li>
<li>Click underlined links in the content to jump to other topics</li>
<li>Use the Back and Forward buttons (or Navigate menu) to retrace your steps</li>
<li>Use Navigate &gt; Index to browse an alphabetical keyword list</li>
</ul>
<p>Use Navigate &gt; Search to find topics by keyword</p>
<h2>Keyboard Shortcuts</h2>
<pre> Alt+Left Back
Alt+Right Forward
Ctrl+F Search
Escape Close viewer</pre>
</div>
<div class="topic" id="help.format">
<h1>Help Source Format (.dhs)</h1>
<p>Help files are authored as plain text .dhs source files using a simple line-oriented directive format. Lines beginning with a period at column 0 are directives. All other lines are body text, which is automatically word-wrapped by the viewer at display time.</p>
<h2>Topic Directives</h2>
<pre> .topic &lt;id&gt; Start a new topic with a unique string ID
.title &lt;text&gt; Set the topic's display title
.toc &lt;depth&gt; &lt;text&gt; Add a table of contents entry (0=root, 1=child, etc.)
.default Mark this topic as the one shown when the file opens</pre>
<h2>Content Directives</h2>
<pre> .h1 &lt;text&gt; Level 1 heading (colored bar)
.h2 &lt;text&gt; Level 2 heading (underlined)
.h3 &lt;text&gt; Level 3 heading (plain)
.hr Horizontal rule
.link &lt;id&gt; &lt;text&gt; Hyperlink to another topic
.image &lt;file.bmp&gt; Inline image (BMP format)</pre>
<h2>Block Directives</h2>
<pre> .list Start a bulleted list
.item &lt;text&gt; List item (must be inside .list)
.endlist End the bulleted list
.table Start a preformatted table block
.endtable End table block
.code Start a preformatted code block
.endcode End code block
.note [info|tip|warning] Start a callout box
.endnote End callout box</pre>
<h2>Index Directives</h2>
<pre> .index &lt;keyword&gt; Add a keyword to the index pointing to this topic</pre>
<h2>Example</h2>
<pre><code>.topic intro
.title Welcome
.toc 0 Welcome
.default
.index Welcome
.h1 Welcome
This is a paragraph of body text. It will be
automatically word-wrapped by the viewer.
.list
.item First item
.item Second item
.endlist
.link other.topic See also: Other Topic
.note info
This is an informational note.
.endnote
.note tip
This is a helpful tip.
.endnote
.note warning
This is a warning message.
.endnote</code></pre>
<h2>Callout Boxes</h2>
<p>Three types of callout boxes are available, each with a distinct colored accent bar:</p>
<blockquote><strong>Note:</strong> Use info notes for general supplementary information.</blockquote>
<blockquote><strong>Tip:</strong> Use tip notes for helpful suggestions and best practices.</blockquote>
<blockquote><strong>Warning:</strong> Use warning notes for important cautions the reader should be aware of.</blockquote>
</div>
<div class="topic" id="help.compiler">
<h1>Help Compiler (dvxhlpc)</h1>
<p>The dvxhlpc tool runs on the host (Linux) and compiles .dhs source files into binary .hlp files for the viewer, and optionally into self-contained HTML.</p>
<h2>Usage</h2>
<pre><code>dvxhlpc -o output.hlp [-i imagedir] [--html out.html] input.dhs [...]</code></pre>
<h2>Options</h2>
<pre> -o output.hlp Output binary help file (required)
-i imagedir Directory to find .image files (default: current dir)
--html out.html Also emit a self-contained HTML file</pre>
<p>Multiple input files are merged into a single help file. This allows per-widget or per-feature documentation fragments to be combined automatically.</p>
<h2>Build Integration</h2>
<p>The standard build pattern globs all fragments:</p>
<pre><code>dvxhlpc -o dvxhelp.hlp docs/src/overview.dhs widgets/*/*.dhs</code></pre>
<p>New widgets or features just drop a .dhs file in their source directory and it appears in the help on the next build.</p>
<h2>HTML Output</h2>
<p>The --html flag produces a single self-contained HTML file with a sidebar table of contents, styled headings, lists, code blocks, notes, and embedded images (base64 data URIs). This is useful for viewing documentation on the host machine without running the DOS help viewer.</p>
</div>
<div class="topic" id="help.integration">
<h1>Application Integration</h1>
<p>Any DVX application can provide context-sensitive help via the F1 key.</p>
<h2>Setting Up Help</h2>
<p>In your appMain, set the help file path on the app context:</p>
<pre><code>snprintf(ctx-&gt;helpFile, sizeof(ctx-&gt;helpFile),
&quot;%s%cMYAPP.HLP&quot;, ctx-&gt;appDir, DVX_PATH_SEP);</code></pre>
<h2>Context-Sensitive Topics</h2>
<p>Update helpTopic as the user navigates your application:</p>
<pre><code>snprintf(ctx-&gt;helpTopic, sizeof(ctx-&gt;helpTopic), &quot;settings.video&quot;);</code></pre>
<p>When the user presses F1, the shell launches the help viewer with your help file opened to the specified topic.</p>
<h2>Launching Help from Menus</h2>
<p>To add a Help menu item that opens your help file:</p>
<pre><code>shellLoadAppWithArgs(ctx, viewerPath, helpFilePath);</code></pre>
</div>
</main>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,37 @@
[sdl]
windowresolution = 1024x768
[dosbox]
machine = svga_s3
memsize = 16
[cpu]
core = dynamic
#cputype = 486
#cpu_cycles = 25000
#cpu_cycles_protected = 25000
[render]
aspect = true
[dos]
umb = true
xms = true
ems = true
ver = 6.22
[mouse]
mouse_capture = seamless
dos_mouse_immediate = true
[serial]
serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1
[autoexec]
mount d ~/dos
rem d:\ctmouse\bin\ctmouse.exe /O
mount c .
c:
cd bin
dvx
rem exit

View file

@ -26,12 +26,15 @@ vesa oldvbe10 = false
umb = true
xms = true
ems = true
ver = 6.22
lfn = false
[serial]
serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1
[autoexec]
mount d ~/dos
mount c .
c:
cd bin
dir
dvx

View file

@ -1,42 +0,0 @@
# List Help Library Makefile for DJGPP cross-compilation
#
# Builds listhelp.lib -- shared list/dropdown helper infrastructure
# (dropdown arrow, item length, keyboard navigation, popup list painting).
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I. -I../core -I../core/platform -I../tasks -I../core/thirdparty
OBJDIR = ../obj/listhelp
LIBSDIR = ../bin/libs
SRCS = listHelp.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGET = $(LIBSDIR)/listhelp.lib
.PHONY: all clean
all: $(TARGET) $(LIBSDIR)/listhelp.dep
$(LIBSDIR)/listhelp.dep: ../config/listhelp.dep | $(LIBSDIR)
sed 's/$$/\r/' $< > $@
$(TARGET): $(OBJS) | $(LIBSDIR)
$(DXE3GEN) -o $(LIBSDIR)/listhelp.dxe -E _widgetDraw -E _widgetDropdown -E _widgetMax -E _widgetNavigate -E _widgetPaint -U $(OBJS)
mv $(LIBSDIR)/listhelp.dxe $@
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(LIBSDIR):
mkdir -p $(LIBSDIR)
# Dependencies
$(OBJDIR)/listHelp.o: listHelp.c listHelp.h ../core/dvxWidgetPlugin.h ../core/dvxWidget.h ../core/dvxTypes.h ../core/dvxApp.h ../core/dvxDraw.h ../core/dvxWm.h ../core/dvxVideo.h
clean:
rm -f $(OBJS) $(TARGET) $(LIBSDIR)/listhelp.dep

View file

@ -1,101 +0,0 @@
# listhelp -- Shared List/Dropdown Helper Library
Shared infrastructure for list and dropdown widgets, built as
`listhelp.lib` (DXE3 module). Provides dropdown arrow drawing, item
measurement, keyboard navigation, popup rectangle calculation, and
popup list painting.
Used by: Dropdown, ComboBox, ListBox, ListView, TreeView.
## API Reference
### Dropdown Arrow Glyph
```c
void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops,
int32_t centerX, int32_t centerY, uint32_t color);
```
Draws the small downward-pointing triangle glyph used on dropdown
buttons.
### Item Measurement
```c
int32_t widgetMaxItemLen(const char **items, int32_t count);
```
Scans an array of item strings and returns the length of the longest
one. Used to size dropdown popups and list columns to fit their
content.
### Keyboard Navigation
```c
int32_t widgetNavigateIndex(int32_t key, int32_t current,
int32_t count, int32_t pageSize);
```
Maps arrow key presses to index changes for list navigation. Handles
Up, Down, Home, End, PageUp, and PageDown. Returns the new selected
index, clamped to valid range.
### Popup Rectangle Calculation
```c
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font,
int32_t contentH, int32_t itemCount,
int32_t *popX, int32_t *popY,
int32_t *popW, int32_t *popH);
```
Computes the screen rectangle for a dropdown popup overlay. Positions
the popup below the widget (or above if there is not enough room
below). Limits height to `DROPDOWN_MAX_VISIBLE` items.
### Popup List Painting
```c
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops,
const BitmapFontT *font, const ColorSchemeT *colors,
int32_t popX, int32_t popY, int32_t popW, int32_t popH,
const char **items, int32_t itemCount,
int32_t hoverIdx, int32_t scrollPos);
```
Renders the popup overlay list with items, selection highlight, scroll
position, and beveled border. Used by Dropdown and ComboBox for their
popup overlays.
### Constants
| Name | Value | Description |
|------|-------|-------------|
| `DROPDOWN_BTN_WIDTH` | 16 | Width of dropdown arrow button area |
| `DROPDOWN_MAX_VISIBLE` | 8 | Maximum visible items in popup list |
## Exported Symbols
Matches prefixes: `_widgetDraw*`, `_widgetDropdown*`, `_widgetMax*`,
`_widgetNavigate*`, `_widgetPaint*`.
## Files
| File | Description |
|------|-------------|
| `listHelp.h` | Public API header |
| `listHelp.c` | Complete implementation |
| `Makefile` | Builds `bin/libs/listhelp.lib` + dep file |
## Build
```
make # builds bin/libs/listhelp.lib + listhelp.dep
make clean # removes objects, library, and dep file
```
Depends on: `libtasks.lib`, `libdvx.lib` (via listhelp.dep).

View file

@ -1,181 +0,0 @@
// listHelp.c -- Shared list/dropdown helper functions
//
// Implements dropdown arrow drawing, item length scanning, keyboard
// navigation, and popup list painting used by ListBox, Dropdown,
// ComboBox, ListView, and TreeView widgets.
#include "listHelp.h"
#include "../texthelp/textHelp.h"
#include <string.h>
// ============================================================
// widgetDrawDropdownArrow
// ============================================================
//
// Draws a small downward-pointing filled triangle (7, 5, 3, 1 pixels
// wide across 4 rows) centered at the given position. Used by both
// Dropdown and ComboBox for the drop button arrow glyph.
void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color) {
for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, centerX - 3 + i, centerY + i, 7 - i * 2, color);
}
}
// ============================================================
// widgetMaxItemLen
// ============================================================
//
// Scans an array of string items and returns the maximum strlen.
// Shared by ListBox, Dropdown, and ComboBox to cache the widest
// item length for calcMinSize without duplicating the loop.
int32_t widgetMaxItemLen(const char **items, int32_t count) {
int32_t maxLen = 0;
for (int32_t i = 0; i < count; i++) {
int32_t slen = (int32_t)strlen(items[i]);
if (slen > maxLen) {
maxLen = slen;
}
}
return maxLen;
}
// ============================================================
// widgetNavigateIndex
// ============================================================
//
// Shared keyboard navigation for list-like widgets (ListBox, Dropdown,
// ListView, etc.). Encapsulates the Up/Down/Home/End/PgUp/PgDn logic
// so each widget doesn't have to reimplement index clamping.
//
// Key values use the 0x100 flag to mark extended scan codes (arrow
// keys, Home, End, etc.) -- this is the DVX convention for passing
// scan codes through the same int32_t channel as ASCII values.
//
// Returns -1 for unrecognized keys so callers can check whether the
// key was consumed.
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) {
if (key == (0x50 | 0x100)) {
// Down arrow
if (current < count - 1) {
return current + 1;
}
return current < 0 ? 0 : current;
}
if (key == (0x48 | 0x100)) {
// Up arrow
if (current > 0) {
return current - 1;
}
return current < 0 ? 0 : current;
}
if (key == (0x47 | 0x100)) {
// Home
return 0;
}
if (key == (0x4F | 0x100)) {
// End
return count - 1;
}
if (key == (0x51 | 0x100)) {
// Page Down
int32_t n = current + pageSize;
return n >= count ? count - 1 : n;
}
if (key == (0x49 | 0x100)) {
// Page Up
int32_t n = current - pageSize;
return n < 0 ? 0 : n;
}
return -1;
}
// ============================================================
// widgetPaintPopupList
// ============================================================
//
// Shared popup list painting for Dropdown and ComboBox.
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) {
// Draw popup border
BevelStyleT bevel;
bevel.highlight = colors->windowHighlight;
bevel.shadow = colors->windowShadow;
bevel.face = colors->contentBg;
bevel.width = 2;
drawBevel(d, ops, popX, popY, popW, popH, &bevel);
// Draw items
int32_t visibleItems = popH / font->charHeight;
int32_t textX = popX + TEXT_INPUT_PAD;
int32_t textY = popY + 2;
int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4;
for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) {
int32_t idx = scrollPos + i;
int32_t iy = textY + i * font->charHeight;
uint32_t ifg = colors->contentFg;
uint32_t ibg = colors->contentBg;
if (idx == hoverIdx) {
ifg = colors->menuHighlightFg;
ibg = colors->menuHighlightBg;
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
}
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
}
}
// ============================================================
// widgetDropdownPopupRect
// ============================================================
//
// Calculates the screen rectangle for a dropdown/combobox popup list.
// Shared between Dropdown and ComboBox since they have identical
// popup positioning logic.
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
int32_t visibleItems = itemCount;
if (visibleItems > DROPDOWN_MAX_VISIBLE) {
visibleItems = DROPDOWN_MAX_VISIBLE;
}
if (visibleItems < 1) {
visibleItems = 1;
}
*popX = w->x;
*popW = w->w;
*popH = visibleItems * font->charHeight + 4;
if (w->y + w->h + *popH <= contentH) {
*popY = w->y + w->h;
} else {
*popY = w->y - *popH;
if (*popY < 0) {
*popY = 0;
}
}
}

View file

@ -1,45 +0,0 @@
// listHelp.h -- Public API for the shared list/dropdown helper library
//
// Declares dropdown arrow drawing, item length scanning, keyboard
// navigation, and popup list painting shared across widget DXEs
// (ListBox, Dropdown, ComboBox, ListView, TreeView).
#ifndef LIST_HELP_H
#define LIST_HELP_H
#include "../core/dvxWidgetPlugin.h"
#define DROPDOWN_BTN_WIDTH 16
#define DROPDOWN_MAX_VISIBLE 8
// ============================================================
// Dropdown arrow glyph
// ============================================================
void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color);
// ============================================================
// Item measurement
// ============================================================
int32_t widgetMaxItemLen(const char **items, int32_t count);
// ============================================================
// Keyboard navigation (Up/Down/Home/End/PgUp/PgDn)
// ============================================================
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize);
// ============================================================
// Popup rect calculation
// ============================================================
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH);
// ============================================================
// Popup list painting
// ============================================================
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos);
#endif // LIST_HELP_H

View file

@ -1,54 +0,0 @@
# DVX Loader Makefile for DJGPP cross-compilation
#
# Builds the bootstrap loader (dvx.exe) that loads DXE modules.
# Links dvxPlatformDos.c directly -- the platform layer provides
# the DXE export table via platformRegisterDxeExports().
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff
CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../tasks -I../core/thirdparty
LDFLAGS = -lm
OBJDIR = ../obj/loader
POBJDIR = ../obj/loader/platform
BINDIR = ../bin
SRCS = loaderMain.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
POBJS = $(POBJDIR)/dvxPlatformDos.o
TARGET = $(BINDIR)/dvx.exe
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(POBJS) | $(BINDIR)
$(CC) $(CFLAGS) -o $@ $(OBJS) $(POBJS) $(LDFLAGS) -Wl,-Map=$(BINDIR)/dvx.map
$(EXE2COFF) $@
cat $(CWSDSTUB) $(BINDIR)/dvx > $@
rm -f $(BINDIR)/dvx
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(POBJDIR)/dvxPlatformDos.o: ../core/platform/dvxPlatformDos.c | $(POBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(POBJDIR):
mkdir -p $(POBJDIR)
$(BINDIR):
mkdir -p $(BINDIR)
# Dependencies
$(OBJDIR)/loaderMain.o: loaderMain.c ../core/platform/dvxPlatform.h ../core/dvxTypes.h
$(POBJDIR)/dvxPlatformDos.o: ../core/platform/dvxPlatformDos.c ../core/platform/dvxPlatform.h ../core/dvxTypes.h ../core/dvxPalette.h
clean:
rm -f $(OBJS) $(POBJS) $(TARGET) $(BINDIR)/dvx.map

View file

@ -1,133 +0,0 @@
# DVX Loader
Bootstrap loader for the DVX desktop environment. Builds as `dvx.exe`
-- the only native executable in the system. Everything else is a
dynamically loaded DXE3 module.
## What It Does
1. Changes working directory to the directory containing `dvx.exe`
2. Truncates `dvx.log` and initializes logging
3. Calls `platformInit()` to suppress Ctrl+C and install signal handlers
4. Calls `platformRegisterDxeExports()` to register platform and C
runtime symbols (libc, libm, libgcc) for DXE module resolution
5. Scans and loads all modules in two phases (see below)
6. Finds `shellMain()` via `dlsym` across loaded modules
7. Calls `shellMain()` -- the shell takes over from here
8. On return, closes all module handles in reverse load order
## Two-Phase Module Loading
### Phase 1: Libraries (libs/*.lib)
Recursively scans the `LIBS/` directory for `.lib` files. Each module may have a
`.dep` file (same base name, `.dep` extension) listing base names of
modules that must load first. The loader resolves the dependency graph
and loads in topological order.
Load order (via dep files):
```
libtasks.lib (no deps)
libdvx.lib (deps: libtasks)
texthelp.lib (deps: libtasks, libdvx)
listhelp.lib (deps: libtasks, libdvx)
dvxshell.lib (deps: libtasks, libdvx, texthelp, listhelp)
taskmgr.lib (deps: dvxshell, libtasks, libdvx, texthelp, listhelp)
```
### Phase 2: Widgets (widgets/*.wgt)
Recursively scans the `WIDGETS/` directory for `.wgt` files. Widget modules may
also have `.dep` files (e.g., `textinpt.dep` lists `texthelp`).
Loaded in topological order, same as libs.
After loading each module, the loader checks for a `wgtRegister`
export. If present, it is called immediately so the widget registers
its class(es) and API with the core.
## Dependency File Format
Plain text, one dependency base name per line. Empty lines and lines
starting with `#` are ignored. Names are case-insensitive.
Example (`combobox.dep`):
```
texthelp
listhelp
```
## Hosted Components
### dvxLog()
The global logging function is defined in `loaderMain.c` and exported
to all DXE modules. It appends a line to `dvx.log`, opening and
closing the file per write so it is never held open.
```c
void dvxLog(const char *fmt, ...);
```
### stb_ds
The `STB_DS_IMPLEMENTATION` is compiled into the loader with
`STBDS_REALLOC` and `STBDS_FREE` overridden to use `dvxRealloc` and
`dvxFree`. This means all `arrput`/`hmput`/`arrfree` calls in DXE
code route through the per-app memory tracker, so stb_ds memory is
attributed correctly in the Task Manager's memory column. stb_ds
functions are exported to all DXE modules via the platform export
table.
### Platform Layer
`dvxPlatformDos.c` is compiled into the loader (not into libdvx.lib).
All platform functions are exported to DXE modules. This includes:
* Video: VESA VBE init, LFB mapping, mode enumeration
* Input: INT 33h mouse, INT 16h keyboard, CuteMouse wheel API
* Spans: `rep stosl`/`rep movsd` asm inner loops (8/16/32 bpp)
* DXE: symbol registration, symbol overrides
* Crash: signal handler installation, register dump logging
* System: memory info, directory creation, path utilities
### Per-App Memory Tracking
The DXE export table maps standard C allocation symbols to tracked
wrappers:
| C Symbol | Mapped To | Effect |
|----------|-----------|--------|
| `malloc` | `dvxMalloc` | Allocations attributed to `currentAppId` |
| `calloc` | `dvxCalloc` | Same |
| `realloc` | `dvxRealloc` | Transfers attribution on resize |
| `free` | `dvxFree` | Decrements app's tracked usage |
| `strdup` | `dvxStrdup` | Tracks the duplicated string |
This is transparent to DXE code -- apps call `malloc` normally and the
tracked wrapper runs instead. The Task Manager reads per-app usage via
`dvxMemGetAppUsage()`. When an app is reaped, `dvxMemResetApp()` zeroes
its counter.
## Files
| File | Description |
|------|-------------|
| `loaderMain.c` | Entry point, module scanner, dependency resolver, logger |
| `Makefile` | Builds `bin/dvx.exe` |
## Build
```
make # builds bin/dvx.exe
make clean # removes objects and binary
```
The loader links `loaderMain.c` + `dvxPlatformDos.c` into a native
DJGPP executable. The CWSDPMI stub is prepended via exe2coff +
CWSDSTUB.EXE for standalone execution.

View file

@ -1,478 +0,0 @@
// loaderMain.c -- DVX bootstrap loader entry point
//
// Loads all DXE modules from two directories:
// libs/ *.lib -- core libraries (libtasks, libdvx, dvxshell)
// widgets/ *.wgt -- widget type plugins (box, button, listview, etc.)
//
// Each module may have a .dep file (same base name, .dep extension)
// listing base names of modules that must be loaded before it.
// The loader resolves the dependency graph and loads in topological
// order. After loading, any module that exports wgtRegister() has
// it called. Finally, the loader finds and calls shellMain().
#include "dvxPlatform.h"
#include <ctype.h>
#include <dirent.h>
#include <dlfcn.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
// Route stb_ds allocations through the tracking wrappers so that
// arrput/arrfree in DXE code is tracked per-app.
extern void *dvxRealloc(void *ptr, size_t size);
extern void dvxFree(void *ptr);
#define STBDS_REALLOC(c, p, s) dvxRealloc((p), (s))
#define STBDS_FREE(c, p) dvxFree(p)
#define STB_DS_IMPLEMENTATION
#include "stb_ds.h"
// ============================================================
// Constants
// ============================================================
#define LIBS_DIR "LIBS"
#define WIDGET_DIR "WIDGETS"
#define LOG_PATH "dvx.log"
// ============================================================
// dvxLog -- append a line to dvx.log
// ============================================================
//
// Global logging function exported to all DXE modules.
// Opens/closes the file per-write so it's never held open.
void dvxLog(const char *fmt, ...) {
FILE *f = fopen(LOG_PATH, "a");
if (!f) {
return;
}
va_list ap;
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
fprintf(f, "\n");
fclose(f);
}
// ============================================================
// Module entry for dependency resolution
// ============================================================
typedef struct {
char path[260];
char baseName[16];
char **deps;
bool loaded;
void *handle;
} ModuleT;
// ============================================================
// Prototypes
// ============================================================
static bool allDepsLoaded(const ModuleT *mod, const ModuleT *mods);
static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize);
static void *findSymbol(void **handles, const char *symbol);
static void freeMods(ModuleT *mods);
static void **loadAllModules(void);
static void loadInOrder(ModuleT *mods);
static void logAndReadDeps(ModuleT *mods);
static void readDeps(ModuleT *mod);
static void scanDir(const char *dirPath, const char *ext, ModuleT **mods);
// ============================================================
// extractBaseName -- strip directory and extension, lowercase
// ============================================================
static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize) {
// Find last directory separator
const char *start = path;
const char *p = path;
while (*p) {
if (*p == '/' || *p == '\\') {
start = p + 1;
}
p++;
}
// Copy up to the extension
int32_t extLen = strlen(ext);
int32_t len = strlen(start);
if (len > extLen && strcasecmp(start + len - extLen, ext) == 0) {
len -= extLen;
}
if (len >= outSize) {
len = outSize - 1;
}
for (int32_t i = 0; i < len; i++) {
out[i] = tolower((unsigned char)start[i]);
}
out[len] = '\0';
}
// ============================================================
// readDeps -- parse .dep file for a module
// ============================================================
//
// The .dep file has the same path as the module but with a .dep
// extension. Each line is a dependency base name. Empty lines
// and lines starting with # are ignored.
static void readDeps(ModuleT *mod) {
// Build dep file path: replace extension with .dep
char depPath[260];
strncpy(depPath, mod->path, sizeof(depPath) - 1);
depPath[sizeof(depPath) - 1] = '\0';
char *dot = strrchr(depPath, '.');
if (!dot) {
return;
}
strcpy(dot, ".dep");
FILE *f = fopen(depPath, "r");
if (!f) {
return;
}
char line[64];
while (fgets(line, sizeof(line), f)) {
// Strip \r and \n
int32_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
line[--len] = '\0';
}
// Skip empty lines and comments
if (len == 0 || line[0] == '#') {
continue;
}
// Lowercase the dep name for case-insensitive matching
for (int32_t i = 0; i < len; i++) {
line[i] = tolower((unsigned char)line[i]);
}
arrput(mod->deps, strdup(line));
}
fclose(f);
}
// ============================================================
// scanDir -- recursively find modules with a given extension
// ============================================================
static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
DIR *dir = opendir(dirPath);
if (!dir) {
return;
}
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
const char *name = ent->d_name;
// Skip . and ..
if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) {
continue;
}
char path[260];
snprintf(path, sizeof(path), "%s/%s", dirPath, name);
// Check for matching extension
int32_t nameLen = strlen(name);
int32_t extLen = strlen(ext);
if (nameLen > extLen && strcasecmp(name + nameLen - extLen, ext) == 0) {
ModuleT mod;
memset(&mod, 0, sizeof(mod));
strncpy(mod.path, path, sizeof(mod.path) - 1);
extractBaseName(path, ext, mod.baseName, sizeof(mod.baseName));
arrput(*mods, mod);
continue;
}
// Recurse into subdirectories
struct stat st;
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
scanDir(path, ext, mods);
}
}
closedir(dir);
}
// ============================================================
// allDepsLoaded -- check if a module's dependencies are satisfied
// ============================================================
//
// A dep is satisfied if either:
// 1. No module with that base name exists (external, assumed OK)
// 2. The module with that base name is already loaded
static bool allDepsLoaded(const ModuleT *mod, const ModuleT *mods) {
for (int32_t d = 0; d < arrlen(mod->deps); d++) {
for (int32_t j = 0; j < arrlen(mods); j++) {
if (strcasecmp(mods[j].baseName, mod->deps[d]) == 0) {
if (!mods[j].loaded) {
return false;
}
break;
}
}
}
return true;
}
// ============================================================
// loadInOrder -- topological sort and load
// ============================================================
//
// Repeatedly scans the module list for entries whose dependencies
// are all satisfied, loads them, and marks them done. Stops when
// all modules are loaded or no progress can be made (circular dep).
static void loadInOrder(ModuleT *mods) {
typedef void (*RegFnT)(void);
int32_t total = arrlen(mods);
int32_t loaded = 0;
bool progress;
do {
progress = false;
for (int32_t i = 0; i < total; i++) {
if (mods[i].loaded) {
continue;
}
if (!allDepsLoaded(&mods[i], mods)) {
continue;
}
dvxLog("Loading: %s", mods[i].path);
mods[i].handle = dlopen(mods[i].path, RTLD_GLOBAL);
if (!mods[i].handle) {
const char *err = dlerror();
dvxLog(" FAILED: %s", err ? err : "(unknown)");
mods[i].loaded = true;
loaded++;
progress = true;
continue;
}
RegFnT regFn = (RegFnT)dlsym(mods[i].handle, "_wgtRegister");
if (regFn) {
regFn();
}
mods[i].loaded = true;
loaded++;
progress = true;
}
} while (progress && loaded < total);
if (loaded < total) {
fprintf(stderr, "Module loader: %d of %d modules could not be loaded (circular deps or missing deps)\n", total - loaded, total);
for (int32_t i = 0; i < total; i++) {
if (!mods[i].loaded) {
fprintf(stderr, " %s\n", mods[i].path);
}
}
}
}
// ============================================================
// loadAllModules -- scan libs/ and widgets/, load in dep order
// ============================================================
//
// Returns a stb_ds dynamic array of dlopen handles.
static void logAndReadDeps(ModuleT *mods) {
dvxLog("Discovered %d modules:", arrlen(mods));
for (int32_t i = 0; i < arrlen(mods); i++) {
dvxLog(" [%d] %s (base: %s)", i, mods[i].path, mods[i].baseName);
}
for (int32_t i = 0; i < arrlen(mods); i++) {
readDeps(&mods[i]);
if (arrlen(mods[i].deps) > 0) {
dvxLog(" %s deps:", mods[i].baseName);
for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
dvxLog(" %s", mods[i].deps[d]);
}
}
}
}
static void freeMods(ModuleT *mods) {
for (int32_t i = 0; i < arrlen(mods); i++) {
for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
free(mods[i].deps[d]);
}
arrfree(mods[i].deps);
}
arrfree(mods);
}
static void **loadAllModules(void) {
void **handles = NULL;
// Phase 1: load libraries in dependency order (libs before widgets)
ModuleT *libs = NULL;
scanDir(LIBS_DIR, ".lib", &libs);
logAndReadDeps(libs);
loadInOrder(libs);
for (int32_t i = 0; i < arrlen(libs); i++) {
if (libs[i].handle) {
arrput(handles, libs[i].handle);
}
}
freeMods(libs);
// Phase 2: load widgets in dependency order (all libs already loaded)
ModuleT *widgets = NULL;
scanDir(WIDGET_DIR, ".wgt", &widgets);
logAndReadDeps(widgets);
loadInOrder(widgets);
for (int32_t i = 0; i < arrlen(widgets); i++) {
if (widgets[i].handle) {
arrput(handles, widgets[i].handle);
}
}
freeMods(widgets);
return handles;
}
// ============================================================
// findSymbol -- search loaded handles for a symbol
// ============================================================
static void *findSymbol(void **handles, const char *symbol) {
for (int32_t i = 0; i < arrlen(handles); i++) {
void *sym = dlsym(handles[i], symbol);
if (sym) {
return sym;
}
}
return NULL;
}
// ============================================================
// main
// ============================================================
int main(int argc, char *argv[]) {
// Change to the directory containing the executable so relative
// paths (LIBS/, WIDGETS/, APPS/, CONFIG/) resolve correctly.
char exeDir[260];
strncpy(exeDir, argv[0], sizeof(exeDir) - 1);
exeDir[sizeof(exeDir) - 1] = '\0';
char *sep = platformPathDirEnd(exeDir);
if (sep) {
*sep = '\0';
platformChdir(exeDir);
}
// Truncate log, then use append-per-write
FILE *logInit = fopen(LOG_PATH, "w");
if (logInit) {
fclose(logInit);
}
// Suppress Ctrl+C before anything else
platformInit();
dvxLog("DVX Loader starting...");
// Register platform + libc/libm/runtime symbols for DXE resolution
platformRegisterDxeExports();
dvxLog("Platform exports registered.");
// Load all modules from libs/ and widgets/ in dependency order.
// Each module may have a .dep file specifying load-before deps.
// Widget modules that export wgtRegister() get it called.
void **handles = loadAllModules();
if (!handles || arrlen(handles) == 0) {
fprintf(stderr, "No modules loaded from %s/ or %s/\n", LIBS_DIR, WIDGET_DIR);
arrfree(handles);
return 1;
}
// Find and call shellMain from whichever module exports it
typedef int (*ShellMainFnT)(int, char **);
ShellMainFnT shellMain = (ShellMainFnT)findSymbol(handles, "_shellMain");
if (!shellMain) {
dvxLog("ERROR: No module exports shellMain");
for (int32_t i = arrlen(handles) - 1; i >= 0; i--) {
dlclose(handles[i]);
}
arrfree(handles);
return 1;
}
int result = shellMain(argc, argv);
// Clean up in reverse load order
for (int32_t i = arrlen(handles) - 1; i >= 0; i--) {
dlclose(handles[i]);
}
arrfree(handles);
return result;
}

46
mkcd.sh
View file

@ -1,5 +1,28 @@
#!/bin/bash
# mkcd.sh -- Build DVX and create a CD-ROM ISO image for 86Box
# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# mkcd.sh -- Build DVX and create a CD-ROM ISO image for the target emulator
#
# Usage: ./mkcd.sh
#
@ -7,8 +30,8 @@
# an ISO 9660 image from the bin/ directory. The ISO uses short
# 8.3 filenames (-iso-level 1) for DOS compatibility.
#
# The ISO is placed in 86Box's data directory so it can be mounted
# as a CD-ROM drive.
# The ISO is placed in the emulator's data directory so it can be
# mounted as a CD-ROM drive.
set -e
@ -21,7 +44,7 @@ echo "Building DVX..."
make -C "$SCRIPT_DIR" all
# Verify core build output exists
for f in dvx.exe libs/libtasks.lib libs/libdvx.lib libs/dvxshell.lib; do
for f in dvx.exe libs/kpunch/libtasks/libtasks.lib libs/kpunch/libdvx/libdvx.lib libs/kpunch/dvxshell/dvxshell.lib; do
if [ ! -f "$SCRIPT_DIR/bin/$f" ]; then
echo "ERROR: bin/$f not found -- build failed?"
exit 1
@ -30,21 +53,24 @@ done
# Verify widget DXEs exist
WGT_COUNT=0
for f in "$SCRIPT_DIR"/bin/widgets/*.wgt; do
[ -f "$f" ] && WGT_COUNT=$((WGT_COUNT + 1))
done
while IFS= read -r f; do
WGT_COUNT=$((WGT_COUNT + 1))
done < <(find "$SCRIPT_DIR/bin/widgets/" -name "*.wgt" -type f 2>/dev/null)
echo "$WGT_COUNT widget modules found in bin/widgets/."
# Clean emulator local-filesystem artifacts
find "$SCRIPT_DIR/bin/" -name ".DBLOCALFILE*" -delete 2>/dev/null
# Create the ISO image
# -iso-level 1: strict 8.3 filenames (DOS compatibility)
# -J: Joliet extensions (long names for Windows/Linux)
# -R: Rock Ridge (long names for Linux)
# -V: volume label
# Note: -R (Rock Ridge) is omitted because DOS surfaces its
# attribute sidecar files (._ATR_) as visible junk files.
echo "Creating ISO image..."
mkisofs \
-iso-level 1 \
-J \
-R \
-V "DVX" \
-o "$ISO_PATH" \
"$SCRIPT_DIR/bin/"
@ -52,5 +78,5 @@ mkisofs \
echo "ISO created: $ISO_PATH"
echo "Size: $(du -h "$ISO_PATH" | cut -f1)"
echo ""
echo "In 86Box, mount $ISO_PATH as a CD-ROM drive."
echo "Mount $ISO_PATH as a CD-ROM drive in the target emulator."
echo "Then from DOS: D:\\DVX.EXE (or whatever drive letter)"

View file

@ -1,38 +0,0 @@
# Packet Serial Transport Makefile for DJGPP cross-compilation
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar
RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
OBJDIR = ../obj/packet
LIBDIR = ../lib
SRCS = packet.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGET = $(LIBDIR)/libpacket.a
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) | $(LIBDIR)
$(AR) rcs $@ $(OBJS)
$(RANLIB) $@
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(LIBDIR):
mkdir -p $(LIBDIR)
# Dependencies
$(OBJDIR)/packet.o: packet.c packet.h ../rs232/rs232.h
clean:
rm -rf $(OBJDIR) $(TARGET)

View file

@ -1,428 +0,0 @@
# Packet -- Reliable Serial Transport with HDLC Framing
Packetized serial transport providing reliable, ordered delivery over an
unreliable serial link. Uses HDLC-style byte-stuffed framing, CRC-16-CCITT
error detection, and a Go-Back-N sliding window protocol for automatic
retransmission.
This layer sits on top of an already-open rs232 COM port. It does not
open or close the serial port itself.
## Architecture
```
Application
|
| pktSend() queue a packet for reliable delivery
| pktPoll() receive, process ACKs/NAKs, check retransmit timers
|
[Packet Layer] framing, CRC, sequencing, sliding window ARQ
|
[rs232] raw byte I/O via ISR-driven ring buffers
|
UART
```
The packet layer adds framing, error detection, and reliability to the
raw byte stream provided by rs232. The caller provides a receive callback
that is invoked synchronously from `pktPoll()` for each complete, CRC-verified,
in-order data packet.
## Frame Format
Before byte stuffing, each frame has the following layout:
```
[0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI]
```
| Field | Size | Description |
|-----------|-----------|----------------------------------------------|
| `0x7E` | 1 byte | Flag byte -- frame delimiter |
| `SEQ` | 1 byte | Sequence number (wrapping uint8_t, 0-255) |
| `TYPE` | 1 byte | Frame type (DATA, ACK, NAK, RST) |
| `LEN` | 1 byte | Payload length (0-255) |
| Payload | 0-255 | Application data |
| `CRC` | 2 bytes | CRC-16-CCITT, little-endian, over SEQ..payload |
The header is 3 bytes (SEQ + TYPE + LEN), the CRC is 2 bytes, so the
minimum frame size (no payload) is 5 bytes. The maximum frame size (255-byte
payload) is 260 bytes before byte stuffing.
### Frame Types
| Type | Value | Description |
|--------|-------|----------------------------------------------------|
| `DATA` | 0x00 | Carries application payload |
| `ACK` | 0x01 | Cumulative acknowledgment -- next expected sequence |
| `NAK` | 0x02 | Negative ack -- request retransmit from this seq |
| `RST` | 0x03 | Connection reset -- clear all state |
### Byte Stuffing
HDLC transparency encoding ensures the flag byte (0x7E) and escape byte
(0x7D) never appear in the frame body. Within the frame data (everything
between flags), these two bytes are escaped by prefixing with 0x7D and
XORing the original byte with 0x20:
- `0x7E` becomes `0x7D 0x5E`
- `0x7D` becomes `0x7D 0x5D`
In the worst case, every byte in the frame is escaped, doubling the wire
size. In practice, these byte values are uncommon in typical data and the
overhead is minimal.
### Why HDLC Framing
HDLC's flag-byte + byte-stuffing scheme is the simplest way to delimit
variable-length frames on a raw byte stream. The 0x7E flag byte
unambiguously marks frame boundaries. This is proven, lightweight, and
requires zero buffering at the framing layer.
The alternative -- length-prefixed framing -- is fragile on noisy links
because a corrupted length field permanently desynchronizes the receiver.
With HDLC framing, the receiver can always resynchronize by hunting for
the next flag byte.
## CRC-16-CCITT
Error detection uses CRC-16-CCITT (polynomial 0x1021, initial value
0xFFFF). The CRC covers the SEQ, TYPE, LEN, and payload fields. It is
stored little-endian in the frame (CRC low byte first, then CRC high byte).
The CRC is computed via a 256-entry lookup table (512 bytes of `.rodata`).
Table-driven CRC is approximately 10x faster than bit-by-bit computation
on a 486 -- a worthwhile trade for a function called on every frame
transmitted and received.
## Go-Back-N Sliding Window Protocol
### Why Go-Back-N
Go-Back-N ARQ is simpler than Selective Repeat -- the receiver does not
need an out-of-order reassembly buffer and only tracks a single expected
sequence number. This works well for the low bandwidth-delay product of a
serial link. On a 115200 bps local connection, the round-trip time is
negligible, so the window rarely fills.
Go-Back-N's retransmit-all-from-NAK behavior wastes bandwidth on lossy
links, but serial links are nearly lossless. The CRC check is primarily a
safety net for electrical noise, not a routine error recovery mechanism.
### Protocol Details
The sliding window is configurable from 1 to 8 slots (default 4).
Sequence numbers are 8-bit unsigned integers that wrap naturally at 256.
The sequence space (256) is much larger than 2x the maximum window (16),
so there is no ambiguity between old and new frames.
**Sender behavior:**
- Assigns a monotonically increasing sequence number to each DATA frame
- Retains a copy of each sent frame in a retransmit slot until it is
acknowledged
- When the window is full (`txCount >= windowSize`), blocks or returns
`PKT_ERR_TX_FULL` depending on the `block` parameter
**Receiver behavior:**
- Accepts frames strictly in order (`seq == rxExpectSeq`)
- On in-order delivery, increments `rxExpectSeq` and sends an ACK
carrying the new expected sequence number (cumulative acknowledgment)
- Out-of-order frames within the window trigger a NAK for the expected
sequence number
- Duplicate and out-of-window frames are silently discarded
**ACK processing:**
- ACKs carry the next expected sequence number (cumulative)
- On receiving an ACK, the sender frees all retransmit slots with
sequence numbers less than the ACK's sequence number
**NAK processing:**
- A NAK requests retransmission from a specific sequence number
- The sender retransmits that frame AND all subsequent unacknowledged
frames (the Go-Back-N property)
- Each retransmitted slot has its timer reset
**RST processing:**
- Resets all sequence numbers and buffers to zero on both sides
- The remote side also sends a RST in response
### Timer-Based Retransmission
Each retransmit slot tracks the time it was last (re)transmitted. If
500ms elapses without an ACK, the slot is retransmitted and the timer
is reset. This handles the case where an ACK or NAK was lost on the
wire -- without this safety net, the connection would stall permanently.
The 500ms timeout is conservative for a local serial link (RTT is under
1ms) but accounts for the remote side being busy processing. On BBS
connections through the Linux proxy, the round-trip includes TCP latency,
making the generous timeout appropriate.
### Receive State Machine
Incoming bytes from the serial port are fed through a three-state HDLC
deframing state machine:
| State | Description |
|----------|------------------------------------------------------|
| `HUNT` | Discarding bytes until a flag (0x7E) is seen |
| `ACTIVE` | Accumulating frame bytes; flag ends frame, ESC escapes |
| `ESCAPE` | Previous byte was 0x7D; XOR this byte with 0x20 |
The flag byte serves double duty: it ends the current frame AND starts
the next one. Back-to-back frames share a single flag byte, saving
bandwidth. A frame is only processed if it meets the minimum size
requirement (5 bytes), so spurious flags produce harmless zero-length
"frames" that are discarded.
## API Reference
### Types
```c
// Receive callback -- called for each verified, in-order data packet
typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len);
// Opaque connection handle
typedef struct PktConnS PktConnT;
```
### Constants
| Name | Value | Description |
|------------------------|-------|--------------------------------------|
| `PKT_MAX_PAYLOAD` | 255 | Maximum payload bytes per packet |
| `PKT_DEFAULT_WINDOW` | 4 | Default sliding window size |
| `PKT_MAX_WINDOW` | 8 | Maximum sliding window size |
| `PKT_SUCCESS` | 0 | Success |
| `PKT_ERR_INVALID_PORT` | -1 | Invalid COM port |
| `PKT_ERR_NOT_OPEN` | -2 | Connection not open |
| `PKT_ERR_ALREADY_OPEN` | -3 | Connection already open |
| `PKT_ERR_WOULD_BLOCK` | -4 | Operation would block |
| `PKT_ERR_OVERFLOW` | -5 | Buffer overflow |
| `PKT_ERR_INVALID_PARAM`| -6 | Invalid parameter |
| `PKT_ERR_TX_FULL` | -7 | Transmit window full (non-blocking) |
| `PKT_ERR_NO_DATA` | -8 | No data available |
| `PKT_ERR_DISCONNECTED` | -9 | Serial port disconnected or error |
### Functions
#### pktOpen
```c
PktConnT *pktOpen(int com, int windowSize,
PktRecvCallbackT callback, void *callbackCtx);
```
Creates a packetized connection over an already-open COM port.
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
- `windowSize` -- sliding window size (1-8), or 0 for the default (4)
- `callback` -- called from `pktPoll()` for each received, verified,
in-order data packet. The `data` pointer is valid only during the
callback.
- `callbackCtx` -- user pointer passed through to the callback
Returns a connection handle, or `NULL` on failure (allocation error).
#### pktClose
```c
void pktClose(PktConnT *conn);
```
Frees the connection state. Does **not** close the underlying COM port.
The caller is responsible for calling `rs232Close()` separately.
#### pktSend
```c
int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block);
```
Sends a data packet. `len` must be in the range 1 to `PKT_MAX_PAYLOAD`
(255). The data is copied into a retransmit slot before transmission, so
the caller can reuse its buffer immediately.
- `block = true` -- If the transmit window is full, polls internally
(calling `pktPoll()` in a tight loop) until an ACK frees a slot. Returns
`PKT_ERR_DISCONNECTED` if the serial port drops during the wait.
- `block = false` -- Returns `PKT_ERR_TX_FULL` immediately if the window
is full.
Returns `PKT_SUCCESS` on success.
#### pktPoll
```c
int pktPoll(PktConnT *conn);
```
The main work function. Must be called frequently (every iteration of
your main loop or event loop). It performs three tasks:
1. **Drain serial RX** -- reads all available bytes from the rs232 port
and feeds them through the HDLC deframing state machine
2. **Process frames** -- verifies CRC, handles DATA/ACK/NAK/RST frames,
delivers data packets to the callback
3. **Check retransmit timers** -- resends any slots that have timed out
Returns the number of DATA packets delivered to the callback this call,
or `PKT_ERR_DISCONNECTED` if the serial port returned an error, or
`PKT_ERR_INVALID_PARAM` if `conn` is NULL.
The callback is invoked synchronously, so the caller should be prepared
for re-entrant calls to `pktSend()` from within the callback.
#### pktReset
```c
int pktReset(PktConnT *conn);
```
Resets all sequence numbers, TX slots, and RX state to zero. Sends a RST
frame to the remote side so it resets as well. Useful for recovering from
a desynchronized state.
#### pktCanSend
```c
bool pktCanSend(PktConnT *conn);
```
Returns `true` if there is room in the transmit window for another
packet. Useful for non-blocking send loops to avoid calling `pktSend()`
when it would return `PKT_ERR_TX_FULL`.
#### pktGetPending
```c
int pktGetPending(PktConnT *conn);
```
Returns the number of unacknowledged packets currently in the transmit
window. Ranges from 0 (all sent packets acknowledged) to `windowSize`
(window full). Useful for throttling sends and monitoring link health.
## Usage Example
```c
#include "packet.h"
#include "../rs232/rs232.h"
void onPacket(void *ctx, const uint8_t *data, int len) {
// process received packet -- data is valid only during this callback
}
int main(void) {
// Open serial port first (packet layer does not manage it)
rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE);
// Create packet connection with default window size (4)
PktConnT *conn = pktOpen(RS232_COM1, 0, onPacket, NULL);
// Send a packet (blocking -- waits for window space if needed)
uint8_t msg[] = "Hello, packets!";
pktSend(conn, msg, sizeof(msg), true);
// Main loop -- must call pktPoll() frequently
while (1) {
int delivered = pktPoll(conn);
if (delivered == PKT_ERR_DISCONNECTED) {
break;
}
// delivered = number of packets received this iteration
}
pktClose(conn);
rs232Close(RS232_COM1);
return 0;
}
```
### Non-Blocking Send Pattern
```c
// Send as fast as the window allows, doing other work between sends
while (bytesLeft > 0) {
pktPoll(conn); // process ACKs, free window slots
if (pktCanSend(conn)) {
int chunk = bytesLeft;
if (chunk > PKT_MAX_PAYLOAD) {
chunk = PKT_MAX_PAYLOAD;
}
if (pktSend(conn, data + offset, chunk, false) == PKT_SUCCESS) {
offset += chunk;
bytesLeft -= chunk;
}
}
// do other work here (update UI, check for cancel, etc.)
}
// Drain remaining ACKs
while (pktGetPending(conn) > 0) {
pktPoll(conn);
}
```
## Internal Data Structures
### Connection State (PktConnT)
The connection handle contains:
- **COM port index** and **window size** (configuration)
- **Callback** function pointer and context
- **TX state**: next sequence to assign, oldest unacked sequence, array
of retransmit slots, count of slots in use
- **RX state**: next expected sequence, deframing state machine state,
frame accumulation buffer
### Retransmit Slots (TxSlotT)
Each slot holds a copy of the sent payload, its sequence number, payload
length, and a `clock_t` timestamp of when it was last transmitted. The
retransmit timer compares this timestamp against the current time to
detect timeout.
## Building
```
make # builds ../lib/libpacket.a
make clean # removes objects and library
```
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/packet/`, the library in `../lib/`.
Requires `librs232.a` at link time (for `rs232Read()` and `rs232Write()`).
## Files
- `packet.h` -- Public API header (types, constants, function prototypes)
- `packet.c` -- Complete implementation (framing, CRC, ARQ, state machine)
- `Makefile` -- DJGPP cross-compilation build rules
## Dependencies
- `rs232/` -- Serial port I/O (must be linked: `-lrs232`)
## Used By
- `seclink/` -- Secure serial link (adds channel multiplexing and encryption)
- `proxy/` -- Linux serial proxy (uses a socket-based adaptation)

View file

@ -1,7 +0,0 @@
// Stub for DJGPP <go32.h> -- Linux proxy build
#ifndef GO32_H_STUB
#define GO32_H_STUB
#define _dos_ds 0
#endif

View file

@ -1,8 +0,0 @@
// Stub for DJGPP <pc.h> -- Linux proxy build
#ifndef PC_H_STUB
#define PC_H_STUB
static inline void outportb(unsigned short port, unsigned char val) { (void)port; (void)val; }
static inline unsigned char inportb(unsigned short port) { (void)port; return 0; }
#endif

View file

@ -1,7 +0,0 @@
// Stub for DJGPP <sys/farptr.h> -- Linux proxy build
#ifndef FARPTR_H_STUB
#define FARPTR_H_STUB
static inline unsigned long _farpeekl(unsigned short sel, unsigned long ofs) { (void)sel; (void)ofs; return 0; }
#endif

BIN
releases/dvx-a3.zip (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -1,38 +0,0 @@
# RS-232 Serial Library Makefile for DJGPP cross-compilation
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar
RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
OBJDIR = ../obj/rs232
LIBDIR = ../lib
SRCS = rs232.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGET = $(LIBDIR)/librs232.a
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) | $(LIBDIR)
$(AR) rcs $@ $(OBJS)
$(RANLIB) $@
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(LIBDIR):
mkdir -p $(LIBDIR)
# Dependencies
$(OBJDIR)/rs232.o: rs232.c rs232.h
clean:
rm -rf $(OBJDIR) $(TARGET)

View file

@ -1,417 +0,0 @@
# RS232 -- ISR-Driven Serial Port Library for DJGPP
Interrupt-driven UART communication library supporting up to 4 simultaneous
COM ports with ring buffers and hardware/software flow control. Targets
486-class DOS hardware running under DJGPP/DPMI.
Ported from the DOS Serial Library 1.4 by Karl Stenerud (MIT License),
stripped to DJGPP-only codepaths and restyled for the DVX project.
## Architecture
The library is built around a single shared ISR (`comGeneralIsr`) that
services all open COM ports. This design is necessary because COM1/COM3
typically share IRQ4 and COM2/COM4 share IRQ3 -- a single handler that
polls all ports avoids the complexity of per-IRQ dispatch.
```
Application
|
| rs232Read() non-blocking drain from RX ring buffer
| rs232Write() blocking polled write directly to UART THR
| rs232WriteBuf() non-blocking write into TX ring buffer
|
[Ring Buffers] 2048-byte RX + TX per port, power-of-2 bitmask indexing
|
[ISR] comGeneralIsr -- shared handler for all open ports
|
[UART] 8250 / 16450 / 16550 / 16550A hardware
```
### ISR Design
The ISR follows a careful protocol to remain safe under DPMI while
keeping the system responsive:
1. **Mask** all COM port IRQs on the PIC to prevent ISR re-entry
2. **STI** to allow higher-priority interrupts (timer tick, keyboard) through
3. **Loop** over all open ports, draining each UART's pending interrupt
conditions (data ready, TX hold empty, modem status, line status)
4. **CLI**, send EOI to the PIC, re-enable COM IRQs, **STI** before IRET
This mask-then-STI pattern is standard for slow device ISRs on PC
hardware. It prevents the same IRQ from re-entering while allowing the
system timer and keyboard to function during UART processing.
### Ring Buffers
Both RX and TX buffers are 2048 bytes, sized as a power of 2 so that
head/tail wraparound is a single AND operation (bitmask indexing) rather
than an expensive modulo -- critical for ISR-speed code on a 486.
The buffers use a one-slot-wasted design to distinguish full from empty:
`head == tail` means empty, `(head + 1) & MASK == tail` means full.
### Flow Control
Flow control operates entirely within the ISR using watermark thresholds.
When the RX buffer crosses 80% full, the ISR signals the remote side to
stop sending; when it drops below 20%, the ISR allows the remote to
resume. This prevents buffer overflow without any application involvement.
Three modes are supported:
| Mode | Stop Signal | Resume Signal |
|------------|-------------------|-------------------|
| XON/XOFF | Send XOFF (0x13) | Send XON (0x11) |
| RTS/CTS | Deassert RTS | Assert RTS |
| DTR/DSR | Deassert DTR | Assert DTR |
On the TX side, the ISR monitors incoming XON/XOFF bytes and the CTS/DSR
modem status lines to pause and resume transmission from the TX ring
buffer.
### DPMI Memory Locking
The ISR code and all per-port state structures (`sComPorts` array) are
locked in physical memory via `__dpmi_lock_linear_region`. This prevents
page faults during interrupt handling -- a hard requirement for any ISR
running under a DPMI host (DOS extender, Windows 3.x, OS/2 VDM, etc.).
An IRET wrapper is allocated by DPMI to handle the real-mode to
protected-mode transition on hardware interrupt entry.
## UART Type Detection
`rs232GetUartType()` probes the UART hardware to identify the chip:
1. **Scratch register test** -- Writes two known values (0xAA, 0x55) to
UART register 7 and reads them back. The 8250 lacks this register, so
readback fails. If both values read back correctly, the UART is at
least a 16450.
2. **FIFO test** -- Enables the FIFO via the FCR (FIFO Control Register),
then reads bits 7:6 of the IIR (Interrupt Identification Register):
- `0b11` = 16550A (working 16-byte FIFO)
- `0b10` = 16550 (broken FIFO -- present in hardware but unusable)
- `0b00` = 16450 (no FIFO at all)
The original FCR value is restored after probing.
| Constant | Value | Description |
|---------------------|-------|----------------------------------------|
| `RS232_UART_UNKNOWN`| 0 | Unknown or undetected |
| `RS232_UART_8250` | 1 | Original IBM PC -- no FIFO, no scratch |
| `RS232_UART_16450` | 2 | Scratch register present, no FIFO |
| `RS232_UART_16550` | 3 | Broken FIFO (rare, unusable) |
| `RS232_UART_16550A` | 4 | Working 16-byte FIFO (most common) |
On 16550A UARTs, the FIFO trigger threshold is configurable via
`rs232SetFifoThreshold()` with levels of 1, 4, 8, or 14 bytes. The
default is 14, which minimizes interrupt overhead at high baud rates.
## IRQ Auto-Detection
When `rs232Open()` is called without a prior `rs232SetIrq()` override,
the library auto-detects the UART's IRQ by:
1. Saving the current PIC interrupt mask registers (IMR)
2. Enabling all IRQ lines on both PICs
3. Generating a TX Hold Empty interrupt on the UART
4. Reading the PIC's Interrupt Request Register (IRR) to see which line
went high
5. Disabling the interrupt, reading IRR again to mask out persistent bits
6. Re-enabling once more to confirm the detection
7. Restoring the original PIC mask
If auto-detection fails (common on virtualized hardware that does not
model the IRR accurately), the library falls back to the default IRQ for
the port (IRQ4 for COM1/COM3, IRQ3 for COM2/COM4).
The BIOS Data Area (at segment 0x0040) is read to determine each port's
I/O base address. Ports not configured in the BDA are unavailable.
## COM Port Support
| Constant | Value | Default IRQ | Default Base |
|--------------|-------|-------------|--------------|
| `RS232_COM1` | 0 | IRQ 4 | 0x3F8 |
| `RS232_COM2` | 1 | IRQ 3 | 0x2F8 |
| `RS232_COM3` | 2 | IRQ 4 | 0x3E8 |
| `RS232_COM4` | 3 | IRQ 3 | 0x2E8 |
Base addresses are read from the BIOS Data Area at runtime. The default
IRQ values are used only as a fallback when auto-detection fails. Both
the base address and IRQ can be overridden before opening with
`rs232SetBase()` and `rs232SetIrq()`.
## Supported Baud Rates
All standard rates from 50 to 115200 bps are supported. The baud rate
divisor is computed from the standard 1.8432 MHz UART crystal:
| Rate | Divisor | Rate | Divisor |
|--------|---------|--------|---------|
| 50 | 2304 | 4800 | 24 |
| 75 | 1536 | 7200 | 16 |
| 110 | 1047 | 9600 | 12 |
| 150 | 768 | 19200 | 6 |
| 300 | 384 | 38400 | 3 |
| 600 | 192 | 57600 | 2 |
| 1200 | 96 | 115200 | 1 |
| 1800 | 64 | | |
| 2400 | 48 | | |
| 3800 | 32 | | |
Data bits (5-8), parity (N/O/E/M/S), and stop bits (1-2) are configured
by writing the appropriate LCR (Line Control Register) bits.
## API Reference
### Error Codes
| Constant | Value | Description |
|-------------------------------|-------|--------------------------|
| `RS232_SUCCESS` | 0 | Success |
| `RS232_ERR_UNKNOWN` | -1 | Unknown error |
| `RS232_ERR_NOT_OPEN` | -2 | Port not open |
| `RS232_ERR_ALREADY_OPEN` | -3 | Port already open |
| `RS232_ERR_NO_UART` | -4 | No UART detected at base |
| `RS232_ERR_INVALID_PORT` | -5 | Bad port index |
| `RS232_ERR_INVALID_BASE` | -6 | Bad I/O base address |
| `RS232_ERR_INVALID_IRQ` | -7 | Bad IRQ number |
| `RS232_ERR_INVALID_BPS` | -8 | Unsupported baud rate |
| `RS232_ERR_INVALID_DATA` | -9 | Bad data bits (not 5-8) |
| `RS232_ERR_INVALID_PARITY` | -10 | Bad parity character |
| `RS232_ERR_INVALID_STOP` | -11 | Bad stop bits (not 1-2) |
| `RS232_ERR_INVALID_HANDSHAKE` | -12 | Bad handshaking mode |
| `RS232_ERR_INVALID_FIFO` | -13 | Bad FIFO threshold |
| `RS232_ERR_NULL_PTR` | -14 | NULL pointer argument |
| `RS232_ERR_IRQ_NOT_FOUND` | -15 | Could not detect IRQ |
| `RS232_ERR_LOCK_MEM` | -16 | DPMI memory lock failed |
### Handshaking Modes
| Constant | Value | Description |
|---------------------------|-------|----------------------|
| `RS232_HANDSHAKE_NONE` | 0 | No flow control |
| `RS232_HANDSHAKE_XONXOFF` | 1 | Software (XON/XOFF) |
| `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) |
| `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) |
### Open / Close
```c
int rs232Open(int com, int32_t bps, int dataBits, char parity,
int stopBits, int handshake);
```
Opens a COM port. Reads the UART base address from the BIOS data area,
auto-detects the IRQ, locks ISR memory via DPMI, installs the ISR, and
configures the UART for the specified parameters. Returns `RS232_SUCCESS`
or an error code.
- `com` -- port index (`RS232_COM1` through `RS232_COM4`)
- `bps` -- baud rate (50 through 115200)
- `dataBits` -- 5, 6, 7, or 8
- `parity` -- `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark),
`'S'` (space)
- `stopBits` -- 1 or 2
- `handshake` -- `RS232_HANDSHAKE_*` constant
```c
int rs232Close(int com);
```
Closes the port, disables UART interrupts, removes the ISR, restores the
original interrupt vector, and unlocks DPMI memory (when the last port
closes).
### Read / Write
```c
int rs232Read(int com, char *data, int len);
```
Non-blocking read. Drains up to `len` bytes from the RX ring buffer.
Returns the number of bytes actually read (0 if the buffer is empty).
If flow control is active and the buffer drops below the low-water mark,
the ISR will re-enable receive from the remote side.
```c
int rs232Write(int com, const char *data, int len);
```
Blocking polled write. Sends `len` bytes directly to the UART THR
(Transmit Holding Register), bypassing the TX ring buffer entirely.
Polls LSR for THR empty before each byte. Returns `RS232_SUCCESS` or
an error code.
```c
int rs232WriteBuf(int com, const char *data, int len);
```
Non-blocking buffered write. Copies as many bytes as will fit into the
TX ring buffer. The ISR drains the TX buffer to the UART automatically.
Returns the number of bytes actually queued. If the buffer is full, some
bytes may be dropped.
### Buffer Management
```c
int rs232ClearRxBuffer(int com);
int rs232ClearTxBuffer(int com);
```
Discard all data in the receive or transmit ring buffer by resetting
head and tail pointers to zero.
### Getters
```c
int rs232GetBase(int com); // UART I/O base address
int32_t rs232GetBps(int com); // Current baud rate
int rs232GetCts(int com); // CTS line state (0 or 1)
int rs232GetData(int com); // Data bits setting
int rs232GetDsr(int com); // DSR line state (0 or 1)
int rs232GetDtr(int com); // DTR line state (0 or 1)
int rs232GetHandshake(int com); // Handshaking mode
int rs232GetIrq(int com); // IRQ number
int rs232GetLsr(int com); // Line Status Register
int rs232GetMcr(int com); // Modem Control Register
int rs232GetMsr(int com); // Modem Status Register
char rs232GetParity(int com); // Parity setting ('N','O','E','M','S')
int rs232GetRts(int com); // RTS line state (0 or 1)
int rs232GetRxBuffered(int com); // Bytes waiting in RX buffer
int rs232GetStop(int com); // Stop bits setting
int rs232GetTxBuffered(int com); // Bytes waiting in TX buffer
int rs232GetUartType(int com); // UART type (RS232_UART_* constant)
```
Most getters return cached register values from the per-port state
structure, avoiding unnecessary I/O port reads. `rs232GetUartType()`
actively probes the hardware (see UART Type Detection above).
### Setters
```c
int rs232Set(int com, int32_t bps, int dataBits, char parity,
int stopBits, int handshake);
```
Reconfigure all port parameters at once. The port must already be open.
```c
int rs232SetBase(int com, int base); // Override I/O base (before open)
int rs232SetBps(int com, int32_t bps); // Change baud rate
int rs232SetData(int com, int dataBits); // Change data bits
int rs232SetDtr(int com, bool dtr); // Assert/deassert DTR
int rs232SetFifoThreshold(int com, int thr); // FIFO trigger (1, 4, 8, 14)
int rs232SetHandshake(int com, int handshake); // Change flow control mode
int rs232SetIrq(int com, int irq); // Override IRQ (before open)
int rs232SetMcr(int com, int mcr); // Write Modem Control Register
int rs232SetParity(int com, char parity); // Change parity
int rs232SetRts(int com, bool rts); // Assert/deassert RTS
int rs232SetStop(int com, int stopBits); // Change stop bits
```
## Usage Example
```c
#include "rs232.h"
int main(void) {
// Open COM1 at 115200 8N1, no flow control
int rc = rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE);
if (rc != RS232_SUCCESS) {
return 1;
}
// Identify the UART chip
int uartType = rs232GetUartType(RS232_COM1);
// uartType == RS232_UART_16550A on most 486+ systems
// Enable 16550A FIFO with trigger at 14 bytes
if (uartType == RS232_UART_16550A) {
rs232SetFifoThreshold(RS232_COM1, 14);
}
// Blocking send
rs232Write(RS232_COM1, "Hello\r\n", 7);
// Non-blocking receive loop
char buf[128];
int n;
while ((n = rs232Read(RS232_COM1, buf, sizeof(buf))) > 0) {
// process buf[0..n-1]
}
rs232Close(RS232_COM1);
return 0;
}
```
## Implementation Notes
- The single shared ISR handles all four COM ports. On entry it disables
UART interrupts for all open ports on the PIC, then re-enables CPU
interrupts (STI) so higher-priority devices (timer, keyboard) are
serviced promptly.
- Ring buffers use power-of-2 sizes (2048 bytes) with bitmask indexing
for zero-branch wraparound. Each port uses 4KB total (2KB RX + 2KB TX).
- Flow control watermarks are at 80% (assert stop) and 20% (deassert
stop) of buffer capacity. These percentages are defined as compile-time
constants and apply to both RX and TX directions.
- DPMI `__dpmi_lock_linear_region` is used to pin the ISR code, ring
buffers, and port state in physical memory. The ISR code region is
locked for 2048 bytes starting at the `comGeneralIsr` function address.
- `rs232Write()` is a blocking polled write that bypasses the TX ring
buffer entirely. It writes directly to the UART THR register, polling
LSR for readiness between each byte. `rs232WriteBuf()` is the
non-blocking alternative that queues into the TX ring buffer for ISR
draining.
- Per-port state is stored in a static array of `Rs232StateT` structures
(`sComPorts[4]`). This array is locked in physical memory alongside the
ISR code.
- The BIOS Data Area (real-mode address 0040:0000) is read via DJGPP's
far pointer API (`_farpeekw`) to obtain port base addresses at runtime.
## Building
```
make # builds ../lib/librs232.a
make clean # removes objects and library
```
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/rs232/`, the library in `../lib/`.
## Files
- `rs232.h` -- Public API header
- `rs232.c` -- Complete implementation (ISR, DPMI, ring buffers, UART I/O)
- `Makefile` -- DJGPP cross-compilation build rules
## Used By
- `packet/` -- Packetized serial transport layer (HDLC framing, CRC, ARQ)
- `seclink/` -- Secure serial link (opens and closes the COM port)
- `proxy/` -- Linux serial proxy (uses a socket-based shim of this API)

27
run.sh
View file

@ -1,2 +1,27 @@
#!/bin/bash
flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x.conf
# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
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

View file

@ -1,38 +0,0 @@
# SecLink Library Makefile for DJGPP cross-compilation
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar
RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
OBJDIR = ../obj/seclink
LIBDIR = ../lib
SRCS = secLink.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGET = $(LIBDIR)/libseclink.a
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) | $(LIBDIR)
$(AR) rcs $@ $(OBJS)
$(RANLIB) $@
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(LIBDIR):
mkdir -p $(LIBDIR)
# Dependencies
$(OBJDIR)/secLink.o: secLink.c secLink.h ../rs232/rs232.h ../packet/packet.h ../security/security.h
clean:
rm -rf $(OBJDIR) $(TARGET)

View file

@ -1,450 +0,0 @@
# SecLink -- Secure Serial Link Library
SecLink is the top-level API for the DVX serial/networking stack. It
composes three lower-level libraries into a single interface for reliable,
optionally encrypted, channel-multiplexed serial communication:
- **rs232** -- ISR-driven UART I/O with ring buffers and flow control
- **packet** -- HDLC framing, CRC-16, Go-Back-N sliding window ARQ
- **security** -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR encryption
SecLink adds channel multiplexing and per-packet encryption control on
top of the packet layer's reliable delivery.
## Architecture
```
Application
|
| secLinkSend() send data on a channel, optionally encrypted
| secLinkPoll() receive, decrypt, deliver to callback
| secLinkHandshake() DH key exchange (blocking)
|
[SecLink] channel header, encrypt/decrypt, key management
|
[Packet] HDLC framing, CRC-16, Go-Back-N ARQ
|
[RS232] ISR-driven UART, 2048-byte ring buffers
|
UART Hardware
```
### Channel Multiplexing
SecLink prepends a one-byte header to every packet's payload before
handing it to the packet layer:
```
Bit 7 Bits 6..0
----- ---------
Encrypt Channel (0-127)
```
This allows up to 128 independent logical channels over a single serial
link. Each channel can carry a different type of traffic (terminal data,
file transfer, control messages, etc.) without needing separate framing
or sequencing per stream. The receive callback includes the channel
number so the application can dispatch accordingly.
The encrypt flag (bit 7) tells the receiver whether the payload portion
of this packet is encrypted. The channel header byte itself is always
sent in the clear.
### Mixed Clear and Encrypted Traffic
Unencrypted packets can be sent before or after the DH handshake. This
enables a startup protocol (version negotiation, capability exchange)
before keys are established. Encrypted packets require a completed
handshake -- attempting to send an encrypted packet before the handshake
returns `SECLINK_ERR_NOT_READY`.
On the receive side, encrypted packets arriving before the handshake is
complete are silently dropped. Cleartext packets are delivered regardless
of handshake state.
## Lifecycle
```
secLinkOpen() Open COM port and packet layer
secLinkHandshake() DH key exchange (blocks until both sides complete)
secLinkSend() Send data on a channel (encrypted or cleartext)
secLinkPoll() Receive and deliver packets to callback
secLinkClose() Tear down everything (ciphers, packet, COM port)
```
### Handshake Protocol
The DH key exchange uses the packet layer's reliable delivery, so lost
packets are automatically retransmitted. Both sides can send their public
key simultaneously -- there is no initiator/responder distinction.
1. Both sides generate a DH keypair (256-bit private, 1024-bit public)
2. Both sides send their 128-byte public key as a single packet
3. On receiving the remote's public key, each side immediately computes
the shared secret (`remote^private mod p`)
4. Each side derives separate TX and RX cipher keys from the master key
5. Cipher contexts are created and the link transitions to READY state
6. The DH context (containing the private key) is destroyed immediately
**Directional key derivation:**
The side with the lexicographically lower public key uses
`masterKey XOR 0xAA` for TX and `masterKey XOR 0x55` for RX. The other
side uses the reverse assignment. This is critical for CTR mode security:
if both sides used the same key and counter, they would produce identical
keystreams, and XORing two ciphertexts would reveal the XOR of the
plaintexts. The XOR-derived directional keys ensure each direction has a
unique keystream even though both sides start their counters at zero.
**Forward secrecy:**
The DH context (containing the private key and shared secret) is
destroyed immediately after deriving the session cipher keys. Even if
the application's long-term state is compromised later, past session
keys cannot be recovered from memory.
## Payload Size
The maximum payload per `secLinkSend()` call is `SECLINK_MAX_PAYLOAD`
(254 bytes). This is the packet layer's 255-byte maximum minus the
1-byte channel header that SecLink prepends.
For sending data larger than 254 bytes, use `secLinkSendBuf()` which
automatically splits the data into 254-byte chunks and sends each one
with blocking delivery.
## API Reference
### Types
```c
// Receive callback -- delivers plaintext with channel number
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len,
uint8_t channel);
// Opaque connection handle
typedef struct SecLinkS SecLinkT;
```
The receive callback is invoked from `secLinkPoll()` for each incoming
packet. Encrypted packets are decrypted before delivery -- the callback
always receives plaintext regardless of whether encryption was used on
the wire. The `data` pointer is valid only during the callback.
### Constants
| Name | Value | Description |
|-------------------------|-------|---------------------------------------|
| `SECLINK_MAX_PAYLOAD` | 254 | Max bytes per `secLinkSend()` call |
| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number |
| `SECLINK_SUCCESS` | 0 | Operation succeeded |
| `SECLINK_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
| `SECLINK_ERR_SERIAL` | -2 | Serial port open failed |
| `SECLINK_ERR_ALLOC` | -3 | Memory allocation failed |
| `SECLINK_ERR_HANDSHAKE` | -4 | DH key exchange failed |
| `SECLINK_ERR_NOT_READY` | -5 | Encryption requested before handshake |
| `SECLINK_ERR_SEND` | -6 | Packet layer send failed or window full |
### Functions
#### secLinkOpen
```c
SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity,
int stopBits, int handshake,
SecLinkRecvT callback, void *ctx);
```
Opens the COM port via rs232, creates the packet layer with default
window size (4), and returns a link handle. The callback is invoked from
`secLinkPoll()` for each received packet (decrypted if applicable).
Returns `NULL` on failure (serial port error, packet layer allocation
error, or memory allocation failure). On failure, all partially
initialized resources are cleaned up.
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
- `bps` -- baud rate (50 through 115200)
- `dataBits` -- 5, 6, 7, or 8
- `parity` -- `'N'`, `'O'`, `'E'`, `'M'`, or `'S'`
- `stopBits` -- 1 or 2
- `handshake` -- `RS232_HANDSHAKE_*` constant
- `callback` -- receive callback function
- `ctx` -- user pointer passed through to the callback
#### secLinkClose
```c
void secLinkClose(SecLinkT *link);
```
Full teardown in order: destroys TX and RX cipher contexts (secure zero),
destroys the DH context if still present, closes the packet layer, closes
the COM port, zeroes the link structure, and frees memory.
#### secLinkHandshake
```c
int secLinkHandshake(SecLinkT *link);
```
Performs a Diffie-Hellman key exchange. Blocks until both sides have
exchanged public keys and derived cipher keys. The RNG must be seeded
(via `secRngSeed()` or `secRngAddEntropy()`) before calling this.
Internally:
1. Creates a DH context and generates keys
2. Sends the 128-byte public key via the packet layer (blocking)
3. Polls the packet layer in a loop until the remote's public key arrives
4. Computes the shared secret and derives directional cipher keys
5. Destroys the DH context (forward secrecy)
6. Transitions the link to READY state
Returns `SECLINK_SUCCESS` or `SECLINK_ERR_HANDSHAKE` on failure
(DH key generation failure, send failure, or serial disconnect during
the exchange).
#### secLinkSend
```c
int secLinkSend(SecLinkT *link, const uint8_t *data, int len,
uint8_t channel, bool encrypt, bool block);
```
Sends up to `SECLINK_MAX_PAYLOAD` (254) bytes on the given channel.
- `channel` -- logical channel number (0-127)
- `encrypt` -- if `true`, encrypts the payload before sending. Requires
a completed handshake; returns `SECLINK_ERR_NOT_READY` otherwise.
- `block` -- if `true`, waits for transmit window space. If `false`,
returns `SECLINK_ERR_SEND` when the packet layer's window is full.
**Cipher counter safety:** The function checks transmit window space
BEFORE encrypting the payload. If it encrypted first and then the send
failed, the cipher counter would advance without the data being sent,
permanently desynchronizing the TX cipher state from the remote's RX
cipher. This ordering is critical for correctness.
The channel header byte is prepended to the data, and only the payload
portion (not the header) is encrypted.
Returns `SECLINK_SUCCESS` or an error code.
#### secLinkSendBuf
```c
int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len,
uint8_t channel, bool encrypt);
```
Sends an arbitrarily large buffer by splitting it into
`SECLINK_MAX_PAYLOAD`-byte (254-byte) chunks. Always blocks until all
data is sent. The receiver sees multiple packets on the same channel and
must reassemble if needed.
Returns `SECLINK_SUCCESS` or the first error encountered.
#### secLinkPoll
```c
int secLinkPoll(SecLinkT *link);
```
Delegates to `pktPoll()` to read serial data, process frames, handle
ACKs and retransmits. Received packets are routed through an internal
callback that:
- During handshake: expects a 128-byte DH public key
- When ready: strips the channel header, decrypts the payload if the
encrypt flag is set, and forwards plaintext to the user callback
Returns the number of packets delivered, or a negative error code.
Must be called frequently (every iteration of your main loop).
#### secLinkGetPending
```c
int secLinkGetPending(SecLinkT *link);
```
Returns the number of unacknowledged packets in the transmit window.
Delegates directly to `pktGetPending()`. Useful for non-blocking send
loops to determine when there is room to send more data.
#### secLinkIsReady
```c
bool secLinkIsReady(SecLinkT *link);
```
Returns `true` if the DH handshake is complete and the link is ready for
encrypted communication. Cleartext sends do not require the link to be
ready.
## Usage Examples
### Basic Encrypted Link
```c
#include "secLink.h"
#include "../security/security.h"
void onRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) {
// handle received plaintext on 'channel'
}
int main(void) {
// Seed the RNG before handshake
uint8_t entropy[16];
secRngGatherEntropy(entropy, sizeof(entropy));
secRngSeed(entropy, sizeof(entropy));
// Open link on COM1 at 115200 8N1, no flow control
SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1,
RS232_HANDSHAKE_NONE, onRecv, NULL);
if (!link) {
return 1;
}
// DH key exchange (blocks until both sides complete)
if (secLinkHandshake(link) != SECLINK_SUCCESS) {
secLinkClose(link);
return 1;
}
// Send encrypted data on channel 0
const char *msg = "Hello, secure world!";
secLinkSend(link, (const uint8_t *)msg, strlen(msg), 0, true, true);
// Main loop
while (1) {
secLinkPoll(link);
}
secLinkClose(link);
return 0;
}
```
### Mixed Encrypted and Cleartext Channels
```c
#define CHAN_CONTROL 0 // cleartext control channel
#define CHAN_DATA 1 // encrypted data channel
// Cleartext status message (no handshake needed)
secLinkSend(link, statusMsg, statusLen, CHAN_CONTROL, false, true);
// Encrypted payload (requires completed handshake)
secLinkSend(link, payload, payloadLen, CHAN_DATA, true, true);
```
### Non-Blocking File Transfer
```c
int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize,
uint8_t channel, bool encrypt) {
int offset = 0;
int bytesLeft = fileSize;
while (bytesLeft > 0) {
secLinkPoll(link); // process ACKs, free window slots
if (secLinkGetPending(link) < 4) { // window has room
int chunk = bytesLeft;
if (chunk > SECLINK_MAX_PAYLOAD) {
chunk = SECLINK_MAX_PAYLOAD;
}
int rc = secLinkSend(link, fileData + offset, chunk,
channel, encrypt, false);
if (rc == SECLINK_SUCCESS) {
offset += chunk;
bytesLeft -= chunk;
}
// SECLINK_ERR_SEND means window full, retry next iteration
}
// Application can do other work here:
// update progress bar, check for cancel, etc.
}
// Drain remaining ACKs
while (secLinkGetPending(link) > 0) {
secLinkPoll(link);
}
return SECLINK_SUCCESS;
}
```
### Blocking Bulk Transfer
```c
// Send an entire file in one call (blocks until complete)
secLinkSendBuf(link, fileData, fileSize, CHAN_DATA, true);
```
## Internal State Machine
SecLink maintains a three-state internal state machine:
| State | Value | Description |
|--------------|-------|----------------------------------------------|
| `STATE_INIT` | 0 | Link open, no handshake attempted yet |
| `STATE_HANDSHAKE` | 1 | DH key exchange in progress |
| `STATE_READY` | 2 | Handshake complete, ciphers ready |
Transitions:
- `INIT -> HANDSHAKE`: when `secLinkHandshake()` is called
- `HANDSHAKE -> READY`: when the remote's public key is received and
cipher keys are derived
- Any state -> cleanup: when `secLinkClose()` is called
Cleartext packets can be sent and received in any state. Encrypted
packets require `STATE_READY`.
## Building
```
make # builds ../lib/libseclink.a
make clean # removes objects and library
```
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/seclink/`, the library in `../lib/`.
Link against all four libraries in this order:
```
-lseclink -lpacket -lsecurity -lrs232
```
## Files
- `secLink.h` -- Public API header (types, constants, function prototypes)
- `secLink.c` -- Complete implementation (handshake, send, receive, state
machine)
- `Makefile` -- DJGPP cross-compilation build rules
## Dependencies
SecLink requires these libraries (all built into `../lib/`):
| Library | Purpose |
|------------------|---------------------------------------------|
| `librs232.a` | Serial port driver (ISR, ring buffers) |
| `libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
| `libsecurity.a` | DH key exchange, XTEA-CTR cipher, RNG |

View file

@ -1,38 +0,0 @@
# Security Library Makefile for DJGPP cross-compilation
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar
RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586
OBJDIR = ../obj/security
LIBDIR = ../lib
SRCS = security.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGET = $(LIBDIR)/libsecurity.a
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) | $(LIBDIR)
$(AR) rcs $@ $(OBJS)
$(RANLIB) $@
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(LIBDIR):
mkdir -p $(LIBDIR)
# Dependencies
$(OBJDIR)/security.o: security.c security.h
clean:
rm -rf $(OBJDIR) $(TARGET)

View file

@ -1,454 +0,0 @@
# Security -- Diffie-Hellman Key Exchange and XTEA-CTR Cipher
Cryptographic library providing Diffie-Hellman key exchange, XTEA
symmetric encryption in CTR mode, and a DRBG-based pseudo-random number
generator. Optimized for 486-class DOS hardware running under DJGPP/DPMI.
This library has no dependencies on the serial stack and can be used
independently for any application requiring key exchange, encryption,
or random number generation.
## Components
### 1. XTEA Cipher (CTR Mode)
XTEA (eXtended Tiny Encryption Algorithm) is a 64-bit block cipher with a
128-bit key and 32 Feistel rounds. In CTR (counter) mode, it operates as
a stream cipher: an incrementing counter is encrypted with the key to
produce a keystream, which is XOR'd with the plaintext. Because XOR is
its own inverse, the same operation encrypts and decrypts.
**Why XTEA instead of AES or DES:**
XTEA requires zero lookup tables, no key schedule, and compiles to
approximately 20 instructions per round (shifts, adds, and XORs only).
This makes it ideal for a 486 where the data cache is tiny (8KB) and
AES's 4KB S-boxes would thrash it. DES is similarly table-heavy and has
a complex key schedule. XTEA has no library dependencies -- the entire
cipher fits in about a dozen lines of C. At 32 rounds, XTEA provides
128-bit security with negligible per-byte overhead even on the slowest
target hardware.
**CTR mode properties:**
- Encrypt and decrypt are the same function (XOR is symmetric)
- No padding required -- operates on arbitrary-length data
- Random access possible (set the counter to any value)
- CRITICAL: the same counter value must never be reused with the same key.
Reuse reveals the XOR of two plaintexts. The secLink layer prevents this
by deriving separate TX/RX cipher keys for each direction.
**XTEA block cipher internals:**
The Feistel network uses the golden-ratio constant (delta = 0x9E3779B9)
as a round key mixer. Each round combines the two 32-bit halves using
shifts, additions, and XORs. The delta ensures each round uses a
different effective subkey, preventing slide attacks. No S-boxes or lookup
tables are involved anywhere in the computation.
### 2. Diffie-Hellman Key Exchange (1024-bit)
Uses the RFC 2409 Group 2 safe prime (1024-bit MODP group) with a
generator of 2. Private exponents are 256 bits for fast computation on
486-class hardware.
**Why 1024-bit DH with 256-bit private exponents:**
RFC 2409 Group 2 provides a well-audited, interoperable safe prime.
256-bit private exponents (versus full 1024-bit) reduce the modular
exponentiation from approximately 1024 squarings+multiplies to approximately
256 squarings + approximately 128 multiplies (half the exponent bits are 1 on
average). This makes key generation feasible on a 486 in under a second
rather than minutes. The security reduction is negligible -- Pollard's
rho on a 256-bit exponent requires approximately 2^128 operations, matching
XTEA's key strength.
**Key validation:**
`secDhComputeSecret()` validates that the remote public key is in the
range [2, p-2] to prevent small-subgroup attacks. Keys of 0, 1, or p-1
would produce trivially guessable shared secrets.
**Key derivation:**
The 128-byte shared secret is reduced to a symmetric key via XOR-folding:
each byte of the secret is XOR'd into the output key at position
`i % keyLen`. For a 16-byte XTEA key, each output byte is the XOR of
8 secret bytes, providing thorough mixing. A proper KDF (HKDF, etc.)
would be more rigorous but adds complexity and code size for marginal
benefit in this use case.
### 3. Pseudo-Random Number Generator
XTEA-CTR based DRBG (Deterministic Random Bit Generator). The RNG
encrypts a monotonically increasing 64-bit counter with a 128-bit XTEA
key, producing 8 bytes of pseudorandom output per block. The counter
never repeats (64-bit space is sufficient for any practical session
length), so the output is a pseudorandom stream as long as the key has
sufficient entropy.
**Hardware entropy sources:**
- PIT (Programmable Interval Timer) -- runs at 1.193182 MHz. Its LSBs
change rapidly and provide approximately 10 bits of entropy per read,
depending on timing jitter. Two readings with intervening code execution
provide additional jitter.
- BIOS tick count -- 18.2 Hz timer at real-mode address 0040:046C. Adds
a few more bits of entropy.
Total from hardware: roughly 20 bits of real entropy per call to
`secRngGatherEntropy()`. This is not enough on its own for
cryptographic use but is sufficient to seed the DRBG when supplemented
by user interaction timing (keyboard, mouse jitter).
**Seeding and mixing:**
The seed function (`secRngSeed()`) XOR-folds the entropy into the XTEA
key, derives the initial counter from the key bits, and then generates and
discards 64 bytes to advance past any weak initial output. This discard
step is standard DRBG practice -- it ensures the first bytes the caller
receives do not leak information about the seed material.
Additional entropy can be stirred in at any time via `secRngAddEntropy()`
without resetting the RNG state. This function XOR-folds new entropy into
the key and then re-mixes by encrypting the key with itself, diffusing
the new entropy across all key bits.
Auto-seeding: if `secRngBytes()` is called before `secRngSeed()`, it
automatically gathers hardware entropy and seeds itself as a safety net.
## BigNum Arithmetic
All modular arithmetic uses a 1024-bit big number type (`BigNumT`)
stored as 32 x `uint32_t` words in little-endian order. Operations:
| Function | Description |
|----------------|------------------------------------------------------|
| `bnAdd` | Add two bignums, return carry |
| `bnSub` | Subtract two bignums, return borrow |
| `bnCmp` | Compare two bignums (-1, 0, +1) |
| `bnBit` | Test a single bit by index |
| `bnBitLength` | Find the highest set bit position |
| `bnShiftLeft1` | Left-shift by 1, return carry |
| `bnClear` | Zero all words |
| `bnSet` | Set to a 32-bit value (clear upper words) |
| `bnCopy` | Copy from source to destination |
| `bnFromBytes` | Convert big-endian byte array to little-endian words |
| `bnToBytes` | Convert little-endian words to big-endian byte array |
| `bnMontMul` | Montgomery multiplication (CIOS variant) |
| `bnModExp` | Modular exponentiation via Montgomery multiply |
## Montgomery Multiplication
The CIOS (Coarsely Integrated Operand Scanning) variant computes
`a * b * R^(-1) mod m` in a single pass without explicit division by the
modulus. This replaces the expensive modular reduction step (division by a
1024-bit number) with cheaper additions and right-shifts.
For each of the 32 outer iterations (one per word of operand `a`):
1. Accumulate `a[i] * b` into the temporary product `t`
2. Compute the Montgomery reduction factor `u = t[0] * m0inv mod 2^32`
3. Add `u * mod` to `t` and shift right by 32 bits (implicit division)
After all iterations, the result is in the range [0, 2m), so a single
conditional subtraction brings it into [0, m).
**Montgomery constants** (computed once, lazily on first DH use):
- `R^2 mod p` -- computed via 2048 iterations of shift-left-1 with
conditional subtraction. This is the Montgomery domain conversion
factor.
- `-p[0]^(-1) mod 2^32` -- computed via Newton's method (5 iterations,
doubling precision each step: 1->2->4->8->16->32 correct bits). This
is the Montgomery reduction constant.
**Modular exponentiation** uses left-to-right binary square-and-multiply
scanning. For a 256-bit private exponent, this requires approximately 256
squarings plus approximately 128 multiplies (half the bits are 1 on average),
where each operation is a Montgomery multiplication on 32-word numbers.
## Secure Zeroing
Key material (private keys, shared secrets, cipher contexts) is erased
using a volatile-pointer loop:
```c
static void secureZero(void *ptr, int len) {
volatile uint8_t *p = (volatile uint8_t *)ptr;
for (int i = 0; i < len; i++) {
p[i] = 0;
}
}
```
The `volatile` qualifier prevents the compiler from optimizing away the
zeroing as a dead store. Without it, the compiler would see that the
buffer is about to be freed and remove the memset entirely. This is
critical for preventing sensitive key material from lingering in freed
memory where a later `malloc` could expose it.
## Performance
At serial port speeds, XTEA-CTR encryption overhead is minimal:
| Speed | Blocks/sec | CPU Cycles/sec | % of 33 MHz 486 |
|----------|------------|----------------|------------------|
| 9600 | 120 | ~240K | < 1% |
| 57600 | 720 | ~1.4M | ~4% |
| 115200 | 1440 | ~2.9M | ~9% |
DH key exchange takes approximately 0.3 seconds at 66 MHz or 0.6 seconds
at 33 MHz (256-bit private exponent, 1024-bit modulus, Montgomery
multiplication).
## API Reference
### Constants
| Name | Value | Description |
|---------------------|-------|-----------------------------------|
| `SEC_DH_KEY_SIZE` | 128 | DH public key size in bytes |
| `SEC_XTEA_KEY_SIZE` | 16 | XTEA key size in bytes |
| `SEC_SUCCESS` | 0 | Success |
| `SEC_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
| `SEC_ERR_NOT_READY` | -2 | Keys not yet generated/derived |
| `SEC_ERR_ALLOC` | -3 | Memory allocation failed |
### Types
```c
typedef struct SecDhS SecDhT; // Opaque DH context
typedef struct SecCipherS SecCipherT; // Opaque cipher context
```
### RNG Functions
```c
int secRngGatherEntropy(uint8_t *buf, int len);
```
Reads hardware entropy from the PIT counter and BIOS tick count. Returns
the number of bytes written (up to 8). Provides roughly 20 bits of true
entropy -- not sufficient alone, but enough to seed the DRBG when
supplemented by user interaction timing.
```c
void secRngSeed(const uint8_t *entropy, int len);
```
Initializes the DRBG with the given entropy. XOR-folds the input into
the XTEA key, derives the initial counter, and generates and discards 64
bytes to advance past weak initial output.
```c
void secRngAddEntropy(const uint8_t *data, int len);
```
Mixes additional entropy into the running RNG state without resetting it.
XOR-folds data into the key and re-mixes by encrypting the key with
itself. Use this to stir in keyboard timing, mouse jitter, or other
runtime entropy sources.
```c
void secRngBytes(uint8_t *buf, int len);
```
Generates `len` pseudorandom bytes. Auto-seeds from hardware entropy if
not previously seeded. Produces 8 bytes per XTEA block encryption of the
internal counter.
### Diffie-Hellman Functions
```c
SecDhT *secDhCreate(void);
```
Allocates a new DH context. Returns `NULL` on allocation failure. The
context must be destroyed with `secDhDestroy()` when no longer needed.
```c
int secDhGenerateKeys(SecDhT *dh);
```
Generates a 256-bit random private key and computes the corresponding
1024-bit public key (`g^private mod p`). Lazily initializes Montgomery
constants on first call. The RNG should be seeded before calling this.
```c
int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
```
Exports the public key as a big-endian byte array into `buf`. On entry,
`*len` must be at least `SEC_DH_KEY_SIZE` (128). On return, `*len` is
set to 128.
```c
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
```
Computes the shared secret from the remote side's public key
(`remote^private mod p`). Validates the remote key is in range [2, p-2].
Both sides compute this independently and arrive at the same value.
```c
int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen);
```
Derives a symmetric key by XOR-folding the 128-byte shared secret down
to `keyLen` bytes. Each output byte is the XOR of `128/keyLen` input
bytes.
```c
void secDhDestroy(SecDhT *dh);
```
Securely zeroes the entire DH context (private key, shared secret, public
key) and frees the memory. Must be called to prevent key material from
lingering in the heap.
### Cipher Functions
```c
SecCipherT *secCipherCreate(const uint8_t *key);
```
Creates an XTEA-CTR cipher context with the given 16-byte key. The
internal counter starts at zero. Returns `NULL` on allocation failure or
NULL key.
```c
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
```
Encrypts or decrypts `data` in place. CTR mode is symmetric -- the same
function handles both directions. The internal counter advances by one
for every 8 bytes processed (one XTEA block). The counter must never
repeat with the same key; callers are responsible for ensuring this
(secLink handles it by using separate cipher instances per direction).
```c
void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi);
```
Sets the 64-bit nonce/counter to a specific value. Both the nonce
(baseline) and the running counter are set to the same value. Call this
before encrypting if you need a deterministic starting point.
```c
void secCipherDestroy(SecCipherT *c);
```
Securely zeroes the cipher context (key and counter state) and frees the
memory.
## Usage Examples
### Full Key Exchange
```c
#include "security.h"
#include <string.h>
// Seed the RNG
uint8_t entropy[16];
secRngGatherEntropy(entropy, sizeof(entropy));
secRngSeed(entropy, sizeof(entropy));
// Create DH context and generate keys
SecDhT *dh = secDhCreate();
secDhGenerateKeys(dh);
// Export public key to send to remote
uint8_t myPub[SEC_DH_KEY_SIZE];
int pubLen = SEC_DH_KEY_SIZE;
secDhGetPublicKey(dh, myPub, &pubLen);
// ... send myPub to remote, receive remotePub ...
// Compute shared secret and derive a 16-byte XTEA key
secDhComputeSecret(dh, remotePub, SEC_DH_KEY_SIZE);
uint8_t key[SEC_XTEA_KEY_SIZE];
secDhDeriveKey(dh, key, SEC_XTEA_KEY_SIZE);
secDhDestroy(dh); // private key no longer needed
// Create cipher and encrypt
SecCipherT *cipher = secCipherCreate(key);
uint8_t message[] = "Secret message";
secCipherCrypt(cipher, message, sizeof(message));
// message is now encrypted
// Decrypt (reset counter first, then apply same operation)
secCipherSetNonce(cipher, 0, 0);
secCipherCrypt(cipher, message, sizeof(message));
// message is now plaintext again
secCipherDestroy(cipher);
```
### Standalone Encryption (Without DH)
```c
// XTEA-CTR can be used independently of Diffie-Hellman
uint8_t key[SEC_XTEA_KEY_SIZE] = { /* your key */ };
SecCipherT *c = secCipherCreate(key);
uint8_t data[1024];
// ... fill data ...
secCipherCrypt(c, data, sizeof(data)); // encrypt in place
secCipherDestroy(c);
```
### Random Number Generation
```c
// Seed from hardware
uint8_t hwEntropy[16];
secRngGatherEntropy(hwEntropy, sizeof(hwEntropy));
secRngSeed(hwEntropy, sizeof(hwEntropy));
// Stir in user-derived entropy (keyboard timing, etc.)
uint8_t userEntropy[4];
// ... gather from timing events ...
secRngAddEntropy(userEntropy, sizeof(userEntropy));
// Generate random bytes
uint8_t randomBuf[32];
secRngBytes(randomBuf, sizeof(randomBuf));
```
## Building
```
make # builds ../lib/libsecurity.a
make clean # removes objects and library
```
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/security/`, the library in `../lib/`.
No external dependencies -- the library is self-contained. It uses only
DJGPP's `<pc.h>`, `<sys/farptr.h>`, and `<go32.h>` for hardware entropy
collection (PIT and BIOS tick count access).
## Files
- `security.h` -- Public API header (types, constants, function prototypes)
- `security.c` -- Complete implementation (bignum, Montgomery, DH, XTEA, RNG)
- `Makefile` -- DJGPP cross-compilation build rules
## Used By
- `seclink/` -- Secure serial link (DH handshake, cipher creation, RNG seeding)

View file

@ -1,56 +0,0 @@
# DVX Serial Stack Makefile for DJGPP cross-compilation
#
# Builds serial.lib -- combines rs232, packet, security, and seclink
# into a single DXE library loaded by the DVX system.
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../rs232 -I../packet -I../security -I../seclink -I../core -I../core/platform -I../tasks
OBJDIR = ../obj/serial
LIBSDIR = ../bin/libs
SRCS = ../rs232/rs232.c ../packet/packet.c ../security/security.c ../seclink/secLink.c
OBJS = $(OBJDIR)/rs232.o $(OBJDIR)/packet.o $(OBJDIR)/security.o $(OBJDIR)/secLink.o
TARGET = $(LIBSDIR)/serial.lib
.PHONY: all clean
all: $(TARGET) $(LIBSDIR)/serial.dep
$(LIBSDIR)/serial.dep: ../config/serial.dep | $(LIBSDIR)
sed 's/$$/\r/' $< > $@
$(TARGET): $(OBJS) | $(LIBSDIR)
$(DXE3GEN) -o $(LIBSDIR)/serial.dxe \
-E _rs232 -E _pkt -E _secLink -E _secDh -E _secCipher -E _secRng \
-U $(OBJS)
mv $(LIBSDIR)/serial.dxe $@
$(OBJDIR)/rs232.o: ../rs232/rs232.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/packet.o: ../packet/packet.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/security.o: ../security/security.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/secLink.o: ../seclink/secLink.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(LIBSDIR):
mkdir -p $(LIBSDIR)
# Dependencies
$(OBJDIR)/rs232.o: ../rs232/rs232.c ../rs232/rs232.h
$(OBJDIR)/packet.o: ../packet/packet.c ../packet/packet.h ../rs232/rs232.h
$(OBJDIR)/security.o: ../security/security.c ../security/security.h
$(OBJDIR)/secLink.o: ../seclink/secLink.c ../seclink/secLink.h ../rs232/rs232.h ../packet/packet.h ../security/security.h
clean:
rm -f $(OBJS) $(TARGET) $(LIBSDIR)/serial.dep

View file

@ -1,69 +0,0 @@
# DVX Shell Makefile for DJGPP cross-compilation
#
# Builds dvxshell.lib -- the shell module loaded by the DVX loader.
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../widgets -I../tasks -I../core/thirdparty
OBJDIR = ../obj/shell
LIBSDIR = ../bin/libs
CONFIGDIR = ../bin/config
THEMEDIR = ../bin/config/themes
WPAPERDIR = ../bin/config/wpaper
SRCS = shellMain.c shellApp.c shellInfo.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGET = $(LIBSDIR)/dvxshell.lib
.PHONY: all clean
THEMES = $(THEMEDIR)/geos.thm $(THEMEDIR)/win31.thm $(THEMEDIR)/cde.thm
WPAPERS = $(WPAPERDIR)/blueglow.jpg $(WPAPERDIR)/swoop.jpg $(WPAPERDIR)/triangle.jpg
all: $(TARGET) $(LIBSDIR)/dvxshell.dep $(CONFIGDIR)/dvx.ini $(THEMES) $(WPAPERS)
$(LIBSDIR)/dvxshell.dep: ../config/dvxshell.dep | $(LIBSDIR)
sed 's/$$/\r/' $< > $@
$(TARGET): $(OBJS) | $(LIBSDIR)
$(DXE3GEN) -o $(LIBSDIR)/dvxshell.dxe -E _shell -U $(OBJS)
mv $(LIBSDIR)/dvxshell.dxe $@
$(CONFIGDIR)/dvx.ini: ../config/dvx.ini | $(CONFIGDIR)
sed 's/$$/\r/' $< > $@
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
$(LIBSDIR):
mkdir -p $(LIBSDIR)
$(CONFIGDIR):
mkdir -p $(CONFIGDIR)
$(THEMEDIR):
mkdir -p $(THEMEDIR)
$(THEMEDIR)/%.thm: ../config/themes/%.thm | $(THEMEDIR)
sed 's/$$/\r/' $< > $@
$(WPAPERDIR):
mkdir -p $(WPAPERDIR)
$(WPAPERDIR)/%.jpg: ../config/wpaper/%.jpg | $(WPAPERDIR)
cp $< $@
# Dependencies
SHELL_DEPS = shellApp.h ../core/dvxWidget.h ../core/dvxApp.h ../core/dvxTypes.h ../core/platform/dvxPlatform.h
$(OBJDIR)/shellMain.o: shellMain.c $(SHELL_DEPS)
$(OBJDIR)/shellApp.o: shellApp.c $(SHELL_DEPS)
$(OBJDIR)/shellInfo.o: shellInfo.c shellInfo.h $(SHELL_DEPS)
clean:
rm -f $(OBJS) $(TARGET) $(LIBSDIR)/dvxshell.dep
rm -rf $(WPAPERDIR) $(THEMEDIR) $(CONFIGDIR)

View file

@ -1,180 +0,0 @@
# DVX Shell (dvxshell.lib)
The DVX Shell is a DXE3 module loaded by the DVX loader at startup.
It initializes the GUI subsystem, loads DXE3 application modules on
demand, runs the cooperative main loop, and provides crash recovery so
a faulting app does not bring down the entire system.
## Entry Point
The loader finds and calls `shellMain()` after all libs and widgets
are loaded. `shellMain()`:
1. Loads preferences from `CONFIG/DVX.INI`
2. Initializes the GUI via `dvxInit()` with configured video mode
3. Applies saved mouse, color, and wallpaper settings from INI
4. Shows a splash screen ("DVX - DOS Visual eXecutive / Loading...")
5. Initializes the cooperative task system (`tsInit()`)
6. Sets shell task (task 0) to `TS_PRIORITY_HIGH`
7. Gathers system information via the platform layer
8. Initializes the app slot table
9. Points the memory tracker at `currentAppId` for per-app attribution
10. Registers idle callback, Ctrl+Esc handler, and title change handler
11. Installs the crash handler (before loading apps, so init crashes are caught)
12. Loads the desktop app (default: `apps/progman/progman.app`)
13. Dismisses the splash screen
14. Enters the main loop
## Main Loop
Each iteration of the main loop does four things:
1. `dvxUpdate()` -- process input events, dispatch callbacks, composite
dirty rects, flush to LFB
2. `tsYield()` -- give CPU time to app tasks (if any are active)
3. `shellReapApps()` -- clean up any apps that terminated this frame
4. `shellDesktopUpdate()` -- notify desktop app if apps were reaped
An idle callback (`idleYield`) is also registered so that `dvxUpdate()`
yields to app tasks during quiet frames.
## App Lifecycle
### DXE App Contract
Every DXE app exports two symbols:
* `appDescriptor` (`AppDescriptorT`) -- metadata:
- `name` -- display name (max 64 chars)
- `hasMainLoop` -- true for main-loop apps, false for callback-only
- `multiInstance` -- true to allow multiple instances via temp copy
- `stackSize` -- `SHELL_STACK_DEFAULT` (32KB) or explicit byte count
- `priority` -- `TS_PRIORITY_NORMAL` or custom
* `appMain` (`int appMain(DxeAppContextT *)`) -- entry point
Optional export: `appShutdown` (`void appShutdown(void)`) -- called
during graceful shutdown.
### Callback-Only Apps (hasMainLoop = false)
`appMain()` is called directly in the shell's task 0. It creates
windows, registers callbacks, and returns immediately. The app lives
entirely through GUI callbacks. The shell reaps callback-only apps
automatically when their last window closes -- `shellReapApps()` checks
each frame for running callback apps with zero remaining windows.
### Main-Loop Apps (hasMainLoop = true)
A dedicated cooperative task is created. `appMain()` runs in that task
and can do its own polling/processing loop, calling `tsYield()` to
share CPU. Lifecycle ends when `appMain()` returns or the task is
killed.
### App States
```
Free -> Loaded -> Running -> Terminating -> Free
```
| State | Description |
|-------|-------------|
| `AppStateFreeE` | Slot available for reuse |
| `AppStateLoadedE` | DXE loaded, not yet started (transient) |
| `AppStateRunningE` | Entry point called, active |
| `AppStateTerminatingE` | Shutdown in progress, awaiting reap |
### App Slots
App slots are managed as a stb_ds dynamic array (no fixed max). Each
slot tracks: app ID, name, path, DXE handle, state, task ID, entry/
shutdown function pointers, and a pointer to the `DxeAppContextT`
passed to the app.
`DxeAppContextT` is heap-allocated (via `calloc`) so its address is
stable across `sApps` array reallocs -- apps save this pointer in their
static globals and it must not move. The shell frees it during reap.
The `DxeAppContextT` gives each app:
- `shellCtx` -- pointer to the shell's `AppContextT`
- `appId` -- this app's unique ID
- `appDir` -- directory containing the `.app` file (for resources)
- `configDir` -- writable config directory (`CONFIG/<apppath>/`)
### App ID Tracking
`ctx->currentAppId` on AppContextT tracks which app is currently
executing. The shell sets this before calling app code.
`dvxCreateWindow()` stamps `win->appId` directly so the shell can
associate windows with apps for cleanup.
For main-loop apps, `appTaskWrapper` receives the app ID (as an int
cast to `void *`), not a direct pointer to `ShellAppT`. This is because
the `sApps` dynamic array may reallocate between `tsCreate` and the
first time the task runs, which would invalidate a direct pointer.
The shell calls `dvxSetBusy()` before `dlopen` to show the hourglass
cursor during app loading, and clears it after `appMain` returns (for
callback apps) or after task creation (for main-loop apps).
## Crash Recovery
The platform layer installs signal handlers for SIGSEGV, SIGFPE, and
SIGILL via `platformInstallCrashHandler()`. If a crash occurs:
1. Platform handler logs signal name and register dump (DJGPP)
2. Handler `longjmp`s to the `setjmp` point in `shellMain()`
3. `tsRecoverToMain()` fixes the scheduler's bookkeeping
4. Shell logs app-specific info (name, path, task ID)
5. Crashed app is force-killed (`shellForceKillApp()`)
6. Error dialog is shown to the user
7. Desktop is notified to refresh
8. Main loop continues normally
This gives Windows 3.1-style fault tolerance -- one bad app does not
take down the whole system.
## Task Manager Integration
The Task Manager is a separate DXE (`taskmgr.lib` in `taskmgr/`), not
built into the shell. It registers itself at load time via a DXE
constructor that sets the `shellCtrlEscFn` function pointer. The shell
calls this pointer on Ctrl+Esc. If `taskmgr.lib` is not loaded,
`shellCtrlEscFn` is NULL and Ctrl+Esc does nothing.
See `taskmgr/README.md` for full Task Manager documentation.
## Desktop Update Notifications
Apps (especially the desktop app) register callbacks via
`shellRegisterDesktopUpdate()` to be notified when app state changes
(load, reap, crash, title change). Multiple callbacks are supported.
## Files
| File | Description |
|------|-------------|
| `shellMain.c` | Entry point, main loop, crash recovery, splash screen, idle callback |
| `shellApp.h` | App lifecycle types: `AppDescriptorT`, `DxeAppContextT`, `ShellAppT`, `AppStateE`; `shellCtrlEscFn` extern |
| `shellApp.c` | App loading, reaping, task creation, DXE management, per-app memory tracking |
| `shellInfo.h` | System information wrapper |
| `shellInfo.c` | Gathers and caches hardware info via platform layer |
| `Makefile` | Builds `bin/libs/dvxshell.lib` + config/themes/wallpapers |
## Build
```
make # builds dvxshell.lib + dvxshell.dep + config files
make clean # removes objects, library, and config output
```
Depends on: `libtasks.lib`, `libdvx.lib`, `texthelp.lib`, `listhelp.lib`
(via dvxshell.dep).

View file

@ -1,20 +0,0 @@
// shellInfo.h -- System information display for DVX Shell
//
// Thin wrapper around platformGetSystemInfo(). Calls the platform
// layer to gather hardware info, logs each line to DVX.LOG, and
// caches the result for the Program Manager's "System Information"
// dialog.
#ifndef SHELL_INFO_H
#define SHELL_INFO_H
#include "dvxApp.h"
// Gather all hardware information via the platform layer, log it,
// and store for later retrieval. Call once after dvxInit.
void shellInfoInit(AppContextT *ctx);
// Return the formatted system information text. The pointer is valid
// for the lifetime of the process (static buffer in the platform layer).
const char *shellGetSystemInfo(void);
#endif // SHELL_INFO_H

169
src/apps/kpunch/Makefile Normal file
View file

@ -0,0 +1,169 @@
# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# DVX Shell Applications Makefile -- builds DXE3 modules
#
# Source tree is now one dir per app, mirroring bin/apps/kpunch/.
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
CFLAGS = -O2 -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -march=i486 -mtune=i586 -I../../libs/kpunch/libdvx -I../../libs/kpunch/libdvx/platform -I../../libs/kpunch/libdvx/thirdparty -I../../widgets/kpunch -I../../libs/kpunch/libtasks -I../../libs/kpunch/dvxshell
OBJDIR = ../../../obj/apps
BINDIR = ../../../bin/apps
DVXRES = ../../../bin/host/dvxres
# C apps: one directory, one .c file each
C_APPS = progman clock dvxdemo cpanel dvxhelp
BASCOMP = ../../../bin/host/bascomp
# BASIC apps: each is a .dbp project in its own directory.
# BASIC-only notepad, imgview, etc. replace the old C versions.
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo widshow
.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS)
all: $(C_APPS) dvxbasic $(BASIC_APPS)
dvxbasic:
$(MAKE) -C dvxbasic
cpanel: $(BINDIR)/kpunch/cpanel/cpanel.app
progman: $(BINDIR)/kpunch/progman/progman.app
clock: $(BINDIR)/kpunch/clock/clock.app
dvxdemo: $(BINDIR)/kpunch/dvxdemo/dvxdemo.app
dvxhelp: $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
$(BINDIR)/kpunch/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/kpunch/cpanel
$(DXE3GEN) -o $@ -U $<
cd cpanel && ../$(DVXRES) build ../$@ cpanel.res
$(BINDIR)/kpunch/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/kpunch/progman
$(DXE3GEN) -o $@ -U $<
$(BINDIR)/kpunch/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/kpunch/clock
$(DXE3GEN) -o $@ -U $<
cd clock && ../$(DVXRES) build ../$@ clock.res
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
$(DXE3GEN) -o $@ -U $<
cd dvxdemo && ../$(DVXRES) build ../$@ dvxdemo.res
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/
$(BINDIR)/kpunch/dvxhelp/dvxhelp.app: $(OBJDIR)/dvxhelp.o dvxhelp/dvxhelp.res dvxhelp/icon32.bmp | $(BINDIR)/kpunch/dvxhelp
$(DXE3GEN) -o $@ -U $<
cd dvxhelp && ../$(DVXRES) build ../$@ dvxhelp.res
$(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/progman.o: progman/progman.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/clock.o: clock/clock.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
# BASIC apps (compiled from .dbp projects via bascomp). Source for each app
# is under the app's own directory.
iconed: $(BINDIR)/kpunch/iconed/iconed.app
$(BINDIR)/kpunch/iconed/iconed.app: iconed/iconed.dbp iconed/iconed.frm iconed/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/iconed dvxbasic
$(BASCOMP) iconed/iconed.dbp -o $@ -release
notepad: $(BINDIR)/kpunch/notepad/notepad.app
$(BINDIR)/kpunch/notepad/notepad.app: notepad/notepad.dbp notepad/notepad.frm notepad/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/notepad dvxbasic
$(BASCOMP) notepad/notepad.dbp -o $@ -release
imgview: $(BINDIR)/kpunch/imgview/imgview.app
$(BINDIR)/kpunch/imgview/imgview.app: imgview/imgview.dbp imgview/imgview.frm imgview/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/imgview dvxbasic
$(BASCOMP) imgview/imgview.dbp -o $@ -release
helpedit: $(BINDIR)/kpunch/dvxhelp/helpedit.app
$(BINDIR)/kpunch/dvxhelp/helpedit.app: dvxhelp/helpedit/helpedit.dbp dvxhelp/helpedit/helpedit.frm dvxhelp/helpedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/dvxhelp dvxbasic
$(BASCOMP) dvxhelp/helpedit/helpedit.dbp -o $@ -release
$(DVXRES) add $@ helpfile text "dvxhelp.hlp"
resedit: $(BINDIR)/kpunch/resedit/resedit.app
$(BINDIR)/kpunch/resedit/resedit.app: resedit/resedit.dbp resedit/resedit.frm resedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/resedit dvxbasic
$(BASCOMP) resedit/resedit.dbp -o $@ -release
basdemo: $(BINDIR)/kpunch/basdemo/basdemo.app
$(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic
$(BASCOMP) basdemo/basdemo.dbp -o $@ -release
widshow: $(BINDIR)/kpunch/widshow/widshow.app
$(BINDIR)/kpunch/widshow/widshow.app: widshow/widshow.dbp widshow/widshow.frm widshow/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/widshow dvxbasic
$(BASCOMP) widshow/widshow.dbp -o $@ -release
$(OBJDIR):
mkdir -p $(OBJDIR)
$(BINDIR)/kpunch/cpanel: ; mkdir -p $@
$(BINDIR)/kpunch/progman: ; mkdir -p $@
$(BINDIR)/kpunch/clock: ; mkdir -p $@
$(BINDIR)/kpunch/dvxdemo: ; mkdir -p $@
$(BINDIR)/kpunch/dvxhelp: ; mkdir -p $@
$(BINDIR)/kpunch/iconed: ; mkdir -p $@
$(BINDIR)/kpunch/notepad: ; mkdir -p $@
$(BINDIR)/kpunch/imgview: ; mkdir -p $@
$(BINDIR)/kpunch/resedit: ; mkdir -p $@
$(BINDIR)/kpunch/basdemo: ; mkdir -p $@
$(BINDIR)/kpunch/widshow: ; mkdir -p $@
# Header dependencies
COMMON_H = ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxDlg.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxWm.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h
# Every widget public header. Apps that pull in widget APIs (e.g. dvxdemo)
# must rebuild when these change, otherwise the struct-layout contract
# between the widget DXE's sApi and the app's cast to RadioApiT/etc.
# silently drifts and produces late-binding crashes.
WIDGET_H = $(wildcard ../../widgets/kpunch/*/*.h)
$(OBJDIR)/cpanel.o: cpanel/cpanel.c $(COMMON_H) ../../libs/kpunch/libdvx/dvxPrefs.h ../../libs/kpunch/libdvx/platform/dvxPlat.h
$(OBJDIR)/progman.o: progman/progman.c $(COMMON_H) ../../libs/kpunch/dvxshell/shellInf.h
$(OBJDIR)/clock.o: clock/clock.c ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxDraw.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h ../../libs/kpunch/libtasks/taskSwch.h
$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c $(COMMON_H) $(WIDGET_H)
clean:
rm -f $(OBJDIR)/*.o
rm -f $(BINDIR)/kpunch/cpanel/cpanel.app
rm -f $(BINDIR)/kpunch/progman/progman.app
rm -f $(BINDIR)/kpunch/clock/clock.app
rm -f $(BINDIR)/kpunch/dvxdemo/dvxdemo.app $(addprefix $(BINDIR)/kpunch/dvxdemo/,$(DVXDEMO_BMPS))
rm -f $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
rm -f $(BINDIR)/kpunch/iconed/iconed.app
rm -f $(BINDIR)/kpunch/notepad/notepad.app
rm -f $(BINDIR)/kpunch/imgview/imgview.app
rm -f $(BINDIR)/kpunch/dvxhelp/helpedit.app
rm -f $(BINDIR)/kpunch/resedit/resedit.app
rm -f $(BINDIR)/kpunch/basdemo/basdemo.app
rm -f $(BINDIR)/kpunch/widshow/widshow.app
$(MAKE) -C dvxbasic clean

472
src/apps/kpunch/README.md Normal file
View file

@ -0,0 +1,472 @@
# DVX Shell Applications
Every DVX application is a DXE3 module packaged as a `.app` file,
installed under `bin/apps/kpunch/<name>/<name>.app`. The Program
Manager scans this directory at startup and displays each discovered
app as a launchable icon on the desktop.
This directory holds the source for the bundled applications plus
the BASIC-based sample apps. Two flavours of app live side by side:
* **C apps** -- written directly against the DVX SDK headers,
compiled to a DXE3 by `dxe3gen`. See `progman/`, `clock/`,
`dvxdemo/`, `cpanel/`, `dvxhelp/`.
* **BASIC apps** -- `.dbp` projects compiled to `.app` files by the
host-side `bascomp`. The compiler links the BASIC bytecode into a
copy of `basstub.app`, which acts as the runtime host. See
`iconed/`, `notepad/`, `imgview/`, `basdemo/`, `resedit/`,
`dvxhelp/helpedit/`.
The rest of this document covers writing a C app against the SDK.
For BASIC apps, see `dvxbasic/README.md`.
## DXE App Contract
Every `.app` binary exports two required symbols and one optional
symbol:
```c
AppDescriptorT appDescriptor; // required
int32_t appMain(DxeAppContextT *ctx); // required
void appShutdown(void); // optional
```
### `appDescriptor`
A statically-initialised struct read by the shell at load time to
decide how to launch the app:
```c
typedef struct {
char name[SHELL_APP_NAME_MAX]; // display name (<= 64 chars)
bool hasMainLoop; // see below
bool multiInstance; // may have multiple copies open
int32_t stackSize; // SHELL_STACK_DEFAULT or byte count
int32_t priority; // TS_PRIORITY_* for main-loop apps
} AppDescriptorT;
```
Example (from `clock/clock.c`):
```c
AppDescriptorT appDescriptor = {
.name = "Clock",
.hasMainLoop = true,
.multiInstance = true,
.stackSize = SHELL_STACK_DEFAULT,
.priority = TS_PRIORITY_LOW
};
```
### `appMain`
Entry point. The shell calls it with a `DxeAppContextT` that exposes
the shell's GUI context, the app's ID, its install directory, a
writable config directory, launch arguments, and a help-topic query
callback for F1.
Returning from `appMain` signals that the app has finished; the
shell unloads the DXE, frees per-app memory, and reclaims the app
slot.
### `appShutdown`
Called by the shell when the app is force-killed (by the Task
Manager) or when the shell is shutting down with this app still
running. Main-loop apps use it to set a flag that their main loop
observes, so the loop exits cleanly and `appMain` can return.
## App Types
### Callback-only apps (`hasMainLoop = false`)
`appMain` creates windows, registers callbacks, and returns 0. The
shell drives all further work by invoking those callbacks in
response to user input and timers. No dedicated task is allocated;
no per-app stack is consumed. The app's lifetime ends when the last
window closes (or when every callback has released all references).
Use this pattern for event-driven UI: Notepad, Program Manager, DVX
Demo, Control Panel, Image Viewer.
### Main-loop apps (`hasMainLoop = true`)
The shell creates a cooperative task for the app and calls
`appMain` from inside it. `appMain` runs a loop that calls
`tsYield()` periodically to share the CPU. Exit by breaking out of
the loop and returning.
Use this pattern when the app needs continuous background work that
does not map cleanly onto event callbacks: Clock (polls the system
clock every second), terminal emulators, games.
## Resource Conventions
Apps attach resources to their `.app` binary using `dvxres build`
(see `src/tools/README.md`). The standard resource names are:
| Name | Type | Meaning |
|---------------|--------|---------|
| `icon32` | icon | 32x32 BMP shown in Program Manager and the Task Manager. |
| `name` | text | Human-readable display name (overrides `appDescriptor.name` for UI where available). |
| `author` | text | Author / maintainer name. |
| `publisher` | text | Publisher or studio. |
| `copyright` | text | Copyright notice. |
| `version` | text | Version string, free-form. |
| `description` | text | One-sentence description shown in About dialogs. |
| `helpfile` | text | Optional relative path to a `.hlp` file for F1 context help. |
Additional application-specific resources may be attached with any
unique name. The runtime reads them via
`dvxResOpen(appPath)` / `dvxResRead(handle, name, &size)` /
`dvxResClose(handle)`.
Typical `.res` manifest:
```
# clock.res
icon32 icon icon32.bmp
name text "Clock"
author text "Scott Duensing"
copyright text "Copyright 2026 Scott Duensing"
publisher text "Kangaroo Punch Studios"
description text "Digital clock with date display"
```
## Bundled Applications
### Program Manager (progman)
| | |
|---|---|
| File | `apps/kpunch/progman/progman.app` |
| Type | Callback-only |
| Multi-instance | No |
The desktop. Recursively scans `apps/` for `.app` files and displays
them as a launchable grid with icons, names, and publishers. Double
click or Enter to launch. Includes a Help menu with the system help
viewer and the About / System Info dialog. Registers with
`shellRegisterDesktopUpdate()` so the grid refreshes when apps are
loaded, terminated, or crash.
Widget headers used: `box/box.h`, `button/button.h`, `label/label.h`,
`statusBar/statBar.h`, `textInput/textInpt.h`, `imageButton/imgBtn.h`,
`scrollPane/scrlPane.h`, `wrapBox/wrapBox.h`.
### Notepad (notepad, BASIC)
| | |
|---|---|
| File | `apps/kpunch/notepad/notepad.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
Plain-text editor with File menu (New, Open, Save, Save As) and Edit
menu (Cut, Copy, Paste, Select All). Built from a BASIC project
(`notepad.dbp` + `notepad.frm`); demonstrates the form designer,
TextInput widget, and common dialog integration.
### Clock (clock)
| | |
|---|---|
| File | `apps/kpunch/clock/clock.app` |
| Type | Main-loop |
| Multi-instance | Yes |
Digital clock showing time and date. Polls the system clock every
second and invalidates the window to trigger a repaint. Uses the raw
`onPaint` callback (no widgets) to draw centered text. Reference
implementation for main-loop apps.
### DVX Demo (dvxdemo)
| | |
|---|---|
| File | `apps/kpunch/dvxdemo/dvxdemo.app` |
| Type | Callback-only |
| Multi-instance | No |
Widget toolkit showcase. Opens multiple windows demonstrating
virtually every widget type:
* Main window -- raw paint callbacks (gradients, patterns, text)
* Widget demo -- form widgets (TextInput, Checkbox, Radio, ListBox)
* Controls window -- tabbed advanced widgets (Dropdown,
ProgressBar, Slider, Spinner, TreeView, ListView, ScrollPane,
Toolbar, Canvas, Splitter, Image)
* Terminal window -- AnsiTerm widget
Resources include `logo.bmp`, `new.bmp`, `open.bmp`, `sample.bmp`,
`save.bmp`. Reference implementation for widget-based UIs.
### Control Panel (cpanel)
| | |
|---|---|
| File | `apps/kpunch/cpanel/cpanel.app` |
| Type | Callback-only |
| Multi-instance | No |
System configuration with four tabs:
* **Mouse** -- scroll direction, wheel speed, double-click speed,
acceleration, cursor speed
* **Colors** -- every system colour with live preview; theme load
and save
* **Desktop** -- wallpaper image and display mode
* **Video** -- resolution and colour depth switching
Changes preview live. OK writes to `CONFIG/DVX.INI`; Cancel reverts
to the snapshot taken when the panel opened.
### Image Viewer (imgview, BASIC)
| | |
|---|---|
| File | `apps/kpunch/imgview/imgview.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
Displays BMP, PNG, JPEG, and GIF images scaled to fit the window
with aspect ratio preserved. Resize to zoom. Open via the File menu
or via launch arguments. Built from a BASIC project using the
Canvas widget.
### DVX BASIC (dvxbasic)
| | |
|---|---|
| File | `apps/kpunch/dvxbasic/dvxbasic.app` |
| Type | Callback-only |
| Multi-instance | No |
Visual Basic 3 compatible IDE: form designer, per-procedure code
editor, project manager, compiler, and runtime VM. See
`dvxbasic/README.md` for a detailed architecture description.
`dvxbasic/` also builds:
* `bin/libs/kpunch/basrt/basrt.lib` -- the BASIC runtime (VM +
values) as a separately loadable library, so compiled BASIC apps
do not carry their own copy of the runtime.
* `bin/apps/kpunch/dvxbasic/basstub.app` -- the stub used as the
template for every compiled BASIC app.
### DVX Help Viewer (dvxhelp)
| | |
|---|---|
| File | `apps/kpunch/dvxhelp/dvxhelp.app` |
| Type | Callback-only |
| Multi-instance | Yes |
Renders compiled `.hlp` files produced by `dvxhlpc`. Supports table
of contents, keyword index, full-text search (trigram), hyperlinks,
and inline images.
### Icon Editor (iconed, BASIC)
| | |
|---|---|
| File | `apps/kpunch/iconed/iconed.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
Paint-program-style editor for creating 32x32 BMP icons. Pen /
eraser / fill tools, 16-colour palette, BMP save / load.
### Help Editor (helpedit, BASIC)
| | |
|---|---|
| File | `apps/kpunch/dvxhelp/helpedit.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
Editor for `.dhs` help source files with syntax-aware controls. Runs
the compiled system help viewer by default for live preview.
### Resource Editor (resedit, BASIC)
| | |
|---|---|
| File | `apps/kpunch/resedit/resedit.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
GUI wrapper around `dvxres`: open a `.app` / `.wgt` / `.lib`,
browse its resources, add, replace, extract, or remove them.
### BASIC Demo (basdemo, BASIC)
| | |
|---|---|
| File | `apps/kpunch/basdemo/basdemo.app` |
| Type | BASIC (main-loop via basstub) |
| Multi-instance | No |
Gallery of short BASIC programs demonstrating the language and the
form designer. Intended as sample source for reading.
## Build
```
make # build every app (C apps + BASIC apps)
make <appname> # build a single app
make clean # remove all app build artifacts
```
The `apps/kpunch/Makefile` orchestrates both C and BASIC builds:
* **C apps** compile each `<app>/<app>.c` to an object file, run
`dxe3gen -U` on it to produce `<app>.app`, then attach resources
with `dvxres build` if a `.res` manifest exists.
* **BASIC apps** run `bascomp <app>.dbp -o <app>.app -release`,
which handles project-file parsing, compilation, resource
attachment, and linking into `basstub.app`.
Build output goes to `bin/apps/kpunch/<app>/<app>.app`.
## Writing a New C App
Minimal skeleton:
```c
#include "dvxApp.h"
#include "dvxWgt.h"
#include "shellApp.h"
#include <stdint.h>
#include <stdbool.h>
// Prototypes
static int32_t appMainImpl(DxeAppContextT *ctx);
static void onClose(WindowT *win);
// Required descriptor
AppDescriptorT appDescriptor = {
.name = "My App",
.hasMainLoop = false,
.multiInstance = false,
.stackSize = SHELL_STACK_DEFAULT,
.priority = 0
};
static void onClose(WindowT *win) {
(void)win;
// Callback-only apps: nothing to do; the shell reaps us when
// the last window closes.
}
int32_t appMain(DxeAppContextT *ctx) {
return appMainImpl(ctx);
}
static int32_t appMainImpl(DxeAppContextT *ctx) {
AppContextT *ac = ctx->shellCtx;
WindowT *win = dvxCreateWindow(ac, appDescriptor.name, 100, 100, 320, 240, true);
if (!win) {
return -1;
}
win->onClose = onClose;
return 0;
}
```
Add a Makefile target that mirrors the existing apps (see
`apps/kpunch/Makefile` for the pattern), drop a `.res` manifest next
to the source, and provide a 32x32 `icon32.bmp`. `make <app>` should
then produce `bin/apps/kpunch/<app>/<app>.app`.
## Testing Locally
After `make`, the app is deployed in-tree under `bin/apps/kpunch/`.
Launch the full system (`bin/dvx.exe`) and the Program Manager picks
up the new app automatically on the next boot.
To iterate quickly: rebuild the single app, restart DVX, and launch
it. The log at `bin/DVX.LOG` records load errors, missing
dependencies, and per-app crash diagnostics.
## Files
```
apps/kpunch/
Makefile top-level app build
README.md this file
# C apps
progman/
progman.c Program Manager
dvxhelp.hcf help-compiler config (system reference)
clock/
clock.c digital clock (main-loop reference)
clock.res
icon32.bmp
dvxdemo/
dvxdemo.c widget showcase
dvxdemo.res
icon32.bmp
logo.bmp / new.bmp / open.bmp / sample.bmp / save.bmp
cpanel/
cpanel.c Control Panel
cpanel.res
icon32.bmp
dvxhelp/
dvxhelp.c help viewer
dvxhelp.res
help.dhs source for the dvxhelp app's own help
hlpformat.h binary format shared with the compiler
sample.dhs small sample source
icon32.bmp
helpedit/ BASIC project: help editor
helpedit.dbp
helpedit.frm
ICON32.BMP
# BASIC apps
iconed/
iconed.dbp
iconed.frm
ICON32.BMP
notepad/
notepad.dbp
notepad.frm
ICON32.BMP
imgview/
imgview.dbp
imgview.frm
ICON32.BMP
basdemo/
basdemo.dbp
basdemo.frm
ICON32.BMP
resedit/
resedit.dbp
resedit.frm
ICON32.BMP
dvxbasic/ see dvxbasic/README.md
compiler/ lexer, parser, codegen, strip, obfuscate
runtime/ VM + value system
formrt/ form runtime (BASIC <-> DVX widgets)
ide/ IDE front end
stub/ bascomp + basstub
...
```

BIN
src/apps/kpunch/basdemo/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,18 @@
[Project]
Name = BASIC Demo
Author = Scott Duensing
Publisher = Kangaroo Punch Studios
Copyright = Copyright 2026 Scott Duensing
Description = Comprehensive tour of DVX BASIC features
Icon = ICON32.BMP
[Modules]
File0 = ../../../include/basic/commdlg.bas
Count = 1
[Forms]
File0 = basdemo.frm
Count = 1
[Settings]
StartupForm = BasicDemo

View file

@ -0,0 +1,879 @@
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
' The MIT License (MIT)
'
' Copyright (C) 2026 Scott Duensing
'
' Permission is hereby granted, free of charge, to any person obtaining a copy
' of this software and associated documentation files (the "Software"), to
' deal in the Software without restriction, including without limitation the
' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
' sell copies of the Software, and to permit persons to whom the Software is
' furnished to do so, subject to the following conditions:
'
' The above copyright notice and this permission notice shall be included in
' all copies or substantial portions of the Software.
'
' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
' IN THE SOFTWARE.
OPTION EXPLICIT
TYPE PointT
x AS INTEGER
y AS INTEGER
END TYPE
' Module-level state. Grouped here up front (rather than sprinkled
' between SUB definitions) so the compiler sees a simple top-level
' run-once block followed by a flat list of SUBs / FUNCTIONs.
DIM gfxWin AS LONG
DIM dynForm AS LONG
DIM dynCount AS INTEGER
DIM timerWin AS LONG
DIM tickCount AS LONG
gfxWin = 0
dynForm = 0
dynCount = 0
timerWin = 0
tickCount = 0
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 ""
SUB Say(s AS STRING)
OutArea.AppendText s + CHR$(10)
END SUB
SUB Header(title AS STRING)
Say ""
Say "--- " + title + " ---"
END SUB
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 + "Copyright 2026 Scott Duensing"
MsgBox msg, vbOKOnly, "About"
END SUB
SUB btnTypes_Click
' Types: INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
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
SUB btnMath_Click
' Math: integer + float operators, built-in functions
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
SUB btnStrings_Click
' Strings: concatenation, LEFT$/RIGHT$/MID$/LEN/INSTR/UCASE$/LCASE$
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
SUB btnArrays_Click
' Arrays: 1D, 2D, LBOUND/UBOUND, REDIM PRESERVE
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
SUB btnData_Click
' DATA / READ / RESTORE
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
SUB btnFlow_Click
' Control flow: IF, SELECT CASE, FOR, DO WHILE, GOSUB
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
SUB btnUdt_Click
' User-Defined Type
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
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
SUB btnError_Click
' ON ERROR GOTO
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
SUB btnFormat_Click
' PRINT USING / FORMAT$
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
SUB btnFileIO_Click
' File I/O
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
SUB btnSystem_Click
' System: App object, environment, current directory
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
SUB btnIni_Click
' INI read/write
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
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
SUB mnuGraphics_Click
IF gfxWin <> 0 THEN
EXIT SUB
END IF
DIM frm AS LONG
SET frm = CreateForm("GraphicsForm", 380, 360)
GraphicsForm.Caption = "Graphics Demo"
gfxWin = frm
DIM cv AS LONG
SET cv = CreateControl(frm, "PictureBox", "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 BasicDemo_Unload
' Closing the main form shuts down the whole app, including any
' child forms (Graphics, Dynamic, Timer) the user left open.
END
END SUB
SUB GraphicsForm_Unload
gfxWin = 0
END SUB
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
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
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
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

@ -1,3 +1,25 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// clock.c -- Clock DXE application (main-loop with tsYield)
//
// This is a main-loop DXE app (hasMainLoop = true), in contrast to callback-
@ -19,11 +41,11 @@
// would starve the shell and all other apps.
#include "dvxApp.h"
#include "dvxWidget.h"
#include "dvxWgt.h"
#include "dvxDraw.h"
#include "dvxVideo.h"
#include "shellApp.h"
#include "taskswitch.h"
#include "taskSwch.h"
#include <stdint.h>
#include <stdbool.h>
@ -77,9 +99,15 @@ AppDescriptorT appDescriptor = {
.priority = TS_PRIORITY_LOW
};
// ============================================================
// Callbacks (fire in task 0 during dvxUpdate)
// ============================================================
// The shell calls appShutdown (if exported) when force-killing an app via
// Task Manager or during shell shutdown. For main-loop apps, this is the
// signal to break out of the main loop. Without this, shellForceKillApp
// would have to terminate the task forcibly, potentially leaking resources.
void appShutdown(void) {
sState.quit = true;
}
// Setting quit = true tells the main loop (running in a separate task) to
// exit. The main loop then destroys the window and returns from appMain,
@ -135,9 +163,6 @@ static void onPaint(WindowT *win, RectT *dirty) {
drawText(&cd, ops, font, dateX, dateY, sState.dateStr, colors->contentFg, colors->contentBg, true);
}
// ============================================================
// Time update
// ============================================================
static void updateTime(void) {
time_t now = time(NULL);
@ -157,26 +182,11 @@ static void updateTime(void) {
hour12 = 12;
}
snprintf(sState.timeStr, sizeof(sState.timeStr), "%d:%02d:%02d %s", hour12, tm->tm_min, tm->tm_sec, ampm);
snprintf(sState.timeStr, sizeof(sState.timeStr), "%d:%02d:%02d %s", (int)hour12, tm->tm_min, tm->tm_sec, ampm);
snprintf(sState.dateStr, sizeof(sState.dateStr), "%04d-%02d-%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
sState.lastUpdate = now;
}
// ============================================================
// Shutdown hook (optional DXE export)
// ============================================================
// The shell calls appShutdown (if exported) when force-killing an app via
// Task Manager or during shell shutdown. For main-loop apps, this is the
// signal to break out of the main loop. Without this, shellForceKillApp
// would have to terminate the task forcibly, potentially leaking resources.
void appShutdown(void) {
sState.quit = true;
}
// ============================================================
// Entry point (runs in its own task)
// ============================================================
// This runs in its own task. The shell creates the task before calling
// appMain, and reaps it when appMain returns.
@ -203,10 +213,8 @@ int32_t appMain(DxeAppContextT *ctx) {
sWin->onClose = onClose;
sWin->onPaint = onPaint;
// Initial paint -- dvxInvalidateWindow calls onPaint automatically
dvxInvalidateWindow(ac, sWin);
// Main loop: check if the second has changed, repaint if so, then yield.
// First paint happens automatically on the next dvxUpdate frame.
// tsYield() transfers control back to the shell's task scheduler.
// On a 486, time() resolution is 1 second, so we yield many times per
// second between actual updates -- this keeps CPU usage near zero.

View file

@ -0,0 +1,7 @@
# clock.res -- Resource manifest for Clock
icon32 icon icon32.bmp
name text "Clock"
author text "Scott Duensing"
copyright text "Copyright 2026 Scott Duensing"
publisher text "Kangaroo Punch Studios"
description text "Digital clock with date display"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
# cpanel.res -- Resource manifest for Control Panel
icon32 icon icon32.bmp
name text "Control Panel"
author text "Scott Duensing"
copyright text "Copyright 2026 Scott Duensing"
publisher text "Kangaroo Punch Studios"
description text "System settings and preferences"

View file

@ -0,0 +1,233 @@
# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# DVX BASIC Makefile for DJGPP cross-compilation
#
# Builds:
# basrt.lib -- BASIC runtime library (VM + values + dlregsym init)
# dvxbasic.app -- BASIC IDE (compiler + UI, links against basrt.lib)
#
# The runtime is a separate library so compiled BASIC apps can use
# it without including the compiler. The library's constructor calls
# dlregsym() to register its exports, making them available to DXEs
# loaded later (apps, widgets, etc.).
DJGPP_PREFIX = $(HOME)/djgpp/djgpp
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen
CFLAGS = -O2 -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -march=i486 -mtune=i586 -I../../../libs/kpunch/libdvx -I../../../libs/kpunch/libdvx/platform -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks -I../../../libs/kpunch/libdvx/thirdparty -I.
OBJDIR = ../../../../obj/dvxbasic
LIBSDIR = ../../../../bin/libs
APPDIR = ../../../../bin/apps/kpunch/dvxbasic
HOSTDIR = ../../../../bin/host
DVXRES = $(HOSTDIR)/dvxres
# Runtime library objects (VM + values + form runtime + serialization)
RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/frmParser.o $(OBJDIR)/serialize.o
RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt
RT_TARGET = $(RT_TARGETDIR)/basrt.lib
# Compiler objects (only needed by the IDE)
COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/symtab.o $(OBJDIR)/strip.o $(OBJDIR)/obfuscate.o $(OBJDIR)/compact.o $(OBJDIR)/basBuild.o
# IDE app objects
IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideMenuEditor.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o
APP_OBJS = $(IDE_OBJS)
APP_TARGET = $(APPDIR)/dvxbasic.app
# Standalone stub (embedded as IDE resource)
STUB_OBJS = $(OBJDIR)/basstub.o
STUB_TARGET = $(OBJDIR)/basstub.app
# Native test programs (host gcc, not cross-compiled)
HOSTCC = gcc
HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -D_GNU_SOURCE -I. -I../../../libs/kpunch/libdvx -I../../../libs/kpunch/libdvx/platform -I../../../libs/kpunch/libdvx/thirdparty
TEST_COMPILER = $(HOSTDIR)/test_compiler
TEST_VM = $(HOSTDIR)/test_vm
TEST_LEX = $(HOSTDIR)/test_lex
TEST_QUICK = $(HOSTDIR)/test_quick
TEST_COMPACT = $(HOSTDIR)/test_compact
TEST_SUITE = $(HOSTDIR)/test_suite
STB_DS_IMPL = ../../../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c
PLATFORM_UTIL = ../../../libs/kpunch/libdvx/platform/dvxPlatformUtil.c
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 $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_LEX_SRCS = test_lex.c compiler/lexer.c
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_COMPACT_SRCS = test_compact.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_SUITE_SRCS = test_suite.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
# Command-line compiler (host tool)
BASCOMP_SRCS = stub/bascomp.c basBuild.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/obfuscate.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c ../../../libs/kpunch/libdvx/dvxPrefs.c ../../../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
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../../../libs/kpunch/libdvx -I../../../libs/kpunch/libdvx/platform -I../../../libs/kpunch/libdvx/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
all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(TEST_SUITE)
$(TEST_SUITE)
$(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm
$(TEST_SUITE): $(TEST_SUITE_SRCS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_SUITE_SRCS) -lm
$(TEST_VM): $(TEST_VM_SRCS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_VM_SRCS) -lm
$(TEST_LEX): $(TEST_LEX_SRCS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -w -o $@ $(TEST_LEX_SRCS) -lm
$(TEST_QUICK): $(TEST_QUICK_SRCS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_QUICK_SRCS) -lm
$(TEST_COMPACT): $(TEST_COMPACT_SRCS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPACT_SRCS) -lm
# Host command-line compiler -- basstub.app is appended as a STUB
# resource so bascomp is self-contained (no BASSTUB.APP companion file).
$(BASCOMP_TARGET): $(BASCOMP_SRCS) $(STUB_TARGET) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $@ $(BASCOMP_SRCS) -lm
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
# DOS command-line compiler (same STUB embed as the host build)
$(SYSTEMDIR)/BASCOMP.EXE: $(BASCOMP_SRCS) $(STUB_TARGET) | $(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
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
$(HOSTDIR):
mkdir -p $(HOSTDIR)
$(SYSTEMDIR):
mkdir -p $(SYSTEMDIR)
# Runtime library DXE (exports symbols via dlregsym constructor)
$(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR)
$(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe -U $(RT_OBJS)
mv $(RT_TARGETDIR)/basrt.dxe $@
$(RT_TARGETDIR)/basrt.dep: basrt.dep | $(RT_TARGETDIR)
sed 's/$$/\r/' $< > $@
# Standalone stub DXE (embedded as resource in IDE app)
$(STUB_TARGET): $(STUB_OBJS) | $(APPDIR)
$(DXE3GEN) -o $@ -U $(STUB_OBJS)
# IDE app DXE (compiler linked in, runtime from basrt.lib, stub embedded)
$(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) $(STUB_TARGET) dvxbasic.res | $(APPDIR)
$(DXE3GEN) -o $@ -U $(COMP_OBJS) $(APP_OBJS)
$(DVXRES) build $@ dvxbasic.res
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
# Object files
$(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h formrt/frmParser.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/frmParser.o: formrt/frmParser.c formrt/frmParser.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/serialize.o: runtime/serialize.c runtime/serialize.h runtime/vm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/strip.o: compiler/strip.c compiler/strip.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
$(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)/basBuild.o: basBuild.c basBuild.h basRes.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/basstub.o: stub/basstub.c runtime/vm.h runtime/serialize.h formrt/formrt.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideMenuEditor.o: ide/ideMenuEditor.c ide/ideMenuEditor.h ide/ideDesigner.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideMenuEditor.h ide/ideProject.h ide/ideToolbox.h ide/ideProperties.h compiler/parser.h runtime/vm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideProject.o: ide/ideProject.c ide/ideProject.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideToolbox.o: ide/ideToolbox.c ide/ideToolbox.h ide/ideDesigner.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/lexer.o: compiler/lexer.c compiler/lexer.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/parser.o: compiler/parser.c compiler/parser.h compiler/lexer.h compiler/codegen.h compiler/symtab.h compiler/opcodes.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/symtab.o: compiler/symtab.c compiler/symtab.h compiler/opcodes.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/values.o: runtime/values.c runtime/values.h compiler/opcodes.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/vm.o: runtime/vm.c runtime/vm.h runtime/values.h compiler/opcodes.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
# Directories
$(OBJDIR):
mkdir -p $(OBJDIR)
$(LIBSDIR):
mkdir -p $(LIBSDIR)
$(RT_TARGETDIR):
mkdir -p $(RT_TARGETDIR)
$(APPDIR):
mkdir -p $(APPDIR)
clean:
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) $(TEST_COMPACT)

View file

@ -0,0 +1,55 @@
# DVX BASIC
A Visual Basic 3 clone for the DVX GUI system. Provides a visual form
designer, per-procedure code editor, project management, and a stack-
based bytecode compiler and VM for running event-driven BASIC programs.
## Documentation
For BASIC authors, the complete user reference is split across several
`.dhs` help sources in this directory, all compiled into the DVX BASIC
Reference:
- `langref.dhs` -- language reference: data types, keywords, operators,
control flow, built-in functions, I/O, error codes.
- `ideguide.dhs` -- IDE guide: menus, toolbar, project model, form
designer, code editor, debugger, preferences.
- `ctrlover.dhs` -- control overview: common widget properties and
events from a BASIC author's perspective.
- `form.dhs` -- form lifecycle, properties, events.
- `basrt.dhs` -- BASIC runtime reference: DECLARE LIBRARY mechanism,
shipped include files.
After `make`, the compiled form is `bin/apps/kpunch/dvxbasic/dvxbasic.hlp`
(viewable via the Help Viewer app or from within the IDE via F1) and
`docs/dvx_basic_reference.html`.
## Subdirectories (for maintainers)
- `compiler/` -- lexer, parser, codegen, symbol table, opcodes.
Single-pass compiler that translates BASIC source to stack-based
p-code.
- `runtime/` -- VM and tagged value system (values.c, vm.c, serialize.c).
- `formrt/` -- form runtime, the bridge between the BASIC VM and the
DVX widget system.
- `ide/` -- the IDE itself (main, designer, project, properties,
toolbox, menu editor).
- `stub/` -- `basstub` (runtime loader for compiled BASIC `.app`
files) and `bascomp` (standalone command-line compiler).
## Project Files
project.dbp INI-format project file
module.bas BASIC module (code only)
form.frm Form file (layout + code after the form's End)
`.bas` modules compile before `.frm` code sections, so CONST
declarations are visible to form event handlers.
## Build
make -C src/apps/kpunch/dvxbasic # cross-compile for DOS
make -C src/apps/kpunch/dvxbasic tests # build native test programs
Test programs: `test_compiler`, `test_vm`, `test_lex`, `test_quick`,
`test_compact`.

View file

@ -0,0 +1,138 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// basBuild.c -- shared "emit .app resource set" step
//
// See basBuild.h for the public contract. This file used to live inline
// in two places: ideMain.c (the IDE's Make Executable path) and
// bascomp.c (the standalone command-line compiler). Both implementations
// were essentially identical, so they are now consolidated here.
//
// The canonical emit order is:
// 1. BAS_RES_NAME (always, falls back to "BASIC App")
// 2. BAS_RES_AUTHOR / PUBLISHER / VERSION / COPYRIGHT / DESCRIPTION
// (each skipped if empty)
// 3. BAS_RES_ICON32 (file via iconPath, else iconData bytes)
// 4. BAS_RES_HELPFILE (filename only, if helpFile set)
// 5. BAS_RES_MODULE (bytecode)
// 6. BAS_RES_DEBUG (optional)
// 7. FORM0, FORM1, ... (one per spec->formCount)
#include "basBuild.h"
#include "basRes.h"
#include "../../../libs/kpunch/libdvx/dvxRes.h"
#include "../../../libs/kpunch/libdvx/platform/dvxPlat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ------------------------------------------------------------
// Internal helpers
// ------------------------------------------------------------
static void appendText(const char *path, const char *name, const char *value) {
if (!value || !value[0]) {
return;
}
dvxResAppend(path, name, DVX_RES_TEXT, value, (uint32_t)strlen(value) + 1);
}
static void emitIcon(const char *path, const BasBuildSpecT *spec) {
// If a disk path was given, load it and embed. Otherwise use the
// pre-loaded bytes (used by the IDE for its "noicon" fallback).
if (spec->iconPath && spec->iconPath[0]) {
int32_t iconLen = 0;
char *iconData = platformReadFile(spec->iconPath, &iconLen);
if (iconData) {
dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
free(iconData);
}
return;
}
if (spec->iconData && spec->iconSize > 0) {
dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, spec->iconData, (uint32_t)spec->iconSize);
}
}
// ------------------------------------------------------------
// Public API
// ------------------------------------------------------------
int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec) {
if (!outPath || !spec) {
return -1;
}
// Project metadata. Name is required -- fall back to a generic label
// so the compiled app always has something sensible to display.
const char *projName = (spec->projName && spec->projName[0]) ? spec->projName : "BASIC App";
appendText(outPath, BAS_RES_NAME, projName);
appendText(outPath, BAS_RES_AUTHOR, spec->author);
appendText(outPath, BAS_RES_PUBLISHER, spec->publisher);
appendText(outPath, BAS_RES_VERSION, spec->version);
appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright);
appendText(outPath, BAS_RES_DESCRIPTION, spec->description);
// Icon.
emitIcon(outPath, spec);
// Help file name (just the basename -- stub resolves it next to the app).
if (spec->helpFile && spec->helpFile[0]) {
const char *helpBase = platformPathBaseName(spec->helpFile);
appendText(outPath, BAS_RES_HELPFILE, helpBase);
}
// Bytecode module.
if (spec->moduleData && spec->moduleLen > 0) {
dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, spec->moduleData, (uint32_t)spec->moduleLen);
}
// Optional debug info.
if (spec->debugData && spec->debugLen > 0) {
dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, spec->debugData, (uint32_t)spec->debugLen);
}
// Form resources. Callers pre-strip / pre-obfuscate as needed; we just
// write them out as FORM0, FORM1, ...
for (int32_t i = 0; i < spec->formCount; i++) {
if (!spec->formData || !spec->formData[i] || !spec->formLens || spec->formLens[i] <= 0) {
continue;
}
char resName[16];
snprintf(resName, sizeof(resName), "FORM%ld", (long)i);
dvxResAppend(outPath, resName, DVX_RES_BINARY, spec->formData[i], (uint32_t)spec->formLens[i]);
}
return 0;
}

View file

@ -0,0 +1,87 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// basBuild.h -- shared "emit .app resource set" step
//
// Both the DVX BASIC IDE (ideMain.c) and the standalone command-line
// compiler (bascomp.c) need to attach the same group of resources to
// an output .app file after writing the stub DXE. This header exposes
// a single function that takes the metadata, bytecode, debug info and
// form texts and appends all resources in the canonical order.
//
// Callers are still responsible for:
// - writing the stub to outPath before calling us
// - copying the .hlp file next to outPath (if any)
// - freeing any buffers they passed in via BasBuildSpecT
//
// Resource names use BAS_RES_* from basRes.h so both callers stay in
// sync automatically.
#ifndef BAS_BUILD_H
#define BAS_BUILD_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
// Basic metadata (any NULL/empty is skipped).
const char *projName;
const char *author;
const char *publisher;
const char *version;
const char *copyright;
const char *description;
const char *helpFile; // just the filename, not a path
// Icon: either a file to load and embed, OR pre-loaded bytes.
// If iconPath is set (non-NULL and non-empty) it is read from disk.
// Otherwise iconData / iconSize are used (may themselves be NULL/0).
const char *iconPath;
const void *iconData;
int32_t iconSize;
// Bytecode module and optional debug info.
const void *moduleData;
int32_t moduleLen;
const void *debugData; // may be NULL
int32_t debugLen;
// Forms: parallel arrays of formCount entries, each a text blob.
// formData[i] points at formLens[i] bytes of (already stripped /
// possibly obfuscated) form source to embed as resource FORMi.
int32_t formCount;
const uint8_t *const *formData;
const int32_t *formLens;
} BasBuildSpecT;
// Append the full resource set to outPath. Returns 0 on success, non-zero
// on failure. The stub must already have been written to outPath.
int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec);
#ifdef __cplusplus
}
#endif
#endif // BAS_BUILD_H

View file

@ -0,0 +1,79 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// basRes.h -- single source of truth for BASIC project / binary layout
//
// Keys used in .dbp project files and names of resources written into the
// compiled .app DXE. These were previously scattered as raw string literals
// across the IDE (ideMain.c, ideProject.c) and the standalone compiler
// (bascomp.c), making it easy for one side to drift from the other.
// All readers and writers now include this header.
#ifndef BAS_RES_H
#define BAS_RES_H
// ------------------------------------------------------------
// [Project] section of a .dbp file
// ------------------------------------------------------------
#define BAS_INI_SECTION_PROJECT "Project"
#define BAS_INI_KEY_NAME "Name"
#define BAS_INI_KEY_AUTHOR "Author"
#define BAS_INI_KEY_PUBLISHER "Publisher"
#define BAS_INI_KEY_VERSION "Version"
#define BAS_INI_KEY_COPYRIGHT "Copyright"
#define BAS_INI_KEY_DESCRIPTION "Description"
#define BAS_INI_KEY_ICON "Icon"
#define BAS_INI_KEY_HELPFILE "HelpFile"
// [Settings] / [Modules] / [Forms] sections of a .dbp file
#define BAS_INI_SECTION_SETTINGS "Settings"
#define BAS_INI_KEY_STARTUPFORM "StartupForm"
#define BAS_INI_KEY_OPTIONEXPLICIT "OptionExplicit"
#define BAS_INI_SECTION_MODULES "Modules"
#define BAS_INI_SECTION_FORMS "Forms"
// ------------------------------------------------------------
// Resource names inside a compiled .app DXE
// ------------------------------------------------------------
// Text metadata -- mirrors the [Project] keys above but lowercase
// (resource names are case-sensitive; apps query these at runtime).
#define BAS_RES_NAME "name"
#define BAS_RES_AUTHOR "author"
#define BAS_RES_PUBLISHER "publisher"
#define BAS_RES_VERSION "version"
#define BAS_RES_COPYRIGHT "copyright"
#define BAS_RES_DESCRIPTION "description"
#define BAS_RES_HELPFILE "helpfile"
// Icon -- 32x32 application icon
#define BAS_RES_ICON32 "icon32"
// Binary payload -- bytecode + debug info
#define BAS_RES_MODULE "MODULE"
#define BAS_RES_DEBUG "DEBUG"
// Stub DXE embedded in the IDE app for release builds
#define BAS_RES_STUB "STUB"
#endif // BAS_RES_H

View file

@ -0,0 +1,558 @@
# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
.section Libraries
.topic lib.basrt
.title BASIC Runtime Library
.toc 0 BASIC Runtime Library
.index BASIC Runtime
.index BasVmT
.index BasValueT
.index BasStringT
.index BasArrayT
.index BasModuleT
.h1 BASIC Runtime Library
Stack-based p-code virtual machine and value system for DVX BASIC. Embeddable: the host provides I/O and UI callbacks. No DVX GUI dependencies in the core runtime.
Headers: apps/dvxbasic/runtime/vm.h, apps/dvxbasic/runtime/values.h
.link lib.basrt.values Value System (values.h)
.link lib.basrt.vm Virtual Machine (vm.h)
.topic lib.basrt.values
.title Value System
.toc 1 Value System
.index BasValueT
.index BasStringT
.index BasArrayT
.index BasUdtT
.index basStringNew
.index basArrayNew
.index basValCopy
.index basValRelease
.h1 Value System
Tagged union value type for the VM evaluation stack, variables, and array elements. Strings, arrays, and UDT instances are reference-counted for automatic memory management without a garbage collector.
Header: apps/dvxbasic/runtime/values.h
.h2 Type Tags
.table
Constant Value C Union Field Description
-------- ----- ------------- -----------
BAS_TYPE_INTEGER 0 intVal 16-bit signed integer.
BAS_TYPE_LONG 1 longVal 32-bit signed integer.
BAS_TYPE_SINGLE 2 sngVal 32-bit float.
BAS_TYPE_DOUBLE 3 dblVal 64-bit float.
BAS_TYPE_STRING 4 strVal Reference-counted dynamic string.
BAS_TYPE_BOOLEAN 5 boolVal True (-1) or False (0).
BAS_TYPE_ARRAY 6 arrVal Reference-counted array.
BAS_TYPE_UDT 7 udtVal Reference-counted user-defined type.
BAS_TYPE_OBJECT 8 objVal Opaque host pointer (form, control).
BAS_TYPE_REF 9 refVal ByRef pointer to a BasValueT slot.
.endtable
.h2 BasValueT
Tagged union holding any BASIC value.
.code
struct BasValueTag {
uint8_t type; // BAS_TYPE_*
union {
int16_t intVal;
int32_t longVal;
float sngVal;
double dblVal;
BasStringT *strVal;
int16_t boolVal;
BasArrayT *arrVal;
BasUdtT *udtVal;
void *objVal;
BasValueT *refVal;
};
};
.endcode
.h2 Value Constructors
.code
BasValueT basValInteger(int16_t v);
BasValueT basValLong(int32_t v);
BasValueT basValSingle(float v);
BasValueT basValDouble(double v);
BasValueT basValString(BasStringT *s);
BasValueT basValStringFromC(const char *text);
BasValueT basValBool(bool v);
BasValueT basValObject(void *obj);
.endcode
Each returns a BasValueT with the appropriate type tag set. basValString increments the string's reference count.
.h2 Value Lifetime
.code
BasValueT basValCopy(BasValueT v);
void basValRelease(BasValueT *v);
.endcode
.table
Function Description
-------- -----------
basValCopy Copy a value. Increments reference count for strings, arrays, and UDTs.
basValRelease Release a value. Decrements reference count and frees if it reaches zero.
.endtable
.h2 Type Conversion
.code
BasValueT basValToInteger(BasValueT v);
BasValueT basValToLong(BasValueT v);
BasValueT basValToSingle(BasValueT v);
BasValueT basValToDouble(BasValueT v);
BasValueT basValToString(BasValueT v);
BasValueT basValToBool(BasValueT v);
.endcode
Each returns a new value of the target type. The original is not released; the caller manages both lifetimes.
.h2 Value Utilities
.table
Function Description
-------- -----------
basValToNumber(v) Convert any numeric value to double.
basValFormatString(v) Return a new ref-counted string representation of v.
basValIsTruthy(v) True if non-zero number or non-empty string.
basValCompare(a, b) Compare two values. Returns -1, 0, or 1.
basValCompareCI(a, b) Case-insensitive comparison (OPTION COMPARE TEXT).
basValPromoteType(a, b) Determine common type for binary ops (e.g. Integer + Single -> Single).
.endtable
.h2 BasStringT
Reference-counted string with flexible array member for inline storage.
.code
typedef struct {
int32_t refCount;
int32_t len;
int32_t cap;
char data[];
} BasStringT;
.endcode
.h3 String Functions
.table
Function Description
-------- -----------
basStringNew(text, len) Allocate from a C string. refCount starts at 1.
basStringAlloc(cap) Allocate an empty string with given capacity.
basStringRef(s) Increment reference count. Returns s.
basStringUnref(s) Decrement reference count. Frees when it reaches zero.
basStringConcat(a, b) Concatenate two strings. Returns a new string (refCount 1).
basStringSub(s, start, len) Extract a substring. Returns a new string (refCount 1).
basStringCompare(a, b) Compare. Returns <0, 0, >0 (like strcmp).
basStringCompareCI(a, b) Case-insensitive compare.
basStringSystemInit() Initialize the string system and empty string singleton.
basStringSystemShutdown() Shut down the string system.
.endtable
The global basEmptyString is a singleton that is never freed.
.h2 BasArrayT
Reference-counted multi-dimensional array (up to BAS_ARRAY_MAX_DIMS = 8 dimensions).
.code
typedef struct {
int32_t refCount;
uint8_t elementType;
int32_t dims;
int32_t lbound[BAS_ARRAY_MAX_DIMS];
int32_t ubound[BAS_ARRAY_MAX_DIMS];
int32_t totalElements;
BasValueT *elements;
} BasArrayT;
.endcode
.h3 Array Functions
.table
Function Description
-------- -----------
basArrayNew(dims, lbounds, ubounds, type) Allocate an array. refCount starts at 1.
basArrayFree(arr) Free all elements and release the array.
basArrayRef(arr) Increment reference count.
basArrayUnref(arr) Decrement reference count. Frees at zero.
basArrayIndex(arr, indices, ndims) Compute flat index from multi-dimensional indices. Returns -1 if out of bounds.
.endtable
.h2 BasUdtT
Reference-counted user-defined type instance.
.code
typedef struct {
int32_t refCount;
int32_t typeId;
int32_t fieldCount;
BasValueT *fields;
} BasUdtT;
.endcode
.h3 UDT Functions
.table
Function Description
-------- -----------
basUdtNew(typeId, fieldCount) Allocate a UDT instance. refCount starts at 1.
basUdtFree(udt) Free all fields and release.
basUdtRef(udt) Increment reference count.
basUdtUnref(udt) Decrement reference count. Frees at zero.
.endtable
.topic lib.basrt.vm
.title Virtual Machine
.toc 1 Virtual Machine
.index BasVmT
.index BasModuleT
.index BasVmResultE
.index basVmCreate
.index basVmRun
.index basVmStep
.index basVmDestroy
.index basVmCallSub
.h1 Virtual Machine
Stack-based p-code interpreter. Executes compiled BASIC bytecode modules. The host provides I/O, UI, SQL, and external library callbacks. The VM has no DVX dependencies; it can be embedded in any C program.
Header: apps/dvxbasic/runtime/vm.h
.h2 VM Limits
.table
Constant Value Description
-------- ----- -----------
BAS_VM_STACK_SIZE 256 Evaluation stack depth.
BAS_VM_CALL_STACK_SIZE 64 Maximum call nesting depth.
BAS_VM_MAX_GLOBALS 512 Global variable slots.
BAS_VM_MAX_LOCALS 64 Local variables per stack frame.
BAS_VM_MAX_FOR_DEPTH 32 Maximum nested FOR loop depth.
BAS_VM_MAX_FILES 16 Open file channels (1-based).
.endtable
.h2 BasVmResultE
Result codes returned by basVmRun and basVmStep.
.table
Code Value Description
---- ----- -----------
BAS_VM_OK 0 Program completed normally.
BAS_VM_HALTED 1 HALT instruction reached.
BAS_VM_YIELDED 2 DoEvents yielded control.
BAS_VM_ERROR 3 Runtime error.
BAS_VM_STACK_OVERFLOW 4 Evaluation stack overflow.
BAS_VM_STACK_UNDERFLOW 5 Evaluation stack underflow.
BAS_VM_CALL_OVERFLOW 6 Call stack overflow.
BAS_VM_DIV_BY_ZERO 7 Division by zero.
BAS_VM_TYPE_MISMATCH 8 Type mismatch in operation.
BAS_VM_OUT_OF_MEMORY 9 Memory allocation failed.
BAS_VM_BAD_OPCODE 10 Unknown opcode encountered.
BAS_VM_FILE_ERROR 11 File I/O error.
BAS_VM_SUBSCRIPT_RANGE 12 Array subscript out of range.
BAS_VM_USER_ERROR 13 ON ERROR raised by program.
BAS_VM_STEP_LIMIT 14 Step limit reached (not an error).
BAS_VM_BREAKPOINT 15 Breakpoint or step completed (not an error).
.endtable
.h2 Lifecycle
.code
BasVmT *basVmCreate(void);
void basVmDestroy(BasVmT *vm);
void basVmLoadModule(BasVmT *vm, BasModuleT *module);
void basVmReset(BasVmT *vm);
.endcode
.table
Function Description
-------- -----------
basVmCreate Allocate and initialize a new VM instance.
basVmDestroy Destroy the VM and free all resources.
basVmLoadModule Load a compiled module (BasModuleT) into the VM.
basVmReset Reset to initial state (clear stack, globals, PC).
.endtable
.h2 Execution
.code
BasVmResultE basVmRun(BasVmT *vm);
BasVmResultE basVmStep(BasVmT *vm);
void basVmSetStepLimit(BasVmT *vm, int32_t limit);
.endcode
.table
Function Description
-------- -----------
basVmRun Execute the loaded module until it ends, halts, yields, errors, or hits a breakpoint/step limit.
basVmStep Execute a single instruction and return. Useful for debugger stepping.
basVmSetStepLimit Set maximum instructions per basVmRun call. 0 = unlimited (default). Returns BAS_VM_STEP_LIMIT when reached.
.endtable
.h2 I/O Callbacks
The host provides these callbacks for PRINT, INPUT, and DoEvents statements.
.code
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx);
void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx);
.endcode
.h3 Callback Types
.code
typedef void (*BasPrintFnT)(void *ctx, const char *text, bool newline);
typedef bool (*BasInputFnT)(void *ctx, const char *prompt,
char *buf, int32_t bufSize);
typedef bool (*BasDoEventsFnT)(void *ctx);
.endcode
.table
Type Description
---- -----------
BasPrintFnT Called for PRINT output. text is null-terminated. newline indicates line advance.
BasInputFnT Called for INPUT. Fill buf (up to bufSize-1 chars). Return true on success, false on cancel.
BasDoEventsFnT Called for DoEvents. Process pending events and return. Return false to stop the program.
.endtable
.h2 UI Callbacks
For form and control integration. The VM resolves all UI operations through these callbacks, keeping it independent of any specific GUI toolkit.
.code
void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui);
.endcode
.h3 BasUiCallbacksT
.table
Field Signature Description
----- --------- -----------
getProp BasValueT (*)(ctx, ctrlRef, propName) Get a control property value.
setProp void (*)(ctx, ctrlRef, propName, value) Set a control property.
callMethod BasValueT (*)(ctx, ctrlRef, methodName, args, argc) Call a method on a control.
createCtrl void *(*)(ctx, formRef, typeName, ctrlName) Create a control on a form.
findCtrl void *(*)(ctx, formRef, ctrlName) Find a control by name.
findCtrlIdx void *(*)(ctx, formRef, ctrlName, index) Find a control array element.
loadForm void *(*)(ctx, formName) Load a form by name.
unloadForm void (*)(ctx, formRef) Unload a form.
showForm void (*)(ctx, formRef, modal) Show a form (modal or modeless).
hideForm void (*)(ctx, formRef) Hide a form (keep in memory).
msgBox int32_t (*)(ctx, message, flags) Display a message box.
inputBox BasStringT *(*)(ctx, prompt, title, defaultText) Display an input box.
ctx void * User pointer passed to all callbacks.
.endtable
.h2 SQL Callbacks
For database integration.
.code
void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql);
.endcode
.h3 BasSqlCallbacksT
.table
Field Signature Description
----- --------- -----------
sqlOpen int32_t (*)(path) Open a database. Returns handle.
sqlClose void (*)(db) Close a database.
sqlExec bool (*)(db, sql) Execute a non-query SQL statement.
sqlError const char *(*)(db) Get last error message.
sqlQuery int32_t (*)(db, sql) Execute a query. Returns result set handle.
sqlNext bool (*)(rs) Advance to next row.
sqlEof bool (*)(rs) Check if at end of result set.
sqlFieldCount int32_t (*)(rs) Get number of columns.
sqlFieldName const char *(*)(rs, col) Get column name by index.
sqlFieldText const char *(*)(rs, col) Get column value as text by index.
sqlFieldByName const char *(*)(rs, name) Get column value as text by name.
sqlFieldInt int32_t (*)(rs, col) Get column value as integer.
sqlFieldDbl double (*)(rs, col) Get column value as double.
sqlFreeResult void (*)(rs) Free a result set.
sqlAffectedRows int32_t (*)(db) Get number of rows affected by last statement.
.endtable
.h2 External Library Callbacks
For DECLARE LIBRARY support. The VM resolves external functions at runtime via the host.
.code
void basVmSetExternCallbacks(BasVmT *vm,
const BasExternCallbacksT *ext);
.endcode
.h3 BasExternCallbacksT
.table
Field Signature Description
----- --------- -----------
resolveExtern void *(*)(ctx, libName, funcName) Resolve a native function by library and symbol name. Cached after first call.
callExtern BasValueT (*)(ctx, funcPtr, funcName, args, argc, retType) Call a resolved native function, marshalling arguments and return value.
ctx void * User pointer passed to both callbacks.
.endtable
.h2 Form Context
Set the active form context during event dispatch.
.code
void basVmSetCurrentForm(BasVmT *vm, void *formRef);
void basVmSetCurrentFormVars(BasVmT *vm,
BasValueT *vars, int32_t count);
.endcode
.h2 Stack Access
Push and pop values on the evaluation stack for host integration.
.code
bool basVmPush(BasVmT *vm, BasValueT val);
bool basVmPop(BasVmT *vm, BasValueT *val);
.endcode
Both return true on success, false on stack overflow/underflow.
.h2 Error Reporting
.code
const char *basVmGetError(const BasVmT *vm);
.endcode
Returns the current error message string. Valid after basVmRun returns BAS_VM_ERROR.
.h2 Sub/Function Calls from Host
Call a SUB or FUNCTION by its code address from host code.
.code
bool basVmCallSub(BasVmT *vm, int32_t codeAddr);
bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr,
const BasValueT *args, int32_t argCount);
bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr,
const BasValueT *args, int32_t argCount,
BasValueT *outArgs, int32_t outCount);
.endcode
.table
Function Description
-------- -----------
basVmCallSub Call a SUB with no arguments.
basVmCallSubWithArgs Call a SUB with arguments pushed onto the stack frame.
basVmCallSubWithArgsOut Call a SUB and read back modified argument values after it returns.
.endtable
All three push a call frame, execute until the SUB returns, then restore the previous execution state. Return true on normal completion, false on error or if the VM is not idle.
.h2 Debugger API
.code
void basVmSetBreakpoints(BasVmT *vm,
int32_t *lines, int32_t count);
void basVmStepInto(BasVmT *vm);
void basVmStepOver(BasVmT *vm);
void basVmStepOut(BasVmT *vm);
void basVmRunToCursor(BasVmT *vm, int32_t line);
int32_t basVmGetCurrentLine(const BasVmT *vm);
.endcode
.table
Function Description
-------- -----------
basVmSetBreakpoints Set the breakpoint list (sorted array of source line numbers, host-owned memory).
basVmStepInto Break at the next OP_LINE instruction.
basVmStepOver Break when call depth returns to the current level.
basVmStepOut Break when call depth drops below the current level.
basVmRunToCursor Break when execution reaches the specified source line.
basVmGetCurrentLine Get the current source line number (from the last OP_LINE instruction).
.endtable
The breakpoint callback notifies the host when a breakpoint fires during nested sub calls:
.code
typedef void (*BasBreakpointFnT)(void *ctx, int32_t line);
.endcode
.h2 BasModuleT
Compiled module produced by the BASIC compiler and loaded into the VM.
.code
typedef struct {
uint8_t *code;
int32_t codeLen;
BasStringT **constants;
int32_t constCount;
int32_t globalCount;
int32_t entryPoint;
BasValueT *dataPool;
int32_t dataCount;
BasProcEntryT *procs;
int32_t procCount;
BasFormVarInfoT *formVarInfo;
int32_t formVarInfoCount;
BasDebugVarT *debugVars;
int32_t debugVarCount;
BasDebugUdtDefT *debugUdtDefs;
int32_t debugUdtDefCount;
} BasModuleT;
.endcode
.table
Field Description
----- -----------
code P-code bytecode array.
codeLen Length of bytecode in bytes.
constants String constant pool.
constCount Number of string constants.
globalCount Number of global variable slots needed.
entryPoint PC of the first instruction (module-level code).
dataPool DATA statement value pool (for READ).
dataCount Number of values in the data pool.
procs Procedure table (SUBs and FUNCTIONs).
procCount Number of procedures.
formVarInfo Per-form variable counts and init code addresses.
formVarInfoCount Number of forms with form-scoped variables.
debugVars Variable names and metadata for the debugger.
debugVarCount Number of debug variable entries.
debugUdtDefs UDT type definitions for the debugger.
debugUdtDefCount Number of debug UDT definitions.
.endtable

View file

@ -0,0 +1,39 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// basEvents.h -- canonical list of BASIC event suffixes
//
// A procedure whose name ends with one of these suffixes is treated as an
// event handler (Ctrl_Load, Form_Click, etc). Referenced by the stripper
// (to retain handlers in release builds), the obfuscator (to preserve event
// naming in rewritten forms), and the IDE (to populate the Object/Event
// dropdowns). Keep them in one place so adding a new event only touches
// one file.
#ifndef BAS_EVENTS_H
#define BAS_EVENTS_H
// NULL-terminated list of event suffixes. Case-insensitive match.
// Declared extern here, defined once in strip.c.
extern const char *basEventSuffixes[];
#endif // BAS_EVENTS_H

View file

@ -0,0 +1,393 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// codegen.c -- DVX BASIC p-code emitter implementation
#include "codegen.h"
#include "symtab.h"
#include "opcodes.h"
#include "thirdparty/stb_ds_wrap.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// Function prototypes (alphabetical)
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len);
bool basAddData(BasCodeGenT *cg, BasValueT val);
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName);
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg);
BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab);
void basCodeGenFree(BasCodeGenT *cg);
void basCodeGenInit(BasCodeGenT *cg);
int32_t basCodePos(const BasCodeGenT *cg);
void basEmit16(BasCodeGenT *cg, int16_t v);
void basEmit8(BasCodeGenT *cg, uint8_t b);
void basEmitDouble(BasCodeGenT *cg, double v);
void basEmitFloat(BasCodeGenT *cg, float v);
void basEmitU16(BasCodeGenT *cg, uint16_t v);
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val);
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) {
// Check if this string is already in the pool
for (int32_t i = 0; i < cg->constCount; i++) {
if (cg->constants[i]->len == len && memcmp(cg->constants[i]->data, text, len) == 0) {
return (uint16_t)i;
}
}
uint16_t idx = (uint16_t)cg->constCount;
BasStringT *s = basStringNew(text, len);
arrput(cg->constants, s);
cg->constCount = (int32_t)arrlen(cg->constants);
return idx;
}
bool basAddData(BasCodeGenT *cg, BasValueT val) {
BasValueT copy = basValCopy(val);
arrput(cg->dataPool, copy);
cg->dataCount = (int32_t)arrlen(cg->dataPool);
return true;
}
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName) {
BasDebugVarT dv;
memset(&dv, 0, sizeof(dv));
snprintf(dv.name, BAS_MAX_PROC_NAME, "%s", name);
dv.scope = scope;
dv.dataType = dataType;
dv.index = index;
dv.procIndex = procIndex;
if (formName && formName[0]) {
snprintf(dv.formName, BAS_MAX_PROC_NAME, "%s", formName);
}
arrput(cg->debugVars, dv);
cg->debugVarCount = (int32_t)arrlen(cg->debugVars);
}
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
BasModuleT *mod = (BasModuleT *)calloc(1, sizeof(BasModuleT));
if (!mod) {
return NULL;
}
// Copy code
mod->code = (uint8_t *)malloc(cg->codeLen);
if (!mod->code) {
free(mod);
return NULL;
}
memcpy(mod->code, cg->code, cg->codeLen);
mod->codeLen = cg->codeLen;
// Copy constant pool (share string refs)
if (cg->constCount > 0) {
mod->constants = (BasStringT **)malloc(cg->constCount * sizeof(BasStringT *));
if (!mod->constants) {
free(mod->code);
free(mod);
return NULL;
}
for (int32_t i = 0; i < cg->constCount; i++) {
mod->constants[i] = basStringRef(cg->constants[i]);
}
}
mod->constCount = cg->constCount;
mod->globalCount = cg->globalCount;
mod->entryPoint = 0;
// Copy data pool
if (cg->dataCount > 0) {
mod->dataPool = (BasValueT *)malloc(cg->dataCount * sizeof(BasValueT));
if (!mod->dataPool) {
free(mod->constants);
free(mod->code);
free(mod);
return NULL;
}
for (int32_t i = 0; i < cg->dataCount; i++) {
mod->dataPool[i] = basValCopy(cg->dataPool[i]);
}
}
mod->dataCount = cg->dataCount;
// Copy form variable info
if (cg->formVarInfoCount > 0) {
mod->formVarInfo = (BasFormVarInfoT *)malloc(cg->formVarInfoCount * sizeof(BasFormVarInfoT));
if (mod->formVarInfo) {
memcpy(mod->formVarInfo, cg->formVarInfo, cg->formVarInfoCount * sizeof(BasFormVarInfoT));
}
mod->formVarInfoCount = cg->formVarInfoCount;
}
// Copy debug variable info
if (cg->debugVarCount > 0) {
mod->debugVars = (BasDebugVarT *)malloc(cg->debugVarCount * sizeof(BasDebugVarT));
if (mod->debugVars) {
memcpy(mod->debugVars, cg->debugVars, cg->debugVarCount * sizeof(BasDebugVarT));
}
mod->debugVarCount = cg->debugVarCount;
}
// Copy UDT type definitions for debugger
if (cg->debugUdtDefCount > 0) {
mod->debugUdtDefs = (BasDebugUdtDefT *)malloc(cg->debugUdtDefCount * sizeof(BasDebugUdtDefT));
if (mod->debugUdtDefs) {
for (int32_t i = 0; i < cg->debugUdtDefCount; i++) {
mod->debugUdtDefs[i] = cg->debugUdtDefs[i];
mod->debugUdtDefs[i].fields = NULL;
if (cg->debugUdtDefs[i].fieldCount > 0 && cg->debugUdtDefs[i].fields) {
mod->debugUdtDefs[i].fields = (BasDebugFieldT *)malloc(
cg->debugUdtDefs[i].fieldCount * sizeof(BasDebugFieldT));
if (mod->debugUdtDefs[i].fields) {
memcpy(mod->debugUdtDefs[i].fields, cg->debugUdtDefs[i].fields,
cg->debugUdtDefs[i].fieldCount * sizeof(BasDebugFieldT));
}
}
}
}
mod->debugUdtDefCount = cg->debugUdtDefCount;
}
return mod;
}
BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
BasModuleT *mod = basCodeGenBuildModule(cg);
if (!mod || !symtab) {
return mod;
}
BasSymTabT *tab = (BasSymTabT *)symtab;
// Count SUB/FUNCTION entries
int32_t procCount = 0;
// Count globals that need runtime type init (currently: STRING).
// This list survives stripping, unlike debugVars.
int32_t globalInitCount = 0;
for (int32_t i = 0; i < tab->count; i++) {
BasSymbolT *s = tab->symbols[i];
if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) {
procCount++;
}
if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) {
globalInitCount++;
}
}
if (globalInitCount > 0) {
mod->globalInits = (BasGlobalInitT *)malloc(globalInitCount * sizeof(BasGlobalInitT));
if (mod->globalInits) {
int32_t gi = 0;
for (int32_t i = 0; i < tab->count; i++) {
BasSymbolT *s = tab->symbols[i];
if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) {
mod->globalInits[gi].index = s->index;
mod->globalInits[gi].dataType = s->dataType;
gi++;
}
}
mod->globalInitCount = gi;
}
}
if (procCount == 0) {
return mod;
}
mod->procs = (BasProcEntryT *)malloc(procCount * sizeof(BasProcEntryT));
if (!mod->procs) {
return mod;
}
int32_t idx = 0;
for (int32_t i = 0; i < tab->count; i++) {
BasSymbolT *s = tab->symbols[i];
if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) {
BasProcEntryT *p = &mod->procs[idx++];
strncpy(p->name, s->name, BAS_MAX_PROC_NAME - 1);
p->name[BAS_MAX_PROC_NAME - 1] = '\0';
strncpy(p->formName, s->formName, BAS_MAX_PROC_NAME - 1);
p->formName[BAS_MAX_PROC_NAME - 1] = '\0';
p->codeAddr = s->codeAddr;
p->paramCount = s->paramCount;
p->localCount = s->localCount;
p->returnType = s->dataType;
p->isFunction = (s->kind == SYM_FUNCTION);
}
}
mod->procCount = idx;
return mod;
}
void basCodeGenFree(BasCodeGenT *cg) {
for (int32_t i = 0; i < cg->constCount; i++) {
basStringUnref(cg->constants[i]);
}
for (int32_t i = 0; i < cg->dataCount; i++) {
basValRelease(&cg->dataPool[i]);
}
arrfree(cg->code);
arrfree(cg->constants);
arrfree(cg->dataPool);
arrfree(cg->formVarInfo);
arrfree(cg->debugVars);
for (int32_t i = 0; i < cg->debugUdtDefCount; i++) {
free(cg->debugUdtDefs[i].fields);
}
arrfree(cg->debugUdtDefs);
cg->code = NULL;
cg->constants = NULL;
cg->dataPool = NULL;
cg->formVarInfo = NULL;
cg->debugVars = NULL;
cg->debugUdtDefs = NULL;
cg->constCount = 0;
cg->dataCount = 0;
cg->codeLen = 0;
cg->formVarInfoCount = 0;
cg->debugVarCount = 0;
cg->debugUdtDefCount = 0;
}
void basCodeGenInit(BasCodeGenT *cg) {
memset(cg, 0, sizeof(*cg));
}
int32_t basCodePos(const BasCodeGenT *cg) {
return cg->codeLen;
}
void basEmit16(BasCodeGenT *cg, int16_t v) {
uint8_t buf[2];
memcpy(buf, &v, 2);
arrput(cg->code, buf[0]);
arrput(cg->code, buf[1]);
cg->codeLen = (int32_t)arrlen(cg->code);
}
void basEmit8(BasCodeGenT *cg, uint8_t b) {
arrput(cg->code, b);
cg->codeLen = (int32_t)arrlen(cg->code);
}
void basEmitDouble(BasCodeGenT *cg, double v) {
uint8_t buf[sizeof(double)];
memcpy(buf, &v, sizeof(double));
for (int32_t i = 0; i < (int32_t)sizeof(double); i++) {
arrput(cg->code, buf[i]);
}
cg->codeLen = (int32_t)arrlen(cg->code);
}
void basEmitFloat(BasCodeGenT *cg, float v) {
uint8_t buf[sizeof(float)];
memcpy(buf, &v, sizeof(float));
for (int32_t i = 0; i < (int32_t)sizeof(float); i++) {
arrput(cg->code, buf[i]);
}
cg->codeLen = (int32_t)arrlen(cg->code);
}
void basEmitU16(BasCodeGenT *cg, uint16_t v) {
uint8_t buf[2];
memcpy(buf, &v, 2);
arrput(cg->code, buf[0]);
arrput(cg->code, buf[1]);
cg->codeLen = (int32_t)arrlen(cg->code);
}
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name) {
if (!mod || !mod->procs || !name) {
return NULL;
}
for (int32_t i = 0; i < mod->procCount; i++) {
if (strcasecmp(mod->procs[i].name, name) == 0) {
return &mod->procs[i];
}
}
return NULL;
}
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val) {
if (pos >= 0 && pos + 2 <= cg->codeLen) {
memcpy(&cg->code[pos], &val, 2);
}
}

View file

@ -0,0 +1,114 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// codegen.h -- DVX BASIC p-code emitter
//
// Builds a p-code byte stream and string constant pool from
// calls made by the parser. Provides helpers for backpatching
// forward jumps.
//
// Embeddable: no DVX dependencies, pure C.
#ifndef DVXBASIC_CODEGEN_H
#define DVXBASIC_CODEGEN_H
#include "../runtime/vm.h"
#include "../runtime/values.h"
#include <stdint.h>
#include <stdbool.h>
// ============================================================
// Code generator state
// ============================================================
typedef struct {
uint8_t *code; // stb_ds dynamic array
int32_t codeLen;
BasStringT **constants; // stb_ds dynamic array
int32_t constCount;
int32_t globalCount;
BasValueT *dataPool; // stb_ds dynamic array
int32_t dataCount;
BasFormVarInfoT *formVarInfo; // stb_ds dynamic array
int32_t formVarInfoCount;
BasDebugVarT *debugVars; // stb_ds dynamic array
int32_t debugVarCount;
int32_t debugProcCount; // incremented per SUB/FUNCTION for debug var tracking
BasDebugUdtDefT *debugUdtDefs; // stb_ds dynamic array
int32_t debugUdtDefCount;
} BasCodeGenT;
// ============================================================
// API
// ============================================================
void basCodeGenInit(BasCodeGenT *cg);
void basCodeGenFree(BasCodeGenT *cg);
// Emit single byte
void basEmit8(BasCodeGenT *cg, uint8_t b);
// Emit 16-bit signed value
void basEmit16(BasCodeGenT *cg, int16_t v);
// Emit 16-bit unsigned value
void basEmitU16(BasCodeGenT *cg, uint16_t v);
// Emit 32-bit float
void basEmitFloat(BasCodeGenT *cg, float v);
// Emit 64-bit double
void basEmitDouble(BasCodeGenT *cg, double v);
// Get current code position (for jump targets)
int32_t basCodePos(const BasCodeGenT *cg);
// Patch a 16-bit value at a previous position (for backpatching jumps)
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val);
// Add a string to the constant pool. Returns the pool index.
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len);
// Add a value to the data pool (for DATA statements). Returns true on success.
bool basAddData(BasCodeGenT *cg, BasValueT val);
// Build a BasModuleT from the generated code. The caller takes
// ownership of the module and must free it with basModuleFree().
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg);
// Build a module with procedure table from parser symbol table.
// symtab is a BasSymTabT* (cast to void* to avoid circular include).
BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab);
// Free a module built by basCodeGenBuildModule.
void basModuleFree(BasModuleT *mod);
// Add a debug variable entry (for debugger variable display).
// formName is only used for SCOPE_FORM vars (NULL or "" for others).
void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName);
// Find a procedure by name in a module's procedure table.
// Case-insensitive. Returns NULL if not found.
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
#endif // DVXBASIC_CODEGEN_H

View file

@ -0,0 +1,604 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// 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>
// Function prototypes (alphabetical)
int32_t basCompactBytecode(BasModuleT *mod);
static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, int32_t *outNewLen);
static bool isGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos);
static int32_t opOperandSize(uint8_t op);
static int16_t readI16LE(const uint8_t *p);
static int32_t readI32LE(const uint8_t *p);
static uint16_t readU16LE(const uint8_t *p);
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t newCodeLen);
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);
static void writeI16LE(uint8_t *p, int16_t v);
static void writeI32LE(uint8_t *p, int32_t v);
static void writeU16LE(uint8_t *p, uint16_t v);
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_FOR_INIT: {
// [uint16 varIdx] [uint8 scope] [int16 skipOffset]
int16_t oldOff = readI16LE(oldCode + oldPc + 4);
if (!remapRelI16(newCode, newPc, 4, oldOff,
oldPc + 6, newPc + 6,
remap, oldCodeLen, newCodeLen, false)) {
ok = false;
}
break;
}
case OP_ON_ERROR: {
int16_t oldOff = readI16LE(oldCode + oldPc + 1);
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;
}
// ============================================================
// 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;
}
// ============================================================
// 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_STR_OCT: case OP_CONV_BOOL:
case OP_MATH_TIMER: case OP_DATE_STR: case OP_TIME_STR:
case OP_SLEEP: case OP_ENVIRON:
case OP_READ_DATA: case OP_RESTORE:
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_PUSH_ARR_ADDR:
case OP_PRINT_SPC: case OP_FILE_OPEN:
case OP_CALL_METHOD: case OP_SHOW_FORM:
case OP_LBOUND: case OP_UBOUND:
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:
return 3;
case OP_PUSH_INT32: case OP_PUSH_FLT32:
case OP_CALL:
return 4;
case OP_FOR_INIT:
case OP_FOR_NEXT:
return 5;
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 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 uint16_t readU16LE(const uint8_t *p) {
return (uint16_t)p[0] | ((uint16_t)p[1] << 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 < INT16_MIN || newOffset > INT16_MAX) {
return false;
}
writeI16LE(newCode + newOpPos + operandOffset, (int16_t)newOffset);
return true;
}
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 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);
}
static void writeU16LE(uint8_t *p, uint16_t v) {
p[0] = (uint8_t)(v & 0xFF);
p[1] = (uint8_t)((v >> 8) & 0xFF);
}

View file

@ -0,0 +1,43 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// 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

@ -0,0 +1,897 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// lexer.c -- DVX BASIC lexer implementation
//
// Single-pass tokenizer. Keywords are case-insensitive. Identifiers
// preserve their original case for display but comparisons are
// case-insensitive. Line continuations (underscore at end of line)
// are handled transparently.
#include "lexer.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// Keyword table
// ============================================================
typedef struct {
const char *text;
uint8_t textLen; // precomputed so lookupKeyword() can length-reject cheaply
BasTokenTypeE type;
} KeywordEntryT;
#define KW(s, t) { s, (uint8_t)(sizeof(s) - 1), t }
static const KeywordEntryT sKeywords[] = {
KW("AND", TOK_AND),
KW("APP", TOK_APP),
KW("APPEND", TOK_APPEND),
KW("AS", TOK_AS),
KW("BASE", TOK_BASE),
KW("BINARY", TOK_BINARY),
KW("BOOLEAN", TOK_BOOLEAN),
KW("BYVAL", TOK_BYVAL),
KW("CALL", TOK_CALL),
KW("CASE", TOK_CASE),
KW("CHDIR", TOK_CHDIR),
KW("CHDRIVE", TOK_CHDRIVE),
KW("CLOSE", TOK_CLOSE),
KW("CREATECONTROL", TOK_CREATECONTROL),
KW("CREATEFORM", TOK_CREATEFORM),
KW("CURDIR", TOK_CURDIR),
KW("CURDIR$", TOK_CURDIR),
KW("CONST", TOK_CONST),
KW("DATA", TOK_DATA),
KW("DECLARE", TOK_DECLARE),
KW("DEF", TOK_DEF),
KW("DEFDBL", TOK_DEFDBL),
KW("DEFINT", TOK_DEFINT),
KW("DEFLNG", TOK_DEFLNG),
KW("DEFSNG", TOK_DEFSNG),
KW("DEFSTR", TOK_DEFSTR),
KW("DIM", TOK_DIM),
KW("DIR", TOK_DIR),
KW("DIR$", TOK_DIR),
KW("DO", TOK_DO),
KW("DOEVENTS", TOK_DOEVENTS),
KW("DOUBLE", TOK_DOUBLE),
KW("ELSE", TOK_ELSE),
KW("ELSEIF", TOK_ELSEIF),
KW("END", TOK_END),
KW("EOF", TOK_EOF_KW),
KW("EQV", TOK_EQV),
KW("ERASE", TOK_ERASE),
KW("ERR", TOK_ERR),
KW("ERROR", TOK_ERROR_KW),
KW("EXPLICIT", TOK_EXPLICIT),
KW("EXIT", TOK_EXIT),
KW("FALSE", TOK_FALSE_KW),
KW("FILECOPY", TOK_FILECOPY),
KW("FILELEN", TOK_FILELEN),
KW("FOR", TOK_FOR),
KW("FUNCTION", TOK_FUNCTION),
KW("GET", TOK_GET),
KW("GETATTR", TOK_GETATTR),
KW("GOSUB", TOK_GOSUB),
KW("GOTO", TOK_GOTO),
KW("HIDE", TOK_HIDE),
KW("IF", TOK_IF),
KW("IMP", TOK_IMP),
KW("INIREAD", TOK_INIREAD),
KW("INIREAD$", TOK_INIREAD),
KW("INIWRITE", TOK_INIWRITE),
KW("INPUT", TOK_INPUT),
KW("INTEGER", TOK_INTEGER),
KW("IS", TOK_IS),
KW("KILL", TOK_KILL),
KW("LBOUND", TOK_LBOUND),
KW("LET", TOK_LET),
KW("LINE", TOK_LINE),
KW("LOAD", TOK_LOAD),
KW("LONG", TOK_LONG),
KW("LOOP", TOK_LOOP),
KW("ME", TOK_ME),
KW("MKDIR", TOK_MKDIR),
KW("MOD", TOK_MOD),
KW("INPUTBOX", TOK_INPUTBOX),
KW("INPUTBOX$", TOK_INPUTBOX),
KW("MSGBOX", TOK_MSGBOX),
KW("NAME", TOK_NAME),
KW("NEXT", TOK_NEXT),
KW("NOT", TOK_NOT),
KW("NOTHING", TOK_NOTHING),
KW("ON", TOK_ON),
KW("OPEN", TOK_OPEN),
KW("OPTIONAL", TOK_OPTIONAL),
KW("OPTION", TOK_OPTION),
KW("OR", TOK_OR),
KW("OUTPUT", TOK_OUTPUT),
KW("PRESERVE", TOK_PRESERVE),
KW("PRINT", TOK_PRINT),
KW("PUT", TOK_PUT),
KW("RANDOM", TOK_RANDOM),
KW("RANDOMIZE", TOK_RANDOMIZE),
KW("READ", TOK_READ),
KW("REDIM", TOK_REDIM),
KW("REM", TOK_REM),
KW("REMOVECONTROL", TOK_REMOVECONTROL),
KW("RESTORE", TOK_RESTORE),
KW("RESUME", TOK_RESUME),
KW("RETURN", TOK_RETURN),
KW("RMDIR", TOK_RMDIR),
KW("SEEK", TOK_SEEK),
KW("SELECT", TOK_SELECT),
KW("SET", TOK_SET),
KW("SETATTR", TOK_SETATTR),
KW("SETEVENT", TOK_SETEVENT),
KW("SHARED", TOK_SHARED),
KW("SHELL", TOK_SHELL),
KW("SHOW", TOK_SHOW),
KW("SINGLE", TOK_SINGLE),
KW("SLEEP", TOK_SLEEP),
KW("STATIC", TOK_STATIC),
KW("STEP", TOK_STEP),
KW("STRING", TOK_STRING_KW),
KW("SUB", TOK_SUB),
KW("SWAP", TOK_SWAP),
KW("THEN", TOK_THEN),
KW("TIMER", TOK_TIMER),
KW("TO", TOK_TO),
KW("TRUE", TOK_TRUE_KW),
KW("TYPE", TOK_TYPE),
KW("UBOUND", TOK_UBOUND),
KW("UNLOAD", TOK_UNLOAD),
KW("UNTIL", TOK_UNTIL),
KW("WEND", TOK_WEND),
KW("WHILE", TOK_WHILE),
KW("WITH", TOK_WITH),
KW("WRITE", TOK_WRITE),
KW("XOR", TOK_XOR),
{ NULL, 0, TOK_ERROR }
};
#undef KW
#define KEYWORD_COUNT (sizeof(sKeywords) / sizeof(sKeywords[0]) - 1)
// Function prototypes (alphabetical)
static char advance(BasLexerT *lex);
static bool atEnd(const BasLexerT *lex);
void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen);
const char *basLexerKeywordAt(int32_t i);
BasKeywordClassE basLexerKeywordClass(int32_t i);
int32_t basLexerKeywordCount(void);
BasTokenTypeE basLexerNext(BasLexerT *lex);
BasTokenTypeE basLexerPeek(const BasLexerT *lex);
const char *basTokenName(BasTokenTypeE type);
static BasTokenTypeE lookupKeyword(const char *text, int32_t len);
static char peek(const BasLexerT *lex);
static char peekNext(const BasLexerT *lex);
static void setError(BasLexerT *lex, const char *msg);
static void skipLineComment(BasLexerT *lex);
static void skipWhitespace(BasLexerT *lex);
static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex);
static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex);
static BasTokenTypeE tokenizeNumber(BasLexerT *lex);
static BasTokenTypeE tokenizeString(BasLexerT *lex);
static char upperChar(char c);
static char advance(BasLexerT *lex) {
if (atEnd(lex)) {
return '\0';
}
char c = lex->source[lex->pos++];
if (c == '\n') {
lex->line++;
lex->col = 1;
} else {
lex->col++;
}
return c;
}
static bool atEnd(const BasLexerT *lex) {
return lex->pos >= lex->sourceLen;
}
void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen) {
memset(lex, 0, sizeof(*lex));
lex->source = source;
lex->sourceLen = (sourceLen < 0) ? (int32_t)strlen(source) : sourceLen;
lex->pos = 0;
lex->line = 1;
lex->col = 1;
// Prime the first token
basLexerNext(lex);
}
const char *basLexerKeywordAt(int32_t i) {
if (i < 0 || i >= (int32_t)KEYWORD_COUNT) {
return NULL;
}
return sKeywords[i].text;
}
BasKeywordClassE basLexerKeywordClass(int32_t i) {
if (i < 0 || i >= (int32_t)KEYWORD_COUNT) {
return BAS_KW_CLASS_OTHER;
}
switch (sKeywords[i].type) {
case TOK_BOOLEAN:
case TOK_DOUBLE:
case TOK_INTEGER:
case TOK_LONG:
case TOK_SINGLE:
case TOK_STRING_KW:
return BAS_KW_CLASS_TYPE;
case TOK_TRUE_KW:
case TOK_FALSE_KW:
case TOK_NOTHING:
return BAS_KW_CLASS_LITERAL;
default:
return BAS_KW_CLASS_OTHER;
}
}
int32_t basLexerKeywordCount(void) {
return (int32_t)KEYWORD_COUNT;
}
BasTokenTypeE basLexerNext(BasLexerT *lex) {
skipWhitespace(lex);
lex->token.line = lex->line;
lex->token.col = lex->col;
lex->token.textLen = 0;
lex->token.text[0] = '\0';
if (atEnd(lex)) {
lex->token.type = TOK_EOF;
return TOK_EOF;
}
char c = peek(lex);
// Newline
if (c == '\n') {
advance(lex);
lex->token.type = TOK_NEWLINE;
lex->token.text[0] = '\n';
lex->token.text[1] = '\0';
lex->token.textLen = 1;
return TOK_NEWLINE;
}
// Carriage return (handle CR, CRLF)
if (c == '\r') {
advance(lex);
if (!atEnd(lex) && peek(lex) == '\n') {
advance(lex);
}
lex->token.type = TOK_NEWLINE;
lex->token.text[0] = '\n';
lex->token.text[1] = '\0';
lex->token.textLen = 1;
return TOK_NEWLINE;
}
// Comment (apostrophe)
if (c == '\'') {
skipLineComment(lex);
lex->token.type = TOK_NEWLINE;
lex->token.text[0] = '\n';
lex->token.text[1] = '\0';
lex->token.textLen = 1;
return TOK_NEWLINE;
}
// String literal
if (c == '"') {
lex->token.type = tokenizeString(lex);
return lex->token.type;
}
// Number
if (isdigit((unsigned char)c) || (c == '.' && isdigit((unsigned char)peekNext(lex)))) {
lex->token.type = tokenizeNumber(lex);
return lex->token.type;
}
// Numeric-base literals: &H hex, &O octal, &B binary. &B is an
// extension beyond classic QBASIC; it's convenient for bitmask
// work in the widget/graphics code.
if (c == '&') {
char n = upperChar(peekNext(lex));
if (n == 'H' || n == 'O' || n == 'B') {
lex->token.type = tokenizeHexLiteral(lex);
return lex->token.type;
}
}
// Identifier or keyword
if (isalpha((unsigned char)c) || c == '_') {
lex->token.type = tokenizeIdentOrKeyword(lex);
return lex->token.type;
}
// Single and multi-character operators/punctuation
advance(lex);
switch (c) {
case '+':
lex->token.type = TOK_PLUS;
break;
case '-':
lex->token.type = TOK_MINUS;
break;
case '*':
lex->token.type = TOK_STAR;
break;
case '/':
lex->token.type = TOK_SLASH;
break;
case '\\':
lex->token.type = TOK_BACKSLASH;
break;
case '^':
lex->token.type = TOK_CARET;
break;
case '&':
lex->token.type = TOK_AMPERSAND;
break;
case '(':
lex->token.type = TOK_LPAREN;
break;
case ')':
lex->token.type = TOK_RPAREN;
break;
case ',':
lex->token.type = TOK_COMMA;
break;
case ';':
lex->token.type = TOK_SEMICOLON;
break;
case ':':
lex->token.type = TOK_COLON;
break;
case '.':
lex->token.type = TOK_DOT;
break;
case '#':
lex->token.type = TOK_HASH;
break;
case '?':
lex->token.type = TOK_PRINT;
break;
case '=':
lex->token.type = TOK_EQ;
break;
case '<':
if (!atEnd(lex) && peek(lex) == '>') {
advance(lex);
lex->token.type = TOK_NE;
} else if (!atEnd(lex) && peek(lex) == '=') {
advance(lex);
lex->token.type = TOK_LE;
} else {
lex->token.type = TOK_LT;
}
break;
case '>':
if (!atEnd(lex) && peek(lex) == '=') {
advance(lex);
lex->token.type = TOK_GE;
} else {
lex->token.type = TOK_GT;
}
break;
default:
setError(lex, "Unexpected character");
lex->token.type = TOK_ERROR;
break;
}
// Store the operator text
if (lex->token.type != TOK_ERROR) {
lex->token.text[0] = c;
lex->token.textLen = 1;
if (lex->token.type == TOK_NE || lex->token.type == TOK_LE || lex->token.type == TOK_GE) {
lex->token.text[1] = lex->source[lex->pos - 1];
lex->token.textLen = 2;
}
lex->token.text[lex->token.textLen] = '\0';
}
return lex->token.type;
}
BasTokenTypeE basLexerPeek(const BasLexerT *lex) {
return lex->token.type;
}
const char *basTokenName(BasTokenTypeE type) {
switch (type) {
case TOK_INT_LIT: return "integer";
case TOK_LONG_LIT: return "long";
case TOK_FLOAT_LIT: return "float";
case TOK_STRING_LIT: return "string";
case TOK_IDENT: return "identifier";
case TOK_DOT: return "'.'";
case TOK_COMMA: return "','";
case TOK_SEMICOLON: return "';'";
case TOK_COLON: return "':'";
case TOK_LPAREN: return "'('";
case TOK_RPAREN: return "')'";
case TOK_HASH: return "'#'";
case TOK_PLUS: return "'+'";
case TOK_MINUS: return "'-'";
case TOK_STAR: return "'*'";
case TOK_SLASH: return "'/'";
case TOK_BACKSLASH: return "'\\'";
case TOK_CARET: return "'^'";
case TOK_AMPERSAND: return "'&'";
case TOK_EQ: return "'='";
case TOK_NE: return "'<>'";
case TOK_LT: return "'<'";
case TOK_GT: return "'>'";
case TOK_LE: return "'<='";
case TOK_GE: return "'>='";
case TOK_NEWLINE: return "newline";
case TOK_EOF: return "end of file";
case TOK_ERROR: return "error";
default: break;
}
// Keywords
for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) {
if (sKeywords[i].type == type) {
return sKeywords[i].text;
}
}
return "?";
}
static BasTokenTypeE lookupKeyword(const char *text, int32_t len) {
// Case-insensitive keyword lookup. Short-circuits on length mismatch
// (via cached keyword length) and on the very first character, both of
// which reject the vast majority of entries before doing a full scan.
char firstUp = (text[0] >= 'a' && text[0] <= 'z') ? (char)(text[0] - 32) : text[0];
for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) {
const KeywordEntryT *kw = &sKeywords[i];
if (kw->textLen != len || kw->text[0] != firstUp) {
continue;
}
bool match = true;
for (int32_t j = 1; j < len; j++) {
if (upperChar(text[j]) != kw->text[j]) {
match = false;
break;
}
}
if (match) {
return kw->type;
}
}
return TOK_IDENT;
}
static char peek(const BasLexerT *lex) {
if (atEnd(lex)) {
return '\0';
}
return lex->source[lex->pos];
}
static char peekNext(const BasLexerT *lex) {
if (lex->pos + 1 >= lex->sourceLen) {
return '\0';
}
return lex->source[lex->pos + 1];
}
static void setError(BasLexerT *lex, const char *msg) {
snprintf(lex->error, sizeof(lex->error), "Line %d, Col %d: %s", (int)lex->line, (int)lex->col, msg);
}
static void skipLineComment(BasLexerT *lex) {
while (!atEnd(lex) && peek(lex) != '\n' && peek(lex) != '\r') {
advance(lex);
}
}
//
// Skips spaces and tabs. Does NOT skip newlines (they are tokens).
// Handles line continuation: underscore followed by newline joins
// the next line to the current logical line.
static void skipWhitespace(BasLexerT *lex) {
while (!atEnd(lex)) {
char c = peek(lex);
if (c == ' ' || c == '\t') {
advance(lex);
continue;
}
// Line continuation: _ at end of line
if (c == '_') {
int32_t savedPos = lex->pos;
int32_t savedLine = lex->line;
int32_t savedCol = lex->col;
advance(lex);
// Skip spaces/tabs after underscore
while (!atEnd(lex) && (peek(lex) == ' ' || peek(lex) == '\t')) {
advance(lex);
}
// Must be followed by newline
if (!atEnd(lex) && (peek(lex) == '\n' || peek(lex) == '\r')) {
advance(lex);
if (!atEnd(lex) && peek(lex) == '\n' && lex->source[lex->pos - 1] == '\r') {
advance(lex);
}
continue; // Continue skipping whitespace on next line
}
// Not a continuation -- put back
lex->pos = savedPos;
lex->line = savedLine;
lex->col = savedCol;
break;
}
break;
}
}
static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
advance(lex); // skip &
char base = upperChar(peek(lex));
advance(lex); // skip H/O/B
int32_t shift;
int32_t maxDigit;
if (base == 'O') {
shift = 3;
maxDigit = 7;
} else if (base == 'B') {
shift = 1;
maxDigit = 1;
} else {
shift = 4;
maxDigit = 15;
}
int32_t idx = 0;
int32_t value = 0;
for (;;) {
if (atEnd(lex)) {
break;
}
char c = peek(lex);
int32_t digit;
if (c >= '0' && c <= '9') {
digit = c - '0';
} else if (shift == 4 && c >= 'A' && c <= 'F') {
digit = c - 'A' + 10;
} else if (shift == 4 && c >= 'a' && c <= 'f') {
digit = c - 'a' + 10;
} else {
break;
}
if (digit > maxDigit) {
break;
}
advance(lex);
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = c;
}
value = (value << shift) | digit;
}
lex->token.text[idx] = '\0';
lex->token.textLen = idx;
// Check for trailing & (long suffix)
if (!atEnd(lex) && peek(lex) == '&') {
advance(lex);
lex->token.longVal = (int64_t)value;
return TOK_LONG_LIT;
}
lex->token.intVal = value;
return TOK_INT_LIT;
}
static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
int32_t idx = 0;
while (!atEnd(lex) && (isalnum((unsigned char)peek(lex)) || peek(lex) == '_')) {
char c = advance(lex);
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = c;
}
}
lex->token.text[idx] = '\0';
lex->token.textLen = idx;
// Check for type suffix
if (!atEnd(lex)) {
char c = peek(lex);
if (c == '%' || c == '&' || c == '!' || c == '#' || c == '$') {
advance(lex);
lex->token.text[idx++] = c;
lex->token.text[idx] = '\0';
lex->token.textLen = idx;
}
}
// Check if this is a keyword
// For suffix-bearing identifiers, only check the base (without suffix)
int32_t baseLen = idx;
if (baseLen > 0) {
char last = lex->token.text[baseLen - 1];
if (last == '%' || last == '&' || last == '!' || last == '#' || last == '$') {
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
if (kwType == TOK_REM) {
skipLineComment(lex);
lex->token.type = TOK_NEWLINE;
lex->token.text[0] = '\n';
lex->token.text[1] = '\0';
lex->token.textLen = 1;
return TOK_NEWLINE;
}
// Accept the keyword if it's a plain keyword (no suffix on source) or
// if it explicitly matched a $-suffixed entry in the keyword table.
if (kwType != TOK_IDENT && (baseLen == idx || matchedWithSuffix)) {
return kwType;
}
return TOK_IDENT;
}
static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
int32_t idx = 0;
bool hasDecimal = false;
bool hasExp = false;
// Integer part
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = advance(lex);
} else {
advance(lex);
}
}
// Decimal part
if (!atEnd(lex) && peek(lex) == '.' && isdigit((unsigned char)peekNext(lex))) {
hasDecimal = true;
lex->token.text[idx++] = advance(lex); // .
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = advance(lex);
} else {
advance(lex);
}
}
}
// Exponent
if (!atEnd(lex) && (upperChar(peek(lex)) == 'E' || upperChar(peek(lex)) == 'D')) {
hasExp = true;
lex->token.text[idx++] = advance(lex);
if (!atEnd(lex) && (peek(lex) == '+' || peek(lex) == '-')) {
lex->token.text[idx++] = advance(lex);
}
while (!atEnd(lex) && isdigit((unsigned char)peek(lex))) {
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = advance(lex);
} else {
advance(lex);
}
}
}
lex->token.text[idx] = '\0';
lex->token.textLen = idx;
// Check for type suffix
if (!atEnd(lex)) {
char c = peek(lex);
if (c == '%') {
advance(lex);
lex->token.intVal = (int32_t)atoi(lex->token.text);
return TOK_INT_LIT;
}
if (c == '&') {
advance(lex);
lex->token.longVal = (int64_t)atol(lex->token.text);
return TOK_LONG_LIT;
}
if (c == '!') {
advance(lex);
lex->token.dblVal = atof(lex->token.text);
return TOK_FLOAT_LIT;
}
if (c == '#') {
advance(lex);
lex->token.dblVal = atof(lex->token.text);
return TOK_FLOAT_LIT;
}
}
// No suffix: determine type from content
if (hasDecimal || hasExp) {
lex->token.dblVal = atof(lex->token.text);
return TOK_FLOAT_LIT;
}
long val = atol(lex->token.text);
if (val >= INT16_MIN && val <= INT16_MAX) {
lex->token.intVal = (int32_t)val;
return TOK_INT_LIT;
}
lex->token.longVal = (int64_t)val;
return TOK_LONG_LIT;
}
static BasTokenTypeE tokenizeString(BasLexerT *lex) {
advance(lex); // skip opening quote
int32_t idx = 0;
while (!atEnd(lex) && peek(lex) != '"' && peek(lex) != '\n' && peek(lex) != '\r') {
if (idx < BAS_MAX_TOKEN_LEN - 1) {
lex->token.text[idx++] = advance(lex);
} else {
advance(lex);
}
}
if (atEnd(lex) || peek(lex) != '"') {
setError(lex, "Unterminated string literal");
lex->token.text[idx] = '\0';
lex->token.textLen = idx;
return TOK_ERROR;
}
advance(lex); // skip closing quote
lex->token.text[idx] = '\0';
lex->token.textLen = idx;
return TOK_STRING_LIT;
}
static char upperChar(char c) {
if (c >= 'a' && c <= 'z') {
return c - 32;
}
return c;
}

View file

@ -0,0 +1,293 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// lexer.h -- DVX BASIC lexer (tokenizer)
//
// Converts BASIC source text into a stream of tokens. Case-insensitive
// for keywords. Handles line continuations (_), comments (' and REM),
// type suffixes (%, &, !, #, $), and string literals.
//
// Embeddable: no DVX dependencies, pure C.
#ifndef DVXBASIC_LEXER_H
#define DVXBASIC_LEXER_H
#include <stdint.h>
#include <stdbool.h>
// ============================================================
// Token types
// ============================================================
typedef enum {
// Literals
TOK_INT_LIT, // integer literal (123, &HFF)
TOK_LONG_LIT, // long literal (123&)
TOK_FLOAT_LIT, // float literal (3.14, 1.5E10)
TOK_STRING_LIT, // "string literal"
// Identifiers and symbols
TOK_IDENT, // variable/function name
TOK_DOT, // .
TOK_COMMA, // ,
TOK_SEMICOLON, // ;
TOK_COLON, // :
TOK_LPAREN, // (
TOK_RPAREN, // )
TOK_HASH, // # (file channel)
// Operators
TOK_PLUS, // +
TOK_MINUS, // -
TOK_STAR, // *
TOK_SLASH, // /
TOK_BACKSLASH, // \ (integer divide)
TOK_CARET, // ^
TOK_AMPERSAND, // & (string concat or hex prefix)
TOK_EQ, // =
TOK_NE, // <>
TOK_LT, // <
TOK_GT, // >
TOK_LE, // <=
TOK_GE, // >=
// Type suffixes (attached to identifier)
TOK_SUFFIX_INT, // %
TOK_SUFFIX_LONG, // &
TOK_SUFFIX_SINGLE, // !
TOK_SUFFIX_DOUBLE, // #
TOK_SUFFIX_STRING, // $
// Keywords
TOK_AND,
TOK_APP,
TOK_AS,
TOK_BASE,
TOK_BOOLEAN,
TOK_BYVAL,
TOK_CALL,
TOK_CASE,
TOK_CLOSE,
TOK_CONST,
TOK_CREATECONTROL,
TOK_CREATEFORM,
TOK_DATA,
TOK_DECLARE,
TOK_DEF,
TOK_DEFDBL,
TOK_DEFINT,
TOK_DEFLNG,
TOK_DEFSNG,
TOK_DEFSTR,
TOK_DIM,
TOK_DO,
TOK_DOEVENTS,
TOK_DOUBLE,
TOK_ELSE,
TOK_ELSEIF,
TOK_END,
TOK_EOF_KW, // EOF (keyword, not end-of-file)
TOK_EQV,
TOK_ERASE,
TOK_ERR,
TOK_ERROR_KW,
TOK_EXPLICIT,
TOK_EXIT,
TOK_FALSE_KW,
TOK_FOR,
TOK_FUNCTION,
TOK_GET,
TOK_GOSUB,
TOK_GOTO,
TOK_HIDE,
TOK_IF,
TOK_IMP,
TOK_INPUT,
TOK_INTEGER,
TOK_IS,
TOK_LBOUND,
TOK_LET,
TOK_LINE,
TOK_LOAD,
TOK_LONG,
TOK_LOOP,
TOK_ME,
TOK_MOD,
TOK_INPUTBOX,
TOK_MSGBOX,
TOK_NEXT,
TOK_NOT,
TOK_NOTHING,
TOK_ON,
TOK_OPEN,
TOK_OPTIONAL,
TOK_OPTION,
TOK_OR,
TOK_OUTPUT,
TOK_PRESERVE,
TOK_PRINT,
TOK_PUT,
TOK_RANDOMIZE,
TOK_READ,
TOK_REDIM,
TOK_REM,
TOK_REMOVECONTROL,
TOK_RESTORE,
TOK_RESUME,
TOK_RETURN,
TOK_SEEK,
TOK_SELECT,
TOK_SET,
TOK_SETEVENT,
TOK_SHARED,
TOK_SHELL,
TOK_SHOW,
TOK_SINGLE,
TOK_SLEEP,
TOK_INIREAD,
TOK_INIWRITE,
TOK_STATIC,
TOK_STEP,
TOK_STRING_KW,
TOK_SUB,
TOK_SWAP,
TOK_THEN,
TOK_TIMER,
TOK_TO,
TOK_TRUE_KW,
TOK_TYPE,
TOK_UBOUND,
TOK_UNLOAD,
TOK_UNTIL,
TOK_WEND,
TOK_WHILE,
TOK_WITH,
TOK_WRITE,
TOK_XOR,
// Filesystem keywords
TOK_CHDIR,
TOK_CHDRIVE,
TOK_CURDIR,
TOK_DIR,
TOK_FILECOPY,
TOK_FILELEN,
TOK_GETATTR,
TOK_KILL,
TOK_MKDIR,
TOK_NAME,
TOK_RMDIR,
TOK_SETATTR,
// File modes
TOK_APPEND,
TOK_BINARY,
TOK_RANDOM,
// Special
TOK_NEWLINE, // end of logical line
TOK_EOF, // end of source
TOK_ERROR // lexer error
} BasTokenTypeE;
// ============================================================
// Token
// ============================================================
#define BAS_MAX_TOKEN_LEN 256
#define BAS_LEX_ERROR_LEN 256
typedef struct {
BasTokenTypeE type;
int32_t line; // 1-based source line number
int32_t col; // 1-based column number
// Value (depends on type)
union {
int32_t intVal;
int64_t longVal;
float fltVal;
double dblVal;
};
char text[BAS_MAX_TOKEN_LEN]; // raw text of the token
int32_t textLen;
} BasTokenT;
// ============================================================
// Lexer state
// ============================================================
typedef struct {
const char *source; // source text (not owned)
int32_t sourceLen;
int32_t pos; // current position in source
int32_t line; // current line (1-based)
int32_t col; // current column (1-based)
BasTokenT token; // current token
char error[BAS_LEX_ERROR_LEN];
} BasLexerT;
// ============================================================
// API
// ============================================================
// Initialize lexer with source text. The source must remain valid
// for the lifetime of the lexer.
void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen);
// Advance to the next token. Returns the token type.
// The token is available in lex->token.
BasTokenTypeE basLexerNext(BasLexerT *lex);
// Peek at the current token type without advancing.
BasTokenTypeE basLexerPeek(const BasLexerT *lex);
// Return human-readable name for a token type.
const char *basTokenName(BasTokenTypeE type);
// ============================================================
// Keyword iteration
// ============================================================
//
// The lexer's internal keyword table is the one source of truth.
// Other modules (e.g. the IDE syntax highlighter) iterate through
// it here instead of keeping their own parallel list.
typedef enum {
BAS_KW_CLASS_OTHER = 0, // control-flow / statement / operator keyword
BAS_KW_CLASS_TYPE = 1, // INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
BAS_KW_CLASS_LITERAL = 2 // TRUE, FALSE, NOTHING
} BasKeywordClassE;
// Number of keywords in the table (excluding the trailing NULL sentinel
// and any duplicate dollar-suffixed aliases like DIR$/DIR).
int32_t basLexerKeywordCount(void);
// Return the text of keyword i (uppercase). i must be in [0, count).
// Duplicates such as DIR and DIR$ are both returned at their own index.
const char *basLexerKeywordAt(int32_t i);
// Classify the keyword at index i.
BasKeywordClassE basLexerKeywordClass(int32_t i);
#endif // DVXBASIC_LEXER_H

View file

@ -0,0 +1,625 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// obfuscate.c -- Release build name obfuscation
//
// See obfuscate.h for the high-level description.
#include "obfuscate.h"
#include "basEvents.h"
#include "../runtime/values.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// 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;
// Function prototypes (alphabetical)
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms);
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap);
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *map);
static int32_t findFormEndPos(const char *text, int32_t len);
static bool isEventSuffix(const char *suffix);
static bool isIdentChar(int c);
static bool isValidIdent(const char *name);
static const char *nameMapAdd(NameMapT *m, const char *name);
static void nameMapFree(NameMapT *m);
static void nameMapInit(NameMapT *m);
static const char *nameMapLookup(const NameMapT *m, const char *name);
static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize);
static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText);
static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap);
static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map);
static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map);
static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map);
static const char *skipWhitespace(const char *p, const char *end);
// ============================================================
// 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);
}
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;
}
// Strip leading whitespace -- the form parser trims per line
// anyway, so shipping indentation just bloats the embedded resource.
int32_t writeLen = contentEnd - firstNonWs;
if (outLen + writeLen + 1 >= outCap) {
break;
}
memcpy(outBuf + outLen, src + firstNonWs, writeLen);
outLen += writeLen;
outBuf[outLen++] = '\n';
}
return outLen;
}
// ============================================================
// 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;
}
// Check if suffix is a known event name.
static bool isEventSuffix(const char *suffix) {
for (int32_t i = 0; basEventSuffixes[i]; i++) {
if (strcasecmp(suffix, basEventSuffixes[i]) == 0) {
return true;
}
}
return false;
}
// ============================================================
// 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 == '_';
}
// 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;
}
// 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;
}
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;
}
static void nameMapInit(NameMapT *m) {
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;
}
// 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;
}
// ============================================================
// 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;
}
// 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;
}
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);
}
}
}
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);
}
}
}
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;
}
// Remap the owning form name (used at runtime to bind form-scope
// variables). The form itself gets renamed by the same pass.
if (proc->formName[0]) {
const char *mappedForm = nameMapLookup(map, proc->formName);
if (mappedForm) {
snprintf(proc->formName, sizeof(proc->formName), "%s", mappedForm);
}
}
// Find last underscore
char *underscore = strrchr(proc->name, '_');
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);
}
}
}
// ============================================================
// .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;
}

View file

@ -0,0 +1,68 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// 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

@ -0,0 +1,412 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// opcodes.h -- DVX BASIC bytecode instruction definitions
//
// Stack-based p-code for the DVX BASIC virtual machine.
// Embeddable: no DVX dependencies, pure C.
#ifndef DVXBASIC_OPCODES_H
#define DVXBASIC_OPCODES_H
// ============================================================
// Variable scope tags
// Emitted in bytecode (e.g. OP_FOR scopeTag byte) and consumed
// by the VM to choose between globals, call-frame locals, and
// form-scoped variables. Numeric values are part of the bytecode
// ABI -- do not reorder.
// ============================================================
typedef enum {
SCOPE_GLOBAL = 0,
SCOPE_LOCAL = 1,
SCOPE_FORM = 2 // per-form variable (persists while form is loaded)
} BasScopeE;
// ============================================================
// File channel modes (BasFileChannelT.mode and OP_FILE_OPEN arg).
// Numeric values are part of the bytecode ABI -- do not reorder.
// ============================================================
typedef enum {
BAS_FILE_MODE_CLOSED = 0,
BAS_FILE_MODE_INPUT = 1,
BAS_FILE_MODE_OUTPUT = 2,
BAS_FILE_MODE_APPEND = 3,
BAS_FILE_MODE_RANDOM = 4,
BAS_FILE_MODE_BINARY = 5
} BasFileModeE;
// ============================================================
// Data type tags (used in Value representation)
// ============================================================
#define BAS_TYPE_INTEGER 0 // 16-bit signed
#define BAS_TYPE_LONG 1 // 32-bit signed
#define BAS_TYPE_SINGLE 2 // 32-bit float
#define BAS_TYPE_DOUBLE 3 // 64-bit float
#define BAS_TYPE_STRING 4 // ref-counted dynamic string
#define BAS_TYPE_BOOLEAN 5 // True (-1) or False (0)
#define BAS_TYPE_ARRAY 6 // ref-counted array
#define BAS_TYPE_UDT 7 // ref-counted user-defined type
#define BAS_TYPE_OBJECT 8 // opaque host object (form, control, etc.)
#define BAS_TYPE_REF 9 // ByRef pointer to a BasValueT slot
// ============================================================
// Stack operations
// ============================================================
#define OP_NOP 0x00
#define OP_PUSH_INT16 0x01 // [int16] push 16-bit integer
#define OP_PUSH_INT32 0x02 // [int32] push 32-bit integer
#define OP_PUSH_FLT32 0x03 // [float32] push 32-bit float
#define OP_PUSH_FLT64 0x04 // [float64] push 64-bit float
#define OP_PUSH_STR 0x05 // [uint16 idx] push string from constant pool
#define OP_PUSH_TRUE 0x06 // push boolean True (-1)
#define OP_PUSH_FALSE 0x07 // push boolean False (0)
#define OP_POP 0x08 // discard top of stack
#define OP_DUP 0x09 // duplicate top of stack
// ============================================================
// Variable access
// ============================================================
#define OP_LOAD_LOCAL 0x10 // [uint16 idx] push local variable
#define OP_STORE_LOCAL 0x11 // [uint16 idx] pop to local variable
#define OP_LOAD_GLOBAL 0x12 // [uint16 idx] push global variable
#define OP_STORE_GLOBAL 0x13 // [uint16 idx] pop to global variable
#define OP_LOAD_REF 0x14 // dereference top of stack (ByRef)
#define OP_STORE_REF 0x15 // store through reference on stack
#define OP_LOAD_ARRAY 0x16 // [uint8 dims] indices on stack, array ref below
#define OP_STORE_ARRAY 0x17 // [uint8 dims] value, indices, array ref on stack
#define OP_LOAD_FIELD 0x18 // [uint16 fieldIdx] load UDT field
#define OP_STORE_FIELD 0x19 // [uint16 fieldIdx] store UDT field
#define OP_PUSH_LOCAL_ADDR 0x1A // [uint16 idx] push address of local (for ByRef)
#define OP_PUSH_GLOBAL_ADDR 0x1B // [uint16 idx] push address of global (for ByRef)
#define OP_STORE_ARRAY_FIELD 0x1C // [uint8 dims, uint16 fieldIdx] value, indices, array on stack
// ============================================================
// Arithmetic (integer)
// ============================================================
#define OP_ADD_INT 0x20
#define OP_SUB_INT 0x21
#define OP_MUL_INT 0x22
#define OP_IDIV_INT 0x23 // integer divide (\)
#define OP_MOD_INT 0x24
#define OP_NEG_INT 0x25
// ============================================================
// Arithmetic (float)
// ============================================================
#define OP_ADD_FLT 0x26
#define OP_SUB_FLT 0x27
#define OP_MUL_FLT 0x28
#define OP_DIV_FLT 0x29 // float divide (/)
#define OP_NEG_FLT 0x2A
#define OP_POW 0x2B // exponentiation (^)
// ============================================================
// String operations
// ============================================================
#define OP_STR_CONCAT 0x30
#define OP_STR_LEFT 0x31
#define OP_STR_RIGHT 0x32
#define OP_STR_MID 0x33 // 3 args: str, start, len
#define OP_STR_MID2 0x34 // 2 args: str, start (to end)
#define OP_STR_LEN 0x35
#define OP_STR_INSTR 0x36 // 2 args: str, find
#define OP_STR_INSTR3 0x37 // 3 args: start, str, find
#define OP_STR_UCASE 0x38
#define OP_STR_LCASE 0x39
#define OP_STR_TRIM 0x3A
#define OP_STR_LTRIM 0x3B
#define OP_STR_RTRIM 0x3C
#define OP_STR_CHR 0x3D
#define OP_STR_ASC 0x3E
#define OP_STR_SPACE 0x3F
// ============================================================
// Comparison (push boolean result)
// ============================================================
#define OP_CMP_EQ 0x40
#define OP_CMP_NE 0x41
#define OP_CMP_LT 0x42
#define OP_CMP_GT 0x43
#define OP_CMP_LE 0x44
#define OP_CMP_GE 0x45
// ============================================================
// Logical / bitwise
// ============================================================
#define OP_AND 0x48
#define OP_OR 0x49
#define OP_NOT 0x4A
#define OP_XOR 0x4B
#define OP_EQV 0x4C
#define OP_IMP 0x4D
// ============================================================
// Control flow
// ============================================================
#define OP_JMP 0x50 // [int16 offset] unconditional jump
#define OP_JMP_TRUE 0x51 // [int16 offset] jump if TOS is true
#define OP_JMP_FALSE 0x52 // [int16 offset] jump if TOS is false
#define OP_CALL 0x53 // [uint16 addr] [uint8 argc] [uint8 baseSlot]
#define OP_GOSUB_RET 0x54 // pop PC from eval stack, jump (GOSUB return)
#define OP_RET 0x55 // return from subroutine
#define OP_RET_VAL 0x56 // return from function (value on stack)
#define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 scope] [int16 skipOffset] init FOR, skip body if range empty
#define OP_FOR_NEXT 0x58 // [uint16 varIdx] [uint8 isLocal] [int16 loopTop]
#define OP_FOR_POP 0x59 // pop top FOR stack entry (for EXIT FOR)
// ============================================================
// Type conversion
// ============================================================
#define OP_CONV_INT_FLT 0x60 // int -> float
#define OP_CONV_FLT_INT 0x61 // float -> int (banker's rounding)
#define OP_CONV_INT_STR 0x62 // int -> string
#define OP_CONV_STR_INT 0x63 // string -> int (VAL)
#define OP_CONV_FLT_STR 0x64 // float -> string
#define OP_CONV_STR_FLT 0x65 // string -> float (VAL)
#define OP_CONV_INT_LONG 0x66 // int16 -> int32
#define OP_CONV_LONG_INT 0x67 // int32 -> int16
// ============================================================
// I/O
// ============================================================
#define OP_PRINT 0x70 // print TOS to current output
#define OP_PRINT_NL 0x71 // print newline
#define OP_PRINT_TAB 0x72 // print tab (14-column zones)
#define OP_PRINT_SPC 0x73 // [uint8 n] print n spaces
#define OP_INPUT 0x74 // read line into string on stack
#define OP_FILE_OPEN 0x75 // [uint8 mode] filename, channel# on stack
#define OP_FILE_CLOSE 0x76 // channel# on stack
#define OP_FILE_PRINT 0x77 // channel#, value on stack
#define OP_FILE_INPUT 0x78 // channel# on stack, push string
#define OP_FILE_EOF 0x79 // channel# on stack, push boolean
#define OP_FILE_LINE_INPUT 0x7A // channel# on stack, push string
// ============================================================
// UI / Event (used when form system is active)
// ============================================================
//
// All UI opcodes are name-based: control references, property names,
// method names, and form names are strings resolved at runtime.
// This allows third-party widget DXEs and new properties to work
// without recompiling the BASIC runtime.
//
// Stack convention:
// LOAD_PROP: ... controlRef propNameStr -> ... value
// STORE_PROP: ... controlRef propNameStr value -> ...
// CALL_METHOD: ... controlRef methodNameStr [args] -> ... [result]
// LOAD_FORM: ... formNameStr -> ... formRef
// CREATE_CTRL: ... formRef typeNameStr nameStr -> ... controlRef
#define OP_LOAD_PROP 0x80 // pop propName, pop ctrlRef, push property value
#define OP_STORE_PROP 0x81 // pop value, pop propName, pop ctrlRef, set property
#define OP_CALL_METHOD 0x82 // [uint8 argc] pop methodName, pop ctrlRef, pop args, push result
#define OP_LOAD_FORM 0x83 // pop formName string, push form reference
#define OP_UNLOAD_FORM 0x84 // pop formRef, unload it
#define OP_SHOW_FORM 0x85 // [uint8 modal] pop formRef, show it
#define OP_HIDE_FORM 0x86 // pop formRef, hide it
#define OP_DO_EVENTS 0x87
#define OP_MSGBOX 0x88 // pop flags, pop message string, push result
#define OP_INPUTBOX 0x89 // pop default, pop title, pop prompt, push result string
#define OP_ME_REF 0x8A // push current form reference
#define OP_CREATE_CTRL 0x8B // pop name, pop typeName, pop formRef, push controlRef
#define OP_FIND_CTRL 0x8C // pop ctrlName, pop formRef, push controlRef
#define OP_CTRL_REF 0x8D // [uint16 nameConstIdx] push named control on current form
#define OP_FIND_CTRL_IDX 0x8E // pop index, pop ctrlName, pop formRef, push ctrlRef
#define OP_LOAD_FORM_VAR 0x8F // [uint16 idx] push currentFormVars[idx]
#define OP_STORE_FORM_VAR 0x9B // [uint16 idx] pop, store to currentFormVars[idx]
#define OP_PUSH_FORM_ADDR 0x9C // [uint16 idx] push &currentFormVars[idx] (ByRef)
#define OP_CREATE_CTRL_EX 0x9D // pop parentRef, pop name, pop type, pop formRef, push ctrlRef
#define OP_CREATE_FORM 0xF0 // pop height, pop width, pop nameStr, push formRef
#define OP_SET_EVENT 0xF1 // pop handlerNameStr, pop eventNameStr, pop ctrlRef
#define OP_REMOVE_CTRL 0xF2 // pop ctrlNameStr, pop formRef
// ============================================================
// Array / misc
// ============================================================
#define OP_DIM_ARRAY 0x90 // [uint8 dims] [uint8 type] bounds on stack
#define OP_REDIM 0x91 // [uint8 dims] [uint8 preserve] bounds on stack
#define OP_ERASE 0x92 // array ref on stack
#define OP_LBOUND 0x93 // [uint8 dim] array ref on stack
#define OP_UBOUND 0x94 // [uint8 dim] array ref on stack
#define OP_ON_ERROR 0x95 // [int16 handler] set error handler (0 = disable)
#define OP_RESUME 0x96 // resume after error
#define OP_RESUME_NEXT 0x97 // resume at next statement
#define OP_RAISE_ERR 0x98 // error number on stack
#define OP_ERR_NUM 0x99 // push current error number
#define OP_ERR_CLEAR 0x9A // clear error state
// ============================================================
// Math built-ins (single opcode each for common functions)
// ============================================================
#define OP_MATH_ABS 0xA0
#define OP_MATH_INT 0xA1 // floor
#define OP_MATH_FIX 0xA2 // truncate toward zero
#define OP_MATH_SGN 0xA3
#define OP_MATH_SQR 0xA4
#define OP_MATH_SIN 0xA5
#define OP_MATH_COS 0xA6
#define OP_MATH_TAN 0xA7
#define OP_MATH_ATN 0xA8
#define OP_MATH_LOG 0xA9
#define OP_MATH_EXP 0xAA
#define OP_MATH_RND 0xAB
#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
// ============================================================
#define OP_STR_VAL 0xB0 // VAL(s$) -> number
#define OP_STR_STRF 0xB1 // STR$(n) -> string
#define OP_STR_HEX 0xB2 // HEX$(n) -> string
#define OP_STR_STRING 0xB3 // STRING$(n, char) -> string
#define OP_STR_OCT 0xF3 // OCT$(n) -> string
#define OP_CONV_BOOL 0xF4 // CBOOL(n) -> -1 (true) or 0 (false)
#define OP_PUSH_ARR_ADDR 0xF5 // [uint8 dims] pop dims indices, pop array ref, push REF to element
// ============================================================
// Extended built-ins
// ============================================================
#define OP_MATH_TIMER 0xB4 // push seconds since midnight as DOUBLE
#define OP_DATE_STR 0xB5 // push DATE$ string "MM-DD-YYYY"
#define OP_TIME_STR 0xB6 // push TIME$ string "HH:MM:SS"
#define OP_SLEEP 0xB7 // pop seconds, sleep
#define OP_ENVIRON 0xB8 // pop env var name, push value string
// ============================================================
// DATA/READ/RESTORE
// ============================================================
#define OP_READ_DATA 0xB9 // push next value from data pool
#define OP_RESTORE 0xBA // reset data pointer to 0
// ============================================================
// WRITE # (comma-delimited with quoted strings)
// ============================================================
#define OP_FILE_WRITE 0xBB // pop channel + value, write in WRITE format
#define OP_FILE_WRITE_SEP 0xBC // pop channel, write comma separator
#define OP_FILE_WRITE_NL 0xBD // pop channel, write newline
// ============================================================
// Random/Binary file I/O
// ============================================================
#define OP_FILE_GET 0xBE // pop channel + recno, read record, push value
#define OP_FILE_PUT 0xBF // pop channel + recno + value, write record
#define OP_FILE_SEEK 0xC0 // pop channel + position, seek
#define OP_FILE_LOF 0xC1 // pop channel, push file length
#define OP_FILE_LOC 0xC2 // pop channel, push current position
#define OP_FILE_FREEFILE 0xC3 // push next free channel number
#define OP_FILE_INPUT_N 0xC4 // pop channel + n, read n chars, push string
// ============================================================
// Fixed-length strings and MID$ assignment
// ============================================================
#define OP_STR_FIXLEN 0xC5 // [uint16 len] pop string, pad/truncate, push
#define OP_STR_MID_ASGN 0xC6 // pop replacement, len, start, str; push modified
// ============================================================
// PRINT USING
// ============================================================
#define OP_PRINT_USING 0xC7 // pop format + value, push formatted string
// ============================================================
// SPC(n) and TAB(n) with stack-based argument
// ============================================================
#define OP_PRINT_TAB_N 0xC8 // pop column count, print spaces to reach column
#define OP_PRINT_SPC_N 0xC9 // pop count, print that many spaces
#define OP_FORMAT 0xCA // pop format string + value, push formatted string
#define OP_SHELL 0xCB // pop command string, call system(), push return value
#define OP_COMPARE_MODE 0xCC // [uint8 mode] set string compare mode (0=binary, 1=text)
// ============================================================
// External library calls (DECLARE LIBRARY)
// ============================================================
//
// Calls native functions exported by dynamically loaded libraries.
// The VM resolves library + function name on first call via a host
// callback, caches the result, and marshals arguments through a
// second callback. This allows BASIC programs to use any library
// (serial, security, third-party) without recompiling the runtime.
#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
#define OP_APP_PATH 0xDD // push App.Path string
#define OP_APP_CONFIG 0xDE // push App.Config string
#define OP_APP_DATA 0xDF // push App.Data string
// INI file operations
#define OP_INI_READ 0xE0 // pop default, pop key, pop section, pop file, push string
#define OP_INI_WRITE 0xE1 // pop value, pop key, pop section, pop file
// Filesystem operations
#define OP_FS_KILL 0xE2 // pop filename, delete file
#define OP_FS_NAME 0xE3 // pop newname, pop oldname, rename
#define OP_FS_FILECOPY 0xE4 // pop dst, pop src, copy file
#define OP_FS_MKDIR 0xE5 // pop path, create directory
#define OP_FS_RMDIR 0xE6 // pop path, remove directory
#define OP_FS_CHDIR 0xE7 // pop path, change directory
#define OP_FS_CHDRIVE 0xE8 // pop drive, change drive
#define OP_FS_CURDIR 0xE9 // push current directory string
#define OP_FS_DIR 0xEA // pop pattern, push first matching filename
#define OP_FS_DIR_NEXT 0xEB // push next matching filename (no args)
#define OP_FS_FILELEN 0xEC // pop filename, push file length
#define OP_FS_GETATTR 0xED // pop filename, push attributes integer
#define OP_FS_SETATTR 0xEE // pop attrs, pop filename, set attributes
// Debug
#define OP_LINE 0xEF // [uint16 lineNum] set current source line for debugger
// ============================================================
// Halt
// ============================================================
#define OP_END 0xFE // explicit END statement -- terminates program
#define OP_HALT 0xFF // implicit end of module
#endif // DVXBASIC_OPCODES_H

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,117 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// parser.h -- DVX BASIC parser (recursive descent)
//
// Single-pass compiler: reads tokens from the lexer and emits
// p-code directly via the code generator. No AST. Forward
// references to SUBs/FUNCTIONs are resolved via backpatching.
//
// Embeddable: no DVX dependencies, pure C.
#ifndef DVXBASIC_PARSER_H
#define DVXBASIC_PARSER_H
#include "lexer.h"
#include "codegen.h"
#include "symtab.h"
#include "../runtime/vm.h"
#include <stdint.h>
#include <stdbool.h>
// ============================================================
// Parser state
// ============================================================
// Parse-error string sizes. PARSE_ERROR_LEN is the final message stored
// on the parser; PARSE_ERR_SCRATCH is used for temporary formatting before
// copying into it (with a "Line N: " prefix).
#define BAS_PARSE_ERROR_LEN 1024
#define BAS_PARSE_ERR_SCRATCH 512
// Optional compile-time validator for CtrlName.Member references.
// The IDE populates this from the project's .frm files + widget DXE
// metadata so typos die at compile time instead of at event-click
// time. bascomp leaves it NULL (runs on the host, no widget DXEs)
// and falls back to the runtime error net. Dynamically-created
// controls aren't in the map, so lookupCtrlType returns NULL and
// validation is skipped for those -- no false positives.
typedef struct {
// Return the widget type name for a control declared in a .frm,
// or NULL if the control isn't statically known.
const char *(*lookupCtrlType)(void *ctx, const char *ctrlName);
// Return true if `methodName` is valid for widget type `wgtType`
// (or a common method like Refresh/SetFocus). Called with the
// type string returned by lookupCtrlType.
bool (*isMethodValid)(void *ctx, const char *wgtType, const char *methodName);
// Return true if `propName` is a valid property on wgtType.
bool (*isPropValid)(void *ctx, const char *wgtType, const char *propName);
void *ctx;
} BasCtrlValidatorT;
typedef struct {
BasLexerT lex;
BasCodeGenT cg;
BasSymTabT sym;
char error[BAS_PARSE_ERROR_LEN];
bool hasError;
int32_t errorLine;
int32_t prevLine; // line of the previous token (for error reporting)
int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName
int32_t optionBase; // default array lower bound (0 or 1)
bool optionCompareText; // true = case-insensitive string comparison
bool optionExplicit; // true = variables must be declared with DIM
uint8_t defType[26]; // default type per letter (A-Z), set by DEFINT etc.
char currentProc[BAS_MAX_TOKEN_LEN]; // name of current SUB/FUNCTION
// Per-form init block tracking
int32_t formInitJmpAddr; // code position of JMP to patch (-1 = none)
int32_t formInitCodeStart; // code position where init block starts (-1 = none)
// Optional compile-time CtrlName.Member validator (IDE-only).
const BasCtrlValidatorT *validator;
} BasParserT;
// ============================================================
// API
// ============================================================
// Initialize parser with source text.
void basParserInit(BasParserT *p, const char *source, int32_t sourceLen);
// Attach an optional compile-time validator for CtrlName.Member
// references. The parser borrows the pointer -- caller owns the
// underlying struct and must keep it alive until basParserFree.
void basParserSetValidator(BasParserT *p, const BasCtrlValidatorT *v);
// Parse the entire source and generate p-code.
// Returns true on success, false on error (check p->error).
bool basParse(BasParserT *p);
// Build a module from the parsed code. Returns NULL on error.
// Caller owns the module and must free with basModuleFree().
BasModuleT *basParserBuildModule(BasParserT *p);
// Free parser resources.
void basParserFree(BasParserT *p);
#endif // DVXBASIC_PARSER_H

View file

@ -0,0 +1,141 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// strip.c -- Release build stripping
//
// Removes debug information from a compiled module:
// - Clears debug variable info (names, scopes, types)
// - Clears debug UDT definitions
// - Mangles procedure names that aren't needed for runtime dispatch.
// The form runtime dispatches events by name (Control_Event pattern)
// 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
// bytecode compaction and offset rewriting).
#include "strip.h"
#include "basEvents.h"
#include "../runtime/values.h"
#include <stdio.h>
#include <stdlib.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. Declared in basEvents.h; defined here as the single source
// of truth.
const char *basEventSuffixes[] = {
"Load", "Unload", "QueryUnload", "Resize", "Activate", "Deactivate",
"Click", "DblClick", "Change", "Timer",
"GotFocus", "LostFocus",
"KeyPress", "KeyDown", "KeyUp",
"MouseDown", "MouseUp", "MouseMove",
"Scroll", "Reposition", "Validate",
NULL
};
// Function prototypes (alphabetical)
void basStripModule(BasModuleT *mod);
static bool nameEndsWithEventSuffix(const char *name);
static bool nameInConstantPool(const BasModuleT *mod, const char *name);
void basStripModule(BasModuleT *mod) {
if (!mod) {
return;
}
// Clear debug variable info
free(mod->debugVars);
mod->debugVars = NULL;
mod->debugVarCount = 0;
// Clear debug UDT definitions
if (mod->debugUdtDefs) {
for (int32_t i = 0; i < mod->debugUdtDefCount; i++) {
free(mod->debugUdtDefs[i].fields);
}
free(mod->debugUdtDefs);
mod->debugUdtDefs = NULL;
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++);
}
}
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; basEventSuffixes[i]; i++) {
if (strcasecmp(suffix, basEventSuffixes[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;
}

View file

@ -0,0 +1,43 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// strip.h -- Release build stripping
//
// Removes debug information from a compiled module to hinder
// decompilation. Clears debug variable info and debug UDT
// definitions, and mangles proc names that aren't needed for
// runtime name-based dispatch.
#ifndef DVXBASIC_STRIP_H
#define DVXBASIC_STRIP_H
#include "../runtime/vm.h"
// Strip debug info from a module for release builds:
// - Clear debug variable info
// - 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);
#endif // DVXBASIC_STRIP_H

View file

@ -0,0 +1,269 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// symtab.c -- DVX BASIC symbol table implementation
#include "symtab.h"
#include "thirdparty/stb_ds_wrap.h"
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
// Function prototypes (alphabetical)
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
int32_t basSymTabAllocSlot(BasSymTabT *tab);
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName);
void basSymTabEnterLocal(BasSymTabT *tab);
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name);
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name);
void basSymTabInit(BasSymTabT *tab);
int32_t basSymTabLeaveFormScope(BasSymTabT *tab);
void basSymTabLeaveLocal(BasSymTabT *tab);
static bool namesEqual(const char *a, const char *b);
static uint32_t nameHashCI(const char *name);
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) {
// Determine scope: local > form > global.
// Only variables get SCOPE_FORM; SUBs/FUNCTIONs/CONSTs remain global.
BasScopeE scope;
if (tab->inLocalScope) {
scope = SCOPE_LOCAL;
} else if (tab->inFormScope && kind == SYM_VARIABLE) {
scope = SCOPE_FORM;
} else {
scope = SCOPE_GLOBAL;
}
uint32_t h = nameHashCI(name);
// Check for duplicate in current scope (skip ended form symbols)
for (int32_t i = 0; i < tab->count; i++) {
if (tab->symbols[i]->formScopeEnded) {
continue;
}
if (tab->symbols[i]->nameHash == h && tab->symbols[i]->scope == scope && namesEqual(tab->symbols[i]->name, name)) {
return NULL; // duplicate
}
}
BasSymbolT *sym = (BasSymbolT *)calloc(1, sizeof(BasSymbolT));
if (!sym) {
return NULL;
}
strncpy(sym->name, name, BAS_MAX_SYMBOL_NAME - 1);
sym->name[BAS_MAX_SYMBOL_NAME - 1] = '\0';
sym->nameHash = h;
sym->kind = kind;
sym->scope = scope;
sym->dataType = dataType;
sym->isDefined = true;
// Record owning form for both SCOPE_FORM vars AND SUBs/FUNCTIONs
// declared inside BEGINFORM...ENDFORM. SUBs stay at SCOPE_GLOBAL
// (callable from anywhere) but carry the owning form so the VM can
// bind form-scope vars correctly when the SUB is dispatched as an
// event handler for a different form's control.
if (tab->inFormScope && tab->formScopeName[0] &&
(scope == SCOPE_FORM || kind == SYM_SUB || kind == SYM_FUNCTION)) {
strncpy(sym->formName, tab->formScopeName, BAS_MAX_SYMBOL_NAME - 1);
sym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
}
arrput(tab->symbols, sym);
tab->count = (int32_t)arrlen(tab->symbols);
return sym;
}
int32_t basSymTabAllocSlot(BasSymTabT *tab) {
if (tab->inLocalScope) {
return tab->nextLocalIdx++;
}
if (tab->inFormScope) {
return tab->nextFormVarIdx++;
}
return tab->nextGlobalIdx++;
}
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName) {
tab->inFormScope = true;
strncpy(tab->formScopeName, formName, BAS_MAX_SYMBOL_NAME - 1);
tab->formScopeName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
tab->nextFormVarIdx = 0;
tab->formScopeSymStart = tab->count;
}
void basSymTabEnterLocal(BasSymTabT *tab) {
tab->inLocalScope = true;
tab->nextLocalIdx = 0;
}
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) {
uint32_t h = nameHashCI(name);
// Search local scope first
if (tab->inLocalScope) {
for (int32_t i = tab->count - 1; i >= 0; i--) {
BasSymbolT *s = tab->symbols[i];
if (s->nameHash == h && s->scope == SCOPE_LOCAL && namesEqual(s->name, name)) {
return s;
}
}
}
// Search form scope and global scope
for (int32_t i = tab->count - 1; i >= 0; i--) {
BasSymbolT *s = tab->symbols[i];
if (s->formScopeEnded) {
continue;
}
if (s->nameHash == h && (s->scope == SCOPE_FORM || s->scope == SCOPE_GLOBAL) &&
namesEqual(s->name, name)) {
return s;
}
}
return NULL;
}
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name) {
uint32_t h = nameHashCI(name);
for (int32_t i = 0; i < tab->count; i++) {
BasSymbolT *s = tab->symbols[i];
if (s->formScopeEnded) {
continue;
}
if (s->nameHash == h && s->scope == SCOPE_GLOBAL && namesEqual(s->name, name)) {
return s;
}
}
return NULL;
}
void basSymTabInit(BasSymTabT *tab) {
memset(tab, 0, sizeof(*tab));
}
int32_t basSymTabLeaveFormScope(BasSymTabT *tab) {
int32_t varCount = tab->nextFormVarIdx;
// Mark all form-scope symbols added since BEGINFORM as ended
for (int32_t i = tab->formScopeSymStart; i < tab->count; i++) {
if (tab->symbols[i]->scope == SCOPE_FORM) {
tab->symbols[i]->formScopeEnded = true;
}
}
tab->inFormScope = false;
tab->formScopeName[0] = '\0';
tab->nextFormVarIdx = 0;
tab->formScopeSymStart = 0;
return varCount;
}
void basSymTabLeaveLocal(BasSymTabT *tab) {
// Remove all local symbols, freeing their dynamic arrays and the
// symbol struct itself.
int32_t newCount = 0;
for (int32_t i = 0; i < tab->count; i++) {
if (tab->symbols[i]->scope == SCOPE_LOCAL) {
arrfree(tab->symbols[i]->patchAddrs);
arrfree(tab->symbols[i]->fields);
free(tab->symbols[i]);
} else {
if (i != newCount) {
tab->symbols[newCount] = tab->symbols[i];
}
newCount++;
}
}
arrsetlen(tab->symbols, newCount);
tab->count = newCount;
tab->inLocalScope = false;
tab->nextLocalIdx = 0;
}
// ============================================================
// Case-insensitive FNV-1a hash used to accelerate symbol-table lookups.
// Caller computes hash of search-name once; each entry stores its own
// precomputed hash, so per-entry comparison is a fast uint32 compare
// that nearly always rejects non-matches without calling namesEqual.
// ============================================================
static uint32_t nameHashCI(const char *name) {
uint32_t h = 0x811C9DC5u;
while (*name) {
char c = *name;
if (c >= 'a' && c <= 'z') {
c -= 32;
}
h ^= (uint32_t)(uint8_t)c;
h *= 0x01000193u;
name++;
}
return h;
}
// ============================================================
// Case-insensitive name comparison
// ============================================================
static bool namesEqual(const char *a, const char *b) {
while (*a && *b) {
char ca = *a >= 'a' && *a <= 'z' ? *a - 32 : *a;
char cb = *b >= 'a' && *b <= 'z' ? *b - 32 : *b;
if (ca != cb) {
return false;
}
a++;
b++;
}
return *a == *b;
}

View file

@ -0,0 +1,166 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// symtab.h -- DVX BASIC symbol table
//
// Tracks variables, constants, subroutines, functions, and labels
// during compilation. Supports nested scopes (global + one local
// scope per SUB/FUNCTION).
//
// Embeddable: no DVX dependencies, pure C.
#ifndef DVXBASIC_SYMTAB_H
#define DVXBASIC_SYMTAB_H
#include "../compiler/opcodes.h"
#include <stdint.h>
#include <stdbool.h>
// ============================================================
// Symbol kinds
// ============================================================
typedef enum {
SYM_VARIABLE,
SYM_CONST,
SYM_SUB,
SYM_FUNCTION,
SYM_LABEL,
SYM_TYPE_DEF // user-defined TYPE
} BasSymKindE;
// BasScopeE moved to opcodes.h (shared with runtime).
// ============================================================
// Symbol entry
// ============================================================
#define BAS_MAX_SYMBOL_NAME 64
#define BAS_MAX_PARAMS 16
// UDT field definition
typedef struct {
char name[BAS_MAX_SYMBOL_NAME];
uint8_t dataType; // BAS_TYPE_*
int32_t udtTypeId; // if dataType == BAS_TYPE_UDT, index of the TYPE_DEF symbol
} BasFieldDefT;
typedef struct {
char name[BAS_MAX_SYMBOL_NAME];
uint32_t nameHash; // FNV-1a over uppercase(name); fast-reject during lookup
BasSymKindE kind;
BasScopeE scope;
uint8_t dataType; // BAS_TYPE_* for variables/functions
int32_t index; // slot index (local or global)
int32_t codeAddr; // PC address for SUB/FUNCTION/LABEL
int32_t localCount; // number of local variables (for SUB/FUNCTION, set on leave)
bool isDefined; // false = forward-declared
bool isArray;
bool isShared;
bool isExtern; // true = external library function (DECLARE LIBRARY)
bool formScopeEnded; // true = form scope ended, invisible to lookups
char formName[BAS_MAX_SYMBOL_NAME]; // form name for SCOPE_FORM vars
int32_t udtTypeId; // for variables of BAS_TYPE_UDT: index of TYPE_DEF symbol
int32_t fixedLen; // for STRING * n: fixed length (0 = variable-length)
uint16_t externLibIdx; // constant pool index for library name (if isExtern)
uint16_t externFuncIdx; // constant pool index for function name (if isExtern)
// For SUB/FUNCTION: parameter info
int32_t paramCount;
int32_t requiredParams; // count of non-optional params
uint8_t paramTypes[BAS_MAX_PARAMS];
bool paramByVal[BAS_MAX_PARAMS];
bool paramOptional[BAS_MAX_PARAMS]; // true = OPTIONAL parameter
// Forward-reference backpatch list (code addresses to patch when defined)
int32_t *patchAddrs; // stb_ds dynamic array
int32_t patchCount;
// For CONST: the constant value
union {
int32_t constInt;
double constDbl;
};
char constStr[256];
// For TYPE_DEF: field definitions
BasFieldDefT *fields; // stb_ds dynamic array
int32_t fieldCount;
} BasSymbolT;
// ============================================================
// Symbol table
// ============================================================
typedef struct {
// Array of POINTERS to heap-allocated symbols (stb_ds array of pointers).
// Pointers-of-pointers rather than array-of-structs because callers
// routinely hold a BasSymbolT * across parsing operations that may
// trigger basSymTabAdd (which grows this array). An array of structs
// would be reallocated on growth, invalidating any held pointer.
// Indirection via a stable heap pointer per symbol avoids that.
BasSymbolT **symbols;
int32_t count;
int32_t nextGlobalIdx; // next global variable slot
int32_t nextLocalIdx; // next local variable slot (reset per SUB/FUNCTION)
bool inLocalScope; // true when inside SUB/FUNCTION
bool inFormScope; // true inside BEGINFORM...ENDFORM
char formScopeName[BAS_MAX_SYMBOL_NAME]; // current form name
int32_t nextFormVarIdx; // next form-level variable slot
int32_t formScopeSymStart; // symbol count at BEGINFORM (for marking ended)
} BasSymTabT;
// ============================================================
// API
// ============================================================
void basSymTabInit(BasSymTabT *tab);
// Add a symbol. Returns the symbol pointer, or NULL if the table is full
// or the name already exists in the current scope.
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
// Look up a symbol by name. Searches local scope first, then global.
// Case-insensitive.
BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name);
// Look up a symbol in the global scope only.
BasSymbolT *basSymTabFindGlobal(BasSymTabT *tab, const char *name);
// Enter local scope (called at SUB/FUNCTION start).
void basSymTabEnterLocal(BasSymTabT *tab);
// Leave local scope (called at END SUB/FUNCTION). Removes local symbols.
void basSymTabLeaveLocal(BasSymTabT *tab);
// Allocate the next variable slot (global, local, or form depending on scope).
int32_t basSymTabAllocSlot(BasSymTabT *tab);
// Enter form scope (called at BEGINFORM). Form-level DIMs create SCOPE_FORM variables.
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName);
// Leave form scope (called at ENDFORM). Marks form-scope symbols as ended.
// Returns the number of form variables allocated.
int32_t basSymTabLeaveFormScope(BasSymTabT *tab);
#endif // DVXBASIC_SYMTAB_H

View file

@ -0,0 +1,420 @@
# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
.topic ctrl.common.props
.title Common Properties, Events, and Methods
.toc 0 Common Properties, Events, and Methods
.default
.index Common Properties
.index Common Events
.index Common Methods
.index Properties
.index Events
.index Methods
.h1 Common Properties, Events, and Methods
Every control in DVX BASIC inherits a set of common properties, events, and methods. These are handled by the form runtime before dispatching to widget-specific interface descriptors.
.h2 Common Properties
.table
Property Type R/W Description
---------- ------- --- -------------------------------------------
Name String R The control's name (e.g. "Command1"). Read-only at runtime.
Left Integer R/W X position in pixels relative to the parent container.
Top Integer R/W Y position in pixels relative to the parent container.
Width Integer R/W Current width in pixels. Setting this changes the minimum width constraint.
Height Integer R/W Current height in pixels. Setting this changes the minimum height constraint.
MinWidth Integer R/W Minimum width for layout. Alias for Width in the setter.
MinHeight Integer R/W Minimum height for layout. Alias for Height in the setter.
MaxWidth Integer R/W Maximum width cap (0 = no limit, stretch to fill).
MaxHeight Integer R/W Maximum height cap (0 = no limit, stretch to fill).
Weight Integer R/W Layout weight. 0 = fixed size, >0 = share extra space proportionally.
Visible Boolean R/W Whether the control is visible.
Enabled Boolean R/W Whether the control accepts user input.
BackColor Long R/W Background color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
ForeColor Long R/W Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order.
ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.
.endtable
.h2 Common Events
These events are wired on every control loaded from a .frm file. Controls created dynamically at runtime via code only receive Click, DblClick, Change, GotFocus, and LostFocus; the keyboard, mouse, and scroll events below require the control to be defined in the .frm file.
.table
Event Parameters Description
--------- ------------------------------------------- -------------------------------------------
Click (none) Fires when the control is clicked.
DblClick (none) Fires when the control is double-clicked.
Change (none) Fires when the control's value or text changes.
GotFocus (none) Fires when the control receives keyboard focus.
LostFocus (none) Fires when the control loses keyboard focus.
KeyPress KeyAscii As Integer Fires when a printable key is pressed. KeyAscii is the ASCII code.
KeyDown KeyCode As Integer, Shift As Integer Fires when any key is pressed down. KeyCode is the scan code; Shift indicates modifier keys.
KeyUp KeyCode As Integer, Shift As Integer Fires when a key is released.
MouseDown Button As Integer, X As Integer, Y As Integer Fires when a mouse button is pressed over the control.
MouseUp Button As Integer, X As Integer, Y As Integer Fires when a mouse button is released over the control.
MouseMove Button As Integer, X As Integer, Y As Integer Fires when the mouse moves over the control.
Scroll Delta As Integer Fires when the control is scrolled (mouse wheel or scrollbar).
.endtable
.h2 Common Methods
.table
Method Parameters Description
-------- ---------- -------------------------------------------
SetFocus (none) Gives keyboard focus to this control.
Refresh (none) Forces the control to repaint.
.endtable
.link ctrl.form Form
.link ctrl.databinding Data Binding
.link ctrl.frm FRM File Format
.topic ctrl.databinding
.title Data Binding
.toc 1 Data Binding
.index Data Binding
.index DataSource
.index DataField
.index Master-Detail
.h1 Data Binding
DVX BASIC provides VB3-style data binding through three properties that can be set on most controls:
.table
Property Set On Description
---------- ----------- -------------------------------------------
DataSource Any control Name of the Data control to bind to (e.g. "Data1").
DataField Any control Column name from the Data control's recordset to display.
.endtable
.h2 How It Works
.list
.item Place a Data control on the form and set its DatabaseName and RecordSource properties.
.item Place one or more display/edit controls (TextBox, Label, etc.) and set their DataSource to the Data control's name and DataField to a column name.
.item When the form loads, the Data control auto-refreshes: it opens the database, runs the query, and navigates to the first record.
.item Bound controls are updated automatically each time the Data control repositions (the Reposition event fires, and the runtime pushes the current record's field values into all bound controls).
.item When a bound control loses focus (LostFocus), its current text is written back to the Data control's record cache, and Update is called automatically to persist changes.
.endlist
.h2 Master-Detail Binding
For hierarchical data (e.g. orders and order items), use two Data controls:
.list
.item A master Data control bound to the parent table.
.item A detail Data control with its MasterSource set to the master's name, MasterField set to the key column in the master, and DetailField set to the foreign key column in the detail table.
.endlist
When the master record changes, the detail Data control automatically re-queries using the master's current value for filtering. All controls bound to the detail are refreshed.
.h2 DBGrid Binding
Set the DBGrid's DataSource to a Data control name. The grid auto-populates columns from the query results and refreshes whenever the Data control refreshes.
.h2 Example
.code
VERSION DVX 1.00
Begin Form frmData
Caption = "Data Binding Example"
AutoSize = False
Width = 400
Height = 280
Begin Data Data1
DatabaseName = "myapp.db"
RecordSource = "customers"
End
Begin Label lblName
Caption = "Name:"
End
Begin TextBox txtName
DataSource = "Data1"
DataField = "name"
End
Begin Label lblEmail
Caption = "Email:"
End
Begin TextBox txtEmail
DataSource = "Data1"
DataField = "email"
End
End
Sub Data1_Reposition ()
Print "Current record changed"
End Sub
Sub Data1_Validate (Cancel As Integer)
If txtName.Text = "" Then
MsgBox "Name cannot be empty!"
Cancel = 1
End If
End Sub
.endcode
.link ctrl.data Data
.link ctrl.dbgrid DBGrid
.topic ctrl.menus
.title Menu System
.toc 1 Menu System
.index Menu
.index Menu Bar
.index Submenu
.index Separator
.h1 Menu System
Menus are defined in the .frm file using Begin Menu blocks. Each menu item has a name, caption, and nesting level. Menu items fire Click events dispatched as MenuName_Click.
.h2 FRM Syntax
.code
Begin Form Form1
Caption = "Menu Demo"
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuOpen
Caption = "&Open"
End
Begin Menu mnuSave
Caption = "&Save"
End
Begin Menu mnuSep1
Caption = "-"
End
Begin Menu mnuExit
Caption = "E&xit"
End
End
Begin Menu mnuEdit
Caption = "&Edit"
Begin Menu mnuCopy
Caption = "&Copy"
End
Begin Menu mnuPaste
Caption = "&Paste"
End
End
End
.endcode
.h2 Menu Item Properties
.table
Property Type Description
-------- ------- -------------------------------------------
Caption String The text displayed. Use & for accelerator key. Set to "-" for a separator.
Checked Boolean Whether the menu item shows a checkmark.
Enabled Boolean Whether the menu item is enabled (default True).
.endtable
.h2 Nesting
Menu items are nested by placing Begin Menu blocks inside other Begin Menu blocks:
.list
.item Level 0: top-level menu bar headers (e.g. "File", "Edit").
.item Level 1: items within a top-level menu.
.item Level 2+: submenu items.
.endlist
A level-0 menu that contains children becomes a top-level menu header. A non-level-0 menu that contains children becomes a submenu.
.h2 Event Dispatch
Each clickable menu item (not headers, not separators) receives a unique numeric ID at load time. When clicked, the form's onMenu handler maps the ID to the menu item's name and fires MenuName_Click.
.code
Sub mnuOpen_Click ()
MsgBox "Open was clicked"
End Sub
Sub mnuExit_Click ()
Unload Form1
End Sub
.endcode
.link ctrl.form Form
.link ctrl.frm FRM File Format
.topic ctrl.arrays
.title Control Arrays
.toc 1 Control Arrays
.index Control Arrays
.index Index Property
.h1 Control Arrays
DVX BASIC supports VB-style control arrays. Multiple controls can share the same name, differentiated by an Index property. When an event fires on a control array element, the element's index is passed as the first parameter.
.h2 Defining Control Arrays in FRM
.code
Begin CommandButton Command1
Caption = "Button A"
Index = 0
End
Begin CommandButton Command1
Caption = "Button B"
Index = 1
End
Begin CommandButton Command1
Caption = "Button C"
Index = 2
End
.endcode
.h2 Event Handler Convention
When a control has an Index property (>= 0), the event handler receives Index As Integer as the first parameter, before any event-specific parameters.
.code
Sub Command1_Click (Index As Integer)
Select Case Index
Case 0
MsgBox "Button A clicked"
Case 1
MsgBox "Button B clicked"
Case 2
MsgBox "Button C clicked"
End Select
End Sub
.endcode
.h2 Accessing Array Elements in Code
Use the indexed form ControlName(Index) to access a specific element:
.code
Command1(0).Caption = "New Text"
Command1(1).Enabled = False
.endcode
.note info
Control array elements share the same event handler Sub. The runtime prepends the Index argument automatically. If you define parameters on the Sub, Index comes first, followed by the event's own parameters (e.g. KeyPress would be Sub Ctrl1_KeyPress (Index As Integer, KeyAscii As Integer)).
.endnote
.link ctrl.common.props Common Properties, Events, and Methods
.link ctrl.frm FRM File Format
.topic ctrl.frm
.title FRM File Format
.toc 1 FRM File Format
.index FRM
.index .frm
.index Form File
.h1 FRM File Format
The .frm file is a text file that describes a form's layout, controls, menus, and code. It follows a format compatible with VB3 .frm files, with DVX-specific extensions.
.h2 Structure
.code
VERSION DVX 1.00
Begin Form FormName
form-level properties...
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuOpen
Caption = "&Open"
End
End
Begin TypeName ControlName
property = value
...
End
Begin Frame Frame1
Caption = "Group"
Begin TypeName ChildName
...
End
End
End
BASIC code follows...
Sub FormName_Load ()
...
End Sub
.endcode
.h2 Rules
.list
.item The VERSION line is optional. VERSION DVX 1.00 marks a native DVX form. VB forms with version <= 2.0 are accepted for import.
.item The form block begins with Begin Form Name and ends with End.
.item Controls are nested with Begin TypeName Name / End.
.item Container controls (Frame, VBox, HBox, Toolbar, TabStrip, ScrollPane, Splitter, WrapBox) can have child controls nested inside them.
.item Properties are assigned as Key = Value. String values are optionally quoted.
.item Everything after the form's closing End is BASIC source code.
.item Comments in the form section use ' (single quote).
.item Blank lines are ignored in the form section.
.endlist
.h2 Common FRM Properties
.table
Property Applies To Description
----------------------- --------------- -------------------------------------------
Caption Form, controls Display text or window title.
Text TextBox, ComboBox Initial text content.
MinWidth / Width Controls Minimum width. Both names are accepted.
MinHeight / Height Controls Minimum height. Both names are accepted.
MaxWidth Controls Maximum width (0 = no cap).
MaxHeight Controls Maximum height (0 = no cap).
Weight Controls Layout weight for flexible sizing.
Left Form, controls X position (used by Form when Centered=False; informational for controls).
Top Form, controls Y position.
Index Controls Control array index (-1 or absent = not in array).
Visible Controls Initial visibility.
Enabled Controls Initial enabled state.
Layout Form "VBox" or "HBox".
AutoSize Form Auto-fit window to content.
Resizable Form Allow runtime resizing.
Centered Form Center window on screen.
DatabaseName Data SQLite database file path.
RecordSource Data Table name or SQL query.
DataSource Bound controls Name of the Data control.
DataField Bound controls Column name in the recordset.
.endtable
.link ctrl.form Form
.link ctrl.common.props Common Properties, Events, and Methods
.link ctrl.menus Menu System
.link ctrl.arrays Control Arrays

View file

@ -0,0 +1,27 @@
# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# DVX BASIC Help configuration
# Compiled into DVXBASIC.HLP in this directory
output = DVXBASIC.HLP
source = APPS/KPUNCH/DVXBASIC/*.DHS
source = WIDGETS/*.BHS

View file

@ -0,0 +1,21 @@
# dvxbasic.res -- Resource manifest for DVX BASIC
icon32 icon icon32.bmp
name text "DVX BASIC"
author text "Scott Duensing"
copyright text "Copyright 2026 Scott Duensing"
publisher text "Kangaroo Punch Studios"
description text "BASIC language IDE and runtime"
# Toolbar icons (16x16)
tb_open icon tb_open.bmp
tb_save icon tb_save.bmp
tb_run icon tb_run.bmp
tb_stop icon tb_stop.bmp
tb_code icon tb_code.bmp
tb_design icon tb_design.bmp
tb_debug icon tb_debug.bmp
tb_stepin icon tb_stepinto.bmp
tb_stepov icon tb_stepover.bmp
tb_stepou icon tb_stepout.bmp
tb_runtoc icon tb_runtocur.bmp
# Placeholder icon (32x32)
noicon icon noicon.bmp

View file

@ -0,0 +1,104 @@
# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
.topic ctrl.form
.title Form
.toc 1 Form
.index Form
.index Window
.index Caption
.index AutoSize
.index Resizable
.index Load
.index Unload
.index Show
.index Hide
.h1 Form
VB Equivalent: Form -- DVX Widget: Window + VBox/HBox root
The Form is the top-level container representing a DVX window. It is declared in the .frm file with Begin Form FormName. All controls are children of the form's content box, which uses either VBox (default) or HBox layout.
.h2 Form Properties
.table
Property Type Default Description
---------- ------- -------------- -------------------------------------------
Name String "Form1" The form's name, used for event dispatch and Load statement.
Caption String (same as Name) Window title bar text.
Width Integer 400 Window width in pixels. Setting this disables AutoSize.
Height Integer 300 Window height in pixels. Setting this disables AutoSize.
Left Integer 0 Initial X position. Used when Centered is False.
Top Integer 0 Initial Y position. Used when Centered is False.
Layout String "VBox" Content box layout: "VBox" (vertical) or "HBox" (horizontal).
AutoSize Boolean False When True, the window shrink-wraps to fit its content.
Resizable Boolean True Whether the user can resize the window at runtime.
Centered Boolean True When True, the window is centered on screen. When False, Left/Top are used.
.endtable
.h2 Form Events
.table
Event Parameters Description
----------- --------------------- -------------------------------------------
Load (none) Fires after the form and all controls are created. This is the default event.
Unload (none) Fires when the form is being closed or unloaded.
QueryUnload Cancel As Integer Fires before Unload. Set Cancel = 1 to abort the close.
Resize (none) Fires when the window is resized by the user.
Activate (none) Fires when the window gains focus.
Deactivate (none) Fires when the window loses focus.
.endtable
.h2 Form Methods
.table
Statement Description
------------------ -------------------------------------------
Load FormName Load the form (creates the window and controls, fires Load event).
Unload FormName Unload the form (fires Unload, destroys window).
FormName.Show Make the form visible.
FormName.Show 1 Show as modal dialog (blocks until closed).
FormName.Hide Hide the form without unloading it.
.endtable
.h2 Example
.code
Sub Form_Load ()
Form1.Caption = "Hello World"
Print "Form loaded!"
End Sub
Sub Form_QueryUnload (Cancel As Integer)
If MsgBox("Really quit?", 4) <> 6 Then
Cancel = 1
End If
End Sub
Sub Form_Resize ()
Print "Window resized"
End Sub
.endcode
.link ctrl.common.props Common Properties, Events, and Methods
.link ctrl.frm FRM File Format

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more