diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html index bdd50b2..a9b5bc1 100644 --- a/docs/dvx_basic_reference.html +++ b/docs/dvx_basic_reference.html @@ -1224,6 +1224,12 @@ name$ = "Hello" ' String
Note: Both E and D can introduce an exponent (e.g. 1.5E10 and 2.5D3 are equivalent forms of a scientific-notation double).
When mixing types in expressions, values are automatically promoted to a common type: Integer -> Long -> Single -> Double. Strings are not automatically converted to numbers (use VAL and STR$).
+Storing a value into a variable declared as Integer, Long or Single (by suffix, by AS clause, by DEFINT/DEFLNG/DEFSNG, or as a typed parameter, TYPE field or array element) converts the value to that type. Integer and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit. Single keeps about 7 significant digits. A variable with no declared type keeps whatever type the assigned value has.
+Dim n As Integer
+n = 3.7 ' n is 4
+n = 40000 ' Error 6: Overflow
+x = 3.7 ' x has no declared type and keeps 3.7
Boolean values use -1 for True and 0 for False. Any non-zero numeric value is treated as True in a conditional context. The keywords True and False are reserved and may be used anywhere a Boolean value is expected.
See also: Conversion Functions
@@ -1326,7 +1332,7 @@ Dim matrix(1 To 10, 1 To 10) As Single Dim Shared globalFlag As Boolean Dim record As PersonType Dim fixedStr As String * 20 -Note: DIM SHARED at module level makes a variable accessible from every procedure without passing it as a parameter. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls).+
Note: DIM SHARED at module level makes a variable accessible from every procedure without passing it as a parameter. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A variable that is first used inside a SUB or FUNCTION without a DIM is also local to that procedure; to share it with module-level code or other procedures, declare it with DIM SHARED at module level.
Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.
Reallocates a dynamic array, optionally preserving existing data.
@@ -1663,7 +1669,7 @@ ON ERROR GOTO 0 ' Disable error handler RESUME ' Retry the statement that caused the error RESUME NEXT ' Continue at the next statement after the error ERROR n ' Raise a runtime error with error number n -The ERR keyword returns the current error number in expressions (it is 0 when no error is active).
+The ERR keyword returns the current error number in expressions (it is 0 when no error is active). RESUME or RESUME NEXT executed while no error is active raises error 20 (RESUME without error).
On Error GoTo ErrorHandler
Open "missing.txt" For Input As #1
Exit Sub
@@ -1676,16 +1682,20 @@ ErrorHandler:
------ -------
1 FOR loop error (NEXT without FOR, NEXT variable mismatch, FOR stack underflow)
4 Out of DATA
+ 5 Illegal function call (bad argument to a built-in function)
+ 6 Overflow (value does not fit the target type)
7 Out of memory
9 Subscript out of range / invalid variable or field index
11 Division by zero
13 Type mismatch / not an array / not a TYPE instance
+ 20 RESUME without error
26 FOR loop nesting too deep
51 Internal error (bad opcode)
52 Bad file number or file not open
53 File not found
54 Bad file mode
58 File already exists or rename failed
+ 59 Bad record length (OPEN ... LEN must be 1 to 32767)
67 Too many files open
75 Path/file access error
76 Path not found
@@ -1720,7 +1730,7 @@ OPEN filename$ FOR BINARY AS #channel
INPUT Open for sequential reading. File must exist.
OUTPUT Open for sequential writing. Creates or truncates.
APPEND Open for sequential writing at end of file.
- RANDOM Open for random-access record I/O.
+ RANDOM Open for random-access record I/O. LEN sets the record size in bytes (default 128).
BINARY Open for raw binary I/O.
Closes an open file channel.
@@ -1729,8 +1739,11 @@ OPEN filename$ FOR BINARY AS #channelWrites text to a file.
PRINT #channel, expression
Reads comma-delimited data from a file.
-INPUT #channel, variable
+Reads comma-delimited data from a file, one field per variable. Quoted strings written by WRITE # are read back without their quotes; numeric variables receive the converted value.
+INPUT #channel, variable [, variable ...]
+Open "data.txt" For Input As #1
+Input #1, name$, age%, score#
+Close #1
Reads an entire line from a file into a string variable.
LINE INPUT #channel, variable$
@@ -1740,9 +1753,17 @@ OPEN filename$ FOR BINARY AS #channel
Write #1, "Scott", 42, 3.14
' Output: "Scott",42,3.14
Read and write records in RANDOM or BINARY mode files.
+Read and write records in RANDOM or BINARY mode files. When recordNum is omitted the transfer starts at the current file position.
GET #channel, [recordNum], variable
PUT #channel, [recordNum], variable
+In RANDOM mode recordNum is a 1-based record number and each record is LEN bytes (see OPEN). Numbers are stored in their binary form (Integer 2 bytes, Long 4, Single 4, Double 8); a String is stored as a 2-byte length followed by its characters.
+In BINARY mode recordNum is a 1-based byte position. Numbers are stored as in RANDOM mode. PUT writes the characters of a String with no length prefix, and GET reads as many bytes as the String variable currently holds, so set its length first (for example with SPACE$ or by declaring it as STRING * n).
+Dim buf As String
+Open "raw.bin" For Binary As #1
+buf = Space$(16)
+Get #1, 1, buf ' read bytes 1-16
+Put #1, 33, "TAG" ' write 3 bytes at position 33
+Close #1
Sets the file position. As a function, returns the current position.
SEEK #channel, position ' Statement: set position
@@ -1812,23 +1833,24 @@ WEND
CHR$(n) String Character with ASCII code n
FORMAT$(value, fmt$) String Formats a numeric value using a format string
HEX$(n) String Hexadecimal representation of n (uppercase, no leading &H)
- INSTR(s$, find$) Integer Position of find$ in s$ (1-based), 0 if not found
- INSTR(start, s$, find$) Integer Search starting at position start (1-based)
+ INSTR(s$, find$) Long Position of find$ in s$ (1-based), 0 if not found
+ INSTR(start, s$, find$) Long Search starting at position start (1-based)
LCASE$(s$) String Converts s$ to lowercase
LEFT$(s$, n) String Leftmost n characters of s$
- LEN(s$) Integer Length of s$ in characters
+ LEN(s$) Long Length of s$ in characters
LTRIM$(s$) String Removes leading spaces from s$
MID$(s$, start) String Substring from start (1-based) to end of string
MID$(s$, start, length) String Substring of length characters starting at start
OCT$(n) String Octal representation of n (no leading &O)
RIGHT$(s$, n) String Rightmost n characters of s$
RTRIM$(s$) String Removes trailing spaces from s$
- SPACE$(n) String String of n spaces
+ SPACE$(n) String String of n spaces (n up to 65535)
STR$(n) String Converts number n to string (leading space for non-negative)
- STRING$(n, char) String String of n copies of char (char can be an ASCII code or single-character string)
+ STRING$(n, char) String String of n copies of char (char can be an ASCII code or single-character string; n up to 65535)
TRIM$(s$) String Removes leading and trailing spaces from s$
UCASE$(s$) String Converts s$ to uppercase
VAL(s$) Double Converts string s$ to a numeric value; stops at first non-numeric character
+A negative count or position passed to LEFT$, RIGHT$, MID$, SPACE$ or STRING$, or a count above 65535 passed to SPACE$ or STRING$, raises error 5 (Illegal function call).
FORMAT$ formats a numeric value using a BASIC-style format string. The format characters are the same as the ones used by PRINT USING.
s$ = FORMAT$(value, fmt$)
@@ -1883,8 +1905,8 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background
-------- ------- -----------
CBOOL(n) Boolean Returns True (-1) if n is nonzero or a non-empty string; False (0) otherwise
CDBL(n) Double Converts n to Double
- CINT(n) Integer Converts n to Integer (rounds half away from zero)
- CLNG(n) Long Converts n to Long
+ CINT(n) Integer Converts n to Integer; halves round to the nearest even number (2.5 -> 2, 3.5 -> 4); error 6 (Overflow) outside -32768 to 32767
+ CLNG(n) Long Converts n to Long with the same rounding; error 6 (Overflow) outside the Long range
CSNG(n) Single Converts n to Single
CSTR(n) String Converts n to its String representation
diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html
index bb45760..3bf17b2 100644
--- a/docs/dvx_system_reference.html
+++ b/docs/dvx_system_reference.html
@@ -354,7 +354,6 @@ img { max-width: 100%; }
Explicit use (e.g. in the Task Manager) can include dvxMem.h to call:
Function Purpose -------- ------- - dvxMemSnapshotLoad Baseline a newly-loaded app's memory state dvxMemGetAppUsage Query current bytes allocated for an app dvxMemResetApp Free every tracked allocation charged to an app
The dvxMemAppIdPtr pointer is set by the shell to &ctx->currentAppId so the allocator always knows which app to charge.
@@ -2127,13 +2124,6 @@ prefsClose(h); a, b Input rectangles result Output: intersection rectangle (valid only when return is true)Returns: true if the rectangles overlap, false if disjoint.
-bool rectIsEmpty(const RectT *r);
-Test whether a rectangle has zero or negative area.
-Parameter Description - --------- ----------- - r Rectangle to test-
Returns: true if w <= 0 or h <= 0.
void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t orient, int32_t mx, int32_t my);
+void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, ScrollbarOrientE orient, int32_t mx, int32_t my);
Handle an initial click on a scrollbar. Determines what was hit (arrows, trough, or thumb) and either adjusts the value immediately or begins a thumb drag.
Parameter Description --------- ----------- stack Window stack dl Dirty list idx Stack index of window - orient SCROLL_VERTICAL or SCROLL_HORIZONTAL + orient ScrollbarVerticalE or ScrollbarHorizontalE mx, my Click screen coordinates
void wmScrollbarDrag(WindowStackT *stack, DirtyListT *dl, int32_t mx, int32_t my);
@@ -3818,12 +3808,6 @@ if (r == ID_YES) { saveFile(); }
char *dvxStrdup(const char *s);
Tracked strdup.
void dvxMemSnapshotLoad(int32_t appId);
-Record a baseline memory snapshot for the given app. Called right before app code starts so later calls to dvxMemGetAppUsage can report net growth.
-Parameter Description - --------- ----------- - appId App ID to snapshot
uint32_t dvxMemGetAppUsage(int32_t appId);
Return the total bytes currently charged to the given app ID.
@@ -4126,6 +4110,9 @@ bool platformKeyUpRead(PlatformKeyEventT *evt);const char *platformPathBaseName(const char *path);
Return a pointer to the leaf (basename) portion of path.
+bool platformCopyFile(const char *srcPath, const char *dstPath);
+Byte-for-byte file copy. Returns false on any open, read, or write failure; a partially written destination is removed so no truncated copy is left behind.
char *platformReadFile(const char *path, int32_t *outLen);
Slurp a whole file into a freshly malloc'd, NUL-terminated buffer. Caller frees. Binary-safe: the NUL is past the end of the reported length.
@@ -5622,9 +5609,7 @@ BasValueT basValToBool(BasValueT v); 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. + basStringCompareCI(a, b) Case-insensitive compare.The global basEmptyString is a singleton that is never freed.
Reference-counted multi-dimensional array (up to BAS_ARRAY_MAX_DIMS = 8 dimensions).
@@ -5680,20 +5665,18 @@ BasValueT basValToBool(BasValueT v); ---- ----- ----------- 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). + BAS_VM_ERROR 2 Runtime error. + BAS_VM_STACK_OVERFLOW 3 Evaluation stack overflow. + BAS_VM_STACK_UNDERFLOW 4 Evaluation stack underflow. + BAS_VM_CALL_OVERFLOW 5 Call stack overflow. + BAS_VM_DIV_BY_ZERO 6 Division by zero. + BAS_VM_TYPE_MISMATCH 7 Type mismatch in operation. + BAS_VM_OUT_OF_MEMORY 8 Memory allocation failed. + BAS_VM_BAD_OPCODE 9 Unknown opcode encountered. + BAS_VM_FILE_ERROR 10 File I/O error. + BAS_VM_SUBSCRIPT_RANGE 11 Array subscript out of range. + BAS_VM_STEP_LIMIT 12 Step limit reached (not an error). + BAS_VM_BREAKPOINT 13 Breakpoint or step completed (not an error).BasVmT *basVmCreate(void);
void basVmDestroy(BasVmT *vm);
diff --git a/src/apps/kpunch/dvxbasic/Makefile b/src/apps/kpunch/dvxbasic/Makefile
index d17de9a..bbcdcb7 100644
--- a/src/apps/kpunch/dvxbasic/Makefile
+++ b/src/apps/kpunch/dvxbasic/Makefile
@@ -34,7 +34,9 @@
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.
+WARNFLAGS = -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation
+DVX_INCLUDES = -I../../../libs/kpunch/libdvx -I../../../libs/kpunch/libdvx/platform -I../../../libs/kpunch/libdvx/thirdparty -I.
+CFLAGS = -O2 $(WARNFLAGS) -march=i486 -mtune=i586 $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks -MMD -MP
OBJDIR = ../../../../obj/dvxbasic
LIBSDIR = ../../../../bin/libs
@@ -61,7 +63,11 @@ 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
+HOSTCFLAGS = -O2 $(WARNFLAGS) -Wno-stringop-truncation -D_GNU_SOURCE $(DVX_INCLUDES)
+
+# Every header a host harness can pull in; listed as a prerequisite so a
+# header edit rebuilds the harness instead of leaving a stale binary.
+HEADERS = $(wildcard *.h compiler/*.h runtime/*.h formrt/*.h)
TEST_COMPILER = $(HOSTDIR)/test_compiler
TEST_VM = $(HOSTDIR)/test_vm
@@ -77,7 +83,7 @@ TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c runtime/serialize.c
TEST_LEX_SRCS = test_lex.c compiler/lexer.c
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_COMPACT_SRCS = test_compact.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
-TEST_SUITE_SRCS = test_suite.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
+TEST_SUITE_SRCS = test_suite.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 $(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)
@@ -85,7 +91,7 @@ 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.
+DOSCFLAGS = -O2 $(WARNFLAGS) -march=i486 -mtune=i586 $(DVX_INCLUDES)
EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff
CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE
SYSTEMDIR = ../../../../bin/system
@@ -107,37 +113,40 @@ tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(T
$(TEST_COMPACT)
$(TEST_SUITE)
-$(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(HOSTDIR)
+$(TEST_COMPILER): $(TEST_COMPILER_SRCS) $(HEADERS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm
-$(TEST_SUITE): $(TEST_SUITE_SRCS) | $(HOSTDIR)
+$(TEST_SUITE): $(TEST_SUITE_SRCS) $(HEADERS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_SUITE_SRCS) -lm
-$(TEST_VM): $(TEST_VM_SRCS) | $(HOSTDIR)
+$(TEST_VM): $(TEST_VM_SRCS) $(HEADERS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_VM_SRCS) -lm
-$(TEST_LEX): $(TEST_LEX_SRCS) | $(HOSTDIR)
+$(TEST_LEX): $(TEST_LEX_SRCS) $(HEADERS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -w -o $@ $(TEST_LEX_SRCS) -lm
-$(TEST_QUICK): $(TEST_QUICK_SRCS) | $(HOSTDIR)
+$(TEST_QUICK): $(TEST_QUICK_SRCS) $(HEADERS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_QUICK_SRCS) -lm
-$(TEST_COMPACT): $(TEST_COMPACT_SRCS) | $(HOSTDIR)
+$(TEST_COMPACT): $(TEST_COMPACT_SRCS) $(HEADERS) | $(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) ../../../tools/dvxResWrite.h $(STUB_TARGET) $(DVXRES) | $(HOSTDIR)
+# resource so bascomp is self-contained (no BASSTUB.APP companion file),
+# and noicon.bmp as the ICON32 fallback for projects without an icon.
+$(BASCOMP_TARGET): $(BASCOMP_SRCS) $(HEADERS) ../../../tools/dvxResWrite.h $(STUB_TARGET) noicon.bmp $(DVXRES) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $@ $(BASCOMP_SRCS) -lm
$(DVXRES) add $@ STUB binary @$(STUB_TARGET)
+ $(DVXRES) add $@ noicon icon @noicon.bmp
-# DOS command-line compiler (same STUB embed as the host build)
-$(SYSTEMDIR)/BASCOMP.EXE: $(BASCOMP_SRCS) ../../../tools/dvxResWrite.h $(STUB_TARGET) $(DVXRES) | $(SYSTEMDIR)
+# DOS command-line compiler (same STUB / noicon embed as the host build)
+$(SYSTEMDIR)/BASCOMP.EXE: $(BASCOMP_SRCS) $(HEADERS) ../../../tools/dvxResWrite.h $(STUB_TARGET) noicon.bmp $(DVXRES) | $(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)
+ $(DVXRES) add $@ noicon icon @noicon.bmp
$(HOSTDIR):
mkdir -p $(HOSTDIR)
@@ -154,7 +163,7 @@ $(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)
+$(STUB_TARGET): $(STUB_OBJS) | $(OBJDIR)
$(DXE3GEN) -o $@ -U $(STUB_OBJS)
# IDE app DXE (compiler linked in, runtime from basrt.lib, stub embedded)
@@ -164,66 +173,28 @@ $(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) $(STUB_TARGET) dvxbasic.res | $(APPDIR)
$(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)
+# Object files. One pattern rule per source directory; header
+# dependencies come from the compiler (-MMD -MP writes a .d next to each
+# object) so no hand-written list can drift out of date.
+$(OBJDIR)/%.o: compiler/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
-$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h formrt/frmParser.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
+$(OBJDIR)/%.o: runtime/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
-$(OBJDIR)/frmParser.o: formrt/frmParser.c formrt/frmParser.h | $(OBJDIR)
+$(OBJDIR)/%.o: formrt/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
-$(OBJDIR)/serialize.o: runtime/serialize.c runtime/serialize.h runtime/vm.h | $(OBJDIR)
+$(OBJDIR)/%.o: ide/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
-$(OBJDIR)/strip.o: compiler/strip.c compiler/strip.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
+$(OBJDIR)/%.o: stub/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
-$(OBJDIR)/obfuscate.o: compiler/obfuscate.c compiler/obfuscate.h runtime/vm.h runtime/values.h | $(OBJDIR)
+$(OBJDIR)/%.o: %.c | $(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 $@ $<
+-include $(wildcard $(OBJDIR)/*.d)
# Directories
$(OBJDIR):
@@ -239,5 +210,5 @@ $(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)
+ rm -rf $(RT_OBJS) $(COMP_OBJS) $(IDE_OBJS) $(STUB_OBJS) $(OBJDIR)/*.d $(RT_TARGET) $(APP_TARGET) $(STUB_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR)
+ rm -f $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(TEST_SUITE)
diff --git a/src/apps/kpunch/dvxbasic/basBuild.c b/src/apps/kpunch/dvxbasic/basBuild.c
index 2496edd..96e7563 100644
--- a/src/apps/kpunch/dvxbasic/basBuild.c
+++ b/src/apps/kpunch/dvxbasic/basBuild.c
@@ -20,36 +20,67 @@
// 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
+// basBuild.c -- shared "compile a project into an .app" back end
//
-// 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
+// See basBuild.h for the public contract. This pipeline 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.
+// were essentially identical, so they are 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)
+// Pipeline:
+// 1. Strip comments from every .frm text.
+// 2. Serialize the module; release builds deserialize a private copy,
+// strip its debug info, obfuscate form/control names (rewriting the
+// .frm texts too), compact the bytecode and re-serialize.
+// 3. Debug builds serialize the debug info.
+// 4. Extract the STUB resource from selfPath and write it as outPath.
+// 5. Append the resource set in canonical order:
+// BAS_RES_NAME (always, falls back to "BASIC App")
+// BAS_RES_AUTHOR / PUBLISHER / VERSION / COPYRIGHT / DESCRIPTION
+// BAS_RES_ICON32 (project icon, else the NOICON fallback)
+// BAS_RES_HELPFILE (basename only)
+// BAS_RES_MODULE, BAS_RES_DEBUG (debug builds)
+// FORM0, FORM1, ... cut at the end of the outer Begin Form block
+// so the .frm's BASIC code section (already compiled into
+// MODULE) never ships as a resource
+// 6. Copy the help file next to outPath.
#include "basBuild.h"
#include "basRes.h"
-#include "../../../libs/kpunch/libdvx/dvxRes.h"
-#include "../../../libs/kpunch/libdvx/platform/dvxPlat.h"
+#include "compiler/compact.h"
+#include "compiler/obfuscate.h"
+#include "compiler/strip.h"
+#include "runtime/serialize.h"
+#include "dvxRes.h"
+#include "dvxTypes.h"
+#include "dvxPlat.h"
+#include "stb_ds_wrap.h"
+
+#include
#include
#include
#include
+// Slack added to a .frm buffer for basStripFrmComments output.
+#define BAS_BUILD_STRIP_MARGIN 16
-// ------------------------------------------------------------
-// Internal helpers
-// ------------------------------------------------------------
+// Longest progress line handed to the log callback.
+#define BAS_BUILD_LOG_BUF 128
+
+// Separator used when joining project-relative paths. '/' is accepted by
+// DJGPP as well as every host OS bascomp runs on; DVX_PATH_SEP is not.
+#define BAS_BUILD_SEP "/"
+
+// Function prototypes (alphabetical)
+static int32_t appendText(const char *path, const char *name, const char *value);
+const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec);
+static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...);
+static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec);
+static int32_t emitIcon(const char *outPath, const BasBuildSpecT *spec);
+static int32_t emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount);
+static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms, int32_t frmCount, uint8_t **outMod, int32_t *outModLen, uint8_t **outDbg, int32_t *outDbgLen);
+static const char *writeStub(const char *outPath, const BasBuildSpecT *spec);
static int32_t appendText(const char *path, const char *name, const char *value) {
@@ -61,45 +92,165 @@ static int32_t appendText(const char *path, const char *name, const char *value)
}
-static int32_t 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);
+const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) {
+ if (!outPath || !spec || !spec->module || !spec->selfPath) {
+ return "Invalid build request.";
+ }
- if (!iconData) {
- // A named icon that cannot be read is a build failure --
- // returning success here would ship the app without its
- // ICON32 resource and never tell anyone.
- return -1;
+ // Strip comments from every .frm text. Comments are source-only and
+ // must not ship in the embedded resource for either build mode.
+ uint8_t **frmData = NULL; // stb_ds: stripped form text
+ int32_t *frmLens = NULL; // stb_ds: length of stripped form text
+ BasObfFrmT *obfFrms = NULL; // stb_ds: obfuscated variants (release)
+
+ for (int32_t i = 0; i < spec->frmCount; i++) {
+ const char *src = spec->frmSources[i];
+
+ if (!src) {
+ continue;
}
- int32_t rc = dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
- free(iconData);
- return rc;
+ int32_t srcLen = (int32_t)strlen(src);
+ int32_t stripCap = srcLen + BAS_BUILD_STRIP_MARGIN;
+ uint8_t *stripped = (uint8_t *)malloc(stripCap);
+
+ if (!stripped) {
+ continue;
+ }
+
+ int32_t strippedLen = basStripFrmComments(src, srcLen, stripped, stripCap);
+ BasObfFrmT empty = { NULL, 0 };
+
+ arrput(frmData, stripped);
+ arrput(frmLens, strippedLen);
+ arrput(obfFrms, empty);
}
- if (spec->iconData && spec->iconSize > 0) {
- return dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, spec->iconData, (uint32_t)spec->iconSize);
+ int32_t frmCount = (int32_t)arrlen(frmData);
+ uint8_t *modData = NULL;
+ int32_t modLen = 0;
+ uint8_t *dbgData = NULL;
+ int32_t dbgLen = 0;
+ const char *failure = prepareModule(spec, frmData, frmLens, obfFrms, frmCount, &modData, &modLen, &dbgData, &dbgLen);
+
+ if (!failure) {
+ failure = writeStub(outPath, spec);
}
- return 0;
+ if (!failure && emitResources(outPath, spec, modData, modLen, dbgData, dbgLen, frmData, frmLens, obfFrms, frmCount) != 0) {
+ failure = "Failed writing resources to output file.";
+ }
+
+ if (!failure) {
+ failure = copyHelpFile(outPath, spec);
+ }
+
+ free(modData);
+ free(dbgData);
+
+ for (int32_t i = 0; i < frmCount; i++) {
+ free(frmData[i]);
+ free(obfFrms[i].data);
+ }
+
+ arrfree(frmData);
+ arrfree(frmLens);
+ arrfree(obfFrms);
+
+ return failure;
}
-// ------------------------------------------------------------
-// Public API
-// ------------------------------------------------------------
-
-
-int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec) {
- if (!outPath || !spec) {
- return -1;
+static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...) {
+ if (!spec->log) {
+ return;
}
- // Accumulate every append/write result so a disk-full or I/O failure on
- // any resource is reported instead of being silently treated as success.
+ char msg[BAS_BUILD_LOG_BUF];
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(msg, sizeof(msg), fmt, ap);
+ va_end(ap);
+ spec->log(msg);
+}
+
+
+// Copy the project's help file next to the output app; the HELPFILE
+// resource itself names only the basename, which the stub resolves
+// relative to the app.
+static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec) {
+ if (!spec->helpFile || !spec->helpFile[0]) {
+ return NULL;
+ }
+
+ char helpSrc[DVX_MAX_PATH];
+ snprintf(helpSrc, sizeof(helpSrc), "%s" BAS_BUILD_SEP "%s", spec->projectDir, spec->helpFile);
+
+ char outDir[DVX_MAX_PATH];
+ snprintf(outDir, sizeof(outDir), "%s", outPath);
+ char *sep = platformPathDirEnd(outDir);
+
+ if (sep) {
+ *sep = '\0';
+ } else {
+ outDir[0] = '.';
+ outDir[1] = '\0';
+ }
+
+ char helpDst[DVX_MAX_PATH];
+ snprintf(helpDst, sizeof(helpDst), "%s" BAS_BUILD_SEP "%s", outDir, platformPathBaseName(spec->helpFile));
+
+ if (!platformCopyFile(helpSrc, helpDst)) {
+ return "Failed copying help file to output directory.";
+ }
+
+ return NULL;
+}
+
+
+// Embed the project icon, or the NOICON fallback carried by the running
+// executable when the project names none.
+static int32_t emitIcon(const char *outPath, const BasBuildSpecT *spec) {
+ char iconPath[DVX_MAX_PATH];
+ void *iconData = NULL;
+ int32_t iconLen = 0;
+
+ if (spec->iconPath && spec->iconPath[0]) {
+ snprintf(iconPath, sizeof(iconPath), "%s" BAS_BUILD_SEP "%s", spec->projectDir, spec->iconPath);
+ iconData = platformReadFile(iconPath, &iconLen);
+
+ if (!iconData) {
+ // A named icon that cannot be read is a build failure --
+ // shipping the app without its ICON32 resource would never
+ // tell anyone.
+ return -1;
+ }
+ } else {
+ DvxResHandleT *selfRes = dvxResOpen(spec->selfPath);
+
+ if (selfRes) {
+ uint32_t size = 0;
+
+ iconData = dvxResRead(selfRes, BAS_RES_NOICON, &size);
+ iconLen = (int32_t)size;
+ dvxResClose(selfRes);
+ }
+
+ if (!iconData) {
+ return 0;
+ }
+ }
+
+ int32_t rc = dvxResAppend(outPath, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
+ free(iconData);
+ return rc;
+}
+
+
+static int32_t emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount) {
+ // Accumulate every append result so a disk-full or I/O failure on any
+ // resource is reported instead of being silently treated as success.
int32_t rc = 0;
// Project metadata. Name is required -- fall back to a generic label
@@ -113,35 +264,36 @@ int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec) {
rc |= appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright);
rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description);
- // Icon.
rc |= 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);
- rc |= appendText(outPath, BAS_RES_HELPFILE, helpBase);
+ rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile));
}
- // Bytecode module.
- if (spec->moduleData && spec->moduleLen > 0) {
- rc |= dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, spec->moduleData, (uint32_t)spec->moduleLen);
+ rc |= dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen);
+
+ if (dbgData && dbgLen > 0) {
+ rc |= dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen);
}
- // Optional debug info.
- if (spec->debugData && spec->debugLen > 0) {
- rc |= 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, ... Number the OUTPUT densely with
- // outIdx rather than the source index i: skipping an empty/NULL form
- // by the source index would leave a numbering gap (FORM0, FORM2, ...)
- // and the stub's reader stops at the first missing index, silently
- // dropping every later form.
+ // Form resources. Release builds embed the obfuscated variant when
+ // one was produced. Number the OUTPUT densely with outIdx rather
+ // than the source index: skipping an empty form by source index would
+ // leave a numbering gap (FORM0, FORM2, ...) and the stub's reader
+ // stops at the first missing index, silently dropping every later
+ // form.
int32_t outIdx = 0;
- for (int32_t i = 0; i < spec->formCount; i++) {
- if (!spec->formData || !spec->formData[i] || !spec->formLens || spec->formLens[i] <= 0) {
+ for (int32_t i = 0; i < frmCount; i++) {
+ const uint8_t *data = frmData[i];
+ int32_t len = frmLens[i];
+
+ if (spec->release && obfFrms[i].data) {
+ data = (const uint8_t *)obfFrms[i].data;
+ len = obfFrms[i].len;
+ }
+
+ if (!data || len <= 0) {
continue;
}
@@ -154,11 +306,105 @@ int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec) {
break;
}
- char resName[16];
+ char resName[BAS_RES_FORM_NAME_LEN];
+ int32_t formLen = basFindFormEndPos((const char *)data, len);
+
snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)outIdx);
- rc |= dvxResAppend(outPath, resName, DVX_RES_BINARY, spec->formData[i], (uint32_t)spec->formLens[i]);
+ rc |= dvxResAppend(outPath, resName, DVX_RES_BINARY, data, (uint32_t)formLen);
outIdx++;
}
return rc;
}
+
+
+// Serialize the module (and, for debug builds, its debug info). Release
+// builds run strip -> obfuscate -> compact on a private copy so the
+// caller's module stays intact; obfuscation also fills obfFrms.
+static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms, int32_t frmCount, uint8_t **outMod, int32_t *outModLen, uint8_t **outDbg, int32_t *outDbgLen) {
+ uint8_t *modData = basModuleSerialize(spec->module, outModLen);
+
+ if (!modData) {
+ return "Failed to serialize module.";
+ }
+
+ if (!spec->release) {
+ *outMod = modData;
+ *outDbg = basDebugSerialize(spec->module, outDbgLen);
+ return NULL;
+ }
+
+ BasModuleT *modCopy = basModuleDeserialize(modData, *outModLen);
+ free(modData);
+
+ if (!modCopy) {
+ return "Failed to prepare release module.";
+ }
+
+ basStripModule(modCopy);
+ buildLog(spec, " stripped debug info");
+
+ if (frmCount > 0) {
+ const char **frmTexts = NULL;
+
+ for (int32_t i = 0; i < frmCount; i++) {
+ arrput(frmTexts, (const char *)frmData[i]);
+ }
+
+ basObfuscateNames(modCopy, frmTexts, frmLens, frmCount, obfFrms);
+ arrfree(frmTexts);
+ buildLog(spec, " obfuscated %d form(s)", (int)frmCount);
+ }
+
+ int32_t removed = basCompactBytecode(modCopy);
+
+ if (removed > 0) {
+ buildLog(spec, " compacted bytecode (-%d bytes)", (int)removed);
+ }
+
+ *outMod = basModuleSerialize(modCopy, outModLen);
+ basModuleFree(modCopy);
+
+ if (!*outMod) {
+ return "Failed to serialize release module.";
+ }
+
+ return NULL;
+}
+
+
+// Extract the STUB resource from the running executable and write it as
+// the output file; every resource is appended behind it.
+static const char *writeStub(const char *outPath, const BasBuildSpecT *spec) {
+ DvxResHandleT *selfRes = dvxResOpen(spec->selfPath);
+
+ if (!selfRes) {
+ return "Cannot open own executable to read embedded stub.";
+ }
+
+ uint32_t stubSize = 0;
+ void *stubData = dvxResRead(selfRes, BAS_RES_STUB, &stubSize);
+ dvxResClose(selfRes);
+
+ if (!stubData || stubSize == 0) {
+ free(stubData);
+ return "STUB resource not found in own executable.";
+ }
+
+ FILE *outFile = fopen(outPath, "wb");
+
+ if (!outFile) {
+ free(stubData);
+ return "Cannot create output file.";
+ }
+
+ size_t stubWritten = fwrite(stubData, 1, stubSize, outFile);
+ int32_t stubClose = fclose(outFile);
+ free(stubData);
+
+ if (stubWritten != stubSize || stubClose != 0) {
+ return "Failed writing stub to output file.";
+ }
+
+ return NULL;
+}
diff --git a/src/apps/kpunch/dvxbasic/basBuild.h b/src/apps/kpunch/dvxbasic/basBuild.h
index e489362..5d80444 100644
--- a/src/apps/kpunch/dvxbasic/basBuild.h
+++ b/src/apps/kpunch/dvxbasic/basBuild.h
@@ -20,65 +20,68 @@
// 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
+// basBuild.h -- shared "compile a project into an .app" back end
//
-// 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.
+// Both the DVX BASIC IDE (ideMain.c, Make Executable) and the standalone
+// command-line compiler (bascomp.c) turn a compiled module plus a set of
+// .frm texts into a finished .app. The whole back half of that job --
+// release stripping, form/control name obfuscation, bytecode compaction,
+// serialization, stub extraction, resource emission, help-file copy and
+// the "noicon" fallback -- lives here so both front ends produce
+// byte-identical artifacts.
//
-// 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.
+// Callers own every buffer they pass in via BasBuildSpecT; the module is
+// never modified (release builds work on a private copy).
#ifndef BAS_BUILD_H
#define BAS_BUILD_H
+#include "runtime/vm.h"
+
+#include
#include
#ifdef __cplusplus
extern "C" {
#endif
+// Progress callback (may be NULL); receives one line per pipeline stage.
+typedef void (*BasBuildLogFnT)(const char *msg);
+
typedef struct {
- // Basic metadata (any NULL/empty is skipped).
+ // Project metadata (any NULL/empty is skipped; projName falls back
+ // to a generic label).
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;
+ // Project directory; iconPath and helpFile are relative to it.
+ const char *projectDir;
+ const char *iconPath; // NULL/empty: embed the NOICON fallback
+ const char *helpFile; // NULL/empty: no help file
- // Bytecode module and optional debug info.
- const void *moduleData;
- int32_t moduleLen;
- const void *debugData; // may be NULL
- int32_t debugLen;
+ // Path of the running executable; it carries the STUB and NOICON
+ // resources.
+ const char *selfPath;
- // 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;
+ // Compiled module. Never modified.
+ const BasModuleT *module;
+
+ // Raw .frm texts (NUL-terminated), one per form. Comments are
+ // stripped here; release builds additionally obfuscate them.
+ const char *const *frmSources;
+ int32_t frmCount;
+
+ bool release;
+ BasBuildLogFnT log; // may be NULL
} 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);
+// Build outPath from spec. Returns NULL on success, otherwise a static
+// error message. On failure outPath may be left partially written.
+const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec);
#ifdef __cplusplus
}
diff --git a/src/apps/kpunch/dvxbasic/basRes.h b/src/apps/kpunch/dvxbasic/basRes.h
index f2e82dd..6625746 100644
--- a/src/apps/kpunch/dvxbasic/basRes.h
+++ b/src/apps/kpunch/dvxbasic/basRes.h
@@ -87,11 +87,13 @@ const char *dvxSkipWs(const char *s);
// Stub DXE embedded in the IDE app for release builds
#define BAS_RES_STUB "STUB"
+#define BAS_RES_NOICON "noicon" // fallback ICON32 source in the IDE / bascomp
// Per-form binary resource name, e.g. FORM0, FORM1, ... Shared by the
// basBuild writer and the basstub reader so the naming cannot drift; %ld is
// the dense form index.
#define BAS_RES_FORM_FMT "FORM%ld"
+#define BAS_RES_FORM_NAME_LEN 16 // buffer for a formatted FORMn resource name
// ------------------------------------------------------------
// Form resource scan limit
@@ -105,8 +107,11 @@ const char *dvxSkipWs(const char *s);
// Begin-Form name extraction (shared by bascomp and basstub)
// ------------------------------------------------------------
-// Length of the "Begin Form " keyword prefix scanned for below.
-#define BAS_BEGIN_FORM_PREFIX_LEN 11
+// Lengths of the .frm keywords scanned for below.
+#define BAS_BEGIN_PREFIX_LEN 6 // "Begin "
+#define BAS_FORM_PREFIX_LEN 5 // "Form "
+#define BAS_BEGIN_FORM_PREFIX_LEN 11 // "Begin Form "
+#define BAS_END_KEYWORD_LEN 3 // "End"
// Extract the form name from a "Begin Form " line in .frm text.
// Writes a NUL-terminated name into nameBuf (max nameBufSize bytes,
@@ -157,4 +162,72 @@ static inline bool basExtractFormName(const char *frmText, char *nameBuf, int32_
return false;
}
+
+// ------------------------------------------------------------
+// Outer "Begin Form ... End" extent (shared by bascomp, basBuild and the
+// obfuscator)
+// ------------------------------------------------------------
+
+// Returns the offset just past the line holding the End that closes the
+// outermost Begin Form block in text[0..len), i.e. the length of the pure
+// form-definition part. Everything after it is the .frm's BASIC code
+// section. Returns len when no Begin Form block is found or closed.
+// Lines are delimited by LF or CR LF; an End keyword counts only when it
+// stands alone or is followed by whitespace.
+static inline int32_t basFindFormEndPos(const char *text, int32_t len) {
+ int32_t nesting = 0;
+ bool inForm = false;
+ int32_t pos = 0;
+
+ while (pos < len) {
+ int32_t lineStart = pos;
+
+ while (pos < len && text[pos] != '\n' && text[pos] != '\r') {
+ pos++;
+ }
+
+ int32_t lineEnd = pos;
+
+ if (pos < len && text[pos] == '\r') {
+ pos++;
+ }
+
+ if (pos < len && text[pos] == '\n') {
+ pos++;
+ }
+
+ int32_t l = lineStart;
+
+ while (l < lineEnd && (text[l] == ' ' || text[l] == '\t')) {
+ l++;
+ }
+
+ int32_t rest = lineEnd - l;
+
+ if (rest >= BAS_BEGIN_PREFIX_LEN && strncasecmp(text + l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) {
+ if (!inForm) {
+ int32_t r = l + BAS_BEGIN_PREFIX_LEN;
+
+ while (r < lineEnd && (text[r] == ' ' || text[r] == '\t')) {
+ r++;
+ }
+
+ if (lineEnd - r >= BAS_FORM_PREFIX_LEN && strncasecmp(text + r, "Form ", BAS_FORM_PREFIX_LEN) == 0) {
+ inForm = true;
+ }
+ }
+
+ nesting++;
+ } else if (rest >= BAS_END_KEYWORD_LEN && strncasecmp(text + l, "End", BAS_END_KEYWORD_LEN) == 0 && (rest == BAS_END_KEYWORD_LEN || text[l + BAS_END_KEYWORD_LEN] == ' ' || text[l + BAS_END_KEYWORD_LEN] == '\t')) {
+ nesting--;
+
+ if (inForm && nesting == 0) {
+ return pos;
+ }
+ }
+ }
+
+ return len;
+}
+
#endif // BAS_RES_H
diff --git a/src/apps/kpunch/dvxbasic/basrt.dhs b/src/apps/kpunch/dvxbasic/basrt.dhs
index f1bed22..a1b85f4 100644
--- a/src/apps/kpunch/dvxbasic/basrt.dhs
+++ b/src/apps/kpunch/dvxbasic/basrt.dhs
@@ -178,8 +178,6 @@ typedef struct {
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.
@@ -276,20 +274,18 @@ Result codes returned by basVmRun and basVmStep.
---- ----- -----------
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).
+ BAS_VM_ERROR 2 Runtime error.
+ BAS_VM_STACK_OVERFLOW 3 Evaluation stack overflow.
+ BAS_VM_STACK_UNDERFLOW 4 Evaluation stack underflow.
+ BAS_VM_CALL_OVERFLOW 5 Call stack overflow.
+ BAS_VM_DIV_BY_ZERO 6 Division by zero.
+ BAS_VM_TYPE_MISMATCH 7 Type mismatch in operation.
+ BAS_VM_OUT_OF_MEMORY 8 Memory allocation failed.
+ BAS_VM_BAD_OPCODE 9 Unknown opcode encountered.
+ BAS_VM_FILE_ERROR 10 File I/O error.
+ BAS_VM_SUBSCRIPT_RANGE 11 Array subscript out of range.
+ BAS_VM_STEP_LIMIT 12 Step limit reached (not an error).
+ BAS_VM_BREAKPOINT 13 Breakpoint or step completed (not an error).
.endtable
.h2 Lifecycle
diff --git a/src/apps/kpunch/dvxbasic/compiler/basEvents.h b/src/apps/kpunch/dvxbasic/compiler/basEvents.h
index 3ffaf67..c14c458 100644
--- a/src/apps/kpunch/dvxbasic/compiler/basEvents.h
+++ b/src/apps/kpunch/dvxbasic/compiler/basEvents.h
@@ -26,16 +26,94 @@
// 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.
+// dropdowns and generate handler stubs). Keep them in one place so adding
+// a new event only touches one file.
+//
+// BAS_EVENT_LIST is the master table. Each entry carries:
+// suffix -- the event name appended after "_" in the handler name
+// scope -- BasEventScopeE flags: which Object kinds fire it
+// params -- the handler's parameter list exactly as the runtime
+// passes it (formrt.c fireCtrlEvent / FireEventWithCancel),
+// "" for events with no arguments. Control-array handlers
+// receive an extra leading "Index As Integer".
+// Consumers expand it with an X macro so no parallel list can drift.
#ifndef BAS_EVENTS_H
#define BAS_EVENTS_H
#include
+#include
-// NULL-terminated list of event suffixes. Case-insensitive match.
-// Declared extern here, defined once in strip.c.
+typedef enum {
+ BAS_EVT_SCOPE_CTRL = 1 << 0, // fired on controls (widgets, Timer)
+ BAS_EVT_SCOPE_FORM = 1 << 1 // fired on the form itself
+} BasEventScopeE;
+
+#define BAS_EVT_SCOPE_BOTH (BAS_EVT_SCOPE_CTRL | BAS_EVT_SCOPE_FORM)
+
+// Event names. The runtime fires events by these names and the table
+// below is built from them, so a rename touches one line.
+#define BAS_EVT_LOAD "Load"
+#define BAS_EVT_UNLOAD "Unload"
+#define BAS_EVT_QUERYUNLOAD "QueryUnload"
+#define BAS_EVT_RESIZE "Resize"
+#define BAS_EVT_ACTIVATE "Activate"
+#define BAS_EVT_DEACTIVATE "Deactivate"
+#define BAS_EVT_CLICK "Click"
+#define BAS_EVT_DBLCLICK "DblClick"
+#define BAS_EVT_CHANGE "Change"
+#define BAS_EVT_TIMER "Timer"
+#define BAS_EVT_GOTFOCUS "GotFocus"
+#define BAS_EVT_LOSTFOCUS "LostFocus"
+#define BAS_EVT_KEYPRESS "KeyPress"
+#define BAS_EVT_KEYDOWN "KeyDown"
+#define BAS_EVT_KEYUP "KeyUp"
+#define BAS_EVT_MOUSEDOWN "MouseDown"
+#define BAS_EVT_MOUSEUP "MouseUp"
+#define BAS_EVT_MOUSEMOVE "MouseMove"
+#define BAS_EVT_SCROLL "Scroll"
+#define BAS_EVT_REPOSITION "Reposition"
+#define BAS_EVT_VALIDATE "Validate"
+
+#define BAS_EVENT_LIST(X) \
+ X(BAS_EVT_LOAD, BAS_EVT_SCOPE_FORM, "") \
+ X(BAS_EVT_UNLOAD, BAS_EVT_SCOPE_FORM, "") \
+ X(BAS_EVT_QUERYUNLOAD, BAS_EVT_SCOPE_FORM, "Cancel As Integer") \
+ X(BAS_EVT_RESIZE, BAS_EVT_SCOPE_FORM, "") \
+ X(BAS_EVT_ACTIVATE, BAS_EVT_SCOPE_FORM, "") \
+ X(BAS_EVT_DEACTIVATE, BAS_EVT_SCOPE_FORM, "") \
+ X(BAS_EVT_CLICK, BAS_EVT_SCOPE_CTRL, "") \
+ X(BAS_EVT_DBLCLICK, BAS_EVT_SCOPE_CTRL, "") \
+ X(BAS_EVT_CHANGE, BAS_EVT_SCOPE_CTRL, "") \
+ X(BAS_EVT_TIMER, BAS_EVT_SCOPE_CTRL, "") \
+ X(BAS_EVT_GOTFOCUS, BAS_EVT_SCOPE_CTRL, "") \
+ X(BAS_EVT_LOSTFOCUS, BAS_EVT_SCOPE_CTRL, "") \
+ X(BAS_EVT_KEYPRESS, BAS_EVT_SCOPE_BOTH, "KeyAscii As Integer") \
+ X(BAS_EVT_KEYDOWN, BAS_EVT_SCOPE_BOTH, "KeyCode As Integer, Shift As Integer") \
+ X(BAS_EVT_KEYUP, BAS_EVT_SCOPE_BOTH, "KeyCode As Integer, Shift As Integer") \
+ X(BAS_EVT_MOUSEDOWN, BAS_EVT_SCOPE_BOTH, "Button As Integer, X As Integer, Y As Integer") \
+ X(BAS_EVT_MOUSEUP, BAS_EVT_SCOPE_BOTH, "Button As Integer, X As Integer, Y As Integer") \
+ X(BAS_EVT_MOUSEMOVE, BAS_EVT_SCOPE_BOTH, "Button As Integer, X As Integer, Y As Integer") \
+ X(BAS_EVT_SCROLL, BAS_EVT_SCOPE_CTRL, "Delta As Integer") \
+ X(BAS_EVT_REPOSITION, BAS_EVT_SCOPE_CTRL, "") \
+ X(BAS_EVT_VALIDATE, BAS_EVT_SCOPE_CTRL, "Cancel As Integer")
+
+// One row of BAS_EVENT_LIST, for consumers that want a runtime table
+// (e.g. `static const BasEventInfoT sEvents[] = { BAS_EVENT_LIST(BAS_EVENT_ROW) };`).
+typedef struct {
+ const char *suffix;
+ uint8_t scope; // BasEventScopeE flags
+ const char *params; // handler parameter list, "" when none
+} BasEventInfoT;
+
+#define BAS_EVENT_ROW(suffix, scope, params) { suffix, scope, params },
+
+// Expands to just the suffix string followed by a comma, for a plain
+// NULL-terminated const char *[] view of the list.
+#define BAS_EVENT_SUFFIX(suffix, scope, params) suffix,
+
+// NULL-terminated list of event suffixes (BAS_EVENT_LIST order).
+// Case-insensitive match. Declared extern here, defined once in strip.c.
extern const char *basEventSuffixes[];
// True when suffix case-insensitively equals one of basEventSuffixes.
diff --git a/src/apps/kpunch/dvxbasic/compiler/codegen.c b/src/apps/kpunch/dvxbasic/compiler/codegen.c
index a1756da..9306d27 100644
--- a/src/apps/kpunch/dvxbasic/compiler/codegen.c
+++ b/src/apps/kpunch/dvxbasic/compiler/codegen.c
@@ -50,11 +50,18 @@ 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);
+static uint32_t constantHash(const char *text, int32_t len);
+static bool isEmittableProc(const BasSymbolT *s);
+static bool needsGlobalInit(const BasSymbolT *s);
uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) {
- // Check if this string is already in the pool
+ // Intern: compare the precomputed hash and length before touching the
+ // string bytes so the scan over the pool is a few integer compares per
+ // entry instead of a memcmp.
+ uint32_t h = constantHash(text, len);
+
for (int32_t i = 0; i < cg->constCount; i++) {
- if (cg->constants[i]->len == len && memcmp(cg->constants[i]->data, text, len) == 0) {
+ if (cg->constHashes[i] == h && cg->constants[i]->len == len && memcmp(cg->constants[i]->data, text, len) == 0) {
return (uint16_t)i;
}
}
@@ -67,6 +74,7 @@ uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) {
uint16_t idx = (uint16_t)cg->constCount;
BasStringT *s = basStringNew(text, len);
arrput(cg->constants, s);
+ arrput(cg->constHashes, h);
cg->constCount = (int32_t)arrlen(cg->constants);
return idx;
}
@@ -82,14 +90,14 @@ void 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) {
BasDebugVarT dv;
memset(&dv, 0, sizeof(dv));
- snprintf(dv.name, BAS_MAX_PROC_NAME, "%s", name);
+ snprintf(dv.name, BAS_MAX_IDENT, "%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);
+ snprintf(dv.formName, BAS_MAX_IDENT, "%s", formName);
}
arrput(cg->debugVars, dv);
cg->debugVarCount = (int32_t)arrlen(cg->debugVars);
@@ -97,6 +105,13 @@ void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uin
BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) {
+ // basVmLoadModule refuses a module with more globals than it has
+ // slots; never build one (the parser reports the error with a
+ // message, this is the last line of defence for other front ends).
+ if (cg->globalCount > BAS_VM_MAX_GLOBALS) {
+ return NULL;
+ }
+
BasModuleT *mod = (BasModuleT *)calloc(1, sizeof(BasModuleT));
if (!mod) {
@@ -232,11 +247,11 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
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) {
+ if (isEmittableProc(s)) {
procCount++;
}
- if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) {
+ if (needsGlobalInit(s)) {
globalInitCount++;
}
}
@@ -250,7 +265,7 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
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) {
+ if (needsGlobalInit(s)) {
mod->globalInits[gi].index = s->index;
mod->globalInits[gi].dataType = s->dataType;
gi++;
@@ -276,12 +291,12 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
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) {
+ if (isEmittableProc(s)) {
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';
+ strncpy(p->name, s->name, BAS_MAX_IDENT - 1);
+ p->name[BAS_MAX_IDENT - 1] = '\0';
+ strncpy(p->formName, s->formName, BAS_MAX_IDENT - 1);
+ p->formName[BAS_MAX_IDENT - 1] = '\0';
p->codeAddr = s->codeAddr;
p->paramCount = s->paramCount;
p->localCount = s->localCount;
@@ -324,6 +339,7 @@ void basCodeGenFree(BasCodeGenT *cg) {
arrfree(cg->code);
arrfree(cg->constants);
+ arrfree(cg->constHashes);
arrfree(cg->dataPool);
arrfree(cg->formVarInfo);
arrfree(cg->debugVars);
@@ -333,18 +349,10 @@ void basCodeGenFree(BasCodeGenT *cg) {
}
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;
+
+ // Leave the generator in the same state as basCodeGenInit so a reuse
+ // after Free starts clean (no stale counts or overflow flag).
+ basCodeGenInit(cg);
}
@@ -415,3 +423,30 @@ void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val) {
memcpy(&cg->code[pos], &val, 2);
}
}
+
+
+// FNV-1a over the constant's bytes (case-sensitive: constants are exact).
+static uint32_t constantHash(const char *text, int32_t len) {
+ uint32_t h = BAS_FNV1A_OFFSET;
+
+ for (int32_t i = 0; i < len; i++) {
+ h ^= (uint32_t)(uint8_t)text[i];
+ h *= BAS_FNV1A_PRIME;
+ }
+
+ return h;
+}
+
+
+// A SUB/FUNCTION that has a body in this module (not a forward stub or
+// a DECLARE LIBRARY extern) and therefore gets a proc-table entry.
+static bool isEmittableProc(const BasSymbolT *s) {
+ return (s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern;
+}
+
+
+// Global scalars that must start as a typed value rather than numeric 0
+// (currently STRING); recorded in the module's globalInits table.
+static bool needsGlobalInit(const BasSymbolT *s) {
+ return s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray;
+}
diff --git a/src/apps/kpunch/dvxbasic/compiler/codegen.h b/src/apps/kpunch/dvxbasic/compiler/codegen.h
index 01c318d..8da95e7 100644
--- a/src/apps/kpunch/dvxbasic/compiler/codegen.h
+++ b/src/apps/kpunch/dvxbasic/compiler/codegen.h
@@ -40,11 +40,6 @@
// Constant pool index is emitted as uint16_t; pool cannot exceed this.
#define BAS_MAX_CONSTANTS 0x10000
-// Jump offsets are signed 16-bit and call addresses are uint16_t; a module
-// larger than this would silently miscompile when those values wrap. This
-// conservative bound keeps every offset and address in range.
-#define BAS_MAX_CODE_SIZE 32767
-
// ============================================================
// Code generator state
// ============================================================
@@ -53,6 +48,7 @@ typedef struct {
uint8_t *code; // stb_ds dynamic array
int32_t codeLen;
BasStringT **constants; // stb_ds dynamic array
+ uint32_t *constHashes; // stb_ds dynamic array, parallel to constants (FNV-1a of the text)
int32_t constCount;
int32_t globalCount;
BasValueT *dataPool; // stb_ds dynamic array
diff --git a/src/apps/kpunch/dvxbasic/compiler/compact.c b/src/apps/kpunch/dvxbasic/compiler/compact.c
index b0ec3f4..1b14775 100644
--- a/src/apps/kpunch/dvxbasic/compiler/compact.c
+++ b/src/apps/kpunch/dvxbasic/compiler/compact.c
@@ -26,6 +26,11 @@
// each), and rewrites all code-address references so control flow
// still lands on the correct instructions.
//
+// A module that uses ON ERROR keeps its statement boundaries: every
+// OP_LINE is replaced by a 1-byte OP_STMT instead of being dropped, so
+// the VM's error dispatcher can still truncate the eval stack to the
+// failing statement and RESUME / RESUME NEXT land on statement starts.
+//
// Address references:
// - BasProcEntryT::codeAddr (absolute)
// - BasFormVarInfoT::initCodeAddr (absolute, negative = no init)
@@ -48,14 +53,15 @@
#define BAS_OPERAND_U16_MAX 0xFFFF // max absolute address encodable in a uint16 operand
+// Instruction sizes of the two statement-boundary encodings.
+#define BAS_LINE_INST_SIZE (1 + BAS_OPERAND_U16) // OP_LINE [uint16 lineNum]
+#define BAS_STMT_INST_SIZE 1 // OP_STMT
+
// 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 int32_t *buildRemap(const uint8_t *code, int32_t codeLen, bool keepStmt, int32_t *outNewLen);
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 codeLen, 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);
@@ -71,14 +77,16 @@ int32_t basCompactBytecode(BasModuleT *mod) {
const uint8_t *oldCode = mod->code;
int32_t oldCodeLen = mod->codeLen;
- // Count OP_LINE occurrences. If none, nothing to do.
+ // Count OP_LINE occurrences and note whether the module traps errors.
+ // If there are no OP_LINEs, nothing to do.
int32_t lineCount = 0;
+ bool keepStmt = false;
{
int32_t pc = 0;
while (pc < oldCodeLen) {
uint8_t op = oldCode[pc];
- int32_t operand = opOperandSize(op);
+ int32_t operand = basOpcodeOperandSize(op);
if (operand < 0 || pc + 1 + operand > oldCodeLen) {
return 0; // unknown opcode -- skip compaction
@@ -88,6 +96,10 @@ int32_t basCompactBytecode(BasModuleT *mod) {
lineCount++;
}
+ if (op == OP_ON_ERROR || op == OP_RESUME || op == OP_RESUME_NEXT) {
+ keepStmt = true;
+ }
+
pc += 1 + operand;
}
@@ -101,7 +113,7 @@ int32_t basCompactBytecode(BasModuleT *mod) {
}
int32_t newCodeLen = 0;
- int32_t *remap = buildRemap(oldCode, oldCodeLen, &newCodeLen);
+ int32_t *remap = buildRemap(oldCode, oldCodeLen, keepStmt, &newCodeLen);
if (!remap) {
return 0;
@@ -114,22 +126,25 @@ int32_t basCompactBytecode(BasModuleT *mod) {
return 0;
}
- // Copy bytes (skipping OP_LINE) and rewrite address operands.
- bool ok = true;
+ // Copy bytes (dropping or shrinking 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 operand = basOpcodeOperandSize(op);
int32_t instSize = 1 + operand;
+ int32_t newPc = remap[oldPc];
if (op == OP_LINE) {
+ if (keepStmt) {
+ newCode[newPc] = OP_STMT;
+ }
+
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);
@@ -198,8 +213,8 @@ int32_t basCompactBytecode(BasModuleT *mod) {
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 (basIsGosubPush(oldCode, oldCodeLen, oldPc)) {
+ int32_t oldAddr = basReadI32LE(oldCode + oldPc + 1);
if (oldAddr < 0 || oldAddr > oldCodeLen) {
ok = false;
@@ -295,12 +310,13 @@ int32_t basCompactBytecode(BasModuleT *mod) {
// ============================================================
//
// 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.
+// For OP_LINE bytes: when keepStmt is set the opcode byte maps to the
+// 1-byte OP_STMT that replaces it; otherwise (and for the operand bytes)
+// 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) {
+static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, bool keepStmt, int32_t *outNewLen) {
int32_t *remap = (int32_t *)malloc((codeLen + 1) * sizeof(int32_t));
if (!remap) {
@@ -312,7 +328,7 @@ static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, int32_t *outNew
while (oldPc < codeLen) {
uint8_t op = code[oldPc];
- int32_t operand = opOperandSize(op);
+ int32_t operand = basOpcodeOperandSize(op);
if (operand < 0) {
free(remap);
@@ -327,8 +343,16 @@ static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, int32_t *outNew
}
if (op == OP_LINE) {
- // These bytes are removed; they map to where the next instruction starts.
- for (int32_t i = 0; i < instSize; i++) {
+ // Dropped entirely, or shrunk to OP_STMT: the opcode byte maps
+ // to the boundary marker (if kept) and the operand bytes to the
+ // next instruction.
+ remap[oldPc] = newPc;
+
+ if (keepStmt) {
+ newPc += BAS_STMT_INST_SIZE;
+ }
+
+ for (int32_t i = 1; i < BAS_LINE_INST_SIZE; i++) {
remap[oldPc + i] = newPc;
}
} else {
@@ -353,160 +377,6 @@ static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, int32_t *outNew
}
-// ============================================================
-// 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:
-// 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.
-// This switch is the single machine-readable operand-size table. The
-// trailing comments in opcodes.h are human hints and must be kept in
-// sync with this function when adding opcodes.
-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)
// ============================================================
@@ -515,14 +385,6 @@ static int16_t readI16LE(const uint8_t *p) {
}
-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);
}
diff --git a/src/apps/kpunch/dvxbasic/compiler/lexer.c b/src/apps/kpunch/dvxbasic/compiler/lexer.c
index 71a8666..6290a03 100644
--- a/src/apps/kpunch/dvxbasic/compiler/lexer.c
+++ b/src/apps/kpunch/dvxbasic/compiler/lexer.c
@@ -28,8 +28,10 @@
// are handled transparently.
#include "lexer.h"
+#include "opcodes.h"
#include
+#include
#include
#include
#include
@@ -47,130 +49,130 @@ typedef struct {
#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),
+ 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("CONST", TOK_CONST),
+ KW("CREATECONTROL", TOK_CREATECONTROL),
+ KW("CREATEFORM", TOK_CREATEFORM),
+ KW("CURDIR", TOK_CURDIR),
+ KW("CURDIR$", TOK_CURDIR),
+ 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("EXIT", TOK_EXIT),
+ KW("EXPLICIT", TOK_EXPLICIT),
+ 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("INPUTBOX", TOK_INPUTBOX),
+ KW("INPUTBOX$", TOK_INPUTBOX),
+ 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("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("OPTION", TOK_OPTION),
+ KW("OPTIONAL", TOK_OPTIONAL),
+ 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 }
};
@@ -178,12 +180,38 @@ static const KeywordEntryT sKeywords[] = {
#define KEYWORD_COUNT (sizeof(sKeywords) / sizeof(sKeywords[0]) - 1)
+// ============================================================
+// Type-suffix table
+// ============================================================
+//
+// Single source of truth for the BASIC type-suffix characters and the
+// type each one implies; basIsTypeSuffixChar and basTypeSuffixType (and
+// through them the parser) all consult this table.
+
+typedef struct {
+ char suffix;
+ uint8_t dataType;
+} TypeSuffixEntryT;
+
+static const TypeSuffixEntryT sTypeSuffixes[] = {
+ { '%', BAS_TYPE_INTEGER },
+ { '&', BAS_TYPE_LONG },
+ { '!', BAS_TYPE_SINGLE },
+ { '#', BAS_TYPE_DOUBLE },
+ { '$', BAS_TYPE_STRING },
+};
+
+#define TYPE_SUFFIX_COUNT ((int32_t)(sizeof(sTypeSuffixes) / sizeof(sTypeSuffixes[0])))
+
// Function prototypes (alphabetical)
static char advance(BasLexerT *lex);
static void appendTokenChar(BasLexerT *lex, int32_t *idx, char c);
static bool atEnd(const BasLexerT *lex);
+char basAsciiUpper(char c);
+bool basIsIdentChar(char c);
bool basIsTypeSuffixChar(char c);
+bool basIsValidIdent(const char *name);
void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen);
const char *basLexerKeywordAt(int32_t i);
BasKeywordClassE basLexerKeywordClass(int32_t i);
@@ -191,6 +219,8 @@ int32_t basLexerKeywordCount(void);
BasTokenTypeE basLexerNext(BasLexerT *lex);
BasTokenTypeE basLexerPeek(const BasLexerT *lex);
const char *basTokenName(BasTokenTypeE type);
+int32_t basTypeSuffixType(char c);
+static bool lexIntegerLiteral(BasLexerT *lex, int64_t minVal, int64_t maxVal, const char *what, int64_t *outVal);
static BasTokenTypeE lookupKeyword(const char *text, int32_t len);
static BasTokenTypeE makeNewlineToken(BasLexerT *lex);
static char peek(const BasLexerT *lex);
@@ -202,7 +232,6 @@ 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)) {
@@ -211,7 +240,9 @@ static char advance(BasLexerT *lex) {
char c = lex->source[lex->pos++];
- if (c == '\n') {
+ // LF, or a CR that is not the first half of a CRLF pair, ends a line
+ // (the LF of a CRLF pair is counted when it is consumed).
+ if (c == '\n' || (c == '\r' && peek(lex) != '\n')) {
lex->line++;
lex->col = 1;
} else {
@@ -234,8 +265,37 @@ static bool atEnd(const BasLexerT *lex) {
}
+char basAsciiUpper(char c) {
+ if (c >= 'a' && c <= 'z') {
+ c -= ('a' - 'A');
+ }
+
+ return c;
+}
+
+
+bool basIsIdentChar(char c) {
+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_';
+}
+
+
bool basIsTypeSuffixChar(char c) {
- return c == '%' || c == '&' || c == '!' || c == '#' || c == '$';
+ return basTypeSuffixType(c) >= 0;
+}
+
+
+bool basIsValidIdent(const char *name) {
+ if (!name || !basIsIdentChar(name[0]) || (name[0] >= '0' && name[0] <= '9')) {
+ return false;
+ }
+
+ for (const char *p = name + 1; *p; p++) {
+ if (!basIsIdentChar(*p)) {
+ return false;
+ }
+ }
+
+ return true;
}
@@ -345,7 +405,7 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) {
// extension beyond classic QBASIC; it's convenient for bitmask
// work in the widget/graphics code.
if (c == '&') {
- char n = upperChar(peekNext(lex));
+ char n = basAsciiUpper(peekNext(lex));
if (n == 'H' || n == 'O' || n == 'B') {
lex->token.type = tokenizeHexLiteral(lex);
@@ -354,7 +414,7 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) {
}
// Identifier or keyword
- if (isalpha((unsigned char)c) || c == '_') {
+ if (basIsIdentChar(c) && !isdigit((unsigned char)c)) {
lex->token.type = tokenizeIdentOrKeyword(lex);
return lex->token.type;
}
@@ -520,11 +580,44 @@ const char *basTokenName(BasTokenTypeE type) {
}
+int32_t basTypeSuffixType(char c) {
+ for (int32_t i = 0; i < TYPE_SUFFIX_COUNT; i++) {
+ if (sTypeSuffixes[i].suffix == c) {
+ return sTypeSuffixes[i].dataType;
+ }
+ }
+
+ return -1;
+}
+
+
+// Convert the decimal digits in token.text to an integer and range-check
+// the result against [minVal, maxVal]. On overflow the token becomes
+// TOK_ERROR naming `what`, and false is returned. strtoll is used rather
+// than atol so the conversion is identical on the 32-bit DOS build and
+// the 64-bit host build of the compiler.
+static bool lexIntegerLiteral(BasLexerT *lex, int64_t minVal, int64_t maxVal, const char *what, int64_t *outVal) {
+ errno = 0;
+ long long val = strtoll(lex->token.text, NULL, 10);
+
+ if (errno == ERANGE || val < minVal || val > maxVal) {
+ char buf[BAS_LEX_ERROR_LEN];
+ snprintf(buf, sizeof(buf), "%s literal out of range", what);
+ setError(lex, buf);
+ lex->token.type = TOK_ERROR;
+ return false;
+ }
+
+ *outVal = (int64_t)val;
+ return true;
+}
+
+
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 = upperChar(text[0]);
+ char firstUp = basAsciiUpper(text[0]);
for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) {
const KeywordEntryT *kw = &sKeywords[i];
@@ -536,7 +629,7 @@ static BasTokenTypeE lookupKeyword(const char *text, int32_t len) {
bool match = true;
for (int32_t j = 1; j < len; j++) {
- if (upperChar(text[j]) != kw->text[j]) {
+ if (basAsciiUpper(text[j]) != kw->text[j]) {
match = false;
break;
}
@@ -642,7 +735,7 @@ static void skipWhitespace(BasLexerT *lex) {
static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
advance(lex); // skip &
- char base = upperChar(peek(lex));
+ char base = basAsciiUpper(peek(lex));
advance(lex); // skip H/O/B
int32_t shift;
@@ -705,8 +798,10 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
return TOK_ERROR;
}
- // Runtime INTEGER and LONG are both 32-bit; silently truncating a
- // wider literal would miscompile the constant, so reject it.
+ // The widest runtime integer (LONG) is 32 bits; silently truncating a
+ // wider literal would miscompile the constant, so reject it. Note the
+ // literal is NOT typed by width: &HFFFF is 65535 (a LONG), not QBASIC's
+ // 16-bit -1 -- the parser picks the push width from the value.
if (overflow) {
setError(lex, "Hexadecimal/octal/binary literal exceeds 32 bits");
lex->token.type = TOK_ERROR;
@@ -731,18 +826,20 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
int32_t idx = 0;
- while (!atEnd(lex) && (isalnum((unsigned char)peek(lex)) || peek(lex) == '_')) {
+ while (!atEnd(lex) && basIsIdentChar(peek(lex))) {
appendTokenChar(lex, &idx, advance(lex));
}
lex->token.text[idx] = '\0';
lex->token.textLen = idx;
- // Check for type suffix
+ // Check for type suffix. A keyword only ever takes '$' (CURDIR$,
+ // DIR$, INPUT$ ...); any other suffix after a keyword belongs to the
+ // next token, so PRINT#1 / CLOSE#1 / INPUT#1 lex as keyword + '#'.
if (!atEnd(lex)) {
char c = peek(lex);
- if (basIsTypeSuffixChar(c)) {
+ if (basIsTypeSuffixChar(c) && (c == '$' || lookupKeyword(lex->token.text, idx) == TOK_IDENT)) {
advance(lex);
appendTokenChar(lex, &idx, c);
lex->token.text[idx] = '\0';
@@ -815,7 +912,7 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
// always store 'E' in the buffer so atof() can parse it. Require at
// least one digit after the marker (and optional sign); otherwise the
// marker is not part of the number (e.g. "1E" tokenizes as 1 then E).
- char marker = upperChar(peek(lex));
+ char marker = basAsciiUpper(peek(lex));
if (!atEnd(lex) && (marker == 'E' || marker == 'D')) {
int32_t savedIdx = idx;
@@ -850,17 +947,28 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
// Check for type suffix
if (!atEnd(lex)) {
- char c = peek(lex);
+ char c = peek(lex);
+ int64_t val;
if (c == '%') {
advance(lex);
- lex->token.intVal = (int32_t)atoi(lex->token.text);
+
+ if (!lexIntegerLiteral(lex, INT16_MIN, INT16_MAX, "INTEGER", &val)) {
+ return TOK_ERROR;
+ }
+
+ lex->token.intVal = (int32_t)val;
return TOK_INT_LIT;
}
if (c == '&') {
advance(lex);
- lex->token.longVal = (int64_t)atol(lex->token.text);
+
+ if (!lexIntegerLiteral(lex, INT32_MIN, INT32_MAX, "LONG", &val)) {
+ return TOK_ERROR;
+ }
+
+ lex->token.longVal = val;
return TOK_LONG_LIT;
}
@@ -877,14 +985,21 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
return TOK_FLOAT_LIT;
}
- long val = atol(lex->token.text);
+ // Unsuffixed integers must fit LONG; anything wider would be silently
+ // truncated by the 32-bit runtime.
+ int64_t val;
+ int64_t maxVal = lex->allowNegMagnitude ? BAS_LONG_NEG_MAGNITUDE : INT32_MAX;
+
+ if (!lexIntegerLiteral(lex, INT32_MIN, maxVal, "Integer", &val)) {
+ return TOK_ERROR;
+ }
if (val >= INT16_MIN && val <= INT16_MAX) {
lex->token.intVal = (int32_t)val;
return TOK_INT_LIT;
}
- lex->token.longVal = (int64_t)val;
+ lex->token.longVal = val;
return TOK_LONG_LIT;
}
@@ -927,12 +1042,3 @@ static BasTokenTypeE tokenizeString(BasLexerT *lex) {
return TOK_STRING_LIT;
}
-
-
-static char upperChar(char c) {
- if (c >= 'a' && c <= 'z') {
- return c - 32;
- }
-
- return c;
-}
diff --git a/src/apps/kpunch/dvxbasic/compiler/lexer.h b/src/apps/kpunch/dvxbasic/compiler/lexer.h
index 144bb4d..f066795 100644
--- a/src/apps/kpunch/dvxbasic/compiler/lexer.h
+++ b/src/apps/kpunch/dvxbasic/compiler/lexer.h
@@ -212,6 +212,10 @@ typedef enum {
#define BAS_MAX_STRING_LEN (BAS_MAX_TOKEN_LEN - 1)
#define BAS_LEX_ERROR_LEN 256
+// Magnitude of LONG's most negative value (-2147483648). Only accepted
+// as a literal directly after unary minus, where its negation fits.
+#define BAS_LONG_NEG_MAGNITUDE ((int64_t)INT32_MAX + 1)
+
typedef struct {
BasTokenTypeE type;
int32_t line; // 1-based source line number
@@ -240,6 +244,7 @@ typedef struct {
int32_t col; // current column (1-based)
BasTokenT token; // current token
char error[BAS_LEX_ERROR_LEN];
+ bool allowNegMagnitude; // next integer literal may be BAS_LONG_NEG_MAGNITUDE (set by the parser after unary minus)
} BasLexerT;
// ============================================================
@@ -260,9 +265,26 @@ BasTokenTypeE basLexerPeek(const BasLexerT *lex);
// Return human-readable name for a token type.
const char *basTokenName(BasTokenTypeE type);
+// True when c may appear inside an identifier (ASCII letter, digit or
+// underscore; locale independent).
+bool basIsIdentChar(char c);
+
+// True when name is a well-formed identifier: starts with an ASCII
+// letter or underscore and contains only identifier characters.
+bool basIsValidIdent(const char *name);
+
// True when c is a BASIC type-suffix character (% & ! # $).
bool basIsTypeSuffixChar(char c);
+// Map a type-suffix character to its BAS_TYPE_* code, or -1 when c is
+// not a suffix character. The suffix table in lexer.c is the single
+// source of truth for both this and basIsTypeSuffixChar.
+int32_t basTypeSuffixType(char c);
+
+// Fold a single ASCII byte to upper case. Char in, char out (not int
+// via toupper) so bytes >= 0x80 pass through unchanged.
+char basAsciiUpper(char c);
+
// ============================================================
// Keyword iteration
// ============================================================
diff --git a/src/apps/kpunch/dvxbasic/compiler/obfuscate.c b/src/apps/kpunch/dvxbasic/compiler/obfuscate.c
index 1321149..59411e3 100644
--- a/src/apps/kpunch/dvxbasic/compiler/obfuscate.c
+++ b/src/apps/kpunch/dvxbasic/compiler/obfuscate.c
@@ -26,13 +26,19 @@
#include "obfuscate.h"
#include "basEvents.h"
+#include "lexer.h"
+#include "../basRes.h"
#include "../runtime/values.h"
-#include
#include
#include
#include
+#define BAS_OBF_TOKEN_LEN BAS_MAX_IDENT // longest form/control/type token kept from a .frm line
+#define BAS_OBF_MAPPED_LEN 16 // "C" + decimal index + NUL
+#define BAS_OBF_MAP_INIT_CAP 16 // initial name-map capacity (doubles on growth)
+#define BAS_REM_KEYWORD_LEN 3 // "REM"
+
// ============================================================
// Name map
// ============================================================
@@ -52,10 +58,8 @@ typedef struct {
// 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 isIdentChar(int c);
-static bool isValidIdent(const char *name);
+static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved);
+static void emitBytes(uint8_t *out, int32_t outCap, int32_t *outLen, const char *src, int32_t len);
static const char *nameMapAdd(NameMapT *m, const char *name);
static void nameMapFree(NameMapT *m);
static void nameMapInit(NameMapT *m);
@@ -76,17 +80,36 @@ void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr
return;
}
+ // Pass 1: collect all names from all .frm texts. A control whose name
+ // is also a control type or a property key (a TextBox named "Text", a
+ // Timer named "Timer") is left alone: the constant pool cannot tell a
+ // name reference apart from the identically spelled property/type name
+ // used by OP_LOAD_PROP and friends.
+ NameMapT names;
+ NameMapT reserved;
NameMapT map;
+
+ nameMapInit(&names);
+ nameMapInit(&reserved);
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);
+ collectNamesFromFrm(frmTexts[i], frmLens[i], &names, &reserved);
}
}
- // Pass 2: rewrite each .frm
+ for (int32_t i = 0; i < names.count; i++) {
+ if (!nameMapLookup(&reserved, names.entries[i].orig)) {
+ nameMapAdd(&map, names.entries[i].orig);
+ }
+ }
+
+ nameMapFree(&names);
+ nameMapFree(&reserved);
+
+ // Pass 2: rewrite each .frm, sized by a measuring pass so a form with
+ // many short names that grow ("a" -> "C12") can never be truncated.
for (int32_t i = 0; i < frmCount; i++) {
outFrms[i].data = NULL;
outFrms[i].len = 0;
@@ -95,12 +118,9 @@ void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr
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);
+ int32_t strippedLen = basFindFormEndPos(frmTexts[i], frmLens[i]);
+ int32_t outCap = rewriteFrmText(frmTexts[i], strippedLen, &map, NULL, 0) + 1;
+ uint8_t *outBuf = malloc(outCap);
if (!outBuf) {
continue;
@@ -109,7 +129,7 @@ void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr
int32_t outLen = rewriteFrmText(frmTexts[i], strippedLen, &map, outBuf, outCap);
// Ensure trailing newline
- if (outLen > 0 && outBuf[outLen - 1] != '\n' && outLen < outCap) {
+ if (outLen > 0 && outBuf[outLen - 1] != '\n') {
outBuf[outLen++] = '\n';
}
@@ -175,11 +195,11 @@ int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, in
firstNonWs++;
}
- if (contentEnd - firstNonWs >= 3 &&
- strncasecmp(src + firstNonWs, "REM", 3) == 0 &&
- (contentEnd - firstNonWs == 3 ||
- src[firstNonWs + 3] == ' ' ||
- src[firstNonWs + 3] == '\t')) {
+ if (contentEnd - firstNonWs >= BAS_REM_KEYWORD_LEN &&
+ strncasecmp(src + firstNonWs, "REM", BAS_REM_KEYWORD_LEN) == 0 &&
+ (contentEnd - firstNonWs == BAS_REM_KEYWORD_LEN ||
+ src[firstNonWs + BAS_REM_KEYWORD_LEN] == ' ' ||
+ src[firstNonWs + BAS_REM_KEYWORD_LEN] == '\t')) {
contentEnd = firstNonWs;
}
@@ -211,11 +231,13 @@ int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, in
// ============================================================
-// Pass 1: collect all form/control names from .frm texts
+// Pass 1: collect form/control names and the identifiers they must not
+// collide with
// ============================================================
-// Scan a .frm text and add all "Begin " names to the map.
-static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *map) {
+// Scan a .frm text: every "Begin " adds Name to names, and
+// Type plus every " = ..." property key goes into reserved.
+static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved) {
const char *p = text;
const char *end = text + len;
@@ -239,124 +261,55 @@ static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *map) {
// Trim leading whitespace
const char *l = skipWhitespace(lineStart, lineEnd);
+ char token[BAS_OBF_TOKEN_LEN];
+
+ if ((lineEnd - l) >= BAS_BEGIN_PREFIX_LEN && strncasecmp(l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) {
+ l = skipWhitespace(l + BAS_BEGIN_PREFIX_LEN, lineEnd);
+ l = readToken(l, lineEnd, token, sizeof(token));
+
+ if (token[0] == '\0') {
+ continue;
+ }
+
+ nameMapAdd(reserved, token);
+
+ l = skipWhitespace(l, lineEnd);
+ readToken(l, lineEnd, token, sizeof(token));
+
+ if (token[0] && basIsValidIdent(token)) {
+ nameMapAdd(names, token);
+ }
- // Check "Begin "
- if ((lineEnd - l) < 6 || strncasecmp(l, "Begin ", 6) != 0) {
continue;
}
- l += 6;
+ // " = value": the key is a property name.
+ l = readToken(l, lineEnd, token, sizeof(token));
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);
+ if (l < lineEnd && *l == '=' && token[0] && basIsValidIdent(token)) {
+ nameMapAdd(reserved, token);
}
}
}
// ============================================================
-// Pass 2: strip BASIC code from .frm text (everything after outer End)
+// Pass 2: rewrite .frm text with mapped names
// ============================================================
-// 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);
- }
- }
+// Appends len bytes of src to out (bounded by outCap) and always advances
+// *outLen, so a NULL out measures the exact output size.
+static void emitBytes(uint8_t *out, int32_t outCap, int32_t *outLen, const char *src, int32_t len) {
+ if (out && *outLen + len <= outCap) {
+ memcpy(out + *outLen, src, len);
}
- return len;
+ *outLen += len;
}
-// ============================================================
-// Pass 3: rewrite .frm text with mapped names
-// ============================================================
-
// Returns true if c is a valid identifier character.
-static bool isIdentChar(int c) {
- return isalnum(c) || c == '_';
-}
-
-
-// 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);
@@ -365,7 +318,7 @@ static const char *nameMapAdd(NameMapT *m, const char *name) {
}
if (m->count >= m->cap) {
- int32_t newCap = m->cap == 0 ? 16 : m->cap * 2;
+ int32_t newCap = m->cap == 0 ? BAS_OBF_MAP_INIT_CAP : m->cap * 2;
NameEntryT *newEntries = realloc(m->entries, newCap * sizeof(NameEntryT));
if (!newEntries) {
@@ -376,7 +329,7 @@ static const char *nameMapAdd(NameMapT *m, const char *name) {
m->cap = newCap;
}
- char mapped[16];
+ char mapped[BAS_OBF_MAPPED_LEN];
snprintf(mapped, sizeof(mapped), "C%ld", (long)(m->count + 1));
m->entries[m->count].orig = strdup(name);
@@ -449,67 +402,100 @@ static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText) {
}
-// 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).
+// Rewrites the .frm text line by line. Only positions that hold a
+// form/control NAME are remapped: the name token of a "Begin "
+// line and a property value that is exactly a mapped name (bare, or the
+// whole content of a quoted string such as DataSource = "datCat"). Type
+// tokens and property keys are copied verbatim. Identifiers of any length
+// are copied from the source span, never through a fixed buffer. With out
+// NULL nothing is written and the return value is the required size.
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;
+ int32_t outLen = 0;
+ const char *p = src;
+ const char *end = src + srcLen;
- while (i < srcLen) {
- char c = src[i];
+ while (p < end) {
+ const char *lineStart = p;
- if (c == '"') {
- inStr = !inStr;
-
- if (outLen < outCap) {
- out[outLen++] = (uint8_t)c;
- }
-
- i++;
- continue;
+ while (p < end && *p != '\n' && *p != '\r') {
+ p++;
}
- // Read identifier
- if (!inStr && (isalpha((unsigned char)c) || c == '_')) {
- int32_t identStart = i;
+ const char *lineEnd = p;
- while (i < srcLen && isIdentChar((unsigned char)src[i])) {
- i++;
+ if (p < end && *p == '\r') {
+ p++;
+ }
+
+ if (p < end && *p == '\n') {
+ p++;
+ }
+
+ const char *l = skipWhitespace(lineStart, lineEnd);
+ const char *replaceStart = NULL; // span of the original name to swap
+ const char *replaceEnd = NULL;
+ const char *mapped = NULL;
+ char token[BAS_OBF_TOKEN_LEN];
+
+ if ((lineEnd - l) >= BAS_BEGIN_PREFIX_LEN && strncasecmp(l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) {
+ // Begin : only the name token is a candidate.
+ const char *t = skipWhitespace(l + BAS_BEGIN_PREFIX_LEN, lineEnd);
+ t = readToken(t, lineEnd, token, sizeof(token));
+ t = skipWhitespace(t, lineEnd);
+
+ const char *nameStart = t;
+ t = readToken(t, lineEnd, token, sizeof(token));
+
+ if (token[0] && t - nameStart == (int32_t)strlen(token)) {
+ mapped = nameMapLookup(map, token);
+ replaceStart = nameStart;
+ replaceEnd = t;
}
+ } else {
+ // = value: a value that is exactly a mapped name (bare or
+ // quoted) refers to a control and follows the rename.
+ const char *t = readToken(l, lineEnd, token, sizeof(token));
+ t = skipWhitespace(t, lineEnd);
- int32_t identLen = i - identStart;
- char ident[128];
+ if (t < lineEnd && *t == '=') {
+ t = skipWhitespace(t + 1, lineEnd);
- if (identLen >= (int32_t)sizeof(ident)) {
- identLen = (int32_t)sizeof(ident) - 1;
- }
+ const char *valueStart = t;
+ const char *valueEnd = lineEnd;
- 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];
+ while (valueEnd > valueStart && (valueEnd[-1] == ' ' || valueEnd[-1] == '\t')) {
+ valueEnd--;
}
- } else {
- for (int32_t k = 0; k < identLen && outLen < outCap; k++) {
- out[outLen++] = (uint8_t)ident[k];
+
+ if (valueEnd - valueStart >= 2 && *valueStart == '"' && valueEnd[-1] == '"') {
+ valueStart++;
+ valueEnd--;
+ }
+
+ int32_t valueLen = (int32_t)(valueEnd - valueStart);
+ bool isIdent = (valueLen > 0 && valueLen < (int32_t)sizeof(token));
+
+ for (int32_t k = 0; isIdent && k < valueLen; k++) {
+ isIdent = basIsIdentChar(valueStart[k]);
+ }
+
+ if (isIdent) {
+ memcpy(token, valueStart, valueLen);
+ token[valueLen] = '\0';
+ mapped = nameMapLookup(map, token);
+ replaceStart = valueStart;
+ replaceEnd = valueEnd;
}
}
-
- continue;
}
- if (outLen < outCap) {
- out[outLen++] = (uint8_t)c;
+ if (mapped) {
+ emitBytes(out, outCap, &outLen, lineStart, (int32_t)(replaceStart - lineStart));
+ emitBytes(out, outCap, &outLen, mapped, (int32_t)strlen(mapped));
+ emitBytes(out, outCap, &outLen, replaceEnd, (int32_t)(p - replaceEnd));
+ } else {
+ emitBytes(out, outCap, &outLen, lineStart, (int32_t)(p - lineStart));
}
-
- i++;
}
return outLen;
@@ -524,7 +510,8 @@ static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) {
// separating name references from literals would give name refs their own
// pool slots and change emitted bytecode for every release build, so it is
// deferred; avoid string literals that exactly match a control/form name
- // in release builds.
+ // in release builds. Names that collide with a property key or control
+ // type are never mapped at all (see basObfuscateNames).
for (int32_t i = 0; i < mod->constCount; i++) {
const BasStringT *s = mod->constants[i];
@@ -536,6 +523,32 @@ static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) {
if (mapped) {
replaceConstant(mod, i, mapped);
+ continue;
+ }
+
+ // "_" handler names used as SetEvent targets follow the
+ // procedure rename so basModuleFindProc still resolves them.
+ const char *underscore = strrchr(s->data, '_');
+
+ if (!underscore || !basEventSuffixMatch(underscore + 1)) {
+ continue;
+ }
+
+ int32_t prefixLen = (int32_t)(underscore - s->data);
+ char prefix[BAS_MAX_IDENT];
+
+ if (prefixLen >= (int32_t)sizeof(prefix)) {
+ continue;
+ }
+
+ memcpy(prefix, s->data, prefixLen);
+ prefix[prefixLen] = '\0';
+ mapped = nameMapLookup(map, prefix);
+
+ if (mapped) {
+ char newName[BAS_MAX_IDENT];
+ snprintf(newName, sizeof(newName), "%s_%s", mapped, underscore + 1);
+ replaceConstant(mod, i, newName);
}
}
}
@@ -586,7 +599,7 @@ static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
// Split on underscore
int32_t prefixLen = (int32_t)(underscore - proc->name);
- char prefix[BAS_MAX_PROC_NAME];
+ char prefix[BAS_MAX_IDENT];
if (prefixLen >= (int32_t)sizeof(prefix)) {
prefixLen = (int32_t)sizeof(prefix) - 1;
@@ -598,7 +611,7 @@ static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
const char *mapped = nameMapLookup(map, prefix);
if (mapped) {
- char newName[BAS_MAX_PROC_NAME];
+ char newName[BAS_MAX_IDENT];
snprintf(newName, sizeof(newName), "%s_%s", mapped, suffix);
snprintf(proc->name, sizeof(proc->name), "%s", newName);
}
diff --git a/src/apps/kpunch/dvxbasic/compiler/obfuscate.h b/src/apps/kpunch/dvxbasic/compiler/obfuscate.h
index 5e59124..127aa50 100644
--- a/src/apps/kpunch/dvxbasic/compiler/obfuscate.h
+++ b/src/apps/kpunch/dvxbasic/compiler/obfuscate.h
@@ -41,10 +41,13 @@ typedef struct {
// 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
+// generates C1..Cn (skipping any name that is also a control type or a
+// property key, since bytecode cannot tell those apart), then rewrites:
+// - The .frm text (Begin-line names and property values naming a
+// control, stripping the trailing BASIC code section after the
+// outer form closes)
+// - Module string constants matching any original name, and
+// _ handler names used as SetEvent targets
// - Procedure names matching _
// - formVarInfo entries keyed by form name
//
diff --git a/src/apps/kpunch/dvxbasic/compiler/opcodes.h b/src/apps/kpunch/dvxbasic/compiler/opcodes.h
index 8c752c5..d0939ed 100644
--- a/src/apps/kpunch/dvxbasic/compiler/opcodes.h
+++ b/src/apps/kpunch/dvxbasic/compiler/opcodes.h
@@ -28,6 +28,9 @@
#ifndef DVXBASIC_OPCODES_H
#define DVXBASIC_OPCODES_H
+#include
+#include
+
// ============================================================
// Variable scope tags
// Emitted in bytecode (e.g. OP_FOR scopeTag byte) and consumed
@@ -70,6 +73,7 @@ typedef enum {
#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
+#define BAS_TYPE_ELEM_REF 10 // ByRef array element: counted BasArrayT* + flat index
// ============================================================
// Stack operations
@@ -78,13 +82,13 @@ typedef enum {
#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
+#define OP_STMT 0x0A // statement boundary without a line number (release builds that keep ON ERROR/RESUME semantics)
// ============================================================
// Variable access
@@ -94,8 +98,6 @@ typedef enum {
#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
@@ -119,11 +121,7 @@ typedef enum {
// 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 (^)
// ============================================================
@@ -181,7 +179,7 @@ typedef enum {
#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_NEXT 0x58 // [uint16 varIdx] [uint8 scope] [int16 loopTop]
#define OP_FOR_POP 0x59 // pop top FOR stack entry (for EXIT FOR)
// ============================================================
@@ -192,10 +190,8 @@ typedef enum {
#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_STR_FLT 0x65 // any -> double (VAL, CDBL)
#define OP_CONV_INT_LONG 0x66 // int16 -> int32
-#define OP_CONV_LONG_INT 0x67 // int32 -> int16
// ============================================================
// I/O
@@ -204,9 +200,8 @@ typedef enum {
#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_OPEN 0x75 // [uint8 mode] filename, channel#, record length 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
@@ -242,22 +237,15 @@ typedef enum {
#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 ¤tFormVars[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_REDIM 0x91 // [uint8 dims] [uint8 preserve] [uint8 type] bounds on stack (UDT: typeId, fieldCount after bounds)
#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
@@ -266,7 +254,9 @@ typedef enum {
#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
+#define OP_STORE_FORM_VAR 0x9B // [uint16 idx] pop, store to currentFormVars[idx]
+#define OP_PUSH_FORM_ADDR 0x9C // [uint16 idx] push ¤tFormVars[idx] (ByRef)
+#define OP_CREATE_CTRL_EX 0x9D // pop parentRef, pop name, pop type, pop formRef, push ctrlRef
// ============================================================
// Math built-ins (single opcode each for common functions)
@@ -284,7 +274,7 @@ typedef enum {
#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_MATH_RANDOMIZE 0xAC // seed on stack (BAS_RANDOMIZE_TIMER_SEED = seed from the clock)
#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
@@ -330,7 +320,7 @@ typedef enum {
// Random/Binary file I/O
// ============================================================
-#define OP_FILE_GET 0xBE // pop channel + recno, read record, push value
+#define OP_FILE_GET 0xBE // pop type (+ current string value for STRING) + recno + channel, 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
@@ -359,7 +349,7 @@ typedef enum {
#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)
+#define OP_COMPARE_MODE 0xCC // [uint8 mode] set string compare mode (BAS_COMPARE_MODE_*)
// ============================================================
// External library calls (DECLARE LIBRARY)
@@ -402,6 +392,11 @@ typedef enum {
// Debug
#define OP_LINE 0xEF // [uint16 lineNum] set current source line for debugger
+// Dynamic form construction
+#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
+
// ============================================================
// Halt
// ============================================================
@@ -409,4 +404,169 @@ typedef enum {
#define OP_END 0xFE // explicit END statement -- terminates program
#define OP_HALT 0xFF // implicit end of module
+// ============================================================
+// Operand-size table and bytecode pattern helpers
+// ============================================================
+//
+// Single machine-readable source of truth for the operand byte count of
+// every opcode (the trailing comments above are human hints). Shared by
+// the VM (RESUME NEXT statement walk) and the release compactor.
+
+
+// OP_COMPARE_MODE operand values (OPTION COMPARE BINARY / TEXT).
+#define BAS_COMPARE_MODE_BINARY 0
+#define BAS_COMPARE_MODE_TEXT 1
+
+// Seed value pushed by RANDOMIZE TIMER: OP_MATH_RANDOMIZE seeds from the
+// clock when it pops this value.
+#define BAS_RANDOMIZE_TIMER_SEED (-1)
+
+// Operand widths (bytes after the 1-byte opcode).
+#define BAS_OPERAND_NONE 0
+#define BAS_OPERAND_U8 1
+#define BAS_OPERAND_U16 2
+#define BAS_OPERAND_U8_U16 3
+#define BAS_OPERAND_I32 4
+#define BAS_OPERAND_FOR 5 // [uint16 varIdx] [uint8 scope] [int16 offset]
+#define BAS_OPERAND_CALL_EXTERN 6 // [uint16 lib] [uint16 func] [uint8 argc] [uint8 retType]
+#define BAS_OPERAND_F64 8
+#define BAS_OPERAND_UNKNOWN (-1)
+
+// GOSUB call sequence: OP_PUSH_INT32 OP_JMP , where
+// returnAddr equals the address right after the OP_JMP.
+#define BAS_GOSUB_PATTERN_LEN 8 // PUSH_INT32(1+4) + JMP(1+2)
+#define BAS_GOSUB_JMP_OFFSET 5 // byte offset of the OP_JMP inside the pattern
+
+// Returns operand byte count for op, or BAS_OPERAND_UNKNOWN.
+static inline int32_t basOpcodeOperandSize(uint8_t op) {
+ switch (op) {
+ case OP_NOP:
+ case OP_PUSH_TRUE: case OP_PUSH_FALSE:
+ case OP_POP: case OP_DUP: case OP_STMT:
+ 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_DIV_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_STR_FLT: case OP_CONV_INT_LONG:
+ 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_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 BAS_OPERAND_NONE;
+
+ case OP_LOAD_ARRAY: case OP_STORE_ARRAY:
+ case OP_PUSH_ARR_ADDR:
+ case OP_FILE_OPEN:
+ case OP_CALL_METHOD: case OP_SHOW_FORM:
+ case OP_LBOUND: case OP_UBOUND:
+ case OP_COMPARE_MODE:
+ return BAS_OPERAND_U8;
+
+ 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_LOAD_FORM_VAR: case OP_STORE_FORM_VAR:
+ case OP_PUSH_FORM_ADDR:
+ case OP_DIM_ARRAY:
+ case OP_ON_ERROR:
+ case OP_STR_FIXLEN:
+ case OP_LINE:
+ return BAS_OPERAND_U16;
+
+ case OP_STORE_ARRAY_FIELD:
+ case OP_REDIM:
+ return BAS_OPERAND_U8_U16;
+
+ case OP_PUSH_INT32:
+ case OP_CALL:
+ return BAS_OPERAND_I32;
+
+ case OP_FOR_INIT:
+ case OP_FOR_NEXT:
+ return BAS_OPERAND_FOR;
+
+ case OP_CALL_EXTERN:
+ return BAS_OPERAND_CALL_EXTERN;
+
+ case OP_PUSH_FLT64:
+ return BAS_OPERAND_F64;
+
+ default:
+ return BAS_OPERAND_UNKNOWN;
+ }
+}
+
+
+// Little-endian int32 operand read (bytecode is always LE regardless of host).
+static inline int32_t basReadI32LE(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));
+}
+
+
+// True when code[pos] starts a GOSUB call sequence (see BAS_GOSUB_PATTERN_LEN).
+static inline bool basIsGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos) {
+ if (pos + BAS_GOSUB_PATTERN_LEN > codeLen) {
+ return false;
+ }
+
+ if (code[pos] != OP_PUSH_INT32 || code[pos + BAS_GOSUB_JMP_OFFSET] != OP_JMP) {
+ return false;
+ }
+
+ return basReadI32LE(code + pos + 1) == pos + BAS_GOSUB_PATTERN_LEN;
+}
+
#endif // DVXBASIC_OPCODES_H
diff --git a/src/apps/kpunch/dvxbasic/compiler/parser.c b/src/apps/kpunch/dvxbasic/compiler/parser.c
index 5cdae1c..0b3f238 100644
--- a/src/apps/kpunch/dvxbasic/compiler/parser.c
+++ b/src/apps/kpunch/dvxbasic/compiler/parser.c
@@ -78,7 +78,7 @@ static const BuiltinFuncT builtinFuncs[] = {
// Conversion functions
{"CBOOL", OP_CONV_BOOL, 1, 1, BAS_TYPE_BOOLEAN},
- {"CDBL", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_DOUBLE},
+ {"CDBL", OP_CONV_STR_FLT, 1, 1, BAS_TYPE_DOUBLE}, // any value -> DOUBLE
{"CINT", OP_CONV_FLT_INT, 1, 1, BAS_TYPE_INTEGER},
{"CLNG", OP_CONV_INT_LONG, 1, 1, BAS_TYPE_LONG},
{"CSNG", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_SINGLE},
@@ -115,34 +115,82 @@ static const BuiltinFuncT builtinFuncs[] = {
// PC positioned just past the operand.
#define BAS_JUMP_OPERAND_SIZE 2
+// Parsed SUB/FUNCTION parameter list. Every declaration form (SUB,
+// FUNCTION, DEF FN, DECLARE, DECLARE LIBRARY, prescan) goes through
+// parseParamList so the grammar exists exactly once.
+typedef struct {
+ int32_t count;
+ int32_t required; // index of the last non-OPTIONAL parameter + 1
+ uint8_t types[BAS_MAX_PARAMS];
+ bool byVal[BAS_MAX_PARAMS];
+ bool optional[BAS_MAX_PARAMS];
+} ParamListT;
+
+// parseParamList behaviour flags
+#define PARAM_REGISTER (1u << 0) // add each parameter as a SCOPE_LOCAL variable symbol
+#define PARAM_PRESCAN (1u << 1) // accept AS for a TYPE not parsed yet
+#define PARAM_FORCE_BYVAL (1u << 2) // DEF FN: parameters are always by value
+
+// State saved around a FOR / DO / WHILE loop body.
+typedef struct {
+ ExitListT list;
+ int32_t selectBase;
+} LoopSaveT;
+
+// State saved around a SUB / FUNCTION body: EXIT lists, loop bookkeeping
+// and SELECT depth all restart at zero inside the procedure so an EXIT
+// or RETURN in the body can never be patched against the caller's
+// constructs.
+typedef struct {
+ ExitListT exitFor;
+ ExitListT exitDo;
+ ExitListT exitSub;
+ ExitListT exitFunc;
+ int32_t forSelectBase;
+ int32_t doSelectBase;
+ int32_t selectDepth;
+ int32_t forDepth;
+ int32_t doDepth;
+ int32_t lastLabelSelectDepth;
+ int32_t prologueJmp; // operand address of the JMP at the procedure entry
+ int32_t bodyAddr; // first byte of the body (prologue jumps back here)
+ bool isFunction;
+} ProcSaveT;
+
// ============================================================
// Prototypes
// ============================================================
+static void addPatchAddr(BasSymbolT *sym, int32_t addr);
static void addPredefConst(BasParserT *p, const char *name, int32_t val);
static void addPredefConsts(BasParserT *p);
static void advance(BasParserT *p);
+static void applyParamList(BasSymbolT *sym, const ParamListT *pl);
bool basParse(BasParserT *p);
BasModuleT *basParserBuildModule(BasParserT *p);
void basParserFree(BasParserT *p);
void basParserInit(BasParserT *p, const char *source, int32_t sourceLen);
static bool check(BasParserT *p, BasTokenTypeE type);
static bool checkCtrlArrayAccess(BasParserT *p);
+static void checkIdentLength(BasParserT *p);
static bool checkKeyword(BasParserT *p, const char *kw);
-static bool checkKeywordText(const char *text, const char *kw);
static bool clampArgCount(BasParserT *p, int32_t argc);
static bool clampParamCount(BasParserT *p, int32_t paramCount);
static void closeSelectCase(BasParserT *p, int32_t **endJumps);
static void collectDebugGlobals(BasParserT *p);
static void collectDebugLocals(BasParserT *p, int32_t procIndex);
static void emitByRefArg(BasParserT *p);
+static void emitCallWithArgs(BasParserT *p, BasSymbolT *sym, int32_t argc);
static void emitFunctionCall(BasParserT *p, BasSymbolT *sym);
+static void emitInputConv(BasParserT *p, const BasSymbolT *sym);
static void emitGotoWithSelectPops(BasParserT *p, const char *labelName);
static int32_t emitJump(BasParserT *p, uint8_t opcode);
static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName);
static void emitLoad(BasParserT *p, BasSymbolT *sym);
+static void emitMethodCallStatement(BasParserT *p);
static void emitSelectPops(BasParserT *p, int32_t count);
static void emitStore(BasParserT *p, BasSymbolT *sym);
+static void emitStoreConv(BasParserT *p, uint8_t dataType);
static void emitUdtInit(BasParserT *p, int32_t udtTypeId);
static BasSymbolT *ensureVariable(BasParserT *p, const char *name);
static void error(BasParserT *p, const char *msg);
@@ -152,15 +200,20 @@ static void exitListInit(ExitListT *el);
static void exitListPatch(ExitListT *el, BasParserT *p);
static void expect(BasParserT *p, BasTokenTypeE type);
static void expectEndOfStatement(BasParserT *p);
+static bool exprEnter(BasParserT *p);
+static void exprLeave(BasParserT *p);
static const BuiltinFuncT *findBuiltin(const char *name);
static BasSymbolT *findTypeDef(BasParserT *p, const char *name);
static BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId);
+static void loopBegin(BasParserT *p, bool isFor, LoopSaveT *save);
+static void loopEnd(BasParserT *p, bool isFor, LoopSaveT *save);
static bool match(BasParserT *p, BasTokenTypeE type);
static bool nameHasTypeSuffix(const char *name);
static void parseAddExpr(BasParserT *p);
static void parseAndExpr(BasParserT *p);
static void parseAssignOrCall(BasParserT *p);
static void parseBeginForm(BasParserT *p);
+static int32_t parseCallArgs(BasParserT *p, BasSymbolT *sym, bool parens);
static void parseChDir(BasParserT *p);
static void parseChDrive(BasParserT *p);
static void parseClose(BasParserT *p);
@@ -195,7 +248,6 @@ static void parseKill(BasParserT *p);
static void parseLineInput(BasParserT *p);
static void parseMkDir(BasParserT *p);
static void parseModule(BasParserT *p);
-static void prescanSignatures(BasParserT *p);
static void parseMulDivExpr(BasParserT *p);
static void parseMulExpr(BasParserT *p);
static void parseName(BasParserT *p);
@@ -205,6 +257,7 @@ static void parseOnError(BasParserT *p);
static void parseOpen(BasParserT *p);
static void parseOption(BasParserT *p);
static void parseOrExpr(BasParserT *p);
+static bool parseParamList(BasParserT *p, ParamListT *pl, uint32_t flags);
static void parsePowExpr(BasParserT *p);
static void parsePowOperand(BasParserT *p);
static void parsePrimary(BasParserT *p);
@@ -221,6 +274,7 @@ static void parseSelectCase(BasParserT *p);
static void parseSetAttr(BasParserT *p);
static void parseSetEvent(BasParserT *p);
static void parseShell(BasParserT *p);
+static uint8_t parseShowModalFlag(BasParserT *p);
static void parseSleep(BasParserT *p);
static void parseStatement(BasParserT *p);
static void parseStatic(BasParserT *p);
@@ -234,13 +288,25 @@ static void parseXorExpr(BasParserT *p);
static void patchCallAddrs(BasParserT *p, BasSymbolT *sym);
static void patchJump(BasParserT *p, int32_t addr);
static void patchLabelRefs(BasParserT *p, BasSymbolT *sym);
+static void prescanSignatures(BasParserT *p);
+static void procBegin(BasParserT *p, ProcSaveT *save, bool isFunction);
+static void procEnd(BasParserT *p, ProcSaveT *save);
static int16_t relJumpOffset(BasParserT *p, int32_t target, int32_t operandAddr);
static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName);
+static uint8_t resolveParamType(BasParserT *p, bool prescan, int32_t *outUdtTypeId);
static uint8_t resolveTypeName(BasParserT *p);
static void shiftBackpatchAddrs(BasParserT *p, int32_t from, int32_t delta);
static void skipNewlines(BasParserT *p);
static uint8_t suffixToType(const char *name);
+// Record a code position that must be patched once sym (a forward-
+// referenced SUB/FUNCTION/label) is defined.
+static void addPatchAddr(BasSymbolT *sym, int32_t addr) {
+ arrput(sym->patchAddrs, addr);
+ sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
+}
+
+
static void addPredefConst(BasParserT *p, const char *name, int32_t val) {
BasSymbolT *sym = basSymTabAdd(&p->sym, name, SYM_CONST, BAS_TYPE_LONG);
if (sym) {
@@ -293,6 +359,23 @@ static void advance(BasParserT *p) {
basLexerNext(&p->lex);
if (p->lex.token.type == TOK_ERROR) {
error(p, p->lex.error);
+ return;
+ }
+ checkIdentLength(p);
+}
+
+
+// Copy a parsed parameter list onto a SUB/FUNCTION symbol and mark the
+// signature as known (call-site arity checks trust hasSignature).
+static void applyParamList(BasSymbolT *sym, const ParamListT *pl) {
+ sym->paramCount = pl->count;
+ sym->requiredParams = pl->required;
+ sym->hasSignature = true;
+
+ for (int32_t i = 0; i < pl->count; i++) {
+ sym->paramTypes[i] = pl->types[i];
+ sym->paramByVal[i] = pl->byVal[i];
+ sym->paramOptional[i] = pl->optional[i];
}
}
@@ -316,6 +399,14 @@ bool basParse(BasParserT *p) {
error(p, "Module too large (code size exceeds 16-bit address range)");
}
+ // The VM's global slot table is fixed-size; a module needing more
+ // would be refused at load time with no source position.
+ if (p->sym.nextGlobalIdx > BAS_VM_MAX_GLOBALS) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "Too many module-level variables (%d, limit is %d)", (int)p->sym.nextGlobalIdx, (int)BAS_VM_MAX_GLOBALS);
+ error(p, buf);
+ }
+
return !p->hasError;
}
@@ -355,6 +446,7 @@ void basParserFree(BasParserT *p) {
void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) {
memset(p, 0, sizeof(BasParserT));
+ memset(p->defType, BAS_DEFTYPE_NONE, sizeof(p->defType));
basLexerInit(&p->lex, source, sourceLen);
basCodeGenInit(&p->cg);
basSymTabInit(&p->sym);
@@ -364,7 +456,13 @@ void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) {
addPredefConsts(p);
- // basLexerInit already primes the first token -- no advance needed
+ // basLexerInit already primes the first token -- no advance needed,
+ // but the primed token bypassed advance(), so vet it here.
+ if (p->lex.token.type == TOK_ERROR) {
+ error(p, p->lex.error);
+ } else {
+ checkIdentLength(p);
+ }
}
@@ -417,35 +515,21 @@ static bool checkCtrlArrayAccess(BasParserT *p) {
}
-static bool checkKeyword(BasParserT *p, const char *kw) {
- if (p->lex.token.type != TOK_IDENT) {
- return false;
+// Identifiers must fit BasSymbolT.name. The lexer accepts names up to
+// BAS_MAX_TOKEN_LEN, so reject longer ones here (the one place every
+// token passes through) rather than truncating them into a name that no
+// later reference would match.
+static void checkIdentLength(BasParserT *p) {
+ if (p->lex.token.type == TOK_IDENT && p->lex.token.textLen >= BAS_MAX_IDENT) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "Identifier too long (maximum is %d characters)", (int)(BAS_MAX_IDENT - 1));
+ error(p, buf);
}
- // Case-insensitive comparison
- const char *a = p->lex.token.text;
- const char *b = kw;
- while (*a && *b) {
- if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) {
- return false;
- }
- a++;
- b++;
- }
- return *a == '\0' && *b == '\0';
}
-static bool checkKeywordText(const char *text, const char *kw) {
- const char *a = text;
- const char *b = kw;
- while (*a && *b) {
- if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) {
- return false;
- }
- a++;
- b++;
- }
- return *a == '\0' && *b == '\0';
+static bool checkKeyword(BasParserT *p, const char *kw) {
+ return p->lex.token.type == TOK_IDENT && strcasecmp(p->lex.token.text, kw) == 0;
}
@@ -508,14 +592,14 @@ static void collectDebugGlobals(BasParserT *p) {
// Collect UDT type definitions for watch window field access
BasDebugUdtDefT def;
memset(&def, 0, sizeof(def));
- snprintf(def.name, BAS_MAX_PROC_NAME, "%s", s->name);
+ snprintf(def.name, BAS_MAX_IDENT, "%s", s->name);
def.typeId = s->index;
def.fieldCount = (int32_t)arrlen(s->fields);
def.fields = (BasDebugFieldT *)malloc(def.fieldCount * sizeof(BasDebugFieldT));
if (def.fields) {
for (int32_t f = 0; f < def.fieldCount; f++) {
- snprintf(def.fields[f].name, BAS_MAX_PROC_NAME, "%s", s->fields[f].name);
+ snprintf(def.fields[f].name, BAS_MAX_IDENT, "%s", s->fields[f].name);
def.fields[f].dataType = s->fields[f].dataType;
}
} else {
@@ -652,70 +736,53 @@ static void emitByRefArg(BasParserT *p) {
}
-static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
- // Parse argument list
- expect(p, TOK_LPAREN);
- int32_t argc = 0;
- if (!check(p, TOK_RPAREN)) {
- if (argc < sym->paramCount && !sym->paramByVal[argc]) {
- emitByRefArg(p);
- } else {
- parseExpression(p);
- }
- argc++;
- while (match(p, TOK_COMMA)) {
- if (argc < sym->paramCount && !sym->paramByVal[argc]) {
- emitByRefArg(p);
- } else {
- parseExpression(p);
- }
- argc++;
- }
- }
- expect(p, TOK_RPAREN);
-
+// Emit the call of a SUB/FUNCTION whose argc arguments are already on
+// the stack: validate the count against the signature, pad omitted
+// OPTIONAL parameters with zero-values, then emit OP_CALL_EXTERN or
+// OP_CALL (recording a forward reference when the body is not yet
+// defined). Shared by every call form: name(args), bare "name args",
+// CALL name(args) and CALL name.
+static void emitCallWithArgs(BasParserT *p, BasSymbolT *sym, int32_t argc) {
if (p->hasError) {
return;
}
- // Determine minimum required arguments
- int32_t minArgs = sym->requiredParams;
- if (minArgs == 0 && sym->paramCount > 0) {
- // No optional params declared -- all are required
- bool hasOptional = false;
- for (int32_t i = 0; i < sym->paramCount; i++) {
- if (sym->paramOptional[i]) {
- hasOptional = true;
- break;
- }
- }
- if (!hasOptional) {
- minArgs = sym->paramCount;
+ // Minimum acceptable count: everything up to the last required
+ // parameter when OPTIONALs are declared, otherwise the full list.
+ int32_t minArgs = sym->paramCount;
+ bool hasOptional = false;
+
+ for (int32_t i = 0; i < sym->paramCount; i++) {
+ if (sym->paramOptional[i]) {
+ hasOptional = true;
+ break;
}
}
+ if (hasOptional) {
+ minArgs = sym->requiredParams;
+ }
+
if (argc < minArgs || argc > sym->paramCount) {
- char buf[BAS_PARSE_ERR_SCRATCH];
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ const char *what = (sym->kind == SYM_FUNCTION) ? "Function" : "Sub";
+
if (minArgs == sym->paramCount) {
- snprintf(buf, sizeof(buf), "Function '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc);
+ snprintf(buf, sizeof(buf), "%s '%s' expects %d arguments, got %d", what, sym->name, (int)sym->paramCount, (int)argc);
} else {
- snprintf(buf, sizeof(buf), "Function '%s' expects %d to %d arguments, got %d", sym->name, (int)minArgs, (int)sym->paramCount, (int)argc);
+ snprintf(buf, sizeof(buf), "%s '%s' expects %d to %d arguments, got %d", what, sym->name, (int)minArgs, (int)sym->paramCount, (int)argc);
}
+
error(p, buf);
return;
}
// Push default zero-values for omitted optional parameters
for (int32_t i = argc; i < sym->paramCount; i++) {
- uint8_t pdt = sym->paramTypes[i];
- if (pdt == BAS_TYPE_STRING) {
+ if (sym->paramTypes[i] == BAS_TYPE_STRING) {
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, emptyIdx);
- } else if (pdt == BAS_TYPE_OBJECT) {
- // Nothing -- push NULL object
- basEmit8(&p->cg, OP_PUSH_INT16);
- basEmit16(&p->cg, 0);
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0);
@@ -724,12 +791,12 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
argc = sym->paramCount;
+ if (clampArgCount(p, argc)) {
+ return;
+ }
+
// External library function: emit OP_CALL_EXTERN
if (sym->isExtern) {
- if (clampArgCount(p, argc)) {
- return;
- }
-
basEmit8(&p->cg, OP_CALL_EXTERN);
basEmitU16(&p->cg, sym->externLibIdx);
basEmitU16(&p->cg, sym->externFuncIdx);
@@ -738,7 +805,7 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
return;
}
- // Internal BASIC function: emit OP_CALL
+ // Internal BASIC procedure: emit OP_CALL
// baseSlot: functions reserve slot 0 for the return value
uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0;
@@ -750,11 +817,18 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
// If not yet defined, record the address for backpatching
if (!sym->isDefined) {
- arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
+ addPatchAddr(sym, addrPos);
}
}
+// name(arg, ...) in either statement or expression context.
+static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
+ int32_t argc = parseCallArgs(p, sym, true);
+ emitCallWithArgs(p, sym, argc);
+}
+
+
// Emit a jump to a GOTO target, discarding the live test values of any
// SELECT CASE blocks the jump leaves. Shared by GOTO and the ON expr
// GOTO arms so both discard the same way.
@@ -799,6 +873,21 @@ static void emitGotoWithSelectPops(BasParserT *p, const char *labelName) {
}
+// INPUT / INPUT # read a string; convert it for a numeric target. The
+// INTEGER conversion range-checks to 16 bits itself; every other numeric
+// type goes through the double conversion and is narrowed by emitStore.
+static void emitInputConv(BasParserT *p, const BasSymbolT *sym) {
+ if (sym->dataType == BAS_TYPE_STRING) {
+ return;
+ }
+ if (sym->dataType == BAS_TYPE_INTEGER) {
+ basEmit8(&p->cg, OP_CONV_STR_INT);
+ } else {
+ basEmit8(&p->cg, OP_CONV_STR_FLT);
+ }
+}
+
+
static int32_t emitJump(BasParserT *p, uint8_t opcode) {
basEmit8(&p->cg, opcode);
int32_t addr = basCodePos(&p->cg);
@@ -844,6 +933,16 @@ static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName
static void emitLoad(BasParserT *p, BasSymbolT *sym) {
+ // Only variables and constants have a value to load; a SUB, label or
+ // TYPE name used as an operand would otherwise read slot `index` of
+ // the globals as if it were a variable.
+ if (sym->kind != SYM_VARIABLE && sym->kind != SYM_CONST) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "'%s' is not a variable", sym->name);
+ error(p, buf);
+ return;
+ }
+
if (sym->kind == SYM_CONST) {
// Emit the constant value directly
if (sym->dataType == BAS_TYPE_STRING) {
@@ -877,6 +976,30 @@ static void emitLoad(BasParserT *p, BasSymbolT *sym) {
}
+// Statement-form method call "obj.Method arg, arg": the object and
+// method name are already on the stack. Arguments are comma-separated
+// (VB style, no parentheses); the return value is discarded.
+static void emitMethodCallStatement(BasParserT *p) {
+ int32_t argc = 0;
+
+ while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE) && !p->hasError) {
+ if (argc > 0) {
+ expect(p, TOK_COMMA);
+ }
+ parseExpression(p);
+ argc++;
+ }
+
+ if (clampArgCount(p, argc)) {
+ return;
+ }
+
+ basEmit8(&p->cg, OP_CALL_METHOD);
+ basEmit8(&p->cg, (uint8_t)argc);
+ basEmit8(&p->cg, OP_POP); // discard return value (statement form)
+}
+
+
// Emit OP_POP `count` times. Used to discard the live SELECT CASE test
// values from the eval stack when EXIT/GOTO jumps out of one or more
// enclosing SELECT constructs (see selectDepth).
@@ -896,6 +1019,14 @@ static void emitStore(BasParserT *p, BasSymbolT *sym) {
error(p, "Cannot assign to a constant");
return;
}
+ // Likewise a SUB/FUNCTION/label/TYPE name is not a store target; the
+ // FUNCTION return-value assignment is handled before reaching here.
+ if (sym->kind != SYM_VARIABLE) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "Cannot assign to '%s'", sym->name);
+ error(p, buf);
+ return;
+ }
// Fixed-length string: pad/truncate before storing. Skip for arrays:
// OP_STR_FIXLEN would convert the array reference being stored at DIM
// time into a fixed-width string, destroying the array (per-element
@@ -904,6 +1035,13 @@ static void emitStore(BasParserT *p, BasSymbolT *sym) {
basEmit8(&p->cg, OP_STR_FIXLEN);
basEmitU16(&p->cg, (uint16_t)sym->fixedLen);
}
+ // Explicitly typed scalar: coerce the value to the declared type so
+ // i% = 1.7 stores 2 and i% = 40000 raises Overflow. Arrays store a
+ // reference here (DIM/REDIM); their elements convert in the
+ // OP_STORE_ARRAY path.
+ if (sym->isTyped && !sym->isArray) {
+ emitStoreConv(p, sym->dataType);
+ }
if (sym->scope == SCOPE_LOCAL) {
basEmit8(&p->cg, OP_STORE_LOCAL);
basEmitU16(&p->cg, (uint16_t)sym->index);
@@ -917,6 +1055,27 @@ static void emitStore(BasParserT *p, BasSymbolT *sym) {
}
+// Emit the conversion that coerces the value on top of the stack to a
+// declared numeric type before it is stored. Shared by scalar, array
+// element and TYPE field stores. STRING, DOUBLE, BOOLEAN, OBJECT and
+// user types store the value as it is.
+static void emitStoreConv(BasParserT *p, uint8_t dataType) {
+ switch (dataType) {
+ case BAS_TYPE_INTEGER:
+ basEmit8(&p->cg, OP_CONV_FLT_INT);
+ break;
+ case BAS_TYPE_LONG:
+ basEmit8(&p->cg, OP_CONV_INT_LONG);
+ break;
+ case BAS_TYPE_SINGLE:
+ basEmit8(&p->cg, OP_CONV_INT_FLT);
+ break;
+ default:
+ break;
+ }
+}
+
+
// emitUdtInit -- emit code to initialize nested UDT fields after a UDT
// has been created and is on top of the stack. For each field that is
// itself a UDT, we DUP the parent, allocate the child UDT, and store it
@@ -984,9 +1143,10 @@ static BasSymbolT *ensureVariable(BasParserT *p, const char *name) {
// Auto-declare (QB implicit declaration)
// Use suffix type if present, otherwise defType for the first letter
- uint8_t dt = suffixToType(name);
+ uint8_t dt = suffixToType(name);
+ bool isTyped = nameHasTypeSuffix(name);
- if (dt == BAS_TYPE_SINGLE && !nameHasTypeSuffix(name) && name[0] != '\0') {
+ if (!isTyped && name[0] != '\0') {
// suffixToType returns SINGLE both for an explicit '!' suffix and
// for no suffix at all. Only apply the DEFxxx override when there
// is NO explicit suffix; an explicit '!' must stay SINGLE.
@@ -995,8 +1155,9 @@ static BasSymbolT *ensureVariable(BasParserT *p, const char *name) {
if (firstLetter >= 'A' && firstLetter <= 'Z') {
uint8_t defDt = p->defType[firstLetter - 'A'];
- if (defDt != 0) {
- dt = defDt;
+ if (defDt != BAS_DEFTYPE_NONE) {
+ dt = defDt;
+ isTyped = true;
}
}
}
@@ -1008,8 +1169,22 @@ static BasSymbolT *ensureVariable(BasParserT *p, const char *name) {
return NULL;
}
- sym->scope = SCOPE_GLOBAL;
- sym->index = basSymTabAllocGlobalSlot(&p->sym);
+ sym->isTyped = isTyped;
+
+ // QB semantics: a variable first used inside a SUB/FUNCTION is local
+ // to that procedure. Anywhere else (module level, form scope) it is
+ // a module global. Implicit STRING locals are initialised to "" by
+ // the procedure prologue (see procEnd) since there is no DIM to emit
+ // the init at.
+ if (p->sym.inLocalScope) {
+ sym->scope = SCOPE_LOCAL;
+ sym->index = basSymTabAllocSlot(&p->sym);
+ sym->isImplicit = true;
+ } else {
+ sym->scope = SCOPE_GLOBAL;
+ sym->index = basSymTabAllocGlobalSlot(&p->sym);
+ }
+
sym->isDefined = true;
return sym;
}
@@ -1094,21 +1269,26 @@ static void expectEndOfStatement(BasParserT *p) {
}
+// Recursion guard for expression parsing. Returns false (with an error
+// set) once the nesting limit is hit; callers return immediately.
+static bool exprEnter(BasParserT *p) {
+ if (p->exprDepth >= BAS_MAX_EXPR_DEPTH) {
+ error(p, "Expression nested too deeply");
+ return false;
+ }
+ p->exprDepth++;
+ return true;
+}
+
+
+static void exprLeave(BasParserT *p) {
+ p->exprDepth--;
+}
+
+
static const BuiltinFuncT *findBuiltin(const char *name) {
for (int32_t i = 0; builtinFuncs[i].name != NULL; i++) {
- // Case-insensitive comparison
- const char *a = name;
- const char *b = builtinFuncs[i].name;
- bool match = true;
- while (*a && *b) {
- if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) {
- match = false;
- break;
- }
- a++;
- b++;
- }
- if (match && *a == '\0' && *b == '\0') {
+ if (strcasecmp(name, builtinFuncs[i].name) == 0) {
return &builtinFuncs[i];
}
}
@@ -1118,22 +1298,8 @@ static const BuiltinFuncT *findBuiltin(const char *name) {
static BasSymbolT *findTypeDef(BasParserT *p, const char *name) {
for (int32_t i = 0; i < p->sym.count; i++) {
- if (p->sym.symbols[i]->kind == SYM_TYPE_DEF) {
- // Case-insensitive comparison
- const char *a = p->sym.symbols[i]->name;
- const char *b = name;
- bool eq = true;
- while (*a && *b) {
- if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) {
- eq = false;
- break;
- }
- a++;
- b++;
- }
- if (eq && *a == '\0' && *b == '\0') {
- return p->sym.symbols[i];
- }
+ if (p->sym.symbols[i]->kind == SYM_TYPE_DEF && strcasecmp(p->sym.symbols[i]->name, name) == 0) {
+ return p->sym.symbols[i];
}
}
return NULL;
@@ -1150,6 +1316,47 @@ static BasSymbolT *findTypeDefById(BasParserT *p, int32_t typeId) {
}
+// Open a FOR (isFor) or DO/WHILE loop: start a fresh EXIT list, record
+// the SELECT depth EXIT must unwind to, and bump the loop depth that
+// makes a stray EXIT FOR / EXIT DO a compile error.
+static void loopBegin(BasParserT *p, bool isFor, LoopSaveT *save) {
+ ExitListT *list = isFor ? &p->exitForList : &p->exitDoList;
+ int32_t *base = isFor ? &p->forSelectBase : &p->doSelectBase;
+
+ save->list = *list;
+ save->selectBase = *base;
+
+ exitListInit(list);
+ *base = p->selectDepth;
+
+ if (isFor) {
+ p->forDepth++;
+ } else {
+ p->doDepth++;
+ }
+}
+
+
+// Close the loop opened by loopBegin: land every EXIT on the current
+// position (freeing the list) and restore the enclosing loop's state.
+// Runs on the error path too, so no EXIT list is ever leaked.
+static void loopEnd(BasParserT *p, bool isFor, LoopSaveT *save) {
+ ExitListT *list = isFor ? &p->exitForList : &p->exitDoList;
+ int32_t *base = isFor ? &p->forSelectBase : &p->doSelectBase;
+
+ exitListPatch(list, p);
+
+ *list = save->list;
+ *base = save->selectBase;
+
+ if (isFor) {
+ p->forDepth--;
+ } else {
+ p->doDepth--;
+ }
+}
+
+
static bool match(BasParserT *p, BasTokenTypeE type) {
// Once an error is set, advance() is a no-op (the token is frozen),
// so a true return here would spin any 'while (match(...))' loop
@@ -1215,7 +1422,7 @@ static void parseAssignOrCall(BasParserT *p) {
advance(p);
// MID$ statement: MID$(var$, start [, len]) = replacement$
- if (checkKeywordText(name, "MID$") && check(p, TOK_LPAREN)) {
+ if (strcasecmp(name, "MID$") == 0 && check(p, TOK_LPAREN)) {
expect(p, TOK_LPAREN);
// First arg: target string variable
@@ -1306,6 +1513,7 @@ static void parseAssignOrCall(BasParserT *p) {
// Final field: store value
expect(p, TOK_EQ);
parseExpression(p);
+ emitStoreConv(p, typeSym->fields[fieldIdx].dataType);
basEmit8(&p->cg, OP_STORE_FIELD);
basEmitU16(&p->cg, (uint16_t)fieldIdx);
return;
@@ -1320,7 +1528,7 @@ static void parseAssignOrCall(BasParserT *p) {
// Emit: push current form ref, push ctrl name, FIND_CTRL
advance(p); // consume DOT
- // Accept any identifier or keyword as a member name — keywords
+ // Accept any identifier or keyword as a member name -- keywords
// like Load, Show, Hide, Clear are valid method names on controls.
if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
errorExpected(p, "property or method name");
@@ -1350,19 +1558,7 @@ static void parseAssignOrCall(BasParserT *p) {
basEmitU16(&p->cg, nameIdx);
basEmit8(&p->cg, OP_LOAD_FORM);
}
- uint8_t modal = 0;
- if (check(p, TOK_INT_LIT)) {
- if (p->lex.token.intVal != 0) {
- modal = 1;
- }
- advance(p);
- } else if (check(p, TOK_IDENT)) {
- BasSymbolT *modSym = basSymTabFind(&p->sym, p->lex.token.text);
- if (modSym && modSym->kind == SYM_CONST && modSym->constInt != 0) {
- modal = 1;
- }
- advance(p);
- }
+ uint8_t modal = parseShowModalFlag(p);
basEmit8(&p->cg, OP_SHOW_FORM);
basEmit8(&p->cg, modal);
return;
@@ -1456,25 +1652,7 @@ static void parseAssignOrCall(BasParserT *p) {
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, methodNameIdx);
- // Parse arguments (space-separated, like VB)
- int32_t argc = 0;
- while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE) && !p->hasError) {
- if (argc > 0) {
- if (check(p, TOK_COMMA)) {
- advance(p);
- }
- }
- parseExpression(p);
- argc++;
- }
-
- if (clampArgCount(p, argc)) {
- return;
- }
-
- basEmit8(&p->cg, OP_CALL_METHOD);
- basEmit8(&p->cg, (uint8_t)argc);
- basEmit8(&p->cg, OP_POP); // discard return value (statement form)
+ emitMethodCallStatement(p);
return;
}
@@ -1528,22 +1706,7 @@ static void parseAssignOrCall(BasParserT *p) {
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, methodNameIdx);
- int32_t argc = 0;
- while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE) && !p->hasError) {
- if (argc > 0 && check(p, TOK_COMMA)) {
- advance(p);
- }
- parseExpression(p);
- argc++;
- }
-
- if (clampArgCount(p, argc)) {
- return;
- }
-
- basEmit8(&p->cg, OP_CALL_METHOD);
- basEmit8(&p->cg, (uint8_t)argc);
- basEmit8(&p->cg, OP_POP); // discard return value
+ emitMethodCallStatement(p);
}
return;
}
@@ -1589,6 +1752,7 @@ static void parseAssignOrCall(BasParserT *p) {
advance(p); // consume field name
expect(p, TOK_EQ);
parseExpression(p);
+ emitStoreConv(p, typeSym->fields[fieldIdx].dataType);
basEmit8(&p->cg, OP_STORE_ARRAY_FIELD);
basEmit8(&p->cg, (uint8_t)dims);
basEmitU16(&p->cg, (uint16_t)fieldIdx);
@@ -1598,6 +1762,9 @@ static void parseAssignOrCall(BasParserT *p) {
expect(p, TOK_EQ);
parseExpression(p);
+ if (sym->isTyped) {
+ emitStoreConv(p, sym->dataType);
+ }
basEmit8(&p->cg, OP_STORE_ARRAY);
basEmit8(&p->cg, (uint8_t)dims);
return;
@@ -1612,12 +1779,16 @@ static void parseAssignOrCall(BasParserT *p) {
if (sym == NULL) {
return;
}
- if (sym->kind == SYM_CONST) {
- error(p, "Cannot assign to a constant");
- return;
- }
- // Check if this is a function name (assigning return value)
+ // Assigning to the enclosing FUNCTION's own name sets its return
+ // value. Any other procedure name is not a store target (a
+ // STORE_LOCAL 0 here would clobber the caller's first slot).
if (sym->kind == SYM_FUNCTION) {
+ if (!p->currentProcIsFunction || strcasecmp(name, p->currentProc) != 0) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "Cannot assign to function '%s' outside its own body", sym->name);
+ error(p, buf);
+ return;
+ }
parseExpression(p);
// Store to the implicit return-value local slot (index 0 in function scope)
basEmit8(&p->cg, OP_STORE_LOCAL);
@@ -1643,96 +1814,8 @@ static void parseAssignOrCall(BasParserT *p) {
}
if (sym->kind == SYM_SUB) {
- int32_t argc = 0;
- if (!check(p, TOK_NEWLINE) && !check(p, TOK_EOF) && !check(p, TOK_COLON) && !check(p, TOK_ELSE)) {
- if (argc < sym->paramCount && !sym->paramByVal[argc]) {
- emitByRefArg(p);
- } else {
- parseExpression(p);
- }
- argc++;
- while (match(p, TOK_COMMA)) {
- if (argc < sym->paramCount && !sym->paramByVal[argc]) {
- emitByRefArg(p);
- } else {
- parseExpression(p);
- }
- argc++;
- }
- }
-
- // Determine the minimum acceptable count (ignore trailing optionals).
- int32_t minArgs = sym->requiredParams;
- bool hasOptional = false;
-
- for (int32_t i = 0; i < sym->paramCount; i++) {
- if (sym->paramOptional[i]) {
- hasOptional = true;
- break;
- }
- }
-
- if (!hasOptional) {
- minArgs = sym->paramCount;
- }
-
- if (!p->hasError && (argc < minArgs || argc > sym->paramCount)) {
- char buf[BAS_PARSE_ERR_SCRATCH];
-
- if (minArgs == sym->paramCount) {
- snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc);
- } else {
- snprintf(buf, sizeof(buf), "Sub '%s' expects %d to %d arguments, got %d", sym->name, (int)minArgs, (int)sym->paramCount, (int)argc);
- }
-
- error(p, buf);
- return;
- }
-
- // Pad missing optional arguments with zero-valued defaults so
- // the callee's OP_CALL receives a full parameter list.
- while (argc < sym->paramCount) {
- uint8_t pType = sym->paramTypes[argc];
-
- if (pType == BAS_TYPE_STRING) {
- uint16_t idx = basAddConstant(&p->cg, "", 0);
- basEmit8(&p->cg, OP_PUSH_STR);
- basEmitU16(&p->cg, idx);
- } else {
- basEmit8(&p->cg, OP_PUSH_INT16);
- basEmit16(&p->cg, 0);
- }
-
- argc++;
- }
-
- // External library SUB: emit OP_CALL_EXTERN
- if (sym->isExtern) {
- if (clampArgCount(p, argc)) {
- return;
- }
-
- basEmit8(&p->cg, OP_CALL_EXTERN);
- basEmitU16(&p->cg, sym->externLibIdx);
- basEmitU16(&p->cg, sym->externFuncIdx);
- basEmit8(&p->cg, (uint8_t)argc);
- basEmit8(&p->cg, sym->dataType);
- return;
- }
-
- {
- uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0;
- basEmit8(&p->cg, OP_CALL);
- int32_t addrPos = basCodePos(&p->cg);
- basEmitU16(&p->cg, (uint16_t)sym->codeAddr);
- basEmit8(&p->cg, (uint8_t)argc);
- basEmit8(&p->cg, baseSlot);
-
- if (!sym->isDefined) {
- arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
- }
- }
-
+ int32_t argc = parseCallArgs(p, sym, false);
+ emitCallWithArgs(p, sym, argc);
return;
}
@@ -1752,9 +1835,15 @@ static void parseBeginForm(BasParserT *p) {
return;
}
- char formName[BAS_MAX_SYMBOL_NAME];
- strncpy(formName, p->lex.token.text, BAS_MAX_SYMBOL_NAME - 1);
- formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
+ if (p->lex.token.textLen >= BAS_MAX_IDENT) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "Form name too long (maximum is %d characters)", (int)(BAS_MAX_IDENT - 1));
+ error(p, buf);
+ return;
+ }
+
+ char formName[BAS_MAX_IDENT];
+ strcpy(formName, p->lex.token.text);
advance(p);
if (p->sym.inFormScope) {
@@ -1775,11 +1864,44 @@ static void parseBeginForm(BasParserT *p) {
// runs at form load time, not at program startup.
basEmit8(&p->cg, OP_JMP);
p->formInitJmpAddr = basCodePos(&p->cg);
- basEmit16(&p->cg, 0); // placeholder — patched at ENDFORM
+ basEmit16(&p->cg, 0); // placeholder -- patched at ENDFORM
p->formInitCodeStart = basCodePos(&p->cg);
}
+// Parse the argument list of a SUB/FUNCTION call and push each value (or
+// address, for BYREF parameters). parens=true expects "(args)"; false
+// parses the bare "name arg, arg" statement form up to end of statement.
+// Returns the number of arguments parsed.
+static int32_t parseCallArgs(BasParserT *p, BasSymbolT *sym, bool parens) {
+ int32_t argc = 0;
+ bool more;
+
+ if (parens) {
+ expect(p, TOK_LPAREN);
+ more = !check(p, TOK_RPAREN);
+ } else {
+ more = !check(p, TOK_NEWLINE) && !check(p, TOK_EOF) && !check(p, TOK_COLON) && !check(p, TOK_ELSE);
+ }
+
+ while (more && !p->hasError) {
+ if (argc < sym->paramCount && !sym->paramByVal[argc]) {
+ emitByRefArg(p);
+ } else {
+ parseExpression(p);
+ }
+ argc++;
+ more = match(p, TOK_COMMA);
+ }
+
+ if (parens) {
+ expect(p, TOK_RPAREN);
+ }
+
+ return argc;
+}
+
+
static void parseChDir(BasParserT *p) {
// CHDIR path
advance(p);
@@ -1961,11 +2083,13 @@ static void parseData(BasParserT *p) {
}
if (check(p, TOK_INT_LIT)) {
+ // Hex/octal/binary literals arrive as TOK_INT_LIT with up to
+ // 32 significant bits; keep the wide ones as LONG.
int32_t val = p->lex.token.intVal;
if (isNeg) {
val = -val;
}
- BasValueT v = basValInteger((int16_t)val);
+ BasValueT v = (val >= INT16_MIN && val <= INT16_MAX) ? basValInteger((int16_t)val) : basValLong(val);
basAddData(&p->cg, v);
advance(p);
} else if (check(p, TOK_LONG_LIT)) {
@@ -2050,50 +2174,10 @@ static void parseDeclare(BasParserT *p) {
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
- // Parse parameter list
- int32_t paramCount = 0;
- uint8_t paramTypes[BAS_MAX_PARAMS];
- bool paramByVal[BAS_MAX_PARAMS];
+ ParamListT pl;
- if (match(p, TOK_LPAREN)) {
- while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) {
- if (paramCount > 0) {
- expect(p, TOK_COMMA);
- }
-
- bool byVal = false;
-
- if (match(p, TOK_BYVAL)) {
- byVal = true;
- }
-
- if (!check(p, TOK_IDENT)) {
- errorExpected(p, "parameter name");
- return;
- }
-
- if (clampParamCount(p, paramCount)) {
- return;
- }
-
- char paramName[BAS_MAX_TOKEN_LEN];
- strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
- paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
- advance(p);
-
- uint8_t pdt = suffixToType(paramName);
-
- if (match(p, TOK_AS)) {
- pdt = resolveTypeName(p);
- }
-
- paramTypes[paramCount] = pdt;
- paramByVal[paramCount] = byVal;
-
- paramCount++;
- }
-
- expect(p, TOK_RPAREN);
+ if (!parseParamList(p, &pl, 0)) {
+ return;
}
// Return type for FUNCTION
@@ -2108,32 +2192,47 @@ static void parseDeclare(BasParserT *p) {
}
// Add to symbol table as forward declaration
- BasSymbolT *sym = basSymTabAdd(&p->sym, name, kind, returnType);
- bool added = (sym != NULL);
+ BasSymbolT *sym = basSymTabFindGlobal(&p->sym, name);
if (sym == NULL) {
- // Might already be declared -- look it up
- sym = basSymTabFind(&p->sym, name);
+ sym = basSymTabAdd(&p->sym, name, kind, returnType);
if (sym == NULL) {
error(p, "Symbol table full");
return;
}
- }
- sym->scope = SCOPE_GLOBAL;
-
- if (added || !sym->isDefined) {
+ sym->scope = SCOPE_GLOBAL;
sym->isDefined = false;
sym->codeAddr = 0;
+ } else if (sym->kind != kind) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "'%s' is already declared as something else", sym->name);
+ error(p, buf);
+ return;
}
- sym->paramCount = paramCount;
+ // The prescan (or an earlier definition/DECLARE) may already have
+ // recorded the real signature; a DECLARE that disagrees with it is an
+ // error rather than a silent partial overwrite. A call-site stub has
+ // no signature yet and simply takes this one.
+ if (sym->hasSignature) {
+ bool matches = (sym->paramCount == pl.count);
- for (int32_t i = 0; i < paramCount; i++) {
- sym->paramTypes[i] = paramTypes[i];
- sym->paramByVal[i] = paramByVal[i];
+ for (int32_t i = 0; matches && i < pl.count; i++) {
+ matches = (sym->paramTypes[i] == pl.types[i]);
+ }
+
+ if (!matches) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "DECLARE for '%s' does not match its definition", sym->name);
+ error(p, buf);
+ }
+
+ return;
}
+
+ applyParamList(sym, &pl);
}
@@ -2221,46 +2320,10 @@ static void parseDeclareLibrary(BasParserT *p) {
uint16_t funcNameIdx = basAddConstant(&p->cg, externName, (int32_t)strlen(externName));
- // Parse parameter list
- int32_t paramCount = 0;
- uint8_t paramTypes[BAS_MAX_PARAMS];
- bool paramByVal[BAS_MAX_PARAMS];
+ ParamListT pl;
- if (match(p, TOK_LPAREN)) {
- while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) {
- if (paramCount > 0) {
- expect(p, TOK_COMMA);
- }
-
- bool byVal = match(p, TOK_BYVAL);
-
- if (!check(p, TOK_IDENT)) {
- errorExpected(p, "parameter name");
- return;
- }
-
- if (clampParamCount(p, paramCount)) {
- return;
- }
-
- char paramName[BAS_MAX_TOKEN_LEN];
- strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
- paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
- advance(p);
-
- uint8_t pdt = suffixToType(paramName);
-
- if (match(p, TOK_AS)) {
- pdt = resolveTypeName(p);
- }
-
- paramTypes[paramCount] = pdt;
- paramByVal[paramCount] = byVal;
-
- paramCount++;
- }
-
- expect(p, TOK_RPAREN);
+ if (!parseParamList(p, &pl, 0)) {
+ return;
}
// Return type for FUNCTION
@@ -2274,29 +2337,31 @@ static void parseDeclareLibrary(BasParserT *p) {
return;
}
- // Register as extern symbol
- BasSymbolT *sym = basSymTabAdd(&p->sym, funcName, kind, returnType);
+ // Register as extern symbol (the prescan may already have added
+ // it from this same DECLARE line; reuse that entry).
+ BasSymbolT *sym = basSymTabFindGlobal(&p->sym, funcName);
if (sym == NULL) {
- sym = basSymTabFind(&p->sym, funcName);
+ sym = basSymTabAdd(&p->sym, funcName, kind, returnType);
if (sym == NULL) {
error(p, "Symbol table full");
return;
}
+ } else if (sym->kind != kind) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "'%s' is already declared as something else", sym->name);
+ error(p, buf);
+ return;
}
- sym->scope = SCOPE_GLOBAL;
- sym->isDefined = true;
- sym->isExtern = true;
+ sym->scope = SCOPE_GLOBAL;
+ sym->dataType = returnType;
+ sym->isDefined = true;
+ sym->isExtern = true;
sym->externLibIdx = libNameIdx;
sym->externFuncIdx = funcNameIdx;
- sym->paramCount = paramCount;
-
- for (int32_t i = 0; i < paramCount; i++) {
- sym->paramTypes[i] = paramTypes[i];
- sym->paramByVal[i] = paramByVal[i];
- }
+ applyParamList(sym, &pl);
}
}
@@ -2319,6 +2384,14 @@ static void parseDef(BasParserT *p) {
return;
}
+ // A DEF FN body gets its own local scope; opening one inside a
+ // SUB/FUNCTION would reset that procedure's slot numbering and free
+ // its locals on exit. QB forbids it too.
+ if (p->sym.inLocalScope) {
+ error(p, "DEF FN is not allowed inside SUB or FUNCTION");
+ return;
+ }
+
advance(p);
int32_t skipJump = emitJump(p, OP_JMP);
@@ -2327,54 +2400,10 @@ static void parseDef(BasParserT *p) {
basSymTabEnterLocal(&p->sym);
basSymTabAllocSlot(&p->sym); // slot 0 for return value
- int32_t paramCount = 0;
- uint8_t paramTypes[BAS_MAX_PARAMS];
- bool paramByVal[BAS_MAX_PARAMS];
+ ParamListT pl;
- if (match(p, TOK_LPAREN)) {
- while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) {
- if (paramCount > 0) {
- expect(p, TOK_COMMA);
- }
-
- if (!check(p, TOK_IDENT)) {
- errorExpected(p, "parameter name");
- return;
- }
-
- if (clampParamCount(p, paramCount)) {
- return;
- }
-
- char paramName[BAS_MAX_TOKEN_LEN];
- strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
- paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
- advance(p);
-
- uint8_t pdt = suffixToType(paramName);
- int32_t pUdtTypeId = -1;
- if (match(p, TOK_AS)) {
- pdt = resolveTypeName(p);
- if (pdt == BAS_TYPE_UDT) {
- pUdtTypeId = p->lastUdtTypeId;
- }
- }
-
- BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
- if (paramSym == NULL) {
- error(p, "Symbol table full");
- return;
- }
- paramSym->scope = SCOPE_LOCAL;
- paramSym->index = basSymTabAllocSlot(&p->sym);
- paramSym->isDefined = true;
- paramSym->udtTypeId = pUdtTypeId;
-
- paramTypes[paramCount] = pdt;
- paramByVal[paramCount] = true;
- paramCount++;
- }
- expect(p, TOK_RPAREN);
+ if (!parseParamList(p, &pl, PARAM_REGISTER | PARAM_FORCE_BYVAL)) {
+ return;
}
expect(p, TOK_EQ);
@@ -2389,25 +2418,18 @@ static void parseDef(BasParserT *p) {
p->cg.debugProcCount++;
basSymTabLeaveLocal(&p->sym);
- uint8_t returnType = suffixToType(name);
- bool savedLocal = p->sym.inLocalScope;
- p->sym.inLocalScope = false;
- BasSymbolT *funcSym = basSymTabAdd(&p->sym, name, SYM_FUNCTION, returnType);
- p->sym.inLocalScope = savedLocal;
+ uint8_t returnType = suffixToType(name);
+ BasSymbolT *funcSym = basSymTabAdd(&p->sym, name, SYM_FUNCTION, returnType);
if (funcSym == NULL) {
error(p, "Could not register DEF function");
return;
}
- funcSym->codeAddr = funcAddr;
- funcSym->isDefined = true;
- funcSym->paramCount = paramCount;
- funcSym->scope = SCOPE_GLOBAL;
- for (int32_t i = 0; i < paramCount; i++) {
- funcSym->paramTypes[i] = paramTypes[i];
- funcSym->paramByVal[i] = paramByVal[i];
- }
+ funcSym->codeAddr = funcAddr;
+ funcSym->isDefined = true;
+ funcSym->scope = SCOPE_GLOBAL;
+ applyParamList(funcSym, &pl);
patchCallAddrs(p, funcSym);
patchJump(p, skipJump);
@@ -2454,6 +2476,11 @@ static void parseDefType(BasParserT *p, uint8_t dataType) {
advance(p);
}
+ if (endLetter < startLetter) {
+ error(p, "Letter range must be in ascending order (e.g. A-Z)");
+ return;
+ }
+
// Set default type for the range
for (char c = startLetter; c <= endLetter; c++) {
p->defType[c - 'A'] = dataType;
@@ -2502,22 +2529,28 @@ static void parseDim(BasParserT *p) {
}
// Optional AS type
- uint8_t dt = suffixToType(name);
+ uint8_t dt = suffixToType(name);
+ bool isTyped = nameHasTypeSuffix(name);
int32_t udtTypeId = -1;
int32_t fixedLen = 0;
if (match(p, TOK_AS)) {
- dt = resolveTypeName(p);
+ dt = resolveTypeName(p);
+ isTyped = true;
if (dt == BAS_TYPE_UDT) {
udtTypeId = p->lastUdtTypeId;
}
// Check for STRING * n (fixed-length string)
if (dt == BAS_TYPE_STRING && check(p, TOK_STAR)) {
advance(p);
- if (check(p, TOK_INT_LIT)) {
+ if (!check(p, TOK_INT_LIT)) {
+ error(p, "Expected integer after STRING *");
+ } else if (p->lex.token.intVal < 1 || p->lex.token.intVal > UINT16_MAX) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "STRING * length must be 1 to %d", (int)UINT16_MAX);
+ error(p, buf);
+ } else {
fixedLen = p->lex.token.intVal;
advance(p);
- } else {
- error(p, "Expected integer after STRING *");
}
}
}
@@ -2577,6 +2610,7 @@ static void parseDim(BasParserT *p) {
sym->isShared = isShared;
sym->udtTypeId = udtTypeId;
sym->fixedLen = fixedLen;
+ sym->isTyped = isTyped;
sym->scope = newScope;
@@ -2677,11 +2711,8 @@ static void parseDo(BasParserT *p) {
// LOOP [WHILE|UNTIL cond]
advance(p); // consume DO
- ExitListT savedExitDo = p->exitDoList;
- exitListInit(&p->exitDoList);
-
- int32_t savedDoSelectBase = p->doSelectBase;
- p->doSelectBase = p->selectDepth;
+ LoopSaveT save;
+ loopBegin(p, false, &save);
int32_t loopTop = basCodePos(&p->cg);
@@ -2711,6 +2742,7 @@ static void parseDo(BasParserT *p) {
}
if (p->hasError) {
+ loopEnd(p, false, &save);
return;
}
@@ -2744,9 +2776,7 @@ static void parseDo(BasParserT *p) {
}
// Patch all EXIT DO jumps to here
- exitListPatch(&p->exitDoList, p);
- p->exitDoList = savedExitDo;
- p->doSelectBase = savedDoSelectBase;
+ loopEnd(p, false, &save);
}
@@ -2767,7 +2797,7 @@ static void parseEndForm(BasParserT *p) {
}
// Capture form name before leaving scope
- char formName[BAS_MAX_SYMBOL_NAME];
+ char formName[BAS_MAX_IDENT];
strncpy(formName, p->sym.formScopeName, sizeof(formName) - 1);
formName[sizeof(formName) - 1] = '\0';
@@ -2837,8 +2867,15 @@ static void parseErase(BasParserT *p) {
static void parseExit(BasParserT *p) {
advance(p); // consume EXIT
+ // Each form is only legal inside its construct; the depth counters
+ // and currentProc flags are reset per procedure (procBegin), so an
+ // EXIT can never be patched against a loop or procedure of the caller.
if (check(p, TOK_FOR)) {
advance(p);
+ if (p->forDepth == 0) {
+ error(p, "EXIT FOR outside FOR loop");
+ return;
+ }
// Discard the test values of any SELECT CASE bodies this EXIT
// jumps out of, then drop the VM for-frame and jump.
emitSelectPops(p, p->selectDepth - p->forSelectBase);
@@ -2847,17 +2884,29 @@ static void parseExit(BasParserT *p) {
exitListAdd(&p->exitForList, addr);
} else if (check(p, TOK_DO)) {
advance(p);
+ if (p->doDepth == 0) {
+ error(p, "EXIT DO outside DO or WHILE loop");
+ return;
+ }
emitSelectPops(p, p->selectDepth - p->doSelectBase);
int32_t addr = emitJump(p, OP_JMP);
exitListAdd(&p->exitDoList, addr);
} else if (check(p, TOK_SUB)) {
advance(p);
+ if (!p->sym.inLocalScope || p->currentProcIsFunction) {
+ error(p, "EXIT SUB outside SUB");
+ return;
+ }
// Leaving the procedure entirely: pop every open SELECT value.
emitSelectPops(p, p->selectDepth);
int32_t addr = emitJump(p, OP_JMP);
exitListAdd(&p->exitSubList, addr);
} else if (check(p, TOK_FUNCTION)) {
advance(p);
+ if (!p->sym.inLocalScope || !p->currentProcIsFunction) {
+ error(p, "EXIT FUNCTION outside FUNCTION");
+ return;
+ }
emitSelectPops(p, p->selectDepth);
int32_t addr = emitJump(p, OP_JMP);
exitListAdd(&p->exitFuncList, addr);
@@ -2868,7 +2917,11 @@ static void parseExit(BasParserT *p) {
static void parseExpression(BasParserT *p) {
+ if (!exprEnter(p)) {
+ return;
+ }
parseImpExpr(p);
+ exprLeave(p);
}
@@ -2888,14 +2941,6 @@ static void parseFor(BasParserT *p) {
// NEXT [var]
advance(p); // consume FOR
- ExitListT savedExitFor = p->exitForList;
- exitListInit(&p->exitForList);
-
- // Record SELECT depth at loop entry so EXIT FOR knows how many
- // enclosing SELECT test values to pop on the way out.
- int32_t savedForSelectBase = p->forSelectBase;
- p->forSelectBase = p->selectDepth;
-
// Loop variable
if (!check(p, TOK_IDENT)) {
errorExpected(p, "loop variable");
@@ -2912,6 +2957,11 @@ static void parseFor(BasParserT *p) {
return;
}
+ // Open the loop (fresh EXIT FOR list, SELECT depth for EXIT to unwind
+ // to) only once the header is known to be well-formed.
+ LoopSaveT save;
+ loopBegin(p, true, &save);
+
// = start
expect(p, TOK_EQ);
parseExpression(p);
@@ -2952,13 +3002,21 @@ static void parseFor(BasParserT *p) {
}
if (p->hasError) {
+ loopEnd(p, true, &save);
return;
}
expect(p, TOK_NEXT);
- // Optional variable name after NEXT (we just skip it)
+ // Optional variable name after NEXT must name this loop's variable
if (check(p, TOK_IDENT)) {
+ if (strcasecmp(p->lex.token.text, loopVar->name) != 0) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "NEXT %s does not match FOR %s", p->lex.token.text, loopVar->name);
+ error(p, buf);
+ loopEnd(p, true, &save);
+ return;
+ }
advance(p);
}
@@ -2970,15 +3028,10 @@ static void parseFor(BasParserT *p) {
basEmit16(&p->cg, backOffset);
// Patch FOR_INIT's forward skip offset to point past FOR_NEXT.
- int32_t loopEnd = basCodePos(&p->cg);
- int16_t skipOffset = relJumpOffset(p, loopEnd, skipOffsetPos);
- p->cg.code[skipOffsetPos] = (uint8_t)(skipOffset & 0xFF);
- p->cg.code[skipOffsetPos + 1] = (uint8_t)((skipOffset >> 8) & 0xFF);
+ patchJump(p, skipOffsetPos);
// Patch all EXIT FOR jumps to here
- exitListPatch(&p->exitForList, p);
- p->exitForList = savedExitFor;
- p->forSelectBase = savedForSelectBase;
+ loopEnd(p, true, &save);
}
@@ -2998,6 +3051,11 @@ static void parseFunction(BasParserT *p) {
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
+ if (p->sym.inLocalScope) {
+ error(p, "FUNCTION cannot be defined inside SUB or FUNCTION");
+ return;
+ }
+
// Save current proc name for STATIC variable mangling
strncpy(p->currentProc, name, BAS_MAX_TOKEN_LEN - 1);
p->currentProc[BAS_MAX_TOKEN_LEN - 1] = '\0';
@@ -3010,84 +3068,17 @@ static void parseFunction(BasParserT *p) {
// Enter local scope
basSymTabEnterLocal(&p->sym);
- ExitListT savedExitFunc = p->exitFuncList;
- exitListInit(&p->exitFuncList);
+ ProcSaveT save;
+ procBegin(p, &save, true);
// Allocate slot 0 for return value
basSymTabAllocSlot(&p->sym);
- // Parse parameter list
- int32_t paramCount = 0;
- int32_t requiredCount = 0;
- bool seenOptional = false;
- uint8_t paramTypes[BAS_MAX_PARAMS];
- bool paramByVal[BAS_MAX_PARAMS];
- bool paramOptional[BAS_MAX_PARAMS];
+ ParamListT pl;
- if (match(p, TOK_LPAREN)) {
- while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) {
- if (paramCount > 0) {
- expect(p, TOK_COMMA);
- }
-
- bool optional = false;
- if (match(p, TOK_OPTIONAL)) {
- optional = true;
- seenOptional = true;
- } else if (seenOptional) {
- error(p, "Required parameter cannot follow Optional parameter");
- return;
- }
-
- bool byVal = false;
- if (match(p, TOK_BYVAL)) {
- byVal = true;
- }
-
- if (!check(p, TOK_IDENT)) {
- errorExpected(p, "parameter name");
- return;
- }
-
- if (clampParamCount(p, paramCount)) {
- return;
- }
-
- char paramName[BAS_MAX_TOKEN_LEN];
- strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
- paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
- advance(p);
-
- uint8_t pdt = suffixToType(paramName);
- int32_t pUdtTypeId = -1;
- if (match(p, TOK_AS)) {
- pdt = resolveTypeName(p);
- if (pdt == BAS_TYPE_UDT) {
- pUdtTypeId = p->lastUdtTypeId;
- }
- }
-
- BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
- if (paramSym == NULL) {
- error(p, "Symbol table full");
- return;
- }
- paramSym->scope = SCOPE_LOCAL;
- paramSym->index = basSymTabAllocSlot(&p->sym);
- paramSym->isDefined = true;
- paramSym->udtTypeId = pUdtTypeId;
-
- paramTypes[paramCount] = pdt;
- paramByVal[paramCount] = byVal;
- paramOptional[paramCount] = optional;
-
- if (!optional) {
- requiredCount = paramCount + 1;
- }
-
- paramCount++;
- }
- expect(p, TOK_RPAREN);
+ if (!parseParamList(p, &pl, PARAM_REGISTER)) {
+ procEnd(p, &save);
+ return;
}
// Return type
@@ -3096,13 +3087,20 @@ static void parseFunction(BasParserT *p) {
returnType = resolveTypeName(p);
}
- // Register the function in the symbol table (global scope entry)
- // We need to temporarily leave local scope to add to global
+ // Register the function in the symbol table (global scope entry).
+ // The prescan / a DECLARE / a call-site stub may already have added
+ // it; a symbol that is already defined is a duplicate body.
BasSymbolT *existing = basSymTabFindGlobal(&p->sym, name);
- BasSymbolT *funcSym = NULL;
+ BasSymbolT *funcSym = NULL;
if (existing != NULL && existing->kind == SYM_FUNCTION) {
- // Forward-declared, now define it
+ if (existing->isDefined) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "Function '%s' is already defined", existing->name);
+ error(p, buf);
+ procEnd(p, &save);
+ return;
+ }
funcSym = existing;
} else {
// Temporarily store the local state, add globally
@@ -3114,24 +3112,20 @@ static void parseFunction(BasParserT *p) {
if (funcSym == NULL) {
error(p, "Could not register function");
+ procEnd(p, &save);
return;
}
- funcSym->codeAddr = funcAddr;
- funcSym->isDefined = true;
- funcSym->paramCount = paramCount;
- funcSym->requiredParams = requiredCount;
- funcSym->scope = SCOPE_GLOBAL;
- for (int32_t i = 0; i < paramCount; i++) {
- funcSym->paramTypes[i] = paramTypes[i];
- funcSym->paramByVal[i] = paramByVal[i];
- funcSym->paramOptional[i] = paramOptional[i];
- }
+ funcSym->codeAddr = funcAddr;
+ funcSym->isDefined = true;
+ funcSym->scope = SCOPE_GLOBAL;
+ funcSym->dataType = returnType;
+ applyParamList(funcSym, &pl);
// Record the owning form -- see parseSub for the rationale.
if (p->sym.inFormScope && p->sym.formScopeName[0]) {
- strncpy(funcSym->formName, p->sym.formScopeName, BAS_MAX_SYMBOL_NAME - 1);
- funcSym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
+ strncpy(funcSym->formName, p->sym.formScopeName, BAS_MAX_IDENT - 1);
+ funcSym->formName[BAS_MAX_IDENT - 1] = '\0';
}
// Backpatch any forward-reference calls to this function
@@ -3158,19 +3152,13 @@ static void parseFunction(BasParserT *p) {
skipNewlines(p);
}
- // Patch EXIT FUNCTION jumps
- exitListPatch(&p->exitFuncList, p);
- p->exitFuncList = savedExitFunc;
-
- // Load return value from slot 0 and return
- basEmit8(&p->cg, OP_LOAD_LOCAL);
- basEmitU16(&p->cg, 0);
- basEmit8(&p->cg, OP_RET_VAL);
+ // Patch EXIT FUNCTION jumps, emit the epilogue and prologue, and
+ // restore the enclosing state
+ procEnd(p, &save);
// Leave local scope
collectDebugLocals(p, p->cg.debugProcCount++);
basSymTabLeaveLocal(&p->sym);
- p->currentProc[0] = '\0';
// Patch the skip jump
patchJump(p, skipJump);
@@ -3213,6 +3201,12 @@ static void parseGet(BasParserT *p) {
return;
}
+ // A STRING target reads as many bytes as it already holds in BINARY
+ // mode, so its current value goes on the stack ahead of the type.
+ if (sym->dataType == BAS_TYPE_STRING) {
+ emitLoad(p, sym);
+ }
+
// Push variable type so VM knows how many bytes to read
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, (int16_t)sym->dataType);
@@ -3470,35 +3464,41 @@ static void parseInput(BasParserT *p) {
// Comma separator
expect(p, TOK_COMMA);
- // Target variable
- if (!check(p, TOK_IDENT)) {
- errorExpected(p, "variable name");
- return;
- }
-
- char varName[BAS_MAX_TOKEN_LEN];
- strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
- varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
- advance(p);
-
- basEmit8(&p->cg, OP_FILE_INPUT);
-
- BasSymbolT *sym = ensureVariable(p, varName);
-
- if (sym != NULL) {
- // If the variable is numeric, convert the input string
- if (sym->dataType != BAS_TYPE_STRING) {
- if (sym->dataType == BAS_TYPE_INTEGER || sym->dataType == BAS_TYPE_LONG) {
- basEmit8(&p->cg, OP_CONV_STR_INT);
- } else {
- basEmit8(&p->cg, OP_CONV_STR_FLT);
- }
+ // One OP_FILE_INPUT per comma-separated target. Each consumes
+ // the channel, so it is duplicated ahead of every field but the
+ // last.
+ for (;;) {
+ if (!check(p, TOK_IDENT)) {
+ errorExpected(p, "variable name");
+ return;
}
- emitStore(p, sym);
- }
+ char varName[BAS_MAX_TOKEN_LEN];
+ strncpy(varName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
+ varName[BAS_MAX_TOKEN_LEN - 1] = '\0';
+ advance(p);
- return;
+ bool more = check(p, TOK_COMMA);
+
+ if (more) {
+ basEmit8(&p->cg, OP_DUP);
+ }
+
+ basEmit8(&p->cg, OP_FILE_INPUT);
+
+ BasSymbolT *sym = ensureVariable(p, varName);
+
+ if (sym == NULL) {
+ return;
+ }
+
+ emitInputConv(p, sym);
+ emitStore(p, sym);
+
+ if (!match(p, TOK_COMMA)) {
+ return;
+ }
+ }
}
// Check for optional prompt string
@@ -3539,15 +3539,7 @@ static void parseInput(BasParserT *p) {
return;
}
- // If the variable is numeric, we need to convert the input string
- if (sym->dataType != BAS_TYPE_STRING) {
- if (sym->dataType == BAS_TYPE_INTEGER || sym->dataType == BAS_TYPE_LONG) {
- basEmit8(&p->cg, OP_CONV_STR_INT);
- } else {
- basEmit8(&p->cg, OP_CONV_STR_FLT);
- }
- }
-
+ emitInputConv(p, sym);
emitStore(p, sym);
}
@@ -3612,189 +3604,6 @@ static void parseMkDir(BasParserT *p) {
}
-// Walk the token stream from current position, find every
-// top-level SUB/FUNCTION declaration, extract the signature
-// (name, params, return type), and register it in the symbol
-// table. Does not emit code. Saves and restores lexer position
-// so the main parse pass starts from the same point. This gives
-// VB-style forward visibility: call sites that appear earlier in
-// the source than the SUB definition still resolve to the right
-// paramCount / types.
-static void prescanSignatures(BasParserT *p) {
- BasLexerT savedLex = p->lex;
- bool savedErr = p->hasError;
- int32_t savedErrLn = p->errorLine;
- char savedErrMsg[BAS_PARSE_ERR_SCRATCH];
- snprintf(savedErrMsg, sizeof(savedErrMsg), "%s", p->error);
-
- while (!check(p, TOK_EOF)) {
- // Best-effort: clear any scan error so we continue to the next
- // declaration. The main parse pass will re-surface real errors
- // with full location info. This must happen on EVERY iteration:
- // advance() refuses to move while hasError is set, so a lexer
- // error (TOK_ERROR) left latched would spin this loop forever.
- if (p->hasError) {
- p->hasError = false;
- p->errorLine = 0;
- p->error[0] = '\0';
- }
-
- // "END SUB" / "END FUNCTION" consume the END and the following
- // keyword as separate tokens; skip END so the next-iteration
- // SUB/FUNCTION check doesn't misinterpret it as a declaration.
- if (check(p, TOK_END)) {
- advance(p);
- continue;
- }
-
- bool isFn = check(p, TOK_FUNCTION);
- bool isSub = check(p, TOK_SUB);
-
- if (!isFn && !isSub) {
- advance(p);
- continue;
- }
-
- advance(p); // consume SUB / FUNCTION
-
- if (!check(p, TOK_IDENT)) {
- continue;
- }
-
- char name[BAS_MAX_TOKEN_LEN];
- strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
- name[BAS_MAX_TOKEN_LEN - 1] = '\0';
- advance(p);
-
- // Param list
- int32_t paramCount = 0;
- int32_t requiredCount = 0;
- uint8_t paramTypes[BAS_MAX_PARAMS] = {0};
- bool paramByVal[BAS_MAX_PARAMS] = {0};
- bool paramOptional[BAS_MAX_PARAMS] = {0};
-
- if (check(p, TOK_LPAREN)) {
- advance(p);
-
- while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !check(p, TOK_NEWLINE)) {
- if (paramCount > 0) {
- if (!check(p, TOK_COMMA)) {
- break;
- }
- advance(p);
- }
-
- bool optional = false;
-
- if (check(p, TOK_OPTIONAL)) {
- optional = true;
- advance(p);
- }
-
- bool byVal = false;
-
- if (check(p, TOK_BYVAL)) {
- byVal = true;
- advance(p);
- }
-
- if (!check(p, TOK_IDENT)) {
- break;
- }
-
- // Prescan is best-effort: too-many-parameters is surfaced
- // with full location info by the real parse pass, so here
- // we simply stop scanning this signature's parameters.
- if (clampParamCount(p, paramCount)) {
- break;
- }
-
- char paramName[BAS_MAX_TOKEN_LEN];
- strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
- paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
- advance(p);
-
- uint8_t pdt = suffixToType(paramName);
-
- if (check(p, TOK_AS)) {
- advance(p);
- // resolveTypeName sets hasError on miss; we clear
- // at function end so one broken signature doesn't
- // stop us from discovering the rest.
- pdt = resolveTypeName(p);
-
- if (p->hasError) {
- break;
- }
- }
-
- paramTypes[paramCount] = pdt;
- paramByVal[paramCount] = byVal;
- paramOptional[paramCount] = optional;
-
- if (!optional) {
- requiredCount = paramCount + 1;
- }
-
- paramCount++;
- }
-
- if (check(p, TOK_RPAREN)) {
- advance(p);
- }
- }
-
- // FUNCTION return type (AS clause; or suffix on name)
- uint8_t returnType = suffixToType(name);
-
- if (isFn && check(p, TOK_AS)) {
- advance(p);
- returnType = resolveTypeName(p);
- }
-
- // Register / update the symbol. If a call site already
- // created a forward-ref stub, update it in place.
- BasSymbolT *sym = basSymTabFindGlobal(&p->sym, name);
-
- if (sym == NULL) {
- bool savedLocal = p->sym.inLocalScope;
- p->sym.inLocalScope = false;
- sym = basSymTabAdd(&p->sym, name,
- isFn ? SYM_FUNCTION : SYM_SUB,
- returnType);
- p->sym.inLocalScope = savedLocal;
- }
-
- if (sym != NULL) {
- sym->scope = SCOPE_GLOBAL;
- sym->dataType = returnType;
- sym->paramCount = paramCount;
- sym->requiredParams = requiredCount;
-
- for (int32_t i = 0; i < paramCount; i++) {
- sym->paramTypes[i] = paramTypes[i];
- sym->paramByVal[i] = paramByVal[i];
- sym->paramOptional[i] = paramOptional[i];
- }
-
- // basSymTabAdd defaults isDefined=true; clear it so
- // call sites that encounter this symbol before the real
- // body is parsed register themselves as forward-refs
- // (patchAddrs). The real parseSub/parseFunction pass
- // sets isDefined=true and fills in codeAddr, at which
- // point patchCallAddrs backpatches the forward refs.
- sym->isDefined = false;
- sym->codeAddr = 0;
- }
- }
-
- p->lex = savedLex;
- p->hasError = savedErr;
- p->errorLine = savedErrLn;
- snprintf(p->error, sizeof(p->error), "%s", savedErrMsg);
-}
-
-
static void parseModule(BasParserT *p) {
// VB semantics: all SUB/FUNCTION declarations are visible from
// anywhere in the module regardless of source order. Do a
@@ -3881,7 +3690,11 @@ static void parseName(BasParserT *p) {
static void parseNotExpr(BasParserT *p) {
if (check(p, TOK_NOT)) {
advance(p);
+ if (!exprEnter(p)) {
+ return;
+ }
parseNotExpr(p);
+ exprLeave(p);
basEmit8(&p->cg, OP_NOT);
return;
}
@@ -4109,17 +3922,18 @@ static void parseOpen(BasParserT *p) {
// Channel number expression
parseExpression(p);
- // Optional LEN = recordsize (for RANDOM mode)
+ // Optional LEN = recordsize: the RANDOM-mode record length used by
+ // GET/PUT record numbers. Defaults when omitted.
if (checkKeyword(p, "LEN")) {
advance(p); // consume LEN
expect(p, TOK_EQ);
- // For now we just parse and discard -- record length is not
- // enforced at the VM level (GET/PUT use variable type size)
parseExpression(p);
- basEmit8(&p->cg, OP_POP);
+ } else {
+ basEmit8(&p->cg, OP_PUSH_INT16);
+ basEmit16(&p->cg, BAS_VM_RANDOM_RECORD_SIZE);
}
- // Emit: stack has [filename, channel] -- OP_FILE_OPEN reads mode byte
+ // Emit: stack has [filename, channel, recLen] -- OP_FILE_OPEN reads mode byte
basEmit8(&p->cg, OP_FILE_OPEN);
basEmit8(&p->cg, mode);
}
@@ -4156,11 +3970,11 @@ static void parseOption(BasParserT *p) {
if (check(p, TOK_BINARY)) {
advance(p);
basEmit8(&p->cg, OP_COMPARE_MODE);
- basEmit8(&p->cg, 0);
+ basEmit8(&p->cg, BAS_COMPARE_MODE_BINARY);
} else if (checkKeyword(p, "TEXT")) {
advance(p);
basEmit8(&p->cg, OP_COMPARE_MODE);
- basEmit8(&p->cg, 1);
+ basEmit8(&p->cg, BAS_COMPARE_MODE_TEXT);
} else {
error(p, "Expected BINARY or TEXT after OPTION COMPARE");
}
@@ -4188,6 +4002,96 @@ static void parseOrExpr(BasParserT *p) {
}
+// Parse an optional "(param, ...)" list for any SUB/FUNCTION
+// declaration form. Each parameter is [OPTIONAL] [BYVAL] name [AS type].
+// With PARAM_REGISTER the parameters are also added as SCOPE_LOCAL
+// variables in slot order (the caller has already entered local scope).
+// Returns false with an error set on any problem.
+static bool parseParamList(BasParserT *p, ParamListT *pl, uint32_t flags) {
+ memset(pl, 0, sizeof(*pl));
+
+ if (!match(p, TOK_LPAREN)) {
+ return !p->hasError;
+ }
+
+ bool seenOptional = false;
+
+ while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) {
+ if (pl->count > 0) {
+ expect(p, TOK_COMMA);
+ }
+
+ bool optional = match(p, TOK_OPTIONAL);
+
+ if (optional) {
+ seenOptional = true;
+ } else if (seenOptional) {
+ error(p, "Required parameter cannot follow Optional parameter");
+ return false;
+ }
+
+ bool byVal = match(p, TOK_BYVAL) || (flags & PARAM_FORCE_BYVAL) != 0;
+
+ if (!check(p, TOK_IDENT)) {
+ errorExpected(p, "parameter name");
+ return false;
+ }
+
+ if (clampParamCount(p, pl->count)) {
+ return false;
+ }
+
+ char paramName[BAS_MAX_TOKEN_LEN];
+ strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
+ paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
+ advance(p);
+
+ uint8_t pdt = suffixToType(paramName);
+ bool pTyped = nameHasTypeSuffix(paramName);
+ int32_t pUdtTypeId = -1;
+
+ if (match(p, TOK_AS)) {
+ pdt = resolveParamType(p, (flags & PARAM_PRESCAN) != 0, &pUdtTypeId);
+ pTyped = true;
+ }
+
+ if (p->hasError) {
+ return false;
+ }
+
+ if (flags & PARAM_REGISTER) {
+ BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
+
+ if (paramSym == NULL) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "Duplicate parameter name '%s'", paramName);
+ error(p, buf);
+ return false;
+ }
+
+ paramSym->scope = SCOPE_LOCAL;
+ paramSym->index = basSymTabAllocSlot(&p->sym);
+ paramSym->isDefined = true;
+ paramSym->udtTypeId = pUdtTypeId;
+ paramSym->isTyped = pTyped;
+ }
+
+ pl->types[pl->count] = pdt;
+ pl->byVal[pl->count] = byVal;
+ pl->optional[pl->count] = optional;
+
+ if (!optional) {
+ pl->required = pl->count + 1;
+ }
+
+ pl->count++;
+ }
+
+ expect(p, TOK_RPAREN);
+ return !p->hasError;
+}
+
+
static void parsePowExpr(BasParserT *p) {
parsePrimary(p);
while (!p->hasError && check(p, TOK_CARET)) {
@@ -4202,15 +4106,17 @@ static void parsePowExpr(BasParserT *p) {
static void parsePowOperand(BasParserT *p) {
- if (check(p, TOK_MINUS)) {
- advance(p);
- parsePowOperand(p);
- basEmit8(&p->cg, OP_NEG_INT);
- return;
- }
- if (check(p, TOK_PLUS)) {
+ if (check(p, TOK_MINUS) || check(p, TOK_PLUS)) {
+ bool negate = check(p, TOK_MINUS);
advance(p); // unary plus is a no-op
+ if (!exprEnter(p)) {
+ return;
+ }
parsePowOperand(p);
+ exprLeave(p);
+ if (negate) {
+ basEmit8(&p->cg, OP_NEG_INT);
+ }
return;
}
parsePrimary(p);
@@ -4577,7 +4483,7 @@ static void parsePrimary(BasParserT *p) {
advance(p);
// INPUT$(n, #channel) -- special handling for optional # in second arg
- if (checkKeywordText(name, "INPUT$") && check(p, TOK_LPAREN)) {
+ if (strcasecmp(name, "INPUT$") == 0 && check(p, TOK_LPAREN)) {
expect(p, TOK_LPAREN);
parseExpression(p); // n (number of chars)
expect(p, TOK_COMMA);
@@ -4867,6 +4773,13 @@ static void parsePrimary(BasParserT *p) {
return;
}
+ // A FUNCTION named without an argument list is a call with no
+ // arguments (QB allows "x = f" for a parameterless FUNCTION).
+ if (sym != NULL && sym->kind == SYM_FUNCTION) {
+ emitCallWithArgs(p, sym, 0);
+ return;
+ }
+
// Plain variable reference
sym = ensureVariable(p, name);
if (sym != NULL) {
@@ -5109,7 +5022,7 @@ static void parseRead(BasParserT *p) {
static void parseRedim(BasParserT *p) {
- // REDIM [PRESERVE] var(bounds) AS type
+ // REDIM [PRESERVE] var(bounds) [AS type]
advance(p); // consume REDIM
uint8_t preserve = 0;
@@ -5128,11 +5041,16 @@ static void parseRedim(BasParserT *p) {
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
- BasSymbolT *sym = basSymTabFind(&p->sym, name);
+ // ensureVariable finds an existing symbol or auto-declares one; a
+ // SUB/FUNCTION/label/CONST of that name is not a REDIM target.
+ BasSymbolT *sym = ensureVariable(p, name);
if (sym == NULL) {
- sym = ensureVariable(p, name);
+ return;
}
- if (sym == NULL) {
+ if (sym->kind != SYM_VARIABLE) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "'%s' is not a variable", name);
+ error(p, buf);
return;
}
sym->isArray = true;
@@ -5146,18 +5064,40 @@ static void parseRedim(BasParserT *p) {
parseDimBounds(p, &dims);
expect(p, TOK_RPAREN);
- // Optional AS type
+ // Element type: the AS clause when given, else the variable's own
+ // declared type (a first REDIM adopts the AS type so later element
+ // and field accesses resolve against it).
+ uint8_t dt = sym->dataType;
+ int32_t udtTypeId = sym->udtTypeId;
if (match(p, TOK_AS)) {
- resolveTypeName(p);
+ dt = resolveTypeName(p);
+ udtTypeId = (dt == BAS_TYPE_UDT) ? p->lastUdtTypeId : -1;
+ sym->dataType = dt;
+ sym->udtTypeId = udtTypeId;
}
if (p->hasError) {
return;
}
+ // UDT element arrays carry typeId and fieldCount after the bounds,
+ // exactly as OP_DIM_ARRAY does, so REDIM can seed every element.
+ if (dt == BAS_TYPE_UDT) {
+ BasSymbolT *typeSym = (udtTypeId >= 0) ? findTypeDefById(p, udtTypeId) : NULL;
+ if (typeSym == NULL) {
+ error(p, "Unknown TYPE definition");
+ return;
+ }
+ basEmit8(&p->cg, OP_PUSH_INT16);
+ basEmit16(&p->cg, (int16_t)udtTypeId);
+ basEmit8(&p->cg, OP_PUSH_INT16);
+ basEmit16(&p->cg, (int16_t)typeSym->fieldCount);
+ }
+
basEmit8(&p->cg, OP_REDIM);
basEmit8(&p->cg, (uint8_t)dims);
basEmit8(&p->cg, preserve);
+ basEmit8(&p->cg, dt);
emitStore(p, sym);
}
@@ -5310,20 +5250,41 @@ static void parseSelectCase(BasParserT *p) {
uint8_t cmpOp;
- if (check(p, TOK_LT)) { cmpOp = OP_CMP_LT; advance(p); }
- else if (check(p, TOK_GT)) { cmpOp = OP_CMP_GT; advance(p); }
- else if (check(p, TOK_LE)) { cmpOp = OP_CMP_LE; advance(p); }
- else if (check(p, TOK_GE)) { cmpOp = OP_CMP_GE; advance(p); }
- else if (check(p, TOK_EQ)) { cmpOp = OP_CMP_EQ; advance(p); }
- else if (check(p, TOK_NE)) { cmpOp = OP_CMP_NE; advance(p); }
- else {
- error(p, "Expected comparison operator after IS");
- arrfree(bodyJumps);
- arrfree(endJumps);
- p->selectDepth--;
- return;
+ switch (p->lex.token.type) {
+ case TOK_LT:
+ cmpOp = OP_CMP_LT;
+ break;
+
+ case TOK_GT:
+ cmpOp = OP_CMP_GT;
+ break;
+
+ case TOK_LE:
+ cmpOp = OP_CMP_LE;
+ break;
+
+ case TOK_GE:
+ cmpOp = OP_CMP_GE;
+ break;
+
+ case TOK_EQ:
+ cmpOp = OP_CMP_EQ;
+ break;
+
+ case TOK_NE:
+ cmpOp = OP_CMP_NE;
+ break;
+
+ default:
+ error(p, "Expected comparison operator after IS");
+ arrfree(bodyJumps);
+ arrfree(endJumps);
+ p->selectDepth--;
+ return;
}
+ advance(p);
+
basEmit8(&p->cg, OP_DUP);
parseExpression(p);
basEmit8(&p->cg, cmpOp);
@@ -5454,6 +5415,36 @@ static void parseShell(BasParserT *p) {
}
+// Optional modal flag after form.Show / Me.Show: a literal, TRUE/FALSE
+// or a CONST. OP_SHOW_FORM takes the flag as an immediate operand, so a
+// variable cannot be accepted here.
+static uint8_t parseShowModalFlag(BasParserT *p) {
+ uint8_t modal = 0;
+
+ if (check(p, TOK_INT_LIT)) {
+ modal = (p->lex.token.intVal != 0);
+ advance(p);
+ } else if (check(p, TOK_TRUE_KW)) {
+ modal = 1;
+ advance(p);
+ } else if (check(p, TOK_FALSE_KW)) {
+ advance(p);
+ } else if (check(p, TOK_IDENT)) {
+ BasSymbolT *modSym = basSymTabFind(&p->sym, p->lex.token.text);
+
+ if (modSym == NULL || modSym->kind != SYM_CONST) {
+ error(p, "Show modal flag must be a literal or CONST");
+ return 0;
+ }
+
+ modal = (modSym->constInt != 0);
+ advance(p);
+ }
+
+ return modal;
+}
+
+
static void parseSleep(BasParserT *p) {
// SLEEP [seconds]
// If no argument, default to 1 second
@@ -5482,11 +5473,24 @@ static void parseStatement(BasParserT *p) {
return;
}
+ // Every nested block (IF/FOR/DO/SELECT/SUB body) recurses through
+ // here; bound the depth so a pathological input is an error, not a
+ // stack overflow.
+ if (p->blockDepth >= BAS_MAX_BLOCK_DEPTH) {
+ error(p, "Statements nested too deeply");
+ return;
+ }
+
+ p->blockDepth++;
+
// Emit source line number for debugger (before statement code)
basEmit8(&p->cg, OP_LINE);
basEmitU16(&p->cg, (uint16_t)p->lex.token.line);
- BasTokenTypeE tt = p->lex.token.type;
+ // A label "Name:" consumes its own colon and may be followed by a
+ // statement on the same line, so it skips the end-of-statement check.
+ bool endOfStatement = true;
+ BasTokenTypeE tt = p->lex.token.type;
switch (tt) {
case TOK_PRINT:
@@ -5694,7 +5698,11 @@ static void parseStatement(BasParserT *p) {
// Inside SUB/FUNCTION: return from subroutine
basEmit8(&p->cg, OP_RET);
} else {
- // Module level: GOSUB return (pop PC from eval stack)
+ // Module level: GOSUB return (pop PC from eval stack).
+ // Any SELECT CASE opened since the handler's label still
+ // has its test value above the return address; discard
+ // those first or OP_GOSUB_RET would pop a test value.
+ emitSelectPops(p, p->selectDepth - p->lastLabelSelectDepth);
basEmit8(&p->cg, OP_GOSUB_RET);
}
break;
@@ -5729,49 +5737,22 @@ static void parseStatement(BasParserT *p) {
sym->scope = SCOPE_GLOBAL;
sym->isDefined = false;
sym->codeAddr = 0;
+ } else if (sym->kind != SYM_SUB && sym->kind != SYM_FUNCTION) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "'%s' is not a SUB or FUNCTION", sym->name);
+ error(p, buf);
+ break;
}
+ // CALL name(args) or bare CALL name (no arguments)
+ int32_t argc = 0;
+
if (check(p, TOK_LPAREN)) {
- emitFunctionCall(p, sym);
- } else {
- // CALL with no arguments. For an already-defined target,
- // verify it does not require arguments. Forward references
- // (isDefined == false) have no param info yet, so they are
- // not checked here -- this mirrors the arity logic in
- // emitFunctionCall() and the bare-sub-call path.
- int32_t minArgs = sym->requiredParams;
- if (minArgs == 0 && sym->paramCount > 0) {
- bool hasOptional = false;
- for (int32_t i = 0; i < sym->paramCount; i++) {
- if (sym->paramOptional[i]) {
- hasOptional = true;
- break;
- }
- }
- if (!hasOptional) {
- minArgs = sym->paramCount;
- }
- }
-
- if (sym->isDefined && minArgs > 0) {
- char buf[BAS_PARSE_ERR_SCRATCH];
- snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got 0", sym->name, (int)minArgs);
- error(p, buf);
- break;
- }
-
- uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0;
- basEmit8(&p->cg, OP_CALL);
- int32_t addrPos = basCodePos(&p->cg);
- basEmitU16(&p->cg, (uint16_t)sym->codeAddr);
- basEmit8(&p->cg, 0);
- basEmit8(&p->cg, baseSlot);
-
- if (!sym->isDefined) {
- arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
- }
+ argc = parseCallArgs(p, sym, true);
}
+ emitCallWithArgs(p, sym, argc);
+
if (sym->kind == SYM_FUNCTION) {
basEmit8(&p->cg, OP_POP); // discard return value
}
@@ -5783,7 +5764,7 @@ static void parseStatement(BasParserT *p) {
if (check(p, TOK_TIMER)) {
advance(p);
basEmit8(&p->cg, OP_PUSH_INT16);
- basEmit16(&p->cg, -1);
+ basEmit16(&p->cg, BAS_RANDOMIZE_TIMER_SEED);
} else {
parseExpression(p);
}
@@ -5954,19 +5935,7 @@ static void parseStatement(BasParserT *p) {
if (strcasecmp(meMember, "Show") == 0) {
// Me.Show [modal]
basEmit8(&p->cg, OP_ME_REF);
- uint8_t modal = 0;
- if (check(p, TOK_INT_LIT)) {
- if (p->lex.token.intVal != 0) {
- modal = 1;
- }
- advance(p);
- } else if (check(p, TOK_IDENT)) {
- BasSymbolT *modSym = basSymTabFind(&p->sym, p->lex.token.text);
- if (modSym && modSym->kind == SYM_CONST && modSym->constInt != 0) {
- modal = 1;
- }
- advance(p);
- }
+ uint8_t modal = parseShowModalFlag(p);
basEmit8(&p->cg, OP_SHOW_FORM);
basEmit8(&p->cg, modal);
} else if (strcasecmp(meMember, "Hide") == 0) {
@@ -6016,20 +5985,7 @@ static void parseStatement(BasParserT *p) {
uint16_t methodIdx = basAddConstant(&p->cg, propName, (int32_t)strlen(propName));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, methodIdx);
- int32_t argc = 0;
- while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE) && !p->hasError) {
- if (argc > 0 && check(p, TOK_COMMA)) {
- advance(p);
- }
- parseExpression(p);
- argc++;
- }
- if (clampArgCount(p, argc)) {
- return;
- }
- basEmit8(&p->cg, OP_CALL_METHOD);
- basEmit8(&p->cg, (uint8_t)argc);
- basEmit8(&p->cg, OP_POP);
+ emitMethodCallStatement(p);
}
} else if (check(p, TOK_EQ)) {
// Me.Property = expr (form-level property set)
@@ -6046,20 +6002,7 @@ static void parseStatement(BasParserT *p) {
uint16_t methodIdx = basAddConstant(&p->cg, meMember, (int32_t)strlen(meMember));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, methodIdx);
- int32_t argc = 0;
- while (!check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE) && !p->hasError) {
- if (argc > 0 && check(p, TOK_COMMA)) {
- advance(p);
- }
- parseExpression(p);
- argc++;
- }
- if (clampArgCount(p, argc)) {
- return;
- }
- basEmit8(&p->cg, OP_CALL_METHOD);
- basEmit8(&p->cg, (uint8_t)argc);
- basEmit8(&p->cg, OP_POP);
+ emitMethodCallStatement(p);
}
break;
}
@@ -6100,13 +6043,22 @@ static void parseStatement(BasParserT *p) {
// values of the SELECT blocks it actually leaves.
BasSymbolT *sym = basSymTabFind(&p->sym, labelName);
if (sym != NULL && sym->kind == SYM_LABEL) {
+ if (sym->isDefined) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "Duplicate label '%s'", labelName);
+ error(p, buf);
+ break;
+ }
// Forward-declared label -- now define it
sym->codeAddr = basCodePos(&p->cg);
sym->isDefined = true;
sym->localCount = p->selectDepth;
patchLabelRefs(p, sym);
} else if (sym == NULL) {
+ bool savedLocal = p->sym.inLocalScope;
+ p->sym.inLocalScope = false;
sym = basSymTabAdd(&p->sym, labelName, SYM_LABEL, 0);
+ p->sym.inLocalScope = savedLocal;
if (sym == NULL) {
error(p, "Symbol table full");
break;
@@ -6119,10 +6071,13 @@ static void parseStatement(BasParserT *p) {
char buf[BAS_PARSE_ERR_SCRATCH];
snprintf(buf, sizeof(buf), "Name '%s' already used", labelName);
error(p, buf);
+ break;
}
+ p->lastLabelSelectDepth = p->selectDepth;
// After the label, there may be a statement on the same line
// which will be parsed on the next iteration
- return;
+ endOfStatement = false;
+ break;
}
// Not a label -- restore and parse as assignment/call
@@ -6144,7 +6099,9 @@ static void parseStatement(BasParserT *p) {
}
}
- if (!p->hasError) {
+ p->blockDepth--;
+
+ if (endOfStatement && !p->hasError) {
expectEndOfStatement(p);
}
}
@@ -6172,9 +6129,11 @@ static void parseStatic(BasParserT *p) {
advance(p);
// Optional AS type
- uint8_t dt = suffixToType(varName);
+ uint8_t dt = suffixToType(varName);
+ bool isTyped = nameHasTypeSuffix(varName);
if (match(p, TOK_AS)) {
- dt = resolveTypeName(p);
+ dt = resolveTypeName(p);
+ isTyped = true;
}
if (p->hasError) {
@@ -6190,13 +6149,17 @@ static void parseStatic(BasParserT *p) {
return;
}
- // Create a mangled global name: "procName$varName"
- // Truncation is intentional -- symbol names are clamped to BAS_MAX_SYMBOL_NAME.
- char mangledName[BAS_MAX_SYMBOL_NAME * 2 + 1];
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wformat-truncation"
- snprintf(mangledName, sizeof(mangledName), "%s$%s", p->currentProc, varName);
-#pragma GCC diagnostic pop
+ // Create a mangled global name: "procName$varName". It must fit a
+ // symbol name like any other; the symbol table never truncates.
+ char mangledName[BAS_MAX_IDENT * 2 + 1];
+ int32_t mangledLen = snprintf(mangledName, sizeof(mangledName), "%s$%s", p->currentProc, varName);
+
+ if (mangledLen >= BAS_MAX_IDENT) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "STATIC name '%s' is too long (procedure plus variable name must be under %d characters)", varName, (int)BAS_MAX_IDENT);
+ error(p, buf);
+ return;
+ }
// Create the global variable with the mangled name
bool savedLocal = p->sym.inLocalScope;
@@ -6211,6 +6174,7 @@ static void parseStatic(BasParserT *p) {
globalSym->scope = SCOPE_GLOBAL;
globalSym->index = basSymTabAllocGlobalSlot(&p->sym);
globalSym->isDefined = true;
+ globalSym->isTyped = isTyped;
// Create a local alias that maps to this global's index
BasSymbolT *localSym = basSymTabAdd(&p->sym, varName, SYM_VARIABLE, dt);
@@ -6240,6 +6204,11 @@ static void parseSub(BasParserT *p) {
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
+ if (p->sym.inLocalScope) {
+ error(p, "SUB cannot be defined inside SUB or FUNCTION");
+ return;
+ }
+
// Save current proc name for STATIC variable mangling
strncpy(p->currentProc, name, BAS_MAX_TOKEN_LEN - 1);
p->currentProc[BAS_MAX_TOKEN_LEN - 1] = '\0';
@@ -6252,88 +6221,29 @@ static void parseSub(BasParserT *p) {
// Enter local scope
basSymTabEnterLocal(&p->sym);
- ExitListT savedExitSub = p->exitSubList;
- exitListInit(&p->exitSubList);
+ ProcSaveT save;
+ procBegin(p, &save, false);
- // Parse parameter list
- int32_t paramCount = 0;
- int32_t requiredCount = 0;
- bool seenOptional = false;
- uint8_t paramTypes[BAS_MAX_PARAMS];
- bool paramByVal[BAS_MAX_PARAMS];
- bool paramOptional[BAS_MAX_PARAMS];
+ ParamListT pl;
- if (match(p, TOK_LPAREN)) {
- while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !p->hasError) {
- if (paramCount > 0) {
- expect(p, TOK_COMMA);
- }
-
- bool optional = false;
- if (match(p, TOK_OPTIONAL)) {
- optional = true;
- seenOptional = true;
- } else if (seenOptional) {
- error(p, "Required parameter cannot follow Optional parameter");
- return;
- }
-
- bool byVal = false;
- if (match(p, TOK_BYVAL)) {
- byVal = true;
- }
-
- if (!check(p, TOK_IDENT)) {
- errorExpected(p, "parameter name");
- return;
- }
-
- if (clampParamCount(p, paramCount)) {
- return;
- }
-
- char paramName[BAS_MAX_TOKEN_LEN];
- strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
- paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
- advance(p);
-
- uint8_t pdt = suffixToType(paramName);
- int32_t pUdtTypeId = -1;
- if (match(p, TOK_AS)) {
- pdt = resolveTypeName(p);
- if (pdt == BAS_TYPE_UDT) {
- pUdtTypeId = p->lastUdtTypeId;
- }
- }
-
- BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
- if (paramSym == NULL) {
- error(p, "Symbol table full");
- return;
- }
- paramSym->scope = SCOPE_LOCAL;
- paramSym->index = basSymTabAllocSlot(&p->sym);
- paramSym->isDefined = true;
- paramSym->udtTypeId = pUdtTypeId;
-
- paramTypes[paramCount] = pdt;
- paramByVal[paramCount] = byVal;
- paramOptional[paramCount] = optional;
-
- if (!optional) {
- requiredCount = paramCount + 1;
- }
-
- paramCount++;
- }
- expect(p, TOK_RPAREN);
+ if (!parseParamList(p, &pl, PARAM_REGISTER)) {
+ procEnd(p, &save);
+ return;
}
- // Register the sub in the symbol table (global scope)
+ // Register the sub in the symbol table (global scope). See
+ // parseFunction for the duplicate-body rule.
BasSymbolT *existing = basSymTabFindGlobal(&p->sym, name);
- BasSymbolT *subSym = NULL;
+ BasSymbolT *subSym = NULL;
if (existing != NULL && existing->kind == SYM_SUB) {
+ if (existing->isDefined) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "Sub '%s' is already defined", existing->name);
+ error(p, buf);
+ procEnd(p, &save);
+ return;
+ }
subSym = existing;
} else {
bool savedLocal = p->sym.inLocalScope;
@@ -6344,19 +6254,14 @@ static void parseSub(BasParserT *p) {
if (subSym == NULL) {
error(p, "Could not register subroutine");
+ procEnd(p, &save);
return;
}
- subSym->codeAddr = subAddr;
- subSym->isDefined = true;
- subSym->paramCount = paramCount;
- subSym->requiredParams = requiredCount;
- subSym->scope = SCOPE_GLOBAL;
- for (int32_t i = 0; i < paramCount; i++) {
- subSym->paramTypes[i] = paramTypes[i];
- subSym->paramByVal[i] = paramByVal[i];
- subSym->paramOptional[i] = paramOptional[i];
- }
+ subSym->codeAddr = subAddr;
+ subSym->isDefined = true;
+ subSym->scope = SCOPE_GLOBAL;
+ applyParamList(subSym, &pl);
// Record the owning form so fireCtrlEvent can bind the SUB's
// form-scope variables at call time. Prescan adds SUB symbols
@@ -6364,8 +6269,8 @@ static void parseSub(BasParserT *p) {
// wasn't populated by basSymTabAdd; set it here once we know we
// are inside a form scope.
if (p->sym.inFormScope && p->sym.formScopeName[0]) {
- strncpy(subSym->formName, p->sym.formScopeName, BAS_MAX_SYMBOL_NAME - 1);
- subSym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
+ strncpy(subSym->formName, p->sym.formScopeName, BAS_MAX_IDENT - 1);
+ subSym->formName[BAS_MAX_IDENT - 1] = '\0';
}
// Backpatch any forward-reference calls to this sub
@@ -6389,16 +6294,13 @@ static void parseSub(BasParserT *p) {
skipNewlines(p);
}
- // Patch EXIT SUB jumps
- exitListPatch(&p->exitSubList, p);
- p->exitSubList = savedExitSub;
-
- basEmit8(&p->cg, OP_RET);
+ // Patch EXIT SUB jumps, emit the epilogue and prologue, and restore
+ // the enclosing state
+ procEnd(p, &save);
// Leave local scope
collectDebugLocals(p, p->cg.debugProcCount++);
basSymTabLeaveLocal(&p->sym);
- p->currentProc[0] = '\0';
// Patch the skip jump
patchJump(p, skipJump);
@@ -6505,17 +6407,31 @@ static void parseType(BasParserT *p) {
BasFieldDefT field;
memset(&field, 0, sizeof(field));
- // Truncation is intentional -- field names are clamped to BAS_MAX_SYMBOL_NAME.
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wformat-truncation"
- snprintf(field.name, BAS_MAX_SYMBOL_NAME, "%s", p->lex.token.text);
-#pragma GCC diagnostic pop
+ // Identifiers are already bounded by checkIdentLength.
+ snprintf(field.name, sizeof(field.name), "%s", p->lex.token.text);
advance(p);
+ if (resolveFieldIndex(typeSym, field.name) >= 0) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "Duplicate field '%s' in TYPE '%s'", field.name, typeSym->name);
+ error(p, buf);
+ return;
+ }
+
expect(p, TOK_AS);
field.dataType = resolveTypeName(p);
if (field.dataType == BAS_TYPE_UDT) {
field.udtTypeId = p->lastUdtTypeId;
+
+ // The TYPE symbol is registered before its fields so nested
+ // types resolve; a field of the type being defined would make
+ // emitUdtInit recurse without end.
+ if (field.udtTypeId == typeSym->index) {
+ char buf[BAS_PARSE_ERR_SCRATCH];
+ snprintf(buf, sizeof(buf), "TYPE '%s' cannot contain a field of its own type", typeSym->name);
+ error(p, buf);
+ return;
+ }
}
arrput(typeSym->fields, field);
@@ -6528,15 +6444,31 @@ static void parseType(BasParserT *p) {
static void parseUnaryExpr(BasParserT *p) {
- if (check(p, TOK_MINUS)) {
- advance(p);
- parseUnaryExpr(p);
- basEmit8(&p->cg, OP_NEG_INT);
- return;
- }
- if (check(p, TOK_PLUS)) {
+ if (check(p, TOK_MINUS) || check(p, TOK_PLUS)) {
+ bool negate = check(p, TOK_MINUS);
+ // Let the lexer accept the magnitude of LONG's most negative
+ // value so "-2147483648" is a literal rather than an overflow.
+ p->lex.allowNegMagnitude = negate;
advance(p); // unary plus is a no-op
+ p->lex.allowNegMagnitude = false;
+ if (!exprEnter(p)) {
+ return;
+ }
+ if (negate && check(p, TOK_LONG_LIT) && p->lex.token.longVal == BAS_LONG_NEG_MAGNITUDE) {
+ // Fold the sign into the literal: the positive magnitude does
+ // not fit LONG, its negation does.
+ basEmit8(&p->cg, OP_PUSH_INT32);
+ basEmit16(&p->cg, (int16_t)(INT32_MIN & 0xFFFF));
+ basEmit16(&p->cg, (int16_t)((INT32_MIN >> 16) & 0xFFFF));
+ advance(p);
+ exprLeave(p);
+ return;
+ }
parseUnaryExpr(p);
+ exprLeave(p);
+ if (negate) {
+ basEmit8(&p->cg, OP_NEG_INT);
+ }
return;
}
parsePowExpr(p);
@@ -6549,11 +6481,9 @@ static void parseWhile(BasParserT *p) {
// WEND
advance(p); // consume WHILE
- ExitListT savedExitDo = p->exitDoList;
- exitListInit(&p->exitDoList);
-
- int32_t savedDoSelectBase = p->doSelectBase;
- p->doSelectBase = p->selectDepth;
+ // WHILE/WEND shares the DO exit list: EXIT DO leaves it.
+ LoopSaveT save;
+ loopBegin(p, false, &save);
int32_t loopTop = basCodePos(&p->cg);
@@ -6569,6 +6499,7 @@ static void parseWhile(BasParserT *p) {
}
if (p->hasError) {
+ loopEnd(p, false, &save);
return;
}
@@ -6583,9 +6514,7 @@ static void parseWhile(BasParserT *p) {
patchJump(p, falseJump);
// Patch EXIT DO jumps (WHILE/WEND uses the DO exit list)
- exitListPatch(&p->exitDoList, p);
- p->exitDoList = savedExitDo;
- p->doSelectBase = savedDoSelectBase;
+ loopEnd(p, false, &save);
}
@@ -6712,6 +6641,204 @@ static void patchLabelRefs(BasParserT *p, BasSymbolT *sym) {
}
+// Walk the token stream from current position, find every
+// top-level SUB/FUNCTION declaration, extract the signature
+// (name, params, return type), and register it in the symbol
+// table. Does not emit code. Saves and restores lexer position
+// so the main parse pass starts from the same point. This gives
+// VB-style forward visibility: call sites that appear earlier in
+// the source than the SUB definition still resolve to the right
+// paramCount / types.
+//
+// Lexer errors are skipped here (the main pass reports them with full
+// context); a malformed signature is a real syntax error and is
+// reported immediately, before a call site can misreport it as an
+// arity mismatch against a half-registered signature.
+static void prescanSignatures(BasParserT *p) {
+ BasLexerT savedLex = p->lex;
+
+ while (!check(p, TOK_EOF)) {
+ // Clear a latched lexer error so the walk continues; advance()
+ // refuses to move while hasError is set, so leaving it latched
+ // would spin this loop forever.
+ if (p->hasError) {
+ p->hasError = false;
+ p->errorLine = 0;
+ p->error[0] = '\0';
+ }
+
+ // "END SUB" / "END FUNCTION" consume the END and the following
+ // keyword as separate tokens; skip END so the next-iteration
+ // SUB/FUNCTION check doesn't misinterpret it as a declaration.
+ if (check(p, TOK_END)) {
+ advance(p);
+ continue;
+ }
+
+ bool isFn = check(p, TOK_FUNCTION);
+ bool isSub = check(p, TOK_SUB);
+
+ if (!isFn && !isSub) {
+ advance(p);
+ continue;
+ }
+
+ advance(p); // consume SUB / FUNCTION
+
+ if (!check(p, TOK_IDENT)) {
+ continue;
+ }
+
+ char name[BAS_MAX_TOKEN_LEN];
+ strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
+ name[BAS_MAX_TOKEN_LEN - 1] = '\0';
+ advance(p);
+
+ ParamListT pl;
+
+ if (!parseParamList(p, &pl, PARAM_PRESCAN)) {
+ break;
+ }
+
+ // FUNCTION return type (AS clause; or suffix on name)
+ uint8_t returnType = suffixToType(name);
+
+ if (isFn && match(p, TOK_AS)) {
+ int32_t udtTypeId;
+ returnType = resolveParamType(p, true, &udtTypeId);
+ }
+
+ if (p->hasError) {
+ break;
+ }
+
+ // Register / update the symbol. If a call site already
+ // created a forward-ref stub, update it in place.
+ BasSymbolT *sym = basSymTabFindGlobal(&p->sym, name);
+
+ if (sym == NULL) {
+ bool savedLocal = p->sym.inLocalScope;
+ p->sym.inLocalScope = false;
+ sym = basSymTabAdd(&p->sym, name, isFn ? SYM_FUNCTION : SYM_SUB, returnType);
+ p->sym.inLocalScope = savedLocal;
+ }
+
+ if (sym != NULL) {
+ sym->scope = SCOPE_GLOBAL;
+ sym->dataType = returnType;
+ applyParamList(sym, &pl);
+
+ // basSymTabAdd defaults isDefined=true; clear it so
+ // call sites that encounter this symbol before the real
+ // body is parsed register themselves as forward-refs
+ // (patchAddrs). The real parseSub/parseFunction pass
+ // sets isDefined=true and fills in codeAddr, at which
+ // point patchCallAddrs backpatches the forward refs.
+ sym->isDefined = false;
+ sym->codeAddr = 0;
+ }
+ }
+
+ p->lex = savedLex;
+}
+
+
+// Enter a SUB/FUNCTION body: save the enclosing EXIT lists and loop /
+// SELECT bookkeeping, start them fresh, and emit the entry jump the
+// prologue (procEnd) will patch.
+static void procBegin(BasParserT *p, ProcSaveT *save, bool isFunction) {
+ save->exitFor = p->exitForList;
+ save->exitDo = p->exitDoList;
+ save->exitSub = p->exitSubList;
+ save->exitFunc = p->exitFuncList;
+ save->forSelectBase = p->forSelectBase;
+ save->doSelectBase = p->doSelectBase;
+ save->selectDepth = p->selectDepth;
+ save->forDepth = p->forDepth;
+ save->doDepth = p->doDepth;
+ save->lastLabelSelectDepth = p->lastLabelSelectDepth;
+ save->isFunction = isFunction;
+
+ exitListInit(&p->exitForList);
+ exitListInit(&p->exitDoList);
+ exitListInit(&p->exitSubList);
+ exitListInit(&p->exitFuncList);
+ p->forSelectBase = 0;
+ p->doSelectBase = 0;
+ p->selectDepth = 0;
+ p->forDepth = 0;
+ p->doDepth = 0;
+ p->lastLabelSelectDepth = 0;
+ p->currentProcIsFunction = isFunction;
+
+ // Entry jump to the prologue. Implicit STRING locals only become
+ // known while the body is parsed, so their "" initialisation is
+ // emitted after the body and reached through this jump.
+ save->prologueJmp = emitJump(p, OP_JMP);
+ save->bodyAddr = basCodePos(&p->cg);
+}
+
+
+// Leave a SUB/FUNCTION body: land EXIT SUB/FUNCTION on the epilogue,
+// emit the return, emit the prologue for implicit STRING locals, and
+// restore the enclosing state. Runs on the error path too so the EXIT
+// lists opened by procBegin are never leaked.
+static void procEnd(BasParserT *p, ProcSaveT *save) {
+ exitListPatch(&p->exitSubList, p);
+ exitListPatch(&p->exitFuncList, p);
+
+ if (save->isFunction) {
+ // Load return value from slot 0 and return
+ basEmit8(&p->cg, OP_LOAD_LOCAL);
+ basEmitU16(&p->cg, 0);
+ basEmit8(&p->cg, OP_RET_VAL);
+ } else {
+ basEmit8(&p->cg, OP_RET);
+ }
+
+ // Prologue: "" into every implicit STRING local, then back to the
+ // body. With nothing to initialise the entry jump falls through.
+ int32_t prologueStart = basCodePos(&p->cg);
+
+ for (int32_t i = 0; i < p->sym.count; i++) {
+ BasSymbolT *sym = p->sym.symbols[i];
+
+ if (sym->scope == SCOPE_LOCAL && sym->kind == SYM_VARIABLE && sym->isImplicit && sym->dataType == BAS_TYPE_STRING) {
+ uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
+ basEmit8(&p->cg, OP_PUSH_STR);
+ basEmitU16(&p->cg, emptyIdx);
+ basEmit8(&p->cg, OP_STORE_LOCAL);
+ basEmitU16(&p->cg, (uint16_t)sym->index);
+ }
+ }
+
+ if (basCodePos(&p->cg) != prologueStart) {
+ basEmit8(&p->cg, OP_JMP);
+ basEmit16(&p->cg, relJumpOffset(p, save->bodyAddr, basCodePos(&p->cg)));
+ basPatch16(&p->cg, save->prologueJmp, relJumpOffset(p, prologueStart, save->prologueJmp));
+ } else {
+ basPatch16(&p->cg, save->prologueJmp, relJumpOffset(p, save->bodyAddr, save->prologueJmp));
+ }
+
+ // A loop left open by an error still owns its EXIT list.
+ arrfree(p->exitForList.patchAddr);
+ arrfree(p->exitDoList.patchAddr);
+
+ p->exitForList = save->exitFor;
+ p->exitDoList = save->exitDo;
+ p->exitSubList = save->exitSub;
+ p->exitFuncList = save->exitFunc;
+ p->forSelectBase = save->forSelectBase;
+ p->doSelectBase = save->doSelectBase;
+ p->selectDepth = save->selectDepth;
+ p->forDepth = save->forDepth;
+ p->doDepth = save->doDepth;
+ p->lastLabelSelectDepth = save->lastLabelSelectDepth;
+ p->currentProcIsFunction = false;
+ p->currentProc[0] = '\0';
+}
+
+
// Compute the operand for a relative jump whose 2-byte operand lives at
// operandAddr. A span outside the signed 16-bit range would silently
// wrap and jump somewhere wild, so surface it as a compile error; the
@@ -6729,18 +6856,7 @@ static int16_t relJumpOffset(BasParserT *p, int32_t target, int32_t operandAddr)
static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName) {
for (int32_t i = 0; i < typeSym->fieldCount; i++) {
- const char *a = typeSym->fields[i].name;
- const char *b = fieldName;
- bool eq = true;
- while (*a && *b) {
- if (toupper((unsigned char)*a) != toupper((unsigned char)*b)) {
- eq = false;
- break;
- }
- a++;
- b++;
- }
- if (eq && *a == '\0' && *b == '\0') {
+ if (strcasecmp(typeSym->fields[i].name, fieldName) == 0) {
return i;
}
}
@@ -6748,6 +6864,28 @@ static int32_t resolveFieldIndex(BasSymbolT *typeSym, const char *fieldName) {
}
+// Type after AS in a parameter list or FUNCTION return clause. During
+// the prescan the TYPE statements have not been parsed yet, so an
+// unknown identifier is accepted as a user type there (the main pass
+// validates it); otherwise this is resolveTypeName plus the UDT id.
+static uint8_t resolveParamType(BasParserT *p, bool prescan, int32_t *outUdtTypeId) {
+ *outUdtTypeId = -1;
+
+ if (prescan && check(p, TOK_IDENT) && findTypeDef(p, p->lex.token.text) == NULL) {
+ advance(p);
+ return BAS_TYPE_UDT;
+ }
+
+ uint8_t dt = resolveTypeName(p);
+
+ if (dt == BAS_TYPE_UDT) {
+ *outUdtTypeId = p->lastUdtTypeId;
+ }
+
+ return dt;
+}
+
+
static uint8_t resolveTypeName(BasParserT *p) {
// Expect a type keyword after AS
if (check(p, TOK_INTEGER)) {
@@ -6842,20 +6980,11 @@ static uint8_t suffixToType(const char *name) {
if (len == 0) {
return BAS_TYPE_SINGLE; // QB default
}
- switch (name[len - 1]) {
- case '%':
- return BAS_TYPE_INTEGER;
- case '&':
- return BAS_TYPE_LONG;
- case '!':
- return BAS_TYPE_SINGLE;
- case '#':
- return BAS_TYPE_DOUBLE;
- case '$':
- return BAS_TYPE_STRING;
- default:
- return BAS_TYPE_SINGLE; // QB default
+ int32_t dt = basTypeSuffixType(name[len - 1]);
+ if (dt < 0) {
+ return BAS_TYPE_SINGLE; // QB default
}
+ return (uint8_t)dt;
}
diff --git a/src/apps/kpunch/dvxbasic/compiler/parser.h b/src/apps/kpunch/dvxbasic/compiler/parser.h
index 27ea9ce..350d262 100644
--- a/src/apps/kpunch/dvxbasic/compiler/parser.h
+++ b/src/apps/kpunch/dvxbasic/compiler/parser.h
@@ -49,6 +49,18 @@
#define BAS_PARSE_ERROR_LEN 1024
#define BAS_PARSE_ERR_SCRATCH 512
+// Number of letters covered by DEFINT/DEFLNG/DEFSNG/DEFDBL/DEFSTR (A-Z).
+#define BAS_DEFTYPE_LETTERS ('Z' - 'A' + 1)
+#define BAS_DEFTYPE_NONE 0xFF // defType[] entry with no DEFxxx in effect (BAS_TYPE_INTEGER is 0, so 0 cannot mean unset)
+
+// Recursion guards for the recursive-descent parser. Each nested
+// expression or block costs a dozen or more C stack frames, so an
+// unbounded nesting depth would overflow the (small) DOS stack long
+// before the source became unreadable. These limits keep a
+// pathological input a compile error instead of a crash.
+#define BAS_MAX_EXPR_DEPTH 64
+#define BAS_MAX_BLOCK_DEPTH 32
+
// 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
@@ -87,7 +99,8 @@ typedef struct {
int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName
int32_t optionBase; // default array lower bound (0 or 1)
bool optionExplicit; // true = variables must be declared with DIM
- uint8_t defType[26]; // default type per letter (A-Z), set by DEFINT etc.
+ bool currentProcIsFunction; // true while parsing a FUNCTION body (EXIT FUNCTION / return-value assignment)
+ uint8_t defType[BAS_DEFTYPE_LETTERS]; // 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)
@@ -104,6 +117,18 @@ typedef struct {
int32_t selectDepth;
int32_t forSelectBase;
int32_t doSelectBase;
+ // Number of FOR / DO-WHILE loops currently open in the procedure (or
+ // module body) being parsed, so EXIT FOR / EXIT DO outside a loop is a
+ // compile error rather than a jump patched against an outer scope.
+ int32_t forDepth;
+ int32_t doDepth;
+ // SELECT depth at the most recently defined label. A module-level
+ // RETURN discards the test values of every SELECT opened since that
+ // label so OP_GOSUB_RET pops the return address, not a test value.
+ int32_t lastLabelSelectDepth;
+ // Recursion guards (see BAS_MAX_EXPR_DEPTH / BAS_MAX_BLOCK_DEPTH).
+ int32_t exprDepth;
+ int32_t blockDepth;
// Optional compile-time CtrlName.Member validator (IDE-only).
const BasCtrlValidatorT *validator;
} BasParserT;
diff --git a/src/apps/kpunch/dvxbasic/compiler/strip.c b/src/apps/kpunch/dvxbasic/compiler/strip.c
index 162e969..c34392b 100644
--- a/src/apps/kpunch/dvxbasic/compiler/strip.c
+++ b/src/apps/kpunch/dvxbasic/compiler/strip.c
@@ -30,8 +30,7 @@
// 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).
+// OP_LINE removal is a separate pass: see compact.c.
#include "strip.h"
#include "basEvents.h"
@@ -47,12 +46,7 @@
// 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",
+ BAS_EVENT_LIST(BAS_EVENT_SUFFIX)
NULL
};
@@ -120,7 +114,7 @@ void basStripModule(BasModuleT *mod) {
// Skip any generated name that collides with a kept proc (event
// handler or constant-pool-referenced) or a constant-pool entry, so
// basModuleFindProc can't resolve the wrong procedure at runtime.
- char cand[BAS_MAX_PROC_NAME];
+ char cand[BAS_MAX_IDENT];
snprintf(cand, sizeof(cand), "F%ld", (long)nextMangled++);
diff --git a/src/apps/kpunch/dvxbasic/compiler/strip.h b/src/apps/kpunch/dvxbasic/compiler/strip.h
index 7b4a283..ec499d1 100644
--- a/src/apps/kpunch/dvxbasic/compiler/strip.h
+++ b/src/apps/kpunch/dvxbasic/compiler/strip.h
@@ -25,7 +25,8 @@
// 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.
+// runtime name-based dispatch. OP_LINE removal is the separate
+// compaction pass in compact.h.
#ifndef DVXBASIC_STRIP_H
#define DVXBASIC_STRIP_H
diff --git a/src/apps/kpunch/dvxbasic/compiler/symtab.c b/src/apps/kpunch/dvxbasic/compiler/symtab.c
index 58e96d2..50b6c8a 100644
--- a/src/apps/kpunch/dvxbasic/compiler/symtab.c
+++ b/src/apps/kpunch/dvxbasic/compiler/symtab.c
@@ -29,13 +29,9 @@
#include
#include
-// Distance from a lowercase ASCII letter to its uppercase counterpart
-// ('a' - 'A'). Used by basAsciiUpper to fold case without touching
-// bytes outside a-z.
-#define BAS_ASCII_CASE_OFFSET 32
-
// Function prototypes (alphabetical)
+static void basSymbolFree(BasSymbolT *sym);
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
int32_t basSymTabAllocGlobalSlot(BasSymTabT *tab);
int32_t basSymTabAllocSlot(BasSymTabT *tab);
@@ -47,10 +43,19 @@ void basSymTabFree(BasSymTabT *tab);
void basSymTabInit(BasSymTabT *tab);
int32_t basSymTabLeaveFormScope(BasSymTabT *tab);
void basSymTabLeaveLocal(BasSymTabT *tab);
-static char basAsciiUpper(char c);
-static void basSymbolFree(BasSymbolT *sym);
-static bool namesEqual(const char *a, const char *b);
static uint32_t nameHashCI(const char *name);
+static bool namesEqual(const char *a, const char *b);
+
+
+// ============================================================
+// Free a single symbol: its dynamic arrays and the struct itself.
+// ============================================================
+static void basSymbolFree(BasSymbolT *sym) {
+ arrfree(sym->patchAddrs);
+ arrfree(sym->fields);
+ free(sym);
+}
+
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) {
// Determine scope: local > form > global.
@@ -65,6 +70,12 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
scope = SCOPE_GLOBAL;
}
+ // Never truncate: a clipped name would hash and compare differently
+ // from every later reference, silently splitting one variable in two.
+ if (strlen(name) >= BAS_MAX_IDENT) {
+ return NULL;
+ }
+
uint32_t h = nameHashCI(name);
// Check for duplicate in current scope (skip ended form symbols)
@@ -84,8 +95,7 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
return NULL;
}
- strncpy(sym->name, name, BAS_MAX_SYMBOL_NAME - 1);
- sym->name[BAS_MAX_SYMBOL_NAME - 1] = '\0';
+ strcpy(sym->name, name);
sym->nameHash = h;
sym->kind = kind;
sym->scope = scope;
@@ -99,8 +109,8 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
// 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';
+ strncpy(sym->formName, tab->formScopeName, BAS_MAX_IDENT - 1);
+ sym->formName[BAS_MAX_IDENT - 1] = '\0';
}
arrput(tab->symbols, sym);
@@ -129,8 +139,8 @@ int32_t basSymTabAllocSlot(BasSymTabT *tab) {
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';
+ strncpy(tab->formScopeName, formName, BAS_MAX_IDENT - 1);
+ tab->formScopeName[BAS_MAX_IDENT - 1] = '\0';
tab->nextFormVarIdx = 0;
tab->formScopeSymStart = tab->count;
}
@@ -251,30 +261,6 @@ void basSymTabLeaveLocal(BasSymTabT *tab) {
}
-// ============================================================
-// Fold a single ASCII byte to upper case. Char in, char out (not int
-// via toupper) so bytes >= 0x80 pass through unchanged and the uint8_t
-// then uint32_t cast in nameHashCI stays well defined.
-// ============================================================
-static char basAsciiUpper(char c) {
- if (c >= 'a' && c <= 'z') {
- c -= BAS_ASCII_CASE_OFFSET;
- }
-
- return c;
-}
-
-
-// ============================================================
-// Free a single symbol: its dynamic arrays and the struct itself.
-// ============================================================
-static void basSymbolFree(BasSymbolT *sym) {
- arrfree(sym->patchAddrs);
- arrfree(sym->fields);
- free(sym);
-}
-
-
// ============================================================
// Case-insensitive FNV-1a hash used to accelerate symbol-table lookups.
// Caller computes hash of search-name once; each entry stores its own
@@ -282,12 +268,12 @@ static void basSymbolFree(BasSymbolT *sym) {
// that nearly always rejects non-matches without calling namesEqual.
// ============================================================
static uint32_t nameHashCI(const char *name) {
- uint32_t h = 0x811C9DC5u;
+ uint32_t h = BAS_FNV1A_OFFSET;
while (*name) {
char c = basAsciiUpper(*name);
h ^= (uint32_t)(uint8_t)c;
- h *= 0x01000193u;
+ h *= BAS_FNV1A_PRIME;
name++;
}
diff --git a/src/apps/kpunch/dvxbasic/compiler/symtab.h b/src/apps/kpunch/dvxbasic/compiler/symtab.h
index b9731d6..28b778d 100644
--- a/src/apps/kpunch/dvxbasic/compiler/symtab.h
+++ b/src/apps/kpunch/dvxbasic/compiler/symtab.h
@@ -32,6 +32,8 @@
#define DVXBASIC_SYMTAB_H
#include "../compiler/opcodes.h"
+#include "../compiler/lexer.h"
+#include "../runtime/vm.h"
#include
#include
@@ -55,13 +57,20 @@ typedef enum {
// Symbol entry
// ============================================================
-#define BAS_MAX_SYMBOL_NAME 64
+// Symbol names share BAS_MAX_IDENT (vm.h) with every name buffer they are
+// copied into; CONST strings are copied straight from a token, so that
+// limit is defined in terms of the token buffer.
#define BAS_MAX_PARAMS 16
-#define BAS_MAX_CONST_STR 256 // max CONST string-literal length
+#define BAS_MAX_CONST_STR BAS_MAX_TOKEN_LEN // max CONST string-literal length
+
+// FNV-1a parameters shared by the case-insensitive symbol hash (symtab.c)
+// and the constant-pool interning hash (codegen.c).
+#define BAS_FNV1A_OFFSET 0x811C9DC5u
+#define BAS_FNV1A_PRIME 0x01000193u
// UDT field definition
typedef struct {
- char name[BAS_MAX_SYMBOL_NAME];
+ char name[BAS_MAX_IDENT];
uint8_t dataType; // BAS_TYPE_*
int32_t udtTypeId; // if dataType == BAS_TYPE_UDT, index of the TYPE_DEF symbol
} BasFieldDefT;
@@ -105,9 +114,12 @@ typedef struct {
bool isShared;
bool isExtern; // true = external library function (DECLARE LIBRARY)
bool formScopeEnded; // true = form scope ended, invisible to lookups
+ bool hasSignature; // true once paramCount/paramTypes describe the real declaration (false for a call-site stub)
+ bool isImplicit; // variable created by first use (no DIM); STRING locals need a prologue init
+ bool isTyped; // variable declared with an explicit numeric type (suffix, AS clause or DEFxxx); stores convert to it
- char name[BAS_MAX_SYMBOL_NAME];
- char formName[BAS_MAX_SYMBOL_NAME]; // form name for SCOPE_FORM vars
+ char name[BAS_MAX_IDENT];
+ char formName[BAS_MAX_IDENT]; // form name for SCOPE_FORM vars
uint8_t paramTypes[BAS_MAX_PARAMS];
bool paramByVal[BAS_MAX_PARAMS];
bool paramOptional[BAS_MAX_PARAMS]; // true = OPTIONAL parameter
@@ -131,7 +143,7 @@ typedef struct {
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
+ char formScopeName[BAS_MAX_IDENT]; // current form name
int32_t nextFormVarIdx; // next form-level variable slot
int32_t formScopeSymStart; // symbol count at BEGINFORM (for marking ended)
} BasSymTabT;
@@ -146,7 +158,9 @@ void basSymTabInit(BasSymTabT *tab);
void basSymTabFree(BasSymTabT *tab);
// Add a symbol. Returns the symbol pointer, or NULL if the name already
-// exists in the current scope or memory allocation fails.
+// exists in the current scope, is too long for BasSymbolT.name, or memory
+// allocation fails. Names are never truncated: a truncated name would
+// hash and compare differently from the source spelling.
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
// Look up a symbol by name. Searches local scope first, then global.
diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c
index 63b38f7..2dc3483 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formrt.c
+++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c
@@ -27,6 +27,7 @@
// registered by .wgt DXE files. No hardcoded control types.
#include "formrt.h"
+#include "../compiler/basEvents.h"
#include "../compiler/opcodes.h"
#include "../../../../libs/kpunch/serial/rs232/rs232.h"
#include "../../../../libs/kpunch/serial/seclink/secLink.h"
@@ -46,6 +47,7 @@
#include "dataCtrl/dataCtrl.h"
#include "dbGrid/dbGrid.h"
#include "frmParser.h"
+#include "../../../../tools/hlpcCompile.h"
#include "thirdparty/stb_ds_wrap.h"
#include
@@ -60,8 +62,6 @@
// Defines
// ============================================================
-#define DEFAULT_FORM_W 400
-#define DEFAULT_FORM_H 300
#define MAX_EVENT_NAME_LEN 128
#define MENU_ID_BASE 10000
@@ -69,20 +69,42 @@
#define BAS_CHOICE_MAX_ITEMS 64
#define BAS_CHOICE_BUF_LEN 1024
#define BAS_FILTER_BUF_LEN 1024
+#define BAS_INPUTBOX_BUF_LEN 512 // InputBox result text (both BASIC entry points)
+
+// Runtime-error report buffers: the formatted detail block, the summary
+// line, and the modal box that shows both.
+#define BAS_ERR_DETAIL_LEN 512
+#define BAS_ERR_SUMMARY_LEN 128
+#define BAS_ERR_BOX_LEN (BAS_ERR_SUMMARY_LEN + BAS_ERR_DETAIL_LEN)
+
+// dlsym name buffer: "_" + the longest BASIC identifier.
+#define BAS_MANGLED_NAME_LEN (BAS_MAX_IDENT + 2)
+
+// Comma-separated property-name list for "Valid: ..." diagnostics.
+#define BAS_PROP_LIST_LEN 256
+
+// ResGetText result cap (static return buffer).
+#define BAS_RES_TEXT_LEN 1024
// Module-level form runtime pointer for onFormClose callback
static BasFormRtT *sFormRt = NULL;
-// True while a basFormRtLoadFrm is in progress. The .frm parser calls
-// basFormRtLoadForm via onFormBegin to create the bare form before it
-// starts firing onCtrlBegin callbacks; if we let basFormRtLoadForm run
-// the form's init-code during that window the init fires against a
-// control list that is still empty (and then crashes with
-// "unknown control: cboSize" etc.). basFormRtLoadFrm sets this true
-// around frmParse so the nested LoadForm skips init; the outer
-// caller runs init (or the event loop) after parsing completes.
+// True only while frmParse is running inside basFormRtLoadFrm. The
+// parser calls basFormRtLoadForm via onFormBegin to create the bare form
+// before it starts firing onCtrlBegin callbacks; while this is set that
+// nested LoadForm must neither consult the .frm cache (it would recurse
+// into basFormRtLoadFrm forever) nor run the form's init code (it would
+// fire against a control list that is still empty). basFormRtLoadFrm
+// clears it again BEFORE its post-parse tail fires Resize/Load, so a
+// 'Load Form2' from Form1_Load takes the normal cached-.frm path.
static bool sLoadingFrm = false;
+// Tracks whether the most recent basInputBox2 call was cancelled.
+// BASIC callers query this via basInputCancelled so they can tell the
+// difference between the user hitting Cancel and the user clicking OK
+// on an empty field.
+static bool sLastInputBoxCancelled = false;
+
// ============================================================
// Module-scope declarations
@@ -206,8 +228,8 @@ static DvxResHandleT *sResHandles[RES_MAX_HANDLES];
// ============================================================
typedef struct {
- char caption[256];
- char name[BAS_MAX_CTRL_NAME];
+ char caption[FRM_MAX_LINE_LEN];
+ char name[BAS_MAX_IDENT];
int32_t level;
bool checked;
bool radioCheck;
@@ -220,19 +242,137 @@ typedef struct {
BasFormRtT *rt;
BasFormT *form;
BasControlT *current;
- WidgetT *parentStack[BAS_MAX_FRM_NESTING];
+ WidgetT *parentStack[FRM_MAX_NESTING];
int32_t nestDepth;
- bool containerStack[BAS_MAX_FRM_NESTING];
+ bool containerStack[FRM_MAX_NESTING];
int32_t containerDepth;
BasFrmMenuItemT *menuItems; // stb_ds array
int32_t curMenuItemIdx;
} BasFrmLoadCtxT;
+// ============================================================
+// Property descriptor tables (see formrt.h: BasPropDescT)
+// ============================================================
+
+// Form-object property ids. Each entry designates its slot in
+// sFormProps[], so the enum and the table cannot drift apart.
+typedef enum {
+ FORM_PROP_NAME,
+ FORM_PROP_CAPTION,
+ FORM_PROP_WIDTH,
+ FORM_PROP_HEIGHT,
+ FORM_PROP_LEFT,
+ FORM_PROP_TOP,
+ FORM_PROP_VISIBLE,
+ FORM_PROP_RESIZABLE,
+ FORM_PROP_AUTOSIZE,
+ FORM_PROP_CENTERED,
+ FORM_PROP_LAYOUT,
+ FORM_PROP_CONTEXTMENU,
+ FORM_PROP_HELPTOPIC,
+ FORM_PROP_COUNT
+} BasFormPropE;
+
+static const BasPropDescT sFormProps[FORM_PROP_COUNT] = {
+ [FORM_PROP_NAME] = { "Name", WGT_IFACE_STRING, true, false },
+ [FORM_PROP_CAPTION] = { "Caption", WGT_IFACE_STRING, true, true },
+ [FORM_PROP_WIDTH] = { "Width", WGT_IFACE_INT, true, true },
+ [FORM_PROP_HEIGHT] = { "Height", WGT_IFACE_INT, true, true },
+ [FORM_PROP_LEFT] = { "Left", WGT_IFACE_INT, true, true },
+ [FORM_PROP_TOP] = { "Top", WGT_IFACE_INT, true, true },
+ [FORM_PROP_VISIBLE] = { "Visible", WGT_IFACE_BOOL, true, true },
+ [FORM_PROP_RESIZABLE] = { "Resizable", WGT_IFACE_BOOL, true, true },
+ [FORM_PROP_AUTOSIZE] = { "AutoSize", WGT_IFACE_BOOL, true, true },
+ [FORM_PROP_CENTERED] = { "Centered", WGT_IFACE_BOOL, true, true },
+ [FORM_PROP_LAYOUT] = { "Layout", WGT_IFACE_STRING, true, false },
+ [FORM_PROP_CONTEXTMENU] = { "ContextMenu", WGT_IFACE_STRING, true, true },
+ [FORM_PROP_HELPTOPIC] = { "HelpTopic", WGT_IFACE_STRING, true, true },
+};
+
+// Common control property ids (every non-menu control, consulted after
+// the widget's own interface so a widget-declared property of the same
+// name -- Timer.Enabled, Data.Caption, TabControl.TabIndex -- wins).
+typedef enum {
+ CTRL_PROP_NAME,
+ CTRL_PROP_LEFT,
+ CTRL_PROP_TOP,
+ CTRL_PROP_WIDTH,
+ CTRL_PROP_HEIGHT,
+ CTRL_PROP_MINWIDTH,
+ CTRL_PROP_MINHEIGHT,
+ CTRL_PROP_MAXWIDTH,
+ CTRL_PROP_MAXHEIGHT,
+ CTRL_PROP_WEIGHT,
+ CTRL_PROP_VISIBLE,
+ CTRL_PROP_ENABLED,
+ CTRL_PROP_READONLY,
+ CTRL_PROP_BACKCOLOR,
+ CTRL_PROP_FORECOLOR,
+ CTRL_PROP_CAPTION,
+ CTRL_PROP_TEXT,
+ CTRL_PROP_TOOLTIPTEXT,
+ CTRL_PROP_CONTEXTMENU,
+ CTRL_PROP_HELPTOPIC,
+ CTRL_PROP_DATASOURCE,
+ CTRL_PROP_DATAFIELD,
+ CTRL_PROP_LISTCOUNT,
+ CTRL_PROP_COUNT
+} BasCtrlPropE;
+
+static const BasPropDescT sCtrlProps[CTRL_PROP_COUNT] = {
+ [CTRL_PROP_NAME] = { "Name", WGT_IFACE_STRING, true, false },
+ [CTRL_PROP_LEFT] = { "Left", WGT_IFACE_INT, true, true },
+ [CTRL_PROP_TOP] = { "Top", WGT_IFACE_INT, true, true },
+ [CTRL_PROP_WIDTH] = { "Width", WGT_IFACE_INT, true, true },
+ [CTRL_PROP_HEIGHT] = { "Height", WGT_IFACE_INT, true, true },
+ [CTRL_PROP_MINWIDTH] = { "MinWidth", WGT_IFACE_INT, true, true },
+ [CTRL_PROP_MINHEIGHT] = { "MinHeight", WGT_IFACE_INT, true, true },
+ [CTRL_PROP_MAXWIDTH] = { "MaxWidth", WGT_IFACE_INT, true, true },
+ [CTRL_PROP_MAXHEIGHT] = { "MaxHeight", WGT_IFACE_INT, true, true },
+ [CTRL_PROP_WEIGHT] = { "Weight", WGT_IFACE_INT, true, true },
+ [CTRL_PROP_VISIBLE] = { "Visible", WGT_IFACE_BOOL, true, true },
+ [CTRL_PROP_ENABLED] = { "Enabled", WGT_IFACE_BOOL, true, true },
+ [CTRL_PROP_READONLY] = { "ReadOnly", WGT_IFACE_BOOL, true, true },
+ [CTRL_PROP_BACKCOLOR] = { "BackColor", WGT_IFACE_INT, true, true },
+ [CTRL_PROP_FORECOLOR] = { "ForeColor", WGT_IFACE_INT, true, true },
+ [CTRL_PROP_CAPTION] = { "Caption", WGT_IFACE_STRING, true, true },
+ [CTRL_PROP_TEXT] = { "Text", WGT_IFACE_STRING, true, true },
+ [CTRL_PROP_TOOLTIPTEXT] = { "ToolTipText", WGT_IFACE_STRING, true, true },
+ [CTRL_PROP_CONTEXTMENU] = { "ContextMenu", WGT_IFACE_STRING, true, true },
+ [CTRL_PROP_HELPTOPIC] = { "HelpTopic", WGT_IFACE_STRING, true, true },
+ [CTRL_PROP_DATASOURCE] = { "DataSource", WGT_IFACE_STRING, true, true },
+ [CTRL_PROP_DATAFIELD] = { "DataField", WGT_IFACE_STRING, true, true },
+ [CTRL_PROP_LISTCOUNT] = { "ListCount", WGT_IFACE_INT, true, false },
+};
+
+
+// Common method names, indexed by BasCommonMethodE.
+static const char *const sCommonMethodNames[BAS_CM_COUNT] = {
+ [BAS_CM_SETFOCUS] = "SetFocus",
+ [BAS_CM_REFRESH] = "Refresh",
+ [BAS_CM_SETREADONLY] = "SetReadOnly",
+ [BAS_CM_SETENABLED] = "SetEnabled",
+ [BAS_CM_SETVISIBLE] = "SetVisible",
+ [BAS_CM_POPUPMENU] = "PopupMenu",
+ [BAS_CM_CREATEMENU] = "CreateMenu",
+ [BAS_CM_ADDMENUITEM] = "AddMenuItem",
+ [BAS_CM_ADDMENUSEPARATOR] = "AddMenuSeparator",
+ [BAS_CM_ADDSUBMENU] = "AddSubMenu",
+ [BAS_CM_DESTROYMENU] = "DestroyMenu",
+ [BAS_CM_ADDBUTTON] = "AddButton",
+ [BAS_CM_ADDTEXTBUTTON] = "AddTextButton",
+ [BAS_CM_ADDSEPARATOR] = "AddSeparator",
+ [BAS_CM_CLEAR] = "Clear",
+ [BAS_CM_BUTTONCOUNT] = "ButtonCount",
+};
+
+
// ============================================================
// Prototypes
// ============================================================
+static BasFormT *allocForm(BasFormRtT *rt, const char *formName, bool resizable, bool centered, int32_t width, int32_t height);
int32_t basChoiceDialog(const char *title, const char *prompt, const char *items, int32_t defaultIdx);
BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType);
void *basExternResolve(void *ctx, const char *libName, const char *funcName);
@@ -240,6 +380,8 @@ const char *basFileOpen(const char *title, const char *filter);
const char *basFileSave(const char *title, const char *filter);
void basFormRtBindVm(BasFormRtT *rt);
BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc);
+int32_t basFormRtCommonMethodId(const char *name);
+const BasPropDescT *basFormRtCommonProps(int32_t *count);
BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module);
WidgetT *basFormRtCreateContentBox(WidgetT *root, const char *layout);
void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const char *ctrlName);
@@ -248,25 +390,27 @@ void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_
WindowT *basFormRtCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox);
void basFormRtDestroy(BasFormRtT *rt);
void basFormRtEventLoop(BasFormRtT *rt);
+const BasPropDescT *basFormRtFindCommonProp(const char *name);
void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName);
void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32_t index);
+const BasPropDescT *basFormRtFindFormProp(const char *name);
bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName);
bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount);
static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName);
+const BasPropDescT *basFormRtFormProps(int32_t *count);
BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName);
void basFormRtHideForm(void *ctx, void *formRef);
static void basFormRtInitFormVars(BasFormRtT *rt, BasFormT *form, bool runInit);
static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText);
void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName);
-static void *basFormRtLoadCfm(BasFormRtT *rt, const uint8_t *data, int32_t dataLen);
void *basFormRtLoadForm(void *ctx, const char *formName);
BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen);
int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title);
-void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...);
-void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *data, int32_t dataLen);
void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen);
void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName);
void basFormRtRunSimple(BasFormRtT *rt);
+void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...);
+void basFormRtSerialShutdown(void);
void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName);
void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value);
void basFormRtShowForm(void *ctx, void *formRef, bool modal);
@@ -277,12 +421,8 @@ int32_t basInputCancelled(void);
int32_t basIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal);
static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
static uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult);
-static bool menuContainsMenu(const MenuT *tree, const MenuT *target);
-static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId, bool *fromBar);
-static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc);
int32_t basPromptSave(const char *title);
static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc);
-static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name);
void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt);
void CommClose(int32_t handle);
void CommDetach(int32_t handle);
@@ -302,27 +442,44 @@ static int32_t commTermWrite(void *ctx, const uint8_t *data, int32_t len);
WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent);
WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *parent, bool allowData);
static const char *ctrlDisplayName(const BasControlT *ctrl);
+static BasControlT *ctrlFromWidget(WidgetT *w, BasFormRtT **outRt);
+static void destroyFormNow(BasFormRtT *rt, BasFormT *form);
static void detachFormForUnload(BasFormRtT *rt, BasFormT *form);
+static BasControlT *findCtrlByWidget(BasFormT *form, const WidgetT *w);
static BasControlT *findCtrlInForm(BasFormT *form, const char *name);
static WidgetT *findCtrlWidgetByName(const char *name);
+static BasFormT *findFormByWindow(const WindowT *win);
+static BasControlT *findMenuProxyInForm(BasFormT *form, const char *name);
+static BasControlT *findNamedObjectInForm(BasFormT *form, const char *name);
+static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name);
+static const BasPropDescT *findPropDesc(const BasPropDescT *table, int32_t count, const char *name);
static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount);
+static void fireCtrlEventOut(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount);
+static bool fireEventArgsOut(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount);
+static void formatPropNames(const BasPropDescT *table, int32_t count, bool forWrite, char *buf, int32_t bufSize);
static void formRtCommIdleSync(void);
static void formRtDetachTermsForForm(BasFormT *form);
static bool formRtDetachTermsForWidget(WidgetT *widget);
static void formRtSerIdleSync(void);
static void freeControl(BasControlT *ctrl);
-static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char *name);
-static void frmLoad_onCtrlEnd(void *userData);
-static void frmLoad_onCtrlProp(void *userData, const char *key, const char *value);
-static bool frmLoad_onFormBegin(void *userData, const char *name);
-static void frmLoad_onFormProp(void *userData, const char *key, const char *value);
-static void frmLoad_onMenuBegin(void *userData, const char *name, int32_t level);
-static void frmLoad_onMenuEnd(void *userData);
-static void frmLoad_onMenuProp(void *userData, const char *key, const char *value);
+static void frmLoadOnCtrlBegin(void *userData, const char *typeName, const char *name);
+static void frmLoadOnCtrlEnd(void *userData);
+static void frmLoadOnCtrlProp(void *userData, const char *key, const char *value);
+static bool frmLoadOnFormBegin(void *userData, const char *name);
+static void frmLoadOnFormProp(void *userData, const char *key, const char *value);
+static void frmLoadOnMenuBegin(void *userData, const char *name, int32_t level);
+static void frmLoadOnMenuEnd(void *userData);
+static void frmLoadOnMenuProp(void *userData, const char *key, const char *value);
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled);
+static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName);
static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled);
int32_t HelpCompile(const char *inputFile, const char *outputFile);
void HelpView(const char *hlpFile);
+static int32_t ifaceEnumIndex(const WgtPropDescT *p, const char *name);
+static const WgtMethodDescT *ifaceFindMethod(const WgtIfaceT *iface, const char *name, uint8_t sig);
+static const char *ifaceGetStringProp(const BasControlT *ctrl, const char *propName);
+static bool menuContainsMenu(const MenuT *tree, const MenuT *target);
+static int32_t nextMenuItemId(BasFormT *form);
static void onFormActivate(WindowT *win);
static void onFormClose(WindowT *win);
static void onFormDeactivate(WindowT *win);
@@ -342,16 +499,21 @@ static void onWidgetMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y);
static void onWidgetScroll(WidgetT *w, int32_t delta);
static bool onWidgetValidate(WidgetT *w);
static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, char *buf, int32_t bufSize);
+static const char *popupMenuNameFor(const BasFormT *form, const MenuT *menu);
static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl);
-static bool resolveShellIdle(void);
-const char *resolveTypeName(const char *typeName);
+static void removeCtrlTree(BasFormRtT *rt, BasFormT *form, BasControlT *ctrl);
int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile);
int32_t ResAddText(const char *path, const char *name, const char *text);
void ResClose(int32_t handle);
int32_t ResCount(int32_t handle);
int32_t ResExtract(const char *path, const char *name, const char *outFile);
const char *ResGetText(const char *path, const char *name);
+static void resizeIfaceBuffer(BasControlT *ctrl);
const char *ResName(int32_t handle, int32_t index);
+static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId, bool *fromBar);
+static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc);
+static bool resolveShellIdle(void);
+const char *resolveTypeName(const char *typeName);
int32_t ResOpen(const char *path);
int32_t ResRemove(const char *path, const char *name);
int32_t ResSize(int32_t handle, int32_t index);
@@ -377,6 +539,8 @@ static int32_t serTermRead(void *ctx, uint8_t *buf, int32_t maxLen);
static int32_t serTermWrite(void *ctx, const uint8_t *data, int32_t len);
int32_t SerWrite(int32_t com, const char *data);
static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value);
+static void setControlString(char *dst, int32_t dstSize, BasValueT value);
+static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, BasValueT value);
static bool setIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value);
int32_t SQLAffected(int32_t db);
void SQLClose(int32_t db);
@@ -397,9 +561,56 @@ static void unloadFormNow(BasFormRtT *rt, BasFormT *form);
static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl);
bool wgtApplyPropFromString(WidgetT *w, const WgtPropDescT *p, const char *val);
bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize);
+static bool widgetHasAncestor(const WidgetT *w, const WidgetT *ancestor);
static void wireWidgetEvents(WidgetT *w, BasControlT *ctrl);
static BasValueT zeroValue(void);
+
+// Create the window and BasFormT for a new form and register it in
+// rt->forms. Shared by CreateForm and the bare-form path of LoadForm;
+// callers set any form-specific frm* fields afterwards.
+static BasFormT *allocForm(BasFormRtT *rt, const char *formName, bool resizable, bool centered, int32_t width, int32_t height) {
+ WidgetT *root;
+ WidgetT *contentBox;
+ WindowT *win = basFormRtCreateFormWindow(rt->ctx, formName, "VBox", resizable, centered, false, width, height, 0, 0, &root, &contentBox);
+
+ if (!win) {
+ return NULL;
+ }
+
+ BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT));
+
+ if (!form) {
+ dvxDestroyWindow(rt->ctx, win);
+ return NULL;
+ }
+
+ arrput(rt->forms, form);
+
+ snprintf(form->name, BAS_MAX_IDENT, "%s", formName);
+ snprintf(form->frmLayout, sizeof(form->frmLayout), "VBox");
+ win->onClose = onFormClose;
+ win->onResize = onFormResize;
+ win->onFocus = onFormActivate;
+ win->onBlur = onFormDeactivate;
+ form->window = win;
+ form->root = root;
+ form->contentBox = contentBox;
+ form->ctx = rt->ctx;
+ form->vm = rt->vm;
+ form->module = rt->module;
+
+ // Synthetic control for form-level property access. Its name field
+ // stays empty on purpose: a control name buffer cannot hold a full
+ // BAS_MAX_IDENT name, so form->name is the single source of
+ // truth and readers go through ctrlDisplayName().
+ form->formCtrl.widget = root;
+ form->formCtrl.form = form;
+
+ return form;
+}
+
+
int32_t basChoiceDialog(const char *title, const char *prompt, const char *items, int32_t defaultIdx) {
if (!sFormRt || !items) {
return -1;
@@ -452,7 +663,7 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha
int32_t tempStringCount = 0;
int32_t nativeCount = 0;
- for (int32_t i = 0; i < argc && i < BAS_VM_MAX_CALL_ARGS; i++) {
+ for (int32_t i = 0; i < argc; i++) {
switch (args[i].type) {
case BAS_TYPE_STRING: {
BasStringT *s = basValFormatString(args[i]);
@@ -494,11 +705,18 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha
uint32_t rawResult = basNativeCall(funcPtr, nativeArgs, nativeCount, fpReturn, &dblResult);
+ // The i386 ABI leaves the upper bits of a sub-32-bit return (bool,
+ // int16_t) undefined, so narrow by the declared return type instead
+ // of trusting all of EAX.
if (fpReturn) {
result = basValDouble(dblResult);
} else if (retType == BAS_TYPE_STRING) {
const char *str = (const char *)(uintptr_t)rawResult;
result = basValStringFromC(str ? str : "");
+ } else if (retType == BAS_TYPE_BOOLEAN) {
+ result = basValBool((uint8_t)rawResult != 0);
+ } else if (retType == BAS_TYPE_INTEGER) {
+ result = basValLong((int16_t)rawResult);
} else {
result = basValLong((int32_t)rawResult);
}
@@ -515,7 +733,7 @@ void *basExternResolve(void *ctx, const char *libName, const char *funcName) {
(void)ctx;
(void)libName;
- char mangledName[256];
+ char mangledName[BAS_MANGLED_NAME_LEN];
snprintf(mangledName, sizeof(mangledName), "_%s", funcName);
return dlsym(NULL, mangledName);
@@ -804,6 +1022,23 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName,
}
+int32_t basFormRtCommonMethodId(const char *name) {
+ for (int32_t i = 0; i < BAS_CM_COUNT; i++) {
+ if (strcasecmp(sCommonMethodNames[i], name) == 0) {
+ return i;
+ }
+ }
+
+ return BAS_CM_NONE;
+}
+
+
+const BasPropDescT *basFormRtCommonProps(int32_t *count) {
+ *count = CTRL_PROP_COUNT;
+ return sCtrlProps;
+}
+
+
BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module) {
BasFormRtT *rt = (BasFormRtT *)calloc(1, sizeof(BasFormRtT));
@@ -837,8 +1072,11 @@ WidgetT *basFormRtCreateContentBox(WidgetT *root, const char *layout) {
if (api) {
WidgetT *(*createFn)(WidgetT *) = *(WidgetT *(*const *)(WidgetT *))api;
WidgetT *box = createFn(root);
- box->weight = WGT_WEIGHT_FILL;
- return box;
+
+ if (box) {
+ box->weight = WGT_WEIGHT_FILL;
+ return box;
+ }
}
}
}
@@ -848,10 +1086,20 @@ WidgetT *basFormRtCreateContentBox(WidgetT *root, const char *layout) {
void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const char *ctrlName) {
- BasFormRtT *rt = (BasFormRtT *)ctx;
- BasFormT *form = (BasFormT *)formRef;
+ return basFormRtCreateCtrlEx(ctx, formRef, typeName, ctrlName, NULL);
+}
+
+
+void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef) {
+ BasFormRtT *rt = (BasFormRtT *)ctx;
+ BasFormT *form = (BasFormT *)formRef;
if (!form) {
+ basFormRtRuntimeError(rt,
+ "CreateControl: form reference is NULL",
+ "Type: %s\nControl name: %s",
+ typeName ? typeName : "(null)",
+ ctrlName ? ctrlName : "(null)");
return NULL;
}
@@ -868,8 +1116,17 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const
return NULL;
}
- // Create the widget
- WidgetT *parent = form->contentBox ? form->contentBox : form->root;
+ // Parent widget: a container control's widget when given, otherwise
+ // the form's content box.
+ WidgetT *parent = NULL;
+
+ if (parentRef) {
+ parent = ((BasControlT *)parentRef)->widget;
+ }
+
+ if (!parent) {
+ parent = form->contentBox ? form->contentBox : form->root;
+ }
if (!parent) {
basFormRtRuntimeError(rt,
@@ -896,22 +1153,21 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const
wgtSetName(widget, ctrlName);
- // Initialize control entry
BasControlT *ctrl = (BasControlT *)calloc(1, sizeof(BasControlT));
if (!ctrl) {
+ wgtDestroy(widget);
return NULL;
}
ctrl->index = -1;
- snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
- snprintf(ctrl->typeName, BAS_MAX_CTRL_NAME, "%s", typeName ? typeName : "");
+ snprintf(ctrl->name, BAS_MAX_IDENT, "%s", ctrlName);
+ snprintf(ctrl->typeName, BAS_MAX_IDENT, "%s", typeName ? typeName : "");
ctrl->widget = widget;
ctrl->form = form;
ctrl->iface = wgtGetIface(wgtTypeName);
arrput(form->controls, ctrl);
-
// Wire up event callbacks (key/mouse/scroll included so dynamically
// created controls match the .frm-load path; dispatch no-ops when
// BASIC has no matching handler).
@@ -921,77 +1177,6 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const
}
-void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef) {
- BasFormRtT *rt = (BasFormRtT *)ctx;
- BasFormT *form = (BasFormT *)formRef;
-
- if (!form) {
- basFormRtRuntimeError(rt,
- "CreateControl: form reference is NULL",
- "Type: %s\nControl name: %s",
- typeName ? typeName : "(null)",
- ctrlName ? ctrlName : "(null)");
- return NULL;
- }
-
- const char *wgtTypeName = resolveTypeName(typeName);
-
- if (!wgtTypeName) {
- basFormRtRuntimeError(rt,
- "CreateControl: unknown control type",
- "Requested type: %s\nForm: %s\nControl name: %s",
- typeName ? typeName : "(null)",
- form->name,
- ctrlName ? ctrlName : "(null)");
- return NULL;
- }
-
- // Determine parent widget: if parentRef is a control, use its widget;
- // otherwise fall back to the form's content box.
- WidgetT *parent = NULL;
-
- if (parentRef) {
- BasControlT *parentCtrl = (BasControlT *)parentRef;
- parent = parentCtrl->widget;
- }
-
- if (!parent) {
- parent = form->contentBox ? form->contentBox : form->root;
- }
-
- if (!parent) {
- return NULL;
- }
-
- WidgetT *widget = createWidget(wgtTypeName, parent);
-
- if (!widget) {
- return NULL;
- }
-
- wgtSetName(widget, ctrlName);
-
- BasControlT *ctrl = (BasControlT *)calloc(1, sizeof(BasControlT));
-
- if (!ctrl) {
- return NULL;
- }
-
- ctrl->index = -1;
- snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
- snprintf(ctrl->typeName, BAS_MAX_CTRL_NAME, "%s", typeName ? typeName : "");
- ctrl->widget = widget;
- ctrl->form = form;
- ctrl->iface = wgtGetIface(wgtTypeName);
- arrput(form->controls, ctrl);
-
-
- wireWidgetEvents(widget, ctrl);
-
- return ctrl;
-}
-
-
void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_t height) {
BasFormRtT *rt = (BasFormRtT *)ctx;
@@ -1007,48 +1192,23 @@ void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_
}
if (width <= 0) {
- width = DEFAULT_FORM_W;
+ width = BAS_DEFAULT_FORM_W;
}
if (height <= 0) {
- height = DEFAULT_FORM_H;
+ height = BAS_DEFAULT_FORM_H;
}
- WidgetT *root;
- WidgetT *contentBox;
- WindowT *win = basFormRtCreateFormWindow(rt->ctx, formName, "VBox", true, true, false, width, height, 0, 0, &root, &contentBox);
+ BasFormT *form = allocForm(rt, formName, true, true, width, height);
- if (!win) {
+ if (!form) {
return NULL;
}
- BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT));
- arrput(rt->forms, form);
-
- snprintf(form->name, BAS_MAX_FORM_NAME, "%s", formName);
- snprintf(form->frmLayout, sizeof(form->frmLayout), "VBox");
form->frmWidth = width;
form->frmHeight = height;
form->frmResizable = true;
form->frmCentered = true;
- win->onClose = onFormClose;
- win->onResize = onFormResize;
- win->onFocus = onFormActivate;
- win->onBlur = onFormDeactivate;
- form->window = win;
- form->root = root;
- form->contentBox = contentBox;
- form->ctx = rt->ctx;
- form->vm = rt->vm;
- form->module = rt->module;
-
- // Synthetic control for form-level property access. Its name field
- // stays empty on purpose: a control name buffer cannot hold a full
- // BAS_MAX_FORM_NAME name, so form->name is the single source of
- // truth and readers go through ctrlDisplayName().
- memset(&form->formCtrl, 0, sizeof(form->formCtrl));
- form->formCtrl.widget = root;
- form->formCtrl.form = form;
// Allocate per-form variable storage from module metadata
basFormRtInitFormVars(rt, form, true);
@@ -1058,8 +1218,8 @@ void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_
WindowT *basFormRtCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox) {
- int32_t defW = (width > 0) ? width : DEFAULT_FORM_W;
- int32_t defH = (height > 0) ? height : DEFAULT_FORM_H;
+ int32_t defW = (width > 0) ? width : BAS_DEFAULT_FORM_W;
+ int32_t defH = (height > 0) ? height : BAS_DEFAULT_FORM_H;
WindowT *win = dvxCreateWindowCentered(ctx, title, defW, defH, resizable);
@@ -1113,16 +1273,7 @@ void basFormRtDestroy(BasFormRtT *rt) {
basFormRtSerialShutdown();
for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
- BasFormT *form = rt->forms[i];
-
- basFormRtTeardownForm(rt, form);
-
- if (form->window) {
- dvxDestroyWindow(rt->ctx, form->window);
- form->window = NULL;
- }
-
- free(form);
+ destroyFormNow(rt, rt->forms[i]);
}
arrfree(rt->forms);
@@ -1131,9 +1282,8 @@ void basFormRtDestroy(BasFormRtT *rt) {
// detached from rt->forms, so the loop above missed them. This runs
// only on normal appMain exit and IDE stop -- a force-kill never
// reaches basFormRtDestroy; there the shell's per-app window scan
- // reclaims the windows before dlclose. Mirror the loop body above
- // instead of calling unloadFormNow, which would re-touch vm fields
- // on a dying runtime.
+ // reclaims the windows before dlclose. destroyFormNow (not
+ // unloadFormNow) so nothing re-touches vm fields on a dying runtime.
while (arrlen(rt->pendingCtrlFree) > 0) {
BasControlT *ctrl = arrpop(rt->pendingCtrlFree);
@@ -1143,14 +1293,7 @@ void basFormRtDestroy(BasFormRtT *rt) {
while (arrlen(rt->pendingUnload) > 0) {
BasFormT *form = arrpop(rt->pendingUnload);
- basFormRtTeardownForm(rt, form);
-
- if (form->window) {
- dvxDestroyWindow(rt->ctx, form->window);
- form->window = NULL;
- }
-
- free(form);
+ destroyFormNow(rt, form);
}
arrfree(rt->pendingCtrlFree);
@@ -1162,50 +1305,10 @@ void basFormRtDestroy(BasFormRtT *rt) {
}
arrfree(rt->frmCache);
-
- // Free compiled form cache
- for (int32_t i = 0; i < rt->cfmCacheCount; i++) {
- free(rt->cfmCache[i].data);
- }
-
- arrfree(rt->cfmCache);
free(rt);
}
-// Release all serial/secLink resources and unregister both idle pollers from
-// the shell. Safe to call repeatedly. The pollers live in this shared
-// runtime DXE but iterate per-app slots/terminals, so they MUST be dropped
-// when a BASIC app goes away -- on normal exit via basFormRtDestroy, and on
-// force-kill (where basFormRtDestroy never runs) via the stub's _appShutdown.
-// NOTE: closes every open connection/port, which is correct for the single
-// foreground BASIC app; concurrent BASIC instances sharing these module-scope
-// slots is a separate, pre-existing limitation.
-void basFormRtSerialShutdown(void) {
- for (int32_t i = 0; i < (int32_t)arrlen(sCommSlots); i++) {
- if (sCommSlots[i]) {
- CommClose(i + 1);
- }
- }
-
- arrfree(sCommSlots);
- sCommSlots = NULL;
-
- if (sSerApiResolved && sSerApi.close) {
- for (int32_t i = 0; i < RS232_NUM_PORTS; i++) {
- if (sSerAttach[i].attached) {
- SerClose(i + 1);
- }
- }
- }
-
- if (sShellUnregisterIdle) {
- sShellUnregisterIdle(commIdlePoll, NULL);
- sShellUnregisterIdle(serIdlePoll, NULL);
- }
-}
-
-
void basFormRtEventLoop(BasFormRtT *rt) {
if (!rt || !rt->ctx || !rt->vm) {
return;
@@ -1219,6 +1322,11 @@ void basFormRtEventLoop(BasFormRtT *rt) {
}
+const BasPropDescT *basFormRtFindCommonProp(const char *name) {
+ return findPropDesc(sCtrlProps, CTRL_PROP_COUNT, name);
+}
+
+
void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
BasFormRtT *rt = (BasFormRtT *)ctx;
BasFormT *form = (BasFormT *)formRef;
@@ -1233,77 +1341,28 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
return NULL;
}
- // Check if the name refers to the current form itself
- if (strcasecmp(form->name, ctrlName) == 0) {
- return &form->formCtrl;
+ // The current form first (a local miss is expected for cross-form
+ // access; don't spam the log), then every other loaded form: dynamic
+ // forms created by CreateForm/CreateControl hold controls that the
+ // calling SUB references by name (e.g. a mnuXxx_Click on the main
+ // form sets properties on a control just created on a new form).
+ BasControlT *hit = findNamedObjectInForm(form, ctrlName);
+
+ if (hit || !rt) {
+ return hit;
}
- // Search controls on the current form
- BasControlT *localHit = findCtrlInForm(form, ctrlName);
+ for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
+ BasFormT *other = rt->forms[i];
- if (localHit) {
- return localHit;
- }
-
- // (local miss is expected for cross-form access; don't spam the log)
-
- // Search menu items on the current form
- for (int32_t i = 0; i < form->menuIdMapCount; i++) {
- if (strcasecmp(form->menuIdMap[i].name, ctrlName) == 0) {
- // Create proxy on first access
- if (!form->menuIdMap[i].proxy) {
- BasControlT *proxy = (BasControlT *)calloc(1, sizeof(BasControlT));
-
- if (proxy) {
- snprintf(proxy->name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
- proxy->form = form;
- proxy->menuId = form->menuIdMap[i].id;
- form->menuIdMap[i].proxy = proxy;
- }
- }
- return form->menuIdMap[i].proxy;
+ if (other == form) {
+ continue;
}
- }
- // Search across all loaded forms (for cross-form property access).
- // Dynamic forms created by CreateForm/CreateControl hold controls
- // that the calling SUB references by name (e.g. a mnuXxx_Click on
- // the main form sets properties on a control just created on a new
- // form). Check form names first, then each form's controls, then
- // each form's menu items.
- if (rt) {
- for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
- BasFormT *other = rt->forms[i];
+ hit = findNamedObjectInForm(other, ctrlName);
- if (other == form) {
- continue;
- }
-
- if (strcasecmp(other->name, ctrlName) == 0) {
- return &other->formCtrl;
- }
-
- BasControlT *otherHit = findCtrlInForm(other, ctrlName);
-
- if (otherHit) {
- return otherHit;
- }
-
- for (int32_t j = 0; j < other->menuIdMapCount; j++) {
- if (strcasecmp(other->menuIdMap[j].name, ctrlName) == 0) {
- if (!other->menuIdMap[j].proxy) {
- BasControlT *proxy = (BasControlT *)calloc(1, sizeof(BasControlT));
-
- if (proxy) {
- snprintf(proxy->name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
- proxy->form = other;
- proxy->menuId = other->menuIdMap[j].id;
- other->menuIdMap[j].proxy = proxy;
- }
- }
- return other->menuIdMap[j].proxy;
- }
- }
+ if (hit) {
+ return hit;
}
}
@@ -1334,84 +1393,41 @@ void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32
}
+const BasPropDescT *basFormRtFindFormProp(const char *name) {
+ return findPropDesc(sFormProps, FORM_PROP_COUNT, name);
+}
+
+
bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName) {
return basFormRtFireEventArgs(rt, form, ctrlName, eventName, NULL, 0);
}
bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount) {
- if (!rt || !form || !rt->vm || !rt->module) {
- return false;
- }
-
- char handlerName[MAX_EVENT_NAME_LEN];
- snprintf(handlerName, sizeof(handlerName), "%s_%s", ctrlName, eventName);
-
- const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName);
-
- if (!proc) {
- return false;
- }
-
- if (proc->isFunction) {
- return false;
- }
-
- // Strict parameter matching: the sub must declare exactly the
- // number of parameters the event provides, or zero (no params).
- if (proc->paramCount != 0 && proc->paramCount != argCount) {
- return false;
- }
-
- return rtCallHandler(rt, form, proc, proc->codeAddr, args, argCount, NULL, 0);
+ return fireEventArgsOut(rt, form, ctrlName, eventName, args, argCount, NULL, 0);
}
-// ============================================================
-// basFormRtFireEventWithCancel -- fire an event that has a Cancel
-// parameter (first arg, Integer). Returns true if Cancel was set
-// to non-zero by the event handler.
-
+// Fire an event that has a Cancel parameter (first arg, Integer).
+// Returns true if the handler set Cancel to non-zero.
static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName) {
- if (!rt || !form || !rt->vm || !rt->module) {
- return false;
- }
+ BasValueT cancelArg = basValLong(0);
+ BasValueT outCancel = basValLong(0);
- char handlerName[MAX_EVENT_NAME_LEN];
- snprintf(handlerName, sizeof(handlerName), "%s_%s", ctrlName, eventName);
-
- const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName);
-
- if (!proc || proc->isFunction) {
- return false;
- }
-
- // Must accept 0 or 1 parameter
- if (proc->paramCount != 0 && proc->paramCount != 1) {
- return false;
- }
-
- bool cancelled = false;
-
- if (proc->paramCount == 1) {
- BasValueT args[1];
- args[0] = basValLong(0); // Cancel = 0 (don't cancel)
-
- BasValueT outArgs[1];
- memset(outArgs, 0, sizeof(outArgs));
-
- if (rtCallHandler(rt, form, proc, proc->codeAddr, args, 1, outArgs, 1)) {
- cancelled = (basValToNumber(outArgs[0]) != 0);
- basValRelease(&outArgs[0]);
- }
- } else {
- rtCallHandler(rt, form, proc, proc->codeAddr, NULL, 0, NULL, 0);
- }
+ fireEventArgsOut(rt, form, ctrlName, eventName, &cancelArg, 1, &outCancel, 1);
+ bool cancelled = basValIsTruthy(outCancel);
+ basValRelease(&outCancel);
return cancelled;
}
+const BasPropDescT *basFormRtFormProps(int32_t *count) {
+ *count = FORM_PROP_COUNT;
+ return sFormProps;
+}
+
+
BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
BasFormRtT *rt = (BasFormRtT *)ctx;
BasControlT *ctrl = (BasControlT *)ctrlRef;
@@ -1462,78 +1478,26 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
// Form-level properties use the window and BasFormT, not the root widget
if (ctrl->form && ctrl == &ctrl->form->formCtrl) {
- WindowT *win = ctrl->form->window;
- BasFormT *frm = ctrl->form;
-
- if (strcasecmp(propName, "Name") == 0) { return basValStringFromC(frm->name); }
- if (strcasecmp(propName, "Caption") == 0) { return basValStringFromC(win ? win->title : ""); }
- if (strcasecmp(propName, "Width") == 0) { return basValLong(win ? win->w : 0); }
- if (strcasecmp(propName, "Height") == 0) { return basValLong(win ? win->h : 0); }
- if (strcasecmp(propName, "Left") == 0) { return basValLong(win ? win->x : 0); }
- if (strcasecmp(propName, "Top") == 0) { return basValLong(win ? win->y : 0); }
- if (strcasecmp(propName, "Visible") == 0) { return basValBool(win && win->visible); }
- if (strcasecmp(propName, "Resizable") == 0) { return basValBool(win && win->resizable); }
- if (strcasecmp(propName, "AutoSize") == 0) { return basValBool(frm->frmAutoSize); }
- if (strcasecmp(propName, "Centered") == 0) { return basValBool(frm->frmCentered); }
- if (strcasecmp(propName, "Layout") == 0) { return basValStringFromC(frm->frmLayout); }
-
- basFormRtRuntimeError(rt,
- "Unknown form property",
- "Form: %s\nProperty: %s\nValid: Name, Caption, Width, Height, Left, Top, Visible, Resizable, AutoSize, Centered, Layout.",
- frm->name, propName ? propName : "?");
- return zeroValue();
+ return getFormProp(rt, ctrl->form, propName);
}
- // Common properties (Name, Left, Top, Width, Height, Visible, Enabled)
- bool handled;
- BasValueT val = getCommonProp(ctrl, propName, &handled);
+ // Widget-declared properties first, so a widget's own Enabled /
+ // Caption / TabIndex overrides the generic common-property handling.
+ bool handled = false;
+ BasValueT val = zeroValue();
+
+ if (ctrl->iface) {
+ val = getIfaceProp(ctrl->iface, ctrl->widget, propName, &handled);
+ }
+
+ if (!handled) {
+ val = getCommonProp(ctrl, propName, &handled);
+ }
if (handled) {
return val;
}
- // "Caption" and "Text" map to wgtGetText for all widgets
- if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) {
- const char *text = wgtGetText(ctrl->widget);
- return basValStringFromC(text ? text : "");
- }
-
- // Help topic
- if (strcasecmp(propName, "HelpTopic") == 0) {
- return basValStringFromC(ctrl->helpTopic);
- }
-
- // Data binding properties
- if (strcasecmp(propName, "DataSource") == 0) {
- return basValStringFromC(ctrl->dataSource);
- }
-
- if (strcasecmp(propName, "DataField") == 0) {
- return basValStringFromC(ctrl->dataField);
- }
-
- // "ListCount" for any widget with item storage
- if (strcasecmp(propName, "ListCount") == 0) {
- if (ctrl->iface) {
- for (int32_t m = 0; m < ctrl->iface->methodCount; m++) {
- if (strcasecmp(ctrl->iface->methods[m].name, "ListCount") == 0) {
- return basValLong(((int32_t (*)(const WidgetT *))ctrl->iface->methods[m].fn)(ctrl->widget));
- }
- }
- }
-
- return basValLong(0);
- }
-
- // Interface descriptor properties
- if (ctrl->iface) {
- val = getIfaceProp(ctrl->iface, ctrl->widget, propName, &handled);
-
- if (handled) {
- return val;
- }
- }
-
basFormRtRuntimeError(rt,
"Property not found on control",
"Control: %s\nType: %s\nProperty: %s\nThe control has no readable property by that name.",
@@ -1608,7 +1572,7 @@ static void basFormRtInitFormVars(BasFormRtT *rt, BasFormT *form, bool runInit)
static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText) {
BasFormRtT *rt = (BasFormRtT *)ctx;
- char buf[256];
+ char buf[BAS_INPUTBOX_BUF_LEN];
buf[0] = '\0';
@@ -1630,11 +1594,6 @@ void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName) {
basFormRtLoadForm(rt, rt->frmCache[i].formName);
}
- // Load compiled form cache entries (skip if already loaded)
- for (int32_t i = 0; i < rt->cfmCacheCount; i++) {
- basFormRtLoadForm(rt, rt->cfmCache[i].formName);
- }
-
// Show the startup form (named or first)
int32_t formCount = (int32_t)arrlen(rt->forms);
@@ -1655,12 +1614,6 @@ void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName) {
}
-static void *basFormRtLoadCfm(BasFormRtT *rt, const uint8_t *data, int32_t dataLen) {
- (void)rt; (void)data; (void)dataLen;
- return NULL;
-}
-
-
void *basFormRtLoadForm(void *ctx, const char *formName) {
BasFormRtT *rt = (BasFormRtT *)ctx;
@@ -1671,12 +1624,14 @@ void *basFormRtLoadForm(void *ctx, const char *formName) {
}
}
- // Check the .frm cache for reload after unload.
- // sLoadingFrm (module-scope) prevents recursion: basFormRtLoadFrm
- // calls basFormRtLoadForm via frmLoad_onFormBegin, which would
- // re-enter this function. The guard lets the recursive call fall
- // through to bare form creation, which basFormRtLoadFrm then
- // populates.
+ // Check the .frm cache for reload after unload. sLoadingFrm is set
+ // only while basFormRtLoadFrm's frmParse is running: the parser
+ // re-enters this function via frmLoadOnFormBegin, and that nested
+ // call must fall through to bare-form creation (which the parse
+ // then populates) instead of recursing into another basFormRtLoadFrm.
+ // The flag is NOT set here -- basFormRtLoadFrm clears it before its
+ // Resize/Load tail, so a 'Load Form2' issued from Form1_Load still
+ // finds Form2's cached .frm.
if (!sLoadingFrm) {
for (int32_t i = 0; i < rt->frmCacheCount; i++) {
if (strcasecmp(rt->frmCache[i].formName, formName) == 0) {
@@ -1686,54 +1641,18 @@ void *basFormRtLoadForm(void *ctx, const char *formName) {
// malloc-stable even if nested loads realloc the cache
// array, and basFormRtLoadFrm's re-cache block sees the
// entry already present and skips it.
- sLoadingFrm = true;
- BasFormT *form = basFormRtLoadFrm(rt, rt->frmCache[i].frmSource, rt->frmCache[i].frmSourceLen);
- sLoadingFrm = false;
- return form;
+ return basFormRtLoadFrm(rt, rt->frmCache[i].frmSource, rt->frmCache[i].frmSourceLen);
}
}
}
- // Check the compiled form cache (standalone apps)
- for (int32_t i = 0; i < rt->cfmCacheCount; i++) {
- if (strcasecmp(rt->cfmCache[i].formName, formName) == 0) {
- return basFormRtLoadCfm(rt, rt->cfmCache[i].data, rt->cfmCache[i].dataLen);
- }
- }
+ // No cache entry: create a bare form (first-time load without .frm file)
+ BasFormT *form = allocForm(rt, formName, false, true, BAS_DEFAULT_FORM_W, BAS_DEFAULT_FORM_H);
- // No cache entry — create a bare form (first-time load without .frm file)
- WidgetT *root;
- WidgetT *bareContentBox;
- WindowT *win = basFormRtCreateFormWindow(rt->ctx, formName, "VBox", false, true, false, DEFAULT_FORM_W, DEFAULT_FORM_H, 0, 0, &root, &bareContentBox);
-
- if (!win) {
+ if (!form) {
return NULL;
}
- BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT));
- arrput(rt->forms, form);
-
- snprintf(form->name, BAS_MAX_FORM_NAME, "%s", formName);
- snprintf(form->frmLayout, sizeof(form->frmLayout), "VBox");
- win->onClose = onFormClose;
- win->onResize = onFormResize;
- win->onFocus = onFormActivate;
- win->onBlur = onFormDeactivate;
- form->window = win;
- form->root = root;
- form->contentBox = bareContentBox;
- form->ctx = rt->ctx;
- form->vm = rt->vm;
- form->module = rt->module;
-
- // Initialize synthetic control for form-level property access. Its
- // name field stays empty on purpose: a control name buffer cannot
- // hold a full BAS_MAX_FORM_NAME name, so form->name is the single
- // source of truth and readers go through ctrlDisplayName().
- memset(&form->formCtrl, 0, sizeof(form->formCtrl));
- form->formCtrl.widget = root;
- form->formCtrl.form = form;
-
// Allocate per-form variable storage. Init code runs later:
// - If we were reached recursively from basFormRtLoadFrm (sLoadingFrm
// is true), it runs AFTER parsing populates form->controls, so
@@ -1760,29 +1679,31 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
FrmParserCbsT cbs;
memset(&cbs, 0, sizeof(cbs));
cbs.userData = &ctx;
- cbs.onFormBegin = frmLoad_onFormBegin;
- cbs.onFormProp = frmLoad_onFormProp;
- cbs.onMenuBegin = frmLoad_onMenuBegin;
- cbs.onMenuEnd = frmLoad_onMenuEnd;
- cbs.onMenuProp = frmLoad_onMenuProp;
- cbs.onCtrlBegin = frmLoad_onCtrlBegin;
- cbs.onCtrlEnd = frmLoad_onCtrlEnd;
- cbs.onCtrlProp = frmLoad_onCtrlProp;
+ cbs.onFormBegin = frmLoadOnFormBegin;
+ cbs.onFormProp = frmLoadOnFormProp;
+ cbs.onMenuBegin = frmLoadOnMenuBegin;
+ cbs.onMenuEnd = frmLoadOnMenuEnd;
+ cbs.onMenuProp = frmLoadOnMenuProp;
+ cbs.onCtrlBegin = frmLoadOnCtrlBegin;
+ cbs.onCtrlEnd = frmLoadOnCtrlEnd;
+ cbs.onCtrlProp = frmLoadOnCtrlProp;
- // Set the guard so nested basFormRtLoadForm (fired from
- // frmLoad_onFormBegin) skips the form's init code -- running it
- // here would fire against an empty control list. The outer
- // caller (IDE runModule or basFormRtLoadAllForms) runs module-
- // level / init code after parsing finishes. Save+restore so
- // nested basFormRtLoadForm -> basFormRtLoadFrm reentry still
- // terminates the inner guard correctly.
+ // Set the guard so the nested basFormRtLoadForm (fired from
+ // frmLoadOnFormBegin) creates a bare form and skips its init code --
+ // running it here would fire against an empty control list; init
+ // runs below once the controls exist. Save+restore so a nested
+ // basFormRtLoadForm -> basFormRtLoadFrm reentry still terminates
+ // the inner guard correctly.
bool savedLoadingFrm = sLoadingFrm;
int32_t formsBefore = (int32_t)arrlen(rt->forms);
sLoadingFrm = true;
bool parsed = frmParse(source, sourceLen, &cbs);
sLoadingFrm = savedLoadingFrm;
- if (!parsed) {
+ // A callback that raised a runtime error (unknown widget type) has
+ // already halted the VM; treat the load as failed rather than firing
+ // Resize/Load on a half-built form and forcing the VM back to running.
+ if (!parsed || rt->terminated) {
// Discard a half-built form created by THIS parse (nesting
// overflow etc.), or it would stay findable in rt->forms with
// missing controls. Created-by-us means it sits past the
@@ -1853,7 +1774,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
bar = wmAddMenuBar(form->window);
}
- MenuT *menuStack[16];
+ MenuT *menuStack[FRM_MAX_NESTING];
memset(menuStack, 0, sizeof(menuStack));
bool topIsPopup = false;
MenuT *curTopPopup = NULL;
@@ -1878,7 +1799,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
if (curTopPopup) {
BasFrmPopupMenuT entry;
memset(&entry, 0, sizeof(entry));
- snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", mi->name);
+ snprintf(entry.name, BAS_MAX_IDENT, "%s", mi->name);
entry.menu = curTopPopup;
entry.ownsMenu = true; // resource-built popup root
arrput(form->popupMenus, entry);
@@ -1911,7 +1832,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
BasMenuIdMapT map;
memset(&map, 0, sizeof(map));
map.id = id;
- snprintf(map.name, BAS_MAX_CTRL_NAME, "%s", mi->name);
+ snprintf(map.name, BAS_MAX_IDENT, "%s", mi->name);
arrput(form->menuIdMap, map);
form->menuIdMapCount = (int32_t)arrlen(form->menuIdMap);
}
@@ -1928,24 +1849,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
// known until properties are parsed, so this must happen after
// loading but before dvxFitWindow.
for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
- if (!form->controls[i]->widget) {
- continue;
- }
-
- const WgtIfaceT *ifc = form->controls[i]->iface;
-
- if (ifc) {
- WidgetT *wgt = form->controls[i]->widget;
-
- for (int32_t m = 0; m < ifc->methodCount; m++) {
- if (strcasecmp(ifc->methods[m].name, "Resize") == 0 &&
- ifc->methods[m].sig == WGT_SIG_INT_INT &&
- wgt->minW > 0 && wgt->minH > 0) {
- ((void (*)(WidgetT *, int32_t, int32_t))ifc->methods[m].fn)(wgt, wgt->minW, wgt->minH);
- break;
- }
- }
- }
+ resizeIfaceBuffer(form->controls[i]);
}
// Apply form properties after Resize calls so that
@@ -1983,7 +1887,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
if (!cached) {
BasFrmCacheT entry;
memset(&entry, 0, sizeof(entry));
- snprintf(entry.formName, BAS_MAX_FORM_NAME, "%s", form->name);
+ snprintf(entry.formName, BAS_MAX_IDENT, "%s", form->name);
entry.frmSource = (char *)malloc(sourceLen + 1);
entry.frmSourceLen = sourceLen;
@@ -2008,7 +1912,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
// Fire the Load event now that the form and controls are ready
if (form && !form->unloading) {
- basFormRtFireEvent(rt, form, form->name, "Load");
+ basFormRtFireEvent(rt, form, form->name, BAS_EVT_LOAD);
}
// Auto-refresh Data controls that have no MasterSource (masters/standalone).
@@ -2023,17 +1927,8 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
continue;
}
- // Skip details — they'll be refreshed by the cascade
- const char *ms = NULL;
-
- if (dc->iface) {
- for (int32_t p = 0; p < dc->iface->propCount; p++) {
- if (strcasecmp(dc->iface->props[p].name, "MasterSource") == 0 && dc->iface->props[p].getFn) {
- ms = ((const char *(*)(const WidgetT *))dc->iface->props[p].getFn)(dc->widget);
- break;
- }
- }
- }
+ // Skip details -- they'll be refreshed by the cascade
+ const char *ms = ifaceGetStringProp(dc, "MasterSource");
if (ms && ms[0]) {
continue;
@@ -2064,6 +1959,84 @@ int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const cha
}
+void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen) {
+ if (!rt || !formName || !source || sourceLen <= 0) {
+ return;
+ }
+
+ BasFrmCacheT entry;
+ snprintf(entry.formName, BAS_MAX_IDENT, "%s", formName);
+ entry.frmSource = (char *)malloc(sourceLen + 1);
+
+ if (!entry.frmSource) {
+ return;
+ }
+
+ entry.frmSourceLen = sourceLen;
+ memcpy(entry.frmSource, source, sourceLen);
+ entry.frmSource[sourceLen] = '\0';
+ arrput(rt->frmCache, entry);
+ rt->frmCacheCount = (int32_t)arrlen(rt->frmCache);
+}
+
+
+void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName) {
+ BasFormRtT *rt = (BasFormRtT *)ctx;
+ BasFormT *form = (BasFormT *)formRef;
+
+ if (!rt || !form || !ctrlName) {
+ return;
+ }
+
+ BasControlT *ctrl = findCtrlInForm(form, ctrlName);
+
+ if (ctrl) {
+ removeCtrlTree(rt, form, ctrl);
+ }
+}
+
+
+void basFormRtRunSimple(BasFormRtT *rt) {
+ if (!rt || !rt->vm) {
+ return;
+ }
+
+ BasVmT *vm = rt->vm;
+ basVmSetStepLimit(vm, BAS_VM_DEFAULT_STEP_SLICE);
+
+ BasVmResultE result;
+
+ do {
+ result = basVmRun(vm);
+
+ if (result == BAS_VM_STEP_LIMIT) {
+ if (!dvxUpdate(rt->ctx)) {
+ break;
+ }
+ } else if (result == BAS_VM_ERROR) {
+ // basFormRtRuntimeError already surfaces its own error
+ // dialog and sets rt->terminated, so only show this generic
+ // box for VM-internal errors (division by zero, type
+ // mismatch, etc.) that didn't come through that path.
+ if (!rt->terminated) {
+ const char *errMsg = basVmGetError(vm);
+ char buf[BAS_ERR_BOX_LEN];
+ snprintf(buf, sizeof(buf), "Runtime error:\n%s", errMsg ? errMsg : "Unknown error");
+ dvxMessageBox(rt->ctx, "Error", buf, 0);
+ }
+ break;
+ } else {
+ break;
+ }
+ } while (1);
+
+ // VB-style event loop: keep alive while forms are open
+ if (result == BAS_VM_HALTED) {
+ basFormRtEventLoop(rt);
+ }
+}
+
+
// Report a non-recoverable runtime error. Logs to DVX.LOG with the
// summary on one line and the details indented beneath, then shows a
// modal MessageBox with the details, then halts the VM so execution
@@ -2083,7 +2056,7 @@ void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *deta
return;
}
- char details[512];
+ char details[BAS_ERR_DETAIL_LEN];
va_list ap;
va_start(ap, detailFmt);
@@ -2104,7 +2077,7 @@ void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *deta
while (*p) {
const char *nl = strchr(p, '\n');
int32_t len = nl ? (int32_t)(nl - p) : (int32_t)strlen(p);
- char line[256];
+ char line[BAS_ERR_DETAIL_LEN];
if (len >= (int32_t)sizeof(line)) {
len = sizeof(line) - 1;
@@ -2172,7 +2145,7 @@ void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *deta
}
if (rt && rt->ctx && !rt->suppressErrorDialog) {
- char boxMsg[640];
+ char boxMsg[BAS_ERR_BOX_LEN];
snprintf(boxMsg, sizeof(boxMsg), "%s\n\n%s",
summary ? summary : "Runtime error",
details);
@@ -2181,140 +2154,35 @@ void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *deta
}
-void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *data, int32_t dataLen) {
- if (!rt || !formName || !data || dataLen <= 0) {
- return;
- }
-
- BasCfmCacheT entry;
- snprintf(entry.formName, BAS_MAX_FORM_NAME, "%s", formName);
- entry.data = (uint8_t *)malloc(dataLen);
-
- if (!entry.data) {
- return;
- }
-
- entry.dataLen = dataLen;
- memcpy(entry.data, data, dataLen);
- arrput(rt->cfmCache, entry);
- rt->cfmCacheCount = (int32_t)arrlen(rt->cfmCache);
-}
-
-
-void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen) {
- if (!rt || !formName || !source || sourceLen <= 0) {
- return;
- }
-
- BasFrmCacheT entry;
- snprintf(entry.formName, BAS_MAX_FORM_NAME, "%s", formName);
- entry.frmSource = (char *)malloc(sourceLen + 1);
-
- if (!entry.frmSource) {
- return;
- }
-
- entry.frmSourceLen = sourceLen;
- memcpy(entry.frmSource, source, sourceLen);
- entry.frmSource[sourceLen] = '\0';
- arrput(rt->frmCache, entry);
- rt->frmCacheCount = (int32_t)arrlen(rt->frmCache);
-}
-
-
-void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName) {
- BasFormRtT *rt = (BasFormRtT *)ctx;
- BasFormT *form = (BasFormT *)formRef;
-
- if (!rt || !form || !ctrlName) {
- return;
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
- if (strcasecmp(form->controls[i]->name, ctrlName) == 0) {
- BasControlT *ctrl = form->controls[i];
-
- // Detach NOW: destroy the widget immediately (wgtDestroy bumps
- // sWidgetGen and unregisters timers/poll entries, and all
- // widget dispatch re-checks the gen) and pull the control out
- // of the form so name lookups miss it and a later form
- // teardown cannot double-free it.
- if (ctrl->widget) {
- // Serial/secLink terminal bindings and the shared tooltip
- // pointer may reference this widget; detach them first or
- // the idle pollers / compositor would dereference the
- // freed widget.
- if (formRtDetachTermsForWidget(ctrl->widget)) {
- formRtSerIdleSync();
- }
-
- if (rt->ctx->tooltipText) {
- dirtyListAdd(&rt->ctx->dirty, rt->ctx->tooltipX, rt->ctx->tooltipY, rt->ctx->tooltipW, rt->ctx->tooltipH);
- rt->ctx->tooltipText = NULL;
- }
-
- wgtDestroy(ctrl->widget);
- ctrl->widget = NULL;
- }
-
- arrdel(form->controls, i);
-
- // Free LATER when a handler is on the stack: fireCtrlEvent
- // writes ctrl->eventFiring after the handler returns, and the
- // handler that called RemoveControl may be the control's own.
- // freeControl also releases ctrl->tooltip; a bare free(ctrl)
- // leaked it.
- if (rt->eventDepth > 0) {
- arrput(rt->pendingCtrlFree, ctrl);
- } else {
- freeControl(ctrl);
- }
-
- return;
+// Release all serial/secLink resources and unregister both idle pollers from
+// the shell. Safe to call repeatedly. The pollers live in this shared
+// runtime DXE but iterate per-app slots/terminals, so they MUST be dropped
+// when a BASIC app goes away -- on normal exit via basFormRtDestroy, and on
+// force-kill (where basFormRtDestroy never runs) via the stub's _appShutdown.
+// NOTE: closes every open connection/port, which is correct for the single
+// foreground BASIC app; concurrent BASIC instances sharing these module-scope
+// slots is a separate, pre-existing limitation.
+void basFormRtSerialShutdown(void) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sCommSlots); i++) {
+ if (sCommSlots[i]) {
+ CommClose(i + 1);
}
}
-}
+ arrfree(sCommSlots);
+ sCommSlots = NULL;
-void basFormRtRunSimple(BasFormRtT *rt) {
- if (!rt || !rt->vm) {
- return;
+ if (sSerApiResolved && sSerApi.close) {
+ for (int32_t i = 0; i < RS232_NUM_PORTS; i++) {
+ if (sSerAttach[i].attached) {
+ SerClose(i + 1);
+ }
+ }
}
- BasVmT *vm = rt->vm;
- basVmSetStepLimit(vm, BAS_VM_DEFAULT_STEP_SLICE);
-
- BasVmResultE result;
-
- do {
- result = basVmRun(vm);
-
- if (result == BAS_VM_STEP_LIMIT) {
- if (!dvxUpdate(rt->ctx)) {
- break;
- }
- } else if (result == BAS_VM_YIELDED) {
- // DoEvents returned, continue
- } else if (result == BAS_VM_ERROR) {
- // basFormRtRuntimeError already surfaces its own error
- // dialog and sets rt->terminated, so only show this generic
- // box for VM-internal errors (division by zero, type
- // mismatch, etc.) that didn't come through that path.
- if (!rt->terminated) {
- const char *errMsg = basVmGetError(vm);
- char buf[512];
- snprintf(buf, sizeof(buf), "Runtime error:\n%s", errMsg ? errMsg : "Unknown error");
- dvxMessageBox(rt->ctx, "Error", buf, 0);
- }
- break;
- } else {
- break;
- }
- } while (1);
-
- // VB-style event loop: keep alive while forms are open
- if (result == BAS_VM_HALTED) {
- basFormRtEventLoop(rt);
+ if (sShellUnregisterIdle) {
+ sShellUnregisterIdle(commIdlePoll, NULL);
+ sShellUnregisterIdle(serIdlePoll, NULL);
}
}
@@ -2330,7 +2198,7 @@ void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const ch
// Check for existing override on this event and update it
for (int32_t i = 0; i < ctrl->eventOverrideCount; i++) {
if (strcasecmp(ctrl->eventOverrides[i].eventName, eventName) == 0) {
- snprintf(ctrl->eventOverrides[i].handlerName, BAS_MAX_CTRL_NAME, "%s", handlerName);
+ snprintf(ctrl->eventOverrides[i].handlerName, BAS_MAX_IDENT, "%s", handlerName);
return;
}
}
@@ -2338,8 +2206,8 @@ void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const ch
// Add new override
if (ctrl->eventOverrideCount < BAS_MAX_EVENT_OVERRIDES) {
BasEventOverrideT *ov = &ctrl->eventOverrides[ctrl->eventOverrideCount++];
- snprintf(ov->eventName, BAS_MAX_CTRL_NAME, "%s", eventName);
- snprintf(ov->handlerName, BAS_MAX_CTRL_NAME, "%s", handlerName);
+ snprintf(ov->eventName, BAS_MAX_IDENT, "%s", eventName);
+ snprintf(ov->handlerName, BAS_MAX_IDENT, "%s", handlerName);
}
}
@@ -2402,148 +2270,20 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT
// Form-level property assignment uses the window and BasFormT
if (ctrl->form && ctrl == &ctrl->form->formCtrl) {
- WindowT *win = ctrl->form->window;
- BasFormT *frm = ctrl->form;
-
- if (strcasecmp(propName, "Caption") == 0) {
- BasStringT *s = basValFormatString(value);
- if (win) { dvxSetTitle(rt->ctx, win, s->data); }
- basStringUnref(s);
- return;
- }
-
- if (strcasecmp(propName, "Visible") == 0) {
- if (win) {
- if (basValIsTruthy(value)) {
- dvxShowWindow(rt->ctx, win);
- } else {
- dvxHideWindow(rt->ctx, win);
- }
- }
- return;
- }
-
- if (strcasecmp(propName, "Width") == 0 && win) {
- dvxResizeWindow(rt->ctx, win, (int32_t)basValToNumber(value), win->h);
- return;
- }
-
- if (strcasecmp(propName, "Height") == 0 && win) {
- dvxResizeWindow(rt->ctx, win, win->w, (int32_t)basValToNumber(value));
- return;
- }
-
- if (strcasecmp(propName, "Left") == 0 && win) {
- dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
- win->x = (int32_t)basValToNumber(value);
- dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
- return;
- }
-
- if (strcasecmp(propName, "Top") == 0 && win) {
- dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
- win->y = (int32_t)basValToNumber(value);
- dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
- return;
- }
-
- if (strcasecmp(propName, "Resizable") == 0 && win) {
- dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
- win->resizable = basValIsTruthy(value);
- dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
- return;
- }
-
- if (strcasecmp(propName, "AutoSize") == 0) {
- frm->frmAutoSize = basValIsTruthy(value);
- if (frm->frmAutoSize && win) {
- dvxFitWindow(rt->ctx, win);
- }
- return;
- }
-
- if (strcasecmp(propName, "Centered") == 0 && win) {
- frm->frmCentered = basValIsTruthy(value);
- if (frm->frmCentered) {
- dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
- win->x = (rt->ctx->display.width - win->w) / 2;
- win->y = (rt->ctx->display.height - win->h) / 2;
- dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
- }
- return;
- }
-
- if (strcasecmp(propName, "ContextMenu") == 0) {
- BasStringT *s = basValFormatString(value);
- MenuT *m = NULL;
-
- if (s->len > 0) {
- BasFrmPopupMenuT *pm = findPopupMenu(frm, s->data);
-
- if (pm) {
- m = pm->menu;
- }
- }
-
- if (win) {
- win->contextMenu = m;
- }
-
- basStringUnref(s);
- return;
- }
-
- basFormRtRuntimeError(rt,
- "Unknown form property",
- "Form: %s\nProperty: %s\nValid: Caption, Visible, Width, Height, Left, Top, Resizable, AutoSize, Centered, ContextMenu.",
- frm->name, propName ? propName : "?");
+ setFormProp(rt, ctrl->form, propName, value);
+ return;
+ }
+
+ // Widget-declared properties first, then the common set (see
+ // basFormRtGetProp for the override rationale).
+ if (ctrl->iface && setIfaceProp(ctrl->iface, ctrl->widget, propName, value)) {
return;
}
- // Common properties
if (setCommonProp(ctrl, propName, value)) {
return;
}
- // "Caption" and "Text": pass directly to the widget (all widgets
- // strdup their text internally).
- if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) {
- BasStringT *s = basValFormatString(value);
- wgtSetText(ctrl->widget, s->data);
- basStringUnref(s);
- return;
- }
-
- // Help topic
- if (strcasecmp(propName, "HelpTopic") == 0) {
- BasStringT *s = basValFormatString(value);
- snprintf(ctrl->helpTopic, BAS_MAX_CTRL_NAME, "%s", s->data);
- basStringUnref(s);
- return;
- }
-
- // Data binding properties (stored on BasControlT, not on the widget)
- if (strcasecmp(propName, "DataSource") == 0) {
- BasStringT *s = basValFormatString(value);
- snprintf(ctrl->dataSource, BAS_MAX_CTRL_NAME, "%s", s->data);
- basStringUnref(s);
- return;
- }
-
- if (strcasecmp(propName, "DataField") == 0) {
- BasStringT *s = basValFormatString(value);
- snprintf(ctrl->dataField, BAS_MAX_CTRL_NAME, "%s", s->data);
- basStringUnref(s);
- return;
- }
-
- // Interface descriptor properties
- if (ctrl->iface) {
- if (setIfaceProp(ctrl->iface, ctrl->widget, propName, value)) {
- return;
- }
- }
-
// None of the common, form-local, or iface property paths matched.
// A silent no-op used to hide typos; now it's a loud runtime error
// that mirrors the method-not-found diagnostic.
@@ -2678,12 +2418,12 @@ void basFormRtUnloadForm(void *ctx, void *formRef) {
form->unloading = true;
// QueryUnload: give the form a chance to cancel
- if (basFormRtFireEventWithCancel(rt, form, form->name, "QueryUnload")) {
+ if (basFormRtFireEventWithCancel(rt, form, form->name, BAS_EVT_QUERYUNLOAD)) {
form->unloading = false;
return;
}
- basFormRtFireEvent(rt, form, form->name, "Unload");
+ basFormRtFireEvent(rt, form, form->name, BAS_EVT_UNLOAD);
// Commit point: the form becomes unfindable, invisible, and
// event-inert right now, on both the deferred and immediate paths.
@@ -2703,20 +2443,13 @@ void basFormRtUnloadForm(void *ctx, void *formRef) {
}
-// Tracks whether the most recent basInputBox2 call was cancelled.
-// BASIC callers query this via basInputCancelled so they can tell the
-// difference between the user hitting Cancel and the user clicking OK
-// on an empty field.
-static bool sLastInputBoxCancelled = false;
-
-
const char *basInputBox2(const char *title, const char *prompt, const char *defaultText) {
if (!sFormRt) {
sLastInputBoxCancelled = true;
return "";
}
- static char buf[512];
+ static char buf[BAS_INPUTBOX_BUF_LEN];
buf[0] = '\0';
if (dvxInputBox(sFormRt->ctx, title, prompt, defaultText, buf, sizeof(buf))) {
@@ -2817,91 +2550,6 @@ static uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t
}
-// True if target is tree itself or any submenu nested within it (any depth).
-// Used by DestroyMenu to purge every popupMenus alias and contextMenu pointer
-// that references a tree about to be freed.
-static bool menuContainsMenu(const MenuT *tree, const MenuT *target) {
- if (!tree || !target) {
- return false;
- }
-
- if (tree == target) {
- return true;
- }
-
- for (int32_t i = 0; i < tree->itemCount; i++) {
- if (menuContainsMenu(tree->items[i].subMenu, target)) {
- return true;
- }
- }
-
- return false;
-}
-
-
-// Resolve a menu item by command id across both the form's menu bar AND its
-// popup-only menus, so .Checked/.Enabled work on Visible=False popup items
-// (which have no menu-bar entry) as well as bar items. When fromBar is
-// non-NULL it is set true if the item was found on the menu bar, so callers
-// can pick the bar radio-group setter over the popup one without re-walking.
-static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId, bool *fromBar) {
- if (fromBar) {
- *fromBar = false;
- }
-
- if (!form || menuId <= 0) {
- return NULL;
- }
-
- // Menu-bar items: walk every top-level menu (and its submenus) on the
- // form's window menu bar.
- if (form->window && form->window->menuBar) {
- MenuBarT *bar = form->window->menuBar;
-
- for (int32_t i = 0; i < bar->menuCount; i++) {
- MenuItemT *item = wmMenuFindItemInMenu(bar->menus[i], menuId);
-
- if (item) {
- if (fromBar) {
- *fromBar = true;
- }
-
- return item;
- }
- }
- }
-
- // Popup-only items: search each named popup/context menu the form owns.
- for (int32_t i = 0; i < form->popupMenuCount; i++) {
- MenuItemT *item = wmMenuFindItemInMenu(form->popupMenus[i].menu, menuId);
-
- if (item) {
- return item;
- }
- }
-
- return NULL;
-}
-
-
-// Find the loaded form whose .frm declared this SUB. Returns NULL if
-// the SUB is module-global (no BEGINFORM scope) or the owning form
-// isn't currently loaded -- callers should fall back to ctrl->form.
-static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc) {
- if (!rt || !proc || !proc->formName[0]) {
- return NULL;
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
- if (strcasecmp(rt->forms[i]->name, proc->formName) == 0) {
- return rt->forms[i];
- }
- }
-
- return NULL;
-}
-
-
int32_t basPromptSave(const char *title) {
if (!sFormRt) {
return DVX_SAVE_NO;
@@ -2911,64 +2559,32 @@ int32_t basPromptSave(const char *title) {
}
-// findPopupMenu -- look up a named popup menu on a form. Returns
-// NULL if no such menu exists.
-static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name) {
- if (!form || !name) {
- return NULL;
- }
-
- for (int32_t i = 0; i < form->popupMenuCount; i++) {
- if (strcasecmp(form->popupMenus[i].name, name) == 0) {
- return &form->popupMenus[i];
- }
- }
-
- return NULL;
-}
-
-
-// Allocate the next menu-item ID for a form. IDs are unique across
-// menu bar + popups on the same form and reused by menuIdMap for
-// event dispatch. Starts from MENU_ID_BASE + the highest in-use ID
-// so runtime-added items don't collide with .frm-loaded items.
-static int32_t nextMenuItemId(BasFormT *form) {
- int32_t maxId = MENU_ID_BASE - 1;
-
- for (int32_t i = 0; i < form->menuIdMapCount; i++) {
- if (form->menuIdMap[i].id > maxId) {
- maxId = form->menuIdMap[i].id;
- }
- }
-
- return maxId + 1;
-}
-
-
static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc) {
- if (strcasecmp(methodName, "SetFocus") == 0) {
+ int32_t methodId = basFormRtCommonMethodId(methodName);
+
+ if (methodId == BAS_CM_SETFOCUS) {
wgtSetFocused(ctrl->widget);
return zeroValue();
}
- if (strcasecmp(methodName, "Refresh") == 0) {
+ if (methodId == BAS_CM_REFRESH) {
wgtInvalidatePaint(ctrl->widget);
return zeroValue();
}
- if (strcasecmp(methodName, "SetReadOnly") == 0) {
+ if (methodId == BAS_CM_SETREADONLY) {
bool ro = (argc >= 1) ? (basValToNumber(args[0]) != 0.0) : true;
wgtSetReadOnly(ctrl->widget, ro);
return zeroValue();
}
- if (strcasecmp(methodName, "SetEnabled") == 0) {
+ if (methodId == BAS_CM_SETENABLED) {
bool en = (argc >= 1) ? (basValToNumber(args[0]) != 0.0) : true;
wgtSetEnabled(ctrl->widget, en);
return zeroValue();
}
- if (strcasecmp(methodName, "SetVisible") == 0) {
+ if (methodId == BAS_CM_SETVISIBLE) {
bool vis = (argc >= 1) ? (basValToNumber(args[0]) != 0.0) : true;
wgtSetVisible(ctrl->widget, vis);
return zeroValue();
@@ -2984,9 +2600,10 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
// menu-bar items so nameClick handlers fire regardless of which
// surface the user picked from.
- BasFormT *menuForm = ctrl ? ctrl->form : NULL;
+ // ctrl and ctrl->widget are guaranteed non-NULL by basFormRtCallMethod.
+ BasFormT *menuForm = ctrl->form;
- if (strcasecmp(methodName, "PopupMenu") == 0) {
+ if (methodId == BAS_CM_POPUPMENU) {
// PopupMenu name$ [, x%, y%]
if (!menuForm || !menuForm->window || argc < 1) {
return zeroValue();
@@ -3004,19 +2621,16 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
int32_t screenY = sFormRt ? sFormRt->ctx->mouseY : 0;
if (argc >= 3) {
- // Explicit coords are relative to the control if we have
- // one with a widget; form-level PopupMenu treats them as
- // client-area coords translated through the window.
- int32_t x = (int32_t)basValToNumber(args[1]);
- int32_t y = (int32_t)basValToNumber(args[2]);
+ // Explicit coords are relative to the control. WidgetT x/y
+ // are window-content-relative (the form's root widget sits at
+ // 0,0), and dvxShowContextMenu wants screen coords, so add
+ // the window origin and its content offset.
+ WindowT *win = menuForm->window;
+ int32_t x = (int32_t)basValToNumber(args[1]);
+ int32_t y = (int32_t)basValToNumber(args[2]);
- if (ctrl && ctrl->widget) {
- screenX = ctrl->widget->x + x;
- screenY = ctrl->widget->y + y;
- } else if (menuForm->window) {
- screenX = menuForm->window->x + menuForm->window->contentX + x;
- screenY = menuForm->window->y + menuForm->window->contentY + y;
- }
+ screenX = win->x + win->contentX + ctrl->widget->x + x;
+ screenY = win->y + win->contentY + ctrl->widget->y + y;
}
if (sFormRt && sFormRt->ctx) {
@@ -3026,7 +2640,7 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
return zeroValue();
}
- if (strcasecmp(methodName, "CreateMenu") == 0) {
+ if (methodId == BAS_CM_CREATEMENU) {
// CreateMenu name$ -- allocate an empty named popup menu.
// No-op if a menu with that name already exists.
if (!menuForm || argc < 1) {
@@ -3038,7 +2652,7 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
if (!findPopupMenu(menuForm, s->data)) {
BasFrmPopupMenuT entry;
memset(&entry, 0, sizeof(entry));
- snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", s->data);
+ snprintf(entry.name, BAS_MAX_IDENT, "%s", s->data);
entry.menu = wmCreateMenu();
entry.ownsMenu = true; // CreateMenu root
arrput(menuForm->popupMenus, entry);
@@ -3049,7 +2663,7 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
return zeroValue();
}
- if (strcasecmp(methodName, "AddMenuItem") == 0) {
+ if (methodId == BAS_CM_ADDMENUITEM) {
// AddMenuItem parentMenu$, itemName$, caption$
if (!menuForm || argc < 3) {
return zeroValue();
@@ -3068,7 +2682,7 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
BasMenuIdMapT map;
memset(&map, 0, sizeof(map));
map.id = id;
- snprintf(map.name, BAS_MAX_CTRL_NAME, "%s", itemN->data);
+ snprintf(map.name, BAS_MAX_IDENT, "%s", itemN->data);
arrput(menuForm->menuIdMap, map);
menuForm->menuIdMapCount = (int32_t)arrlen(menuForm->menuIdMap);
@@ -3083,7 +2697,7 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
return zeroValue();
}
- if (strcasecmp(methodName, "AddMenuSeparator") == 0) {
+ if (methodId == BAS_CM_ADDMENUSEPARATOR) {
// AddMenuSeparator parentMenu$
if (!menuForm || argc < 1) {
return zeroValue();
@@ -3100,7 +2714,7 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
return zeroValue();
}
- if (strcasecmp(methodName, "AddSubMenu") == 0) {
+ if (methodId == BAS_CM_ADDSUBMENU) {
// AddSubMenu parentMenu$, childName$, caption$
// The new submenu can have AddMenuItem etc. called on it
// via `childName`.
@@ -3120,7 +2734,7 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
if (sub) {
BasFrmPopupMenuT entry;
memset(&entry, 0, sizeof(entry));
- snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", childN->data);
+ snprintf(entry.name, BAS_MAX_IDENT, "%s", childN->data);
// NB: sub is owned by parentPm->menu (wmFreeMenu
// recurses); we store a non-owning reference here so
// AddMenuItem etc. can look it up by name. Don't
@@ -3150,9 +2764,9 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
// CreateControl(form, "ImageButton", "tbNew", toolbar) -- these
// just shorten the common case.
- bool isToolbar = (ctrl && strcasecmp(ctrl->typeName, "Toolbar") == 0);
+ bool isToolbar = (strcasecmp(ctrl->typeName, "Toolbar") == 0);
- if (isToolbar && strcasecmp(methodName, "AddButton") == 0) {
+ if (isToolbar && methodId == BAS_CM_ADDBUTTON) {
// AddButton name$, iconPath$
//
// Routed entirely through the generic runtime-creation +
@@ -3161,7 +2775,7 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
// resolves the widget via wgtFindByBasName at call time, and
// setProp "Picture" dispatches to whichever setter the
// ImageButton DXE registered in its iface.
- if (!menuForm || !ctrl->widget || argc < 2) {
+ if (!menuForm || argc < 2) {
return zeroValue();
}
@@ -3181,9 +2795,9 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
return zeroValue();
}
- if (isToolbar && strcasecmp(methodName, "AddTextButton") == 0) {
+ if (isToolbar && methodId == BAS_CM_ADDTEXTBUTTON) {
// AddTextButton name$, caption$
- if (!menuForm || !ctrl->widget || argc < 2) {
+ if (!menuForm || argc < 2) {
return zeroValue();
}
@@ -3203,12 +2817,12 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
return zeroValue();
}
- if (isToolbar && strcasecmp(methodName, "AddSeparator") == 0) {
+ if (isToolbar && methodId == BAS_CM_ADDSEPARATOR) {
// "Line" resolves to the separator widget via the iface
// registry. The separator widget auto-orients to its parent
// (vertical inside a horizontal container, horizontal inside
// a vertical one), so we don't need a distinct VLine basName.
- if (menuForm && ctrl->widget) {
+ if (menuForm) {
basFormRtCreateCtrlEx(sFormRt, menuForm, "Line", "", ctrl);
wgtInvalidate(ctrl->widget);
}
@@ -3216,30 +2830,27 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
return zeroValue();
}
- if (isToolbar && strcasecmp(methodName, "Clear") == 0) {
- // Destroy every child. Also remove their BasControlT
- // entries from the form so stale name lookups don't linger.
- if (!menuForm || !ctrl->widget) {
+ if (isToolbar && methodId == BAS_CM_CLEAR) {
+ // Destroy every child through the same path as RemoveControl so
+ // each child's BasControlT (and any grandchild entries, terminal
+ // bindings, tooltips) is detached now and freed only once no
+ // handler -- possibly the clicked button's own -- is on the stack.
+ if (!menuForm) {
return zeroValue();
}
WidgetT *child = ctrl->widget->firstChild;
while (child) {
- WidgetT *next = child->nextSibling;
+ WidgetT *next = child->nextSibling;
+ BasControlT *childCtrl = findCtrlByWidget(menuForm, child);
- // Remove matching BasControlT from the form's control
- // list so its name becomes unknown again.
- for (int32_t i = 0; i < (int32_t)arrlen(menuForm->controls); i++) {
- if (menuForm->controls[i]->widget == child) {
- free(menuForm->controls[i]->tooltip);
- free(menuForm->controls[i]);
- arrdel(menuForm->controls, i);
- break;
- }
+ if (childCtrl) {
+ removeCtrlTree(sFormRt, menuForm, childCtrl);
+ } else {
+ wgtDestroy(child);
}
- wgtDestroy(child);
child = next;
}
@@ -3247,19 +2858,17 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
return zeroValue();
}
- if (isToolbar && strcasecmp(methodName, "ButtonCount") == 0) {
+ if (isToolbar && methodId == BAS_CM_BUTTONCOUNT) {
int32_t count = 0;
- if (ctrl->widget) {
- for (WidgetT *c = ctrl->widget->firstChild; c; c = c->nextSibling) {
- count++;
- }
+ for (WidgetT *c = ctrl->widget->firstChild; c; c = c->nextSibling) {
+ count++;
}
return basValLong(count);
}
- if (strcasecmp(methodName, "DestroyMenu") == 0) {
+ if (methodId == BAS_CM_DESTROYMENU) {
// DestroyMenu name$ -- free a top-level popup menu and
// remove its entry. Submenus are freed recursively as
// part of their owning parent; calling DestroyMenu on a
@@ -3324,11 +2933,10 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
// names. This is a runtime safety net -- the parser should reject
// unknown methods at compile time once bascomp can see widget
// interface metadata.
- const char *ctrlName = ctrl ? ctrlDisplayName(ctrl) : "?";
basFormRtRuntimeError(sFormRt,
"Method not found on control",
"Method: %s\nControl: %s\nThe control has no method by that name.",
- methodName, ctrlName);
+ methodName, ctrlDisplayName(ctrl));
return zeroValue();
}
@@ -3354,6 +2962,17 @@ void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32
return;
}
+ // One binding per terminal and per connection: drop whatever this
+ // terminal was draining before, and unbind the terminal this slot
+ // previously fed, or two AnsiTerms end up draining the same link.
+ if (formRtDetachTermsForWidget(termWidget)) {
+ formRtSerIdleSync();
+ }
+
+ if (slot->attachedTerm) {
+ wgtAnsiTermSetComm(slot->attachedTerm, NULL, NULL, NULL);
+ }
+
slot->termChannel = channel;
slot->termEncrypt = (encrypt != 0);
@@ -3441,90 +3060,6 @@ static void commIdlePoll(void *ctx) {
}
-// Register or unregister the secLink idle poller with the shell based on
-// whether any connection is still open. Idempotent on both sides, so this
-// is safe to call from every CommAttach and CommClose. commIdlePoll keeps
-// every open link pumped (handshake/retransmit) regardless of which app is
-// foreground, so it stays registered until the last connection closes.
-static void formRtCommIdleSync(void) {
- bool needed = false;
-
- for (int32_t i = 0; i < (int32_t)arrlen(sCommSlots); i++) {
- if (sCommSlots[i] && sCommSlots[i]->active) {
- needed = true;
- break;
- }
- }
-
- if (needed) {
- if (resolveShellIdle()) {
- sShellRegisterIdle(commIdlePoll, NULL);
- } else {
- dvxLog("BASIC: shell idle registry unavailable; secLink polling disabled");
- }
- } else if (sShellUnregisterIdle) {
- sShellUnregisterIdle(commIdlePoll, NULL);
- }
-}
-
-
-// Detach any raw-serial or secLink terminal binding whose AnsiTerm widget
-// lives on this form. Called from basFormRtTeardownForm before the form's
-// controls array is freed and its widgets are destroyed; without this,
-// serIdlePoll and the later Comm/Ser close paths would dereference the
-// freed widget. The port/connection itself stays open -- only the
-// terminal binding is dropped, mirroring SerDetach/CommDetach.
-static void formRtDetachTermsForForm(BasFormT *form) {
- bool serDetached = false;
-
- for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
- WidgetT *widget = form->controls[i]->widget;
-
- if (!widget) {
- continue;
- }
-
- if (formRtDetachTermsForWidget(widget)) {
- serDetached = true;
- }
- }
-
- // Drop the raw-serial idle poller if no attached terminal remains.
- // The secLink poller stays registered while any connection is open
- // (it pumps handshake/retransmit even without a terminal).
- if (serDetached) {
- formRtSerIdleSync();
- }
-}
-
-
-// Detach any raw-serial or secLink terminal binding on a single widget.
-// Returns true if a raw-serial binding was dropped -- the caller then
-// syncs the idle poller via formRtSerIdleSync. Shared by the per-form
-// teardown loop above and basFormRtRemoveCtrl's immediate widget destroy.
-static bool formRtDetachTermsForWidget(WidgetT *widget) {
- bool serDetached = false;
-
- for (int32_t j = 0; j < RS232_NUM_PORTS; j++) {
- if (sSerAttach[j].attached && sSerAttach[j].term == widget) {
- wgtAnsiTermSetComm(widget, NULL, NULL, NULL);
- sSerAttach[j].attached = false;
- sSerAttach[j].term = NULL;
- serDetached = true;
- }
- }
-
- for (int32_t j = 0; j < (int32_t)arrlen(sCommSlots); j++) {
- if (sCommSlots[j] && sCommSlots[j]->attachedTerm == widget) {
- wgtAnsiTermSetComm(widget, NULL, NULL, NULL);
- sCommSlots[j]->attachedTerm = NULL;
- }
- }
-
- return serDetached;
-}
-
-
int32_t CommIsReady(int32_t handle) {
CommSlotT *slot = commGetSlot(handle);
@@ -3666,7 +3201,7 @@ static bool commResolveApi(void) {
int32_t CommSend(int32_t handle, const char *data, int32_t channel, int32_t encrypt) {
CommSlotT *slot = commGetSlot(handle);
- if (!slot || !data || !sCommApi.sendBuf) {
+ if (!slot || !data || !sCommApi.sendBuf || channel < 0 || channel >= SECLINK_NUM_CHANNELS) {
return 0;
}
@@ -3787,7 +3322,7 @@ WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *p
// Authoritative name of a control for diagnostics and lookups. The
// synthetic formCtrl carries no name copy of its own: form names hold
-// up to BAS_MAX_FORM_NAME - 1 chars, more than a BAS_MAX_CTRL_NAME
+// up to BAS_MAX_IDENT - 1 chars, more than a BAS_MAX_IDENT
// buffer, so form->name is the single source of truth.
static const char *ctrlDisplayName(const BasControlT *ctrl) {
if (ctrl->form && ctrl == &ctrl->form->formCtrl) {
@@ -3798,6 +3333,39 @@ static const char *ctrlDisplayName(const BasControlT *ctrl) {
}
+// Resolve a widget callback back to its BasControlT and runtime. NULL
+// (and *outRt unset) when the widget is not a BASIC control or its form
+// has no VM to dispatch into.
+static BasControlT *ctrlFromWidget(WidgetT *w, BasFormRtT **outRt) {
+ BasControlT *ctrl = (BasControlT *)w->userData;
+
+ if (!ctrl || !ctrl->form || !ctrl->form->vm) {
+ return NULL;
+ }
+
+ *outRt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
+ return *outRt ? ctrl : NULL;
+}
+
+
+// Free a form's resources, window, and the BasFormT itself. The form
+// must already be out of rt->forms (or the caller is about to arrfree
+// that array). Clears the modal gate if this window held it.
+static void destroyFormNow(BasFormRtT *rt, BasFormT *form) {
+ basFormRtTeardownForm(rt, form);
+
+ if (form->window) {
+ if (rt->ctx->modalWindow == form->window) {
+ rt->ctx->modalWindow = NULL;
+ }
+
+ dvxDestroyWindow(rt->ctx, form->window);
+ }
+
+ free(form);
+}
+
+
// The unload commit point, shared by the deferred path, the immediate
// path, and the load-failure discard. Makes the form unfindable
// (removed from rt->forms, so name lookups, Load/Show, and the
@@ -3824,11 +3392,26 @@ static void detachFormForUnload(BasFormRtT *rt, BasFormT *form) {
}
-// Case-insensitive scan of a single form's controls by name. The shared core
-// of every control-name lookup (basFormRtFindCtrl and findCtrlWidgetByName).
+// Control whose live widget is exactly w, or NULL.
+static BasControlT *findCtrlByWidget(BasFormT *form, const WidgetT *w) {
+ for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
+ if (form->controls[i]->widget == w) {
+ return form->controls[i];
+ }
+ }
+
+ return NULL;
+}
+
+
+// Case-insensitive scan of a single form's controls by bare name. The
+// shared core of every unindexed control lookup (basFormRtFindCtrl,
+// basFormRtRemoveCtrl, findCtrlWidgetByName). Control-array elements
+// are never matched here: an unindexed reference to an array is an
+// error in VB, and matching one would hit an arbitrary element.
static BasControlT *findCtrlInForm(BasFormT *form, const char *name) {
for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
- if (strcasecmp(form->controls[i]->name, name) == 0) {
+ if (form->controls[i]->index < 0 && strcasecmp(form->controls[i]->name, name) == 0) {
return form->controls[i];
}
}
@@ -3856,7 +3439,105 @@ static WidgetT *findCtrlWidgetByName(const char *name) {
}
+// Loaded form owning this window, or NULL. Shared by every window
+// callback (onForm*).
+static BasFormT *findFormByWindow(const WindowT *win) {
+ if (!sFormRt) {
+ return NULL;
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
+ if (sFormRt->forms[i]->window == win) {
+ return sFormRt->forms[i];
+ }
+ }
+
+ return NULL;
+}
+
+
+// Menu item on a form by name, lazily creating its property proxy (a
+// widget-less BasControlT carrying the menu id) on first access.
+static BasControlT *findMenuProxyInForm(BasFormT *form, const char *name) {
+ for (int32_t i = 0; i < form->menuIdMapCount; i++) {
+ if (strcasecmp(form->menuIdMap[i].name, name) != 0) {
+ continue;
+ }
+
+ if (!form->menuIdMap[i].proxy) {
+ BasControlT *proxy = (BasControlT *)calloc(1, sizeof(BasControlT));
+
+ if (proxy) {
+ snprintf(proxy->name, BAS_MAX_IDENT, "%s", name);
+ proxy->form = form;
+ proxy->menuId = form->menuIdMap[i].id;
+ form->menuIdMap[i].proxy = proxy;
+ }
+ }
+
+ return form->menuIdMap[i].proxy;
+ }
+
+ return NULL;
+}
+
+
+// Everything BASIC can name on one form: the form itself, its controls,
+// then its menu items.
+static BasControlT *findNamedObjectInForm(BasFormT *form, const char *name) {
+ if (strcasecmp(form->name, name) == 0) {
+ return &form->formCtrl;
+ }
+
+ BasControlT *hit = findCtrlInForm(form, name);
+
+ if (hit) {
+ return hit;
+ }
+
+ return findMenuProxyInForm(form, name);
+}
+
+
+// findPopupMenu -- look up a named popup menu on a form. Returns
+// NULL if no such menu exists.
+static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name) {
+ if (!form || !name) {
+ return NULL;
+ }
+
+ for (int32_t i = 0; i < form->popupMenuCount; i++) {
+ if (strcasecmp(form->popupMenus[i].name, name) == 0) {
+ return &form->popupMenus[i];
+ }
+ }
+
+ return NULL;
+}
+
+
+// Case-insensitive descriptor lookup in a property table.
+static const BasPropDescT *findPropDesc(const BasPropDescT *table, int32_t count, const char *name) {
+ for (int32_t i = 0; i < count; i++) {
+ if (strcasecmp(table[i].name, name) == 0) {
+ return &table[i];
+ }
+ }
+
+ return NULL;
+}
+
+
static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount) {
+ fireCtrlEventOut(rt, ctrl, eventName, args, argCount, NULL, 0);
+}
+
+
+// Full control-event dispatch: detached-form guard, same-event re-entry
+// guard, control-array Index prefix, SetEvent overrides, and optional
+// out-args (ByRef parameters such as Validate's Cancel) copied back from
+// the handler's leading locals. outArgs, when given, maps 1:1 onto args.
+static void fireCtrlEventOut(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount) {
// A detached form is committed to unload: its Unload event has
// already fired, so no further events (Timer, key, mouse, blur
// bridges) may be delivered to its controls.
@@ -3880,10 +3561,15 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
// Build final argument list (prepend Index for control arrays). The
// array is sized to the actual arg count so any number of event args
- // passes through intact; the +1 is for the Index prefix.
+ // passes through intact; the +1 is for the Index prefix. The out
+ // array gets the same prefix slot because the VM copies back the
+ // handler's leading locals positionally.
BasValueT *allArgs = NULL;
+ BasValueT *allOut = NULL;
const BasValueT *finalArgs = args;
+ BasValueT *finalOut = outArgs;
int32_t finalArgCount = argCount;
+ int32_t finalOutCount = outArgCount;
if (ctrl->index >= 0) {
allArgs = (BasValueT *)malloc(sizeof(BasValueT) * (argCount + 1));
@@ -3900,6 +3586,18 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
finalArgs = allArgs;
finalArgCount = argCount + 1;
+
+ if (outArgs && outArgCount > 0) {
+ allOut = (BasValueT *)calloc(outArgCount + 1, sizeof(BasValueT));
+
+ if (!allOut) {
+ free(allArgs);
+ return;
+ }
+
+ finalOut = allOut;
+ finalOutCount = outArgCount + 1;
+ }
}
// Resolve any SetEvent override FIRST. An override entry consumes
@@ -3922,6 +3620,7 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
if (overridden && (!overrideProc || overrideProc->isFunction)) {
free(allArgs);
+ free(allOut);
return;
}
@@ -3933,7 +3632,7 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
if (claimedGuard) {
ctrl->eventFiring = true;
- snprintf(ctrl->firingEventName, sizeof(ctrl->firingEventName), "%s", eventName ? eventName : "");
+ snprintf(ctrl->firingEventName, sizeof(ctrl->firingEventName), "%s", eventName);
}
// Rule-N native-bridge bracket: the eventFiring writes after the
@@ -3944,9 +3643,9 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
if (overridden) {
rt->vm->errorMsg[0] = '\0';
rt->vm->errorNumber = 0;
- rtCallHandler(rt, ctrl->form, overrideProc, overrideProc->codeAddr, finalArgs, finalArgCount, NULL, 0);
+ rtCallHandler(rt, ctrl->form, overrideProc, overrideProc->codeAddr, finalArgs, finalArgCount, finalOut, finalOutCount);
} else {
- basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, finalArgs, finalArgCount);
+ fireEventArgsOut(rt, ctrl->form, ctrl->name, eventName, finalArgs, finalArgCount, finalOut, finalOutCount);
}
if (claimedGuard) {
@@ -3954,11 +3653,183 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
ctrl->firingEventName[0] = '\0';
}
+ // Strip the Index prefix back out of the returned out-args.
+ if (allOut) {
+ basValRelease(&allOut[0]);
+
+ for (int32_t i = 0; i < outArgCount; i++) {
+ basValRelease(&outArgs[i]);
+ outArgs[i] = allOut[i + 1];
+ }
+ }
+
free(allArgs);
+ free(allOut);
rtEventLeave(rt);
}
+// Locate CtrlName_EventName and call it. The SUB must declare exactly
+// the number of parameters the event provides, or none (then it is
+// called with no args and nothing is copied back). outArgs, when
+// given, receives the handler's leading locals positionally (ByRef
+// event parameters such as Cancel). Single source of truth for the
+// Args, WithCancel, and control-event fire paths.
+static bool fireEventArgsOut(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount) {
+ if (!rt || !form || !rt->vm || !rt->module) {
+ return false;
+ }
+
+ char handlerName[MAX_EVENT_NAME_LEN];
+ snprintf(handlerName, sizeof(handlerName), "%s_%s", ctrlName, eventName);
+
+ const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName);
+
+ if (!proc || proc->isFunction) {
+ return false;
+ }
+
+ if (proc->paramCount == 0) {
+ return rtCallHandler(rt, form, proc, proc->codeAddr, NULL, 0, NULL, 0);
+ }
+
+ if (proc->paramCount != argCount) {
+ return false;
+ }
+
+ return rtCallHandler(rt, form, proc, proc->codeAddr, args, argCount, outArgs, outArgCount);
+}
+
+
+// Comma-separated list of the table's readable (or writable) property
+// names for "Valid: ..." diagnostics.
+static void formatPropNames(const BasPropDescT *table, int32_t count, bool forWrite, char *buf, int32_t bufSize) {
+ int32_t pos = 0;
+
+ buf[0] = '\0';
+
+ for (int32_t i = 0; i < count; i++) {
+ bool ok = forWrite ? table[i].writable : table[i].readable;
+
+ if (!ok || pos >= bufSize) {
+ continue;
+ }
+
+ pos += snprintf(buf + pos, bufSize - pos, "%s%s", pos ? ", " : "", table[i].name);
+ }
+}
+
+
+// Register or unregister the secLink idle poller with the shell based on
+// whether any connection is still open. Idempotent on both sides, so this
+// is safe to call from every CommAttach and CommClose. commIdlePoll keeps
+// every open link pumped (handshake/retransmit) regardless of which app is
+// foreground, so it stays registered until the last connection closes.
+static void formRtCommIdleSync(void) {
+ bool needed = false;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(sCommSlots); i++) {
+ if (sCommSlots[i] && sCommSlots[i]->active) {
+ needed = true;
+ break;
+ }
+ }
+
+ if (needed) {
+ if (resolveShellIdle()) {
+ sShellRegisterIdle(commIdlePoll, NULL);
+ } else {
+ dvxLog("BASIC: shell idle registry unavailable; secLink polling disabled");
+ }
+ } else if (sShellUnregisterIdle) {
+ sShellUnregisterIdle(commIdlePoll, NULL);
+ }
+}
+
+
+// Detach any raw-serial or secLink terminal binding whose AnsiTerm widget
+// lives on this form. Called from basFormRtTeardownForm before the form's
+// controls array is freed and its widgets are destroyed; without this,
+// serIdlePoll and the later Comm/Ser close paths would dereference the
+// freed widget. The port/connection itself stays open -- only the
+// terminal binding is dropped, mirroring SerDetach/CommDetach.
+static void formRtDetachTermsForForm(BasFormT *form) {
+ bool serDetached = false;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
+ WidgetT *widget = form->controls[i]->widget;
+
+ if (!widget) {
+ continue;
+ }
+
+ if (formRtDetachTermsForWidget(widget)) {
+ serDetached = true;
+ }
+ }
+
+ // Drop the raw-serial idle poller if no attached terminal remains.
+ // The secLink poller stays registered while any connection is open
+ // (it pumps handshake/retransmit even without a terminal).
+ if (serDetached) {
+ formRtSerIdleSync();
+ }
+}
+
+
+// Detach any raw-serial or secLink terminal binding on a single widget.
+// Returns true if a raw-serial binding was dropped -- the caller then
+// syncs the idle poller via formRtSerIdleSync. Shared by the per-form
+// teardown loop above and basFormRtRemoveCtrl's immediate widget destroy.
+static bool formRtDetachTermsForWidget(WidgetT *widget) {
+ bool serDetached = false;
+
+ for (int32_t j = 0; j < RS232_NUM_PORTS; j++) {
+ if (sSerAttach[j].attached && sSerAttach[j].term == widget) {
+ wgtAnsiTermSetComm(widget, NULL, NULL, NULL);
+ sSerAttach[j].attached = false;
+ sSerAttach[j].term = NULL;
+ serDetached = true;
+ }
+ }
+
+ for (int32_t j = 0; j < (int32_t)arrlen(sCommSlots); j++) {
+ if (sCommSlots[j] && sCommSlots[j]->attachedTerm == widget) {
+ wgtAnsiTermSetComm(widget, NULL, NULL, NULL);
+ sCommSlots[j]->attachedTerm = NULL;
+ }
+ }
+
+ return serDetached;
+}
+
+
+// Register or unregister the raw-serial idle poller with the shell based on
+// whether any port is still attached to a terminal. Idempotent, so it is
+// safe to call from SerAttach, SerDetach, and SerClose. serIdlePoll only
+// services attached terminals, so it is dropped as soon as none remain.
+static void formRtSerIdleSync(void) {
+ bool needed = false;
+
+ for (int32_t i = 0; i < RS232_NUM_PORTS; i++) {
+ if (sSerAttach[i].attached) {
+ needed = true;
+ break;
+ }
+ }
+
+ if (needed) {
+ if (resolveShellIdle()) {
+ sShellRegisterIdle(serIdlePoll, NULL);
+ } else {
+ dvxLog("BASIC: shell idle registry unavailable; serial polling disabled");
+ }
+ } else if (sShellUnregisterIdle) {
+ sShellUnregisterIdle(serIdlePoll, NULL);
+ }
+}
+
+
// Free a control and its heap-owned tooltip. NULL-safe: also used for
// menuIdMap proxies, which are calloc'd with no tooltip (free(NULL) is
// a no-op), so this is the single owner of control teardown.
@@ -3972,19 +3843,15 @@ static void freeControl(BasControlT *ctrl) {
}
-// ============================================================
-// frmParser callbacks for basFormRtLoadFrm
-// ============================================================
-
-static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char *name) {
+static void frmLoadOnCtrlBegin(void *userData, const char *typeName, const char *name) {
BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
- if (!ctx->form || ctx->nestDepth <= 0) {
+ if (!ctx->form || ctx->nestDepth <= 0 || ctx->rt->terminated) {
return;
}
// Clear any stale pointer first: if this control fails to create (unknown
- // type or NULL widget) ctx->current stays NULL, so frmLoad_onCtrlProp's
+ // type or NULL widget) ctx->current stays NULL, so frmLoadOnCtrlProp's
// guard drops its props instead of overwriting the previous control's.
ctx->current = NULL;
@@ -3996,7 +3863,7 @@ static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char
ctx->parentStack[0] = ctx->form->contentBox;
}
- const char *wgtTypeName = resolveTypeName(typeName);
+ const char *wgtTypeName = resolveTypeName(typeName);
bool isCtrlContainer = false;
if (!wgtTypeName) {
@@ -4005,53 +3872,54 @@ static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char
// control and surface later as a misleading "control not
// found" runtime error. The IDE validator blocks these at
// compile time; this is the safety net for bascomp standalone.
- basFormRtRuntimeError(sFormRt,
+ // basFormRtLoadFrm sees rt->terminated and discards the form.
+ basFormRtRuntimeError(ctx->rt,
"Unknown widget type in form",
"Form: %s\nControl: %s\nType: %s\nNot a recognized widget type (check spelling; DVX uses VB6-style names, e.g. SpinButton not Spinner).",
ctx->form->name,
name ? name : "(null)",
typeName ? typeName : "(null)");
+ return;
}
- if (wgtTypeName) {
- WidgetT *parent = ctx->parentStack[ctx->nestDepth - 1];
- WidgetT *widget = createWidget(wgtTypeName, parent);
+ WidgetT *parent = ctx->parentStack[ctx->nestDepth - 1];
+ WidgetT *widget = createWidget(wgtTypeName, parent);
- if (widget) {
- wgtSetName(widget, name);
+ if (widget) {
+ const WgtIfaceT *ctrlIface = wgtGetIface(wgtTypeName);
- BasControlT *ctrlEntry = (BasControlT *)calloc(1, sizeof(BasControlT));
+ wgtSetName(widget, name);
- if (ctrlEntry) {
- snprintf(ctrlEntry->name, BAS_MAX_CTRL_NAME, "%s", name);
- snprintf(ctrlEntry->typeName, BAS_MAX_CTRL_NAME, "%s", typeName);
- ctrlEntry->index = -1;
- ctrlEntry->widget = widget;
- ctrlEntry->form = ctx->form;
- ctrlEntry->iface = wgtGetIface(wgtTypeName);
- arrput(ctx->form->controls, ctrlEntry);
+ BasControlT *ctrlEntry = (BasControlT *)calloc(1, sizeof(BasControlT));
- ctx->current = ctrlEntry;
+ if (ctrlEntry) {
+ snprintf(ctrlEntry->name, BAS_MAX_IDENT, "%s", name);
+ snprintf(ctrlEntry->typeName, BAS_MAX_IDENT, "%s", typeName);
+ ctrlEntry->index = -1;
+ ctrlEntry->widget = widget;
+ ctrlEntry->form = ctx->form;
+ ctrlEntry->iface = ctrlIface;
+ arrput(ctx->form->controls, ctrlEntry);
- wireWidgetEvents(widget, ctrlEntry);
- }
+ ctx->current = ctrlEntry;
- const WgtIfaceT *ctrlIface = wgtGetIface(wgtTypeName);
- isCtrlContainer = ctrlIface && ctrlIface->isContainer;
+ wireWidgetEvents(widget, ctrlEntry);
+ }
- if (isCtrlContainer && ctx->nestDepth < BAS_MAX_FRM_NESTING) {
- ctx->parentStack[ctx->nestDepth++] = widget;
- }
+ isCtrlContainer = ctrlIface && ctrlIface->isContainer;
+
+ if (isCtrlContainer && ctx->nestDepth < FRM_MAX_NESTING) {
+ ctx->parentStack[ctx->nestDepth++] = widget;
}
}
- if (ctx->containerDepth < BAS_MAX_FRM_NESTING) {
+ if (ctx->containerDepth < FRM_MAX_NESTING) {
ctx->containerStack[ctx->containerDepth++] = isCtrlContainer;
}
}
-static void frmLoad_onCtrlEnd(void *userData) {
+static void frmLoadOnCtrlEnd(void *userData) {
BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
if (ctx->containerDepth > 0) {
@@ -4066,56 +3934,72 @@ static void frmLoad_onCtrlEnd(void *userData) {
}
-static void frmLoad_onCtrlProp(void *userData, const char *key, const char *value) {
- BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
+static void frmLoadOnCtrlProp(void *userData, const char *key, const char *value) {
+ BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
+ BasControlT *ctrl = ctx->current;
- if (!ctx->current) {
+ if (!ctrl || ctx->rt->terminated) {
return;
}
// Control array index is stored on the struct, not as a widget property
if (strcasecmp(key, "Index") == 0) {
- ctx->current->index = atoi(value);
+ ctrl->index = atoi(value);
return;
}
- char scratch[BAS_MAX_FRM_LINE_LEN];
+ char scratch[FRM_MAX_LINE_LEN];
snprintf(scratch, sizeof(scratch), "%s", value);
frmStripQuotes(scratch);
- if (strcasecmp(key, "HelpTopic") == 0) {
- snprintf(ctx->current->helpTopic, sizeof(ctx->current->helpTopic), "%s", scratch);
- return;
- }
-
// Layout property on a container: replace the parentStack entry
// with a layout box inside the container widget. VBox is the
// default for Frame, so no wrapper needed.
- if (strcasecmp(key, "Layout") == 0 && ctx->current->widget && ctx->nestDepth > 0) {
+ if (strcasecmp(key, "Layout") == 0 && ctx->nestDepth > 0) {
if (strcasecmp(scratch, "VBox") != 0) {
- ctx->parentStack[ctx->nestDepth - 1] = basFormRtCreateContentBox(ctx->current->widget, scratch);
+ ctx->parentStack[ctx->nestDepth - 1] = basFormRtCreateContentBox(ctrl->widget, scratch);
}
+
return;
}
- BasValueT val;
+ // Widget-declared properties are classified by the descriptor's own
+ // type -- enum by name, True/False, decimal, or string -- never by
+ // how the value happened to be quoted. A read-only descriptor is
+ // recognized and ignored, as at runtime.
+ const WgtPropDescT *ip = ctrl->iface ? wgtIfaceFindProp(ctrl->iface, key) : NULL;
- if (value[0] == '"') {
- val = basValStringFromC(scratch);
- } else if (strcasecmp(value, "True") == 0) {
- val = basValBool(true);
- } else if (strcasecmp(value, "False") == 0) {
- val = basValBool(false);
- } else {
- val = basValLong(atoi(value));
+ if (ip) {
+ if (ip->setFn && !wgtApplyPropFromString(ctrl->widget, ip, scratch)) {
+ basFormRtRuntimeError(ctx->rt,
+ "Bad property value in form",
+ "Form: %s\nControl: %s\nProperty: %s\nValue: %s\nNot a valid value for this property.",
+ ctx->form->name, ctrl->name, key, scratch);
+ }
+
+ return;
}
- basFormRtSetProp(ctx->rt, ctx->current, key, val);
+ // Common runtime-owned properties are typed by the shared table.
+ // Anything else goes through as a string so basFormRtSetProp reports
+ // the unknown name.
+ const BasPropDescT *cp = basFormRtFindCommonProp(key);
+ BasValueT val;
+
+ if (cp && cp->type == WGT_IFACE_BOOL) {
+ val = basValBool(frmParseBool(scratch));
+ } else if (cp && cp->type == WGT_IFACE_INT) {
+ val = basValLong(atoi(scratch));
+ } else {
+ val = basValStringFromC(scratch);
+ }
+
+ basFormRtSetProp(ctx->rt, ctrl, key, val);
basValRelease(&val);
}
-static bool frmLoad_onFormBegin(void *userData, const char *name) {
+static bool frmLoadOnFormBegin(void *userData, const char *name) {
BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
ctx->form = (BasFormT *)basFormRtLoadForm(ctx->rt, name);
@@ -4126,50 +4010,92 @@ static bool frmLoad_onFormBegin(void *userData, const char *name) {
// contentBox may already be set from basFormRtLoadForm. It gets
// replaced after Layout is known via onCtrlBegin's lazy creation.
- ctx->nestDepth = 1;
+ ctx->nestDepth = 1;
ctx->parentStack[0] = ctx->form->contentBox;
ctx->current = NULL;
return true;
}
-static void frmLoad_onFormProp(void *userData, const char *key, const char *value) {
+// Form-level .frm properties. Geometry and flags are accumulated on the
+// BasFormT and applied by basFormRtLoadFrm after every control exists
+// (window fitting needs the final control set); only Caption is
+// immediate. The key is validated and typed through sFormProps.
+static void frmLoadOnFormProp(void *userData, const char *key, const char *value) {
BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
if (!ctx->form) {
return;
}
- char text[BAS_MAX_FRM_LINE_LEN];
+ const BasPropDescT *pd = basFormRtFindFormProp(key);
+
+ if (!pd) {
+ return;
+ }
+
+ char text[FRM_MAX_LINE_LEN];
snprintf(text, sizeof(text), "%s", value);
frmStripQuotes(text);
- if (strcasecmp(key, "Caption") == 0) {
- dvxSetTitle(ctx->rt->ctx, ctx->form->window, text);
- } else if (strcasecmp(key, "Width") == 0) {
- ctx->form->frmWidth = atoi(value);
- } else if (strcasecmp(key, "Height") == 0) {
- ctx->form->frmHeight = atoi(value);
- } else if (strcasecmp(key, "Left") == 0) {
- ctx->form->frmLeft = atoi(value);
- } else if (strcasecmp(key, "Top") == 0) {
- ctx->form->frmTop = atoi(value);
- } else if (strcasecmp(key, "Resizable") == 0) {
- ctx->form->frmResizable = frmParseBool(text);
- ctx->form->frmHasResizable = true;
- } else if (strcasecmp(key, "Centered") == 0) {
- ctx->form->frmCentered = frmParseBool(text);
- } else if (strcasecmp(key, "AutoSize") == 0) {
- ctx->form->frmAutoSize = frmParseBool(text);
- } else if (strcasecmp(key, "Layout") == 0) {
- snprintf(ctx->form->frmLayout, sizeof(ctx->form->frmLayout), "%s", text);
- } else if (strcasecmp(key, "HelpTopic") == 0) {
- snprintf(ctx->form->helpTopic, sizeof(ctx->form->helpTopic), "%s", text);
+ int32_t num = (pd->type == WGT_IFACE_INT) ? atoi(text) : 0;
+ bool flag = (pd->type == WGT_IFACE_BOOL) && frmParseBool(text);
+ BasFormT *frm = ctx->form;
+
+ switch ((BasFormPropE)(pd - sFormProps)) {
+ case FORM_PROP_CAPTION:
+ dvxSetTitle(ctx->rt->ctx, frm->window, text);
+ break;
+
+ case FORM_PROP_WIDTH:
+ frm->frmWidth = num;
+ break;
+
+ case FORM_PROP_HEIGHT:
+ frm->frmHeight = num;
+ break;
+
+ case FORM_PROP_LEFT:
+ frm->frmLeft = num;
+ break;
+
+ case FORM_PROP_TOP:
+ frm->frmTop = num;
+ break;
+
+ case FORM_PROP_RESIZABLE:
+ frm->frmResizable = flag;
+ frm->frmHasResizable = true;
+ break;
+
+ case FORM_PROP_CENTERED:
+ frm->frmCentered = flag;
+ break;
+
+ case FORM_PROP_AUTOSIZE:
+ frm->frmAutoSize = flag;
+ break;
+
+ case FORM_PROP_LAYOUT:
+ snprintf(frm->frmLayout, sizeof(frm->frmLayout), "%s", text);
+ break;
+
+ case FORM_PROP_HELPTOPIC:
+ snprintf(frm->helpTopic, sizeof(frm->helpTopic), "%s", text);
+ break;
+
+ case FORM_PROP_NAME:
+ case FORM_PROP_VISIBLE:
+ case FORM_PROP_CONTEXTMENU:
+ case FORM_PROP_COUNT:
+ // Name comes from the Begin line; Visible and ContextMenu are
+ // runtime-only (the menu does not exist until the parse ends).
+ break;
}
}
-static void frmLoad_onMenuBegin(void *userData, const char *name, int32_t level) {
+static void frmLoadOnMenuBegin(void *userData, const char *name, int32_t level) {
BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
if (!ctx->form) {
@@ -4178,7 +4104,7 @@ static void frmLoad_onMenuBegin(void *userData, const char *name, int32_t level)
BasFrmMenuItemT mi;
memset(&mi, 0, sizeof(mi));
- snprintf(mi.name, BAS_MAX_CTRL_NAME, "%s", name);
+ snprintf(mi.name, BAS_MAX_IDENT, "%s", name);
mi.level = level;
mi.enabled = true;
mi.visible = true;
@@ -4188,14 +4114,14 @@ static void frmLoad_onMenuBegin(void *userData, const char *name, int32_t level)
}
-static void frmLoad_onMenuEnd(void *userData) {
+static void frmLoadOnMenuEnd(void *userData) {
BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
ctx->curMenuItemIdx = -1;
}
-static void frmLoad_onMenuProp(void *userData, const char *key, const char *value) {
+static void frmLoadOnMenuProp(void *userData, const char *key, const char *value) {
BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
if (ctx->curMenuItemIdx < 0 || ctx->curMenuItemIdx >= (int32_t)arrlen(ctx->menuItems)) {
@@ -4205,7 +4131,7 @@ static void frmLoad_onMenuProp(void *userData, const char *key, const char *valu
// Resolve the pointer fresh each use -- arrput on nested menus
// may have reallocated the array.
BasFrmMenuItemT *mip = &ctx->menuItems[ctx->curMenuItemIdx];
- char text[BAS_MAX_FRM_LINE_LEN];
+ char text[FRM_MAX_LINE_LEN];
snprintf(text, sizeof(text), "%s", value);
frmStripQuotes(text);
@@ -4216,100 +4142,163 @@ static void frmLoad_onMenuProp(void *userData, const char *key, const char *valu
} else if (strcasecmp(key, "RadioCheck") == 0) {
mip->radioCheck = frmParseBool(text);
} else if (strcasecmp(key, "Enabled") == 0) {
- // Default-true: anything not "False" enables the item.
- mip->enabled = (strcasecmp(text, "False") != 0);
+ // Default-true: an unrecognized value leaves the item enabled.
+ mip->enabled = frmParseBoolDefault(text, true);
} else if (strcasecmp(key, "Visible") == 0) {
// Top-level only: Visible=False marks the menu as a popup
// (not part of the menu bar). Ignored on nested items.
- mip->visible = (strcasecmp(text, "False") != 0);
+ mip->visible = frmParseBoolDefault(text, true);
}
}
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled) {
- *handled = true;
+ const BasPropDescT *pd = basFormRtFindCommonProp(propName);
+ WidgetT *w = ctrl->widget;
- if (strcasecmp(propName, "Name") == 0) {
- return basValStringFromC(ctrl->name);
+ *handled = (pd != NULL);
+
+ if (!pd) {
+ return zeroValue();
}
- if (strcasecmp(propName, "Left") == 0) {
- return basValLong(ctrl->widget->x);
- }
+ switch ((BasCtrlPropE)(pd - sCtrlProps)) {
+ case CTRL_PROP_NAME:
+ return basValStringFromC(ctrl->name);
- if (strcasecmp(propName, "Top") == 0) {
- return basValLong(ctrl->widget->y);
- }
+ case CTRL_PROP_LEFT:
+ return basValLong(w->x);
- if (strcasecmp(propName, "Width") == 0 || strcasecmp(propName, "MinWidth") == 0) {
- return basValLong(ctrl->widget->w);
- }
+ case CTRL_PROP_TOP:
+ return basValLong(w->y);
- if (strcasecmp(propName, "Height") == 0 || strcasecmp(propName, "MinHeight") == 0) {
- return basValLong(ctrl->widget->h);
- }
+ case CTRL_PROP_WIDTH:
+ case CTRL_PROP_MINWIDTH:
+ return basValLong(w->w);
- if (strcasecmp(propName, "MaxWidth") == 0) {
- return basValLong(ctrl->widget->maxW ? (ctrl->widget->maxW & WGT_SIZE_VAL_MASK) : 0);
- }
+ case CTRL_PROP_HEIGHT:
+ case CTRL_PROP_MINHEIGHT:
+ return basValLong(w->h);
- if (strcasecmp(propName, "MaxHeight") == 0) {
- return basValLong(ctrl->widget->maxH ? (ctrl->widget->maxH & WGT_SIZE_VAL_MASK) : 0);
- }
+ case CTRL_PROP_MAXWIDTH:
+ return basValLong(w->maxW ? (w->maxW & WGT_SIZE_VAL_MASK) : 0);
- if (strcasecmp(propName, "Weight") == 0) {
- return basValLong(ctrl->widget->weight);
- }
+ case CTRL_PROP_MAXHEIGHT:
+ return basValLong(w->maxH ? (w->maxH & WGT_SIZE_VAL_MASK) : 0);
- // Visible/Enabled: a widget interface may register a property of this
- // name that overrides the generic flag (e.g. Timer.Enabled drives its
- // running state, VB-style). Consult the interface once via getIfaceProp;
- // if it does not claim the name, fall back to the widget flag. This
- // resolves the override in a single table scan instead of an ifaceHasProp
- // probe here plus a re-scan in getIfaceProp.
- if (strcasecmp(propName, "Visible") == 0) {
- if (ctrl->iface) {
- bool ifaceHandled = false;
- BasValueT ifaceVal = getIfaceProp(ctrl->iface, ctrl->widget, "Visible", &ifaceHandled);
+ case CTRL_PROP_WEIGHT:
+ return basValLong(w->weight);
- if (ifaceHandled) {
- return ifaceVal;
- }
+ case CTRL_PROP_VISIBLE:
+ return basValBool(w->visible);
+
+ case CTRL_PROP_ENABLED:
+ return basValBool(w->enabled);
+
+ case CTRL_PROP_READONLY:
+ return basValBool(w->readOnly);
+
+ case CTRL_PROP_BACKCOLOR:
+ return basValLong((int32_t)w->bgColor);
+
+ case CTRL_PROP_FORECOLOR:
+ return basValLong((int32_t)w->fgColor);
+
+ case CTRL_PROP_CAPTION:
+ case CTRL_PROP_TEXT: {
+ // Caption and Text both map to the widget's text.
+ const char *text = wgtGetText(w);
+ return basValStringFromC(text ? text : "");
}
- return basValBool(ctrl->widget->visible);
- }
+ case CTRL_PROP_TOOLTIPTEXT:
+ return basValStringFromC(ctrl->tooltip ? ctrl->tooltip : "");
- if (strcasecmp(propName, "Enabled") == 0) {
- if (ctrl->iface) {
- bool ifaceHandled = false;
- BasValueT ifaceVal = getIfaceProp(ctrl->iface, ctrl->widget, "Enabled", &ifaceHandled);
+ case CTRL_PROP_CONTEXTMENU:
+ return basValStringFromC(popupMenuNameFor(ctrl->form, w->contextMenu));
- if (ifaceHandled) {
- return ifaceVal;
- }
+ case CTRL_PROP_HELPTOPIC:
+ return basValStringFromC(ctrl->helpTopic);
+
+ case CTRL_PROP_DATASOURCE:
+ return basValStringFromC(ctrl->dataSource);
+
+ case CTRL_PROP_DATAFIELD:
+ return basValStringFromC(ctrl->dataField);
+
+ case CTRL_PROP_LISTCOUNT: {
+ // Any widget with item storage exposes a ListCount method.
+ const WgtMethodDescT *m = ifaceFindMethod(ctrl->iface, "ListCount", WGT_SIG_RET_INT);
+ return basValLong(m ? ((int32_t (*)(const WidgetT *))m->fn)(w) : 0);
}
- return basValBool(ctrl->widget->enabled);
+ case CTRL_PROP_COUNT:
+ break;
}
- if (strcasecmp(propName, "TabIndex") == 0) {
- return basValLong(0);
+ return zeroValue();
+}
+
+
+// FormName.Prop reads: window geometry/state plus the BasFormT flags.
+static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName) {
+ WindowT *win = frm->window;
+ const BasPropDescT *pd = basFormRtFindFormProp(propName);
+
+ if (!pd || !pd->readable) {
+ char valid[BAS_PROP_LIST_LEN];
+ formatPropNames(sFormProps, FORM_PROP_COUNT, false, valid, sizeof(valid));
+ basFormRtRuntimeError(rt,
+ "Unknown form property",
+ "Form: %s\nProperty: %s\nValid: %s.",
+ frm->name, propName, valid);
+ return zeroValue();
}
- if (strcasecmp(propName, "BackColor") == 0) {
- return basValLong((int32_t)ctrl->widget->bgColor);
+ switch ((BasFormPropE)(pd - sFormProps)) {
+ case FORM_PROP_NAME:
+ return basValStringFromC(frm->name);
+
+ case FORM_PROP_CAPTION:
+ return basValStringFromC(win ? win->title : "");
+
+ case FORM_PROP_WIDTH:
+ return basValLong(win ? win->w : 0);
+
+ case FORM_PROP_HEIGHT:
+ return basValLong(win ? win->h : 0);
+
+ case FORM_PROP_LEFT:
+ return basValLong(win ? win->x : 0);
+
+ case FORM_PROP_TOP:
+ return basValLong(win ? win->y : 0);
+
+ case FORM_PROP_VISIBLE:
+ return basValBool(win && win->visible);
+
+ case FORM_PROP_RESIZABLE:
+ return basValBool(win && win->resizable);
+
+ case FORM_PROP_AUTOSIZE:
+ return basValBool(frm->frmAutoSize);
+
+ case FORM_PROP_CENTERED:
+ return basValBool(frm->frmCentered);
+
+ case FORM_PROP_LAYOUT:
+ return basValStringFromC(frm->frmLayout);
+
+ case FORM_PROP_CONTEXTMENU:
+ return basValStringFromC(popupMenuNameFor(frm, win ? win->contextMenu : NULL));
+
+ case FORM_PROP_HELPTOPIC:
+ return basValStringFromC(frm->helpTopic);
+
+ case FORM_PROP_COUNT:
+ break;
}
- if (strcasecmp(propName, "ForeColor") == 0) {
- return basValLong((int32_t)ctrl->widget->fgColor);
- }
-
- if (strcasecmp(propName, "ToolTipText") == 0) {
- return basValStringFromC(ctrl->tooltip ? ctrl->tooltip : "");
- }
-
- *handled = false;
return zeroValue();
}
@@ -4376,7 +4365,7 @@ int32_t HelpCompile(const char *inputFile, const char *outputFile) {
const char *inputs[1];
inputs[0] = inputFile;
- int32_t rc = sHlpcCompile(inputs, 1, outputFile, NULL, NULL, 1, NULL, NULL);
+ int32_t rc = sHlpcCompile(inputs, 1, outputFile, NULL, NULL, HLPC_QUIET, NULL, NULL);
return (rc == 0) ? -1 : 0;
}
@@ -4407,23 +4396,107 @@ void HelpView(const char *hlpFile) {
}
+// Enumerator index for a named value of an ENUM property, or -1 when the
+// name (or the property's name table) is absent. Shared by the runtime
+// setter and the .frm/designer string path so both agree on unknowns.
+static int32_t ifaceEnumIndex(const WgtPropDescT *p, const char *name) {
+ if (!p->enumNames || !name) {
+ return -1;
+ }
+
+ for (int32_t en = 0; p->enumNames[en]; en++) {
+ if (strcasecmp(p->enumNames[en], name) == 0) {
+ return en;
+ }
+ }
+
+ return -1;
+}
+
+
+// Interface method by name and signature, or NULL. A NULL iface is a
+// miss, so callers need no separate guard.
+static const WgtMethodDescT *ifaceFindMethod(const WgtIfaceT *iface, const char *name, uint8_t sig) {
+ if (!iface) {
+ return NULL;
+ }
+
+ for (int32_t m = 0; m < iface->methodCount; m++) {
+ if (iface->methods[m].sig == sig && strcasecmp(iface->methods[m].name, name) == 0) {
+ return &iface->methods[m];
+ }
+ }
+
+ return NULL;
+}
+
+
+// Read a STRING interface property through its getter; NULL when the
+// control has no such readable property.
+static const char *ifaceGetStringProp(const BasControlT *ctrl, const char *propName) {
+ if (!ctrl->iface || !ctrl->widget) {
+ return NULL;
+ }
+
+ const WgtPropDescT *pd = wgtIfaceFindProp(ctrl->iface, propName);
+
+ if (!pd || pd->type != WGT_IFACE_STRING || !pd->getFn) {
+ return NULL;
+ }
+
+ return ((const char *(*)(const WidgetT *))pd->getFn)(ctrl->widget);
+}
+
+
+// True if target is tree itself or any submenu nested within it (any depth).
+// Used by DestroyMenu to purge every popupMenus alias and contextMenu pointer
+// that references a tree about to be freed.
+static bool menuContainsMenu(const MenuT *tree, const MenuT *target) {
+ if (!tree || !target) {
+ return false;
+ }
+
+ if (tree == target) {
+ return true;
+ }
+
+ for (int32_t i = 0; i < tree->itemCount; i++) {
+ if (menuContainsMenu(tree->items[i].subMenu, target)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+// Allocate the next menu-item ID for a form. IDs are unique across
+// menu bar + popups on the same form and reused by menuIdMap for
+// event dispatch. Starts from MENU_ID_BASE + the highest in-use ID
+// so runtime-added items don't collide with .frm-loaded items.
+static int32_t nextMenuItemId(BasFormT *form) {
+ int32_t maxId = MENU_ID_BASE - 1;
+
+ for (int32_t i = 0; i < form->menuIdMapCount; i++) {
+ if (form->menuIdMap[i].id > maxId) {
+ maxId = form->menuIdMap[i].id;
+ }
+ }
+
+ return maxId + 1;
+}
+
+
static void onFormActivate(WindowT *win) {
// Chain the default widget focus handler: this overrides
// wgtInitWindow's win->onFocus, so without this the widget focus
// bridge (icon refresh, focus bookkeeping) never runs for forms.
widgetOnFocus(win);
- if (!sFormRt) {
- return;
- }
+ BasFormT *form = findFormByWindow(win);
- for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
- BasFormT *form = sFormRt->forms[i];
-
- if (form->window == win) {
- basFormRtFireEvent(sFormRt, form, form->name, "Activate");
- return;
- }
+ if (form) {
+ basFormRtFireEvent(sFormRt, form, form->name, BAS_EVT_ACTIVATE);
}
}
@@ -4434,26 +4507,19 @@ static void onFormActivate(WindowT *win) {
// re-entrancy guard and deferred frees) as the UNLOAD statement.
static void onFormClose(WindowT *win) {
- if (!sFormRt) {
+ BasFormT *form = findFormByWindow(win);
+
+ if (!form) {
return;
}
- for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
- BasFormT *form = sFormRt->forms[i];
+ basFormRtUnloadForm(sFormRt, form);
- if (form->window == win) {
- basFormRtUnloadForm(sFormRt, form);
-
- // If no forms are left, stop the VM. A QueryUnload cancel
- // leaves the form in sFormRt->forms, so this stays false
- // then; a committed unload is detached (removed) even when
- // its frees are deferred.
- if (arrlen(sFormRt->forms) == 0 && sFormRt->vm) {
- sFormRt->vm->running = false;
- }
-
- return;
- }
+ // If no forms are left, stop the VM. A QueryUnload cancel leaves the
+ // form in sFormRt->forms, so this stays false then; a committed
+ // unload is detached (removed) even when its frees are deferred.
+ if (arrlen(sFormRt->forms) == 0 && sFormRt->vm) {
+ sFormRt->vm->running = false;
}
}
@@ -4464,40 +4530,25 @@ static void onFormDeactivate(WindowT *win) {
// data-binding write-back), which this override otherwise suppresses.
widgetOnBlur(win);
- if (!sFormRt) {
- return;
- }
+ BasFormT *form = findFormByWindow(win);
- for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
- BasFormT *form = sFormRt->forms[i];
-
- if (form->window == win) {
- basFormRtFireEvent(sFormRt, form, form->name, "Deactivate");
- return;
- }
+ if (form) {
+ basFormRtFireEvent(sFormRt, form, form->name, BAS_EVT_DEACTIVATE);
}
}
-// onFormMenu -- dispatch menu clicks as ControlName_Click events
-// ============================================================
-
+// Dispatch menu clicks as ControlName_Click events.
static void onFormMenu(WindowT *win, int32_t menuId) {
- if (!sFormRt) {
+ BasFormT *form = findFormByWindow(win);
+
+ if (!form) {
return;
}
- for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
- BasFormT *form = sFormRt->forms[i];
-
- if (form->window == win) {
- for (int32_t j = 0; j < form->menuIdMapCount; j++) {
- if (form->menuIdMap[j].id == menuId) {
- basFormRtFireEvent(sFormRt, form, form->menuIdMap[j].name, "Click");
- return;
- }
- }
-
+ for (int32_t j = 0; j < form->menuIdMapCount; j++) {
+ if (form->menuIdMap[j].id == menuId) {
+ basFormRtFireEvent(sFormRt, form, form->menuIdMap[j].name, BAS_EVT_CLICK);
return;
}
}
@@ -4508,38 +4559,28 @@ static void onFormResize(WindowT *win, int32_t newW, int32_t newH) {
// Let the widget system re-evaluate scrollbars for the new size
widgetOnResize(win, newW, newH);
- if (!sFormRt) {
- return;
- }
+ BasFormT *form = findFormByWindow(win);
- for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
- BasFormT *form = sFormRt->forms[i];
-
- if (form->window == win) {
- basFormRtFireEvent(sFormRt, form, form->name, "Resize");
- return;
- }
+ if (form) {
+ basFormRtFireEvent(sFormRt, form, form->name, BAS_EVT_RESIZE);
}
}
static void onWidgetBlur(WidgetT *w) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
+ if (!ctrl) {
return;
}
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
// Rule-N native-bridge bracket: the data-binding write-back below
// reads ctrl and ctrl->form AFTER the LostFocus fire, so a deferred
// unload from the handler must not flush (and free them) until the
// bracket closes.
- if (rt) {
- rtEventEnter(rt);
- fireCtrlEvent(rt, ctrl, "LostFocus", NULL, 0);
- }
+ rtEventEnter(rt);
+ fireCtrlEvent(rt, ctrl, BAS_EVT_LOSTFOCUS, NULL, 0);
// Write-back: if this control is data-bound, update the Data control's
// cache and persist to the database
@@ -4562,247 +4603,175 @@ static void onWidgetBlur(WidgetT *w) {
}
}
- if (rt) {
- rtEventLeave(rt);
- }
+ rtEventLeave(rt);
}
static void onWidgetChange(WidgetT *w) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
+ if (!ctrl) {
return;
}
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (rt) {
- // Data controls fire "Reposition", update bound controls, and cascade to details.
- // Rule-N native-bridge bracket: refreshDetailControls reads
- // ctrl->form AFTER the Reposition fire, so a deferred unload from
- // the handler must not flush until the bracket closes.
- if (strcasecmp(ctrl->typeName, "Data") == 0) {
- rtEventEnter(rt);
- updateBoundControls(ctrl->form, ctrl);
- fireCtrlEvent(rt, ctrl, "Reposition", NULL, 0);
- refreshDetailControls(ctrl->form, ctrl);
- rtEventLeave(rt);
- return;
- }
-
- // Timer widgets fire "Timer" event, everything else fires "Change"
- const char *evtName = (strcasecmp(ctrl->typeName, "Timer") == 0) ? "Timer" : "Change";
- fireCtrlEvent(rt, ctrl, evtName, NULL, 0);
+ // Data controls fire "Reposition", update bound controls, and cascade to details.
+ // Rule-N native-bridge bracket: refreshDetailControls reads
+ // ctrl->form AFTER the Reposition fire, so a deferred unload from
+ // the handler must not flush until the bracket closes.
+ if (strcasecmp(ctrl->typeName, "Data") == 0) {
+ rtEventEnter(rt);
+ updateBoundControls(ctrl->form, ctrl);
+ fireCtrlEvent(rt, ctrl, BAS_EVT_REPOSITION, NULL, 0);
+ refreshDetailControls(ctrl->form, ctrl);
+ rtEventLeave(rt);
+ return;
}
+
+ // Timer widgets fire "Timer" event, everything else fires "Change"
+ const char *evtName = (strcasecmp(ctrl->typeName, "Timer") == 0) ? BAS_EVT_TIMER : BAS_EVT_CHANGE;
+ fireCtrlEvent(rt, ctrl, evtName, NULL, 0);
}
static void onWidgetClick(WidgetT *w) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
- return;
- }
-
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (rt) {
- fireCtrlEvent(rt, ctrl, "Click", NULL, 0);
+ if (ctrl) {
+ fireCtrlEvent(rt, ctrl, BAS_EVT_CLICK, NULL, 0);
}
}
static void onWidgetDblClick(WidgetT *w) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
- return;
- }
-
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (rt) {
- fireCtrlEvent(rt, ctrl, "DblClick", NULL, 0);
+ if (ctrl) {
+ fireCtrlEvent(rt, ctrl, BAS_EVT_DBLCLICK, NULL, 0);
}
}
static void onWidgetFocus(WidgetT *w) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
- return;
- }
-
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (rt) {
- fireCtrlEvent(rt, ctrl, "GotFocus", NULL, 0);
+ if (ctrl) {
+ fireCtrlEvent(rt, ctrl, BAS_EVT_GOTFOCUS, NULL, 0);
}
}
static void onWidgetKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
- return;
- }
-
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (rt) {
+ if (ctrl) {
BasValueT args[2];
args[0] = basValLong(keyCode);
args[1] = basValLong(shift);
- fireCtrlEvent(rt, ctrl, "KeyDown", args, 2);
+ fireCtrlEvent(rt, ctrl, BAS_EVT_KEYDOWN, args, 2);
}
}
static void onWidgetKeyPress(WidgetT *w, int32_t keyAscii) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
- return;
- }
-
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (rt) {
+ if (ctrl) {
BasValueT args[1];
args[0] = basValLong(keyAscii);
- fireCtrlEvent(rt, ctrl, "KeyPress", args, 1);
+ fireCtrlEvent(rt, ctrl, BAS_EVT_KEYPRESS, args, 1);
}
}
static void onWidgetKeyUp(WidgetT *w, int32_t keyCode, int32_t shift) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
- return;
- }
-
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (rt) {
+ if (ctrl) {
BasValueT args[2];
args[0] = basValLong(keyCode);
args[1] = basValLong(shift);
- fireCtrlEvent(rt, ctrl, "KeyUp", args, 2);
+ fireCtrlEvent(rt, ctrl, BAS_EVT_KEYUP, args, 2);
}
}
static void onWidgetMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
- return;
- }
-
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (rt) {
+ if (ctrl) {
BasValueT args[3];
args[0] = basValLong(button);
args[1] = basValLong(x);
args[2] = basValLong(y);
- fireCtrlEvent(rt, ctrl, "MouseDown", args, 3);
+ fireCtrlEvent(rt, ctrl, BAS_EVT_MOUSEDOWN, args, 3);
}
}
static void onWidgetMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
- return;
- }
-
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (rt) {
+ if (ctrl) {
BasValueT args[3];
args[0] = basValLong(button);
args[1] = basValLong(x);
args[2] = basValLong(y);
- fireCtrlEvent(rt, ctrl, "MouseMove", args, 3);
+ fireCtrlEvent(rt, ctrl, BAS_EVT_MOUSEMOVE, args, 3);
}
}
static void onWidgetMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
- return;
- }
-
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (rt) {
+ if (ctrl) {
BasValueT args[3];
args[0] = basValLong(button);
args[1] = basValLong(x);
args[2] = basValLong(y);
- fireCtrlEvent(rt, ctrl, "MouseUp", args, 3);
+ fireCtrlEvent(rt, ctrl, BAS_EVT_MOUSEUP, args, 3);
}
}
static void onWidgetScroll(WidgetT *w, int32_t delta) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
- return;
- }
-
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (rt) {
+ if (ctrl) {
BasValueT args[1];
args[0] = basValLong(delta);
- fireCtrlEvent(rt, ctrl, "Scroll", args, 1);
+ fireCtrlEvent(rt, ctrl, BAS_EVT_SCROLL, args, 1);
}
}
+// Validate(Cancel As Integer): goes through the full control-event path
+// (detached-form guard, SetEvent override, control-array Index prefix)
+// with Cancel as an out-arg. Returns false when the handler cancelled.
static bool onWidgetValidate(WidgetT *w) {
- BasControlT *ctrl = (BasControlT *)w->userData;
+ BasFormRtT *rt;
+ BasControlT *ctrl = ctrlFromWidget(w, &rt);
- if (!ctrl || !ctrl->form || !ctrl->form->vm) {
+ if (!ctrl) {
return true;
}
- BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
-
- if (!rt || !rt->module) {
- return true;
- }
-
- // Look for Data1_Validate(Cancel As Integer) handler
- char handlerName[MAX_EVENT_NAME_LEN];
- snprintf(handlerName, sizeof(handlerName), "%s_Validate", ctrl->name);
- const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName);
-
- if (!proc || proc->isFunction) {
- return true;
- }
-
- // Pass Cancel = 0 (False). If the handler sets it to non-zero, cancel.
BasValueT cancelArg = basValLong(0);
BasValueT outCancel = basValLong(0);
- if (proc->paramCount == 1) {
- rtCallHandler(rt, ctrl->form, proc, proc->codeAddr, &cancelArg, 1, &outCancel, 1);
- } else {
- rtCallHandler(rt, ctrl->form, proc, proc->codeAddr, NULL, 0, NULL, 0);
- }
+ fireCtrlEventOut(rt, ctrl, BAS_EVT_VALIDATE, &cancelArg, 1, &outCancel, 1);
- // Non-zero Cancel means abort the write
bool cancelled = basValIsTruthy(outCancel);
basValRelease(&outCancel);
return !cancelled;
@@ -4873,16 +4842,26 @@ static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, ch
p = pipe ? pipe + 1 : p + strlen(p);
}
- if (count <= 0) {
- out[0].label = "All Files (*.*)";
- count = 1;
- }
-
*outFilters = out;
return count;
}
+// Name of the form's popup menu that owns this MenuT, or "" (also for
+// NULL, i.e. no context menu set).
+static const char *popupMenuNameFor(const BasFormT *form, const MenuT *menu) {
+ if (menu && form) {
+ for (int32_t i = 0; i < form->popupMenuCount; i++) {
+ if (form->popupMenus[i].menu == menu) {
+ return form->popupMenus[i].name;
+ }
+ }
+ }
+
+ return "";
+}
+
+
// refreshDetailControls -- cascade master-detail: refresh detail Data controls
static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) {
if (!masterCtrl->widget) {
@@ -4902,23 +4881,9 @@ static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) {
continue;
}
- // Read MasterSource, MasterField, DetailField from the widget's interface
- const char *ms = NULL;
- const char *mf = NULL;
-
- for (int32_t p = 0; p < ctrl->iface->propCount; p++) {
- const WgtPropDescT *pd = &ctrl->iface->props[p];
-
- if (!pd->getFn) {
- continue;
- }
-
- if (strcasecmp(pd->name, "MasterSource") == 0) {
- ms = ((const char *(*)(const WidgetT *))pd->getFn)(ctrl->widget);
- } else if (strcasecmp(pd->name, "MasterField") == 0) {
- mf = ((const char *(*)(const WidgetT *))pd->getFn)(ctrl->widget);
- }
- }
+ // Read MasterSource and MasterField from the widget's interface
+ const char *ms = ifaceGetStringProp(ctrl, "MasterSource");
+ const char *mf = ifaceGetStringProp(ctrl, "MasterField");
if (!ms || !ms[0] || strcasecmp(ms, masterCtrl->name) != 0) {
continue;
@@ -4940,6 +4905,48 @@ static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) {
}
+// Detach a control and every control whose widget lives under it, then
+// destroy the widget subtree. Terminal bindings are dropped first (the
+// idle pollers would otherwise dereference freed widgets), the entries
+// leave form->controls immediately so name lookups miss them, and the
+// BasControlT structs are freed only when no handler is on the stack
+// (fireCtrlEventOut writes ctrl->eventFiring after the handler returns,
+// and that handler may be the removed control's own). Shared by
+// RemoveControl and Toolbar.Clear. The tooltip the compositor may be
+// showing is hidden by wgtDestroy itself.
+static void removeCtrlTree(BasFormRtT *rt, BasFormT *form, BasControlT *ctrl) {
+ WidgetT *widget = ctrl->widget;
+ bool serDetached = false;
+
+ for (int32_t i = (int32_t)arrlen(form->controls) - 1; i >= 0; i--) {
+ BasControlT *c = form->controls[i];
+
+ if (c != ctrl && !(widget && c->widget && widgetHasAncestor(c->widget, widget))) {
+ continue;
+ }
+
+ if (c->widget && formRtDetachTermsForWidget(c->widget)) {
+ serDetached = true;
+ }
+
+ c->widget = NULL;
+ arrdel(form->controls, i);
+
+ if (rt->eventDepth > 0) {
+ arrput(rt->pendingCtrlFree, c);
+ } else {
+ freeControl(c);
+ }
+ }
+
+ if (serDetached) {
+ formRtSerIdleSync();
+ }
+
+ wgtDestroy(widget);
+}
+
+
int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile) {
if (!path || !name || !srcFile) {
return 0;
@@ -5029,7 +5036,7 @@ int32_t ResExtract(const char *path, const char *name, const char *outFile) {
const char *ResGetText(const char *path, const char *name) {
- static char sBuf[1024];
+ static char sBuf[BAS_RES_TEXT_LEN];
sBuf[0] = '\0';
if (!path || !name) {
@@ -5062,6 +5069,25 @@ const char *ResGetText(const char *path, const char *name) {
}
+// Widgets with a pixel buffer (Canvas, Image, PictureBox) expose
+// Resize(w, h) so the buffer can follow the layout size once Width and
+// Height are known. Runs after .frm parsing and on every runtime
+// Width/Height assignment.
+static void resizeIfaceBuffer(BasControlT *ctrl) {
+ WidgetT *w = ctrl->widget;
+
+ if (!w || w->minW <= 0 || w->minH <= 0) {
+ return;
+ }
+
+ const WgtMethodDescT *m = ifaceFindMethod(ctrl->iface, "Resize", WGT_SIG_INT_INT);
+
+ if (m) {
+ ((void (*)(WidgetT *, int32_t, int32_t))m->fn)(w, w->minW, w->minH);
+ }
+}
+
+
const char *ResName(int32_t handle, int32_t index) {
int32_t idx = handle - 1;
@@ -5074,6 +5100,84 @@ const char *ResName(int32_t handle, int32_t index) {
}
+// Resolve a menu item by command id across both the form's menu bar AND its
+// popup-only menus, so .Checked/.Enabled work on Visible=False popup items
+// (which have no menu-bar entry) as well as bar items. When fromBar is
+// non-NULL it is set true if the item was found on the menu bar, so callers
+// can pick the bar radio-group setter over the popup one without re-walking.
+static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId, bool *fromBar) {
+ if (fromBar) {
+ *fromBar = false;
+ }
+
+ if (!form || menuId <= 0) {
+ return NULL;
+ }
+
+ // Menu-bar items: walk every top-level menu (and its submenus) on the
+ // form's window menu bar.
+ if (form->window && form->window->menuBar) {
+ MenuBarT *bar = form->window->menuBar;
+
+ for (int32_t i = 0; i < bar->menuCount; i++) {
+ MenuItemT *item = wmMenuFindItemInMenu(bar->menus[i], menuId);
+
+ if (item) {
+ if (fromBar) {
+ *fromBar = true;
+ }
+
+ return item;
+ }
+ }
+ }
+
+ // Popup-only items: search each named popup/context menu the form owns.
+ for (int32_t i = 0; i < form->popupMenuCount; i++) {
+ MenuItemT *item = wmMenuFindItemInMenu(form->popupMenus[i].menu, menuId);
+
+ if (item) {
+ return item;
+ }
+ }
+
+ return NULL;
+}
+
+
+// Find the loaded form whose .frm declared this SUB. Returns NULL if
+// the SUB is module-global (no BEGINFORM scope) or the owning form
+// isn't currently loaded -- callers should fall back to ctrl->form.
+static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc) {
+ if (!rt || !proc || !proc->formName[0]) {
+ return NULL;
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
+ if (strcasecmp(rt->forms[i]->name, proc->formName) == 0) {
+ return rt->forms[i];
+ }
+ }
+
+ return NULL;
+}
+
+
+// Lazily resolve the shell's idle-handler registry (mirrors the
+// _shellLoadAppWithArgs resolution used by HelpView). Returns false when
+// the host does not export the registry -- e.g. a headless/proxy build with
+// no shell event loop -- in which case polling is simply not driven.
+static bool resolveShellIdle(void) {
+ if (!sShellIdleResolved) {
+ sShellIdleResolved = true;
+ sShellRegisterIdle = dlsym(NULL, "_shellRegisterIdle");
+ sShellUnregisterIdle = dlsym(NULL, "_shellUnregisterIdle");
+ }
+
+ return sShellRegisterIdle != NULL;
+}
+
+
// Resolve a type name (VB-style or DVX widget name) to a DVX
// widget type name. First tries wgtFindByBasName for VB names
// like "CommandButton", then falls back to direct widget name.
@@ -5149,114 +5253,6 @@ int32_t ResType(int32_t handle, int32_t index) {
}
-void SerAttach(int32_t com, const char *termCtrlName) {
- if (!serResolveApi() || !sFormRt || !termCtrlName) {
- return;
- }
-
- int32_t idx = com - 1;
-
- if (idx < 0 || idx >= RS232_NUM_PORTS) {
- return;
- }
-
- // Find the terminal widget by control name
- WidgetT *termWidget = findCtrlWidgetByName(termCtrlName);
-
- if (!termWidget) {
- return;
- }
-
- sSerAttach[idx].com = idx;
- sSerAttach[idx].term = termWidget;
- sSerAttach[idx].attached = true;
-
- wgtAnsiTermSetComm(termWidget, &sSerAttach[idx], serTermRead, serTermWrite);
-
- // Register the serial poller with the shell's idle registry so this
- // terminal is fed every idle frame regardless of foreground app.
- formRtSerIdleSync();
-}
-
-
-int32_t SerAvailable(int32_t com) {
- if (!serResolveApi() || !sSerApi.getRxBuffered) {
- return 0;
- }
-
- return sSerApi.getRxBuffered(com - 1);
-}
-
-
-void SerClose(int32_t com) {
- if (!serResolveApi()) {
- return;
- }
-
- // Detach terminal if attached
- int32_t idx = com - 1;
-
- if (idx >= 0 && idx < RS232_NUM_PORTS && sSerAttach[idx].attached) {
- wgtAnsiTermSetComm(sSerAttach[idx].term, NULL, NULL, NULL);
- sSerAttach[idx].attached = false;
- sSerAttach[idx].term = NULL;
- formRtSerIdleSync();
- }
-
- sSerApi.close(com - 1);
-}
-
-
-void SerDetach(int32_t com) {
- int32_t idx = com - 1;
-
- if (idx < 0 || idx >= RS232_NUM_PORTS || !sSerAttach[idx].attached) {
- return;
- }
-
- wgtAnsiTermSetComm(sSerAttach[idx].term, NULL, NULL, NULL);
- sSerAttach[idx].attached = false;
- sSerAttach[idx].term = NULL;
- formRtSerIdleSync();
-}
-
-
-void SerFlush(int32_t com) {
- if (!serResolveApi() || !sSerApi.clearRxBuffer) {
- return;
- }
-
- sSerApi.clearRxBuffer(com - 1);
-}
-
-
-int32_t SerGetBase(int32_t com) {
- if (!serResolveApi() || !sSerApi.getBase) {
- return 0;
- }
-
- return sSerApi.getBase(com - 1);
-}
-
-
-int32_t SerGetIrq(int32_t com) {
- if (!serResolveApi() || !sSerApi.getIrq) {
- return 0;
- }
-
- return sSerApi.getIrq(com - 1);
-}
-
-
-int32_t SerGetUart(int32_t com) {
- if (!serResolveApi() || !sSerApi.getUartType) {
- return 0;
- }
-
- return sSerApi.getUartType(com - 1);
-}
-
-
// The single VM-entry wrapper for firing BASIC handler code. Owns, in
// order: the prevForm/prevVars/prevVarCount save, the owning-form vars
// binding, the rtEventEnter/rtEventLeave pairing, and the restore --
@@ -5356,6 +5352,122 @@ static void rtEventLeave(BasFormRtT *rt) {
}
+void SerAttach(int32_t com, const char *termCtrlName) {
+ if (!serResolveApi() || !sFormRt || !termCtrlName) {
+ return;
+ }
+
+ int32_t idx = com - 1;
+
+ if (idx < 0 || idx >= RS232_NUM_PORTS) {
+ return;
+ }
+
+ // Find the terminal widget by control name
+ WidgetT *termWidget = findCtrlWidgetByName(termCtrlName);
+
+ if (!termWidget) {
+ return;
+ }
+
+ // One binding per terminal and per port: unbind whatever this
+ // terminal was draining and whatever terminal this port was feeding.
+ formRtDetachTermsForWidget(termWidget);
+
+ if (sSerAttach[idx].attached) {
+ wgtAnsiTermSetComm(sSerAttach[idx].term, NULL, NULL, NULL);
+ }
+
+ sSerAttach[idx].com = idx;
+ sSerAttach[idx].term = termWidget;
+ sSerAttach[idx].attached = true;
+
+ wgtAnsiTermSetComm(termWidget, &sSerAttach[idx], serTermRead, serTermWrite);
+
+ // Register the serial poller with the shell's idle registry so this
+ // terminal is fed every idle frame regardless of foreground app.
+ formRtSerIdleSync();
+}
+
+
+int32_t SerAvailable(int32_t com) {
+ if (!serResolveApi() || !sSerApi.getRxBuffered) {
+ return 0;
+ }
+
+ return sSerApi.getRxBuffered(com - 1);
+}
+
+
+void SerClose(int32_t com) {
+ if (!serResolveApi()) {
+ return;
+ }
+
+ // Detach terminal if attached
+ int32_t idx = com - 1;
+
+ if (idx >= 0 && idx < RS232_NUM_PORTS && sSerAttach[idx].attached) {
+ wgtAnsiTermSetComm(sSerAttach[idx].term, NULL, NULL, NULL);
+ sSerAttach[idx].attached = false;
+ sSerAttach[idx].term = NULL;
+ formRtSerIdleSync();
+ }
+
+ sSerApi.close(com - 1);
+}
+
+
+void SerDetach(int32_t com) {
+ int32_t idx = com - 1;
+
+ if (idx < 0 || idx >= RS232_NUM_PORTS || !sSerAttach[idx].attached) {
+ return;
+ }
+
+ wgtAnsiTermSetComm(sSerAttach[idx].term, NULL, NULL, NULL);
+ sSerAttach[idx].attached = false;
+ sSerAttach[idx].term = NULL;
+ formRtSerIdleSync();
+}
+
+
+void SerFlush(int32_t com) {
+ if (!serResolveApi() || !sSerApi.clearRxBuffer) {
+ return;
+ }
+
+ sSerApi.clearRxBuffer(com - 1);
+}
+
+
+int32_t SerGetBase(int32_t com) {
+ if (!serResolveApi() || !sSerApi.getBase) {
+ return 0;
+ }
+
+ return sSerApi.getBase(com - 1);
+}
+
+
+int32_t SerGetIrq(int32_t com) {
+ if (!serResolveApi() || !sSerApi.getIrq) {
+ return 0;
+ }
+
+ return sSerApi.getIrq(com - 1);
+}
+
+
+int32_t SerGetUart(int32_t com) {
+ if (!serResolveApi() || !sSerApi.getUartType) {
+ return 0;
+ }
+
+ return sSerApi.getUartType(com - 1);
+}
+
+
// Idle callback for raw serial: polls all attached ports
static void serIdlePoll(void *ctx) {
(void)ctx;
@@ -5368,32 +5480,6 @@ static void serIdlePoll(void *ctx) {
}
-// Register or unregister the raw-serial idle poller with the shell based on
-// whether any port is still attached to a terminal. Idempotent, so it is
-// safe to call from SerAttach, SerDetach, and SerClose. serIdlePoll only
-// services attached terminals, so it is dropped as soon as none remain.
-static void formRtSerIdleSync(void) {
- bool needed = false;
-
- for (int32_t i = 0; i < RS232_NUM_PORTS; i++) {
- if (sSerAttach[i].attached) {
- needed = true;
- break;
- }
- }
-
- if (needed) {
- if (resolveShellIdle()) {
- sShellRegisterIdle(serIdlePoll, NULL);
- } else {
- dvxLog("BASIC: shell idle registry unavailable; serial polling disabled");
- }
- } else if (sShellUnregisterIdle) {
- sShellUnregisterIdle(serIdlePoll, NULL);
- }
-}
-
-
int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake) {
if (!serResolveApi()) {
return 0;
@@ -5422,21 +5508,6 @@ const char *SerRead(int32_t com) {
}
-// Lazily resolve the shell's idle-handler registry (mirrors the
-// _shellLoadAppWithArgs resolution used by HelpView). Returns false when
-// the host does not export the registry -- e.g. a headless/proxy build with
-// no shell event loop -- in which case polling is simply not driven.
-static bool resolveShellIdle(void) {
- if (!sShellIdleResolved) {
- sShellIdleResolved = true;
- sShellRegisterIdle = dlsym(NULL, "_shellRegisterIdle");
- sShellUnregisterIdle = dlsym(NULL, "_shellUnregisterIdle");
- }
-
- return sShellRegisterIdle != NULL;
-}
-
-
static bool serResolveApi(void) {
if (sSerApiResolved) {
return sSerApi.open != NULL;
@@ -5519,152 +5590,259 @@ int32_t SerWrite(int32_t com, const char *data) {
static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value) {
- if (strcasecmp(propName, "Left") == 0) {
- ctrl->widget->x = (int32_t)basValToNumber(value);
- wgtInvalidate(ctrl->widget);
- return true;
+ const BasPropDescT *pd = basFormRtFindCommonProp(propName);
+ WidgetT *w = ctrl->widget;
+
+ if (!pd) {
+ return false;
}
- if (strcasecmp(propName, "Top") == 0) {
- ctrl->widget->y = (int32_t)basValToNumber(value);
- wgtInvalidate(ctrl->widget);
- return true;
+ switch ((BasCtrlPropE)(pd - sCtrlProps)) {
+ case CTRL_PROP_LEFT:
+ w->x = (int32_t)basValToNumber(value);
+ wgtInvalidate(w);
+ break;
+
+ case CTRL_PROP_TOP:
+ w->y = (int32_t)basValToNumber(value);
+ wgtInvalidate(w);
+ break;
+
+ case CTRL_PROP_WIDTH:
+ case CTRL_PROP_MINWIDTH:
+ w->minW = wgtPixels((int32_t)basValToNumber(value));
+ wgtInvalidate(w);
+ resizeIfaceBuffer(ctrl);
+ break;
+
+ case CTRL_PROP_HEIGHT:
+ case CTRL_PROP_MINHEIGHT:
+ w->minH = wgtPixels((int32_t)basValToNumber(value));
+ wgtInvalidate(w);
+ resizeIfaceBuffer(ctrl);
+ break;
+
+ case CTRL_PROP_MAXWIDTH: {
+ int32_t mw = (int32_t)basValToNumber(value);
+ w->maxW = mw > 0 ? wgtPixels(mw) : 0;
+ wgtInvalidate(w);
+ break;
+ }
+
+ case CTRL_PROP_MAXHEIGHT: {
+ int32_t mh = (int32_t)basValToNumber(value);
+ w->maxH = mh > 0 ? wgtPixels(mh) : 0;
+ wgtInvalidate(w);
+ break;
+ }
+
+ case CTRL_PROP_WEIGHT:
+ w->weight = (int32_t)basValToNumber(value);
+ wgtInvalidate(w);
+ break;
+
+ case CTRL_PROP_VISIBLE:
+ wgtSetVisible(w, basValIsTruthy(value));
+ break;
+
+ case CTRL_PROP_ENABLED:
+ wgtSetEnabled(w, basValIsTruthy(value));
+ break;
+
+ case CTRL_PROP_READONLY:
+ wgtSetReadOnly(w, basValIsTruthy(value));
+ break;
+
+ case CTRL_PROP_BACKCOLOR:
+ w->bgColor = (uint32_t)(int32_t)basValToNumber(value);
+ wgtInvalidatePaint(w);
+ break;
+
+ case CTRL_PROP_FORECOLOR:
+ w->fgColor = (uint32_t)(int32_t)basValToNumber(value);
+ wgtInvalidatePaint(w);
+ break;
+
+ case CTRL_PROP_CAPTION:
+ case CTRL_PROP_TEXT: {
+ // Widgets strdup their text internally.
+ BasStringT *s = basValFormatString(value);
+ wgtSetText(w, s->data);
+ basStringUnref(s);
+ break;
+ }
+
+ case CTRL_PROP_TOOLTIPTEXT: {
+ // wgtSetTooltip references ctrl->tooltip, which must outlive
+ // the widget (freed by freeControl).
+ BasStringT *s = basValFormatString(value);
+ free(ctrl->tooltip);
+ ctrl->tooltip = (s->len > 0) ? strdup(s->data) : NULL;
+ wgtSetTooltip(w, ctrl->tooltip);
+ basStringUnref(s);
+ break;
+ }
+
+ case CTRL_PROP_CONTEXTMENU: {
+ // Set the auto-right-click menu. Empty string clears it.
+ BasStringT *s = basValFormatString(value);
+ BasFrmPopupMenuT *pm = (s->len > 0) ? findPopupMenu(ctrl->form, s->data) : NULL;
+
+ w->contextMenu = pm ? pm->menu : NULL;
+ basStringUnref(s);
+ break;
+ }
+
+ case CTRL_PROP_HELPTOPIC:
+ setControlString(ctrl->helpTopic, sizeof(ctrl->helpTopic), value);
+ break;
+
+ case CTRL_PROP_DATASOURCE:
+ setControlString(ctrl->dataSource, sizeof(ctrl->dataSource), value);
+ break;
+
+ case CTRL_PROP_DATAFIELD:
+ setControlString(ctrl->dataField, sizeof(ctrl->dataField), value);
+ break;
+
+ case CTRL_PROP_NAME:
+ case CTRL_PROP_LISTCOUNT:
+ case CTRL_PROP_COUNT:
+ // Read-only: let basFormRtSetProp report it.
+ return false;
}
- if (strcasecmp(propName, "Width") == 0 || strcasecmp(propName, "MinWidth") == 0) {
- int32_t w = (int32_t)basValToNumber(value);
- ctrl->widget->minW = wgtPixels(w);
- wgtInvalidate(ctrl->widget);
- // Widgets with a pixel-buffer (Canvas, Image) need their
- // internal buffer resized, not just the layout dimension.
- // basFormRtLoadFrm does this once at .frm parse time; for
- // dynamic controls (CreateControl + Width/Height properties)
- // we run the equivalent here.
- if (ctrl->iface && ctrl->widget->minW > 0 && ctrl->widget->minH > 0) {
- for (int32_t m = 0; m < ctrl->iface->methodCount; m++) {
- if (strcasecmp(ctrl->iface->methods[m].name, "Resize") == 0 &&
- ctrl->iface->methods[m].sig == WGT_SIG_INT_INT) {
- ((void (*)(WidgetT *, int32_t, int32_t))ctrl->iface->methods[m].fn)(ctrl->widget, ctrl->widget->minW, ctrl->widget->minH);
- break;
+ return true;
+}
+
+
+// Store a BASIC value's text into a fixed-size BasControlT string field.
+static void setControlString(char *dst, int32_t dstSize, BasValueT value) {
+ BasStringT *s = basValFormatString(value);
+ snprintf(dst, dstSize, "%s", s->data);
+ basStringUnref(s);
+}
+
+
+// FormName.Prop = value: window geometry/state plus the BasFormT flags.
+static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, BasValueT value) {
+ WindowT *win = frm->window;
+ const BasPropDescT *pd = basFormRtFindFormProp(propName);
+
+ if (!pd || !pd->writable) {
+ char valid[BAS_PROP_LIST_LEN];
+ formatPropNames(sFormProps, FORM_PROP_COUNT, true, valid, sizeof(valid));
+ basFormRtRuntimeError(rt,
+ "Unknown form property",
+ "Form: %s\nProperty: %s\nValid: %s.",
+ frm->name, propName, valid);
+ return;
+ }
+
+ // Geometry writes need the window; a windowless form has nothing to
+ // move (every path below that touches win is guarded by this).
+ bool hasWin = (win != NULL);
+ int32_t num = (int32_t)basValToNumber(value);
+ bool flag = basValIsTruthy(value);
+
+ switch ((BasFormPropE)(pd - sFormProps)) {
+ case FORM_PROP_CAPTION:
+ if (hasWin) {
+ BasStringT *s = basValFormatString(value);
+ dvxSetTitle(rt->ctx, win, s->data);
+ basStringUnref(s);
+ }
+ break;
+
+ case FORM_PROP_VISIBLE:
+ if (hasWin) {
+ if (flag) {
+ dvxShowWindow(rt->ctx, win);
+ } else {
+ dvxHideWindow(rt->ctx, win);
}
}
- }
- return true;
- }
+ break;
- if (strcasecmp(propName, "Height") == 0 || strcasecmp(propName, "MinHeight") == 0) {
- int32_t h = (int32_t)basValToNumber(value);
- ctrl->widget->minH = wgtPixels(h);
- wgtInvalidate(ctrl->widget);
- if (ctrl->iface && ctrl->widget->minW > 0 && ctrl->widget->minH > 0) {
- for (int32_t m = 0; m < ctrl->iface->methodCount; m++) {
- if (strcasecmp(ctrl->iface->methods[m].name, "Resize") == 0 &&
- ctrl->iface->methods[m].sig == WGT_SIG_INT_INT) {
- ((void (*)(WidgetT *, int32_t, int32_t))ctrl->iface->methods[m].fn)(ctrl->widget, ctrl->widget->minW, ctrl->widget->minH);
- break;
- }
+ case FORM_PROP_WIDTH:
+ if (hasWin) {
+ dvxResizeWindow(rt->ctx, win, num, win->h);
}
- }
- return true;
- }
+ break;
- if (strcasecmp(propName, "MaxWidth") == 0) {
- int32_t mw = (int32_t)basValToNumber(value);
- ctrl->widget->maxW = mw > 0 ? wgtPixels(mw) : 0;
- wgtInvalidate(ctrl->widget);
- return true;
- }
-
- if (strcasecmp(propName, "MaxHeight") == 0) {
- int32_t mh = (int32_t)basValToNumber(value);
- ctrl->widget->maxH = mh > 0 ? wgtPixels(mh) : 0;
- wgtInvalidate(ctrl->widget);
- return true;
- }
-
- if (strcasecmp(propName, "Weight") == 0) {
- ctrl->widget->weight = (int32_t)basValToNumber(value);
- wgtInvalidate(ctrl->widget);
- return true;
- }
-
- // Visible/Enabled: defer to an interface property of the same name if
- // the widget registers one (single scan via setIfaceProp), else set the
- // generic widget flag.
- if (strcasecmp(propName, "Visible") == 0) {
- if (ctrl->iface && setIfaceProp(ctrl->iface, ctrl->widget, "Visible", value)) {
- return true;
- }
-
- wgtSetVisible(ctrl->widget, basValIsTruthy(value));
- return true;
- }
-
- if (strcasecmp(propName, "Enabled") == 0) {
- if (ctrl->iface && setIfaceProp(ctrl->iface, ctrl->widget, "Enabled", value)) {
- return true;
- }
-
- wgtSetEnabled(ctrl->widget, basValIsTruthy(value));
- return true;
- }
-
- if (strcasecmp(propName, "ReadOnly") == 0) {
- wgtSetReadOnly(ctrl->widget, basValIsTruthy(value));
- return true;
- }
-
- if (strcasecmp(propName, "TabIndex") == 0) {
- return true;
- }
-
- if (strcasecmp(propName, "BackColor") == 0) {
- ctrl->widget->bgColor = (uint32_t)(int32_t)basValToNumber(value);
- wgtInvalidatePaint(ctrl->widget);
- return true;
- }
-
- if (strcasecmp(propName, "ForeColor") == 0) {
- ctrl->widget->fgColor = (uint32_t)(int32_t)basValToNumber(value);
- wgtInvalidatePaint(ctrl->widget);
- return true;
- }
-
- if (strcasecmp(propName, "ToolTipText") == 0) {
- BasStringT *s = basValFormatString(value);
- free(ctrl->tooltip);
- ctrl->tooltip = (s->len > 0) ? strdup(s->data) : NULL;
- wgtSetTooltip(ctrl->widget, ctrl->tooltip);
- basStringUnref(s);
- return true;
- }
-
- if (strcasecmp(propName, "ContextMenu") == 0) {
- // Set the auto-right-click menu. Empty string clears it.
- BasStringT *s = basValFormatString(value);
- MenuT *m = NULL;
-
- if (s->len > 0 && ctrl->form) {
- BasFrmPopupMenuT *pm = findPopupMenu(ctrl->form, s->data);
-
- if (pm) {
- m = pm->menu;
+ case FORM_PROP_HEIGHT:
+ if (hasWin) {
+ dvxResizeWindow(rt->ctx, win, win->w, num);
}
+ break;
+
+ case FORM_PROP_LEFT:
+ if (hasWin) {
+ dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
+ win->x = num;
+ dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
+ }
+ break;
+
+ case FORM_PROP_TOP:
+ if (hasWin) {
+ dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
+ win->y = num;
+ dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
+ }
+ break;
+
+ case FORM_PROP_RESIZABLE:
+ if (hasWin) {
+ dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
+ win->resizable = flag;
+ dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
+ }
+ break;
+
+ case FORM_PROP_AUTOSIZE:
+ frm->frmAutoSize = flag;
+
+ if (flag && hasWin) {
+ dvxFitWindow(rt->ctx, win);
+ }
+ break;
+
+ case FORM_PROP_CENTERED:
+ frm->frmCentered = flag;
+
+ if (flag && hasWin) {
+ dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
+ win->x = (rt->ctx->display.width - win->w) / 2;
+ win->y = (rt->ctx->display.height - win->h) / 2;
+ dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
+ }
+ break;
+
+ case FORM_PROP_CONTEXTMENU: {
+ // Attach to the window so right-clicks outside any child
+ // widget still pop the menu. Empty string clears it.
+ BasStringT *s = basValFormatString(value);
+ BasFrmPopupMenuT *pm = (s->len > 0) ? findPopupMenu(frm, s->data) : NULL;
+
+ if (hasWin) {
+ win->contextMenu = pm ? pm->menu : NULL;
+ }
+
+ basStringUnref(s);
+ break;
}
- if (ctrl->widget) {
- ctrl->widget->contextMenu = m;
- } else if (ctrl->form && ctrl->form->window && ctrl == &ctrl->form->formCtrl) {
- // Form-level ContextMenu: attach to the window so clicks
- // outside any child widget still pop up the menu.
- ctrl->form->window->contextMenu = m;
- }
+ case FORM_PROP_HELPTOPIC:
+ setControlString(frm->helpTopic, sizeof(frm->helpTopic), value);
+ break;
- basStringUnref(s);
- return true;
+ case FORM_PROP_NAME:
+ case FORM_PROP_LAYOUT:
+ case FORM_PROP_COUNT:
+ // Read-only: rejected by the writable check above.
+ break;
}
-
- return false;
}
@@ -5691,15 +5869,17 @@ static bool setIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propNam
}
case WGT_IFACE_ENUM:
- if (p->enumNames && value.type == BAS_TYPE_STRING && value.strVal) {
- // Map name to index
- int32_t enumVal = 0;
+ if (value.type == BAS_TYPE_STRING && value.strVal) {
+ // Map name to index; an unknown name is a program bug,
+ // not a silent zero.
+ int32_t enumVal = ifaceEnumIndex(p, value.strVal->data);
- for (int32_t en = 0; p->enumNames[en]; en++) {
- if (strcasecmp(p->enumNames[en], value.strVal->data) == 0) {
- enumVal = en;
- break;
- }
+ if (enumVal < 0) {
+ basFormRtRuntimeError(sFormRt,
+ "Bad enum value for property",
+ "Property: %s\nValue: %s\nNot one of this property's named values.",
+ p->name, value.strVal->data);
+ break;
}
((void (*)(WidgetT *, int32_t))p->setFn)(w, enumVal);
@@ -5886,20 +6066,7 @@ static void unloadFormNow(BasFormRtT *rt, BasFormT *form) {
basVmSetCurrentFormVars(rt->vm, NULL, 0);
}
- basFormRtTeardownForm(rt, form);
-
- if (form->window) {
- if (rt->ctx->modalWindow == form->window) {
- rt->ctx->modalWindow = NULL;
- }
-
- dvxDestroyWindow(rt->ctx, form->window);
- form->window = NULL;
- form->root = NULL;
- form->contentBox = NULL;
- }
-
- free(form);
+ destroyFormNow(rt, form);
}
@@ -5943,14 +6110,15 @@ bool wgtApplyPropFromString(WidgetT *w, const WgtPropDescT *p, const char *val)
return false;
}
- if (p->type == WGT_IFACE_ENUM && p->enumNames) {
- for (int32_t en = 0; p->enumNames[en]; en++) {
- if (strcasecmp(p->enumNames[en], val) == 0) {
- ((void (*)(WidgetT *, int32_t))p->setFn)(w, en);
- return true;
- }
+ if (p->type == WGT_IFACE_ENUM) {
+ int32_t en = ifaceEnumIndex(p, val);
+
+ if (en < 0) {
+ return false;
}
- return false;
+
+ ((void (*)(WidgetT *, int32_t))p->setFn)(w, en);
+ return true;
}
if (p->type == WGT_IFACE_INT) {
@@ -6018,6 +6186,18 @@ bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, in
}
+// True if w is ancestor or sits anywhere beneath it.
+static bool widgetHasAncestor(const WidgetT *w, const WidgetT *ancestor) {
+ for (const WidgetT *p = w; p; p = p->parent) {
+ if (p == ancestor) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
// Wires the full set of widget event callbacks to the runtime dispatchers and
// links the widget back to its BasControlT. Shared by the two dynamic
// create-control paths and the .frm loader so every control behaves the same.
@@ -6044,5 +6224,3 @@ static BasValueT zeroValue(void) {
memset(&v, 0, sizeof(v));
return v;
}
-
-
diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h
index 3541540..6033ec8 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formrt.h
+++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h
@@ -36,6 +36,7 @@
#include "../runtime/values.h"
#include "dvxApp.h"
#include "dvxWgt.h"
+#include "frmParser.h"
// ============================================================
// Forward declarations
@@ -48,14 +49,74 @@ typedef struct BasControlT BasControlT;
// Limits
// ============================================================
-#define BAS_MAX_CTRL_NAME 32
-// Form names use the full identifier length (must equal the compiler-side
-// BAS_MAX_PROC_NAME / BAS_MAX_SYMBOL_NAME in vm.h); otherwise a 32-63 char
-// form name truncates here and form-scope variable binding (keyed by name)
-// silently fails to match the module's 63-char formName.
-#define BAS_MAX_FORM_NAME 64
-#define BAS_MAX_FRM_LINE_LEN 512
-#define BAS_MAX_FRM_NESTING 16
+// Form and control names share the single identifier length BAS_MAX_IDENT
+// (vm.h) with the compiler, so name-keyed lookups never truncate.
+// Window size for a form that gives no Width/Height; the IDE designer
+// uses the same values for a new form.
+#define BAS_DEFAULT_FORM_W 400
+#define BAS_DEFAULT_FORM_H 300
+
+// ============================================================
+// Property descriptors
+// ============================================================
+//
+// The single source of truth for the property names the form runtime
+// accepts on the form object itself and on every non-menu control.
+// getProp/setProp dispatch, the .frm loader's value classification,
+// and the "Valid: ..." runtime-error lists are all driven from these
+// tables; the IDE validator and property grid should consume them too
+// instead of keeping their own copies.
+
+typedef struct {
+ const char *name;
+ uint8_t type; // WGT_IFACE_STRING / WGT_IFACE_INT / WGT_IFACE_BOOL
+ bool readable;
+ bool writable;
+} BasPropDescT;
+
+// Form-object properties (FormName.Prop), in table order.
+const BasPropDescT *basFormRtFormProps(int32_t *count);
+
+// Properties every non-menu control accepts before its widget interface
+// is consulted, in table order.
+const BasPropDescT *basFormRtCommonProps(int32_t *count);
+
+// Case-insensitive lookups; NULL when the name is not in the table.
+const BasPropDescT *basFormRtFindFormProp(const char *name);
+const BasPropDescT *basFormRtFindCommonProp(const char *name);
+
+// ============================================================
+// Common methods
+// ============================================================
+//
+// Methods callCommonMethod() accepts on every non-menu control (the
+// toolbar button methods are answered only by a Toolbar). The IDE
+// validator consumes this table instead of keeping its own list.
+
+typedef enum {
+ BAS_CM_SETFOCUS,
+ BAS_CM_REFRESH,
+ BAS_CM_SETREADONLY,
+ BAS_CM_SETENABLED,
+ BAS_CM_SETVISIBLE,
+ BAS_CM_POPUPMENU,
+ BAS_CM_CREATEMENU,
+ BAS_CM_ADDMENUITEM,
+ BAS_CM_ADDMENUSEPARATOR,
+ BAS_CM_ADDSUBMENU,
+ BAS_CM_DESTROYMENU,
+ BAS_CM_ADDBUTTON,
+ BAS_CM_ADDTEXTBUTTON,
+ BAS_CM_ADDSEPARATOR,
+ BAS_CM_CLEAR,
+ BAS_CM_BUTTONCOUNT,
+ BAS_CM_COUNT
+} BasCommonMethodE;
+
+#define BAS_CM_NONE (-1)
+
+// Case-insensitive lookup; BAS_CM_NONE when name is not a common method.
+int32_t basFormRtCommonMethodId(const char *name);
// ============================================================
// Menu ID to name mapping for event dispatch
@@ -63,13 +124,13 @@ typedef struct BasControlT BasControlT;
typedef struct {
int32_t id;
- char name[BAS_MAX_CTRL_NAME];
+ char name[BAS_MAX_IDENT];
BasControlT *proxy; // heap-allocated proxy for property access (widget=NULL, menuId stored)
} BasMenuIdMapT;
// Named popup / context menu owned by a form.
typedef struct {
- char name[BAS_MAX_CTRL_NAME];
+ char name[BAS_MAX_IDENT];
MenuT *menu; // wmCreateMenu root, or a submenu owned by its root
bool ownsMenu; // true only for the root that wmFreeMenu must free
} BasFrmPopupMenuT;
@@ -82,20 +143,20 @@ typedef struct {
// Event handler override (SetEvent)
typedef struct {
- char eventName[BAS_MAX_CTRL_NAME]; // e.g. "Click"
- char handlerName[BAS_MAX_CTRL_NAME]; // e.g. "HandleOkClick"
+ char eventName[BAS_MAX_IDENT]; // e.g. "Click"
+ char handlerName[BAS_MAX_IDENT]; // e.g. "HandleOkClick"
} BasEventOverrideT;
typedef struct BasControlT {
- char name[BAS_MAX_CTRL_NAME]; // VB control name (e.g. "Command1")
- char typeName[BAS_MAX_CTRL_NAME]; // VB type name (e.g. "CommandButton")
+ char name[BAS_MAX_IDENT]; // VB control name (e.g. "Command1")
+ char typeName[BAS_MAX_IDENT]; // VB type name (e.g. "CommandButton")
int32_t index; // control array index (-1 = not in array)
WidgetT *widget; // the DVX widget
BasFormT *form; // owning form
const WgtIfaceT *iface; // interface descriptor (from .wgt)
- char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding
- char dataField[BAS_MAX_CTRL_NAME]; // column name for binding
- char helpTopic[BAS_MAX_CTRL_NAME]; // help topic ID for F1
+ char dataSource[BAS_MAX_IDENT]; // name of Data control for binding
+ char dataField[BAS_MAX_IDENT]; // column name for binding
+ char helpTopic[BAS_MAX_IDENT]; // help topic ID for F1
char *tooltip; // heap-owned tooltip text (NULL = none); wgtSetTooltip references this buffer, so it must outlive the widget
int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls)
BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES];
@@ -108,7 +169,7 @@ typedef struct BasControlT {
// event is in flight so different-event delivery (e.g. LostFocus
// while Click is still running) is allowed through.
bool eventFiring;
- char firingEventName[BAS_MAX_CTRL_NAME];
+ char firingEventName[BAS_MAX_IDENT];
} BasControlT;
// ============================================================
@@ -116,7 +177,7 @@ typedef struct BasControlT {
// ============================================================
typedef struct BasFormT {
- char name[BAS_MAX_FORM_NAME]; // form name (e.g. "Form1")
+ char name[BAS_MAX_IDENT]; // form name (e.g. "Form1")
WindowT *window; // DVX window
WidgetT *root; // widget root (from wgtInitWindow)
WidgetT *contentBox; // VBox/HBox for user controls
@@ -134,7 +195,7 @@ typedef struct BasFormT {
bool frmCentered;
bool frmAutoSize;
char frmLayout[32]; // "VBox", "HBox", or "WrapBox"
- char helpTopic[BAS_MAX_CTRL_NAME]; // form-level help topic
+ char helpTopic[BAS_MAX_IDENT]; // form-level help topic
// Per-form variable storage (allocated at load, freed at unload)
BasValueT *formVars;
int32_t formVarCount;
@@ -168,29 +229,20 @@ typedef struct BasFormT {
// Cached .frm source for reload after unload
typedef struct {
- char formName[BAS_MAX_FORM_NAME];
+ char formName[BAS_MAX_IDENT];
char *frmSource; // malloc'd copy of .frm text
int32_t frmSourceLen;
} BasFrmCacheT;
-// Cached compiled form binary (for standalone apps)
-typedef struct {
- char formName[BAS_MAX_FORM_NAME];
- uint8_t *data; // malloc'd binary data
- int32_t dataLen;
-} BasCfmCacheT;
-
typedef struct {
AppContextT *ctx; // DVX app context
BasVmT *vm; // shared VM instance
BasModuleT *module; // compiled module
BasFormT **forms; // stb_ds array of heap-allocated pointers
BasFormT *currentForm; // form currently dispatching events
- char helpFile[256]; // project help file path (for F1)
+ char helpFile[DVX_MAX_PATH]; // project help file path (for F1)
BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources
int32_t frmCacheCount;
- BasCfmCacheT *cfmCache; // stb_ds array of compiled form binaries
- int32_t cfmCacheCount;
// Set true when a runtime error has halted the program; the event
// loop exits at the next pump so the app doesn't stumble forward
// with a halted VM.
@@ -221,7 +273,7 @@ typedef struct {
// ctrl we'd otherwise have no way to tell the user WHICH control
// was missing. This is the last requested name; if the lookup
// succeeded this value is still the name of that control.
- char lastLookupName[BAS_MAX_CTRL_NAME];
+ char lastLookupName[BAS_MAX_IDENT];
} BasFormRtT;
// ============================================================
@@ -231,8 +283,8 @@ typedef struct {
// Initialize the form runtime with a DVX context and a compiled module.
BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module);
-// Load all cached forms (.frm text and compiled binaries) and show the
-// startup form. Called before bytecode execution begins.
+// Load all cached .frm forms and show the startup form. Called before
+// bytecode execution begins.
void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName);
// VB-style event loop: pump DVX events until all forms are closed.
@@ -277,9 +329,6 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha
// Register .frm source text for lazy loading when bytecode calls Load.
void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen);
-// Register a compiled form binary for lazy loading when bytecode calls Load.
-void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *data, int32_t dataLen);
-
// ---- Widget creation ----
// Create a widget by resolved (DVX) type name. Returns NULL if the type
diff --git a/src/apps/kpunch/dvxbasic/formrt/frmParser.c b/src/apps/kpunch/dvxbasic/formrt/frmParser.c
index 1ee3b9e..98929f3 100644
--- a/src/apps/kpunch/dvxbasic/formrt/frmParser.c
+++ b/src/apps/kpunch/dvxbasic/formrt/frmParser.c
@@ -31,15 +31,13 @@
#include
#include
-#define FRM_MAX_LINE_LEN 512
-#define FRM_MAX_TOKEN_LEN 64
-#define FRM_MAX_NESTING 16
#define FRM_MAX_VB_VERSION 2.0 // highest VB form VERSION we can import (VB4+ rejected)
// Prototypes (alphabetical)
bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb);
bool frmParseBool(const char *val);
+bool frmParseBoolDefault(const char *val, bool defaultValue);
void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax);
void frmStripQuotes(char *val);
static bool readToken(const char **p, char *buf, int32_t bufMax);
@@ -120,17 +118,23 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
BlkTypeE blkType;
bool doPush = false;
- readToken(&rest, typeName, FRM_MAX_TOKEN_LEN);
- rest = dvxSkipWs(rest);
- readToken(&rest, ctrlName, FRM_MAX_TOKEN_LEN);
-
- if (typeName[0] == '\0') {
+ if (!readToken(&rest, typeName, FRM_MAX_TOKEN_LEN)) {
continue;
}
+ rest = dvxSkipWs(rest);
+ readToken(&rest, ctrlName, FRM_MAX_TOKEN_LEN);
+
// Classify the block first so overflow is checked once and all
// begin side-effects stay in lockstep with the stack push.
if (strcasecmp(typeName, "Form") == 0) {
+ // A second Begin Form inside an open form is malformed: its
+ // End would otherwise terminate the whole file early and
+ // silently drop every control after it.
+ if (inForm) {
+ return false;
+ }
+
blkType = BLK_FORM;
doPush = true;
} else if (strcasecmp(typeName, "Menu") == 0 && inForm) {
@@ -249,11 +253,24 @@ bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
bool frmParseBool(const char *val) {
+ return frmParseBoolDefault(val, false);
+}
+
+
+bool frmParseBoolDefault(const char *val, bool defaultValue) {
if (!val) {
+ return defaultValue;
+ }
+
+ if (strcasecmp(val, "True") == 0 || strcmp(val, "-1") == 0) {
+ return true;
+ }
+
+ if (strcasecmp(val, "False") == 0 || strcmp(val, "0") == 0) {
return false;
}
- return (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0);
+ return defaultValue;
}
@@ -312,13 +329,19 @@ void frmStripQuotes(char *val) {
// Read a whitespace-delimited token starting at *p. Advances *p past
-// the token.
+// the WHOLE token even when it overflows buf (the stored copy is
+// truncated), so an over-long type name never bleeds into the control
+// name read next. Returns false if no token was present.
static bool readToken(const char **p, char *buf, int32_t bufMax) {
const char *cur = *p;
int32_t len = 0;
- while (*cur && *cur != ' ' && *cur != '\t' && *cur != '\r' && *cur != '\n' && len < bufMax - 1) {
- buf[len++] = *cur++;
+ while (*cur && *cur != ' ' && *cur != '\t' && *cur != '\r' && *cur != '\n') {
+ if (len < bufMax - 1) {
+ buf[len++] = *cur;
+ }
+
+ cur++;
}
buf[len] = '\0';
diff --git a/src/apps/kpunch/dvxbasic/formrt/frmParser.h b/src/apps/kpunch/dvxbasic/formrt/frmParser.h
index b7d00b2..c146893 100644
--- a/src/apps/kpunch/dvxbasic/formrt/frmParser.h
+++ b/src/apps/kpunch/dvxbasic/formrt/frmParser.h
@@ -35,6 +35,12 @@
#include
#include
+// Parser limits. Consumers size their own line/name buffers and
+// nesting stacks from these so the two sides can never disagree.
+#define FRM_MAX_LINE_LEN 512 // longest "Key = Value" line, including the value
+#define FRM_MAX_TOKEN_LEN 64 // longest Begin-line token or property key
+#define FRM_MAX_NESTING 16 // deepest Begin/End block nesting
+
// Callbacks supplied by the consumer. Any field may be NULL.
typedef struct FrmParserCbsT {
void *userData;
@@ -85,4 +91,9 @@ void frmStripQuotes(char *val);
// anything else -> false.
bool frmParseBool(const char *val);
+// Classify a value string as a BASIC boolean with an explicit default:
+// True / -1 -> true, False / 0 -> false, anything else -> defaultValue.
+// Use for properties whose unset state is true (menu Enabled/Visible).
+bool frmParseBoolDefault(const char *val, bool defaultValue);
+
#endif // DVXBASIC_FRMPARSER_H
diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
index e814c8d..73a6bbb 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
@@ -34,7 +34,6 @@
#include "dvxWm.h"
#include "stb_ds_wrap.h"
-#include
#include
#include
#include
@@ -45,61 +44,123 @@
// Constants
// ============================================================
-#define DEFAULT_FORM_W 400
-#define DEFAULT_FORM_H 300
-#define DEFAULT_CTRL_W 100
-#define DEFAULT_CTRL_H 30
-#define MIN_CTRL_SIZE 8
-#define DSGN_INDENT_SPACES 4
-#define DSGN_MAX_INDENT_BUF 32
-#define DSGN_MAX_NEST_DEPTH 32
+#define DEFAULT_CTRL_W 100
+#define DEFAULT_CTRL_H 30
+#define MIN_CTRL_SIZE 8
+#define DSGN_INDENT_SPACES 4
+#define DSGN_MAX_INDENT_BUF 32
#define DSGN_SEL_HANDLE_COUNT 8 // resize handles drawn around a selected control
+#define DSGN_NEWLINE_ROOM 2 // "\n" before and after the code section
+// Names of the .frm keys handled outside the integer table.
+#define DSGN_KEY_CAPTION "Caption"
+#define DSGN_KEY_TEXT "Text"
+#define DSGN_KEY_LAYOUT "Layout"
+#define DSGN_KEY_VISIBLE "Visible"
+#define DSGN_KEY_ENABLED "Enabled"
+#define DSGN_KEY_NAME "Name"
+#define DSGN_KEY_LEFT "Left"
+#define DSGN_KEY_TOP "Top"
+#define DSGN_KEY_WIDTH "Width"
+#define DSGN_KEY_HEIGHT "Height"
+#define DSGN_KEY_AUTOSIZE "AutoSize"
+#define DSGN_KEY_RESIZABLE "Resizable"
+#define DSGN_KEY_CENTERED "Centered"
+#define DSGN_KEY_HELPTOPIC "HelpTopic"
-// ============================================================
-// Default event for the Form type (not a widget, so not in iface)
-// ============================================================
-
+// The Form type is not a widget, so it has no interface default event.
static const char *FORM_DEFAULT_EVENT = "Load";
+// ============================================================
+// Built-in integer control properties (see DsgnIntPropT)
+// ============================================================
+
+static const DsgnIntPropT sIntProps[] = {
+ { "Left", NULL, offsetof(DsgnControlT, left), DSGN_SAVE_IF_NONZERO },
+ { "Top", NULL, offsetof(DsgnControlT, top), DSGN_SAVE_IF_NONZERO },
+ { "MinWidth", "Width", offsetof(DsgnControlT, width), DSGN_SAVE_ALWAYS },
+ { "MinHeight", "Height", offsetof(DsgnControlT, height), DSGN_SAVE_ALWAYS },
+ { "MaxWidth", NULL, offsetof(DsgnControlT, maxWidth), DSGN_SAVE_IF_POSITIVE },
+ { "MaxHeight", NULL, offsetof(DsgnControlT, maxHeight), DSGN_SAVE_IF_POSITIVE },
+ { "Weight", NULL, offsetof(DsgnControlT, weight), DSGN_SAVE_IF_POSITIVE }
+};
+
+#define DSGN_INT_PROP_COUNT ((int32_t)(sizeof(sIntProps) / sizeof(sIntProps[0])))
+
// ============================================================
// Prototypes
// ============================================================
-// dsgnCreateDesignWidget is declared in ideDesigner.h (non-static)
-static void dsgnLoad_onCtrlBegin(void *userData, const char *typeName, const char *name);
-static void dsgnLoad_onCtrlEnd(void *userData);
-static void dsgnLoad_onCtrlProp(void *userData, const char *key, const char *value);
-static bool dsgnLoad_onFormBegin(void *userData, const char *name);
-static void dsgnLoad_onFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen);
-static void dsgnLoad_onFormProp(void *userData, const char *key, const char *value);
-static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level);
-static void dsgnLoad_onMenuEnd(void *userData);
-static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *value);
+static WidgetT *containerChildParent(DsgnControlT *pc);
+static int32_t emitControlHead(const DsgnControlT *ctrl, char *buf, int32_t bufSize, int32_t pos, const char *pad);
static int32_t emitPad(char *buf, int32_t bufSize, int32_t pos, int32_t count);
+static int32_t emitPropLine(char *buf, int32_t bufSize, int32_t pos, const char *pad, const char *name, uint8_t type, const char *value);
static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y);
static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y);
-static void rebuildWidgets(DsgnStateT *ds);
+static int32_t intPropGet(const DsgnControlT *ctrl, const DsgnIntPropT *ip);
+static void intPropSet(DsgnControlT *ctrl, const DsgnIntPropT *ip, int32_t val);
+static void loadOnCtrlBegin(void *userData, const char *typeName, const char *name);
+static void loadOnCtrlEnd(void *userData);
+static void loadOnCtrlProp(void *userData, const char *key, const char *value);
+static bool loadOnFormBegin(void *userData, const char *name);
+static void loadOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen);
+static void loadOnFormProp(void *userData, const char *key, const char *value);
+static void loadOnMenuBegin(void *userData, const char *name, int32_t level);
+static void loadOnMenuEnd(void *userData);
+static void loadOnMenuProp(void *userData, const char *key, const char *value);
+static DsgnFormT *newFormDefaults(const char *name, bool autoSize);
static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, int32_t pos, const char *parentName, int32_t indent);
-static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value);
-static void syncWidgetGeom(DsgnControlT *ctrl);
+static void trySwapNeighbor(DsgnStateT *ds, int32_t y, int32_t dir);
// ============================================================
-// .frm parsing context (used by dsgnLoad_* callbacks)
+// .frm parsing context (used by the loadOn* callbacks)
// ============================================================
+//
+// parentStack mirrors the parser's Begin/End block nesting: every control
+// block pushes its name, so a child's parentName is always the enclosing
+// block whether or not that type's widget is loaded. nestDepth keeps
+// counting past FRM_MAX_NESTING so pushes and pops stay paired; only
+// the stack writes and reads are bounded.
typedef struct {
DsgnFormT *form;
DsgnControlT *current;
int32_t curMenuItemIdx;
- char parentStack[BAS_MAX_FRM_NESTING][DSGN_MAX_NAME];
+ char parentStack[FRM_MAX_NESTING][DSGN_MAX_NAME];
int32_t nestDepth;
- bool containerStack[BAS_MAX_FRM_NESTING];
- int32_t containerDepth;
} DsgnFrmLoadCtxT;
+// Widget that a container's children attach to. Containers with a
+// non-VBox Layout property nest their children inside a content box tagged
+// with the control pointer so it is created exactly once.
+static WidgetT *containerChildParent(DsgnControlT *pc) {
+ WidgetT *parent = pc->widget;
+ const char *layout = dsgnControlGetPropValue(pc, DSGN_KEY_LAYOUT);
+
+ if (!layout || !layout[0] || strcasecmp(layout, DSGN_VBOX_LAYOUT) == 0) {
+ return parent;
+ }
+
+ // pc is non-NULL, so a NULL firstChild->userData also fails the == pc
+ // test -- no separate NULL check needed.
+ if (parent->firstChild && parent->firstChild->userData == (void *)pc) {
+ return parent->firstChild;
+ }
+
+ WidgetT *box = basFormRtCreateContentBox(parent, layout);
+
+ // basFormRtCreateContentBox returns the container itself when the
+ // layout type is not a loaded parent container; only tag a real box.
+ if (box != parent) {
+ box->userData = (void *)pc;
+ }
+
+ return box;
+}
+
+
void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize) {
// Look up the name prefix from the widget interface descriptor.
// Falls back to the type name itself if no prefix is registered.
@@ -154,7 +215,7 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) {
for (int32_t i = 0; i < menuCount; i++) {
const DsgnMenuItemT *mi = &form->menuItems[i];
- bool isSep = (mi->caption[0] == '-');
+ bool isSep = (mi->caption[0] == '-');
bool isSubParent = (i + 1 < menuCount && form->menuItems[i + 1].level > mi->level);
if (mi->level == 0) {
@@ -169,19 +230,29 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) {
for (int32_t d = 1; d < DSGN_MENU_STACK_DEPTH; d++) {
menuStack[d] = NULL;
}
- } else if (isSep && mi->level > 0 && mi->level < DSGN_MENU_STACK_DEPTH && menuStack[mi->level - 1]) {
- wmAddMenuSeparator(menuStack[mi->level - 1]);
- } else if (isSubParent && mi->level > 0 && mi->level < DSGN_MENU_STACK_DEPTH && menuStack[mi->level - 1]) {
- menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
- } else if (mi->level > 0 && mi->level < DSGN_MENU_STACK_DEPTH && menuStack[mi->level - 1]) {
+
+ continue;
+ }
+
+ MenuT *parentMenu = (mi->level > 0 && mi->level < DSGN_MENU_STACK_DEPTH) ? menuStack[mi->level - 1] : NULL;
+
+ if (!parentMenu) {
+ continue;
+ }
+
+ if (isSep) {
+ wmAddMenuSeparator(parentMenu);
+ } else if (isSubParent) {
+ menuStack[mi->level] = wmAddSubMenu(parentMenu, mi->caption);
+ } else {
int32_t id = DSGN_MENU_ID_BASE + i;
if (mi->radioCheck) {
- wmAddMenuRadioItem(menuStack[mi->level - 1], mi->caption, id, mi->checked);
+ wmAddMenuRadioItem(parentMenu, mi->caption, id, mi->checked);
} else if (mi->checked) {
- wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true);
+ wmAddMenuCheckItem(parentMenu, mi->caption, id, true);
} else {
- wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id);
+ wmAddMenuItem(parentMenu, mi->caption, id);
}
}
}
@@ -199,13 +270,6 @@ const char *dsgnControlGetPropValue(const DsgnControlT *ctrl, const char *name)
}
-// dsgnCreateFormWindow / dsgnCreateContentBox
-// Thin wrappers around formrt functions for backward compatibility.
-WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout) {
- return basFormRtCreateContentBox(root, layout);
-}
-
-
// Create a real DVX widget for design-time display. Uses the shared
// createWidgetByIface switch from formrt; design-time refuses
// WGT_CREATE_PARENT_DATA widgets (Image/ImageButton) since we have no
@@ -227,11 +291,9 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) {
}
-WindowT *dsgnCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox) {
- return basFormRtCreateFormWindow(ctx, title, layout, resizable, centered, autoSize, width, height, left, top, outRoot, outContentBox);
-}
-
-
+// Single pass in controls[] order: a container always precedes its children
+// (the loader, the tree reorder, and the sibling-only canvas reorder all
+// preserve that), so each child finds its parent's widget already created.
void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
if (!ds->form || !contentBox) {
return;
@@ -240,9 +302,6 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
ds->form->contentBox = contentBox;
int32_t count = (int32_t)arrlen(ds->form->controls);
- // Two passes: first create all controls (so containers exist),
- // then parent children inside their containers.
- // Pass 1: create all widgets as top-level children
for (int32_t i = 0; i < count; i++) {
DsgnControlT *ctrl = ds->form->controls[i];
@@ -250,39 +309,13 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
continue;
}
- // Find the parent widget. For containers with a non-VBox Layout
- // property, create a content box inside so children use the
- // correct layout direction.
WidgetT *parent = contentBox;
if (ctrl->parentName[0]) {
for (int32_t j = 0; j < count; j++) {
if (j != i && ds->form->controls[j]->widget &&
strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0) {
- DsgnControlT *pc = ds->form->controls[j];
- parent = pc->widget;
- const char *layout = dsgnControlGetPropValue(pc, "Layout");
-
- if (layout && layout[0] && strcasecmp(layout, "VBox") != 0) {
- // Check if we already created a content box inside.
- // pc is non-NULL, so a NULL firstChild->userData also
- // fails the == pc test -- no separate NULL check needed.
- if (parent->firstChild &&
- parent->firstChild->userData == (void *)pc) {
- parent = parent->firstChild;
- } else {
- WidgetT *box = dsgnCreateContentBox(parent, layout);
-
- // dsgnCreateContentBox returns the container itself
- // when the layout type is not a loaded parent
- // container; only tag and descend into a real box.
- if (box != parent) {
- box->userData = (void *)pc;
- parent = box;
- }
- }
- }
-
+ parent = containerChildParent(ds->form->controls[j]);
break;
}
}
@@ -298,31 +331,18 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
wgtSetName(w, ctrl->name);
// Set Caption/Text
- const char *caption = dsgnControlGetPropValue(ctrl, "Caption");
- const char *text = dsgnControlGetPropValue(ctrl, "Text");
+ const char *caption = dsgnControlGetPropValue(ctrl, DSGN_KEY_CAPTION);
+ const char *text = dsgnControlGetPropValue(ctrl, DSGN_KEY_TEXT);
- if (caption) { wgtSetText(w, caption); }
- if (text) { wgtSetText(w, text); }
-
- // Set size hints for the layout engine.
- // minW/minH set the floor; maxW/maxH cap the size.
- if (ctrl->width > 0) {
- w->minW = wgtPixels(ctrl->width);
+ if (caption) {
+ wgtSetText(w, caption);
}
- if (ctrl->height > 0) {
- w->minH = wgtPixels(ctrl->height);
+ if (text) {
+ wgtSetText(w, text);
}
- if (ctrl->maxWidth > 0) {
- w->maxW = wgtPixels(ctrl->maxWidth);
- }
-
- if (ctrl->maxHeight > 0) {
- w->maxH = wgtPixels(ctrl->maxHeight);
- }
-
- w->weight = ctrl->weight;
+ dsgnSyncWidgetGeom(ctrl);
// Do not apply ctrl->visible here: hidden widgets get no layout
// geometry, which would make a Visible=False control invisible
@@ -372,6 +392,91 @@ const char *dsgnDefaultEvent(const char *typeName) {
}
+int32_t dsgnEmitControl(const DsgnControlT *ctrl, char *buf, int32_t bufSize, int32_t pos, int32_t indent) {
+ char pad[DSGN_MAX_INDENT_BUF];
+ int32_t padLen = indent * DSGN_INDENT_SPACES;
+
+ if (padLen > DSGN_MAX_INDENT_BUF - 1) {
+ padLen = DSGN_MAX_INDENT_BUF - 1;
+ }
+
+ memset(pad, ' ', padLen);
+ pad[padLen] = '\0';
+
+ pos = emitControlHead(ctrl, buf, bufSize, pos, pad);
+ return emitClamped(buf, bufSize, pos, "%sEnd\n", pad);
+}
+
+
+const DsgnIntPropT *dsgnFindIntProp(const char *name) {
+ for (int32_t i = 0; i < DSGN_INT_PROP_COUNT; i++) {
+ const DsgnIntPropT *ip = &sIntProps[i];
+
+ if (strcasecmp(ip->name, name) == 0 || (ip->alias && strcasecmp(ip->alias, name) == 0)) {
+ return ip;
+ }
+ }
+
+ return NULL;
+}
+
+
+// Text value of one runtime form property as the designer stores it.
+// Returns false for properties the designer has no field for (Visible,
+// ContextMenu: runtime-only), so both the .frm writer and the property
+// grid can walk basFormRtFormProps() and skip those.
+bool dsgnFormPropValue(const DsgnFormT *form, const char *name, char *out, int32_t outSize) {
+ const char *text = NULL;
+ int32_t num = 0;
+ bool flag = false;
+ bool isNum = false;
+ bool isFlag = false;
+
+ if (strcasecmp(name, DSGN_KEY_NAME) == 0) {
+ text = form->name;
+ } else if (strcasecmp(name, DSGN_KEY_CAPTION) == 0) {
+ text = form->caption;
+ } else if (strcasecmp(name, DSGN_KEY_LAYOUT) == 0) {
+ text = form->layout;
+ } else if (strcasecmp(name, DSGN_KEY_HELPTOPIC) == 0) {
+ text = form->helpTopic;
+ } else if (strcasecmp(name, DSGN_KEY_LEFT) == 0) {
+ num = form->left;
+ isNum = true;
+ } else if (strcasecmp(name, DSGN_KEY_TOP) == 0) {
+ num = form->top;
+ isNum = true;
+ } else if (strcasecmp(name, DSGN_KEY_WIDTH) == 0) {
+ num = form->width;
+ isNum = true;
+ } else if (strcasecmp(name, DSGN_KEY_HEIGHT) == 0) {
+ num = form->height;
+ isNum = true;
+ } else if (strcasecmp(name, DSGN_KEY_AUTOSIZE) == 0) {
+ flag = form->autoSize;
+ isFlag = true;
+ } else if (strcasecmp(name, DSGN_KEY_RESIZABLE) == 0) {
+ flag = form->resizable;
+ isFlag = true;
+ } else if (strcasecmp(name, DSGN_KEY_CENTERED) == 0) {
+ flag = form->centered;
+ isFlag = true;
+ }
+
+ if (text) {
+ snprintf(out, outSize, "%s", text);
+ } else if (isNum) {
+ snprintf(out, outSize, "%d", (int)num);
+ } else if (isFlag) {
+ snprintf(out, outSize, "%s", flag ? "True" : "False");
+ } else {
+ return false;
+ }
+
+ return true;
+}
+
+
void dsgnFree(DsgnStateT *ds) {
if (ds->form) {
for (int32_t i = 0; i < arrlen(ds->form->controls); i++) {
@@ -396,16 +501,35 @@ bool dsgnIfaceHasProp(const char *typeName, const char *propName) {
}
+bool dsgnIfacePropValue(const DsgnControlT *ctrl, const WgtPropDescT *p, char *out, int32_t outSize) {
+ const char *stored = dsgnControlGetPropValue(ctrl, p->name);
+
+ if (stored) {
+ snprintf(out, outSize, "%s", stored);
+ return true;
+ }
+
+ return wgtPropValueToString(ctrl->widget, p, out, outSize);
+}
+
+
void dsgnInit(DsgnStateT *ds, AppContextT *ctx) {
memset(ds, 0, sizeof(*ds));
- ds->selectedIdx = -1;
- ds->activeTool[0] = '\0';
- ds->mode = DSGN_IDLE;
+ ds->selectedIdx = -1;
ds->activeHandle = HANDLE_NONE;
ds->ctx = ctx;
}
+const DsgnIntPropT *dsgnIntPropAt(int32_t idx) {
+ if (idx < 0 || idx >= DSGN_INT_PROP_COUNT) {
+ return NULL;
+ }
+
+ return &sIntProps[idx];
+}
+
+
bool dsgnIsContainer(const char *typeName) {
const char *wgtName = wgtFindByBasName(typeName);
@@ -428,24 +552,12 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
dsgnFree(ds);
- DsgnFormT *form = (DsgnFormT *)calloc(1, sizeof(DsgnFormT));
+ DsgnFormT *form = newFormDefaults("Form1", true);
if (!form) {
return false;
}
- form->controls = NULL;
- form->width = DEFAULT_FORM_W;
- form->height = DEFAULT_FORM_H;
- form->left = 0;
- form->top = 0;
- snprintf(form->layout, DSGN_MAX_NAME, "VBox");
- form->centered = true;
- form->autoSize = true;
- form->resizable = true;
- snprintf(form->name, DSGN_MAX_NAME, "Form1");
- snprintf(form->caption, DSGN_MAX_TEXT, "Form1");
-
DsgnFrmLoadCtxT ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.form = form;
@@ -454,305 +566,67 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
FrmParserCbsT cbs;
memset(&cbs, 0, sizeof(cbs));
cbs.userData = &ctx;
- cbs.onFormBegin = dsgnLoad_onFormBegin;
- cbs.onFormProp = dsgnLoad_onFormProp;
- cbs.onFormEnd = dsgnLoad_onFormEnd;
- cbs.onMenuBegin = dsgnLoad_onMenuBegin;
- cbs.onMenuEnd = dsgnLoad_onMenuEnd;
- cbs.onMenuProp = dsgnLoad_onMenuProp;
- cbs.onCtrlBegin = dsgnLoad_onCtrlBegin;
- cbs.onCtrlEnd = dsgnLoad_onCtrlEnd;
- cbs.onCtrlProp = dsgnLoad_onCtrlProp;
-
- if (!frmParse(source, sourceLen, &cbs)) {
- // The callbacks may already have allocated controls/menuItems/code
- // before a mid-stream failure; mirror dsgnFree's teardown so a bare
- // free(form) doesn't leak them.
- for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
- free(form->controls[i]);
- }
- arrfree(form->controls);
- arrfree(form->menuItems);
- free(form->code);
- free(form);
- return false;
- }
+ cbs.onFormBegin = loadOnFormBegin;
+ cbs.onFormProp = loadOnFormProp;
+ cbs.onFormEnd = loadOnFormEnd;
+ cbs.onMenuBegin = loadOnMenuBegin;
+ cbs.onMenuEnd = loadOnMenuEnd;
+ cbs.onMenuProp = loadOnMenuProp;
+ cbs.onCtrlBegin = loadOnCtrlBegin;
+ cbs.onCtrlEnd = loadOnCtrlEnd;
+ cbs.onCtrlProp = loadOnCtrlProp;
ds->form = form;
ds->selectedIdx = -1;
- return true;
-}
-
-static void dsgnLoad_onCtrlBegin(void *userData, const char *typeName, const char *name) {
- DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
-
- if (!ctx->form) {
- return;
- }
-
- DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
-
- if (!cp) {
- if (ctx->containerDepth < BAS_MAX_FRM_NESTING) {
- ctx->containerStack[ctx->containerDepth++] = false;
- }
- return;
- }
-
- cp->index = -1;
- cp->visible = true;
- cp->enabled = true;
- snprintf(cp->name, DSGN_MAX_NAME, "%s", name);
- snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
-
- if (ctx->nestDepth > 0) {
- snprintf(cp->parentName, DSGN_MAX_NAME, "%s", ctx->parentStack[ctx->nestDepth - 1]);
- }
-
- cp->width = DEFAULT_CTRL_W;
- cp->height = DEFAULT_CTRL_H;
- arrput(ctx->form->controls, cp);
- ctx->current = ctx->form->controls[arrlen(ctx->form->controls) - 1];
-
- bool isCtrl = dsgnIsContainer(typeName);
-
- if (ctx->containerDepth < BAS_MAX_FRM_NESTING) {
- ctx->containerStack[ctx->containerDepth++] = isCtrl;
- }
-
- // Guard against parentStack's own bound (now BAS_MAX_FRM_NESTING) so it
- // stays in step with containerStack; a smaller limit silently skipped the
- // push for deep nesting and desynced nestDepth on the matching CtrlEnd.
- if (isCtrl && ctx->nestDepth < BAS_MAX_FRM_NESTING - 1) {
- snprintf(ctx->parentStack[ctx->nestDepth], DSGN_MAX_NAME, "%s", name);
- ctx->nestDepth++;
- }
-}
-
-
-static void dsgnLoad_onCtrlEnd(void *userData) {
- DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
-
- if (ctx->containerDepth > 0) {
- ctx->containerDepth--;
-
- if (ctx->containerStack[ctx->containerDepth] && ctx->nestDepth > 0) {
- ctx->nestDepth--;
- }
- }
-
- ctx->current = NULL;
-}
-
-
-static void dsgnLoad_onCtrlProp(void *userData, const char *key, const char *value) {
- DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
-
- if (!ctx->current) {
- return;
- }
-
- char val[DSGN_MAX_TEXT];
- snprintf(val, sizeof(val), "%s", value);
- frmStripQuotes(val);
-
- DsgnControlT *cc = ctx->current;
-
- if (strcasecmp(key, "Left") == 0) {
- cc->left = atoi(val);
- } else if (strcasecmp(key, "Top") == 0) {
- cc->top = atoi(val);
- } else if (strcasecmp(key, "MinWidth") == 0 || strcasecmp(key, "Width") == 0) {
- cc->width = atoi(val);
- } else if (strcasecmp(key, "MinHeight") == 0 || strcasecmp(key, "Height") == 0) {
- cc->height = atoi(val);
- } else if (strcasecmp(key, "MaxWidth") == 0) {
- cc->maxWidth = atoi(val);
- } else if (strcasecmp(key, "MaxHeight") == 0) {
- cc->maxHeight = atoi(val);
- } else if (strcasecmp(key, "Weight") == 0) {
- cc->weight = atoi(val);
- } else if (strcasecmp(key, "Index") == 0) {
- cc->index = atoi(val);
- } else if (strcasecmp(key, "HelpTopic") == 0) {
- snprintf(cc->helpTopic, DSGN_MAX_NAME, "%s", val);
- } else if (strcasecmp(key, "Visible") == 0 && !dsgnIfaceHasProp(cc->typeName, "Visible")) {
- cc->visible = frmParseBool(val);
- } else if (strcasecmp(key, "Enabled") == 0 && !dsgnIfaceHasProp(cc->typeName, "Enabled")) {
- cc->enabled = frmParseBool(val);
- } else if (strcasecmp(key, "TabIndex") == 0) {
- // ignored -- DVX has no tab order
- } else {
- setPropValue(cc, key, val);
- }
-}
-
-
-static bool dsgnLoad_onFormBegin(void *userData, const char *name) {
- DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
-
- if (!ctx->form) {
+ if (!frmParse(source, sourceLen, &cbs)) {
+ // The callbacks may already have allocated controls/menuItems/code
+ // before a mid-stream failure; dsgnFree tears all of it down.
+ dsgnFree(ds);
return false;
}
- snprintf(ctx->form->name, DSGN_MAX_NAME, "%s", name);
- snprintf(ctx->form->caption, DSGN_MAX_TEXT, "%s", name);
- ctx->current = NULL;
- ctx->nestDepth = 0;
return true;
}
-static void dsgnLoad_onFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen) {
- DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
-
- if (!ctx->form || trailingLen <= 0) {
- return;
- }
-
- // Skip leading whitespace/blank lines
- const char *codeStart = trailingSrc;
- const char *codeEnd = trailingSrc + trailingLen;
-
- while (codeStart < codeEnd && (*codeStart == '\r' || *codeStart == '\n' || *codeStart == ' ' || *codeStart == '\t')) {
- codeStart++;
- }
-
- if (codeStart >= codeEnd) {
- return;
- }
-
- int32_t codeLen = (int32_t)(codeEnd - codeStart);
- ctx->form->code = (char *)malloc(codeLen + 1);
-
- if (ctx->form->code) {
- memcpy(ctx->form->code, codeStart, codeLen);
- ctx->form->code[codeLen] = '\0';
- }
+void dsgnMenuItemInit(DsgnMenuItemT *mi) {
+ memset(mi, 0, sizeof(*mi));
+ mi->enabled = true;
+ mi->visible = true;
}
-static void dsgnLoad_onFormProp(void *userData, const char *key, const char *value) {
- DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
-
- if (!ctx->form) {
- return;
+bool dsgnNameInUse(const DsgnFormT *form, const char *name, const char *exceptName, bool checkMenus) {
+ if (exceptName && strcasecmp(name, exceptName) == 0) {
+ return false;
}
- char val[DSGN_MAX_TEXT];
- snprintf(val, sizeof(val), "%s", value);
- frmStripQuotes(val);
-
- DsgnFormT *ff = ctx->form;
-
- if (strcasecmp(key, "Caption") == 0) {
- snprintf(ff->caption, DSGN_MAX_TEXT, "%s", val);
- } else if (strcasecmp(key, "Layout") == 0) {
- strncpy(ff->layout, val, DSGN_MAX_NAME - 1);
- ff->layout[DSGN_MAX_NAME - 1] = '\0';
- } else if (strcasecmp(key, "AutoSize") == 0) {
- ff->autoSize = frmParseBool(val);
- } else if (strcasecmp(key, "Resizable") == 0) {
- ff->resizable = frmParseBool(val);
- } else if (strcasecmp(key, "Centered") == 0) {
- ff->centered = frmParseBool(val);
- } else if (strcasecmp(key, "Left") == 0) {
- ff->left = atoi(val);
- } else if (strcasecmp(key, "Top") == 0) {
- ff->top = atoi(val);
- } else if (strcasecmp(key, "Width") == 0) {
- ff->width = atoi(val);
- ff->autoSize = false;
- } else if (strcasecmp(key, "Height") == 0) {
- ff->height = atoi(val);
- ff->autoSize = false;
- } else if (strcasecmp(key, "HelpTopic") == 0) {
- snprintf(ff->helpTopic, DSGN_MAX_NAME, "%s", val);
- }
-}
-
-
-static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level) {
- DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
-
- if (!ctx->form) {
- return;
+ if (strcasecmp(form->name, name) == 0) {
+ return true;
}
- DsgnMenuItemT mi;
- memset(&mi, 0, sizeof(mi));
- snprintf(mi.name, DSGN_MAX_NAME, "%s", name);
- mi.level = level;
- mi.enabled = true;
- mi.visible = true;
- arrput(ctx->form->menuItems, mi);
- ctx->curMenuItemIdx = (int32_t)arrlen(ctx->form->menuItems) - 1;
- ctx->current = NULL;
-}
-
-
-static void dsgnLoad_onMenuEnd(void *userData) {
- DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
-
- ctx->curMenuItemIdx = -1;
-}
-
-
-static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *value) {
- DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
-
- if (!ctx->form ||
- ctx->curMenuItemIdx < 0 ||
- ctx->curMenuItemIdx >= (int32_t)arrlen(ctx->form->menuItems)) {
- return;
+ for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
+ if (strcasecmp(form->controls[i]->name, name) == 0) {
+ return true;
+ }
}
- // Resolve pointer fresh each write -- arrput on nested menus may
- // have reallocated the array.
- DsgnMenuItemT *mip = &ctx->form->menuItems[ctx->curMenuItemIdx];
- char val[DSGN_MAX_TEXT];
- snprintf(val, sizeof(val), "%s", value);
- frmStripQuotes(val);
-
- if (strcasecmp(key, "Caption") == 0) {
- snprintf(mip->caption, DSGN_MAX_TEXT, "%s", val);
- } else if (strcasecmp(key, "Checked") == 0) {
- mip->checked = frmParseBool(val);
- } else if (strcasecmp(key, "RadioCheck") == 0) {
- mip->radioCheck = frmParseBool(val);
- } else if (strcasecmp(key, "Enabled") == 0) {
- mip->enabled = frmParseBool(val);
- } else if (strcasecmp(key, "Visible") == 0) {
- mip->visible = (strcasecmp(val, "False") != 0);
+ if (checkMenus) {
+ for (int32_t i = 0; i < (int32_t)arrlen(form->menuItems); i++) {
+ if (strcasecmp(form->menuItems[i].name, name) == 0) {
+ return true;
+ }
+ }
}
+
+ return false;
}
void dsgnNewForm(DsgnStateT *ds, const char *name) {
dsgnFree(ds);
-
- DsgnFormT *form = (DsgnFormT *)calloc(1, sizeof(DsgnFormT));
-
- if (!form) {
- ds->form = NULL;
- ds->selectedIdx = -1;
- return;
- }
-
- form->controls = NULL;
- form->width = DEFAULT_FORM_W;
- form->height = DEFAULT_FORM_H;
- form->left = 0;
- form->top = 0;
- snprintf(form->layout, DSGN_MAX_NAME, "VBox");
- form->centered = true;
- form->autoSize = false;
- form->resizable = true;
- snprintf(form->name, DSGN_MAX_NAME, "%s", name);
- snprintf(form->caption, DSGN_MAX_TEXT, "%s", name);
-
- ds->form = form;
+ ds->form = newFormDefaults(name, false);
ds->selectedIdx = -1;
}
@@ -844,8 +718,8 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
}
}
- // Delete every flagged control, highest index first so earlier indices
- // stay valid, adjusting the selection for each removal below it.
+ // Delete every flagged control, highest index first so earlier
+ // indices stay valid.
for (int32_t i = count - 1; i >= 0; i--) {
bool remove = false;
@@ -859,10 +733,6 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
if (remove) {
free(ds->form->controls[i]);
arrdel(ds->form->controls, i);
-
- if (i < ds->selectedIdx) {
- ds->selectedIdx--;
- }
}
}
@@ -870,7 +740,7 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
ds->selectedIdx = -1;
ds->form->dirty = true;
- rebuildWidgets(ds);
+ dsgnRebuildWidgets(ds);
}
}
@@ -903,46 +773,25 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
break;
}
- if (ctrl->width < MIN_CTRL_SIZE) { ctrl->width = MIN_CTRL_SIZE; }
- if (ctrl->height < MIN_CTRL_SIZE) { ctrl->height = MIN_CTRL_SIZE; }
+ if (ctrl->width < MIN_CTRL_SIZE) {
+ ctrl->width = MIN_CTRL_SIZE;
+ }
- syncWidgetGeom(ctrl);
+ if (ctrl->height < MIN_CTRL_SIZE) {
+ ctrl->height = MIN_CTRL_SIZE;
+ }
+
+ dsgnSyncWidgetGeom(ctrl);
ds->form->dirty = true;
} else if (ds->mode == DSGN_REORDERING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
- // Determine if we should swap with a neighbor based on drag direction
+ // Swap with the neighbor in the drag direction once the pointer
+ // passes its midpoint.
int32_t dy = y - ds->dragStartY;
- DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
- if (dy > 0 && ctrl->widget) {
- // Dragging down -- swap with next control if past its midpoint
- if (ds->selectedIdx < ctrlCount - 1) {
- DsgnControlT *next = ds->form->controls[ds->selectedIdx + 1];
-
- if (next->widget && y > next->widget->y + next->widget->h / 2) {
- DsgnControlT *tmp = ds->form->controls[ds->selectedIdx];
- ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx + 1];
- ds->form->controls[ds->selectedIdx + 1] = tmp;
- rebuildWidgets(ds);
- ds->selectedIdx++;
- ds->dragStartY = y;
- ds->form->dirty = true;
- }
- }
- } else if (dy < 0 && ctrl->widget) {
- // Dragging up -- swap with previous control if past its midpoint
- if (ds->selectedIdx > 0) {
- DsgnControlT *prev = ds->form->controls[ds->selectedIdx - 1];
-
- if (prev->widget && y < prev->widget->y + prev->widget->h / 2) {
- DsgnControlT *tmp = ds->form->controls[ds->selectedIdx];
- ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx - 1];
- ds->form->controls[ds->selectedIdx - 1] = tmp;
- rebuildWidgets(ds);
- ds->selectedIdx--;
- ds->dragStartY = y;
- ds->form->dirty = true;
- }
- }
+ if (dy > 0) {
+ trySwapNeighbor(ds, y, 1);
+ } else if (dy < 0) {
+ trySwapNeighbor(ds, y, -1);
}
}
@@ -1002,7 +851,7 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
cp->width = DEFAULT_CTRL_W;
cp->height = DEFAULT_CTRL_H;
- setPropValue(cp, "Caption", cp->name);
+ dsgnSetPropValue(cp, DSGN_KEY_CAPTION, cp->name);
// Determine parent: if click is inside a container, nest there
WidgetT *parentWidget = ds->form->contentBox;
@@ -1018,29 +867,7 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
if (x >= wx && x < wx + ww && y >= wy && y < wy + wh) {
snprintf(cp->parentName, DSGN_MAX_NAME, "%s", pc->name);
- parentWidget = pc->widget;
- const char *layout = dsgnControlGetPropValue(pc, "Layout");
-
- if (layout && layout[0] && strcasecmp(layout, "VBox") != 0) {
- // Mirror dsgnCreateWidgets: non-VBox containers nest
- // children inside a tagged content box so the live designer
- // layout matches the rebuilt/saved/runtime form.
- if (parentWidget->firstChild &&
- parentWidget->firstChild->userData == (void *)pc) {
- parentWidget = parentWidget->firstChild;
- } else {
- WidgetT *box = dsgnCreateContentBox(parentWidget, layout);
-
- // dsgnCreateContentBox returns the container itself
- // when the layout type is not a loaded parent
- // container; only tag and descend into a real box.
- if (box != parentWidget) {
- box->userData = (void *)pc;
- parentWidget = box;
- }
- }
- }
-
+ parentWidget = containerChildParent(pc);
break;
}
}
@@ -1049,11 +876,7 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
// Create the live widget
if (parentWidget) {
cp->widget = dsgnCreateDesignWidget(typeName, parentWidget);
-
- if (cp->widget) {
- cp->widget->minW = wgtPixels(cp->width);
- cp->widget->minH = wgtPixels(cp->height);
- }
+ dsgnSyncWidgetGeom(cp);
}
arrput(ds->form->controls, cp);
@@ -1063,7 +886,7 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
DsgnControlT *stable = ds->form->controls[ds->selectedIdx];
if (stable->widget) {
- const char *caption = dsgnControlGetPropValue(stable, "Caption");
+ const char *caption = dsgnControlGetPropValue(stable, DSGN_KEY_CAPTION);
wgtSetName(stable->widget, stable->name);
wgtSetText(stable->widget, caption ? caption : stable->name);
}
@@ -1080,10 +903,7 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
// Draw selection handles on the window's painted surface.
// Called after widgets have painted, using direct display drawing.
-void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) {
- (void)winX;
- (void)winY;
-
+void dsgnPaintOverlay(DsgnStateT *ds) {
if (!ds->form || !ds->form->controls || !ds->ctx || !ds->formWin || !ds->formWin->contentBuf) {
return;
}
@@ -1133,6 +953,27 @@ void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) {
}
+void dsgnRebuildWidgets(DsgnStateT *ds) {
+ WidgetT *parent = ds->form ? ds->form->contentBox : NULL;
+
+ if (!parent) {
+ return;
+ }
+
+ // Destroy all existing widget children before recreating them, so
+ // the old subtrees are freed instead of leaked.
+ wgtDestroyChildren(parent);
+
+ int32_t count = (int32_t)arrlen(ds->form->controls);
+
+ for (int32_t i = 0; i < count; i++) {
+ ds->form->controls[i]->widget = NULL;
+ }
+
+ dsgnCreateWidgets(ds, parent);
+}
+
+
int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
if (!ds->form || !buf || bufSize <= 0) {
return -1;
@@ -1142,24 +983,35 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
pos = emitClamped(buf, bufSize, pos, "VERSION DVX 1.00\n");
pos = emitClamped(buf, bufSize, pos, "Begin Form %s\n", ds->form->name);
- pos = emitClamped(buf, bufSize, pos, " Caption = \"%s\"\n", ds->form->caption);
- pos = emitClamped(buf, bufSize, pos, " Layout = %s\n", ds->form->layout);
- pos = emitClamped(buf, bufSize, pos, " AutoSize = %s\n", ds->form->autoSize ? "True" : "False");
- pos = emitClamped(buf, bufSize, pos, " Resizable = %s\n", ds->form->resizable ? "True" : "False");
- pos = emitClamped(buf, bufSize, pos, " Centered = %s\n", ds->form->centered ? "True" : "False");
- if (!ds->form->centered) {
- pos = emitClamped(buf, bufSize, pos, " Left = %d\n", (int)ds->form->left);
- pos = emitClamped(buf, bufSize, pos, " Top = %d\n", (int)ds->form->top);
- }
+ // Form header: every runtime form property the designer stores, in
+ // table order. Name comes from the Begin line; Left/Top are implied
+ // by Centered and Width/Height by AutoSize; HelpTopic is optional.
+ // Layout is read-only at runtime but is the loader's container type.
+ int32_t formPropCount = 0;
+ const BasPropDescT *formProps = basFormRtFormProps(&formPropCount);
- if (!ds->form->autoSize) {
- pos = emitClamped(buf, bufSize, pos, " Width = %d\n", (int)ds->form->width);
- pos = emitClamped(buf, bufSize, pos, " Height = %d\n", (int)ds->form->height);
- }
+ for (int32_t i = 0; i < formPropCount; i++) {
+ const BasPropDescT *pd = &formProps[i];
+ char valBuf[DSGN_MAX_TEXT];
- if (ds->form->helpTopic[0]) {
- pos = emitClamped(buf, bufSize, pos, " HelpTopic = \"%s\"\n", ds->form->helpTopic);
+ if (strcasecmp(pd->name, DSGN_KEY_NAME) == 0) {
+ continue;
+ }
+
+ if (ds->form->centered && (strcasecmp(pd->name, DSGN_KEY_LEFT) == 0 || strcasecmp(pd->name, DSGN_KEY_TOP) == 0)) {
+ continue;
+ }
+
+ if (ds->form->autoSize && (strcasecmp(pd->name, DSGN_KEY_WIDTH) == 0 || strcasecmp(pd->name, DSGN_KEY_HEIGHT) == 0)) {
+ continue;
+ }
+
+ if (!dsgnFormPropValue(ds->form, pd->name, valBuf, sizeof(valBuf)) || (strcasecmp(pd->name, DSGN_KEY_HELPTOPIC) == 0 && !valBuf[0])) {
+ continue;
+ }
+
+ pos = emitPropLine(buf, bufSize, pos, "", pd->name, pd->type, valBuf);
}
// Output menu items as nested Begin Menu blocks
@@ -1227,30 +1079,32 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
pos = emitClamped(buf, bufSize, pos, "End\n");
+ // emitClamped and emitPad both park pos at bufSize-1 once the buffer
+ // is full, so reaching it means the form text was cut short.
+ if (pos >= bufSize - 1) {
+ return -1;
+ }
+
// Append code section if present
if (ds->form->code && ds->form->code[0]) {
int32_t codeLen = (int32_t)strlen(ds->form->code);
- int32_t avail = bufSize - pos - 2; // room for \n prefix and \n suffix
+ int32_t avail = bufSize - pos - DSGN_NEWLINE_ROOM - 1;
- if (avail > 0) {
+ if (codeLen > avail) {
+ return -1;
+ }
+
+ buf[pos++] = '\n';
+ memcpy(buf + pos, ds->form->code, codeLen);
+ pos += codeLen;
+
+ // Ensure trailing newline
+ if (buf[pos - 1] != '\n') {
buf[pos++] = '\n';
-
- if (codeLen > avail) {
- codeLen = avail;
- }
-
- memcpy(buf + pos, ds->form->code, codeLen);
- pos += codeLen;
-
- // Ensure trailing newline
- if (pos > 0 && buf[pos - 1] != '\n' && pos < bufSize - 1) {
- buf[pos++] = '\n';
- }
-
- buf[pos] = '\0';
}
}
+ buf[pos] = '\0';
return pos;
}
@@ -1268,6 +1122,43 @@ const char *dsgnSelectedName(const DsgnStateT *ds) {
}
+void dsgnSetPropValue(DsgnControlT *ctrl, const char *name, const char *value) {
+ for (int32_t i = 0; i < ctrl->propCount; i++) {
+ if (strcasecmp(ctrl->props[i].name, name) == 0) {
+ snprintf(ctrl->props[i].value, DSGN_MAX_TEXT, "%s", value);
+ return;
+ }
+ }
+
+ if (ctrl->propCount < DSGN_MAX_PROPS) {
+ snprintf(ctrl->props[ctrl->propCount].name, DSGN_MAX_NAME, "%s", name);
+ snprintf(ctrl->props[ctrl->propCount].value, DSGN_MAX_TEXT, "%s", value);
+ ctrl->propCount++;
+ }
+}
+
+
+// Update the live widget's size hints from the design data. minW/minH set
+// the floor; maxW/maxH cap the size (0 = no cap).
+void dsgnSyncWidgetGeom(DsgnControlT *ctrl) {
+ if (!ctrl->widget) {
+ return;
+ }
+
+ if (ctrl->width > 0) {
+ ctrl->widget->minW = wgtPixels(ctrl->width);
+ }
+
+ if (ctrl->height > 0) {
+ ctrl->widget->minH = wgtPixels(ctrl->height);
+ }
+
+ ctrl->widget->maxW = ctrl->maxWidth > 0 ? wgtPixels(ctrl->maxWidth) : 0;
+ ctrl->widget->maxH = ctrl->maxHeight > 0 ? wgtPixels(ctrl->maxHeight) : 0;
+ ctrl->widget->weight = ctrl->weight;
+}
+
+
// Clamped formatted append into a fixed buffer. Returns pos unchanged once
// pos has reached bufSize; otherwise performs a size-clamped vsnprintf and
// returns the new position, capped at bufSize-1 on truncation. This avoids
@@ -1301,6 +1192,109 @@ int32_t emitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ..
}
+// Emit "Begin Type Name" and every property line of one control, without
+// the closing End so the .frm writer can nest children first.
+//
+// Every value is written in the canonical form for its type so the runtime
+// loader can classify it the same way whether it came from a fresh
+// placement, a loaded .frm, or the property grid: STRING quoted, BOOL as
+// True/False, INT/FLOAT/ENUM unquoted. Interface-typed keys are never
+// written from the raw props[] strings; only keys unknown to the widget
+// interface go out as quoted strings.
+static int32_t emitControlHead(const DsgnControlT *ctrl, char *buf, int32_t bufSize, int32_t pos, const char *pad) {
+ pos = emitClamped(buf, bufSize, pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name);
+
+ if (ctrl->index >= 0) {
+ pos = emitClamped(buf, bufSize, pos, "%s Index = %d\n", pad, (int)ctrl->index);
+ }
+
+ const char *caption = dsgnControlGetPropValue(ctrl, DSGN_KEY_CAPTION);
+ const char *text = dsgnControlGetPropValue(ctrl, DSGN_KEY_TEXT);
+
+ if (caption) {
+ pos = emitPropLine(buf, bufSize, pos, pad, DSGN_KEY_CAPTION, WGT_IFACE_STRING, caption);
+ }
+
+ if (text) {
+ pos = emitPropLine(buf, bufSize, pos, pad, DSGN_KEY_TEXT, WGT_IFACE_STRING, text);
+ }
+
+ for (int32_t i = 0; i < DSGN_INT_PROP_COUNT; i++) {
+ const DsgnIntPropT *ip = &sIntProps[i];
+ int32_t val = intPropGet(ctrl, ip);
+ bool emit;
+
+ switch (ip->saveRule) {
+ case DSGN_SAVE_IF_NONZERO:
+ emit = (val != 0);
+ break;
+ case DSGN_SAVE_IF_POSITIVE:
+ emit = (val > 0);
+ break;
+ default:
+ emit = true;
+ break;
+ }
+
+ if (emit) {
+ pos = emitClamped(buf, bufSize, pos, "%s %s = %d\n", pad, ip->name, (int)val);
+ }
+ }
+
+ if (ctrl->helpTopic[0]) {
+ pos = emitPropLine(buf, bufSize, pos, pad, DSGN_KEY_HELPTOPIC, WGT_IFACE_STRING, ctrl->helpTopic);
+ }
+
+ if (!ctrl->visible && !dsgnIfaceHasProp(ctrl->typeName, DSGN_KEY_VISIBLE)) {
+ pos = emitClamped(buf, bufSize, pos, "%s %s = False\n", pad, DSGN_KEY_VISIBLE);
+ }
+
+ if (!ctrl->enabled && !dsgnIfaceHasProp(ctrl->typeName, DSGN_KEY_ENABLED)) {
+ pos = emitClamped(buf, bufSize, pos, "%s %s = False\n", pad, DSGN_KEY_ENABLED);
+ }
+
+ // Keys the widget interface does not know: raw strings, quoted.
+ for (int32_t j = 0; j < ctrl->propCount; j++) {
+ const char *name = ctrl->props[j].name;
+
+ if (strcasecmp(name, DSGN_KEY_CAPTION) == 0 || strcasecmp(name, DSGN_KEY_TEXT) == 0) {
+ continue;
+ }
+
+ if (findIfaceProp(ctrl->typeName, name)) {
+ continue;
+ }
+
+ pos = emitPropLine(buf, bufSize, pos, pad, name, WGT_IFACE_STRING, ctrl->props[j].value);
+ }
+
+ // Interface properties, typed.
+ const char *wgtName = wgtFindByBasName(ctrl->typeName);
+ const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;
+
+ if (iface) {
+ for (int32_t j = 0; j < iface->propCount; j++) {
+ const WgtPropDescT *p = &iface->props[j];
+ char valBuf[DSGN_MAX_TEXT];
+
+ if (!p->setFn) {
+ continue;
+ }
+
+ if (strcasecmp(p->name, DSGN_KEY_CAPTION) == 0 || strcasecmp(p->name, DSGN_KEY_TEXT) == 0) {
+ continue;
+ }
+
+ if (dsgnIfacePropValue(ctrl, p, valBuf, sizeof(valBuf))) {
+ pos = emitPropLine(buf, bufSize, pos, pad, p->name, p->type, valBuf);
+ }
+ }
+ }
+
+ return pos;
+}
+
+
static int32_t emitPad(char *buf, int32_t bufSize, int32_t pos, int32_t count) {
int32_t i;
@@ -1321,6 +1315,38 @@ static int32_t emitPad(char *buf, int32_t bufSize, int32_t pos, int32_t count) {
}
+// Emit one "Key = Value" line in the canonical form for the value's
+// interface type.
+static int32_t emitPropLine(char *buf, int32_t bufSize, int32_t pos, const char *pad, const char *name, uint8_t type, const char *value) {
+ switch (type) {
+ case WGT_IFACE_STRING:
+ return emitClamped(buf, bufSize, pos, "%s %s = \"%s\"\n", pad, name, value);
+ case WGT_IFACE_BOOL:
+ return emitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, name, frmParseBool(value) ? "True" : "False");
+ case WGT_IFACE_INT:
+ return emitClamped(buf, bufSize, pos, "%s %s = %d\n", pad, name, atoi(value));
+ default:
+ // FLOAT and ENUM: decimal text or the enum value name, unquoted.
+ return emitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, name, value);
+ }
+}
+
+
+const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName) {
+ if (!typeName || !typeName[0]) {
+ return NULL;
+ }
+
+ const char *wgtName = wgtFindByBasName(typeName);
+
+ if (!wgtName) {
+ return NULL;
+ }
+
+ return wgtIfaceFindProp(wgtGetIface(wgtName), propName);
+}
+
+
static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y) {
int32_t count = (int32_t)arrlen(ds->form->controls);
@@ -1371,147 +1397,283 @@ static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y
}
-// Destroy all live widgets and recreate them in the current
-// array order. This is the safest way to reorder since the
-// widget tree child list matches the creation order.
-static void rebuildWidgets(DsgnStateT *ds) {
- WidgetT *parent = ds->form ? ds->form->contentBox : NULL;
-
- if (!parent) {
- return;
- }
-
- // Destroy all existing widget children before recreating them, so
- // the old subtrees are freed instead of leaked.
- wgtDestroyChildren(parent);
-
- // Clear widget pointers
- int32_t count = (int32_t)arrlen(ds->form->controls);
-
- for (int32_t i = 0; i < count; i++) {
- ds->form->controls[i]->widget = NULL;
- }
-
- // Recreate all widgets in current array order
- dsgnCreateWidgets(ds, parent);
+static int32_t intPropGet(const DsgnControlT *ctrl, const DsgnIntPropT *ip) {
+ return *(const int32_t *)((const char *)ctrl + ip->offset);
}
-// Write controls at a given nesting level with the specified parent name.
+static void intPropSet(DsgnControlT *ctrl, const DsgnIntPropT *ip, int32_t val) {
+ *(int32_t *)((char *)ctrl + ip->offset) = val;
+}
+
+
+static void loadOnCtrlBegin(void *userData, const char *typeName, const char *name) {
+ DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
+
+ if (!ctx->form) {
+ return;
+ }
+
+ DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
+
+ if (cp) {
+ cp->index = -1;
+ cp->visible = true;
+ cp->enabled = true;
+ cp->width = DEFAULT_CTRL_W;
+ cp->height = DEFAULT_CTRL_H;
+ snprintf(cp->name, DSGN_MAX_NAME, "%s", name);
+ snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
+
+ if (ctx->nestDepth > 0 && ctx->nestDepth <= FRM_MAX_NESTING) {
+ snprintf(cp->parentName, DSGN_MAX_NAME, "%s", ctx->parentStack[ctx->nestDepth - 1]);
+ }
+
+ arrput(ctx->form->controls, cp);
+ }
+
+ ctx->current = cp;
+
+ // Push even when the allocation failed so the matching CtrlEnd pop
+ // keeps the depth in step with the parser.
+ if (ctx->nestDepth < FRM_MAX_NESTING) {
+ snprintf(ctx->parentStack[ctx->nestDepth], DSGN_MAX_NAME, "%s", name);
+ }
+
+ ctx->nestDepth++;
+}
+
+
+static void loadOnCtrlEnd(void *userData) {
+ DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
+
+ if (ctx->nestDepth > 0) {
+ ctx->nestDepth--;
+ }
+
+ ctx->current = NULL;
+}
+
+
+static void loadOnCtrlProp(void *userData, const char *key, const char *value) {
+ DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
+
+ if (!ctx->current) {
+ return;
+ }
+
+ char val[DSGN_MAX_TEXT];
+ snprintf(val, sizeof(val), "%s", value);
+ frmStripQuotes(val);
+
+ DsgnControlT *cc = ctx->current;
+ const DsgnIntPropT *ip = dsgnFindIntProp(key);
+
+ if (ip) {
+ intPropSet(cc, ip, atoi(val));
+ } else if (strcasecmp(key, "Index") == 0) {
+ cc->index = atoi(val);
+ } else if (strcasecmp(key, DSGN_KEY_HELPTOPIC) == 0) {
+ snprintf(cc->helpTopic, DSGN_MAX_NAME, "%s", val);
+ } else if (strcasecmp(key, DSGN_KEY_VISIBLE) == 0 && !dsgnIfaceHasProp(cc->typeName, DSGN_KEY_VISIBLE)) {
+ cc->visible = frmParseBool(val);
+ } else if (strcasecmp(key, DSGN_KEY_ENABLED) == 0 && !dsgnIfaceHasProp(cc->typeName, DSGN_KEY_ENABLED)) {
+ cc->enabled = frmParseBool(val);
+ } else if (strcasecmp(key, "TabIndex") == 0) {
+ // ignored -- DVX has no tab order
+ } else {
+ dsgnSetPropValue(cc, key, val);
+ }
+}
+
+
+static bool loadOnFormBegin(void *userData, const char *name) {
+ DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
+
+ if (!ctx->form) {
+ return false;
+ }
+
+ snprintf(ctx->form->name, sizeof(ctx->form->name), "%s", name);
+ snprintf(ctx->form->caption, DSGN_MAX_TEXT, "%s", name);
+ ctx->current = NULL;
+ ctx->nestDepth = 0;
+ return true;
+}
+
+
+static void loadOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen) {
+ DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
+
+ if (!ctx->form || trailingLen <= 0) {
+ return;
+ }
+
+ // Skip leading whitespace/blank lines
+ const char *codeStart = trailingSrc;
+ const char *codeEnd = trailingSrc + trailingLen;
+
+ while (codeStart < codeEnd && (*codeStart == '\r' || *codeStart == '\n' || *codeStart == ' ' || *codeStart == '\t')) {
+ codeStart++;
+ }
+
+ if (codeStart >= codeEnd) {
+ return;
+ }
+
+ int32_t codeLen = (int32_t)(codeEnd - codeStart);
+ ctx->form->code = (char *)malloc(codeLen + 1);
+
+ if (ctx->form->code) {
+ memcpy(ctx->form->code, codeStart, codeLen);
+ ctx->form->code[codeLen] = '\0';
+ }
+}
+
+
+static void loadOnFormProp(void *userData, const char *key, const char *value) {
+ DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
+
+ if (!ctx->form) {
+ return;
+ }
+
+ char val[DSGN_MAX_TEXT];
+ snprintf(val, sizeof(val), "%s", value);
+ frmStripQuotes(val);
+
+ DsgnFormT *ff = ctx->form;
+
+ if (strcasecmp(key, DSGN_KEY_CAPTION) == 0) {
+ snprintf(ff->caption, DSGN_MAX_TEXT, "%s", val);
+ } else if (strcasecmp(key, DSGN_KEY_LAYOUT) == 0) {
+ snprintf(ff->layout, DSGN_MAX_NAME, "%s", val);
+ } else if (strcasecmp(key, DSGN_KEY_AUTOSIZE) == 0) {
+ ff->autoSize = frmParseBool(val);
+ } else if (strcasecmp(key, DSGN_KEY_RESIZABLE) == 0) {
+ ff->resizable = frmParseBool(val);
+ } else if (strcasecmp(key, DSGN_KEY_CENTERED) == 0) {
+ ff->centered = frmParseBool(val);
+ } else if (strcasecmp(key, DSGN_KEY_LEFT) == 0) {
+ ff->left = atoi(val);
+ } else if (strcasecmp(key, DSGN_KEY_TOP) == 0) {
+ ff->top = atoi(val);
+ } else if (strcasecmp(key, DSGN_KEY_WIDTH) == 0) {
+ ff->width = atoi(val);
+ ff->autoSize = false;
+ } else if (strcasecmp(key, DSGN_KEY_HEIGHT) == 0) {
+ ff->height = atoi(val);
+ ff->autoSize = false;
+ } else if (strcasecmp(key, DSGN_KEY_HELPTOPIC) == 0) {
+ snprintf(ff->helpTopic, DSGN_MAX_NAME, "%s", val);
+ }
+}
+
+
+static void loadOnMenuBegin(void *userData, const char *name, int32_t level) {
+ DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
+
+ if (!ctx->form) {
+ return;
+ }
+
+ DsgnMenuItemT mi;
+ dsgnMenuItemInit(&mi);
+ snprintf(mi.name, DSGN_MAX_NAME, "%s", name);
+ mi.level = level;
+ arrput(ctx->form->menuItems, mi);
+ ctx->curMenuItemIdx = (int32_t)arrlen(ctx->form->menuItems) - 1;
+ ctx->current = NULL;
+}
+
+
+static void loadOnMenuEnd(void *userData) {
+ DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
+
+ ctx->curMenuItemIdx = -1;
+}
+
+
+static void loadOnMenuProp(void *userData, const char *key, const char *value) {
+ DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
+
+ if (!ctx->form ||
+ ctx->curMenuItemIdx < 0 ||
+ ctx->curMenuItemIdx >= (int32_t)arrlen(ctx->form->menuItems)) {
+ return;
+ }
+
+ // Resolve pointer fresh each write -- arrput on nested menus may
+ // have reallocated the array.
+ DsgnMenuItemT *mip = &ctx->form->menuItems[ctx->curMenuItemIdx];
+ char val[DSGN_MAX_TEXT];
+ snprintf(val, sizeof(val), "%s", value);
+ frmStripQuotes(val);
+
+ if (strcasecmp(key, DSGN_KEY_CAPTION) == 0) {
+ snprintf(mip->caption, DSGN_MAX_TEXT, "%s", val);
+ } else if (strcasecmp(key, "Checked") == 0) {
+ mip->checked = frmParseBool(val);
+ } else if (strcasecmp(key, "RadioCheck") == 0) {
+ mip->radioCheck = frmParseBool(val);
+ } else if (strcasecmp(key, DSGN_KEY_ENABLED) == 0) {
+ // Default-true flags: the shared parser rule keeps the loader and
+ // the runtime (formrt.c) in agreement.
+ mip->enabled = frmParseBoolDefault(val, true);
+ } else if (strcasecmp(key, DSGN_KEY_VISIBLE) == 0) {
+ mip->visible = frmParseBoolDefault(val, true);
+ }
+}
+
+
+// Allocate a form carrying the defaults every new or loaded form starts
+// from. Returns NULL when out of memory.
+static DsgnFormT *newFormDefaults(const char *name, bool autoSize) {
+ DsgnFormT *form = (DsgnFormT *)calloc(1, sizeof(DsgnFormT));
+
+ if (!form) {
+ return NULL;
+ }
+
+ form->width = BAS_DEFAULT_FORM_W;
+ form->height = BAS_DEFAULT_FORM_H;
+ form->centered = true;
+ form->autoSize = autoSize;
+ form->resizable = true;
+ snprintf(form->layout, DSGN_MAX_NAME, "%s", DSGN_VBOX_LAYOUT);
+ snprintf(form->name, sizeof(form->name), "%s", name);
+ snprintf(form->caption, DSGN_MAX_TEXT, "%s", name);
+ return form;
+}
+
+
+// Write the controls whose parentName matches, each followed by its own
+// children. Every control is offered as a parent so a child of a container
+// whose widget type is not loaded is still written, not silently dropped.
+// The self-parent test and depth cap keep a cycle (A->B->A) finite.
static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, int32_t pos, const char *parentName, int32_t indent) {
int32_t count = (int32_t)arrlen(form->controls);
for (int32_t i = 0; i < count; i++) {
const DsgnControlT *ctrl = form->controls[i];
- // Only output controls whose parent matches
- if (parentName[0] == '\0' && ctrl->parentName[0] != '\0') { continue; }
- if (parentName[0] != '\0' && strcasecmp(ctrl->parentName, parentName) != 0) { continue; }
+ if (strcasecmp(ctrl->parentName, parentName) != 0) {
+ continue;
+ }
- // Indent
- char pad[DSGN_MAX_INDENT_BUF];
+ char pad[DSGN_MAX_INDENT_BUF];
int32_t padLen = indent * DSGN_INDENT_SPACES;
- if (padLen > DSGN_MAX_INDENT_BUF - 1) { padLen = DSGN_MAX_INDENT_BUF - 1; }
+
+ if (padLen > DSGN_MAX_INDENT_BUF - 1) {
+ padLen = DSGN_MAX_INDENT_BUF - 1;
+ }
+
memset(pad, ' ', padLen);
pad[padLen] = '\0';
- pos = emitClamped(buf, bufSize, pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name);
+ pos = emitControlHead(ctrl, buf, bufSize, pos, pad);
- if (ctrl->index >= 0) {
- pos = emitClamped(buf, bufSize, pos, "%s Index = %d\n", pad, (int)ctrl->index);
- }
-
- const char *caption = dsgnControlGetPropValue(ctrl, "Caption");
- const char *text = dsgnControlGetPropValue(ctrl, "Text");
-
- if (caption) { pos = emitClamped(buf, bufSize, pos, "%s Caption = \"%s\"\n", pad, caption); }
- if (text) { pos = emitClamped(buf, bufSize, pos, "%s Text = \"%s\"\n", pad, text); }
-
- pos = emitClamped(buf, bufSize, pos, "%s Left = %d\n", pad, (int)ctrl->left);
- pos = emitClamped(buf, bufSize, pos, "%s Top = %d\n", pad, (int)ctrl->top);
- pos = emitClamped(buf, bufSize, pos, "%s MinWidth = %d\n", pad, (int)ctrl->width);
- pos = emitClamped(buf, bufSize, pos, "%s MinHeight = %d\n", pad, (int)ctrl->height);
-
- if (ctrl->maxWidth > 0) {
- pos = emitClamped(buf, bufSize, pos, "%s MaxWidth = %d\n", pad, (int)ctrl->maxWidth);
- }
-
- if (ctrl->maxHeight > 0) {
- pos = emitClamped(buf, bufSize, pos, "%s MaxHeight = %d\n", pad, (int)ctrl->maxHeight);
- }
-
- if (ctrl->weight > 0) {
- pos = emitClamped(buf, bufSize, pos, "%s Weight = %d\n", pad, (int)ctrl->weight);
- }
-
- if (ctrl->helpTopic[0]) {
- pos = emitClamped(buf, bufSize, pos, "%s HelpTopic = \"%s\"\n", pad, ctrl->helpTopic);
- }
-
- if (!ctrl->visible && !dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
- pos = emitClamped(buf, bufSize, pos, "%s Visible = False\n", pad);
- }
-
- if (!ctrl->enabled && !dsgnIfaceHasProp(ctrl->typeName, "Enabled")) {
- pos = emitClamped(buf, bufSize, pos, "%s Enabled = False\n", pad);
- }
-
- for (int32_t j = 0; j < ctrl->propCount; j++) {
- if (strcasecmp(ctrl->props[j].name, "Caption") == 0) { continue; }
- if (strcasecmp(ctrl->props[j].name, "Text") == 0) { continue; }
-
- pos = emitClamped(buf, bufSize, pos, "%s %s = \"%s\"\n", pad, ctrl->props[j].name, ctrl->props[j].value);
- }
-
- // Save interface properties (Alignment, etc.) read from the live widget
- if (ctrl->widget) {
- const char *wgtName = wgtFindByBasName(ctrl->typeName);
- const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;
-
- if (iface) {
- for (int32_t j = 0; j < iface->propCount; j++) {
- const WgtPropDescT *p = &iface->props[j];
-
- if (!p->getFn || !p->setFn) {
- continue;
- }
-
- // Skip if already saved as a custom prop
- bool already = false;
-
- for (int32_t k = 0; k < ctrl->propCount; k++) {
- if (strcasecmp(ctrl->props[k].name, p->name) == 0) {
- already = true;
- break;
- }
- }
-
- if (already) {
- continue;
- }
-
- // Skip STRING props here: saveControls only emits
- // the iface-known scalar types. String values come
- // through ctrl->props[] (custom props).
- if (p->type == WGT_IFACE_STRING) {
- continue;
- }
-
- char valBuf[DSGN_MAX_TEXT];
-
- if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
- pos = emitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, p->name, valBuf);
- }
- }
- }
- }
-
- // Recursively output children of this container. Guard against a
- // self-parenting container and runaway cycles (A->B->A) via a depth
- // cap so the recursion always terminates.
- if (dsgnIsContainer(ctrl->typeName) &&
- strcasecmp(ctrl->name, parentName) != 0 &&
- indent < DSGN_MAX_NEST_DEPTH) {
+ if (strcasecmp(ctrl->name, parentName) != 0 && indent < DSGN_MAX_NEST_DEPTH) {
pos = saveControls(form, buf, bufSize, pos, ctrl->name, indent + 1);
}
@@ -1522,30 +1684,35 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i
}
-static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value) {
- for (int32_t i = 0; i < ctrl->propCount; i++) {
- if (strcasecmp(ctrl->props[i].name, name) == 0) {
- snprintf(ctrl->props[i].value, DSGN_MAX_TEXT, "%s", value);
- return;
- }
- }
+// Canvas drag-reorder step: swap the selected control with its array
+// neighbor in direction dir (+1 = down, -1 = up) once the pointer passes
+// that neighbor's midpoint. Only siblings (same parentName) swap, so a
+// child can never move ahead of its own container in controls[].
+static void trySwapNeighbor(DsgnStateT *ds, int32_t y, int32_t dir) {
+ int32_t count = (int32_t)arrlen(ds->form->controls);
+ int32_t other = ds->selectedIdx + dir;
+ DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
- if (ctrl->propCount < DSGN_MAX_PROPS) {
- snprintf(ctrl->props[ctrl->propCount].name, DSGN_MAX_NAME, "%s", name);
- snprintf(ctrl->props[ctrl->propCount].value, DSGN_MAX_TEXT, "%s", value);
- ctrl->propCount++;
- }
-}
-
-
-// Update the live widget's position and size from the design data.
-static void syncWidgetGeom(DsgnControlT *ctrl) {
- if (!ctrl->widget) {
+ if (other < 0 || other >= count || !ctrl->widget) {
return;
}
- ctrl->widget->minW = wgtPixels(ctrl->width);
- ctrl->widget->minH = wgtPixels(ctrl->height);
- ctrl->widget->maxW = ctrl->maxWidth > 0 ? wgtPixels(ctrl->maxWidth) : 0;
- ctrl->widget->maxH = ctrl->maxHeight > 0 ? wgtPixels(ctrl->maxHeight) : 0;
+ DsgnControlT *neighbor = ds->form->controls[other];
+
+ if (!neighbor->widget || strcasecmp(neighbor->parentName, ctrl->parentName) != 0) {
+ return;
+ }
+
+ int32_t mid = neighbor->widget->y + neighbor->widget->h / 2;
+
+ if ((dir > 0 && y <= mid) || (dir < 0 && y >= mid)) {
+ return;
+ }
+
+ ds->form->controls[ds->selectedIdx] = neighbor;
+ ds->form->controls[other] = ctrl;
+ dsgnRebuildWidgets(ds);
+ ds->selectedIdx = other;
+ ds->dragStartY = y;
+ ds->form->dirty = true;
}
diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
index 5f31942..04364cb 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
+++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
@@ -32,24 +32,32 @@
#include "dvxApp.h"
#include "dvxWgt.h"
#include "canvas/canvas.h"
+#include "../compiler/lexer.h"
#include "../formrt/formrt.h"
+#include "../formrt/frmParser.h"
#include "stb_ds_wrap.h"
+#include
#include
#include
// ============================================================
// Limits
// ============================================================
+//
+// Name, text, and nesting limits are the .frm limits from formrt.h and
+// frmParser.h so the designer never truncates what the parser and runtime
+// accept.
-#define DSGN_MAX_NAME 32
-#define DSGN_NAME_FMT "%.31s" // keep in sync with DSGN_MAX_NAME
-#define DSGN_MENU_STACK_DEPTH 8 // max menu nesting in designer parent/menu stacks
-#define DSGN_MAX_TEXT 256
-#define DSGN_MAX_PROPS 32
-#define DSGN_HANDLE_SIZE 6
+#define DSGN_MAX_NAME BAS_MAX_IDENT
+#define DSGN_MENU_STACK_DEPTH FRM_MAX_NESTING // max menu nesting (editor and preview bar)
+#define DSGN_MAX_TEXT FRM_MAX_LINE_LEN
+#define DSGN_MAX_PROPS 32
+#define DSGN_HANDLE_SIZE 6
#define DSGN_MENU_ID_BASE 20000 // base ID for designer preview menu items
+#define DSGN_MAX_NEST_DEPTH 32 // container recursion cap (save, cascade)
+#define DSGN_VBOX_LAYOUT "VBox" // default container / form layout name
// ============================================================
// Design-time property (stored as key=value strings)
@@ -77,13 +85,19 @@ typedef struct {
// ============================================================
// Design-time control
// ============================================================
+//
+// props[] holds every non-built-in "Key = Value" pair from the .frm,
+// including widget interface properties, so a control keeps its values
+// before its live widget exists and across widget rebuilds. Interface
+// properties are written back in the canonical form for their type
+// (see dsgnEmitControl); props[] is only ever the raw string store.
typedef struct {
char name[DSGN_MAX_NAME];
char typeName[DSGN_MAX_NAME];
char parentName[DSGN_MAX_NAME]; // empty = top-level (child of form)
int32_t index; // control array index (-1 = not in array)
- int32_t left;
+ int32_t left; // design-time position hint (0 = layout-managed)
int32_t top;
int32_t width;
int32_t height;
@@ -98,13 +112,33 @@ typedef struct {
WidgetT *widget; // live widget (created at design time for WYSIWYG)
} DsgnControlT;
+// ============================================================
+// Built-in integer control properties
+// ============================================================
+//
+// One table drives the .frm loader, the .frm writer, the property grid
+// rows, and the grid editor for every int32_t field of DsgnControlT.
+
+typedef enum {
+ DSGN_SAVE_ALWAYS,
+ DSGN_SAVE_IF_NONZERO,
+ DSGN_SAVE_IF_POSITIVE
+} DsgnSaveRuleE;
+
+typedef struct {
+ const char *name; // .frm key and property-grid row name
+ const char *alias; // alternate .frm key accepted on load (NULL = none)
+ int32_t offset; // offsetof(DsgnControlT, field)
+ DsgnSaveRuleE saveRule;
+} DsgnIntPropT;
+
// ============================================================
// Design-time form
// ============================================================
typedef struct {
// Fields are ordered by alignment to minimize struct padding.
- char name[DSGN_MAX_NAME];
+ char name[BAS_MAX_IDENT];
char caption[DSGN_MAX_TEXT];
int32_t width;
int32_t height;
@@ -181,18 +215,29 @@ void dsgnInit(DsgnStateT *ds, AppContextT *ctx);
// Call after dsgnLoadFrm or dsgnNewForm, with the form window's contentBox.
void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox);
+// Destroy every live widget under the form's contentBox and recreate them in
+// the current controls[] order (the widget child list mirrors that order).
+void dsgnRebuildWidgets(DsgnStateT *ds);
+
// Load a .frm file into the designer.
bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen);
// Save the designer form to .frm text format.
-// Returns the number of bytes written (excluding null), or -1 on error.
+// Returns the number of bytes written (excluding null), or -1 on error or
+// when the form does not fit in bufSize (the caller must not overwrite the
+// original file with the partial result).
int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize);
+// Append one control's "Begin ... End" block (without its children) to buf
+// at pos, indented by indent levels. Returns the new position; shared by the
+// .frm writer and the clipboard copy so both emit identical text.
+int32_t dsgnEmitControl(const DsgnControlT *ctrl, char *buf, int32_t bufSize, int32_t pos, int32_t indent);
+
// Create a new blank form.
void dsgnNewForm(DsgnStateT *ds, const char *name);
// Draw selection handles over the painted window surface.
-void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY);
+void dsgnPaintOverlay(DsgnStateT *ds);
// Handle mouse click on the design surface.
void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag);
@@ -206,6 +251,27 @@ const char *dsgnSelectedName(const DsgnStateT *ds);
// Return a control's property value by name, or NULL if it has no such property.
const char *dsgnControlGetPropValue(const DsgnControlT *ctrl, const char *name);
+// Set (or add) a control's raw string property. Silently ignored when the
+// control already holds DSGN_MAX_PROPS distinct properties.
+void dsgnSetPropValue(DsgnControlT *ctrl, const char *name, const char *value);
+
+// Fetch the current value of a widget interface property as a string: the
+// props[] copy when one exists, else the live widget. Returns false when
+// neither holds a value.
+bool dsgnIfacePropValue(const DsgnControlT *ctrl, const WgtPropDescT *p, char *out, int32_t outSize);
+
+// Push a control's size fields (width/height/max/weight) to its live widget.
+void dsgnSyncWidgetGeom(DsgnControlT *ctrl);
+
+// Built-in integer property table: entry by index (NULL past the end) and
+// lookup by .frm key (name or alias).
+const DsgnIntPropT *dsgnIntPropAt(int32_t idx);
+const DsgnIntPropT *dsgnFindIntProp(const char *name);
+
+// Text value of a runtime form property (basFormRtFormProps) as the designer
+// stores it; false when the designer has no field for it (runtime-only).
+bool dsgnFormPropValue(const DsgnFormT *form, const char *name, char *out, int32_t outSize);
+
// Get the default event name for a control type.
const char *dsgnDefaultEvent(const char *typeName);
@@ -215,6 +281,15 @@ void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t
// Check if a control type is a container (can hold children).
bool dsgnIsContainer(const char *typeName);
+// True if name (case-insensitive) is already the form's name, a control's
+// name, or (when checkMenus) a menu item's name. Entries equal to
+// exceptName are ignored so a control array or the object being renamed
+// does not collide with itself.
+bool dsgnNameInUse(const DsgnFormT *form, const char *name, const char *exceptName, bool checkMenus);
+
+// Initialize a blank menu item (enabled, visible, level 0).
+void dsgnMenuItemInit(DsgnMenuItemT *mi);
+
// Look up a widget type's interface property descriptor by name, or NULL if
// the type has no such interface property. Shared IDE-internal helper.
const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName);
@@ -238,18 +313,6 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent);
// Used in the form designer to preview the menu layout.
void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form);
-// Create a layout container (VBox/HBox/WrapBox) from a layout name string.
-// For VBox, reuses the root directly. For HBox/WrapBox, creates a child.
-WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout);
-
-// Create and configure a window from form properties.
-// Shared by the designer and runtime to ensure consistent behavior.
-// Creates the window, widget root, and layout container (VBox/HBox/WrapBox).
-// Applies sizing (autoSize or explicit) and positioning (centered or explicit).
-// On success, sets *outRoot and *outContentBox and returns the window.
-// On failure, returns NULL.
-WindowT *dsgnCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox);
-
// ============================================================
// Code rename support (implemented in ideMain.c)
// ============================================================
diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c
index 009714a..e3d5ab6 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideMain.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c
@@ -20,14 +20,11 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
-// ideMain.c -- DVX BASIC Runner application
+// ideMain.c -- DVX BASIC IDE application
//
-// A DVX app that loads, compiles, and runs BASIC programs.
-// PRINT output goes to a scrollable TextArea widget. Compile
-// errors are displayed with line numbers.
-//
-// This is Phase 3 of DVX BASIC: proving the compiler and VM
-// work on real hardware inside the DVX windowing system.
+// A DVX app that edits, compiles, runs and debugs BASIC projects.
+// PRINT output goes to a scrollable TextArea widget. Compile and
+// runtime errors are displayed with file/procedure line numbers.
#include "dvxApp.h"
#include "dvxCur.h"
@@ -64,10 +61,8 @@
#include "../basBuild.h"
#include "../basRes.h"
-#include "../compiler/obfuscate.h"
+#include "../compiler/basEvents.h"
#include "../compiler/parser.h"
-#include "../compiler/strip.h"
-#include "../runtime/serialize.h"
#include "../formrt/formrt.h"
#include "../formrt/frmParser.h"
#include "dvxRes.h"
@@ -94,9 +89,110 @@
#define IDE_MAX_OUTPUT 32768
#define IDE_MAX_EVT_LABELS 64 // rows in event-dropdown label buffer
#define IDE_EVT_LABEL_LEN 32 // bytes per event label
-#define IDE_NAME_BUF 64 // object/event/proc/status/value name buffer
#define IDE_WORD_BUF 32 // single-word scratch buffer
#define IDE_SOURCE_MARGIN 64
+#define IDE_FULL_NAME_BUF (BAS_MAX_IDENT * 2) // "Obj.Evt" / "Obj_Evt" combined names
+#define IDE_TITLE_BUF (PRJ_MAX_NAME + 32) // "DVX BASIC - [name] *"
+#define IDE_FORM_TITLE_BUF (DSGN_MAX_TEXT + 32) // "caption [Design] *"
+#define IDE_MSG_BUF (DVX_MAX_PATH + 32) // path plus short message
+#define IDE_PREF_KEY_BUF 32 // "colorN" style preference keys
+#define IDE_IMM_LINE_BUF 512 // one Immediate-window line
+#define IDE_IMM_WRAP_BUF (IDE_IMM_LINE_BUF + 8) // line plus "PRINT " prefix
+#define IDE_SKELETON_BUF 512 // generated "Sub ... End Sub" skeleton
+#define IDE_VALUE_BUF 256 // formatted variable value / watch text
+#define IDE_FIND_TEXT_BUF 256 // find / replace text
+#define IDE_LINE_NUM_BUF 16 // decimal line number text
+#define IDE_TYPE_NAME_BUF 16 // "Integer()" style type column text
+#define IDE_RGB_LABEL_BUF 8 // "255"
+#define IDE_TAB_WIDTH_BUF 8 // tab width text input
+#define IDE_OUTPUT_LINE_RESERVE 256 // headroom kept in sOutputBuf per error line
+#define IDE_QUOTE_OVERHEAD 3 // two quotes plus NUL around a string value
+#define IDE_ARRAY_DIM_RESERVE 10 // room kept for ") [n]" after array dims
+#define IDE_FRM_LINE_CHROME 8 // " key = \"value\"\n" overhead per .frm line
+#define IDE_FRM_BLOCK_CHROME 512 // Begin/End lines and built-in sizes of a .frm block
+
+#define IDE_MAIN_TITLE "DVX BASIC"
+#define IDE_GENERAL_SECTION "(General)"
+#define IDE_GLOBAL_SECTION "(Global)"
+#define IDE_MODULE_FRAME "(module)"
+#define IDE_BREAK_TAG " [break]"
+#define IDE_DEFAULT_FORM "Form1"
+#define IDE_HELP_FILE "dvxbasic.hlp"
+#define IDE_PREFS_FILE "dvxbasic.ini"
+
+// Keyword tags for procedure scanning (case-insensitive prefix match)
+#define KW_SUB "SUB "
+#define KW_FUNCTION "FUNCTION "
+#define KW_END_SUB "END SUB"
+#define KW_END_FUNCTION "END FUNCTION"
+#define KW_REM "REM"
+#define KW_LET "LET "
+#define KW_PRINT "PRINT"
+#define KW_DIM "DIM"
+#define KW_LINE_PREFIX "Line "
+#define FRM_BEGIN_TAG "Begin "
+#define FRM_END_TAG "End"
+#define KW_LEN(s) ((int32_t)sizeof(s) - 1)
+
+// Preference sections/keys used from more than one place
+#define IDE_PREF_KEY_TAB_WIDTH "tabWidth"
+#define IDE_PREF_KEY_USE_SPACES "useSpaces"
+#define IDE_PREF_KEY_RENAME_SKIP "renameSkipComments"
+#define PREF_SEC_SYNTAX "syntax"
+#define PREF_KEY_COLOR_FMT "color%d"
+#define PREF_SEC_RUN "run"
+#define PREF_KEY_SAVE_ON_RUN "saveOnRun"
+#define PREF_KEY_OUTPUT_TO_LOG "outputToLog"
+#define PREF_SEC_VIEW "view"
+#define PREF_KEY_TOOLBAR "toolbar"
+#define PREF_KEY_STATUSBAR "statusbar"
+#define PREF_SEC_RECENT "recent"
+#define PREF_KEY_RECENT_FMT "file%ld"
+#define IDE_DEFAULT_TAB_WIDTH 3
+#define IDE_MIN_TAB_WIDTH 1
+#define IDE_MAX_TAB_WIDTH 8
+
+// Editor colors
+#define IDE_BP_GUTTER_R 200
+#define IDE_BP_GUTTER_G 0
+#define IDE_BP_GUTTER_B 0
+#define IDE_DBG_LINE_R 255
+#define IDE_DBG_LINE_G 255
+#define IDE_DBG_LINE_B 128
+
+// Window geometry
+#define IDE_WIN_MARGIN 10 // gap from screen edge
+#define IDE_TOOLBAR_GAP 2 // gap below the toolbar window
+#define IDE_TOOLBAR_FALLBACK_Y 60 // toolbar bottom when no toolbar window
+#define IDE_TOOLBAR_SEP_W 10 // toolbar group separator width
+#define IDE_TOOLBAR_INIT_H 200 // toolbar window height before fit
+#define IDE_PROJECT_WIN_OFFSET 25 // project window y below toolbar bottom
+#define IDE_BP_WIN_W 320
+#define IDE_DEBUG_WIN_H 180 // breakpoints / call stack / watch height
+#define IDE_CALLSTACK_WIN_W 220
+#define IDE_CALLSTACK_WIN_Y_OFF 210 // below toolbar bottom
+#define IDE_LOCALS_WIN_W 250
+#define IDE_LOCALS_WIN_H 200
+#define IDE_WATCH_WIN_W 280
+#define IDE_WATCH_WIN_X_GAP 260 // watch window sits left of locals
+#define IDE_FIND_WIN_W 320
+#define IDE_FIND_WIN_H 210
+#define IDE_FIND_ROOT_SPACING 3
+#define IDE_PREFS_WIN_W 420
+#define IDE_PREFS_WIN_H 440
+#define IDE_PREFS_LABEL_W 80
+#define IDE_PREFS_TAB_INPUT_W 40
+#define IDE_PREFS_TAB_INPUT_MAX 4
+#define IDE_PREFS_TEXT_MAX 64
+#define IDE_PREFS_VERSION_MAX 16
+#define IDE_PREFS_DESC_MAX 512
+#define IDE_PREFS_BUTTON_W 60
+#define IDE_PREFS_DESC_H 48
+#define IDE_SWATCH_W 64
+#define IDE_SWATCH_H 24
+#define IDE_WIDGET_SPACING 4
+#define IDE_WIDGET_SPACING_SM 2
+#define IDE_WIDGET_SPACING_LG 8
// Menu command IDs
#define CMD_OPEN 100
@@ -126,8 +222,6 @@
#define CMD_PRJ_OPEN 131
#define CMD_PRJ_SAVE 132
#define CMD_PRJ_CLOSE 133
-#define CMD_PRJ_ADD_MOD 134
-#define CMD_PRJ_ADD_FRM 135
#define CMD_PRJ_REMOVE 136
#define CMD_PRJ_PROPS 138
#define CMD_WIN_PROJECT 137
@@ -155,8 +249,6 @@
#define CMD_HELP_CONTENTS 157
#define CMD_HELP_API 158
#define IDE_MAX_IMM 1024
-#define IDE_DESIGN_W 400
-#define IDE_DESIGN_H 300
#define IDE_KEY_CTRL_A 0x01 // Ctrl+A (select all); KEY_CTRL_A not in dvxTypes.h
#define IDE_OUTPUT_WIN_H 120 // Output/Immediate window height
#define IDE_OUTPUT_WIN_GAP 2 // gap between code window and output strip
@@ -169,6 +261,7 @@
#define SYNTAX_NUMBER 4
#define SYNTAX_OPERATOR 5
#define SYNTAX_TYPE 6
+#define SYNTAX_COLOR_COUNT 7
// View mode for activateFile
typedef enum {
@@ -184,36 +277,102 @@ typedef enum {
ScopeProjE
} FindScopeE;
+// Debug state
+typedef enum {
+ DBG_IDLE, // no program loaded
+ DBG_RUNNING, // program executing
+ DBG_PAUSED // stopped at breakpoint/step
+} IdeDebugStateE;
+
+typedef struct {
+ int32_t fileIdx; // project file index
+ int32_t codeLine; // line within file's code section (1-based)
+ char procName[IDE_FULL_NAME_BUF]; // "obj.evt" combined name
+} IdeBreakpointT;
+
+// Procedure table for Object/Event dropdowns
+typedef struct {
+ char objName[BAS_MAX_IDENT];
+ char evtName[BAS_MAX_IDENT];
+ int32_t lineNum; // line of the SUB / FUNCTION declaration
+ int32_t endLineNum; // line of the END SUB / END FUNCTION
+} IdeProcEntryT;
+
+typedef struct {
+ char *key;
+ uint8_t value;
+} SyntaxMapEntryT;
+
+typedef struct {
+ const char *keyword;
+ const char *topic;
+} HelpMapEntryT;
+
+// Row storage for the debug ListViews. Cell text is strdup'd row-major
+// into strs; cells holds the same pointers in the layout
+// wgtListViewSetData expects.
+typedef struct {
+ char **strs; // stb_ds: owned cell text
+ const char **cells; // stb_ds: borrowed pointers into strs
+} IdeListRowsT;
+
+// Compile-time CtrlName.Member validator context
+typedef struct {
+ char name[BAS_MAX_IDENT];
+ char wgtType[BAS_MAX_IDENT]; // "Form" for the form itself, else iface basName
+} IdeCtrlMapEntryT;
+
+typedef struct {
+ char ctrlName[BAS_MAX_IDENT]; // control that has the bad type
+ char typeName[BAS_MAX_IDENT]; // the unrecognised type name
+ char formName[BAS_MAX_IDENT]; // enclosing form (for error message)
+} IdeBadTypeT;
+
+typedef struct {
+ IdeCtrlMapEntryT *entries; // stb_ds: known controls
+ IdeBadTypeT *badTypes; // stb_ds: unknown widget types encountered
+ char currentForm[BAS_MAX_IDENT]; // most recent Begin Form
+} IdeValidatorCtxT;
+
// ============================================================
// Prototypes
// ============================================================
static void activateFile(int32_t fileIdx, IdeViewModeE view);
+static bool addFileToProject(const char *path);
static void applySyntaxColors(void);
static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx);
+static BasVmT *buildSnippetVm(const char *src, BasModuleT **outMod, char *errBuf, int32_t errBufSize);
static void buildVmBreakpoints(void);
static void buildWindow(void);
static uint8_t classifyWord(const char *word, int32_t wordLen);
static void cleanupFormWin(void);
static void clearAllBreakpoints(void);
static void clearOutput(void);
+static void clearVmStepState(void);
static void closeFindDialog(void);
static void closeProject(void);
+static void closeProjectUi(void);
static int cmpEvtPtrs(const void *a, const void *b);
static int cmpStrPtrs(const void *a, const void *b);
static void compileAndRun(void);
static bool compileProject(void);
static int32_t countLines(const char *text);
+static void createEventSubSkeleton(const char *objName, const char *evtName);
static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx);
static void debugNavigateToLine(int32_t concatLine);
+static void debugResume(const char *status);
static void debugSetBreakTitles(bool paused);
static void debugStartOrResume(int32_t cmd);
+static void debugStop(void);
static void debugUpdateWindows(void);
+static void deleteSelectedControl(void);
static bool doEventsCallback(void *ctx);
static void dsgnCopySelected(void);
static void dsgnPasteControl(void);
static int32_t editorLineToCodeLine(int32_t editorLine);
static void ensureProject(const char *filePath);
+static void ensureProjectWindow(bool raise);
static void evaluateImmediate(const char *expr);
static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize);
static bool evtLabelMatches(const char *label, const char *evtName);
@@ -221,10 +380,11 @@ static char *extractNewProcs(const char *buf);
static const BasDebugVarT *findDebugVar(const char *name);
static bool findInProject(const char *needle, bool caseSensitive, bool forward);
static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen);
+static int32_t findUdtFieldIdx(int32_t typeId, const char *fieldName);
static void formatValue(const BasValueT *v, char *buf, int32_t bufSize);
static void freeProcBufs(void);
static BasValueT *getDebugVarSlot(const BasDebugVarT *dv);
-static const char *getEventExtraParams(const char *evtName);
+static const char *getEventParams(const char *evtName);
static bool getFindForward(void);
static bool getFindMatchCase(void);
static FindScopeE getFindScope(void);
@@ -236,6 +396,7 @@ static void handleProjectCmd(int32_t cmd);
static void handleRunCmd(int32_t cmd);
static void handleViewCmd(int32_t cmd);
static void handleWindowCmd(int32_t cmd);
+static bool hasProject(void);
static bool hasUnsavedData(void);
static void helpBuildCtrlTopic(const char *typeName, char *buf, int32_t bufSize);
static const char *helpLookupKeyword(const char *word);
@@ -244,14 +405,21 @@ static void helpSetCtrlTopic(const char *typeName);
void ideRenameInCode(const char *oldName, const char *newName);
static bool immParseScalarFromStr(const char *rhs, const BasValueT *target, BasValueT *outVal);
static void immPrintCallback(void *ctx, const char *text, bool newline);
-static BasValueT *immResolveLhsSlot(const char *lhs, const char **endPtr);
static bool immTryAssign(const char *expr);
static void initSyntaxMap(void);
static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize);
static bool isCtrlArrayInDesigner(const char *ctrlName);
-static bool isIdentChar(char c);
static bool isInStringOrComment(const char *src, int32_t i);
+static bool isMangledDebugName(const char *name);
static bool isReplaceEnabled(void);
+static char *joinProcArrays(const char *general, char **procs);
+static const char *joinProcBufs(void);
+static bool kwMatch(const char *p, const char *kw);
+static void listRowsAdd(IdeListRowsT *rows, const char *text);
+static void listRowsCommit(const IdeListRowsT *rows, WidgetT *list, int32_t rowCount);
+static void listRowsFree(IdeListRowsT *rows);
+static void listRowsReset(IdeListRowsT *rows);
+static char *loadCompileSource(int32_t fileIdx);
static void loadFile(void);
static void loadFormCodeIntoEditor(void);
static void loadFrmFiles(BasFormRtT *rt);
@@ -315,13 +483,20 @@ static void onWatchListDblClick(WidgetT *w);
static void onWatchListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
static void openFindDialog(bool showReplace);
static void openProject(void);
+static void openProjectPath(const char *path);
+static void openSinglePath(const char *path);
+static char *packProcLayout(const char *source);
+static const char *parseIndexList(const char *p, int32_t *indices, int32_t *outCount);
static void parseProcs(const char *source);
+static void prefsSetRgbLabels(uint8_t r, uint8_t g, uint8_t b);
static void prefsUpdateColorSliders(void);
static void prefsUpdateSwatch(void);
static void printCallback(void *ctx, const char *text, bool newline);
static bool procBufContains(const char *hay, const char *needle, int32_t needleLen, bool caseSensitive);
+static const char *procDeclAt(const char *p, bool *outIsSub);
+static void procNameFromDecl(const char *afterKw, char *out, int32_t outSize);
static bool promptAndSave(void);
-static bool readDebugVar(const BasDebugVarT *dv, BasValueT *outVal);
+static void rebuildProcTable(const char *src);
static void recentAdd(const char *path);
static void recentLoad(void);
static void recentOpen(int32_t index);
@@ -329,17 +504,21 @@ static void recentRebuildMenu(void);
static void recentSave(void);
static void removeBreakpointsForFile(int32_t fileIdx);
static char *renameInBuffer(const char *src, const char *oldName, const char *newName);
+static BasValueT *resolveVarPath(const char *path, const char **endPtr);
static void runCached(void);
static void runModule(BasModuleT *mod);
-static void saveActiveFile(void);
+static bool saveActiveFile(void);
+static bool saveAllModified(void);
static bool saveCurProc(void);
static void saveFile(void);
+static bool saveProjectFile(void);
static void selectDropdowns(const char *objName, const char *evtName);
static void setOutputText(const char *text);
static void setStatus(const char *text);
static void showBreakpointWindow(void);
static void showCallStackWindow(void);
static void showCodeWindow(void);
+static void showCompileError(const char *status);
static void showImmediateWindow(void);
static void showLocalsWindow(void);
static void showOutputWindow(void);
@@ -347,10 +526,15 @@ static void showPreferencesDialog(void);
static void showProc(int32_t procIdx);
static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive, bool forward);
static void showWatchWindow(void);
+static const char *skipLine(const char *p);
+static const char *skipToEndProc(const char *declLine, bool isSub, int32_t *outEndLineOffset);
+static void splitProcs(const char *source, char **outGeneral, char ***outProcs);
static void stashCurrentFile(void);
static void stashDesignerState(void);
static void stashFormCode(void);
static void switchToDesign(void);
+static void syncFormDirty(void);
+static void syncVmBreakpoints(void);
static void teardownFormWin(void);
static void toggleBreakpoint(void);
static void toggleBreakpointLine(int32_t editorLine);
@@ -361,21 +545,27 @@ static void updateCallStackWindow(void);
static void updateDirtyIndicators(void);
static void updateDropdowns(void);
static void updateLocalsWindow(void);
+static void updateMainTitle(void);
static void updateProjectMenuState(void);
static void updateWatchWindow(void);
+static void validatorBuildCtrlMap(IdeValidatorCtxT *ctx);
+static bool validatorIsMethodValid(void *ctx, const char *wgtType, const char *methodName);
+static bool validatorIsPropValid(void *ctx, const char *wgtType, const char *propName);
+static const char *validatorLookupCtrlType(void *ctx, const char *ctrlName);
+static void validatorOnCtrlBegin(void *ud, const char *typeName, const char *name);
+static bool validatorOnFormBegin(void *ud, const char *name);
+static void validatorOnMenuBegin(void *ud, const char *name, int32_t level);
+static int32_t vmProcIndexForPc(int32_t pc);
+static const char *vmProcNameForPc(int32_t pc);
static void watchEditSelected(void);
static void watchPrintCallback(void *ctx, const char *text, bool newline);
+static bool writeProjectFile(int32_t idx);
int32_t appMain(DxeAppContextT *ctx);
// ============================================================
// Keyword-to-topic lookup for context-sensitive help (F1)
// ============================================================
-typedef struct {
- const char *keyword;
- const char *topic;
-} HelpMapEntryT;
-
static const HelpMapEntryT sHelpMap[] = {
// Data types
{"Boolean", "lang.datatypes"},
@@ -575,7 +765,7 @@ static const HelpMapEntryT sHelpMap[] = {
static DxeAppContextT *sCtx = NULL;
static char sIdeHelpFile[DVX_MAX_PATH]; // IDE help file (restored after program run)
static AppContextT *sAc = NULL;
-static PrefsHandleT *sPrefs = NULL;
+static PrefsHandleT *sPrefs = NULL;
static WindowT *sWin = NULL; // Main toolbar window
static WindowT *sCodeWin = NULL; // Code editor window
static WindowT *sOutWin = NULL; // Output window
@@ -599,7 +789,7 @@ static WidgetT *sTbCode = NULL;
static WidgetT *sTbDesign = NULL;
static BasVmT *sVm = NULL; // VM instance (non-NULL while running)
static BasModuleT *sCachedModule = NULL; // Last compiled module (for Ctrl+F5)
-static DsgnStateT sDesigner;
+static DsgnStateT sDesigner;
static WindowT *sFormWin = NULL; // Form designer window (separate)
static WindowT *sToolboxWin = NULL;
static WindowT *sPropsWin = NULL;
@@ -607,26 +797,29 @@ static WindowT *sProjectWin = NULL;
static PrjStateT sProject;
static WindowT *sLastFocusWin = NULL; // last focused non-toolbar window
-static char sOutputBuf[IDE_MAX_OUTPUT];
+static char sOutputBuf[IDE_MAX_OUTPUT];
static int32_t sOutputLen = 0;
// Procedure view state -- the editor shows one procedure at a time.
// Each procedure is stored in its own malloc'd buffer. The editor
// swaps directly between buffers with no splicing needed.
-static char *sGeneralBuf = NULL; // (General) section: module-level code
-static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure
-// Cached copy of the source last passed to parseProcs (a .bas file's
-// contents, or a .frm's extracted code section). updateDropdowns
-// scans this so sProcTable.lineNum is in the same line-number space
-// prjMapLine returns -- getFullSource() packs blank lines and diverges.
-static char *sParsedSource = NULL;
-static int32_t sCurProcIdx = -2; // which buffer is in the editor (-1=General, -2=none)
-static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none)
-static int32_t sEditorLineCount = 0; // line count for breakpoint adjustment on edit
+//
+// The canonical text of a file is joinProcArrays(sGeneralBuf, sProcBufs):
+// the (General) section, a blank line, then each procedure separated by
+// one blank line. Everything that reads line numbers (sProcTable,
+// breakpoints, the compiler's source map) works in that layout, and
+// compileProject normalizes every file into it, so there is exactly one
+// line-number space.
+static char *sGeneralBuf = NULL; // (General) section: module-level code
+static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure
+static char *sFullSourceCache = NULL; // last joinProcBufs() result (owned here)
+static int32_t sCurProcIdx = -2; // which buffer is in the editor (-1=General, -2=none)
+static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none)
+static int32_t sEditorLineCount = 0; // line count for breakpoint adjustment on edit
// Find/Replace state
-static char sFindText[256] = "";
-static char sReplaceText[256] = "";
+static char sFindText[IDE_FIND_TEXT_BUF] = "";
+static char sReplaceText[IDE_FIND_TEXT_BUF] = "";
// Find/Replace dialog state (modeless)
static WindowT *sFindWin = NULL;
static WidgetT *sFindInput = NULL;
@@ -638,15 +831,7 @@ static WidgetT *sCaseCheck = NULL;
static WidgetT *sScopeGroup = NULL; // radio group: 0=Func, 1=Obj, 2=File, 3=Proj
static WidgetT *sDirGroup = NULL; // radio group: 0=Fwd, 1=Back
-// Procedure table for Object/Event dropdowns
-typedef struct {
- char objName[IDE_NAME_BUF];
- char evtName[IDE_NAME_BUF];
- int32_t lineNum; // line of the SUB / FUNCTION declaration
- int32_t endLineNum; // line of the END SUB / END FUNCTION
-} IdeProcEntryT;
-
-static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array
+static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array (rebuilt by joinProcBufs)
static const char **sObjItems = NULL; // stb_ds dynamic array
static const char **sEvtItems = NULL; // stb_ds dynamic array
static bool sDropdownNavSuppressed = false;
@@ -660,51 +845,35 @@ static char **sRecentFiles = NULL;
static MenuT *sFileMenu = NULL;
static int32_t sFileMenuBase = 0; // item count before recent files
-// Debug state
-typedef enum {
- DBG_IDLE, // no program loaded
- DBG_RUNNING, // program executing
- DBG_PAUSED // stopped at breakpoint/step
-} IdeDebugStateE;
+static IdeDebugStateE sDbgState = DBG_IDLE;
+static IdeBreakpointT *sBreakpoints = NULL; // stb_ds array
+static int32_t *sVmBreakpoints = NULL; // stb_ds array of concat line numbers (built at compile time)
+static int32_t sDbgCurrentLine = -1; // line where paused (-1 = none)
+static BasFormRtT *sDbgFormRt = NULL; // form runtime for debug session
+static BasModuleT *sDbgModule = NULL; // module for debug session
+static bool sDbgBreakOnStart = false; // break at first statement
+static bool sDbgEnabled = false; // true = debug mode (breakpoints active)
+static WindowT *sLocalsWin = NULL; // Locals window
+static WidgetT *sLocalsList = NULL; // Locals ListView widget
+static WindowT *sCallStackWin = NULL; // Call stack window
+static WidgetT *sCallStackList = NULL; // Call stack ListView widget
+static WindowT *sWatchWin = NULL; // Watch window
+static WidgetT *sWatchList = NULL; // Watch ListView widget
+static WidgetT *sWatchInput = NULL; // Watch expression input
+static char **sWatchExprs = NULL; // stb_ds dynamic array of strdup'd watch expressions
+static WindowT *sBreakpointWin = NULL; // Breakpoints window
+static WidgetT *sBreakpointList = NULL; // Breakpoints ListView widget
-typedef struct {
- int32_t fileIdx; // project file index
- int32_t codeLine; // line within file's code section (1-based)
- int32_t procIdx; // procedure index at time of toggle (-1 = general)
- char procName[BAS_MAX_PROC_NAME * 2]; // "obj.evt" combined name
-} IdeBreakpointT;
-
-static IdeDebugStateE sDbgState = DBG_IDLE;
-static IdeBreakpointT *sBreakpoints = NULL; // stb_ds array
-static int32_t sBreakpointCount = 0;
-static int32_t *sVmBreakpoints = NULL; // stb_ds array of concat line numbers (built at compile time)
-static int32_t sDbgCurrentLine = -1; // line where paused (-1 = none)
-static BasFormRtT *sDbgFormRt = NULL; // form runtime for debug session
-static BasModuleT *sDbgModule = NULL; // module for debug session
-static bool sDbgBreakOnStart = false; // break at first statement
-static bool sDbgEnabled = false; // true = debug mode (breakpoints active)
-static WindowT *sLocalsWin = NULL; // Locals window
-static WidgetT *sLocalsList = NULL; // Locals ListView widget
-static WindowT *sCallStackWin = NULL; // Call stack window
-static WidgetT *sCallStackList = NULL; // Call stack ListView widget
-static WindowT *sWatchWin = NULL; // Watch window
-static WidgetT *sWatchList = NULL; // Watch ListView widget
-static WidgetT *sWatchInput = NULL; // Watch expression input
-static char **sWatchExprs = NULL; // stb_ds dynamic array of strdup'd watch expressions
-static WindowT *sBreakpointWin = NULL; // Breakpoints window
-static WidgetT *sBreakpointList = NULL; // Breakpoints ListView widget
+// Per-refresh display rows for the debug ListViews
+static IdeListRowsT sBpRows;
+static IdeListRowsT sLocalsRows;
+static IdeListRowsT sCallRows;
+static IdeListRowsT sWatchRows;
// ============================================================
// Syntax highlighting tables (shared by several functions)
// ============================================================
-#define SYNTAX_COLOR_COUNT 7
-
-typedef struct {
- char *key;
- uint8_t value;
-} SyntaxMapEntryT;
-
static SyntaxMapEntryT *sSyntaxMap = NULL;
// Color entry names for the Colors tab (matches SYNTAX_* indices)
@@ -758,9 +927,27 @@ static struct {
} sPrefsDlg;
// Watch-expression scratch buffer (used by evalWatchExpr + watchPrintCallback)
-static char sWatchPrintBuf[256];
+static char sWatchPrintBuf[IDE_VALUE_BUF];
static int32_t sWatchPrintLen;
+// ============================================================
+// Event table for the Object/Event dropdowns and handler skeletons
+// ============================================================
+//
+// Expanded from the master BAS_EVENT_LIST in compiler/basEvents.h so the
+// dropdowns, the generated parameter lists and the runtime dispatcher
+// can never disagree.
+
+static const BasEventInfoT sEventTable[] = { BAS_EVENT_LIST(BAS_EVENT_ROW) };
+
+#define IDE_EVENT_COUNT ((int32_t)(sizeof(sEventTable) / sizeof(sEventTable[0])))
+
+// Menu items only fire Click
+#define IDE_MENU_EVENT "Click"
+
+// Buffer for event dropdown labels (with [] for unimplemented)
+static char sEvtLabelBufs[IDE_MAX_EVT_LABELS][IDE_EVT_LABEL_LEN];
+
// ============================================================
// App descriptor
// ============================================================
@@ -794,23 +981,20 @@ static void activateFile(int32_t fileIdx, IdeViewModeE view) {
if (sCodeWin) {
dvxRaiseWindow(sAc, sCodeWin);
} else {
- // Code window was closed — reopen it
+ // Code window was closed -- reopen it and reload the file
showCodeWindow();
if (sCodeWin) {
dvxRaiseWindow(sAc, sCodeWin);
}
- // Reload the file content
- if (sEditor) {
- const char *source = target->buffer;
-
- if (source) {
- parseProcs(source);
- updateDropdowns();
- showProc(-1);
- sEditor->onChange = onEditorChange;
- }
+ if (target->isForm) {
+ loadFormCodeIntoEditor();
+ } else if (sEditor && target->buffer) {
+ parseProcs(target->buffer);
+ sEditorFileIdx = fileIdx;
+ updateDropdowns();
+ showProc(-1);
}
}
}
@@ -851,12 +1035,12 @@ static void activateFile(int32_t fileIdx, IdeViewModeE view) {
free(diskBuf);
} else {
// New blank form -- derive name from filename
- char formName[PRJ_MAX_NAME];
+ char formName[BAS_MAX_IDENT];
const char *base = platformPathBaseName(target->path);
int32_t bl = (int32_t)strlen(base);
- if (bl >= PRJ_MAX_NAME) {
- bl = PRJ_MAX_NAME - 1;
+ if (bl >= BAS_MAX_IDENT) {
+ bl = BAS_MAX_IDENT - 1;
}
memcpy(formName, base, bl);
@@ -879,6 +1063,15 @@ static void activateFile(int32_t fileIdx, IdeViewModeE view) {
if (view == ViewDesignE) {
switchToDesign();
+
+ // The editor must never stay bound to the previous file: an open
+ // code window now shows this form's code, otherwise the stale
+ // proc buffers are released so nothing can be edited into limbo.
+ if (sCodeWin) {
+ loadFormCodeIntoEditor();
+ } else {
+ freeProcBufs();
+ }
} else {
loadFormCodeIntoEditor();
}
@@ -895,7 +1088,6 @@ static void activateFile(int32_t fileIdx, IdeViewModeE view) {
const char *source = target->buffer;
char *diskBuf = NULL;
-
if (!source) {
char fullPath[DVX_MAX_PATH];
prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath));
@@ -919,15 +1111,10 @@ static void activateFile(int32_t fileIdx, IdeViewModeE view) {
parseProcs(source);
free(diskBuf);
+ sEditorFileIdx = fileIdx;
+ sProject.activeFileIdx = fileIdx;
updateDropdowns();
showProc(-1);
-
- if (sEditor && !sEditor->onChange) {
- sEditor->onChange = onEditorChange;
- }
-
- sEditorFileIdx = fileIdx;
- sProject.activeFileIdx = fileIdx;
}
updateProjectMenuState();
@@ -935,6 +1122,45 @@ static void activateFile(int32_t fileIdx, IdeViewModeE view) {
}
+// Adds path to the open project (or activates it when already listed).
+// Returns false when the file lives outside the project directory --
+// PrjFileT.path is project-relative, so such a file could never be
+// read back and the next save would silently create an empty copy.
+static bool addFileToProject(const char *path) {
+ const char *fileName = platformPathBaseName(path);
+ const char *ext = strrchr(path, '.');
+ bool isForm = (ext && strcasecmp(ext, ".frm") == 0);
+ char dir[DVX_MAX_PATH];
+
+ snprintf(dir, sizeof(dir), "%s", path);
+ char *sep = platformPathDirEnd(dir);
+
+ if (sep) {
+ *sep = '\0';
+ } else {
+ dir[0] = '.';
+ dir[1] = '\0';
+ }
+
+ if (strcasecmp(dir, sProject.projectDir) != 0) {
+ dvxErrorBox(sAc, NULL, "Files must be located in the project directory.");
+ return false;
+ }
+
+ for (int32_t i = 0; i < sProject.fileCount; i++) {
+ if (strcasecmp(sProject.files[i].path, fileName) == 0) {
+ activateFile(i, ViewAutoE);
+ return true;
+ }
+ }
+
+ prjAddFile(&sProject, fileName, isForm);
+ prjRebuildTree(&sProject);
+ activateFile(sProject.fileCount - 1, ViewAutoE);
+ return true;
+}
+
+
static void applySyntaxColors(void) {
if (!sEditor) {
return;
@@ -987,14 +1213,14 @@ static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, vo
if (isalpha((unsigned char)ch) || ch == '_') {
int32_t start = i;
- while (i < lineLen && (isalnum((unsigned char)line[i]) || line[i] == '_' || line[i] == '$' || line[i] == '%' || line[i] == '&' || line[i] == '!' || line[i] == '#')) {
+ while (i < lineLen && (basIsIdentChar(line[i]) || basIsTypeSuffixChar(line[i]))) {
i++;
}
int32_t wordLen = i - start;
// Check for REM comment
- if (wordLen == 3 && (line[start] == 'R' || line[start] == 'r') && (line[start + 1] == 'E' || line[start + 1] == 'e') && (line[start + 2] == 'M' || line[start + 2] == 'm')) {
+ if (wordLen == KW_LEN(KW_REM) && strncasecmp(line + start, KW_REM, KW_LEN(KW_REM)) == 0) {
for (int32_t j = start; j < lineLen; j++) {
colors[j] = SYNTAX_COMMENT;
}
@@ -1022,6 +1248,105 @@ static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, vo
}
+// buildSnippetVm -- compile a one-line snippet and return a VM ready to
+// run it. When paused in the debugger, the paused VM's variables are
+// added to the snippet's symbol table (as globals) and their current
+// values copied in, so expressions can reference them by name. On
+// failure returns NULL and copies the error text into errBuf.
+
+static BasVmT *buildSnippetVm(const char *src, BasModuleT **outMod, char *errBuf, int32_t errBufSize) {
+ *outMod = NULL;
+ errBuf[0] = '\0';
+
+ BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
+
+ if (!parser) {
+ snprintf(errBuf, errBufSize, "Out of memory");
+ return NULL;
+ }
+
+ basParserInit(parser, src, (int32_t)strlen(src));
+
+ // Track which debug vars we added and their assigned global indices
+ int32_t varMap[BAS_VM_MAX_GLOBALS]; // maps temp global idx -> debug var idx
+ int32_t varMapCount = 0;
+ bool paused = (sDbgState == DBG_PAUSED && sVm && sDbgModule && sDbgModule->debugVars);
+
+ if (paused) {
+ int32_t curProcIdx = vmProcIndexForPc(sVm->pc);
+
+ for (int32_t i = 0; i < sDbgModule->debugVarCount && varMapCount < BAS_VM_MAX_GLOBALS; i++) {
+ const BasDebugVarT *dv = &sDbgModule->debugVars[i];
+
+ // Skip locals from other procs
+ if (dv->scope == SCOPE_LOCAL && dv->procIndex != curProcIdx) {
+ continue;
+ }
+
+ if (dv->scope == SCOPE_FORM && !sVm->currentFormVars) {
+ continue;
+ }
+
+ if (isMangledDebugName(dv->name)) {
+ continue;
+ }
+
+ // Add to parser's symbol table as a global
+ BasSymbolT *sym = basSymTabAdd(&parser->sym, dv->name, SYM_VARIABLE, dv->dataType);
+
+ if (sym) {
+ sym->scope = SCOPE_GLOBAL;
+ sym->index = varMapCount;
+ varMap[varMapCount] = i;
+ varMapCount++;
+ }
+ }
+
+ parser->cg.globalCount = varMapCount;
+ }
+
+ if (!basParse(parser)) {
+ snprintf(errBuf, errBufSize, "%s", parser->error);
+ basParserFree(parser);
+ free(parser);
+ return NULL;
+ }
+
+ BasModuleT *mod = basParserBuildModule(parser);
+ basParserFree(parser);
+ free(parser);
+
+ if (!mod) {
+ snprintf(errBuf, errBufSize, "Failed to build module");
+ return NULL;
+ }
+
+ BasVmT *vm = basVmCreate();
+
+ if (!vm) {
+ basModuleFree(mod);
+ snprintf(errBuf, errBufSize, "Out of memory");
+ return NULL;
+ }
+
+ basVmLoadModule(vm, mod);
+ vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
+ vm->callDepth = 1;
+
+ // Copy values from the paused VM into the temp VM's globals
+ for (int32_t g = 0; g < varMapCount; g++) {
+ BasValueT *slot = getDebugVarSlot(&sDbgModule->debugVars[varMap[g]]);
+
+ if (slot) {
+ vm->globals[g] = basValCopy(*slot);
+ }
+ }
+
+ *outMod = mod;
+ return vm;
+}
+
+
// ============================================================
// buildVmBreakpoints -- convert IDE breakpoints to VM concat line numbers
// ============================================================
@@ -1034,28 +1359,25 @@ static void buildVmBreakpoints(void) {
arrfree(sVmBreakpoints);
sVmBreakpoints = NULL;
- for (int32_t i = 0; i < sBreakpointCount; i++) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) {
int32_t fileIdx = sBreakpoints[i].fileIdx;
int32_t codeLine = sBreakpoints[i].codeLine;
// Find this file in the source map
for (int32_t m = 0; m < sProject.sourceMapCount; m++) {
if (sProject.sourceMap[m].fileIdx == fileIdx) {
- int32_t base = sProject.sourceMap[m].startLine;
-
- int32_t vmLine = base + codeLine - 1;
+ int32_t vmLine = sProject.sourceMap[m].startLine + codeLine - 1;
arrput(sVmBreakpoints, vmLine);
break;
}
}
}
-
}
static void buildWindow(void) {
// ---- Main toolbar window (top of screen) ----
- sWin = dvxCreateWindow(sAc, "DVX BASIC", 0, 0, sAc->display.width, 200, true);
+ sWin = dvxCreateWindow(sAc, IDE_MAIN_TITLE, 0, 0, sAc->display.width, IDE_TOOLBAR_INIT_H, true);
if (!sWin) {
return;
@@ -1187,7 +1509,7 @@ static void buildWindow(void) {
wgtSetTooltip(tbSave, "Save (Ctrl+S)");
WidgetT *sep1 = wgtVSeparator(tb);
- sep1->minW = wgtPixels(10);
+ sep1->minW = wgtPixels(IDE_TOOLBAR_SEP_W);
// Run group
sTbRun = loadTbIcon(tb, "tb_run", "Run");
@@ -1199,7 +1521,7 @@ static void buildWindow(void) {
wgtSetTooltip(sTbStop, "Stop (Esc)");
WidgetT *sep2 = wgtVSeparator(tb);
- sep2->minW = wgtPixels(10);
+ sep2->minW = wgtPixels(IDE_TOOLBAR_SEP_W);
// Debug group
sTbDebug = loadTbIcon(tb, "tb_debug", "Debug");
@@ -1223,7 +1545,7 @@ static void buildWindow(void) {
wgtSetTooltip(sTbRunToCur, "Run to Cursor (Ctrl+F8)");
WidgetT *sep3 = wgtVSeparator(tb);
- sep3->minW = wgtPixels(10);
+ sep3->minW = wgtPixels(IDE_TOOLBAR_SEP_W);
// View group
sTbCode = loadTbIcon(tb, "tb_code", "Code");
@@ -1248,10 +1570,7 @@ static void buildWindow(void) {
showOutputWindow();
showImmediateWindow();
-
- if (sWin) {
- dvxRaiseWindow(sAc, sWin);
- }
+ dvxRaiseWindow(sAc, sWin);
}
@@ -1283,17 +1602,13 @@ static uint8_t classifyWord(const char *word, int32_t wordLen) {
}
-// ============================================================
-// onFormWinClose
-// ============================================================
-
// cleanupFormWin -- release designer-related state without destroying
// the form window itself (the caller handles that).
static void cleanupFormWin(void) {
sFormWin = NULL;
sDesigner.formWin = NULL;
- // Null out widget pointers — the widgets were destroyed with the window.
+ // Null out widget pointers -- the widgets were destroyed with the window.
// Without this, dsgnCreateWidgets skips controls that have non-NULL
// widget pointers, resulting in an empty form on the second open.
if (sDesigner.form) {
@@ -1318,14 +1633,14 @@ static void cleanupFormWin(void) {
}
-// ============================================================
-// debugLineDecorator -- highlight breakpoints and current debug line
-// ============================================================
-
-
static void clearAllBreakpoints(void) {
arrsetlen(sBreakpoints, 0);
- sBreakpointCount = 0;
+
+ // A live VM still points at sVmBreakpoints; detach before freeing.
+ if (sVm) {
+ basVmSetBreakpoints(sVm, NULL, 0);
+ }
+
arrfree(sVmBreakpoints);
sVmBreakpoints = NULL;
updateBreakpointWindow();
@@ -1339,6 +1654,19 @@ static void clearOutput(void) {
}
+// clearVmStepState -- cancel any pending step/run-to-cursor request
+static void clearVmStepState(void) {
+ if (!sVm) {
+ return;
+ }
+
+ sVm->debugBreak = false;
+ sVm->stepOverDepth = -1;
+ sVm->stepOutDepth = -1;
+ sVm->runToCursorLine = -1;
+}
+
+
static void closeFindDialog(void) {
if (sFindWin) {
dvxDestroyWindow(sAc, sFindWin);
@@ -1355,26 +1683,45 @@ static void closeFindDialog(void) {
}
+// closeProject -- callers prompt for unsaved file edits first (promptAndSave);
+// only the .dbp itself is written here.
static void closeProject(void) {
- if (sProject.projectPath[0] == '\0') {
+ if (!hasProject()) {
return;
}
closeFindDialog();
if (sProject.dirty) {
- prjSave(&sProject);
+ saveProjectFile();
}
- // Close designer windows
- if (sFormWin) {
- dvxDestroyWindow(sAc, sFormWin);
- cleanupFormWin();
+ closeProjectUi();
+ prjClose(&sProject);
+ updateMainTitle();
+ setStatus("Project closed.");
+ updateProjectMenuState();
+}
+
+
+// closeProjectUi -- destroy every per-project window and buffer. Safe to
+// call when nothing is open (every step checks its own state).
+static void closeProjectUi(void) {
+ teardownFormWin();
+
+ // Toolbox/properties can exist without a form window (Window menu)
+ if (sToolboxWin) {
+ tbxDestroy(sAc, sToolboxWin);
+ sToolboxWin = NULL;
+ }
+
+ if (sPropsWin) {
+ prpDestroy(sAc, sPropsWin);
+ sPropsWin = NULL;
}
dsgnFree(&sDesigner);
- // Close code editor
if (sCodeWin) {
dvxDestroyWindow(sAc, sCodeWin);
sCodeWin = NULL;
@@ -1386,23 +1733,10 @@ static void closeProject(void) {
freeProcBufs();
clearAllBreakpoints();
- // Close project window
- prjClose(&sProject);
-
if (sProjectWin) {
prjDestroyWindow(sAc, sProjectWin);
sProjectWin = NULL;
}
-
- if (sWin) {
- dvxSetTitle(sAc, sWin, "DVX BASIC");
- }
-
- if (sStatus) {
- setStatus("Project closed.");
- }
-
- updateProjectMenuState();
}
@@ -1420,8 +1754,14 @@ static int cmpEvtPtrs(const void *a, const void *b) {
}
// Skip brackets for alphabetical comparison
- if (sa[0] == '[') { sa++; }
- if (sb[0] == '[') { sb++; }
+ if (sa[0] == '[') {
+ sa++;
+ }
+
+ if (sb[0] == '[') {
+ sb++;
+ }
+
return strcasecmp(sa, sb);
}
@@ -1440,333 +1780,10 @@ static void compileAndRun(void) {
}
-// ============================================================
-// Compile-time CtrlName.Member validator
-// ============================================================
-//
-// The IDE (unlike bascomp) has widget DXEs loaded, so wgtGetIface
-// returns live interface metadata. Combined with a static scan of
-// the project's .frm files (control name -> widget type), we can
-// reject typos like GfxCanvas.Boggle or LblStatus.Caphtion at
-// compile time instead of letting them surface as runtime errors
-// at event-click time. Dynamically-created controls (via
-// CreateControl at runtime) aren't in the map; lookupCtrlType
-// returns NULL for them and validation is skipped.
-
-typedef struct {
- char name[BAS_MAX_CTRL_NAME];
- char wgtType[BAS_MAX_CTRL_NAME]; // "Form" for the form itself, else iface basName
-} IdeCtrlMapEntryT;
-
-
-typedef struct {
- char ctrlName[BAS_MAX_CTRL_NAME]; // control that has the bad type
- char typeName[BAS_MAX_CTRL_NAME]; // the unrecognised type name
- char formName[BAS_MAX_CTRL_NAME]; // enclosing form (for error message)
-} IdeBadTypeT;
-
-
-typedef struct {
- IdeCtrlMapEntryT *entries; // stb_ds: known controls
- IdeBadTypeT *badTypes; // stb_ds: unknown widget types encountered
- char currentForm[BAS_MAX_CTRL_NAME]; // most recent Begin Form
-} IdeValidatorCtxT;
-
-
-static bool ideValidator_onFormBegin(void *ud, const char *name) {
- IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
- IdeCtrlMapEntryT e;
- memset(&e, 0, sizeof(e));
- snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name ? name : "");
- snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", "Form");
- arrput(v->entries, e);
- snprintf(v->currentForm, BAS_MAX_CTRL_NAME, "%s", name ? name : "");
- return true;
-}
-
-
-static void ideValidator_onCtrlBegin(void *ud, const char *typeName, const char *name) {
- IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
- IdeCtrlMapEntryT e;
- memset(&e, 0, sizeof(e));
- snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name ? name : "");
- snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", typeName ? typeName : "");
- arrput(v->entries, e);
-
- // Also flag unknown widget types. wgtFindByBasName returns NULL
- // for anything not registered by a .wgt DXE loaded by the IDE.
- // Skip internal structural types (they're handled by layout code,
- // not widget DXEs).
- if (typeName && typeName[0]) {
- bool structural = (strcasecmp(typeName, "Form") == 0 ||
- strcasecmp(typeName, "Menu") == 0);
-
- if (!structural && !wgtFindByBasName(typeName)) {
- IdeBadTypeT bad;
- memset(&bad, 0, sizeof(bad));
- snprintf(bad.ctrlName, BAS_MAX_CTRL_NAME, "%s", name ? name : "?");
- snprintf(bad.typeName, BAS_MAX_CTRL_NAME, "%s", typeName);
- snprintf(bad.formName, BAS_MAX_CTRL_NAME, "%s", v->currentForm);
- arrput(v->badTypes, bad);
- }
- }
-}
-
-
-static void ideValidator_onMenuBegin(void *ud, const char *name, int32_t level) {
- (void)level;
- IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
- IdeCtrlMapEntryT e;
- memset(&e, 0, sizeof(e));
- snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name);
- snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", "Menu");
- arrput(v->entries, e);
-}
-
-
-static const char *ideValidator_lookupCtrlType(void *ctx, const char *ctrlName) {
- IdeValidatorCtxT *v = (IdeValidatorCtxT *)ctx;
-
- if (!v || !ctrlName) {
- return NULL;
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(v->entries); i++) {
- if (strcasecmp(v->entries[i].name, ctrlName) == 0) {
- return v->entries[i].wgtType;
- }
- }
-
- return NULL;
-}
-
-
-// Methods that exist on every widget via callCommonMethod() in formrt.c.
-static bool ideValidator_isCommonMethod(const char *methodName) {
- return strcasecmp(methodName, "SetFocus") == 0 ||
- strcasecmp(methodName, "Refresh") == 0 ||
- strcasecmp(methodName, "SetReadOnly") == 0 ||
- strcasecmp(methodName, "SetEnabled") == 0 ||
- strcasecmp(methodName, "SetVisible") == 0 ||
- strcasecmp(methodName, "PopupMenu") == 0 ||
- strcasecmp(methodName, "CreateMenu") == 0 ||
- strcasecmp(methodName, "AddMenuItem") == 0 ||
- strcasecmp(methodName, "AddMenuSeparator") == 0 ||
- strcasecmp(methodName, "AddSubMenu") == 0 ||
- strcasecmp(methodName, "DestroyMenu") == 0 ||
- strcasecmp(methodName, "AddButton") == 0 ||
- strcasecmp(methodName, "AddTextButton") == 0 ||
- strcasecmp(methodName, "AddSeparator") == 0 ||
- strcasecmp(methodName, "Clear") == 0 ||
- strcasecmp(methodName, "ButtonCount") == 0;
-}
-
-
-// Properties that setProp/getProp accept on every non-form, non-menu
-// widget (common props + widget text/data/help aliases).
-static bool ideValidator_isCommonProp(const char *propName) {
- return strcasecmp(propName, "Name") == 0 ||
- strcasecmp(propName, "Left") == 0 ||
- strcasecmp(propName, "Top") == 0 ||
- strcasecmp(propName, "Width") == 0 ||
- strcasecmp(propName, "Height") == 0 ||
- strcasecmp(propName, "MinWidth") == 0 ||
- strcasecmp(propName, "MinHeight") == 0 ||
- strcasecmp(propName, "MaxWidth") == 0 ||
- strcasecmp(propName, "MaxHeight") == 0 ||
- strcasecmp(propName, "Weight") == 0 ||
- strcasecmp(propName, "Visible") == 0 ||
- strcasecmp(propName, "Enabled") == 0 ||
- strcasecmp(propName, "ReadOnly") == 0 ||
- strcasecmp(propName, "TabIndex") == 0 ||
- strcasecmp(propName, "BackColor") == 0 ||
- strcasecmp(propName, "ForeColor") == 0 ||
- strcasecmp(propName, "Caption") == 0 ||
- strcasecmp(propName, "Text") == 0 ||
- strcasecmp(propName, "HelpTopic") == 0 ||
- strcasecmp(propName, "ToolTipText") == 0 ||
- strcasecmp(propName, "ContextMenu") == 0 ||
- strcasecmp(propName, "DataSource") == 0 ||
- strcasecmp(propName, "DataField") == 0 ||
- strcasecmp(propName, "ListCount") == 0;
-}
-
-
-static bool ideValidator_isMethodValid(void *ctx, const char *wgtType, const char *methodName) {
- (void)ctx;
-
- if (!wgtType || !methodName) {
- return true; // be permissive on malformed input
- }
-
- if (ideValidator_isCommonMethod(methodName)) {
- return true;
- }
-
- // Form-level methods
- if (strcasecmp(wgtType, "Form") == 0) {
- return strcasecmp(methodName, "Show") == 0 ||
- strcasecmp(methodName, "Hide") == 0;
- }
-
- // Menu items have no methods beyond common
- if (strcasecmp(wgtType, "Menu") == 0) {
- return false;
- }
-
- // Widget-specific methods from the live iface
- const char *wgtName = wgtFindByBasName(wgtType);
-
- if (!wgtName) {
- return true; // unknown type -- skip validation
- }
-
- const WgtIfaceT *iface = wgtGetIface(wgtName);
-
- if (!iface || !iface->methods) {
- return true;
- }
-
- for (int32_t i = 0; i < iface->methodCount; i++) {
- if (strcasecmp(iface->methods[i].name, methodName) == 0) {
- return true;
- }
- }
-
- return false;
-}
-
-
-static bool ideValidator_isPropValid(void *ctx, const char *wgtType, const char *propName) {
- (void)ctx;
-
- if (!wgtType || !propName) {
- return true;
- }
-
- // Form-level properties
- if (strcasecmp(wgtType, "Form") == 0) {
- return strcasecmp(propName, "Name") == 0 ||
- strcasecmp(propName, "Caption") == 0 ||
- strcasecmp(propName, "Width") == 0 ||
- strcasecmp(propName, "Height") == 0 ||
- strcasecmp(propName, "Left") == 0 ||
- strcasecmp(propName, "Top") == 0 ||
- strcasecmp(propName, "Visible") == 0 ||
- strcasecmp(propName, "Resizable") == 0 ||
- strcasecmp(propName, "AutoSize") == 0 ||
- strcasecmp(propName, "Centered") == 0 ||
- strcasecmp(propName, "ContextMenu") == 0 ||
- strcasecmp(propName, "Layout") == 0;
- }
-
- // Menu items
- if (strcasecmp(wgtType, "Menu") == 0) {
- return strcasecmp(propName, "Name") == 0 ||
- strcasecmp(propName, "Checked") == 0 ||
- strcasecmp(propName, "Enabled") == 0 ||
- strcasecmp(propName, "Visible") == 0 || // top-level: False = popup
- strcasecmp(propName, "RadioCheck") == 0 ||
- strcasecmp(propName, "Caption") == 0;
- }
-
- if (ideValidator_isCommonProp(propName)) {
- return true;
- }
-
- // Widget-specific props from the live iface
- const char *wgtName = wgtFindByBasName(wgtType);
-
- if (!wgtName) {
- return true;
- }
-
- const WgtIfaceT *iface = wgtGetIface(wgtName);
-
- if (!iface || !iface->props) {
- return true;
- }
-
- if (wgtIfaceFindProp(iface, propName)) {
- return true;
- }
-
- return false;
-}
-
-
-// Walk every .frm in the project and populate a (name -> wgtType) map
-// the parser can consult. Caller must arrfree(ctx->entries) when done.
-static void ideBuildCtrlMap(IdeValidatorCtxT *ctx) {
- ctx->entries = NULL;
-
- FrmParserCbsT cbs;
- memset(&cbs, 0, sizeof(cbs));
- cbs.userData = ctx;
- cbs.onFormBegin = ideValidator_onFormBegin;
- cbs.onCtrlBegin = ideValidator_onCtrlBegin;
- cbs.onMenuBegin = ideValidator_onMenuBegin;
-
- for (int32_t i = 0; i < sProject.fileCount; i++) {
- if (!sProject.files[i].isForm) {
- continue;
- }
-
- // Use the buffered source if the file is open in an editor,
- // otherwise load from disk. Mirrors how the designer loads.
- char *diskBuf = NULL;
- const char *src = sProject.files[i].buffer;
- int32_t len = src ? (int32_t)strlen(src) : 0;
-
- if (!src) {
- int32_t dlen = 0;
- char fullPath[DVX_MAX_PATH];
-
- // PrjFileT.path is project-relative; read via the full path so
- // the control map isn't silently empty when the project dir
- // differs from the process CWD.
- prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
- diskBuf = platformReadFile(fullPath, &dlen);
- src = diskBuf;
- len = dlen;
- }
-
- if (src && len > 0) {
- frmParse(src, len, &cbs);
- }
-
- free(diskBuf);
- }
-}
-
-
static bool compileProject(void) {
// Save all dirty files before compiling if Save on Run is enabled
if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) {
- if (sProject.activeFileIdx >= 0) {
- saveActiveFile();
- }
-
- for (int32_t i = 0; i < sProject.fileCount; i++) {
- if (i == sProject.activeFileIdx) {
- continue;
- }
-
- if (sProject.files[i].modified && sProject.files[i].buffer) {
- char fullPath[DVX_MAX_PATH];
- prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
-
- FILE *f = fopen(fullPath, "w");
-
- if (f) {
- fputs(sProject.files[i].buffer, f);
- fclose(f);
- sProject.files[i].modified = false;
- }
- }
- }
-
- updateDirtyIndicators();
+ saveAllModified();
}
clearOutput();
@@ -1779,12 +1796,12 @@ static bool compileProject(void) {
const char *src = NULL;
int32_t srcLen = 0;
- if (sProject.projectPath[0] != '\0' && sProject.fileCount > 0) {
+ if (hasProject() && sProject.fileCount > 0) {
// Stash current editor/designer state into project buffers
+ // (for a form this also writes the editor back to form->code)
stashCurrentFile();
- stashFormCode(); // also stash form code if editor has it
- // Concatenate all .bas files from buffers (or disk if not yet loaded)
+ // Concatenate all files (each normalized to the canonical proc layout)
concatBuf = (char *)malloc(IDE_MAX_SOURCE);
if (!concatBuf) {
@@ -1801,144 +1818,77 @@ static bool compileProject(void) {
// Two passes: .bas modules first (so CONST declarations are
// available), then .frm code sections.
- for (int32_t pass = 0; pass < 2; pass++)
- for (int32_t i = 0; i < sProject.fileCount; i++) {
- // Pass 0: modules only. Pass 1: forms only.
- if (pass == 0 && sProject.files[i].isForm) { continue; }
- if (pass == 1 && !sProject.files[i].isForm) { continue; }
-
- const char *fileSrc = NULL;
- char *diskBuf = NULL;
-
- if (sProject.files[i].isForm) {
- // For .frm files, extract just the code section.
- // If this is the active form in the designer, use form->code.
- if (sDesigner.form && i == sProject.activeFileIdx) {
- fileSrc = sDesigner.form->code;
- } else if (sProject.files[i].buffer) {
- // Parse the .frm to extract the code section using
- // the same parser as the designer (handles nested containers).
- DsgnStateT tmpDs;
- memset(&tmpDs, 0, sizeof(tmpDs));
- dsgnLoadFrm(&tmpDs, sProject.files[i].buffer, (int32_t)strlen(sProject.files[i].buffer));
-
- if (tmpDs.form && tmpDs.form->code) {
- fileSrc = tmpDs.form->code;
- diskBuf = tmpDs.form->code;
- tmpDs.form->code = NULL;
- }
-
- dsgnFree(&tmpDs);
- }
-
- // If no code found from memory, fall through to disk read
- } else {
- fileSrc = sProject.files[i].buffer;
- }
-
- if (!fileSrc) {
- // Not yet loaded into memory -- read from disk
- char fullPath[DVX_MAX_PATH];
- prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
-
- int32_t br = 0;
- diskBuf = platformReadFile(fullPath, &br);
-
- if (!diskBuf) {
+ for (int32_t pass = 0; pass < 2; pass++) {
+ for (int32_t i = 0; i < sProject.fileCount; i++) {
+ // Pass 0: modules only. Pass 1: forms only.
+ if ((pass == 0) == sProject.files[i].isForm) {
continue;
}
- if (br <= 0 || br >= IDE_MAX_SOURCE) {
- free(diskBuf);
- diskBuf = NULL;
- } else {
- fileSrc = diskBuf;
+ char *fileSrc = loadCompileSource(i);
- // For .frm from disk, parse to extract code section
- if (sProject.files[i].isForm) {
- DsgnStateT tmpDs;
- memset(&tmpDs, 0, sizeof(tmpDs));
- dsgnLoadFrm(&tmpDs, diskBuf, br);
-
- if (tmpDs.form && tmpDs.form->code) {
- free(diskBuf);
- diskBuf = tmpDs.form->code;
- fileSrc = diskBuf;
- tmpDs.form->code = NULL;
- } else {
- // No code section: the 'continue' below skips the
- // tail free(diskBuf), so release the file buffer
- // here to avoid leaking it.
- free(diskBuf);
- diskBuf = NULL;
- fileSrc = NULL;
- }
-
- dsgnFree(&tmpDs);
- }
+ if (!fileSrc) {
+ sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[i].path, (int)IDE_MAX_SOURCE);
+ showCompileError("Compilation failed: cannot read file.");
+ free(concatBuf);
+ return false;
}
- }
- if (!fileSrc) {
- continue;
- }
+ int32_t fileLen = (int32_t)strlen(fileSrc);
- // Inject BEGINFORM directive for .frm code sections
- if (sProject.files[i].isForm && sProject.files[i].formName[0]) {
- pos = emitClamped(concatBuf, IDE_MAX_SOURCE, pos, "BEGINFORM \"%s\"\n", sProject.files[i].formName);
- line++;
- }
+ // BEGINFORM/ENDFORM directives plus a trailing newline
+ if (pos + fileLen + IDE_SOURCE_MARGIN >= IDE_MAX_SOURCE) {
+ sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[i].path);
+ showCompileError("Compilation failed: source too large.");
+ free(fileSrc);
+ free(concatBuf);
+ return false;
+ }
- // Record startLine AFTER injected directives so the source
- // map lines match what the editor shows (not the synthetic lines).
- int32_t startLine = line;
-
- int32_t fileLen = (int32_t)strlen(fileSrc);
- int32_t copyLen = fileLen;
-
- if (pos + copyLen >= IDE_MAX_SOURCE - IDE_SOURCE_MARGIN) {
- copyLen = IDE_MAX_SOURCE - IDE_SOURCE_MARGIN - pos;
- }
-
- if (copyLen < 0) {
- copyLen = 0;
- }
-
- memcpy(concatBuf + pos, fileSrc, copyLen);
- pos += copyLen;
-
- // Count lines
- for (int32_t j = 0; j < copyLen; j++) {
- if (fileSrc[j] == '\n') {
+ // Inject BEGINFORM directive for .frm code sections
+ if (sProject.files[i].isForm && sProject.files[i].formName[0]) {
+ pos = emitClamped(concatBuf, IDE_MAX_SOURCE, pos, "BEGINFORM \"%s\"\n", sProject.files[i].formName);
line++;
}
- }
- free(diskBuf);
+ // Record startLine AFTER injected directives so the source
+ // map lines match what the editor shows (not the synthetic lines).
+ int32_t startLine = line;
- // Ensure a trailing newline between files
- if (copyLen > 0 && concatBuf[pos - 1] != '\n' && pos < IDE_MAX_SOURCE - 1) {
- concatBuf[pos++] = '\n';
- line++;
- }
+ memcpy(concatBuf + pos, fileSrc, fileLen);
+ pos += fileLen;
- // Record source map BEFORE injected ENDFORM directive
- {
+ // Count lines
+ for (int32_t j = 0; j < fileLen; j++) {
+ if (fileSrc[j] == '\n') {
+ line++;
+ }
+ }
+
+ free(fileSrc);
+
+ // Ensure a trailing newline between files
+ if (fileLen > 0 && concatBuf[pos - 1] != '\n') {
+ concatBuf[pos++] = '\n';
+ line++;
+ }
+
+ // Record source map BEFORE injected ENDFORM directive
PrjSourceMapT mapEntry;
mapEntry.fileIdx = i;
mapEntry.startLine = startLine;
mapEntry.lineCount = line - startLine;
arrput(sProject.sourceMap, mapEntry);
sProject.sourceMapCount = (int32_t)arrlen(sProject.sourceMap);
- }
- // Inject ENDFORM directive
- if (sProject.files[i].isForm && sProject.files[i].formName[0]) {
- pos = emitClamped(concatBuf, IDE_MAX_SOURCE, pos, "ENDFORM\n");
- line++;
- }
+ // Inject ENDFORM directive
+ if (sProject.files[i].isForm && sProject.files[i].formName[0]) {
+ pos = emitClamped(concatBuf, IDE_MAX_SOURCE, pos, "ENDFORM\n");
+ line++;
+ }
- dvxUpdate(sAc);
+ dvxUpdate(sAc);
+ }
}
concatBuf[pos] = '\0';
@@ -1976,7 +1926,7 @@ static bool compileProject(void) {
// dynamically-created controls (not in the map).
IdeValidatorCtxT validatorCtx;
memset(&validatorCtx, 0, sizeof(validatorCtx));
- ideBuildCtrlMap(&validatorCtx);
+ validatorBuildCtrlMap(&validatorCtx);
// Reject unknown widget types up front -- the .frm parse records
// any `Begin ` whose type isn't registered by a .wgt
@@ -1985,7 +1935,7 @@ static bool compileProject(void) {
if (arrlen(validatorCtx.badTypes) > 0) {
int32_t n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n");
- for (int32_t i = 0; i < (int32_t)arrlen(validatorCtx.badTypes) && n < IDE_MAX_OUTPUT - 256; i++) {
+ for (int32_t i = 0; i < (int32_t)arrlen(validatorCtx.badTypes) && n < IDE_MAX_OUTPUT - IDE_OUTPUT_LINE_RESERVE; i++) {
IdeBadTypeT *b = &validatorCtx.badTypes[i];
n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, n,
" Form '%s' control '%s': type '%s' is not registered.\n",
@@ -1995,15 +1945,7 @@ static bool compileProject(void) {
n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, n,
"\nDVX uses VB6-style widget names (e.g. SpinButton, OptionButton, HScrollBar, CheckBox, DropDown).\n");
sOutputLen = n;
- setOutputText(sOutputBuf);
- showOutputWindow();
-
- if (sOutWin) {
- dvxRaiseWindow(sAc, sOutWin);
- }
-
- setStatus("Compilation failed: unknown widget type.");
- dvxSetBusy(sAc, false);
+ showCompileError("Compilation failed: unknown widget type.");
basParserFree(parser);
free(parser);
free(concatBuf);
@@ -2013,9 +1955,9 @@ static bool compileProject(void) {
}
BasCtrlValidatorT validator;
- validator.lookupCtrlType = ideValidator_lookupCtrlType;
- validator.isMethodValid = ideValidator_isMethodValid;
- validator.isPropValid = ideValidator_isPropValid;
+ validator.lookupCtrlType = validatorLookupCtrlType;
+ validator.isMethodValid = validatorIsMethodValid;
+ validator.isPropValid = validatorIsPropValid;
validator.ctx = &validatorCtx;
basParserSetValidator(parser, &validator);
@@ -2034,8 +1976,8 @@ static bool compileProject(void) {
}
// Navigate to error location and build a user-friendly error message
- char procName[128] = {0};
- int32_t procLine = errLocalLine;
+ char procName[IDE_FULL_NAME_BUF] = {0};
+ int32_t procLine = errLocalLine;
if (parser->errorLine > 0 && errFileIdx >= 0) {
activateFile(errFileIdx, ViewCodeE);
@@ -2056,32 +1998,29 @@ static bool compileProject(void) {
// Strip the "Line NNN: " prefix from the parser error
const char *msg = parser->error;
- if (strncmp(msg, "Line ", 5) == 0) {
- while (*msg && *msg != ':') { msg++; }
- if (*msg == ':') { msg++; }
- while (*msg == ' ') { msg++; }
+
+ if (kwMatch(msg, KW_LINE_PREFIX)) {
+ while (*msg && *msg != ':') {
+ msg++;
+ }
+
+ if (*msg == ':') {
+ msg++;
+ }
+
+ msg = dvxSkipWs(msg);
}
// Show the error with procedure name and proc-relative line
- int32_t n;
if (procName[0]) {
- n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n",
- procName, (int)procLine, msg);
+ sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n",
+ procName, (int)procLine, msg);
} else {
- n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n",
- errFile, (int)errLocalLine, msg);
- }
- sOutputLen = n;
- setOutputText(sOutputBuf);
-
- // Ensure output window is visible
- showOutputWindow();
- if (sOutWin) {
- dvxRaiseWindow(sAc, sOutWin);
+ sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n",
+ errFile, (int)errLocalLine, msg);
}
- setStatus("Compilation failed.");
- dvxSetBusy(sAc, false);
+ showCompileError("Compilation failed.");
basParserFree(parser);
free(parser);
free(concatBuf);
@@ -2137,25 +2076,58 @@ static int32_t countLines(const char *text) {
}
+// createEventSubSkeleton -- append an empty "Sub Obj_Evt ... End Sub" to
+// the proc buffers and show it in the editor. Not marked dirty yet;
+// saveCurProc discards it again if the user adds no code.
+static void createEventSubSkeleton(const char *objName, const char *evtName) {
+ char subName[IDE_SKELETON_BUF];
+ char skeleton[IDE_SKELETON_BUF];
+ const char *params = getEventParams(evtName);
+
+ snprintf(subName, sizeof(subName), "%s_%s", objName, evtName);
+
+ // Control-array handlers take a leading Index argument
+ if (isCtrlArrayInDesigner(objName)) {
+ snprintf(skeleton, sizeof(skeleton), "Sub %s (Index As Integer%s%s)\n\nEnd Sub\n", subName, params[0] ? ", " : "", params);
+ } else {
+ snprintf(skeleton, sizeof(skeleton), "Sub %s (%s)\n\nEnd Sub\n", subName, params);
+ }
+
+ char *buf = strdup(skeleton);
+
+ if (!buf) {
+ return;
+ }
+
+ arrput(sProcBufs, buf);
+
+ // Show the new procedure (it's the last one); showProc stashes the
+ // outgoing editor text and adjusts the index if that discarded an
+ // earlier empty skeleton. Then refresh sProcTable so breakpoints
+ // and line translation know about the new proc.
+ showProc((int32_t)arrlen(sProcBufs) - 1);
+ joinProcBufs();
+}
+
+
static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx) {
AppContextT *ac = (AppContextT *)ctx;
// Convert editor line to file code line
int32_t codeLine = editorLineToCodeLine(lineNum);
-
- int32_t fileIdx = sProject.activeFileIdx;
+ int32_t fileIdx = sEditorFileIdx;
// Breakpoint: red gutter dot
- for (int32_t i = 0; i < sBreakpointCount; i++) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) {
if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) {
- *gutterColor = packColor(&ac->display, 200, 0, 0);
+ *gutterColor = packColor(&ac->display, IDE_BP_GUTTER_R, IDE_BP_GUTTER_G, IDE_BP_GUTTER_B);
break;
}
}
// Current debug line: yellow background (sDbgCurrentLine is editor-local)
if (sDbgState == DBG_PAUSED && lineNum == sDbgCurrentLine) {
- return packColor(&ac->display, 255, 255, 128);
+ return packColor(&ac->display, IDE_DBG_LINE_R, IDE_DBG_LINE_G, IDE_DBG_LINE_B);
}
return 0;
@@ -2188,41 +2160,28 @@ static void debugNavigateToLine(int32_t concatLine) {
// lands on whatever sub owns the saved PC -- typically "(General)"
// or the wrong handler -- and the editor jumps to the wrong place.
const char *procName = NULL;
- char procBuf[128];
+ char procBuf[IDE_FULL_NAME_BUF];
if (sVm && sDbgModule) {
- const char *compiledName = NULL;
- int32_t bestAddr = -1;
- int32_t lookupPc = (sVm->errorMsg[0] && sVm->errorPc > 0)
- ? sVm->errorPc : sVm->pc;
-
- for (int32_t i = 0; i < sDbgModule->procCount; i++) {
- int32_t addr = sDbgModule->procs[i].codeAddr;
-
- if (addr <= lookupPc && addr > bestAddr) {
- bestAddr = addr;
- compiledName = sDbgModule->procs[i].name;
- }
- }
+ int32_t lookupPc = (sVm->errorMsg[0] && sVm->errorPc > 0) ? sVm->errorPc : sVm->pc;
+ int32_t procIdx = vmProcIndexForPc(lookupPc);
// Convert compiled name (Obj_Evt) to dot-separated (Obj.Evt) for matching
- if (compiledName) {
- int32_t procCount = (int32_t)arrlen(sProcTable);
+ if (procIdx >= 0) {
+ const char *compiledName = sDbgModule->procs[procIdx].name;
+ int32_t procCount = (int32_t)arrlen(sProcTable);
for (int32_t i = 0; i < procCount; i++) {
- char fullName[128];
+ char fullName[IDE_FULL_NAME_BUF];
- if (sProcTable[i].objName[0] &&
- strcmp(sProcTable[i].objName, "(General)") != 0) {
- snprintf(fullName, sizeof(fullName), "%s_%s",
- sProcTable[i].objName, sProcTable[i].evtName);
+ if (sProcTable[i].objName[0] && strcmp(sProcTable[i].objName, IDE_GENERAL_SECTION) != 0) {
+ snprintf(fullName, sizeof(fullName), "%s_%s", sProcTable[i].objName, sProcTable[i].evtName);
} else {
snprintf(fullName, sizeof(fullName), "%s", sProcTable[i].evtName);
}
if (strcasecmp(fullName, compiledName) == 0) {
- snprintf(procBuf, sizeof(procBuf), "%s.%s",
- sProcTable[i].objName, sProcTable[i].evtName);
+ snprintf(procBuf, sizeof(procBuf), "%s.%s", sProcTable[i].objName, sProcTable[i].evtName);
procName = procBuf;
break;
}
@@ -2233,6 +2192,28 @@ static void debugNavigateToLine(int32_t concatLine) {
navigateToCodeLine(fileIdx, localLine, procName, true);
}
+
+// debugResume -- common tail of every continue/step command: the caller
+// has already configured the VM's step state.
+static void debugResume(const char *status) {
+ sDbgCurrentLine = -1;
+ sDbgState = DBG_RUNNING;
+ debugSetBreakTitles(false);
+
+ if (sVm) {
+ sVm->debugPaused = false;
+ sVm->running = true;
+ }
+
+ if (sEditor) {
+ wgtInvalidatePaint(sEditor);
+ }
+
+ updateProjectMenuState();
+ setStatus(status);
+}
+
+
static void debugSetBreakTitles(bool paused) {
if (!sDbgFormRt) {
return;
@@ -2246,7 +2227,7 @@ static void debugSetBreakTitles(bool paused) {
}
char *title = form->window->title;
- const char *tag = " [break]";
+ const char *tag = IDE_BREAK_TAG;
int32_t tagLen = (int32_t)strlen(tag);
int32_t titleLen = (int32_t)strlen(title);
@@ -2271,9 +2252,7 @@ static void debugSetBreakTitles(bool paused) {
static void debugStartOrResume(int32_t cmd) {
if (sDbgState == DBG_PAUSED && sVm) {
- // Already paused — apply the appropriate step command and resume
- sDbgCurrentLine = -1;
-
+ // Already paused -- apply the appropriate step command and resume
switch (cmd) {
case CMD_STEP_INTO:
basVmStepInto(sVm);
@@ -2292,24 +2271,17 @@ static void debugStartOrResume(int32_t cmd) {
basVmRunToCursor(sVm, localToConcatLine(wgtTextAreaGetCursorLine(sEditor)));
}
break;
+
+ default:
+ break;
}
- sDbgState = DBG_RUNNING;
- sVm->running = true;
- sVm->debugPaused = false;
- debugSetBreakTitles(false);
-
- if (sEditor) {
- wgtInvalidatePaint(sEditor);
- }
-
- updateProjectMenuState();
- setStatus("Running...");
+ debugResume("Running...");
return;
}
if (sDbgState == DBG_IDLE) {
- // Not running — compile and start in debug mode.
+ // Not running -- compile and start in debug mode.
sDbgEnabled = true;
sDbgBreakOnStart = true;
compileAndRun();
@@ -2318,6 +2290,28 @@ static void debugStartOrResume(int32_t cmd) {
}
+// debugStop -- request the running program to stop. runModule's loops and
+// doEventsCallback observe sStopRequested and unwind.
+static void debugStop(void) {
+ sStopRequested = true;
+
+ if (sVm) {
+ sVm->running = false;
+ sVm->debugPaused = false;
+ }
+
+ sDbgState = DBG_IDLE;
+ sDbgCurrentLine = -1;
+ sDbgEnabled = false;
+
+ if (sEditor) {
+ wgtInvalidatePaint(sEditor);
+ }
+
+ updateProjectMenuState();
+}
+
+
static void debugUpdateWindows(void) {
// Auto-show debug windows if not already open
if (!sLocalsWin) {
@@ -2338,28 +2332,43 @@ static void debugUpdateWindows(void) {
}
+// deleteSelectedControl -- remove the designer's selected control and
+// refresh the properties panel / form window when something changed.
+static void deleteSelectedControl(void) {
+ if (!sDesigner.form || sDesigner.selectedIdx < 0) {
+ return;
+ }
+
+ int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls);
+ dsgnOnKey(&sDesigner, KEY_DELETE);
+ int32_t newCount = (int32_t)arrlen(sDesigner.form->controls);
+
+ if (newCount != prevCount) {
+ prpRebuildTree(&sDesigner);
+ prpRefresh(&sDesigner);
+
+ if (sFormWin) {
+ dvxInvalidateWindow(sAc, sFormWin);
+ }
+ }
+}
+
+
static bool doEventsCallback(void *ctx) {
(void)ctx;
- // Stop if IDE window was closed or DVX is shutting down
- if (!sWin || !sAc->running) {
+ // Stop if IDE window was closed, DVX is shutting down, or the user
+ // pressed Stop (the VM's own pause spin only exits on false here).
+ if (!sWin || !sAc->running || sStopRequested) {
return false;
}
dvxUpdate(sAc);
- return sWin != NULL && sAc->running;
+ return sWin != NULL && sAc->running && !sStopRequested;
}
-// ============================================================
-// onFormWinMouse
-// ============================================================
-//
-// Handle mouse events on the form designer window. Coordinates
-// are relative to the window's client area (content box origin).
-
-
static void dsgnCopySelected(void) {
if (!sDesigner.form || sDesigner.selectedIdx < 0) {
return;
@@ -2371,96 +2380,19 @@ static void dsgnCopySelected(void) {
return;
}
- // Serialize the selected control to FRM text. Sized to hold the
- // worst case (DSGN_MAX_PROPS props x DSGN_MAX_TEXT values + chrome);
- // emitClamped below guarantees no overflow even past this. Heap-
- // allocated so this ~10KB buffer doesn't sit on the task stack.
- int32_t bufSize = DSGN_MAX_PROPS * (DSGN_MAX_NAME + DSGN_MAX_TEXT + 8) + 512;
+ // Serialize the selected control to FRM text with the same writer the
+ // .frm saver uses. Sized to hold the worst case (DSGN_MAX_PROPS props
+ // x DSGN_MAX_TEXT values + chrome); emitClamped guarantees no overflow
+ // even past this. Heap-allocated so this ~10KB buffer doesn't sit on
+ // the task stack.
+ int32_t bufSize = DSGN_MAX_PROPS * (DSGN_MAX_NAME + DSGN_MAX_TEXT + IDE_FRM_LINE_CHROME) + IDE_FRM_BLOCK_CHROME;
char *buf = (char *)malloc((size_t)bufSize);
if (!buf) {
return;
}
- int32_t pos = 0;
- DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx];
- pos = emitClamped(buf, bufSize, pos,"Begin %s %s\n", ctrl->typeName, ctrl->name);
-
- if (ctrl->index >= 0) {
- pos = emitClamped(buf, bufSize, pos," Index = %d\n", (int)ctrl->index);
- }
-
- pos = emitClamped(buf, bufSize, pos," Caption = \"%s\"\n", wgtGetText(ctrl->widget) ? wgtGetText(ctrl->widget) : "");
-
- if (ctrl->width > 0) {
- pos = emitClamped(buf, bufSize, pos," MinWidth = %d\n", (int)ctrl->width);
- }
-
- if (ctrl->height > 0) {
- pos = emitClamped(buf, bufSize, pos," MinHeight = %d\n", (int)ctrl->height);
- }
-
- if (ctrl->maxWidth > 0) {
- pos = emitClamped(buf, bufSize, pos," MaxWidth = %d\n", (int)ctrl->maxWidth);
- }
-
- if (ctrl->maxHeight > 0) {
- pos = emitClamped(buf, bufSize, pos," MaxHeight = %d\n", (int)ctrl->maxHeight);
- }
-
- if (ctrl->weight > 0) {
- pos = emitClamped(buf, bufSize, pos," Weight = %d\n", (int)ctrl->weight);
- }
-
- for (int32_t i = 0; i < ctrl->propCount; i++) {
- if (strcasecmp(ctrl->props[i].name, "Caption") == 0) {
- continue;
- }
-
- pos = emitClamped(buf, bufSize, pos," %s = \"%s\"\n", ctrl->props[i].name, ctrl->props[i].value);
- }
-
- // Save interface properties
- if (ctrl->widget) {
- const char *wgtName = wgtFindByBasName(ctrl->typeName);
- const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;
-
- if (iface) {
- for (int32_t i = 0; i < iface->propCount; i++) {
- const WgtPropDescT *p = &iface->props[i];
-
- if (!p->getFn) {
- continue;
- }
-
- bool already = false;
-
- for (int32_t j = 0; j < ctrl->propCount; j++) {
- if (strcasecmp(ctrl->props[j].name, p->name) == 0) {
- already = true;
- break;
- }
- }
-
- if (already) {
- continue;
- }
-
- // Skip STRING props -- custom props handle those.
- if (p->type == WGT_IFACE_STRING) {
- continue;
- }
-
- char valBuf[DSGN_MAX_TEXT];
-
- if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
- pos = emitClamped(buf, bufSize, pos," %s = %s\n", p->name, valBuf);
- }
- }
- }
- }
-
- pos = emitClamped(buf, bufSize, pos,"End\n");
+ int32_t pos = dsgnEmitControl(sDesigner.form->controls[sDesigner.selectedIdx], buf, bufSize, 0, 0);
dvxClipboardCopy(buf, pos);
free(buf);
}
@@ -2479,12 +2411,12 @@ static void dsgnPasteControl(void) {
}
// Verify it looks like a control definition
- if (strncasecmp(clip, "Begin ", 6) != 0) {
+ if (!kwMatch(clip, FRM_BEGIN_TAG)) {
return;
}
// Parse type and name from "Begin TypeName CtrlName"
- const char *rest = clip + 6;
+ const char *rest = clip + KW_LEN(FRM_BEGIN_TAG);
char typeName[DSGN_MAX_NAME];
char ctrlName[DSGN_MAX_NAME];
@@ -2516,6 +2448,7 @@ static void dsgnPasteControl(void) {
for (int32_t i = 0; i < existCount; i++) {
if (strcasecmp(sDesigner.form->controls[i]->name, ctrlName) == 0) {
nameExists = true;
+
if (sDesigner.form->controls[i]->index > highIdx) {
highIdx = sDesigner.form->controls[i]->index;
}
@@ -2554,7 +2487,14 @@ static void dsgnPasteControl(void) {
dsgnAutoName(&sDesigner, typeName, newName, DSGN_MAX_NAME);
}
- // Create the control
+ // Allocate the control record up front so a failure can't leave a
+ // live widget behind in the content box.
+ DsgnControlT *heapCtrl = (DsgnControlT *)malloc(sizeof(DsgnControlT));
+
+ if (!heapCtrl) {
+ return;
+ }
+
DsgnControlT ctrl;
memset(&ctrl, 0, sizeof(ctrl));
ctrl.index = newIndex;
@@ -2576,7 +2516,7 @@ static void dsgnPasteControl(void) {
line = dvxSkipWs(line);
// "End" terminates
- if (strncasecmp(line, "End", 3) == 0 && (line[3] == '\0' || line[3] == '\r' || line[3] == '\n')) {
+ if (kwMatch(line, FRM_END_TAG) && (line[KW_LEN(FRM_END_TAG)] == '\0' || line[KW_LEN(FRM_END_TAG)] == '\r' || line[KW_LEN(FRM_END_TAG)] == '\n')) {
break;
}
@@ -2645,10 +2585,22 @@ static void dsgnPasteControl(void) {
ctrl.widget = dsgnCreateDesignWidget(typeName, parentWidget);
if (ctrl.widget) {
- if (ctrl.width > 0) { ctrl.widget->minW = wgtPixels(ctrl.width); }
- if (ctrl.height > 0) { ctrl.widget->minH = wgtPixels(ctrl.height); }
- if (ctrl.maxWidth > 0) { ctrl.widget->maxW = wgtPixels(ctrl.maxWidth); }
- if (ctrl.maxHeight > 0) { ctrl.widget->maxH = wgtPixels(ctrl.maxHeight); }
+ if (ctrl.width > 0) {
+ ctrl.widget->minW = wgtPixels(ctrl.width);
+ }
+
+ if (ctrl.height > 0) {
+ ctrl.widget->minH = wgtPixels(ctrl.height);
+ }
+
+ if (ctrl.maxWidth > 0) {
+ ctrl.widget->maxW = wgtPixels(ctrl.maxWidth);
+ }
+
+ if (ctrl.maxHeight > 0) {
+ ctrl.widget->maxH = wgtPixels(ctrl.maxHeight);
+ }
+
ctrl.widget->weight = ctrl.weight;
wgtSetName(ctrl.widget, ctrl.name);
@@ -2693,7 +2645,6 @@ static void dsgnPasteControl(void) {
}
}
- DsgnControlT *heapCtrl = malloc(sizeof(DsgnControlT));
*heapCtrl = ctrl;
arrput(sDesigner.form->controls, heapCtrl);
sDesigner.selectedIdx = (int32_t)arrlen(sDesigner.form->controls) - 1;
@@ -2725,7 +2676,7 @@ static int32_t editorLineToCodeLine(int32_t editorLine) {
// a matching .frm file if one exists alongside the .bas.
static void ensureProject(const char *filePath) {
- if (sProject.projectPath[0] != '\0') {
+ if (hasProject()) {
return;
}
@@ -2800,19 +2751,40 @@ static void ensureProject(const char *filePath) {
sProject.activeFileIdx = -1;
prjLoadAllFiles(&sProject, sAc);
-
- char title[300];
- snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
-
- if (sWin) {
- dvxSetTitle(sAc, sWin, title);
- }
-
+ updateMainTitle();
setStatus("Project created.");
updateProjectMenuState();
}
+// ensureProjectWindow -- create the Project Explorer if needed (wired to
+// the shared menu/accelerator handlers), otherwise refresh its tree.
+static void ensureProjectWindow(bool raise) {
+ if (sProjectWin) {
+ prjRebuildTree(&sProject);
+ } else {
+ sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);
+
+ if (!sProjectWin) {
+ return;
+ }
+
+ sProjectWin->y = toolbarBottom() + IDE_PROJECT_WIN_OFFSET;
+ sProjectWin->onClose = onProjectWinClose;
+ sProjectWin->onMenu = onMenu;
+ sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
+ }
+
+ if (raise) {
+ dvxRaiseWindow(sAc, sProjectWin);
+ }
+}
+
+
+// evaluateImmediate -- compile and execute a single line from the
+// Immediate window. If the line doesn't start with a statement keyword,
+// wrap it in PRINT so expressions produce visible output.
+
static void evaluateImmediate(const char *expr) {
if (!expr || *expr == '\0') {
return;
@@ -2823,54 +2795,27 @@ static void evaluateImmediate(const char *expr) {
return;
}
- char wrapped[1024];
+ char wrapped[IDE_IMM_WRAP_BUF];
// If it already starts with a statement keyword, use as-is
- if (strncasecmp(expr, "PRINT", 5) == 0 || strncasecmp(expr, "DIM", 3) == 0 || strncasecmp(expr, "LET", 3) == 0) {
+ if (kwMatch(expr, KW_PRINT) || kwMatch(expr, KW_DIM) || kwMatch(expr, KW_LET)) {
snprintf(wrapped, sizeof(wrapped), "%s", expr);
} else {
- snprintf(wrapped, sizeof(wrapped), "PRINT %s", expr);
+ snprintf(wrapped, sizeof(wrapped), "%s %s", KW_PRINT, expr);
}
- BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
+ BasModuleT *mod = NULL;
+ char err[IDE_VALUE_BUF];
+ BasVmT *vm = buildSnippetVm(wrapped, &mod, err, sizeof(err));
- if (!parser) {
- return;
- }
-
- basParserInit(parser, wrapped, (int32_t)strlen(wrapped));
-
- if (!basParse(parser)) {
- // Show error inline
+ if (!vm) {
immPrintCallback(NULL, "Error: ", false);
- immPrintCallback(NULL, parser->error, true);
- basParserFree(parser);
- free(parser);
+ immPrintCallback(NULL, err, true);
return;
}
- BasModuleT *mod = basParserBuildModule(parser);
- basParserFree(parser);
- free(parser);
-
- if (!mod) {
- return;
- }
-
- BasVmT *vm = basVmCreate();
- basVmLoadModule(vm, mod);
- vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
- vm->callDepth = 1;
basVmSetPrintCallback(vm, immPrintCallback, NULL);
- // If paused at a breakpoint, copy globals from the running VM
- // so the immediate window can inspect current variable values
- if (sDbgState == DBG_PAUSED && sVm && sDbgModule) {
- for (int32_t g = 0; g < BAS_VM_MAX_GLOBALS && g < sDbgModule->globalCount; g++) {
- vm->globals[g] = basValCopy(sVm->globals[g]);
- }
- }
-
BasVmResultE result = basVmRun(vm);
if (result != BAS_VM_HALTED && result != BAS_VM_OK && result != BAS_VM_BREAKPOINT) {
@@ -2883,107 +2828,29 @@ static void evaluateImmediate(const char *expr) {
}
+// evalWatchExpr -- compile and evaluate an expression using the paused VM's
+// state. Used as a fallback when lookupWatchVar can't handle the
+// expression. Returns the printed result in outBuf.
+
static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize) {
- if (!sVm || !sDbgModule || !sDbgModule->debugVars) {
+ if (sDbgState != DBG_PAUSED || !sVm || !sDbgModule || !sDbgModule->debugVars) {
return false;
}
- // Wrap expression: PRINT expr
- char wrapped[512];
- snprintf(wrapped, sizeof(wrapped), "PRINT %s", expr);
+ char wrapped[IDE_IMM_WRAP_BUF];
+ snprintf(wrapped, sizeof(wrapped), "%s %s", KW_PRINT, expr);
- BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
+ BasModuleT *mod = NULL;
+ char err[IDE_VALUE_BUF];
+ BasVmT *tvm = buildSnippetVm(wrapped, &mod, err, sizeof(err));
- if (!parser) {
+ if (!tvm) {
return false;
}
- basParserInit(parser, wrapped, (int32_t)strlen(wrapped));
-
- // Pre-populate the symbol table with debug vars from the paused VM.
- // All variables are added as globals so the expression can reference them.
- // Find current proc for local variable matching.
- int32_t curProcIdx = -1;
- int32_t bestAddr = -1;
-
- for (int32_t i = 0; i < sDbgModule->procCount; i++) {
- int32_t addr = sDbgModule->procs[i].codeAddr;
-
- if (addr <= sVm->pc && addr > bestAddr) {
- bestAddr = addr;
- curProcIdx = i;
- }
- }
-
- // Track which debug vars we added and their assigned global indices
- int32_t varMap[BAS_VM_MAX_GLOBALS]; // maps temp global idx -> debug var idx
- int32_t varMapCount = 0;
-
- for (int32_t i = 0; i < sDbgModule->debugVarCount && varMapCount < BAS_VM_MAX_GLOBALS; i++) {
- const BasDebugVarT *dv = &sDbgModule->debugVars[i];
-
- // Skip locals from other procs
- if (dv->scope == SCOPE_LOCAL && dv->procIndex != curProcIdx) {
- continue;
- }
-
- if (dv->scope == SCOPE_FORM && !sVm->currentFormVars) {
- continue;
- }
-
- // Skip mangled names
- const char *dollar = strchr(dv->name, '$');
-
- if (dollar && dollar[1] != '\0') {
- continue;
- }
-
- // Add to parser's symbol table as a global
- BasSymbolT *sym = basSymTabAdd(&parser->sym, dv->name, SYM_VARIABLE, dv->dataType);
-
- if (sym) {
- sym->scope = SCOPE_GLOBAL;
- sym->index = varMapCount;
- varMap[varMapCount] = i;
- varMapCount++;
- }
- }
-
- parser->cg.globalCount = varMapCount;
-
- // Parse and compile
- if (!basParse(parser)) {
- basParserFree(parser);
- free(parser);
- return false;
- }
-
- BasModuleT *mod = basParserBuildModule(parser);
- basParserFree(parser);
- free(parser);
-
- if (!mod) {
- return false;
- }
-
- // Create temp VM
- BasVmT *tvm = basVmCreate();
- basVmLoadModule(tvm, mod);
- tvm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
- tvm->callDepth = 1;
-
- // Copy values from the paused VM into the temp VM's globals
- for (int32_t g = 0; g < varMapCount; g++) {
- const BasDebugVarT *dv = &sDbgModule->debugVars[varMap[g]];
- BasValueT val;
- memset(&val, 0, sizeof(val));
- readDebugVar(dv, &val);
- tvm->globals[g] = basValCopy(val);
- }
-
// Set up capture callback
sWatchPrintBuf[0] = '\0';
- sWatchPrintLen = 0;
+ sWatchPrintLen = 0;
basVmSetPrintCallback(tvm, watchPrintCallback, NULL);
// Run
@@ -3032,18 +2899,14 @@ static char *extractNewProcs(const char *buf) {
const char *pos = buf;
while (*pos) {
- const char *trimmed = dvxSkipWs(pos);
+ bool isSub;
- bool isSub = (strncasecmp(trimmed, "SUB ", 4) == 0);
- bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0);
-
- if (isSub || isFunc) {
+ if (procDeclAt(dvxSkipWs(pos), &isSub)) {
found = true;
break;
}
- while (*pos && *pos != '\n') { pos++; }
- if (*pos == '\n') { pos++; }
+ pos = skipLine(pos);
}
if (!found) {
@@ -3063,45 +2926,23 @@ static char *extractNewProcs(const char *buf) {
while (*pos) {
const char *lineStart = pos;
- const char *trimmed = dvxSkipWs(pos);
+ bool isSub = false;
+ const char *afterKw = procDeclAt(dvxSkipWs(pos), &isSub);
- bool isSub = (strncasecmp(trimmed, "SUB ", 4) == 0);
- bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0);
-
- if (isSub || isFunc) {
+ if (afterKw) {
// Extract procedure name for duplicate check
- const char *np = dvxSkipWs(trimmed + (isSub ? 4 : 9));
-
- char newName[128];
- int32_t nn = 0;
-
- while (*np && *np != '(' && *np != ' ' && *np != '\t' && *np != '\n' && nn < 127) {
- newName[nn++] = *np++;
- }
-
- newName[nn] = '\0';
+ char newName[IDE_FULL_NAME_BUF];
+ procNameFromDecl(afterKw, newName, sizeof(newName));
// Check for duplicate against existing proc buffers
bool isDuplicate = false;
for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs); p++) {
- if (!sProcBufs[p]) { continue; }
+ bool existIsSub = false;
+ const char *existDecl = procDeclAt(dvxSkipWs(sProcBufs[p]), &existIsSub);
+ char existName[IDE_FULL_NAME_BUF];
- const char *ep = dvxSkipWs(sProcBufs[p]);
-
- if (strncasecmp(ep, "SUB ", 4) == 0) { ep += 4; }
- else if (strncasecmp(ep, "FUNCTION ", 9) == 0) { ep += 9; }
-
- ep = dvxSkipWs(ep);
-
- char existName[128];
- int32_t en = 0;
-
- while (*ep && *ep != '(' && *ep != ' ' && *ep != '\t' && *ep != '\n' && en < 127) {
- existName[en++] = *ep++;
- }
-
- existName[en] = '\0';
+ procNameFromDecl(existDecl ? existDecl : sProcBufs[p], existName, sizeof(existName));
if (strcasecmp(newName, existName) == 0) {
isDuplicate = true;
@@ -3110,35 +2951,15 @@ static char *extractNewProcs(const char *buf) {
}
// Find End Sub / End Function
- const char *endTag = isSub ? "END SUB" : "END FUNCTION";
- int32_t endTagLen = isSub ? 7 : 12;
- const char *scan = pos;
-
- while (*scan && *scan != '\n') { scan++; }
- if (*scan == '\n') { scan++; }
-
- while (*scan) {
- const char *sl = dvxSkipWs(scan);
-
- if (strncasecmp(sl, endTag, endTagLen) == 0) {
- while (*scan && *scan != '\n') { scan++; }
- if (*scan == '\n') { scan++; }
- break;
- }
-
- while (*scan && *scan != '\n') { scan++; }
- if (*scan == '\n') { scan++; }
- }
+ int32_t endOffset = 0;
+ const char *scan = skipToEndProc(pos, isSub, &endOffset);
if (isDuplicate) {
// Leave it in the General section -- compiler will report the error
- while (*pos && *pos != '\n') {
- remaining[remPos++] = *pos++;
- }
-
- if (*pos == '\n') {
- remaining[remPos++] = *pos++;
- }
+ const char *next = skipLine(pos);
+ memcpy(remaining + remPos, pos, next - pos);
+ remPos += (int32_t)(next - pos);
+ pos = next;
} else {
// Extract this procedure into a new buffer
int32_t procLen = (int32_t)(scan - lineStart);
@@ -3157,13 +2978,10 @@ static char *extractNewProcs(const char *buf) {
}
// Copy non-proc lines to remaining
- while (*pos && *pos != '\n') {
- remaining[remPos++] = *pos++;
- }
-
- if (*pos == '\n') {
- remaining[remPos++] = *pos++;
- }
+ const char *next = skipLine(pos);
+ memcpy(remaining + remPos, pos, next - pos);
+ remPos += (int32_t)(next - pos);
+ pos = next;
}
remaining[remPos] = '\0';
@@ -3173,22 +2991,11 @@ static char *extractNewProcs(const char *buf) {
// findDebugVar -- find a debug variable by name, respecting scope
static const BasDebugVarT *findDebugVar(const char *name) {
- if (!sDbgModule || !sDbgModule->debugVars) {
+ if (!sVm || !sDbgModule || !sDbgModule->debugVars) {
return NULL;
}
- // Find current proc index
- int32_t curProcIdx = -1;
- int32_t bestAddr = -1;
-
- for (int32_t i = 0; i < sDbgModule->procCount; i++) {
- int32_t addr = sDbgModule->procs[i].codeAddr;
-
- if (addr <= sVm->pc && addr > bestAddr) {
- bestAddr = addr;
- curProcIdx = i;
- }
- }
+ int32_t curProcIdx = vmProcIndexForPc(sVm->pc);
for (int32_t i = 0; i < sDbgModule->debugVarCount; i++) {
const BasDebugVarT *dv = &sDbgModule->debugVars[i];
@@ -3212,6 +3019,12 @@ static const BasDebugVarT *findDebugVar(const char *name) {
}
+// findInProject -- search all project files for a text match.
+// Starts from the current editor position in the current file,
+// then continues through subsequent files, wrapping around.
+// Only the file containing the match is activated; other files are
+// searched in their project buffers.
+
static bool findInProject(const char *needle, bool caseSensitive, bool forward) {
if (!needle || !needle[0] || sProject.fileCount == 0) {
return false;
@@ -3311,6 +3124,15 @@ static bool findInProject(const char *needle, bool caseSensitive, bool forward)
for (int32_t attempt = 0; attempt < filesToSearch; attempt++) {
int32_t fileIdx = (startFile + step * attempt + sProject.fileCount * sProject.fileCount) % sProject.fileCount;
+ // Search the file's buffer without activating it. The code of a
+ // .frm is a subset of its buffer, so a match in the .frm chrome
+ // just costs an activation that finds nothing.
+ const char *text = sProject.files[fileIdx].buffer;
+
+ if (!procBufContains(text, needle, needleLen, caseSensitive)) {
+ continue;
+ }
+
// Activate the file to load its proc buffers
activateFile(fileIdx, sProject.files[fileIdx].isForm ? ViewCodeE : ViewAutoE);
@@ -3361,21 +3183,56 @@ static const char *findSubstrNoCase(const char *haystack, const char *needle, in
}
+// findUdtFieldIdx -- field index within a UDT from the debug type table
+static int32_t findUdtFieldIdx(int32_t typeId, const char *fieldName) {
+ for (int32_t t = 0; t < sDbgModule->debugUdtDefCount; t++) {
+ if (sDbgModule->debugUdtDefs[t].typeId != typeId) {
+ continue;
+ }
+
+ for (int32_t f = 0; f < sDbgModule->debugUdtDefs[t].fieldCount; f++) {
+ if (strcasecmp(sDbgModule->debugUdtDefs[t].fields[f].name, fieldName) == 0) {
+ return f;
+ }
+ }
+
+ return -1;
+ }
+
+ return -1;
+}
+
+
static void formatValue(const BasValueT *v, char *buf, int32_t bufSize) {
switch (v->type) {
- case BAS_TYPE_INTEGER: snprintf(buf, bufSize, "%d", (int)v->intVal); break;
- case BAS_TYPE_LONG: snprintf(buf, bufSize, "%ld", (long)v->longVal); break;
- case BAS_TYPE_SINGLE: snprintf(buf, bufSize, "%.6g", (double)v->sngVal); break;
- case BAS_TYPE_DOUBLE: snprintf(buf, bufSize, "%.10g", v->dblVal); break;
- case BAS_TYPE_BOOLEAN: snprintf(buf, bufSize, "%s", v->boolVal ? "True" : "False"); break;
- case BAS_TYPE_STRING: {
+ case BAS_TYPE_INTEGER:
+ snprintf(buf, bufSize, "%d", (int)v->intVal);
+ break;
+
+ case BAS_TYPE_LONG:
+ snprintf(buf, bufSize, "%ld", (long)v->longVal);
+ break;
+
+ case BAS_TYPE_SINGLE:
+ snprintf(buf, bufSize, "%.6g", (double)v->sngVal);
+ break;
+
+ case BAS_TYPE_DOUBLE:
+ snprintf(buf, bufSize, "%.10g", v->dblVal);
+ break;
+
+ case BAS_TYPE_BOOLEAN:
+ snprintf(buf, bufSize, "%s", v->boolVal ? "True" : "False");
+ break;
+
+ case BAS_TYPE_STRING:
if (v->strVal) {
- snprintf(buf, bufSize, "\"%.*s\"", (int)(bufSize - 3), v->strVal->data);
+ snprintf(buf, bufSize, "\"%.*s\"", (int)(bufSize - IDE_QUOTE_OVERHEAD), v->strVal->data);
} else {
snprintf(buf, bufSize, "\"\"");
}
break;
- }
+
case BAS_TYPE_ARRAY: {
BasArrayT *arr = v->arrVal;
@@ -3389,40 +3246,31 @@ static void formatValue(const BasValueT *v, char *buf, int32_t bufSize) {
// 'bufSize - pos' go negative (huge size_t -> OOB write).
int32_t pos = emitClamped(buf, bufSize, 0, "%s(", typeNameStr(arr->elementType));
- for (int32_t d = 0; d < arr->dims && pos < bufSize - 10; d++) {
+ for (int32_t d = 0; d < arr->dims && pos < bufSize - IDE_ARRAY_DIM_RESERVE; d++) {
if (d > 0) {
pos = emitClamped(buf, bufSize, pos, ", ");
}
- pos = emitClamped(buf, bufSize, pos, "%d To %d",
- (int)arr->lbound[d], (int)arr->ubound[d]);
+ pos = emitClamped(buf, bufSize, pos, "%d To %d", (int)arr->lbound[d], (int)arr->ubound[d]);
}
emitClamped(buf, bufSize, pos, ") [%d]", (int)arr->totalElements);
break;
}
- default: snprintf(buf, bufSize, "..."); break;
+
+ default:
+ snprintf(buf, bufSize, "...");
+ break;
}
}
-// ============================================================
-// updateDropdowns
-// ============================================================
-//
-// Scan the source for SUB/FUNCTION declarations and populate
-// the Object and Event dropdowns. Procedure names are split on
-// '_' into ObjectName and EventName (e.g. "Command1_Click").
-
// freeProcBufs -- release all procedure buffers
static void freeProcBufs(void) {
free(sGeneralBuf);
sGeneralBuf = NULL;
- free(sParsedSource);
- sParsedSource = NULL;
-
for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) {
free(sProcBufs[i]);
}
@@ -3431,6 +3279,7 @@ static void freeProcBufs(void) {
sProcBufs = NULL;
sCurProcIdx = -2;
sEditorFileIdx = -1;
+ arrsetlen(sProcTable, 0);
}
@@ -3460,14 +3309,15 @@ static BasValueT *getDebugVarSlot(const BasDebugVarT *dv) {
}
-static const char *getEventExtraParams(const char *evtName) {
- if (strcasecmp(evtName, "KeyPress") == 0) { return ", KeyAscii As Integer"; }
- if (strcasecmp(evtName, "KeyDown") == 0) { return ", KeyCode As Integer, Shift As Integer"; }
- if (strcasecmp(evtName, "KeyUp") == 0) { return ", KeyCode As Integer, Shift As Integer"; }
- if (strcasecmp(evtName, "MouseDown") == 0) { return ", Button As Integer, X As Integer, Y As Integer"; }
- if (strcasecmp(evtName, "MouseUp") == 0) { return ", Button As Integer, X As Integer, Y As Integer"; }
- if (strcasecmp(evtName, "MouseMove") == 0) { return ", Button As Integer, X As Integer, Y As Integer"; }
- if (strcasecmp(evtName, "Scroll") == 0) { return ", Delta As Integer"; }
+// Parameter list the runtime passes to a handler of evtName ("" when
+// none, or for events not in the master table).
+static const char *getEventParams(const char *evtName) {
+ for (int32_t i = 0; i < IDE_EVENT_COUNT; i++) {
+ if (strcasecmp(sEventTable[i].suffix, evtName) == 0) {
+ return sEventTable[i].params;
+ }
+ }
+
return "";
}
@@ -3486,11 +3336,6 @@ static bool getFindMatchCase(void) {
}
-// ============================================================
-// Find/Replace dialog (modeless)
-// ============================================================
-
-
static FindScopeE getFindScope(void) {
if (!sScopeGroup) {
return ScopeProjE;
@@ -3499,82 +3344,29 @@ static FindScopeE getFindScope(void) {
int32_t idx = wgtRadioGetIndex(sScopeGroup);
switch (idx) {
- case 0: return ScopeFuncE;
- case 1: return ScopeObjE;
- case 2: return ScopeFileE;
- default: return ScopeProjE;
+ case 0:
+ return ScopeFuncE;
+
+ case 1:
+ return ScopeObjE;
+
+ case 2:
+ return ScopeFileE;
+
+ default:
+ return ScopeProjE;
}
}
-// getFullSource -- reassemble all buffers into one source string.
-// Returns a pointer into an internal cache (sFullSourceCache) that is
-// owned by getFullSource and freed on the next call. Callers must NOT
-// free it; strdup it if the result must outlive the next call.
-
-static char *sFullSourceCache = NULL;
+// getFullSource -- save the editor and reassemble all buffers into one
+// source string. Returns a pointer into an internal cache that is owned
+// by joinProcBufs and freed on the next call. Callers must NOT free it;
+// strdup it if the result must outlive the next call.
static const char *getFullSource(void) {
saveCurProc();
-
- free(sFullSourceCache);
-
- // Calculate total length
- int32_t totalLen = 0;
-
- if (sGeneralBuf && sGeneralBuf[0]) {
- totalLen += (int32_t)strlen(sGeneralBuf);
- totalLen += 2; // blank line separator
- }
-
- int32_t procCount = (int32_t)arrlen(sProcBufs);
-
- for (int32_t i = 0; i < procCount; i++) {
- if (sProcBufs[i]) {
- totalLen += (int32_t)strlen(sProcBufs[i]);
- totalLen += 2; // newline + blank line between procedures
- }
- }
-
- sFullSourceCache = (char *)malloc(totalLen + 1);
-
- if (!sFullSourceCache) {
- return "";
- }
-
- int32_t pos = 0;
-
- if (sGeneralBuf && sGeneralBuf[0]) {
- int32_t len = (int32_t)strlen(sGeneralBuf);
- memcpy(sFullSourceCache + pos, sGeneralBuf, len);
- pos += len;
-
- if (pos > 0 && sFullSourceCache[pos - 1] != '\n') {
- sFullSourceCache[pos++] = '\n';
- }
-
- sFullSourceCache[pos++] = '\n';
- }
-
- for (int32_t i = 0; i < procCount; i++) {
- if (sProcBufs[i]) {
- int32_t len = (int32_t)strlen(sProcBufs[i]);
- memcpy(sFullSourceCache + pos, sProcBufs[i], len);
- pos += len;
-
- if (pos > 0 && sFullSourceCache[pos - 1] != '\n') {
- sFullSourceCache[pos++] = '\n';
- }
-
- // Blank line between procedures
- if (i < procCount - 1) {
- sFullSourceCache[pos++] = '\n';
- }
- }
- }
-
- sFullSourceCache[pos] = '\0';
- return sFullSourceCache;
+ return joinProcBufs();
}
@@ -3608,16 +3400,8 @@ static void handleEditCmd(int32_t cmd) {
}
case CMD_DELETE:
- if (sFormWin && sDesigner.selectedIdx >= 0) {
- int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls);
- dsgnOnKey(&sDesigner, KEY_DELETE);
- int32_t newCount = (int32_t)arrlen(sDesigner.form->controls);
-
- if (newCount != prevCount) {
- prpRebuildTree(&sDesigner);
- prpRefresh(&sDesigner);
- dvxInvalidateWindow(sAc, sFormWin);
- }
+ if (sFormWin) {
+ deleteSelectedControl();
}
break;
@@ -3636,14 +3420,23 @@ static void handleEditCmd(int32_t cmd) {
case CMD_REPLACE:
openFindDialog(true);
break;
+
+ default:
+ break;
}
}
+// Accelerators bypass the menu enabled state, so anything that changes
+// the project or the compiled module is gated on the program being idle.
static void handleFileCmd(int32_t cmd) {
+ bool isIdle = (sDbgState == DBG_IDLE);
+
switch (cmd) {
case CMD_OPEN:
- loadFile();
+ if (isIdle) {
+ loadFile();
+ }
break;
case CMD_SAVE:
@@ -3651,38 +3444,15 @@ static void handleFileCmd(int32_t cmd) {
break;
case CMD_SAVE_ALL:
- saveFile();
-
- for (int32_t i = 0; i < sProject.fileCount; i++) {
- if (i == sProject.activeFileIdx) {
- continue;
- }
-
- if (sProject.files[i].modified && sProject.files[i].buffer) {
- char fullPath[DVX_MAX_PATH];
- prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
-
- FILE *f = fopen(fullPath, "w");
-
- if (f) {
- fputs(sProject.files[i].buffer, f);
- fclose(f);
- sProject.files[i].modified = false;
- }
- }
+ if (saveAllModified()) {
+ setStatus("All files saved.");
}
-
- if (sProject.projectPath[0] != '\0') {
- prjSave(&sProject);
- sProject.dirty = false;
- }
-
- setStatus("All files saved.");
- updateDirtyIndicators();
break;
case CMD_MAKE_EXE:
- makeExecutable();
+ if (isIdle) {
+ makeExecutable();
+ }
break;
case CMD_EXIT:
@@ -3693,7 +3463,7 @@ static void handleFileCmd(int32_t cmd) {
default:
// Recent files
- if (cmd >= CMD_RECENT_BASE && cmd < CMD_RECENT_BASE + CMD_RECENT_MAX) {
+ if (isIdle && cmd >= CMD_RECENT_BASE && cmd < CMD_RECENT_BASE + CMD_RECENT_MAX) {
recentOpen(cmd - CMD_RECENT_BASE);
}
break;
@@ -3702,35 +3472,37 @@ static void handleFileCmd(int32_t cmd) {
static void handleProjectCmd(int32_t cmd) {
+ bool isIdle = (sDbgState == DBG_IDLE);
+
switch (cmd) {
case CMD_PRJ_NEW:
- newProject();
+ if (isIdle) {
+ newProject();
+ }
break;
case CMD_PRJ_OPEN:
- openProject();
+ if (isIdle) {
+ openProject();
+ }
break;
case CMD_PRJ_SAVE:
- if (sProject.projectPath[0] != '\0') {
- prjSave(&sProject);
- sProject.dirty = false;
+ if (hasProject() && saveProjectFile()) {
setStatus("Project saved.");
}
break;
case CMD_PRJ_CLOSE:
- if (promptAndSave()) {
+ if (isIdle && promptAndSave()) {
closeProject();
}
break;
case CMD_PRJ_PROPS:
- if (sProject.projectPath[0] != '\0') {
+ if (hasProject()) {
if (prjPropertiesDialog(sAc, &sProject, sCtx->appPath)) {
- char title[300];
- snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
- dvxSetTitle(sAc, sWin, title);
+ updateMainTitle();
if (sProjectWin) {
prjRebuildTree(&sProject);
@@ -3742,41 +3514,64 @@ static void handleProjectCmd(int32_t cmd) {
case CMD_PRJ_REMOVE: {
int32_t rmIdx = prjGetSelectedFileIdx();
- if (rmIdx >= 0 && rmIdx < sProject.fileCount) {
- PrjFileT *rmFile = &sProject.files[rmIdx];
- char rmMsg[DVX_MAX_PATH + 32];
- snprintf(rmMsg, sizeof(rmMsg), "Remove %s from the project?", rmFile->path);
+ if (!isIdle || rmIdx < 0 || rmIdx >= sProject.fileCount) {
+ break;
+ }
- if (dvxMessageBox(sAc, "Remove File", rmMsg, MB_YESNO | MB_ICONQUESTION) != ID_YES) {
+ PrjFileT *rmFile = &sProject.files[rmIdx];
+ char rmMsg[IDE_MSG_BUF];
+ snprintf(rmMsg, sizeof(rmMsg), "Remove %s from the project?", rmFile->path);
+
+ if (dvxMessageBox(sAc, "Remove File", rmMsg, MB_YESNO | MB_ICONQUESTION) != ID_YES) {
+ break;
+ }
+
+ if (rmIdx == sProject.activeFileIdx) {
+ stashCurrentFile();
+ }
+
+ if (rmFile->modified) {
+ int32_t result = dvxPromptSave(sAc, IDE_MAIN_TITLE);
+
+ if (result == DVX_SAVE_CANCEL) {
break;
}
- if (rmFile->modified) {
- int32_t result = dvxPromptSave(sAc, "DVX BASIC");
-
- if (result == DVX_SAVE_CANCEL) {
- break;
- }
-
- if (result == DVX_SAVE_YES) {
- saveActiveFile();
- }
+ if (result == DVX_SAVE_YES && !writeProjectFile(rmIdx)) {
+ break;
}
-
- removeBreakpointsForFile(rmIdx);
- prjRemoveFile(&sProject, rmIdx);
-
- if (sProject.activeFileIdx == rmIdx) {
- sProject.activeFileIdx = -1;
- } else if (sProject.activeFileIdx > rmIdx) {
- sProject.activeFileIdx--;
- }
-
- prjRebuildTree(&sProject);
- updateProjectMenuState();
}
+
+ // Detach the editor/designer from the file before it goes away
+ if (rmIdx == sEditorFileIdx) {
+ freeProcBufs();
+
+ if (sEditor) {
+ void (*savedOnChange)(WidgetT *) = sEditor->onChange;
+ sEditor->onChange = NULL;
+ wgtSetText(sEditor, "");
+ sEditor->onChange = savedOnChange;
+ }
+ } else if (sEditorFileIdx > rmIdx) {
+ sEditorFileIdx--;
+ }
+
+ if (rmIdx == sProject.activeFileIdx && rmFile->isForm) {
+ teardownFormWin();
+ dsgnFree(&sDesigner);
+ }
+
+ removeBreakpointsForFile(rmIdx);
+ prjRemoveFile(&sProject, rmIdx); // adjusts activeFileIdx
+ updateDropdowns();
+ prjRebuildTree(&sProject);
+ updateProjectMenuState();
+ updateDirtyIndicators();
break;
}
+
+ default:
+ break;
}
}
@@ -3785,26 +3580,16 @@ static void handleRunCmd(int32_t cmd) {
switch (cmd) {
case CMD_RUN:
if (sDbgState == DBG_PAUSED) {
- // Resume from breakpoint — clear debug mode so it runs free
- sDbgCurrentLine = -1;
- sDbgState = DBG_RUNNING;
- sDbgEnabled = false;
- debugSetBreakTitles(false);
+ // Resume from breakpoint -- clear debug mode so it runs free
+ sDbgEnabled = false;
+ clearVmStepState();
+
if (sVm) {
- sVm->debugPaused = false;
- sVm->debugBreak = false;
- sVm->stepOverDepth = -1;
- sVm->stepOutDepth = -1;
- sVm->runToCursorLine = -1;
basVmSetBreakpoints(sVm, NULL, 0);
- sVm->running = true;
}
- if (sEditor) {
- wgtInvalidatePaint(sEditor);
- }
- updateProjectMenuState();
- setStatus("Running...");
- } else {
+
+ debugResume("Running...");
+ } else if (sDbgState == DBG_IDLE) {
sDbgEnabled = false;
compileAndRun();
}
@@ -3812,23 +3597,9 @@ static void handleRunCmd(int32_t cmd) {
case CMD_DEBUG:
if (sDbgState == DBG_PAUSED) {
- // Already debugging — resume, run to next breakpoint
- sDbgCurrentLine = -1;
- sDbgState = DBG_RUNNING;
- debugSetBreakTitles(false);
- if (sVm) {
- sVm->debugPaused = false;
- sVm->debugBreak = false;
- sVm->stepOverDepth = -1;
- sVm->stepOutDepth = -1;
- sVm->runToCursorLine = -1;
- sVm->running = true;
- }
- if (sEditor) {
- wgtInvalidatePaint(sEditor);
- }
- updateProjectMenuState();
- setStatus("Debugging...");
+ // Already debugging -- resume, run to next breakpoint
+ clearVmStepState();
+ debugResume("Debugging...");
} else if (sDbgState == DBG_IDLE) {
// Start in debug mode with breakpoints
sDbgEnabled = true;
@@ -3837,29 +3608,22 @@ static void handleRunCmd(int32_t cmd) {
break;
case CMD_RUN_NOCMP:
- runCached();
+ if (sDbgState == DBG_IDLE) {
+ runCached();
+ }
break;
case CMD_STOP:
- sStopRequested = true;
- if (sVm) {
- sVm->running = false;
- sVm->debugPaused = false;
+ if (sDbgState != DBG_IDLE) {
+ debugStop();
+ setStatus("Program stopped.");
}
- sDbgState = DBG_IDLE;
- sDbgCurrentLine = -1;
- sDbgEnabled = false;
- if (sEditor) {
- wgtInvalidatePaint(sEditor);
- }
- updateProjectMenuState();
- setStatus("Program stopped.");
break;
case CMD_OUTPUT_TO_LOG:
if (sWin && sWin->menuBar) {
sOutputToLog = wmMenuItemIsChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG);
- prefsSetBool(sPrefs, "run", "outputToLog", sOutputToLog);
+ prefsSetBool(sPrefs, PREF_SEC_RUN, PREF_KEY_OUTPUT_TO_LOG, sOutputToLog);
prefsSave(sPrefs);
}
break;
@@ -3882,10 +3646,13 @@ static void handleRunCmd(int32_t cmd) {
case CMD_SAVE_ON_RUN:
if (sWin && sWin->menuBar) {
bool save = wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN);
- prefsSetBool(sPrefs, "run", "saveOnRun", save);
+ prefsSetBool(sPrefs, PREF_SEC_RUN, PREF_KEY_SAVE_ON_RUN, save);
prefsSave(sPrefs);
}
break;
+
+ default:
+ break;
}
}
@@ -3922,7 +3689,7 @@ static void handleViewCmd(int32_t cmd) {
bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_TOOLBAR);
sToolbar->visible = show;
dvxFitWindowH(sAc, sWin);
- prefsSetBool(sPrefs, "view", "toolbar", show);
+ prefsSetBool(sPrefs, PREF_SEC_VIEW, PREF_KEY_TOOLBAR, show);
prefsSave(sPrefs);
}
break;
@@ -3932,7 +3699,7 @@ static void handleViewCmd(int32_t cmd) {
bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_STATUS);
sStatusBar->visible = show;
dvxFitWindowH(sAc, sWin);
- prefsSetBool(sPrefs, "view", "statusbar", show);
+ prefsSetBool(sPrefs, PREF_SEC_VIEW, PREF_KEY_STATUSBAR, show);
prefsSave(sPrefs);
}
break;
@@ -3964,7 +3731,7 @@ static void handleViewCmd(int32_t cmd) {
int32_t minCount = oldCount < newCount ? oldCount : newCount;
for (int32_t mi = 0; mi < minCount; mi++) {
- if (oldNames[mi][0] && sDesigner.form->menuItems[mi].name[0] &&
+ if (oldNames[mi] && oldNames[mi][0] && sDesigner.form->menuItems[mi].name[0] &&
strcasecmp(oldNames[mi], sDesigner.form->menuItems[mi].name) != 0) {
ideRenameInCode(oldNames[mi], sDesigner.form->menuItems[mi].name);
}
@@ -3989,6 +3756,9 @@ static void handleViewCmd(int32_t cmd) {
}
break;
}
+
+ default:
+ break;
}
}
@@ -3997,9 +3767,6 @@ static void handleWindowCmd(int32_t cmd) {
switch (cmd) {
case CMD_WIN_CODE:
showCodeWindow();
- if (sEditor && !sEditor->onChange) {
- sEditor->onChange = onEditorChange;
- }
break;
case CMD_WIN_OUTPUT:
@@ -4050,19 +3817,20 @@ static void handleWindowCmd(int32_t cmd) {
break;
case CMD_WIN_PROJECT:
- if (!sProjectWin) {
- sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);
+ ensureProjectWindow(true);
+ break;
- if (sProjectWin) {
- sProjectWin->y = toolbarBottom() + 25;
- sProjectWin->onClose = onProjectWinClose;
- }
- }
+ default:
break;
}
}
+static bool hasProject(void) {
+ return sProject.projectPath[0] != '\0';
+}
+
+
static bool hasUnsavedData(void) {
// Check the active editor/designer
if (sDesigner.form && sDesigner.form->dirty) {
@@ -4079,14 +3847,17 @@ static bool hasUnsavedData(void) {
return sProject.dirty;
}
+
// Build control help topic from type name.
// Topic IDs in .bhs files follow the pattern ctrl..
// Generated dynamically so third-party widgets get help automatically.
static void helpBuildCtrlTopic(const char *typeName, char *buf, int32_t bufSize) {
int32_t off = snprintf(buf, bufSize, "ctrl.");
+
for (int32_t i = 0; typeName[i] && off < bufSize - 1; i++) {
buf[off++] = tolower((unsigned char)typeName[i]);
}
+
buf[off] = '\0';
}
@@ -4097,6 +3868,7 @@ static const char *helpLookupKeyword(const char *word) {
return sHelpMap[i].topic;
}
}
+
return NULL;
}
@@ -4111,6 +3883,7 @@ static void helpQueryHandler(void *ctx) {
// Determine which window is focused
WindowT *focusWin = NULL;
+
if (sAc->stack.focusedIdx >= 0 && sAc->stack.focusedIdx < sAc->stack.count) {
focusWin = sAc->stack.windows[sAc->stack.focusedIdx];
}
@@ -4148,32 +3921,22 @@ static void helpQueryHandler(void *ctx) {
}
}
- // Code editor: look up the word under the cursor
- if (focusWin == sCodeWin && sEditor) {
- char word[128];
- if (wgtTextAreaGetWordAtCursor(sEditor, word, sizeof(word)) > 0) {
- const char *topic = helpLookupKeyword(word);
- if (topic) {
- snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic);
- return;
- }
- }
- // No keyword match -- open language reference
- snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.editor");
- return;
- }
+ // Code editor / Immediate window: look up the word under the cursor
+ if ((focusWin == sCodeWin && sEditor) || (focusWin == sImmWin && sImmediate)) {
+ WidgetT *area = (focusWin == sCodeWin) ? sEditor : sImmediate;
+ char word[IDE_FULL_NAME_BUF];
- // Immediate window: look up the word under the cursor
- if (focusWin == sImmWin && sImmediate) {
- char word[128];
- if (wgtTextAreaGetWordAtCursor(sImmediate, word, sizeof(word)) > 0) {
+ if (wgtTextAreaGetWordAtCursor(area, word, sizeof(word)) > 0) {
const char *topic = helpLookupKeyword(word);
+
if (topic) {
snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic);
return;
}
}
- snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.immediate");
+
+ // No keyword match -- open the window's own topic
+ snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", (focusWin == sCodeWin) ? "ide.editor" : "ide.immediate");
return;
}
@@ -4207,38 +3970,24 @@ static void helpQueryHandler(void *ctx) {
return;
}
- // Output window
+ // Remaining windows map straight to a topic
+ const char *topic = "ide.overview";
+
if (focusWin == sOutWin) {
- snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.output");
- return;
+ topic = "ide.output";
+ } else if (focusWin == sProjectWin) {
+ topic = "ide.project";
+ } else if (focusWin == sLocalsWin) {
+ topic = "ide.debug.locals";
+ } else if (focusWin == sCallStackWin) {
+ topic = "ide.debug.callstack";
+ } else if (focusWin == sWatchWin) {
+ topic = "ide.debug.watch";
+ } else if (focusWin == sBreakpointWin) {
+ topic = "ide.debug.breakpoints";
}
- // Project window
- if (focusWin == sProjectWin) {
- snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.project");
- return;
- }
-
- // Debugger windows
- if (focusWin == sLocalsWin) {
- snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.locals");
- return;
- }
- if (focusWin == sCallStackWin) {
- snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.callstack");
- return;
- }
- if (focusWin == sWatchWin) {
- snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.watch");
- return;
- }
- if (focusWin == sBreakpointWin) {
- snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.breakpoints");
- return;
- }
-
- // Default: IDE overview
- snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.overview");
+ snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic);
}
@@ -4247,11 +3996,25 @@ static void helpSetCtrlTopic(const char *typeName) {
}
+// ============================================================
+// ideRenameInCode -- rename form/control references in all .bas files
+// ============================================================
+//
+// Case-insensitive replacement of OldName followed by '.' or '_' with
+// NewName followed by the same delimiter. This handles:
+// ControlName.Property -> NewName.Property
+// ControlName_Click -> NewName_Click (event handlers)
+// FormName.ControlName.X -> NewFormName.ControlName.X
+// Sub FormName_Load -> Sub NewFormName_Load
+
void ideRenameInCode(const char *oldName, const char *newName) {
if (!oldName || !newName || strcasecmp(oldName, newName) == 0) {
return;
}
+ // The editor may hold edits that are not in the proc buffers yet
+ saveCurProc();
+
// Rename in the per-procedure buffers (form code currently being edited)
if (sGeneralBuf) {
char *replaced = renameInBuffer(sGeneralBuf, oldName, newName);
@@ -4286,8 +4049,13 @@ void ideRenameInCode(const char *oldName, const char *newName) {
if (sDesigner.form && sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount &&
sProject.files[sEditorFileIdx].isForm &&
strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) == 0) {
- free(sDesigner.form->code);
- sDesigner.form->code = strdup(getFullSource());
+ char *code = strdup(getFullSource());
+
+ if (code) {
+ free(sDesigner.form->code);
+ sDesigner.form->code = code;
+ }
+
sDesigner.form->dirty = true;
}
@@ -4323,7 +4091,6 @@ void ideRenameInCode(const char *oldName, const char *newName) {
if (br <= 0 || br >= IDE_MAX_SOURCE) {
free(buf);
- buf = NULL;
continue;
}
@@ -4344,10 +4111,6 @@ void ideRenameInCode(const char *oldName, const char *newName) {
}
-// immTryAssign -- handle "varName = expr" when paused at a breakpoint.
-// Evaluates the RHS, looks up the variable, and writes the value back
-// to the running VM. Returns true if handled as an assignment.
-
// immParseScalarFromStr -- parse a string into a typed BasValueT based on the
// target slot's current type. Returns true on success.
@@ -4406,13 +4169,9 @@ static bool immParseScalarFromStr(const char *rhs, const BasValueT *target, BasV
}
-// ============================================================
-// evaluateImmediate
-// ============================================================
-//
-// Compile and execute a single line from the Immediate window.
-// If the line doesn't start with PRINT, wrap it in PRINT so
-// expressions produce visible output.
+// immPrintCallback -- append program output to the Immediate window.
+// The window has a fixed capacity, so the oldest lines scroll off the
+// top once it fills.
static void immPrintCallback(void *ctx, const char *text, bool newline) {
(void)ctx;
@@ -4421,175 +4180,55 @@ static void immPrintCallback(void *ctx, const char *text, bool newline) {
return;
}
- // Append output to the immediate window
- const char *cur = wgtGetText(sImmediate);
- int32_t curLen = cur ? (int32_t)strlen(cur) : 0;
- int32_t textLen = text ? (int32_t)strlen(text) : 0;
+ static char immBuf[IDE_MAX_IMM];
- if (curLen + textLen + 2 < IDE_MAX_IMM) {
- static char immBuf[IDE_MAX_IMM];
- memcpy(immBuf, cur, curLen);
+ const char *cur = wgtGetText(sImmediate);
+ const char *keep = cur ? cur : "";
+ int32_t curLen = (int32_t)strlen(keep);
+ int32_t textLen = text ? (int32_t)strlen(text) : 0;
+ int32_t extra = newline ? 1 : 0;
+
+ // A single oversize chunk is truncated to what can ever fit
+ if (textLen + extra + 1 >= IDE_MAX_IMM) {
+ textLen = IDE_MAX_IMM - 1 - extra;
+ }
+
+ // Drop whole leading lines until the new text fits
+ while (curLen + textLen + extra + 1 >= IDE_MAX_IMM) {
+ const char *nl = strchr(keep, '\n');
+
+ if (!nl) {
+ keep += curLen;
+ curLen = 0;
+ break;
+ }
+
+ curLen -= (int32_t)(nl + 1 - keep);
+ keep = nl + 1;
+ }
+
+ memmove(immBuf, keep, curLen);
+
+ if (textLen > 0) {
memcpy(immBuf + curLen, text, textLen);
curLen += textLen;
-
- if (newline) {
- immBuf[curLen++] = '\n';
- }
-
- immBuf[curLen] = '\0';
- wgtSetText(sImmediate, immBuf);
-
- // Move cursor to end so user can keep typing
- int32_t lines = 1;
-
- for (int32_t i = 0; i < curLen; i++) {
- if (immBuf[i] == '\n') {
- lines++;
- }
- }
-
- wgtTextAreaGoToLine(sImmediate, lines);
}
+
+ if (newline) {
+ immBuf[curLen++] = '\n';
+ }
+
+ immBuf[curLen] = '\0';
+ wgtSetText(sImmediate, immBuf);
+
+ // Move cursor to end so user can keep typing
+ wgtTextAreaGoToLine(sImmediate, countLines(immBuf));
}
-// immResolveLhsSlot -- parse the LHS of an assignment and resolve it to a
-// pointer into the running VM's live data. Handles:
-// varName -- scalar variable
-// varName(i) -- array element
-// varName.field -- UDT field
-// varName(i).field -- array element UDT field
-// Returns NULL if the LHS can't be resolved. *endPtr is set past the LHS.
-
-static BasValueT *immResolveLhsSlot(const char *lhs, const char **endPtr) {
- const char *p = lhs;
-
- // Extract variable name
- const char *nameStart = p;
-
- while ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') ||
- (*p >= '0' && *p <= '9') || *p == '_') {
- p++;
- }
-
- if (p == nameStart) {
- return NULL;
- }
-
- char varName[128];
- int32_t nameLen = (int32_t)(p - nameStart);
-
- if (nameLen >= (int32_t)sizeof(varName)) {
- return NULL;
- }
-
- memcpy(varName, nameStart, nameLen);
- varName[nameLen] = '\0';
-
- // Look up the base variable
- const BasDebugVarT *dv = findDebugVar(varName);
-
- if (!dv) {
- return NULL;
- }
-
- BasValueT *slot = getDebugVarSlot(dv);
-
- if (!slot) {
- return NULL;
- }
-
- // Parse optional array subscript: (idx1, idx2, ...)
- if (*p == '(') {
- p++; // skip '('
-
- if (slot->type != BAS_TYPE_ARRAY || !slot->arrVal) {
- return NULL;
- }
-
- int32_t indices[BAS_ARRAY_MAX_DIMS];
- int32_t numIndices = 0;
-
- while (*p && *p != ')' && numIndices < BAS_ARRAY_MAX_DIMS) {
- while (*p == ' ') { p++; }
- indices[numIndices++] = atoi(p);
-
- // Skip past the number
- if (*p == '-') { p++; }
-
- while (*p >= '0' && *p <= '9') { p++; }
-
- while (*p == ' ') { p++; }
-
- if (*p == ',') { p++; }
- }
-
- if (*p == ')') { p++; }
-
- int32_t flatIdx = basArrayIndex(slot->arrVal, indices, numIndices);
-
- if (flatIdx < 0 || flatIdx >= slot->arrVal->totalElements) {
- return NULL;
- }
-
- slot = &slot->arrVal->elements[flatIdx];
- }
-
- // Parse optional UDT field: .fieldName
- if (*p == '.') {
- p++; // skip '.'
-
- if (slot->type != BAS_TYPE_UDT || !slot->udtVal) {
- return NULL;
- }
-
- const char *fieldStart = p;
-
- while ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') ||
- (*p >= '0' && *p <= '9') || *p == '_') {
- p++;
- }
-
- char fieldName[128];
- int32_t fieldLen = (int32_t)(p - fieldStart);
-
- if (fieldLen <= 0 || fieldLen >= (int32_t)sizeof(fieldName)) {
- return NULL;
- }
-
- memcpy(fieldName, fieldStart, fieldLen);
- fieldName[fieldLen] = '\0';
-
- // Find field index from debug UDT definitions
- int32_t fieldIdx = -1;
-
- for (int32_t t = 0; t < sDbgModule->debugUdtDefCount; t++) {
- if (sDbgModule->debugUdtDefs[t].typeId == slot->udtVal->typeId) {
- for (int32_t f = 0; f < sDbgModule->debugUdtDefs[t].fieldCount; f++) {
- if (strcasecmp(sDbgModule->debugUdtDefs[t].fields[f].name, fieldName) == 0) {
- fieldIdx = f;
- break;
- }
- }
-
- break;
- }
- }
-
- if (fieldIdx < 0 || fieldIdx >= slot->udtVal->fieldCount) {
- return NULL;
- }
-
- slot = &slot->udtVal->fields[fieldIdx];
- }
-
- if (endPtr) {
- *endPtr = p;
- }
-
- return slot;
-}
-
+// immTryAssign -- handle "varName = expr" when paused at a breakpoint.
+// Evaluates the RHS, looks up the variable, and writes the value back
+// to the running VM. Returns true if handled as an assignment.
static bool immTryAssign(const char *expr) {
if (sDbgState != DBG_PAUSED || !sVm || !sDbgModule) {
@@ -4600,20 +4239,20 @@ static bool immTryAssign(const char *expr) {
const char *p = dvxSkipWs(expr);
// Skip optional LET keyword
- if (strncasecmp(p, "LET ", 4) == 0) {
- p = dvxSkipWs(p + 4);
+ if (kwMatch(p, KW_LET)) {
+ p = dvxSkipWs(p + KW_LEN(KW_LET));
}
// Resolve the LHS to a live slot in the VM
const char *afterLhs = NULL;
- BasValueT *slot = immResolveLhsSlot(p, &afterLhs);
+ BasValueT *slot = resolveVarPath(p, &afterLhs);
if (!slot || !afterLhs) {
return false;
}
// Build display name from LHS
- char lhsName[256];
+ char lhsName[IDE_VALUE_BUF];
int32_t lhsLen = (int32_t)(afterLhs - p);
if (lhsLen >= (int32_t)sizeof(lhsName)) {
@@ -4647,10 +4286,10 @@ static bool immTryAssign(const char *expr) {
// Write the value directly to the slot
basValRelease(slot);
- *slot = newVal; // transfer ownership — don't release newVal
+ *slot = newVal; // transfer ownership -- don't release newVal
// Show confirmation
- char confirm[256];
+ char confirm[IDE_VALUE_BUF];
snprintf(confirm, sizeof(confirm), "%s = ", lhsName);
immPrintCallback(NULL, confirm, false);
formatValue(slot, confirm, sizeof(confirm));
@@ -4663,13 +4302,6 @@ static bool immTryAssign(const char *expr) {
}
-// ============================================================
-// basicColorize
-// ============================================================
-//
-// Syntax colorizer callback for BASIC source code. Scans a single
-// line and fills the colors array with syntax color indices.
-
// Hash-based keyword/type lookup using stb_ds.
// Key = uppercase word, value = SYNTAX_KEYWORD or SYNTAX_TYPE.
// Built once on first use, then O(1) per lookup.
@@ -4719,7 +4351,7 @@ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufS
setOutputText(sOutputBuf);
}
- return dvxInputBox(sAc, "DVX BASIC", prompt ? prompt : "Enter value:", NULL, buf, bufSize);
+ return dvxInputBox(sAc, IDE_MAIN_TITLE, prompt ? prompt : "Enter value:", NULL, buf, bufSize);
}
@@ -4740,27 +4372,6 @@ static bool isCtrlArrayInDesigner(const char *ctrlName) {
}
-// ============================================================
-// ideRenameInCode -- rename form/control references in all .bas files
-// ============================================================
-//
-// Case-insensitive replacement of OldName followed by '.' or '_' with
-// NewName followed by the same delimiter. This handles:
-// ControlName.Property -> NewName.Property
-// ControlName_Click -> NewName_Click (event handlers)
-// FormName.ControlName.X -> NewFormName.ControlName.X
-// Sub FormName_Load -> Sub NewFormName_Load
-//
-// Word-boundary check on the left: the character before the match must
-// be a non-identifier character (space, tab, newline, '.', '(', start
-// of string) to avoid replacing "Command1" inside "MyCommand1".
-
-static bool isIdentChar(char c) {
- return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
- (c >= '0' && c <= '9') || c == '_';
-}
-
-
// Check if position i is inside a string literal or comment.
// Scans from the start of the line containing i.
static bool isInStringOrComment(const char *src, int32_t i) {
@@ -4786,11 +4397,197 @@ static bool isInStringOrComment(const char *src, int32_t i) {
}
+// Internal mangled names (e.g. "DoCount$Count" for Static vars) have a $
+// in the middle; string variable names end with $ and are fine.
+static bool isMangledDebugName(const char *name) {
+ const char *dollar = strchr(name, '$');
+
+ return dollar && dollar[1] != '\0';
+}
+
+
static bool isReplaceEnabled(void) {
return sReplCheck && wgtCheckboxIsChecked(sReplCheck);
}
+// joinProcArrays -- build the canonical file text from a (General)
+// buffer and a proc array: General, one blank line, then each proc
+// separated by one blank line. Returns a malloc'd string or NULL.
+static char *joinProcArrays(const char *general, char **procs) {
+ int32_t totalLen = 0;
+ int32_t procCount = (int32_t)arrlen(procs);
+
+ if (general && general[0]) {
+ totalLen += (int32_t)strlen(general);
+ totalLen += 2; // newline + blank line separator
+ }
+
+ for (int32_t i = 0; i < procCount; i++) {
+ if (procs[i]) {
+ totalLen += (int32_t)strlen(procs[i]);
+ totalLen += 2; // newline + blank line between procedures
+ }
+ }
+
+ char *out = (char *)malloc(totalLen + 1);
+
+ if (!out) {
+ return NULL;
+ }
+
+ int32_t pos = 0;
+
+ if (general && general[0]) {
+ int32_t len = (int32_t)strlen(general);
+ memcpy(out + pos, general, len);
+ pos += len;
+
+ if (out[pos - 1] != '\n') {
+ out[pos++] = '\n';
+ }
+
+ out[pos++] = '\n';
+ }
+
+ for (int32_t i = 0; i < procCount; i++) {
+ if (procs[i]) {
+ int32_t len = (int32_t)strlen(procs[i]);
+ memcpy(out + pos, procs[i], len);
+ pos += len;
+
+ if (pos > 0 && out[pos - 1] != '\n') {
+ out[pos++] = '\n';
+ }
+
+ // Blank line between procedures
+ if (i < procCount - 1) {
+ out[pos++] = '\n';
+ }
+ }
+ }
+
+ out[pos] = '\0';
+ return out;
+}
+
+
+// joinProcBufs -- reassemble the editor's buffers into sFullSourceCache
+// and rebuild sProcTable from the result so both always describe the
+// same text. Does not save the editor first (see getFullSource).
+static const char *joinProcBufs(void) {
+ free(sFullSourceCache);
+ sFullSourceCache = joinProcArrays(sGeneralBuf, sProcBufs);
+
+ const char *src = sFullSourceCache ? sFullSourceCache : "";
+ rebuildProcTable(src);
+ return src;
+}
+
+
+// kwMatch -- case-insensitive prefix test against a keyword tag
+static bool kwMatch(const char *p, const char *kw) {
+ return strncasecmp(p, kw, strlen(kw)) == 0;
+}
+
+
+static void listRowsAdd(IdeListRowsT *rows, const char *text) {
+ char *copy = strdup(text);
+
+ if (copy) {
+ arrput(rows->strs, copy);
+ }
+
+ arrput(rows->cells, copy ? copy : "");
+}
+
+
+static void listRowsCommit(const IdeListRowsT *rows, WidgetT *list, int32_t rowCount) {
+ wgtListViewSetData(list, rowCount > 0 ? rows->cells : NULL, rowCount);
+}
+
+
+static void listRowsFree(IdeListRowsT *rows) {
+ listRowsReset(rows);
+ arrfree(rows->strs);
+ arrfree(rows->cells);
+ rows->strs = NULL;
+ rows->cells = NULL;
+}
+
+
+static void listRowsReset(IdeListRowsT *rows) {
+ for (int32_t i = 0; i < (int32_t)arrlen(rows->strs); i++) {
+ free(rows->strs[i]);
+ }
+
+ arrsetlen(rows->strs, 0);
+ arrsetlen(rows->cells, 0);
+}
+
+
+// loadCompileSource -- the code text of project file fileIdx, normalized
+// to the canonical proc layout (malloc'd). Loaded buffers are
+// authoritative; disk is only read for files never loaded. For a .frm
+// the result is just its code section. Returns NULL when the file
+// cannot be read.
+static char *loadCompileSource(int32_t fileIdx) {
+ PrjFileT *file = &sProject.files[fileIdx];
+ char *raw = NULL; // whole-file text read from disk (owned)
+ char *frmCode = NULL; // code section extracted from a .frm (owned)
+ const char *code = NULL;
+
+ if (file->isForm && sDesigner.form && fileIdx == sProject.activeFileIdx) {
+ // Active form: the designer holds the current code
+ code = sDesigner.form->code ? sDesigner.form->code : "";
+ } else {
+ const char *text = file->buffer;
+
+ if (!text) {
+ char fullPath[DVX_MAX_PATH];
+ int32_t len = 0;
+
+ prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath));
+ raw = platformReadFile(fullPath, &len);
+
+ if (!raw) {
+ return NULL;
+ }
+
+ if (len >= IDE_MAX_SOURCE) {
+ free(raw);
+ return NULL;
+ }
+
+ text = raw;
+ }
+
+ if (file->isForm) {
+ // Parse the .frm to extract the code section using the same
+ // parser as the designer (handles nested containers).
+ DsgnStateT tmpDs;
+ memset(&tmpDs, 0, sizeof(tmpDs));
+ dsgnLoadFrm(&tmpDs, text, (int32_t)strlen(text));
+
+ if (tmpDs.form && tmpDs.form->code) {
+ frmCode = tmpDs.form->code;
+ tmpDs.form->code = NULL;
+ }
+
+ dsgnFree(&tmpDs);
+ code = frmCode ? frmCode : "";
+ } else {
+ code = text;
+ }
+ }
+
+ char *packed = packProcLayout(code);
+ free(frmCode);
+ free(raw);
+ return packed;
+}
+
+
static void loadFile(void) {
FileFilterT filters[] = {
{ "BASIC Files (*.bas)" },
@@ -4800,40 +4597,11 @@ static void loadFile(void) {
char path[DVX_MAX_PATH];
- if (!dvxFileDialog(sAc, "Add File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) {
+ if (!dvxFileDialog(sAc, "Add File", FD_OPEN, NULL, filters, (int32_t)(sizeof(filters) / sizeof(filters[0])), path, sizeof(path))) {
return;
}
- const char *ext = strrchr(path, '.');
- bool isForm = (ext && strcasecmp(ext, ".frm") == 0);
-
- if (sProject.projectPath[0] != '\0') {
- // Add the file to the current project
- const char *fileName = platformPathBaseName(path);
-
- prjAddFile(&sProject, fileName, isForm);
- prjRebuildTree(&sProject);
- activateFile(sProject.fileCount - 1, ViewAutoE);
- } else {
- // No project -- create one from this file
- if (!promptAndSave()) {
- return;
- }
-
- ensureProject(path);
-
- if (!sProjectWin) {
- sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);
-
- if (sProjectWin) {
- sProjectWin->y = toolbarBottom() + 25;
- sProjectWin->onClose = onProjectWinClose;
- dvxRaiseWindow(sAc, sProjectWin);
- }
- }
- }
-
- recentAdd(path);
+ openSinglePath(path);
}
@@ -4856,26 +4624,15 @@ static void loadFormCodeIntoEditor(void) {
sDropdownNavSuppressed = saved;
showProc(-1);
-
- if (sEditor && !sEditor->onChange) {
- sEditor->onChange = onEditorChange;
- }
}
-// ============================================================
-// loadFrmFiles
-// ============================================================
-//
-// Load all .frm files listed in the current project into the
-// form runtime for execution.
-
+// loadFrmFiles -- register every .frm listed in the current project with
+// the form runtime. Registration only caches the text; the caller's
+// basFormRtLoadAllForms then instantiates them, so a Form_Load that does
+// "Load Form2" finds Form2's .frm regardless of project file order.
static void loadFrmFiles(BasFormRtT *rt) {
- dvxLog("loadFrmFiles: fileCount=%d", (int)sProject.fileCount);
-
for (int32_t i = 0; i < sProject.fileCount; i++) {
- dvxLog(" file[%d] path=%s isForm=%d", (int)i, sProject.files[i].path, (int)sProject.files[i].isForm);
-
if (!sProject.files[i].isForm) {
continue;
}
@@ -4886,27 +4643,20 @@ static void loadFrmFiles(BasFormRtT *rt) {
int32_t bytesRead = 0;
char *frmBuf = platformReadFile(fullPath, &bytesRead);
- dvxLog(" loadFrmFiles: fullPath=%s bytes=%d frmBuf=%s", fullPath, (int)bytesRead, frmBuf ? "ok" : "null");
-
if (!frmBuf) {
continue;
}
- if (bytesRead <= 0 || bytesRead >= IDE_MAX_SOURCE) {
- free(frmBuf);
- continue;
+ if (bytesRead > 0 && bytesRead < IDE_MAX_SOURCE) {
+ // Cache the form object name in the project file entry
+ char *formName = sProject.files[i].formName;
+
+ if (basExtractFormName(frmBuf, formName, sizeof(sProject.files[i].formName))) {
+ basFormRtRegisterFrm(rt, formName, frmBuf, bytesRead);
+ }
}
- BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead);
free(frmBuf);
-
- dvxLog(" loadFrmFiles: basFormRtLoadFrm returned form='%s'",
- form && form->name[0] ? form->name : "(null)");
-
- // Cache the form object name in the project file entry
- if (form && form->name[0]) {
- snprintf(sProject.files[i].formName, sizeof(sProject.files[i].formName), "%s", form->name);
- }
}
}
@@ -4926,10 +4676,6 @@ static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fal
}
-// ============================================================
-// localToConcatLine -- convert editor-local line to concatenated source line
-// ============================================================
-
// Convert an editor-local line number to a full-source line number.
// The editor shows one procedure at a time (sCurProcIdx), so we need
// to add the procedure's starting line offset within the file's code.
@@ -4945,12 +4691,10 @@ static int32_t localToConcatLine(int32_t editorLine) {
// For projects, add the file's offset in the concatenated source.
// startLine is recorded AFTER the injected BEGINFORM line, so it
// already points at the first real code line (matching prjMapLine).
- if (sProject.sourceMapCount > 0 && sProject.activeFileIdx >= 0) {
+ if (sProject.sourceMapCount > 0 && sEditorFileIdx >= 0) {
for (int32_t i = 0; i < sProject.sourceMapCount; i++) {
- if (sProject.sourceMap[i].fileIdx == sProject.activeFileIdx) {
- int32_t base = sProject.sourceMap[i].startLine;
-
- return base + fileLine - 1;
+ if (sProject.sourceMap[i].fileIdx == sEditorFileIdx) {
+ return sProject.sourceMap[i].startLine + fileLine - 1;
}
}
}
@@ -4959,129 +4703,24 @@ static int32_t localToConcatLine(int32_t editorLine) {
}
-// lookupWatchVar -- evaluate a watch expression
-// Supports: varName, varName(idx), varName(idx1, idx2), varName.field
+// lookupWatchVar -- evaluate a simple watch expression by reading the
+// paused VM directly. Supports varName, varName(idx, ...), varName.field
+// and varName(idx).field; anything else falls back to evalWatchExpr.
static bool lookupWatchVar(const char *expr, BasValueT *outVal) {
- if (!sVm || !sDbgModule) {
+ const char *end = NULL;
+ BasValueT *slot = resolveVarPath(dvxSkipWs(expr), &end);
+
+ if (!slot || !end || *dvxSkipWs(end) != '\0') {
return false;
}
- char buf[256];
- snprintf(buf, sizeof(buf), "%s", expr);
-
- // Split on '.' for UDT field access: "varName.fieldName"
- char *dot = strchr(buf, '.');
- char *fieldName = NULL;
-
- if (dot && dot > buf && !strchr(buf, '(')) {
- // Only treat as field access if no subscript before the dot
- *dot = '\0';
- fieldName = dot + 1;
- }
-
- // Split on '(' for array subscript: "varName(idx1, idx2, ...)"
- char *paren = strchr(buf, '(');
- int32_t indices[BAS_ARRAY_MAX_DIMS];
- int32_t numIndices = 0;
-
- if (paren) {
- char *close = strchr(paren, ')');
-
- if (close) {
- *close = '\0';
- }
-
- *paren = '\0';
- char *arg = paren + 1;
-
- // Parse comma-separated indices
- while (*arg && numIndices < BAS_ARRAY_MAX_DIMS) {
- while (*arg == ' ') { arg++; }
- indices[numIndices++] = atoi(arg);
- char *comma = strchr(arg, ',');
-
- if (comma) {
- arg = comma + 1;
- } else {
- break;
- }
- }
-
- // Check for ".field" after the closing paren
- if (close && close[1] == '.') {
- fieldName = close + 2;
- }
- }
-
- // Look up the variable
- const BasDebugVarT *dv = findDebugVar(buf);
-
- if (!dv) {
- return false;
- }
-
- BasValueT val;
-
- if (!readDebugVar(dv, &val)) {
- return false;
- }
-
- // Apply array subscript
- if (numIndices > 0) {
- if (val.type != BAS_TYPE_ARRAY || !val.arrVal) {
- return false;
- }
-
- int32_t flatIdx = basArrayIndex(val.arrVal, indices, numIndices);
-
- if (flatIdx < 0 || flatIdx >= val.arrVal->totalElements) {
- return false;
- }
-
- val = val.arrVal->elements[flatIdx];
- }
-
- // Apply UDT field access
- if (fieldName && fieldName[0]) {
- if (val.type != BAS_TYPE_UDT || !val.udtVal) {
- return false;
- }
-
- // Find the field by name using debug UDT definitions
- int32_t fieldIdx = -1;
-
- for (int32_t t = 0; t < sDbgModule->debugUdtDefCount; t++) {
- if (sDbgModule->debugUdtDefs[t].typeId == val.udtVal->typeId) {
- for (int32_t f = 0; f < sDbgModule->debugUdtDefs[t].fieldCount; f++) {
- if (strcasecmp(sDbgModule->debugUdtDefs[t].fields[f].name, fieldName) == 0) {
- fieldIdx = f;
- break;
- }
- }
-
- break;
- }
- }
-
- if (fieldIdx < 0 || fieldIdx >= val.udtVal->fieldCount) {
- return false;
- }
-
- val = val.udtVal->fields[fieldIdx];
- }
-
- *outVal = val;
+ *outVal = *slot;
return true;
}
-// ============================================================
-// onMenu
-// ============================================================
-
-
static void makeExecutable(void) {
- if (sProject.projectPath[0] == '\0') {
+ if (!hasProject()) {
setStatus("Save the project first.");
return;
}
@@ -5099,7 +4738,7 @@ static void makeExecutable(void) {
char outPath[DVX_MAX_PATH];
outPath[0] = '\0';
- if (!dvxFileDialog(sAc, "Make Executable", FD_SAVE, NULL, filters, 2, outPath, sizeof(outPath))) {
+ if (!dvxFileDialog(sAc, "Make Executable", FD_SAVE, NULL, filters, (int32_t)(sizeof(filters) / sizeof(filters[0])), outPath, sizeof(outPath))) {
return;
}
@@ -5120,113 +4759,8 @@ static void makeExecutable(void) {
dvxSetBusy(sAc, true);
dvxUpdate(sAc);
- // Make a copy of the module for potential stripping
- int32_t modLen = 0;
- uint8_t *modData = basModuleSerialize(sCachedModule, &modLen);
-
- if (!modData) {
- setStatus("Failed to serialize module.");
- dvxSetBusy(sAc, false);
- return;
- }
-
- // For release, deserialize, strip, re-serialize
- if (release) {
- BasModuleT *modCopy = basModuleDeserialize(modData, modLen);
- free(modData);
-
- if (!modCopy) {
- setStatus("Failed to prepare release module.");
- dvxSetBusy(sAc, false);
- return;
- }
-
- basStripModule(modCopy);
- modData = basModuleSerialize(modCopy, &modLen);
- basModuleFree(modCopy);
-
- if (!modData) {
- setStatus("Failed to serialize stripped module.");
- dvxSetBusy(sAc, false);
- return;
- }
- }
-
- // Serialize debug info from the original (unstripped) module
- int32_t dbgLen = 0;
- uint8_t *dbgData = NULL;
-
- if (!release) {
- dbgData = basDebugSerialize(sCachedModule, &dbgLen);
- }
-
- // Extract stub from our own resources
- DvxResHandleT *selfRes = dvxResOpen(sCtx->appPath);
-
- if (!selfRes) {
- setStatus("Cannot read IDE resources.");
- free(modData);
- free(dbgData);
- dvxSetBusy(sAc, false);
- return;
- }
-
- uint32_t stubSize = 0;
- void *stubData = dvxResRead(selfRes, BAS_RES_STUB, &stubSize);
- dvxResClose(selfRes);
-
- if (!stubData || stubSize == 0) {
- setStatus("Stub not found in IDE resources.");
- free(modData);
- free(dbgData);
- dvxSetBusy(sAc, false);
- return;
- }
-
- // Write stub to output file
- FILE *outFile = fopen(outPath, "wb");
-
- if (!outFile) {
- setStatus("Cannot create output file.");
- free(stubData);
- free(modData);
- free(dbgData);
- dvxSetBusy(sAc, false);
- return;
- }
-
- size_t stubWritten = fwrite(stubData, 1, stubSize, outFile);
- int stubClose = fclose(outFile);
- free(stubData);
-
- if (stubWritten != stubSize || stubClose != 0) {
- setStatus("Failed writing stub to output file.");
- free(modData);
- free(dbgData);
- dvxSetBusy(sAc, false);
- return;
- }
-
- // Build parallel form arrays: load each form, strip comments.
- // The loop preserves the prior quirk where a malloc failure skips the
- // resource but still advances the index -- formData[i] is left NULL
- // in that case and basBuildEmitResources drops it.
- int32_t frmCapacity = 0;
-
- for (int32_t i = 0; i < sProject.fileCount; i++) {
- if (sProject.files[i].isForm) {
- frmCapacity++;
- }
- }
-
- uint8_t **frmData = NULL;
- int32_t *frmLens = NULL;
- int32_t frmCount = 0;
-
- if (frmCapacity > 0) {
- frmData = (uint8_t **)calloc((size_t)frmCapacity, sizeof(uint8_t *));
- frmLens = (int32_t *)calloc((size_t)frmCapacity, sizeof(int32_t));
- }
+ // Gather every .frm text (unsaved editor buffers win over disk).
+ char **frmSources = NULL; // stb_ds: owned form text
for (int32_t i = 0; i < sProject.fileCount; i++) {
if (!sProject.files[i].isForm) {
@@ -5243,51 +4777,11 @@ static void makeExecutable(void) {
frmSrc = platformReadFile(fp, NULL);
}
- if (!frmSrc) {
- continue;
- }
-
- int32_t frmLen = (int32_t)strlen(frmSrc);
-
- // Strip comments and leading whitespace from embedded form text.
- int32_t stripCap = frmLen + 16;
- uint8_t *stripped = (uint8_t *)malloc(stripCap);
-
- if (stripped) {
- int32_t strippedLen = basStripFrmComments(frmSrc, frmLen, stripped, stripCap);
-
- if (frmData && frmLens) {
- frmData[frmCount] = stripped;
- frmLens[frmCount] = strippedLen;
- } else {
- free(stripped);
- }
- }
-
- free(frmSrc);
- frmCount++;
- }
-
- // Resolve icon bytes once -- if the project has no icon path, fall back
- // to the IDE's embedded "noicon" so compiled apps always have something.
- char *iconDiskPath = NULL;
- char iconFullPath[DVX_MAX_PATH];
- void *iconFallback = NULL;
- uint32_t iconFallbackSize = 0;
- DvxResHandleT *ideRes = NULL;
-
- if (sProject.iconPath[0]) {
- snprintf(iconFullPath, sizeof(iconFullPath), "%s" DVX_PATH_SEP "%s", sProject.projectDir, sProject.iconPath);
- iconDiskPath = iconFullPath;
- } else {
- ideRes = dvxResOpen(sCtx->appPath);
-
- if (ideRes) {
- iconFallback = dvxResRead(ideRes, "noicon", &iconFallbackSize);
+ if (frmSrc) {
+ arrput(frmSources, frmSrc);
}
}
- // Hand everything to the shared emitter.
BasBuildSpecT spec;
memset(&spec, 0, sizeof(spec));
spec.projName = sProject.name;
@@ -5296,96 +4790,42 @@ static void makeExecutable(void) {
spec.version = sProject.version;
spec.copyright = sProject.copyright;
spec.description = sProject.description;
+ spec.projectDir = sProject.projectDir;
+ spec.iconPath = sProject.iconPath;
spec.helpFile = sProject.helpFile;
- spec.iconPath = iconDiskPath;
- spec.iconData = iconFallback;
- spec.iconSize = (int32_t)iconFallbackSize;
- spec.moduleData = modData;
- spec.moduleLen = modLen;
- spec.debugData = dbgData;
- spec.debugLen = dbgLen;
- spec.formCount = frmCount;
- spec.formData = (const uint8_t *const *)frmData;
- spec.formLens = frmLens;
+ spec.selfPath = sCtx->appPath;
+ spec.module = sCachedModule;
+ spec.frmSources = (const char *const *)frmSources;
+ spec.frmCount = (int32_t)arrlen(frmSources);
+ spec.release = release;
- int32_t emitRc = basBuildEmitResources(outPath, &spec);
+ const char *failure = basBuildApp(outPath, &spec);
- free(modData);
- free(dbgData);
- free(iconFallback);
-
- if (ideRes) {
- dvxResClose(ideRes);
- }
-
- for (int32_t i = 0; i < frmCount; i++) {
- if (frmData) {
- free(frmData[i]);
- }
- }
-
- free(frmData);
- free(frmLens);
-
- // Copy help file alongside the output app (the resource entry itself was
- // already written by basBuildEmitResources).
- if (sProject.helpFile[0]) {
- const char *helpBase = platformPathBaseName(sProject.helpFile);
-
- char helpSrc[DVX_MAX_PATH];
- snprintf(helpSrc, sizeof(helpSrc), "%s" DVX_PATH_SEP "%s", sProject.projectDir, sProject.helpFile);
-
- char outDir[DVX_MAX_PATH];
- snprintf(outDir, sizeof(outDir), "%s", outPath);
- char *lastSep = platformPathDirEnd(outDir);
-
- if (lastSep) {
- *lastSep = '\0';
- }
-
- char helpDst[DVX_MAX_PATH];
- snprintf(helpDst, sizeof(helpDst), "%s" DVX_PATH_SEP "%s", outDir, helpBase);
-
- FILE *hSrc = fopen(helpSrc, "rb");
-
- if (hSrc) {
- FILE *hDst = fopen(helpDst, "wb");
-
- if (hDst) {
- char cpBuf[4096];
- size_t n;
-
- while ((n = fread(cpBuf, 1, sizeof(cpBuf), hSrc)) > 0) {
- fwrite(cpBuf, 1, n, hDst);
- }
-
- fclose(hDst);
- }
-
- fclose(hSrc);
- }
+ for (int32_t i = 0; i < (int32_t)arrlen(frmSources); i++) {
+ free(frmSources[i]);
}
+ arrfree(frmSources);
dvxSetBusy(sAc, false);
- if (emitRc != 0) {
- setStatus("Failed writing resources to output file.");
+ if (failure) {
+ setStatus(failure);
return;
}
- char msg[512];
+ char msg[IDE_MSG_BUF];
snprintf(msg, sizeof(msg), "Created %s (%s)", outPath, release ? "release" : "debug");
setStatus(msg);
}
static void navigateToBreakpoint(int32_t bpIdx) {
- if (bpIdx < 0 || bpIdx >= sBreakpointCount) {
+ if (bpIdx < 0 || bpIdx >= (int32_t)arrlen(sBreakpoints)) {
return;
}
IdeBreakpointT *bp = &sBreakpoints[bpIdx];
- const char *procName = (bp->procIdx >= 0 && bp->procName[0]) ? bp->procName : NULL;
+ const char *procName = (strcmp(bp->procName, IDE_GENERAL_SECTION) != 0) ? bp->procName : NULL;
navigateToCodeLine(bp->fileIdx, bp->codeLine, procName, false);
}
@@ -5429,9 +4869,8 @@ static void navigateToCodeLine(int32_t fileIdx, int32_t codeLine, const char *pr
if (procName) {
for (int32_t i = 0; i < procCount; i++) {
- char fullName[128];
- snprintf(fullName, sizeof(fullName), "%s.%s",
- sProcTable[i].objName, sProcTable[i].evtName);
+ char fullName[IDE_FULL_NAME_BUF];
+ snprintf(fullName, sizeof(fullName), "%s.%s", sProcTable[i].objName, sProcTable[i].evtName);
if (strcasecmp(fullName, procName) == 0) {
targetProcIdx = i;
@@ -5447,8 +4886,7 @@ static void navigateToCodeLine(int32_t fileIdx, int32_t codeLine, const char *pr
// Update dropdowns to match
if (targetProcIdx >= 0 && targetProcIdx < procCount) {
- selectDropdowns(sProcTable[targetProcIdx].objName,
- sProcTable[targetProcIdx].evtName);
+ selectDropdowns(sProcTable[targetProcIdx].objName, sProcTable[targetProcIdx].evtName);
} else if (sObjDropdown) {
wgtDropdownSetSelected(sObjDropdown, 0);
}
@@ -5471,6 +4909,9 @@ static void navigateToCodeLine(int32_t fileIdx, int32_t codeLine, const char *pr
}
+// navigateToEventSub -- open code editor at the default event sub for the
+// selected control (or form).
+
static void navigateToEventSub(void) {
if (!sDesigner.form) {
return;
@@ -5494,18 +4935,15 @@ static void navigateToEventSub(void) {
}
-// navigateToEventSub -- open code editor at the default event sub for the
-// selected control (or form). Creates the sub skeleton if it doesn't exist.
-// Code is stored in the .frm file's code section (sDesigner.form->code).
+// navigateToNamedEventSub -- open the code editor at Ctrl_Event, creating
+// the sub skeleton if it doesn't exist. Code is stored in the .frm
+// file's code section (sDesigner.form->code).
static void navigateToNamedEventSub(const char *ctrlName, const char *eventName) {
if (!sDesigner.form || !ctrlName || !eventName) {
return;
}
- char subName[128];
- snprintf(subName, sizeof(subName), "%s_%s", ctrlName, eventName);
-
// Load form code into editor (stashes existing code, parses procs,
// populates dropdowns without triggering navigation)
loadFormCodeIntoEditor();
@@ -5516,46 +4954,35 @@ static void navigateToNamedEventSub(const char *ctrlName, const char *eventName)
// Search for existing procedure
int32_t procCount = (int32_t)arrlen(sProcTable);
+ int32_t procIdx = -1;
for (int32_t i = 0; i < procCount; i++) {
- char fullName[128];
- snprintf(fullName, sizeof(fullName), "%s_%s", sProcTable[i].objName, sProcTable[i].evtName);
-
- if (strcasecmp(fullName, subName) == 0) {
- stashDesignerState();
- showProc(i);
- if (sEditor && !sEditor->onChange) {
- sEditor->onChange = onEditorChange;
- }
- selectDropdowns(ctrlName, eventName);
- return;
+ if (strcasecmp(sProcTable[i].objName, ctrlName) == 0 && strcasecmp(sProcTable[i].evtName, eventName) == 0) {
+ procIdx = i;
+ break;
}
}
- // Not found -- create a new sub skeleton for editing.
- // Don't mark dirty yet; saveCurProc will discard it if the
- // user doesn't add any code.
- char skeleton[512];
-
- if (isCtrlArrayInDesigner(ctrlName)) {
- snprintf(skeleton, sizeof(skeleton), "Sub %s (Index As Integer%s)\n\nEnd Sub\n", subName, getEventExtraParams(eventName));
+ if (procIdx >= 0) {
+ showProc(procIdx);
} else {
- snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName);
+ createEventSubSkeleton(ctrlName, eventName);
}
- arrput(sProcBufs, strdup(skeleton));
-
- // Show the new procedure (it's the last one)
- stashDesignerState();
- showProc((int32_t)arrlen(sProcBufs) - 1);
- if (sEditor && !sEditor->onChange) {
- sEditor->onChange = onEditorChange;
+ if (sCodeWin) {
+ dvxRaiseWindow(sAc, sCodeWin);
}
+
+ setStatus("Code view.");
selectDropdowns(ctrlName, eventName);
}
static void newProject(void) {
+ if (!promptAndSave()) {
+ return;
+ }
+
char name[PRJ_MAX_NAME];
if (!dvxInputBox(sAc, "New Project", "Project name:", "", name, sizeof(name))) {
@@ -5594,27 +5021,10 @@ static void newProject(void) {
prjNew(&sProject, name, dir, sPrefs);
snprintf(sProject.projectPath, sizeof(sProject.projectPath), "%s", dbpPath);
- prjSave(&sProject);
- sProject.dirty = false;
-
- // Create and show project window
- if (!sProjectWin) {
- sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);
-
- if (sProjectWin) {
- sProjectWin->y = toolbarBottom() + 25;
- sProjectWin->onClose = onProjectWinClose;
- sProjectWin->onMenu = onMenu;
- sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
- }
- } else {
- prjRebuildTree(&sProject);
- }
-
- char title[300];
- snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
- dvxSetTitle(sAc, sWin, title);
+ saveProjectFile();
+ ensureProjectWindow(false);
+ updateMainTitle();
setStatus("New project created.");
updateProjectMenuState();
}
@@ -5670,18 +5080,18 @@ static void onBreakpointListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift)
return;
}
- if (!sBreakpointList || sBreakpointCount == 0) {
+ if (!sBreakpointList || arrlen(sBreakpoints) == 0) {
return;
}
// Remove selected breakpoints in reverse order to preserve indices
- for (int32_t i = sBreakpointCount - 1; i >= 0; i--) {
+ for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i >= 0; i--) {
if (wgtListViewIsItemSelected(sBreakpointList, i)) {
arrdel(sBreakpoints, i);
}
}
- sBreakpointCount = (int32_t)arrlen(sBreakpoints);
+ syncVmBreakpoints();
updateBreakpointWindow();
// Repaint editor to remove breakpoint dots
@@ -5691,30 +5101,6 @@ static void onBreakpointListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift)
}
-// ============================================================
-// showBreakpointWindow
-// ============================================================
-
-// Dynamic per-refresh display buffers (stb_ds). Strings are strdup'd and freed
-// on each refresh.
-static char **sBpFiles = NULL;
-static char **sBpProcs = NULL;
-static char **sBpLines = NULL;
-static const char **sBpCells = NULL;
-
-static char **sLocalsNames = NULL;
-static char **sLocalsTypes = NULL;
-static char **sLocalsValues = NULL;
-static const char **sLocalsCells = NULL;
-
-static char **sCallNames = NULL;
-static char **sCallLines = NULL;
-static const char **sCallCells = NULL;
-
-static char **sWatchExprBuf = NULL;
-static char **sWatchValBuf = NULL;
-static const char **sWatchCells = NULL;
-
static void onBreakpointWinClose(WindowT *win) {
dvxHideWindow(sAc, win);
}
@@ -5731,16 +5117,7 @@ static void onClose(WindowT *win) {
}
// Stop any running program
- sStopRequested = true;
-
- if (sVm) {
- sVm->running = false;
- sVm->debugPaused = false;
- }
-
- sDbgState = DBG_IDLE;
- sDbgCurrentLine = -1;
- sDbgEnabled = false;
+ debugStop();
// Prevent stale focus tracking during shutdown
sLastFocusWin = NULL;
@@ -5755,92 +5132,60 @@ static void onClose(WindowT *win) {
sToolbar = NULL;
sStatusBar = NULL;
- // Close all child windows
- // Close all child windows
- if (sCodeWin && sCodeWin != win) {
- dvxDestroyWindow(sAc, sCodeWin);
- }
-
- sCodeWin = NULL;
-
- if (sOutWin && sOutWin != win) {
- dvxDestroyWindow(sAc, sOutWin);
- }
-
- sOutWin = NULL;
-
- if (sImmWin && sImmWin != win) {
- dvxDestroyWindow(sAc, sImmWin);
- }
-
- sImmWin = NULL;
-
- if (sLocalsWin && sLocalsWin != win) {
- dvxDestroyWindow(sAc, sLocalsWin);
- }
-
- sLocalsWin = NULL;
- sLocalsList = NULL;
-
- if (sCallStackWin && sCallStackWin != win) {
- dvxDestroyWindow(sAc, sCallStackWin);
- }
-
- sCallStackWin = NULL;
- sCallStackList = NULL;
-
- if (sWatchWin && sWatchWin != win) {
- dvxDestroyWindow(sAc, sWatchWin);
- }
-
- sWatchWin = NULL;
- sWatchList = NULL;
- sWatchInput = NULL;
-
- if (sBreakpointWin && sBreakpointWin != win) {
- dvxDestroyWindow(sAc, sBreakpointWin);
- }
-
- sBreakpointWin = NULL;
- sBreakpointList = NULL;
-
- if (sFormWin) {
- dvxDestroyWindow(sAc, sFormWin);
- cleanupFormWin();
- }
-
- if (sToolboxWin) {
- tbxDestroy(sAc, sToolboxWin);
- sToolboxWin = NULL;
- }
-
- if (sPropsWin) {
- prpDestroy(sAc, sPropsWin);
- sPropsWin = NULL;
- }
-
- if (sProjectWin) {
- prjDestroyWindow(sAc, sProjectWin);
- sProjectWin = NULL;
- }
-
+ // Close the project (designer, code, project windows and buffers),
+ // then whatever can exist without one (toolbox/properties).
closeProject();
+ closeProjectUi();
+
+ if (sOutWin) {
+ dvxDestroyWindow(sAc, sOutWin);
+ sOutWin = NULL;
+ }
+
+ if (sImmWin) {
+ dvxDestroyWindow(sAc, sImmWin);
+ sImmWin = NULL;
+ }
+
+ if (sLocalsWin) {
+ dvxDestroyWindow(sAc, sLocalsWin);
+ sLocalsWin = NULL;
+ sLocalsList = NULL;
+ }
+
+ if (sCallStackWin) {
+ dvxDestroyWindow(sAc, sCallStackWin);
+ sCallStackWin = NULL;
+ sCallStackList = NULL;
+ }
+
+ if (sWatchWin) {
+ dvxDestroyWindow(sAc, sWatchWin);
+ sWatchWin = NULL;
+ sWatchList = NULL;
+ sWatchInput = NULL;
+ }
+
+ if (sBreakpointWin) {
+ dvxDestroyWindow(sAc, sBreakpointWin);
+ sBreakpointWin = NULL;
+ sBreakpointList = NULL;
+ }
// Don't destroy win here -- the shell manages it. Destroying
// it from inside onClose crashes because the calling code in
// dvxApp.c still references the window after the callback returns.
sWin = NULL;
- if (sCachedModule) {
+ // A running program is still executing sCachedModule (this close
+ // arrived through its event pump); runModule frees it on exit.
+ if (sCachedModule && !sVm) {
basModuleFree(sCachedModule);
sCachedModule = NULL;
}
- dsgnFree(&sDesigner);
-
- freeProcBufs();
-
arrfree(sProcTable);
+ sProcTable = NULL;
for (int32_t i = 0; i < (int32_t)arrlen(sObjItems); i++) {
free((void *)sObjItems[i]);
@@ -5848,79 +5193,41 @@ static void onClose(WindowT *win) {
arrfree(sObjItems);
arrfree(sEvtItems);
- sProcTable = NULL;
- sObjItems = NULL;
- sEvtItems = NULL;
+ sObjItems = NULL;
+ sEvtItems = NULL;
- // Free dynamic strdup'd cell buffers for debug windows
- for (int32_t i = 0; i < (int32_t)arrlen(sBpFiles); i++) {
- free(sBpFiles[i]);
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(sBpProcs); i++) {
- free(sBpProcs[i]);
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(sBpLines); i++) {
- free(sBpLines[i]);
- }
-
- arrfree(sBpFiles);
- arrfree(sBpProcs);
- arrfree(sBpLines);
- arrfree(sBpCells);
-
- for (int32_t i = 0; i < (int32_t)arrlen(sLocalsNames); i++) {
- free(sLocalsNames[i]);
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(sLocalsTypes); i++) {
- free(sLocalsTypes[i]);
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(sLocalsValues); i++) {
- free(sLocalsValues[i]);
- }
-
- arrfree(sLocalsNames);
- arrfree(sLocalsTypes);
- arrfree(sLocalsValues);
- arrfree(sLocalsCells);
-
- for (int32_t i = 0; i < (int32_t)arrlen(sCallNames); i++) {
- free(sCallNames[i]);
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(sCallLines); i++) {
- free(sCallLines[i]);
- }
-
- arrfree(sCallNames);
- arrfree(sCallLines);
- arrfree(sCallCells);
+ listRowsFree(&sBpRows);
+ listRowsFree(&sLocalsRows);
+ listRowsFree(&sCallRows);
+ listRowsFree(&sWatchRows);
for (int32_t i = 0; i < (int32_t)arrlen(sWatchExprs); i++) {
free(sWatchExprs[i]);
}
- for (int32_t i = 0; i < (int32_t)arrlen(sWatchExprBuf); i++) {
- free(sWatchExprBuf[i]);
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(sWatchValBuf); i++) {
- free(sWatchValBuf[i]);
- }
-
arrfree(sWatchExprs);
- arrfree(sWatchExprBuf);
- arrfree(sWatchValBuf);
- arrfree(sWatchCells);
+ sWatchExprs = NULL;
for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) {
free(sRecentFiles[i]);
}
arrfree(sRecentFiles);
+ sRecentFiles = NULL;
+
+ arrfree(sBreakpoints);
+ sBreakpoints = NULL;
+ arrfree(sVmBreakpoints);
+ sVmBreakpoints = NULL;
+
+ free(sFullSourceCache);
+ sFullSourceCache = NULL;
+
+ shfree(sSyntaxMap);
+ sSyntaxMap = NULL;
+
+ prefsClose(sPrefs);
+ sPrefs = NULL;
dvxDestroyWindow(sAc, win);
}
@@ -5966,15 +5273,7 @@ static void onColorSliderChange(WidgetT *w) {
uint8_t g = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderG);
uint8_t b = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderB);
- static char rBuf[8];
- static char gBuf[8];
- static char bBuf[8];
- snprintf(rBuf, sizeof(rBuf), "%d", (int)r);
- snprintf(gBuf, sizeof(gBuf), "%d", (int)g);
- snprintf(bBuf, sizeof(bBuf), "%d", (int)b);
- wgtSetText(sPrefsDlg.lblR, rBuf);
- wgtSetText(sPrefsDlg.lblG, gBuf);
- wgtSetText(sPrefsDlg.lblB, bBuf);
+ prefsSetRgbLabels(r, g, b);
sPrefsDlg.syntaxColors[idx] = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
prefsUpdateSwatch();
@@ -5989,53 +5288,56 @@ static void onContentFocus(WindowT *win) {
static void onEditorChange(WidgetT *w) {
(void)w;
+ if (!sEditor) {
+ return;
+ }
+
+ // Track the line count on every edit so the delta applied to
+ // breakpoints below never accumulates across edits.
+ const char *text = wgtGetText(sEditor);
+ int32_t newLineCount = countLines(text);
+ int32_t delta = newLineCount - sEditorLineCount;
+
+ sEditorLineCount = newLineCount;
+
// Adjust breakpoints when lines are added or removed
- if (sEditor && sBreakpointCount > 0) {
- const char *text = wgtGetText(sEditor);
- int32_t newLineCount = countLines(text);
- int32_t delta = newLineCount - sEditorLineCount;
+ if (delta != 0 && arrlen(sBreakpoints) > 0) {
+ int32_t cursorLine = wgtTextAreaGetCursorLine(sEditor);
- if (delta != 0) {
- int32_t fileIdx = sProject.activeFileIdx;
- int32_t cursorLine = wgtTextAreaGetCursorLine(sEditor);
+ // Convert editor cursor line to file code line
+ int32_t editCodeLine = editorLineToCodeLine(cursorLine);
- // Convert editor cursor line to file code line
- int32_t editCodeLine = editorLineToCodeLine(cursorLine);
+ bool changed = false;
- bool changed = false;
-
- for (int32_t i = sBreakpointCount - 1; i >= 0; i--) {
- if (sBreakpoints[i].fileIdx != fileIdx) {
- continue;
- }
-
- if (sBreakpoints[i].codeLine >= editCodeLine) {
- sBreakpoints[i].codeLine += delta;
-
- // Remove if shifted to invalid line
- if (sBreakpoints[i].codeLine < 1) {
- arrdel(sBreakpoints, i);
- sBreakpointCount = (int32_t)arrlen(sBreakpoints);
- }
-
- changed = true;
- }
+ for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i >= 0; i--) {
+ if (sBreakpoints[i].fileIdx != sEditorFileIdx) {
+ continue;
}
- if (changed) {
- updateBreakpointWindow();
- }
+ if (sBreakpoints[i].codeLine >= editCodeLine) {
+ sBreakpoints[i].codeLine += delta;
- sEditorLineCount = newLineCount;
+ // Remove if shifted to invalid line
+ if (sBreakpoints[i].codeLine < 1) {
+ arrdel(sBreakpoints, i);
+ }
+
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ syncVmBreakpoints();
+ updateBreakpointWindow();
}
}
- // Mark the active file as modified
- if (sProject.activeFileIdx >= 0 && sProject.activeFileIdx < sProject.fileCount) {
- sProject.files[sProject.activeFileIdx].modified = true;
+ // Mark the file the editor is bound to as modified
+ if (sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount) {
+ sProject.files[sEditorFileIdx].modified = true;
// Only mark form dirty when editing the form's code, not a .bas file
- if (sProject.files[sProject.activeFileIdx].isForm && sDesigner.form) {
+ if (sProject.files[sEditorFileIdx].isForm && sDesigner.form) {
sDesigner.form->dirty = true;
}
}
@@ -6044,11 +5346,8 @@ static void onEditorChange(WidgetT *w) {
}
-// ============================================================
-// onEvtDropdownChange
-// ============================================================
-//
-// Navigate to the selected procedure when the event dropdown changes.
+// onEvtDropdownChange -- navigate to the selected procedure when the
+// event dropdown changes.
static void onEvtDropdownChange(WidgetT *w) {
(void)w;
@@ -6073,13 +5372,14 @@ static void onEvtDropdownChange(WidgetT *w) {
const char *selEvt = sEvtItems[evtIdx];
// (Global) shows the General module-level section
- if (strcasecmp(selEvt, "(Global)") == 0) {
+ if (strcasecmp(selEvt, IDE_GLOBAL_SECTION) == 0) {
showProc(-1);
return;
}
// Strip brackets if present (unimplemented event)
- char evtName[IDE_NAME_BUF];
+ char evtName[BAS_MAX_IDENT];
+
if (selEvt[0] == '[') {
snprintf(evtName, sizeof(evtName), "%s", selEvt + 1);
int32_t len = (int32_t)strlen(evtName);
@@ -6102,22 +5402,7 @@ static void onEvtDropdownChange(WidgetT *w) {
}
}
- // Not found -- create a new sub skeleton for editing.
- // Don't mark dirty yet; saveCurProc will discard it if the
- // user doesn't add any code.
- char subName[128];
- snprintf(subName, sizeof(subName), "%s_%s", selObj, evtName);
-
- char skeleton[512];
-
- if (isCtrlArrayInDesigner(selObj)) {
- snprintf(skeleton, sizeof(skeleton), "Sub %s (Index As Integer%s)\n\nEnd Sub\n", subName, getEventExtraParams(evtName));
- } else {
- snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName);
- }
-
- arrput(sProcBufs, strdup(skeleton));
- showProc((int32_t)arrlen(sProcBufs) - 1);
+ createEventSubSkeleton(selObj, evtName);
}
@@ -6235,17 +5520,7 @@ static void onFormWinKey(WindowT *win, int32_t key, int32_t mod) {
// Ctrl+X: cut selected control
if (key == KEY_CTRL_X && (mod & ACCEL_CTRL)) {
dsgnCopySelected();
-
- if (sDesigner.selectedIdx >= 0) {
- dsgnOnKey(&sDesigner, KEY_DELETE);
- prpRebuildTree(&sDesigner);
- prpRefresh(&sDesigner);
-
- if (sFormWin) {
- dvxInvalidateWindow(sAc, sFormWin);
- }
- }
-
+ deleteSelectedControl();
return;
}
@@ -6256,19 +5531,7 @@ static void onFormWinKey(WindowT *win, int32_t key, int32_t mod) {
}
if (key == KEY_DELETE && sDesigner.selectedIdx >= 0) {
- int32_t prevCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0;
- dsgnOnKey(&sDesigner, KEY_DELETE);
- int32_t newCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0;
-
- if (newCount != prevCount) {
- prpRebuildTree(&sDesigner);
- prpRefresh(&sDesigner);
-
- if (sFormWin) {
- dvxInvalidateWindow(sAc, sFormWin);
- }
- }
-
+ deleteSelectedControl();
return;
}
@@ -6300,6 +5563,9 @@ static void onFormWinMenu(WindowT *win, int32_t menuId) {
}
+// onFormWinMouse -- handle mouse events on the form designer window.
+// Coordinates are relative to the window's client area (content box origin).
+
static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
(void)win;
static int32_t lastButtons = 0;
@@ -6327,10 +5593,7 @@ static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons)
}
prpRefresh(&sDesigner);
-
- if (sFormWin) {
- dvxInvalidateWindow(sAc, sFormWin);
- }
+ dvxInvalidateWindow(sAc, sFormWin);
if (clicks >= 2 && sDesigner.activeTool[0] == '\0') {
navigateToEventSub();
@@ -6339,18 +5602,12 @@ static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons)
// Drag
dsgnOnMouse(&sDesigner, x, y, true);
prpRefresh(&sDesigner);
-
- if (sFormWin) {
- dvxInvalidateWindow(sAc, sFormWin);
- }
+ dvxInvalidateWindow(sAc, sFormWin);
} else if (!isDown && wasDown) {
// Release
dsgnOnMouse(&sDesigner, x, y, false);
prpRefresh(&sDesigner);
-
- if (sFormWin) {
- dvxInvalidateWindow(sAc, sFormWin);
- }
+ dvxInvalidateWindow(sAc, sFormWin);
}
lastButtons = buttons;
@@ -6359,11 +5616,7 @@ static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons)
}
-// ============================================================
-// onFormWinPaint
-// ============================================================
-//
-// Draw selection handles after widgets have painted.
+// onFormWinPaint -- draw selection handles after widgets have painted.
static void onFormWinPaint(WindowT *win, RectT *dirtyArea) {
if (!win) {
@@ -6382,10 +5635,7 @@ static void onFormWinPaint(WindowT *win, RectT *dirtyArea) {
widgetOnPaint(win, dirtyArea);
// Then draw selection handles on top
- int32_t winX = win->contentX;
- int32_t winY = win->contentY;
-
- dsgnPaintOverlay(&sDesigner, winX, winY);
+ dsgnPaintOverlay(&sDesigner);
}
@@ -6411,11 +5661,8 @@ static void onGutterClick(WidgetT *w, int32_t lineNum) {
}
-// ============================================================
-// onImmediateChange
-// ============================================================
-//
-// Detect Enter in the Immediate window and evaluate the last line.
+// onImmediateChange -- detect Enter in the Immediate window and evaluate
+// the last line.
static void onImmediateChange(WidgetT *w) {
(void)w;
@@ -6449,7 +5696,7 @@ static void onImmediateChange(WidgetT *w) {
}
// Extract the line
- char expr[512];
+ char expr[IDE_IMM_LINE_BUF];
int32_t lineLen = lineEnd - lineStart;
if (lineLen >= (int32_t)sizeof(expr)) {
@@ -6487,9 +5734,7 @@ static void onMenu(WindowT *win, int32_t menuId) {
}
if (menuId == CMD_HELP_CONTENTS) {
- char hlpPath[DVX_MAX_PATH];
- snprintf(hlpPath, sizeof(hlpPath), "%s" DVX_PATH_SEP "%s", sCtx->appDir, "dvxbasic.hlp");
- shellLoadAppWithArgs(sAc, DVX_HELP_VIEWER_APP_LITERAL, hlpPath);
+ shellLoadAppWithArgs(sAc, DVX_HELP_VIEWER_APP_LITERAL, sIdeHelpFile);
}
if (menuId == CMD_HELP_API) {
@@ -6510,35 +5755,8 @@ static void onMenu(WindowT *win, int32_t menuId) {
}
-// ============================================================
-// onObjDropdownChange
-// ============================================================
-//
-// Update the Event dropdown when the Object selection changes.
-
-// Event subsets shown in the Object/Event dropdowns. The master list
-// of every event the runtime can fire lives in basEventSuffixes[] in
-// compiler/basEvents.h; these are filtered views for the designer.
-
-// Control/widget events (everything except form-lifecycle events)
-static const char *sCommonEvents[] = {
- "Click", "DblClick", "Change", "GotFocus", "LostFocus",
- "KeyPress", "KeyDown", "KeyUp",
- "MouseDown", "MouseUp", "MouseMove", "Scroll",
- "Reposition", "Validate",
- NULL
-};
-
-// Form-specific events (lifecycle + input)
-static const char *sFormEvents[] = {
- "Load", "QueryUnload", "Unload", "Resize", "Activate", "Deactivate",
- "KeyPress", "KeyDown", "KeyUp",
- "MouseDown", "MouseUp", "MouseMove",
- NULL
-};
-
-// Buffer for event dropdown labels (with [] for unimplemented)
-static char sEvtLabelBufs[IDE_MAX_EVT_LABELS][IDE_EVT_LABEL_LEN];
+// onObjDropdownChange -- update the Event dropdown when the Object
+// selection changes.
static void onObjDropdownChange(WidgetT *w) {
(void)w;
@@ -6567,12 +5785,9 @@ static void onObjDropdownChange(WidgetT *w) {
}
}
- // Determine which event list to use
- const char **availEvents = sCommonEvents;
-
- if (strcasecmp(selObj, "(General)") == 0) {
+ if (strcasecmp(selObj, IDE_GENERAL_SECTION) == 0) {
// Always include (Global) to access module-level code
- arrput(sEvtItems, "(Global)");
+ arrput(sEvtItems, IDE_GLOBAL_SECTION);
for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) {
arrput(sEvtItems, existingEvts[i]);
@@ -6598,8 +5813,7 @@ static void onObjDropdownChange(WidgetT *w) {
bool isForm = false;
if (sDesigner.form && strcasecmp(selObj, sDesigner.form->name) == 0) {
- isForm = true;
- availEvents = sFormEvents;
+ isForm = true;
}
// Check if this is a menu item (only event is Click)
@@ -6614,11 +5828,24 @@ static void onObjDropdownChange(WidgetT *w) {
}
}
+ // Standard events for this kind of object, from the master table
+ // (NULL-terminated stb_ds temp array)
+ const char **availEvents = NULL;
+
if (isMenuItem) {
- static const char *sMenuEvents[] = { "Click", NULL };
- availEvents = sMenuEvents;
+ arrput(availEvents, IDE_MENU_EVENT);
+ } else {
+ uint8_t scope = isForm ? BAS_EVT_SCOPE_FORM : BAS_EVT_SCOPE_CTRL;
+
+ for (int32_t i = 0; i < IDE_EVENT_COUNT; i++) {
+ if (sEventTable[i].scope & scope) {
+ arrput(availEvents, sEventTable[i].suffix);
+ }
+ }
}
+ arrput(availEvents, NULL);
+
// Get widget-specific events from the interface
const WgtIfaceT *iface = NULL;
@@ -6709,12 +5936,7 @@ static void onObjDropdownChange(WidgetT *w) {
int32_t evtCount = (int32_t)arrlen(sEvtItems);
for (int32_t j = 0; j < evtCount; j++) {
- const char *label = sEvtItems[j];
-
- // Strip brackets for comparison
- if (label[0] == '[') { label++; }
-
- if (strcasecmp(label, existingEvts[i]) == 0) {
+ if (evtLabelMatches(sEvtItems[j], existingEvts[i])) {
alreadyListed = true;
break;
}
@@ -6728,6 +5950,7 @@ static void onObjDropdownChange(WidgetT *w) {
}
arrfree(existingEvts);
+ arrfree(availEvents);
int32_t evtCount = (int32_t)arrlen(sEvtItems);
@@ -6792,10 +6015,6 @@ static void onPrefsCancel(WidgetT *w) {
}
-// ============================================================
-// Preferences dialog
-// ============================================================
-
static void onPrefsOk(WidgetT *w) {
(void)w;
sPrefsDlg.accepted = true;
@@ -6879,6 +6098,11 @@ static void onReplaceAll(WidgetT *w) {
stashCurrentFile();
for (int32_t i = 0; i < sProject.fileCount; i++) {
+ // Skip files whose buffer has no match at all
+ if (!procBufContains(sProject.files[i].buffer, sFindText, needleLen, caseSens)) {
+ continue;
+ }
+
activateFile(i, sProject.files[i].isForm ? ViewCodeE : ViewAutoE);
// wgtTextAreaReplaceAll only touches the proc currently shown
@@ -6934,7 +6158,7 @@ static void onReplaceAll(WidgetT *w) {
}
}
- char statusBuf[IDE_NAME_BUF];
+ char statusBuf[BAS_MAX_IDENT];
snprintf(statusBuf, sizeof(statusBuf), "%d replacement(s) made.", (int)totalCount);
setStatus(statusBuf);
}
@@ -6944,38 +6168,88 @@ static void onReplCheckChange(WidgetT *w) {
(void)w;
bool show = isReplaceEnabled();
- if (sReplInput) { sReplInput->enabled = show; }
- if (sBtnReplace) { sBtnReplace->enabled = show; }
- if (sBtnReplAll) { sBtnReplAll->enabled = show; }
+ if (sReplInput) {
+ sReplInput->enabled = show;
+ }
+
+ if (sBtnReplace) {
+ sBtnReplace->enabled = show;
+ }
+
+ if (sBtnReplAll) {
+ sBtnReplAll->enabled = show;
+ }
if (sFindWin) {
dvxInvalidateWindow(sAc, sFindWin);
}
}
-static void onTbCode(WidgetT *w) { (void)w; handleViewCmd(CMD_VIEW_CODE); }
+
+static void onTbCode(WidgetT *w) {
+ (void)w;
+ handleViewCmd(CMD_VIEW_CODE);
+}
-static void onTbDebug(WidgetT *w) { (void)w; handleRunCmd(CMD_DEBUG); }
-
-static void onTbDesign(WidgetT *w) { (void)w; handleViewCmd(CMD_VIEW_DESIGN); }
+static void onTbDebug(WidgetT *w) {
+ (void)w;
+ handleRunCmd(CMD_DEBUG);
+}
-static void onTbOpen(WidgetT *w) { (void)w; handleProjectCmd(CMD_PRJ_OPEN); }
+static void onTbDesign(WidgetT *w) {
+ (void)w;
+ handleViewCmd(CMD_VIEW_DESIGN);
+}
-static void onTbRun(WidgetT *w) { (void)w; handleRunCmd(CMD_RUN); }
-static void onTbRunToCur(WidgetT *w) { (void)w; handleRunCmd(CMD_RUN_TO_CURSOR); }
+static void onTbOpen(WidgetT *w) {
+ (void)w;
+ handleProjectCmd(CMD_PRJ_OPEN);
+}
-static void onTbSave(WidgetT *w) { (void)w; handleFileCmd(CMD_SAVE); }
-static void onTbStepInto(WidgetT *w) { (void)w; handleRunCmd(CMD_STEP_INTO); }
+static void onTbRun(WidgetT *w) {
+ (void)w;
+ handleRunCmd(CMD_RUN);
+}
-static void onTbStepOut(WidgetT *w) { (void)w; handleRunCmd(CMD_STEP_OUT); }
-static void onTbStepOver(WidgetT *w) { (void)w; handleRunCmd(CMD_STEP_OVER); }
+static void onTbRunToCur(WidgetT *w) {
+ (void)w;
+ handleRunCmd(CMD_RUN_TO_CURSOR);
+}
-static void onTbStop(WidgetT *w) { (void)w; handleRunCmd(CMD_STOP); }
+
+static void onTbSave(WidgetT *w) {
+ (void)w;
+ handleFileCmd(CMD_SAVE);
+}
+
+
+static void onTbStepInto(WidgetT *w) {
+ (void)w;
+ handleRunCmd(CMD_STEP_INTO);
+}
+
+
+static void onTbStepOut(WidgetT *w) {
+ (void)w;
+ handleRunCmd(CMD_STEP_OUT);
+}
+
+
+static void onTbStepOver(WidgetT *w) {
+ (void)w;
+ handleRunCmd(CMD_STEP_OVER);
+}
+
+
+static void onTbStop(WidgetT *w) {
+ (void)w;
+ handleRunCmd(CMD_STOP);
+}
static void onWatchClose(WindowT *win) {
@@ -7002,7 +6276,13 @@ static void onWatchInputKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
}
// Add to watch list
- arrput(sWatchExprs, strdup(text));
+ char *copy = strdup(text);
+
+ if (!copy) {
+ return;
+ }
+
+ arrput(sWatchExprs, copy);
updateWatchWindow();
// Clear input
@@ -7020,7 +6300,7 @@ static void onWatchListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
(void)w;
(void)shift;
- // Enter — edit selected item
+ // Enter -- edit selected item
if (keyCode == '\r' || keyCode == '\n') {
watchEditSelected();
return;
@@ -7050,7 +6330,7 @@ static void onWatchListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
static void openFindDialog(bool showReplace) {
if (sFindWin) {
- // Already open — just toggle replace mode and raise
+ // Already open -- just toggle replace mode and raise
if (sReplCheck) {
wgtCheckboxSetChecked(sReplCheck, showReplace);
onReplCheckChange(sReplCheck);
@@ -7060,7 +6340,7 @@ static void openFindDialog(bool showReplace) {
return;
}
- sFindWin = dvxCreateWindowCentered(sAc, "Find / Replace", 320, 210, false);
+ sFindWin = dvxCreateWindowCentered(sAc, "Find / Replace", IDE_FIND_WIN_W, IDE_FIND_WIN_H, false);
if (!sFindWin) {
return;
@@ -7071,29 +6351,29 @@ static void openFindDialog(bool showReplace) {
sFindWin->accelTable = sWin ? sWin->accelTable : NULL;
WidgetT *root = wgtInitWindow(sAc, sFindWin);
- root->spacing = wgtPixels(3);
+ root->spacing = wgtPixels(IDE_FIND_ROOT_SPACING);
// Find row
WidgetT *findRow = wgtHBox(root);
- findRow->spacing = wgtPixels(4);
+ findRow->spacing = wgtPixels(IDE_WIDGET_SPACING);
wgtLabel(findRow, "Find:");
- sFindInput = wgtTextInput(findRow, 256);
+ sFindInput = wgtTextInput(findRow, IDE_FIND_TEXT_BUF);
sFindInput->weight = WGT_WEIGHT_FILL;
wgtSetText(sFindInput, sFindText);
// Replace checkbox + input
WidgetT *replRow = wgtHBox(root);
- replRow->spacing = wgtPixels(4);
+ replRow->spacing = wgtPixels(IDE_WIDGET_SPACING);
sReplCheck = wgtCheckbox(replRow, "Replace:");
wgtCheckboxSetChecked(sReplCheck, showReplace);
sReplCheck->onChange = onReplCheckChange;
- sReplInput = wgtTextInput(replRow, 256);
+ sReplInput = wgtTextInput(replRow, IDE_FIND_TEXT_BUF);
sReplInput->weight = WGT_WEIGHT_FILL;
wgtSetText(sReplInput, sReplaceText);
// Options row: scope + direction + case
WidgetT *optRow = wgtHBox(root);
- optRow->spacing = wgtPixels(8);
+ optRow->spacing = wgtPixels(IDE_WIDGET_SPACING_LG);
// Scope
WidgetT *scopeFrame = wgtFrame(optRow, "Scope");
@@ -7103,7 +6383,7 @@ static void openFindDialog(bool showReplace) {
wgtRadio(sScopeGroup, "Object");
wgtRadio(sScopeGroup, "File");
wgtRadio(sScopeGroup, "Project");
- wgtRadioGroupSetSelected(sScopeGroup, 3); // Project
+ wgtRadioGroupSetSelected(sScopeGroup, ScopeProjE);
// Direction
WidgetT *dirFrame = wgtFrame(optRow, "Direction");
@@ -7119,7 +6399,7 @@ static void openFindDialog(bool showReplace) {
// Buttons
WidgetT *btnRow = wgtHBox(root);
- btnRow->spacing = wgtPixels(4);
+ btnRow->spacing = wgtPixels(IDE_WIDGET_SPACING);
btnRow->align = AlignEndE;
WidgetT *btnFind = wgtButton(btnRow, "Find Next");
@@ -7142,6 +6422,10 @@ static void openFindDialog(bool showReplace) {
static void openProject(void) {
+ if (!promptAndSave()) {
+ return;
+ }
+
FileFilterT filters[] = {
{ "Project Files (*.dbp)" },
{ "All Files (*.*)" }
@@ -7149,141 +6433,135 @@ static void openProject(void) {
char path[DVX_MAX_PATH];
- if (!dvxFileDialog(sAc, "Open Project", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
+ if (!dvxFileDialog(sAc, "Open Project", FD_OPEN, NULL, filters, (int32_t)(sizeof(filters) / sizeof(filters[0])), path, sizeof(path))) {
return;
}
closeProject();
+ openProjectPath(path);
+}
+
+// openProjectPath -- load a .dbp (the previous project is already closed)
+static void openProjectPath(const char *path) {
if (!prjLoad(&sProject, path)) {
dvxErrorBox(sAc, NULL, "Could not open project file.");
return;
}
prjLoadAllFiles(&sProject, sAc);
-
- // Create and show project window
- if (!sProjectWin) {
- sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);
-
- if (sProjectWin) {
- sProjectWin->y = toolbarBottom() + 25;
- sProjectWin->onClose = onProjectWinClose;
- sProjectWin->onMenu = onMenu;
- sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
- }
- } else {
- prjRebuildTree(&sProject);
- }
-
- char title[300];
- snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
- dvxSetTitle(sAc, sWin, title);
-
+ ensureProjectWindow(false);
+ updateMainTitle();
setStatus("Project loaded.");
updateProjectMenuState();
recentAdd(path);
}
+// openSinglePath -- open a .bas/.frm: added to the current project, or
+// used to create an implicit project when none is open.
+static void openSinglePath(const char *path) {
+ if (hasProject()) {
+ if (addFileToProject(path)) {
+ recentAdd(path);
+ }
+
+ return;
+ }
+
+ if (!promptAndSave()) {
+ return;
+ }
+
+ ensureProject(path);
+ ensureProjectWindow(true);
+ updateProjectMenuState();
+ recentAdd(path);
+}
+
+
+// packProcLayout -- normalize source text to the canonical proc layout
+// (see joinProcArrays). Returns a malloc'd string or NULL.
+static char *packProcLayout(const char *source) {
+ char *general = NULL;
+ char **procs = NULL;
+
+ splitProcs(source, &general, &procs);
+
+ char *packed = joinProcArrays(general, procs);
+
+ free(general);
+
+ for (int32_t i = 0; i < (int32_t)arrlen(procs); i++) {
+ free(procs[i]);
+ }
+
+ arrfree(procs);
+ return packed;
+}
+
+
+// parseIndexList -- parse "i1, i2, ...)" starting just after the '('.
+// Returns the position after the closing ')' (or where parsing stopped).
+static const char *parseIndexList(const char *p, int32_t *indices, int32_t *outCount) {
+ int32_t n = 0;
+
+ while (*p && *p != ')' && n < BAS_ARRAY_MAX_DIMS) {
+ const char *start = p;
+
+ p = dvxSkipWs(p);
+ indices[n++] = atoi(p);
+
+ // Skip past the number
+ if (*p == '-') {
+ p++;
+ }
+
+ while (*p >= '0' && *p <= '9') {
+ p++;
+ }
+
+ p = dvxSkipWs(p);
+
+ if (*p == ',') {
+ p++;
+ }
+
+ // Anything unparseable would otherwise spin forever
+ if (p == start) {
+ break;
+ }
+ }
+
+ if (*p == ')') {
+ p++;
+ }
+
+ *outCount = n;
+ return p;
+}
+
+
// parseProcs -- split source into (General) + per-procedure buffers
static void parseProcs(const char *source) {
freeProcBufs();
+ splitProcs(source ? source : "", &sGeneralBuf, &sProcBufs);
+}
- free(sParsedSource);
- sParsedSource = source ? strdup(source) : NULL;
- if (!source) {
- sGeneralBuf = strdup("");
- return;
- }
+// prefsSetRgbLabels -- show the slider values next to the sliders
+static void prefsSetRgbLabels(uint8_t r, uint8_t g, uint8_t b) {
+ char rBuf[IDE_RGB_LABEL_BUF];
+ char gBuf[IDE_RGB_LABEL_BUF];
+ char bBuf[IDE_RGB_LABEL_BUF];
- const char *pos = source;
- const char *genEnd = source; // end of (General) section
-
- while (*pos) {
- const char *lineStart = pos;
-
- // Skip leading whitespace
- const char *trimmed = dvxSkipWs(pos);
-
- bool isSub = (strncasecmp(trimmed, "SUB ", 4) == 0);
- bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0);
-
- if (isSub || isFunc) {
- // On first proc, mark end of (General) section
- if (arrlen(sProcBufs) == 0) {
- genEnd = lineStart;
- }
-
- // Find End Sub / End Function
- const char *endTag = isSub ? "END SUB" : "END FUNCTION";
- int32_t endTagLen = isSub ? 7 : 12;
- const char *scan = pos;
-
- // Advance past the Sub/Function line
- while (*scan && *scan != '\n') { scan++; }
- if (*scan == '\n') { scan++; }
-
- // Scan for End Sub/Function
- while (*scan) {
- const char *sl = dvxSkipWs(scan);
-
- if (strncasecmp(sl, endTag, endTagLen) == 0) {
- while (*scan && *scan != '\n') { scan++; }
- if (*scan == '\n') { scan++; }
- break;
- }
-
- while (*scan && *scan != '\n') { scan++; }
- if (*scan == '\n') { scan++; }
- }
-
- // Extract this procedure
- int32_t procLen = (int32_t)(scan - lineStart);
- char *procBuf = (char *)malloc(procLen + 1);
-
- if (procBuf) {
- memcpy(procBuf, lineStart, procLen);
- procBuf[procLen] = '\0';
- }
-
- arrput(sProcBufs, procBuf);
- pos = scan;
- continue;
- }
-
- // Advance to next line
- while (*pos && *pos != '\n') { pos++; }
- if (*pos == '\n') { pos++; }
- }
-
- // If no procs found, the entire source is the (General) section
- if (arrlen(sProcBufs) == 0) {
- genEnd = pos; // pos is at the end of the source
- }
-
- // Extract (General) section
- int32_t genLen = (int32_t)(genEnd - source);
-
- // Trim trailing blank lines
- while (genLen > 0 && (source[genLen - 1] == '\n' || source[genLen - 1] == '\r' ||
- source[genLen - 1] == ' ' || source[genLen - 1] == '\t')) {
- genLen--;
- }
-
- sGeneralBuf = (char *)malloc(genLen + 2);
-
- if (sGeneralBuf) {
- memcpy(sGeneralBuf, source, genLen);
- sGeneralBuf[genLen] = '\n';
- sGeneralBuf[genLen + 1] = '\0';
-
- if (genLen == 0) {
- sGeneralBuf[0] = '\0';
- }
- }
+ snprintf(rBuf, sizeof(rBuf), "%d", (int)r);
+ snprintf(gBuf, sizeof(gBuf), "%d", (int)g);
+ snprintf(bBuf, sizeof(bBuf), "%d", (int)b);
+ wgtSetText(sPrefsDlg.lblR, rBuf);
+ wgtSetText(sPrefsDlg.lblG, gBuf);
+ wgtSetText(sPrefsDlg.lblB, bBuf);
}
@@ -7302,17 +6580,7 @@ static void prefsUpdateColorSliders(void) {
wgtSliderSetValue(sPrefsDlg.sliderR, r);
wgtSliderSetValue(sPrefsDlg.sliderG, g);
wgtSliderSetValue(sPrefsDlg.sliderB, b);
-
- static char rBuf[8];
- static char gBuf[8];
- static char bBuf[8];
- snprintf(rBuf, sizeof(rBuf), "%d", (int)r);
- snprintf(gBuf, sizeof(gBuf), "%d", (int)g);
- snprintf(bBuf, sizeof(bBuf), "%d", (int)b);
- wgtSetText(sPrefsDlg.lblR, rBuf);
- wgtSetText(sPrefsDlg.lblG, gBuf);
- wgtSetText(sPrefsDlg.lblB, bBuf);
-
+ prefsSetRgbLabels(r, g, b);
prefsUpdateSwatch();
}
@@ -7331,11 +6599,6 @@ static void prefsUpdateSwatch(void) {
}
-// ============================================================
-// onOpenClick
-// ============================================================
-
-
static void printCallback(void *ctx, const char *text, bool newline) {
(void)ctx;
@@ -7384,56 +6647,142 @@ static bool procBufContains(const char *hay, const char *needle, int32_t needleL
}
-// ============================================================
-// promptAndSave -- ask user to save, discard, or cancel
-// ============================================================
-//
+// procDeclAt -- if p (already whitespace-skipped) starts a SUB or
+// FUNCTION declaration, returns the text after the keyword; else NULL.
+static const char *procDeclAt(const char *p, bool *outIsSub) {
+ if (kwMatch(p, KW_SUB)) {
+ *outIsSub = true;
+ return p + KW_LEN(KW_SUB);
+ }
+
+ if (kwMatch(p, KW_FUNCTION)) {
+ *outIsSub = false;
+ return p + KW_LEN(KW_FUNCTION);
+ }
+
+ return NULL;
+}
+
+
+// procNameFromDecl -- copy the procedure name that follows a SUB/FUNCTION
+// keyword (stops at '(' or whitespace).
+static void procNameFromDecl(const char *afterKw, char *out, int32_t outSize) {
+ const char *p = dvxSkipWs(afterKw);
+ int32_t n = 0;
+
+ while (*p && *p != '(' && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && n < outSize - 1) {
+ out[n++] = *p++;
+ }
+
+ out[n] = '\0';
+}
+
+
+// promptAndSave -- ask user to save, discard, or cancel.
// Returns true if the caller should proceed (user saved or discarded).
-// Returns false if the user cancelled.
+// Returns false if the user cancelled or a save failed.
static bool promptAndSave(void) {
if (!hasUnsavedData()) {
return true;
}
- int32_t result = dvxPromptSave(sAc, "DVX BASIC");
+ int32_t result = dvxPromptSave(sAc, IDE_MAIN_TITLE);
if (result == DVX_SAVE_YES) {
- saveFile();
- return true;
+ return saveAllModified();
}
return result == DVX_SAVE_NO;
}
-// ============================================================
-// updateWatchWindow -- evaluate watch expressions
-// ============================================================
+// rebuildProcTable -- scan canonical-layout source for SUB/FUNCTION
+// declarations and record each proc's object/event split plus its line
+// range. Procedure names are split on '_' into ObjectName and EventName
+// (e.g. "Command1_Click") using the designer's known object names.
-// readDebugVar -- read a debug variable's value from the paused VM
-static bool readDebugVar(const BasDebugVarT *dv, BasValueT *outVal) {
- BasValueT val;
- memset(&val, 0, sizeof(val));
+static void rebuildProcTable(const char *src) {
+ arrsetlen(sProcTable, 0);
- if (dv->scope == SCOPE_LOCAL && sVm->callDepth > 0) {
- BasCallFrameT *frame = &sVm->callStack[sVm->callDepth - 1];
+ // Collect all known object names once; they only depend on sDesigner.form
+ // and never change during the proc scan below.
+ const char **objNames = NULL; // stb_ds temp array
- if (dv->index >= 0 && dv->index < BAS_VM_MAX_LOCALS) {
- val = frame->locals[dv->index];
+ if (sDesigner.form) {
+ arrput(objNames, sDesigner.form->name);
+
+ for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls); ci++) {
+ arrput(objNames, sDesigner.form->controls[ci]->name);
}
- } else if (dv->scope == SCOPE_GLOBAL) {
- if (dv->index >= 0 && dv->index < BAS_VM_MAX_GLOBALS) {
- val = sVm->globals[dv->index];
- }
- } else if (dv->scope == SCOPE_FORM && sVm->currentFormVars) {
- if (dv->index >= 0 && dv->index < sVm->currentFormVarCount) {
- val = sVm->currentFormVars[dv->index];
+
+ for (int32_t mi = 0; mi < (int32_t)arrlen(sDesigner.form->menuItems); mi++) {
+ arrput(objNames, sDesigner.form->menuItems[mi].name);
}
}
- *outVal = val;
- return true;
+ // Scan line by line for SUB / FUNCTION
+ const char *pos = src;
+ int32_t lineNum = 1;
+
+ while (*pos) {
+ bool isSub = false;
+ const char *afterKw = procDeclAt(dvxSkipWs(pos), &isSub);
+
+ if (!afterKw) {
+ pos = skipLine(pos);
+ lineNum++;
+ continue;
+ }
+
+ char procName[BAS_MAX_IDENT];
+ procNameFromDecl(afterKw, procName, sizeof(procName));
+
+ // Find End Sub / End Function and record its line.
+ int32_t endOffset = 0;
+ const char *scan = skipToEndProc(pos, isSub, &endOffset);
+
+ IdeProcEntryT entry;
+ memset(&entry, 0, sizeof(entry));
+ entry.lineNum = lineNum;
+ entry.endLineNum = lineNum + endOffset;
+
+ // Match proc name against known objects: form name, controls,
+ // menu items. Try each as a prefix followed by "_". This handles
+ // names with multiple underscores correctly (e.g., "cmdOK_Click"
+ // matches "cmdOK", not "This_Is_A_Dumb_Name" matching a control
+ // named "This").
+ bool isEvent = false;
+
+ for (int32_t oi = 0; oi < (int32_t)arrlen(objNames); oi++) {
+ int32_t objLen = (int32_t)strlen(objNames[oi]);
+
+ if (objLen > 0 && strncasecmp(procName, objNames[oi], objLen) == 0 && procName[objLen] == '_') {
+ snprintf(entry.objName, sizeof(entry.objName), "%s", objNames[oi]);
+ snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName + objLen + 1);
+ isEvent = true;
+ break;
+ }
+ }
+
+ if (!isEvent) {
+ snprintf(entry.objName, sizeof(entry.objName), "%s", IDE_GENERAL_SECTION);
+ snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName);
+ }
+
+ arrput(sProcTable, entry);
+
+ // Skip to end of this proc, counting the lines we passed
+ for (const char *c = pos; c < scan; c++) {
+ if (*c == '\n') {
+ lineNum++;
+ }
+ }
+
+ pos = scan;
+ }
+
+ arrfree(objNames);
}
@@ -7462,7 +6811,13 @@ static void recentAdd(const char *path) {
}
// Insert new entry at the top
- arrins(sRecentFiles, 0, strdup(path));
+ char *copy = strdup(path);
+
+ if (!copy) {
+ return;
+ }
+
+ arrins(sRecentFiles, 0, copy);
recentSave();
recentRebuildMenu();
}
@@ -7481,12 +6836,16 @@ static void recentLoad(void) {
}
for (int32_t i = 0; i < CMD_RECENT_MAX; i++) {
- char key[16];
- snprintf(key, sizeof(key), "file%ld", (long)i);
- const char *val = prefsGetString(sPrefs, "recent", key, "");
+ char key[IDE_PREF_KEY_BUF];
+ snprintf(key, sizeof(key), PREF_KEY_RECENT_FMT, (long)i);
+ const char *val = prefsGetString(sPrefs, PREF_SEC_RECENT, key, "");
if (val[0]) {
- arrput(sRecentFiles, strdup(val));
+ char *copy = strdup(val);
+
+ if (copy) {
+ arrput(sRecentFiles, copy);
+ }
}
}
}
@@ -7497,58 +6856,23 @@ static void recentOpen(int32_t index) {
return;
}
- const char *path = sRecentFiles[index];
- const char *ext = strrchr(path, '.');
+ // recentAdd reorders the list, so work from a private copy of the path
+ char path[DVX_MAX_PATH];
+ snprintf(path, sizeof(path), "%s", sRecentFiles[index]);
+
+ const char *ext = strrchr(path, '.');
if (ext && strcasecmp(ext, ".dbp") == 0) {
// Project file
- closeProject();
-
- if (!prjLoad(&sProject, path)) {
- dvxErrorBox(sAc, NULL, "Could not open project file.");
- return;
- }
-
- prjLoadAllFiles(&sProject, sAc);
-
- if (!sProjectWin) {
- sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);
-
- if (sProjectWin) {
- sProjectWin->y = toolbarBottom() + 25;
- sProjectWin->onClose = onProjectWinClose;
- sProjectWin->onMenu = onMenu;
- sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
- }
- } else {
- prjRebuildTree(&sProject);
- }
-
- char title[300];
- snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
- dvxSetTitle(sAc, sWin, title);
- setStatus("Project loaded.");
- } else {
- // Single file
if (!promptAndSave()) {
return;
}
- ensureProject(path);
-
- if (!sProjectWin) {
- sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);
-
- if (sProjectWin) {
- sProjectWin->y = toolbarBottom() + 25;
- sProjectWin->onClose = onProjectWinClose;
- dvxRaiseWindow(sAc, sProjectWin);
- }
- }
+ closeProject();
+ openProjectPath(path);
+ } else {
+ openSinglePath(path);
}
-
- updateProjectMenuState();
- recentAdd(path);
}
@@ -7588,14 +6912,9 @@ static void recentSave(void) {
int32_t count = (int32_t)arrlen(sRecentFiles);
for (int32_t i = 0; i < CMD_RECENT_MAX; i++) {
- char key[16];
- snprintf(key, sizeof(key), "file%ld", (long)i);
-
- if (i < count) {
- prefsSetString(sPrefs, "recent", key, sRecentFiles[i]);
- } else {
- prefsSetString(sPrefs, "recent", key, "");
- }
+ char key[IDE_PREF_KEY_BUF];
+ snprintf(key, sizeof(key), PREF_KEY_RECENT_FMT, (long)i);
+ prefsSetString(sPrefs, PREF_SEC_RECENT, key, i < count ? sRecentFiles[i] : "");
}
prefsSave(sPrefs);
@@ -7605,7 +6924,7 @@ static void recentSave(void) {
static void removeBreakpointsForFile(int32_t fileIdx) {
// Remove breakpoints for the given file and adjust indices for
// files above the removed one (since file indices shift down).
- for (int32_t i = sBreakpointCount - 1; i >= 0; i--) {
+ for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i >= 0; i--) {
if (sBreakpoints[i].fileIdx == fileIdx) {
arrdel(sBreakpoints, i);
} else if (sBreakpoints[i].fileIdx > fileIdx) {
@@ -7613,11 +6932,14 @@ static void removeBreakpointsForFile(int32_t fileIdx) {
}
}
- sBreakpointCount = (int32_t)arrlen(sBreakpoints);
+ syncVmBreakpoints();
updateBreakpointWindow();
}
+// renameInBuffer -- word-boundary rename of OldName followed by '.' or
+// '_'. The character before the match must be a non-identifier
+// character to avoid replacing "Command1" inside "MyCommand1".
static char *renameInBuffer(const char *src, const char *oldName, const char *newName) {
if (!src || !oldName || !newName || !oldName[0]) {
return NULL;
@@ -7626,7 +6948,7 @@ static char *renameInBuffer(const char *src, const char *oldName, const char *ne
int32_t oldLen = (int32_t)strlen(oldName);
int32_t newLen = (int32_t)strlen(newName);
int32_t srcLen = (int32_t)strlen(src);
- bool skipComments = prefsGetBool(sPrefs, "editor", "renameSkipComments", true);
+ bool skipComments = prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, true);
// First pass: count replacements to compute output size
int32_t count = 0;
@@ -7642,7 +6964,7 @@ static char *renameInBuffer(const char *src, const char *oldName, const char *ne
continue;
}
- if (i > 0 && isIdentChar(src[i - 1])) {
+ if (i > 0 && basIsIdentChar(src[i - 1])) {
continue;
}
@@ -7674,7 +6996,7 @@ static char *renameInBuffer(const char *src, const char *oldName, const char *ne
char after = src[i + oldLen];
if ((after == '.' || after == '_') &&
- (i == 0 || !isIdentChar(src[i - 1])) &&
+ (i == 0 || !basIsIdentChar(src[i - 1])) &&
(!skipComments || !isInStringOrComment(src, i))) {
memcpy(out + op, newName, newLen);
op += newLen;
@@ -7691,6 +7013,116 @@ static char *renameInBuffer(const char *src, const char *oldName, const char *ne
}
+// resolveVarPath -- parse a variable reference and resolve it to a
+// pointer into the running VM's live data. Handles:
+// varName -- scalar variable
+// varName(i) -- array element
+// varName.field -- UDT field
+// varName(i).field -- array element UDT field
+// Returns NULL if it can't be resolved. *endPtr is set past the path.
+
+static BasValueT *resolveVarPath(const char *path, const char **endPtr) {
+ const char *p = path;
+
+ // Extract variable name (identifier plus optional type suffix)
+ const char *nameStart = p;
+
+ while (basIsIdentChar(*p)) {
+ p++;
+ }
+
+ if (p == nameStart) {
+ return NULL;
+ }
+
+ if (basIsTypeSuffixChar(*p)) {
+ p++;
+ }
+
+ char varName[IDE_FULL_NAME_BUF];
+ int32_t nameLen = (int32_t)(p - nameStart);
+
+ if (nameLen >= (int32_t)sizeof(varName)) {
+ return NULL;
+ }
+
+ memcpy(varName, nameStart, nameLen);
+ varName[nameLen] = '\0';
+
+ // Look up the base variable
+ const BasDebugVarT *dv = findDebugVar(varName);
+
+ if (!dv) {
+ return NULL;
+ }
+
+ BasValueT *slot = getDebugVarSlot(dv);
+
+ if (!slot) {
+ return NULL;
+ }
+
+ // Parse optional array subscript: (idx1, idx2, ...)
+ if (*p == '(') {
+ if (slot->type != BAS_TYPE_ARRAY || !slot->arrVal) {
+ return NULL;
+ }
+
+ int32_t indices[BAS_ARRAY_MAX_DIMS];
+ int32_t numIndices = 0;
+
+ p = parseIndexList(p + 1, indices, &numIndices);
+
+ int32_t flatIdx = basArrayIndex(slot->arrVal, indices, numIndices);
+
+ if (flatIdx < 0 || flatIdx >= slot->arrVal->totalElements) {
+ return NULL;
+ }
+
+ slot = &slot->arrVal->elements[flatIdx];
+ }
+
+ // Parse optional UDT field: .fieldName
+ if (*p == '.') {
+ p++; // skip '.'
+
+ if (slot->type != BAS_TYPE_UDT || !slot->udtVal) {
+ return NULL;
+ }
+
+ const char *fieldStart = p;
+
+ while (basIsIdentChar(*p)) {
+ p++;
+ }
+
+ char fieldName[IDE_FULL_NAME_BUF];
+ int32_t fieldLen = (int32_t)(p - fieldStart);
+
+ if (fieldLen <= 0 || fieldLen >= (int32_t)sizeof(fieldName)) {
+ return NULL;
+ }
+
+ memcpy(fieldName, fieldStart, fieldLen);
+ fieldName[fieldLen] = '\0';
+
+ int32_t fieldIdx = findUdtFieldIdx(slot->udtVal->typeId, fieldName);
+
+ if (fieldIdx < 0 || fieldIdx >= slot->udtVal->fieldCount) {
+ return NULL;
+ }
+
+ slot = &slot->udtVal->fields[fieldIdx];
+ }
+
+ if (endPtr) {
+ *endPtr = p;
+ }
+
+ return slot;
+}
+
+
static void runCached(void) {
if (!sCachedModule) {
setStatus("No compiled program. Press F5 to compile first.");
@@ -7703,26 +7135,54 @@ static void runCached(void) {
static void runModule(BasModuleT *mod) {
- setStatus("Running...");
+ // Create VM
+ BasVmT *vm = basVmCreate();
+ if (!vm) {
+ setStatus("Out of memory creating VM.");
+ return;
+ }
+
+ // Create form runtime (bridges UI opcodes to DVX widgets)
+ BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod);
+
+ if (!formRt) {
+ basVmDestroy(vm);
+ setStatus("Out of memory creating form runtime.");
+ return;
+ }
+
+ setStatus("Running...");
closeFindDialog();
- // Hide designer windows while the program runs.
- // Keep the code window visible if debugging (breakpoints or step-into).
+ // Hide the designer, code and project windows while the program runs;
+ // they are restored (where they were visible) when it ends.
bool hadFormWin = sFormWin && sFormWin->visible;
bool hadToolbox = sToolboxWin && sToolboxWin->visible;
bool hadProps = sPropsWin && sPropsWin->visible;
bool hadCodeWin = sCodeWin && sCodeWin->visible;
bool hadPrjWin = sProjectWin && sProjectWin->visible;
- if (sFormWin) { dvxHideWindow(sAc, sFormWin); }
- if (sToolboxWin) { dvxHideWindow(sAc, sToolboxWin); }
- if (sPropsWin) { dvxHideWindow(sAc, sPropsWin); }
- if (sCodeWin) { dvxHideWindow(sAc, sCodeWin); }
- if (sProjectWin) { dvxHideWindow(sAc, sProjectWin); }
+ if (sFormWin) {
+ dvxHideWindow(sAc, sFormWin);
+ }
+
+ if (sToolboxWin) {
+ dvxHideWindow(sAc, sToolboxWin);
+ }
+
+ if (sPropsWin) {
+ dvxHideWindow(sAc, sPropsWin);
+ }
+
+ if (sCodeWin) {
+ dvxHideWindow(sAc, sCodeWin);
+ }
+
+ if (sProjectWin) {
+ dvxHideWindow(sAc, sProjectWin);
+ }
- // Create VM
- BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
// Set App.Path/Config/Data. In the IDE, config and data live under
@@ -7730,8 +7190,8 @@ static void runModule(BasModuleT *mod) {
// Standalone apps use the DVX root-level CONFIG/ and DATA/ directories
// since the app directory (on CD-ROM) is read-only.
snprintf(vm->appPath, sizeof(vm->appPath), "%s", sProject.projectDir);
- snprintf(vm->appConfig, sizeof(vm->appConfig), "%s/CONFIG", sProject.projectDir);
- snprintf(vm->appData, sizeof(vm->appData), "%s/DATA", sProject.projectDir);
+ snprintf(vm->appConfig, sizeof(vm->appConfig), "%s" DVX_PATH_SEP "CONFIG", sProject.projectDir);
+ snprintf(vm->appData, sizeof(vm->appData), "%s" DVX_PATH_SEP "DATA", sProject.projectDir);
platformMkdirRecursive(vm->appConfig);
platformMkdirRecursive(vm->appData);
@@ -7774,36 +7234,32 @@ static void runModule(BasModuleT *mod) {
extCb.ctx = NULL;
basVmSetExternCallbacks(vm, &extCb);
- // Create form runtime (bridges UI opcodes to DVX widgets)
- BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod);
-
// The IDE surfaces runtime errors via the Output pane and jumps
// the editor to the faulting line, so suppress the form runtime's
// modal error dialog. Keeping the dialog on top of an already-
// modal-heavy IDE session was producing phantom "press OK twice"
// UX as the dialog's nested event pump raced with other queued
// events coming out of the user's program.
- if (formRt) {
- formRt->suppressErrorDialog = true;
- }
+ formRt->suppressErrorDialog = true;
- sVm = vm;
- sDbgFormRt = formRt;
- sDbgModule = mod;
- sDbgState = DBG_RUNNING;
+ sVm = vm;
+ sDbgFormRt = formRt;
+ sDbgModule = mod;
+ sDbgState = DBG_RUNNING;
+ sStopRequested = false;
// Set project help file on form runtime for F1 context help
- if (formRt && sProject.helpFile[0]) {
+ if (sProject.helpFile[0]) {
snprintf(formRt->helpFile, sizeof(formRt->helpFile), "%s" DVX_PATH_SEP "%s",
sProject.projectDir, sProject.helpFile);
}
+
updateProjectMenuState();
// Set breakpoints BEFORE loading forms so breakpoints in form
// init code (module-level statements inside BEGINFORM) fire.
if (sDbgEnabled) {
- buildVmBreakpoints();
- basVmSetBreakpoints(vm, sVmBreakpoints, (int32_t)arrlen(sVmBreakpoints));
+ syncVmBreakpoints();
if (sDbgBreakOnStart) {
basVmStepInto(vm);
@@ -7818,17 +7274,16 @@ static void runModule(BasModuleT *mod) {
// Run in slices of 10000 steps, yielding to DVX between slices
basVmSetStepLimit(vm, BAS_VM_DEFAULT_STEP_SLICE);
- BasVmResultE result;
- sStopRequested = false;
+ BasVmResultE result = BAS_VM_HALTED;
for (;;) {
- if (sDbgState == DBG_PAUSED) {
- // Paused at breakpoint/step — spin on GUI events until user acts
- dvxUpdate(sAc);
+ if (!sWin || !sAc->running || sStopRequested) {
+ break;
+ }
- if (!sWin || !sAc->running || sStopRequested) {
- break;
- }
+ if (sDbgState == DBG_PAUSED) {
+ // Paused at breakpoint/step -- spin on GUI events until user acts
+ dvxUpdate(sAc);
// User may have pressed F5 (continue), F8 (step), or Esc (stop)
if (sDbgState == DBG_RUNNING) {
@@ -7851,11 +7306,6 @@ static void runModule(BasModuleT *mod) {
if (result == BAS_VM_STEP_LIMIT) {
dvxUpdate(sAc);
-
- if (!sWin || !sAc->running || sStopRequested) {
- break;
- }
-
continue;
}
@@ -7883,29 +7333,13 @@ static void runModule(BasModuleT *mod) {
// VB-style event loop: after module-level code finishes,
// keep processing events as long as any form is loaded.
// The program ends when all forms are unloaded (closed).
- if (result == BAS_VM_HALTED && formRt && (int32_t)arrlen(formRt->forms) > 0) {
+ // Breakpoints inside event handlers pause inside the VM's own
+ // spin (basVmCallSub -> doEventsCallback), so nothing here needs
+ // to watch for DBG_PAUSED.
+ if (result == BAS_VM_HALTED && (int32_t)arrlen(formRt->forms) > 0) {
setStatus("Running (event loop)...");
- sStopRequested = false;
-
- while (sWin && sAc->running && formRt && (int32_t)arrlen(formRt->forms) > 0 && !sStopRequested && !vm->ended) {
-
- if (sDbgState == DBG_PAUSED) {
- // Paused inside an event handler
- debugNavigateToLine(sDbgCurrentLine);
- debugUpdateWindows();
- setStatus("Paused.");
-
- // Wait for user to resume
- while (sDbgState == DBG_PAUSED && sWin && sAc->running && !sStopRequested) {
- dvxUpdate(sAc);
- }
-
- if (sDbgState == DBG_RUNNING) {
- vm->running = true;
- setStatus("Running (event loop)...");
- }
- }
+ while (sWin && sAc->running && (int32_t)arrlen(formRt->forms) > 0 && !sStopRequested && !vm->ended) {
dvxUpdate(sAc);
}
}
@@ -7947,8 +7381,8 @@ static void runModule(BasModuleT *mod) {
// it lands on whichever proc just finished instead of
// correctly reporting that the error is outside any sub.
// Use the proc's endLineNum to gate membership.
- char procName[128] = {0};
- int32_t procLine = errLocalLine;
+ char procName[IDE_FULL_NAME_BUF] = {0};
+ int32_t procLine = errLocalLine;
if (errFileIdx >= 0) {
activateFile(errFileIdx, ViewCodeE);
@@ -7996,9 +7430,15 @@ static void runModule(BasModuleT *mod) {
basFormRtDestroy(formRt);
basVmDestroy(vm);
- // If the IDE was closed while the program was running, skip
- // all UI updates — the windows are already destroyed.
+ // If the IDE was closed while the program was running, skip all UI
+ // updates -- the windows are already destroyed -- and release the
+ // module onClose left for us.
if (!sWin) {
+ if (sCachedModule) {
+ basModuleFree(sCachedModule);
+ sCachedModule = NULL;
+ }
+
return;
}
@@ -8021,11 +7461,25 @@ static void runModule(BasModuleT *mod) {
dvxRaiseWindow(sAc, sCodeWin);
}
} else {
- if (hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); }
- if (hadToolbox && sToolboxWin) { dvxShowWindow(sAc, sToolboxWin); }
- if (hadProps && sPropsWin) { dvxShowWindow(sAc, sPropsWin); }
- if (hadCodeWin && sCodeWin) { dvxShowWindow(sAc, sCodeWin); }
- if (hadPrjWin && sProjectWin) { dvxShowWindow(sAc, sProjectWin); }
+ if (hadFormWin && sFormWin) {
+ dvxShowWindow(sAc, sFormWin);
+ }
+
+ if (hadToolbox && sToolboxWin) {
+ dvxShowWindow(sAc, sToolboxWin);
+ }
+
+ if (hadProps && sPropsWin) {
+ dvxShowWindow(sAc, sPropsWin);
+ }
+
+ if (hadCodeWin && sCodeWin) {
+ dvxShowWindow(sAc, sCodeWin);
+ }
+
+ if (hadPrjWin && sProjectWin) {
+ dvxShowWindow(sAc, sProjectWin);
+ }
}
// Repaint to clear destroyed runtime forms and restore designer
@@ -8033,43 +7487,56 @@ static void runModule(BasModuleT *mod) {
}
-static void saveActiveFile(void) {
- if (sProject.projectPath[0] == '\0') {
- return;
+// saveActiveFile -- write the active file (editor/designer state first
+// stashed into its buffer). Returns false when nothing was written.
+static bool saveActiveFile(void) {
+ if (!hasProject()) {
+ return false;
}
int32_t idx = sProject.activeFileIdx;
if (idx < 0 || idx >= sProject.fileCount) {
- return;
+ return false;
}
// Ensure buffer is up-to-date with editor/designer state
stashCurrentFile();
- PrjFileT *file = &sProject.files[idx];
- char fullPath[DVX_MAX_PATH];
- prjFullPath(&sProject, idx, fullPath, sizeof(fullPath));
-
- if (file->buffer) {
- FILE *f = fopen(fullPath, "w");
-
- if (f) {
- fputs(file->buffer, f);
- fclose(f);
- file->modified = false;
-
- if (file->isForm && sDesigner.form) {
- sDesigner.form->dirty = false;
- }
- } else {
- dvxErrorBox(sAc, NULL, "Could not write file.");
- return;
- }
+ if (!writeProjectFile(idx)) {
+ return false;
}
setStatus("Saved.");
updateDirtyIndicators();
+ return true;
+}
+
+
+// saveAllModified -- write every modified file plus the .dbp when dirty.
+// Returns false if any write failed (the user has already been told).
+static bool saveAllModified(void) {
+ if (!hasProject()) {
+ return true;
+ }
+
+ stashCurrentFile();
+ syncFormDirty();
+
+ bool ok = true;
+
+ for (int32_t i = 0; i < sProject.fileCount; i++) {
+ if (sProject.files[i].modified && !writeProjectFile(i)) {
+ ok = false;
+ }
+ }
+
+ if (sProject.dirty && !saveProjectFile()) {
+ ok = false;
+ }
+
+ updateDirtyIndicators();
+ return ok;
}
@@ -8091,9 +7558,14 @@ static bool saveCurProc(void) {
if (sCurProcIdx == -1) {
// General section -- check for embedded proc declarations
char *cleaned = extractNewProcs(edText);
+ char *updated = cleaned ? cleaned : strdup(edText);
+
+ if (!updated) {
+ return false;
+ }
free(sGeneralBuf);
- sGeneralBuf = cleaned ? cleaned : strdup(edText);
+ sGeneralBuf = updated;
if (cleaned) {
wgtSetText(sEditor, sGeneralBuf);
@@ -8101,230 +7573,207 @@ static bool saveCurProc(void) {
}
return false;
- } else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) {
- // Get the name of the current proc so we can identify its block
- // regardless of position in the editor.
- char ownName[128] = "";
+ }
- if (sCurProcIdx < (int32_t)arrlen(sProcTable)) {
- snprintf(ownName, sizeof(ownName), "%s_%s",
- sProcTable[sCurProcIdx].objName,
- sProcTable[sCurProcIdx].evtName);
- }
+ if (sCurProcIdx < 0 || sCurProcIdx >= (int32_t)arrlen(sProcBufs)) {
+ return false;
+ }
- // If ownName is empty (General proc without underscore), extract
- // it from the first Sub/Function line of the existing buffer.
- if (ownName[0] == '\0' || strcmp(ownName, "(General)_") == 0) {
- const char *ep = dvxSkipWs(sProcBufs[sCurProcIdx]);
- if (strncasecmp(ep, "SUB ", 4) == 0) { ep += 4; }
- else if (strncasecmp(ep, "FUNCTION ", 9) == 0) { ep += 9; }
- ep = dvxSkipWs(ep);
- int32_t n = 0;
- while (*ep && *ep != '(' && *ep != ' ' && *ep != '\t' && *ep != '\n' && n < 127) {
- ownName[n++] = *ep++;
- }
- ownName[n] = '\0';
- }
+ // Get the name of the current proc so we can identify its block
+ // regardless of position in the editor.
+ char ownName[IDE_FULL_NAME_BUF] = "";
- // Count Sub/Function blocks in the editor text
- int32_t blockCount = 0;
- const char *scan = edText;
+ if (sCurProcIdx < (int32_t)arrlen(sProcTable) && strcmp(sProcTable[sCurProcIdx].objName, IDE_GENERAL_SECTION) != 0) {
+ snprintf(ownName, sizeof(ownName), "%s_%s", sProcTable[sCurProcIdx].objName, sProcTable[sCurProcIdx].evtName);
+ }
- while (*scan) {
- const char *tl = dvxSkipWs(scan);
- if (strncasecmp(tl, "SUB ", 4) == 0 || strncasecmp(tl, "FUNCTION ", 9) == 0) {
- blockCount++;
- }
- while (*scan && *scan != '\n') { scan++; }
- if (*scan == '\n') { scan++; }
- }
+ // General procs (no object prefix) take their name from the
+ // declaration line of the existing buffer.
+ if (ownName[0] == '\0') {
+ bool isSub = false;
+ const char *afterKw = procDeclAt(dvxSkipWs(sProcBufs[sCurProcIdx]), &isSub);
- if (blockCount <= 1) {
- // Single proc (or none) -- check for empty skeleton
- const char *p = dvxSkipWs(edText);
- bool isSub = (strncasecmp(p, "SUB ", 4) == 0);
- const char *endTag = isSub ? "END SUB" : "END FUNCTION";
- int32_t endTagLen = isSub ? 7 : 12;
+ procNameFromDecl(afterKw ? afterKw : sProcBufs[sCurProcIdx], ownName, sizeof(ownName));
+ }
- // Skip declaration line
- while (*p && *p != '\n') { p++; }
- if (*p == '\n') { p++; }
+ // Count Sub/Function blocks in the editor text
+ int32_t blockCount = 0;
- bool bodyEmpty = true;
+ for (const char *scan = edText; *scan; scan = skipLine(scan)) {
+ bool isSub;
- while (*p) {
- const char *line = dvxSkipWs(p);
- if (strncasecmp(line, endTag, endTagLen) == 0) { break; }
- while (*p && *p != '\n') {
- if (*p != ' ' && *p != '\t' && *p != '\r') { bodyEmpty = false; }
- p++;
- }
- if (*p == '\n') { p++; }
- }
-
- if (bodyEmpty) {
- free(sProcBufs[sCurProcIdx]);
- arrdel(sProcBufs, sCurProcIdx);
- sCurProcIdx = -2;
- return true;
- } else {
- free(sProcBufs[sCurProcIdx]);
- sProcBufs[sCurProcIdx] = strdup(edText);
- return false;
- }
- } else {
- // Multiple proc blocks in the editor. Find the one matching
- // ownName, keep it in this buffer, extract the rest.
- const char *pos = edText;
- const char *ownStart = NULL;
- const char *ownEnd = NULL;
- char *extras = (char *)malloc(strlen(edText) + 1);
- int32_t extPos = 0;
-
- if (!extras) {
- free(sProcBufs[sCurProcIdx]);
- sProcBufs[sCurProcIdx] = strdup(edText);
- } else {
- while (*pos) {
- const char *lineStart = pos;
- const char *tl = dvxSkipWs(pos);
-
- bool isSub = (strncasecmp(tl, "SUB ", 4) == 0);
- bool isFunc = (strncasecmp(tl, "FUNCTION ", 9) == 0);
-
- if (isSub || isFunc) {
- // Extract the proc name
- const char *np = dvxSkipWs(tl + (isSub ? 4 : 9));
- char name[128];
- int32_t nn = 0;
- while (*np && *np != '(' && *np != ' ' && *np != '\t' && *np != '\n' && nn < 127) {
- name[nn++] = *np++;
- }
- name[nn] = '\0';
-
- // Find the matching End tag
- const char *endTag = isSub ? "END SUB" : "END FUNCTION";
- int32_t endTagLen = isSub ? 7 : 12;
- const char *s = pos;
- while (*s && *s != '\n') { s++; }
- if (*s == '\n') { s++; }
-
- while (*s) {
- const char *sl = dvxSkipWs(s);
- if (strncasecmp(sl, endTag, endTagLen) == 0) {
- while (*s && *s != '\n') { s++; }
- if (*s == '\n') { s++; }
- break;
- }
- while (*s && *s != '\n') { s++; }
- if (*s == '\n') { s++; }
- }
-
- if (strcasecmp(name, ownName) == 0) {
- ownStart = lineStart;
- ownEnd = s;
- } else {
- // Copy this block to extras
- int32_t blen = (int32_t)(s - lineStart);
- memcpy(extras + extPos, lineStart, blen);
- extPos += blen;
- }
-
- pos = s;
- continue;
- }
-
- // Non-proc line (blank lines between blocks etc.) -- skip
- while (*pos && *pos != '\n') { pos++; }
- if (*pos == '\n') { pos++; }
- }
-
- extras[extPos] = '\0';
-
- // Update this buffer with just the owned proc
- if (ownStart && ownEnd) {
- int32_t keepLen = (int32_t)(ownEnd - ownStart);
- char *kept = (char *)malloc(keepLen + 1);
- if (kept) {
- memcpy(kept, ownStart, keepLen);
- kept[keepLen] = '\0';
- free(sProcBufs[sCurProcIdx]);
- sProcBufs[sCurProcIdx] = kept;
- }
- }
-
- // Extract extra procs into their own buffers. extractNewProcs
- // returns leftover (duplicate-named) blocks in a malloc'd
- // buffer; append them to the General section so the compiler
- // reports the duplicate rather than silently losing the code.
- if (extPos > 0) {
- char *remaining = extractNewProcs(extras);
-
- if (remaining && remaining[0]) {
- int32_t genLen = sGeneralBuf ? (int32_t)strlen(sGeneralBuf) : 0;
- int32_t remLen = (int32_t)strlen(remaining);
- char *merged = (char *)malloc((size_t)(genLen + remLen + 2));
-
- if (merged) {
- int32_t mpos = 0;
-
- if (genLen > 0) {
- memcpy(merged, sGeneralBuf, (size_t)genLen);
- mpos = genLen;
- merged[mpos++] = '\n';
- }
-
- memcpy(merged + mpos, remaining, (size_t)remLen);
- mpos += remLen;
- merged[mpos] = '\0';
- free(sGeneralBuf);
- sGeneralBuf = merged;
- }
- }
-
- free(remaining);
- }
-
- free(extras);
-
- // Update editor to show only this proc. Extraction keeps the
- // owned proc at sProcBufs[sCurProcIdx] and only appends new
- // procs to the end, so no earlier index shifts -- report false.
- wgtSetText(sEditor, sProcBufs[sCurProcIdx]);
- return false;
- }
+ if (procDeclAt(dvxSkipWs(scan), &isSub)) {
+ blockCount++;
}
}
+ if (blockCount <= 1) {
+ // Single proc (or none) -- check for empty skeleton
+ bool isSub = false;
+ const char *p = dvxSkipWs(edText);
+ const char *afterKw = procDeclAt(p, &isSub);
+ const char *endTag = (afterKw && !isSub) ? KW_END_FUNCTION : KW_END_SUB;
+
+ // Skip declaration line
+ p = skipLine(p);
+
+ bool bodyEmpty = true;
+
+ while (*p) {
+ if (kwMatch(dvxSkipWs(p), endTag)) {
+ break;
+ }
+
+ while (*p && *p != '\n') {
+ if (*p != ' ' && *p != '\t' && *p != '\r') {
+ bodyEmpty = false;
+ }
+
+ p++;
+ }
+
+ if (*p == '\n') {
+ p++;
+ }
+ }
+
+ if (bodyEmpty) {
+ free(sProcBufs[sCurProcIdx]);
+ arrdel(sProcBufs, sCurProcIdx);
+ sCurProcIdx = -2;
+ return true;
+ }
+
+ char *copy = strdup(edText);
+
+ if (copy) {
+ free(sProcBufs[sCurProcIdx]);
+ sProcBufs[sCurProcIdx] = copy;
+ }
+
+ return false;
+ }
+
+ // Multiple proc blocks in the editor. Find the one matching
+ // ownName, keep it in this buffer, extract the rest.
+ const char *pos = edText;
+ const char *ownStart = NULL;
+ const char *ownEnd = NULL;
+ char *extras = (char *)malloc(strlen(edText) + 1);
+ int32_t extPos = 0;
+
+ if (!extras) {
+ char *copy = strdup(edText);
+
+ if (copy) {
+ free(sProcBufs[sCurProcIdx]);
+ sProcBufs[sCurProcIdx] = copy;
+ }
+
+ return false;
+ }
+
+ while (*pos) {
+ bool isSub = false;
+ const char *afterKw = procDeclAt(dvxSkipWs(pos), &isSub);
+
+ if (!afterKw) {
+ // Non-proc line (blank lines between blocks etc.) -- skip
+ pos = skipLine(pos);
+ continue;
+ }
+
+ char name[IDE_FULL_NAME_BUF];
+ procNameFromDecl(afterKw, name, sizeof(name));
+
+ int32_t endOffset = 0;
+ const char *blockEnd = skipToEndProc(pos, isSub, &endOffset);
+
+ if (strcasecmp(name, ownName) == 0) {
+ ownStart = pos;
+ ownEnd = blockEnd;
+ } else {
+ // Copy this block to extras
+ int32_t blen = (int32_t)(blockEnd - pos);
+ memcpy(extras + extPos, pos, blen);
+ extPos += blen;
+ }
+
+ pos = blockEnd;
+ }
+
+ extras[extPos] = '\0';
+
+ // Update this buffer with just the owned proc
+ if (ownStart && ownEnd) {
+ int32_t keepLen = (int32_t)(ownEnd - ownStart);
+ char *kept = (char *)malloc(keepLen + 1);
+
+ if (kept) {
+ memcpy(kept, ownStart, keepLen);
+ kept[keepLen] = '\0';
+ free(sProcBufs[sCurProcIdx]);
+ sProcBufs[sCurProcIdx] = kept;
+ }
+ }
+
+ // Extract extra procs into their own buffers. extractNewProcs
+ // returns leftover (duplicate-named) blocks in a malloc'd
+ // buffer; append them to the General section so the compiler
+ // reports the duplicate rather than silently losing the code.
+ if (extPos > 0) {
+ char *remaining = extractNewProcs(extras);
+
+ if (remaining && remaining[0]) {
+ int32_t genLen = sGeneralBuf ? (int32_t)strlen(sGeneralBuf) : 0;
+ int32_t remLen = (int32_t)strlen(remaining);
+ char *merged = (char *)malloc((size_t)(genLen + remLen + 2));
+
+ if (merged) {
+ int32_t mpos = 0;
+
+ if (genLen > 0) {
+ memcpy(merged, sGeneralBuf, (size_t)genLen);
+ mpos = genLen;
+ merged[mpos++] = '\n';
+ }
+
+ memcpy(merged + mpos, remaining, (size_t)remLen);
+ mpos += remLen;
+ merged[mpos] = '\0';
+ free(sGeneralBuf);
+ sGeneralBuf = merged;
+ }
+ }
+
+ free(remaining);
+ }
+
+ free(extras);
+
+ // Update editor to show only this proc. Extraction keeps the
+ // owned proc at sProcBufs[sCurProcIdx] and only appends new
+ // procs to the end, so no earlier index shifts -- report false.
+ wgtSetText(sEditor, sProcBufs[sCurProcIdx]);
return false;
}
+// saveFile -- Ctrl+S: the active file plus any other modified forms
static void saveFile(void) {
- if (sProject.projectPath[0] == '\0' || sProject.activeFileIdx < 0) {
+ if (!hasProject() || sProject.activeFileIdx < 0) {
return;
}
- // Save the active project file
- saveActiveFile();
+ if (!saveActiveFile()) {
+ return;
+ }
+
+ syncFormDirty();
- // Also save any other dirty forms
for (int32_t i = 0; i < sProject.fileCount; i++) {
- if (i == sProject.activeFileIdx) {
- continue;
- }
-
- if (sProject.files[i].isForm && sProject.files[i].modified && sProject.files[i].buffer) {
- char fullPath[DVX_MAX_PATH];
- prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
-
- FILE *f = fopen(fullPath, "w");
-
- if (f) {
- fputs(sProject.files[i].buffer, f);
- fclose(f);
- sProject.files[i].modified = false;
- }
+ if (i != sProject.activeFileIdx && sProject.files[i].isForm && sProject.files[i].modified) {
+ writeProjectFile(i);
}
}
@@ -8332,8 +7781,17 @@ static void saveFile(void) {
}
-// selectDropdowns -- set the Object and Event dropdowns to match a
-// given control name and event name.
+// saveProjectFile -- write the .dbp; dirty is only cleared on success
+static bool saveProjectFile(void) {
+ if (!prjSave(&sProject)) {
+ dvxErrorBox(sAc, NULL, "Could not write the project file.");
+ return false;
+ }
+
+ sProject.dirty = false;
+ return true;
+}
+
// selectDropdowns -- set the Object and Event dropdown selections to
// match a given control/event without triggering navigation callbacks.
@@ -8402,10 +7860,10 @@ static void showBreakpointWindow(void) {
return;
}
- int32_t winW = 320;
- int32_t winH = 180;
- int32_t winX = sAc->display.width - winW - 10;
- int32_t winY = sAc->display.height - winH - 10;
+ int32_t winW = IDE_BP_WIN_W;
+ int32_t winH = IDE_DEBUG_WIN_H;
+ int32_t winX = sAc->display.width - winW - IDE_WIN_MARGIN;
+ int32_t winY = sAc->display.height - winH - IDE_WIN_MARGIN;
sBreakpointWin = dvxCreateWindow(sAc, "Breakpoints", winX, winY, winW, winH, true);
@@ -8432,7 +7890,7 @@ static void showBreakpointWindow(void) {
{ "Line", wgtChars(6), ListViewAlignRightE },
};
- wgtListViewSetColumns(sBreakpointList, cols, 3);
+ wgtListViewSetColumns(sBreakpointList, cols, (int32_t)(sizeof(cols) / sizeof(cols[0])));
}
}
}
@@ -8449,10 +7907,10 @@ static void showCallStackWindow(void) {
return;
}
- int32_t winW = 220;
- int32_t winH = 180;
+ int32_t winW = IDE_CALLSTACK_WIN_W;
+ int32_t winH = IDE_DEBUG_WIN_H;
int32_t winX = sAc->display.width - winW;
- int32_t winY = toolbarBottom() + 210;
+ int32_t winY = toolbarBottom() + IDE_CALLSTACK_WIN_Y_OFF;
sCallStackWin = dvxCreateWindow(sAc, "Call Stack", winX, winY, winW, winH, true);
@@ -8475,7 +7933,7 @@ static void showCallStackWindow(void) {
{ "Line", wgtChars(6), ListViewAlignRightE },
};
- wgtListViewSetColumns(sCallStackList, cols, 2);
+ wgtListViewSetColumns(sCallStackList, cols, (int32_t)(sizeof(cols) / sizeof(cols[0])));
}
}
}
@@ -8494,68 +7952,81 @@ static void showCodeWindow(void) {
sCodeWin = dvxCreateWindow(sAc, "Code", 0, codeY, sAc->display.width, codeH, true);
+ if (!sCodeWin) {
+ return;
+ }
+
// Ensure position is below the toolbar (dvxCreateWindow may adjust)
- if (sCodeWin) {
- sCodeWin->y = codeY;
+ sCodeWin->y = codeY;
+ sCodeWin->onMenu = onMenu;
+ sCodeWin->onFocus = onContentFocus;
+ sCodeWin->onClose = onCodeWinClose;
+ sCodeWin->accelTable = sWin ? sWin->accelTable : NULL;
+ sLastFocusWin = sCodeWin;
+
+ WidgetT *codeRoot = wgtInitWindow(sAc, sCodeWin);
+
+ WidgetT *dropdownRow = wgtHBox(codeRoot);
+ dropdownRow->spacing = wgtPixels(IDE_WIDGET_SPACING);
+
+ wgtLabel(dropdownRow, "Object:");
+
+ sObjDropdown = wgtDropdown(dropdownRow);
+ sObjDropdown->weight = WGT_WEIGHT_FILL;
+ sObjDropdown->onChange = onObjDropdownChange;
+ wgtDropdownSetItems(sObjDropdown, NULL, 0);
+
+ wgtLabel(dropdownRow, "Function:");
+
+ sEvtDropdown = wgtDropdown(dropdownRow);
+ sEvtDropdown->weight = WGT_WEIGHT_FILL;
+ sEvtDropdown->onChange = onEvtDropdownChange;
+ wgtDropdownSetItems(sEvtDropdown, NULL, 0);
+
+ sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE);
+ sEditor->weight = WGT_WEIGHT_FILL;
+ wgtTextAreaSetColorize(sEditor, basicColorize, NULL);
+
+ // Apply saved syntax colors
+ uint32_t initColors[SYNTAX_COLOR_COUNT];
+
+ for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) {
+ char key[IDE_PREF_KEY_BUF];
+ snprintf(key, sizeof(key), PREF_KEY_COLOR_FMT, (int)i);
+ initColors[i] = (uint32_t)prefsGetInt(sPrefs, PREF_SEC_SYNTAX, key, (int32_t)sDefaultSyntaxColors[i]);
}
- if (sCodeWin) {
- sCodeWin->onMenu = onMenu;
- sCodeWin->onFocus = onContentFocus;
- sCodeWin->onClose = onCodeWinClose;
- sCodeWin->accelTable = sWin ? sWin->accelTable : NULL;
- sLastFocusWin = sCodeWin;
+ wgtTextAreaSetSyntaxColors(sEditor, initColors, SYNTAX_COLOR_COUNT);
- WidgetT *codeRoot = wgtInitWindow(sAc, sCodeWin);
+ wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, sAc);
+ wgtTextAreaSetGutterClick(sEditor, onGutterClick);
+ wgtTextAreaSetShowLineNumbers(sEditor, true);
+ wgtTextAreaSetAutoIndent(sEditor, true);
+ wgtTextAreaSetCaptureTabs(sEditor, true);
+ wgtTextAreaSetTabWidth(sEditor, prefsGetInt(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_TAB_WIDTH, IDE_DEFAULT_TAB_WIDTH));
+ wgtTextAreaSetUseTabChar(sEditor, !prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_USE_SPACES, true));
- WidgetT *dropdownRow = wgtHBox(codeRoot);
- dropdownRow->spacing = wgtPixels(4);
+ // showProc suppresses onChange while it loads content, so the
+ // handler can be attached right away without false dirty marking.
+ sEditor->onChange = onEditorChange;
- wgtLabel(dropdownRow, "Object:");
+ updateProjectMenuState();
+ updateDirtyIndicators();
+}
- sObjDropdown = wgtDropdown(dropdownRow);
- sObjDropdown->weight = WGT_WEIGHT_FILL;
- sObjDropdown->onChange = onObjDropdownChange;
- wgtDropdownSetItems(sObjDropdown, NULL, 0);
- wgtLabel(dropdownRow, "Function:");
+// showCompileError -- present sOutputBuf (already filled) as the
+// compile result and leave the busy state.
+static void showCompileError(const char *status) {
+ setOutputText(sOutputBuf);
+ showOutputWindow();
- sEvtDropdown = wgtDropdown(dropdownRow);
- sEvtDropdown->weight = WGT_WEIGHT_FILL;
- sEvtDropdown->onChange = onEvtDropdownChange;
- wgtDropdownSetItems(sEvtDropdown, NULL, 0);
-
- sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE);
- sEditor->weight = WGT_WEIGHT_FILL;
- wgtTextAreaSetColorize(sEditor, basicColorize, NULL);
-
- // Apply saved syntax colors
- {
- uint32_t initColors[SYNTAX_COLOR_COUNT];
-
- for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) {
- char key[32];
- snprintf(key, sizeof(key), "color%d", (int)i);
- initColors[i] = (uint32_t)prefsGetInt(sPrefs, "syntax", key, (int32_t)sDefaultSyntaxColors[i]);
- }
-
- wgtTextAreaSetSyntaxColors(sEditor, initColors, SYNTAX_COLOR_COUNT);
- }
-
- wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, sAc);
- wgtTextAreaSetGutterClick(sEditor, onGutterClick);
- wgtTextAreaSetShowLineNumbers(sEditor, true);
- wgtTextAreaSetAutoIndent(sEditor, true);
- wgtTextAreaSetCaptureTabs(sEditor, true);
- wgtTextAreaSetTabWidth(sEditor, prefsGetInt(sPrefs, "editor", "tabWidth", 3));
- wgtTextAreaSetUseTabChar(sEditor, !prefsGetBool(sPrefs, "editor", "useSpaces", true));
-
- // onChange is set after initial content is loaded by the caller
- // (navigateToEventSub, onPrjFileDblClick, etc.) to prevent false dirty marking.
-
- updateProjectMenuState();
- updateDirtyIndicators();
+ if (sOutWin) {
+ dvxRaiseWindow(sAc, sOutWin);
}
+
+ setStatus(status);
+ dvxSetBusy(sAc, false);
}
@@ -8602,8 +8073,8 @@ static void showLocalsWindow(void) {
return;
}
- int32_t winW = 250;
- int32_t winH = 200;
+ int32_t winW = IDE_LOCALS_WIN_W;
+ int32_t winH = IDE_LOCALS_WIN_H;
int32_t winX = sAc->display.width - winW;
int32_t winY = toolbarBottom();
@@ -8629,7 +8100,7 @@ static void showLocalsWindow(void) {
{ "Value", wgtChars(16), ListViewAlignLeftE },
};
- wgtListViewSetColumns(sLocalsList, cols, 3);
+ wgtListViewSetColumns(sLocalsList, cols, (int32_t)(sizeof(cols) / sizeof(cols[0])));
}
}
}
@@ -8669,7 +8140,7 @@ static void showOutputWindow(void) {
static void showPreferencesDialog(void) {
memset(&sPrefsDlg, 0, sizeof(sPrefsDlg));
- WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", 420, 440, false);
+ WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", IDE_PREFS_WIN_W, IDE_PREFS_WIN_H, false);
if (!win) {
return;
@@ -8679,7 +8150,7 @@ static void showPreferencesDialog(void) {
win->maxH = win->h;
WidgetT *root = wgtInitWindow(sAc, win);
- root->spacing = wgtPixels(4);
+ root->spacing = wgtPixels(IDE_WIDGET_SPACING);
// ---- Tab control ----
WidgetT *tabs = wgtTabControl(root);
@@ -8687,87 +8158,87 @@ static void showPreferencesDialog(void) {
// ======== General tab ========
WidgetT *generalPage = wgtTabPage(tabs, "General");
- generalPage->spacing = wgtPixels(4);
+ generalPage->spacing = wgtPixels(IDE_WIDGET_SPACING);
// Editor section
WidgetT *edFrame = wgtFrame(generalPage, "Editor");
- edFrame->spacing = wgtPixels(2);
+ edFrame->spacing = wgtPixels(IDE_WIDGET_SPACING_SM);
sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming");
- wgtCheckboxSetChecked(sPrefsDlg.renameSkipComments, prefsGetBool(sPrefs, "editor", "renameSkipComments", true));
+ wgtCheckboxSetChecked(sPrefsDlg.renameSkipComments, prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, true));
sPrefsDlg.optionExplicit = wgtCheckbox(edFrame, "OPTION EXPLICIT default for new projects");
- wgtCheckboxSetChecked(sPrefsDlg.optionExplicit, prefsGetBool(sPrefs, "editor", "optionExplicit", false));
+ wgtCheckboxSetChecked(sPrefsDlg.optionExplicit, prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_OPTION_EXPLICIT, false));
WidgetT *tabRow = wgtHBox(edFrame);
- tabRow->spacing = wgtPixels(4);
+ tabRow->spacing = wgtPixels(IDE_WIDGET_SPACING);
wgtLabel(tabRow, "Tab width:");
- sPrefsDlg.tabWidthInput = wgtTextInput(tabRow, 4);
- sPrefsDlg.tabWidthInput->maxW = wgtPixels(40);
+ sPrefsDlg.tabWidthInput = wgtTextInput(tabRow, IDE_PREFS_TAB_INPUT_MAX);
+ sPrefsDlg.tabWidthInput->maxW = wgtPixels(IDE_PREFS_TAB_INPUT_W);
- char tabBuf[8];
- snprintf(tabBuf, sizeof(tabBuf), "%d", (int)prefsGetInt(sPrefs, "editor", "tabWidth", 3));
+ char tabBuf[IDE_TAB_WIDTH_BUF];
+ snprintf(tabBuf, sizeof(tabBuf), "%d", (int)prefsGetInt(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_TAB_WIDTH, IDE_DEFAULT_TAB_WIDTH));
wgtSetText(sPrefsDlg.tabWidthInput, tabBuf);
sPrefsDlg.useSpaces = wgtCheckbox(edFrame, "Insert spaces instead of tabs");
- wgtCheckboxSetChecked(sPrefsDlg.useSpaces, prefsGetBool(sPrefs, "editor", "useSpaces", true));
+ wgtCheckboxSetChecked(sPrefsDlg.useSpaces, prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_USE_SPACES, true));
// Project Defaults section
WidgetT *prjFrame = wgtFrame(generalPage, "New Project Defaults");
- prjFrame->spacing = wgtPixels(2);
+ prjFrame->spacing = wgtPixels(IDE_WIDGET_SPACING_SM);
prjFrame->weight = WGT_WEIGHT_FILL;
WidgetT *r1 = wgtHBox(prjFrame);
- r1->spacing = wgtPixels(4);
+ r1->spacing = wgtPixels(IDE_WIDGET_SPACING);
WidgetT *l1 = wgtLabel(r1, "Author:");
- l1->minW = wgtPixels(80);
- sPrefsDlg.defAuthor = wgtTextInput(r1, 64);
+ l1->minW = wgtPixels(IDE_PREFS_LABEL_W);
+ sPrefsDlg.defAuthor = wgtTextInput(r1, IDE_PREFS_TEXT_MAX);
sPrefsDlg.defAuthor->weight = WGT_WEIGHT_FILL;
- wgtSetText(sPrefsDlg.defAuthor, prefsGetString(sPrefs, "defaults", "author", ""));
+ wgtSetText(sPrefsDlg.defAuthor, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_AUTHOR, ""));
WidgetT *r2 = wgtHBox(prjFrame);
- r2->spacing = wgtPixels(4);
+ r2->spacing = wgtPixels(IDE_WIDGET_SPACING);
WidgetT *l2 = wgtLabel(r2, "Publisher:");
- l2->minW = wgtPixels(80);
- sPrefsDlg.defPublisher = wgtTextInput(r2, 64);
+ l2->minW = wgtPixels(IDE_PREFS_LABEL_W);
+ sPrefsDlg.defPublisher = wgtTextInput(r2, IDE_PREFS_TEXT_MAX);
sPrefsDlg.defPublisher->weight = WGT_WEIGHT_FILL;
- wgtSetText(sPrefsDlg.defPublisher, prefsGetString(sPrefs, "defaults", "publisher", ""));
+ wgtSetText(sPrefsDlg.defPublisher, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_PUBLISHER, ""));
WidgetT *r3 = wgtHBox(prjFrame);
- r3->spacing = wgtPixels(4);
+ r3->spacing = wgtPixels(IDE_WIDGET_SPACING);
WidgetT *l3 = wgtLabel(r3, "Version:");
- l3->minW = wgtPixels(80);
- sPrefsDlg.defVersion = wgtTextInput(r3, 16);
+ l3->minW = wgtPixels(IDE_PREFS_LABEL_W);
+ sPrefsDlg.defVersion = wgtTextInput(r3, IDE_PREFS_VERSION_MAX);
sPrefsDlg.defVersion->weight = WGT_WEIGHT_FILL;
- wgtSetText(sPrefsDlg.defVersion, prefsGetString(sPrefs, "defaults", "version", "1.0"));
+ wgtSetText(sPrefsDlg.defVersion, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_VERSION, IDE_PREF_DEFAULT_VERSION));
WidgetT *r4 = wgtHBox(prjFrame);
- r4->spacing = wgtPixels(4);
+ r4->spacing = wgtPixels(IDE_WIDGET_SPACING);
WidgetT *l4 = wgtLabel(r4, "Copyright:");
- l4->minW = wgtPixels(80);
- sPrefsDlg.defCopyright = wgtTextInput(r4, 64);
+ l4->minW = wgtPixels(IDE_PREFS_LABEL_W);
+ sPrefsDlg.defCopyright = wgtTextInput(r4, IDE_PREFS_TEXT_MAX);
sPrefsDlg.defCopyright->weight = WGT_WEIGHT_FILL;
- wgtSetText(sPrefsDlg.defCopyright, prefsGetString(sPrefs, "defaults", "copyright", ""));
+ wgtSetText(sPrefsDlg.defCopyright, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_COPYRIGHT, ""));
wgtLabel(prjFrame, "Description:");
- sPrefsDlg.defDescription = wgtTextArea(prjFrame, 512);
+ sPrefsDlg.defDescription = wgtTextArea(prjFrame, IDE_PREFS_DESC_MAX);
sPrefsDlg.defDescription->weight = WGT_WEIGHT_FILL;
- sPrefsDlg.defDescription->minH = wgtPixels(48);
- wgtSetText(sPrefsDlg.defDescription, prefsGetString(sPrefs, "defaults", "description", ""));
+ sPrefsDlg.defDescription->minH = wgtPixels(IDE_PREFS_DESC_H);
+ wgtSetText(sPrefsDlg.defDescription, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_DESCRIPTION, ""));
// ======== Colors tab ========
WidgetT *colorsPage = wgtTabPage(tabs, "Colors");
- colorsPage->spacing = wgtPixels(4);
+ colorsPage->spacing = wgtPixels(IDE_WIDGET_SPACING);
// Load current colors from prefs (or defaults)
for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) {
- char key[32];
- snprintf(key, sizeof(key), "color%d", (int)i);
- sPrefsDlg.syntaxColors[i] = (uint32_t)prefsGetInt(sPrefs, "syntax", key, (int32_t)sDefaultSyntaxColors[i]);
+ char key[IDE_PREF_KEY_BUF];
+ snprintf(key, sizeof(key), PREF_KEY_COLOR_FMT, (int)i);
+ sPrefsDlg.syntaxColors[i] = (uint32_t)prefsGetInt(sPrefs, PREF_SEC_SYNTAX, key, (int32_t)sDefaultSyntaxColors[i]);
}
WidgetT *colorsHBox = wgtHBox(colorsPage);
- colorsHBox->spacing = wgtPixels(8);
+ colorsHBox->spacing = wgtPixels(IDE_WIDGET_SPACING_LG);
colorsHBox->weight = WGT_WEIGHT_FILL;
// Left: color list
@@ -8779,7 +8250,7 @@ static void showPreferencesDialog(void) {
// Right: RGB sliders + value labels + swatch preview
WidgetT *sliderBox = wgtVBox(colorsHBox);
- sliderBox->spacing = wgtPixels(2);
+ sliderBox->spacing = wgtPixels(IDE_WIDGET_SPACING_SM);
sliderBox->weight = WGT_WEIGHT_FILL;
wgtLabel(sliderBox, "Red:");
@@ -8801,25 +8272,25 @@ static void showPreferencesDialog(void) {
wgtLabelSetAlign(sPrefsDlg.lblB, AlignEndE);
wgtLabel(sliderBox, "Preview:");
- sPrefsDlg.colorSwatch = wgtCanvas(sliderBox, 64, 24);
+ sPrefsDlg.colorSwatch = wgtCanvas(sliderBox, IDE_SWATCH_W, IDE_SWATCH_H);
- // Select first color entry and load sliders
- wgtListBoxSetSelected(sPrefsDlg.colorList, 1);
+ // Start on the Keywords entry (the most commonly customized) and load sliders
+ wgtListBoxSetSelected(sPrefsDlg.colorList, SYNTAX_KEYWORD);
prefsUpdateColorSliders();
wgtTabControlSetActive(tabs, 0);
// ---- OK / Cancel ----
WidgetT *btnRow = wgtHBox(root);
- btnRow->spacing = wgtPixels(8);
+ btnRow->spacing = wgtPixels(IDE_WIDGET_SPACING_LG);
btnRow->align = AlignEndE;
WidgetT *btnOk = wgtButton(btnRow, "OK");
- btnOk->minW = wgtPixels(60);
+ btnOk->minW = wgtPixels(IDE_PREFS_BUTTON_W);
btnOk->onClick = onPrefsOk;
WidgetT *btnCancel = wgtButton(btnRow, "Cancel");
- btnCancel->minW = wgtPixels(60);
+ btnCancel->minW = wgtPixels(IDE_PREFS_BUTTON_W);
btnCancel->onClick = onPrefsCancel;
dvxFitWindow(sAc, win);
@@ -8833,17 +8304,22 @@ static void showPreferencesDialog(void) {
if (sPrefsDlg.accepted) {
// General tab
- prefsSetBool(sPrefs, "editor", "renameSkipComments", wgtCheckboxIsChecked(sPrefsDlg.renameSkipComments));
- prefsSetBool(sPrefs, "editor", "optionExplicit", wgtCheckboxIsChecked(sPrefsDlg.optionExplicit));
- prefsSetBool(sPrefs, "editor", "useSpaces", wgtCheckboxIsChecked(sPrefsDlg.useSpaces));
+ prefsSetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, wgtCheckboxIsChecked(sPrefsDlg.renameSkipComments));
+ prefsSetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_OPTION_EXPLICIT, wgtCheckboxIsChecked(sPrefsDlg.optionExplicit));
+ prefsSetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_USE_SPACES, wgtCheckboxIsChecked(sPrefsDlg.useSpaces));
const char *tw = wgtGetText(sPrefsDlg.tabWidthInput);
- int32_t tabW = tw ? atoi(tw) : 3;
+ int32_t tabW = tw ? atoi(tw) : IDE_DEFAULT_TAB_WIDTH;
- if (tabW < 1) { tabW = 1; }
- if (tabW > 8) { tabW = 8; }
+ if (tabW < IDE_MIN_TAB_WIDTH) {
+ tabW = IDE_MIN_TAB_WIDTH;
+ }
- prefsSetInt(sPrefs, "editor", "tabWidth", tabW);
+ if (tabW > IDE_MAX_TAB_WIDTH) {
+ tabW = IDE_MAX_TAB_WIDTH;
+ }
+
+ prefsSetInt(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_TAB_WIDTH, tabW);
if (sEditor) {
wgtTextAreaSetTabWidth(sEditor, tabW);
@@ -8853,25 +8329,25 @@ static void showPreferencesDialog(void) {
const char *val;
val = wgtGetText(sPrefsDlg.defAuthor);
- prefsSetString(sPrefs, "defaults", "author", val ? val : "");
+ prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_AUTHOR, val ? val : "");
val = wgtGetText(sPrefsDlg.defPublisher);
- prefsSetString(sPrefs, "defaults", "publisher", val ? val : "");
+ prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_PUBLISHER, val ? val : "");
val = wgtGetText(sPrefsDlg.defVersion);
- prefsSetString(sPrefs, "defaults", "version", val ? val : "1.0");
+ prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_VERSION, val ? val : IDE_PREF_DEFAULT_VERSION);
val = wgtGetText(sPrefsDlg.defCopyright);
- prefsSetString(sPrefs, "defaults", "copyright", val ? val : "");
+ prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_COPYRIGHT, val ? val : "");
val = wgtGetText(sPrefsDlg.defDescription);
- prefsSetString(sPrefs, "defaults", "description", val ? val : "");
+ prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_DESCRIPTION, val ? val : "");
// Colors tab
for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) {
- char key[32];
- snprintf(key, sizeof(key), "color%d", (int)i);
- prefsSetInt(sPrefs, "syntax", key, (int32_t)sPrefsDlg.syntaxColors[i]);
+ char key[IDE_PREF_KEY_BUF];
+ snprintf(key, sizeof(key), PREF_KEY_COLOR_FMT, (int)i);
+ prefsSetInt(sPrefs, PREF_SEC_SYNTAX, key, (int32_t)sPrefsDlg.syntaxColors[i]);
}
applySyntaxColors();
@@ -8895,10 +8371,14 @@ static void showProc(int32_t procIdx) {
// target index since arrdel shifts everything after it.
if (sCurProcIdx >= -1) {
int32_t deletedIdx = sCurProcIdx;
- bool changed = saveCurProc();
- if (changed && deletedIdx >= 0 && procIdx > deletedIdx) {
- procIdx--;
+ if (saveCurProc()) {
+ // The proc list changed; keep sProcTable in step with it
+ joinProcBufs();
+
+ if (deletedIdx >= 0 && procIdx > deletedIdx) {
+ procIdx--;
+ }
}
}
@@ -8919,11 +8399,6 @@ static void showProc(int32_t procIdx) {
}
-// findInProject -- search all project files for a text match.
-// Starts from the current editor position in the current file,
-// then continues through subsequent files, wrapping around.
-// Opens the file and selects the match when found.
-
// showProcAndFind -- switch to a procedure, sync the dropdowns, and
// select the search match in the editor.
@@ -8962,9 +8437,9 @@ static void showWatchWindow(void) {
return;
}
- int32_t winW = 280;
- int32_t winH = 180;
- int32_t winX = sAc->display.width - winW - 260;
+ int32_t winW = IDE_WATCH_WIN_W;
+ int32_t winH = IDE_DEBUG_WIN_H;
+ int32_t winX = sAc->display.width - winW - IDE_WATCH_WIN_X_GAP;
int32_t winY = toolbarBottom();
sWatchWin = dvxCreateWindow(sAc, "Watch", winX, winY, winW, winH, true);
@@ -8979,7 +8454,7 @@ static void showWatchWindow(void) {
if (root) {
// Expression input at top
- sWatchInput = wgtTextInput(root, 256);
+ sWatchInput = wgtTextInput(root, IDE_VALUE_BUF);
if (sWatchInput) {
sWatchInput->onKeyDown = onWatchInputKeyDown;
@@ -8998,7 +8473,7 @@ static void showWatchWindow(void) {
{ "Value", wgtChars(20), ListViewAlignLeftE },
};
- wgtListViewSetColumns(sWatchList, cols, 2);
+ wgtListViewSetColumns(sWatchList, cols, (int32_t)(sizeof(cols) / sizeof(cols[0])));
}
}
}
@@ -9007,6 +8482,105 @@ static void showWatchWindow(void) {
}
+// skipLine -- advance past the end of the current line (or to the NUL)
+static const char *skipLine(const char *p) {
+ while (*p && *p != '\n') {
+ p++;
+ }
+
+ if (*p == '\n') {
+ p++;
+ }
+
+ return p;
+}
+
+
+// skipToEndProc -- from the start of a SUB/FUNCTION declaration line,
+// return the position just past the matching END SUB/END FUNCTION line
+// (or the end of the text). *outEndLineOffset receives the END line's
+// distance in lines from the declaration (the last line's when missing).
+static const char *skipToEndProc(const char *declLine, bool isSub, int32_t *outEndLineOffset) {
+ const char *endTag = isSub ? KW_END_SUB : KW_END_FUNCTION;
+ const char *scan = skipLine(declLine);
+ int32_t line = 1;
+ bool found = false;
+
+ while (*scan) {
+ if (kwMatch(dvxSkipWs(scan), endTag)) {
+ found = true;
+ scan = skipLine(scan);
+ break;
+ }
+
+ scan = skipLine(scan);
+ line++;
+ }
+
+ *outEndLineOffset = found ? line : line - 1;
+ return scan;
+}
+
+
+// splitProcs -- split source into a (General) buffer plus one buffer per
+// SUB/FUNCTION block. Every line outside a procedure (including any
+// between or after procedures) belongs to General, so nothing is lost
+// when the file is reassembled. Caller frees both results.
+static void splitProcs(const char *source, char **outGeneral, char ***outProcs) {
+ int32_t srcLen = (int32_t)strlen(source);
+ char *general = (char *)malloc(srcLen + 2); // room for '\n' + NUL
+ int32_t genLen = 0;
+ char **procs = NULL;
+ const char *pos = source;
+
+ while (*pos) {
+ bool isSub = false;
+
+ if (procDeclAt(dvxSkipWs(pos), &isSub)) {
+ int32_t endOffset = 0;
+ const char *end = skipToEndProc(pos, isSub, &endOffset);
+ int32_t procLen = (int32_t)(end - pos);
+ char *procBuf = (char *)malloc(procLen + 1);
+
+ if (procBuf) {
+ memcpy(procBuf, pos, procLen);
+ procBuf[procLen] = '\0';
+ arrput(procs, procBuf);
+ }
+
+ pos = end;
+ continue;
+ }
+
+ const char *next = skipLine(pos);
+
+ if (general) {
+ memcpy(general + genLen, pos, next - pos);
+ genLen += (int32_t)(next - pos);
+ }
+
+ pos = next;
+ }
+
+ if (general) {
+ // Trim trailing blank lines; a non-empty section ends in one newline
+ while (genLen > 0 && (general[genLen - 1] == '\n' || general[genLen - 1] == '\r' ||
+ general[genLen - 1] == ' ' || general[genLen - 1] == '\t')) {
+ genLen--;
+ }
+
+ if (genLen > 0) {
+ general[genLen++] = '\n';
+ }
+
+ general[genLen] = '\0';
+ }
+
+ *outGeneral = general;
+ *outProcs = procs;
+}
+
+
// stashCurrentFile -- stash the currently active file's editor/designer
// state back into its project buffer. This is caching only -- does not
// mark modified.
@@ -9025,25 +8599,31 @@ static void stashCurrentFile(void) {
// Serialize form designer state to .frm text
char *frmBuf = (char *)malloc(IDE_MAX_SOURCE);
- if (frmBuf) {
- int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE);
-
- free(cur->buffer);
-
- if (frmLen > 0) {
- frmBuf[frmLen] = '\0';
- cur->buffer = frmBuf;
- } else {
- free(frmBuf);
- cur->buffer = NULL;
- }
+ if (!frmBuf) {
+ return;
}
+
+ int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE);
+
+ // dsgnSaveFrm returns -1 when the form does not fit; keep the
+ // previous buffer rather than a truncated one.
+ if (frmLen < 0) {
+ free(frmBuf);
+ dvxErrorBox(sAc, NULL, "Form is too large to save; its code section does not fit.");
+ return;
+ }
+
+ frmBuf[frmLen] = '\0';
+ free(cur->buffer);
+ cur->buffer = frmBuf;
} else if (!cur->isForm && sEditorFileIdx == sProject.activeFileIdx) {
// Stash full source (only if editor has this file's code)
- saveCurProc();
- const char *src = getFullSource();
- free(cur->buffer);
- cur->buffer = src ? strdup(src) : NULL;
+ char *copy = strdup(getFullSource());
+
+ if (copy) {
+ free(cur->buffer);
+ cur->buffer = copy;
+ }
}
}
@@ -9095,9 +8675,12 @@ static void stashFormCode(void) {
return;
}
- saveCurProc();
- free(sDesigner.form->code);
- sDesigner.form->code = strdup(getFullSource());
+ char *code = strdup(getFullSource());
+
+ if (code) {
+ free(sDesigner.form->code);
+ sDesigner.form->code = code;
+ }
}
@@ -9106,33 +8689,29 @@ static void switchToDesign(void) {
// If already open, just bring to front
if (sFormWin) {
+ dvxRaiseWindow(sAc, sFormWin);
return;
}
// If no form is loaded, create a blank one
if (!sDesigner.form) {
- dsgnNewForm(&sDesigner, "Form1");
+ dsgnNewForm(&sDesigner, IDE_DEFAULT_FORM);
}
- // Create the form designer window using the shared form window builder
- const char *formName = sDesigner.form ? sDesigner.form->name : "Form1";
- DsgnFormT *form = sDesigner.form;
+ if (!sDesigner.form) {
+ return;
+ }
- char title[128];
- snprintf(title, sizeof(title), "%s [Design]", formName);
+ DsgnFormT *form = sDesigner.form;
+
+ // Create the form designer window using the shared form window builder
+ char title[IDE_FORM_TITLE_BUF];
+ snprintf(title, sizeof(title), "%s [Design]", form->name);
WidgetT *root;
WidgetT *contentBox;
- sFormWin = dsgnCreateFormWindow(sAc, title,
- form ? form->layout : "VBox",
- form ? form->resizable : true,
- false,
- false,
- form ? form->width : IDE_DESIGN_W,
- form ? form->height : IDE_DESIGN_H,
- 0, 0,
- &root, &contentBox);
+ sFormWin = basFormRtCreateFormWindow(sAc, title, form->layout, form->resizable, false, false, form->width, form->height, 0, 0, &root, &contentBox);
if (!sFormWin) {
return;
@@ -9145,7 +8724,7 @@ static void switchToDesign(void) {
sDesigner.formWin = sFormWin;
// Build preview menu bar from form's menu items
- dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form);
+ dsgnBuildPreviewMenuBar(sFormWin, form);
// Override paint and mouse AFTER wgtInitWindow (which sets widgetOnPaint)
sFormWin->onPaint = onFormWinPaint;
@@ -9158,19 +8737,18 @@ static void switchToDesign(void) {
dsgnCreateWidgets(&sDesigner, contentBox);
// Set form caption as window title
- if (sDesigner.form && sDesigner.form->caption[0]) {
- char winTitle[280];
- snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDesigner.form->caption);
- dvxSetTitle(sAc, sFormWin, winTitle);
+ if (form->caption[0]) {
+ snprintf(title, sizeof(title), "%s [Design]", form->caption);
+ dvxSetTitle(sAc, sFormWin, title);
}
// Size the form window
- if (sDesigner.form && sDesigner.form->autoSize) {
+ if (form->autoSize) {
dvxFitWindow(sAc, sFormWin);
- sDesigner.form->width = sFormWin->w;
- sDesigner.form->height = sFormWin->h;
- } else if (sDesigner.form) {
- dvxResizeWindow(sAc, sFormWin, sDesigner.form->width, sDesigner.form->height);
+ form->width = sFormWin->w;
+ form->height = sFormWin->h;
+ } else {
+ dvxResizeWindow(sAc, sFormWin, form->width, form->height);
}
// Create toolbox and properties windows
@@ -9199,6 +8777,33 @@ static void switchToDesign(void) {
}
+// syncFormDirty -- mirror form->dirty onto the project file entry so the
+// tree, title bar and save logic all see a single consistent flag.
+static void syncFormDirty(void) {
+ if (!sDesigner.form || !sDesigner.form->dirty) {
+ return;
+ }
+
+ int32_t idx = sProject.activeFileIdx;
+
+ if (idx >= 0 && idx < sProject.fileCount && sProject.files[idx].isForm) {
+ sProject.files[idx].modified = true;
+ }
+}
+
+
+// syncVmBreakpoints -- push the IDE breakpoint list to the live VM (no-op
+// when nothing is running or the program was started without debugging).
+static void syncVmBreakpoints(void) {
+ if (!sVm || !sDbgEnabled) {
+ return;
+ }
+
+ buildVmBreakpoints();
+ basVmSetBreakpoints(sVm, sVmBreakpoints, (int32_t)arrlen(sVmBreakpoints));
+}
+
+
static void teardownFormWin(void) {
if (sFormWin) {
dvxDestroyWindow(sAc, sFormWin);
@@ -9217,16 +8822,20 @@ static void toggleBreakpoint(void) {
static void toggleBreakpointLine(int32_t editorLine) {
- int32_t fileIdx = sProject.activeFileIdx;
+ int32_t fileIdx = sEditorFileIdx;
+
+ if (fileIdx < 0) {
+ return;
+ }
// Convert editor line to file code line by adding proc offset
int32_t codeLine = editorLineToCodeLine(editorLine);
- // Check if this breakpoint already exists — remove it
- for (int32_t i = 0; i < sBreakpointCount; i++) {
+ // Check if this breakpoint already exists -- remove it
+ for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) {
if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) {
arrdel(sBreakpoints, i);
- sBreakpointCount = (int32_t)arrlen(sBreakpoints);
+ syncVmBreakpoints();
if (sEditor) {
wgtInvalidatePaint(sEditor);
@@ -9244,14 +8853,9 @@ static void toggleBreakpointLine(int32_t editorLine) {
if (text) {
// Find the start of editorLine (1-based)
const char *p = text;
- int32_t ln = 1;
- while (*p && ln < editorLine) {
- if (*p == '\n') {
- ln++;
- }
-
- p++;
+ for (int32_t ln = 1; *p && ln < editorLine; ln++) {
+ p = skipLine(p);
}
// Skip leading whitespace
@@ -9267,17 +8871,14 @@ static void toggleBreakpointLine(int32_t editorLine) {
return;
}
- if (strncasecmp(p, "REM ", 4) == 0 || strncasecmp(p, "REM\n", 4) == 0 ||
- strncasecmp(p, "REM\r", 4) == 0 || strcasecmp(p, "REM") == 0) {
+ if (kwMatch(p, KW_REM) && !basIsIdentChar(p[KW_LEN(KW_REM)])) {
return;
}
// SUB/FUNCTION declaration or END SUB/FUNCTION
- if (strncasecmp(p, "SUB ", 4) == 0 || strncasecmp(p, "FUNCTION ", 9) == 0) {
- return;
- }
+ bool isSub;
- if (strncasecmp(p, "END SUB", 7) == 0 || strncasecmp(p, "END FUNCTION", 12) == 0) {
+ if (procDeclAt(p, &isSub) || kwMatch(p, KW_END_SUB) || kwMatch(p, KW_END_FUNCTION)) {
return;
}
}
@@ -9288,17 +8889,15 @@ static void toggleBreakpointLine(int32_t editorLine) {
memset(&bp, 0, sizeof(bp));
bp.fileIdx = fileIdx;
bp.codeLine = codeLine;
- bp.procIdx = sCurProcIdx;
if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) {
- snprintf(bp.procName, sizeof(bp.procName), "%s.%s",
- sProcTable[sCurProcIdx].objName, sProcTable[sCurProcIdx].evtName);
+ snprintf(bp.procName, sizeof(bp.procName), "%s.%s", sProcTable[sCurProcIdx].objName, sProcTable[sCurProcIdx].evtName);
} else {
- snprintf(bp.procName, sizeof(bp.procName), "(General)");
+ snprintf(bp.procName, sizeof(bp.procName), "%s", IDE_GENERAL_SECTION);
}
arrput(sBreakpoints, bp);
- sBreakpointCount = (int32_t)arrlen(sBreakpoints);
+ syncVmBreakpoints();
if (sEditor) {
wgtInvalidatePaint(sEditor);
@@ -9309,26 +8908,38 @@ static void toggleBreakpointLine(int32_t editorLine) {
static int32_t toolbarBottom(void) {
- return sWin ? sWin->y + sWin->h + 2 : 60;
+ return sWin ? sWin->y + sWin->h + IDE_TOOLBAR_GAP : IDE_TOOLBAR_FALLBACK_Y;
}
-// ============================================================
-// updateLocalsWindow -- refresh locals display from VM state
-// ============================================================
-
-
static const char *typeNameStr(uint8_t dt) {
switch (dt) {
- case BAS_TYPE_INTEGER: return "Integer";
- case BAS_TYPE_LONG: return "Long";
- case BAS_TYPE_SINGLE: return "Single";
- case BAS_TYPE_DOUBLE: return "Double";
- case BAS_TYPE_STRING: return "String";
- case BAS_TYPE_BOOLEAN: return "Boolean";
- case BAS_TYPE_ARRAY: return "Array";
- case BAS_TYPE_UDT: return "UDT";
- default: return "?";
+ case BAS_TYPE_INTEGER:
+ return "Integer";
+
+ case BAS_TYPE_LONG:
+ return "Long";
+
+ case BAS_TYPE_SINGLE:
+ return "Single";
+
+ case BAS_TYPE_DOUBLE:
+ return "Double";
+
+ case BAS_TYPE_STRING:
+ return "String";
+
+ case BAS_TYPE_BOOLEAN:
+ return "Boolean";
+
+ case BAS_TYPE_ARRAY:
+ return "Array";
+
+ case BAS_TYPE_UDT:
+ return "UDT";
+
+ default:
+ return "?";
}
}
@@ -9338,89 +8949,41 @@ static void updateBreakpointWindow(void) {
return;
}
- // Free previous pass's strdup'd strings
- for (int32_t i = 0; i < (int32_t)arrlen(sBpFiles); i++) {
- free(sBpFiles[i]);
- }
+ listRowsReset(&sBpRows);
- for (int32_t i = 0; i < (int32_t)arrlen(sBpProcs); i++) {
- free(sBpProcs[i]);
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(sBpLines); i++) {
- free(sBpLines[i]);
- }
-
- arrsetlen(sBpFiles, 0);
- arrsetlen(sBpProcs, 0);
- arrsetlen(sBpLines, 0);
- arrsetlen(sBpCells, 0);
-
- if (sBreakpointCount == 0) {
- wgtListViewSetData(sBreakpointList, NULL, 0);
- return;
- }
-
- int32_t count = sBreakpointCount;
+ int32_t count = (int32_t)arrlen(sBreakpoints);
for (int32_t i = 0; i < count; i++) {
- char fileBuf[DVX_MAX_PATH];
- char procBuf[BAS_MAX_PROC_NAME * 2];
- char lineBuf[12];
+ char lineBuf[IDE_LINE_NUM_BUF];
// File name
if (sBreakpoints[i].fileIdx >= 0 && sBreakpoints[i].fileIdx < sProject.fileCount) {
- snprintf(fileBuf, sizeof(fileBuf), "%s", sProject.files[sBreakpoints[i].fileIdx].path);
+ listRowsAdd(&sBpRows, sProject.files[sBreakpoints[i].fileIdx].path);
} else {
- snprintf(fileBuf, sizeof(fileBuf), "?");
+ listRowsAdd(&sBpRows, "?");
}
// Procedure name
- snprintf(procBuf, sizeof(procBuf), "%s", sBreakpoints[i].procName);
+ listRowsAdd(&sBpRows, sBreakpoints[i].procName);
// Line number
snprintf(lineBuf, sizeof(lineBuf), "%d", (int)sBreakpoints[i].codeLine);
-
- arrput(sBpFiles, strdup(fileBuf));
- arrput(sBpProcs, strdup(procBuf));
- arrput(sBpLines, strdup(lineBuf));
+ listRowsAdd(&sBpRows, lineBuf);
}
- for (int32_t i = 0; i < count; i++) {
- arrput(sBpCells, sBpFiles[i]);
- arrput(sBpCells, sBpProcs[i]);
- arrput(sBpCells, sBpLines[i]);
- }
-
- wgtListViewSetData(sBreakpointList, sBpCells, count);
+ listRowsCommit(&sBpRows, sBreakpointList, count);
}
-// ============================================================
-// updateCallStackWindow
-// ============================================================
-
-
static void updateCallStackWindow(void) {
if (!sCallStackList || !sCallStackWin || !sCallStackWin->visible) {
return;
}
- // Free previous pass's strdup'd strings
- for (int32_t i = 0; i < (int32_t)arrlen(sCallNames); i++) {
- free(sCallNames[i]);
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(sCallLines); i++) {
- free(sCallLines[i]);
- }
-
- arrsetlen(sCallNames, 0);
- arrsetlen(sCallLines, 0);
- arrsetlen(sCallCells, 0);
+ listRowsReset(&sCallRows);
if (sDbgState != DBG_PAUSED || !sVm || !sDbgModule) {
- wgtListViewSetData(sCallStackList, NULL, 0);
+ listRowsCommit(&sCallRows, sCallStackList, 0);
return;
}
@@ -9428,105 +8991,28 @@ static void updateCallStackWindow(void) {
// Current location first
if (sDbgCurrentLine > 0) {
- // Find proc name for current PC
- const char *procName = "(module)";
+ char lineBuf[IDE_LINE_NUM_BUF];
- for (int32_t i = 0; i < sDbgModule->procCount; i++) {
- if (sDbgModule->procs[i].codeAddr <= sVm->pc) {
- bool best = true;
-
- for (int32_t j = 0; j < sDbgModule->procCount; j++) {
- if (sDbgModule->procs[j].codeAddr > sDbgModule->procs[i].codeAddr &&
- sDbgModule->procs[j].codeAddr <= sVm->pc) {
- best = false;
- break;
- }
- }
-
- if (best) {
- procName = sDbgModule->procs[i].name;
- }
- }
- }
-
- char nameBuf[BAS_MAX_PROC_NAME];
- char lineBuf[16];
-
- snprintf(nameBuf, sizeof(nameBuf), "%s", procName);
snprintf(lineBuf, sizeof(lineBuf), "%d", (int)sDbgCurrentLine);
- arrput(sCallNames, strdup(nameBuf));
- arrput(sCallLines, strdup(lineBuf));
+ listRowsAdd(&sCallRows, vmProcNameForPc(sVm->pc));
+ listRowsAdd(&sCallRows, lineBuf);
rowCount++;
}
// Walk call stack (skip frame 0 which is the implicit module frame)
for (int32_t d = sVm->callDepth - 2; d >= 0; d--) {
- int32_t retPc = sVm->callStack[d + 1].returnPc;
- const char *name = "(module)";
-
- for (int32_t i = 0; i < sDbgModule->procCount; i++) {
- if (sDbgModule->procs[i].codeAddr <= retPc) {
- bool best = true;
-
- for (int32_t j = 0; j < sDbgModule->procCount; j++) {
- if (sDbgModule->procs[j].codeAddr > sDbgModule->procs[i].codeAddr &&
- sDbgModule->procs[j].codeAddr <= retPc) {
- best = false;
- break;
- }
- }
-
- if (best) {
- name = sDbgModule->procs[i].name;
- }
- }
- }
-
- char nameBuf[BAS_MAX_PROC_NAME];
-
- snprintf(nameBuf, sizeof(nameBuf), "%s", name);
- arrput(sCallNames, strdup(nameBuf));
- arrput(sCallLines, strdup(""));
+ listRowsAdd(&sCallRows, vmProcNameForPc(sVm->callStack[d + 1].returnPc));
+ listRowsAdd(&sCallRows, "");
rowCount++;
}
- for (int32_t i = 0; i < rowCount; i++) {
- arrput(sCallCells, sCallNames[i]);
- arrput(sCallCells, sCallLines[i]);
- }
-
- wgtListViewSetData(sCallStackList, sCallCells, rowCount);
+ listRowsCommit(&sCallRows, sCallStackList, rowCount);
}
static void updateDirtyIndicators(void) {
- // Sync form->dirty to the project file entry so the tree, title
- // bar, and save logic all see a single consistent modified flag.
- if (sDesigner.form && sDesigner.form->dirty) {
- int32_t idx = sProject.activeFileIdx;
- if (idx >= 0 && idx < sProject.fileCount && sProject.files[idx].isForm) {
- sProject.files[idx].modified = true;
- }
- }
-
- // Toolbar title: "DVX BASIC - [ProjectName] *"
- if (sWin && sProject.projectPath[0] != '\0') {
- char title[300];
- bool anyDirty = sProject.dirty;
-
- if (!anyDirty) {
- for (int32_t i = 0; i < sProject.fileCount; i++) {
- if (sProject.files[i].modified) {
- anyDirty = true;
- break;
- }
- }
- }
-
- snprintf(title, sizeof(title), "DVX BASIC - [%s]%s",
- sProject.name, anyDirty ? " *" : "");
- dvxSetTitle(sAc, sWin, title);
- }
+ syncFormDirty();
+ updateMainTitle();
// Code window title -- only shows * when code has been edited
if (sCodeWin) {
@@ -9542,14 +9028,14 @@ static void updateDirtyIndicators(void) {
codeFile = sProject.files[sProject.activeFileIdx].path;
}
- char codeTitle[DVX_MAX_PATH + 16];
+ char codeTitle[IDE_MSG_BUF];
snprintf(codeTitle, sizeof(codeTitle), "Code - %s%s", codeFile, codeDirty ? " *" : "");
dvxSetTitle(sAc, sCodeWin, codeTitle);
}
// Design window title
if (sFormWin && sDesigner.form) {
- char title[280];
+ char title[IDE_FORM_TITLE_BUF];
snprintf(title, sizeof(title), "%s [Design]%s",
sDesigner.form->caption[0] ? sDesigner.form->caption : sDesigner.form->name,
sDesigner.form->dirty ? " *" : "");
@@ -9563,12 +9049,16 @@ static void updateDirtyIndicators(void) {
}
+// updateDropdowns -- refresh sProcTable from the editor's buffers and
+// repopulate the Object dropdown (which cascades into the Event dropdown).
+
static void updateDropdowns(void) {
+ // Rebuilds sProcTable as a side effect
+ getFullSource();
+
// Reset dynamic arrays. sObjItems owns strdup'd copies -- free them
// first so no entry survives a subsequent controls/menuItems mutation
// that could invalidate the source pointer.
- arrsetlen(sProcTable, 0);
-
for (int32_t i = 0; i < (int32_t)arrlen(sObjItems); i++) {
free((void *)sObjItems[i]);
}
@@ -9580,162 +9070,12 @@ static void updateDropdowns(void) {
return;
}
- // Scan the ORIGINAL parsed source (a .bas file or a .frm's code
- // section) so line numbers match what prjMapLine returns.
- // getFullSource() packs blank lines between procs, so its line
- // numbering diverges from the file and breaks runtime error
- // navigation and the navigateToCodeLine line translation.
- const char *src = sParsedSource;
-
- if (!src) {
- src = getFullSource();
- }
-
- if (!src) {
- return;
- }
-
- // Collect all known object names once; they only depend on sDesigner.form
- // and never change during the proc scan below.
- const char *objNames[512];
- int32_t objNameCount = 0;
-
- if (sDesigner.form) {
- objNames[objNameCount++] = sDesigner.form->name;
-
- for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls) && objNameCount < 511; ci++) {
- objNames[objNameCount++] = sDesigner.form->controls[ci]->name;
- }
-
- for (int32_t mi = 0; mi < (int32_t)arrlen(sDesigner.form->menuItems) && objNameCount < 511; mi++) {
- objNames[objNameCount++] = sDesigner.form->menuItems[mi].name;
- }
- }
-
- // Scan line by line for SUB / FUNCTION
- const char *pos = src;
- int32_t lineNum = 1;
-
- while (*pos) {
- const char *lineStart = pos;
-
- // Skip leading whitespace
- pos = dvxSkipWs(pos);
-
- // Check for SUB or FUNCTION keyword
- bool isSub = (strncasecmp(pos, "SUB ", 4) == 0);
- bool isFunc = (strncasecmp(pos, "FUNCTION ", 9) == 0);
-
- if (isSub || isFunc) {
- pos = dvxSkipWs(pos + (isSub ? 4 : 9));
-
- char procName[IDE_NAME_BUF];
- int32_t nameLen = 0;
-
- while (*pos && *pos != '(' && *pos != ' ' && *pos != '\t' && *pos != '\n' && *pos != '\r' && nameLen < 63) {
- procName[nameLen++] = *pos++;
- }
-
- procName[nameLen] = '\0';
-
- // Find End Sub / End Function and record its line.
- const char *endTag = isSub ? "END SUB" : "END FUNCTION";
- int32_t endTagLen = isSub ? 7 : 12;
- const char *scan = pos;
- int32_t scanLine = lineNum; // line of the Sub/Function declaration
- int32_t endLineNum = 0;
-
- while (*scan) {
- const char *sl = dvxSkipWs(scan);
-
- if (strncasecmp(sl, endTag, endTagLen) == 0) {
- endLineNum = scanLine;
-
- // Advance past the End line
- while (*scan && *scan != '\n') {
- scan++;
- }
-
- if (*scan == '\n') {
- scan++;
- scanLine++;
- }
-
- break;
- }
-
- while (*scan && *scan != '\n') {
- scan++;
- }
-
- if (*scan == '\n') {
- scan++;
- scanLine++;
- }
- }
-
- IdeProcEntryT entry;
- memset(&entry, 0, sizeof(entry));
- entry.lineNum = lineNum;
- entry.endLineNum = endLineNum > 0 ? endLineNum : scanLine;
-
- // Match proc name against known objects: form name,
- // controls, menu items. Try each as a prefix followed
- // by "_". This handles names with multiple underscores
- // correctly (e.g., "cmdOK_Click" matches "cmdOK", not
- // "This_Is_A_Dumb_Name" matching a control named "This").
- bool isEvent = false;
-
- // Try each known object name as prefix + "_" (table hoisted above).
- for (int32_t oi = 0; oi < objNameCount; oi++) {
- int32_t nameLen = (int32_t)strlen(objNames[oi]);
-
- if (nameLen > 0 && strncasecmp(procName, objNames[oi], nameLen) == 0 && procName[nameLen] == '_') {
- snprintf(entry.objName, sizeof(entry.objName), "%s", objNames[oi]);
- snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName + nameLen + 1);
- isEvent = true;
- break;
- }
- }
-
- if (!isEvent) {
- snprintf(entry.objName, sizeof(entry.objName), "%s", "(General)");
- snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName);
- }
-
- arrput(sProcTable, entry);
-
- // Skip to end of this proc (already scanned)
- pos = scan;
-
- // Count lines we skipped
- for (const char *c = lineStart; c < scan; c++) {
- if (*c == '\n') {
- lineNum++;
- }
- }
-
- continue;
- }
-
- // Advance to end of line
- while (*pos && *pos != '\n') {
- pos++;
- }
-
- if (*pos == '\n') {
- pos++;
- }
-
- lineNum++;
- }
-
// Build object names for the Object dropdown. All entries are
// strdup'd so sObjItems owns its strings -- the designer's menuItems
// array in particular is an stb_ds struct array whose backing
// storage can move on the next arrput, which would dangle any name
// pointer we borrowed from &menuItems[i].name.
- arrput(sObjItems, strdup("(General)"));
+ arrput(sObjItems, strdup(IDE_GENERAL_SECTION));
if (sDesigner.form) {
arrput(sObjItems, strdup(sDesigner.form->name));
@@ -9754,6 +9094,13 @@ static void updateDropdowns(void) {
}
}
+ // A failed strdup would put a NULL name in the dropdown
+ for (int32_t i = (int32_t)arrlen(sObjItems) - 1; i >= 0; i--) {
+ if (!sObjItems[i]) {
+ arrdel(sObjItems, i);
+ }
+ }
+
// Sort object items alphabetically, keeping (General) first
int32_t objCount = (int32_t)arrlen(sObjItems);
@@ -9777,41 +9124,15 @@ static void updateLocalsWindow(void) {
return;
}
- // Free previous pass's strdup'd strings
- for (int32_t i = 0; i < (int32_t)arrlen(sLocalsNames); i++) {
- free(sLocalsNames[i]);
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(sLocalsTypes); i++) {
- free(sLocalsTypes[i]);
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(sLocalsValues); i++) {
- free(sLocalsValues[i]);
- }
-
- arrsetlen(sLocalsNames, 0);
- arrsetlen(sLocalsTypes, 0);
- arrsetlen(sLocalsValues, 0);
- arrsetlen(sLocalsCells, 0);
+ listRowsReset(&sLocalsRows);
if (sDbgState != DBG_PAUSED || !sVm || !sDbgModule) {
- wgtListViewSetData(sLocalsList, NULL, 0);
+ listRowsCommit(&sLocalsRows, sLocalsList, 0);
return;
}
// Find which procedure we're in by matching PC to proc table
- int32_t curProcIdx = -1;
- int32_t bestAddr = -1;
-
- for (int32_t i = 0; i < sDbgModule->procCount; i++) {
- int32_t addr = sDbgModule->procs[i].codeAddr;
-
- if (addr <= sVm->pc && addr > bestAddr) {
- bestAddr = addr;
- curProcIdx = i;
- }
- }
+ int32_t curProcIdx = vmProcIndexForPc(sVm->pc);
// Collect matching debug vars
int32_t rowCount = 0;
@@ -9825,15 +9146,8 @@ static void updateLocalsWindow(void) {
continue;
}
- // Skip internal mangled names (e.g. "DoCount$Count" for Static vars).
- // String variable names end with $ (e.g. "name$") — those are fine.
- // Mangled names have $ in the middle.
- {
- const char *dollar = strchr(dv->name, '$');
-
- if (dollar && dollar[1] != '\0') {
- continue;
- }
+ if (isMangledDebugName(dv->name)) {
+ continue;
}
// For form-scope vars, only show if we're in that form's context
@@ -9853,33 +9167,20 @@ static void updateLocalsWindow(void) {
}
}
- char nameBuf[BAS_MAX_PROC_NAME];
- char typeBuf[16];
- char valueBuf[IDE_NAME_BUF];
-
- snprintf(nameBuf, sizeof(nameBuf), "%s", dv->name);
+ char typeBuf[IDE_TYPE_NAME_BUF];
+ char valueBuf[BAS_MAX_IDENT];
// Read the value first so we can use it for the type column
- BasValueT val;
- memset(&val, 0, sizeof(val));
+ BasValueT val;
+ BasValueT *slot = getDebugVarSlot(dv);
- if (dv->scope == SCOPE_LOCAL && sVm->callDepth > 0) {
- BasCallFrameT *frame = &sVm->callStack[sVm->callDepth - 1];
-
- if (dv->index >= 0 && dv->index < BAS_VM_MAX_LOCALS) {
- val = frame->locals[dv->index];
- }
- } else if (dv->scope == SCOPE_GLOBAL) {
- if (dv->index >= 0 && dv->index < BAS_VM_MAX_GLOBALS) {
- val = sVm->globals[dv->index];
- }
- } else if (dv->scope == SCOPE_FORM && sVm->currentFormVars) {
- if (dv->index >= 0 && dv->index < sVm->currentFormVarCount) {
- val = sVm->currentFormVars[dv->index];
- }
+ if (slot) {
+ val = *slot;
+ } else {
+ memset(&val, 0, sizeof(val));
}
- // Type column — arrays show "Array(type)" with element type
+ // Type column -- arrays show "Array(type)" with element type
if (dv->dataType == BAS_TYPE_ARRAY && val.arrVal) {
snprintf(typeBuf, sizeof(typeBuf), "%s()", typeNameStr(val.arrVal->elementType));
} else {
@@ -9888,20 +9189,39 @@ static void updateLocalsWindow(void) {
formatValue(&val, valueBuf, sizeof(valueBuf));
- arrput(sLocalsNames, strdup(nameBuf));
- arrput(sLocalsTypes, strdup(typeBuf));
- arrput(sLocalsValues, strdup(valueBuf));
+ listRowsAdd(&sLocalsRows, dv->name);
+ listRowsAdd(&sLocalsRows, typeBuf);
+ listRowsAdd(&sLocalsRows, valueBuf);
rowCount++;
}
}
- for (int32_t i = 0; i < rowCount; i++) {
- arrput(sLocalsCells, sLocalsNames[i]);
- arrput(sLocalsCells, sLocalsTypes[i]);
- arrput(sLocalsCells, sLocalsValues[i]);
+ listRowsCommit(&sLocalsRows, sLocalsList, rowCount);
+}
+
+
+// updateMainTitle -- "DVX BASIC - [ProjectName] *" (the * when anything
+// is unsaved), or just "DVX BASIC" with no project open.
+static void updateMainTitle(void) {
+ if (!sWin) {
+ return;
}
- wgtListViewSetData(sLocalsList, sLocalsCells, rowCount);
+ char title[IDE_TITLE_BUF];
+
+ if (hasProject()) {
+ bool anyDirty = sProject.dirty;
+
+ for (int32_t i = 0; i < sProject.fileCount && !anyDirty; i++) {
+ anyDirty = sProject.files[i].modified;
+ }
+
+ snprintf(title, sizeof(title), "%s - [%s]%s", IDE_MAIN_TITLE, sProject.name, anyDirty ? " *" : "");
+ } else {
+ snprintf(title, sizeof(title), "%s", IDE_MAIN_TITLE);
+ }
+
+ dvxSetTitle(sAc, sWin, title);
}
@@ -9910,20 +9230,23 @@ static void updateProjectMenuState(void) {
return;
}
- bool hasProject = (sProject.projectPath[0] != '\0');
- bool hasFile = (hasProject && sProject.activeFileIdx >= 0);
+ bool project = hasProject();
+ bool hasFile = (project && sProject.activeFileIdx >= 0);
bool hasForm = (hasFile && sProject.files[sProject.activeFileIdx].isForm);
bool isIdle = (sDbgState == DBG_IDLE);
bool isPaused = (sDbgState == DBG_PAUSED);
bool isRunning = (sDbgState == DBG_RUNNING);
- bool canRun = hasProject && (isIdle || isPaused);
+ bool canRun = project && (isIdle || isPaused);
bool canStop = isRunning || isPaused;
// Project menu
- wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_SAVE, hasProject && sProject.dirty);
- wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_CLOSE, hasProject);
- wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_PROPS, hasProject);
- wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, prjGetSelectedFileIdx() >= 0);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_SAVE, project && sProject.dirty);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_CLOSE, project && isIdle);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_NEW, isIdle);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_OPEN, isIdle);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_OPEN, isIdle);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_PROPS, project);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, isIdle && prjGetSelectedFileIdx() >= 0);
// Save: only when active file is dirty
bool fileDirty = hasFile && sProject.files[sProject.activeFileIdx].modified;
@@ -9935,9 +9258,7 @@ static void updateProjectMenuState(void) {
bool anyDirty = false;
for (int32_t i = 0; i < sProject.fileCount && !anyDirty; i++) {
- if (sProject.files[i].modified) {
- anyDirty = true;
- }
+ anyDirty = sProject.files[i].modified;
}
if (sDesigner.form && sDesigner.form->dirty) {
@@ -9945,16 +9266,16 @@ static void updateProjectMenuState(void) {
}
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, anyDirty);
- wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_EXE, hasProject && isIdle);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_EXE, project && isIdle);
// Edit menu
- wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject);
- wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT, hasProject);
- wmMenuItemSetEnabled(sWin->menuBar, CMD_REPLACE, hasProject);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, project);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT, project);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_REPLACE, project);
- // View menu — consider both active file and project tree selection
+ // View menu -- consider both active file and project tree selection
int32_t selIdx = prjGetSelectedFileIdx();
- bool selIsFile = (hasProject && selIdx >= 0 && selIdx < sProject.fileCount);
+ bool selIsFile = (project && selIdx >= 0 && selIdx < sProject.fileCount);
bool selIsForm = (selIsFile && sProject.files[selIdx].isForm);
bool canCode = hasFile || selIsFile;
bool canDesign = hasForm || selIsForm;
@@ -9965,7 +9286,7 @@ static void updateProjectMenuState(void) {
// Run menu
wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN, canRun);
- wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN_NOCMP, canRun && sCachedModule != NULL);
+ wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN_NOCMP, project && isIdle && sCachedModule != NULL);
wmMenuItemSetEnabled(sWin->menuBar, CMD_DEBUG, canRun);
wmMenuItemSetEnabled(sWin->menuBar, CMD_STOP, canStop);
wmMenuItemSetEnabled(sWin->menuBar, CMD_STEP_INTO, canRun);
@@ -9975,48 +9296,59 @@ static void updateProjectMenuState(void) {
wmMenuItemSetEnabled(sWin->menuBar, CMD_TOGGLE_BP, hasFile);
// Toolbar buttons
- if (sTbRun) { wgtSetEnabled(sTbRun, canRun); }
- if (sTbStop) { wgtSetEnabled(sTbStop, canStop); }
- if (sTbDebug) { wgtSetEnabled(sTbDebug, canRun); }
- if (sTbStepInto) { wgtSetEnabled(sTbStepInto, canRun); }
- if (sTbStepOver) { wgtSetEnabled(sTbStepOver, isPaused); }
- if (sTbStepOut) { wgtSetEnabled(sTbStepOut, isPaused); }
- if (sTbRunToCur) { wgtSetEnabled(sTbRunToCur, isPaused); }
- if (sTbCode) { wgtSetEnabled(sTbCode, canCode); }
- if (sTbDesign) { wgtSetEnabled(sTbDesign, canDesign); }
+ if (sTbRun) {
+ wgtSetEnabled(sTbRun, canRun);
+ }
+
+ if (sTbStop) {
+ wgtSetEnabled(sTbStop, canStop);
+ }
+
+ if (sTbDebug) {
+ wgtSetEnabled(sTbDebug, canRun);
+ }
+
+ if (sTbStepInto) {
+ wgtSetEnabled(sTbStepInto, canRun);
+ }
+
+ if (sTbStepOver) {
+ wgtSetEnabled(sTbStepOver, isPaused);
+ }
+
+ if (sTbStepOut) {
+ wgtSetEnabled(sTbStepOut, isPaused);
+ }
+
+ if (sTbRunToCur) {
+ wgtSetEnabled(sTbRunToCur, isPaused);
+ }
+
+ if (sTbCode) {
+ wgtSetEnabled(sTbCode, canCode);
+ }
+
+ if (sTbDesign) {
+ wgtSetEnabled(sTbDesign, canDesign);
+ }
}
+// updateWatchWindow -- evaluate watch expressions
+
static void updateWatchWindow(void) {
if (!sWatchList || !sWatchWin || !sWatchWin->visible) {
return;
}
- // Free previous pass's strdup'd strings
- for (int32_t i = 0; i < (int32_t)arrlen(sWatchExprBuf); i++) {
- free(sWatchExprBuf[i]);
- }
-
- for (int32_t i = 0; i < (int32_t)arrlen(sWatchValBuf); i++) {
- free(sWatchValBuf[i]);
- }
-
- arrsetlen(sWatchExprBuf, 0);
- arrsetlen(sWatchValBuf, 0);
- arrsetlen(sWatchCells, 0);
+ listRowsReset(&sWatchRows);
int32_t count = (int32_t)arrlen(sWatchExprs);
- if (count == 0) {
- wgtListViewSetData(sWatchList, NULL, 0);
- return;
- }
-
for (int32_t i = 0; i < count; i++) {
- char exprBuf[256];
- char valBuf[256];
+ char valBuf[IDE_VALUE_BUF];
- snprintf(exprBuf, sizeof(exprBuf), "%s", sWatchExprs[i]);
+ valBuf[0] = '\0';
if (sDbgState == DBG_PAUSED && sVm && sDbgModule) {
BasValueT val;
@@ -10025,25 +9357,257 @@ static void updateWatchWindow(void) {
if (lookupWatchVar(sWatchExprs[i], &val)) {
// Simple variable/field/subscript lookup succeeded
formatValue(&val, valBuf, sizeof(valBuf));
- } else if (evalWatchExpr(sWatchExprs[i], valBuf, sizeof(valBuf))) {
- // Expression compiled and evaluated successfully
- } else {
+ } else if (!evalWatchExpr(sWatchExprs[i], valBuf, sizeof(valBuf))) {
snprintf(valBuf, sizeof(valBuf), "");
}
- } else {
- valBuf[0] = '\0';
}
- arrput(sWatchExprBuf, strdup(exprBuf));
- arrput(sWatchValBuf, strdup(valBuf));
+ listRowsAdd(&sWatchRows, sWatchExprs[i]);
+ listRowsAdd(&sWatchRows, valBuf);
}
- for (int32_t i = 0; i < count; i++) {
- arrput(sWatchCells, sWatchExprBuf[i]);
- arrput(sWatchCells, sWatchValBuf[i]);
+ listRowsCommit(&sWatchRows, sWatchList, count);
+}
+
+
+// ============================================================
+// Compile-time CtrlName.Member validator
+// ============================================================
+//
+// The IDE (unlike bascomp) has widget DXEs loaded, so wgtGetIface
+// returns live interface metadata. Combined with a static scan of
+// the project's .frm files (control name -> widget type), we can
+// reject typos like GfxCanvas.Boggle or LblStatus.Caphtion at
+// compile time instead of letting them surface as runtime errors
+// at event-click time. Dynamically-created controls (via
+// CreateControl at runtime) aren't in the map; lookupCtrlType
+// returns NULL for them and validation is skipped.
+
+// Walk every .frm in the project and populate a (name -> wgtType) map
+// the parser can consult. Caller must arrfree(ctx->entries) when done.
+static void validatorBuildCtrlMap(IdeValidatorCtxT *ctx) {
+ ctx->entries = NULL;
+
+ FrmParserCbsT cbs;
+ memset(&cbs, 0, sizeof(cbs));
+ cbs.userData = ctx;
+ cbs.onFormBegin = validatorOnFormBegin;
+ cbs.onCtrlBegin = validatorOnCtrlBegin;
+ cbs.onMenuBegin = validatorOnMenuBegin;
+
+ for (int32_t i = 0; i < sProject.fileCount; i++) {
+ if (!sProject.files[i].isForm) {
+ continue;
+ }
+
+ // Use the buffered source if the file is open in an editor,
+ // otherwise load from disk. Mirrors how the designer loads.
+ char *diskBuf = NULL;
+ const char *src = sProject.files[i].buffer;
+ int32_t len = src ? (int32_t)strlen(src) : 0;
+
+ if (!src) {
+ int32_t dlen = 0;
+ char fullPath[DVX_MAX_PATH];
+
+ // PrjFileT.path is project-relative; read via the full path so
+ // the control map isn't silently empty when the project dir
+ // differs from the process CWD.
+ prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
+ diskBuf = platformReadFile(fullPath, &dlen);
+ src = diskBuf;
+ len = dlen;
+ }
+
+ if (src && len > 0) {
+ frmParse(src, len, &cbs);
+ }
+
+ free(diskBuf);
+ }
+}
+
+
+static bool validatorIsMethodValid(void *ctx, const char *wgtType, const char *methodName) {
+ (void)ctx;
+
+ if (!wgtType || !methodName) {
+ return true; // be permissive on malformed input
}
- wgtListViewSetData(sWatchList, sWatchCells, count);
+ if (basFormRtCommonMethodId(methodName) != BAS_CM_NONE) {
+ return true;
+ }
+
+ // Form-level methods
+ if (strcasecmp(wgtType, "Form") == 0) {
+ return strcasecmp(methodName, "Show") == 0 ||
+ strcasecmp(methodName, "Hide") == 0;
+ }
+
+ // Menu items have no methods beyond common
+ if (strcasecmp(wgtType, "Menu") == 0) {
+ return false;
+ }
+
+ // Widget-specific methods from the live iface
+ const char *wgtName = wgtFindByBasName(wgtType);
+
+ if (!wgtName) {
+ return true; // unknown type -- skip validation
+ }
+
+ const WgtIfaceT *iface = wgtGetIface(wgtName);
+
+ if (!iface || !iface->methods) {
+ return true;
+ }
+
+ for (int32_t i = 0; i < iface->methodCount; i++) {
+ if (strcasecmp(iface->methods[i].name, methodName) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+static bool validatorIsPropValid(void *ctx, const char *wgtType, const char *propName) {
+ (void)ctx;
+
+ if (!wgtType || !propName) {
+ return true;
+ }
+
+ // Form-level properties (the runtime's own table)
+ if (strcasecmp(wgtType, "Form") == 0) {
+ return basFormRtFindFormProp(propName) != NULL;
+ }
+
+ // Menu items
+ if (strcasecmp(wgtType, "Menu") == 0) {
+ return strcasecmp(propName, "Name") == 0 ||
+ strcasecmp(propName, "Checked") == 0 ||
+ strcasecmp(propName, "Enabled") == 0 ||
+ strcasecmp(propName, "Visible") == 0 || // top-level: False = popup
+ strcasecmp(propName, "RadioCheck") == 0 ||
+ strcasecmp(propName, "Caption") == 0;
+ }
+
+ // Properties every non-menu control accepts (the runtime's own table)
+ if (basFormRtFindCommonProp(propName)) {
+ return true;
+ }
+
+ // Widget-specific props from the live iface
+ const char *wgtName = wgtFindByBasName(wgtType);
+
+ if (!wgtName) {
+ return true;
+ }
+
+ const WgtIfaceT *iface = wgtGetIface(wgtName);
+
+ if (!iface || !iface->props) {
+ return true;
+ }
+
+ return wgtIfaceFindProp(iface, propName) != NULL;
+}
+
+
+static const char *validatorLookupCtrlType(void *ctx, const char *ctrlName) {
+ IdeValidatorCtxT *v = (IdeValidatorCtxT *)ctx;
+
+ if (!v || !ctrlName) {
+ return NULL;
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(v->entries); i++) {
+ if (strcasecmp(v->entries[i].name, ctrlName) == 0) {
+ return v->entries[i].wgtType;
+ }
+ }
+
+ return NULL;
+}
+
+
+static void validatorOnCtrlBegin(void *ud, const char *typeName, const char *name) {
+ IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
+ IdeCtrlMapEntryT e;
+ memset(&e, 0, sizeof(e));
+ snprintf(e.name, BAS_MAX_IDENT, "%s", name ? name : "");
+ snprintf(e.wgtType, BAS_MAX_IDENT, "%s", typeName ? typeName : "");
+ arrput(v->entries, e);
+
+ // Also flag unknown widget types. wgtFindByBasName returns NULL
+ // for anything not registered by a .wgt DXE loaded by the IDE.
+ // Skip internal structural types (they're handled by layout code,
+ // not widget DXEs).
+ if (typeName && typeName[0]) {
+ bool structural = (strcasecmp(typeName, "Form") == 0 ||
+ strcasecmp(typeName, "Menu") == 0);
+
+ if (!structural && !wgtFindByBasName(typeName)) {
+ IdeBadTypeT bad;
+ memset(&bad, 0, sizeof(bad));
+ snprintf(bad.ctrlName, BAS_MAX_IDENT, "%s", name ? name : "?");
+ snprintf(bad.typeName, BAS_MAX_IDENT, "%s", typeName);
+ snprintf(bad.formName, BAS_MAX_IDENT, "%s", v->currentForm);
+ arrput(v->badTypes, bad);
+ }
+ }
+}
+
+
+static bool validatorOnFormBegin(void *ud, const char *name) {
+ IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
+ IdeCtrlMapEntryT e;
+ memset(&e, 0, sizeof(e));
+ snprintf(e.name, BAS_MAX_IDENT, "%s", name ? name : "");
+ snprintf(e.wgtType, BAS_MAX_IDENT, "%s", "Form");
+ arrput(v->entries, e);
+ snprintf(v->currentForm, BAS_MAX_IDENT, "%s", name ? name : "");
+ return true;
+}
+
+
+static void validatorOnMenuBegin(void *ud, const char *name, int32_t level) {
+ (void)level;
+ IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
+ IdeCtrlMapEntryT e;
+ memset(&e, 0, sizeof(e));
+ snprintf(e.name, BAS_MAX_IDENT, "%s", name);
+ snprintf(e.wgtType, BAS_MAX_IDENT, "%s", "Menu");
+ arrput(v->entries, e);
+}
+
+
+// vmProcIndexForPc -- index of the compiled proc whose codeAddr is the
+// greatest one <= pc, or -1 for module-level code. Requires sDbgModule.
+static int32_t vmProcIndexForPc(int32_t pc) {
+ int32_t best = -1;
+ int32_t bestAddr = -1;
+
+ for (int32_t i = 0; i < sDbgModule->procCount; i++) {
+ int32_t addr = sDbgModule->procs[i].codeAddr;
+
+ if (addr <= pc && addr > bestAddr) {
+ bestAddr = addr;
+ best = i;
+ }
+ }
+
+ return best;
+}
+
+
+static const char *vmProcNameForPc(int32_t pc) {
+ int32_t idx = vmProcIndexForPc(pc);
+
+ return idx >= 0 ? sDbgModule->procs[idx].name : IDE_MODULE_FRAME;
}
@@ -10072,10 +9636,6 @@ static void watchEditSelected(void) {
}
-// evalWatchExpr -- compile and evaluate an expression using the paused VM's state.
-// Used as a fallback when lookupWatchVar can't handle the expression.
-// Returns the printed result in outBuf.
-
static void watchPrintCallback(void *ctx, const char *text, bool newline) {
(void)ctx;
@@ -10096,24 +9656,57 @@ static void watchPrintCallback(void *ctx, const char *text, bool newline) {
}
+// writeProjectFile -- write files[idx].buffer to disk (a NULL buffer is
+// an empty file). Clears the modified flags only when every write call
+// succeeded; reports the failure to the user otherwise.
+static bool writeProjectFile(int32_t idx) {
+ PrjFileT *file = &sProject.files[idx];
+ char fullPath[DVX_MAX_PATH];
+
+ prjFullPath(&sProject, idx, fullPath, sizeof(fullPath));
+
+ FILE *f = fopen(fullPath, "w");
+ bool ok = (f != NULL);
+
+ if (f) {
+ ok = (fputs(file->buffer ? file->buffer : "", f) != EOF);
+ ok = (fclose(f) == 0) && ok;
+ }
+
+ if (!ok) {
+ char msg[IDE_MSG_BUF];
+ snprintf(msg, sizeof(msg), "Could not write %s.", file->path);
+ dvxErrorBox(sAc, NULL, msg);
+ return false;
+ }
+
+ file->modified = false;
+
+ if (file->isForm && sDesigner.form && idx == sProject.activeFileIdx) {
+ sDesigner.form->dirty = false;
+ }
+
+ return true;
+}
+
+
int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx;
sAc = ctx->shellCtx;
// Set help file and context-sensitive F1 handler
- snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%s", ctx->appDir, "dvxbasic.hlp");
+ snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%s", ctx->appDir, IDE_HELP_FILE);
snprintf(sIdeHelpFile, sizeof(sIdeHelpFile), "%s", ctx->helpFile);
ctx->onHelpQuery = helpQueryHandler;
ctx->helpQueryCtx = NULL;
- basStringSystemInit();
prjInit(&sProject);
buildWindow();
// Load persisted settings
shellEnsureConfigDir(sCtx);
char prefsPath[DVX_MAX_PATH];
- shellConfigPath(sCtx, "dvxbasic.ini", prefsPath, sizeof(prefsPath));
+ shellConfigPath(sCtx, IDE_PREFS_FILE, prefsPath, sizeof(prefsPath));
sPrefs = prefsLoad(prefsPath);
if (!sPrefs) {
@@ -10121,23 +9714,23 @@ int32_t appMain(DxeAppContextT *ctx) {
prefsSaveAs(sPrefs, prefsPath);
}
- if (sToolbar && sWin && sWin->menuBar) {
- bool showTb = prefsGetBool(sPrefs, "view", "toolbar", true);
- sToolbar->visible = showTb;
- wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_TOOLBAR, showTb);
- }
-
- if (sStatusBar && sWin && sWin->menuBar) {
- bool showSb = prefsGetBool(sPrefs, "view", "statusbar", true);
- sStatusBar->visible = showSb;
- wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_STATUS, showSb);
- }
-
if (sWin && sWin->menuBar) {
- bool saveOnRun = prefsGetBool(sPrefs, "run", "saveOnRun", true);
+ if (sToolbar) {
+ bool showTb = prefsGetBool(sPrefs, PREF_SEC_VIEW, PREF_KEY_TOOLBAR, true);
+ sToolbar->visible = showTb;
+ wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_TOOLBAR, showTb);
+ }
+
+ if (sStatusBar) {
+ bool showSb = prefsGetBool(sPrefs, PREF_SEC_VIEW, PREF_KEY_STATUSBAR, true);
+ sStatusBar->visible = showSb;
+ wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_STATUS, showSb);
+ }
+
+ bool saveOnRun = prefsGetBool(sPrefs, PREF_SEC_RUN, PREF_KEY_SAVE_ON_RUN, true);
wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun);
- sOutputToLog = prefsGetBool(sPrefs, "run", "outputToLog", false);
+ sOutputToLog = prefsGetBool(sPrefs, PREF_SEC_RUN, PREF_KEY_OUTPUT_TO_LOG, false);
wmMenuItemSetChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG, sOutputToLog);
}
@@ -10149,11 +9742,7 @@ int32_t appMain(DxeAppContextT *ctx) {
dvxFitWindowH(sAc, sWin);
}
- sOutputBuf[0] = '\0';
- sOutputLen = 0;
-
updateProjectMenuState();
setStatus("Ready.");
return 0;
}
-
diff --git a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c
index 270e905..6ec2655 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c
@@ -45,9 +45,22 @@
// Constants
// ============================================================
-#define MAX_MENU_LEVEL 5
-#define ARROW_STR "-> "
-#define MED_MSG_BUF 128 // error-message scratch buffer
+#define MAX_MENU_LEVEL (DSGN_MENU_STACK_DEPTH - 1) // deepest level the preview bar can show
+#define ARROW_STR "-> "
+#define MED_MSG_BUF 128 // error-message scratch buffer
+#define MED_SEP_PREFIX "mnuSep" // auto-generated separator name prefix
+#define MED_SEP_PREFIX_LEN ((int32_t)(sizeof(MED_SEP_PREFIX) - 1))
+#define MED_NAME_PREFIX "mnu" // auto-generated item name prefix
+#define MED_LABEL_BUF (DSGN_MAX_TEXT + DSGN_MENU_STACK_DEPTH * (int32_t)sizeof(ARROW_STR))
+#define MED_DLG_W 360
+#define MED_DLG_H 420
+#define MED_ROW_SPACING 4
+#define MED_CHECK_SPACING 12
+#define MED_OK_SPACING 8
+#define MED_LABEL_W 60
+#define MED_ARROW_BTN_W 32
+#define MED_OK_BTN_W 60
+#define MED_DLG_TITLE "Menu Editor"
// ============================================================
// Dialog state
@@ -134,8 +147,8 @@ static void applyFields(void) {
int32_t itemCount = (int32_t)arrlen(sMed.items);
for (int32_t i = 0; i < itemCount; i++) {
- if (i != sMed.selectedIdx && strncasecmp(sMed.items[i].name, "mnuSep", 6) == 0) {
- int32_t n = atoi(sMed.items[i].name + 6);
+ if (i != sMed.selectedIdx && strncasecmp(sMed.items[i].name, MED_SEP_PREFIX, MED_SEP_PREFIX_LEN) == 0) {
+ int32_t n = atoi(sMed.items[i].name + MED_SEP_PREFIX_LEN);
if (n >= sepNum) {
sepNum = n + 1;
@@ -143,13 +156,10 @@ static void applyFields(void) {
}
}
- snprintf(autoName, DSGN_MAX_NAME, "mnuSep%d", (int)sepNum);
+ snprintf(autoName, DSGN_MAX_NAME, MED_SEP_PREFIX "%d", (int)sepNum);
} else {
// Normal item: strip & and non-alphanumeric, prefix "mnu"
- int32_t p = 0;
- autoName[p++] = 'm';
- autoName[p++] = 'n';
- autoName[p++] = 'u';
+ int32_t p = snprintf(autoName, DSGN_MAX_NAME, "%s", MED_NAME_PREFIX);
for (const char *c = mi->caption; *c && p < DSGN_MAX_NAME - 1; c++) {
if (*c == '&') {
@@ -174,7 +184,7 @@ static void applyFields(void) {
mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb);
// Popup checkbox only meaningful on top-level items; for nested
// items `visible` stays true (the field is ignored there).
- if (mi->level == 0 && sMed.popupCb) {
+ if (mi->level == 0) {
mi->visible = !wgtCheckboxIsChecked(sMed.popupCb);
} else {
mi->visible = true;
@@ -208,10 +218,8 @@ static void loadFields(void) {
wgtCheckboxSetChecked(sMed.checkedCb, false);
wgtCheckboxSetChecked(sMed.radioCheckCb, false);
wgtCheckboxSetChecked(sMed.enabledCb, true);
- if (sMed.popupCb) {
- wgtCheckboxSetChecked(sMed.popupCb, false);
- wgtSetEnabled(sMed.popupCb, false);
- }
+ wgtCheckboxSetChecked(sMed.popupCb, false);
+ wgtSetEnabled(sMed.popupCb, false);
sMed.nameAutoGen = true; // new blank item -- auto-gen eligible
return;
}
@@ -223,11 +231,10 @@ static void loadFields(void) {
wgtCheckboxSetChecked(sMed.checkedCb, mi->checked);
wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck);
wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled);
- if (sMed.popupCb) {
- // Popup (Visible=False) only applies to top-level menus.
- wgtCheckboxSetChecked(sMed.popupCb, mi->level == 0 && !mi->visible);
- wgtSetEnabled(sMed.popupCb, mi->level == 0);
- }
+
+ // Popup (Visible=False) only applies to top-level menus.
+ wgtCheckboxSetChecked(sMed.popupCb, mi->level == 0 && !mi->visible);
+ wgtSetEnabled(sMed.popupCb, mi->level == 0);
}
@@ -247,14 +254,12 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
// If empty, start with one blank item so the user can type immediately
if (arrlen(sMed.items) == 0) {
DsgnMenuItemT mi;
- memset(&mi, 0, sizeof(mi));
- mi.enabled = true;
- mi.visible = true;
+ dsgnMenuItemInit(&mi);
arrput(sMed.items, mi);
}
// Create modal dialog
- WindowT *win = dvxCreateWindowCentered(ctx, "Menu Editor", 360, 420, false);
+ WindowT *win = dvxCreateWindowCentered(ctx, MED_DLG_TITLE, MED_DLG_W, MED_DLG_H, false);
if (!win) {
arrfree(sMed.items);
@@ -265,29 +270,30 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
win->maxH = win->h;
WidgetT *root = wgtInitWindow(ctx, win);
- root->spacing = wgtPixels(4);
+ root->spacing = wgtPixels(MED_ROW_SPACING);
- // Caption row
+ // Caption row. Text inputs hold maxLen characters, so pass one less
+ // than the field size to leave room for the terminator.
WidgetT *capRow = wgtHBox(root);
- capRow->spacing = wgtPixels(4);
+ capRow->spacing = wgtPixels(MED_ROW_SPACING);
WidgetT *capLbl = wgtLabel(capRow, "Caption:");
- capLbl->minW = wgtPixels(60);
- sMed.captionInput = wgtTextInput(capRow, DSGN_MAX_TEXT);
+ capLbl->minW = wgtPixels(MED_LABEL_W);
+ sMed.captionInput = wgtTextInput(capRow, DSGN_MAX_TEXT - 1);
sMed.captionInput->weight = WGT_WEIGHT_FILL;
- sMed.captionInput->onChange = onCaptionChange;
+ sMed.captionInput->onChange = onCaptionChange;
// Name row
WidgetT *namRow = wgtHBox(root);
- namRow->spacing = wgtPixels(4);
+ namRow->spacing = wgtPixels(MED_ROW_SPACING);
WidgetT *namLbl = wgtLabel(namRow, "Name:");
- namLbl->minW = wgtPixels(60);
- sMed.nameInput = wgtTextInput(namRow, DSGN_MAX_NAME);
+ namLbl->minW = wgtPixels(MED_LABEL_W);
+ sMed.nameInput = wgtTextInput(namRow, DSGN_MAX_NAME - 1);
sMed.nameInput->weight = WGT_WEIGHT_FILL;
- sMed.nameInput->onChange = onNameChange;
+ sMed.nameInput->onChange = onNameChange;
// Check row
WidgetT *chkRow = wgtHBox(root);
- chkRow->spacing = wgtPixels(12);
+ chkRow->spacing = wgtPixels(MED_CHECK_SPACING);
sMed.checkedCb = wgtCheckbox(chkRow, "Checked");
sMed.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck");
sMed.enabledCb = wgtCheckbox(chkRow, "Enabled");
@@ -301,27 +307,27 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
// Arrow buttons
WidgetT *arrowRow = wgtHBox(root);
- arrowRow->spacing = wgtPixels(4);
+ arrowRow->spacing = wgtPixels(MED_ROW_SPACING);
WidgetT *btnOut = wgtButton(arrowRow, "<-");
btnOut->onClick = onOutdent;
- btnOut->minW = wgtPixels(32);
+ btnOut->minW = wgtPixels(MED_ARROW_BTN_W);
WidgetT *btnIn = wgtButton(arrowRow, "->");
btnIn->onClick = onIndent;
- btnIn->minW = wgtPixels(32);
+ btnIn->minW = wgtPixels(MED_ARROW_BTN_W);
WidgetT *btnUp = wgtButton(arrowRow, "Up");
btnUp->onClick = onMoveUp;
- btnUp->minW = wgtPixels(32);
+ btnUp->minW = wgtPixels(MED_ARROW_BTN_W);
WidgetT *btnDn = wgtButton(arrowRow, "Dn");
btnDn->onClick = onMoveDown;
- btnDn->minW = wgtPixels(32);
+ btnDn->minW = wgtPixels(MED_ARROW_BTN_W);
// Action buttons
WidgetT *actRow = wgtHBox(root);
- actRow->spacing = wgtPixels(4);
+ actRow->spacing = wgtPixels(MED_ROW_SPACING);
WidgetT *btnNext = wgtButton(actRow, "&Next");
btnNext->onClick = onNext;
@@ -334,15 +340,15 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
// OK / Cancel
WidgetT *okRow = wgtHBox(root);
- okRow->spacing = wgtPixels(8);
+ okRow->spacing = wgtPixels(MED_OK_SPACING);
WidgetT *btnOk = wgtButton(okRow, "OK");
btnOk->onClick = onOk;
- btnOk->minW = wgtPixels(60);
+ btnOk->minW = wgtPixels(MED_OK_BTN_W);
WidgetT *btnCancel = wgtButton(okRow, "Cancel");
btnCancel->onClick = onCancel;
- btnCancel->minW = wgtPixels(60);
+ btnCancel->minW = wgtPixels(MED_OK_BTN_W);
// Populate
rebuildList();
@@ -463,9 +469,7 @@ static void onInsert(WidgetT *w) {
applyFields();
DsgnMenuItemT mi;
- memset(&mi, 0, sizeof(mi));
- mi.enabled = true;
- mi.visible = true;
+ dsgnMenuItemInit(&mi);
// Insert after the current item's subtree, at the same level
int32_t insertAt;
@@ -620,9 +624,7 @@ static void onNext(WidgetT *w) {
} else {
// Append new item
DsgnMenuItemT mi;
- memset(&mi, 0, sizeof(mi));
- mi.enabled = true;
- mi.visible = true;
+ dsgnMenuItemInit(&mi);
if (count > 0) {
mi.level = sMed.items[count - 1].level;
@@ -649,33 +651,48 @@ static void onOk(WidgetT *w) {
}
}
- // Validate: check names are non-empty and unique
- int32_t count = (int32_t)arrlen(sMed.items);
+ // Validate: every name is an identifier, unique among the menu items,
+ // and not the name of the form or one of its controls. A name with
+ // spaces would be written as "Begin Menu File Menu" and mis-parsed.
+ int32_t count = (int32_t)arrlen(sMed.items);
+ int32_t badIdx = -1;
+ const char *reason = NULL;
+ char msg[MED_MSG_BUF];
- for (int32_t i = 0; i < count; i++) {
- if (sMed.items[i].name[0] == '\0') {
- dvxErrorBox(sMed.ctx, "Menu Editor", "All menu items must have a Name.");
- sMed.selectedIdx = i;
- rebuildList();
- loadFields();
- wgtSetFocused(sMed.captionInput);
- return;
+ for (int32_t i = 0; i < count && badIdx < 0; i++) {
+ const char *name = sMed.items[i].name;
+
+ if (name[0] == '\0') {
+ badIdx = i;
+ reason = "All menu items must have a Name.";
+ } else if (!basIsValidIdent(name)) {
+ badIdx = i;
+ snprintf(msg, sizeof(msg), "Menu name must be an identifier (letters, digits, underscores): %s", name);
+ reason = msg;
+ } else if (dsgnNameInUse(sMed.form, name, NULL, false)) {
+ badIdx = i;
+ snprintf(msg, sizeof(msg), "Menu name is already used by the form or a control: %s", name);
+ reason = msg;
}
- for (int32_t j = i + 1; j < count; j++) {
- if (strcasecmp(sMed.items[i].name, sMed.items[j].name) == 0) {
- char msg[MED_MSG_BUF];
- snprintf(msg, sizeof(msg), "Duplicate menu name: %s", sMed.items[i].name);
- dvxErrorBox(sMed.ctx, "Menu Editor", msg);
- sMed.selectedIdx = j;
- rebuildList();
- loadFields();
- wgtSetFocused(sMed.captionInput);
- return;
+ for (int32_t j = i + 1; j < count && badIdx < 0; j++) {
+ if (strcasecmp(name, sMed.items[j].name) == 0) {
+ badIdx = j;
+ snprintf(msg, sizeof(msg), "Duplicate menu name: %s", name);
+ reason = msg;
}
}
}
+ if (badIdx >= 0) {
+ dvxErrorBox(sMed.ctx, MED_DLG_TITLE, reason);
+ sMed.selectedIdx = badIdx;
+ rebuildList();
+ loadFields();
+ wgtSetFocused(sMed.captionInput);
+ return;
+ }
+
// Copy working items back to form
arrfree(sMed.form->menuItems);
sMed.form->menuItems = NULL;
@@ -727,7 +744,7 @@ static void rebuildList(void) {
arrsetlen(sLabels, 0);
for (int32_t i = 0; i < count; i++) {
- char buf[DSGN_MAX_TEXT + 32];
+ char buf[MED_LABEL_BUF];
int32_t pos = 0;
buf[0] = '\0';
diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.c b/src/apps/kpunch/dvxbasic/ide/ideProject.c
index e1061ba..4fc5e61 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideProject.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideProject.c
@@ -71,7 +71,11 @@
#define PRJ_WIN_W 180
#define PRJ_WIN_H 300
-#define PRJ_COPY_BUF_SIZE 4096 // file-copy chunk
+#define PRJ_WIN_Y 250
+#define PRJ_ICON_SIZE 32 // project icon must be PRJ_ICON_SIZE square
+#define PRJ_MSG_BUF 128 // error-message scratch buffer
+#define PRJ_MODIFIED_SUFFIX " *" // tree label suffix for an unsaved file
+#define PRJ_PLACEHOLDER_BPP 4 // bytes per pixel of the 1x1 icon placeholder
#define PRJ_INI_KEY_LEN 16 // "FileNNN" INI key buffer size
#define PRJ_FILE_KEY_FMT "File%d" // INI key for an indexed module/form path
#define PRJ_COUNT_KEY "Count" // INI key for the file count in a section
@@ -82,6 +86,8 @@
#define PPD_BTN_W 70
#define PPD_BTN_H 24
#define PPD_DESC_H 60
+#define PPD_ROOT_SPACING 2
+#define PPD_ROW_SPACING 4
// ============================================================
// Module state
@@ -121,7 +127,7 @@ static struct {
static void onPrjWinClose(WindowT *win);
static void onTreeItemDblClick(WidgetT *w);
static void onTreeSelChanged(WidgetT *w);
-static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t maxLen);
+static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t fieldSize);
static void ppdLoadIconPreview(void);
static void ppdOnBrowseHelp(WidgetT *w);
static void ppdOnBrowseIcon(WidgetT *w);
@@ -162,14 +168,17 @@ static void onTreeSelChanged(WidgetT *w) {
}
-static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t maxLen) {
+// Label + text input row. fieldSize is the size of the char[] the value
+// is copied back into; the input accepts one character less so the copy
+// never truncates.
+static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t fieldSize) {
WidgetT *row = wgtHBox(parent);
- row->spacing = wgtPixels(4);
+ row->spacing = wgtPixels(PPD_ROW_SPACING);
WidgetT *lbl = wgtLabel(row, labelText);
lbl->minW = wgtPixels(PPD_LABEL_W);
- WidgetT *input = wgtTextInput(row, maxLen);
+ WidgetT *input = wgtTextInput(row, fieldSize - 1);
input->weight = WGT_WEIGHT_FILL;
wgtSetText(input, value);
@@ -217,7 +226,7 @@ static void ppdOnBrowseHelp(WidgetT *w) {
char path[DVX_MAX_PATH];
- if (!dvxFileDialog(sPpd.ctx, "Select Help File", FD_SAVE, NULL, filters, 2, path, sizeof(path))) {
+ if (!dvxFileDialog(sPpd.ctx, "Select Help File", FD_SAVE, NULL, filters, (int32_t)(sizeof(filters) / sizeof(filters[0])), path, sizeof(path))) {
return;
}
@@ -238,7 +247,7 @@ static void ppdOnBrowseIcon(WidgetT *w) {
char path[DVX_MAX_PATH];
- if (dvxFileDialog(sPpd.ctx, "Select Icon", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
+ if (dvxFileDialog(sPpd.ctx, "Select Icon", FD_OPEN, NULL, filters, (int32_t)(sizeof(filters) / sizeof(filters[0])), path, sizeof(path))) {
if (!validateIcon(path, true)) {
return;
}
@@ -268,7 +277,7 @@ static void ppdOnBrowseIcon(WidgetT *w) {
if (existing) {
fclose(existing);
- char msg[DVX_MAX_PATH + 32];
+ char msg[DVX_MAX_PATH + PRJ_MSG_BUF];
snprintf(msg, sizeof(msg), "%s already exists.\nOverwrite it?", fname);
int32_t ow = dvxMessageBox(sPpd.ctx, "Overwrite", msg, MB_YESNO | MB_ICONQUESTION);
@@ -277,42 +286,7 @@ static void ppdOnBrowseIcon(WidgetT *w) {
}
}
- // Copy the file
- FILE *src = fopen(path, "rb");
-
- if (!src) {
- dvxErrorBox(sPpd.ctx, NULL, "Could not read source file.");
- return;
- }
-
- FILE *dst = fopen(destPath, "wb");
-
- if (!dst) {
- fclose(src);
- dvxErrorBox(sPpd.ctx, NULL, "Could not write to project directory.");
- return;
- }
-
- char buf[PRJ_COPY_BUF_SIZE];
- size_t n;
- bool copyOk = true;
-
- while ((n = fread(buf, 1, sizeof(buf), src)) > 0) {
- if (fwrite(buf, 1, n, dst) != n) {
- copyOk = false;
- break;
- }
- }
-
- if (ferror(src)) {
- copyOk = false;
- }
-
- fclose(src);
- fclose(dst);
-
- if (!copyOk) {
- remove(destPath);
+ if (!platformCopyFile(path, destPath)) {
dvxErrorBox(sPpd.ctx, NULL, "Failed to copy icon into the project directory.");
return;
}
@@ -386,7 +360,7 @@ WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onCli
sOnClick = onClick;
sOnSelChange = onSelChange;
- sPrjWin = dvxCreateWindow(ctx, "Project", 0, 250, PRJ_WIN_W, PRJ_WIN_H, true);
+ sPrjWin = dvxCreateWindow(ctx, "Project", 0, PRJ_WIN_Y, PRJ_WIN_W, PRJ_WIN_H, true);
if (!sPrjWin) {
return NULL;
@@ -434,10 +408,11 @@ void prjDestroyWindow(AppContextT *ctx, WindowT *win) {
sLabels = NULL;
}
- sPrjWin = NULL;
- sTree = NULL;
- sPrj = NULL;
- sOnClick = NULL;
+ sPrjWin = NULL;
+ sTree = NULL;
+ sPrj = NULL;
+ sOnClick = NULL;
+ sOnSelChange = NULL;
}
@@ -479,7 +454,9 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
return false;
}
- prjInit(prj);
+ // Release whatever project was open (file buffers, arrays) before
+ // starting from a blank state.
+ prjClose(prj);
snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath);
// Derive project directory
@@ -551,7 +528,7 @@ void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
// Extract form name from .frm files
if (prj->files[i].isForm) {
- basExtractFormName(buf, prj->files[i].formName, PRJ_MAX_NAME);
+ basExtractFormName(buf, prj->files[i].formName, sizeof(prj->files[i].formName));
}
// Yield between files to keep the UI responsive
@@ -618,12 +595,12 @@ void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandle
// Apply defaults from preferences
if (prefs) {
- snprintf(prj->author, sizeof(prj->author), "%s", prefsGetString(prefs, "defaults", "author", ""));
- snprintf(prj->publisher, sizeof(prj->publisher), "%s", prefsGetString(prefs, "defaults", "publisher", ""));
- snprintf(prj->version, sizeof(prj->version), "%s", prefsGetString(prefs, "defaults", "version", "1.0"));
- snprintf(prj->copyright, sizeof(prj->copyright), "%s", prefsGetString(prefs, "defaults", "copyright", ""));
- snprintf(prj->description, sizeof(prj->description), "%s", prefsGetString(prefs, "defaults", "description", ""));
- prj->optionExplicit = prefsGetBool(prefs, "editor", "optionExplicit", false);
+ snprintf(prj->author, sizeof(prj->author), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_AUTHOR, ""));
+ snprintf(prj->publisher, sizeof(prj->publisher), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_PUBLISHER, ""));
+ snprintf(prj->version, sizeof(prj->version), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_VERSION, IDE_PREF_DEFAULT_VERSION));
+ snprintf(prj->copyright, sizeof(prj->copyright), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_COPYRIGHT, ""));
+ snprintf(prj->description, sizeof(prj->description), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_DESCRIPTION, ""));
+ prj->optionExplicit = prefsGetBool(prefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_OPTION_EXPLICIT, false);
}
}
@@ -657,18 +634,18 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
return false;
}
- root->spacing = wgtPixels(2);
+ root->spacing = wgtPixels(PPD_ROOT_SPACING);
- sPpd.name = ppdAddRow(root, "Name:", prj->name, PRJ_MAX_NAME);
- sPpd.author = ppdAddRow(root, "Author:", prj->author, PRJ_MAX_STRING);
- sPpd.publisher = ppdAddRow(root, "Publisher:", prj->publisher, PRJ_MAX_STRING);
- sPpd.version = ppdAddRow(root, "Version:", prj->version, PRJ_MAX_NAME);
- sPpd.copyright = ppdAddRow(root, "Copyright:", prj->copyright, PRJ_MAX_STRING);
+ sPpd.name = ppdAddRow(root, "Name:", prj->name, sizeof(prj->name));
+ sPpd.author = ppdAddRow(root, "Author:", prj->author, sizeof(prj->author));
+ sPpd.publisher = ppdAddRow(root, "Publisher:", prj->publisher, sizeof(prj->publisher));
+ sPpd.version = ppdAddRow(root, "Version:", prj->version, sizeof(prj->version));
+ sPpd.copyright = ppdAddRow(root, "Copyright:", prj->copyright, sizeof(prj->copyright));
// Startup form dropdown
{
WidgetT *sfRow = wgtHBox(root);
- sfRow->spacing = wgtPixels(4);
+ sfRow->spacing = wgtPixels(PPD_ROW_SPACING);
WidgetT *sfLbl = wgtLabel(sfRow, "Startup Form:");
sfLbl->minW = wgtPixels(PPD_LABEL_W);
@@ -714,7 +691,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
// Icon row: label + preview + Browse button
{
WidgetT *iconRow = wgtHBox(root);
- iconRow->spacing = wgtPixels(4);
+ iconRow->spacing = wgtPixels(PPD_ROW_SPACING);
WidgetT *iconLbl = wgtLabel(iconRow, "Icon:");
iconLbl->minW = wgtPixels(PPD_LABEL_W);
@@ -728,8 +705,8 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
if (noIconData) {
sPpd.iconPreview = wgtImage(iconRow, noIconData, niW, niH, niP);
} else {
- uint8_t *placeholder = (uint8_t *)calloc(4, 1);
- sPpd.iconPreview = wgtImage(iconRow, placeholder, 1, 1, 4);
+ uint8_t *placeholder = (uint8_t *)calloc(PRJ_PLACEHOLDER_BPP, 1);
+ sPpd.iconPreview = wgtImage(iconRow, placeholder, 1, 1, PRJ_PLACEHOLDER_BPP);
}
WidgetT *browseBtn = wgtButton(iconRow, "Browse...");
@@ -742,12 +719,12 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
// Help file row
{
WidgetT *hlpRow = wgtHBox(root);
- hlpRow->spacing = wgtPixels(4);
+ hlpRow->spacing = wgtPixels(PPD_ROW_SPACING);
WidgetT *hlpLbl = wgtLabel(hlpRow, "Help File:");
hlpLbl->minW = wgtPixels(PPD_LABEL_W);
- sPpd.helpFileInput = wgtTextInput(hlpRow, DVX_MAX_PATH);
+ sPpd.helpFileInput = wgtTextInput(hlpRow, (int32_t)sizeof(prj->helpFile) - 1);
sPpd.helpFileInput->weight = WGT_WEIGHT_FILL;
wgtSetText(sPpd.helpFileInput, prj->helpFile);
@@ -761,7 +738,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
// Description: label above, textarea below (matches Preferences layout)
wgtLabel(root, "Description:");
- sPpd.description = wgtTextArea(root, PRJ_MAX_DESC);
+ sPpd.description = wgtTextArea(root, (int32_t)sizeof(prj->description) - 1);
sPpd.description->weight = WGT_WEIGHT_FILL;
sPpd.description->minH = wgtPixels(PPD_DESC_H);
wgtSetText(sPpd.description, prj->description);
@@ -886,8 +863,8 @@ void prjRebuildTree(PrjStateT *prj) {
wgtTreeItemSetExpanded(modsNode, true);
for (int32_t i = 0; i < prj->fileCount; i++) {
- char buf[DVX_MAX_PATH + 4];
- snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? " *" : "");
+ char buf[DVX_MAX_PATH + sizeof(PRJ_MODIFIED_SUFFIX)];
+ snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? PRJ_MODIFIED_SUFFIX : "");
char *label = strdup(buf);
arrput(sLabels, label);
WidgetT *item = wgtTreeItem(prj->files[i].isForm ? formsNode : modsNode, label);
@@ -955,16 +932,6 @@ bool prjSave(const PrjStateT *prj) {
}
-bool prjSaveAs(PrjStateT *prj, const char *dbpPath) {
- snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath);
-
- // Update project directory
- prjDeriveDir(prj, dbpPath);
-
- return prjSave(prj);
-}
-
-
// Write the project files whose isForm flag matches into an INI section as
// File0, File1, ... plus a Count entry.
static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm) {
@@ -982,7 +949,7 @@ static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char
}
-// validateIcon -- check that an image file is a valid 32x32 icon.
+// validateIcon -- check that an image file is a valid PRJ_ICON_SIZE square icon.
// Returns true if valid. Shows an error dialog and returns false if not.
static bool validateIcon(const char *fullPath, bool showErrors) {
int32_t infoW = 0;
@@ -995,10 +962,10 @@ static bool validateIcon(const char *fullPath, bool showErrors) {
return false;
}
- if (infoW != 32 || infoH != 32) {
+ if (infoW != PRJ_ICON_SIZE || infoH != PRJ_ICON_SIZE) {
if (showErrors) {
- char msg[128];
- snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH);
+ char msg[PRJ_MSG_BUF];
+ snprintf(msg, sizeof(msg), "Icon must be %dx%d pixels.\nThis image is %dx%d.", PRJ_ICON_SIZE, PRJ_ICON_SIZE, (int)infoW, (int)infoH);
dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING);
}
return false;
diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.h b/src/apps/kpunch/dvxbasic/ide/ideProject.h
index 59a8b56..9819f1a 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideProject.h
+++ b/src/apps/kpunch/dvxbasic/ide/ideProject.h
@@ -28,6 +28,7 @@
#include "dvxApp.h"
#include "dvxPrefs.h"
#include "dvxTypes.h"
+#include "../formrt/formrt.h"
#include
#include
@@ -40,6 +41,18 @@
#define PRJ_MAX_STRING 128
#define PRJ_MAX_DESC 512
+// IDE preference sections and keys shared by the project code and the
+// IDE preferences dialog (ideMain.c).
+#define IDE_PREF_SECTION_DEFAULTS "defaults"
+#define IDE_PREF_KEY_AUTHOR "author"
+#define IDE_PREF_KEY_PUBLISHER "publisher"
+#define IDE_PREF_KEY_VERSION "version"
+#define IDE_PREF_KEY_COPYRIGHT "copyright"
+#define IDE_PREF_KEY_DESCRIPTION "description"
+#define IDE_PREF_DEFAULT_VERSION "1.0"
+#define IDE_PREF_SECTION_EDITOR "editor"
+#define IDE_PREF_KEY_OPTION_EXPLICIT "optionExplicit"
+
// ============================================================
// Project file entry
// ============================================================
@@ -48,7 +61,7 @@ typedef struct {
// Fields are ordered by alignment to minimize struct padding.
char *buffer; // in-memory edit buffer (malloc'd, NULL = not loaded)
char path[DVX_MAX_PATH]; // relative path (8.3 DOS name)
- char formName[PRJ_MAX_NAME]; // form object name (from "Begin Form ")
+ char formName[BAS_MAX_IDENT]; // form object name (from "Begin Form ")
bool isForm; // true = .frm, false = .bas
bool modified; // true = buffer has unsaved changes
} PrjFileT;
@@ -77,7 +90,7 @@ typedef struct {
char name[PRJ_MAX_NAME];
char projectPath[DVX_MAX_PATH]; // full path to .dbp file
char projectDir[DVX_MAX_PATH]; // directory containing .dbp
- char startupForm[PRJ_MAX_NAME];
+ char startupForm[BAS_MAX_IDENT];
// Project metadata (for binary generation)
char author[PRJ_MAX_STRING];
char publisher[PRJ_MAX_STRING];
@@ -97,7 +110,6 @@ typedef struct {
void prjInit(PrjStateT *prj);
bool prjLoad(PrjStateT *prj, const char *dbpPath);
bool prjSave(const PrjStateT *prj);
-bool prjSaveAs(PrjStateT *prj, const char *dbpPath);
void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandleT *prefs);
void prjClose(PrjStateT *prj);
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm);
diff --git a/src/apps/kpunch/dvxbasic/ide/ideProperties.c b/src/apps/kpunch/dvxbasic/ide/ideProperties.c
index 581b043..4d45b8e 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideProperties.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideProperties.c
@@ -28,6 +28,7 @@
// property value to edit it via an InputBox dialog.
#include "ideProperties.h"
+#include "../formrt/formrt.h"
#include "../formrt/frmParser.h"
#include "dvxDlg.h"
#include "dvxWm.h"
@@ -48,6 +49,9 @@
#define PRP_WIN_W 220
#define PRP_WIN_H 400
+#define PRP_WIN_RIGHT_MARGIN 10 // gap between the window and the screen edge
+#define PRP_WIN_Y 30
+#define PRP_INT_BUF 32 // decimal int32_t text
#define PRP_QUERY_BUF 512 // SQL probe scratch buffer
#define PRP_PROMPT_BUF 128 // input-dialog prompt buffer
#define PRP_TITLE_SUFFIX_PAD 16 // pad over DSGN_MAX_TEXT for " [Design]" suffix
@@ -96,7 +100,15 @@ static char **sTreeLabels = NULL; // stb_ds array of strdup'd strings
#define PRP_CELL_COLUMNS 2 // property grid: name column + value column
#define PRP_MAX_LAYOUT_NAMES 32 // designer dropdown: max layout container types
#define PRP_MAX_DATA_NAMES 16 // designer dropdown: max Data controls (excludes "(none)")
-#define PRP_MAX_NEST_DEPTH 32 // container recursion cap; matches DSGN_MAX_NEST_DEPTH in ideDesigner.c
+#define PRP_SELECT_PREFIX "SELECT " // RecordSource that is already a query
+#define PRP_SELECT_PREFIX_LEN ((int32_t)(sizeof(PRP_SELECT_PREFIX) - 1))
+#define PRP_DIALOG_TITLE "Properties"
+
+// Tree parent recorded for a control by collectTreeOrder, parallel to the
+// collected control array.
+typedef struct {
+ char name[DSGN_MAX_NAME];
+} PrpParentT;
static char **sCellData = NULL; // stb_ds array of strdup'd strings
@@ -106,7 +118,7 @@ static char **sCellData = NULL; // stb_ds array of strdup'd strings
static void addPropRow(const char *name, const char *value);
static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled, int32_t depth);
-static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, const char *parentName);
+static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, PrpParentT **outParents, const char *parentName);
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index);
static void freeCellData(void);
static void freeTreeLabels(void);
@@ -121,6 +133,7 @@ static void parseTreeLabel(const char *label, char *outName, int32
static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const char *selectPrompt, char (*names)[DSGN_MAX_NAME], int32_t count, const char *curValue, char *newValue, int32_t newValueSize);
static void resolveDbPath(const char *dbName, char *out, int32_t outSize);
static bool treeOrderMatches(void);
+static bool validateNewName(const char *name, int32_t maxLen, const char *exceptName);
static void addPropRow(const char *name, const char *value) {
@@ -151,15 +164,17 @@ static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visib
// recursion always terminates.
if (dsgnIsContainer(child->typeName) &&
strcasecmp(child->name, parentName) != 0 &&
- depth < PRP_MAX_NEST_DEPTH) {
+ depth < DSGN_MAX_NEST_DEPTH) {
cascadeToChildren(ds, child->name, visible, enabled, depth + 1);
}
}
}
-// Walk tree items recursively, collecting control names in order.
-static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, const char *parentName) {
+// Walk tree items recursively, collecting controls in tree order together
+// with the tree parent each one sits under. The controls themselves are
+// not modified; the caller decides whether to apply the new parents.
+static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, PrpParentT **outParents, const char *parentName) {
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
const char *label = (const char *)item->userData;
@@ -174,12 +189,15 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t src
for (int32_t i = 0; i < srcCount; i++) {
if (strcmp(srcArr[i]->name, itemName) == 0 && srcArr[i]->index == itemIndex) {
- snprintf(srcArr[i]->parentName, DSGN_MAX_NAME, "%s", parentName);
+ PrpParentT tp;
+
+ snprintf(tp.name, DSGN_MAX_NAME, "%s", parentName);
arrput(*outArr, srcArr[i]);
+ arrput(*outParents, tp);
// Recurse into children (for containers)
if (item->firstChild) {
- collectTreeOrder(item, srcArr, srcCount, outArr, itemName);
+ collectTreeOrder(item, srcArr, srcCount, outArr, outParents, itemName);
}
break;
@@ -189,25 +207,6 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t src
}
-// Shared IDE-internal helper (declared in ideDesigner.h); also wrapped by
-// dsgnIfaceHasProp.
-const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName) {
- if (!typeName || !typeName[0]) {
- return NULL;
- }
-
- const char *wgtName = wgtFindByBasName(typeName);
-
- if (!wgtName) {
- return NULL;
- }
-
- const WgtIfaceT *iface = wgtGetIface(wgtName);
-
- return wgtIfaceFindProp(iface, propName);
-}
-
-
// Walk tree items recursively to find the one matching a control name.
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index) {
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
@@ -317,7 +316,7 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
// Query with LIMIT 0 to get column names without fetching rows
char query[PRP_QUERY_BUF];
- if (strncasecmp(recSrc, "SELECT ", 7) == 0) {
+ if (strncasecmp(recSrc, PRP_SELECT_PREFIX, PRP_SELECT_PREFIX_LEN) == 0) {
snprintf(query, sizeof(query), "%s LIMIT 0", recSrc);
} else {
snprintf(query, sizeof(query), "SELECT * FROM %s LIMIT 0", recSrc);
@@ -363,34 +362,22 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
}
-// Determine the data type of a property by name. Checks built-in
-// properties first, then looks up the widget's interface descriptor.
-// Returns WGT_IFACE_STRING, WGT_IFACE_INT, or WGT_IFACE_BOOL.
+// Determine the editor type of a property by name. Designer-only rows
+// and the special editors come first, then the widget's own interface
+// (which wins over the runtime tables, matching setProp dispatch), then
+// the runtime form/control property tables for the rest.
static uint8_t getPropType(const char *propName, const char *typeName) {
- // Read-only properties
+ // Designer-only read-only rows
if (strcasecmp(propName, "Type") == 0) { return PROP_TYPE_READONLY; }
if (strcasecmp(propName, "Index") == 0) { return PROP_TYPE_READONLY; }
- if (strcasecmp(propName, "BOF") == 0) { return PROP_TYPE_READONLY; }
- if (strcasecmp(propName, "EOF") == 0) { return PROP_TYPE_READONLY; }
- // Known built-in types
- if (strcasecmp(propName, "Name") == 0) { return PROP_TYPE_STRING; }
- if (strcasecmp(propName, "Caption") == 0) { return PROP_TYPE_STRING; }
- if (strcasecmp(propName, "MinWidth") == 0) { return PROP_TYPE_INT; }
- if (strcasecmp(propName, "MinHeight") == 0) { return PROP_TYPE_INT; }
- if (strcasecmp(propName, "MaxWidth") == 0) { return PROP_TYPE_INT; }
- if (strcasecmp(propName, "MaxHeight") == 0) { return PROP_TYPE_INT; }
- if (strcasecmp(propName, "Weight") == 0) { return PROP_TYPE_INT; }
- if (strcasecmp(propName, "Left") == 0) { return PROP_TYPE_INT; }
- if (strcasecmp(propName, "Top") == 0) { return PROP_TYPE_INT; }
- if (strcasecmp(propName, "AutoSize") == 0) { return PROP_TYPE_BOOL; }
- if (strcasecmp(propName, "Resizable") == 0) { return PROP_TYPE_BOOL; }
- if (strcasecmp(propName, "Centered") == 0) { return PROP_TYPE_BOOL; }
- if (strcasecmp(propName, "Visible") == 0) { return PROP_TYPE_BOOL; }
- if (strcasecmp(propName, "Enabled") == 0) { return PROP_TYPE_BOOL; }
- if (strcasecmp(propName, "Layout") == 0) { return PROP_TYPE_LAYOUT; }
- if (strcasecmp(propName, "HelpTopic") == 0) { return PROP_TYPE_STRING; }
- if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; }
+ // Designer-editable at design time even though the runtime rejects
+ // assignment (Name is the object identity, Layout the container type).
+ if (strcasecmp(propName, "Name") == 0) { return PROP_TYPE_STRING; }
+ if (strcasecmp(propName, "Layout") == 0) { return PROP_TYPE_LAYOUT; }
+
+ // Special editors
+ if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; }
if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; }
if (strcasecmp(propName, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; }
if (strcasecmp(propName, "KeyColumn") == 0) { return PROP_TYPE_DATAFIELD; }
@@ -398,21 +385,23 @@ static uint8_t getPropType(const char *propName, const char *typeName) {
if (strcasecmp(propName, "MasterField") == 0) { return PROP_TYPE_DATAFIELD; }
if (strcasecmp(propName, "DetailField") == 0) { return PROP_TYPE_DATAFIELD; }
- // Look up in the widget's interface descriptor
- if (typeName && typeName[0]) {
- const char *wgtName = wgtFindByBasName(typeName);
+ const WgtPropDescT *p = findIfaceProp(typeName, propName);
- if (wgtName) {
- const WgtIfaceT *iface = wgtGetIface(wgtName);
- const WgtPropDescT *p = wgtIfaceFindProp(iface, propName);
-
- if (p) {
- return p->type;
- }
- }
+ if (p) {
+ return p->setFn ? p->type : PROP_TYPE_READONLY;
}
- return PROP_TYPE_STRING;
+ // Runtime tables: the form object when no control is selected, else
+ // the common control properties. Non-writable entries are read-only.
+ const BasPropDescT *pd = typeName[0] ? basFormRtFindCommonProp(propName) : basFormRtFindFormProp(propName);
+
+ if (pd) {
+ return pd->writable ? pd->type : PROP_TYPE_READONLY;
+ }
+
+ // Designer storage fields not in the runtime tables (MinWidth alias
+ // Width etc. are; this covers any table drift) and unknown keys.
+ return dsgnFindIntProp(propName) ? PROP_TYPE_INT : PROP_TYPE_STRING;
}
@@ -533,6 +522,12 @@ static void onPropDblClick(WidgetT *w) {
} else {
DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx];
+ // A freshly placed container has no Layout entry yet; create
+ // the default so the grid row is editable.
+ if (!dsgnControlGetPropValue(ctrl, "Layout")) {
+ dsgnSetPropValue(ctrl, "Layout", DSGN_VBOX_LAYOUT);
+ }
+
for (int32_t pi = 0; pi < ctrl->propCount; pi++) {
if (strcasecmp(ctrl->props[pi].name, "Layout") == 0) {
layoutField = ctrl->props[pi].value;
@@ -574,19 +569,9 @@ static void onPropDblClick(WidgetT *w) {
// layoutField points at (it points at a selected container's
// Layout prop when one is selected). Per-container layouts are
// applied separately inside dsgnCreateWidgets.
- WidgetT *contentBox = dsgnCreateContentBox(root, sDs->form->layout);
-
- int32_t cc = (int32_t)arrlen(sDs->form->controls);
-
- for (int32_t ci = 0; ci < cc; ci++) {
- sDs->form->controls[ci]->widget = NULL;
- }
-
- dsgnCreateWidgets(sDs, contentBox);
-
- if (sDs->formWin) {
- dvxInvalidateWindow(sPrpCtx, sDs->formWin);
- }
+ sDs->form->contentBox = basFormRtCreateContentBox(root, sDs->form->layout);
+ dsgnRebuildWidgets(sDs);
+ dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
prpRefresh(sDs);
@@ -763,12 +748,16 @@ static void onPropDblClick(WidgetT *w) {
char oldName[DSGN_MAX_NAME];
snprintf(oldName, sizeof(oldName), "%s", ctrl->name);
+ if (!validateNewName(newValue, DSGN_MAX_NAME, oldName)) {
+ return;
+ }
+
// Rename all members of a control array, not just the selected one
for (int32_t i = 0; i < count; i++) {
DsgnControlT *c = sDs->form->controls[i];
if (strcasecmp(c->name, oldName) == 0) {
- snprintf(c->name, DSGN_MAX_NAME, DSGN_NAME_FMT, newValue);
+ snprintf(c->name, DSGN_MAX_NAME, "%s", newValue);
if (c->widget) {
wgtSetName(c->widget, c->name);
@@ -784,7 +773,7 @@ static void onPropDblClick(WidgetT *w) {
DsgnControlT *c = sDs->form->controls[i];
if (strcasecmp(c->parentName, oldName) == 0) {
- snprintf(c->parentName, DSGN_MAX_NAME, DSGN_NAME_FMT, newValue);
+ snprintf(c->parentName, DSGN_MAX_NAME, "%s", newValue);
}
}
@@ -806,36 +795,11 @@ static void onPropDblClick(WidgetT *w) {
ideRenameInCode(oldName, newValue);
prpRebuildTree(sDs);
- } else if (strcasecmp(propName, "MinWidth") == 0) {
- ctrl->width = atoi(newValue);
+ } else if (dsgnFindIntProp(propName)) {
+ const DsgnIntPropT *ip = dsgnFindIntProp(propName);
- if (ctrl->widget) {
- ctrl->widget->minW = wgtPixels(ctrl->width);
- }
- } else if (strcasecmp(propName, "MinHeight") == 0) {
- ctrl->height = atoi(newValue);
-
- if (ctrl->widget) {
- ctrl->widget->minH = wgtPixels(ctrl->height);
- }
- } else if (strcasecmp(propName, "MaxWidth") == 0) {
- ctrl->maxWidth = atoi(newValue);
-
- if (ctrl->widget) {
- ctrl->widget->maxW = ctrl->maxWidth > 0 ? wgtPixels(ctrl->maxWidth) : 0;
- }
- } else if (strcasecmp(propName, "MaxHeight") == 0) {
- ctrl->maxHeight = atoi(newValue);
-
- if (ctrl->widget) {
- ctrl->widget->maxH = ctrl->maxHeight > 0 ? wgtPixels(ctrl->maxHeight) : 0;
- }
- } else if (strcasecmp(propName, "Weight") == 0) {
- ctrl->weight = atoi(newValue);
-
- if (ctrl->widget) {
- ctrl->widget->weight = ctrl->weight;
- }
+ *(int32_t *)((char *)ctrl + ip->offset) = atoi(newValue);
+ dsgnSyncWidgetGeom(ctrl);
} else if (strcasecmp(propName, "Visible") == 0 && !dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
bool val = frmParseBool(newValue);
ctrl->visible = val;
@@ -874,43 +838,16 @@ static void onPropDblClick(WidgetT *w) {
const WgtPropDescT *p = wgtIfaceFindProp(iface, propName);
if (p && p->setFn) {
- if (p->type == WGT_IFACE_STRING) {
- // Strings must outlive this function, so the
- // ctrl->props[] copy is what we pass to setFn
- // (not the newValue buffer).
- bool found = false;
+ // props[] is the design-time store (it survives a
+ // widget rebuild and is what gets saved); the live
+ // widget mirrors it. Strings are passed from the
+ // props[] copy so they outlive this function.
+ dsgnSetPropValue(ctrl, propName, newValue);
- for (int32_t j = 0; j < ctrl->propCount; j++) {
- if (strcasecmp(ctrl->props[j].name, propName) == 0) {
- snprintf(ctrl->props[j].value, DSGN_MAX_TEXT, "%s", newValue);
- ((void (*)(WidgetT *, const char *))p->setFn)(ctrl->widget, ctrl->props[j].value);
- found = true;
- break;
- }
- }
+ const char *stored = dsgnControlGetPropValue(ctrl, propName);
- if (!found && ctrl->propCount < DSGN_MAX_PROPS) {
- snprintf(ctrl->props[ctrl->propCount].name, DSGN_MAX_NAME, "%s", propName);
- snprintf(ctrl->props[ctrl->propCount].value, DSGN_MAX_TEXT, "%s", newValue);
- ((void (*)(WidgetT *, const char *))p->setFn)(ctrl->widget, ctrl->props[ctrl->propCount].value);
- ctrl->propCount++;
- }
- } else {
- wgtApplyPropFromString(ctrl->widget, p, newValue);
-
- // Keep any ctrl->props[] copy loaded from the
- // .frm in sync. saveControls/prpRefresh prefer
- // the props[] value over the live widget when an
- // entry exists, so a stale copy would revert the
- // edit. Only update an existing entry; iface
- // props absent from props[] are persisted by
- // reading the live widget.
- for (int32_t j = 0; j < ctrl->propCount; j++) {
- if (strcasecmp(ctrl->props[j].name, propName) == 0) {
- snprintf(ctrl->props[j].value, DSGN_MAX_TEXT, "%s", newValue);
- break;
- }
- }
+ if (stored) {
+ wgtApplyPropFromString(ctrl->widget, p, stored);
}
ifaceHandled = true;
@@ -921,29 +858,14 @@ static void onPropDblClick(WidgetT *w) {
if (!ifaceHandled) {
// Custom prop storage
- bool found = false;
-
- for (int32_t i = 0; i < ctrl->propCount; i++) {
- if (strcasecmp(ctrl->props[i].name, propName) == 0) {
- snprintf(ctrl->props[i].value, DSGN_MAX_TEXT, "%s", newValue);
- found = true;
- break;
- }
- }
-
- if (!found && ctrl->propCount < DSGN_MAX_PROPS) {
- snprintf(ctrl->props[ctrl->propCount].name, DSGN_MAX_NAME, "%s", propName);
- snprintf(ctrl->props[ctrl->propCount].value, DSGN_MAX_TEXT, "%s", newValue);
- ctrl->propCount++;
- }
+ dsgnSetPropValue(ctrl, propName, newValue);
// Update widget text from the persistent props array
if (ctrl->widget && (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0)) {
- for (int32_t i = 0; i < ctrl->propCount; i++) {
- if (strcasecmp(ctrl->props[i].name, propName) == 0) {
- wgtSetText(ctrl->widget, ctrl->props[i].value);
- break;
- }
+ const char *stored = dsgnControlGetPropValue(ctrl, propName);
+
+ if (stored) {
+ wgtSetText(ctrl->widget, stored);
}
}
}
@@ -956,19 +878,14 @@ static void onPropDblClick(WidgetT *w) {
}
} else {
if (strcasecmp(propName, "Name") == 0) {
- char oldName[DSGN_MAX_NAME];
+ char oldName[BAS_MAX_IDENT];
snprintf(oldName, sizeof(oldName), "%s", sDs->form->name);
- // Length-clamped memcpy instead of strncpy/snprintf because
- // GCC warns about both when source exceeds the buffer.
- int32_t nl = (int32_t)strlen(newValue);
-
- if (nl >= DSGN_MAX_NAME) {
- nl = DSGN_MAX_NAME - 1;
+ if (!validateNewName(newValue, BAS_MAX_IDENT, oldName)) {
+ return;
}
- memcpy(sDs->form->name, newValue, nl);
- sDs->form->name[nl] = '\0';
+ snprintf(sDs->form->name, sizeof(sDs->form->name), "%s", newValue);
ideRenameInCode(oldName, sDs->form->name);
prpRebuildTree(sDs);
} else if (strcasecmp(propName, "Caption") == 0) {
@@ -1047,39 +964,57 @@ static void onTreeChange(WidgetT *w) {
}
// Actual reorder happened -- rebuild the controls array from tree order.
- int32_t count = (int32_t)arrlen(sDs->form->controls);
- DsgnControlT **newArr = NULL;
- WidgetT *formItem = sTree->firstChild;
+ int32_t count = (int32_t)arrlen(sDs->form->controls);
+ DsgnControlT **newArr = NULL;
+ PrpParentT *newParents = NULL;
+ WidgetT *formItem = sTree->firstChild;
if (!formItem) {
return;
}
- collectTreeOrder(formItem, sDs->form->controls, count, &newArr, "");
+ collectTreeOrder(formItem, sDs->form->controls, count, &newArr, &newParents, "");
- // If we lost items (dragged above form), revert
- if ((int32_t)arrlen(newArr) != count) {
+ // Revert if items were lost (dragged above the form) or a control was
+ // dropped into something that cannot hold children: only containers
+ // are written with nested blocks, so a non-container parent would drop
+ // the control from the saved .frm.
+ bool valid = ((int32_t)arrlen(newArr) == count);
+
+ for (int32_t i = 0; valid && i < count; i++) {
+ const char *pName = newParents[i].name;
+
+ if (pName[0] == '\0') {
+ continue;
+ }
+
+ valid = false;
+
+ for (int32_t j = 0; j < count; j++) {
+ if (strcasecmp(newArr[j]->name, pName) == 0) {
+ valid = dsgnIsContainer(newArr[j]->typeName);
+ break;
+ }
+ }
+ }
+
+ if (!valid) {
arrfree(newArr);
+ arrfree(newParents);
prpRebuildTree(sDs);
return;
}
- arrfree(sDs->form->controls);
- sDs->form->controls = newArr;
- sDs->form->dirty = true;
-
- if (sDs->form->contentBox) {
- wgtDestroyChildren(sDs->form->contentBox);
-
- int32_t newCount = (int32_t)arrlen(sDs->form->controls);
-
- for (int32_t i = 0; i < newCount; i++) {
- sDs->form->controls[i]->widget = NULL;
- }
-
- dsgnCreateWidgets(sDs, sDs->form->contentBox);
+ for (int32_t i = 0; i < count; i++) {
+ snprintf(newArr[i]->parentName, DSGN_MAX_NAME, "%s", newParents[i].name);
}
+ arrfree(newParents);
+ arrfree(sDs->form->controls);
+ sDs->form->controls = newArr;
+ sDs->form->dirty = true;
+
+ dsgnRebuildWidgets(sDs);
prpRebuildTree(sDs);
if (sDs->formWin) {
@@ -1220,8 +1155,8 @@ WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) {
sDs = ds;
sPrpCtx = ctx;
- int32_t winX = ctx->display.width - PRP_WIN_W - 10;
- WindowT *win = dvxCreateWindow(ctx, "Properties", winX, 30, PRP_WIN_W, PRP_WIN_H, true);
+ int32_t winX = ctx->display.width - PRP_WIN_W - PRP_WIN_RIGHT_MARGIN;
+ WindowT *win = dvxCreateWindow(ctx, PRP_DIALOG_TITLE, winX, PRP_WIN_Y, PRP_WIN_W, PRP_WIN_H, true);
if (!win) {
return NULL;
@@ -1246,12 +1181,12 @@ WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) {
sPropList = wgtListView(splitter);
sPropList->onDblClick = onPropDblClick;
- static const ListViewColT cols[2] = {
+ static const ListViewColT cols[PRP_CELL_COLUMNS] = {
{ "Property", 0, ListViewAlignLeftE },
{ "Value", 0, ListViewAlignLeftE }
};
- wgtListViewSetColumns(sPropList, cols, 2);
+ wgtListViewSetColumns(sPropList, cols, PRP_CELL_COLUMNS);
prpRebuildTree(ds);
prpRefresh(ds);
@@ -1326,7 +1261,7 @@ void prpRebuildTree(DsgnStateT *ds) {
if (ctrl->parentName[0]) {
for (int32_t j = 0; j < i; j++) {
- if (strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0 && treeItems) {
+ if (strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0) {
treeParent = treeItems[j];
break;
}
@@ -1390,7 +1325,7 @@ void prpRefresh(DsgnStateT *ds) {
if (ds->selectedIdx >= 0 && ds->selectedIdx < count) {
DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
- char buf[32];
+ char buf[PRP_INT_BUF];
addPropRow("Name", ctrl->name);
@@ -1401,20 +1336,12 @@ void prpRefresh(DsgnStateT *ds) {
addPropRow("Type", ctrl->typeName);
- snprintf(buf, sizeof(buf), "%d", (int)ctrl->width);
- addPropRow("MinWidth", buf);
+ for (int32_t i = 0; dsgnIntPropAt(i); i++) {
+ const DsgnIntPropT *ip = dsgnIntPropAt(i);
- snprintf(buf, sizeof(buf), "%d", (int)ctrl->height);
- addPropRow("MinHeight", buf);
-
- snprintf(buf, sizeof(buf), "%d", (int)ctrl->maxWidth);
- addPropRow("MaxWidth", buf);
-
- snprintf(buf, sizeof(buf), "%d", (int)ctrl->maxHeight);
- addPropRow("MaxHeight", buf);
-
- snprintf(buf, sizeof(buf), "%d", (int)ctrl->weight);
- addPropRow("Weight", buf);
+ snprintf(buf, sizeof(buf), "%d", (int)*(const int32_t *)((const char *)ctrl + ip->offset));
+ addPropRow(ip->name, buf);
+ }
if (!dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
addPropRow("Visible", ctrl->visible ? "True" : "False");
@@ -1430,6 +1357,12 @@ void prpRefresh(DsgnStateT *ds) {
addPropRow(ctrl->props[i].name, ctrl->props[i].value);
}
+ // A container placed on the canvas has no Layout entry until the
+ // user picks one; show the default so the row is there to edit.
+ if (dsgnIsContainer(ctrl->typeName) && !dsgnControlGetPropValue(ctrl, "Layout")) {
+ addPropRow("Layout", DSGN_VBOX_LAYOUT);
+ }
+
// Widget interface properties (from the .wgt descriptor)
const char *wgtName = wgtFindByBasName(ctrl->typeName);
@@ -1474,28 +1407,19 @@ void prpRefresh(DsgnStateT *ds) {
}
}
} else {
- char buf[32];
+ // One row per runtime form property the designer stores, in
+ // table order; runtime-only ones (Visible, ContextMenu) have no
+ // designer field and are skipped.
+ int32_t formPropCount = 0;
+ const BasPropDescT *formProps = basFormRtFormProps(&formPropCount);
- addPropRow("Name", ds->form->name);
- addPropRow("Caption", ds->form->caption);
- addPropRow("Layout", ds->form->layout);
- addPropRow("AutoSize", ds->form->autoSize ? "True" : "False");
- addPropRow("Resizable", ds->form->resizable ? "True" : "False");
- addPropRow("Centered", ds->form->centered ? "True" : "False");
+ for (int32_t i = 0; i < formPropCount; i++) {
+ char valBuf[DSGN_MAX_TEXT];
- snprintf(buf, sizeof(buf), "%d", (int)ds->form->left);
- addPropRow("Left", buf);
-
- snprintf(buf, sizeof(buf), "%d", (int)ds->form->top);
- addPropRow("Top", buf);
-
- snprintf(buf, sizeof(buf), "%d", (int)ds->form->width);
- addPropRow("Width", buf);
-
- snprintf(buf, sizeof(buf), "%d", (int)ds->form->height);
- addPropRow("Height", buf);
-
- addPropRow("HelpTopic", ds->form->helpTopic);
+ if (dsgnFormPropValue(ds->form, formProps[i].name, valBuf, sizeof(valBuf))) {
+ addPropRow(formProps[i].name, valBuf);
+ }
+ }
}
wgtListViewSetData(sPropList, (const char **)sCellData, (int32_t)arrlen(sCellData) / PRP_CELL_COLUMNS);
@@ -1528,39 +1452,20 @@ static bool treeOrderMatches(void) {
return true;
}
- int32_t count = (int32_t)arrlen(sDs->form->controls);
+ int32_t count = (int32_t)arrlen(sDs->form->controls);
+ DsgnControlT **newArr = NULL;
+ PrpParentT *newParents = NULL;
- // collectTreeOrder rewrites each control's parentName from the tree
- // nesting as a side effect, and newArr holds pointers INTO the
- // controls array -- so a post-call 'newArr[i]->parentName vs
- // controls[i]->parentName' compares each string to itself and never
- // detects a reparent. Snapshot the parentNames first and compare
- // against that instead; order is detected via pointer identity
- // (tree DFS order vs model order).
- char (*oldParents)[DSGN_MAX_NAME] = NULL;
-
- if (count > 0) {
- oldParents = (char (*)[DSGN_MAX_NAME])malloc((size_t)count * DSGN_MAX_NAME);
-
- if (!oldParents) {
- return true;
- }
-
- for (int32_t i = 0; i < count; i++) {
- snprintf(oldParents[i], DSGN_MAX_NAME, "%s", sDs->form->controls[i]->parentName);
- }
- }
-
- DsgnControlT **newArr = NULL;
-
- collectTreeOrder(formItem, sDs->form->controls, count, &newArr, "");
+ // Order is detected via pointer identity (tree DFS order vs model
+ // order); a reparent shows up as a differing tree parent name.
+ collectTreeOrder(formItem, sDs->form->controls, count, &newArr, &newParents, "");
bool match = ((int32_t)arrlen(newArr) == count);
if (match) {
for (int32_t i = 0; i < count; i++) {
if (newArr[i] != sDs->form->controls[i] ||
- strcmp(sDs->form->controls[i]->parentName, oldParents[i]) != 0) {
+ strcasecmp(sDs->form->controls[i]->parentName, newParents[i].name) != 0) {
match = false;
break;
}
@@ -1568,6 +1473,33 @@ static bool treeOrderMatches(void) {
}
arrfree(newArr);
- free(oldParents);
+ arrfree(newParents);
return match;
}
+
+
+// Reject a control or form rename that is not a BASIC identifier, is too
+// long for a maxLen-byte field, or collides with another object on the form.
+// Shows the reason and returns false on rejection.
+static bool validateNewName(const char *name, int32_t maxLen, const char *exceptName) {
+ char msg[PRP_PROMPT_BUF];
+
+ if (!basIsValidIdent(name)) {
+ dvxErrorBox(sPrpCtx, PRP_DIALOG_TITLE, "Name must start with a letter or underscore and contain only letters, digits, and underscores.");
+ return false;
+ }
+
+ if ((int32_t)strlen(name) >= maxLen) {
+ snprintf(msg, sizeof(msg), "Name must be shorter than %d characters.", (int)maxLen);
+ dvxErrorBox(sPrpCtx, PRP_DIALOG_TITLE, msg);
+ return false;
+ }
+
+ if (dsgnNameInUse(sDs->form, name, exceptName, true)) {
+ snprintf(msg, sizeof(msg), "The name %s is already in use.", name);
+ dvxErrorBox(sPrpCtx, PRP_DIALOG_TITLE, msg);
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
index 47cb770..805d97b 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
@@ -43,9 +43,17 @@
// Constants
// ============================================================
-#define TBX_COLS 4
-#define TBX_WIN_W 120
-#define TBX_WIN_H 250
+#define TBX_COLS 4
+#define TBX_WIN_W 120
+#define TBX_WIN_H 250
+#define TBX_TOOLTIP_LEN 64 // tooltip text loaded from the .wgt "name" resource
+#define TBX_RES_NAME_LEN 32 // resource name scratch buffer
+
+// .wgt resource contract: a toolbox icon and display name per interface;
+// the second and later interfaces in one .wgt use a "-N" suffix.
+#define TBX_RES_ICON "icon24"
+#define TBX_RES_NAME "name"
+#define TBX_RES_SUFFIX_FMT "%s-%d"
// ============================================================
// Per-tool entry
@@ -53,7 +61,7 @@
typedef struct {
char typeName[DSGN_MAX_NAME];
- char tooltip[64];
+ char tooltip[TBX_TOOLTIP_LEN];
} TbxToolEntryT;
// ============================================================
@@ -139,15 +147,15 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
if (wgtPath) {
// Build suffixed resource names: "icon24", "icon24-2", etc.
- char iconResName[32];
- char nameResName[32];
+ char iconResName[TBX_RES_NAME_LEN];
+ char nameResName[TBX_RES_NAME_LEN];
if (pathIdx <= 1) {
- snprintf(iconResName, sizeof(iconResName), "icon24");
- snprintf(nameResName, sizeof(nameResName), "name");
+ snprintf(iconResName, sizeof(iconResName), "%s", TBX_RES_ICON);
+ snprintf(nameResName, sizeof(nameResName), "%s", TBX_RES_NAME);
} else {
- snprintf(iconResName, sizeof(iconResName), "icon24-%d", (int)pathIdx);
- snprintf(nameResName, sizeof(nameResName), "name-%d", (int)pathIdx);
+ snprintf(iconResName, sizeof(iconResName), TBX_RES_SUFFIX_FMT, TBX_RES_ICON, (int)pathIdx);
+ snprintf(nameResName, sizeof(nameResName), TBX_RES_SUFFIX_FMT, TBX_RES_NAME, (int)pathIdx);
}
iconData = dvxResLoadIcon(ctx, wgtPath, iconResName, &iconW, &iconH, &iconPitch);
@@ -162,7 +170,7 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
int32_t toolIdx = (int32_t)arrlen(sTbxTools) - 1;
// Start a new row every TBX_COLS buttons
- if (col == 0 || !row) {
+ if (col == 0) {
row = wgtHBox(root);
row->spacing = 0;
}
diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs
index 6665eb4..2526830 100644
--- a/src/apps/kpunch/dvxbasic/langref.dhs
+++ b/src/apps/kpunch/dvxbasic/langref.dhs
@@ -98,6 +98,17 @@ Both E and D can introduce an exponent (e.g. 1.5E10 and 2.5D3 are equivalent for
When mixing types in expressions, values are automatically promoted to a common type: Integer -> Long -> Single -> Double. Strings are not automatically converted to numbers (use VAL and STR$).
+.h2 Assignment Conversion
+
+Storing a value into a variable declared as Integer, Long or Single (by suffix, by AS clause, by DEFINT/DEFLNG/DEFSNG, or as a typed parameter, TYPE field or array element) converts the value to that type. Integer and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit. Single keeps about 7 significant digits. A variable with no declared type keeps whatever type the assigned value has.
+
+.code
+Dim n As Integer
+n = 3.7 ' n is 4
+n = 40000 ' Error 6: Overflow
+x = 3.7 ' x has no declared type and keeps 3.7
+.endcode
+
.h2 Boolean Values
Boolean values use -1 for True and 0 for False. Any non-zero numeric value is treated as True in a conditional context. The keywords True and False are reserved and may be used anywhere a Boolean value is expected.
@@ -288,7 +299,7 @@ Dim fixedStr As String * 20
.endcode
.note info
-DIM SHARED at module level makes a variable accessible from every procedure without passing it as a parameter. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls).
+DIM SHARED at module level makes a variable accessible from every procedure without passing it as a parameter. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A variable that is first used inside a SUB or FUNCTION without a DIM is also local to that procedure; to share it with module-level code or other procedures, declare it with DIM SHARED at module level.
.endnote
Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.
@@ -933,7 +944,7 @@ RESUME NEXT ' Continue at the next statement after the error
ERROR n ' Raise a runtime error with error number n
.endcode
-The ERR keyword returns the current error number in expressions (it is 0 when no error is active).
+The ERR keyword returns the current error number in expressions (it is 0 when no error is active). RESUME or RESUME NEXT executed while no error is active raises error 20 (RESUME without error).
.code
On Error GoTo ErrorHandler
@@ -952,16 +963,20 @@ ErrorHandler:
------ -------
1 FOR loop error (NEXT without FOR, NEXT variable mismatch, FOR stack underflow)
4 Out of DATA
+ 5 Illegal function call (bad argument to a built-in function)
+ 6 Overflow (value does not fit the target type)
7 Out of memory
9 Subscript out of range / invalid variable or field index
11 Division by zero
13 Type mismatch / not an array / not a TYPE instance
+ 20 RESUME without error
26 FOR loop nesting too deep
51 Internal error (bad opcode)
52 Bad file number or file not open
53 File not found
54 Bad file mode
58 File already exists or rename failed
+ 59 Bad record length (OPEN ... LEN must be 1 to 32767)
67 Too many files open
75 Path/file access error
76 Path not found
@@ -1045,7 +1060,7 @@ OPEN filename$ FOR BINARY AS #channel
INPUT Open for sequential reading. File must exist.
OUTPUT Open for sequential writing. Creates or truncates.
APPEND Open for sequential writing at end of file.
- RANDOM Open for random-access record I/O.
+ RANDOM Open for random-access record I/O. LEN sets the record size in bytes (default 128).
BINARY Open for raw binary I/O.
.endtable
@@ -1067,10 +1082,16 @@ PRINT #channel, expression
.h2 INPUT #
-Reads comma-delimited data from a file.
+Reads comma-delimited data from a file, one field per variable. Quoted strings written by WRITE # are read back without their quotes; numeric variables receive the converted value.
.code
-INPUT #channel, variable
+INPUT #channel, variable [, variable ...]
+.endcode
+
+.code
+Open "data.txt" For Input As #1
+Input #1, name$, age%, score#
+Close #1
.endcode
.h2 LINE INPUT #
@@ -1096,13 +1117,26 @@ Write #1, "Scott", 42, 3.14
.h2 GET / PUT
-Read and write records in RANDOM or BINARY mode files.
+Read and write records in RANDOM or BINARY mode files. When recordNum is omitted the transfer starts at the current file position.
.code
GET #channel, [recordNum], variable
PUT #channel, [recordNum], variable
.endcode
+In RANDOM mode recordNum is a 1-based record number and each record is LEN bytes (see OPEN). Numbers are stored in their binary form (Integer 2 bytes, Long 4, Single 4, Double 8); a String is stored as a 2-byte length followed by its characters.
+
+In BINARY mode recordNum is a 1-based byte position. Numbers are stored as in RANDOM mode. PUT writes the characters of a String with no length prefix, and GET reads as many bytes as the String variable currently holds, so set its length first (for example with SPACE$ or by declaring it as STRING * n).
+
+.code
+Dim buf As String
+Open "raw.bin" For Binary As #1
+buf = Space$(16)
+Get #1, 1, buf ' read bytes 1-16
+Put #1, 33, "TAG" ' write 3 bytes at position 33
+Close #1
+.endcode
+
.h2 SEEK
Sets the file position. As a function, returns the current position.
@@ -1276,25 +1310,27 @@ bytes = FILELEN(filename$)
CHR$(n) String Character with ASCII code n
FORMAT$(value, fmt$) String Formats a numeric value using a format string
HEX$(n) String Hexadecimal representation of n (uppercase, no leading &H)
- INSTR(s$, find$) Integer Position of find$ in s$ (1-based), 0 if not found
- INSTR(start, s$, find$) Integer Search starting at position start (1-based)
+ INSTR(s$, find$) Long Position of find$ in s$ (1-based), 0 if not found
+ INSTR(start, s$, find$) Long Search starting at position start (1-based)
LCASE$(s$) String Converts s$ to lowercase
LEFT$(s$, n) String Leftmost n characters of s$
- LEN(s$) Integer Length of s$ in characters
+ LEN(s$) Long Length of s$ in characters
LTRIM$(s$) String Removes leading spaces from s$
MID$(s$, start) String Substring from start (1-based) to end of string
MID$(s$, start, length) String Substring of length characters starting at start
OCT$(n) String Octal representation of n (no leading &O)
RIGHT$(s$, n) String Rightmost n characters of s$
RTRIM$(s$) String Removes trailing spaces from s$
- SPACE$(n) String String of n spaces
+ SPACE$(n) String String of n spaces (n up to 65535)
STR$(n) String Converts number n to string (leading space for non-negative)
- STRING$(n, char) String String of n copies of char (char can be an ASCII code or single-character string)
+ STRING$(n, char) String String of n copies of char (char can be an ASCII code or single-character string; n up to 65535)
TRIM$(s$) String Removes leading and trailing spaces from s$
UCASE$(s$) String Converts s$ to uppercase
VAL(s$) Double Converts string s$ to a numeric value; stops at first non-numeric character
.endtable
+A negative count or position passed to LEFT$, RIGHT$, MID$, SPACE$ or STRING$, or a count above 65535 passed to SPACE$ or STRING$, raises error 5 (Illegal function call).
+
.h2 FORMAT$
FORMAT$ formats a numeric value using a BASIC-style format string. The format characters are the same as the ones used by PRINT USING.
@@ -1407,8 +1443,8 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background
-------- ------- -----------
CBOOL(n) Boolean Returns True (-1) if n is nonzero or a non-empty string; False (0) otherwise
CDBL(n) Double Converts n to Double
- CINT(n) Integer Converts n to Integer (rounds half away from zero)
- CLNG(n) Long Converts n to Long
+ CINT(n) Integer Converts n to Integer; halves round to the nearest even number (2.5 -> 2, 3.5 -> 4); error 6 (Overflow) outside -32768 to 32767
+ CLNG(n) Long Converts n to Long with the same rounding; error 6 (Overflow) outside the Long range
CSNG(n) Single Converts n to Single
CSTR(n) String Converts n to its String representation
.endtable
diff --git a/src/apps/kpunch/dvxbasic/runtime/basErrors.h b/src/apps/kpunch/dvxbasic/runtime/basErrors.h
index 8c9791c..e793057 100644
--- a/src/apps/kpunch/dvxbasic/runtime/basErrors.h
+++ b/src/apps/kpunch/dvxbasic/runtime/basErrors.h
@@ -38,16 +38,19 @@
#define BAS_ERR_NEXT_WITHOUT_FOR 1
#define BAS_ERR_OUT_OF_DATA 4
#define BAS_ERR_ILLEGAL_FUNC_CALL 5
+#define BAS_ERR_OVERFLOW 6
#define BAS_ERR_OUT_OF_MEMORY 7
#define BAS_ERR_SUBSCRIPT_RANGE 9
#define BAS_ERR_DIV_BY_ZERO 11
#define BAS_ERR_TYPE_MISMATCH 13
+#define BAS_ERR_RESUME_WITHOUT_ERR 20
#define BAS_ERR_FOR_NESTING 26
#define BAS_ERR_BAD_OPCODE 51
#define BAS_ERR_BAD_FILE_NUM 52
#define BAS_ERR_FILE_NOT_FOUND 53
#define BAS_ERR_BAD_FILE_MODE 54
#define BAS_ERR_FILE_EXISTS 58
+#define BAS_ERR_BAD_RECORD_LEN 59
#define BAS_ERR_TOO_MANY_FILES 67
#define BAS_ERR_PATH_FILE_ACCESS 75
#define BAS_ERR_PATH_NOT_FOUND 76
diff --git a/src/apps/kpunch/dvxbasic/runtime/serialize.c b/src/apps/kpunch/dvxbasic/runtime/serialize.c
index ffb797f..46e50ad 100644
--- a/src/apps/kpunch/dvxbasic/runtime/serialize.c
+++ b/src/apps/kpunch/dvxbasic/runtime/serialize.c
@@ -48,7 +48,18 @@
// localCount int32
// returnType uint8
// isFunction uint8
-// nameLen uint16 + nameLen bytes
+// name uint16 len + bytes
+// formName uint16 len + bytes
+//
+// formVarInfoCount int32, then that many entries:
+// formName uint16 len + bytes
+// varCount int32
+// initCodeAddr int32
+// initCodeLen int32
+//
+// globalInitCount int32, then that many entries:
+// index int32
+// dataType uint8
#include "serialize.h"
#include "../compiler/opcodes.h"
@@ -143,8 +154,8 @@ void basDebugDeserialize(BasModuleT *mod, const uint8_t *data, int32_t dataLen)
for (int32_t i = 0; i < mod->debugVarCount; i++) {
BasDebugVarT *v = &mod->debugVars[i];
- rStrInto(&r, v->name, BAS_MAX_PROC_NAME);
- rStrInto(&r, v->formName, BAS_MAX_PROC_NAME);
+ rStrInto(&r, v->name, BAS_MAX_IDENT);
+ rStrInto(&r, v->formName, BAS_MAX_IDENT);
v->scope = rU8(&r);
v->dataType = rU8(&r);
v->index = rI32(&r);
@@ -372,8 +383,8 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
p->returnType = rU8(&r);
p->isFunction = rU8(&r) != 0;
- rStrInto(&r, p->name, BAS_MAX_PROC_NAME);
- rStrInto(&r, p->formName, BAS_MAX_PROC_NAME);
+ rStrInto(&r, p->name, BAS_MAX_IDENT);
+ rStrInto(&r, p->formName, BAS_MAX_IDENT);
}
}
@@ -390,7 +401,7 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
BasFormVarInfoT *fv = &mod->formVarInfo[i];
- rStrInto(&r, fv->formName, BAS_MAX_PROC_NAME);
+ rStrInto(&r, fv->formName, BAS_MAX_IDENT);
fv->varCount = rI32(&r);
fv->initCodeAddr = rI32(&r);
fv->initCodeLen = rI32(&r);
diff --git a/src/apps/kpunch/dvxbasic/runtime/values.c b/src/apps/kpunch/dvxbasic/runtime/values.c
index 44c4a8d..31a4549 100644
--- a/src/apps/kpunch/dvxbasic/runtime/values.c
+++ b/src/apps/kpunch/dvxbasic/runtime/values.c
@@ -36,6 +36,11 @@
#include
#define BAS_STRING_IMMORTAL_REFCOUNT 999999 // sentinel refCount; empty string is never freed (see basStringUnref)
+#define BAS_NUM_FORMAT_BUF_LEN 64 // scratch for the widest %g rendering of a double
+#define BAS_SINGLE_SIG_DIGITS 7 // significant digits shown for SINGLE (VB3)
+#define BAS_DOUBLE_SIG_DIGITS 15 // significant digits shown for DOUBLE (VB3)
+#define BAS_RADIX_HEX 16
+#define BAS_RADIX_OCT 8
// ============================================================
// String system
@@ -56,7 +61,6 @@ int32_t basArrayIndex(BasArrayT *arr, int32_t *indices, int32_t ndims);
BasArrayT *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t elementType);
BasArrayT *basArrayRef(BasArrayT *arr);
void basArrayUnref(BasArrayT *arr);
-static int32_t basClampToRange(double n, int32_t lo, int32_t hi);
BasStringT *basStringAlloc(int32_t cap);
int32_t basStringCompare(const BasStringT *a, const BasStringT *b);
int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b);
@@ -64,9 +68,8 @@ BasStringT *basStringConcat(const BasStringT *a, const BasStringT *b);
BasStringT *basStringNew(const char *text, int32_t len);
BasStringT *basStringRef(BasStringT *s);
BasStringT *basStringSub(const BasStringT *s, int32_t start, int32_t len);
-void basStringSystemInit(void);
-void basStringSystemShutdown(void);
void basStringUnref(BasStringT *s);
+BasUdtT *basUdtClone(const BasUdtT *udt);
void basUdtFree(BasUdtT *udt);
BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount);
BasUdtT *basUdtRef(BasUdtT *udt);
@@ -82,18 +85,18 @@ BasValueT basValInteger(int16_t v);
bool basValIsTruthy(BasValueT v);
BasValueT basValLong(int32_t v);
BasValueT basValObject(void *obj);
-uint8_t basValPromoteType(uint8_t a, uint8_t b);
+double basParseNumber(const char *s);
void basValRelease(BasValueT *v);
+bool basValRoundToInt32(BasValueT v, int32_t lo, int32_t hi, int32_t *out);
BasValueT basValSingle(float v);
BasValueT basValString(BasStringT *s);
BasValueT basValStringFromC(const char *text);
BasValueT basValToBool(BasValueT v);
BasValueT basValToDouble(BasValueT v);
-BasValueT basValToInteger(BasValueT v);
-BasValueT basValToLong(BasValueT v);
+int32_t basValToInt32(BasValueT v);
double basValToNumber(BasValueT v);
-BasValueT basValToSingle(BasValueT v);
BasValueT basValToString(BasValueT v);
+static int32_t formatDouble(double n, int32_t sigDigits, char *buf, int32_t bufSize);
// ============================================================
// Array system
@@ -225,19 +228,6 @@ void basArrayUnref(BasArrayT *arr) {
}
-static int32_t basClampToRange(double n, int32_t lo, int32_t hi) {
- if (n <= (double)lo) {
- return lo;
- }
-
- if (n >= (double)hi) {
- return hi;
- }
-
- return (int32_t)(n + (n > 0 ? 0.5 : -0.5));
-}
-
-
BasStringT *basStringAlloc(int32_t cap) {
if (cap < 1) {
cap = 1;
@@ -365,17 +355,6 @@ BasStringT *basStringSub(const BasStringT *s, int32_t start, int32_t len) {
}
-void basStringSystemInit(void) {
- // Nothing to do -- the empty string singleton is statically initialized,
- // including its trailing null. Kept for symmetry with the shutdown call.
-}
-
-
-void basStringSystemShutdown(void) {
- // Nothing to do -- empty string is static
-}
-
-
void basStringUnref(BasStringT *s) {
if (!s || s == basEmptyString) {
return;
@@ -392,6 +371,35 @@ void basStringUnref(BasStringT *s) {
// ============================================================
// UDT system
// ============================================================
+BasUdtT *basUdtClone(const BasUdtT *udt) {
+ BasUdtT *copy = basUdtNew(udt->typeId, udt->fieldCount);
+
+ if (!copy) {
+ return NULL;
+ }
+
+ for (int32_t i = 0; i < udt->fieldCount; i++) {
+ const BasValueT *f = &udt->fields[i];
+
+ if (f->type == BAS_TYPE_UDT && f->udtVal) {
+ BasUdtT *nested = basUdtClone(f->udtVal);
+
+ if (!nested) {
+ basUdtFree(copy);
+ return NULL;
+ }
+
+ copy->fields[i].type = BAS_TYPE_UDT;
+ copy->fields[i].udtVal = nested;
+ } else {
+ copy->fields[i] = basValCopy(*f);
+ }
+ }
+
+ return copy;
+}
+
+
void basUdtFree(BasUdtT *udt) {
if (!udt) {
return;
@@ -455,6 +463,78 @@ void basUdtUnref(BasUdtT *udt) {
// ============================================================
// Value constructors / refcount helpers
// ============================================================
+double basParseNumber(const char *s) {
+ if (!s) {
+ return 0.0;
+ }
+
+ while (*s == ' ' || *s == '\t') {
+ s++;
+ }
+
+ // &H / &O prefixed integers (VAL("&HFF") = 255).
+ if (s[0] == '&' && (s[1] == 'H' || s[1] == 'h' || s[1] == 'O' || s[1] == 'o')) {
+ int32_t radix = (s[1] == 'H' || s[1] == 'h') ? BAS_RADIX_HEX : BAS_RADIX_OCT;
+ return (double)(int32_t)strtoul(s + 2, NULL, radix);
+ }
+
+ // Decimal: hand-scan the accepted span so atof's extra syntax (hex
+ // floats, inf, nan) is never reached, and 'D' exponents are accepted.
+ char buf[BAS_NUM_FORMAT_BUF_LEN];
+ int32_t len = 0;
+ const char *p = s;
+
+ if (*p == '+' || *p == '-') {
+ buf[len++] = *p++;
+ }
+
+ bool sawDigit = false;
+
+ while (isdigit((unsigned char)*p) && len < BAS_NUM_FORMAT_BUF_LEN - 1) {
+ buf[len++] = *p++;
+ sawDigit = true;
+ }
+
+ if (*p == '.' && len < BAS_NUM_FORMAT_BUF_LEN - 1) {
+ buf[len++] = *p++;
+
+ while (isdigit((unsigned char)*p) && len < BAS_NUM_FORMAT_BUF_LEN - 1) {
+ buf[len++] = *p++;
+ sawDigit = true;
+ }
+ }
+
+ if (!sawDigit) {
+ return 0.0;
+ }
+
+ if ((*p == 'E' || *p == 'e' || *p == 'D' || *p == 'd') && len < BAS_NUM_FORMAT_BUF_LEN - 3) {
+ const char *q = p + 1;
+ char expBuf[BAS_NUM_FORMAT_BUF_LEN];
+ int32_t expLen = 0;
+
+ if (*q == '+' || *q == '-') {
+ expBuf[expLen++] = *q++;
+ }
+
+ if (isdigit((unsigned char)*q)) {
+ while (isdigit((unsigned char)*q) && expLen < BAS_NUM_FORMAT_BUF_LEN - 1) {
+ expBuf[expLen++] = *q++;
+ }
+
+ if (len + 1 + expLen < BAS_NUM_FORMAT_BUF_LEN) {
+ buf[len++] = 'e';
+ memcpy(buf + len, expBuf, expLen);
+ len += expLen;
+ }
+ }
+ }
+
+ buf[len] = '\0';
+ return atof(buf);
+}
+
+
BasValueT basValBool(bool v) {
BasValueT val;
val.type = BAS_TYPE_BOOLEAN;
@@ -504,6 +584,8 @@ BasValueT basValCopy(BasValueT v) {
basArrayRef(v.arrVal);
} else if (v.type == BAS_TYPE_UDT && v.udtVal) {
basUdtRef(v.udtVal);
+ } else if (v.type == BAS_TYPE_ELEM_REF) {
+ basArrayRef(v.elemRef.arr);
}
return v;
@@ -519,7 +601,7 @@ BasValueT basValDouble(double v) {
BasStringT *basValFormatString(BasValueT v) {
- char buf[64];
+ char buf[BAS_NUM_FORMAT_BUF_LEN];
switch (v.type) {
case BAS_TYPE_INTEGER:
@@ -530,14 +612,11 @@ BasStringT *basValFormatString(BasValueT v) {
snprintf(buf, sizeof(buf), "%ld", (long)v.longVal);
return basStringNew(buf, (int32_t)strlen(buf));
- case BAS_TYPE_SINGLE: {
- snprintf(buf, sizeof(buf), "%g", (double)v.sngVal);
- return basStringNew(buf, (int32_t)strlen(buf));
- }
+ case BAS_TYPE_SINGLE:
+ return basStringNew(buf, formatDouble((double)v.sngVal, BAS_SINGLE_SIG_DIGITS, buf, sizeof(buf)));
case BAS_TYPE_DOUBLE:
- snprintf(buf, sizeof(buf), "%g", v.dblVal);
- return basStringNew(buf, (int32_t)strlen(buf));
+ return basStringNew(buf, formatDouble(v.dblVal, BAS_DOUBLE_SIG_DIGITS, buf, sizeof(buf)));
case BAS_TYPE_BOOLEAN:
return basStringNew(v.boolVal ? "True" : "False", v.boolVal ? 4 : 5);
@@ -601,31 +680,6 @@ BasValueT basValObject(void *obj) {
}
-uint8_t basValPromoteType(uint8_t a, uint8_t b) {
- // String stays string (concat, not arithmetic)
- if (a == BAS_TYPE_STRING || b == BAS_TYPE_STRING) {
- return BAS_TYPE_STRING;
- }
-
- // Double wins over everything
- if (a == BAS_TYPE_DOUBLE || b == BAS_TYPE_DOUBLE) {
- return BAS_TYPE_DOUBLE;
- }
-
- // Single wins over integer/long
- if (a == BAS_TYPE_SINGLE || b == BAS_TYPE_SINGLE) {
- return BAS_TYPE_SINGLE;
- }
-
- // Long wins over integer
- if (a == BAS_TYPE_LONG || b == BAS_TYPE_LONG) {
- return BAS_TYPE_LONG;
- }
-
- return BAS_TYPE_INTEGER;
-}
-
-
void basValRelease(BasValueT *v) {
if (v->type == BAS_TYPE_STRING) {
basStringUnref(v->strVal);
@@ -636,10 +690,33 @@ void basValRelease(BasValueT *v) {
} else if (v->type == BAS_TYPE_UDT) {
basUdtUnref(v->udtVal);
v->udtVal = NULL;
+ } else if (v->type == BAS_TYPE_ELEM_REF) {
+ basArrayUnref(v->elemRef.arr);
+ v->elemRef.arr = NULL;
}
}
+bool basValRoundToInt32(BasValueT v, int32_t lo, int32_t hi, int32_t *out) {
+ double n = basValToNumber(v);
+
+ if (isnan(n)) {
+ return false;
+ }
+
+ // rint() rounds half to even under the default rounding mode, which is
+ // exactly the VB CINT/CLNG banker's rule (2.5 -> 2, 3.5 -> 4).
+ double r = rint(n);
+
+ if (r < (double)lo || r > (double)hi) {
+ return false;
+ }
+
+ *out = (int32_t)r;
+ return true;
+}
+
+
BasValueT basValSingle(float v) {
BasValueT val;
val.type = BAS_TYPE_SINGLE;
@@ -677,21 +754,22 @@ BasValueT basValToDouble(BasValueT v) {
}
-BasValueT basValToInteger(BasValueT v) {
+int32_t basValToInt32(BasValueT v) {
double n = basValToNumber(v);
- // Round half away from zero, clamping to the INTEGER range first
- // so the float-to-int cast is always in range.
- int32_t rounded = basClampToRange(n, INT16_MIN, INT16_MAX);
- return basValInteger((int16_t)rounded);
-}
+ if (isnan(n)) {
+ return 0;
+ }
-BasValueT basValToLong(BasValueT v) {
- double n = basValToNumber(v);
- // Round half away from zero, clamping to the LONG range first
- // so the float-to-int cast is always in range.
- int32_t rounded = basClampToRange(n, INT32_MIN, INT32_MAX);
- return basValLong(rounded);
+ if (n >= (double)INT32_MAX) {
+ return INT32_MAX;
+ }
+
+ if (n <= (double)INT32_MIN) {
+ return INT32_MIN;
+ }
+
+ return (int32_t)n;
}
@@ -714,7 +792,7 @@ double basValToNumber(BasValueT v) {
case BAS_TYPE_STRING:
if (v.strVal && v.strVal->len > 0) {
- return atof(v.strVal->data);
+ return basParseNumber(v.strVal->data);
}
return 0.0;
@@ -725,11 +803,6 @@ double basValToNumber(BasValueT v) {
}
-BasValueT basValToSingle(BasValueT v) {
- return basValSingle((float)basValToNumber(v));
-}
-
-
BasValueT basValToString(BasValueT v) {
if (v.type == BAS_TYPE_STRING) {
// Normalize a NULL strVal to the empty string so callers can
@@ -748,3 +821,23 @@ BasValueT basValToString(BasValueT v) {
result.strVal = s;
return result;
}
+
+
+// Renders n with sigDigits significant digits the way VB PRINT/STR$ do:
+// fixed notation while it fits, otherwise E notation with an upper-case
+// exponent marker. Returns the length written.
+static int32_t formatDouble(double n, int32_t sigDigits, char *buf, int32_t bufSize) {
+ int32_t len = snprintf(buf, bufSize, "%.*g", (int)sigDigits, n);
+
+ if (len >= bufSize) {
+ len = bufSize - 1;
+ }
+
+ for (int32_t i = 0; i < len; i++) {
+ if (buf[i] == 'e') {
+ buf[i] = 'E';
+ }
+ }
+
+ return len;
+}
diff --git a/src/apps/kpunch/dvxbasic/runtime/values.h b/src/apps/kpunch/dvxbasic/runtime/values.h
index b0bbdf2..c4181e9 100644
--- a/src/apps/kpunch/dvxbasic/runtime/values.h
+++ b/src/apps/kpunch/dvxbasic/runtime/values.h
@@ -76,10 +76,6 @@ int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b);
// The empty string singleton (never freed).
extern BasStringT *basEmptyString;
-// Initialize/shutdown the string system.
-void basStringSystemInit(void);
-void basStringSystemShutdown(void);
-
// ============================================================
// Forward declarations
// ============================================================
@@ -140,6 +136,11 @@ BasUdtT *basUdtRef(BasUdtT *udt);
// Decrement reference count. Frees if count reaches zero.
void basUdtUnref(BasUdtT *udt);
+// Deep copy: a new instance (refCount 1) whose fields are copies of the
+// source's, nested UDT fields cloned recursively. Gives TYPE variables
+// value semantics on assignment and BYVAL parameter passing.
+BasUdtT *basUdtClone(const BasUdtT *udt);
+
// ============================================================
// Tagged value
// ============================================================
@@ -157,6 +158,10 @@ struct BasValueTag {
BasUdtT *udtVal; // BAS_TYPE_UDT (ref-counted)
void *objVal; // BAS_TYPE_OBJECT (opaque host pointer)
BasValueT *refVal; // BAS_TYPE_REF (ByRef pointer to variable slot)
+ struct {
+ BasArrayT *arr; // BAS_TYPE_ELEM_REF: array holding the element (a counted reference)
+ int32_t idx; // flat element index into arr->elements
+ } elemRef;
};
};
@@ -189,6 +194,21 @@ BasValueT basValToBool(BasValueT v);
// Get the numeric value as a double (for mixed-type arithmetic).
double basValToNumber(BasValueT v);
+// Numeric value truncated toward zero and saturated to the int32 range
+// (NaN yields 0). Use for counts, indices and flags read from BASIC
+// values where an out-of-range double must not hit the UB cast.
+int32_t basValToInt32(BasValueT v);
+
+// Numeric value rounded half-to-even (VB CINT/CLNG semantics) and range
+// checked against [lo, hi]. Returns false (and leaves *out untouched)
+// when the value is NaN or outside the range -- the caller raises Overflow.
+bool basValRoundToInt32(BasValueT v, int32_t lo, int32_t hi, int32_t *out);
+
+// VAL() semantics: leading whitespace, optional sign, decimal digits with
+// optional fraction and E/D exponent, or an &H / &O prefixed integer.
+// Stops at the first character that cannot continue the number.
+double basParseNumber(const char *s);
+
// Get the string representation. Returns a new ref-counted string.
BasStringT *basValFormatString(BasValueT v);
@@ -202,8 +222,4 @@ int32_t basValCompare(BasValueT a, BasValueT b);
// Compare two values case-insensitively (for OPTION COMPARE TEXT).
int32_t basValCompareCI(BasValueT a, BasValueT b);
-// Determine the common type for a binary operation (type promotion).
-// Integer + Single -> Single, etc.
-uint8_t basValPromoteType(uint8_t a, uint8_t b);
-
#endif // DVXBASIC_VALUES_H
diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.c b/src/apps/kpunch/dvxbasic/runtime/vm.c
index 7c0fd5b..ae84eb3 100644
--- a/src/apps/kpunch/dvxbasic/runtime/vm.c
+++ b/src/apps/kpunch/dvxbasic/runtime/vm.c
@@ -35,10 +35,12 @@
#include
#include
#include
+#include
#include
#include
#include
#include
+#include
#include
#include
@@ -70,6 +72,34 @@
#define BAS_ATTR_READONLY 1 // file is read-only
#define BAS_ATTR_DIRECTORY 16 // path is a directory
+// Scratch buffer capacities used by individual opcode handlers.
+#define BAS_INPUT_PROMPT_LEN 512 // OP_INPUT: user prompt + "? "
+#define BAS_INPUT_LINE_LEN 1024 // OP_INPUT: longest line the host input callback may return
+#define BAS_FILE_LINE_CHUNK 1024 // INPUT #/LINE INPUT #: fgets chunk while growing a line
+#define BAS_RADIX_STR_LEN 16 // HEX$/OCT$ output ("FFFFFFFF", "37777777777")
+#define BAS_TIME_STR_LEN 16 // TIME$ "HH:MM:SS"
+#define BAS_DATE_STR_LEN 32 // DATE$ "MM-DD-YYYY"
+#define BAS_SCI_FMT_LEN 32 // printf format built for PRINT USING ^^^^
+#define BAS_FORMAT_PERCENT "PERCENT" // FORMAT$ named format
+#define BAS_SCI_MARKER "^^^^" // PRINT USING scientific-notation marker
+
+// Directory permission bits for MKDIR on POSIX hosts (rwxr-xr-x).
+#define BAS_MKDIR_MODE 0755
+
+// Largest count STRING$/SPACE$/INPUT$ accept (VB3 string space limit).
+#define BAS_STRING_FUNC_MAX 65535
+
+// TIMER: seconds per hour/minute and microseconds per second.
+#define BAS_SECS_PER_HOUR 3600.0
+#define BAS_SECS_PER_MINUTE 60.0
+#define BAS_USEC_PER_SEC 1000000.0
+
+// RGB packing: bit offsets and per-channel mask.
+#define BAS_RGB_RED_SHIFT 16
+#define BAS_RGB_GREEN_SHIFT 8
+#define BAS_RGB_CHANNEL_MASK 0xFF
+#define BAS_CHAR_MASK 0xFF
+
// Divergence between the PRINT USING and FORMAT$ numeric formatters,
// captured as options so a single helper serves both call sites.
typedef struct BasNumFormatT {
@@ -86,12 +116,16 @@ typedef struct BasNumFormatT {
int32_t digitsAfter;
} BasNumFormatT;
+static BasVmResultE allocArray(BasVmT *vm, int32_t dims, uint8_t elementType, BasArrayT **outArr);
+static BasVmResultE boundOp(BasVmT *vm, uint8_t op);
+static int32_t clampComponent(int32_t c);
static BasCallFrameT *currentFrame(BasVmT *vm);
static void defaultPrint(void *ctx, const char *text, bool newline);
+static BasValueT *derefSlot(BasValueT *slot);
static void dirClose(BasVmT *vm);
static const char *dirNext(BasVmT *vm);
-static int32_t forStackFloor(BasVmT *vm);
-static void forStackTrim(BasVmT *vm, int32_t newDepth);
+static bool dispatchError(BasVmT *vm, BasVmResultE result, int32_t floorDepth, int32_t fallbackPc);
+static BasVmResultE divByZero(BasVmT *vm);
static BasVmResultE execArith(BasVmT *vm, uint8_t op);
static BasVmResultE execCompare(BasVmT *vm, uint8_t op);
static BasVmResultE execFileOp(BasVmT *vm, uint8_t op);
@@ -100,22 +134,38 @@ static BasVmResultE execLogical(BasVmT *vm, uint8_t op);
static BasVmResultE execMath(BasVmT *vm, uint8_t op);
static BasVmResultE execPrint(BasVmT *vm);
static BasVmResultE execStringOp(BasVmT *vm, uint8_t op);
+static BasStringT *fillString(char ch, int32_t count);
static void formatNumber(double n, const BasNumFormatT *f, char *out, size_t outSize);
+static int32_t forStackFloor(BasVmT *vm);
+static void forStackTrim(BasVmT *vm, int32_t newDepth);
+static void localNow(struct tm *out, int32_t *outUsec);
static int32_t nextStatementPc(BasVmT *vm);
-static int32_t opcodeOperandSize(uint8_t op);
static bool operandFits(BasVmT *vm, int32_t size);
+static void parseNumFormat(const char *fmt, int32_t fmtLen, BasNumFormatT *f, bool *outSci);
static bool pop(BasVmT *vm, BasValueT *val);
+static bool popArgs(BasVmT *vm, int32_t count, ...);
static void popCallFrame(BasVmT *vm);
static BasVmResultE popFileChannel(BasVmT *vm, int32_t *outChannel, bool requireOpen);
+static bool popStringArg(BasVmT *vm, BasValueT *out);
+static void preserveElements(const BasArrayT *oldArr, BasArrayT *newArr);
static void primeModuleFrame(BasVmT *vm);
static bool push(BasVmT *vm, BasValueT val);
static void putBounded(char *out, int32_t *idx, int32_t limit, char c);
+static BasStringT *readField(FILE *fp);
static int16_t readInt16(BasVmT *vm);
+static BasStringT *readLine(FILE *fp);
+static void readRaw(FILE *fp, void *dst, size_t size);
static uint16_t readUint16(BasVmT *vm);
static uint8_t readUint8(BasVmT *vm);
static void releaseVmState(BasVmT *vm);
+static BasVmResultE roundedOperand(BasVmT *vm, BasValueT v, int32_t *out);
static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning);
static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg);
+static void seekRecord(BasVmT *vm, int32_t channel, int32_t recno);
+static void storeValue(BasValueT *target, BasValueT val);
+static int32_t stringFind(const BasStringT *haystack, int32_t start, const BasStringT *needle);
+static BasValueT strValue(BasStringT *s);
+static BasValueT udtDetach(BasValueT val);
static bool validArrayDims(BasVmT *vm, int32_t dims);
@@ -205,7 +255,6 @@ BasVmT *basVmCreate(void) {
vm->stepOutDepth = -1;
vm->runToCursorLine = -1;
vm->outArgFrame = -1;
- basStringSystemInit();
return vm;
}
@@ -228,22 +277,22 @@ void basVmDestroy(BasVmT *vm) {
// Close Dir$ iterator
dirClose(vm);
- basStringSystemShutdown();
free(vm);
}
-int32_t basVmGetCurrentLine(const BasVmT *vm) {
- return vm ? vm->currentLine : 0;
-}
-
-
const char *basVmGetError(const BasVmT *vm) {
return vm ? vm->errorMsg : "";
}
void basVmLoadModule(BasVmT *vm, BasModuleT *module) {
+ if (module->globalCount > BAS_VM_MAX_GLOBALS) {
+ vm->module = NULL;
+ snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Program needs %d global variables; the limit is %d", (int)module->globalCount, (int)BAS_VM_MAX_GLOBALS);
+ return;
+ }
+
vm->module = module;
vm->pc = module->entryPoint;
@@ -277,65 +326,14 @@ void basVmLoadModule(BasVmT *vm, BasModuleT *module) {
}
-bool basVmPop(BasVmT *vm, BasValueT *val) {
- return pop(vm, val);
-}
-
-
-bool basVmPush(BasVmT *vm, BasValueT val) {
- return push(vm, val);
-}
-
-
-void basVmReset(BasVmT *vm) {
- // Release eval stack, globals, call-frame locals and FOR-stack values
- // before the counts are zeroed below (the release loops need them).
- releaseVmState(vm);
-
- vm->sp = 0;
- vm->stmtSp = 0;
- vm->stmtPc = -1;
- vm->callDepth = 0;
- vm->forDepth = 0;
-
- // Re-establish the implicit module frame that basVmLoadModule primes.
- // Leaving callDepth at 0 would silently kill module-level ON ERROR
- // trapping after a reset: the error dispatcher only walks frames while
- // callDepth > 0, so it would never inspect frame 0's handler.
- if (vm->module) {
- primeModuleFrame(vm);
- } else {
- // No module yet: hosts that re-prime callDepth = 1 by hand rely on
- // frame 0's saved context being sane; clear it so a stale value
- // from a previous run cannot leak into the FOR-stack floor or
- // error-resume logic.
- vm->callStack[0].savedForDepth = 0;
- vm->callStack[0].savedStmtPc = -1;
- vm->callStack[0].savedStmtSp = 0;
+BasVmResultE basVmRun(BasVmT *vm) {
+ // basVmLoadModule refused the module (too many globals): report why
+ // instead of returning a clean HALTED for a program that never ran.
+ if (!vm->module && vm->errorMsg[0] != '\0') {
+ return BAS_VM_ERROR;
}
- vm->outArgs = NULL;
- vm->outArgCount = 0;
- vm->outArgFrame = -1;
- vm->pc = vm->module ? vm->module->entryPoint : 0;
- vm->running = false;
- vm->badOperand = false;
- vm->yielded = false;
- vm->dataPtr = 0;
- vm->errorHandler = 0;
- vm->errorNumber = 0;
- vm->errorPc = 0;
- vm->errorNextPc = 0;
- vm->errorLine = 0;
- vm->currentOpPc = 0;
- vm->inErrorHandler = false;
- vm->errorMsg[0] = '\0';
-}
-
-
-BasVmResultE basVmRun(BasVmT *vm) {
vm->running = true;
- vm->yielded = false;
vm->stepCount = 0;
while (vm->running) {
@@ -350,73 +348,17 @@ BasVmResultE basVmRun(BasVmT *vm) {
vm->stepCount++;
if (result != BAS_VM_OK) {
- // If an error handler is set and this is a trappable error,
- // unwind call frames until we find the SUB whose ON ERROR
- // GOTO registered a handler, then jump there. Without this
- // unwind an error raised inside a called SUB would fire the
- // outer SUB's handler but OP_RET would then pop the inner
- // frame and resume after the call site (effectively RESUME
- // NEXT semantics), which is not what ON ERROR promises.
- if (!vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) {
- int32_t target = 0;
- int32_t resumePc = -1;
+ // Debugger pauses are not errors: hand them straight back to
+ // the host without consulting ON ERROR handlers.
+ if (result == BAS_VM_BREAKPOINT) {
+ vm->running = false;
+ return result;
+ }
- while (vm->callDepth > 0) {
- BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1];
-
- if (frame->errorHandler != 0) {
- target = frame->errorHandler;
- break;
- }
-
- // No handler on this frame -- discard it and keep
- // unwinding. frame-local values need releasing.
- for (int32_t li = 0; li < frame->localCount; li++) {
- basValRelease(&frame->locals[li]);
- }
-
- // Restore the caller's context: drop the callee's FOR
- // frames and bring back the statement boundary of the
- // calling statement. RESUME must land in the handler
- // frame's code, never in the discarded callee's.
- forStackTrim(vm, frame->savedForDepth);
- vm->stmtPc = frame->savedStmtPc;
- vm->stmtSp = frame->savedStmtSp;
- resumePc = frame->returnPc;
-
- vm->callDepth--;
- }
-
- if (target != 0) {
- // Release any operands the failed statement half-pushed
- // before the error fired, then truncate the eval stack
- // back to the statement boundary so trapped errors in a
- // loop don't leak values and grow sp without bound.
- if (vm->sp > vm->stmtSp) {
- for (int32_t si = vm->stmtSp; si < vm->sp; si++) {
- basValRelease(&vm->stack[si]);
- }
-
- vm->sp = vm->stmtSp;
- }
-
- // The eval stack is now at the failing statement's
- // boundary, so RESUME must re-run that statement from
- // its start and RESUME NEXT must continue at the next
- // statement -- resuming mid-statement would pop values
- // from beneath the truncated boundary. Without
- // statement info (OP_LINE stripped by release
- // compaction) fall back to the call site after an
- // unwind, or to the failing instruction.
- int32_t nextPc = nextStatementPc(vm);
-
- vm->errorPc = (vm->stmtPc >= 0) ? vm->stmtPc : ((resumePc >= 0) ? resumePc : savedPc);
- vm->errorNextPc = (nextPc >= 0) ? nextPc : ((resumePc >= 0) ? resumePc : vm->pc);
- vm->inErrorHandler = true;
- vm->errorHandler = target;
- vm->pc = target;
- continue;
- }
+ // Trappable error with a handler in scope: the dispatcher has
+ // unwound to the handler frame and pointed pc at it.
+ if (dispatchError(vm, result, 0, savedPc)) {
+ continue;
}
vm->running = false;
@@ -556,21 +498,6 @@ BasVmResultE basVmStep(BasVmT *vm) {
break;
}
- case OP_PUSH_FLT32: {
- float val = 0.0f;
-
- if (operandFits(vm, (int32_t)sizeof(float))) {
- memcpy(&val, &vm->module->code[vm->pc], sizeof(float));
- vm->pc += sizeof(float);
- }
-
- if (!push(vm, basValSingle(val))) {
- return BAS_VM_STACK_OVERFLOW;
- }
-
- break;
- }
-
case OP_PUSH_FLT64: {
double val = 0.0;
@@ -587,16 +514,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
case OP_PUSH_STR: {
- uint16_t idx = readUint16(vm);
+ uint16_t idx = readUint16(vm);
+ BasStringT *str = (idx < (uint16_t)vm->module->constCount) ? vm->module->constants[idx] : basEmptyString;
- if (idx < (uint16_t)vm->module->constCount) {
- if (!push(vm, basValString(vm->module->constants[idx]))) {
- return BAS_VM_STACK_OVERFLOW;
- }
- } else {
- if (!push(vm, basValStringFromC(""))) {
- return BAS_VM_STACK_OVERFLOW;
- }
+ if (!push(vm, basValString(str))) {
+ return BAS_VM_STACK_OVERFLOW;
}
break;
@@ -630,16 +552,20 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT dup = basValCopy(vm->stack[vm->sp - 1]);
-
- if (!push(vm, dup)) {
- basValRelease(&dup);
+ if (!push(vm, basValCopy(vm->stack[vm->sp - 1]))) {
return BAS_VM_STACK_OVERFLOW;
}
break;
}
+ case OP_STMT:
+ // Release-build statement boundary: same bookkeeping as OP_LINE
+ // minus the line number and debugger checks.
+ vm->stmtSp = vm->sp;
+ vm->stmtPc = vm->currentOpPc;
+ break;
+
// ============================================================
// Variable access
// ============================================================
@@ -654,12 +580,14 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
// ByRef: if the local holds a reference, dereference it
- BasValueT *slot = &frame->locals[idx];
- BasValueT val = (slot->type == BAS_TYPE_REF) ? *slot->refVal : *slot;
- BasValueT lv = basValCopy(val);
+ BasValueT *slot = derefSlot(&frame->locals[idx]);
- if (!push(vm, lv)) {
- basValRelease(&lv);
+ if (!slot) {
+ runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Subscript out of range");
+ return BAS_VM_SUBSCRIPT_RANGE;
+ }
+
+ if (!push(vm, basValCopy(*slot))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -682,16 +610,15 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
// ByRef: if the local holds a reference, store through it
- BasValueT *slot = &frame->locals[idx];
+ BasValueT *slot = derefSlot(&frame->locals[idx]);
- if (slot->type == BAS_TYPE_REF) {
- basValRelease(slot->refVal);
- *slot->refVal = val;
- } else {
- basValRelease(slot);
- *slot = val;
+ if (!slot) {
+ basValRelease(&val);
+ runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Subscript out of range");
+ return BAS_VM_SUBSCRIPT_RANGE;
}
+ storeValue(slot, val);
break;
}
@@ -703,10 +630,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_ERROR;
}
- BasValueT gv = basValCopy(vm->globals[idx]);
-
- if (!push(vm, gv)) {
- basValRelease(&gv);
+ if (!push(vm, basValCopy(vm->globals[idx]))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -727,8 +651,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_ERROR;
}
- basValRelease(&vm->globals[idx]);
- vm->globals[idx] = val;
+ storeValue(&vm->globals[idx], val);
break;
}
@@ -771,42 +694,6 @@ BasVmResultE basVmStep(BasVmT *vm) {
break;
}
- case OP_LOAD_REF: {
- if (vm->sp < 1) {
- return BAS_VM_STACK_UNDERFLOW;
- }
-
- BasValueT *top = &vm->stack[vm->sp - 1];
-
- if (top->type != BAS_TYPE_REF || !top->refVal) {
- runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Expected reference");
- return BAS_VM_ERROR;
- }
-
- BasValueT val = basValCopy(*top->refVal);
- *top = val;
- break;
- }
-
- case OP_STORE_REF: {
- BasValueT val;
- BasValueT ref;
-
- if (!pop(vm, &val) || !pop(vm, &ref)) {
- return BAS_VM_STACK_UNDERFLOW;
- }
-
- if (ref.type != BAS_TYPE_REF || !ref.refVal) {
- basValRelease(&val);
- runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Expected reference");
- return BAS_VM_ERROR;
- }
-
- basValRelease(ref.refVal);
- *ref.refVal = val;
- break;
- }
-
// ============================================================
// Arithmetic
// ============================================================
@@ -816,9 +703,6 @@ BasVmResultE basVmStep(BasVmT *vm) {
case OP_MUL_INT:
case OP_IDIV_INT:
case OP_MOD_INT:
- case OP_ADD_FLT:
- case OP_SUB_FLT:
- case OP_MUL_FLT:
case OP_DIV_FLT:
case OP_POW:
return execArith(vm, op);
@@ -847,26 +731,6 @@ BasVmResultE basVmStep(BasVmT *vm) {
break;
}
- case OP_NEG_FLT: {
- if (vm->sp < 1) {
- return BAS_VM_STACK_UNDERFLOW;
- }
-
- BasValueT *top = &vm->stack[vm->sp - 1];
-
- if (top->type == BAS_TYPE_SINGLE) {
- top->sngVal = -top->sngVal;
- } else if (top->type == BAS_TYPE_DOUBLE) {
- top->dblVal = -top->dblVal;
- } else {
- double n = basValToNumber(*top);
- basValRelease(top);
- *top = basValDouble(-n);
- }
-
- break;
- }
-
// ============================================================
// String operations
// ============================================================
@@ -994,16 +858,18 @@ BasVmResultE basVmStep(BasVmT *vm) {
// Zero all local slots
memset(frame->locals, 0, sizeof(frame->locals));
- // Pop arguments into locals starting at baseSlot (in reverse order)
+ // Pop arguments into locals starting at baseSlot (in reverse
+ // order). A BYVAL TYPE argument must be the callee's own copy.
for (int32_t i = baseSlot + argc - 1; i >= baseSlot; i--) {
if (!pop(vm, &frame->locals[i])) {
return BAS_VM_STACK_UNDERFLOW;
}
+
+ frame->locals[i] = udtDetach(frame->locals[i]);
}
// The callee starts with no handler; if it raises an error
// the dispatcher will unwind to the nearest frame with one.
- vm->errorHandler = 0;
vm->pc = addr;
break;
}
@@ -1035,7 +901,6 @@ BasVmResultE basVmStep(BasVmT *vm) {
popCallFrame(vm);
if (!push(vm, retVal)) {
- basValRelease(&retVal);
return BAS_VM_STACK_OVERFLOW;
}
@@ -1050,7 +915,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- vm->pc = (int32_t)basValToNumber(retAddr);
+ vm->pc = basValToInt32(retAddr);
basValRelease(&retAddr);
break;
}
@@ -1062,7 +927,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT stepVal;
BasValueT limitVal;
- if (!pop(vm, &stepVal) || !pop(vm, &limitVal)) {
+ if (!popArgs(vm, 2, &stepVal, &limitVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -1119,8 +984,8 @@ BasVmResultE basVmStep(BasVmT *vm) {
// ByRef loop variable (e.g. FOR over a BYREF SUB parameter):
// the local slot holds a reference, so operate on the caller's
// storage just as OP_LOAD_LOCAL/OP_STORE_LOCAL do.
- if (varSlot && varSlot->type == BAS_TYPE_REF) {
- varSlot = varSlot->refVal;
+ if (varSlot) {
+ varSlot = derefSlot(varSlot);
}
if (varSlot) {
@@ -1206,8 +1071,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
// ByRef loop variable: dereference so the increment writes the
// caller's storage, matching OP_STORE_LOCAL.
- if (varSlot->type == BAS_TYPE_REF) {
- varSlot = varSlot->refVal;
+ varSlot = derefSlot(varSlot);
+
+ if (!varSlot) {
+ runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Subscript out of range");
+ return BAS_VM_SUBSCRIPT_RANGE;
}
// Increment: var = var + step. Preserve the loop variable's
@@ -1304,21 +1172,25 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
case OP_CONV_FLT_INT:
- case OP_CONV_STR_INT:
- case OP_CONV_LONG_INT: {
+ case OP_CONV_STR_INT: {
if (vm->sp < 1) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT *top = &vm->stack[vm->sp - 1];
- BasValueT conv = basValToInteger(*top);
+ int32_t n;
+
+ if (!basValRoundToInt32(*top, INT16_MIN, INT16_MAX, &n)) {
+ runtimeError(vm, BAS_ERR_OVERFLOW, "Overflow");
+ return BAS_VM_ERROR;
+ }
+
basValRelease(top);
- *top = conv;
+ *top = basValInteger((int16_t)n);
break;
}
- case OP_CONV_INT_STR:
- case OP_CONV_FLT_STR: {
+ case OP_CONV_INT_STR: {
if (vm->sp < 1) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -1348,9 +1220,15 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
BasValueT *top = &vm->stack[vm->sp - 1];
- BasValueT conv = basValToLong(*top);
+ int32_t n;
+
+ if (!basValRoundToInt32(*top, INT32_MIN, INT32_MAX, &n)) {
+ runtimeError(vm, BAS_ERR_OVERFLOW, "Overflow");
+ return BAS_VM_ERROR;
+ }
+
basValRelease(top);
- *top = conv;
+ *top = basValLong(n);
break;
}
@@ -1373,20 +1251,6 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
break;
- case OP_PRINT_SPC: {
- uint8_t n = readUint8(vm);
- char spaces[BAS_PRINT_SPC_MAX + 1];
- int32_t count = n;
- memset(spaces, ' ', count);
- spaces[count] = '\0';
-
- if (vm->printFn) {
- vm->printFn(vm->printCtx, spaces, false);
- }
-
- break;
- }
-
case OP_PRINT_SPC_N: {
BasValueT nVal;
@@ -1394,7 +1258,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t count = (int32_t)basValToNumber(nVal);
+ int32_t count = basValToInt32(nVal);
basValRelease(&nVal);
if (count < 0) {
@@ -1423,7 +1287,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t col = (int32_t)basValToNumber(nVal);
+ int32_t col = basValToInt32(nVal);
basValRelease(&nVal);
if (col < 1) {
@@ -1453,7 +1317,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT val;
BasValueT fmtVal;
- if (!pop(vm, &val) || !pop(vm, &fmtVal)) {
+ if (!popArgs(vm, 2, &val, &fmtVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -1506,96 +1370,19 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
} else {
// Numeric formatting
- double n = basValToNumber(val);
+ double n = basValToNumber(val);
+ BasNumFormatT f;
+ bool sciNotation;
- // Parse format flags
- bool asteriskFill = false; // ** fill leading spaces with *
- bool dollarFloat = false; // $$ floating dollar sign
- bool plusAtStart = false; // + at start: always show sign
- bool plusAtEnd = false; // + at end: always show sign
- bool minusAtEnd = false; // - at end: show minus for negative
- bool sciNotation = false; // ^^^^ scientific notation
- bool hasDecimal = false;
- bool hasComma = false;
- int32_t digitsBefore = 0;
- int32_t digitsAfter = 0;
-
- // Check for ** at start
- if (fmtLen >= 2 && fmt[0] == '*' && fmt[1] == '*') {
- asteriskFill = true;
- }
-
- // Check for $$ (may follow **)
- int32_t scanStart = 0;
- if (asteriskFill) {
- scanStart = 2;
- }
- if (fmtLen >= scanStart + 2 && fmt[scanStart] == '$' && fmt[scanStart + 1] == '$') {
- dollarFloat = true;
- }
-
- // Check for + at start or end
- if (fmtLen > 0 && fmt[0] == '+') {
- plusAtStart = true;
- }
- if (fmtLen > 0 && fmt[fmtLen - 1] == '+') {
- plusAtEnd = true;
- }
-
- // Check for - at end
- if (fmtLen > 0 && fmt[fmtLen - 1] == '-') {
- minusAtEnd = true;
- }
-
- // Check for ^^^^ (scientific notation)
- for (int32_t i = 0; i <= fmtLen - 4; i++) {
- if (fmt[i] == '^' && fmt[i+1] == '^' && fmt[i+2] == '^' && fmt[i+3] == '^') {
- sciNotation = true;
- break;
- }
- }
-
- // Count # and 0 digits before and after decimal
- for (int32_t i = 0; i < fmtLen; i++) {
- if (fmt[i] == '.') {
- hasDecimal = true;
- } else if (fmt[i] == ',') {
- hasComma = true;
- } else if (fmt[i] == '#' || fmt[i] == '0') {
- if (hasDecimal) {
- digitsAfter++;
- } else {
- digitsBefore++;
- }
- } else if (fmt[i] == '*') {
- if (!hasDecimal) {
- digitsBefore++;
- }
- }
- }
+ parseNumFormat(fmt, fmtLen, &f, &sciNotation);
if (sciNotation) {
- // Scientific notation
- char sciFmt[32];
- int32_t decimals = hasDecimal ? digitsAfter : 0;
+ char sciFmt[BAS_SCI_FMT_LEN];
+ int32_t decimals = f.hasDecimal ? f.digitsAfter : 0;
+
snprintf(sciFmt, sizeof(sciFmt), "%%.%dE", (int)decimals);
snprintf(buf, sizeof(buf), sciFmt, n);
} else {
- // Standard formatting via the shared bounded helper.
- BasNumFormatT f;
-
- f.hasDecimal = hasDecimal;
- f.hasComma = hasComma;
- f.plusAtStart = plusAtStart;
- f.plusAtEnd = plusAtEnd;
- f.minusAtEnd = minusAtEnd;
- f.asteriskFill = asteriskFill;
- f.dollarFloat = dollarFloat;
- f.zeroPad = false;
- f.formatPad = false;
- f.digitsBefore = digitsBefore;
- f.digitsAfter = digitsAfter;
-
formatNumber(n, &f, buf, sizeof(buf));
}
}
@@ -1624,7 +1411,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
// Build the full prompt: "user prompt? "
- char promptBuf[512];
+ char promptBuf[BAS_INPUT_PROMPT_LEN];
const char *userPrompt = "";
if (promptVal.type == BAS_TYPE_STRING && promptVal.strVal) {
@@ -1637,7 +1424,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
snprintf(promptBuf, sizeof(promptBuf), "? ");
}
- char buf[1024];
+ char buf[BAS_INPUT_LINE_LEN];
buf[0] = '\0';
if (vm->inputFn) {
@@ -1680,26 +1467,19 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT vg;
BasValueT vr;
- if (!pop(vm, &vb) || !pop(vm, &vg) || !pop(vm, &vr)) {
+ if (!popArgs(vm, 3, &vb, &vg, &vr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t r = (int32_t)basValToNumber(vr);
- int32_t g = (int32_t)basValToNumber(vg);
- int32_t b = (int32_t)basValToNumber(vb);
+ int32_t r = clampComponent(basValToInt32(vr));
+ int32_t g = clampComponent(basValToInt32(vg));
+ int32_t b = clampComponent(basValToInt32(vb));
basValRelease(&vr);
basValRelease(&vg);
basValRelease(&vb);
- if (r < 0) { r = 0; }
- if (r > BAS_RGB_COMPONENT_MAX) { r = BAS_RGB_COMPONENT_MAX; }
- if (g < 0) { g = 0; }
- if (g > BAS_RGB_COMPONENT_MAX) { g = BAS_RGB_COMPONENT_MAX; }
- if (b < 0) { b = 0; }
- if (b > BAS_RGB_COMPONENT_MAX) { b = BAS_RGB_COMPONENT_MAX; }
-
- if (!push(vm, basValLong((r << 16) | (g << 8) | b))) {
+ if (!push(vm, basValLong((r << BAS_RGB_RED_SHIFT) | (g << BAS_RGB_GREEN_SHIFT) | b))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -1715,17 +1495,17 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t color = (int32_t)basValToNumber(vc);
+ int32_t color = basValToInt32(vc);
basValRelease(&vc);
int32_t component;
if (op == OP_GET_RED) {
- component = (color >> 16) & 0xFF;
+ component = (color >> BAS_RGB_RED_SHIFT) & BAS_RGB_CHANNEL_MASK;
} else if (op == OP_GET_GREEN) {
- component = (color >> 8) & 0xFF;
+ component = (color >> BAS_RGB_GREEN_SHIFT) & BAS_RGB_CHANNEL_MASK;
} else {
- component = color & 0xFF;
+ component = color & BAS_RGB_CHANNEL_MASK;
}
if (!push(vm, basValInteger((int16_t)component))) {
@@ -1757,20 +1537,29 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT *top = &vm->stack[vm->sp - 1];
- double n = basValToNumber(*top);
- basValRelease(top);
+ BasValueT *top = &vm->stack[vm->sp - 1];
+ BasValueT num = *top;
- char buf[64];
-
- if (n >= 0.0) {
- snprintf(buf, sizeof(buf), " %g", n);
- } else {
- snprintf(buf, sizeof(buf), "%g", n);
+ // Same digits as PRINT (basValFormatString), so STR$ and PRINT
+ // never disagree; booleans and strings go through as numbers.
+ if (num.type == BAS_TYPE_BOOLEAN || num.type == BAS_TYPE_STRING) {
+ num = basValDouble(basValToNumber(num));
}
- top->type = BAS_TYPE_STRING;
- top->strVal = basStringNew(buf, (int32_t)strlen(buf));
+ BasStringT *digits = basValFormatString(num);
+ BasStringT *result;
+
+ if (digits->data[0] == '-') {
+ result = basStringRef(digits);
+ } else {
+ BasStringT *space = basStringNew(" ", 1);
+ result = basStringConcat(space, digits);
+ basStringUnref(space);
+ }
+
+ basStringUnref(digits);
+ basValRelease(top);
+ *top = strValue(result);
break;
}
@@ -1780,8 +1569,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
BasValueT *top = &vm->stack[vm->sp - 1];
- int32_t n = (int32_t)basValToNumber(*top);
- char buf[16];
+ int32_t n = basValToInt32(*top);
+ char buf[BAS_RADIX_STR_LEN];
+
snprintf(buf, sizeof(buf), "%X", (unsigned int)n);
basValRelease(top);
*top = basValStringFromC(buf);
@@ -1794,8 +1584,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
BasValueT *top = &vm->stack[vm->sp - 1];
- int32_t n = (int32_t)basValToNumber(*top);
- char buf[16];
+ int32_t n = basValToInt32(*top);
+ char buf[BAS_RADIX_STR_LEN];
+
snprintf(buf, sizeof(buf), "%o", (unsigned int)n);
basValRelease(top);
*top = basValStringFromC(buf);
@@ -1827,11 +1618,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT charVal;
BasValueT countVal;
- if (!pop(vm, &charVal) || !pop(vm, &countVal)) {
+ if (!popArgs(vm, 2, &charVal, &countVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t count = (int32_t)basValToNumber(countVal);
+ int32_t count = basValToInt32(countVal);
basValRelease(&countVal);
char ch;
@@ -1839,33 +1630,20 @@ BasVmResultE basVmStep(BasVmT *vm) {
if (charVal.type == BAS_TYPE_STRING && charVal.strVal && charVal.strVal->len > 0) {
ch = charVal.strVal->data[0];
} else {
- ch = (char)(int32_t)basValToNumber(charVal);
+ ch = (char)(basValToInt32(charVal) & BAS_CHAR_MASK);
}
basValRelease(&charVal);
- if (count < 0) {
- count = 0;
+ if (count < 0 || count > BAS_STRING_FUNC_MAX) {
+ runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
+ return BAS_VM_ERROR;
}
- if (count > INT16_MAX) {
- count = INT16_MAX;
- }
-
- BasStringT *s = basStringAlloc(count + 1);
-
- if (s->cap >= count + 1) {
- memset(s->data, ch, count);
- s->data[count] = '\0';
- s->len = count;
- }
-
- if (!push(vm, basValString(s))) {
- basStringUnref(s);
+ if (!push(vm, strValue(fillString(ch, count)))) {
return BAS_VM_STACK_OVERFLOW;
}
- basStringUnref(s);
break;
}
@@ -1874,10 +1652,14 @@ BasVmResultE basVmStep(BasVmT *vm) {
// ============================================================
case OP_MATH_TIMER: {
- // Push seconds since midnight as a double
- time_t now = time(NULL);
- struct tm *t = localtime(&now);
- double secs = (double)t->tm_hour * 3600.0 + (double)t->tm_min * 60.0 + (double)t->tm_sec;
+ // Push seconds since midnight as a double, with sub-second
+ // resolution so timing loops do not stall for up to a second.
+ struct tm t;
+ int32_t usec;
+
+ localNow(&t, &usec);
+
+ double secs = (double)t.tm_hour * BAS_SECS_PER_HOUR + (double)t.tm_min * BAS_SECS_PER_MINUTE + (double)t.tm_sec + (double)usec / BAS_USEC_PER_SEC;
if (!push(vm, basValDouble(secs))) {
return BAS_VM_STACK_OVERFLOW;
@@ -1888,10 +1670,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
case OP_DATE_STR: {
// Push DATE$ as "MM-DD-YYYY"
- time_t now = time(NULL);
- struct tm *t = localtime(&now);
- char buf[32];
- snprintf(buf, sizeof(buf), "%02d-%02d-%04d", t->tm_mon + 1, t->tm_mday, t->tm_year + 1900);
+ struct tm t;
+ char buf[BAS_DATE_STR_LEN];
+
+ localNow(&t, NULL);
+ snprintf(buf, sizeof(buf), "%02d-%02d-%04d", t.tm_mon + 1, t.tm_mday, t.tm_year + 1900);
if (!push(vm, basValStringFromC(buf))) {
return BAS_VM_STACK_OVERFLOW;
@@ -1902,10 +1685,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
case OP_TIME_STR: {
// Push TIME$ as "HH:MM:SS"
- time_t now = time(NULL);
- struct tm *t = localtime(&now);
- char buf[16];
- snprintf(buf, sizeof(buf), "%02d:%02d:%02d", t->tm_hour, t->tm_min, t->tm_sec);
+ struct tm t;
+ char buf[BAS_TIME_STR_LEN];
+
+ localNow(&t, NULL);
+ snprintf(buf, sizeof(buf), "%02d:%02d:%02d", t.tm_hour, t.tm_min, t.tm_sec);
if (!push(vm, basValStringFromC(buf))) {
return BAS_VM_STACK_OVERFLOW;
@@ -1922,7 +1706,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t secs = (int32_t)basValToNumber(val);
+ int32_t secs = basValToInt32(val);
basValRelease(&val);
if (secs > 0) {
@@ -2019,13 +1803,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
// dispatcher walks frames (innermost outward) to find a
// handler and unwinds the stack to that frame before
// jumping. Module-level code lives in frame 0.
- BasCallFrameT *frame = currentFrame(vm);
-
- if (frame) {
- frame->errorHandler = target;
- }
-
- vm->errorHandler = target;
+ currentFrame(vm)->errorHandler = target;
break;
}
@@ -2035,22 +1813,17 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
break;
- case OP_ERR_CLEAR:
- vm->errorNumber = 0;
- vm->errorMsg[0] = '\0';
- break;
-
case OP_RESUME:
- // RESUME -- re-execute the statement that caused the error
- vm->pc = vm->errorPc;
- vm->errorNumber = 0;
- vm->errorMsg[0] = '\0';
- vm->inErrorHandler = false;
- break;
-
case OP_RESUME_NEXT:
- // RESUME NEXT -- continue at next statement after the error
- vm->pc = vm->errorNextPc;
+ // RESUME re-executes the statement that raised the error;
+ // RESUME NEXT continues at the statement after it. Outside a
+ // handler there is nothing to resume (QBASIC error 20).
+ if (!vm->inErrorHandler) {
+ runtimeError(vm, BAS_ERR_RESUME_WITHOUT_ERR, "RESUME without error");
+ return BAS_VM_ERROR;
+ }
+
+ vm->pc = (op == OP_RESUME) ? vm->errorPc : vm->errorNextPc;
vm->errorNumber = 0;
vm->errorMsg[0] = '\0';
vm->inErrorHandler = false;
@@ -2063,7 +1836,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t errNum = (int32_t)basValToNumber(errVal);
+ int32_t errNum = basValToInt32(errVal);
basValRelease(&errVal);
runtimeError(vm, errNum, "User-defined error");
return BAS_VM_ERROR;
@@ -2082,12 +1855,12 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT fieldCountVal;
BasValueT typeIdVal;
- if (!pop(vm, &fieldCountVal) || !pop(vm, &typeIdVal)) {
+ if (!popArgs(vm, 2, &fieldCountVal, &typeIdVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t fieldCount = (int32_t)basValToNumber(fieldCountVal);
- int32_t typeId = (int32_t)basValToNumber(typeIdVal);
+ int32_t fieldCount = basValToInt32(fieldCountVal);
+ int32_t typeId = basValToInt32(typeIdVal);
basValRelease(&fieldCountVal);
basValRelease(&typeIdVal);
@@ -2103,75 +1876,17 @@ BasVmResultE basVmStep(BasVmT *vm) {
udtVal.udtVal = udt;
if (!push(vm, udtVal)) {
- basUdtFree(udt);
return BAS_VM_STACK_OVERFLOW;
}
break;
}
- if (!validArrayDims(vm, dims)) {
- return BAS_VM_ERROR;
- }
+ BasArrayT *arr;
+ BasVmResultE rc = allocArray(vm, dims, elementType, &arr);
- // For UDT arrays, parser pushes typeId and fieldCount after bounds
- int32_t udtTypeId = -1;
- int32_t udtFieldCnt = 0;
-
- if (elementType == BAS_TYPE_UDT) {
- BasValueT fcVal;
- BasValueT tiVal;
-
- if (!pop(vm, &fcVal) || !pop(vm, &tiVal)) {
- return BAS_VM_STACK_UNDERFLOW;
- }
-
- udtFieldCnt = (int32_t)basValToNumber(fcVal);
- udtTypeId = (int32_t)basValToNumber(tiVal);
- basValRelease(&fcVal);
- basValRelease(&tiVal);
- }
-
- // Normal array allocation: parser pushes (lbound, ubound) pairs per dim
- int32_t lbounds[BAS_ARRAY_MAX_DIMS];
- int32_t ubounds[BAS_ARRAY_MAX_DIMS];
-
- // Pop bounds in reverse order (last dim first)
- for (int32_t d = dims - 1; d >= 0; d--) {
- BasValueT ubVal;
- BasValueT lbVal;
-
- if (!pop(vm, &ubVal) || !pop(vm, &lbVal)) {
- return BAS_VM_STACK_UNDERFLOW;
- }
-
- ubounds[d] = (int32_t)basValToNumber(ubVal);
- lbounds[d] = (int32_t)basValToNumber(lbVal);
- basValRelease(&ubVal);
- basValRelease(&lbVal);
- }
-
- BasArrayT *arr = basArrayNew(dims, lbounds, ubounds, elementType);
-
- if (!arr) {
- runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory allocating array");
- return BAS_VM_OUT_OF_MEMORY;
- }
-
- // Initialize UDT array elements with proper UDT instances
- if (elementType == BAS_TYPE_UDT && udtTypeId >= 0) {
- for (int32_t i = 0; i < arr->totalElements; i++) {
- BasUdtT *udt = basUdtNew(udtTypeId, udtFieldCnt);
-
- if (!udt) {
- basArrayFree(arr);
- runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory allocating TYPE array elements");
- return BAS_VM_OUT_OF_MEMORY;
- }
-
- arr->elements[i].type = BAS_TYPE_UDT;
- arr->elements[i].udtVal = udt;
- }
+ if (rc != BAS_VM_OK) {
+ return rc;
}
BasValueT arrVal;
@@ -2179,7 +1894,6 @@ BasVmResultE basVmStep(BasVmT *vm) {
arrVal.arrVal = arr;
if (!push(vm, arrVal)) {
- basArrayFree(arr);
return BAS_VM_STACK_OVERFLOW;
}
@@ -2203,7 +1917,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- indices[d] = (int32_t)basValToNumber(idxVal);
+ indices[d] = basValToInt32(idxVal);
basValRelease(&idxVal);
}
@@ -2232,7 +1946,6 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&arrRef);
if (!push(vm, elem)) {
- basValRelease(&elem);
return BAS_VM_STACK_OVERFLOW;
}
@@ -2240,11 +1953,12 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
case OP_PUSH_ARR_ADDR: {
- // Pass `arr(i)` as a BYREF parameter: push a BAS_TYPE_REF
- // pointing into the array's element storage. The array is
- // ref-counted, and the caller still holds a reference via
- // the local it came from, so the element memory is stable
- // for the duration of the call.
+ // Pass `arr(i)` as a BYREF parameter: push a BAS_TYPE_ELEM_REF
+ // that owns a counted reference to the array plus the element
+ // index. Holding the reference keeps the element storage alive
+ // even if the callee REDIMs or ERASEs the array variable, and
+ // the index is resolved on every load/store instead of caching
+ // a raw element pointer.
uint8_t dims = readUint8(vm);
if (!validArrayDims(vm, dims)) {
@@ -2260,7 +1974,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- indices[d] = (int32_t)basValToNumber(idxVal);
+ indices[d] = basValToInt32(idxVal);
basValRelease(&idxVal);
}
@@ -2284,10 +1998,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_SUBSCRIPT_RANGE;
}
+ // The popped arrRef's reference count transfers to the REF.
BasValueT ref;
- ref.type = BAS_TYPE_REF;
- ref.refVal = &arrRef.arrVal->elements[flatIdx];
- basValRelease(&arrRef);
+ ref.type = BAS_TYPE_ELEM_REF;
+ ref.elemRef.arr = arrRef.arrVal;
+ ref.elemRef.idx = flatIdx;
if (!push(vm, ref)) {
return BAS_VM_STACK_OVERFLOW;
@@ -2321,7 +2036,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- indices[d] = (int32_t)basValToNumber(idxVal);
+ indices[d] = basValToInt32(idxVal);
basValRelease(&idxVal);
}
@@ -2349,8 +2064,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_SUBSCRIPT_RANGE;
}
- basValRelease(&arrRef.arrVal->elements[flatIdx]);
- arrRef.arrVal->elements[flatIdx] = storeVal;
+ storeValue(&arrRef.arrVal->elements[flatIdx], storeVal);
basValRelease(&arrRef);
break;
}
@@ -2381,7 +2095,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
- indices[d] = (int32_t)basValToNumber(idxVal);
+ indices[d] = basValToInt32(idxVal);
basValRelease(&idxVal);
}
@@ -2426,73 +2140,37 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_ERROR;
}
- basValRelease(&elem->udtVal->fields[fieldIdx]);
- elem->udtVal->fields[fieldIdx] = storeVal;
+ storeValue(&elem->udtVal->fields[fieldIdx], storeVal);
basValRelease(&arrRef);
break;
}
case OP_REDIM: {
- uint8_t dims = readUint8(vm);
- uint8_t preserve = readUint8(vm);
+ uint8_t dims = readUint8(vm);
+ uint8_t preserve = readUint8(vm);
+ uint8_t elementType = readUint8(vm);
- if (!validArrayDims(vm, dims)) {
- return BAS_VM_ERROR;
+ // Same stack layout as OP_DIM_ARRAY (bounds, then UDT typeId and
+ // fieldCount), with the old array reference underneath.
+ BasArrayT *newArr;
+ BasVmResultE rc = allocArray(vm, dims, elementType, &newArr);
+
+ if (rc != BAS_VM_OK) {
+ return rc;
}
- int32_t lbounds[BAS_ARRAY_MAX_DIMS];
- int32_t ubounds[BAS_ARRAY_MAX_DIMS];
-
- for (int32_t d = dims - 1; d >= 0; d--) {
- BasValueT ubVal;
- BasValueT lbVal;
-
- if (!pop(vm, &ubVal) || !pop(vm, &lbVal)) {
- return BAS_VM_STACK_UNDERFLOW;
- }
-
- ubounds[d] = (int32_t)basValToNumber(ubVal);
- lbounds[d] = (int32_t)basValToNumber(lbVal);
- basValRelease(&ubVal);
- basValRelease(&lbVal);
- }
-
- // Pop old array reference
BasValueT oldRef;
if (!pop(vm, &oldRef)) {
+ basArrayUnref(newArr);
return BAS_VM_STACK_UNDERFLOW;
}
- uint8_t elementType = BAS_TYPE_INTEGER;
-
- if (oldRef.type == BAS_TYPE_ARRAY && oldRef.arrVal) {
- elementType = oldRef.arrVal->elementType;
- }
-
- BasArrayT *newArr = basArrayNew(dims, lbounds, ubounds, elementType);
-
- if (!newArr) {
- basValRelease(&oldRef);
- runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory in REDIM");
- return BAS_VM_OUT_OF_MEMORY;
- }
-
- // Copy old elements if PRESERVE
- if (preserve && oldRef.type == BAS_TYPE_ARRAY && oldRef.arrVal) {
- int32_t copyCount = oldRef.arrVal->totalElements;
-
- if (copyCount > newArr->totalElements) {
- copyCount = newArr->totalElements;
- }
-
- for (int32_t i = 0; i < copyCount; i++) {
- // basArrayNew pre-initialized STRING elements with an
- // empty-string ref; release it before overwriting so
- // the copy doesn't drop that reference on the floor.
- basValRelease(&newArr->elements[i]);
- newArr->elements[i] = basValCopy(oldRef.arrVal->elements[i]);
- }
+ // PRESERVE copies the elements whose subscripts exist in both
+ // shapes, element by element, so a multi-dimensional array with
+ // a changed row stride keeps every value at its own subscript.
+ if (preserve && oldRef.type == BAS_TYPE_ARRAY && oldRef.arrVal && oldRef.arrVal->dims == newArr->dims) {
+ preserveElements(oldRef.arrVal, newArr);
}
basValRelease(&oldRef);
@@ -2502,7 +2180,6 @@ BasVmResultE basVmStep(BasVmT *vm) {
arrVal.arrVal = newArr;
if (!push(vm, arrVal)) {
- basArrayFree(newArr);
return BAS_VM_STACK_OVERFLOW;
}
@@ -2529,65 +2206,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
break;
}
- case OP_LBOUND: {
- uint8_t dim = readUint8(vm);
- BasValueT arrRef;
-
- if (!pop(vm, &arrRef)) {
- return BAS_VM_STACK_UNDERFLOW;
- }
-
- if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) {
- basValRelease(&arrRef);
- runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not an array");
- return BAS_VM_TYPE_MISMATCH;
- }
-
- if (dim < 1 || dim > (uint8_t)arrRef.arrVal->dims) {
- basValRelease(&arrRef);
- runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid dimension for LBOUND");
- return BAS_VM_SUBSCRIPT_RANGE;
- }
-
- int32_t lb = arrRef.arrVal->lbound[dim - 1];
- basValRelease(&arrRef);
-
- if (!push(vm, basValLong(lb))) {
- return BAS_VM_STACK_OVERFLOW;
- }
-
- break;
- }
-
- case OP_UBOUND: {
- uint8_t dim = readUint8(vm);
- BasValueT arrRef;
-
- if (!pop(vm, &arrRef)) {
- return BAS_VM_STACK_UNDERFLOW;
- }
-
- if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) {
- basValRelease(&arrRef);
- runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not an array");
- return BAS_VM_TYPE_MISMATCH;
- }
-
- if (dim < 1 || dim > (uint8_t)arrRef.arrVal->dims) {
- basValRelease(&arrRef);
- runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid dimension for UBOUND");
- return BAS_VM_SUBSCRIPT_RANGE;
- }
-
- int32_t ub = arrRef.arrVal->ubound[dim - 1];
- basValRelease(&arrRef);
-
- if (!push(vm, basValLong(ub))) {
- return BAS_VM_STACK_OVERFLOW;
- }
-
- break;
- }
+ case OP_LBOUND:
+ case OP_UBOUND:
+ return boundOp(vm, op);
case OP_LOAD_FIELD: {
uint16_t fieldIdx = readUint16(vm);
@@ -2613,7 +2234,6 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&udtRef);
if (!push(vm, fieldVal)) {
- basValRelease(&fieldVal);
return BAS_VM_STACK_OVERFLOW;
}
@@ -2649,8 +2269,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_ERROR;
}
- basValRelease(&udtRef.udtVal->fields[fieldIdx]);
- udtRef.udtVal->fields[fieldIdx] = storeVal;
+ storeValue(&udtRef.udtVal->fields[fieldIdx], storeVal);
basValRelease(&udtRef);
break;
}
@@ -2660,7 +2279,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
// ============================================================
case OP_READ_DATA: {
- if (!vm->module || vm->dataPtr >= vm->module->dataCount) {
+ if (vm->dataPtr >= vm->module->dataCount) {
runtimeError(vm, BAS_ERR_OUT_OF_DATA, "Out of DATA");
return BAS_VM_ERROR;
}
@@ -2685,87 +2304,34 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT fmtVal;
BasValueT val;
- if (!pop(vm, &fmtVal) || !pop(vm, &val)) {
+ if (!popArgs(vm, 2, &fmtVal, &val)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT fmtStr = basValToString(fmtVal);
basValRelease(&fmtVal);
- const char *fmt = fmtStr.strVal->data;
- int32_t fmtLen = fmtStr.strVal->len;
- double n = basValToNumber(val);
+ const char *fmt = fmtStr.strVal->data;
+ int32_t fmtLen = fmtStr.strVal->len;
+ double n = basValToNumber(val);
basValRelease(&val);
char buf[BAS_FORMAT_BUF_SIZE];
buf[0] = '\0';
- // Check for "percent" format
- bool isPercent = false;
- if (fmtLen == 7) {
- isPercent = true;
- const char *pct = "PERCENT";
- for (int32_t i = 0; i < 7; i++) {
- if (toupper((unsigned char)fmt[i]) != pct[i]) {
- isPercent = false;
- break;
- }
- }
- }
- if (isPercent) {
+ if (strcasecmp(fmt, BAS_FORMAT_PERCENT) == 0) {
snprintf(buf, sizeof(buf), "%.0f%%", n * 100.0);
} else {
- // Count format characters
- int32_t hashBefore = 0;
- int32_t zeroBefore = 0;
- int32_t hashAfter = 0;
- int32_t zeroAfter = 0;
- bool hasDecimal = false;
- bool hasComma = false;
- bool plusStart = false;
- bool plusEnd = false;
- bool minusEnd = false;
-
- for (int32_t i = 0; i < fmtLen; i++) {
- if (fmt[i] == '+' && i == 0) {
- plusStart = true;
- } else if (fmt[i] == '+' && i == fmtLen - 1) {
- plusEnd = true;
- } else if (fmt[i] == '-' && i == fmtLen - 1) {
- minusEnd = true;
- } else if (fmt[i] == '.') {
- hasDecimal = true;
- } else if (fmt[i] == ',') {
- hasComma = true;
- } else if (fmt[i] == '#') {
- if (hasDecimal) {
- hashAfter++;
- } else {
- hashBefore++;
- }
- } else if (fmt[i] == '0') {
- if (hasDecimal) {
- zeroAfter++;
- } else {
- zeroBefore++;
- }
- }
- }
-
- // Format via the shared bounded helper.
BasNumFormatT f;
+ bool sciNotation;
- f.hasDecimal = hasDecimal;
- f.hasComma = hasComma;
- f.plusAtStart = plusStart;
- f.plusAtEnd = plusEnd;
- f.minusAtEnd = minusEnd;
+ parseNumFormat(fmt, fmtLen, &f, &sciNotation);
+
+ // FORMAT$ has no ** / $$ fills; digit positions before the
+ // number are zero-filled whenever a 0 placeholder appears.
f.asteriskFill = false;
f.dollarFloat = false;
- f.zeroPad = (zeroBefore > 0);
f.formatPad = true;
- f.digitsBefore = hashBefore + zeroBefore;
- f.digitsAfter = hashAfter + zeroAfter;
formatNumber(n, &f, buf, sizeof(buf));
}
@@ -2814,7 +2380,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
case OP_COMPARE_MODE: {
uint8_t mode = readUint8(vm);
- vm->compareTextMode = (mode != 0);
+ vm->compareTextMode = (mode == BAS_COMPARE_MODE_TEXT);
break;
}
@@ -2823,12 +2389,18 @@ BasVmResultE basVmStep(BasVmT *vm) {
// ============================================================
case OP_END:
- vm->running = false;
- vm->ended = true;
- return BAS_VM_HALTED;
-
case OP_HALT:
+ // A handler that ends the program has dealt with the error:
+ // clear it so the host does not report a trapped error as an
+ // unhandled one.
+ if (vm->inErrorHandler) {
+ vm->inErrorHandler = false;
+ vm->errorNumber = 0;
+ vm->errorMsg[0] = '\0';
+ }
+
vm->running = false;
+ vm->ended = vm->ended || (op == OP_END);
return BAS_VM_HALTED;
// ============================================================
@@ -2839,21 +2411,25 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT propNameVal;
BasValueT ctrlRefVal;
- if (!pop(vm, &propNameVal) || !pop(vm, &ctrlRefVal)) {
+ if (!popArgs(vm, 2, &propNameVal, &ctrlRefVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
+ BasValueT result = basValInteger(0);
+
if (vm->ui.getProp && ctrlRefVal.type == BAS_TYPE_OBJECT) {
BasValueT sv = basValToString(propNameVal);
- BasValueT result = vm->ui.getProp(vm->ui.ctx, ctrlRefVal.objVal, sv.strVal->data);
+ result = vm->ui.getProp(vm->ui.ctx, ctrlRefVal.objVal, sv.strVal->data);
basValRelease(&sv);
- push(vm, result);
- } else {
- push(vm, basValInteger(0));
}
basValRelease(&propNameVal);
basValRelease(&ctrlRefVal);
+
+ if (!push(vm, result)) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
@@ -2862,7 +2438,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT propNameVal;
BasValueT ctrlRefVal;
- if (!pop(vm, &value) || !pop(vm, &propNameVal) || !pop(vm, &ctrlRefVal)) {
+ if (!popArgs(vm, 3, &value, &propNameVal, &ctrlRefVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -2936,13 +2512,12 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_STACK_UNDERFLOW;
}
+ BasValueT result = basValInteger(0);
+
if (vm->ui.callMethod && ctrlRefVal.type == BAS_TYPE_OBJECT) {
BasValueT sv = basValToString(methodNameVal);
- BasValueT result = vm->ui.callMethod(vm->ui.ctx, ctrlRefVal.objVal, sv.strVal->data, args, argCount);
+ result = vm->ui.callMethod(vm->ui.ctx, ctrlRefVal.objVal, sv.strVal->data, args, argCount);
basValRelease(&sv);
- push(vm, result);
- } else {
- push(vm, basValInteger(0));
}
for (int32_t i = 0; i < argCount; i++) {
@@ -2951,6 +2526,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&methodNameVal);
basValRelease(&ctrlRefVal);
+
+ if (!push(vm, result)) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
@@ -2975,7 +2555,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
basValRelease(&nameVal);
- push(vm, basValObject(formRef));
+
+ if (!push(vm, basValObject(formRef))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
@@ -3031,11 +2615,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT flagsVal;
BasValueT msgVal;
- if (!pop(vm, &titleVal) || !pop(vm, &flagsVal) || !pop(vm, &msgVal)) {
+ if (!popArgs(vm, 3, &titleVal, &flagsVal, &msgVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t flags = (int32_t)basValToNumber(flagsVal);
+ int32_t flags = basValToInt32(flagsVal);
int32_t result = 1; // default OK
if (vm->ui.msgBox) {
@@ -3049,7 +2633,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&titleVal);
basValRelease(&flagsVal);
basValRelease(&msgVal);
- push(vm, basValInteger((int16_t)result));
+
+ if (!push(vm, basValInteger((int16_t)result))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
@@ -3058,7 +2646,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT titleVal;
BasValueT promptVal;
- if (!pop(vm, &defaultVal) || !pop(vm, &titleVal) || !pop(vm, &promptVal)) {
+ if (!popArgs(vm, 3, &defaultVal, &titleVal, &promptVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -3078,20 +2666,18 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&titleVal);
basValRelease(&promptVal);
- if (result) {
- BasValueT rv;
- rv.type = BAS_TYPE_STRING;
- rv.strVal = result;
- push(vm, rv);
- } else {
- push(vm, basValStringFromC(""));
+ if (!push(vm, strValue(result ? result : basStringRef(basEmptyString)))) {
+ return BAS_VM_STACK_OVERFLOW;
}
break;
}
case OP_ME_REF:
- push(vm, basValObject(vm->currentForm));
+ if (!push(vm, basValObject(vm->currentForm))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
case OP_CREATE_CTRL: {
@@ -3099,7 +2685,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT typeVal;
BasValueT formVal;
- if (!pop(vm, &nameVal) || !pop(vm, &typeVal) || !pop(vm, &formVal)) {
+ if (!popArgs(vm, 3, &nameVal, &typeVal, &formVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -3116,7 +2702,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&nameVal);
basValRelease(&typeVal);
basValRelease(&formVal);
- push(vm, basValObject(ctrlRef));
+
+ if (!push(vm, basValObject(ctrlRef))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
@@ -3127,7 +2717,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT typeVal;
BasValueT formVal;
- if (!pop(vm, &parentVal) || !pop(vm, &nameVal) || !pop(vm, &typeVal) || !pop(vm, &formVal)) {
+ if (!popArgs(vm, 4, &parentVal, &nameVal, &typeVal, &formVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -3146,7 +2736,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&nameVal);
basValRelease(&typeVal);
basValRelease(&formVal);
- push(vm, basValObject(ctrlRef));
+
+ if (!push(vm, basValObject(ctrlRef))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
@@ -3154,7 +2748,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT nameVal;
BasValueT formVal;
- if (!pop(vm, &nameVal) || !pop(vm, &formVal)) {
+ if (!popArgs(vm, 2, &nameVal, &formVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -3170,25 +2764,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&nameVal);
basValRelease(&formVal);
- push(vm, basValObject(ctrlRef));
- break;
- }
- case OP_CTRL_REF: {
- uint16_t nameIdx = readUint16(vm);
- const char *ctrlName = "";
-
- if (nameIdx < (uint16_t)vm->module->constCount) {
- ctrlName = vm->module->constants[nameIdx]->data;
+ if (!push(vm, basValObject(ctrlRef))) {
+ return BAS_VM_STACK_OVERFLOW;
}
- void *ctrlRef = NULL;
-
- if (vm->ui.findCtrl && vm->currentForm) {
- ctrlRef = vm->ui.findCtrl(vm->ui.ctx, vm->currentForm, ctrlName);
- }
-
- push(vm, basValObject(ctrlRef));
break;
}
@@ -3198,7 +2778,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT nameVal;
BasValueT formVal;
- if (!pop(vm, &idxVal) || !pop(vm, &nameVal) || !pop(vm, &formVal)) {
+ if (!popArgs(vm, 3, &idxVal, &nameVal, &formVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -3206,7 +2786,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
if (vm->ui.findCtrlIdx) {
void *formRef = (formVal.type == BAS_TYPE_OBJECT) ? formVal.objVal : vm->currentForm;
- int32_t index = (int32_t)basValToNumber(idxVal);
+ int32_t index = basValToInt32(idxVal);
BasValueT sv = basValToString(nameVal);
ctrlRef = vm->ui.findCtrlIdx(vm->ui.ctx, formRef, sv.strVal->data, index);
basValRelease(&sv);
@@ -3215,7 +2795,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&idxVal);
basValRelease(&nameVal);
basValRelease(&formVal);
- push(vm, basValObject(ctrlRef));
+
+ if (!push(vm, basValObject(ctrlRef))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
@@ -3225,7 +2809,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT widthVal;
BasValueT nameVal;
- if (!pop(vm, &heightVal) || !pop(vm, &widthVal) || !pop(vm, &nameVal)) {
+ if (!popArgs(vm, 3, &heightVal, &widthVal, &nameVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -3233,8 +2817,8 @@ BasVmResultE basVmStep(BasVmT *vm) {
if (vm->ui.createForm) {
BasValueT sv = basValToString(nameVal);
- int32_t w = (int32_t)basValToNumber(widthVal);
- int32_t h = (int32_t)basValToNumber(heightVal);
+ int32_t w = basValToInt32(widthVal);
+ int32_t h = basValToInt32(heightVal);
formRef = vm->ui.createForm(vm->ui.ctx, sv.strVal->data, w, h);
basValRelease(&sv);
}
@@ -3242,7 +2826,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&nameVal);
basValRelease(&widthVal);
basValRelease(&heightVal);
- push(vm, basValObject(formRef));
+
+ if (!push(vm, basValObject(formRef))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
@@ -3252,7 +2840,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT eventVal;
BasValueT ctrlVal;
- if (!pop(vm, &handlerVal) || !pop(vm, &eventVal) || !pop(vm, &ctrlVal)) {
+ if (!popArgs(vm, 3, &handlerVal, &eventVal, &ctrlVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -3275,7 +2863,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT nameVal;
BasValueT formVal;
- if (!pop(vm, &nameVal) || !pop(vm, &formVal)) {
+ if (!popArgs(vm, 2, &nameVal, &formVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -3302,10 +2890,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_ERROR;
}
- BasValueT fv = basValCopy(vm->currentFormVars[idx]);
-
- if (!push(vm, fv)) {
- basValRelease(&fv);
+ if (!push(vm, basValCopy(vm->currentFormVars[idx]))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -3326,8 +2911,7 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_ERROR;
}
- basValRelease(&vm->currentFormVars[idx]);
- vm->currentFormVars[idx] = val;
+ storeValue(&vm->currentFormVars[idx], val);
break;
}
@@ -3342,7 +2926,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
BasValueT ref;
ref.type = BAS_TYPE_REF;
ref.refVal = &vm->currentFormVars[idx];
- push(vm, ref);
+
+ if (!push(vm, ref)) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
@@ -3395,10 +2983,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
}
- char msg[256];
- const char *fn = funcNameIdx < (uint16_t)vm->module->constCount
- ? vm->module->constants[funcNameIdx]->data : "?";
- snprintf(msg, sizeof(msg), "External function not found: %s", fn);
+ char msg[BAS_VM_ERROR_MSG_LEN];
+
+ snprintf(msg, sizeof(msg), "External function not found: %s", funcName[0] ? funcName : "?");
runtimeError(vm, BAS_ERR_EXTERNAL_NOT_FOUND, msg);
return BAS_VM_ERROR;
}
@@ -3459,12 +3046,15 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
// Push return value (void functions still push a dummy 0)
- push(vm, result);
+ if (!push(vm, result)) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
// ============================================================
- // SQL database operations
+ // Debugger statement boundaries
// ============================================================
case OP_LINE: {
@@ -3517,27 +3107,29 @@ BasVmResultE basVmStep(BasVmT *vm) {
break;
}
- case OP_APP_PATH: {
- push(vm, basValStringFromC(vm->appPath));
- break;
- }
-
- case OP_APP_CONFIG: {
- push(vm, basValStringFromC(vm->appConfig));
- break;
- }
-
+ case OP_APP_PATH:
+ case OP_APP_CONFIG:
case OP_APP_DATA: {
- push(vm, basValStringFromC(vm->appData));
+ const char *path = (op == OP_APP_PATH) ? vm->appPath : (op == OP_APP_CONFIG) ? vm->appConfig : vm->appData;
+
+ if (!push(vm, basValStringFromC(path))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
case OP_INI_READ: {
// Stack: file, section, key, default -> result string
- BasValueT defVal, keyVal, secVal, fileVal;
- if (!pop(vm, &defVal) || !pop(vm, &keyVal) || !pop(vm, &secVal) || !pop(vm, &fileVal)) {
+ BasValueT defVal;
+ BasValueT keyVal;
+ BasValueT secVal;
+ BasValueT fileVal;
+
+ if (!popArgs(vm, 4, &defVal, &keyVal, &secVal, &fileVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
+
BasStringT *fileStr = basValFormatString(fileVal);
BasStringT *secStr = basValFormatString(secVal);
BasStringT *keyStr = basValFormatString(keyVal);
@@ -3588,7 +3180,8 @@ BasVmResultE basVmStep(BasVmT *vm) {
fclose(fp);
}
- push(vm, basValStringFromC(result));
+ BasValueT resultVal = basValStringFromC(result);
+
basStringUnref(fileStr);
basStringUnref(secStr);
basStringUnref(keyStr);
@@ -3597,15 +3190,25 @@ BasVmResultE basVmStep(BasVmT *vm) {
basValRelease(&secVal);
basValRelease(&keyVal);
basValRelease(&defVal);
+
+ if (!push(vm, resultVal)) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
break;
}
case OP_INI_WRITE: {
// Stack: file, section, key, value
- BasValueT valVal, keyVal, secVal, fileVal;
- if (!pop(vm, &valVal) || !pop(vm, &keyVal) || !pop(vm, &secVal) || !pop(vm, &fileVal)) {
+ BasValueT valVal;
+ BasValueT keyVal;
+ BasValueT secVal;
+ BasValueT fileVal;
+
+ if (!popArgs(vm, 4, &valVal, &keyVal, &secVal, &fileVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
+
BasStringT *fileStr = basValFormatString(fileVal);
BasStringT *secStr = basValFormatString(secVal);
BasStringT *keyStr = basValFormatString(keyVal);
@@ -3748,6 +3351,124 @@ void basVmStepOver(BasVmT *vm) {
}
+// Shared body of OP_DIM_ARRAY / OP_REDIM: pops the UDT typeId/fieldCount
+// pair (UDT element type only) and the (lbound, ubound) pairs for each
+// dimension, allocates the array and seeds UDT elements with fresh
+// instances. On success *outArr holds a refCount-1 array.
+static BasVmResultE allocArray(BasVmT *vm, int32_t dims, uint8_t elementType, BasArrayT **outArr) {
+ if (!validArrayDims(vm, dims)) {
+ return BAS_VM_ERROR;
+ }
+
+ int32_t udtTypeId = -1;
+ int32_t udtFieldCnt = 0;
+
+ if (elementType == BAS_TYPE_UDT) {
+ BasValueT fcVal;
+ BasValueT tiVal;
+
+ if (!popArgs(vm, 2, &fcVal, &tiVal)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ udtFieldCnt = basValToInt32(fcVal);
+ udtTypeId = basValToInt32(tiVal);
+ basValRelease(&fcVal);
+ basValRelease(&tiVal);
+ }
+
+ int32_t lbounds[BAS_ARRAY_MAX_DIMS];
+ int32_t ubounds[BAS_ARRAY_MAX_DIMS];
+
+ // Pop bounds in reverse order (last dim first)
+ for (int32_t d = dims - 1; d >= 0; d--) {
+ BasValueT ubVal;
+ BasValueT lbVal;
+
+ if (!popArgs(vm, 2, &ubVal, &lbVal)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ ubounds[d] = basValToInt32(ubVal);
+ lbounds[d] = basValToInt32(lbVal);
+ basValRelease(&ubVal);
+ basValRelease(&lbVal);
+ }
+
+ BasArrayT *arr = basArrayNew(dims, lbounds, ubounds, elementType);
+
+ if (!arr) {
+ runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory allocating array");
+ return BAS_VM_OUT_OF_MEMORY;
+ }
+
+ // Initialize UDT array elements with proper UDT instances
+ if (elementType == BAS_TYPE_UDT && udtTypeId >= 0) {
+ for (int32_t i = 0; i < arr->totalElements; i++) {
+ BasUdtT *udt = basUdtNew(udtTypeId, udtFieldCnt);
+
+ if (!udt) {
+ basArrayFree(arr);
+ runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory allocating TYPE array elements");
+ return BAS_VM_OUT_OF_MEMORY;
+ }
+
+ arr->elements[i].type = BAS_TYPE_UDT;
+ arr->elements[i].udtVal = udt;
+ }
+ }
+
+ *outArr = arr;
+ return BAS_VM_OK;
+}
+
+
+// OP_LBOUND / OP_UBOUND: [uint8 dim] array ref on stack -> LONG bound.
+static BasVmResultE boundOp(BasVmT *vm, uint8_t op) {
+ uint8_t dim = readUint8(vm);
+ BasValueT arrRef;
+
+ if (!pop(vm, &arrRef)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) {
+ basValRelease(&arrRef);
+ runtimeError(vm, BAS_ERR_TYPE_MISMATCH, "Not an array");
+ return BAS_VM_TYPE_MISMATCH;
+ }
+
+ if (dim < 1 || dim > (uint8_t)arrRef.arrVal->dims) {
+ basValRelease(&arrRef);
+ runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, (op == OP_LBOUND) ? "Invalid dimension for LBOUND" : "Invalid dimension for UBOUND");
+ return BAS_VM_SUBSCRIPT_RANGE;
+ }
+
+ int32_t bound = (op == OP_LBOUND) ? arrRef.arrVal->lbound[dim - 1] : arrRef.arrVal->ubound[dim - 1];
+ basValRelease(&arrRef);
+
+ if (!push(vm, basValLong(bound))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
+ return BAS_VM_OK;
+}
+
+
+// Clamps an RGB channel value into [0, BAS_RGB_COMPONENT_MAX].
+static int32_t clampComponent(int32_t c) {
+ if (c < 0) {
+ return 0;
+ }
+
+ if (c > BAS_RGB_COMPONENT_MAX) {
+ return BAS_RGB_COMPONENT_MAX;
+ }
+
+ return c;
+}
+
+
static BasCallFrameT *currentFrame(BasVmT *vm) {
if (vm->callDepth <= 0) {
// Module-level: use callStack[0] as implicit main frame
@@ -3768,6 +3489,29 @@ static void defaultPrint(void *ctx, const char *text, bool newline) {
}
+// Resolves a variable slot through any ByRef indirection it holds: a plain
+// slot is returned as-is, a BAS_TYPE_REF yields the referenced slot, and a
+// BAS_TYPE_ELEM_REF yields the array element it names (NULL if the index
+// no longer exists in that array).
+static BasValueT *derefSlot(BasValueT *slot) {
+ if (slot->type == BAS_TYPE_REF) {
+ return slot->refVal;
+ }
+
+ if (slot->type == BAS_TYPE_ELEM_REF) {
+ BasArrayT *arr = slot->elemRef.arr;
+
+ if (!arr || slot->elemRef.idx < 0 || slot->elemRef.idx >= arr->totalElements) {
+ return NULL;
+ }
+
+ return &arr->elements[slot->elemRef.idx];
+ }
+
+ return slot;
+}
+
+
// Dir$ state lives in BasVmT (vm->dirHandle/dirPattern/dirPath) so that a
// second embedded VM cannot close another VM's in-progress enumeration.
static void dirClose(BasVmT *vm) {
@@ -3796,27 +3540,93 @@ static const char *dirNext(BasVmT *vm) {
}
-// Lowest forStack index the currently-executing call frame may unwind
-// to. FOR frames below this belong to callers and must never be
-// touched by NEXT mismatch recovery or FOR re-entry cleanup.
-static int32_t forStackFloor(BasVmT *vm) {
- if (vm->callDepth > 0) {
- return vm->callStack[vm->callDepth - 1].savedForDepth;
+// ON ERROR dispatch shared by basVmRun and runSubLoop. Given a failing
+// step result, unwinds call frames (never below floorDepth) until it finds
+// one whose ON ERROR GOTO registered a handler, truncates the eval stack to
+// the failing statement's boundary, records the RESUME / RESUME NEXT
+// targets and points pc at the handler. Returns true when execution should
+// continue in the handler, false when the error is untrapped (frames
+// without a handler have already been discarded).
+//
+// Without the unwind an error raised inside a called SUB would fire the
+// outer SUB's handler but OP_RET would then pop the inner frame and resume
+// after the call site (effectively RESUME NEXT semantics), which is not
+// what ON ERROR promises.
+static bool dispatchError(BasVmT *vm, BasVmResultE result, int32_t floorDepth, int32_t fallbackPc) {
+ if (vm->inErrorHandler || result == BAS_VM_HALTED || result == BAS_VM_BAD_OPCODE) {
+ return false;
}
- return 0;
+ // Eval/call stack exhaustion bypasses runtimeError(), so give the
+ // handler a real ERR value instead of 0.
+ if (result == BAS_VM_STACK_OVERFLOW || result == BAS_VM_STACK_UNDERFLOW || result == BAS_VM_CALL_OVERFLOW) {
+ runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of stack space");
+ }
+
+ int32_t target = 0;
+ int32_t resumePc = -1;
+
+ while (vm->callDepth > floorDepth) {
+ BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1];
+
+ if (frame->errorHandler != 0) {
+ target = frame->errorHandler;
+ break;
+ }
+
+ // No handler on this frame -- discard it and keep unwinding.
+ // Frame-local values need releasing.
+ for (int32_t li = 0; li < frame->localCount; li++) {
+ basValRelease(&frame->locals[li]);
+ }
+
+ // Restore the caller's context: drop the callee's FOR frames and
+ // bring back the statement boundary of the calling statement.
+ // RESUME must land in the handler frame's code, never in the
+ // discarded callee's.
+ forStackTrim(vm, frame->savedForDepth);
+ vm->stmtPc = frame->savedStmtPc;
+ vm->stmtSp = frame->savedStmtSp;
+ resumePc = frame->returnPc;
+
+ vm->callDepth--;
+ }
+
+ if (target == 0) {
+ return false;
+ }
+
+ // Release any operands the failed statement half-pushed before the
+ // error fired, then truncate the eval stack back to the statement
+ // boundary so trapped errors in a loop don't leak values and grow sp
+ // without bound.
+ if (vm->sp > vm->stmtSp) {
+ for (int32_t si = vm->stmtSp; si < vm->sp; si++) {
+ basValRelease(&vm->stack[si]);
+ }
+
+ vm->sp = vm->stmtSp;
+ }
+
+ // The eval stack is now at the failing statement's boundary, so RESUME
+ // must re-run that statement from its start and RESUME NEXT must
+ // continue at the next statement -- resuming mid-statement would pop
+ // values from beneath the truncated boundary. Without statement info
+ // fall back to the call site after an unwind, or to the failing
+ // instruction.
+ int32_t nextPc = nextStatementPc(vm);
+
+ vm->errorPc = (vm->stmtPc >= 0) ? vm->stmtPc : ((resumePc >= 0) ? resumePc : fallbackPc);
+ vm->errorNextPc = (nextPc >= 0) ? nextPc : ((resumePc >= 0) ? resumePc : vm->pc);
+ vm->inErrorHandler = true;
+ vm->pc = target;
+ return true;
}
-// Release FOR-stack entries above newDepth and truncate the stack to it.
-// Used when returning from a call frame, unwinding to an error handler,
-// and discarding stale frames left by EXIT DO / GOTO jumps out of FORs.
-static void forStackTrim(BasVmT *vm, int32_t newDepth) {
- while (vm->forDepth > newDepth) {
- BasForStateT *fs = &vm->forStack[--vm->forDepth];
- basValRelease(&fs->limit);
- basValRelease(&fs->step);
- }
+static BasVmResultE divByZero(BasVmT *vm) {
+ runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero");
+ return BAS_VM_DIV_BY_ZERO;
}
@@ -3824,12 +3634,12 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
BasValueT b;
BasValueT a;
- if (!pop(vm, &b) || !pop(vm, &a)) {
+ if (!popArgs(vm, 2, &b, &a)) {
return BAS_VM_STACK_UNDERFLOW;
}
// VB behavior: + on two strings concatenates
- if ((op == OP_ADD_INT || op == OP_ADD_FLT) && a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) {
+ if (op == OP_ADD_INT && a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) {
BasStringT *sa = a.strVal ? a.strVal : basEmptyString;
BasStringT *sb = b.strVal ? b.strVal : basEmptyString;
BasStringT *cat = basStringConcat(sa, sb);
@@ -3837,10 +3647,10 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
basValRelease(&a);
basValRelease(&b);
- BasValueT result;
- result.type = BAS_TYPE_STRING;
- result.strVal = cat;
- push(vm, result);
+ if (!push(vm, strValue(cat))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
return BAS_VM_OK;
}
@@ -3850,66 +3660,54 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
&& op != OP_DIV_FLT
&& (a.type == BAS_TYPE_INTEGER || a.type == BAS_TYPE_LONG)
&& (b.type == BAS_TYPE_INTEGER || b.type == BAS_TYPE_LONG)) {
- int32_t ia = (a.type == BAS_TYPE_INTEGER) ? (int32_t)a.intVal : a.longVal;
- int32_t ib = (b.type == BAS_TYPE_INTEGER) ? (int32_t)b.intVal : b.longVal;
- int32_t ir = 0;
+ int32_t ia = (a.type == BAS_TYPE_INTEGER) ? (int32_t)a.intVal : a.longVal;
+ int32_t ib = (b.type == BAS_TYPE_INTEGER) ? (int32_t)b.intVal : b.longVal;
+ int64_t wide = 0;
bool handled = true;
switch (op) {
case OP_ADD_INT:
- case OP_ADD_FLT: {
- int64_t t = (int64_t)ia + (int64_t)ib;
- if (t < INT32_MIN || t > INT32_MAX) { handled = false; break; }
- ir = (int32_t)t;
+ wide = (int64_t)ia + (int64_t)ib;
break;
- }
+
case OP_SUB_INT:
- case OP_SUB_FLT: {
- int64_t t = (int64_t)ia - (int64_t)ib;
- if (t < INT32_MIN || t > INT32_MAX) { handled = false; break; }
- ir = (int32_t)t;
+ wide = (int64_t)ia - (int64_t)ib;
break;
- }
+
case OP_MUL_INT:
- case OP_MUL_FLT: {
- int64_t t = (int64_t)ia * (int64_t)ib;
- if (t < INT32_MIN || t > INT32_MAX) { handled = false; break; }
- ir = (int32_t)t;
+ wide = (int64_t)ia * (int64_t)ib;
break;
- }
+
case OP_IDIV_INT:
if (ib == 0) {
- runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero");
- return BAS_VM_DIV_BY_ZERO;
+ return divByZero(vm);
}
- if (ia == INT32_MIN && ib == -1) {
- handled = false;
- break;
- }
- ir = ia / ib;
+
+ wide = (int64_t)ia / (int64_t)ib;
break;
+
case OP_MOD_INT:
if (ib == 0) {
- runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero");
- return BAS_VM_DIV_BY_ZERO;
+ return divByZero(vm);
}
- if (ia == INT32_MIN && ib == -1) {
- ir = 0;
- break;
- }
- ir = ia % ib;
+
+ // INT32_MIN MOD -1 is 0 mathematically; the int64 division
+ // avoids the int32 overflow trap.
+ wide = (int64_t)ia % (int64_t)ib;
break;
+
default:
handled = false;
break;
}
- if (handled) {
- if (ir >= INT16_MIN && ir <= INT16_MAX) {
- push(vm, basValInteger((int16_t)ir));
- } else {
- push(vm, basValLong(ir));
+ if (handled && wide >= INT32_MIN && wide <= INT32_MAX) {
+ int32_t ir = (int32_t)wide;
+
+ if (!push(vm, (ir >= INT16_MIN && ir <= INT16_MAX) ? basValInteger((int16_t)ir) : basValLong(ir))) {
+ return BAS_VM_STACK_OVERFLOW;
}
+
return BAS_VM_OK;
}
}
@@ -3918,8 +3716,8 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
// the result as a float. The parser always emits OP_ADD_INT (the
// VM promotes based on operand types), so the opcode alone can't
// tell us whether this is an integer or floating-point op.
- uint8_t aType = a.type;
- uint8_t bType = b.type;
+ uint8_t aType = a.type;
+ uint8_t bType = b.type;
bool hadFloat = (aType == BAS_TYPE_SINGLE || aType == BAS_TYPE_DOUBLE ||
bType == BAS_TYPE_SINGLE || bType == BAS_TYPE_DOUBLE);
@@ -3932,57 +3730,51 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
switch (op) {
case OP_ADD_INT:
- case OP_ADD_FLT:
result = na + nb;
break;
case OP_SUB_INT:
- case OP_SUB_FLT:
result = na - nb;
break;
case OP_MUL_INT:
- case OP_MUL_FLT:
result = na * nb;
break;
case OP_IDIV_INT:
- if ((int32_t)nb == 0) {
- runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero");
- return BAS_VM_DIV_BY_ZERO;
+ case OP_MOD_INT: {
+ // \ and MOD round their operands to LONG first (11.5 \ 2 = 6,
+ // 7.5 MOD 2 = 0) and raise Overflow when they do not fit.
+ int32_t ia;
+ int32_t ib;
+ BasVmResultE rc = roundedOperand(vm, basValDouble(na), &ia);
+
+ if (rc != BAS_VM_OK) {
+ return rc;
}
- if ((int32_t)na == INT32_MIN && (int32_t)nb == -1) {
- result = -(double)INT32_MIN;
- break;
+ rc = roundedOperand(vm, basValDouble(nb), &ib);
+
+ if (rc != BAS_VM_OK) {
+ return rc;
}
- result = (double)((int32_t)na / (int32_t)nb);
+ if (ib == 0) {
+ return divByZero(vm);
+ }
+
+ result = (op == OP_IDIV_INT) ? (double)((int64_t)ia / (int64_t)ib) : (double)((int64_t)ia % (int64_t)ib);
break;
+ }
case OP_DIV_FLT:
if (nb == 0.0) {
- runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero");
- return BAS_VM_DIV_BY_ZERO;
+ return divByZero(vm);
}
result = na / nb;
break;
- case OP_MOD_INT:
- if ((int32_t)nb == 0) {
- runtimeError(vm, BAS_ERR_DIV_BY_ZERO, "Division by zero");
- return BAS_VM_DIV_BY_ZERO;
- }
-
- if ((int32_t)na == INT32_MIN && (int32_t)nb == -1) {
- result = 0.0;
- break;
- }
-
- result = (double)((int32_t)na % (int32_t)nb);
- break;
-
case OP_POW:
result = pow(na, nb);
break;
@@ -3995,18 +3787,19 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
// Return appropriate type. An op that would normally produce an
// integer result (OP_ADD_INT, etc.) still has to yield a float when
// either operand was a float -- otherwise 1.5 + 2.25 truncates to 3.
- bool intOp = (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT);
+ bool intOp = (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT);
+ BasValueT out;
- if (intOp && !hadFloat) {
- if (result >= (double)INT16_MIN && result <= (double)INT16_MAX) {
- push(vm, basValInteger((int16_t)result));
- } else if (result >= (double)INT32_MIN && result <= (double)INT32_MAX) {
- push(vm, basValLong((int32_t)result));
- } else {
- push(vm, basValDouble(result));
- }
+ if (intOp && !hadFloat && result >= (double)INT16_MIN && result <= (double)INT16_MAX) {
+ out = basValInteger((int16_t)result);
+ } else if (intOp && !hadFloat && result >= (double)INT32_MIN && result <= (double)INT32_MAX) {
+ out = basValLong((int32_t)result);
} else {
- push(vm, basValDouble(result));
+ out = basValDouble(result);
+ }
+
+ if (!push(vm, out)) {
+ return BAS_VM_STACK_OVERFLOW;
}
return BAS_VM_OK;
@@ -4017,7 +3810,7 @@ static BasVmResultE execCompare(BasVmT *vm, uint8_t op) {
BasValueT b;
BasValueT a;
- if (!pop(vm, &b) || !pop(vm, &a)) {
+ if (!popArgs(vm, 2, &b, &a)) {
return BAS_VM_STACK_UNDERFLOW;
}
@@ -4032,22 +3825,46 @@ static BasVmResultE execCompare(BasVmT *vm, uint8_t op) {
} else {
cmp = vm->compareTextMode ? basValCompareCI(a, b) : basValCompare(a, b);
}
+
basValRelease(&a);
basValRelease(&b);
bool result;
switch (op) {
- case OP_CMP_EQ: result = (cmp == 0); break;
- case OP_CMP_NE: result = (cmp != 0); break;
- case OP_CMP_LT: result = (cmp < 0); break;
- case OP_CMP_GT: result = (cmp > 0); break;
- case OP_CMP_LE: result = (cmp <= 0); break;
- case OP_CMP_GE: result = (cmp >= 0); break;
- default: result = false; break;
+ case OP_CMP_EQ:
+ result = (cmp == 0);
+ break;
+
+ case OP_CMP_NE:
+ result = (cmp != 0);
+ break;
+
+ case OP_CMP_LT:
+ result = (cmp < 0);
+ break;
+
+ case OP_CMP_GT:
+ result = (cmp > 0);
+ break;
+
+ case OP_CMP_LE:
+ result = (cmp <= 0);
+ break;
+
+ case OP_CMP_GE:
+ result = (cmp >= 0);
+ break;
+
+ default:
+ result = false;
+ break;
+ }
+
+ if (!push(vm, basValBool(result))) {
+ return BAS_VM_STACK_OVERFLOW;
}
- push(vm, basValBool(result));
return BAS_VM_OK;
}
@@ -4055,7 +3872,16 @@ static BasVmResultE execCompare(BasVmT *vm, uint8_t op) {
static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
switch (op) {
case OP_FILE_OPEN: {
- uint8_t mode = readUint8(vm);
+ uint8_t mode = readUint8(vm);
+ BasValueT recLenVal;
+
+ if (!pop(vm, &recLenVal)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ int32_t recLen = basValToInt32(recLenVal);
+ basValRelease(&recLenVal);
+
int32_t channel;
BasVmResultE chResult = popFileChannel(vm, &channel, false);
@@ -4073,14 +3899,17 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
return chResult;
}
- BasValueT filenameVal;
+ BasValueT fnStr;
- if (!pop(vm, &filenameVal)) {
+ if (!popStringArg(vm, &fnStr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT fnStr = basValToString(filenameVal);
- basValRelease(&filenameVal);
+ if (recLen < 1 || recLen > BAS_VM_MAX_RECORD_LEN) {
+ basValRelease(&fnStr);
+ runtimeError(vm, BAS_ERR_BAD_RECORD_LEN, "Bad record length");
+ return BAS_VM_FILE_ERROR;
+ }
// Close existing file on this channel
if (vm->files[channel].handle) {
@@ -4095,16 +3924,20 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
case BAS_FILE_MODE_INPUT:
modeStr = "rb";
break;
+
case BAS_FILE_MODE_OUTPUT:
modeStr = "wb";
break;
+
case BAS_FILE_MODE_APPEND:
modeStr = "ab";
break;
+
case BAS_FILE_MODE_RANDOM:
case BAS_FILE_MODE_BINARY:
modeStr = "r+b";
break;
+
default:
basValRelease(&fnStr);
runtimeError(vm, BAS_ERR_BAD_FILE_MODE, "Bad file mode");
@@ -4114,13 +3947,13 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
// For RANDOM/BINARY: create file if it doesn't exist, then reopen r+b
if (mode == BAS_FILE_MODE_RANDOM || mode == BAS_FILE_MODE_BINARY) {
FILE *test = fopen(fnStr.strVal->data, "r");
+
if (!test) {
// Create the file
test = fopen(fnStr.strVal->data, "w+b");
- if (test) {
- fclose(test);
- }
- } else {
+ }
+
+ if (test) {
fclose(test);
}
}
@@ -4135,6 +3968,7 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
vm->files[channel].handle = fp;
vm->files[channel].mode = mode;
+ vm->files[channel].recLen = recLen;
break;
}
@@ -4176,16 +4010,15 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
BasStringT *s = basValFormatString(val);
basValRelease(&val);
-
- if (s) {
- fputs(s->data, (FILE *)vm->files[channel].handle);
- basStringUnref(s);
- }
-
+ fputs(s->data, (FILE *)vm->files[channel].handle);
+ basStringUnref(s);
break;
}
- case OP_FILE_INPUT: {
+ case OP_FILE_INPUT:
+ case OP_FILE_LINE_INPUT: {
+ // INPUT # reads one comma-delimited, optionally quoted field;
+ // LINE INPUT # reads the whole line. Neither has a length cap.
int32_t channel;
BasVmResultE chResult = popFileChannel(vm, &channel, true);
@@ -4193,19 +4026,10 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
return chResult;
}
- char buf[1024];
- buf[0] = '\0';
+ FILE *fp = (FILE *)vm->files[channel].handle;
+ BasStringT *s = (op == OP_FILE_INPUT) ? readField(fp) : readLine(fp);
- if (fgets(buf, sizeof(buf), (FILE *)vm->files[channel].handle)) {
- // Strip trailing newline
- int32_t len = (int32_t)strlen(buf);
-
- while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
- buf[--len] = '\0';
- }
- }
-
- if (!push(vm, basValStringFromC(buf))) {
+ if (!push(vm, strValue(s))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -4234,32 +4058,6 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
break;
}
- case OP_FILE_LINE_INPUT: {
- int32_t channel;
- BasVmResultE chResult = popFileChannel(vm, &channel, true);
-
- if (chResult != BAS_VM_OK) {
- return chResult;
- }
-
- char buf[1024];
- buf[0] = '\0';
-
- if (fgets(buf, sizeof(buf), (FILE *)vm->files[channel].handle)) {
- int32_t len = (int32_t)strlen(buf);
-
- while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
- buf[--len] = '\0';
- }
- }
-
- if (!push(vm, basValStringFromC(buf))) {
- return BAS_VM_STACK_OVERFLOW;
- }
-
- break;
- }
-
case OP_FILE_WRITE: {
// Pop value and channel, write value in WRITE format
BasValueT val;
@@ -4281,43 +4079,26 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
if (val.type == BAS_TYPE_STRING) {
// Strings: enclosed in quotes
fputc('"', fp);
+
if (val.strVal) {
- fputs(val.strVal->data, fp);
+ fwrite(val.strVal->data, 1, val.strVal->len, fp);
}
+
fputc('"', fp);
} else {
// Numbers: no leading space (unlike PRINT)
BasStringT *s = basValFormatString(val);
- if (s) {
- // Skip leading space that basValFormatString adds for positive numbers
- const char *text = s->data;
- if (*text == ' ') {
- text++;
- }
- fputs(text, fp);
- basStringUnref(s);
- }
+ fputs(s->data, fp);
+ basStringUnref(s);
}
basValRelease(&val);
break;
}
- case OP_FILE_WRITE_SEP: {
- // Pop channel, write comma separator
- int32_t channel;
- BasVmResultE chResult = popFileChannel(vm, &channel, true);
-
- if (chResult != BAS_VM_OK) {
- return chResult;
- }
-
- fputc(',', (FILE *)vm->files[channel].handle);
- break;
- }
-
+ case OP_FILE_WRITE_SEP:
case OP_FILE_WRITE_NL: {
- // Pop channel, write newline
+ // Pop channel, write the comma separator or record newline
int32_t channel;
BasVmResultE chResult = popFileChannel(vm, &channel, true);
@@ -4325,24 +4106,44 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
return chResult;
}
- fputc('\n', (FILE *)vm->files[channel].handle);
+ fputc((op == OP_FILE_WRITE_SEP) ? ',' : '\n', (FILE *)vm->files[channel].handle);
break;
}
case OP_FILE_GET: {
- // Pop type, recno, channel; read data; push value
+ // Pop type, [current string value], recno, channel; read
+ // data; push value. A STRING target carries its current
+ // value so BINARY mode knows how many bytes to read.
BasValueT typeVal;
- BasValueT recnoVal;
- if (!pop(vm, &typeVal) || !pop(vm, &recnoVal)) {
+ if (!pop(vm, &typeVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t recno = (int32_t)basValToNumber(recnoVal);
- int32_t dataType = (int32_t)basValToNumber(typeVal);
- basValRelease(&recnoVal);
+ int32_t dataType = basValToInt32(typeVal);
+ int32_t strLen = 0;
basValRelease(&typeVal);
+ if (dataType == BAS_TYPE_STRING) {
+ BasValueT curVal;
+
+ if (!pop(vm, &curVal)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ strLen = (curVal.type == BAS_TYPE_STRING && curVal.strVal) ? curVal.strVal->len : 0;
+ basValRelease(&curVal);
+ }
+
+ BasValueT recnoVal;
+
+ if (!pop(vm, &recnoVal)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ int32_t recno = basValToInt32(recnoVal);
+ basValRelease(&recnoVal);
+
int32_t channel;
BasVmResultE chResult = popFileChannel(vm, &channel, true);
@@ -4351,13 +4152,7 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
}
FILE *fp = (FILE *)vm->files[channel].handle;
-
- // Seek to record position if recno > 0
- // (recno is 1-based in QB; 0 means current position)
- if (recno > 0) {
- // For simplicity, use fixed-size records for RANDOM
- fseek(fp, (long)(recno - 1) * BAS_VM_RANDOM_RECORD_SIZE, SEEK_SET);
- }
+ seekRecord(vm, channel, recno);
BasValueT result;
memset(&result, 0, sizeof(result));
@@ -4365,56 +4160,66 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
switch (dataType) {
case BAS_TYPE_INTEGER: {
int16_t val = 0;
- if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ }
+ readRaw(fp, &val, sizeof(val));
result = basValInteger(val);
break;
}
+
case BAS_TYPE_LONG: {
int32_t val = 0;
- if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ }
+ readRaw(fp, &val, sizeof(val));
result = basValLong(val);
break;
}
+
case BAS_TYPE_SINGLE: {
float val = 0.0f;
- if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ }
+ readRaw(fp, &val, sizeof(val));
result = basValSingle(val);
break;
}
+
case BAS_TYPE_DOUBLE: {
double val = 0.0;
- if (fread(&val, sizeof(val), 1, fp) < 1) { /* EOF ok */ }
+ readRaw(fp, &val, sizeof(val));
result = basValDouble(val);
break;
}
+
case BAS_TYPE_STRING: {
- // Read a length-prefixed string (int16 len + data)
- int16_t len = 0;
- if (fread(&len, sizeof(len), 1, fp) < 1) { /* EOF ok */ }
- if (len < 0) {
- len = 0;
+ // BINARY reads as many raw bytes as the variable
+ // already holds (QB semantics); RANDOM records carry
+ // an int16 length prefix written by PUT.
+ int32_t len = strLen;
+
+ if (vm->files[channel].mode != BAS_FILE_MODE_BINARY) {
+ int16_t prefix = 0;
+ readRaw(fp, &prefix, sizeof(prefix));
+ len = (prefix < 0) ? 0 : prefix;
}
+
char *buf = (char *)malloc(len + 1);
+
if (buf) {
- // Terminate at the bytes actually read so a short or
- // truncated record yields a shorter string rather than
- // exposing uninitialized heap past the read.
+ // Size the string by the bytes actually read so a
+ // short or truncated record yields a shorter string
+ // rather than exposing uninitialized heap.
int32_t bytesRead = (int32_t)fread(buf, 1, len, fp);
- buf[bytesRead] = '\0';
- result = basValStringFromC(buf);
+ result = strValue(basStringNew(buf, bytesRead));
free(buf);
} else {
- result = basValStringFromC("");
+ result = basValString(NULL);
}
+
break;
}
+
default:
result = basValInteger(0);
break;
}
if (!push(vm, result)) {
- basValRelease(&result);
return BAS_VM_STACK_OVERFLOW;
}
@@ -4426,11 +4231,11 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
BasValueT val;
BasValueT recnoVal;
- if (!pop(vm, &val) || !pop(vm, &recnoVal)) {
+ if (!popArgs(vm, 2, &val, &recnoVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t recno = (int32_t)basValToNumber(recnoVal);
+ int32_t recno = basValToInt32(recnoVal);
basValRelease(&recnoVal);
int32_t channel;
@@ -4442,10 +4247,7 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
}
FILE *fp = (FILE *)vm->files[channel].handle;
-
- if (recno > 0) {
- fseek(fp, (long)(recno - 1) * BAS_VM_RANDOM_RECORD_SIZE, SEEK_SET);
- }
+ seekRecord(vm, channel, recno);
switch (val.type) {
case BAS_TYPE_INTEGER: {
@@ -4453,36 +4255,54 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
fwrite(&v, sizeof(v), 1, fp);
break;
}
+
case BAS_TYPE_LONG: {
int32_t v = val.longVal;
fwrite(&v, sizeof(v), 1, fp);
break;
}
+
case BAS_TYPE_SINGLE: {
float v = val.sngVal;
fwrite(&v, sizeof(v), 1, fp);
break;
}
+
case BAS_TYPE_DOUBLE: {
double v = val.dblVal;
fwrite(&v, sizeof(v), 1, fp);
break;
}
+
case BAS_TYPE_STRING: {
- // Write length-prefixed string. Clamp before the int16
- // cast so an over-long string can't wrap to a negative
- // prefix that desyncs the length from the payload.
+ // RANDOM records carry an int16 length prefix so GET can
+ // size the string; BINARY writes the raw bytes. Clamp
+ // before the int16 cast so an over-long string can't
+ // wrap to a negative prefix.
int32_t slen = val.strVal ? val.strVal->len : 0;
+
+ if (vm->files[channel].mode == BAS_FILE_MODE_BINARY) {
+ if (slen > 0) {
+ fwrite(val.strVal->data, 1, slen, fp);
+ }
+
+ break;
+ }
+
if (slen > INT16_MAX) {
slen = INT16_MAX;
}
+
int16_t len = (int16_t)slen;
fwrite(&len, sizeof(len), 1, fp);
- if (len > 0 && val.strVal) {
+
+ if (len > 0) {
fwrite(val.strVal->data, 1, len, fp);
}
+
break;
}
+
default: {
int16_t zero = 0;
fwrite(&zero, sizeof(zero), 1, fp);
@@ -4503,7 +4323,7 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t pos = (int32_t)basValToNumber(posVal);
+ int32_t pos = basValToInt32(posVal);
basValRelease(&posVal);
int32_t channel;
@@ -4605,15 +4425,12 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t n = (int32_t)basValToNumber(nVal);
+ int32_t n = basValToInt32(nVal);
basValRelease(&nVal);
- if (n < 0) {
- n = 0;
- }
-
- if (n > INT16_MAX) {
- n = INT16_MAX;
+ if (n < 0 || n > BAS_STRING_FUNC_MAX) {
+ runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
+ return BAS_VM_ERROR;
}
char *buf = (char *)malloc(n + 1);
@@ -4623,18 +4440,11 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
return BAS_VM_OUT_OF_MEMORY;
}
- int32_t bytesRead = (int32_t)fread(buf, 1, n, (FILE *)vm->files[channel].handle);
- buf[bytesRead] = '\0';
-
- BasStringT *s = basStringNew(buf, bytesRead);
+ int32_t bytesRead = (int32_t)fread(buf, 1, n, (FILE *)vm->files[channel].handle);
+ BasStringT *s = basStringNew(buf, bytesRead);
free(buf);
- BasValueT result;
- result.type = BAS_TYPE_STRING;
- result.strVal = s;
-
- if (!push(vm, result)) {
- basStringUnref(s);
+ if (!push(vm, strValue(s))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -4652,174 +4462,140 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
switch (op) {
case OP_FS_KILL: {
- BasValueT fnVal;
+ BasValueT fnStr;
- if (!pop(vm, &fnVal)) {
+ if (!popStringArg(vm, &fnStr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT fnStr = basValToString(fnVal);
- basValRelease(&fnVal);
+ int32_t rc = remove(fnStr.strVal->data);
+ basValRelease(&fnStr);
- if (remove(fnStr.strVal->data) != 0) {
- basValRelease(&fnStr);
+ if (rc != 0) {
runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found");
return BAS_VM_FILE_ERROR;
}
- basValRelease(&fnStr);
break;
}
case OP_FS_NAME: {
- BasValueT newVal;
- BasValueT oldVal;
+ BasValueT newStr;
+ BasValueT oldStr;
- if (!pop(vm, &newVal) || !pop(vm, &oldVal)) {
+ if (!popStringArg(vm, &newStr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT newStr = basValToString(newVal);
- BasValueT oldStr = basValToString(oldVal);
- basValRelease(&newVal);
- basValRelease(&oldVal);
-
- if (rename(oldStr.strVal->data, newStr.strVal->data) != 0) {
- basValRelease(&oldStr);
+ if (!popStringArg(vm, &oldStr)) {
basValRelease(&newStr);
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ int32_t rc = rename(oldStr.strVal->data, newStr.strVal->data);
+ basValRelease(&oldStr);
+ basValRelease(&newStr);
+
+ if (rc != 0) {
runtimeError(vm, BAS_ERR_FILE_EXISTS, "File already exists or rename failed");
return BAS_VM_FILE_ERROR;
}
- basValRelease(&oldStr);
- basValRelease(&newStr);
break;
}
case OP_FS_FILECOPY: {
- BasValueT dstVal;
- BasValueT srcVal;
+ BasValueT dstStr;
+ BasValueT srcStr;
- if (!pop(vm, &dstVal) || !pop(vm, &srcVal)) {
+ if (!popStringArg(vm, &dstStr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT dstStr = basValToString(dstVal);
- BasValueT srcStr = basValToString(srcVal);
- basValRelease(&dstVal);
- basValRelease(&srcVal);
-
- FILE *fin = fopen(srcStr.strVal->data, "rb");
- FILE *fout = NULL;
-
- if (fin) {
- fout = fopen(dstStr.strVal->data, "wb");
+ if (!popStringArg(vm, &srcStr)) {
+ basValRelease(&dstStr);
+ return BAS_VM_STACK_UNDERFLOW;
}
+ bool copied = platformCopyFile(srcStr.strVal->data, dstStr.strVal->data);
+
basValRelease(&srcStr);
basValRelease(&dstStr);
- if (!fin || !fout) {
- if (fin) {
- fclose(fin);
- }
-
- if (fout) {
- fclose(fout);
- }
-
+ if (!copied) {
runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found or cannot create destination");
return BAS_VM_FILE_ERROR;
}
- char buf[4096];
- size_t n;
-
- while ((n = fread(buf, 1, sizeof(buf), fin)) > 0) {
- fwrite(buf, 1, n, fout);
- }
-
- fclose(fin);
- fclose(fout);
break;
}
case OP_FS_MKDIR: {
- BasValueT pathVal;
+ BasValueT pathStr;
- if (!pop(vm, &pathVal)) {
+ if (!popStringArg(vm, &pathStr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT pathStr = basValToString(pathVal);
- basValRelease(&pathVal);
-
#ifdef _WIN32
- int rc = mkdir(pathStr.strVal->data);
+ int32_t rc = mkdir(pathStr.strVal->data);
#else
- int rc = mkdir(pathStr.strVal->data, 0755);
+ int32_t rc = mkdir(pathStr.strVal->data, BAS_MKDIR_MODE);
#endif
+ basValRelease(&pathStr);
+
if (rc != 0) {
- basValRelease(&pathStr);
runtimeError(vm, BAS_ERR_PATH_FILE_ACCESS, "Path/File access error");
return BAS_VM_FILE_ERROR;
}
- basValRelease(&pathStr);
break;
}
case OP_FS_RMDIR: {
- BasValueT pathVal;
+ BasValueT pathStr;
- if (!pop(vm, &pathVal)) {
+ if (!popStringArg(vm, &pathStr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT pathStr = basValToString(pathVal);
- basValRelease(&pathVal);
+ int32_t rc = rmdir(pathStr.strVal->data);
+ basValRelease(&pathStr);
- if (rmdir(pathStr.strVal->data) != 0) {
- basValRelease(&pathStr);
+ if (rc != 0) {
runtimeError(vm, BAS_ERR_PATH_FILE_ACCESS, "Path/File access error");
return BAS_VM_FILE_ERROR;
}
- basValRelease(&pathStr);
break;
}
case OP_FS_CHDIR: {
- BasValueT pathVal;
+ BasValueT pathStr;
- if (!pop(vm, &pathVal)) {
+ if (!popStringArg(vm, &pathStr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT pathStr = basValToString(pathVal);
- basValRelease(&pathVal);
+ int32_t rc = platformChdir(pathStr.strVal->data);
+ basValRelease(&pathStr);
- if (platformChdir(pathStr.strVal->data) != 0) {
- basValRelease(&pathStr);
+ if (rc != 0) {
runtimeError(vm, BAS_ERR_PATH_NOT_FOUND, "Path not found");
return BAS_VM_FILE_ERROR;
}
- basValRelease(&pathStr);
break;
}
case OP_FS_CHDRIVE: {
- BasValueT driveVal;
+ BasValueT driveStr;
- if (!pop(vm, &driveVal)) {
+ if (!popStringArg(vm, &driveStr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT driveStr = basValToString(driveVal);
- basValRelease(&driveVal);
-
// Drive letters are a DOS-ism; platformChdir is a no-op on
// platforms without them.
if (driveStr.strVal->data[0]) {
@@ -4841,13 +4617,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
cwd[0] = '\0';
}
- BasStringT *s = basStringNew(cwd, (int32_t)strlen(cwd));
- BasValueT result;
- result.type = BAS_TYPE_STRING;
- result.strVal = s;
-
- if (!push(vm, result)) {
- basStringUnref(s);
+ if (!push(vm, basValStringFromC(cwd))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -4855,15 +4625,12 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
}
case OP_FS_DIR: {
- BasValueT patVal;
+ BasValueT patStr;
- if (!pop(vm, &patVal)) {
+ if (!popStringArg(vm, &patStr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT patStr = basValToString(patVal);
- basValRelease(&patVal);
-
dirClose(vm);
// Split pattern into directory + filename pattern
@@ -4899,15 +4666,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
vm->dirHandle = opendir(vm->dirPath);
- const char *match = dirNext(vm);
- const char *text = match ? match : "";
- BasStringT *s = basStringNew(text, (int32_t)strlen(text));
- BasValueT result;
- result.type = BAS_TYPE_STRING;
- result.strVal = s;
-
- if (!push(vm, result)) {
- basStringUnref(s);
+ if (!push(vm, basValStringFromC(dirNext(vm)))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -4915,15 +4674,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
}
case OP_FS_DIR_NEXT: {
- const char *match = dirNext(vm);
- const char *text = match ? match : "";
- BasStringT *s = basStringNew(text, (int32_t)strlen(text));
- BasValueT result;
- result.type = BAS_TYPE_STRING;
- result.strVal = s;
-
- if (!push(vm, result)) {
- basStringUnref(s);
+ if (!push(vm, basValStringFromC(dirNext(vm)))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -4931,17 +4682,14 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
}
case OP_FS_FILELEN: {
- BasValueT fnVal;
+ BasValueT fnStr;
- if (!pop(vm, &fnVal)) {
+ if (!popStringArg(vm, &fnStr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT fnStr = basValToString(fnVal);
- basValRelease(&fnVal);
-
struct stat st;
- int32_t size = 0;
+ int32_t size = 0;
if (stat(fnStr.strVal->data, &st) == 0) {
size = (int32_t)st.st_size;
@@ -4949,11 +4697,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
basValRelease(&fnStr);
- BasValueT result;
- result.type = BAS_TYPE_LONG;
- result.longVal = size;
-
- if (!push(vm, result)) {
+ if (!push(vm, basValLong(size))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -4961,17 +4705,14 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
}
case OP_FS_GETATTR: {
- BasValueT fnVal;
+ BasValueT fnStr;
- if (!pop(vm, &fnVal)) {
+ if (!popStringArg(vm, &fnStr)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT fnStr = basValToString(fnVal);
- basValRelease(&fnVal);
-
struct stat st;
- int32_t attrs = 0;
+ int32_t attrs = 0;
if (stat(fnStr.strVal->data, &st) == 0) {
if (S_ISDIR(st.st_mode)) {
@@ -4985,11 +4726,7 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
basValRelease(&fnStr);
- BasValueT result;
- result.type = BAS_TYPE_INTEGER;
- result.intVal = (int16_t)attrs;
-
- if (!push(vm, result)) {
+ if (!push(vm, basValInteger((int16_t)attrs))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -4998,15 +4735,18 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
case OP_FS_SETATTR: {
BasValueT attrVal;
- BasValueT fnVal;
+ BasValueT fnStr;
- if (!pop(vm, &attrVal) || !pop(vm, &fnVal)) {
+ if (!pop(vm, &attrVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT fnStr = basValToString(fnVal);
- int32_t attrs = (int32_t)basValToNumber(attrVal);
- basValRelease(&fnVal);
+ if (!popStringArg(vm, &fnStr)) {
+ basValRelease(&attrVal);
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ int32_t attrs = basValToInt32(attrVal);
basValRelease(&attrVal);
struct stat st;
@@ -5036,56 +4776,84 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
static BasVmResultE execLogical(BasVmT *vm, uint8_t op) {
+ // Bitwise operators work on LONG operands: round (never truncate) so
+ // NOT 0.6 is NOT 1 = -2, and raise Overflow when a value does not fit.
if (op == OP_NOT) {
if (vm->sp < 1) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT *top = &vm->stack[vm->sp - 1];
- int32_t n = (int32_t)basValToNumber(*top);
+ BasValueT *top = &vm->stack[vm->sp - 1];
+ int32_t n;
+ BasVmResultE rc = roundedOperand(vm, *top, &n);
+
+ if (rc != BAS_VM_OK) {
+ return rc;
+ }
+
basValRelease(top);
int32_t r = ~n;
// Promote to LONG when the result exceeds INTEGER range, matching
// execArith -- truncation corrupts LONG masks like NOT &H10000.
- if (r >= INT16_MIN && r <= INT16_MAX) {
- *top = basValInteger((int16_t)r);
- } else {
- *top = basValLong(r);
- }
-
+ *top = (r >= INT16_MIN && r <= INT16_MAX) ? basValInteger((int16_t)r) : basValLong(r);
return BAS_VM_OK;
}
BasValueT b;
BasValueT a;
- if (!pop(vm, &b) || !pop(vm, &a)) {
+ if (!popArgs(vm, 2, &b, &a)) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t na = (int32_t)basValToNumber(a);
- int32_t nb = (int32_t)basValToNumber(b);
+ int32_t na;
+ int32_t nb;
+ BasVmResultE rc = roundedOperand(vm, a, &na);
+
+ if (rc == BAS_VM_OK) {
+ rc = roundedOperand(vm, b, &nb);
+ }
+
basValRelease(&a);
basValRelease(&b);
+ if (rc != BAS_VM_OK) {
+ return rc;
+ }
+
int32_t result;
switch (op) {
- case OP_AND: result = na & nb; break;
- case OP_OR: result = na | nb; break;
- case OP_XOR: result = na ^ nb; break;
- case OP_EQV: result = ~(na ^ nb); break;
- case OP_IMP: result = (~na) | nb; break;
- default: result = 0; break;
+ case OP_AND:
+ result = na & nb;
+ break;
+
+ case OP_OR:
+ result = na | nb;
+ break;
+
+ case OP_XOR:
+ result = na ^ nb;
+ break;
+
+ case OP_EQV:
+ result = ~(na ^ nb);
+ break;
+
+ case OP_IMP:
+ result = (~na) | nb;
+ break;
+
+ default:
+ result = 0;
+ break;
}
// Promote to LONG when the result exceeds INTEGER range, matching
// execArith -- truncation corrupts LONG masks like c& AND &HFF0000.
- if (result >= INT16_MIN && result <= INT16_MAX) {
- push(vm, basValInteger((int16_t)result));
- } else {
- push(vm, basValLong(result));
+ if (!push(vm, (result >= INT16_MIN && result <= INT16_MAX) ? basValInteger((int16_t)result) : basValLong(result))) {
+ return BAS_VM_STACK_OVERFLOW;
}
return BAS_VM_OK;
@@ -5122,7 +4890,7 @@ static BasVmResultE execMath(BasVmT *vm, uint8_t op) {
double n = basValToNumber(val);
basValRelease(&val);
- if (n < 0) {
+ if (n == BAS_RANDOMIZE_TIMER_SEED) {
srand((unsigned int)time(NULL));
} else {
srand((unsigned int)n);
@@ -5137,22 +4905,63 @@ static BasVmResultE execMath(BasVmT *vm, uint8_t op) {
}
BasValueT *top = &vm->stack[vm->sp - 1];
- double n = basValToNumber(*top);
- double result;
+ double n = basValToNumber(*top);
+ double result;
+
+ // Domain errors are Illegal function call in BASIC, never NaN.
+ if ((op == OP_MATH_SQR && n < 0) || (op == OP_MATH_LOG && n <= 0)) {
+ runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
+ return BAS_VM_ERROR;
+ }
switch (op) {
- case OP_MATH_ABS: result = fabs(n); break;
- case OP_MATH_INT: result = floor(n); break;
- case OP_MATH_FIX: result = (n >= 0) ? floor(n) : ceil(n); break;
- case OP_MATH_SGN: result = (n > 0) ? 1.0 : (n < 0) ? -1.0 : 0.0; break;
- case OP_MATH_SQR: result = sqrt(n); break;
- case OP_MATH_SIN: result = sin(n); break;
- case OP_MATH_COS: result = cos(n); break;
- case OP_MATH_TAN: result = tan(n); break;
- case OP_MATH_ATN: result = atan(n); break;
- case OP_MATH_LOG: result = log(n); break;
- case OP_MATH_EXP: result = exp(n); break;
- default: result = 0.0; break;
+ case OP_MATH_ABS:
+ result = fabs(n);
+ break;
+
+ case OP_MATH_INT:
+ result = floor(n);
+ break;
+
+ case OP_MATH_FIX:
+ result = (n >= 0) ? floor(n) : ceil(n);
+ break;
+
+ case OP_MATH_SGN:
+ result = (n > 0) ? 1.0 : (n < 0) ? -1.0 : 0.0;
+ break;
+
+ case OP_MATH_SQR:
+ result = sqrt(n);
+ break;
+
+ case OP_MATH_SIN:
+ result = sin(n);
+ break;
+
+ case OP_MATH_COS:
+ result = cos(n);
+ break;
+
+ case OP_MATH_TAN:
+ result = tan(n);
+ break;
+
+ case OP_MATH_ATN:
+ result = atan(n);
+ break;
+
+ case OP_MATH_LOG:
+ result = log(n);
+ break;
+
+ case OP_MATH_EXP:
+ result = exp(n);
+ break;
+
+ default:
+ result = 0.0;
+ break;
}
basValRelease(top);
@@ -5174,7 +4983,7 @@ static BasVmResultE execPrint(BasVmT *vm) {
BasStringT *s = basValFormatString(val);
basValRelease(&val);
- if (vm->printFn && s) {
+ if (vm->printFn) {
vm->printFn(vm->printCtx, s->data, false);
if (isNumeric) {
@@ -5190,131 +4999,113 @@ static BasVmResultE execPrint(BasVmT *vm) {
static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
switch (op) {
case OP_STR_CONCAT: {
- BasValueT b;
- BasValueT a;
+ BasValueT sb;
+ BasValueT sa;
- if (!pop(vm, &b) || !pop(vm, &a)) {
+ if (!popStringArg(vm, &sb)) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT sa = basValToString(a);
- BasValueT sb = basValToString(b);
- basValRelease(&a);
- basValRelease(&b);
+ if (!popStringArg(vm, &sa)) {
+ basValRelease(&sb);
+ return BAS_VM_STACK_UNDERFLOW;
+ }
BasStringT *result = basStringConcat(sa.strVal, sb.strVal);
basValRelease(&sa);
basValRelease(&sb);
- BasValueT rv;
- rv.type = BAS_TYPE_STRING;
- rv.strVal = result;
- push(vm, rv);
- return BAS_VM_OK;
- }
-
- case OP_STR_LEFT: {
- BasValueT nVal;
- BasValueT sVal;
-
- if (!pop(vm, &nVal) || !pop(vm, &sVal)) {
- return BAS_VM_STACK_UNDERFLOW;
+ if (!push(vm, strValue(result))) {
+ return BAS_VM_STACK_OVERFLOW;
}
- int32_t n = (int32_t)basValToNumber(nVal);
- basValRelease(&nVal);
-
- BasValueT sv = basValToString(sVal);
- basValRelease(&sVal);
-
- BasStringT *result = basStringSub(sv.strVal, 0, n);
- basValRelease(&sv);
-
- BasValueT rv;
- rv.type = BAS_TYPE_STRING;
- rv.strVal = result;
- push(vm, rv);
return BAS_VM_OK;
}
+ case OP_STR_LEFT:
case OP_STR_RIGHT: {
BasValueT nVal;
- BasValueT sVal;
+ BasValueT sv;
- if (!pop(vm, &nVal) || !pop(vm, &sVal)) {
+ if (!pop(vm, &nVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t n = (int32_t)basValToNumber(nVal);
+ if (!popStringArg(vm, &sv)) {
+ basValRelease(&nVal);
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ int32_t n = basValToInt32(nVal);
basValRelease(&nVal);
- BasValueT sv = basValToString(sVal);
- basValRelease(&sVal);
+ if (n < 0) {
+ basValRelease(&sv);
+ runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
+ return BAS_VM_ERROR;
+ }
- int32_t start = sv.strVal->len - n;
+ int32_t start = 0;
- if (start < 0) {
- start = 0;
+ if (op == OP_STR_RIGHT) {
+ start = sv.strVal->len - n;
+
+ if (start < 0) {
+ start = 0;
+ }
}
BasStringT *result = basStringSub(sv.strVal, start, n);
basValRelease(&sv);
- BasValueT rv;
- rv.type = BAS_TYPE_STRING;
- rv.strVal = result;
- push(vm, rv);
+ if (!push(vm, strValue(result))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
return BAS_VM_OK;
}
- case OP_STR_MID: {
+ case OP_STR_MID:
+ case OP_STR_MID2: {
+ // MID$(s$, start[, len]); a missing len means "to the end".
BasValueT lenVal;
BasValueT startVal;
- BasValueT sVal;
+ BasValueT sv;
+ int32_t len = -1;
- if (!pop(vm, &lenVal) || !pop(vm, &startVal) || !pop(vm, &sVal)) {
+ if (op == OP_STR_MID) {
+ if (!pop(vm, &lenVal)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ len = basValToInt32(lenVal);
+ basValRelease(&lenVal);
+ }
+
+ if (!pop(vm, &startVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t start = (int32_t)basValToNumber(startVal) - 1; // 1-based to 0-based
- int32_t len = (int32_t)basValToNumber(lenVal);
+ int32_t start = basValToInt32(startVal);
basValRelease(&startVal);
- basValRelease(&lenVal);
- BasValueT sv = basValToString(sVal);
- basValRelease(&sVal);
-
- BasStringT *result = basStringSub(sv.strVal, start, len);
- basValRelease(&sv);
-
- BasValueT rv;
- rv.type = BAS_TYPE_STRING;
- rv.strVal = result;
- push(vm, rv);
- return BAS_VM_OK;
- }
-
- case OP_STR_MID2: {
- BasValueT startVal;
- BasValueT sVal;
-
- if (!pop(vm, &startVal) || !pop(vm, &sVal)) {
+ if (!popStringArg(vm, &sv)) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t start = (int32_t)basValToNumber(startVal) - 1;
- basValRelease(&startVal);
+ if (start < 1 || (op == OP_STR_MID && len < 0)) {
+ basValRelease(&sv);
+ runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
+ return BAS_VM_ERROR;
+ }
- BasValueT sv = basValToString(sVal);
- basValRelease(&sVal);
-
- BasStringT *result = basStringSub(sv.strVal, start, sv.strVal->len - start);
+ BasStringT *result = basStringSub(sv.strVal, start - 1, len);
basValRelease(&sv);
- BasValueT rv;
- rv.type = BAS_TYPE_STRING;
- rv.strVal = result;
- push(vm, rv);
+ if (!push(vm, strValue(result))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
return BAS_VM_OK;
}
@@ -5324,71 +5115,59 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
}
BasValueT *top = &vm->stack[vm->sp - 1];
- BasValueT sv = basValToString(*top);
- int32_t len = sv.strVal ? sv.strVal->len : 0;
+ BasValueT sv = basValToString(*top);
+ int32_t len = sv.strVal->len;
basValRelease(&sv);
basValRelease(top);
- *top = basValInteger((int16_t)len);
- return BAS_VM_OK;
- }
-
- case OP_STR_INSTR: {
- BasValueT findVal;
- BasValueT sVal;
-
- if (!pop(vm, &findVal) || !pop(vm, &sVal)) {
- return BAS_VM_STACK_UNDERFLOW;
- }
-
- BasValueT sv = basValToString(sVal);
- BasValueT fv = basValToString(findVal);
- basValRelease(&sVal);
- basValRelease(&findVal);
-
- int32_t pos = 0;
- char *found = strstr(sv.strVal->data, fv.strVal->data);
-
- if (found) {
- pos = (int32_t)(found - sv.strVal->data) + 1; // 1-based
- }
-
- basValRelease(&sv);
- basValRelease(&fv);
- push(vm, basValInteger((int16_t)pos));
+ *top = basValLong(len);
return BAS_VM_OK;
}
+ case OP_STR_INSTR:
case OP_STR_INSTR3: {
- // INSTR(start, string, find)
- BasValueT findVal;
- BasValueT sVal;
+ // INSTR([start,] string, find); positions are 1-based, LONG.
+ BasValueT fv;
+ BasValueT sv;
BasValueT startVal;
+ int32_t startPos = 0;
- if (!pop(vm, &findVal) || !pop(vm, &sVal) || !pop(vm, &startVal)) {
+ if (!popStringArg(vm, &fv)) {
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t startPos = (int32_t)basValToNumber(startVal) - 1; // 0-based
- basValRelease(&startVal);
+ if (!popStringArg(vm, &sv)) {
+ basValRelease(&fv);
+ return BAS_VM_STACK_UNDERFLOW;
+ }
- BasValueT sv = basValToString(sVal);
- BasValueT fv = basValToString(findVal);
- basValRelease(&sVal);
- basValRelease(&findVal);
+ if (op == OP_STR_INSTR3) {
+ if (!pop(vm, &startVal)) {
+ basValRelease(&fv);
+ basValRelease(&sv);
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ startPos = basValToInt32(startVal) - 1;
+ basValRelease(&startVal);
+ }
int32_t pos = 0;
if (startPos >= 0 && startPos < sv.strVal->len) {
- char *found = strstr(sv.strVal->data + startPos, fv.strVal->data);
+ int32_t found = stringFind(sv.strVal, startPos, fv.strVal);
- if (found) {
- pos = (int32_t)(found - sv.strVal->data) + 1; // 1-based
+ if (found >= 0) {
+ pos = found + 1;
}
}
basValRelease(&sv);
basValRelease(&fv);
- push(vm, basValInteger((int16_t)pos));
+
+ if (!push(vm, basValLong(pos))) {
+ return BAS_VM_STACK_OVERFLOW;
+ }
+
return BAS_VM_OK;
}
@@ -5402,7 +5181,7 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
}
BasValueT *top = &vm->stack[vm->sp - 1];
- BasValueT sv = basValToString(*top);
+ BasValueT sv = basValToString(*top);
basValRelease(top);
BasStringT *src = sv.strVal;
@@ -5438,8 +5217,7 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
}
basValRelease(&sv);
- top->type = BAS_TYPE_STRING;
- top->strVal = result;
+ *top = strValue(result);
return BAS_VM_OK;
}
@@ -5448,11 +5226,12 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
return BAS_VM_STACK_UNDERFLOW;
}
+ // A one-byte string, length-sized so CHR$(0) is a real NUL
+ // character rather than an empty string.
BasValueT *top = &vm->stack[vm->sp - 1];
- int32_t code = (int32_t)basValToNumber(*top);
- char buf[2] = { (char)(code & 0xFF), '\0' };
+ char ch = (char)(basValToInt32(*top) & BAS_CHAR_MASK);
basValRelease(top);
- *top = basValStringFromC(buf);
+ *top = strValue(basStringNew(&ch, 1));
return BAS_VM_OK;
}
@@ -5461,9 +5240,9 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
return BAS_VM_STACK_UNDERFLOW;
}
- BasValueT *top = &vm->stack[vm->sp - 1];
- BasValueT sv = basValToString(*top);
- int32_t code = (sv.strVal && sv.strVal->len > 0) ? (unsigned char)sv.strVal->data[0] : 0;
+ BasValueT *top = &vm->stack[vm->sp - 1];
+ BasValueT sv = basValToString(*top);
+ int32_t code = (sv.strVal->len > 0) ? (unsigned char)sv.strVal->data[0] : 0;
basValRelease(&sv);
basValRelease(top);
*top = basValInteger((int16_t)code);
@@ -5476,27 +5255,15 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
}
BasValueT *top = &vm->stack[vm->sp - 1];
- int32_t n = (int32_t)basValToNumber(*top);
+ int32_t n = basValToInt32(*top);
+
+ if (n < 0 || n > BAS_STRING_FUNC_MAX) {
+ runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
+ return BAS_VM_ERROR;
+ }
+
basValRelease(top);
-
- if (n < 0) {
- n = 0;
- }
-
- if (n > INT16_MAX) {
- n = INT16_MAX;
- }
-
- BasStringT *s = basStringAlloc(n + 1);
-
- if (s->cap >= n + 1) {
- memset(s->data, ' ', n);
- s->data[n] = '\0';
- s->len = n;
- }
-
- top->type = BAS_TYPE_STRING;
- top->strVal = s;
+ *top = strValue(fillString(' ', n));
return BAS_VM_OK;
}
@@ -5509,21 +5276,17 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
}
BasValueT *top = &vm->stack[vm->sp - 1];
- BasValueT sv = basValToString(*top);
+ BasValueT sv = basValToString(*top);
basValRelease(top);
- BasStringT *src = sv.strVal;
+ BasStringT *src = sv.strVal;
BasStringT *result = basStringAlloc(fixLen + 1);
if (result->cap >= (int32_t)fixLen + 1) {
- int32_t srcLen = src ? src->len : 0;
- int32_t copyLen = srcLen < (int32_t)fixLen ? srcLen : (int32_t)fixLen;
+ int32_t copyLen = src->len < (int32_t)fixLen ? src->len : (int32_t)fixLen;
result->len = fixLen;
-
- if (copyLen > 0 && src) {
- memcpy(result->data, src->data, copyLen);
- }
+ memcpy(result->data, src->data, copyLen);
// Pad with spaces
for (int32_t i = copyLen; i < (int32_t)fixLen; i++) {
@@ -5534,9 +5297,7 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
}
basValRelease(&sv);
-
- top->type = BAS_TYPE_STRING;
- top->strVal = result;
+ *top = strValue(result);
return BAS_VM_OK;
}
@@ -5547,23 +5308,23 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
BasValueT startVal;
BasValueT strVal;
- if (!pop(vm, &replVal) || !pop(vm, &lenVal) || !pop(vm, &startVal) || !pop(vm, &strVal)) {
+ if (!popArgs(vm, 4, &replVal, &lenVal, &startVal, &strVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
BasValueT sv = basValToString(strVal);
BasValueT rv = basValToString(replVal);
- int32_t start = (int32_t)basValToNumber(startVal) - 1; // 1-based to 0-based
- int32_t len = (int32_t)basValToNumber(lenVal);
+ int32_t start = basValToInt32(startVal) - 1; // 1-based to 0-based
+ int32_t len = basValToInt32(lenVal);
basValRelease(&strVal);
basValRelease(&startVal);
basValRelease(&lenVal);
basValRelease(&replVal);
- BasStringT *src = sv.strVal;
- BasStringT *repl = rv.strVal;
- int32_t srcLen = src ? src->len : 0;
- int32_t replLen = repl ? repl->len : 0;
+ BasStringT *src = sv.strVal;
+ BasStringT *repl = rv.strVal;
+ int32_t srcLen = src->len;
+ int32_t replLen = repl->len;
// If len is 0, use replacement length
if (len <= 0) {
@@ -5576,28 +5337,23 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
}
// Create a copy of the original string
- BasStringT *result = basStringNew(src ? src->data : "", srcLen);
+ BasStringT *result = basStringNew(src->data, srcLen);
// Replace characters
if (start >= 0 && start < srcLen && len > 0) {
int32_t maxReplace = srcLen - start;
+
if (len > maxReplace) {
len = maxReplace;
}
- if (repl) {
- memcpy(result->data + start, repl->data, len);
- }
+
+ memcpy(result->data + start, repl->data, len);
}
basValRelease(&sv);
basValRelease(&rv);
- BasValueT resultVal;
- resultVal.type = BAS_TYPE_STRING;
- resultVal.strVal = result;
-
- if (!push(vm, resultVal)) {
- basStringUnref(result);
+ if (!push(vm, strValue(result))) {
return BAS_VM_STACK_OVERFLOW;
}
@@ -5610,6 +5366,20 @@ static BasVmResultE execStringOp(BasVmT *vm, uint8_t op) {
}
+// A new string of count copies of ch.
+static BasStringT *fillString(char ch, int32_t count) {
+ BasStringT *s = basStringAlloc(count + 1);
+
+ if (s->cap >= count + 1) {
+ memset(s->data, ch, count);
+ s->data[count] = '\0';
+ s->len = count;
+ }
+
+ return s;
+}
+
+
// Shared numeric formatter for OP_PRINT_USING (numeric branch) and OP_FORMAT
// (non-PERCENT branch). Renders n into out per the flags in f. Every cursor
// write is bounded against outSize, and every intermediate buffer is sized to
@@ -5731,11 +5501,15 @@ static void formatNumber(double n, const BasNumFormatT *f, char *out, size_t out
// Decimal part. Missing digits fill with '0' (PRINT USING behavior);
// FORMAT$ always has exactly 'decimals' digits so this never triggers.
+ // decimals comes from the runtime format string, so it can exceed the
+ // digits snprintf produced into decPart -- never read past decLen.
if (f->hasDecimal) {
+ int32_t decLen = (int32_t)strlen(decPart);
+
putBounded(out, &idx, limit, '.');
for (int32_t i = 0; i < decimals; i++) {
- putBounded(out, &idx, limit, decPart[i] ? decPart[i] : '0');
+ putBounded(out, &idx, limit, (i < decLen) ? decPart[i] : '0');
}
}
@@ -5762,17 +5536,63 @@ static void formatNumber(double n, const BasNumFormatT *f, char *out, size_t out
}
+// Lowest forStack index the currently-executing call frame may unwind
+// to. FOR frames below this belong to callers and must never be
+// touched by NEXT mismatch recovery or FOR re-entry cleanup.
+static int32_t forStackFloor(BasVmT *vm) {
+ if (vm->callDepth > 0) {
+ return vm->callStack[vm->callDepth - 1].savedForDepth;
+ }
+
+ return 0;
+}
+
+
+// Release FOR-stack entries above newDepth and truncate the stack to it.
+// Used when returning from a call frame, unwinding to an error handler,
+// and discarding stale frames left by EXIT DO / GOTO jumps out of FORs.
+static void forStackTrim(BasVmT *vm, int32_t newDepth) {
+ while (vm->forDepth > newDepth) {
+ BasForStateT *fs = &vm->forStack[--vm->forDepth];
+ basValRelease(&fs->limit);
+ basValRelease(&fs->step);
+ }
+}
+
+
+// Local wall-clock time, with the sub-second part in microseconds when
+// outUsec is non-NULL. Shared by TIMER, DATE$ and TIME$.
+static void localNow(struct tm *out, int32_t *outUsec) {
+ struct timeval tv;
+
+ gettimeofday(&tv, NULL);
+
+ time_t secs = tv.tv_sec;
+ struct tm *local = localtime(&secs);
+
+ if (local) {
+ *out = *local;
+ } else {
+ memset(out, 0, sizeof(*out));
+ }
+
+ if (outUsec) {
+ *outUsec = (int32_t)tv.tv_usec;
+ }
+}
+
+
// Returns the bytecode address the error dispatcher should record for
// RESUME NEXT: the statement that follows the failing one. Walks the
// bytecode instruction by instruction from the failing statement's
-// OP_LINE (vm->stmtPc). The walk lands on the next OP_LINE or on a
-// structural opcode that IS what runs after the statement (a loop-closing
-// OP_JMP/OP_FOR_NEXT, a SUB's OP_RET epilogue, OP_END/OP_HALT); a GOSUB's
-// PUSH_INT32+JMP pair is stepped over as a unit so the walk continues at
-// its return address. Returns -1 when no statement boundary is known
-// (OP_LINE stripped by release compaction) or when the walk reaches an
-// opcode that pops values the trap already released (conditional jumps,
-// OP_RET_VAL); the caller then falls back to a coarser resume target.
+// OP_LINE/OP_STMT (vm->stmtPc). The walk lands on the next statement
+// boundary or on a structural opcode that IS what runs after the statement
+// (a loop-closing OP_JMP/OP_FOR_NEXT, a SUB's OP_RET epilogue,
+// OP_END/OP_HALT); a GOSUB's PUSH_INT32+JMP pair is stepped over as a unit
+// so the walk continues at its return address. Returns -1 when no
+// statement boundary is known or when the walk reaches an opcode that pops
+// values the trap already released (conditional jumps, OP_RET_VAL); the
+// caller then falls back to a coarser resume target.
static int32_t nextStatementPc(BasVmT *vm) {
if (vm->stmtPc < 0 || !vm->module) {
return -1;
@@ -5786,18 +5606,14 @@ static int32_t nextStatementPc(BasVmT *vm) {
uint8_t op = code[pc];
if (pc > vm->stmtPc) {
- // GOSUB pattern: OP_PUSH_INT32 OP_JMP . Step
- // over both so a failed GOSUB statement resumes after the call.
- if (op == OP_PUSH_INT32 && pc + 8 <= codeLen && code[pc + 5] == OP_JMP) {
- int32_t retAddr = (int32_t)((uint32_t)code[pc + 1] | ((uint32_t)code[pc + 2] << 8) | ((uint32_t)code[pc + 3] << 16) | ((uint32_t)code[pc + 4] << 24));
-
- if (retAddr == pc + 8) {
- pc += 8;
- continue;
- }
+ // Step over a GOSUB call sequence so a failed GOSUB statement
+ // resumes after the call.
+ if (basIsGosubPush(code, codeLen, pc)) {
+ pc += BAS_GOSUB_PATTERN_LEN;
+ continue;
}
- if (op == OP_LINE || op == OP_JMP || op == OP_FOR_NEXT || op == OP_RET || op == OP_END || op == OP_HALT) {
+ if (op == OP_LINE || op == OP_STMT || op == OP_JMP || op == OP_FOR_NEXT || op == OP_RET || op == OP_END || op == OP_HALT) {
return pc;
}
@@ -5806,7 +5622,7 @@ static int32_t nextStatementPc(BasVmT *vm) {
}
}
- int32_t operand = opcodeOperandSize(op);
+ int32_t operand = basOpcodeOperandSize(op);
if (operand < 0) {
return -1;
@@ -5819,126 +5635,6 @@ static int32_t nextStatementPc(BasVmT *vm) {
}
-// Operand byte count for each opcode (excluding the 1-byte opcode), or -1
-// if unknown. Used only to walk instruction boundaries for RESUME NEXT.
-// Keep in sync with opOperandSize in compiler/compact.c (the compiler-side
-// copy) and the operand comments in compiler/opcodes.h.
-static int32_t opcodeOperandSize(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;
- }
-}
-
-
// Returns whether `size` bytes can be read at vm->pc without running past
// the end of code[]. On overflow it flags the VM and clamps pc to codeLen so
// the next basVmStep entry raises BAS_VM_BAD_OPCODE. Compared as
@@ -5955,6 +5651,45 @@ static inline bool operandFits(BasVmT *vm, int32_t size) {
}
+// Scans a PRINT USING / FORMAT$ numeric format string into f. Both
+// statements share the placeholder grammar: '#', '0' (and '*' before the
+// decimal point) are digit positions, '.' and ',' set the decimal and
+// thousands flags, a leading '+' or a trailing '+'/'-' selects the sign
+// style, leading "**" and "$$" select fills, and "^^^^" requests
+// scientific notation (reported through outSci).
+static void parseNumFormat(const char *fmt, int32_t fmtLen, BasNumFormatT *f, bool *outSci) {
+ memset(f, 0, sizeof(*f));
+
+ f->asteriskFill = (fmtLen >= 2 && fmt[0] == '*' && fmt[1] == '*');
+
+ int32_t scanStart = f->asteriskFill ? 2 : 0;
+
+ f->dollarFloat = (fmtLen >= scanStart + 2 && fmt[scanStart] == '$' && fmt[scanStart + 1] == '$');
+ f->plusAtStart = (fmtLen > 0 && fmt[0] == '+');
+ f->plusAtEnd = (fmtLen > 0 && fmt[fmtLen - 1] == '+');
+ f->minusAtEnd = (fmtLen > 0 && fmt[fmtLen - 1] == '-');
+ *outSci = (strstr(fmt, BAS_SCI_MARKER) != NULL);
+
+ for (int32_t i = 0; i < fmtLen; i++) {
+ if (fmt[i] == '.') {
+ f->hasDecimal = true;
+ } else if (fmt[i] == ',') {
+ f->hasComma = true;
+ } else if (fmt[i] == '#' || fmt[i] == '0' || (fmt[i] == '*' && !f->hasDecimal)) {
+ if (f->hasDecimal) {
+ f->digitsAfter++;
+ } else {
+ f->digitsBefore++;
+ }
+
+ if (fmt[i] == '0' && !f->hasDecimal) {
+ f->zeroPad = true;
+ }
+ }
+ }
+}
+
+
static bool pop(BasVmT *vm, BasValueT *val) {
if (vm->sp <= 0) {
return false;
@@ -5965,9 +5700,38 @@ static bool pop(BasVmT *vm, BasValueT *val) {
}
+// Pops count values, top of stack first, into the BasValueT pointers that
+// follow. On underflow every value already popped is released so a
+// malformed program cannot leak them, and false is returned.
+static bool popArgs(BasVmT *vm, int32_t count, ...) {
+ va_list ap;
+
+ va_start(ap, count);
+
+ for (int32_t i = 0; i < count; i++) {
+ BasValueT *out = va_arg(ap, BasValueT *);
+
+ if (!pop(vm, out)) {
+ va_end(ap);
+ va_start(ap, count);
+
+ for (int32_t j = 0; j < i; j++) {
+ basValRelease(va_arg(ap, BasValueT *));
+ }
+
+ va_end(ap);
+ return false;
+ }
+ }
+
+ va_end(ap);
+ return true;
+}
+
+
// Pops the top call frame (the shared SUB/FUNCTION return epilogue used by
// OP_RET and OP_RET_VAL). The caller MUST have already verified callDepth > 0.
-// Copies out-args, releases locals, restores PC and the caller's error handler.
+// Copies out-args, releases locals and restores PC.
static void popCallFrame(BasVmT *vm) {
BasCallFrameT *frame = &vm->callStack[--vm->callDepth];
@@ -5997,10 +5761,6 @@ static void popCallFrame(BasVmT *vm) {
frame->errorHandler = 0;
vm->pc = frame->returnPc;
- // Restore the active handler to whatever the caller had set
- BasCallFrameT *caller = currentFrame(vm);
- vm->errorHandler = caller ? caller->errorHandler : 0;
-
// If this SUB owned the active error handler, any handler body that ran
// inside it is now done -- clear the flag so the next error can trap again.
// QBASIC documents this: an unresumed handler is cleared when the procedure
@@ -6020,7 +5780,7 @@ static BasVmResultE popFileChannel(BasVmT *vm, int32_t *outChannel, bool require
return BAS_VM_STACK_UNDERFLOW;
}
- int32_t channel = (int32_t)basValToNumber(channelVal);
+ int32_t channel = basValToInt32(channelVal);
basValRelease(&channelVal);
if (channel < 1 || channel >= BAS_VM_MAX_FILES || (requireOpen && !vm->files[channel].handle)) {
@@ -6033,12 +5793,57 @@ static BasVmResultE popFileChannel(BasVmT *vm, int32_t *outChannel, bool require
}
+// Pops one operand and converts it to a STRING value (*out always holds a
+// non-NULL strVal on success). Returns false on stack underflow.
+static bool popStringArg(BasVmT *vm, BasValueT *out) {
+ BasValueT raw;
+
+ if (!pop(vm, &raw)) {
+ return false;
+ }
+
+ *out = basValToString(raw);
+ basValRelease(&raw);
+ return true;
+}
+
+
+// REDIM PRESERVE: copies every element whose subscripts are valid in both
+// the old and the new array, walking the new array's subscript space and
+// mapping each position back through the old array's own strides. Both
+// arrays must have the same dimension count.
+static void preserveElements(const BasArrayT *oldArr, BasArrayT *newArr) {
+ int32_t idx[BAS_ARRAY_MAX_DIMS];
+
+ for (int32_t d = 0; d < newArr->dims; d++) {
+ idx[d] = newArr->lbound[d];
+ }
+
+ for (int32_t flat = 0; flat < newArr->totalElements; flat++) {
+ int32_t oldFlat = basArrayIndex((BasArrayT *)oldArr, idx, newArr->dims);
+
+ if (oldFlat >= 0) {
+ basValRelease(&newArr->elements[flat]);
+ newArr->elements[flat] = basValCopy(oldArr->elements[oldFlat]);
+ }
+
+ // Advance the subscript counter, last dimension fastest.
+ for (int32_t d = newArr->dims - 1; d >= 0; d--) {
+ if (++idx[d] <= newArr->ubound[d]) {
+ break;
+ }
+
+ idx[d] = newArr->lbound[d];
+ }
+ }
+}
+
+
// Primes callStack[0] as the implicit main (module-level) frame. Module
// code runs in frame 0; without callDepth >= 1 the error dispatcher's
// 'while (callDepth > 0)' unwind never inspects that frame, so a
-// module-level ON ERROR GOTO never traps. Shared by basVmLoadModule and
-// basVmReset so both entry paths keep the same invariant. localCount
-// lets RET and frame-local cleanup see the right slot count.
+// module-level ON ERROR GOTO never traps. localCount lets RET and
+// frame-local cleanup see the right slot count.
static void primeModuleFrame(BasVmT *vm) {
vm->callDepth = 1;
vm->callStack[0].localCount = vm->module->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : vm->module->globalCount;
@@ -6053,8 +5858,11 @@ static void primeModuleFrame(BasVmT *vm) {
}
+// Pushes val, taking ownership of it: on stack overflow the value is
+// released here so no caller has to remember to.
static bool push(BasVmT *vm, BasValueT val) {
if (vm->sp >= BAS_VM_STACK_SIZE) {
+ basValRelease(&val);
return false;
}
@@ -6073,6 +5881,108 @@ static void putBounded(char *out, int32_t *idx, int32_t limit, char c) {
}
+// INPUT #: reads one field the way WRITE # wrote it -- leading blanks are
+// skipped, a quoted field runs to the closing quote, an unquoted field runs
+// to the next comma or end of line, and the field's delimiter (comma or
+// line end) is consumed. Returns the field as a new string.
+static BasStringT *readField(FILE *fp) {
+ int32_t ch = fgetc(fp);
+
+ while (ch == ' ' || ch == '\t') {
+ ch = fgetc(fp);
+ }
+
+ if (ch == EOF) {
+ return basStringRef(basEmptyString);
+ }
+
+ // An empty line between records is its own (empty) field.
+ if (ch == '\n' || ch == '\r') {
+ if (ch == '\r') {
+ ch = fgetc(fp);
+
+ if (ch != '\n' && ch != EOF) {
+ ungetc(ch, fp);
+ }
+ }
+
+ return basStringRef(basEmptyString);
+ }
+
+ int32_t cap = BAS_FILE_LINE_CHUNK;
+ int32_t len = 0;
+ char *buf = (char *)malloc(cap);
+ bool quoted = (ch == '"');
+
+ if (!buf) {
+ return basStringRef(basEmptyString);
+ }
+
+ if (!quoted) {
+ ungetc(ch, fp);
+ }
+
+ for (;;) {
+ ch = fgetc(fp);
+
+ if (ch == EOF) {
+ break;
+ }
+
+ if (quoted) {
+ if (ch == '"') {
+ // Skip to the field delimiter that follows the closing quote.
+ do {
+ ch = fgetc(fp);
+ } while (ch == ' ' || ch == '\t');
+
+ if (ch != ',' && ch != '\n' && ch != '\r' && ch != EOF) {
+ ungetc(ch, fp);
+ }
+
+ break;
+ }
+ } else if (ch == ',' || ch == '\n' || ch == '\r') {
+ break;
+ }
+
+ if (len + 1 >= cap) {
+ int32_t newCap = cap * 2;
+ char *grown = (char *)realloc(buf, newCap);
+
+ if (!grown) {
+ break;
+ }
+
+ buf = grown;
+ cap = newCap;
+ }
+
+ buf[len++] = (char)ch;
+ }
+
+ // Swallow the LF of a CR LF pair so the next field starts cleanly.
+ if (ch == '\r') {
+ ch = fgetc(fp);
+
+ if (ch != '\n' && ch != EOF) {
+ ungetc(ch, fp);
+ }
+ }
+
+ // Unquoted fields drop trailing blanks, as QB does.
+ if (!quoted) {
+ while (len > 0 && (buf[len - 1] == ' ' || buf[len - 1] == '\t')) {
+ len--;
+ }
+ }
+
+ BasStringT *s = basStringNew(buf, len);
+ free(buf);
+ return s;
+}
+
+
// memcpy with constant size is folded to a single load by the compiler and
// is alignment-safe (bytecode operands aren't guaranteed 2-byte aligned).
static inline int16_t readInt16(BasVmT *vm) {
@@ -6087,6 +5997,58 @@ static inline int16_t readInt16(BasVmT *vm) {
}
+// LINE INPUT #: reads a whole line of any length (fgets chunks are joined
+// until the newline arrives), with the trailing CR/LF stripped. Returns an
+// empty string at end of file.
+static BasStringT *readLine(FILE *fp) {
+ int32_t cap = BAS_FILE_LINE_CHUNK;
+ int32_t len = 0;
+ char *buf = (char *)malloc(cap);
+
+ if (!buf) {
+ return basStringRef(basEmptyString);
+ }
+
+ buf[0] = '\0';
+
+ while (fgets(buf + len, cap - len, fp)) {
+ len += (int32_t)strlen(buf + len);
+
+ if (len > 0 && buf[len - 1] == '\n') {
+ break;
+ }
+
+ if (len + 1 >= cap - 1) {
+ int32_t newCap = cap * 2;
+ char *grown = (char *)realloc(buf, newCap);
+
+ if (!grown) {
+ break;
+ }
+
+ buf = grown;
+ cap = newCap;
+ }
+ }
+
+ while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
+ len--;
+ }
+
+ BasStringT *s = basStringNew(buf, len);
+ free(buf);
+ return s;
+}
+
+
+// GET/PUT record read whose short-read result is deliberately ignored: a
+// record past end of file yields the caller's zero default.
+static void readRaw(FILE *fp, void *dst, size_t size) {
+ size_t got = fread(dst, 1, size, fp);
+ (void)got;
+}
+
+
static inline uint16_t readUint16(BasVmT *vm) {
uint16_t val = 0;
@@ -6110,8 +6072,7 @@ static inline uint8_t readUint8(BasVmT *vm) {
// Releases every refcounted value the VM holds: the eval stack, the
// globals, all call-frame locals across callDepth, and the FOR-stack
-// limit/step across forDepth. Shared by basVmDestroy and basVmReset so
-// the teardown lives in one place. Must run BEFORE the caller zeroes
+// limit/step across forDepth. Must run BEFORE the caller zeroes
// sp/callDepth/forDepth, because the loops use those counts.
static void releaseVmState(BasVmT *vm) {
// Release eval stack values
@@ -6139,11 +6100,22 @@ static void releaseVmState(BasVmT *vm) {
}
+// Rounds a numeric operand to LONG (banker's rounding) for the integer
+// operators; raises Overflow when it does not fit.
+static BasVmResultE roundedOperand(BasVmT *vm, BasValueT v, int32_t *out) {
+ if (!basValRoundToInt32(v, INT32_MIN, INT32_MAX, out)) {
+ runtimeError(vm, BAS_ERR_OVERFLOW, "Overflow");
+ return BAS_VM_ERROR;
+ }
+
+ return BAS_VM_OK;
+}
+
+
// Executes instructions until callDepth drops back to savedCallDepth
// (meaning the subroutine returned). Periodically yields via the
// doEvents callback to keep the GUI responsive during long-running
// event handlers.
-
static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning) {
int32_t stepsSinceYield = 0;
bool hadBreakpoint = false;
@@ -6183,63 +6155,12 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
}
if (result != BAS_VM_OK) {
- // Try ON ERROR GOTO: walk call frames inside the current
- // sub-call boundary (savedCallDepth) looking for one that
- // registered a handler. If found, unwind to that frame
- // and jump to the handler. Mirrors basVmRun's dispatcher
- // so event handlers (fired via basVmCallSub) behave like
- // module-level code when it comes to error trapping.
- if (!vm->inErrorHandler && result != BAS_VM_BAD_OPCODE) {
- int32_t target = 0;
- int32_t resumePc = -1;
-
- while (vm->callDepth > savedCallDepth) {
- BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1];
-
- if (frame->errorHandler != 0) {
- target = frame->errorHandler;
- break;
- }
-
- for (int32_t li = 0; li < frame->localCount; li++) {
- basValRelease(&frame->locals[li]);
- }
-
- // Restore the caller's context -- see the basVmRun
- // dispatcher for the rationale.
- forStackTrim(vm, frame->savedForDepth);
- vm->stmtPc = frame->savedStmtPc;
- vm->stmtSp = frame->savedStmtSp;
- resumePc = frame->returnPc;
-
- vm->callDepth--;
- }
-
- if (target != 0) {
- // Release any operands the failed statement half-pushed
- // before the error fired, then truncate the eval stack
- // back to the statement boundary so trapped errors in a
- // loop don't leak values and grow sp without bound.
- if (vm->sp > vm->stmtSp) {
- for (int32_t si = vm->stmtSp; si < vm->sp; si++) {
- basValRelease(&vm->stack[si]);
- }
-
- vm->sp = vm->stmtSp;
- }
-
- // Statement-granular resume targets -- see the basVmRun
- // dispatcher for the rationale.
- int32_t nextPc = nextStatementPc(vm);
-
- vm->errorPc = (vm->stmtPc >= 0) ? vm->stmtPc : ((resumePc >= 0) ? resumePc : stepPc);
- vm->errorNextPc = (nextPc >= 0) ? nextPc : ((resumePc >= 0) ? resumePc : vm->pc);
- vm->inErrorHandler = true;
- vm->errorHandler = target;
- vm->pc = target;
- stepsSinceYield = 0;
- continue;
- }
+ // Try ON ERROR GOTO within the current sub-call boundary
+ // (savedCallDepth) so event handlers fired via basVmCallSub
+ // behave like module-level code when it comes to trapping.
+ if (dispatchError(vm, result, savedCallDepth, stepPc)) {
+ stepsSinceYield = 0;
+ continue;
}
// Frames the dispatcher did not unwind (error raised inside a
@@ -6315,6 +6236,74 @@ static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg) {
}
+// GET/PUT positioning. RANDOM channels address fixed-size records; BINARY
+// channels address bytes. Both are 1-based; 0 keeps the current position.
+static void seekRecord(BasVmT *vm, int32_t channel, int32_t recno) {
+ if (recno <= 0) {
+ return;
+ }
+
+ long offset = (vm->files[channel].mode == BAS_FILE_MODE_BINARY) ? (long)(recno - 1) : (long)(recno - 1) * vm->files[channel].recLen;
+
+ fseek((FILE *)vm->files[channel].handle, offset, SEEK_SET);
+}
+
+
+// Replaces *target with val, releasing the old value. A TYPE value that is
+// still shared with another variable is deep-copied first so assignment has
+// value semantics (b = a must not alias a).
+static void storeValue(BasValueT *target, BasValueT val) {
+ basValRelease(target);
+ *target = udtDetach(val);
+}
+
+
+// Length-aware substring search (strings may contain NUL bytes). Returns
+// the 0-based offset of the first occurrence of needle in haystack at or
+// after start, or -1. An empty needle matches at start (INSTR semantics).
+static int32_t stringFind(const BasStringT *haystack, int32_t start, const BasStringT *needle) {
+ if (needle->len == 0) {
+ return start;
+ }
+
+ for (int32_t i = start; i + needle->len <= haystack->len; i++) {
+ if (memcmp(haystack->data + i, needle->data, needle->len) == 0) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+
+// Wraps a string the caller already owns a reference to (no extra ref).
+static BasValueT strValue(BasStringT *s) {
+ BasValueT v;
+ v.type = BAS_TYPE_STRING;
+ v.strVal = s;
+ return v;
+}
+
+
+// Gives val its own TYPE instance when the one it carries is shared. The
+// shared reference is released; a clone failure leaves the value shared.
+static BasValueT udtDetach(BasValueT val) {
+ if (val.type != BAS_TYPE_UDT || !val.udtVal || val.udtVal->refCount <= 1) {
+ return val;
+ }
+
+ BasUdtT *copy = basUdtClone(val.udtVal);
+
+ if (!copy) {
+ return val;
+ }
+
+ basUdtUnref(val.udtVal);
+ val.udtVal = copy;
+ return val;
+}
+
+
static bool validArrayDims(BasVmT *vm, int32_t dims) {
if (dims < 1 || dims > BAS_ARRAY_MAX_DIMS) {
runtimeError(vm, BAS_ERR_SUBSCRIPT_RANGE, "Invalid array dimension count");
diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.h b/src/apps/kpunch/dvxbasic/runtime/vm.h
index 213f01f..c372a73 100644
--- a/src/apps/kpunch/dvxbasic/runtime/vm.h
+++ b/src/apps/kpunch/dvxbasic/runtime/vm.h
@@ -29,7 +29,7 @@
// BasVmT *vm = basVmCreate();
// basVmSetPrintCallback(vm, myPrintFn, myCtx);
// basVmSetInputCallback(vm, myInputFn, myCtx);
-// basVmLoadModule(vm, compiledCode, codeLen, constants, numConsts);
+// basVmLoadModule(vm, module);
// BasVmResultE result = basVmRun(vm);
// basVmDestroy(vm);
@@ -53,7 +53,8 @@
#define BAS_VM_MAX_LOCALS 64 // locals per stack frame
#define BAS_VM_MAX_FOR_DEPTH 32 // nested FOR loops
#define BAS_VM_MAX_FILES 16 // file channel array size; index 0 unused, so 15 usable channels (1..15)
-#define BAS_VM_RANDOM_RECORD_SIZE 128 // fixed RANDOM-mode record length (bytes)
+#define BAS_VM_RANDOM_RECORD_SIZE 128 // RANDOM-mode record length when OPEN has no LEN clause (bytes)
+#define BAS_VM_MAX_RECORD_LEN 32767 // largest LEN= a RANDOM-mode OPEN accepts
#define BAS_VM_DEFAULT_STEP_SLICE 10000 // bytecode steps per yield
#define BAS_VM_MAX_CALL_ARGS 16 // max args to OP_CALL_METHOD / OP_CALL_EXTERN
@@ -68,7 +69,6 @@
typedef enum {
BAS_VM_OK, // program completed normally
BAS_VM_HALTED, // HALT instruction reached
- BAS_VM_YIELDED, // DoEvents yielded control
BAS_VM_ERROR, // runtime error
BAS_VM_STACK_OVERFLOW,
BAS_VM_STACK_UNDERFLOW,
@@ -79,7 +79,6 @@ typedef enum {
BAS_VM_BAD_OPCODE,
BAS_VM_FILE_ERROR,
BAS_VM_SUBSCRIPT_RANGE,
- BAS_VM_USER_ERROR, // ON ERROR raised
BAS_VM_STEP_LIMIT, // step limit reached (not an error)
BAS_VM_BREAKPOINT // hit breakpoint or step completed (not an error)
} BasVmResultE;
@@ -262,7 +261,6 @@ typedef struct {
typedef struct {
int32_t returnPc; // instruction to return to
- int32_t baseSlot; // base index in locals array
int32_t localCount; // number of locals in this frame
int32_t errorHandler; // ON ERROR GOTO target in this SUB (0 = none)
int32_t savedForDepth; // vm->forDepth when this frame was pushed (restored on return so EXIT SUB/FUNCTION inside FOR bodies cannot leak FOR frames)
@@ -290,17 +288,21 @@ typedef struct {
typedef struct {
void *handle; // FILE* or platform-specific
int32_t mode; // BasFileModeE value
+ int32_t recLen; // RANDOM-mode record length (OPEN ... LEN=)
} BasFileChannelT;
// ============================================================
// Procedure table entry (retained from symbol table for runtime)
// ============================================================
-#define BAS_MAX_PROC_NAME 64
+// Buffer size for every identifier the compiler, runtime, form runtime
+// and IDE store by name: procedures, symbols, forms, controls, events.
+// One define so a name can never fit one table and truncate in another.
+#define BAS_MAX_IDENT 64
typedef struct {
- char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved)
- char formName[BAS_MAX_PROC_NAME]; // owning form (for form-scope vars), "" if global
+ char name[BAS_MAX_IDENT]; // SUB/FUNCTION name (case-preserved)
+ char formName[BAS_MAX_IDENT]; // owning form (for form-scope vars), "" if global
int32_t codeAddr; // entry point in code[]
int32_t paramCount; // number of parameters
int32_t localCount; // number of local variables (for debugger)
@@ -310,13 +312,13 @@ typedef struct {
// Debug UDT field definition (preserved for debugger watch)
typedef struct {
- char name[BAS_MAX_PROC_NAME];
+ char name[BAS_MAX_IDENT];
uint8_t dataType;
} BasDebugFieldT;
// Debug UDT type definition (preserved for debugger watch)
typedef struct {
- char name[BAS_MAX_PROC_NAME];
+ char name[BAS_MAX_IDENT];
int32_t typeId; // matches BasUdtT.typeId
BasDebugFieldT *fields; // malloc'd array
int32_t fieldCount;
@@ -324,8 +326,8 @@ typedef struct {
// Debug variable info (preserved in module for debugger display)
typedef struct {
- char name[BAS_MAX_PROC_NAME];
- char formName[BAS_MAX_PROC_NAME]; // form name for SCOPE_FORM vars (empty for others)
+ char name[BAS_MAX_IDENT];
+ char formName[BAS_MAX_IDENT]; // form name for SCOPE_FORM vars (empty for others)
uint8_t scope; // SCOPE_GLOBAL, SCOPE_LOCAL, SCOPE_FORM
uint8_t dataType; // BAS_TYPE_*
int32_t index; // variable slot index
@@ -337,7 +339,7 @@ typedef struct {
// ============================================================
typedef struct {
- char formName[BAS_MAX_PROC_NAME];
+ char formName[BAS_MAX_IDENT];
int32_t varCount;
int32_t initCodeAddr; // offset in module->code for per-form init (-1 = none)
int32_t initCodeLen; // length of init bytecode
@@ -347,16 +349,16 @@ typedef struct {
// Compiled module (output of the compiler)
// ============================================================
-// Runtime-required global init entry. STRING and SINGLE/DOUBLE
-// globals need to start with the correct slot type even when debug
-// info has been stripped, or operators that switch on slot type
-// (e.g. STRING concat) break on first use.
+// Runtime-required global init entry. STRING globals need to start
+// with an empty string in the slot even when debug info has been
+// stripped, or operators that switch on slot type (STRING concat)
+// break on first use. Only BAS_TYPE_STRING entries are emitted and
+// acted on; other types keep the zero default.
typedef struct {
int32_t index; // global slot index
uint8_t dataType; // BAS_TYPE_*
} BasGlobalInitT;
-
typedef struct {
uint8_t *code; // p-code bytecode
int32_t codeLen;
@@ -390,7 +392,6 @@ typedef struct {
int32_t pc; // program counter
bool running;
bool ended; // END statement executed -- program should terminate
- bool yielded;
bool badOperand; // operand read ran past code[] -- raise BAS_VM_BAD_OPCODE next step
int32_t stepLimit; // max steps per basVmRun (0 = unlimited)
int32_t stepCount; // steps executed in last basVmRun
@@ -410,7 +411,7 @@ typedef struct {
BasValueT stack[BAS_VM_STACK_SIZE];
int32_t sp; // stack pointer (index of next free slot)
int32_t stmtSp; // eval-stack depth snapshot at last OP_LINE (statement boundary)
- int32_t stmtPc; // PC of the OP_LINE that opened the current statement (-1 = none seen, e.g. compacted release bytecode)
+ int32_t stmtPc; // PC of the OP_LINE/OP_STMT that opened the current statement (-1 = none seen)
// Call stack
BasCallFrameT callStack[BAS_VM_CALL_STACK_SIZE];
@@ -440,8 +441,9 @@ typedef struct {
// String comparison mode
bool compareTextMode; // true = case-insensitive comparisons
- // Error handling
- int32_t errorHandler; // PC of ON ERROR GOTO handler (0 = none)
+ // Error handling. The active ON ERROR handler lives on the call frame
+ // that installed it (BasCallFrameT.errorHandler); the dispatcher walks
+ // frames to find it.
int32_t errorNumber; // current Err number
int32_t errorPc; // PC of the instruction that caused the error (for RESUME)
int32_t errorNextPc; // PC of the next instruction after error (for RESUME NEXT)
@@ -497,7 +499,9 @@ BasVmT *basVmCreate(void);
// Destroy a VM instance and free all resources.
void basVmDestroy(BasVmT *vm);
-// Load a compiled module into the VM.
+// Load a compiled module into the VM. A module that needs more global
+// slots than BAS_VM_MAX_GLOBALS is refused: vm->module stays NULL and
+// basVmRun reports BAS_VM_ERROR with the reason in basVmGetError().
void basVmLoadModule(BasVmT *vm, BasModuleT *module);
// Execute the loaded module. Returns when the program ends,
@@ -508,9 +512,6 @@ BasVmResultE basVmRun(BasVmT *vm);
// Useful for stepping/debugging.
BasVmResultE basVmStep(BasVmT *vm);
-// Reset the VM to initial state (clear stack, globals, PC).
-void basVmReset(BasVmT *vm);
-
// Set I/O callbacks.
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx);
@@ -532,10 +533,6 @@ void basVmSetCurrentFormVars(BasVmT *vm, BasValueT *vars, int32_t count);
// The VM remains in a runnable state -- call basVmRun again to continue.
void basVmSetStepLimit(BasVmT *vm, int32_t limit);
-// Push/pop values on the evaluation stack (for host integration).
-bool basVmPush(BasVmT *vm, BasValueT val);
-bool basVmPop(BasVmT *vm, BasValueT *val);
-
// Get the current error message.
const char *basVmGetError(const BasVmT *vm);
@@ -556,9 +553,6 @@ void basVmStepOut(BasVmT *vm);
// Run to cursor: break when reaching the specified source line.
void basVmRunToCursor(BasVmT *vm, int32_t line);
-// Get the current source line (from the last OP_LINE instruction).
-int32_t basVmGetCurrentLine(const BasVmT *vm);
-
// Call a SUB by code address from the host.
// Pushes a call frame, runs until the SUB returns, then restores
// the previous execution state. Returns true if the SUB was called
diff --git a/src/apps/kpunch/dvxbasic/stub/bascomp.c b/src/apps/kpunch/dvxbasic/stub/bascomp.c
index feed6c5..75f9dca 100644
--- a/src/apps/kpunch/dvxbasic/stub/bascomp.c
+++ b/src/apps/kpunch/dvxbasic/stub/bascomp.c
@@ -31,17 +31,9 @@
// The stub (basstub.app) is read from the same directory as the
// compiler executable.
-#include "../compiler/compact.h"
-#include "../compiler/lexer.h"
-#include "../compiler/obfuscate.h"
#include "../compiler/parser.h"
-#include "../compiler/strip.h"
#include "../compiler/symtab.h"
-#include "../compiler/opcodes.h"
#include "../runtime/vm.h"
-#include "../runtime/values.h"
-#include "../runtime/serialize.h"
-#include "../../../../libs/kpunch/libdvx/dvxRes.h"
#include "../../../../libs/kpunch/libdvx/dvxPrefs.h"
#include "../../../../libs/kpunch/libdvx/dvxTypes.h"
#include "../../../../libs/kpunch/libdvx/platform/dvxPlat.h"
@@ -54,6 +46,7 @@
#include
#include
#include
+#include
// Initial capacity of the source-concatenation buffer, and the hard
// ceiling its capacity may reach. The ceiling keeps the int32 capacity
@@ -61,11 +54,20 @@
#define CONCAT_INITIAL_CAP 8192
#define CONCAT_MAX_CAP 0x40000000 // 1 GiB cap; keeps int32 doubling safe
+// Kernel view of the running image on hosted OSes.
+#define SELF_EXE_PATH "/proc/self/exe"
+
// Function prototypes (alphabetical)
+static void buildLog(const char *msg);
static bool concatGrow(char **buf, int32_t *cap, int32_t need);
-static const char *extractFormCode(const char *frmText);
-int main(int argc, char **argv);
+static const char *selfPath(const char *argv0);
static void usage(void);
+int main(int argc, char **argv);
+
+
+static void buildLog(const char *msg) {
+ printf("%s\n", msg);
+}
static bool concatGrow(char **buf, int32_t *cap, int32_t need) {
@@ -97,45 +99,18 @@ static bool concatGrow(char **buf, int32_t *cap, int32_t need) {
}
-static const char *extractFormCode(const char *frmText) {
- if (!frmText) {
- return NULL;
+// Path of this executable, which carries the embedded STUB and NOICON
+// resources. On a hosted OS argv[0] is whatever the shell typed (a bare
+// name when invoked through PATH), so the kernel's view of the running
+// image is preferred; DJGPP always passes a full path in argv[0].
+static const char *selfPath(const char *argv0) {
+ #ifndef __DJGPP__
+ if (access(SELF_EXE_PATH, R_OK) == 0) {
+ return SELF_EXE_PATH;
}
+ #endif
- const char *p = frmText;
- int32_t depth = 0;
- bool inForm = false;
-
- while (*p) {
- // Skip leading whitespace
- p = dvxSkipWs(p);
-
- if (strncasecmp(p, "Begin ", 6) == 0) {
- if (!inForm && strncasecmp(p + 6, "Form ", 5) == 0) {
- inForm = true;
- }
-
- depth++;
- } else if (strncasecmp(p, "End", 3) == 0 && (p[3] == '\0' || p[3] == '\r' || p[3] == '\n' || p[3] == ' ')) {
- depth--;
-
- if (depth <= 0 && inForm) {
- // Skip past this line
- while (*p && *p != '\n') { p++; }
-
- if (*p == '\n') { p++; }
-
- return p;
- }
- }
-
- // Skip to next line
- while (*p && *p != '\n') { p++; }
-
- if (*p == '\n') { p++; }
- }
-
- return NULL;
+ return argv0;
}
@@ -158,7 +133,7 @@ int main(int argc, char **argv) {
const char *outputPath = NULL;
bool release = false;
- for (int i = 1; i < argc; i++) {
+ for (int32_t i = 1; i < argc; i++) {
if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
outputPath = argv[++i];
} else if (strcmp(argv[i], "-release") == 0) {
@@ -207,8 +182,6 @@ int main(int argc, char **argv) {
const char *description = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, "");
const char *iconPath = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, "");
const char *helpFile = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, "");
- const char *startupForm = prefsGetString(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, "");
- (void)startupForm; // used implicitly by stub's basFormRtLoadAllForms
bool optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
// Derive output path
@@ -283,8 +256,9 @@ int main(int argc, char **argv) {
// Pass 0: .bas modules, Pass 1: .frm code sections
for (int32_t pass = 0; pass < 2; pass++) {
for (int32_t i = 0; i < fileCount; i++) {
- if (pass == 0 && files[i].isForm) { continue; }
- if (pass == 1 && !files[i].isForm) { continue; }
+ if (files[i].isForm != (pass == 1)) {
+ continue;
+ }
int32_t srcLen = 0;
char *srcBuf = platformReadFile(files[i].path, &srcLen);
@@ -298,14 +272,11 @@ int main(int argc, char **argv) {
if (files[i].isForm) {
// Extract form name from "Begin Form "
- char formName[BAS_MAX_SYMBOL_NAME] = "";
- basExtractFormName(srcBuf, formName, BAS_MAX_SYMBOL_NAME);
+ char formName[BAS_MAX_IDENT] = "";
+ basExtractFormName(srcBuf, formName, BAS_MAX_IDENT);
- code = extractFormCode(srcBuf);
-
- if (!code) {
- code = "";
- }
+ // The BASIC code section follows the outer Begin Form block.
+ code = srcBuf + basFindFormEndPos(srcBuf, srcLen);
int32_t codeLen = (int32_t)strlen(code);
@@ -387,171 +358,22 @@ int main(int argc, char **argv) {
printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount);
- // Strip for release
- if (release) {
- basStripModule(mod);
- printf(" stripped debug info\n");
- }
-
- // Read all .frm texts up front; they're used for obfuscation and
- // then embedded as FORM0, FORM1, ... resources below. Parallel
- // dynamic arrays, one entry per form file.
- char **frmData = NULL; // stb_ds: strdup'd stripped form text
- int32_t *frmLens = NULL; // stb_ds: length of stripped form text
+ // Raw .frm texts; the shared build pipeline strips comments and, for
+ // release builds, obfuscates them.
+ char **frmSources = NULL; // stb_ds: owned form text
for (int32_t i = 0; i < fileCount; i++) {
if (!files[i].isForm) {
continue;
}
- int32_t flen = 0;
- char *fdata = platformReadFile(files[i].path, &flen);
+ char *fdata = platformReadFile(files[i].path, NULL);
- if (!fdata) {
- continue;
- }
-
- // Strip comments from the .frm text unconditionally. Comments
- // are source-only; they shouldn't ship in the embedded resource
- // for either debug or release builds.
- int32_t stripCap = flen + 16;
- uint8_t *stripped = (uint8_t *)malloc(stripCap);
-
- if (!stripped) {
- free(fdata);
- continue;
- }
-
- int32_t strippedLen = basStripFrmComments(fdata, flen, stripped, stripCap);
- free(fdata);
-
- arrput(frmData, (char *)stripped);
- arrput(frmLens, strippedLen);
- }
-
- int32_t frmCount = (int32_t)arrlen(frmData);
-
- // Obfuscate form/control names in release mode
- BasObfFrmT *obfFrms = NULL;
-
- for (int32_t i = 0; i < frmCount; i++) {
- BasObfFrmT empty = { NULL, 0 };
- arrput(obfFrms, empty);
- }
-
- if (release && frmCount > 0) {
- const char **frmTexts = NULL;
-
- for (int32_t i = 0; i < frmCount; i++) {
- arrput(frmTexts, frmData[i]);
- }
-
- basObfuscateNames(mod, frmTexts, frmLens, frmCount, obfFrms);
- arrfree(frmTexts);
- printf(" obfuscated %d form(s)\n", (int)frmCount);
- }
-
- // Remove OP_LINE instructions and compact the bytecode.
- if (release) {
- int32_t removed = basCompactBytecode(mod);
-
- if (removed > 0) {
- printf(" compacted bytecode (-%d bytes)\n", (int)removed);
+ if (fdata) {
+ arrput(frmSources, fdata);
}
}
- // Serialize module
- int32_t modLen = 0;
- uint8_t *modData = basModuleSerialize(mod, &modLen);
-
- if (!modData) {
- fprintf(stderr, "Error: failed to serialize module.\n");
- basModuleFree(mod);
- goto failForms;
- }
-
- // Serialize debug info
- int32_t dbgLen = 0;
- uint8_t *dbgData = NULL;
-
- if (!release) {
- dbgData = basDebugSerialize(mod, &dbgLen);
- }
-
- basModuleFree(mod);
-
- // Read the stub DXE embedded in our own executable as a resource.
- // The bascomp Makefile appends basstub.app to bascomp post-link
- // via `dvxres add ... STUB binary @basstub.app`, so BASSTUB.APP no
- // longer has to sit alongside the compiler.
- DvxResHandleT *selfRes = dvxResOpen(argv[0]);
-
- if (!selfRes) {
- fprintf(stderr, "Error: cannot open %s to read embedded stub.\n", argv[0]);
- free(modData);
- free(dbgData);
- goto failForms;
- }
-
- uint32_t stubLen = 0;
- void *stubData = dvxResRead(selfRes, BAS_RES_STUB, &stubLen);
- dvxResClose(selfRes);
-
- if (!stubData) {
- fprintf(stderr, "Error: STUB resource not found in %s.\n", argv[0]);
- free(modData);
- free(dbgData);
- goto failForms;
- }
-
- // Write stub to output file
- FILE *out = fopen(outputPath, "wb");
-
- if (!out) {
- fprintf(stderr, "Error: cannot create %s\n", outputPath);
- free(stubData);
- free(modData);
- free(dbgData);
- goto failForms;
- }
-
- size_t stubWritten = fwrite(stubData, 1, stubLen, out);
- int stubClose = fclose(out);
- free(stubData);
-
- if (stubWritten != stubLen || stubClose != 0) {
- fprintf(stderr, "Error: failed writing stub to %s\n", outputPath);
- free(modData);
- free(dbgData);
- goto failForms;
- }
-
- // Pick the right form bytes per build mode: release builds use the
- // obfuscated variant when available, debug builds always use the raw
- // stripped text.
- const uint8_t **emitFormData = NULL;
- int32_t *emitFormLens = NULL;
-
- for (int32_t fi = 0; fi < frmCount; fi++) {
- if (release && obfFrms[fi].data) {
- arrput(emitFormData, (const uint8_t *)obfFrms[fi].data);
- arrput(emitFormLens, obfFrms[fi].len);
- } else {
- arrput(emitFormData, (const uint8_t *)frmData[fi]);
- arrput(emitFormLens, frmLens[fi]);
- }
- }
-
- // Resolve icon disk path (if any) against the project directory.
- char iconFullPath[DVX_MAX_PATH];
- const char *iconDiskPath = NULL;
-
- if (iconPath[0]) {
- snprintf(iconFullPath, sizeof(iconFullPath), "%s/%s", projectDir, iconPath);
- iconDiskPath = iconFullPath;
- }
-
- // Emit all resources via the shared helper.
BasBuildSpecT spec;
memset(&spec, 0, sizeof(spec));
spec.projName = projName;
@@ -560,94 +382,34 @@ int main(int argc, char **argv) {
spec.version = version;
spec.copyright = copyright;
spec.description = description;
+ spec.projectDir = projectDir;
+ spec.iconPath = iconPath;
spec.helpFile = helpFile;
- spec.iconPath = iconDiskPath;
- spec.moduleData = modData;
- spec.moduleLen = modLen;
- spec.debugData = dbgData;
- spec.debugLen = dbgLen;
- spec.formCount = frmCount;
- spec.formData = emitFormData;
- spec.formLens = emitFormLens;
+ spec.selfPath = selfPath(argv[0]);
+ spec.module = mod;
+ spec.frmSources = (const char *const *)frmSources;
+ spec.frmCount = (int32_t)arrlen(frmSources);
+ spec.release = release;
+ spec.log = buildLog;
- int32_t emitRc = basBuildEmitResources(outputPath, &spec);
+ const char *failure = basBuildApp(outputPath, &spec);
- free(modData);
- free(dbgData);
+ for (int32_t i = 0; i < (int32_t)arrlen(frmSources); i++) {
+ free(frmSources[i]);
+ }
- if (emitRc != 0) {
- fprintf(stderr, "Error: failed writing resources to %s\n", outputPath);
+ arrfree(frmSources);
+ basModuleFree(mod);
+ arrfree(files);
+ prefsClose(prefs);
- for (int32_t i = 0; i < frmCount; i++) {
- free(frmData[i]);
- free(obfFrms[i].data);
- }
-
- arrfree(files);
- arrfree(frmData);
- arrfree(frmLens);
- arrfree(obfFrms);
- arrfree(emitFormData);
- arrfree(emitFormLens);
- prefsClose(prefs);
+ if (failure) {
+ fprintf(stderr, "Error: %s\n", failure);
return 1;
}
- // Copy help file to output directory (the HELPFILE resource itself was
- // written by basBuildEmitResources).
- if (helpFile[0]) {
- const char *helpBase = platformPathBaseName(helpFile);
-
- char helpSrc[DVX_MAX_PATH];
- snprintf(helpSrc, sizeof(helpSrc), "%s/%s", projectDir, helpFile);
-
- char outDir[DVX_MAX_PATH];
- snprintf(outDir, sizeof(outDir), "%s", outputPath);
- char *outSep = platformPathDirEnd(outDir);
-
- if (outSep) {
- *outSep = '\0';
- } else {
- outDir[0] = '.';
- outDir[1] = '\0';
- }
-
- char helpDst[DVX_MAX_PATH];
- snprintf(helpDst, sizeof(helpDst), "%s/%s", outDir, helpBase);
-
- int32_t hLen = 0;
- char *hData = platformReadFile(helpSrc, &hLen);
-
- if (hData) {
- FILE *hf = fopen(helpDst, "wb");
-
- if (hf) {
- fwrite(hData, 1, hLen, hf);
- fclose(hf);
- }
-
- free(hData);
- }
- }
-
- // Free .frm buffers
- for (int32_t i = 0; i < frmCount; i++) {
- free(frmData[i]);
- free(obfFrms[i].data);
- }
-
- arrfree(files);
- arrfree(frmData);
- arrfree(frmLens);
- arrfree(obfFrms);
- arrfree(emitFormData);
- arrfree(emitFormLens);
-
- prefsClose(prefs);
-
- // Report the true on-disk size of the finished app, which includes the
- // stub, bytecode, debug info, icon, metadata, and all FORMn resources
- // appended by basBuildEmitResources -- not just stub + module bytes.
+ // Report the true on-disk size of the finished app (stub, bytecode,
+ // debug info, icon, metadata and every FORMn resource).
int32_t outBytes = 0;
FILE *szf = fopen(outputPath, "rb");
@@ -666,21 +428,8 @@ int main(int argc, char **argv) {
return 0;
// ----- Error cleanup paths -----
- // failForms: frm arrays already exist; free their elements and the
- // arrays, then fall through to free files.
- // failFiles: only the files array exists.
- // Each early error frees its own raw malloc'd buffers (modData,
- // dbgData, stubData, concatBuf, parser) before jumping here.
-failForms:
- for (int32_t i = 0; i < frmCount; i++) {
- free(frmData[i]);
- free(obfFrms[i].data);
- }
-
- arrfree(frmData);
- arrfree(frmLens);
- arrfree(obfFrms);
- // fall through
+ // failFiles: only the files array exists. Each early error frees its
+ // own raw malloc'd buffers (concatBuf, parser) before jumping here.
failFiles:
arrfree(files);
prefsClose(prefs);
diff --git a/src/apps/kpunch/dvxbasic/stub/basstub.c b/src/apps/kpunch/dvxbasic/stub/basstub.c
index 27246a9..6020ac9 100644
--- a/src/apps/kpunch/dvxbasic/stub/basstub.c
+++ b/src/apps/kpunch/dvxbasic/stub/basstub.c
@@ -68,12 +68,24 @@ AppDescriptorT appDescriptor = {
static AppContextT *sAc = NULL;
-// Function prototypes (alphabetical; main/appMain last)
+// Function prototypes (alphabetical; appMain last)
void appShutdown(void);
static bool stubDoEvents(void *ctx);
static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize);
static void stubPrint(void *ctx, const char *text, bool newline);
int32_t appMain(DxeAppContextT *ctx);
+
+
+// Resolved by the shell as _appShutdown and called on BOTH graceful reap and
+// force-kill, before this app's DXE is unmapped. On a normal exit appMain
+// already ran basFormRtDestroy; on a force-kill it never returns, so this is
+// the only path that drops the serial/secLink idle pollers from the shell
+// registry -- otherwise they would keep polling this app's freed terminals.
+void appShutdown(void) {
+ basFormRtSerialShutdown();
+}
+
+
static bool stubDoEvents(void *ctx) {
(void)ctx;
@@ -104,14 +116,6 @@ static void stubPrint(void *ctx, const char *text, bool newline) {
}
-// Resolved by the shell as _appShutdown and called on BOTH graceful reap and
-// force-kill, before this app's DXE is unmapped. On a normal exit appMain
-// already ran basFormRtDestroy; on a force-kill it never returns, so this is
-// the only path that drops the serial/secLink idle pollers from the shell
-// registry -- otherwise they would keep polling this app's freed terminals.
-void appShutdown(void) {
- basFormRtSerialShutdown();
-}
int32_t appMain(DxeAppContextT *ctx) {
sAc = ctx->shellCtx;
@@ -123,7 +127,9 @@ int32_t appMain(DxeAppContextT *ctx) {
return 1;
}
- // Read app name and update the shell's app record
+ // Read app name and update the shell's app record. dvxResRead returns
+ // exactly the stored size with no terminator of its own, so bound every
+ // text resource by its size instead of trusting a trailing NUL.
uint32_t nameSize = 0;
char *appName = (char *)dvxResRead(res, BAS_RES_NAME, &nameSize);
@@ -131,7 +137,7 @@ int32_t appMain(DxeAppContextT *ctx) {
ShellAppT *app = shellGetApp(ctx->appId);
if (app) {
- snprintf(app->name, SHELL_APP_NAME_MAX, "%s", appName);
+ snprintf(app->name, SHELL_APP_NAME_MAX, "%.*s", (int)nameSize, appName);
}
free(appName);
@@ -142,7 +148,7 @@ int32_t appMain(DxeAppContextT *ctx) {
char *helpName = (char *)dvxResRead(res, BAS_RES_HELPFILE, &helpNameSize);
if (helpName) {
- snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%s", ctx->appDir, helpName);
+ snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%.*s", ctx->appDir, (int)helpNameSize, helpName);
free(helpName);
}
@@ -176,6 +182,14 @@ int32_t appMain(DxeAppContextT *ctx) {
// Create VM
BasVmT *vm = basVmCreate();
+
+ if (!vm) {
+ dvxMessageBox(sAc, "Error", "Out of memory creating the BASIC runtime.", 0);
+ basModuleFree(mod);
+ dvxResClose(res);
+ return 1;
+ }
+
basVmLoadModule(vm, mod);
basVmSetPrintCallback(vm, stubPrint, NULL);
basVmSetInputCallback(vm, stubInput, NULL);
@@ -201,9 +215,17 @@ int32_t appMain(DxeAppContextT *ctx) {
// Create form runtime
BasFormRtT *rt = basFormRtCreate(sAc, vm, mod);
+ if (!rt) {
+ dvxMessageBox(sAc, "Error", "Out of memory creating the form runtime.", 0);
+ basVmDestroy(vm);
+ basModuleFree(mod);
+ dvxResClose(res);
+ return 1;
+ }
+
// Register .frm source text for lazy loading
for (int32_t i = 0; i < BAS_MAX_FORM_RESOURCES; i++) {
- char resName[16];
+ char resName[BAS_RES_FORM_NAME_LEN];
snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)i);
uint32_t frmSize = 0;
@@ -230,11 +252,9 @@ int32_t appMain(DxeAppContextT *ctx) {
frmText = frmTextZ;
frmText[frmSize] = '\0';
- // Extract form name from "Begin Form " line. Use the full
- // form-name length so a 32-63 char form name is not truncated here
- // (which would break name-keyed form-scope variable binding).
- char frmName[BAS_MAX_FORM_NAME] = "";
- basExtractFormName(frmText, frmName, BAS_MAX_FORM_NAME);
+ // Extract form name from "Begin Form " line.
+ char frmName[BAS_MAX_IDENT] = "";
+ basExtractFormName(frmText, frmName, BAS_MAX_IDENT);
if (frmName[0]) {
basFormRtRegisterFrm(rt, frmName, frmText, (int32_t)frmSize);
diff --git a/src/apps/kpunch/dvxbasic/test_compact.c b/src/apps/kpunch/dvxbasic/test_compact.c
index cea9237..100f7f0 100644
--- a/src/apps/kpunch/dvxbasic/test_compact.c
+++ b/src/apps/kpunch/dvxbasic/test_compact.c
@@ -161,8 +161,6 @@ int main(void) {
printf("DVX BASIC Bytecode Compaction Tests\n");
printf("====================================\n\n");
- basStringSystemInit();
-
// ---- Basic control flow ----
testCompact("FOR loop",
@@ -355,6 +353,120 @@ int main(void) {
"END SUB\n"
);
+ // ---- ON ERROR + RESUME: statement boundaries must survive compaction ----
+
+ testCompact("RESUME NEXT after trapped error",
+ "ON ERROR GOTO h\n"
+ "DIM a AS INTEGER\n"
+ "a = 0\n"
+ "PRINT \"before\"\n"
+ "a = 10 \\ a\n"
+ "PRINT \"after\"; a\n"
+ "END\n"
+ "h:\n"
+ "PRINT \"err\"\n"
+ "RESUME NEXT\n"
+ );
+
+ testCompact("RESUME re-runs the statement",
+ "ON ERROR GOTO h\n"
+ "DIM a AS INTEGER\n"
+ "DIM n AS INTEGER\n"
+ "n = 0\n"
+ "PRINT \"before\"\n"
+ "a = 10 \\ n\n"
+ "PRINT \"after\"; a\n"
+ "END\n"
+ "h:\n"
+ "PRINT \"err\"\n"
+ "n = 5\n"
+ "RESUME\n"
+ );
+
+ testCompact("Error inside GOSUB target",
+ "ON ERROR GOTO h\n"
+ "DIM a AS INTEGER\n"
+ "GOSUB s1\n"
+ "PRINT \"back\"\n"
+ "END\n"
+ "s1:\n"
+ "a = 10 \\ 0\n"
+ "PRINT \"in sub after\"\n"
+ "RETURN\n"
+ "h:\n"
+ "PRINT \"err\"\n"
+ "RESUME NEXT\n"
+ );
+
+ testCompact("Error mid-expression with RESUME NEXT",
+ "ON ERROR GOTO h\n"
+ "DIM s AS STRING\n"
+ "s = \"x\" + STR$(10 \\ 0) + \"y\"\n"
+ "PRINT \"s=\"; s\n"
+ "END\n"
+ "h:\n"
+ "PRINT \"err\"\n"
+ "RESUME NEXT\n"
+ );
+
+ testCompact("Handler in SUB that returns",
+ "SUB risky\n"
+ " ON ERROR GOTO h\n"
+ " PRINT 1 \\ 0\n"
+ " PRINT \"unreached\"\n"
+ " EXIT SUB\n"
+ "h:\n"
+ " PRINT \"caught\"\n"
+ "END SUB\n"
+ "risky\n"
+ "risky\n"
+ "PRINT \"done\"\n"
+ );
+
+ // ---- Remaining control-flow shapes ----
+
+ testCompact("Empty-range FOR",
+ "DIM i AS INTEGER\n"
+ "FOR i = 5 TO 1\n"
+ " PRINT \"never\"\n"
+ "NEXT i\n"
+ "PRINT \"done\"\n"
+ );
+
+ testCompact("ON x GOSUB",
+ "DIM x AS INTEGER\n"
+ "x = 2\n"
+ "ON x GOSUB a, b\n"
+ "PRINT \"end\"\n"
+ "END\n"
+ "a:\n"
+ "PRINT \"a\"\n"
+ "RETURN\n"
+ "b:\n"
+ "PRINT \"b\"\n"
+ "RETURN\n"
+ );
+
+ testCompact("DO LOOP UNTIL",
+ "DIM n AS INTEGER\n"
+ "n = 0\n"
+ "DO\n"
+ " n = n + 1\n"
+ " PRINT n;\n"
+ "LOOP UNTIL n >= 3\n"
+ "PRINT\n"
+ );
+
+ testCompact("WHILE WEND",
+ "DIM n AS INTEGER\n"
+ "n = 3\n"
+ "WHILE n > 0\n"
+ " PRINT n;\n"
+ " n = n - 1\n"
+ "WEND\n"
+ "PRINT\n"
+ );
+
printf("\n%d/%d tests passed\n", (int)(sTotal - sFailed), (int)sTotal);
return sFailed > 0 ? 1 : 0;
}
diff --git a/src/apps/kpunch/dvxbasic/test_compiler.c b/src/apps/kpunch/dvxbasic/test_compiler.c
index 75c9455..f2b2810 100644
--- a/src/apps/kpunch/dvxbasic/test_compiler.c
+++ b/src/apps/kpunch/dvxbasic/test_compiler.c
@@ -28,6 +28,7 @@
#include "runtime/vm.h"
#include "runtime/values.h"
+#include
#include
#include
@@ -36,11 +37,78 @@
// always exiting 0 while printing FAIL.
static int32_t gFailCount = 0;
+// PRINT output captured from the VM for comparison against the expected
+// text of a test program.
+#define CAPTURE_MAX 8192
+
+typedef struct {
+ char buf[CAPTURE_MAX];
+ int32_t len;
+} CaptureT;
+
// Function prototypes
-static void runProgram(const char *name, const char *source);
+static void captureAppend(CaptureT *c, const char *s);
+static void capturePrint(void *ctx, const char *text, bool newline);
+static void expectCompileError(const char *name, const char *source, const char *needle);
+static void runProgram(const char *name, const char *source, const char *expected);
-static void runProgram(const char *name, const char *source) {
+static void captureAppend(CaptureT *c, const char *s) {
+ int32_t n = (int32_t)strlen(s);
+
+ if (c->len + n >= CAPTURE_MAX - 1) {
+ n = CAPTURE_MAX - 1 - c->len;
+ }
+
+ if (n > 0) {
+ memcpy(c->buf + c->len, s, n);
+ c->len += n;
+ c->buf[c->len] = '\0';
+ }
+}
+
+
+static void capturePrint(void *ctx, const char *text, bool newline) {
+ CaptureT *c = (CaptureT *)ctx;
+
+ if (text) {
+ captureAppend(c, text);
+ }
+
+ if (newline) {
+ captureAppend(c, "\n");
+ }
+}
+
+
+// Compile source expecting it to be rejected; the error text must
+// contain needle.
+static void expectCompileError(const char *name, const char *source, const char *needle) {
+ printf("=== %s ===\n", name);
+
+ BasParserT parser;
+ basParserInit(&parser, source, (int32_t)strlen(source));
+
+ bool ok = basParse(&parser);
+
+ if (ok) {
+ printf("FAIL: compiled but expected error containing [%s]\n\n", needle);
+ gFailCount++;
+ } else if (strstr(parser.error, needle) == NULL) {
+ printf("FAIL: error [%s] does not contain [%s]\n\n", parser.error, needle);
+ gFailCount++;
+ } else {
+ printf("OK (expected error: %s)\n\n", parser.error);
+ }
+
+ basParserFree(&parser);
+}
+
+
+// Compile and run source. PRINT output is captured and, when expected
+// is not NULL, must match it exactly; a compile error or a VM error is
+// always a failure.
+static void runProgram(const char *name, const char *source, const char *expected) {
printf("=== %s ===\n", name);
int32_t len = (int32_t)strlen(source);
@@ -64,14 +132,24 @@ static void runProgram(const char *name, const char *source) {
return;
}
+ CaptureT cap;
+ cap.buf[0] = '\0';
+ cap.len = 0;
+
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
+ basVmSetPrintCallback(vm, capturePrint, &cap);
BasVmResultE result = basVmRun(vm);
+ fputs(cap.buf, stdout);
+
if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
printf("[VM error %d: %s]\n", result, basVmGetError(vm));
gFailCount++;
+ } else if (expected != NULL && strcmp(cap.buf, expected) != 0) {
+ printf("FAIL: expected output [%s]\n", expected);
+ gFailCount++;
}
basVmDestroy(vm);
@@ -84,11 +162,10 @@ int main(void) {
printf("DVX BASIC Compiler Tests\n");
printf("========================\n\n");
- basStringSystemInit();
-
// Test 1: Hello World
runProgram("Hello World",
- "PRINT \"Hello, World!\"\n"
+ "PRINT \"Hello, World!\"\n",
+ "Hello, World!\n"
);
// Test 2: Arithmetic
@@ -96,7 +173,8 @@ int main(void) {
"PRINT 2 + 3 * 4\n"
"PRINT 10 \\ 3\n"
"PRINT 10 MOD 3\n"
- "PRINT 2 ^ 8\n"
+ "PRINT 2 ^ 8\n",
+ "14 \n3 \n1 \n256 \n"
);
// Test 3: String operations
@@ -108,7 +186,8 @@ int main(void) {
"PRINT LEFT$(s, 5)\n"
"PRINT RIGHT$(s, 6)\n"
"PRINT MID$(s, 8, 5)\n"
- "PRINT UCASE$(s)\n"
+ "PRINT UCASE$(s)\n",
+ "Hello, BASIC!\n13 \nHello\nBASIC!\nBASIC\nHELLO, BASIC!\n"
);
// Test 4: IF/THEN/ELSE
@@ -121,7 +200,8 @@ int main(void) {
" PRINT \"medium\"\n"
"ELSE\n"
" PRINT \"small\"\n"
- "END IF\n"
+ "END IF\n",
+ "medium\n"
);
// Test 5: FOR loop
@@ -130,7 +210,8 @@ int main(void) {
"FOR i = 1 TO 10\n"
" PRINT i;\n"
"NEXT i\n"
- "PRINT\n"
+ "PRINT\n",
+ "1 2 3 4 5 6 7 8 9 10 \n"
);
// Test 6: DO/WHILE loop
@@ -141,7 +222,8 @@ int main(void) {
" PRINT n;\n"
" n = n + 1\n"
"LOOP\n"
- "PRINT\n"
+ "PRINT\n",
+ "1 2 3 4 5 \n"
);
// Test 7: SUB and FUNCTION
@@ -158,7 +240,8 @@ int main(void) {
"\n"
"FUNCTION Square(x AS INTEGER) AS INTEGER\n"
" Square = x * x\n"
- "END FUNCTION\n"
+ "END FUNCTION\n",
+ "Hello, World!\n49 \n"
);
// Test 8: SELECT CASE
@@ -172,7 +255,8 @@ int main(void) {
" PRINT \"Good\"\n"
" CASE ELSE\n"
" PRINT \"Other\"\n"
- "END SELECT\n"
+ "END SELECT\n",
+ "Good\n"
);
// Test 9: Fibonacci
@@ -189,14 +273,16 @@ int main(void) {
" a = b\n"
" b = temp\n"
"NEXT i\n"
- "PRINT\n"
+ "PRINT\n",
+ "0 1 1 2 3 5 8 13 21 34 \n"
);
// Test 10: Math functions
runProgram("Math Functions",
"PRINT ABS(-42)\n"
"PRINT SQR(144)\n"
- "PRINT INT(3.7)\n"
+ "PRINT INT(3.7)\n",
+ "42 \n12 \n3 \n"
);
// Test 11: File I/O
@@ -218,7 +304,8 @@ int main(void) {
"LOOP\n"
"CLOSE #1\n"
"PRINT count;\n"
- "PRINT \"lines read\"\n"
+ "PRINT \"lines read\"\n",
+ "Hello from BASIC!\nLine two\n42\n3 lines read\n"
);
// Test 12: LINE INPUT# and APPEND
@@ -237,7 +324,8 @@ int main(void) {
"PRINT s$\n"
"LINE INPUT #2, s$\n"
"PRINT s$\n"
- "CLOSE #2\n"
+ "CLOSE #2\n",
+ "First line\nAppended line\n"
);
// Test 13: Array -- 1D with default lbound=0
@@ -250,7 +338,8 @@ int main(void) {
"FOR i = 1 TO 5\n"
" PRINT arr(i);\n"
"NEXT i\n"
- "PRINT\n"
+ "PRINT\n",
+ "1 4 9 16 25 \n"
);
// Expected: 1 4 9 16 25
@@ -261,7 +350,8 @@ int main(void) {
"m(1, 2) = 12\n"
"m(2, 1) = 21\n"
"m(2, 2) = 22\n"
- "PRINT m(1, 1); m(1, 2); m(2, 1); m(2, 2)\n"
+ "PRINT m(1, 1); m(1, 2); m(2, 1); m(2, 2)\n",
+ "11 12 21 22 \n"
);
// Expected: 11 12 21 22
@@ -271,14 +361,16 @@ int main(void) {
"a(1) = 10\n"
"a(2) = 20\n"
"a(3) = 30\n"
- "PRINT a(1); a(2); a(3)\n"
+ "PRINT a(1); a(2); a(3)\n",
+ "10 20 30 \n"
);
// Expected: 10 20 30
// Test 16: LBOUND and UBOUND
runProgram("LBOUND/UBOUND",
"DIM a(5 TO 10) AS INTEGER\n"
- "PRINT LBOUND(a); UBOUND(a)\n"
+ "PRINT LBOUND(a); UBOUND(a)\n",
+ "5 10 \n"
);
// Expected: 5 10
@@ -291,7 +383,8 @@ int main(void) {
"DIM p AS Point\n"
"p.x = 10\n"
"p.y = 20\n"
- "PRINT p.x; p.y\n"
+ "PRINT p.x; p.y\n",
+ "10 20 \n"
);
// Expected: 10 20
@@ -304,7 +397,8 @@ int main(void) {
"DIM i AS INTEGER\n"
"FOR i = 0 TO 2\n"
" PRINT names(i)\n"
- "NEXT i\n"
+ "NEXT i\n",
+ "Alice\nBob\nCharlie\n"
);
// Expected: Alice / Bob / Charlie
@@ -316,7 +410,8 @@ int main(void) {
"a(2) = 300\n"
"REDIM PRESERVE a(5) AS INTEGER\n"
"a(4) = 500\n"
- "PRINT a(0); a(1); a(2); a(4)\n"
+ "PRINT a(0); a(1); a(2); a(4)\n",
+ "100 200 300 500 \n"
);
// Expected: 100 200 300 500
@@ -327,7 +422,8 @@ int main(void) {
"ERASE a\n"
"DIM b(2) AS INTEGER\n"
"b(1) = 99\n"
- "PRINT b(1)\n"
+ "PRINT b(1)\n",
+ "99 \n"
);
// Expected: 99
@@ -345,7 +441,8 @@ int main(void) {
"FOR i = 1 TO 5\n"
" PRINT sums(i);\n"
"NEXT i\n"
- "PRINT\n"
+ "PRINT\n",
+ "1 3 6 10 15 \n"
);
// Expected: 1 3 6 10 15
@@ -359,7 +456,8 @@ int main(void) {
"GOTO skip\n"
"PRINT \"skipped\"\n"
"skip:\n"
- "PRINT \"after\"\n"
+ "PRINT \"after\"\n",
+ "before\nafter\n"
);
// Expected: before / after
@@ -370,7 +468,8 @@ int main(void) {
"top:\n"
"n = n + 1\n"
"IF n < 5 THEN GOTO top\n"
- "PRINT n\n"
+ "PRINT n\n",
+ "5 \n"
);
// Expected: 5
@@ -383,7 +482,8 @@ int main(void) {
"END\n"
"dbl:\n"
"x = x * 2\n"
- "RETURN\n"
+ "RETURN\n",
+ "20 \n"
);
// Expected: 20
@@ -395,7 +495,8 @@ int main(void) {
"END\n"
"handler:\n"
"PRINT \"caught\"\n"
- "PRINT ERR\n"
+ "PRINT ERR\n",
+ "caught\n11 \n"
);
// Expected: caught / 11
@@ -405,7 +506,8 @@ int main(void) {
"x = 42\n"
"IF x > 10 THEN PRINT \"big\"\n"
"IF x < 10 THEN PRINT \"small\"\n"
- "IF x = 42 THEN PRINT \"exact\" ELSE PRINT \"nope\"\n"
+ "IF x = 42 THEN PRINT \"exact\" ELSE PRINT \"nope\"\n",
+ "big\nexact\n"
);
// Expected: big / exact
@@ -413,7 +515,8 @@ int main(void) {
runProgram("Multi-statement :",
"DIM x AS INTEGER\n"
"DIM y AS INTEGER\n"
- "x = 1 : y = 2 : PRINT x + y\n"
+ "x = 1 : y = 2 : PRINT x + y\n",
+ "3 \n"
);
// Expected: 3
@@ -429,7 +532,8 @@ int main(void) {
"b = 20\n"
"SWAP a, b\n"
"PRINT a;\n"
- "PRINT b\n"
+ "PRINT b\n",
+ "20 10 \n"
);
// Expected: 20 10
@@ -437,7 +541,8 @@ int main(void) {
runProgram("TIMER",
"DIM t AS DOUBLE\n"
"t = TIMER\n"
- "IF t > 0 THEN PRINT \"ok\"\n"
+ "IF t > 0 THEN PRINT \"ok\"\n",
+ "ok\n"
);
// Expected: ok
@@ -445,7 +550,8 @@ int main(void) {
runProgram("DATE$",
"DIM d$ AS STRING\n"
"d$ = DATE$\n"
- "IF LEN(d$) > 0 THEN PRINT \"ok\"\n"
+ "IF LEN(d$) > 0 THEN PRINT \"ok\"\n",
+ "ok\n"
);
// Expected: ok
@@ -453,7 +559,8 @@ int main(void) {
runProgram("TIME$",
"DIM t$ AS STRING\n"
"t$ = TIME$\n"
- "IF LEN(t$) > 0 THEN PRINT \"ok\"\n"
+ "IF LEN(t$) > 0 THEN PRINT \"ok\"\n",
+ "ok\n"
);
// Expected: ok
@@ -461,7 +568,8 @@ int main(void) {
runProgram("ENVIRON$",
"DIM p$ AS STRING\n"
"p$ = ENVIRON$(\"HOME\")\n"
- "IF LEN(p$) > 0 THEN PRINT \"ok\"\n"
+ "IF LEN(p$) > 0 THEN PRINT \"ok\"\n",
+ "ok\n"
);
// Expected: ok
@@ -481,7 +589,8 @@ int main(void) {
"PRINT c\n"
"RESTORE\n"
"READ a\n"
- "PRINT a\n"
+ "PRINT a\n",
+ "10 20 hello\n10 \n"
);
// Expected: 10 20 hello / 10
@@ -495,7 +604,8 @@ int main(void) {
"PRINT count\n"
"SUB Increment\n"
" count = count + 1\n"
- "END SUB\n"
+ "END SUB\n",
+ "3 \n"
);
// Expected: 3
@@ -509,7 +619,8 @@ int main(void) {
" n = n + 1\n"
" PRINT n;\n"
"END SUB\n"
- "PRINT\n"
+ "PRINT\n",
+ "1 2 3 \n"
);
// Expected: 1 2 3
@@ -517,7 +628,8 @@ int main(void) {
runProgram("DEF FN",
"DEF FNdouble(x AS INTEGER) = x * 2\n"
"PRINT FNdouble(5)\n"
- "PRINT FNdouble(21)\n"
+ "PRINT FNdouble(21)\n",
+ "10 \n42 \n"
);
// Expected: 10 / 42
@@ -527,7 +639,8 @@ int main(void) {
"DIM arr(3) AS INTEGER\n"
"arr(1) = 10\n"
"arr(3) = 30\n"
- "PRINT arr(1); arr(3)\n"
+ "PRINT arr(1); arr(3)\n",
+ "10 30 \n"
);
// Expected: 10 30
@@ -539,7 +652,8 @@ int main(void) {
"DIM z AS STRING\n"
"READ x, y, z\n"
"PRINT x\n"
- "PRINT z\n"
+ "PRINT z\n",
+ "100 \nworld\n"
);
// Expected: 100 / world
@@ -552,7 +666,8 @@ int main(void) {
"READ a, b\n"
"DATA 3\n"
"READ c\n"
- "PRINT a; b; c\n"
+ "PRINT a; b; c\n",
+ "1 2 3 \n"
);
// Expected: 1 2 3
@@ -568,7 +683,8 @@ int main(void) {
"SUB Modify\n"
" total = total + 50\n"
" msg = \"done\"\n"
- "END SUB\n"
+ "END SUB\n",
+ "150 \ndone\n"
);
// Expected: 150 / done
@@ -585,7 +701,8 @@ int main(void) {
"DIM s AS STRING\n"
"LINE INPUT #1, s\n"
"PRINT s\n"
- "CLOSE #1\n"
+ "CLOSE #1\n",
+ "10,\"hello\",3.14\n"
);
// Expected: 10,"hello",3.14
@@ -593,25 +710,29 @@ int main(void) {
runProgram("FREEFILE",
"DIM f AS INTEGER\n"
"f = FREEFILE\n"
- "PRINT f\n"
+ "PRINT f\n",
+ "1 \n"
);
// Expected: 1
// Test: PRINT USING numeric
runProgram("PRINT USING numeric",
- "PRINT USING \"###.##\"; 3.14159\n"
+ "PRINT USING \"###.##\"; 3.14159\n",
+ " 3.14\n"
);
// Expected: 3.14
// Test: PRINT USING string
runProgram("PRINT USING string",
- "PRINT USING \"!\"; \"Hello\"\n"
+ "PRINT USING \"!\"; \"Hello\"\n",
+ "H\n"
);
// Expected: H
// Test: SPC and TAB in PRINT
runProgram("SPC/TAB",
- "PRINT SPC(3); \"hi\"\n"
+ "PRINT SPC(3); \"hi\"\n",
+ " hi\n"
);
// Expected: hi
@@ -620,7 +741,8 @@ int main(void) {
"DIM s AS STRING * 5\n"
"s = \"Hi\"\n"
"PRINT \"[\" & s & \"]\"\n"
- "PRINT LEN(s)\n"
+ "PRINT LEN(s)\n",
+ "[Hi ]\n5 \n"
);
// Expected: [Hi ] / 5
@@ -629,7 +751,8 @@ int main(void) {
"DIM s AS STRING\n"
"s = \"Hello World\"\n"
"MID$(s, 7, 5) = \"BASIC\"\n"
- "PRINT s\n"
+ "PRINT s\n",
+ "Hello BASIC\n"
);
// Expected: Hello BASIC
@@ -643,7 +766,8 @@ int main(void) {
"DIM r AS INTEGER\n"
"GET #1, , r\n"
"PRINT r\n"
- "CLOSE #1\n"
+ "CLOSE #1\n",
+ "12345 \n"
);
// Expected: 12345
@@ -656,7 +780,8 @@ int main(void) {
"DIM sz AS LONG\n"
"sz = LOF(1)\n"
"IF sz > 0 THEN PRINT \"ok\"\n"
- "CLOSE #1\n"
+ "CLOSE #1\n",
+ "ok\n"
);
// Expected: ok
@@ -669,7 +794,8 @@ int main(void) {
"DIM s AS STRING\n"
"s = INPUT$(3, #1)\n"
"PRINT s\n"
- "CLOSE #1\n"
+ "CLOSE #1\n",
+ "ABC\n"
);
// Expected: ABC
@@ -682,7 +808,8 @@ int main(void) {
"DIM p AS LONG\n"
"p = SEEK(1)\n"
"IF p = 1 THEN PRINT \"ok\"\n"
- "CLOSE #1\n"
+ "CLOSE #1\n",
+ "ok\n"
);
// Expected: ok
@@ -701,7 +828,8 @@ int main(void) {
"GOTO done\n"
"thirty:\n"
"PRINT \"thirty\"\n"
- "done:\n"
+ "done:\n",
+ "twenty\n"
);
// Expected: twenty
@@ -717,7 +845,8 @@ int main(void) {
"GOTO done2\n"
"bb:\n"
"PRINT \"bb\"\n"
- "done2:\n"
+ "done2:\n",
+ "fallthrough\n"
);
// Expected: fallthrough
@@ -739,7 +868,8 @@ int main(void) {
"addThirty:\n"
"result = result + 30\n"
"RETURN\n"
- "endProg:\n"
+ "endProg:\n",
+ "20 \n"
);
// Expected: 20
@@ -748,7 +878,8 @@ int main(void) {
"PRINT FORMAT$(1234.5, \"#,##0.00\")\n"
"PRINT FORMAT$(0.5, \"0.00\")\n"
"PRINT FORMAT$(-42, \"+#0\")\n"
- "PRINT FORMAT$(0.75, \"percent\")\n"
+ "PRINT FORMAT$(0.75, \"percent\")\n",
+ "1,234.50\n0.50\n-42\n75%\n"
);
// Expected: 1,234.50\n0.50\n-42\n75%
@@ -756,14 +887,16 @@ int main(void) {
runProgram("SHELL function",
"DIM r AS INTEGER\n"
"r = SHELL(\"echo hello > /dev/null\")\n"
- "IF r = 0 THEN PRINT \"ok\"\n"
+ "IF r = 0 THEN PRINT \"ok\"\n",
+ "ok\n"
);
// Expected: ok
// Test: SHELL as statement
runProgram("SHELL statement",
"SHELL \"echo hello > /dev/null\"\n"
- "PRINT \"done\"\n"
+ "PRINT \"done\"\n",
+ "done\n"
);
// Expected: done
@@ -777,7 +910,8 @@ int main(void) {
"END IF\n"
"IF \"abc\" < \"XYZ\" THEN\n"
" PRINT \"less\"\n"
- "END IF\n"
+ "END IF\n",
+ "equal\nless\n"
);
// Expected: equal\nless
@@ -788,7 +922,8 @@ int main(void) {
" PRINT \"equal\"\n"
"ELSE\n"
" PRINT \"not equal\"\n"
- "END IF\n"
+ "END IF\n",
+ "not equal\n"
);
// Expected: not equal
@@ -797,7 +932,8 @@ int main(void) {
"PRINT -1 EQV -1\n"
"PRINT 0 EQV 0\n"
"PRINT -1 EQV 0\n"
- "PRINT 0 EQV -1\n"
+ "PRINT 0 EQV -1\n",
+ "-1 \n-1 \n0 \n0 \n"
);
// Expected: -1\n-1\n0\n0
@@ -806,7 +942,8 @@ int main(void) {
"PRINT 0 IMP -1\n"
"PRINT -1 IMP 0\n"
"PRINT -1 IMP -1\n"
- "PRINT 0 IMP 0\n"
+ "PRINT 0 IMP 0\n",
+ "-1 \n0 \n-1 \n-1 \n"
);
// Expected: -1\n0\n-1\n-1
@@ -818,22 +955,25 @@ int main(void) {
"PRINT USING \"+###.##\"; -42.5\n"
"PRINT USING \"###.##-\"; -42.5\n"
"PRINT USING \"###.##-\"; 42.5\n"
- "PRINT USING \"#.##^^^^\"; 1234.5\n"
+ "PRINT USING \"#.##^^^^\"; 1234.5\n",
+ "*1,234.50\n$ 42.50\n+ 42.50\n- 42.50\n 42.50-\n 42.50 \n1.23E+03\n"
);
- // Test: DEFINT
+ // Test: DEFINT -- every A-Z variable is INTEGER, so 3.7 rounds on store
runProgram("DEFINT",
"DEFINT A-Z\n"
"a = 42\n"
"b = 3.7\n"
- "PRINT a; b\n"
+ "PRINT a; b\n",
+ "42 4 \n"
);
// Test: DEFSTR
runProgram("DEFSTR",
"DEFSTR S\n"
"s = \"hello\"\n"
- "PRINT s\n"
+ "PRINT s\n",
+ "hello\n"
);
// Test: DEFINT range
@@ -842,7 +982,8 @@ int main(void) {
"i = 10\n"
"j = 20\n"
"x = 3.14\n"
- "PRINT i; j; x\n"
+ "PRINT i; j; x\n",
+ "10 20 3.14 \n"
);
// Test: OPTION EXPLICIT success
@@ -850,28 +991,16 @@ int main(void) {
"OPTION EXPLICIT\n"
"DIM x AS INTEGER\n"
"x = 42\n"
- "PRINT x\n"
+ "PRINT x\n",
+ "42 \n"
);
// Test: OPTION EXPLICIT failure (should error)
- {
- printf("=== OPTION EXPLICIT error ===\n");
- const char *src =
- "OPTION EXPLICIT\n"
- "x = 42\n";
- int32_t len = (int32_t)strlen(src);
- BasParserT parser;
- basParserInit(&parser, src, len);
- bool ok = basParse(&parser);
- if (!ok) {
- printf("Correctly caught: %s\n", parser.error);
- } else {
- printf("ERROR: should have failed\n");
- gFailCount++;
- }
- basParserFree(&parser);
- printf("\n");
- }
+ expectCompileError("OPTION EXPLICIT error",
+ "OPTION EXPLICIT\n"
+ "x = 42\n",
+ "Variable not declared: x"
+ );
// Test: DECLARE LIBRARY compilation (verify it compiles without error)
{
@@ -936,14 +1065,16 @@ int main(void) {
runProgram("App.Data parses",
"DIM p AS STRING\n"
"p = App.Data\n"
- "PRINT p\n"
+ "PRINT p\n",
+ NULL
);
// Regression test: RGB(r,g,b) packs into 0x00RRGGBB
runProgram("RGB packing",
"PRINT RGB(255, 128, 0)\n"
"PRINT RGB(0, 255, 0)\n"
- "PRINT RGB(0, 0, 255)\n"
+ "PRINT RGB(0, 0, 255)\n",
+ "16744448 \n65280 \n255 \n"
);
// Expected: 16744448 (0xFF8000), 65280 (0x00FF00), 255 (0x0000FF)
@@ -951,7 +1082,8 @@ int main(void) {
runProgram("RGB round-trip",
"DIM c AS LONG\n"
"c = RGB(17, 99, 200)\n"
- "PRINT GetRed(c); GetGreen(c); GetBlue(c)\n"
+ "PRINT GetRed(c); GetGreen(c); GetBlue(c)\n",
+ "17 99 200 \n"
);
// Expected: 17 99 200
@@ -961,7 +1093,8 @@ int main(void) {
runProgram("MsgBox 3-arg compile",
"DIM r AS INTEGER\n"
"r = MsgBox(\"hi\", 0, \"My Title\")\n"
- "PRINT \"ok\"\n"
+ "PRINT \"ok\"\n",
+ "ok\n"
);
// Regression test: CONST accepts AS type
@@ -969,7 +1102,8 @@ int main(void) {
"CONST PI AS DOUBLE = 3.14159\n"
"CONST N AS INTEGER = 42\n"
"PRINT PI\n"
- "PRINT N\n"
+ "PRINT N\n",
+ "3.14159 \n42 \n"
);
// Regression test: PRINT #ch with ; separator
@@ -985,7 +1119,8 @@ int main(void) {
" LINE INPUT #1, s\n"
" PRINT s\n"
"LOOP\n"
- "CLOSE #1\n"
+ "CLOSE #1\n",
+ "Line one\nans = 42\nxyz\n"
);
// Expected: Line one / ans = 42 / xyz
@@ -993,7 +1128,8 @@ int main(void) {
runProgram("IniRead$ tokenizes",
"DIM v AS STRING\n"
"v = IniRead$(\"/tmp/nofile.ini\", \"S\", \"K\", \"default\")\n"
- "PRINT v\n"
+ "PRINT v\n",
+ "default\n"
);
// Expected: default (file doesn't exist)
@@ -1156,7 +1292,8 @@ int main(void) {
"\n"
"SUB NoChange(BYVAL n AS INTEGER)\n"
" n = n + 999\n"
- "END SUB\n"
+ "END SUB\n",
+ "Before: x =10 \nAfter AddTen: x =20 \nBefore: a = hello\nAfter ChangeStr: a = hello world\nBefore: y =100 \nAfter NoChange: y =100 \nAfter AddTen(z+0): z =5 \n"
);
// ============================================================
@@ -1164,66 +1301,77 @@ int main(void) {
// ============================================================
runProgram("LCASE$",
- "PRINT LCASE$(\"HELLO WORLD\")\n"
+ "PRINT LCASE$(\"HELLO WORLD\")\n",
+ "hello world\n"
);
// Expected: hello world
runProgram("TRIM$ LTRIM$ RTRIM$",
"PRINT \"[\" & TRIM$(\" hi \") & \"]\"\n"
"PRINT \"[\" & LTRIM$(\" hi \") & \"]\"\n"
- "PRINT \"[\" & RTRIM$(\" hi \") & \"]\"\n"
+ "PRINT \"[\" & RTRIM$(\" hi \") & \"]\"\n",
+ "[hi]\n[hi ]\n[ hi]\n"
);
// Expected: [hi] / [hi ] / [ hi]
runProgram("INSTR 2-arg",
"PRINT INSTR(\"hello world\", \"world\")\n"
- "PRINT INSTR(\"hello world\", \"xyz\")\n"
+ "PRINT INSTR(\"hello world\", \"xyz\")\n",
+ "7 \n0 \n"
);
// Expected: 7 / 0
runProgram("INSTR 3-arg",
"PRINT INSTR(5, \"abcabc\", \"bc\")\n"
- "PRINT INSTR(1, \"abcabc\", \"bc\")\n"
+ "PRINT INSTR(1, \"abcabc\", \"bc\")\n",
+ "5 \n2 \n"
);
// Expected: 5 / 2
runProgram("CHR$ and ASC",
"PRINT CHR$(65)\n"
- "PRINT ASC(\"Z\")\n"
+ "PRINT ASC(\"Z\")\n",
+ "A\n90 \n"
);
// Expected: A / 90
runProgram("SPACE$",
- "PRINT \"[\" & SPACE$(5) & \"]\"\n"
+ "PRINT \"[\" & SPACE$(5) & \"]\"\n",
+ "[ ]\n"
);
// Expected: [ ]
runProgram("STRING$",
- "PRINT STRING$(5, \"*\")\n"
+ "PRINT STRING$(5, \"*\")\n",
+ "*****\n"
);
// Expected: *****
runProgram("HEX$",
"PRINT HEX$(255)\n"
- "PRINT HEX$(16)\n"
+ "PRINT HEX$(16)\n",
+ "FF\n10\n"
);
// Expected: FF / 10
runProgram("VAL",
"PRINT VAL(\"3.14\")\n"
"PRINT VAL(\"42\")\n"
- "PRINT VAL(\"abc\")\n"
+ "PRINT VAL(\"abc\")\n",
+ "3.14 \n42 \n0 \n"
);
// Expected: 3.14 / 42 / 0
runProgram("STR$",
"PRINT \"[\" & STR$(42) & \"]\"\n"
- "PRINT \"[\" & STR$(-7) & \"]\"\n"
+ "PRINT \"[\" & STR$(-7) & \"]\"\n",
+ "[ 42]\n[-7]\n"
);
// Expected: [ 42] / [-7]
runProgram("MID$ 2-arg",
- "PRINT MID$(\"hello world\", 7)\n"
+ "PRINT MID$(\"hello world\", 7)\n",
+ "world\n"
);
// Expected: world
@@ -1232,7 +1380,8 @@ int main(void) {
"DIM b AS STRING\n"
"a = \"hello\"\n"
"b = \" world\"\n"
- "PRINT a + b\n"
+ "PRINT a + b\n",
+ "hello world\n"
);
// Expected: hello world
@@ -1245,14 +1394,16 @@ int main(void) {
"pi = ATN(1) * 4\n"
"PRINT INT(SIN(pi / 2) * 1000)\n"
"PRINT INT(COS(0) * 1000)\n"
- "PRINT INT(TAN(pi / 4) * 1000)\n"
+ "PRINT INT(TAN(pi / 4) * 1000)\n",
+ "1000 \n1000 \n999 \n"
);
- // Expected: 1000 / 1000 / 1000
+ // Expected: 1000 / 1000 / 999 (TAN(pi/4)*1000 is 999.99..., INT truncates)
runProgram("LOG and EXP",
"PRINT INT(LOG(1))\n"
"PRINT INT(EXP(0))\n"
- "PRINT INT(EXP(1) * 100)\n"
+ "PRINT INT(EXP(1) * 100)\n",
+ "0 \n1 \n271 \n"
);
// Expected: 0 / 1 / 271
@@ -1261,7 +1412,8 @@ int main(void) {
"PRINT FIX(-3.7)\n"
"PRINT SGN(42)\n"
"PRINT SGN(-5)\n"
- "PRINT SGN(0)\n"
+ "PRINT SGN(0)\n",
+ "3 \n-3 \n1 \n-1 \n0 \n"
);
// Expected: 3 / -3 / 1 / -1 / 0
@@ -1269,7 +1421,8 @@ int main(void) {
"RANDOMIZE 12345\n"
"DIM r AS DOUBLE\n"
"r = RND\n"
- "IF r >= 0 AND r < 1 THEN PRINT \"ok\"\n"
+ "IF r >= 0 AND r < 1 THEN PRINT \"ok\"\n",
+ "ok\n"
);
// Expected: ok
@@ -1284,7 +1437,8 @@ int main(void) {
" PRINT n;\n"
" n = n + 1\n"
"LOOP\n"
- "PRINT\n"
+ "PRINT\n",
+ "1 2 3 4 5 \n"
);
// Expected: 1 2 3 4 5
@@ -1295,7 +1449,8 @@ int main(void) {
" PRINT n;\n"
" n = n + 1\n"
"LOOP WHILE n <= 5\n"
- "PRINT\n"
+ "PRINT\n",
+ "1 2 3 4 5 \n"
);
// Expected: 1 2 3 4 5
@@ -1306,7 +1461,8 @@ int main(void) {
" PRINT n;\n"
" n = n + 1\n"
"LOOP UNTIL n > 5\n"
- "PRINT\n"
+ "PRINT\n",
+ "1 2 3 4 5 \n"
);
// Expected: 1 2 3 4 5
@@ -1317,7 +1473,8 @@ int main(void) {
" PRINT n;\n"
" n = n + 1\n"
"WEND\n"
- "PRINT\n"
+ "PRINT\n",
+ "1 2 3 4 5 \n"
);
// Expected: 1 2 3 4 5
@@ -1326,7 +1483,8 @@ int main(void) {
"FOR i = 5 TO 1 STEP -1\n"
" PRINT i;\n"
"NEXT i\n"
- "PRINT\n"
+ "PRINT\n",
+ "5 4 3 2 1 \n"
);
// Expected: 5 4 3 2 1
@@ -1335,7 +1493,8 @@ int main(void) {
"FOR i = 0 TO 10 STEP 2\n"
" PRINT i;\n"
"NEXT i\n"
- "PRINT\n"
+ "PRINT\n",
+ "0 2 4 6 8 10 \n"
);
// Expected: 0 2 4 6 8 10
@@ -1350,7 +1509,8 @@ int main(void) {
" PRINT i;\n"
"NEXT i\n"
"PRINT\n"
- "PRINT \"after\"\n"
+ "PRINT \"after\"\n",
+ "1 2 \nafter\n"
);
// Expected: 1 2 / after
@@ -1363,7 +1523,8 @@ int main(void) {
" PRINT i;\n"
" NEXT i\n"
" PRINT\n"
- "END SUB\n"
+ "END SUB\n",
+ "0 1 2 3 \n"
);
// Expected: 0 1 2 3
@@ -1381,7 +1542,8 @@ int main(void) {
"END SUB\n"
"FUNCTION GetCount() AS LONG\n"
" GetCount = 4\n"
- "END FUNCTION\n"
+ "END FUNCTION\n",
+ "0 1 2 3 \n"
);
// Expected: 0 1 2 3
@@ -1400,9 +1562,11 @@ int main(void) {
" NEXT ix\n"
" PRINT\n"
"END SUB\n"
- "ENDFORM\n"
+ "ENDFORM\n",
+ ""
);
- // Expected: 0 1 2 3
+ // Expected: nothing -- form init code (including the CALL) only runs when
+ // the form is loaded, which this harness never does.
runProgram("EXIT DO",
"DIM n AS INTEGER\n"
@@ -1413,7 +1577,8 @@ int main(void) {
" PRINT n;\n"
"LOOP\n"
"PRINT\n"
- "PRINT \"after\"\n"
+ "PRINT \"after\"\n",
+ "1 2 3 \nafter\n"
);
// Expected: 1 2 3 / after
@@ -1426,7 +1591,8 @@ int main(void) {
" IF n > 3 THEN EXIT SUB\n"
" PRINT n;\n"
"END SUB\n"
- "PRINT\n"
+ "PRINT\n",
+ "1 2 \n"
);
// Expected: 1 2
@@ -1440,7 +1606,8 @@ int main(void) {
" EXIT FUNCTION\n"
" END IF\n"
" Clamp = n\n"
- "END FUNCTION\n"
+ "END FUNCTION\n",
+ "5 \n100 \n"
);
// Expected: 5 / 100
@@ -1454,7 +1621,8 @@ int main(void) {
"CONST GREETING = \"hello\"\n"
"PRINT INT(PI * 100)\n"
"PRINT MAX_SIZE\n"
- "PRINT GREETING\n"
+ "PRINT GREETING\n",
+ "314 \n100 \nhello\n"
);
// Expected: 314 / 100 / hello
@@ -1465,7 +1633,8 @@ int main(void) {
runProgram("END statement",
"PRINT \"before\"\n"
"END\n"
- "PRINT \"after\"\n"
+ "PRINT \"after\"\n",
+ "before\n"
);
// Expected: before
@@ -1480,9 +1649,10 @@ int main(void) {
"b = False\n"
"IF NOT b THEN PRINT \"no\"\n"
"PRINT True\n"
- "PRINT False\n"
+ "PRINT False\n",
+ "yes\nno\nTrue \nFalse \n"
);
- // Expected: yes / no / -1 / 0
+ // Expected: yes / no / True / False
// ============================================================
// Coverage: NOT operator
@@ -1493,7 +1663,8 @@ int main(void) {
"PRINT NOT -1\n"
"DIM x AS INTEGER\n"
"x = 5\n"
- "IF NOT (x > 10) THEN PRINT \"small\"\n"
+ "IF NOT (x > 10) THEN PRINT \"small\"\n",
+ "-1 \n0 \nsmall\n"
);
// Expected: -1 / 0 / small
@@ -1504,7 +1675,8 @@ int main(void) {
runProgram("Bitwise AND OR XOR",
"PRINT 15 AND 9\n"
"PRINT 12 OR 3\n"
- "PRINT 15 XOR 9\n"
+ "PRINT 15 XOR 9\n",
+ "9 \n15 \n6 \n"
);
// Expected: 9 / 15 / 6
@@ -1514,7 +1686,8 @@ int main(void) {
runProgram("SLEEP",
"SLEEP 0\n"
- "PRINT \"ok\"\n"
+ "PRINT \"ok\"\n",
+ "ok\n"
);
// Expected: ok
@@ -1529,7 +1702,8 @@ int main(void) {
"PRINT \"resumed\"\n"
"END\n"
"handler:\n"
- "RESUME NEXT\n"
+ "RESUME NEXT\n",
+ "resumed\n"
);
// Expected: resumed
@@ -1539,9 +1713,10 @@ int main(void) {
"PRINT \"should not print\"\n"
"END\n"
"handler:\n"
- "PRINT \"caught error\"; ERR\n"
+ "PRINT \"caught error\"; ERR\n",
+ "caught error999 \n"
);
- // Expected: caught error 999
+ // Expected: caught error999
// ============================================================
// Coverage: Type suffixes and hex literals
@@ -1553,7 +1728,8 @@ int main(void) {
"x% = 42\n"
"s$ = \"hello\"\n"
"PRINT x%\n"
- "PRINT s$\n"
+ "PRINT s$\n",
+ "42 \nhello\n"
);
// Expected: 42 / hello
@@ -1562,7 +1738,8 @@ int main(void) {
"PRINT &H10\n"
"DIM x AS INTEGER\n"
"x = &H0A\n"
- "PRINT x\n"
+ "PRINT x\n",
+ "255 \n16 \n10 \n"
);
// Expected: 255 / 16 / 10
@@ -1574,7 +1751,8 @@ int main(void) {
"DIM x AS LONG\n"
"x = 100000\n"
"x = x * 2\n"
- "PRINT x\n"
+ "PRINT x\n",
+ "200000 \n"
);
// Expected: 200000
@@ -1586,7 +1764,8 @@ int main(void) {
"DIM a(3) AS INTEGER\n"
"a(1) = 99\n"
"REDIM a(5) AS INTEGER\n"
- "PRINT a(1)\n"
+ "PRINT a(1)\n",
+ "0 \n"
);
// Expected: 0 (data cleared)
@@ -1595,14 +1774,16 @@ int main(void) {
// ============================================================
runProgram("PRINT comma separator",
- "PRINT 1, 2, 3\n"
+ "PRINT 1, 2, 3\n",
+ "1 \t2 \t3 \n"
);
// Expected: 123
runProgram("PRINT bare newline",
"PRINT \"a\"\n"
"PRINT\n"
- "PRINT \"b\"\n"
+ "PRINT \"b\"\n",
+ "a\n\nb\n"
);
// Expected: a / (blank) / b
@@ -1613,7 +1794,8 @@ int main(void) {
runProgram("LET keyword",
"DIM x AS INTEGER\n"
"LET x = 42\n"
- "PRINT x\n"
+ "PRINT x\n",
+ "42 \n"
);
// Expected: 42
@@ -1626,7 +1808,8 @@ int main(void) {
"x = 1 + _\n"
" 2 + _\n"
" 3\n"
- "PRINT x\n"
+ "PRINT x\n",
+ "6 \n"
);
// Expected: 6
@@ -1637,7 +1820,8 @@ int main(void) {
runProgram("REM comment",
"REM This is a comment\n"
"PRINT \"ok\"\n"
- "PRINT \"hi\" REM inline comment\n"
+ "PRINT \"hi\" REM inline comment\n",
+ "ok\nhi\n"
);
// Expected: ok / hi
@@ -1655,7 +1839,8 @@ int main(void) {
" PRINT \"fifteen or twenty\"\n"
" CASE ELSE\n"
" PRINT \"other\"\n"
- "END SELECT\n"
+ "END SELECT\n",
+ "fifteen or twenty\n"
);
// Expected: fifteen or twenty
@@ -1669,7 +1854,8 @@ int main(void) {
" PRINT \"11-20\"\n"
" CASE ELSE\n"
" PRINT \"other\"\n"
- "END SELECT\n"
+ "END SELECT\n",
+ "11-20\n"
);
// Expected: 11-20
@@ -1683,7 +1869,8 @@ int main(void) {
" PRINT \"big\"\n"
" CASE ELSE\n"
" PRINT \"medium\"\n"
- "END SELECT\n"
+ "END SELECT\n",
+ "medium\n"
);
// Expected: medium
@@ -1697,7 +1884,8 @@ int main(void) {
" PRINT \"mid\"\n"
" CASE IS > 6\n"
" PRINT \"high\"\n"
- "END SELECT\n"
+ "END SELECT\n",
+ "mid\n"
);
// Expected: mid
@@ -1714,7 +1902,8 @@ int main(void) {
" PRINT i * 10 + j;\n"
" NEXT j\n"
"NEXT i\n"
- "PRINT\n"
+ "PRINT\n",
+ "11 12 21 22 31 32 \n"
);
// Expected: 11 12 21 22 31 32
@@ -1731,7 +1920,8 @@ int main(void) {
"DIM f AS SINGLE\n"
"i = 10\n"
"f = 3.0\n"
- "PRINT i / f\n"
+ "PRINT i / f\n",
+ "9.5 \n17.5 \n3.33333333333333 \n"
);
// Expected: 9.5 / 17.5 / 3.333...
@@ -1749,7 +1939,8 @@ int main(void) {
" ELSE\n"
" Max = b\n"
" END IF\n"
- "END FUNCTION\n"
+ "END FUNCTION\n",
+ "20 \n30 \n"
);
// Expected: 20 / 30
@@ -1768,7 +1959,8 @@ int main(void) {
" ELSE\n"
" Fact = n * Fact(n - 1)\n"
" END IF\n"
- "END FUNCTION\n"
+ "END FUNCTION\n",
+ "1 \n120 \n3628800 \n"
);
// Expected: 1 / 120 / 3628800
@@ -1780,7 +1972,8 @@ int main(void) {
"IF \"abc\" < \"def\" THEN PRINT \"less\"\n"
"IF \"xyz\" > \"abc\" THEN PRINT \"greater\"\n"
"IF \"abc\" = \"abc\" THEN PRINT \"equal\"\n"
- "IF \"abc\" <> \"xyz\" THEN PRINT \"notequal\"\n"
+ "IF \"abc\" <> \"xyz\" THEN PRINT \"notequal\"\n",
+ "less\ngreater\nequal\nnotequal\n"
);
// Expected: less / greater / equal / notequal
@@ -1791,7 +1984,8 @@ int main(void) {
runProgram("Apostrophe comment",
"PRINT \"before\" ' this is a comment\n"
"' full line comment\n"
- "PRINT \"after\"\n"
+ "PRINT \"after\"\n",
+ "before\nafter\n"
);
// Expected: before / after
@@ -1802,7 +1996,8 @@ int main(void) {
runProgram("SINGLE data type",
"DIM s AS SINGLE\n"
"s = 3.14\n"
- "PRINT INT(s * 100)\n"
+ "PRINT INT(s * 100)\n",
+ "314 \n"
);
// Expected: 314
@@ -1815,9 +2010,10 @@ int main(void) {
"PRINT CLNG(42)\n"
"PRINT CDBL(3)\n"
"PRINT CSNG(3)\n"
- "PRINT \"[\" & CSTR(42) & \"]\"\n"
+ "PRINT \"[\" & CSTR(42) & \"]\"\n",
+ "4 \n42 \n3 \n3 \n[42]\n"
);
- // Expected: 3 / 42 / 3 / 3 / [ 42]
+ // Expected: 4 / 42 / 3 / 3 / [42]
// ============================================================
// Coverage: Me.Show / Me.Hide as statements
@@ -2041,7 +2237,8 @@ int main(void) {
"End Function\n"
"\n"
"CALL Helper\n"
- "PRINT \"sum:\"; Add(3, 4)\n"
+ "PRINT \"sum:\"; Add(3, 4)\n",
+ "module:10 \nhelper called\nsum:7 \n"
);
// ============================================================
@@ -2050,7 +2247,8 @@ int main(void) {
runProgram("? shortcut for PRINT",
"? \"hello\"\n"
- "? 1 + 2\n"
+ "? 1 + 2\n",
+ "hello\n3 \n"
);
// ============================================================
@@ -2111,7 +2309,8 @@ int main(void) {
"RANDOMIZE TIMER\n"
"DIM x AS SINGLE\n"
"x = RND\n"
- "PRINT \"rnd ok\"\n"
+ "PRINT \"rnd ok\"\n",
+ "rnd ok\n"
);
// ============================================================
@@ -2124,7 +2323,8 @@ int main(void) {
"END\n"
"doWork:\n"
"PRINT \"working\"\n"
- "RETURN\n"
+ "RETURN\n",
+ "working\nback\n"
);
// ============================================================
@@ -2135,7 +2335,8 @@ int main(void) {
"GOTO skip\n"
"PRINT \"should not print\"\n"
"skip:\n"
- "PRINT \"jumped\"\n"
+ "PRINT \"jumped\"\n",
+ "jumped\n"
);
// ============================================================
@@ -2154,7 +2355,8 @@ int main(void) {
"pts(2).x = 30\n"
"pts(2).y = 40\n"
"PRINT pts(1).x; pts(1).y\n"
- "PRINT pts(2).x; pts(2).y\n"
+ "PRINT pts(2).x; pts(2).y\n",
+ "10 20 \n30 40 \n"
);
// ============================================================
@@ -2178,7 +2380,8 @@ int main(void) {
"p.addr.zip = 10001\n"
"PRINT p.fullName\n"
"PRINT p.addr.city\n"
- "PRINT p.addr.zip\n"
+ "PRINT p.addr.zip\n",
+ "Alice\nNYC\n10001 \n"
);
// ============================================================
@@ -2195,7 +2398,8 @@ int main(void) {
"PRINT 5 XOR 3\n" // bitwise: 6
"PRINT 2 + 3 * 4 - 1\n" // 2 + 12 - 1 = 13
"PRINT (2 + 3) * (4 - 1)\n" // 5 * 3 = 15
- "PRINT -2 ^ 2\n" // -(2^2) = -4 (VB precedence)
+ "PRINT -2 ^ 2\n", // -(2^2) = -4 (VB precedence)
+ "-1 \n-1 \n-1 \n1 \n7 \n6 \n13 \n15 \n-4 \n"
);
// ============================================================
@@ -2210,7 +2414,8 @@ int main(void) {
" IF j = 2 THEN EXIT FOR\n"
" PRINT i; j\n"
" NEXT j\n"
- "NEXT i\n"
+ "NEXT i\n",
+ "1 1 \n2 1 \n3 1 \n"
);
// ============================================================
@@ -2223,7 +2428,8 @@ int main(void) {
"\n"
"Sub doWork ()\n"
" PRINT \"in sub\"\n"
- "End Sub\n"
+ "End Sub\n",
+ "in sub\nafter call\n"
);
// ============================================================
@@ -2236,7 +2442,8 @@ int main(void) {
"CONST APPNAME = \"DVX\"\n"
"PRINT INT(PI * 100)\n"
"PRINT INT(E * 100)\n"
- "PRINT APPNAME\n"
+ "PRINT APPNAME\n",
+ "314 \n271 \nDVX\n"
);
// ============================================================
@@ -2253,7 +2460,8 @@ int main(void) {
" PRINT \"right\"\n"
" CASE ELSE\n"
" PRINT \"default\"\n"
- "END SELECT\n"
+ "END SELECT\n",
+ "right\n"
);
// ============================================================
@@ -2273,7 +2481,8 @@ int main(void) {
" PRINT \"D\"\n"
"ELSE\n"
" PRINT \"F\"\n"
- "END IF\n"
+ "END IF\n",
+ "F\n"
);
// ============================================================
@@ -2287,7 +2496,8 @@ int main(void) {
" i = i + 1\n"
" IF i = 5 THEN EXIT DO\n"
"LOOP\n"
- "PRINT i\n"
+ "PRINT i\n",
+ "5 \n"
);
// ============================================================
@@ -2303,7 +2513,8 @@ int main(void) {
" END IF\n"
"End Function\n"
"\n"
- "PRINT Fib(0); Fib(1); Fib(5); Fib(10)\n"
+ "PRINT Fib(0); Fib(1); Fib(5); Fib(10)\n",
+ "0 1 5 55 \n"
);
// ============================================================
@@ -2316,7 +2527,8 @@ int main(void) {
"IF \"def\" > \"abc\" THEN PRINT \"gt ok\"\n"
"IF \"abc\" >= \"abc\" THEN PRINT \"ge ok\"\n"
"IF \"abc\" <> \"def\" THEN PRINT \"ne ok\"\n"
- "IF \"abc\" = \"abc\" THEN PRINT \"eq ok\"\n"
+ "IF \"abc\" = \"abc\" THEN PRINT \"eq ok\"\n",
+ "lt ok\nle ok\ngt ok\nge ok\nne ok\neq ok\n"
);
// ============================================================
@@ -2461,7 +2673,8 @@ int main(void) {
" PRINT \"hello\"\n"
"End Sub\n"
"\n"
- "Greet\n"
+ "Greet\n",
+ "hello\n"
);
// ============================================================
@@ -2473,31 +2686,18 @@ int main(void) {
"\n"
"Sub DoWork ()\n"
" PRINT \"worked\"\n"
- "End Sub\n"
+ "End Sub\n",
+ "worked\n"
);
// ============================================================
// Coverage: Unresolved forward reference error
// ============================================================
- {
- printf("=== Unresolved forward reference ===\n");
-
- const char *src = "NeverDefined\n";
- int32_t len = (int32_t)strlen(src);
- BasParserT parser;
- basParserInit(&parser, src, len);
-
- if (!basParse(&parser)) {
- printf("Correctly caught: %s\n", parser.error);
- } else {
- printf("ERROR: should have failed\n");
- gFailCount++;
- }
-
- basParserFree(&parser);
- printf("\n");
- }
+ expectCompileError("Unresolved forward reference",
+ "NeverDefined\n",
+ "Undefined Sub: NeverDefined"
+ );
// ============================================================
// Coverage: Nested UDT field store
@@ -2517,7 +2717,8 @@ int main(void) {
"o.label = \"test\"\n"
"o.child.val = 42\n"
"PRINT o.label\n"
- "PRINT o.child.val\n"
+ "PRINT o.child.val\n",
+ "test\n42 \n"
);
// ============================================================
@@ -2536,7 +2737,8 @@ int main(void) {
"pts(3).x = 30\n"
"pts(3).y = 40\n"
"PRINT pts(1).x; pts(1).y\n"
- "PRINT pts(3).x; pts(3).y\n"
+ "PRINT pts(3).x; pts(3).y\n",
+ "10 20 \n30 40 \n"
);
// ============================================================
@@ -2546,7 +2748,8 @@ int main(void) {
runProgram("Exponent precedence",
"PRINT -2 ^ 2\n" // -(2^2) = -4
"PRINT (-2) ^ 2\n" // (-2)^2 = 4
- "PRINT 3 ^ 2 + 1\n" // 9 + 1 = 10
+ "PRINT 3 ^ 2 + 1\n", // 9 + 1 = 10
+ "-4 \n4 \n10 \n"
);
// ============================================================
@@ -2585,7 +2788,8 @@ int main(void) {
"OPTION EXPLICIT\n"
"DIM x AS INTEGER\n"
"x = 42\n"
- "PRINT x\n"
+ "PRINT x\n",
+ "42 \n"
);
// ============================================================
@@ -2601,7 +2805,8 @@ int main(void) {
"\n"
"Counter\n"
"Counter\n"
- "Counter\n"
+ "Counter\n",
+ "1 \n2 \n3 \n"
);
// ============================================================
@@ -2609,7 +2814,8 @@ int main(void) {
// ============================================================
runProgram("MsgBox statement",
- "MsgBox \"Hello\"\n"
+ "MsgBox \"Hello\"\n",
+ ""
);
// ============================================================
@@ -2617,7 +2823,8 @@ int main(void) {
// ============================================================
runProgram("MsgBox statement with flags",
- "MsgBox \"Save?\", vbYesNo + vbQuestion\n"
+ "MsgBox \"Save?\", vbYesNo + vbQuestion\n",
+ ""
);
// ============================================================
@@ -2627,7 +2834,8 @@ int main(void) {
runProgram("MsgBox function",
"DIM result AS INTEGER\n"
"result = MsgBox(\"Continue?\", vbYesNo)\n"
- "PRINT result\n"
+ "PRINT result\n",
+ "1 \n"
);
// ============================================================
@@ -2637,7 +2845,8 @@ int main(void) {
runProgram("MsgBox function default flags",
"DIM r AS INTEGER\n"
"r = MsgBox(\"OK\")\n"
- "PRINT r\n"
+ "PRINT r\n",
+ "1 \n"
);
// ============================================================
@@ -2658,7 +2867,8 @@ int main(void) {
"PRINT vbCancel\n"
"PRINT vbYes\n"
"PRINT vbNo\n"
- "PRINT vbRetry\n"
+ "PRINT vbRetry\n",
+ "0 \n1 \n2 \n3 \n4 \n16 \n32 \n48 \n64 \n1 \n2 \n3 \n4 \n5 \n"
);
// ============================================================
@@ -2668,7 +2878,8 @@ int main(void) {
runProgram("MsgBox in If condition",
"If MsgBox(\"Exit?\", vbYesNo) = vbYes Then\n"
" PRINT \"yes\"\n"
- "End If\n"
+ "End If\n",
+ ""
);
// ============================================================
@@ -2751,7 +2962,8 @@ int main(void) {
" GetValue = 42\n"
"End Function\n"
"\n"
- "PRINT GetValue()\n"
+ "PRINT GetValue()\n",
+ "42 \n"
);
// ============================================================
@@ -2897,27 +3109,353 @@ int main(void) {
// Coverage: Nested BEGINFORM rejected
// ============================================================
+ expectCompileError("Nested BEGINFORM rejected",
+ "BEGINFORM \"Form1\"\n"
+ "BEGINFORM \"Form2\"\n"
+ "ENDFORM\n"
+ "ENDFORM\n",
+ "Nested BEGINFORM is not allowed"
+ );
+
+ // ============================================================
+ // Regression: compiler bugs found by the aab9fac review
+ // ============================================================
+
+ expectCompileError("Self-referential TYPE rejected",
+ "TYPE T\n"
+ " x AS INTEGER\n"
+ " y AS T\n"
+ "END TYPE\n"
+ "DIM v AS T\n",
+ "cannot contain a field of its own type"
+ );
+
+ expectCompileError("DEF FN inside SUB rejected",
+ "SUB S\n"
+ " DIM x AS INTEGER\n"
+ " DEF FNd(y) = y * 2\n"
+ "END SUB\n",
+ "DEF FN is not allowed inside SUB or FUNCTION"
+ );
+
+ // RETURN inside a SELECT CASE must discard the live test value before
+ // OP_GOSUB_RET pops the return address.
+ runProgram("RETURN inside SELECT CASE",
+ "DIM x AS INTEGER\n"
+ "x = 1\n"
+ "GOSUB Handler\n"
+ "PRINT \"back\"\n"
+ "END\n"
+ "Handler:\n"
+ "SELECT CASE x\n"
+ " CASE 1\n"
+ " PRINT \"one\"\n"
+ " RETURN\n"
+ "END SELECT\n"
+ "RETURN\n",
+ "one\nback\n"
+ );
+
+ expectCompileError("Long identifier rejected",
+ "DIM abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij AS INTEGER\n",
+ "Identifier too long"
+ );
+
+ // A variable first used inside a SUB is local to it: the module-level
+ // y is a different (zero) variable, and the implicit string local is
+ // "" on every call rather than accumulating.
+ runProgram("Implicit variable in SUB is local",
+ "SUB S\n"
+ " y = 4\n"
+ " s$ = s$ & \"a\"\n"
+ " PRINT y; s$\n"
+ "END SUB\n"
+ "S\n"
+ "S\n"
+ "PRINT y\n",
+ "4 a\n4 a\n0 \n"
+ );
+
+ runProgram("Implicit locals are per call in recursion",
+ "FUNCTION fact(n)\n"
+ " IF n <= 1 THEN\n"
+ " fact = 1\n"
+ " ELSE\n"
+ " t = n\n"
+ " fact = t * fact(n - 1)\n"
+ " END IF\n"
+ "END FUNCTION\n"
+ "PRINT fact(5)\n",
+ "120 \n"
+ );
+
+ expectCompileError("Assignment to another FUNCTION name rejected",
+ "FUNCTION F\n"
+ " F = 1\n"
+ "END FUNCTION\n"
+ "SUB S(a)\n"
+ " F = 99\n"
+ "END SUB\n",
+ "Cannot assign to function 'F'"
+ );
+
+ expectCompileError("Assignment to SUB name rejected",
+ "SUB S\n"
+ "END SUB\n"
+ "S = 5\n",
+ "Cannot assign to 'S'"
+ );
+
+ expectCompileError("Assignment to label rejected",
+ "Lbl:\n"
+ "Lbl = 5\n",
+ "Cannot assign to 'Lbl'"
+ );
+
+ expectCompileError("SUB name as operand rejected",
+ "SUB S\n"
+ "END SUB\n"
+ "PRINT S + 1\n",
+ "'S' is not a variable"
+ );
+
+ runProgram("DATA hex literal keeps 32 bits",
+ "DATA &HFFFF, &H12345\n"
+ "READ a&, b&\n"
+ "PRINT a&; b&\n",
+ "65535 74565 \n"
+ );
+
+ expectCompileError("EXIT FOR in SUB without loop rejected",
+ "SUB S\n"
+ " EXIT FOR\n"
+ "END SUB\n"
+ "FOR i = 1 TO 3\n"
+ " S\n"
+ "NEXT i\n",
+ "EXIT FOR outside FOR loop"
+ );
+
+ expectCompileError("EXIT DO outside loop rejected",
+ "EXIT DO\n",
+ "EXIT DO outside DO or WHILE loop"
+ );
+
+ expectCompileError("EXIT SUB at module level rejected",
+ "EXIT SUB\n",
+ "EXIT SUB outside SUB"
+ );
+
+ expectCompileError("EXIT SUB inside FUNCTION rejected",
+ "FUNCTION F\n"
+ " EXIT SUB\n"
+ "END FUNCTION\n",
+ "EXIT SUB outside SUB"
+ );
+
+ runProgram("EXIT FOR out of SELECT CASE",
+ "FOR i = 1 TO 3\n"
+ " SELECT CASE i\n"
+ " CASE 2\n"
+ " EXIT FOR\n"
+ " END SELECT\n"
+ " PRINT i\n"
+ "NEXT i\n"
+ "PRINT \"done\"\n",
+ "1 \ndone\n"
+ );
+
{
- printf("=== Nested BEGINFORM rejected ===\n");
- BasParserT parser;
- basParserInit(&parser,
- "BEGINFORM \"Form1\"\n"
- "BEGINFORM \"Form2\"\n"
- "ENDFORM\n"
- "ENDFORM\n",
- -1);
+ // Nesting limits: deep parentheses, unary operators and blocks must
+ // be compile errors, not stack overflows.
+ static char deepSrc[8192];
+ int32_t pos = 0;
- if (!basParse(&parser)) {
- printf("OK (expected error: %s)\n", parser.error);
- } else {
- printf("FAIL: should have rejected nested BEGINFORM\n");
- gFailCount++;
+ pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "x = ");
+ for (int32_t i = 0; i < BAS_MAX_EXPR_DEPTH + 8; i++) {
+ pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "(");
}
+ pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "1");
+ for (int32_t i = 0; i < BAS_MAX_EXPR_DEPTH + 8; i++) {
+ pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, ")");
+ }
+ snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "\n");
+ expectCompileError("Deep expression nesting rejected", deepSrc, "Expression nested too deeply");
- basParserFree(&parser);
- printf("\n");
+ pos = 0;
+ pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "x = ");
+ for (int32_t i = 0; i < BAS_MAX_EXPR_DEPTH + 8; i++) {
+ pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "-");
+ }
+ snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "1\n");
+ expectCompileError("Deep unary nesting rejected", deepSrc, "Expression nested too deeply");
+
+ pos = 0;
+ for (int32_t i = 0; i < BAS_MAX_BLOCK_DEPTH + 8; i++) {
+ pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "IF 1 THEN\n");
+ }
+ pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "PRINT 1\n");
+ for (int32_t i = 0; i < BAS_MAX_BLOCK_DEPTH + 8; i++) {
+ pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "END IF\n");
+ }
+ expectCompileError("Deep block nesting rejected", deepSrc, "Statements nested too deeply");
}
+ // The signature prescan runs before TYPE statements are parsed; a
+ // parameter typed AS a user TYPE must still count towards the arity.
+ runProgram("Prescan handles UDT parameters",
+ "DIM r AS INTEGER\n"
+ "r = foo(1, 2)\n"
+ "PRINT r\n"
+ "TYPE Pt\n"
+ " x AS INTEGER\n"
+ "END TYPE\n"
+ "FUNCTION foo(a AS INTEGER, b AS INTEGER) AS INTEGER\n"
+ " foo = a + b\n"
+ "END FUNCTION\n"
+ "SUB bar(q AS Pt)\n"
+ " PRINT q.x\n"
+ "END SUB\n",
+ "3 \n"
+ );
+
+ expectCompileError("Duplicate label rejected",
+ "L1:\n"
+ "PRINT 1\n"
+ "L1:\n"
+ "PRINT 2\n",
+ "Duplicate label 'L1'"
+ );
+
+ expectCompileError("Duplicate SUB rejected",
+ "SUB S\n"
+ " PRINT \"first\"\n"
+ "END SUB\n"
+ "SUB S\n"
+ " PRINT \"second\"\n"
+ "END SUB\n",
+ "Sub 'S' is already defined"
+ );
+
+ expectCompileError("Nested SUB rejected",
+ "SUB A\n"
+ " SUB B\n"
+ " END SUB\n"
+ "END SUB\n",
+ "SUB cannot be defined inside SUB or FUNCTION"
+ );
+
+ // PRINT#1 / CLOSE#1 / LINE INPUT#1 with no space before '#'
+ runProgram("PRINT# without a space",
+ "OPEN \"test_hash.txt\" FOR OUTPUT AS #1\n"
+ "PRINT#1, \"hi\"\n"
+ "CLOSE#1\n"
+ "OPEN \"test_hash.txt\" FOR INPUT AS #1\n"
+ "LINE INPUT#1, a$\n"
+ "CLOSE#1\n"
+ "PRINT a$\n"
+ "KILL \"test_hash.txt\"\n",
+ "hi\n"
+ );
+
+ expectCompileError("Integer literal wider than 32 bits rejected",
+ "x = 4294967297\n",
+ "Integer literal out of range"
+ );
+
+ expectCompileError("INTEGER suffix literal out of range rejected",
+ "x = 70000%\n",
+ "INTEGER literal out of range"
+ );
+
+ expectCompileError("Method arguments require commas",
+ "ctl.Method 1 2\n",
+ "Expected ','"
+ );
+
+ expectCompileError("Show modal flag must be a constant",
+ "DIM m AS INTEGER\n"
+ "frm.Show m\n",
+ "Show modal flag must be a literal or CONST"
+ );
+
+ expectCompileError("DECLARE mismatch rejected",
+ "DECLARE SUB S(a AS INTEGER)\n"
+ "SUB S(a AS INTEGER, b AS INTEGER)\n"
+ "END SUB\n",
+ "DECLARE for 'S' does not match its definition"
+ );
+
+ runProgram("DECLARE matching the definition",
+ "DECLARE SUB S(a AS INTEGER, b AS INTEGER)\n"
+ "S 1, 2\n"
+ "SUB S(a AS INTEGER, b AS INTEGER)\n"
+ " PRINT a + b\n"
+ "END SUB\n",
+ "3 \n"
+ );
+
+ expectCompileError("STRING * n out of range rejected",
+ "DIM s AS STRING * &H10005\n",
+ "STRING * length must be 1 to 65535"
+ );
+
+ expectCompileError("NEXT variable mismatch rejected",
+ "FOR i = 1 TO 2\n"
+ "FOR j = 1 TO 2\n"
+ "NEXT i\n"
+ "NEXT j\n",
+ "NEXT i does not match FOR j"
+ );
+
+ expectCompileError("CALL of a variable rejected",
+ "DIM v AS INTEGER\n"
+ "CALL v\n",
+ "'v' is not a SUB or FUNCTION"
+ );
+
+ expectCompileError("CALL without required arguments rejected",
+ "SUB S(a)\n"
+ "END SUB\n"
+ "CALL S\n",
+ "Sub 'S' expects 1 arguments, got 0"
+ );
+
+ runProgram("OPTIONAL padding on every call form",
+ "SUB S(a, OPTIONAL b$)\n"
+ " PRINT a; \"[\"; b$; \"]\"\n"
+ "END SUB\n"
+ "CALL S(1)\n"
+ "S 2, \"x\"\n"
+ "CALL S(3, \"y\")\n",
+ "1 []\n2 [x]\n3 [y]\n"
+ );
+
+ expectCompileError("Duplicate TYPE field rejected",
+ "TYPE T\n"
+ " x AS INTEGER\n"
+ " x AS STRING\n"
+ "END TYPE\n",
+ "Duplicate field 'x' in TYPE 'T'"
+ );
+
+ expectCompileError("DEFINT reversed range rejected",
+ "DEFINT Z-A\n",
+ "Letter range must be in ascending order"
+ );
+
+ expectCompileError("CR-only line endings are counted",
+ "PRINT 1\rPRINT 2\rPRINT )\r",
+ "Line 3:"
+ );
+
+ expectCompileError("STATIC mangled name too long rejected",
+ "SUB abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij\n"
+ " STATIC abcdefghijabcdefghijabcdefghij AS INTEGER\n"
+ "END SUB\n",
+ "STATIC name"
+ );
+
printf("All tests complete. Failures: %d\n", (int)gFailCount);
return gFailCount > 0 ? 1 : 0;
}
diff --git a/src/apps/kpunch/dvxbasic/test_quick.c b/src/apps/kpunch/dvxbasic/test_quick.c
index ad2b807..feb47af 100644
--- a/src/apps/kpunch/dvxbasic/test_quick.c
+++ b/src/apps/kpunch/dvxbasic/test_quick.c
@@ -31,8 +31,6 @@
#include
int main(void) {
- basStringSystemInit();
-
const char *source = "PRINT \"Hello, World!\"\n";
printf("Source: [%s]\n", source);
printf("Source len: %d\n", (int)strlen(source));
diff --git a/src/apps/kpunch/dvxbasic/test_suite.c b/src/apps/kpunch/dvxbasic/test_suite.c
index 5b98bdb..b1643c8 100644
--- a/src/apps/kpunch/dvxbasic/test_suite.c
+++ b/src/apps/kpunch/dvxbasic/test_suite.c
@@ -30,7 +30,11 @@
// Add new tests with TEST_EQ(name, source, expected) -- other helpers
// cover compile errors and runtime errors.
+#include "compiler/compact.h"
+#include "compiler/obfuscate.h"
#include "compiler/parser.h"
+#include "compiler/strip.h"
+#include "runtime/serialize.h"
#include "runtime/vm.h"
#include "runtime/values.h"
@@ -46,6 +50,9 @@
#define CAPTURE_MAX 8192
+// Source buffer for the too-many-globals test: one DIM line per slot plus one.
+#define TOO_MANY_GLOBALS_SRC_MAX ((BAS_VM_MAX_GLOBALS + 1) * 32)
+
typedef struct {
char buf[CAPTURE_MAX];
int32_t len;
@@ -315,7 +322,6 @@ static int32_t runSubAndCapture(const char *source, const char *subName,
vm->errorNumber = 0;
vm->errorMsg[0] = '\0';
vm->inErrorHandler = false;
- vm->errorHandler = 0;
bool ok = basVmCallSub(vm, subAddr);
rc = ok ? 0 : 1;
@@ -401,6 +407,213 @@ static void testRuntimeError(const char *name, const char *source, const char *n
#define TEST_RUNTIME_ERROR(n, s, e) testRuntimeError((n), (s), (e))
+// A breakpoint must pause the program even when ON ERROR GOTO is active:
+// the debugger pause is not a trappable error.
+static void testBreakpointNotTrapped(const char *name) {
+ const char *source =
+ "ON ERROR GOTO h\n"
+ "PRINT \"a\"\n"
+ "PRINT \"b\"\n"
+ "END\n"
+ "h:\n"
+ "PRINT \"HANDLER\"\n"
+ "RESUME NEXT\n";
+ int32_t breakLine = 3;
+ BasParserT parser;
+
+ basParserInit(&parser, source, (int32_t)strlen(source));
+
+ if (!basParse(&parser)) {
+ reportFail(name, parser.error);
+ basParserFree(&parser);
+ return;
+ }
+
+ BasModuleT *mod = basParserBuildModule(&parser);
+ basParserFree(&parser);
+
+ BasVmT *vm = basVmCreate();
+ CaptureT cap;
+
+ captureReset(&cap);
+ basVmLoadModule(vm, mod);
+ basVmSetPrintCallback(vm, capturePrint, &cap);
+ basVmSetBreakpoints(vm, &breakLine, 1);
+
+ BasVmResultE first = basVmRun(vm);
+
+ vm->running = true;
+
+ BasVmResultE second = basVmRun(vm);
+
+ if (first != BAS_VM_BREAKPOINT || second != BAS_VM_HALTED || strcmp(cap.buf, "a\nb\n") != 0) {
+ char detail[512];
+ snprintf(detail, sizeof(detail), "first=%d second=%d out=[%s]", (int)first, (int)second, cap.buf);
+ reportFail(name, detail);
+ } else {
+ reportPass(name);
+ }
+
+ basVmDestroy(vm);
+ basModuleFree(mod);
+}
+
+
+// Full release-build payload path: compile, strip, obfuscate against a
+// .frm, compact, serialize, deserialize, run. Checks that a SetEvent
+// handler name survives obfuscation, that a control named after a
+// property key ("Text") is left alone in both the .frm and the constant
+// pool, and that the round-tripped module still runs.
+static void testReleaseRoundTrip(const char *name) {
+ const char *source =
+ "SUB BtnOK_Click\n"
+ " PRINT \"clicked\"\n"
+ "END SUB\n"
+ "SUB Text_Change\n"
+ "END SUB\n"
+ "DIM h AS STRING\n"
+ "h = \"BtnOK_Click\"\n"
+ "PRINT \"Text\"\n"
+ "PRINT h\n";
+ const char *frm =
+ "Begin Form Form1\n"
+ "Caption = \"Demo\"\n"
+ "Begin CommandButton BtnOK\n"
+ "Caption = \"OK\"\n"
+ "End\n"
+ "Begin TextBox Text\n"
+ "Text = \"abc\"\n"
+ "End\n"
+ "Begin DBGrid Grid1\n"
+ "DataSource = \"BtnOK\"\n"
+ "End\n"
+ "End\n"
+ "Sub Form_Load ()\nEnd Sub\n";
+ BasParserT parser;
+
+ basParserInit(&parser, source, (int32_t)strlen(source));
+
+ if (!basParse(&parser)) {
+ reportFail(name, parser.error);
+ basParserFree(&parser);
+ return;
+ }
+
+ BasModuleT *mod = basParserBuildModule(&parser);
+ basParserFree(&parser);
+
+ basStripModule(mod);
+
+ const char *frmTexts[1] = { frm };
+ int32_t frmLens[1] = { (int32_t)strlen(frm) };
+ BasObfFrmT obf[1];
+
+ basObfuscateNames(mod, frmTexts, frmLens, 1, obf);
+ basCompactBytecode(mod);
+
+ char detail[512] = "";
+ bool ok = true;
+
+ // The button handler was renamed (BtnOK -> C?) and the constant that
+ // names it as a SetEvent target followed the rename.
+ const BasProcEntryT *btnProc = NULL;
+ const BasProcEntryT *textProc = basModuleFindProc(mod, "Text_Change");
+
+ for (int32_t i = 0; i < mod->procCount; i++) {
+ if (strstr(mod->procs[i].name, "_Click")) {
+ btnProc = &mod->procs[i];
+ }
+ }
+
+ if (!btnProc || strncmp(btnProc->name, "BtnOK", 5) == 0) {
+ snprintf(detail, sizeof(detail), "BtnOK_Click was not obfuscated");
+ ok = false;
+ } else if (!basModuleFindProc(mod, btnProc->name)) {
+ snprintf(detail, sizeof(detail), "renamed handler not findable");
+ ok = false;
+ } else if (!textProc) {
+ snprintf(detail, sizeof(detail), "Text_Change must keep its name (Text is a property key)");
+ ok = false;
+ }
+
+ bool sawHandlerConst = false;
+ bool sawTextConst = false;
+
+ for (int32_t i = 0; ok && i < mod->constCount; i++) {
+ if (btnProc && strcmp(mod->constants[i]->data, btnProc->name) == 0) {
+ sawHandlerConst = true;
+ }
+
+ if (strcmp(mod->constants[i]->data, "Text") == 0) {
+ sawTextConst = true;
+ }
+ }
+
+ if (ok && (!sawHandlerConst || !sawTextConst)) {
+ snprintf(detail, sizeof(detail), "constants: handler=%d text=%d", (int)sawHandlerConst, (int)sawTextConst);
+ ok = false;
+ }
+
+ // The .frm keeps the type token and the property key, renames the
+ // button both in its Begin line and where a value names it, and drops
+ // the trailing code section.
+ if (ok && obf[0].data) {
+ const char *text = (const char *)obf[0].data;
+
+ if (!strstr(text, "Begin TextBox Text\n") || !strstr(text, "Text = \"abc\"") || strstr(text, "BtnOK") || strstr(text, "Form_Load") || !strstr(text, "DataSource = \"C")) {
+ snprintf(detail, sizeof(detail), "frm rewrite wrong:\n%s", text);
+ ok = false;
+ }
+ } else if (ok) {
+ snprintf(detail, sizeof(detail), "no obfuscated frm produced");
+ ok = false;
+ }
+
+ // Serialize, deserialize, run.
+ if (ok) {
+ int32_t len = 0;
+ uint8_t *data = basModuleSerialize(mod, &len);
+ BasModuleT *back = data ? basModuleDeserialize(data, len) : NULL;
+
+ free(data);
+
+ if (!back) {
+ snprintf(detail, sizeof(detail), "serialize round trip failed");
+ ok = false;
+ } else {
+ BasVmT *vm = basVmCreate();
+ CaptureT cap;
+ char expected[256];
+
+ captureReset(&cap);
+ basVmLoadModule(vm, back);
+ basVmSetPrintCallback(vm, capturePrint, &cap);
+
+ BasVmResultE rc = basVmRun(vm);
+
+ snprintf(expected, sizeof(expected), "Text\n%s\n", btnProc->name);
+
+ if (rc != BAS_VM_HALTED || strcmp(cap.buf, expected) != 0) {
+ snprintf(detail, sizeof(detail), "rc=%d out=[%s] expected=[%s]", (int)rc, cap.buf, expected);
+ ok = false;
+ }
+
+ basVmDestroy(vm);
+ basModuleFree(back);
+ }
+ }
+
+ free(obf[0].data);
+ basModuleFree(mod);
+
+ if (ok) {
+ reportPass(name);
+ } else {
+ reportFail(name, detail);
+ }
+}
+
+
// ============================================================
// Widget-level event-dispatch harness
// ============================================================
@@ -753,8 +966,6 @@ static void testDispatchEq(const char *name, const TestDispatchT *d, const char
// ============================================================
int main(void) {
- basStringSystemInit();
-
printf("DVX BASIC Regression Suite\n");
printf("==========================\n");
@@ -1041,7 +1252,7 @@ int main(void) {
// --- CAST/conversion ---
TEST_EQ("cint", "PRINT CINT(3.6)\n", "4 \n");
TEST_EQ("clng", "PRINT CLNG(100000)\n", "100000 \n");
- TEST_EQ("cdbl", "PRINT CDBL(1) / 3\n", "0.333333 \n");
+ TEST_EQ("cdbl", "PRINT CDBL(1) / 3\n", "0.333333333333333 \n");
// --- Float arithmetic preserves fractional results ---
// Regression: the VM was promoting OP_ADD_INT results back to int16
@@ -1416,6 +1627,19 @@ int main(void) {
"SUB later\n PRINT \"ok\"\nEND SUB\n",
"ok\n");
+ // --- more module-level variables than the VM has slots ---
+ {
+ char *src = (char *)malloc(TOO_MANY_GLOBALS_SRC_MAX);
+ int32_t used = 0;
+
+ for (int32_t i = 0; i <= BAS_VM_MAX_GLOBALS; i++) {
+ used += snprintf(src + used, TOO_MANY_GLOBALS_SRC_MAX - used, "DIM g%d AS INTEGER\n", (int)i);
+ }
+
+ TEST_COMPILE_ERROR("too-many-globals", src, "Too many module-level variables");
+ free(src);
+ }
+
// --- FUNCTION without explicit return returns default value ---
TEST_EQ("function-default-return",
"FUNCTION zero AS INTEGER\nEND FUNCTION\n"
@@ -1657,8 +1881,9 @@ int main(void) {
TEST_EQ("float-tiny", "PRINT 0.001\n", "0.001 \n");
TEST_EQ("float-neg", "PRINT -3.14\n", "-3.14 \n");
// DVX uses %g formatting (~6 digits of precision) for doubles.
- TEST_EQ("float-via-div", "PRINT 22 / 7\n", "3.14286 \n");
- TEST_EQ("float-large", "PRINT 1000000.5\n", "1e+06 \n");
+ TEST_EQ("float-via-div", "PRINT 22 / 7\n", "3.14285714285714 \n");
+ TEST_EQ("float-large", "PRINT 1000000.5\n", "1000000.5 \n");
+ TEST_EQ("float-huge", "PRINT 1E20\n", "1E+20 \n");
// ============================================================
// Integer division and MOD edge cases
@@ -1755,10 +1980,10 @@ int main(void) {
// ============================================================
// DVX rounds half-away-from-zero (not VB banker's rounding).
- TEST_EQ("cint-round-half-up", "PRINT CINT(2.5)\n", "3 \n");
+ TEST_EQ("cint-round-half-even", "PRINT CINT(2.5)\n", "2 \n");
TEST_EQ("cint-round-half-up2","PRINT CINT(3.5)\n", "4 \n");
TEST_EQ("cint-truncate", "PRINT CINT(2.49)\n", "2 \n");
- TEST_EQ("cint-negative", "PRINT CINT(-2.5)\n", "-3 \n");
+ TEST_EQ("cint-negative", "PRINT CINT(-2.5)\n", "-2 \n");
TEST_EQ("clng-from-double", "PRINT CLNG(3.9)\n", "4 \n");
TEST_EQ("csng", "PRINT CSNG(1.5)\n", "1.5 \n");
TEST_EQ("cstr-int", "PRINT CSTR(42)\n", "42\n");
@@ -1780,7 +2005,7 @@ int main(void) {
// ============================================================
TEST_EQ("math-sqr-zero", "PRINT SQR(0)\n", "0 \n");
- TEST_EQ("math-sqr-float", "PRINT SQR(2)\n", "1.41421 \n");
+ TEST_EQ("math-sqr-float", "PRINT SQR(2)\n", "1.4142135623731 \n");
TEST_EQ("math-cos-zero", "PRINT COS(0)\n", "1 \n");
TEST_EQ("math-sin-zero", "PRINT SIN(0)\n", "0 \n");
TEST_EQ("math-exp-zero", "PRINT EXP(0)\n", "1 \n");
@@ -2316,15 +2541,69 @@ int main(void) {
// Type conversion through assignment
// ============================================================
- // DVX uses dynamic typing: assigning a float to a DIM AS INTEGER
- // slot stores the float value (the DIM only sets the INITIAL type).
- // This differs from QBASIC but is consistent across the language.
- TEST_EQ("dim-int-assign-float-preserves-type",
+ // Stores into an explicitly typed variable coerce to the declared
+ // type: INTEGER/LONG round (banker's) and range-check, SINGLE
+ // narrows. Untyped variables keep the value's own type.
+ TEST_EQ("typed-store-int-rounds",
"DIM n AS INTEGER\n"
"n = 3.7\n"
"PRINT n\n",
+ "4 \n");
+
+ TEST_EQ("typed-store-suffix-rounds",
+ "i% = 2.5\nj% = 3.5\nPRINT i%; j%\n",
+ "2 4 \n");
+
+ TEST_RUNTIME_ERROR("typed-store-int-overflow",
+ "DIM n AS INTEGER\nn = 40000\n",
+ "Overflow");
+
+ TEST_EQ("typed-store-long",
+ "DIM l AS LONG\nl = 40000.4\nPRINT l\n",
+ "40000 \n");
+
+ TEST_RUNTIME_ERROR("typed-store-long-overflow",
+ "l& = 2147483647 + 1\n",
+ "Overflow");
+
+ TEST_EQ("typed-store-single",
+ "DIM s AS SINGLE\ns = 1 / 3\nPRINT s\n",
+ "0.3333333 \n");
+
+ TEST_EQ("typed-store-defint",
+ "DEFINT A-C\nb = 1.5\nPRINT b\n",
+ "2 \n");
+
+ TEST_EQ("typed-store-untyped-keeps-float",
+ "x = 3.7\nPRINT x\n",
"3.7 \n");
+ TEST_EQ("typed-store-array-element",
+ "DIM a(3) AS INTEGER\na(1) = 2.5\na(2) = 3.5\nPRINT a(1); a(2)\n",
+ "2 4 \n");
+
+ TEST_EQ("typed-store-udt-field",
+ "TYPE T\n n AS INTEGER\n l AS LONG\nEND TYPE\n"
+ "DIM v AS T\nv.n = 1.5\nv.l = 2.5\nPRINT v.n; v.l\n",
+ "2 2 \n");
+
+ TEST_EQ("typed-store-udt-array-field",
+ "TYPE T\n n AS INTEGER\nEND TYPE\n"
+ "DIM a(2) AS T\na(1).n = 0.5\nPRINT a(1).n\n",
+ "0 \n");
+
+ TEST_EQ("typed-store-for-var",
+ "FOR i% = 0.6 TO 3\n PRINT i%;\nNEXT\nPRINT\n",
+ "1 2 3 \n");
+
+ TEST_EQ("typed-store-param",
+ "SUB s(n AS INTEGER)\n n = 1.5\n PRINT n\nEND SUB\ns 0\n",
+ "2 \n");
+
+ TEST_EQ("typed-store-read",
+ "DATA 1.5, 2.5\nDIM a AS INTEGER\nDIM b AS INTEGER\nREAD a, b\nPRINT a; b\n",
+ "2 2 \n");
+
TEST_EQ("assign-string-from-num",
"DIM s AS STRING\n"
"s = STR$(42)\n"
@@ -2865,6 +3144,211 @@ int main(void) {
"IF NOT (x < 0 OR x > 100) THEN PRINT \"in-range\"\n",
"in-range\n");
+ // ============================================================
+ // Regressions from the b638ca9 runtime review
+ // ============================================================
+
+ // TYPE values have value semantics: b = a copies, BYVAL copies.
+ TEST_EQ("udt-assign-copies",
+ "TYPE T\n x AS INTEGER\nEND TYPE\n"
+ "DIM a AS T\nDIM b AS T\n"
+ "a.x = 1\nb = a\nb.x = 2\nPRINT a.x; b.x\n",
+ "1 2 \n");
+
+ TEST_EQ("udt-byval-copies",
+ "TYPE T\n x AS INTEGER\nEND TYPE\n"
+ "SUB bump(BYVAL t AS T)\n t.x = 99\nEND SUB\n"
+ "DIM a AS T\na.x = 1\nbump a\nPRINT a.x\n",
+ "1 \n");
+
+ TEST_EQ("udt-byref-shares",
+ "TYPE T\n x AS INTEGER\nEND TYPE\n"
+ "SUB bump(t AS T)\n t.x = 99\nEND SUB\n"
+ "DIM a AS T\na.x = 1\nbump a\nPRINT a.x\n",
+ "99 \n");
+
+ TEST_EQ("udt-array-element-copies",
+ "TYPE T\n x AS INTEGER\nEND TYPE\n"
+ "DIM arr(2) AS T\nDIM v AS T\n"
+ "v.x = 5\narr(1) = v\nv.x = 6\nPRINT arr(1).x\n",
+ "5 \n");
+
+ // BYREF array element survives a REDIM/ERASE of that array in the callee.
+ TEST_EQ("byref-array-element-redim-in-callee",
+ "DIM SHARED a(10) AS INTEGER\n"
+ "SUB f(x AS INTEGER)\n REDIM a(200) AS INTEGER\n x = 5\n PRINT x\nEND SUB\n"
+ "f a(3)\nPRINT \"ok\"\n",
+ "5 \nok\n");
+
+ TEST_EQ("byref-array-element-erase-in-callee",
+ "DIM SHARED a(10) AS INTEGER\n"
+ "SUB f(x AS INTEGER)\n ERASE a\n x = 7\n PRINT x\nEND SUB\n"
+ "f a(3)\nPRINT \"ok\"\n",
+ "7 \nok\n");
+
+ TEST_EQ("byref-array-element-writes-back",
+ "DIM a(5) AS INTEGER\n"
+ "SUB f(x AS INTEGER)\n x = x + 40\nEND SUB\n"
+ "a(2) = 2\nf a(2)\nPRINT a(2)\n",
+ "42 \n");
+
+ // More decimal positions than the formatter's work buffer holds: the
+ // output is bounded, and the digits that fit are right.
+ TEST_EQ("print-using-many-decimals",
+ "DIM f AS STRING\nf = \"#.\" + STRING$(300, \"#\")\n"
+ "PRINT LEFT$(FORMAT$(1.5, f), 6)\n",
+ "1.5000\n");
+
+ // CINT/CLNG use banker's rounding and raise Overflow.
+ TEST_EQ("cint-bankers",
+ "PRINT CINT(2.5); CINT(3.5); CINT(-2.5)\n",
+ "2 4 -2 \n");
+
+ // CDBL keeps double precision; CSNG narrows to single.
+ TEST_EQ("cdbl-csng-precision",
+ "PRINT CDBL(1) / 3; CSNG(1 / 3)\n",
+ "0.333333333333333 0.3333333 \n");
+
+ TEST_RUNTIME_ERROR("cint-overflow",
+ "PRINT CINT(40000)\n",
+ "Overflow");
+
+ // PRINT / STR$ show up to 15 (DOUBLE) and 7 (SINGLE) significant digits.
+ TEST_EQ("print-double-digits",
+ "PRINT 1234567.89\nPRINT 3.14159265358979#\n",
+ "1234567.89 \n3.14159265358979 \n");
+
+ TEST_EQ("str-matches-print",
+ "PRINT STR$(1234567)\nPRINT STR$(-0.5)\n",
+ " 1234567\n-0.5\n");
+
+ // Stack exhaustion reaches ON ERROR with a real ERR value.
+ TEST_EQ("stack-overflow-err-number",
+ "ON ERROR GOTO h\n"
+ "SUB rec\n rec\nEND SUB\n"
+ "rec\nEND\n"
+ "h:\nPRINT ERR\nEND\n",
+ "7 \n");
+
+ // RESUME outside a handler is an error, not a restart from byte 0.
+ TEST_RUNTIME_ERROR("resume-without-error",
+ "PRINT \"start\"\nRESUME\n",
+ "RESUME without error");
+
+ // REDIM keeps the element type and can build TYPE arrays.
+ TEST_EQ("redim-string-array",
+ "REDIM s$(5)\nPRINT \"[\" + s$(1) + \"]\"\n",
+ "[]\n");
+
+ TEST_EQ("redim-udt-array",
+ "TYPE P\n x AS INTEGER\nEND TYPE\n"
+ "DIM a(2) AS P\nREDIM a(5) AS P\na(4).x = 3\nPRINT a(4).x\n",
+ "3 \n");
+
+ TEST_EQ("redim-preserve-2d",
+ "DIM a(1, 1) AS INTEGER\na(1, 0) = 5\na(0, 1) = 7\n"
+ "REDIM PRESERVE a(1, 2) AS INTEGER\nPRINT a(1, 0); a(0, 1); a(1, 2)\n",
+ "5 7 0 \n");
+
+ // A handler that ends the program has handled the error.
+ TEST_EQ("handler-end-clears-error",
+ "ON ERROR GOTO h\nERROR 5\nEND\nh:\nPRINT ERR\nEND\n",
+ "5 \n");
+
+ // File input: fields, quotes, long lines, BINARY positioning.
+ TEST_EQ("file-input-fields",
+ "OPEN \"test_suite_io.tmp\" FOR OUTPUT AS #1\n"
+ "WRITE #1, \"hello, world\", 42\n"
+ "PRINT #1, STRING$(2000, \"x\")\n"
+ "CLOSE #1\n"
+ "DIM a AS STRING\nDIM n AS INTEGER\nDIM l AS STRING\n"
+ "OPEN \"test_suite_io.tmp\" FOR INPUT AS #1\n"
+ "INPUT #1, a\nINPUT #1, n\nLINE INPUT #1, l\nCLOSE #1\n"
+ "KILL \"test_suite_io.tmp\"\n"
+ "PRINT a; n; LEN(l)\n",
+ "hello, world42 2000 \n");
+
+ TEST_EQ("file-binary-put-bytes",
+ "OPEN \"test_suite_bin.tmp\" FOR BINARY AS #1\n"
+ "PUT #1, 3, 200%\nCLOSE #1\n"
+ "PRINT FILELEN(\"test_suite_bin.tmp\")\n"
+ "KILL \"test_suite_bin.tmp\"\n",
+ "4 \n");
+
+ // INPUT # with several targets reads one field per target, in the
+ // WRITE # layout (quoted strings, comma separated).
+ TEST_EQ("file-input-multi-target",
+ "OPEN \"test_suite_io2.tmp\" FOR OUTPUT AS #1\n"
+ "WRITE #1, \"a, b\", 7, 2.5, 100000\n"
+ "CLOSE #1\n"
+ "DIM s AS STRING\nDIM n AS INTEGER\nDIM d AS DOUBLE\nDIM l AS LONG\n"
+ "OPEN \"test_suite_io2.tmp\" FOR INPUT AS #1\n"
+ "INPUT #1, s, n, d, l\nCLOSE #1\n"
+ "KILL \"test_suite_io2.tmp\"\n"
+ "PRINT s; n; d; l\n",
+ "a, b7 2.5 100000 \n");
+
+ // GET of a STRING in BINARY mode reads LEN(var) raw bytes; a
+ // fixed-length string therefore reads its declared width.
+ TEST_EQ("file-binary-get-string",
+ "OPEN \"test_suite_bin2.tmp\" FOR BINARY AS #1\n"
+ "PUT #1, 1, \"HelloWorld\"\nCLOSE #1\n"
+ "DIM s AS STRING * 5\nDIM t AS STRING\n"
+ "OPEN \"test_suite_bin2.tmp\" FOR BINARY AS #1\n"
+ "GET #1, 6, s\nt = SPACE$(3)\nGET #1, 1, t\nCLOSE #1\n"
+ "KILL \"test_suite_bin2.tmp\"\n"
+ "PRINT s; \"|\"; t; \"|\"; LEN(t)\n",
+ "World|Hel|3 \n");
+
+ // OPEN ... LEN= sets the RANDOM record size used by GET/PUT record
+ // numbers: record 2 of an 8-byte file starts at byte 8.
+ TEST_EQ("file-random-len-record-size",
+ "OPEN \"test_suite_rnd.tmp\" FOR RANDOM AS #1 LEN = 8\n"
+ "PUT #1, 2, 123456&\nCLOSE #1\n"
+ "PRINT FILELEN(\"test_suite_rnd.tmp\")\n"
+ "DIM l AS LONG\n"
+ "OPEN \"test_suite_rnd.tmp\" FOR RANDOM AS #1 LEN = 8\n"
+ "GET #1, 2, l\nCLOSE #1\n"
+ "KILL \"test_suite_rnd.tmp\"\n"
+ "PRINT l\n",
+ "12 \n123456 \n");
+
+ TEST_RUNTIME_ERROR("file-random-bad-len",
+ "OPEN \"test_suite_rnd2.tmp\" FOR RANDOM AS #1 LEN = 0\n",
+ "Bad record length");
+
+ // LEN/INSTR are LONG; CHR$(0) is a real character.
+ TEST_EQ("len-long",
+ "DIM s AS STRING\ns = STRING$(60000, \"a\")\nPRINT LEN(s); INSTR(s + \"b\", \"b\")\n",
+ "60000 60001 \n");
+
+ TEST_EQ("chr-zero",
+ "PRINT LEN(CHR$(0)); LEN(\"a\" + CHR$(0) + \"b\"); INSTR(\"a\" + CHR$(0) + \"b\", \"b\")\n",
+ "1 3 3 \n");
+
+ // VAL understands &H/&O and rejects atof-only syntax.
+ TEST_EQ("val-hex-oct",
+ "PRINT VAL(\"&HFF\"); VAL(\"&O17\"); VAL(\" 12.5e1x\"); VAL(\"inf\")\n",
+ "255 15 125 0 \n");
+
+ // \ and MOD round operands; NOT rounds too.
+ TEST_EQ("idiv-mod-round",
+ "PRINT 11.5 \\ 2; 7.5 MOD 2; NOT 0.6\n",
+ "6 0 -2 \n");
+
+ // Domain and argument errors instead of NaN / silent results.
+ TEST_RUNTIME_ERROR("sqr-negative", "PRINT SQR(-1)\n", "Illegal function call");
+ TEST_RUNTIME_ERROR("left-negative", "PRINT LEFT$(\"abc\", -1)\n", "Illegal function call");
+ TEST_RUNTIME_ERROR("string-negative", "PRINT STRING$(-1, \"x\")\n", "Illegal function call");
+
+ TEST_EQ("space-large",
+ "PRINT LEN(SPACE$(40000))\n",
+ "40000 \n");
+
+ // Debugger pause vs ON ERROR, and the release payload path.
+ testBreakpointNotTrapped("breakpoint-not-trapped");
+ testReleaseRoundTrip("release-round-trip");
+
printf("\n--------------------------\n");
printf("PASS: %d FAIL: %d\n", (int)sPassCount, (int)sFailCount);
diff --git a/src/apps/kpunch/dvxbasic/test_vm.c b/src/apps/kpunch/dvxbasic/test_vm.c
index c495ceb..19f0e60 100644
--- a/src/apps/kpunch/dvxbasic/test_vm.c
+++ b/src/apps/kpunch/dvxbasic/test_vm.c
@@ -201,7 +201,7 @@ static void test4(void) {
// FOR_NEXT: increment i, test, jump back
emit8(OP_FOR_NEXT);
emitU16(0); // local index
- emit8(1); // isLocal=1
+ emit8(SCOPE_LOCAL);
int16_t offset = (int16_t)(loopBody - (sCodeLen + 2));
emit16(offset);
@@ -232,8 +232,6 @@ int main(void) {
printf("DVX BASIC VM Tests\n");
printf("==================\n\n");
- basStringSystemInit();
-
test1();
test2();
test3();
diff --git a/src/include/basic/comm.bas b/src/include/basic/comm.bas
index 25d277a..f6fc7c6 100644
--- a/src/include/basic/comm.bas
+++ b/src/include/basic/comm.bas
@@ -63,7 +63,7 @@ DECLARE LIBRARY "basrt"
' Set the I/O base address for a COM port before opening.
' Default bases: COM1=&H3F8, COM2=&H2F8, COM3=&H3E8, COM4=&H2E8
- DECLARE SUB SerSetBase(BYVAL com AS INTEGER, BYVAL base AS INTEGER)
+ DECLARE SUB SerSetBase(BYVAL com AS INTEGER, BYVAL ioBase AS INTEGER)
' Set the IRQ for a COM port before opening.
' Default IRQs: COM1=4, COM2=3, COM3=4, COM4=3
@@ -86,7 +86,7 @@ DECLARE LIBRARY "basrt"
DECLARE SUB SerClose(BYVAL com AS INTEGER)
' Write a string to the serial port. Returns True on success.
- DECLARE FUNCTION SerWrite(BYVAL com AS INTEGER, BYVAL data AS STRING) AS INTEGER
+ DECLARE FUNCTION SerWrite(BYVAL com AS INTEGER, BYVAL payload AS STRING) AS INTEGER
' Read pending data from the serial port. Returns "" if none available.
DECLARE FUNCTION SerRead$(BYVAL com AS INTEGER) AS STRING
@@ -115,7 +115,7 @@ DECLARE LIBRARY "basrt"
' Send data on a logical channel (0-127).
' encrypt: True to encrypt (requires CommHandshake first).
- DECLARE FUNCTION CommSend(BYVAL handle AS INTEGER, BYVAL data AS STRING, BYVAL channel AS INTEGER, BYVAL encrypt AS INTEGER) AS INTEGER
+ DECLARE FUNCTION CommSend(BYVAL handle AS INTEGER, BYVAL payload AS STRING, BYVAL channel AS INTEGER, BYVAL encrypt AS INTEGER) AS INTEGER
' Receive pending data from a specific channel. Returns "" if none.
DECLARE FUNCTION CommRecv$(BYVAL handle AS INTEGER, BYVAL channel AS INTEGER) AS STRING
diff --git a/src/libs/kpunch/dvxshell/shellApp.c b/src/libs/kpunch/dvxshell/shellApp.c
index 36ad3bf..33b8f4d 100644
--- a/src/libs/kpunch/dvxshell/shellApp.c
+++ b/src/libs/kpunch/dvxshell/shellApp.c
@@ -38,6 +38,9 @@
#include
#include "dvxMem.h"
+// File copy chunk size for multi-instance temp copies
+#define COPY_BUF_SIZE 32768
+
// ============================================================
// Module state
// ============================================================
@@ -168,7 +171,7 @@ static int32_t copyFile(const char *src, const char *dst) {
// small as TS_DEFAULT_STACK_SIZE (32KB), which a 32KB stack frame would
// overflow. The cooperative switcher is single-threaded, so a shared
// copy buffer is safe.
- static char buf[32768];
+ static char buf[COPY_BUF_SIZE];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
@@ -286,7 +289,7 @@ static void makeTempPath(const char *origPath, int32_t id, char *out, int32_t ou
}
if (tmpDir && tmpDir[0]) {
- snprintf(out, outSize, "%s/_dvx%02ld%s", tmpDir, (long)id, dot);
+ snprintf(out, outSize, "%s" DVX_PATH_SEP "_dvx%02ld%s", tmpDir, (long)id, dot);
} else {
snprintf(out, outSize, "_dvx%02ld%s", (long)id, dot);
}
@@ -405,6 +408,24 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) {
static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const char *args) {
+ // Check if this DXE is already loaded. If so, check whether the app
+ // allows multiple instances. We read the descriptor from the existing
+ // slot directly -- no need to dlopen again. This runs before the
+ // slot is allocated so a rejected launch never grows the slot index.
+ ShellAppT *existing = findLoadedPath(path);
+
+ if (existing) {
+ // Read multiInstance from the already-loaded descriptor
+ AppDescriptorT *existDesc = (AppDescriptorT *)dlsym(existing->dxeHandle, "_appDescriptor");
+
+ if (!existDesc || !existDesc->multiInstance) {
+ char msg[SHELL_MSG_MAX];
+ snprintf(msg, sizeof(msg), "%s is already running.", platformPathBaseName(path));
+ dvxErrorBox(ctx, NULL, msg);
+ return -1;
+ }
+ }
+
// Allocate a slot
int32_t id = allocSlot();
@@ -413,30 +434,16 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
return -1;
}
- // Check if this DXE is already loaded. If so, check whether the app
- // allows multiple instances. We read the descriptor from the existing
- // slot directly -- no need to dlopen again.
- const char *loadPath = path;
+ const char *loadPath = path;
char tempPath[DVX_MAX_PATH] = {0};
- ShellAppT *existing = findLoadedPath(path);
if (existing) {
- // Read multiInstance from the already-loaded descriptor
- AppDescriptorT *existDesc = (AppDescriptorT *)dlsym(existing->dxeHandle, "_appDescriptor");
-
- if (!existDesc || !existDesc->multiInstance) {
- char msg[320];
- snprintf(msg, sizeof(msg), "%s is already running.", platformPathBaseName(path));
- dvxErrorBox(ctx, NULL, msg);
- return -1;
- }
-
// Multi-instance allowed: copy to a temp file so dlopen gets
// an independent code+data image.
makeTempPath(path, id, tempPath, sizeof(tempPath));
if (copyFile(path, tempPath) != 0) {
- char msg[320];
+ char msg[SHELL_MSG_MAX];
snprintf(msg, sizeof(msg), "Failed to create instance copy of %s.", platformPathBaseName(path));
dvxErrorBox(ctx, NULL, msg);
return -1;
@@ -445,9 +452,6 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
loadPath = tempPath;
}
- // Snapshot free memory before loading so we can estimate app usage
- dvxMemSnapshotLoad(id);
-
// Show hourglass during the load (dlopen + symbol resolution + appMain)
dvxSetBusy(ctx, true);
@@ -455,7 +459,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
void *handle = dlopen(loadPath, RTLD_GLOBAL);
if (!handle) {
- char msg[512];
+ char msg[SHELL_MSG_MAX];
snprintf(msg, sizeof(msg), "Failed to load %s:\n%s", platformPathBaseName(path), dlerror());
dvxLog("DXE load failed: %s", msg);
dvxSetBusy(ctx, false);
@@ -467,7 +471,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
AppDescriptorT *desc = (AppDescriptorT *)dlsym(handle, "_appDescriptor");
if (!desc) {
- char msg[256];
+ char msg[SHELL_MSG_MAX];
snprintf(msg, sizeof(msg), "%s: missing appDescriptor", platformPathBaseName(path));
dvxLog("DXE symbol error: %s", msg);
dvxSetBusy(ctx, false);
@@ -478,7 +482,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
int32_t (*entry)(DxeAppContextT *) = (int32_t (*)(DxeAppContextT *))dlsym(handle, "_appMain");
if (!entry) {
- char msg[256];
+ char msg[SHELL_MSG_MAX];
snprintf(msg, sizeof(msg), "%s: missing appMain", platformPathBaseName(path));
dvxSetBusy(ctx, false);
dvxErrorBox(ctx, NULL, msg);
diff --git a/src/libs/kpunch/dvxshell/shellApp.h b/src/libs/kpunch/dvxshell/shellApp.h
index 96a5600..b3732ac 100644
--- a/src/libs/kpunch/dvxshell/shellApp.h
+++ b/src/libs/kpunch/dvxshell/shellApp.h
@@ -57,6 +57,12 @@
#define SHELL_APP_NAME_MAX 64
+// Launch argument buffer size (DxeAppContextT.args and shell-side copies)
+#define SHELL_ARGS_MAX 1024
+
+// Shared size for shell status/error message buffers
+#define SHELL_MSG_MAX 512
+
// Every DXE app exports a global AppDescriptorT named "appDescriptor".
// The shell reads it at load time to determine how to launch the app.
@@ -87,7 +93,7 @@ typedef struct {
char appPath[DVX_MAX_PATH]; // full path to the .app file
char appDir[DVX_MAX_PATH]; // directory containing the .app file
char configDir[DVX_MAX_PATH]; // writable config directory (CONFIG//)
- char args[1024]; // launch arguments (empty if none)
+ char args[SHELL_ARGS_MAX]; // launch arguments (empty if none)
char helpFile[DVX_MAX_PATH]; // help file path (for F1 context help)
char helpTopic[128]; // current help topic ID (updated by app)
void (*onHelpQuery)(void *ctx); // called on F1 to refresh helpTopic
diff --git a/src/libs/kpunch/dvxshell/shellMain.c b/src/libs/kpunch/dvxshell/shellMain.c
index 03d32b7..2d1708a 100644
--- a/src/libs/kpunch/dvxshell/shellMain.c
+++ b/src/libs/kpunch/dvxshell/shellMain.c
@@ -58,6 +58,7 @@
#include "dvxPlat.h"
#include "stb_ds_wrap.h"
+#include
#include
#include
#include
@@ -146,7 +147,7 @@ static void f1HelpHandler(void *ctx) {
AppContextT *ac = (AppContextT *)ctx;
// Find the focused window's owning app
- char args[1024] = {0};
+ char args[SHELL_ARGS_MAX] = {0};
int32_t focusedAppId = 0;
if (ac->stack.focusedIdx >= 0) {
@@ -344,7 +345,7 @@ int shellMain(int argc, char *argv[]) {
// initialization are caught and recovered from gracefully.
platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, dvxLog);
- // Initialize GUI — switch from VGA splash to VESA mode as late as possible
+ // Initialize GUI -- switch from VGA splash to VESA mode as late as possible
// so the VGA loading splash stays visible through all the init above.
{
int32_t videoW = prefsGetInt(sPrefs, "video", "width", DVX_DEFAULT_VIDEO_W);
@@ -385,10 +386,15 @@ int shellMain(int argc, char *argv[]) {
const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", MOUSE_ACCEL_DEFAULT);
int32_t accelVal = 0;
- if (strcmp(accelStr, "off") == 0) { accelVal = MOUSE_ACCEL_OFF; }
- else if (strcmp(accelStr, "low") == 0) { accelVal = MOUSE_ACCEL_LOW; }
- else if (strcmp(accelStr, "medium") == 0) { accelVal = MOUSE_ACCEL_MEDIUM; }
- else if (strcmp(accelStr, "high") == 0) { accelVal = MOUSE_ACCEL_HIGH; }
+ if (strcmp(accelStr, "off") == 0) {
+ accelVal = MOUSE_ACCEL_OFF;
+ } else if (strcmp(accelStr, "low") == 0) {
+ accelVal = MOUSE_ACCEL_LOW;
+ } else if (strcmp(accelStr, "medium") == 0) {
+ accelVal = MOUSE_ACCEL_MEDIUM;
+ } else if (strcmp(accelStr, "high") == 0) {
+ accelVal = MOUSE_ACCEL_HIGH;
+ }
int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT);
int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT);
@@ -403,9 +409,11 @@ int shellMain(int argc, char *argv[]) {
const char *val = prefsGetString(sPrefs, "colors", dvxColorName((ColorIdE)i), NULL);
if (val) {
- int r, g, b;
+ int32_t r;
+ int32_t g;
+ int32_t b;
- if (sscanf(val, "%d,%d,%d", &r, &g, &b) == 3) {
+ if (sscanf(val, "%" SCNd32 ",%" SCNd32 ",%" SCNd32, &r, &g, &b) == 3) {
sCtx.colorRgb[i][0] = (uint8_t)r;
sCtx.colorRgb[i][1] = (uint8_t)g;
sCtx.colorRgb[i][2] = (uint8_t)b;
@@ -499,7 +507,7 @@ int shellMain(int argc, char *argv[]) {
ShellAppT *app = shellGetApp(crashedAppId);
if (app) {
- char msg[256];
+ char msg[SHELL_MSG_MAX];
snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name);
shellForceKillApp(&sCtx, app);
sCtx.currentAppId = 0;
diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs
index 450244b..d0431ce 100644
--- a/src/libs/kpunch/libdvx/apiref.dhs
+++ b/src/libs/kpunch/libdvx/apiref.dhs
@@ -1096,7 +1096,6 @@ Draw a vertical line (1px wide).
.index dirtyListClear
.index flushRect
.index rectIntersect
-.index rectIsEmpty
.h1 dvxComp.h -- Layer 3: Dirty Rectangle Compositor
@@ -1191,22 +1190,6 @@ Compute the intersection of two rectangles.
Returns: true if the rectangles overlap, false if disjoint.
-.h2 rectIsEmpty
-
-.code
-bool rectIsEmpty(const RectT *r);
-.endcode
-
-Test whether a rectangle has zero or negative area.
-
-.table
- Parameter Description
- --------- -----------
- r Rectangle to test
-.endtable
-
-Returns: true if w <= 0 or h <= 0.
-
.topic api.wm
.title dvxWm.h -- Layer 4: Window Manager
.toc 1 dvxWm.h -- Layer 4: Window Manager
@@ -1897,7 +1880,7 @@ End the current resize operation. Clears resizeWindow state.
.h3 wmScrollbarClick
.code
-void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t orient, int32_t mx, int32_t my);
+void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, ScrollbarOrientE orient, int32_t mx, int32_t my);
.endcode
Handle an initial click on a scrollbar. Determines what was hit (arrows, trough, or thumb) and either adjusts the value immediately or begins a thumb drag.
@@ -1908,7 +1891,7 @@ Handle an initial click on a scrollbar. Determines what was hit (arrows, trough,
stack Window stack
dl Dirty list
idx Stack index of window
- orient SCROLL_VERTICAL or SCROLL_HORIZONTAL
+ orient ScrollbarVerticalE or ScrollbarHorizontalE
mx, my Click screen coordinates
.endtable
@@ -4610,7 +4593,6 @@ Remove a key from a section. No-op if the key does not exist.
.index dvxStrdup
.index dvxMemGetAppUsage
.index dvxMemResetApp
-.index dvxMemSnapshotLoad
.h1 dvxMem.h -- Per-App Memory Tracking
@@ -4672,20 +4654,6 @@ Tracked strdup.
.h2 Accounting
-.h3 dvxMemSnapshotLoad
-
-.code
-void dvxMemSnapshotLoad(int32_t appId);
-.endcode
-
-Record a baseline memory snapshot for the given app. Called right before app code starts so later calls to dvxMemGetAppUsage can report net growth.
-
-.table
- Parameter Description
- --------- -----------
- appId App ID to snapshot
-.endtable
-
.h3 dvxMemGetAppUsage
.code
@@ -5380,6 +5348,14 @@ const char *platformPathBaseName(const char *path);
Return a pointer to the leaf (basename) portion of path.
+.h3 platformCopyFile
+
+.code
+bool platformCopyFile(const char *srcPath, const char *dstPath);
+.endcode
+
+Byte-for-byte file copy. Returns false on any open, read, or write failure; a partially written destination is removed so no truncated copy is left behind.
+
.h3 platformReadFile
.code
diff --git a/src/libs/kpunch/libdvx/arch.dhs b/src/libs/kpunch/libdvx/arch.dhs
index 3674a55..0ef9887 100644
--- a/src/libs/kpunch/libdvx/arch.dhs
+++ b/src/libs/kpunch/libdvx/arch.dhs
@@ -511,7 +511,6 @@ Explicit use (e.g. in the Task Manager) can include dvxMem.h to call:
.table
Function Purpose
-------- -------
- dvxMemSnapshotLoad Baseline a newly-loaded app's memory state
dvxMemGetAppUsage Query current bytes allocated for an app
dvxMemResetApp Free every tracked allocation charged to an app
.endtable
diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c
index cab3f36..51e54c9 100644
--- a/src/libs/kpunch/libdvx/dvxApp.c
+++ b/src/libs/kpunch/libdvx/dvxApp.c
@@ -112,6 +112,20 @@
// RGB pixel stride (bytes per pixel in 24-bit RGB)
#define RGB_CHANNELS 3
+#define RGBA_CHANNELS 4
+#define ALPHA_OPAQUE_MIN 128 // alpha at or above this is opaque
+#define KEY_COLOR_R 255 // magenta transparency key
+#define KEY_COLOR_G 0
+#define KEY_COLOR_B 255
+
+// Tooltip placement relative to the mouse pointer
+#define TOOLTIP_OFFSET_X 12
+#define TOOLTIP_OFFSET_Y 16
+#define TOOLTIP_ABOVE_GAP 4 // gap above the pointer when flipped up
+
+// Popup menu check-mark / radio glyph geometry (relative to glyph center)
+#define CHECK_STROKE_H 2 // height of each check-mark stroke
+#define RADIO_DIAMOND_HALF 2 // half-size of the radio diamond
// ============================================================
// Window callback dispatch with app ID tracking
@@ -134,6 +148,7 @@
// Prototypes
// ============================================================
+static void activatePopupItem(AppContextT *ctx, int32_t itemIdx);
static void addSysMenuItem(AppContextT *ctx, const char *label, int32_t cmd, bool enabled);
static void addSysMenuSeparator(AppContextT *ctx);
static uint8_t *bufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch);
@@ -150,7 +165,10 @@ static void closeSysMenu(AppContextT *ctx);
static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id);
static void compositeAndFlush(AppContextT *ctx);
static void consumeMousePress(AppContextT *ctx);
+static uint8_t *convertRgbToNative(const DisplayT *d, const uint8_t *src, int32_t channels, int32_t w, int32_t h, uint32_t keyColor, bool *outHasAlpha, int32_t *outPitch);
static int32_t countVisibleWindows(const AppContextT *ctx);
+static int32_t cursorForResizeEdge(int32_t edge);
+static void cycleWindowFocus(AppContextT *ctx, bool reverse);
static void deferDestroyWindow(AppContextT *ctx, WindowT *win);
static void destroyWindowPrep(AppContextT *ctx, WindowT *win);
static void destroyWindowSync(AppContextT *ctx, WindowT *win);
@@ -162,6 +180,7 @@ static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y);
static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const MenuT *menu, int32_t px, int32_t py, int32_t pw, int32_t ph, int32_t hoverItem, const RectT *clipTo);
static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData);
static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd);
+static uint8_t *finishLoadImage(const DisplayT *d, uint8_t *rgb, int32_t imgW, int32_t imgH, int32_t *outW, int32_t *outH, int32_t *outPitch);
static WindowT *findWindowById(AppContextT *ctx, int32_t id);
static void flushPendingDestroys(AppContextT *ctx);
static void focusTopmostAlive(AppContextT *ctx);
@@ -171,6 +190,7 @@ static void interactiveSaveBuffer(AppContextT *ctx, const char *title, const uin
static void interactiveScreenshot(AppContextT *ctx);
static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win);
static void invalidateAllWindows(AppContextT *ctx);
+static int32_t nextMenuItem(const bool *firstSeparator, size_t stride, int32_t count, int32_t idx, int32_t dir);
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY);
static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx);
static void openSubMenu(AppContextT *ctx);
@@ -212,6 +232,33 @@ static const int32_t sBayerMatrix[4][4] = {
};
+// ============================================================
+// activatePopupItem -- fire an enabled, non-separator popup menu item
+// ============================================================
+//
+// Shared by the mouse click, Enter and mnemonic paths so all three
+// toggle check/radio state before onMenu sees the item. The popup is
+// closed BEFORE calling onMenu because the handler may open a modal
+// dialog, which runs a nested dvxUpdate loop; a still-active popup
+// would then be drawn/interacted with while stale.
+
+static void activatePopupItem(AppContextT *ctx, int32_t itemIdx) {
+ MenuItemT *item = &ctx->popup.menu->items[itemIdx];
+
+ if (item->type == MenuItemCheckE || item->type == MenuItemRadioE) {
+ clickMenuCheckRadio(ctx->popup.menu, itemIdx);
+ }
+
+ int32_t menuId = item->id;
+ WindowT *win = findWindowById(ctx, ctx->popup.windowId);
+ closeAllPopups(ctx);
+
+ if (win && win->onMenu) {
+ WIN_CALLBACK(ctx, win, win->onMenu(win, menuId));
+ }
+}
+
+
// Appends one item to the window system menu, copying the label (NUL-
// terminated), setting its command/enabled state and parsing its accelerator.
static void addSysMenuItem(AppContextT *ctx, const char *label, int32_t cmd, bool enabled) {
@@ -544,13 +591,13 @@ static bool checkAccelTable(AppContextT *ctx, WindowT *win, int32_t key, int32_t
// Map back to uppercase letter for matching
int32_t matchKey = key;
- if ((modifiers & ACCEL_CTRL) && matchKey >= 0x01 && matchKey <= 0x1A) {
- matchKey = matchKey + 'A' - 1;
+ if ((modifiers & ACCEL_CTRL) && matchKey >= KEY_ASCII_CTRL_A && matchKey <= KEY_ASCII_CTRL_Z) {
+ matchKey = matchKey - KEY_ASCII_CTRL_A + 'A';
}
// Uppercase for case-insensitive letter matching
if (matchKey >= 'a' && matchKey <= 'z') {
- matchKey = matchKey - 32;
+ matchKey = toupper(matchKey);
}
int32_t requiredMods = modifiers & (ACCEL_CTRL | ACCEL_ALT);
@@ -986,6 +1033,57 @@ static void consumeMousePress(AppContextT *ctx) {
}
+// ============================================================
+// convertRgbToNative -- pack an stb_image RGB/RGBA buffer to display format
+// ============================================================
+//
+// channels is 3 (RGB) or 4 (RGBA). For RGBA, pixels below the opaque
+// threshold are replaced by keyColor and *outHasAlpha is set. Returns a
+// malloc'd buffer (pitch in *outPitch) or NULL on allocation failure.
+
+static uint8_t *convertRgbToNative(const DisplayT *d, const uint8_t *src, int32_t channels, int32_t w, int32_t h, uint32_t keyColor, bool *outHasAlpha, int32_t *outPitch) {
+ int32_t bpp = d->format.bytesPerPixel;
+ int32_t pitch = w * bpp;
+ bool hasAlpha = false;
+ uint8_t *buf = (uint8_t *)malloc((size_t)pitch * h);
+
+ if (!buf) {
+ return NULL;
+ }
+
+ for (int32_t y = 0; y < h; y++) {
+ for (int32_t x = 0; x < w; x++) {
+ const uint8_t *px = src + (y * w + x) * channels;
+ uint8_t *dst = buf + y * pitch + x * bpp;
+ uint32_t color;
+
+ if (channels == RGBA_CHANNELS && px[RGB_CHANNELS] < ALPHA_OPAQUE_MIN) {
+ color = keyColor;
+ hasAlpha = true;
+ } else {
+ color = packColor(d, px[0], px[1], px[2]);
+ }
+
+ if (bpp == 1) {
+ *dst = (uint8_t)color;
+ } else if (bpp == 2) {
+ *(uint16_t *)dst = (uint16_t)color;
+ } else {
+ *(uint32_t *)dst = color;
+ }
+ }
+ }
+
+ if (outHasAlpha) {
+ *outHasAlpha = hasAlpha;
+ }
+
+ *outPitch = pitch;
+
+ return buf;
+}
+
+
// ============================================================
// countVisibleWindows -- count non-minimized visible windows
// ============================================================
@@ -1005,6 +1103,101 @@ static int32_t countVisibleWindows(const AppContextT *ctx) {
}
+// ============================================================
+// cursorForResizeEdge -- map a RESIZE_* edge mask to a cursor shape
+// ============================================================
+
+static int32_t cursorForResizeEdge(int32_t edge) {
+ bool horiz = (edge & (RESIZE_LEFT | RESIZE_RIGHT)) != 0;
+ bool vert = (edge & (RESIZE_TOP | RESIZE_BOTTOM)) != 0;
+
+ if (horiz && vert) {
+ if ((edge & RESIZE_LEFT && edge & RESIZE_TOP) ||
+ (edge & RESIZE_RIGHT && edge & RESIZE_BOTTOM)) {
+ return CURSOR_RESIZE_DIAG_NWSE;
+ }
+
+ return CURSOR_RESIZE_DIAG_NESW;
+ }
+
+ if (horiz) {
+ return CURSOR_RESIZE_H;
+ }
+
+ if (vert) {
+ return CURSOR_RESIZE_V;
+ }
+
+ return CURSOR_ARROW;
+}
+
+
+// ============================================================
+// cycleWindowFocus -- Alt+Tab / Shift+Alt+Tab window cycling
+// ============================================================
+//
+// Unlike Windows, there's no task-switcher overlay here -- each press
+// immediately rotates the window stack and focuses the new top.
+// Alt+Tab sends the top window to the bottom of the stack and raises
+// the next visible window below it. Shift+Alt+Tab does the reverse,
+// pulling the bottom-most visible window to the top. Hidden and
+// deferred-destroyed windows are never candidates, and a modal dialog
+// blocks cycling entirely (matching the mouse input gate).
+
+static void cycleWindowFocus(AppContextT *ctx, bool reverse) {
+ WindowStackT *stack = &ctx->stack;
+ int32_t top = stack->count - 1;
+ int32_t target = -1;
+
+ if (stack->count < 2 || ctx->modalWindow) {
+ return;
+ }
+
+ if (reverse) {
+ for (int32_t i = 0; i < top; i++) {
+ if (stack->windows[i]->visible && !stack->windows[i]->destroyPending) {
+ target = i;
+ break;
+ }
+ }
+ } else {
+ for (int32_t i = top - 1; i >= 0; i--) {
+ if (stack->windows[i]->visible && !stack->windows[i]->destroyPending) {
+ target = i;
+ break;
+ }
+ }
+ }
+
+ if (target < 0) {
+ return;
+ }
+
+ if (!reverse) {
+ // Rotate: move top to bottom, shift everything else up. The
+ // focused-index reference follows the moved window so wmSetFocus
+ // below still sees it as the outgoing window and fires onBlur.
+ WindowT *topWin = stack->windows[top];
+ dirtyListAdd(&ctx->dirty, topWin->x, topWin->y, topWin->w, topWin->h);
+
+ for (int32_t i = top; i > 0; i--) {
+ stack->windows[i] = stack->windows[i - 1];
+ }
+
+ stack->windows[0] = topWin;
+
+ if (stack->focusedIdx == top) {
+ stack->focusedIdx = 0;
+ }
+
+ target++;
+ }
+
+ wmRaiseWindow(stack, &ctx->dirty, target);
+ wmSetFocus(stack, &ctx->dirty, top);
+}
+
+
// ============================================================
// deferDestroyWindow
// ============================================================
@@ -1190,54 +1383,20 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
if (next) {
closeOpenPopup(ctx);
- WidgetT *prev = sFocusedWidget;
- sFocusedWidget = next;
- // Snapshot the destroy generation: the blur commit may
- // fire a user Change handler that destroys widgets --
- // prev and next are dangling then.
- uint32_t gen = sWidgetGen;
-
- if (prev) {
- if (prev != next) {
- wclsClearSelection(prev);
- // Commit/clamp any in-progress edit.
- wclsOnBlur(prev);
- }
-
- if (sWidgetGen == gen) {
- wgtInvalidatePaint(prev);
- }
- }
-
- if (sWidgetGen == gen) {
+ // A blur/focus callback may destroy widgets -- next is
+ // dangling then and must not be touched.
+ if (widgetTransferFocus(next)) {
wclsOnAccelActivate(next, win->widgetRoot);
- wgtInvalidatePaint(next);
}
}
} else if (wclsHas(target, WGT_METHOD_ON_ACCEL_ACTIVATE)) {
closeOpenPopup(ctx);
- WidgetT *prev = sFocusedWidget;
- sFocusedWidget = target;
- // Snapshot the destroy generation: the blur commit may
- // fire a user Change handler that destroys widgets --
- // prev and target are dangling then.
- uint32_t gen = sWidgetGen;
-
- if (prev && prev != target) {
- wclsClearSelection(prev);
- // Commit/clamp any in-progress edit.
- wclsOnBlur(prev);
-
- if (sWidgetGen == gen) {
- wgtInvalidatePaint(prev);
- }
- }
-
- if (sWidgetGen == gen) {
+ // A blur/focus callback may destroy widgets -- target is
+ // dangling then and must not be touched.
+ if (widgetTransferFocus(target)) {
wclsOnAccelActivate(target, win->widgetRoot);
- wgtInvalidatePaint(target);
}
}
@@ -1321,7 +1480,7 @@ static void dispatchEvents(AppContextT *ctx) {
if (rWin->onPaint) {
RectT fullRect = {0, 0, rWin->contentW, rWin->contentH};
- rWin->onPaint(rWin, &fullRect);
+ WIN_CALLBACK(ctx, rWin, rWin->onPaint(rWin, &fullRect));
rWin->iconNeedsRefresh = true;
}
@@ -1442,18 +1601,6 @@ static void dispatchEvents(AppContextT *ctx) {
// Clicking a submenu item opens it (already open from hover, but ensure)
openSubMenu(ctx);
} else if (item->enabled && !item->separator) {
- // Toggle check/radio state before closing
- if (item->type == MenuItemCheckE || item->type == MenuItemRadioE) {
- clickMenuCheckRadio(ctx->popup.menu, clickIdx);
- }
-
- // Close popup BEFORE calling onMenu because the menu
- // handler may open a modal dialog, which runs a nested
- // dvxUpdate loop. If the popup were still active, the
- // nested loop would try to draw/interact with a stale popup.
- int32_t menuId = item->id;
- WindowT *win = findWindowById(ctx, ctx->popup.windowId);
- closeAllPopups(ctx);
// Consume the press so that if the menu handler
// re-enters the event loop (CreateForm/Show, modal
// dialog) the still-held button isn't re-dispatched
@@ -1461,10 +1608,7 @@ static void dispatchEvents(AppContextT *ctx) {
// the (now-closed) menu dropdown. The matching
// release is also swallowed via suppressNextMouseUp.
consumeMousePress(ctx);
-
- if (win && win->onMenu) {
- WIN_CALLBACK(ctx, win, win->onMenu(win, menuId));
- }
+ activatePopupItem(ctx, clickIdx);
}
}
}
@@ -1520,7 +1664,7 @@ static void dispatchEvents(AppContextT *ctx) {
if (win->onMouse) {
int32_t relX = mx - win->x - win->contentX;
int32_t relY = my - win->y - win->contentY;
- win->onMouse(win, relX, relY, buttons);
+ WIN_CALLBACK(ctx, win, win->onMouse(win, relX, relY, buttons));
}
// Then check for context menus
@@ -1563,7 +1707,7 @@ static void dispatchEvents(AppContextT *ctx) {
if (win->onMouse) {
int32_t relX = mx - win->x - win->contentX;
int32_t relY = my - win->y - win->contentY;
- win->onMouse(win, relX, relY, buttons);
+ WIN_CALLBACK(ctx, win, win->onMouse(win, relX, relY, buttons));
}
}
}
@@ -1591,7 +1735,7 @@ static void dispatchEvents(AppContextT *ctx) {
if ((!ctx->modalWindow || win == ctx->modalWindow) && win->onMouse) {
int32_t relX = mx - win->x - win->contentX;
int32_t relY = my - win->y - win->contentY;
- win->onMouse(win, relX, relY, buttons);
+ WIN_CALLBACK(ctx, win, win->onMouse(win, relX, relY, buttons));
}
}
}
@@ -1605,7 +1749,7 @@ static void dispatchEvents(AppContextT *ctx) {
int32_t relX = mx - win->x - win->contentX;
int32_t relY = my - win->y - win->contentY;
- win->onMouse(win, relX, relY, buttons);
+ WIN_CALLBACK(ctx, win, win->onMouse(win, relX, relY, buttons));
}
}
@@ -1673,7 +1817,7 @@ static void dispatchEvents(AppContextT *ctx) {
focus = wgtGetFocused();
if (focus && focus->onScroll) {
- focus->onScroll(focus, ctx->mouseWheel * ctx->wheelDirection);
+ WIN_CALLBACK(ctx, win, focus->onScroll(focus, ctx->mouseWheel * ctx->wheelDirection));
}
}
@@ -1699,7 +1843,7 @@ static void dispatchEvents(AppContextT *ctx) {
sb->orient == ScrollbarVerticalE ? sb->length : SCROLLBAR_WIDTH);
if (win->onScroll) {
- win->onScroll(win, sb->orient, sb->value);
+ WIN_CALLBACK(ctx, win, win->onScroll(win, sb->orient, sb->value));
}
}
}
@@ -1840,20 +1984,21 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c
int32_t cx = px + POPUP_BEVEL_WIDTH + MENU_CHECK_WIDTH / 2;
if (item->type == MenuItemCheckE) {
- // Checkmark: small tick shape
- drawVLine(d, ops, cx - 2, cy - 1, 2, fg);
- drawVLine(d, ops, cx - 1, cy, 2, fg);
- drawVLine(d, ops, cx, cy + 1, 2, fg);
- drawVLine(d, ops, cx + 1, cy, 2, fg);
- drawVLine(d, ops, cx + 2, cy - 1, 2, fg);
- drawVLine(d, ops, cx + 3, cy - 2, 2, fg);
+ // Checkmark: small tick shape, one short vertical stroke
+ // per column; the y offsets trace the tick outline.
+ static const int32_t tickDx[] = {-2, -1, 0, 1, 2, 3};
+ static const int32_t tickDy[] = {-1, 0, 1, 0, -1, -2};
+
+ for (size_t k = 0; k < sizeof(tickDx) / sizeof(tickDx[0]); k++) {
+ drawVLine(d, ops, cx + tickDx[k], cy + tickDy[k], CHECK_STROKE_H, fg);
+ }
} else if (item->type == MenuItemRadioE) {
- // Filled diamond bullet (5x5)
- drawHLine(d, ops, cx, cy - 2, 1, fg);
- drawHLine(d, ops, cx - 1, cy - 1, 3, fg);
- drawHLine(d, ops, cx - 2, cy, 5, fg);
- drawHLine(d, ops, cx - 1, cy + 1, 3, fg);
- drawHLine(d, ops, cx, cy + 2, 1, fg);
+ // Filled diamond bullet
+ for (int32_t row = -RADIO_DIAMOND_HALF; row <= RADIO_DIAMOND_HALF; row++) {
+ int32_t half = RADIO_DIAMOND_HALF - (row < 0 ? -row : row);
+
+ drawHLine(d, ops, cx - half, cy + row, half * 2 + 1, fg);
+ }
}
}
@@ -1991,6 +2136,43 @@ static WindowT *findWindowById(AppContextT *ctx, int32_t id) {
}
+// ============================================================
+// finishLoadImage -- shared tail of dvxLoadImage / dvxLoadImageFromMemory
+// ============================================================
+//
+// Takes ownership of the stb_image RGB buffer (NULL if decoding failed)
+// and returns the display-format copy, filling the optional outputs.
+
+static uint8_t *finishLoadImage(const DisplayT *d, uint8_t *rgb, int32_t imgW, int32_t imgH, int32_t *outW, int32_t *outH, int32_t *outPitch) {
+ if (!rgb) {
+ return NULL;
+ }
+
+ int32_t pitch;
+ uint8_t *buf = convertRgbToNative(d, rgb, RGB_CHANNELS, imgW, imgH, 0, NULL, &pitch);
+
+ stbi_image_free(rgb);
+
+ if (!buf) {
+ return NULL;
+ }
+
+ if (outW) {
+ *outW = imgW;
+ }
+
+ if (outH) {
+ *outH = imgH;
+ }
+
+ if (outPitch) {
+ *outPitch = pitch;
+ }
+
+ return buf;
+}
+
+
// ============================================================
// flushPendingDestroys
// ============================================================
@@ -2028,7 +2210,7 @@ static void flushPendingDestroys(AppContextT *ctx) {
// in one place.
static void focusTopmostAlive(AppContextT *ctx) {
for (int32_t i = ctx->stack.count - 1; i >= 0; i--) {
- if (!ctx->stack.windows[i]->destroyPending) {
+ if (!ctx->stack.windows[i]->destroyPending && ctx->stack.windows[i]->visible) {
wmSetFocus(&ctx->stack, &ctx->dirty, i);
break;
}
@@ -2120,19 +2302,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t
sFocusedWidget = NULL;
wgtInvalidatePaint(prev);
-
- // Snapshot the destroy generation: the blur commit below may fire
- // a user Change handler that destroys the widget -- prev is
- // dangling then and must not be dereferenced.
- uint32_t gen = sWidgetGen;
-
- // Commit/clamp any in-progress edit before the app onBlur, so a
- // menu command that follows this click reads the committed value.
- wclsOnBlur(prev);
-
- if (sWidgetGen == gen && prev->onBlur) {
- prev->onBlur(prev);
- }
+ widgetFireFocusChange(prev, NULL);
}
switch (hitPart) {
@@ -2140,7 +2310,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t
if (win->onMouse) {
int32_t relX = mx - win->x - win->contentX;
int32_t relY = my - win->y - win->contentY;
- win->onMouse(win, relX, relY, buttons);
+ WIN_CALLBACK(ctx, win, win->onMouse(win, relX, relY, buttons));
}
break;
@@ -2225,11 +2395,11 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t
break;
case HIT_VSCROLL:
- wmScrollbarClick(&ctx->stack, &ctx->dirty, hitIdx, SCROLL_VERTICAL, mx, my);
+ wmScrollbarClick(&ctx->stack, &ctx->dirty, hitIdx, ScrollbarVerticalE, mx, my);
break;
case HIT_HSCROLL:
- wmScrollbarClick(&ctx->stack, &ctx->dirty, hitIdx, SCROLL_HORIZONTAL, mx, my);
+ wmScrollbarClick(&ctx->stack, &ctx->dirty, hitIdx, ScrollbarHorizontalE, mx, my);
break;
case HIT_MINIMIZE:
@@ -2328,6 +2498,34 @@ static void invalidateAllWindows(AppContextT *ctx) {
}
+// ============================================================
+// nextMenuItem -- step a menu hover index past separators
+// ============================================================
+//
+// firstSeparator points at the separator flag of item 0; stride is the
+// item size, so this serves both SysMenuItemT and MenuItemT arrays.
+// Steps idx by dir (+1/-1) with wraparound, skipping separators; gives
+// up after count steps so an all-separator menu cannot loop forever.
+
+static int32_t nextMenuItem(const bool *firstSeparator, size_t stride, int32_t count, int32_t idx, int32_t dir) {
+ for (int32_t tries = 0; tries < count; tries++) {
+ idx += dir;
+
+ if (idx < 0) {
+ idx = count - 1;
+ } else if (idx >= count) {
+ idx = 0;
+ }
+
+ if (!*(const bool *)((const uint8_t *)firstSeparator + (size_t)idx * stride)) {
+ break;
+ }
+ }
+
+ return idx;
+}
+
+
// ============================================================
// openContextMenu -- open a context menu at a screen position
// ============================================================
@@ -2613,47 +2811,22 @@ static void pollKeyboard(AppContextT *ctx) {
int32_t scancode = evt.scancode;
int32_t ascii = evt.ascii;
- // Alt+Tab / Shift+Alt+Tab -- cycle windows.
- // Unlike Windows, there's no task-switcher overlay here -- each press
- // immediately rotates the window stack and focuses the new top.
- // Alt+Tab rotates the top window to the bottom of the stack (so the
- // second window becomes visible). Shift+Alt+Tab does the reverse,
- // pulling the bottom window to the top.
- if (ascii == 0 && scancode == 0xA5) {
- if (ctx->stack.count > 1) {
- if (shiftHeld) {
- wmRaiseWindow(&ctx->stack, &ctx->dirty, 0);
- wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1);
- } else {
- // Rotate: move top to bottom, shift everything else up
- WindowT *top = ctx->stack.windows[ctx->stack.count - 1];
- dirtyListAdd(&ctx->dirty, top->x, top->y, top->w, top->h);
-
- // Shift all windows up
- for (int32_t i = ctx->stack.count - 1; i > 0; i--) {
- ctx->stack.windows[i] = ctx->stack.windows[i - 1];
- }
-
- ctx->stack.windows[0] = top;
- top->focused = false;
-
- // Focus the new top window
- wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1);
- dirtyListAdd(&ctx->dirty, ctx->stack.windows[ctx->stack.count - 1]->x,
- ctx->stack.windows[ctx->stack.count - 1]->y,
- ctx->stack.windows[ctx->stack.count - 1]->w,
- ctx->stack.windows[ctx->stack.count - 1]->h);
- }
- }
-
+ // Alt+Tab / Shift+Alt+Tab -- cycle windows (see cycleWindowFocus)
+ if (ascii == 0 && scancode == KEY_SCAN_ALT_TAB) {
+ cycleWindowFocus(ctx, shiftHeld);
continue;
}
- // Alt+F4 -- close focused window
- if (ascii == 0 && scancode == 0x6B) {
+ // Alt+F4 -- close focused window. A modal dialog captures all
+ // input, so only the modal window itself may be closed this way.
+ if (ascii == 0 && scancode == KEY_SCAN_ALT_F4) {
if (ctx->stack.focusedIdx >= 0) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
+ if (ctx->modalWindow && win != ctx->modalWindow) {
+ continue;
+ }
+
if (win->onClose) {
WIN_CALLBACK(ctx, win, win->onClose(win));
} else {
@@ -2667,7 +2840,7 @@ static void pollKeyboard(AppContextT *ctx) {
// Ctrl+F12 -- save full screen screenshot
// Ctrl+Shift+F12 -- save focused window screenshot
// BIOS returns scancode 0x58 for F12; Ctrl+F12 = scancode 0x8A.
- if (ascii == 0 && scancode == 0x8A && (shiftFlags & KEY_MOD_CTRL)) {
+ if (ascii == 0 && scancode == KEY_SCAN_CTRL_F12 && (shiftFlags & KEY_MOD_CTRL)) {
if (shiftHeld && ctx->stack.focusedIdx >= 0) {
interactiveWindowScreenshot(ctx, ctx->stack.windows[ctx->stack.focusedIdx]);
} else {
@@ -2678,7 +2851,7 @@ static void pollKeyboard(AppContextT *ctx) {
}
// Ctrl+Esc -- system-wide hotkey (e.g. task manager)
- if (scancode == 0x01 && ascii == KEY_ESCAPE && (shiftFlags & KEY_MOD_CTRL)) {
+ if (scancode == KEY_SCAN_ESC && ascii == KEY_ESCAPE && (shiftFlags & KEY_MOD_CTRL)) {
if (ctx->onCtrlEsc) {
ctx->onCtrlEsc(ctx->ctrlEscCtx);
}
@@ -2687,7 +2860,7 @@ static void pollKeyboard(AppContextT *ctx) {
}
// F1 -- system-wide help
- if (ascii == 0 && scancode == 0x3B && !(shiftFlags & (KEY_MOD_CTRL | KEY_MOD_ALT | KEY_MOD_SHIFT))) {
+ if (ascii == 0 && scancode == KEY_SCAN_F1 && !(shiftFlags & (KEY_MOD_CTRL | KEY_MOD_ALT | KEY_MOD_SHIFT))) {
if (ctx->onF1) {
ctx->onF1(ctx->f1Ctx);
}
@@ -2696,7 +2869,7 @@ static void pollKeyboard(AppContextT *ctx) {
}
// F10 -- activate menu bar
- if (ascii == 0 && scancode == 0x44) {
+ if (ascii == 0 && scancode == KEY_SCAN_F10) {
if (ctx->stack.focusedIdx >= 0) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
@@ -2738,7 +2911,7 @@ static void pollKeyboard(AppContextT *ctx) {
continue;
}
- if (ascii == 0x0D) {
+ if (ascii == KEY_ENTER) {
// Confirm
ctx->kbMoveResize.mode = KbModeNoneE;
continue;
@@ -2748,13 +2921,13 @@ static void pollKeyboard(AppContextT *ctx) {
int32_t oldX = kbWin->x;
int32_t oldY = kbWin->y;
- if (ascii == 0 && scancode == 0x48) {
+ if (ascii == 0 && scancode == KEY_SCAN_UP) {
kbWin->y -= KB_MOVE_STEP;
- } else if (ascii == 0 && scancode == 0x50) {
+ } else if (ascii == 0 && scancode == KEY_SCAN_DOWN) {
kbWin->y += KB_MOVE_STEP;
- } else if (ascii == 0 && scancode == 0x4B) {
+ } else if (ascii == 0 && scancode == KEY_SCAN_LEFT) {
kbWin->x -= KB_MOVE_STEP;
- } else if (ascii == 0 && scancode == 0x4D) {
+ } else if (ascii == 0 && scancode == KEY_SCAN_RIGHT) {
kbWin->x += KB_MOVE_STEP;
}
@@ -2785,13 +2958,13 @@ static void pollKeyboard(AppContextT *ctx) {
int32_t newW = kbWin->w;
int32_t newH = kbWin->h;
- if (ascii == 0 && scancode == 0x4D) {
+ if (ascii == 0 && scancode == KEY_SCAN_RIGHT) {
newW += KB_MOVE_STEP;
- } else if (ascii == 0 && scancode == 0x4B) {
+ } else if (ascii == 0 && scancode == KEY_SCAN_LEFT) {
newW -= KB_MOVE_STEP;
- } else if (ascii == 0 && scancode == 0x50) {
+ } else if (ascii == 0 && scancode == KEY_SCAN_DOWN) {
newH += KB_MOVE_STEP;
- } else if (ascii == 0 && scancode == 0x48) {
+ } else if (ascii == 0 && scancode == KEY_SCAN_UP) {
newH -= KB_MOVE_STEP;
}
@@ -2849,9 +3022,9 @@ static void pollKeyboard(AppContextT *ctx) {
}
// Alt+Space -- open/close system menu
- // Enhanced INT 16h: Alt+Space returns scancode 0x39, ascii 0x20
- // Must check Alt modifier (bit 3) to distinguish from plain Space
- if (scancode == 0x39 && ascii == 0x20 && (shiftFlags & KEY_MOD_ALT)) {
+ // Enhanced INT 16h: Alt+Space returns the plain Space scancode/ascii
+ // pair, so the Alt modifier flag is what distinguishes it.
+ if (scancode == KEY_SCAN_SPACE && ascii == KEY_SPACE && (shiftFlags & KEY_MOD_ALT)) {
if (ctx->sysMenu.active) {
closeSysMenu(ctx);
} else if (ctx->stack.focusedIdx >= 0) {
@@ -2870,45 +3043,11 @@ static void pollKeyboard(AppContextT *ctx) {
} else if (ascii == KEY_ESCAPE) {
closeSysMenu(ctx);
continue;
- } else if (ascii == 0 && scancode == 0x48) {
- // Up arrow
- int32_t idx = ctx->sysMenu.hoverItem;
-
- for (int32_t tries = 0; tries < ctx->sysMenu.itemCount; tries++) {
- idx--;
-
- if (idx < 0) {
- idx = ctx->sysMenu.itemCount - 1;
- }
-
- if (!ctx->sysMenu.items[idx].separator) {
- break;
- }
- }
-
- ctx->sysMenu.hoverItem = idx;
+ } else if (ascii == 0 && (scancode == KEY_SCAN_UP || scancode == KEY_SCAN_DOWN)) {
+ ctx->sysMenu.hoverItem = nextMenuItem(&ctx->sysMenu.items[0].separator, sizeof(ctx->sysMenu.items[0]), ctx->sysMenu.itemCount, ctx->sysMenu.hoverItem, scancode == KEY_SCAN_UP ? -1 : 1);
dirtyListAdd(&ctx->dirty, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH);
continue;
- } else if (ascii == 0 && scancode == 0x50) {
- // Down arrow
- int32_t idx = ctx->sysMenu.hoverItem;
-
- for (int32_t tries = 0; tries < ctx->sysMenu.itemCount; tries++) {
- idx++;
-
- if (idx >= ctx->sysMenu.itemCount) {
- idx = 0;
- }
-
- if (!ctx->sysMenu.items[idx].separator) {
- break;
- }
- }
-
- ctx->sysMenu.hoverItem = idx;
- dirtyListAdd(&ctx->dirty, ctx->sysMenu.popupX, ctx->sysMenu.popupY, ctx->sysMenu.popupW, ctx->sysMenu.popupH);
- continue;
- } else if (ascii == 0x0D) {
+ } else if (ascii == KEY_ENTER) {
// Enter -- execute selected item
if (ctx->sysMenu.hoverItem >= 0 && ctx->sysMenu.hoverItem < ctx->sysMenu.itemCount) {
SysMenuItemT *item = &ctx->sysMenu.items[ctx->sysMenu.hoverItem];
@@ -2964,48 +3103,10 @@ static void pollKeyboard(AppContextT *ctx) {
if (ctx->popup.active && ascii == 0) {
MenuT *curMenu = ctx->popup.menu;
- // Up arrow
- if (scancode == 0x48) {
+ // Up / Down arrow
+ if (scancode == KEY_SCAN_UP || scancode == KEY_SCAN_DOWN) {
if (curMenu && curMenu->itemCount > 0) {
- int32_t idx = ctx->popup.hoverItem;
-
- for (int32_t tries = 0; tries < curMenu->itemCount; tries++) {
- idx--;
-
- if (idx < 0) {
- idx = curMenu->itemCount - 1;
- }
-
- if (!curMenu->items[idx].separator) {
- break;
- }
- }
-
- ctx->popup.hoverItem = idx;
- dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
- }
-
- continue;
- }
-
- // Down arrow
- if (scancode == 0x50) {
- if (curMenu && curMenu->itemCount > 0) {
- int32_t idx = ctx->popup.hoverItem;
-
- for (int32_t tries = 0; tries < curMenu->itemCount; tries++) {
- idx++;
-
- if (idx >= curMenu->itemCount) {
- idx = 0;
- }
-
- if (!curMenu->items[idx].separator) {
- break;
- }
- }
-
- ctx->popup.hoverItem = idx;
+ ctx->popup.hoverItem = nextMenuItem(&curMenu->items[0].separator, sizeof(curMenu->items[0]), curMenu->itemCount, ctx->popup.hoverItem, scancode == KEY_SCAN_UP ? -1 : 1);
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
}
@@ -3013,7 +3114,7 @@ static void pollKeyboard(AppContextT *ctx) {
}
// Left arrow -- close submenu, or switch to previous top-level menu
- if (scancode == 0x4B) {
+ if (scancode == KEY_SCAN_LEFT) {
if (ctx->popup.depth > 0) {
closePopupLevel(ctx);
} else {
@@ -3034,7 +3135,7 @@ static void pollKeyboard(AppContextT *ctx) {
}
// Right arrow -- open submenu, or switch to next top-level menu
- if (scancode == 0x4D) {
+ if (scancode == KEY_SCAN_RIGHT) {
// If hovered item has a submenu, open it
if (curMenu && ctx->popup.hoverItem >= 0 && ctx->popup.hoverItem < curMenu->itemCount) {
MenuItemT *hItem = &curMenu->items[ctx->popup.hoverItem];
@@ -3063,7 +3164,7 @@ static void pollKeyboard(AppContextT *ctx) {
}
// Enter executes highlighted popup menu item (or opens submenu)
- if (ctx->popup.active && ascii == 0x0D) {
+ if (ctx->popup.active && ascii == KEY_ENTER) {
MenuT *curMenu = ctx->popup.menu;
if (curMenu && ctx->popup.hoverItem >= 0 && ctx->popup.hoverItem < curMenu->itemCount) {
@@ -3072,13 +3173,7 @@ static void pollKeyboard(AppContextT *ctx) {
if (item->subMenu && item->enabled) {
openSubMenu(ctx);
} else if (item->enabled && !item->separator) {
- int32_t menuId = item->id;
- WindowT *win = findWindowById(ctx, ctx->popup.windowId);
- closeAllPopups(ctx);
-
- if (win && win->onMenu) {
- WIN_CALLBACK(ctx, win, win->onMenu(win, menuId));
- }
+ activatePopupItem(ctx, ctx->popup.hoverItem);
}
} else {
closeAllPopups(ctx);
@@ -3089,7 +3184,7 @@ static void pollKeyboard(AppContextT *ctx) {
// Check for plain key accelerator in open popup menu
if (ctx->popup.active && ascii != 0) {
- char lc = (ascii >= 'A' && ascii <= 'Z') ? (char)(ascii + 32) : (char)ascii;
+ char lc = (char)tolower(ascii);
MenuT *curMenu = ctx->popup.menu;
// Try matching an item in the current popup
@@ -3103,13 +3198,7 @@ static void pollKeyboard(AppContextT *ctx) {
dirtyListAdd(&ctx->dirty, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH);
openSubMenu(ctx);
} else {
- int32_t menuId = item->id;
- WindowT *win = findWindowById(ctx, ctx->popup.windowId);
- closeAllPopups(ctx);
-
- if (win && win->onMenu) {
- WIN_CALLBACK(ctx, win, win->onMenu(win, menuId));
- }
+ activatePopupItem(ctx, k);
}
goto nextKey;
@@ -3145,7 +3234,7 @@ static void pollKeyboard(AppContextT *ctx) {
// Check accelerator table on focused window
if (ctx->stack.focusedIdx >= 0) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
- int32_t key = ascii ? ascii : (scancode | 0x100);
+ int32_t key = ascii ? ascii : (scancode | KEY_EXT_FLAG);
if (checkAccelTable(ctx, win, key, shiftFlags)) {
continue;
@@ -3153,9 +3242,8 @@ static void pollKeyboard(AppContextT *ctx) {
}
// Tab / Shift-Tab -- cycle focus between widgets
- // Tab: scancode=0x0F, ascii=0x09
- // Shift-Tab: scancode=0x0F, ascii=0x00
- if (scancode == 0x0F && (ascii == 0x09 || ascii == 0)) {
+ // Tab arrives with the Tab ascii code, Shift-Tab with ascii 0.
+ if (scancode == KEY_SCAN_TAB && (ascii == KEY_TAB || ascii == 0)) {
if (ctx->stack.focusedIdx >= 0) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
@@ -3186,7 +3274,7 @@ static void pollKeyboard(AppContextT *ctx) {
if (current && (current->swallowTab ||
(current->wclass && (current->wclass->flags & WCLASS_SWALLOWS_TAB)))) {
if (win->onKey) {
- WIN_CALLBACK(ctx, win, win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags));
+ WIN_CALLBACK(ctx, win, win->onKey(win, ascii ? ascii : (scancode | KEY_EXT_FLAG), shiftFlags));
}
arrfree(fstack);
@@ -3195,7 +3283,7 @@ static void pollKeyboard(AppContextT *ctx) {
WidgetT *next;
- if (ascii == 0x09) {
+ if (ascii == KEY_TAB) {
next = widgetFindNextFocusable(win->widgetRoot, current);
} else {
next = widgetFindPrevFocusable(win->widgetRoot, current);
@@ -3205,47 +3293,11 @@ static void pollKeyboard(AppContextT *ctx) {
// Close any open dropdown popup so its d->open flag
// is not left stale when focus moves off it.
closeOpenPopup(ctx);
- WidgetT *prev = sFocusedWidget;
- // Switch focus BEFORE invalidating so paint sees
- // the correct focused state for both widgets.
- sFocusedWidget = next;
-
- // Snapshot the destroy generation: the blur commit
- // and app callbacks below may destroy widgets (a
- // BASIC Change handler unloading its form) -- prev
- // and next are dangling then and must not be
- // dereferenced.
- uint32_t gen = sWidgetGen;
-
- if (prev) {
- // Focus can wrap to the same widget in a
- // window with one focusable -- don't self-clear.
- if (prev != next) {
- wclsClearSelection(prev);
- // Commit/clamp any in-progress edit before
- // the app onBlur.
- wclsOnBlur(prev);
- }
-
- if (sWidgetGen == gen) {
- wgtInvalidatePaint(prev);
-
- if (prev->onBlur) {
- prev->onBlur(prev);
- }
- }
- }
-
- if (sWidgetGen == gen) {
- wgtInvalidatePaint(next);
-
- if (next->onFocus) {
- next->onFocus(next);
- }
- }
-
- if (sWidgetGen == gen) {
+ // A blur/focus callback may destroy widgets (a
+ // BASIC Change handler unloading its form) -- next
+ // is dangling then and must not be dereferenced.
+ if (widgetTransferFocus(next)) {
// Scroll the widget into view if needed
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
@@ -3296,7 +3348,7 @@ static void pollKeyboard(AppContextT *ctx) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
if (win->onKey) {
- WIN_CALLBACK(ctx, win, win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags));
+ WIN_CALLBACK(ctx, win, win->onKey(win, ascii ? ascii : (scancode | KEY_EXT_FLAG), shiftFlags));
}
}
@@ -3313,7 +3365,7 @@ nextKey:;
int32_t mod = platformKeyboardGetModifiers();
if (win->onKeyUp) {
- win->onKeyUp(win, upEvt.scancode | 0x100, mod);
+ WIN_CALLBACK(ctx, win, win->onKeyUp(win, upEvt.scancode | KEY_EXT_FLAG, mod));
}
}
}
@@ -3661,22 +3713,7 @@ static void updateCursorShape(AppContextT *ctx) {
// During active resize, keep the resize cursor
if (ctx->stack.resizeWindow >= 0) {
- int32_t edge = ctx->stack.resizeEdge;
- bool horiz = (edge & (RESIZE_LEFT | RESIZE_RIGHT)) != 0;
- bool vert = (edge & (RESIZE_TOP | RESIZE_BOTTOM)) != 0;
-
- if (horiz && vert) {
- if ((edge & RESIZE_LEFT && edge & RESIZE_TOP) ||
- (edge & RESIZE_RIGHT && edge & RESIZE_BOTTOM)) {
- newCursor = CURSOR_RESIZE_DIAG_NWSE;
- } else {
- newCursor = CURSOR_RESIZE_DIAG_NESW;
- }
- } else if (horiz) {
- newCursor = CURSOR_RESIZE_H;
- } else {
- newCursor = CURSOR_RESIZE_V;
- }
+ newCursor = cursorForResizeEdge(ctx->stack.resizeEdge);
}
// Active widget drag -- query cursor shape from the dragged widget
else if (sDragWidget) {
@@ -3693,23 +3730,9 @@ static void updateCursorShape(AppContextT *ctx) {
if (hitIdx >= 0 && hitPart == HIT_RESIZE) {
// Hovering over a resize edge
- WindowT *win = ctx->stack.windows[hitIdx];
- int32_t edge = wmResizeEdgeHit(win, mx, my);
- bool horiz = (edge & (RESIZE_LEFT | RESIZE_RIGHT)) != 0;
- bool vert = (edge & (RESIZE_TOP | RESIZE_BOTTOM)) != 0;
+ WindowT *win = ctx->stack.windows[hitIdx];
- if (horiz && vert) {
- if ((edge & RESIZE_LEFT && edge & RESIZE_TOP) ||
- (edge & RESIZE_RIGHT && edge & RESIZE_BOTTOM)) {
- newCursor = CURSOR_RESIZE_DIAG_NWSE;
- } else {
- newCursor = CURSOR_RESIZE_DIAG_NESW;
- }
- } else if (horiz) {
- newCursor = CURSOR_RESIZE_H;
- } else if (vert) {
- newCursor = CURSOR_RESIZE_V;
- }
+ newCursor = cursorForResizeEdge(wmResizeEdgeHit(win, mx, my));
} else if (hitIdx >= 0 && hitPart == HIT_CONTENT) {
WindowT *win = ctx->stack.windows[hitIdx];
@@ -3717,7 +3740,8 @@ static void updateCursorShape(AppContextT *ctx) {
if (win->onCursorQuery) {
int32_t cx = mx - win->x - win->contentX;
int32_t cy = my - win->y - win->contentY;
- int32_t shape = win->onCursorQuery(win, cx, cy);
+ int32_t shape;
+ WIN_CALLBACK(ctx, win, shape = win->onCursorQuery(win, cx, cy));
if (shape > 0) {
newCursor = shape;
@@ -3886,8 +3910,8 @@ static void updateTooltip(AppContextT *ctx) {
int32_t th = ctx->font.charHeight + TOOLTIP_PAD * 2;
// Position below and right of cursor
- ctx->tooltipX = mx + 12;
- ctx->tooltipY = my + 16;
+ ctx->tooltipX = mx + TOOLTIP_OFFSET_X;
+ ctx->tooltipY = my + TOOLTIP_OFFSET_Y;
// Keep on screen
if (ctx->tooltipX + tw > ctx->display.width) {
@@ -3895,7 +3919,7 @@ static void updateTooltip(AppContextT *ctx) {
}
if (ctx->tooltipY + th > ctx->display.height) {
- ctx->tooltipY = my - th - 4;
+ ctx->tooltipY = my - th - TOOLTIP_ABOVE_GAP;
}
ctx->tooltipW = tw;
@@ -4539,6 +4563,22 @@ void dvxHideWindow(AppContextT *ctx, WindowT *win) {
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
win->visible = false;
+
+ // Keyboard input follows focusedIdx unconditionally, so a hidden
+ // window must not keep it. Move focus to the topmost visible window;
+ // if there is none, drop focus entirely (blurring this window).
+ if (ctx->stack.focusedIdx >= 0 && ctx->stack.windows[ctx->stack.focusedIdx] == win) {
+ focusTopmostAlive(ctx);
+
+ if (ctx->stack.windows[ctx->stack.focusedIdx] == win) {
+ ctx->stack.focusedIdx = -1;
+ win->focused = false;
+
+ if (win->onBlur) {
+ WIN_CALLBACK(ctx, win, win->onBlur(win));
+ }
+ }
+ }
}
@@ -4599,6 +4639,12 @@ void dvxInvalidateTooltip(AppContextT *ctx, const char *text) {
// ============================================================
void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) {
+ // A window queued for deferred destruction is inert: its app state
+ // (widget userData etc.) may already be torn down, so never paint it.
+ if (win->destroyPending) {
+ return;
+ }
+
// Call the window's paint callback to update the content buffer
// before marking the screen dirty. This means raw-paint apps only
// need to call dvxInvalidateWindow -- onPaint fires automatically.
@@ -4645,57 +4691,12 @@ uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, i
return NULL;
}
- const DisplayT *d = &ctx->display;
-
int imgW;
int imgH;
int channels;
- uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3);
+ uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, RGB_CHANNELS);
- if (!rgb) {
- return NULL;
- }
-
- int32_t bpp = d->format.bytesPerPixel;
- int32_t pitch = imgW * bpp;
- uint8_t *buf = (uint8_t *)malloc((size_t)pitch * imgH);
-
- if (!buf) {
- stbi_image_free(rgb);
- return NULL;
- }
-
- for (int32_t y = 0; y < imgH; y++) {
- for (int32_t x = 0; x < imgW; x++) {
- const uint8_t *src = rgb + (y * imgW + x) * RGB_CHANNELS;
- uint32_t color = packColor(d, src[0], src[1], src[2]);
- uint8_t *dst = buf + y * pitch + x * bpp;
-
- if (bpp == 1) {
- *dst = (uint8_t)color;
- } else if (bpp == 2) {
- *(uint16_t *)dst = (uint16_t)color;
- } else {
- *(uint32_t *)dst = color;
- }
- }
- }
-
- stbi_image_free(rgb);
-
- if (outW) {
- *outW = imgW;
- }
-
- if (outH) {
- *outH = imgH;
- }
-
- if (outPitch) {
- *outPitch = pitch;
- }
-
- return buf;
+ return finishLoadImage(&ctx->display, rgb, imgW, imgH, outW, outH, outPitch);
}
@@ -4709,102 +4710,22 @@ uint8_t *dvxLoadImageAlpha(const AppContextT *ctx, const uint8_t *data, int32_t
int imgW;
int imgH;
int channels;
- uint8_t *rgba = stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, 4);
+ uint8_t *rgba = stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGBA_CHANNELS);
if (!rgba) {
return NULL;
}
- int32_t bpp = d->format.bytesPerPixel;
- int32_t pitch = imgW * bpp;
- uint32_t keyColor = packColor(d, 255, 0, 255); // magenta
- bool hasAlpha = false;
- uint8_t *buf = (uint8_t *)malloc((size_t)pitch * imgH);
-
- if (!buf) {
- stbi_image_free(rgba);
- return NULL;
- }
-
- for (int32_t y = 0; y < imgH; y++) {
- for (int32_t x = 0; x < imgW; x++) {
- const uint8_t *src = rgba + (y * imgW + x) * 4;
- uint32_t color;
-
- if (src[3] < 128) {
- color = keyColor;
- hasAlpha = true;
- } else {
- color = packColor(d, src[0], src[1], src[2]);
- }
-
- uint8_t *dst = buf + y * pitch + x * bpp;
-
- if (bpp == 1) {
- *dst = (uint8_t)color;
- } else if (bpp == 2) {
- *(uint16_t *)dst = (uint16_t)color;
- } else {
- *(uint32_t *)dst = color;
- }
- }
- }
+ uint32_t keyColor = packColor(d, KEY_COLOR_R, KEY_COLOR_G, KEY_COLOR_B);
+ int32_t pitch;
+ uint8_t *buf = convertRgbToNative(d, rgba, RGBA_CHANNELS, imgW, imgH, keyColor, outHasAlpha, &pitch);
stbi_image_free(rgba);
- if (outW) { *outW = imgW; }
- if (outH) { *outH = imgH; }
- if (outPitch) { *outPitch = pitch; }
- if (outHasAlpha) { *outHasAlpha = hasAlpha; }
- if (outKeyColor) { *outKeyColor = keyColor; }
-
- return buf;
-}
-
-
-uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch) {
- if (!ctx || !data || dataLen <= 0) {
- return NULL;
- }
-
- const DisplayT *d = &ctx->display;
-
- int imgW;
- int imgH;
- int channels;
- uint8_t *rgb = stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, 3);
-
- if (!rgb) {
- return NULL;
- }
-
- int32_t bpp = d->format.bytesPerPixel;
- int32_t pitch = imgW * bpp;
- uint8_t *buf = (uint8_t *)malloc((size_t)pitch * imgH);
-
if (!buf) {
- stbi_image_free(rgb);
return NULL;
}
- for (int32_t y = 0; y < imgH; y++) {
- for (int32_t x = 0; x < imgW; x++) {
- const uint8_t *src = rgb + (y * imgW + x) * 3;
- uint32_t color = packColor(d, src[0], src[1], src[2]);
- uint8_t *dst = buf + y * pitch + x * bpp;
-
- if (bpp == 1) {
- *dst = (uint8_t)color;
- } else if (bpp == 2) {
- *(uint16_t *)dst = (uint16_t)color;
- } else {
- *(uint32_t *)dst = color;
- }
- }
- }
-
- stbi_image_free(rgb);
-
if (outW) {
*outW = imgW;
}
@@ -4817,10 +4738,28 @@ uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int
*outPitch = pitch;
}
+ if (outKeyColor) {
+ *outKeyColor = keyColor;
+ }
+
return buf;
}
+uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch) {
+ if (!ctx || !data || dataLen <= 0) {
+ return NULL;
+ }
+
+ int imgW;
+ int imgH;
+ int channels;
+ uint8_t *rgb = stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGB_CHANNELS);
+
+ return finishLoadImage(&ctx->display, rgb, imgW, imgH, outW, outH, outPitch);
+}
+
+
bool dvxLoadTheme(AppContextT *ctx, const char *filename) {
FILE *fp = fopen(filename, "rb");
@@ -5254,6 +5193,16 @@ void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) {
// ============================================================
bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
+ // buildWallpaperBuf pumps dvxUpdate to keep the UI alive, and a timer
+ // or idle callback fired from there may call back in here. A nested
+ // call would free the buffer being built and clear busy early, so
+ // reject it outright.
+ static bool sBuilding = false;
+
+ if (sBuilding) {
+ return false;
+ }
+
free(ctx->wallpaperBuf);
ctx->wallpaperBuf = NULL;
ctx->wallpaperPitch = 0;
@@ -5281,8 +5230,10 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
int32_t pitch = ctx->display.width * ctx->display.format.bytesPerPixel;
- ctx->wallpaperBuf = buildWallpaperBuf(ctx, rgb, imgW, imgH, ctx->wallpaperMode);
+ sBuilding = true;
+ ctx->wallpaperBuf = buildWallpaperBuf(ctx, rgb, imgW, imgH, ctx->wallpaperMode);
ctx->wallpaperPitch = pitch;
+ sBuilding = false;
dvxSetBusy(ctx, false);
stbi_image_free(rgb);
@@ -5563,7 +5514,7 @@ bool dvxUpdate(AppContextT *ctx) {
// Flush deferred paints. paintNeeded is set by wgtInvalidatePaint
// (PARTIAL) or wgtInvalidate (FULL). Multiple calls per frame are
- // batched into one paint — the highest level wins. This loop runs
+ // batched into one paint -- the highest level wins. This loop runs
// INSIDE the depth-tracked section so an onPaint that destroys its
// own window defers: the paintNeeded/icon writes below then land on
// live memory and the stack is never compacted mid-iteration.
@@ -5608,12 +5559,6 @@ bool dvxUpdate(AppContextT *ctx) {
platformYield();
}
- // Defense in depth: never fire the deferred click into a window that
- // was queued for destruction this frame (its app state may be freed).
- if (sKeyPressedBtn && sKeyPressedBtn->window && sKeyPressedBtn->window->destroyPending) {
- sKeyPressedBtn = NULL;
- }
-
// Release key-pressed button after one frame. The button was set to
// "pressed" state in dispatchAccelKey; here we clear it and fire
// onClick. The one-frame delay ensures the pressed visual state
diff --git a/src/libs/kpunch/libdvx/dvxComp.c b/src/libs/kpunch/libdvx/dvxComp.c
index 0ceed0c..2c1e9b9 100644
--- a/src/libs/kpunch/libdvx/dvxComp.c
+++ b/src/libs/kpunch/libdvx/dvxComp.c
@@ -222,11 +222,6 @@ bool rectIntersect(const RectT *a, const RectT *b, RectT *result) {
}
-bool rectIsEmpty(const RectT *r) {
- return (r->w <= 0 || r->h <= 0);
-}
-
-
// Separating-axis test with a gap tolerance. Two rects merge if they
// overlap OR if the gap between them is <= DIRTY_MERGE_GAP pixels.
// The gap tolerance is the key tuning parameter for the merge algorithm:
@@ -237,10 +232,22 @@ bool rectIsEmpty(const RectT *r) {
// the inner loop of dirtyListMerge.
static inline bool rectsOverlapOrAdjacent(const RectT *a, const RectT *b, int32_t gap) {
- if (a->x + a->w + gap < b->x) { return false; }
- if (b->x + b->w + gap < a->x) { return false; }
- if (a->y + a->h + gap < b->y) { return false; }
- if (b->y + b->h + gap < a->y) { return false; }
+ if (a->x + a->w + gap < b->x) {
+ return false;
+ }
+
+ if (b->x + b->w + gap < a->x) {
+ return false;
+ }
+
+ if (a->y + a->h + gap < b->y) {
+ return false;
+ }
+
+ if (b->y + b->h + gap < a->y) {
+ return false;
+ }
+
return true;
}
diff --git a/src/libs/kpunch/libdvx/dvxComp.h b/src/libs/kpunch/libdvx/dvxComp.h
index 57a5150..1a35b1e 100644
--- a/src/libs/kpunch/libdvx/dvxComp.h
+++ b/src/libs/kpunch/libdvx/dvxComp.h
@@ -72,7 +72,4 @@ void flushRect(DisplayT *d, const RectT *r);
// compositing to clip window content to dirty regions.
bool rectIntersect(const RectT *a, const RectT *b, RectT *result);
-// Returns true if the rectangle has zero or negative area.
-bool rectIsEmpty(const RectT *r);
-
#endif // DVX_COMP_H
diff --git a/src/libs/kpunch/libdvx/dvxDialog.c b/src/libs/kpunch/libdvx/dvxDialog.c
index 9d89c2e..d3aafd7 100644
--- a/src/libs/kpunch/libdvx/dvxDialog.c
+++ b/src/libs/kpunch/libdvx/dvxDialog.c
@@ -483,7 +483,11 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
if (initialDir && initialDir[0]) {
strncpy(sFd.curDir, initialDir, DVX_MAX_PATH - 1);
} else {
- getcwd(sFd.curDir, DVX_MAX_PATH);
+ if (!getcwd(sFd.curDir, DVX_MAX_PATH)) {
+ // Unknown cwd: resolve accepted names relative to '.' rather
+ // than the filesystem root.
+ strcpy(sFd.curDir, ".");
+ }
}
sFd.curDir[DVX_MAX_PATH - 1] = '\0';
@@ -553,10 +557,7 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
wgtDropdownSetItems(sFd.filterDd, filterLabels, fc);
wgtDropdownSetSelected(sFd.filterDd, 0);
-
- if (sFd.filterDd) {
- sFd.filterDd->onChange = fdOnFilterChange;
- }
+ sFd.filterDd->onChange = fdOnFilterChange;
}
}
@@ -849,7 +850,7 @@ int32_t dvxErrorBox(AppContextT *ctx, const char *title, const char *message) {
int32_t dvxInfoBox(AppContextT *ctx, const char *title, const char *message) {
- return dvxMessageBox(ctx, title, message, MB_OK | MB_ICONINFO);
+ return dvxMessageBox(ctx, title ? title : "Information", message, MB_OK | MB_ICONINFO);
}
@@ -1322,31 +1323,22 @@ static void fdLoadDir(void) {
return;
}
- // Sort: build index array, sort, reorder
- int32_t *sortIdx = (int32_t *)malloc(sFd.entryCount * sizeof(int32_t));
+ // Sort: build index array, sort, reorder. On alloc failure, leave
+ // entries in their unsorted-but-valid order rather than failing the
+ // whole listing.
+ int32_t *sortIdx = (int32_t *)malloc(sFd.entryCount * sizeof(int32_t));
+ char **tmpNames = (char **)malloc(sFd.entryCount * sizeof(char *));
+ bool *tmpIsDir = (bool *)malloc(sFd.entryCount * sizeof(bool));
- if (!sortIdx) {
- dvxLog("Dialog: failed to allocate sort index");
- return;
- }
+ if (!sortIdx || !tmpNames || !tmpIsDir) {
+ dvxLog("Dialog: failed to allocate sort arrays");
+ } else {
+ for (int32_t i = 0; i < sFd.entryCount; i++) {
+ sortIdx[i] = i;
+ }
- for (int32_t i = 0; i < sFd.entryCount; i++) {
- sortIdx[i] = i;
- }
+ qsort(sortIdx, sFd.entryCount, sizeof(int32_t), fdEntryCompare);
- qsort(sortIdx, sFd.entryCount, sizeof(int32_t), fdEntryCompare);
-
- // Rebuild arrays in sorted order
- char **tmpNames = (char **)malloc(sFd.entryCount * sizeof(char *));
- bool *tmpIsDir = (bool *)malloc(sFd.entryCount * sizeof(bool));
-
- // On alloc failure, leave entries in their unsorted-but-valid order
- // rather than failing the whole listing.
- if (!tmpNames || !tmpIsDir) {
- dvxLog("Dialog: failed to allocate temp sort arrays");
- }
-
- if (tmpNames && tmpIsDir) {
for (int32_t i = 0; i < sFd.entryCount; i++) {
tmpNames[i] = sFd.entryNames[sortIdx[i]];
tmpIsDir[i] = sFd.entryIsDir[sortIdx[i]];
@@ -1638,7 +1630,7 @@ static void fdOnOk(WidgetT *w) {
static void fdOnPathKey(WidgetT *w, int32_t keyCode, int32_t shift) {
(void)shift;
- if (keyCode != KEY_ASCII_ENTER) {
+ if (keyCode != KEY_ENTER) {
return;
}
diff --git a/src/libs/kpunch/libdvx/dvxMem.h b/src/libs/kpunch/libdvx/dvxMem.h
index c8efe84..eac454f 100644
--- a/src/libs/kpunch/libdvx/dvxMem.h
+++ b/src/libs/kpunch/libdvx/dvxMem.h
@@ -43,7 +43,6 @@ 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);
diff --git a/src/libs/kpunch/libdvx/dvxTypes.h b/src/libs/kpunch/libdvx/dvxTypes.h
index b8c4dc9..7bf4364 100644
--- a/src/libs/kpunch/libdvx/dvxTypes.h
+++ b/src/libs/kpunch/libdvx/dvxTypes.h
@@ -475,9 +475,28 @@ typedef struct {
#define KEY_LEFT (0x4B | KEY_EXT_FLAG)
#define KEY_RIGHT (0x4D | KEY_EXT_FLAG)
+// Raw BIOS scancodes (enhanced INT 16h) and ASCII codes checked directly
+// by the keyboard poller. Extended keys arrive with ascii == 0.
+#define KEY_SCAN_ESC 0x01
+#define KEY_SCAN_TAB 0x0F
+#define KEY_SCAN_SPACE 0x39
+#define KEY_SCAN_F1 0x3B
+#define KEY_SCAN_F10 0x44
+#define KEY_SCAN_UP 0x48
+#define KEY_SCAN_LEFT 0x4B
+#define KEY_SCAN_RIGHT 0x4D
+#define KEY_SCAN_DOWN 0x50
+#define KEY_SCAN_ALT_F4 0x6B
+#define KEY_SCAN_CTRL_F12 0x8A
+#define KEY_SCAN_ALT_TAB 0xA5
+#define KEY_ASCII_CTRL_A 0x01 // BIOS Ctrl+A..Ctrl+Z arrive as 0x01..0x1A
+#define KEY_ASCII_CTRL_Z 0x1A
+
// ASCII control / printable ranges (non-extended codes).
#define KEY_ESCAPE 0x1B
-#define KEY_ASCII_ENTER 0x0D // Enter/Return (carriage return)
+#define KEY_ENTER 0x0D // Enter/Return (carriage return)
+#define KEY_TAB 0x09
+#define KEY_SPACE 0x20
#define KEY_ASCII_DEL 0x7F // not the same as KEY_DELETE (scancode 0x53)
#define KEY_BACKSPACE 0x08 // ASCII backspace (not KEY_DELETE scancode)
#define KEY_ASCII_PRINT_FIRST 0x20 // space
@@ -560,10 +579,6 @@ typedef struct {
#define HIT_MAXIMIZE 8
#define HIT_NONE (-1)
-// Scroll drag orientation
-#define SCROLL_VERTICAL 0
-#define SCROLL_HORIZONTAL 1
-
// Minimized windows display as icons at the bottom of the screen,
// similar to a classic desktop icon bar.
#define ICON_SIZE 64
@@ -625,6 +640,11 @@ typedef struct WindowT {
// Widget tree root (NULL if no widgets)
struct WidgetT *widgetRoot;
+ // Widget that held keyboard focus when this window last lost WM focus.
+ // Restored by the widget paint handler when the window regains focus;
+ // cleared by widgetClearReferences when that widget is destroyed.
+ struct WidgetT *lastFocusWidget;
+
// Context menu (NULL if none, caller owns the MenuT allocation)
MenuT *contextMenu;
@@ -681,7 +701,7 @@ typedef struct {
int32_t resizeWindow;
int32_t resizeEdge;
int32_t scrollWindow; // window being scroll-dragged (HIT_NONE = none)
- int32_t scrollOrient; // SCROLL_VERTICAL or SCROLL_HORIZONTAL
+ ScrollbarOrientE scrollOrient; // orientation of the scrollbar being dragged
int32_t scrollDragOff; // mouse offset from thumb start
} WindowStackT;
@@ -849,6 +869,7 @@ typedef struct {
// ============================================================
#define DVX_MIN(a, b) ((a) < (b) ? (a) : (b))
+#define DVX_ARRAY_LEN(a) ((int32_t)(sizeof(a) / sizeof((a)[0])))
#define DVX_MAX(a, b) ((a) > (b) ? (a) : (b))
#endif // DVX_TYPES_H
diff --git a/src/libs/kpunch/libdvx/dvxWgt.h b/src/libs/kpunch/libdvx/dvxWgt.h
index c45b5e3..b2fb8c2 100644
--- a/src/libs/kpunch/libdvx/dvxWgt.h
+++ b/src/libs/kpunch/libdvx/dvxWgt.h
@@ -552,10 +552,22 @@ void wgtSetEnabled(WidgetT *w, bool enabled);
// Set read-only mode (allows scrolling/selection but blocks editing)
void wgtSetReadOnly(WidgetT *w, bool readOnly);
-// Set/get keyboard focus
+// Set/get keyboard focus. wgtSetFocused refuses disabled or hidden widgets.
void wgtSetFocused(WidgetT *w);
WidgetT *wgtGetFocused(void);
+// Move keyboard focus to w (NULL clears it): clears the previous widget's
+// selection, repaints both, and fires the blur/focus callbacks. Returns
+// false if a callback destroyed widgets -- do not touch w or the previous
+// widget afterwards. This is the single focus-transition path; use it
+// instead of assigning the focused widget directly.
+bool widgetTransferFocus(WidgetT *w);
+
+// Fire the blur/focus callbacks for a transition that has already been
+// recorded (prev lost focus, next gained it) without touching selection or
+// repaint state. Same return contract as widgetTransferFocus.
+bool widgetFireFocusChange(WidgetT *prev, WidgetT *next);
+
// Show/hide a widget
void wgtSetVisible(WidgetT *w, bool visible);
diff --git a/src/libs/kpunch/libdvx/dvxWgtP.h b/src/libs/kpunch/libdvx/dvxWgtP.h
index 7d72f28..977d3e2 100644
--- a/src/libs/kpunch/libdvx/dvxWgtP.h
+++ b/src/libs/kpunch/libdvx/dvxWgtP.h
@@ -166,7 +166,8 @@ void widgetAllocRollback(WidgetT *w);
// struct remain zeroed. Returns NULL on allocation failure.
WidgetT *widgetAllocWithText(WidgetT *parent, int32_t type, size_t dataSize, const char *text);
-// Focus management
+// Focus management. widgetTransferFocus / widgetFireFocusChange are
+// declared in dvxWgt.h (the app layer's Tab/accelerator handlers use them).
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after);
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before);
WidgetT *widgetFindByAccel(WidgetT *root, char key);
@@ -176,6 +177,7 @@ int32_t widgetCountVisibleChildren(const WidgetT *w);
int32_t widgetFrameBorderWidth(const WidgetT *w);
bool widgetIsFocusable(int32_t type);
bool widgetIsHorizContainer(int32_t type);
+bool widgetIsShown(const WidgetT *w);
int32_t multiClickDetect(int32_t vx, int32_t vy);
// Clipboard
@@ -185,7 +187,9 @@ const char *clipboardGet(int32_t *outLen);
// Hit testing
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y);
-// Scrollbar helpers
+// Scrollbar helpers. widgetScrollbarThumb yields a zero-size thumb when
+// trackLen or totalSize is <= 0 and clamps scrollPos, so callers need no
+// guards of their own.
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize);
int32_t widgetScrollbarThumbDragScroll(int32_t trackLen, int32_t total, int32_t visible, int32_t relMouse, int32_t maxScroll);
@@ -212,6 +216,10 @@ ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSi
// Layout functions (widgetLayout.c)
// ============================================================
+// Resolves a box container's padding/gap (tagged sizes with defaults) and
+// its class-supplied extraTop/borderW overrides. font may be NULL when
+// only borderW is wanted (character-unit sizes then resolve to 0).
+void widgetBoxMetrics(const WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW);
void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font);
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font);
void widgetLayoutBox(WidgetT *w, const BitmapFontT *font);
diff --git a/src/libs/kpunch/libdvx/dvxWm.c b/src/libs/kpunch/libdvx/dvxWm.c
index 49f516e..7bb6acb 100644
--- a/src/libs/kpunch/libdvx/dvxWm.c
+++ b/src/libs/kpunch/libdvx/dvxWm.c
@@ -64,7 +64,6 @@
#include
#include
-#include
#include "dvxMem.h"
// ============================================================
@@ -86,6 +85,11 @@
#define MINIMIZE_ICON_SIZE 4 // filled square size for minimize icon
#define DRAG_DEADZONE 4 // wmDragMove: pixels before drag activates
#define CONTENT_CLEAR_BYTE 0xFF // content buffer fill: clean white background
+#define MENU_INIT_CAP 8 // initial capacity of menu item / menu bar arrays
+#define STACK_INIT_CAP 16 // initial capacity of the window stack array
+
+// Title bar gadget square size, shared by geometry, drawing, and min-size.
+#define GADGET_SIZE (CHROME_TITLE_HEIGHT - GADGET_INSET * 2)
// ============================================================
// Title bar gadget geometry
@@ -114,8 +118,11 @@ typedef struct {
// Prototypes
// ============================================================
+static RectT clipSave(const DisplayT *d);
+static void clipRestore(DisplayT *d, const RectT *saved);
static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font);
static void computeTitleGeom(const WindowT *win, TitleGeomT *g);
+static void copyLabel(char *dst, const char *src, size_t cap);
static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t w, int32_t h);
static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win);
static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win);
@@ -125,6 +132,7 @@ static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT
static void freeMenuItemSubMenu(MenuItemT *item);
static void freeMenuRecursive(MenuT *menu);
static bool menuGrowItems(MenuT *menu);
+static int32_t menuLabelWidth(const BitmapFontT *font, const char *label);
static MenuItemT *menuNewItem(MenuT *menu, const char *label);
static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrientE orient, const char *orientName, int32_t min, int32_t max, int32_t pageSize);
static void scrollbarCommitValue(WindowT *win, ScrollbarT *sb, DirtyListT *dl, int32_t sbScreenX, int32_t sbScreenY, int32_t oldValue);
@@ -132,10 +140,26 @@ static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32
static int32_t wmAdjustIndexForRaise(int32_t index, int32_t raisedSlot, int32_t newTop);
static int32_t wmAdjustIndexForRemoval(int32_t index, int32_t removedSlot);
static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY);
+static void wmEffectiveMaxSize(const WindowT *win, const DisplayT *d, int32_t *maxW, int32_t *maxH);
static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu);
static MenuItemT *wmMenuFindItemRecursive(MenuT *menu, int32_t id, MenuT **outMenu);
+// Restores a clip rect captured by clipSave.
+static void clipRestore(DisplayT *d, const RectT *saved) {
+ setClipRect(d, saved->x, saved->y, saved->w, saved->h);
+}
+
+
+// Captures the display's current clip rect so a caller that tightens the
+// clip for its own drawing can put it back for the caller above it.
+static RectT clipSave(const DisplayT *d) {
+ RectT r = { d->clipX, d->clipY, d->clipW, d->clipH };
+
+ return r;
+}
+
+
// Lays out menu bar label positions left-to-right. Each label gets
// padding (CHROME_TITLE_PAD) on both sides and MENU_BAR_GAP between labels.
// Positions are cached and only recomputed when positionsDirty is set (after
@@ -155,8 +179,8 @@ static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) {
int32_t x = CHROME_TOTAL_SIDE;
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
- MenuT *menu = win->menuBar->menus[i];
- int32_t labelW = textWidthAccel(font, menu->label) + CHROME_TITLE_PAD * 2;
+ MenuT *menu = win->menuBar->menus[i];
+ int32_t labelW = menuLabelWidth(font, menu->label);
menu->barX = x;
menu->barW = labelW;
@@ -184,7 +208,7 @@ static void computeTitleGeom(const WindowT *win, TitleGeomT *g) {
g->titleY = win->y + CHROME_BORDER_WIDTH;
g->titleW = win->w - CHROME_BORDER_WIDTH * 2;
g->titleH = CHROME_TITLE_HEIGHT;
- g->gadgetS = g->titleH - GADGET_INSET * 2;
+ g->gadgetS = GADGET_SIZE;
g->gadgetY = g->titleY + GADGET_INSET;
g->closeX = g->titleX + GADGET_PAD;
@@ -212,6 +236,19 @@ static void computeTitleGeom(const WindowT *win, TitleGeomT *g) {
}
+// Copies a caller-supplied title/label into a fixed buffer, always NUL-
+// terminating and treating a NULL source (a DXE/BASIC app passing an unset
+// string) as the empty string.
+static void copyLabel(char *dst, const char *src, size_t cap) {
+ if (!src) {
+ src = "";
+ }
+
+ strncpy(dst, src, cap - 1);
+ dst[cap - 1] = '\0';
+}
+
+
// 4px raised Motif-style border using 3 shades:
// highlight (outer top/left), face (middle), shadow (outer bottom/right)
// with inner crease lines for 3D depth.
@@ -295,13 +332,9 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon
// region. setClipRect only clamps to screen bounds, it does not intersect
// with the prior clip, so we intersect against it with the shared
// rectIntersect primitive.
- int32_t savedClipX = d->clipX;
- int32_t savedClipY = d->clipY;
- int32_t savedClipW = d->clipW;
- int32_t savedClipH = d->clipH;
- RectT barRect = { win->x + CHROME_BORDER_WIDTH, barY, win->w - CHROME_BORDER_WIDTH * 2, barH };
- RectT savedClip = { savedClipX, savedClipY, savedClipW, savedClipH };
- RectT menuClip;
+ RectT savedClip = clipSave(d);
+ RectT barRect = { win->x + CHROME_BORDER_WIDTH, barY, win->w - CHROME_BORDER_WIDTH * 2, barH };
+ RectT menuClip;
if (rectIntersect(&barRect, &savedClip, &menuClip)) {
setClipRect(d, menuClip.x, menuClip.y, menuClip.w, menuClip.h);
@@ -309,7 +342,7 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon
// Menu bar lies fully outside the dirty rect: an empty clip rejects
// every label span while still letting the separator line paint once
// the clip is restored below.
- setClipRect(d, savedClipX, savedClipY, 0, 0);
+ setClipRect(d, savedClip.x, savedClip.y, 0, 0);
}
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
@@ -331,7 +364,7 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon
}
}
- setClipRect(d, savedClipX, savedClipY, savedClipW, savedClipH);
+ clipRestore(d, &savedClip);
drawHLine(d, ops, win->x + CHROME_BORDER_WIDTH, barY + barH - 1,
win->w - CHROME_BORDER_WIDTH * 2, colors->windowShadow);
@@ -423,10 +456,21 @@ static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW
int32_t colStart = 0;
int32_t colEnd = dstW;
- if (dstY < d->clipY) { rowStart = d->clipY - dstY; }
- if (dstY + dstH > d->clipY + d->clipH) { rowEnd = d->clipY + d->clipH - dstY; }
- if (dstX < d->clipX) { colStart = d->clipX - dstX; }
- if (dstX + dstW > d->clipX + d->clipW) { colEnd = d->clipX + d->clipW - dstX; }
+ if (dstY < d->clipY) {
+ rowStart = d->clipY - dstY;
+ }
+
+ if (dstY + dstH > d->clipY + d->clipH) {
+ rowEnd = d->clipY + d->clipH - dstY;
+ }
+
+ if (dstX < d->clipX) {
+ colStart = d->clipX - dstX;
+ }
+
+ if (dstX + dstW > d->clipX + d->clipW) {
+ colEnd = d->clipX + d->clipW - dstX;
+ }
if (rowStart >= rowEnd || colStart >= colEnd) {
return;
@@ -626,7 +670,7 @@ static bool menuGrowItems(MenuT *menu) {
return true;
}
- int32_t newCap = menu->itemCap ? menu->itemCap * 2 : 8;
+ int32_t newCap = menu->itemCap ? menu->itemCap * 2 : MENU_INIT_CAP;
MenuItemT *newBuf = (MenuItemT *)realloc(menu->items, newCap * sizeof(MenuItemT));
if (!newBuf) {
@@ -671,6 +715,13 @@ void menuItemApplyChecked(MenuT *menu, int32_t idx, bool checked) {
}
+// Width of a menu bar label including its padding. Single source of truth
+// for computeMenuBarPositions and wmMinWindowSize.
+static int32_t menuLabelWidth(const BitmapFontT *font, const char *label) {
+ return textWidthAccel(font, label) + CHROME_TITLE_PAD * 2;
+}
+
+
// Grows the item array, takes the next slot, and initializes the common
// fields (zeroed, label copied + NUL-terminated, enabled, accelerator).
// Returns the new item, or NULL if the array could not grow.
@@ -681,10 +732,9 @@ static MenuItemT *menuNewItem(MenuT *menu, const char *label) {
MenuItemT *item = &menu->items[menu->itemCount++];
memset(item, 0, sizeof(*item));
- strncpy(item->label, label, MAX_MENU_LABEL - 1);
- item->label[MAX_MENU_LABEL - 1] = '\0';
+ copyLabel(item->label, label, MAX_MENU_LABEL);
item->enabled = true;
- item->accelKey = accelParse(label);
+ item->accelKey = accelParse(item->label);
return item;
}
@@ -694,11 +744,7 @@ static MenuItemT *menuNewItem(MenuT *menu, const char *label) {
// field init, and content-rect update live in one place.
static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrientE orient, const char *orientName, int32_t min, int32_t max, int32_t pageSize) {
// Free any prior scrollbar so a second add on the same window does not leak.
- if (*slot) {
- free(*slot);
- *slot = NULL;
- }
-
+ free(*slot);
*slot = (ScrollbarT *)malloc(sizeof(ScrollbarT));
if (!*slot) {
@@ -706,6 +752,12 @@ static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrient
return NULL;
}
+ // A negative page size would make the thumb math divide by zero or go
+ // negative; treat it as "no page" instead.
+ if (pageSize < 0) {
+ pageSize = 0;
+ }
+
(*slot)->orient = orient;
(*slot)->min = min;
(*slot)->max = max;
@@ -769,7 +821,7 @@ static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32
return trackLen;
}
- *thumbSize = (int32_t)(((int64_t)sb->pageSize * trackLen) / (range + sb->pageSize));
+ *thumbSize = (int32_t)(((int64_t)sb->pageSize * trackLen) / ((int64_t)range + sb->pageSize));
if (*thumbSize < SCROLLBAR_WIDTH) {
*thumbSize = SCROLLBAR_WIDTH;
@@ -804,6 +856,15 @@ static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT
}
+// Effective maximum frame size for a window: its maxW/maxH constraint
+// clamped to the screen, or the full screen when WM_MAX_FROM_SCREEN.
+// Shared by wmMaximize and wmResizeMove.
+static void wmEffectiveMaxSize(const WindowT *win, const DisplayT *d, int32_t *maxW, int32_t *maxH) {
+ *maxW = (win->maxW == WM_MAX_FROM_SCREEN) ? d->width : DVX_MIN(win->maxW, d->width);
+ *maxH = (win->maxH == WM_MAX_FROM_SCREEN) ? d->height : DVX_MIN(win->maxH, d->height);
+}
+
+
// wmMenuFindItem -- find a menu item by command ID anywhere in a menu bar,
// descending into nested submenus to any depth. When outMenu is non-NULL it
// receives the menu that directly contains the matched item so callers can
@@ -877,7 +938,7 @@ ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page
MenuT *wmAddMenu(MenuBarT *bar, const char *label) {
if (bar->menuCount >= bar->menuCap) {
- int32_t newCap = bar->menuCap ? bar->menuCap * 2 : 8;
+ int32_t newCap = bar->menuCap ? bar->menuCap * 2 : MENU_INIT_CAP;
MenuT **newBuf = (MenuT **)realloc(bar->menus, newCap * sizeof(MenuT *));
if (!newBuf) {
@@ -894,9 +955,8 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label) {
return NULL;
}
- strncpy(menu->label, label, MAX_MENU_LABEL - 1);
- menu->label[MAX_MENU_LABEL - 1] = '\0';
- menu->accelKey = accelParse(label);
+ copyLabel(menu->label, label, MAX_MENU_LABEL);
+ menu->accelKey = accelParse(menu->label);
bar->menus[bar->menuCount] = menu;
bar->menuCount++;
bar->positionsDirty = true;
@@ -930,12 +990,12 @@ MenuBarT *wmAddMenuBar(WindowT *win) {
// before any repaint can observe it.
wmDestroyMenuBar(win, NULL);
- win->menuBar = bar;
- memset(win->menuBar, 0, sizeof(MenuBarT));
- win->menuBar->activeIdx = -1;
+ memset(bar, 0, sizeof(MenuBarT));
+ bar->activeIdx = -1;
+ win->menuBar = bar;
wmUpdateContentRect(win);
- return win->menuBar;
+ return bar;
}
@@ -1111,11 +1171,11 @@ MenuT *wmCreateMenu(void) {
WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) {
if (stack->count >= stack->cap) {
- int32_t newCap = stack->cap ? stack->cap * 2 : 16;
+ int32_t newCap = stack->cap ? stack->cap * 2 : STACK_INIT_CAP;
WindowT **newBuf = (WindowT **)realloc(stack->windows, newCap * sizeof(WindowT *));
if (!newBuf) {
- fprintf(stderr, "WM: Failed to grow window stack\n");
+ dvxLog("WM: failed to grow window stack");
return NULL;
}
@@ -1126,7 +1186,7 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int
WindowT *win = (WindowT *)malloc(sizeof(WindowT));
if (!win) {
- fprintf(stderr, "WM: Failed to allocate window\n");
+ dvxLog("WM: failed to allocate window");
return NULL;
}
@@ -1139,16 +1199,11 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int
win->w = w;
win->h = h;
win->visible = true;
- win->focused = false;
- win->minimized = false;
- win->maximized = false;
win->resizable = resizable;
- win->destroyPending = false;
win->maxW = WM_MAX_FROM_SCREEN;
win->maxH = WM_MAX_FROM_SCREEN;
- strncpy(win->title, title, MAX_TITLE_LEN - 1);
- win->title[MAX_TITLE_LEN - 1] = '\0';
+ copyLabel(win->title, title, MAX_TITLE_LEN);
wmUpdateContentRect(win);
@@ -1252,9 +1307,8 @@ void wmDestroyWindow(WindowStackT *stack, WindowT *win) {
win->contentBuf = NULL;
wmDestroyMenuBar(win, NULL);
+ wmRemoveScrollbars(win);
- free(win->vScroll);
- free(win->hScroll);
free(win->iconData);
free(win);
}
@@ -1356,10 +1410,7 @@ void wmDragMove(WindowStackT *stack, DirtyListT *dl, int32_t mouseX, int32_t mou
// manages its own clip state across multiple windows.
void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo) {
- int32_t savedClipX = d->clipX;
- int32_t savedClipY = d->clipY;
- int32_t savedClipW = d->clipW;
- int32_t savedClipH = d->clipH;
+ RectT savedClip = clipSave(d);
setClipRect(d, clipTo->x, clipTo->y, clipTo->w, clipTo->h);
@@ -1398,8 +1449,7 @@ void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, con
// GEOS Motif-style resize break indicators
drawResizeBreaks(d, ops, colors, win);
- // Restore clip rect
- setClipRect(d, savedClipX, savedClipY, savedClipW, savedClipH);
+ clipRestore(d, &savedClip);
}
@@ -1516,10 +1566,7 @@ void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo) {
- int32_t savedClipX = d->clipX;
- int32_t savedClipY = d->clipY;
- int32_t savedClipW = d->clipW;
- int32_t savedClipH = d->clipH;
+ RectT savedClip = clipSave(d);
setClipRect(d, clipTo->x, clipTo->y, clipTo->w, clipTo->h);
@@ -1531,7 +1578,7 @@ void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colo
wmDrawScrollbar(d, ops, colors, win->hScroll, win->x, win->y);
}
- setClipRect(d, savedClipX, savedClipY, savedClipW, savedClipH);
+ clipRestore(d, &savedClip);
}
@@ -1558,22 +1605,15 @@ void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *co
// Frees a standalone context menu and all its submenus recursively.
-// Unlike freeMenuRecursive (which only frees submenu children because the
-// top-level struct is embedded), this also frees the root MenuT itself.
+// freeMenuRecursive tears down the submenu tree and item array; this also
+// frees the root MenuT itself, which the caller owns.
void wmFreeMenu(MenuT *menu) {
if (!menu) {
return;
}
- // Free submenus recursively
- for (int32_t i = 0; i < menu->itemCount; i++) {
- if (menu->items[i].subMenu) {
- wmFreeMenu(menu->items[i].subMenu);
- }
- }
-
- free(menu->items);
+ freeMenuRecursive(menu);
free(menu);
}
@@ -1754,8 +1794,10 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT
dirtyListAdd(dl, win->x, win->y, win->w, win->h);
- int32_t newW = (win->maxW == WM_MAX_FROM_SCREEN) ? d->width : DVX_MIN(win->maxW, d->width);
- int32_t newH = (win->maxH == WM_MAX_FROM_SCREEN) ? d->height : DVX_MIN(win->maxH, d->height);
+ int32_t newW;
+ int32_t newH;
+
+ wmEffectiveMaxSize(win, d, &newW, &newH);
win->x = 0;
win->y = 0;
@@ -1804,15 +1846,11 @@ void wmMenuItemSetCheckedInMenu(MenuT *menu, int32_t id, bool checked) {
return;
}
- for (int32_t i = 0; i < menu->itemCount; i++) {
- if (menu->items[i].id == id) {
- menuItemApplyChecked(menu, i, checked);
- return;
- }
+ MenuT *owner = NULL;
+ MenuItemT *item = wmMenuFindItemRecursive(menu, id, &owner);
- if (menu->items[i].subMenu) {
- wmMenuItemSetCheckedInMenu(menu->items[i].subMenu, id, checked);
- }
+ if (item) {
+ menuItemApplyChecked(owner, (int32_t)(item - owner->items), checked);
}
}
@@ -1846,6 +1884,12 @@ void wmMinimize(WindowStackT *stack, DirtyListT *dl, WindowT *win) {
win->minimized = true;
+ // Only a focused window gives up focus; minimizing a background window
+ // must not fire onBlur/onFocus on unrelated windows.
+ if (!win->focused) {
+ return;
+ }
+
for (int32_t i = stack->count - 1; i >= 0; i--) {
if (stack->windows[i]->visible && !stack->windows[i]->minimized) {
wmSetFocus(stack, dl, i);
@@ -1973,14 +2017,10 @@ void wmMinimizedIconRect(const WindowStackT *stack, const DisplayT *d, int32_t *
// widths (when a menu bar is present) to size the minimum width.
void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) {
- int32_t gadgetS = CHROME_TITLE_HEIGHT - GADGET_INSET * 2;
- int32_t gadgetPad = GADGET_PAD;
- int32_t charW = FONT_CHAR_WIDTH;
-
- int32_t titleMinW = gadgetPad + gadgetS + gadgetPad + charW + gadgetPad + gadgetS + gadgetPad;
+ int32_t titleMinW = GADGET_PAD + GADGET_SIZE + GADGET_PAD + FONT_CHAR_WIDTH + GADGET_PAD + GADGET_SIZE + GADGET_PAD;
if (win->resizable) {
- titleMinW += gadgetS + gadgetPad;
+ titleMinW += GADGET_SIZE + GADGET_PAD;
}
*minW = titleMinW + CHROME_BORDER_WIDTH * 2;
@@ -1991,11 +2031,7 @@ void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) {
int32_t menuW = CHROME_TOTAL_SIDE;
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
- // Use the same width calc as computeMenuBarPositions so the
- // minimum size and the laid-out bar share one source of truth.
- const char *lbl = win->menuBar->menus[i]->label;
-
- menuW += textWidthAccel(&dvxFont8x16, lbl) + CHROME_TITLE_PAD * 2;
+ menuW += menuLabelWidth(&dvxFont8x16, win->menuBar->menus[i]->label);
if (i < win->menuBar->menuCount - 1) {
menuW += MENU_BAR_GAP;
@@ -2126,10 +2162,8 @@ void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx) {
// paint will show a clean background rather than garbage.
int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) {
- if (win->contentBuf) {
- free(win->contentBuf);
- win->contentBuf = NULL;
- }
+ free(win->contentBuf);
+ win->contentBuf = NULL;
win->contentPitch = win->contentW * d->format.bytesPerPixel;
int32_t bufSize = win->contentPitch * win->contentH;
@@ -2173,11 +2207,21 @@ bool wmRemoveMenuItem(MenuT *menu, int32_t id) {
}
+// Frees both window scrollbars (if present) and clears the slots. The
+// content rect is NOT recomputed here; callers that keep the window alive
+// (widgetManageScrollbars) call wmUpdateContentRect themselves.
+void wmRemoveScrollbars(WindowT *win) {
+ free(win->vScroll);
+ free(win->hScroll);
+ win->vScroll = NULL;
+ win->hScroll = NULL;
+}
+
+
// Initiates a window resize. Unlike drag (which stores mouse-to-origin
// offset), resize stores the absolute mouse position. wmResizeMove computes
-// delta from this position each frame, then conditionally resets it only
-// on axes where the resize was applied. When clamped, the delta accumulates
-// so the border sticks to the mouse when the user reverses direction.
+// the delta from this position each frame and then resets it to the
+// (possibly clamped) edge position, warping the cursor to match.
void wmResizeBegin(WindowStackT *stack, int32_t idx, int32_t edge, int32_t mouseX, int32_t mouseY) {
stack->resizeWindow = idx;
@@ -2267,12 +2311,10 @@ void wmResizeEnd(WindowStackT *stack) {
// clamped to [minW/minH, maxW/maxH].
//
// After resizing, the content buffer is reallocated and the app is notified
-// via onResize + onPaint. dragOffX/Y are reset to the current mouse position
-// only on axes where the resize was actually applied. If clamped (window at
-// min/max size), dragOff is NOT updated on that axis, so the accumulated
-// delta tracks how far the mouse moved past the border. When the user
-// reverses direction, the border immediately follows -- it "sticks" to
-// the mouse pointer instead of creating a dead zone.
+// via onResize + onPaint. dragOffX/Y are always reset to the clamped edge
+// position, and that position is reported back so the caller can warp the
+// cursor onto the edge. Keeping the cursor pinned to the border is what
+// prevents a dead zone when the user reverses direction at a size limit.
//
// If the user resizes while maximized, the maximized flag is cleared.
// This prevents wmRestore from snapping back to the pre-maximize geometry,
@@ -2296,8 +2338,10 @@ void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_
wmMinWindowSize(win, &minW, &minH);
// Compute effective maximum size
- int32_t maxW = (win->maxW == WM_MAX_FROM_SCREEN) ? d->width : DVX_MIN(win->maxW, d->width);
- int32_t maxH = (win->maxH == WM_MAX_FROM_SCREEN) ? d->height : DVX_MIN(win->maxH, d->height);
+ int32_t maxW;
+ int32_t maxH;
+
+ wmEffectiveMaxSize(win, d, &maxW, &maxH);
// Mark old position dirty
dirtyListAdd(dl, win->x, win->y, win->w, win->h);
@@ -2503,13 +2547,13 @@ void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, const DisplayT *d,
// dirtied only if the value actually changed, avoiding unnecessary
// repaints when clicking at the min/max limit.
-void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t orient, int32_t mx, int32_t my) {
+void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, ScrollbarOrientE orient, int32_t mx, int32_t my) {
if (idx < 0 || idx >= stack->count) {
return;
}
WindowT *win = stack->windows[idx];
- ScrollbarT *sb = (orient == SCROLL_VERTICAL) ? win->vScroll : win->hScroll;
+ ScrollbarT *sb = (orient == ScrollbarVerticalE) ? win->vScroll : win->hScroll;
if (!sb) {
return;
@@ -2531,62 +2575,30 @@ void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t
return;
}
- int32_t oldValue = sb->value;
+ // Reduce to one axis: the mouse offset along the bar from its origin.
+ int32_t oldValue = sb->value;
+ bool vertical = (sb->orient == ScrollbarVerticalE);
+ int32_t rel = vertical ? my - sbScreenY : mx - sbScreenX;
+ int32_t thumbStart = SCROLLBAR_WIDTH + thumbPos;
- if (sb->orient == ScrollbarVerticalE) {
- int32_t relY = my - sbScreenY;
-
- // Up arrow button
- if (relY < SCROLLBAR_WIDTH) {
- sb->value -= 1;
- }
- // Down arrow button
- else if (relY >= sb->length - SCROLLBAR_WIDTH) {
- sb->value += 1;
- }
- // Thumb
- else if (relY >= SCROLLBAR_WIDTH + thumbPos &&
- relY < SCROLLBAR_WIDTH + thumbPos + thumbSize) {
- stack->scrollWindow = idx;
- stack->scrollOrient = SCROLL_VERTICAL;
- stack->scrollDragOff = my - (sbScreenY + SCROLLBAR_WIDTH + thumbPos);
- return;
- }
- // Trough above thumb
- else if (relY < SCROLLBAR_WIDTH + thumbPos) {
- sb->value -= sb->pageSize;
- }
- // Trough below thumb
- else {
- sb->value += sb->pageSize;
- }
+ if (rel < SCROLLBAR_WIDTH) {
+ // Decrement arrow button
+ sb->value -= 1;
+ } else if (rel >= sb->length - SCROLLBAR_WIDTH) {
+ // Increment arrow button
+ sb->value += 1;
+ } else if (rel >= thumbStart && rel < thumbStart + thumbSize) {
+ // Thumb: begin drag
+ stack->scrollWindow = idx;
+ stack->scrollOrient = sb->orient;
+ stack->scrollDragOff = rel - thumbStart;
+ return;
+ } else if (rel < thumbStart) {
+ // Trough before thumb
+ sb->value -= sb->pageSize;
} else {
- int32_t relX = mx - sbScreenX;
-
- // Left arrow button
- if (relX < SCROLLBAR_WIDTH) {
- sb->value -= 1;
- }
- // Right arrow button
- else if (relX >= sb->length - SCROLLBAR_WIDTH) {
- sb->value += 1;
- }
- // Thumb
- else if (relX >= SCROLLBAR_WIDTH + thumbPos &&
- relX < SCROLLBAR_WIDTH + thumbPos + thumbSize) {
- stack->scrollWindow = idx;
- stack->scrollOrient = SCROLL_HORIZONTAL;
- stack->scrollDragOff = mx - (sbScreenX + SCROLLBAR_WIDTH + thumbPos);
- return;
- }
- // Trough left of thumb
- else if (relX < SCROLLBAR_WIDTH + thumbPos) {
- sb->value -= sb->pageSize;
- }
- // Trough right of thumb
- else {
- sb->value += sb->pageSize;
- }
+ // Trough after thumb
+ sb->value += sb->pageSize;
}
scrollbarCommitValue(win, sb, dl, sbScreenX, sbScreenY, oldValue);
@@ -2607,7 +2619,7 @@ void wmScrollbarDrag(WindowStackT *stack, DirtyListT *dl, int32_t mx, int32_t my
}
WindowT *win = stack->windows[stack->scrollWindow];
- ScrollbarT *sb = (stack->scrollOrient == SCROLL_VERTICAL) ? win->vScroll : win->hScroll;
+ ScrollbarT *sb = (stack->scrollOrient == ScrollbarVerticalE) ? win->vScroll : win->hScroll;
if (!sb) {
wmScrollbarEnd(stack);
@@ -2765,8 +2777,7 @@ int32_t wmSetIcon(WindowT *win, const char *path, const DisplayT *d) {
// avoiding a full-window repaint for what is purely a chrome change.
void wmSetTitle(WindowT *win, DirtyListT *dl, const char *title) {
- strncpy(win->title, title, MAX_TITLE_LEN - 1);
- win->title[MAX_TITLE_LEN - 1] = '\0';
+ copyLabel(win->title, title, MAX_TITLE_LEN);
// Dirty the title bar area
dirtyListAdd(dl, win->x + CHROME_BORDER_WIDTH,
diff --git a/src/libs/kpunch/libdvx/dvxWm.h b/src/libs/kpunch/libdvx/dvxWm.h
index 402bfae..384927c 100644
--- a/src/libs/kpunch/libdvx/dvxWm.h
+++ b/src/libs/kpunch/libdvx/dvxWm.h
@@ -90,6 +90,10 @@ void wmDestroyMenuBar(WindowT *win, const DisplayT *d);
// Get the minimum window size (accounts for chrome, gadgets, and menu bar).
void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH);
+// Free both window scrollbars (if any) and clear win->vScroll/hScroll.
+// Does not recompute the content rect.
+void wmRemoveScrollbars(WindowT *win);
+
// Append a dropdown menu to the menu bar. Returns the MenuT to populate
// with items. The label supports & accelerator markers (e.g. "&File").
MenuT *wmAddMenu(MenuBarT *bar, const char *label);
@@ -231,7 +235,7 @@ void wmSetTitle(WindowT *win, DirtyListT *dl, const char *title);
// Handle an initial click on a scrollbar. Determines what was hit (up/down
// arrows, page trough area, or thumb) and either adjusts the value
// immediately (arrows, trough) or begins a thumb drag operation.
-void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t orient, int32_t mx, int32_t my);
+void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, ScrollbarOrientE orient, int32_t mx, int32_t my);
// Update the scroll value during an active thumb drag. Maps the mouse
// position along the track to a scroll value proportional to the range.
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlat.h b/src/libs/kpunch/libdvx/platform/dvxPlat.h
index fc4b88b..fa25249 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlat.h
+++ b/src/libs/kpunch/libdvx/platform/dvxPlat.h
@@ -43,6 +43,7 @@
#define DVX_PLAT_H
#include "dvxTypes.h"
+#include "dvxMem.h"
#include
@@ -259,19 +260,9 @@ bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb);
// Calls to dvxFree on non-tracked pointers (magic mismatch) fall through
// to the real free() safely.
-// Per-app memory tracking (header-based).
+// Per-app memory tracking (header-based) is declared in dvxMem.h.
// The DXE export table maps malloc/free/calloc/realloc/strdup to
-// these wrappers. DXE code is tracked transparently.
-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);
+// those wrappers. DXE code is tracked transparently.
// Create a directory and all parent directories (like mkdir -p).
// Returns 0 on success, -1 on failure. Existing directories are not
@@ -297,6 +288,11 @@ char *platformPathDirEnd(const char *path);
// NULL -- if the path has no separator, the whole path is the basename.
const char *platformPathBaseName(const char *path);
+// Byte-for-byte file copy. Returns false on any open/read/write failure;
+// a partially written destination is removed so no truncated copy is
+// left behind.
+bool platformCopyFile(const char *srcPath, const char *dstPath);
+
// Slurp a whole file into a freshly-malloc'd, NUL-terminated buffer.
// Returns NULL on error (file missing, OOM, read truncated). On success,
// *outLen (if non-NULL) receives the byte length (not counting the NUL).
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
index f2ae616..2a2f54f 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
@@ -413,11 +413,6 @@ void dvxMemResetApp(int32_t appId) {
}
-void dvxMemSnapshotLoad(int32_t appId) {
- (void)appId;
-}
-
-
void *dvxRealloc(void *ptr, size_t size) {
if (!ptr) {
return dvxMalloc(size);
@@ -2565,7 +2560,6 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(dvxMemAppIdPtr)
DXE_EXPORT(dvxMemGetAppUsage)
DXE_EXPORT(dvxMemResetApp)
- DXE_EXPORT(dvxMemSnapshotLoad)
DXE_EXPORT(dvxReadDir)
DXE_EXPORT(dvxReadDirFree)
DXE_EXPORT(dvxRealloc)
@@ -2577,6 +2571,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
// --- platform ---
DXE_EXPORT(platformAltScanToChar)
DXE_EXPORT(platformChdir)
+ DXE_EXPORT(platformCopyFile)
DXE_EXPORT(platformFlushRect)
DXE_EXPORT(platformGetMemoryInfo)
DXE_EXPORT(platformGetSystemInfo)
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
index 2342d35..e688aa5 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
@@ -49,6 +49,9 @@
// Permission bits for directories created by dvxMakeDirs (rwxr-xr-x).
#define MKDIR_MODE 0755
+// Transfer block used by platformCopyFile.
+#define COPY_FILE_CHUNK 4096
+
const char *dvxSkipWs(const char *s) {
if (!s) {
@@ -179,6 +182,40 @@ int32_t platformChdir(const char *path) {
}
+bool platformCopyFile(const char *srcPath, const char *dstPath) {
+ FILE *src = fopen(srcPath, "rb");
+
+ if (!src) {
+ return false;
+ }
+
+ FILE *dst = fopen(dstPath, "wb");
+
+ if (!dst) {
+ fclose(src);
+ return false;
+ }
+
+ char buf[COPY_FILE_CHUNK];
+ size_t n;
+ bool ok = true;
+
+ while (ok && (n = fread(buf, 1, sizeof(buf), src)) > 0) {
+ ok = (fwrite(buf, 1, n, dst) == n);
+ }
+
+ ok = ok && !ferror(src);
+ ok = (fclose(dst) == 0) && ok;
+ fclose(src);
+
+ if (!ok) {
+ remove(dstPath);
+ }
+
+ return ok;
+}
+
+
const char *platformPathBaseName(const char *path) {
if (!path) {
return "";
diff --git a/src/libs/kpunch/libdvx/widgetClass.c b/src/libs/kpunch/libdvx/widgetClass.c
index 57bebc0..0e97fba 100644
--- a/src/libs/kpunch/libdvx/widgetClass.c
+++ b/src/libs/kpunch/libdvx/widgetClass.c
@@ -43,9 +43,11 @@
// wgtRegisterClass() call. Index = type ID.
const WidgetClassT **widgetClassTable = NULL;
-// stb_ds string hashmap: key = widget name, value = API pointer
+// stb_ds string hashmap: key = widget name, value = API pointer.
+// Both maps are created with sh_new_strdup so shput copies the key; the
+// registering DXE's string is not borrowed.
typedef struct {
- char *key; // stb_ds string key (heap-allocated by shput)
+ char *key; // stb_ds string key (strdup'd copy owned by the map)
const void *value;
} ApiMapEntryT;
@@ -232,6 +234,10 @@ void wgtRegisterApi(const char *name, const void *api) {
return;
}
+ if (!sApiMap) {
+ sh_new_strdup(sApiMap);
+ }
+
shput(sApiMap, name, api);
}
@@ -263,5 +269,9 @@ void wgtRegisterIface(const char *name, const WgtIfaceT *iface) {
IfaceEntryT entry;
memset(&entry, 0, sizeof(entry));
entry.iface = iface;
+ if (!sIfaceMap) {
+ sh_new_strdup(sIfaceMap);
+ }
+
shput(sIfaceMap, name, entry);
}
diff --git a/src/libs/kpunch/libdvx/widgetCore.c b/src/libs/kpunch/libdvx/widgetCore.c
index 7e20798..a27dbb5 100644
--- a/src/libs/kpunch/libdvx/widgetCore.c
+++ b/src/libs/kpunch/libdvx/widgetCore.c
@@ -102,6 +102,7 @@ static int32_t sClickCount = 0;
// Prototypes
// ============================================================
+static void collectFocusable(WidgetT *w, WidgetT ***list);
static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter);
@@ -137,6 +138,23 @@ const char *clipboardGet(int32_t *outLen) {
}
+// Appends every focusable widget in the subtree rooted at w to *list in
+// depth-first (Tab) order, skipping hidden/disabled subtrees.
+static void collectFocusable(WidgetT *w, WidgetT ***list) {
+ if (!w->visible || !w->enabled) {
+ return;
+ }
+
+ if (widgetIsFocusable(w->type)) {
+ arrput(*list, w);
+ }
+
+ for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
+ collectFocusable(c, list);
+ }
+}
+
+
// Implements Tab-order navigation: finds the next focusable widget
// after 'after' in depth-first tree order. The two-pass approach
// (search from 'after' to end, then wrap to start) ensures circular
@@ -148,17 +166,18 @@ const char *clipboardGet(int32_t *outLen) {
// just to find the next one -- the common case returns quickly.
static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter) {
+ // Mark 'after' as passed BEFORE the visibility gate so a widget that was
+ // hidden or disabled while focused still anchors the search; otherwise
+ // Tab would wrap to the first widget instead of the next one.
+ if (after == NULL || w == after) {
+ *pastAfter = true;
+ }
+
if (!w->visible || !w->enabled) {
return NULL;
}
- if (after == NULL) {
- *pastAfter = true;
- }
-
- if (w == after) {
- *pastAfter = true;
- } else if (*pastAfter && widgetIsFocusable(w->type)) {
+ if (w != after && *pastAfter && widgetIsFocusable(w->type)) {
return w;
}
@@ -332,6 +351,10 @@ void widgetClearReferences(WidgetT *w) {
sKeyPressedBtn = NULL;
}
+ if (w->window && w->window->lastFocusWidget == w) {
+ w->window->lastFocusWidget = NULL;
+ }
+
// The on-screen tooltip may borrow w->tooltip, which dies with the
// widget; hide it so the compositor stops drawing a freed string.
// The context lives in the window root's userData (see wgtInitWindow);
@@ -431,6 +454,8 @@ void widgetDetachWindowReferences(WindowT *win) {
if (sKeyPressedBtn && sKeyPressedBtn->window == win) {
sKeyPressedBtn = NULL;
}
+
+ win->lastFocusWidget = NULL;
}
@@ -489,42 +514,15 @@ WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) {
// Shift+Tab navigation: finds the previous focusable widget.
-// Collects all focusable widgets via DFS, then returns the one
-// before 'before' (with wraparound). Uses stb_ds dynamic arrays
-// so there's no fixed limit on widget count or tree depth.
+// Collects all focusable widgets via a recursive DFS into one
+// stb_ds array, then returns the one before 'before' (with
+// wraparound). A 'before' that is not in the list (hidden or
+// disabled while focused) wraps to the last focusable widget.
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
- WidgetT **list = NULL;
- WidgetT **stack = NULL;
+ WidgetT **list = NULL;
- arrput(stack, root);
-
- while (arrlen(stack) > 0) {
- WidgetT *w = stack[arrlen(stack) - 1];
- arrsetlen(stack, arrlen(stack) - 1);
-
- if (!w->visible || !w->enabled) {
- continue;
- }
-
- if (widgetIsFocusable(w->type)) {
- arrput(list, w);
- }
-
- // Push children in reverse order so first child is processed first
- // Walk to end of sibling list, then push backwards
- WidgetT **children = NULL;
-
- for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
- arrput(children, c);
- }
-
- for (int32_t i = arrlen(children) - 1; i >= 0; i--) {
- arrput(stack, children[i]);
- }
-
- arrfree(children);
- }
+ collectFocusable(root, &list);
WidgetT *result = NULL;
int32_t count = arrlen(list);
@@ -543,22 +541,51 @@ WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
}
arrfree(list);
- arrfree(stack);
return result;
}
-int32_t widgetFrameBorderWidth(const WidgetT *w) {
- if (!wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS)) {
- return 0;
+// Fires the focus-transition callbacks for a change that has ALREADY been
+// recorded in sFocusedWidget: the class-level blur (commit/clamp edits)
+// and app onBlur on prev, then the app onFocus on next. This is the one
+// place the blur/focus sequence lives; every focus path (mouse, Tab,
+// accelerator, wgtSetFocused, window blur, paint-time restore) ends here.
+//
+// Returns false if a callback destroyed widgets (sWidgetGen changed) --
+// prev and next may be dangling then and the caller must not touch them.
+
+bool widgetFireFocusChange(WidgetT *prev, WidgetT *next) {
+ uint32_t gen = sWidgetGen;
+
+ if (prev && prev != next) {
+ // Commit/clamp any in-progress edit before the app onBlur so a
+ // LostFocus handler sees the committed value.
+ wclsOnBlur(prev);
+
+ if (sWidgetGen == gen && prev->onBlur) {
+ prev->onBlur(prev);
+ }
+
+ if (sWidgetGen != gen) {
+ return false;
+ }
}
- int32_t pad = 0;
- int32_t gap = 0;
- int32_t extraTop = 0;
- int32_t borderW = 0;
+ if (next && next != prev && next->onFocus) {
+ next->onFocus(next);
+ }
- wclsGetLayoutMetrics(w, NULL, &pad, &gap, &extraTop, &borderW);
+ return sWidgetGen == gen;
+}
+
+
+int32_t widgetFrameBorderWidth(const WidgetT *w) {
+ int32_t pad;
+ int32_t gap;
+ int32_t extraTop;
+ int32_t borderW;
+
+ widgetBoxMetrics(w, NULL, &pad, &gap, &extraTop, &borderW);
return borderW;
}
@@ -629,6 +656,21 @@ bool widgetIsHorizContainer(int32_t type) {
}
+// True when w and every ancestor are visible, i.e. the widget can actually
+// be seen (a child of a hidden container is hidden even if its own flag is
+// set). Used to keep keyboard focus off widgets the user cannot see.
+
+bool widgetIsShown(const WidgetT *w) {
+ for (const WidgetT *p = w; p; p = p->parent) {
+ if (!p->visible) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
// Register a widget-destroy subscriber. Idempotent: a callback already
// present is not added twice, so a module can register on every attach
// without tracking its own state. Fired by widgetClearReferences for
@@ -688,6 +730,13 @@ void widgetRemoveChild(WidgetT *parent, WidgetT *child) {
// becoming too small to grab with a mouse.
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize) {
+ // No track or no content: no thumb. Callers need no guard of their own.
+ if (trackLen <= 0 || totalSize <= 0) {
+ *thumbPos = 0;
+ *thumbSize = 0;
+ return;
+ }
+
*thumbSize = (trackLen * visibleSize) / totalSize;
if (*thumbSize < SB_MIN_THUMB) {
@@ -701,7 +750,7 @@ void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSi
int32_t maxScroll = totalSize - visibleSize;
if (maxScroll > 0) {
- *thumbPos = ((trackLen - *thumbSize) * scrollPos) / maxScroll;
+ *thumbPos = ((trackLen - *thumbSize) * clampInt(scrollPos, 0, maxScroll)) / maxScroll;
} else {
*thumbPos = 0;
}
@@ -729,6 +778,37 @@ int32_t widgetScrollbarThumbDragScroll(int32_t trackLen, int32_t total, int32_t
}
+// Moves keyboard focus to w (NULL clears focus). Clears the previous
+// widget's selection, repaints both widgets, and fires the blur/focus
+// callbacks via widgetFireFocusChange. A no-op when w already has focus.
+//
+// Returns false if a callback destroyed widgets -- the caller must not
+// dereference w or the previously focused widget afterwards.
+
+bool widgetTransferFocus(WidgetT *w) {
+ WidgetT *prev = sFocusedWidget;
+
+ if (prev == w) {
+ return true;
+ }
+
+ // Switch focus BEFORE invalidating so paint sees the correct focused
+ // state for both widgets.
+ sFocusedWidget = w;
+
+ if (prev) {
+ wclsClearSelection(prev);
+ wgtInvalidatePaint(prev);
+ }
+
+ if (w) {
+ wgtInvalidatePaint(w);
+ }
+
+ return widgetFireFocusChange(prev, w);
+}
+
+
// Remove a widget-destroy subscriber. No-op if the callback is absent, so
// a double-unregister is safe.
void widgetUnregisterDestroyFn(void (*fn)(WidgetT *w)) {
diff --git a/src/libs/kpunch/libdvx/widgetEvent.c b/src/libs/kpunch/libdvx/widgetEvent.c
index af0d3d0..9a56916 100644
--- a/src/libs/kpunch/libdvx/widgetEvent.c
+++ b/src/libs/kpunch/libdvx/widgetEvent.c
@@ -61,7 +61,7 @@ static int32_t sPrevMouseY = -1;
static void dispatchButtonEdges(WidgetT *hit, uint32_t snapGen, int32_t buttons, int32_t prevButtons, int32_t relX, int32_t relY);
static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons);
-static void widgetVirtualSize(const WindowT *win, const WidgetT *root, int32_t *outW, int32_t *outH);
+static void widgetSetRootGeometry(const WindowT *win, WidgetT *root);
// Dispatch mouse button press/release edges to the widget under the cursor.
@@ -148,16 +148,7 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
bool hadHScroll = (win->hScroll != NULL);
// Remove existing scrollbars to measure full available area
- if (hadVScroll) {
- free(win->vScroll);
- win->vScroll = NULL;
- }
-
- if (hadHScroll) {
- free(win->hScroll);
- win->hScroll = NULL;
- }
-
+ wmRemoveScrollbars(win);
wmUpdateContentRect(win);
int32_t availW = win->contentW;
@@ -218,13 +209,12 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
// Install scroll handler
win->onScroll = widgetOnScroll;
- // Layout at the virtual content size (the larger of content area and min size)
- int32_t layoutW;
- int32_t layoutH;
-
- widgetVirtualSize(win, root, &layoutW, &layoutH);
-
- wgtLayout(root, layoutW, layoutH, &ctx->font);
+ // Arrange at the virtual content size using the measure pass already
+ // done above (wgtLayout would re-measure the whole tree). The next
+ // PAINT_FULL arranges again at the same geometry; arranging here too
+ // keeps widget positions valid for callers that read them before then.
+ widgetSetRootGeometry(win, root);
+ widgetLayoutChildren(root, &ctx->font);
}
@@ -241,22 +231,16 @@ void widgetOnBlur(WindowT *win) {
// selection a menu Copy/Cut command is about to act on), and keeping
// the selection visible in an inactive window matches convention.
// Selection clears happen on widget-to-widget focus transitions.
+ //
+ // The widget is remembered on the window so widgetOnPaint can hand
+ // focus back to it (firing onFocus) when the window regains WM focus.
if (sFocusedWidget && sFocusedWidget->window == win) {
WidgetT *prev = sFocusedWidget;
- sFocusedWidget = NULL;
+
+ sFocusedWidget = NULL;
+ win->lastFocusWidget = prev;
wgtInvalidatePaint(prev);
-
- // Snapshot the destroy generation: the blur commit below may fire
- // a user Change handler that destroys the widget -- prev is
- // dangling then and must not be dereferenced.
- uint32_t gen = sWidgetGen;
-
- // Commit/clamp any in-progress edit before the app onBlur.
- wclsOnBlur(prev);
-
- if (sWidgetGen == gen && prev->onBlur) {
- prev->onBlur(prev);
- }
+ widgetFireFocusChange(prev, NULL);
}
}
@@ -294,8 +278,8 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
return;
}
- // Don't dispatch keys to disabled widgets
- if (!focus->enabled) {
+ // Don't dispatch keys to disabled or hidden widgets
+ if (!focus->enabled || !widgetIsShown(focus)) {
return;
}
@@ -342,7 +326,7 @@ void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod) {
return;
}
- if (!focus->enabled) {
+ if (!focus->enabled || !widgetIsShown(focus)) {
return;
}
@@ -402,14 +386,6 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y, int32_t buttons) {
- // Defense in depth: a drag on a deferred-destroyed window must be
- // dropped, not updated -- the widget tree is still allocated but its
- // backing app state may already be freed. (deferDestroyWindow also
- // clears this via widgetDetachWindowReferences.)
- if (sDragWidget && sDragWidget->window && sDragWidget->window->destroyPending) {
- sDragWidget = NULL;
- }
-
// Close popups from other windows
if (sOpenPopup && sOpenPopup->window != win) {
wclsClosePopup(sOpenPopup);
@@ -531,9 +507,16 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
uint32_t upGen = sWidgetGen;
dispatchButtonEdges(upHit, upGen, buttons, sPrevMouseButtons, relX, relY);
+
+ // MouseMove: plain hover motion with no left button held
+ if (sWidgetGen == upGen && (x != sPrevMouseX || y != sPrevMouseY) && upHit->onMouseMove) {
+ upHit->onMouseMove(upHit, buttons, relX, relY);
+ }
}
sPrevMouseButtons = buttons;
+ sPrevMouseX = x;
+ sPrevMouseY = y;
return;
}
@@ -640,24 +623,11 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
sPrevMouseX = vx;
sPrevMouseY = vy;
- // sFocusedWidget is now set directly by the widget's mouse handler
-
- // Fire focus/blur callbacks on transitions. Skipped entirely if a
- // callback destroyed widgets -- prevFocus may be dangling then.
+ // sFocusedWidget is now set directly by the widget's mouse handler.
+ // Fire the blur/focus callbacks for that transition. Skipped entirely
+ // if a callback destroyed widgets -- prevFocus may be dangling then.
if (sWidgetGen == gen) {
- if (prevFocus && prevFocus != sFocusedWidget) {
- // Internal blur (commit/clamp edits) fires before the app onBlur
- // so a LostFocus handler sees the committed value.
- wclsOnBlur(prevFocus);
-
- if (prevFocus->onBlur) {
- prevFocus->onBlur(prevFocus);
- }
- }
-
- if (sFocusedWidget && sFocusedWidget != prevFocus && sFocusedWidget->onFocus) {
- sFocusedWidget->onFocus(sFocusedWidget);
- }
+ widgetFireFocusChange(prevFocus, sFocusedWidget);
}
}
@@ -716,31 +686,31 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
}
// Apply scroll offset and re-layout at virtual size
- int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
- int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
- int32_t layoutW;
- int32_t layoutH;
-
- widgetVirtualSize(win, root, &layoutW, &layoutH);
-
- root->x = -scrollX;
- root->y = -scrollY;
- root->w = layoutW;
- root->h = layoutH;
+ widgetSetRootGeometry(win, root);
if (full) {
widgetLayoutChildren(root, &ctx->font);
}
- // Auto-focus first focusable widget if nothing has focus yet, but
- // only for the window that actually holds WM focus. Background and
- // just-blurred windows are repainted by the deferred-paint loop too,
- // and must not steal global widget focus from the active window.
+ // Restore widget focus if nothing has focus yet, but only for the
+ // window that actually holds WM focus. Background and just-blurred
+ // windows are repainted by the deferred-paint loop too, and must not
+ // steal global widget focus from the active window. The widget that
+ // held focus when the window last blurred gets it back (so GotFocus
+ // pairs with the LostFocus the blur fired); otherwise the first
+ // focusable widget does.
if (!sFocusedWidget && win->focused) {
- WidgetT *first = widgetFindNextFocusable(root, NULL);
+ WidgetT *target = win->lastFocusWidget;
- if (first) {
- sFocusedWidget = first;
+ win->lastFocusWidget = NULL;
+
+ if (!target || !target->enabled || !widgetIsShown(target)) {
+ target = widgetFindNextFocusable(root, NULL);
+ }
+
+ // onFocus may destroy widgets; the tree is not safe to paint then.
+ if (target && !widgetTransferFocus(target)) {
+ return;
}
}
@@ -798,11 +768,13 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
}
-// Computes the virtual content size: the larger of the available content area
-// and the widget tree's measured minimum. Used both to drive scroll ranges
-// (widgetManageScrollbars) and the paint-time layout (widgetOnPaint) so the
-// two never diverge.
-static void widgetVirtualSize(const WindowT *win, const WidgetT *root, int32_t *outW, int32_t *outH) {
- *outW = DVX_MAX(win->contentW, root->calcMinW);
- *outH = DVX_MAX(win->contentH, root->calcMinH);
+// Positions the root at the negative scroll offset and sizes it to the
+// virtual content size: the larger of the available content area and the
+// widget tree's measured minimum. Shared by widgetManageScrollbars and
+// widgetOnPaint so the geometry the two arrange at never diverges.
+static void widgetSetRootGeometry(const WindowT *win, WidgetT *root) {
+ root->x = -(win->hScroll ? win->hScroll->value : 0);
+ root->y = -(win->vScroll ? win->vScroll->value : 0);
+ root->w = DVX_MAX(win->contentW, root->calcMinW);
+ root->h = DVX_MAX(win->contentH, root->calcMinH);
}
diff --git a/src/libs/kpunch/libdvx/widgetLayout.c b/src/libs/kpunch/libdvx/widgetLayout.c
index cd64d5e..423112f 100644
--- a/src/libs/kpunch/libdvx/widgetLayout.c
+++ b/src/libs/kpunch/libdvx/widgetLayout.c
@@ -52,6 +52,35 @@
#include "dvxWgtP.h"
+// Resolves the layout metrics of a box container: padding and gap come
+// from the widget's tagged sizes (falling back to DEFAULT_PADDING /
+// DEFAULT_SPACING when unset), then a class-supplied getLayoutMetrics
+// may override them and add extraTop / borderW (Frame, TabPage).
+// Shared by the measure pass, the arrange pass, and
+// widgetFrameBorderWidth so the three never disagree.
+
+void widgetBoxMetrics(const WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW) {
+ int32_t charW = font ? font->charWidth : 0;
+
+ *pad = wgtResolveSize(w->padding, 0, charW);
+ *gap = wgtResolveSize(w->spacing, 0, charW);
+ *extraTop = 0;
+ *borderW = 0;
+
+ if (*pad == 0) {
+ *pad = DEFAULT_PADDING;
+ }
+
+ if (*gap == 0) {
+ *gap = DEFAULT_SPACING;
+ }
+
+ if (wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS)) {
+ wclsGetLayoutMetrics(w, font, pad, gap, extraTop, borderW);
+ }
+}
+
+
// Measure pass for box containers (VBox, HBox, RadioGroup, StatusBar,
// Toolbar, Frame, TabPage). Recursively measures all visible children,
// then computes this container's minimum size as:
@@ -62,29 +91,16 @@
// flag flips the calculation so VBox and HBox share the same code.
void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
- bool horiz = widgetIsHorizContainer(w->type);
- int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
- int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
- int32_t mainSize = 0;
- int32_t crossSize = 0;
- int32_t count = 0;
+ bool horiz = widgetIsHorizContainer(w->type);
+ int32_t mainSize = 0;
+ int32_t crossSize = 0;
+ int32_t count = 0;
+ int32_t pad;
+ int32_t gap;
+ int32_t frameExtraTop;
+ int32_t metricBorderW;
- if (pad == 0) {
- pad = DEFAULT_PADDING;
- }
-
- if (gap == 0) {
- gap = DEFAULT_SPACING;
- }
-
- // Widgets with getLayoutMetrics override default padding/gap/extraTop
- int32_t frameExtraTop = 0;
- int32_t metricBorderW = 0;
- bool hasMetrics = wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS);
-
- if (hasMetrics) {
- wclsGetLayoutMetrics(w, font, &pad, &gap, &frameExtraTop, &metricBorderW);
- }
+ widgetBoxMetrics(w, font, &pad, &gap, &frameExtraTop, &metricBorderW);
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
@@ -121,13 +137,9 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
w->calcMinH = mainSize + frameExtraTop;
}
- // Border (Frame and similar containers with getLayoutMetrics).
- // Reuse the border width already returned by wclsGetLayoutMetrics
- // above instead of dispatching the metrics vtable call a second time.
- if (hasMetrics) {
- w->calcMinW += metricBorderW * 2;
- w->calcMinH += metricBorderW * 2;
- }
+ // Border (Frame and similar containers with getLayoutMetrics; 0 otherwise)
+ w->calcMinW += metricBorderW * 2;
+ w->calcMinH += metricBorderW * 2;
}
@@ -196,32 +208,25 @@ void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
bool horiz = widgetIsHorizContainer(w->type);
- int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
- int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
+ int32_t pad;
+ int32_t gap;
+ int32_t frameExtraTop;
+ int32_t fb;
- if (pad == 0) {
- pad = DEFAULT_PADDING;
- }
-
- if (gap == 0) {
- gap = DEFAULT_SPACING;
- }
-
- // Widgets with getLayoutMetrics override default padding/gap/extraTop/border
- int32_t frameExtraTop = 0;
- int32_t fb = 0;
-
- if (wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS)) {
- wclsGetLayoutMetrics(w, font, &pad, &gap, &frameExtraTop, &fb);
- }
+ widgetBoxMetrics(w, font, &pad, &gap, &frameExtraTop, &fb);
int32_t innerX = w->x + pad + fb;
int32_t innerY = w->y + pad + fb + frameExtraTop;
int32_t innerW = w->w - pad * 2 - fb * 2;
int32_t innerH = w->h - pad * 2 - fb * 2 - frameExtraTop;
- if (innerW < 0) { innerW = 0; }
- if (innerH < 0) { innerH = 0; }
+ if (innerW < 0) {
+ innerW = 0;
+ }
+
+ if (innerH < 0) {
+ innerH = 0;
+ }
int32_t count = widgetCountVisibleChildren(w);
@@ -268,8 +273,12 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
}
}
- // Second pass: assign positions and sizes
- int32_t pos = (horiz ? innerX : innerY) + alignOffset;
+ // Second pass: assign positions and sizes. weightSeen/extraGiven track
+ // the running weight and pixel totals so the last weighted child gets
+ // whatever integer division left over instead of leaving it unused.
+ int32_t pos = (horiz ? innerX : innerY) + alignOffset;
+ int32_t weightSeen = 0;
+ int32_t extraGiven = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
@@ -281,7 +290,18 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
// Distribute extra space by weight
if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) {
- mainSize += (extraSpace * c->weight) / totalWeight;
+ int32_t share;
+
+ weightSeen += c->weight;
+
+ if (weightSeen >= totalWeight) {
+ share = extraSpace - extraGiven;
+ } else {
+ share = (extraSpace * c->weight) / totalWeight;
+ }
+
+ extraGiven += share;
+ mainSize += share;
}
// Apply max size constraint
@@ -373,9 +393,10 @@ void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
// The root widget is positioned at (0,0) and given the full available
// area, then the arrange pass distributes space to its children.
//
-// This is called from widgetManageScrollbars() and widgetOnPaint(),
-// which may pass a virtual content size larger than the physical
-// window if scrolling is needed.
+// The window paths (widgetManageScrollbars, widgetOnPaint) do not use
+// this: they measure once and arrange at the scrolled root geometry
+// themselves. This entry point serves callers that need a complete
+// layout on demand (e.g. dvxFitWindow).
void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font) {
if (!root) {
diff --git a/src/libs/kpunch/libdvx/widgetOps.c b/src/libs/kpunch/libdvx/widgetOps.c
index 564809e..a72b4ee 100644
--- a/src/libs/kpunch/libdvx/widgetOps.c
+++ b/src/libs/kpunch/libdvx/widgetOps.c
@@ -40,6 +40,9 @@
static bool sFullRepaint = false;
+// Knuth multiplicative hash constant (2^32 / golden ratio).
+#define KNUTH_HASH_MUL 2654435761u
+
// ============================================================
// Prototypes
@@ -48,11 +51,12 @@ static bool sFullRepaint = false;
static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops);
static bool pressableHitTest(const WidgetT *w, const WidgetT *root, int32_t x, int32_t y);
static WidgetT *wgtFindImpl(WidgetT *w, const char *name);
+static bool widgetDropFocusWithin(WidgetT *w);
static void widgetNotifyChildChanged(WidgetT *w);
// Draws a 1px border in a neon color derived from the widget pointer.
-// The Knuth multiplicative hash (2654435761) distributes pointer values
+// The Knuth multiplicative hash (KNUTH_HASH_MUL) distributes pointer values
// across the palette evenly so adjacent containers get different colors.
// This is only active when sDebugLayout is true (toggled via
// wgtSetDebugLayout), used during development to visualize container
@@ -74,8 +78,8 @@ static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops) {
{255, 128, 255}, // orchid
};
- uint32_t h = (uint32_t)(uintptr_t)w * 2654435761u;
- int32_t idx = (int32_t)((h >> 16) % 12);
+ uint32_t h = (uint32_t)(uintptr_t)w * KNUTH_HASH_MUL;
+ int32_t idx = (int32_t)((h >> 16) % DVX_ARRAY_LEN(palette));
uint32_t color = packColor(d, palette[idx][0], palette[idx][1], palette[idx][2]);
drawRectOutline(d, ops, w->x, w->y, w->w, w->h, color);
@@ -270,14 +274,7 @@ void wgtInvalidate(WidgetT *w) {
return;
}
- // Find the root
- WidgetT *root = w;
-
- while (root->parent) {
- root = root->parent;
- }
-
- AppContextT *ctx = (AppContextT *)root->userData;
+ AppContextT *ctx = wgtGetContext(w);
if (!ctx) {
return;
@@ -353,49 +350,31 @@ void wgtSetDebugLayout(AppContextT *ctx, bool enabled) {
void wgtSetEnabled(WidgetT *w, bool enabled) {
- if (w) {
- w->enabled = enabled;
- wgtInvalidatePaint(w);
- }
-}
-
-
-void wgtSetFocused(WidgetT *w) {
- if (!w || !w->enabled) {
+ if (!w) {
return;
}
- WidgetT *prev = sFocusedWidget;
+ w->enabled = enabled;
- if (prev && prev != w) {
- wclsClearSelection(prev);
- wgtInvalidatePaint(prev);
+ // A disabled subtree must not keep keyboard focus. Bail if the blur
+ // callback destroyed widgets -- w may be gone.
+ if (!enabled && !widgetDropFocusWithin(w)) {
+ return;
}
- sFocusedWidget = w;
wgtInvalidatePaint(w);
+}
- if (prev && prev != w) {
- // Snapshot the destroy generation: the blur commit and app onBlur
- // below may fire a user Change handler that destroys widgets --
- // prev and w are dangling then and must not be dereferenced.
- uint32_t gen = sWidgetGen;
- // Commit/clamp any in-progress edit before the app onBlur.
- wclsOnBlur(prev);
-
- if (sWidgetGen == gen && prev->onBlur) {
- prev->onBlur(prev);
- }
-
- if (sWidgetGen != gen) {
- return;
- }
+// Programmatic focus. Disabled or hidden widgets (including children of a
+// hidden container) cannot take focus; the transition itself is the shared
+// widgetTransferFocus path.
+void wgtSetFocused(WidgetT *w) {
+ if (!w || !w->enabled || !widgetIsShown(w)) {
+ return;
}
- if (w->onFocus) {
- w->onFocus(w);
- }
+ widgetTransferFocus(w);
}
@@ -446,14 +425,36 @@ void wgtSetTooltip(WidgetT *w, const char *text) {
void wgtSetVisible(WidgetT *w, bool visible) {
- if (w) {
- w->visible = visible;
-
- // Notify parent chain of child visibility change via onChildChanged vtable
- widgetNotifyChildChanged(w);
-
- wgtInvalidate(w);
+ if (!w) {
+ return;
}
+
+ w->visible = visible;
+
+ // A hidden subtree must not keep keyboard focus. Bail if the blur
+ // callback destroyed widgets -- w may be gone.
+ if (!visible && !widgetDropFocusWithin(w)) {
+ return;
+ }
+
+ // Notify parent chain of child visibility change via onChildChanged vtable
+ widgetNotifyChildChanged(w);
+
+ wgtInvalidate(w);
+}
+
+
+// Clears keyboard focus if the focused widget is w or one of its
+// descendants (used when w is hidden or disabled). Returns false if the
+// blur callback destroyed widgets, in which case w may be dangling.
+static bool widgetDropFocusWithin(WidgetT *w) {
+ for (const WidgetT *p = sFocusedWidget; p; p = p->parent) {
+ if (p == w) {
+ return widgetTransferFocus(NULL);
+ }
+ }
+
+ return true;
}
@@ -625,7 +626,7 @@ void widgetPressableOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y
void widgetPressableOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod;
- if (key == ' ' || key == 0x0D) {
+ if (key == KEY_SPACE || key == KEY_ENTER) {
w->pressed = true;
sKeyPressedBtn = w;
wgtInvalidatePaint(w);
@@ -661,15 +662,27 @@ const char *widgetTextGet(const WidgetT *w) {
// Shared text setter for widgets whose DataT first field is
// `const char *text`. Replaces the owned strdup'd string and
-// recomputes the accelerator from the '&' prefix.
+// recomputes the accelerator from the '&' prefix. The copy is made
+// BEFORE the old string is freed so wgtSetText(w, wgtGetText(w)) is
+// safe and a failed strdup keeps the old text instead of blanking it.
void widgetTextSet(WidgetT *w, const char *text) {
if (!w || !w->data) {
return;
}
const char **slot = (const char **)w->data;
+ char *copy = NULL;
+
+ if (text) {
+ copy = strdup(text);
+
+ if (!copy) {
+ dvxLog("Widget: failed to allocate text");
+ return;
+ }
+ }
free((void *)*slot);
- *slot = text ? strdup(text) : NULL;
- w->accelKey = accelParse(text);
+ *slot = copy;
+ w->accelKey = accelParse(copy);
}
diff --git a/src/libs/kpunch/libdvx/widgetScrollbar.c b/src/libs/kpunch/libdvx/widgetScrollbar.c
index 9de3b77..2fc9589 100644
--- a/src/libs/kpunch/libdvx/widgetScrollbar.c
+++ b/src/libs/kpunch/libdvx/widgetScrollbar.c
@@ -60,14 +60,10 @@ void widgetDrawScrollbarHEx(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT
return;
}
- int32_t trackLen = sbW - barW * 2;
- int32_t thumbPos = 0;
- int32_t thumbSize = 0;
-
- if (trackLen > 0 && totalSize > 0) {
- widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
- }
+ int32_t thumbPos;
+ int32_t thumbSize;
+ widgetScrollbarThumb(sbW - barW * 2, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
drawScrollbar(d, ops, colors, ScrollbarHorizontalE, sbX, sbY, sbW, barW, thumbPos, thumbSize);
}
@@ -82,14 +78,10 @@ void widgetDrawScrollbarVEx(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT
return;
}
- int32_t trackLen = sbH - barW * 2;
- int32_t thumbPos = 0;
- int32_t thumbSize = 0;
-
- if (trackLen > 0 && totalSize > 0) {
- widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
- }
+ int32_t thumbPos;
+ int32_t thumbSize;
+ widgetScrollbarThumb(sbH - barW * 2, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
drawScrollbar(d, ops, colors, ScrollbarVerticalE, sbX, sbY, sbH, barW, thumbPos, thumbSize);
}
@@ -109,25 +101,24 @@ ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSi
return ScrollHitArrowIncE;
}
- int32_t trackLen = sbLen - WGT_SB_W * 2;
+ int32_t thumbPos;
+ int32_t thumbSize;
- if (trackLen > 0 && totalSize > 0) {
- int32_t thumbPos;
- int32_t thumbSize;
- widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
+ widgetScrollbarThumb(sbLen - WGT_SB_W * 2, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
- int32_t trackRel = relPos - WGT_SB_W;
-
- if (trackRel < thumbPos) {
- return ScrollHitPageDecE;
- }
-
- if (trackRel >= thumbPos + thumbSize) {
- return ScrollHitPageIncE;
- }
-
- return ScrollHitThumbE;
+ if (thumbSize <= 0) {
+ return ScrollHitNoneE;
}
- return ScrollHitNoneE;
+ int32_t trackRel = relPos - WGT_SB_W;
+
+ if (trackRel < thumbPos) {
+ return ScrollHitPageDecE;
+ }
+
+ if (trackRel >= thumbPos + thumbSize) {
+ return ScrollHitPageIncE;
+ }
+
+ return ScrollHitThumbE;
}
diff --git a/src/libs/kpunch/libtasks/taskswitch.c b/src/libs/kpunch/libtasks/taskswitch.c
index 3053769..5adaa9a 100644
--- a/src/libs/kpunch/libtasks/taskswitch.c
+++ b/src/libs/kpunch/libtasks/taskswitch.c
@@ -49,6 +49,9 @@
#include
#include
+// ABI-required stack alignment at function entry (bytes)
+#define STACK_ALIGN 16
+
// ============================================================================
// Internal types
// ============================================================================
@@ -380,6 +383,7 @@ static void taskTrampoline(void) {
tsExit();
}
+
uint32_t tsActiveCount(void) {
if (!initialized) {
return 0;
@@ -449,7 +453,7 @@ int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSi
// which switches away without returning, but it satisfies debuggers
// and ABI checkers that expect a return address at the bottom of each frame.
uintptr_t top = (uintptr_t)(task->stack + stackSize);
- top &= ~(uintptr_t)0xF;
+ top &= ~(uintptr_t)(STACK_ALIGN - 1);
top -= sizeof(uintptr_t);
*(uintptr_t *)top = 0; // dummy return address; trampoline never returns
diff --git a/src/libs/kpunch/serial/rs232/rs232.c b/src/libs/kpunch/serial/rs232/rs232.c
index 443f8f4..94ef073 100644
--- a/src/libs/kpunch/serial/rs232/rs232.c
+++ b/src/libs/kpunch/serial/rs232/rs232.c
@@ -342,18 +342,20 @@ static const Rs232BpsMapT sBpsMap[] = {
// Prototypes (alphabetical)
// ========================================================================
-static int32_t bpsToDivisor(int32_t bps);
-static void comGeneralIsr(void);
-static int32_t divisorToBps(uint16_t divisor);
-static void dpmiGetPvect(int vector, _go32_dpmi_seginfo *info);
-static int dpmiLockMemory(void);
-static void dpmiSetPvect(int vector, _go32_dpmi_seginfo *info);
-static void dpmiUnlockMemory(void);
-static int findIrq(int com);
-static void freeIrq(int com);
-static int installIrqHandler(int irq);
-static uint8_t picReadIrr(uint16_t port);
-static void removeIrqHandler(int irq);
+static int32_t bpsToDivisor(int32_t bps);
+static void comGeneralIsr(void);
+static int32_t divisorToBps(uint16_t divisor);
+static void dpmiGetPvect(int vector, _go32_dpmi_seginfo *info);
+static int dpmiLockMemory(void);
+static void dpmiSetPvect(int vector, _go32_dpmi_seginfo *info);
+static void dpmiUnlockMemory(void);
+static int findIrq(int com);
+static void freeIrq(int com);
+static int installIrqHandler(int irq);
+static void irqRestore(uint32_t flags);
+static uint32_t irqSave(void);
+static uint8_t picReadIrr(uint16_t port);
+static void removeIrqHandler(int irq);
int rs232ClearRxBuffer(int com);
int rs232ClearTxBuffer(int com);
int rs232Close(int com);
@@ -779,6 +781,23 @@ static int installIrqHandler(int irq) {
}
+// Disable interrupts, returning the previous EFLAGS so the caller can
+// restore the interrupt state it was entered with instead of blindly
+// re-enabling with STI.
+static uint32_t irqSave(void) {
+ uint32_t flags;
+
+ asm volatile("pushfl\n\tpopl %0\n\tcli" : "=r"(flags) : : "memory");
+
+ return flags;
+}
+
+
+static void irqRestore(uint32_t flags) {
+ asm volatile("pushl %0\n\tpopfl" : : "r"(flags) : "memory", "cc");
+}
+
+
static uint8_t picReadIrr(uint16_t port) {
PIC_WRITE_OCW3(port, PIC_RR);
return inportb(port);
@@ -887,9 +906,9 @@ int32_t rs232GetBps(int com) {
// The ISR must not fire inside the DLAB=1 window: with DLAB set, its
// data register access hits the divisor latch instead of RBR/THR.
- asm("CLI");
+ uint32_t flags = irqSave();
UART_READ_BPS(port, divisor);
- asm("STI");
+ irqRestore(flags);
return divisorToBps(divisor);
}
@@ -1302,9 +1321,9 @@ int rs232SetBps(int com, int32_t bps) {
}
// The ISR must not fire inside the DLAB=1 window (see rs232GetBps).
- asm("CLI");
+ uint32_t flags = irqSave();
UART_WRITE_BPS(port, (uint16_t)divisor);
- asm("STI");
+ irqRestore(flags);
return RS232_SUCCESS;
}
diff --git a/src/libs/kpunch/texthelp/textHelp.c b/src/libs/kpunch/texthelp/textHelp.c
index f077806..f18b890 100644
--- a/src/libs/kpunch/texthelp/textHelp.c
+++ b/src/libs/kpunch/texthelp/textHelp.c
@@ -2490,12 +2490,11 @@ void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorScheme
}
int32_t trackLen = len - thick * 2;
+ int32_t thumbPos;
+ int32_t thumbSize;
+ widgetScrollbarThumb(trackLen, total, visible, scroll, &thumbPos, &thumbSize);
- if (trackLen > 0) {
- int32_t thumbPos;
- int32_t thumbSize;
- widgetScrollbarThumb(trackLen, total, visible, scroll, &thumbPos, &thumbSize);
-
+ if (thumbSize > 0) {
if (vertical) {
drawBevel(d, ops, x, y + thick + thumbPos, thick, thumbSize, &btnBevel);
} else {
diff --git a/src/loader/loaderMain.c b/src/loader/loaderMain.c
index 665a0f7..22ecdf0 100644
--- a/src/loader/loaderMain.c
+++ b/src/loader/loaderMain.c
@@ -728,7 +728,7 @@ static void processHcf(const char *hcfPath, const char *hcfDir) {
dvxLog("helpRecompile: %s -> %s (%d files)", hcfPath, ctx.outputFile, (int)inputCount);
int32_t rc = hlpcCompile((const char **)ctx.inputFiles, inputCount, ctx.outputFile,
- ctx.imgDir[0] ? ctx.imgDir : NULL, NULL, 1,
+ ctx.imgDir[0] ? ctx.imgDir : NULL, NULL, HLPC_QUIET,
hlpcProgressCallback, NULL);
if (rc != 0) {
diff --git a/src/tools/hlpcCompile.h b/src/tools/hlpcCompile.h
index 97df778..5b2d9b6 100644
--- a/src/tools/hlpcCompile.h
+++ b/src/tools/hlpcCompile.h
@@ -47,6 +47,10 @@ typedef void (*HlpcProgressFnT)(void *ctx, int32_t current, int32_t total);
// progressFn: progress callback (NULL = no progress reporting)
// progressCtx: opaque context passed to progressFn
//
+// Values for the quiet parameter.
+#define HLPC_VERBOSE 0
+#define HLPC_QUIET 1
+
// Returns 0 on success, non-zero on error.
int32_t hlpcCompile(const char **inputFiles, int32_t inputCount, const char *outputPath, const char *imageDir, const char *htmlPath, int32_t quiet, HlpcProgressFnT progressFn, void *progressCtx);
diff --git a/src/widgets/kpunch/scrollPane/widgetScrollPane.c b/src/widgets/kpunch/scrollPane/widgetScrollPane.c
index d6fb503..efe4812 100644
--- a/src/widgets/kpunch/scrollPane/widgetScrollPane.c
+++ b/src/widgets/kpunch/scrollPane/widgetScrollPane.c
@@ -682,7 +682,7 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
sp->scrollPosV -= font->charHeight;
} else if (relY >= sbH - SP_SB_W) {
sp->scrollPosV += font->charHeight;
- } else if (trackLen > 0) {
+ } else {
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, contentMinH, innerH, sp->scrollPosV, &thumbPos, &thumbSize);
@@ -726,7 +726,7 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
sp->scrollPosH -= font->charWidth;
} else if (relX >= sbW - SP_SB_W) {
sp->scrollPosH += font->charWidth;
- } else if (trackLen > 0) {
+ } else {
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, contentMinW, innerW, sp->scrollPosH, &thumbPos, &thumbSize);