Large number of bug fixes.

This commit is contained in:
Scott Duensing 2026-08-26 19:22:33 -05:00
parent b638ca9b9a
commit dc8be04ae9
79 changed files with 13581 additions and 11676 deletions

View file

@ -1224,6 +1224,12 @@ name$ = &quot;Hello&quot; ' String</code></pre>
<blockquote><strong>Note:</strong> Both E and D can introduce an exponent (e.g. 1.5E10 and 2.5D3 are equivalent forms of a scientific-notation double).</blockquote> <blockquote><strong>Note:</strong> Both E and D can introduce an exponent (e.g. 1.5E10 and 2.5D3 are equivalent forms of a scientific-notation double).</blockquote>
<h2>Type Promotion</h2> <h2>Type Promotion</h2>
<p>When mixing types in expressions, values are automatically promoted to a common type: Integer -&gt; Long -&gt; Single -&gt; Double. Strings are not automatically converted to numbers (use VAL and STR$).</p> <p>When mixing types in expressions, values are automatically promoted to a common type: Integer -&gt; Long -&gt; Single -&gt; Double. Strings are not automatically converted to numbers (use VAL and STR$).</p>
<h2>Assignment Conversion</h2>
<p>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.</p>
<pre><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</code></pre>
<h2>Boolean Values</h2> <h2>Boolean Values</h2>
<p>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.</p> <p>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.</p>
<p><a href="#lang.func.conversion">See also: Conversion Functions</a></p> <p><a href="#lang.func.conversion">See also: Conversion Functions</a></p>
@ -1326,7 +1332,7 @@ Dim matrix(1 To 10, 1 To 10) As Single
Dim Shared globalFlag As Boolean Dim Shared globalFlag As Boolean
Dim record As PersonType Dim record As PersonType
Dim fixedStr As String * 20</code></pre> Dim fixedStr As String * 20</code></pre>
<blockquote><strong>Note:</strong> 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).</blockquote> <blockquote><strong>Note:</strong> 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.</blockquote>
<p>Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.</p> <p>Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.</p>
<h2>REDIM</h2> <h2>REDIM</h2>
<p>Reallocates a dynamic array, optionally preserving existing data.</p> <p>Reallocates a dynamic array, optionally preserving existing data.</p>
@ -1663,7 +1669,7 @@ ON ERROR GOTO 0 ' Disable error handler
RESUME ' Retry the statement that caused the error RESUME ' Retry the statement that caused the error
RESUME NEXT ' Continue at the next statement after the error RESUME NEXT ' Continue at the next statement after the error
ERROR n ' Raise a runtime error with error number n</code></pre> ERROR n ' Raise a runtime error with error number n</code></pre>
<p>The ERR keyword returns the current error number in expressions (it is 0 when no error is active).</p> <p>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).</p>
<pre><code>On Error GoTo ErrorHandler <pre><code>On Error GoTo ErrorHandler
Open &quot;missing.txt&quot; For Input As #1 Open &quot;missing.txt&quot; For Input As #1
Exit Sub Exit Sub
@ -1676,16 +1682,20 @@ ErrorHandler:
------ ------- ------ -------
1 FOR loop error (NEXT without FOR, NEXT variable mismatch, FOR stack underflow) 1 FOR loop error (NEXT without FOR, NEXT variable mismatch, FOR stack underflow)
4 Out of DATA 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 7 Out of memory
9 Subscript out of range / invalid variable or field index 9 Subscript out of range / invalid variable or field index
11 Division by zero 11 Division by zero
13 Type mismatch / not an array / not a TYPE instance 13 Type mismatch / not an array / not a TYPE instance
20 RESUME without error
26 FOR loop nesting too deep 26 FOR loop nesting too deep
51 Internal error (bad opcode) 51 Internal error (bad opcode)
52 Bad file number or file not open 52 Bad file number or file not open
53 File not found 53 File not found
54 Bad file mode 54 Bad file mode
58 File already exists or rename failed 58 File already exists or rename failed
59 Bad record length (OPEN ... LEN must be 1 to 32767)
67 Too many files open 67 Too many files open
75 Path/file access error 75 Path/file access error
76 Path not found</pre> 76 Path not found</pre>
@ -1720,7 +1730,7 @@ OPEN filename$ FOR BINARY AS #channel</code></pre>
INPUT Open for sequential reading. File must exist. INPUT Open for sequential reading. File must exist.
OUTPUT Open for sequential writing. Creates or truncates. OUTPUT Open for sequential writing. Creates or truncates.
APPEND Open for sequential writing at end of file. 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.</pre> BINARY Open for raw binary I/O.</pre>
<h2>CLOSE</h2> <h2>CLOSE</h2>
<p>Closes an open file channel.</p> <p>Closes an open file channel.</p>
@ -1729,8 +1739,11 @@ OPEN filename$ FOR BINARY AS #channel</code></pre>
<p>Writes text to a file.</p> <p>Writes text to a file.</p>
<pre><code>PRINT #channel, expression</code></pre> <pre><code>PRINT #channel, expression</code></pre>
<h2>INPUT #</h2> <h2>INPUT #</h2>
<p>Reads comma-delimited data from a file.</p> <p>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.</p>
<pre><code>INPUT #channel, variable</code></pre> <pre><code>INPUT #channel, variable [, variable ...]</code></pre>
<pre><code>Open &quot;data.txt&quot; For Input As #1
Input #1, name$, age%, score#
Close #1</code></pre>
<h2>LINE INPUT #</h2> <h2>LINE INPUT #</h2>
<p>Reads an entire line from a file into a string variable.</p> <p>Reads an entire line from a file into a string variable.</p>
<pre><code>LINE INPUT #channel, variable$</code></pre> <pre><code>LINE INPUT #channel, variable$</code></pre>
@ -1740,9 +1753,17 @@ OPEN filename$ FOR BINARY AS #channel</code></pre>
<pre><code>Write #1, &quot;Scott&quot;, 42, 3.14 <pre><code>Write #1, &quot;Scott&quot;, 42, 3.14
' Output: &quot;Scott&quot;,42,3.14</code></pre> ' Output: &quot;Scott&quot;,42,3.14</code></pre>
<h2>GET / PUT</h2> <h2>GET / PUT</h2>
<p>Read and write records in RANDOM or BINARY mode files.</p> <p>Read and write records in RANDOM or BINARY mode files. When recordNum is omitted the transfer starts at the current file position.</p>
<pre><code>GET #channel, [recordNum], variable <pre><code>GET #channel, [recordNum], variable
PUT #channel, [recordNum], variable</code></pre> PUT #channel, [recordNum], variable</code></pre>
<p>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.</p>
<p>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).</p>
<pre><code>Dim buf As String
Open &quot;raw.bin&quot; For Binary As #1
buf = Space$(16)
Get #1, 1, buf ' read bytes 1-16
Put #1, 33, &quot;TAG&quot; ' write 3 bytes at position 33
Close #1</code></pre>
<h2>SEEK</h2> <h2>SEEK</h2>
<p>Sets the file position. As a function, returns the current position.</p> <p>Sets the file position. As a function, returns the current position.</p>
<pre><code>SEEK #channel, position ' Statement: set position <pre><code>SEEK #channel, position ' Statement: set position
@ -1812,23 +1833,24 @@ WEND</code></pre>
CHR$(n) String Character with ASCII code n CHR$(n) String Character with ASCII code n
FORMAT$(value, fmt$) String Formats a numeric value using a format string FORMAT$(value, fmt$) String Formats a numeric value using a format string
HEX$(n) String Hexadecimal representation of n (uppercase, no leading &amp;H) HEX$(n) String Hexadecimal representation of n (uppercase, no leading &amp;H)
INSTR(s$, find$) Integer Position of find$ in s$ (1-based), 0 if not found INSTR(s$, find$) Long Position of find$ in s$ (1-based), 0 if not found
INSTR(start, s$, find$) Integer Search starting at position start (1-based) INSTR(start, s$, find$) Long Search starting at position start (1-based)
LCASE$(s$) String Converts s$ to lowercase LCASE$(s$) String Converts s$ to lowercase
LEFT$(s$, n) String Leftmost n characters of s$ 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$ LTRIM$(s$) String Removes leading spaces from s$
MID$(s$, start) String Substring from start (1-based) to end of string 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 MID$(s$, start, length) String Substring of length characters starting at start
OCT$(n) String Octal representation of n (no leading &amp;O) OCT$(n) String Octal representation of n (no leading &amp;O)
RIGHT$(s$, n) String Rightmost n characters of s$ RIGHT$(s$, n) String Rightmost n characters of s$
RTRIM$(s$) String Removes trailing spaces from 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) 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$ TRIM$(s$) String Removes leading and trailing spaces from s$
UCASE$(s$) String Converts s$ to uppercase UCASE$(s$) String Converts s$ to uppercase
VAL(s$) Double Converts string s$ to a numeric value; stops at first non-numeric character</pre> VAL(s$) Double Converts string s$ to a numeric value; stops at first non-numeric character</pre>
<p>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).</p>
<h2>FORMAT$</h2> <h2>FORMAT$</h2>
<p>FORMAT$ formats a numeric value using a BASIC-style format string. The format characters are the same as the ones used by PRINT USING.</p> <p>FORMAT$ formats a numeric value using a BASIC-style format string. The format characters are the same as the ones used by PRINT USING.</p>
<pre><code>s$ = FORMAT$(value, fmt$)</code></pre> <pre><code>s$ = FORMAT$(value, fmt$)</code></pre>
@ -1883,8 +1905,8 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background</code></pre>
-------- ------- ----------- -------- ------- -----------
CBOOL(n) Boolean Returns True (-1) if n is nonzero or a non-empty string; False (0) otherwise 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 CDBL(n) Double Converts n to Double
CINT(n) Integer Converts n to Integer (rounds half away from zero) CINT(n) Integer Converts n to Integer; halves round to the nearest even number (2.5 -&gt; 2, 3.5 -&gt; 4); error 6 (Overflow) outside -32768 to 32767
CLNG(n) Long Converts n to Long 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 CSNG(n) Single Converts n to Single
CSTR(n) String Converts n to its String representation</pre> CSTR(n) String Converts n to its String representation</pre>
</div> </div>

View file

@ -354,7 +354,6 @@ img { max-width: 100%; }
<li><a href="#api.mem">dvxMem.h</a></li> <li><a href="#api.mem">dvxMem.h</a></li>
<li><a href="#api.mem">dvxMemGetAppUsage</a></li> <li><a href="#api.mem">dvxMemGetAppUsage</a></li>
<li><a href="#api.mem">dvxMemResetApp</a></li> <li><a href="#api.mem">dvxMemResetApp</a></li>
<li><a href="#api.mem">dvxMemSnapshotLoad</a></li>
<li><a href="#api.dialog">dvxMessageBox</a></li> <li><a href="#api.dialog">dvxMessageBox</a></li>
<li><a href="#api.app">dvxMinimizeWindow</a></li> <li><a href="#api.app">dvxMinimizeWindow</a></li>
<li><a href="#api.palette">dvxPal.h</a></li> <li><a href="#api.palette">dvxPal.h</a></li>
@ -526,7 +525,6 @@ img { max-width: 100%; }
<li><a href="#api.draw">rectCopyGrayscale</a></li> <li><a href="#api.draw">rectCopyGrayscale</a></li>
<li><a href="#api.draw">rectFill</a></li> <li><a href="#api.draw">rectFill</a></li>
<li><a href="#api.comp">rectIntersect</a></li> <li><a href="#api.comp">rectIntersect</a></li>
<li><a href="#api.comp">rectIsEmpty</a></li>
<li><a href="#api.types">RectT</a></li> <li><a href="#api.types">RectT</a></li>
<li><a href="#arch.platform">rep movsd</a></li> <li><a href="#arch.platform">rep movsd</a></li>
<li><a href="#arch.platform">rep stosl</a></li> <li><a href="#arch.platform">rep stosl</a></li>
@ -1226,7 +1224,6 @@ prefsClose(h);</code></pre>
<p>Explicit use (e.g. in the Task Manager) can include dvxMem.h to call:</p> <p>Explicit use (e.g. in the Task Manager) can include dvxMem.h to call:</p>
<pre> Function Purpose <pre> Function Purpose
-------- ------- -------- -------
dvxMemSnapshotLoad Baseline a newly-loaded app's memory state
dvxMemGetAppUsage Query current bytes allocated for an app dvxMemGetAppUsage Query current bytes allocated for an app
dvxMemResetApp Free every tracked allocation charged to an app</pre> dvxMemResetApp Free every tracked allocation charged to an app</pre>
<p>The dvxMemAppIdPtr pointer is set by the shell to &amp;ctx-&gt;currentAppId so the allocator always knows which app to charge.</p> <p>The dvxMemAppIdPtr pointer is set by the shell to &amp;ctx-&gt;currentAppId so the allocator always knows which app to charge.</p>
@ -2127,13 +2124,6 @@ prefsClose(h);</code></pre>
a, b Input rectangles a, b Input rectangles
result Output: intersection rectangle (valid only when return is true)</pre> result Output: intersection rectangle (valid only when return is true)</pre>
<p>Returns: true if the rectangles overlap, false if disjoint.</p> <p>Returns: true if the rectangles overlap, false if disjoint.</p>
<h2>rectIsEmpty</h2>
<pre><code>bool rectIsEmpty(const RectT *r);</code></pre>
<p>Test whether a rectangle has zero or negative area.</p>
<pre> Parameter Description
--------- -----------
r Rectangle to test</pre>
<p>Returns: true if w &lt;= 0 or h &lt;= 0.</p>
</div> </div>
<div class="topic" id="api.wm"> <div class="topic" id="api.wm">
<h1>dvxWm.h -- Layer 4: Window Manager</h1> <h1>dvxWm.h -- Layer 4: Window Manager</h1>
@ -2465,14 +2455,14 @@ prefsClose(h);</code></pre>
stack Window stack</pre> stack Window stack</pre>
<h2>Scrollbar Interaction</h2> <h2>Scrollbar Interaction</h2>
<h3>wmScrollbarClick</h3> <h3>wmScrollbarClick</h3>
<pre><code>void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t orient, int32_t mx, int32_t my);</code></pre> <pre><code>void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, ScrollbarOrientE orient, int32_t mx, int32_t my);</code></pre>
<p>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.</p> <p>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.</p>
<pre> Parameter Description <pre> Parameter Description
--------- ----------- --------- -----------
stack Window stack stack Window stack
dl Dirty list dl Dirty list
idx Stack index of window idx Stack index of window
orient SCROLL_VERTICAL or SCROLL_HORIZONTAL orient ScrollbarVerticalE or ScrollbarHorizontalE
mx, my Click screen coordinates</pre> mx, my Click screen coordinates</pre>
<h3>wmScrollbarDrag</h3> <h3>wmScrollbarDrag</h3>
<pre><code>void wmScrollbarDrag(WindowStackT *stack, DirtyListT *dl, int32_t mx, int32_t my);</code></pre> <pre><code>void wmScrollbarDrag(WindowStackT *stack, DirtyListT *dl, int32_t mx, int32_t my);</code></pre>
@ -3818,12 +3808,6 @@ if (r == ID_YES) { saveFile(); }</code></pre>
<pre><code>char *dvxStrdup(const char *s);</code></pre> <pre><code>char *dvxStrdup(const char *s);</code></pre>
<p>Tracked strdup.</p> <p>Tracked strdup.</p>
<h2>Accounting</h2> <h2>Accounting</h2>
<h3>dvxMemSnapshotLoad</h3>
<pre><code>void dvxMemSnapshotLoad(int32_t appId);</code></pre>
<p>Record a baseline memory snapshot for the given app. Called right before app code starts so later calls to dvxMemGetAppUsage can report net growth.</p>
<pre> Parameter Description
--------- -----------
appId App ID to snapshot</pre>
<h3>dvxMemGetAppUsage</h3> <h3>dvxMemGetAppUsage</h3>
<pre><code>uint32_t dvxMemGetAppUsage(int32_t appId);</code></pre> <pre><code>uint32_t dvxMemGetAppUsage(int32_t appId);</code></pre>
<p>Return the total bytes currently charged to the given app ID.</p> <p>Return the total bytes currently charged to the given app ID.</p>
@ -4126,6 +4110,9 @@ bool platformKeyUpRead(PlatformKeyEventT *evt);</code></pre>
<h3>platformPathBaseName</h3> <h3>platformPathBaseName</h3>
<pre><code>const char *platformPathBaseName(const char *path);</code></pre> <pre><code>const char *platformPathBaseName(const char *path);</code></pre>
<p>Return a pointer to the leaf (basename) portion of path.</p> <p>Return a pointer to the leaf (basename) portion of path.</p>
<h3>platformCopyFile</h3>
<pre><code>bool platformCopyFile(const char *srcPath, const char *dstPath);</code></pre>
<p>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.</p>
<h3>platformReadFile</h3> <h3>platformReadFile</h3>
<pre><code>char *platformReadFile(const char *path, int32_t *outLen);</code></pre> <pre><code>char *platformReadFile(const char *path, int32_t *outLen);</code></pre>
<p>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.</p> <p>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.</p>
@ -5622,9 +5609,7 @@ BasValueT basValToBool(BasValueT v);</code></pre>
basStringConcat(a, b) Concatenate two strings. Returns a new string (refCount 1). 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). basStringSub(s, start, len) Extract a substring. Returns a new string (refCount 1).
basStringCompare(a, b) Compare. Returns &lt;0, 0, &gt;0 (like strcmp). basStringCompare(a, b) Compare. Returns &lt;0, 0, &gt;0 (like strcmp).
basStringCompareCI(a, b) Case-insensitive compare. basStringCompareCI(a, b) Case-insensitive compare.</pre>
basStringSystemInit() Initialize the string system and empty string singleton.
basStringSystemShutdown() Shut down the string system.</pre>
<p>The global basEmptyString is a singleton that is never freed.</p> <p>The global basEmptyString is a singleton that is never freed.</p>
<h2>BasArrayT</h2> <h2>BasArrayT</h2>
<p>Reference-counted multi-dimensional array (up to BAS_ARRAY_MAX_DIMS = 8 dimensions).</p> <p>Reference-counted multi-dimensional array (up to BAS_ARRAY_MAX_DIMS = 8 dimensions).</p>
@ -5680,20 +5665,18 @@ BasValueT basValToBool(BasValueT v);</code></pre>
---- ----- ----------- ---- ----- -----------
BAS_VM_OK 0 Program completed normally. BAS_VM_OK 0 Program completed normally.
BAS_VM_HALTED 1 HALT instruction reached. BAS_VM_HALTED 1 HALT instruction reached.
BAS_VM_YIELDED 2 DoEvents yielded control. BAS_VM_ERROR 2 Runtime error.
BAS_VM_ERROR 3 Runtime error. BAS_VM_STACK_OVERFLOW 3 Evaluation stack overflow.
BAS_VM_STACK_OVERFLOW 4 Evaluation stack overflow. BAS_VM_STACK_UNDERFLOW 4 Evaluation stack underflow.
BAS_VM_STACK_UNDERFLOW 5 Evaluation stack underflow. BAS_VM_CALL_OVERFLOW 5 Call stack overflow.
BAS_VM_CALL_OVERFLOW 6 Call stack overflow. BAS_VM_DIV_BY_ZERO 6 Division by zero.
BAS_VM_DIV_BY_ZERO 7 Division by zero. BAS_VM_TYPE_MISMATCH 7 Type mismatch in operation.
BAS_VM_TYPE_MISMATCH 8 Type mismatch in operation. BAS_VM_OUT_OF_MEMORY 8 Memory allocation failed.
BAS_VM_OUT_OF_MEMORY 9 Memory allocation failed. BAS_VM_BAD_OPCODE 9 Unknown opcode encountered.
BAS_VM_BAD_OPCODE 10 Unknown opcode encountered. BAS_VM_FILE_ERROR 10 File I/O error.
BAS_VM_FILE_ERROR 11 File I/O error. BAS_VM_SUBSCRIPT_RANGE 11 Array subscript out of range.
BAS_VM_SUBSCRIPT_RANGE 12 Array subscript out of range. BAS_VM_STEP_LIMIT 12 Step limit reached (not an error).
BAS_VM_USER_ERROR 13 ON ERROR raised by program. BAS_VM_BREAKPOINT 13 Breakpoint or step completed (not an error).</pre>
BAS_VM_STEP_LIMIT 14 Step limit reached (not an error).
BAS_VM_BREAKPOINT 15 Breakpoint or step completed (not an error).</pre>
<h2>Lifecycle</h2> <h2>Lifecycle</h2>
<pre><code>BasVmT *basVmCreate(void); <pre><code>BasVmT *basVmCreate(void);
void basVmDestroy(BasVmT *vm); void basVmDestroy(BasVmT *vm);

View file

@ -34,7 +34,9 @@
DJGPP_PREFIX = $(HOME)/djgpp/djgpp DJGPP_PREFIX = $(HOME)/djgpp/djgpp
CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc 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 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 OBJDIR = ../../../../obj/dvxbasic
LIBSDIR = ../../../../bin/libs LIBSDIR = ../../../../bin/libs
@ -61,7 +63,11 @@ STUB_TARGET = $(OBJDIR)/basstub.app
# Native test programs (host gcc, not cross-compiled) # Native test programs (host gcc, not cross-compiled)
HOSTCC = gcc HOSTCC = gcc
HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -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_COMPILER = $(HOSTDIR)/test_compiler
TEST_VM = $(HOSTDIR)/test_vm 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_LEX_SRCS = test_lex.c compiler/lexer.c
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL) TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_COMPACT_SRCS = test_compact.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL) TEST_COMPACT_SRCS = test_compact.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_SUITE_SRCS = test_suite.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL) 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) # Command-line compiler (host tool)
BASCOMP_SRCS = stub/bascomp.c basBuild.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/obfuscate.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c ../../../libs/kpunch/libdvx/dvxPrefs.c ../../../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL) BASCOMP_SRCS = stub/bascomp.c basBuild.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/obfuscate.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c ../../../libs/kpunch/libdvx/dvxPrefs.c ../../../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
@ -85,7 +91,7 @@ BASCOMP_TARGET = $(HOSTDIR)/bascomp
# DOS command-line compiler # DOS command-line compiler
DOSCC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc 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 EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff
CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE
SYSTEMDIR = ../../../../bin/system SYSTEMDIR = ../../../../bin/system
@ -107,37 +113,40 @@ tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(T
$(TEST_COMPACT) $(TEST_COMPACT)
$(TEST_SUITE) $(TEST_SUITE)
$(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(HOSTDIR) $(TEST_COMPILER): $(TEST_COMPILER_SRCS) $(HEADERS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm $(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 $(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 $(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 $(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 $(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 $(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPACT_SRCS) -lm
# Host command-line compiler -- basstub.app is appended as a STUB # Host command-line compiler -- basstub.app is appended as a STUB
# resource so bascomp is self-contained (no BASSTUB.APP companion file). # resource so bascomp is self-contained (no BASSTUB.APP companion file),
$(BASCOMP_TARGET): $(BASCOMP_SRCS) ../../../tools/dvxResWrite.h $(STUB_TARGET) $(DVXRES) | $(HOSTDIR) # 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 $(HOSTCC) $(HOSTCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $@ $(BASCOMP_SRCS) -lm
$(DVXRES) add $@ STUB binary @$(STUB_TARGET) $(DVXRES) add $@ STUB binary @$(STUB_TARGET)
$(DVXRES) add $@ noicon icon @noicon.bmp
# DOS command-line compiler (same STUB embed as the host build) # DOS command-line compiler (same STUB / noicon embed as the host build)
$(SYSTEMDIR)/BASCOMP.EXE: $(BASCOMP_SRCS) ../../../tools/dvxResWrite.h $(STUB_TARGET) $(DVXRES) | $(SYSTEMDIR) $(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 $(DOSCC) $(DOSCFLAGS) -DBASCOMP_STANDALONE -I../../../tools -o $(SYSTEMDIR)/bascomp.exe $(BASCOMP_SRCS) -lm
$(EXE2COFF) $(SYSTEMDIR)/bascomp.exe $(EXE2COFF) $(SYSTEMDIR)/bascomp.exe
cat $(CWSDSTUB) $(SYSTEMDIR)/bascomp > $@ cat $(CWSDSTUB) $(SYSTEMDIR)/bascomp > $@
rm -f $(SYSTEMDIR)/bascomp $(SYSTEMDIR)/bascomp.exe rm -f $(SYSTEMDIR)/bascomp $(SYSTEMDIR)/bascomp.exe
$(DVXRES) add $@ STUB binary @$(STUB_TARGET) $(DVXRES) add $@ STUB binary @$(STUB_TARGET)
$(DVXRES) add $@ noicon icon @noicon.bmp
$(HOSTDIR): $(HOSTDIR):
mkdir -p $(HOSTDIR) mkdir -p $(HOSTDIR)
@ -154,7 +163,7 @@ $(RT_TARGETDIR)/basrt.dep: basrt.dep | $(RT_TARGETDIR)
sed 's/$$/\r/' $< > $@ sed 's/$$/\r/' $< > $@
# Standalone stub DXE (embedded as resource in IDE app) # Standalone stub DXE (embedded as resource in IDE app)
$(STUB_TARGET): $(STUB_OBJS) | $(APPDIR) $(STUB_TARGET): $(STUB_OBJS) | $(OBJDIR)
$(DXE3GEN) -o $@ -U $(STUB_OBJS) $(DXE3GEN) -o $@ -U $(STUB_OBJS)
# IDE app DXE (compiler linked in, runtime from basrt.lib, stub embedded) # 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) $(DVXRES) add $@ STUB binary @$(STUB_TARGET)
# Object files # Object files. One pattern rule per source directory; header
$(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR) # 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 $@ $< $(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 $@ $< $(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/frmParser.o: formrt/frmParser.c formrt/frmParser.h | $(OBJDIR) $(OBJDIR)/%.o: formrt/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(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 $@ $< $(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 $@ $< $(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 $@ $< $(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/compact.o: compiler/compact.c compiler/compact.h compiler/opcodes.h runtime/vm.h | $(OBJDIR) -include $(wildcard $(OBJDIR)/*.d)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/basBuild.o: basBuild.c basBuild.h basRes.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/basstub.o: stub/basstub.c runtime/vm.h runtime/serialize.h formrt/formrt.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideMenuEditor.o: ide/ideMenuEditor.c ide/ideMenuEditor.h ide/ideDesigner.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideMenuEditor.h ide/ideProject.h ide/ideToolbox.h ide/ideProperties.h compiler/parser.h runtime/vm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideProject.o: ide/ideProject.c ide/ideProject.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideToolbox.o: ide/ideToolbox.c ide/ideToolbox.h ide/ideDesigner.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/lexer.o: compiler/lexer.c compiler/lexer.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/parser.o: compiler/parser.c compiler/parser.h compiler/lexer.h compiler/codegen.h compiler/symtab.h compiler/opcodes.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/symtab.o: compiler/symtab.c compiler/symtab.h compiler/opcodes.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/values.o: runtime/values.c runtime/values.h compiler/opcodes.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/vm.o: runtime/vm.c runtime/vm.h runtime/values.h compiler/opcodes.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
# Directories # Directories
$(OBJDIR): $(OBJDIR):
@ -239,5 +210,5 @@ $(APPDIR):
mkdir -p $(APPDIR) mkdir -p $(APPDIR)
clean: 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 -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) rm -f $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(TEST_SUITE)

View file

@ -20,36 +20,67 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE. // 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 // See basBuild.h for the public contract. This pipeline used to live
// in two places: ideMain.c (the IDE's Make Executable path) and // inline in two places: ideMain.c (the IDE's Make Executable path) and
// bascomp.c (the standalone command-line compiler). Both implementations // 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: // Pipeline:
// 1. BAS_RES_NAME (always, falls back to "BASIC App") // 1. Strip comments from every .frm text.
// 2. BAS_RES_AUTHOR / PUBLISHER / VERSION / COPYRIGHT / DESCRIPTION // 2. Serialize the module; release builds deserialize a private copy,
// (each skipped if empty) // strip its debug info, obfuscate form/control names (rewriting the
// 3. BAS_RES_ICON32 (file via iconPath, else iconData bytes) // .frm texts too), compact the bytecode and re-serialize.
// 4. BAS_RES_HELPFILE (filename only, if helpFile set) // 3. Debug builds serialize the debug info.
// 5. BAS_RES_MODULE (bytecode) // 4. Extract the STUB resource from selfPath and write it as outPath.
// 6. BAS_RES_DEBUG (optional) // 5. Append the resource set in canonical order:
// 7. FORM0, FORM1, ... (one per spec->formCount) // 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 "basBuild.h"
#include "basRes.h" #include "basRes.h"
#include "../../../libs/kpunch/libdvx/dvxRes.h" #include "compiler/compact.h"
#include "../../../libs/kpunch/libdvx/platform/dvxPlat.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 <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
// Slack added to a .frm buffer for basStripFrmComments output.
#define BAS_BUILD_STRIP_MARGIN 16
// ------------------------------------------------------------ // Longest progress line handed to the log callback.
// Internal helpers #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) { 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) { const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) {
// If a disk path was given, load it and embed. Otherwise use the if (!outPath || !spec || !spec->module || !spec->selfPath) {
// pre-loaded bytes (used by the IDE for its "noicon" fallback). return "Invalid build request.";
if (spec->iconPath && spec->iconPath[0]) { }
// 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 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);
}
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);
}
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;
}
static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...) {
if (!spec->log) {
return;
}
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; int32_t iconLen = 0;
char *iconData = platformReadFile(spec->iconPath, &iconLen);
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) { if (!iconData) {
// A named icon that cannot be read is a build failure -- // A named icon that cannot be read is a build failure --
// returning success here would ship the app without its // shipping the app without its ICON32 resource would never
// ICON32 resource and never tell anyone. // tell anyone.
return -1; return -1;
} }
} else {
DvxResHandleT *selfRes = dvxResOpen(spec->selfPath);
int32_t rc = dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen); 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); free(iconData);
return rc; return rc;
} }
if (spec->iconData && spec->iconSize > 0) {
return dvxResAppend(path, BAS_RES_ICON32, DVX_RES_ICON, spec->iconData, (uint32_t)spec->iconSize);
}
return 0; 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.
// ------------------------------------------------------------
// Public API
// ------------------------------------------------------------
int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec) {
if (!outPath || !spec) {
return -1;
}
// 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.
int32_t rc = 0; int32_t rc = 0;
// Project metadata. Name is required -- fall back to a generic label // 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_COPYRIGHT, spec->copyright);
rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description); rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description);
// Icon.
rc |= emitIcon(outPath, spec); rc |= emitIcon(outPath, spec);
// Help file name (just the basename -- stub resolves it next to the app).
if (spec->helpFile && spec->helpFile[0]) { if (spec->helpFile && spec->helpFile[0]) {
const char *helpBase = platformPathBaseName(spec->helpFile); rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile));
rc |= appendText(outPath, BAS_RES_HELPFILE, helpBase);
} }
// Bytecode module. rc |= dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen);
if (spec->moduleData && spec->moduleLen > 0) {
rc |= dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, spec->moduleData, (uint32_t)spec->moduleLen); if (dbgData && dbgLen > 0) {
rc |= dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen);
} }
// Optional debug info. // Form resources. Release builds embed the obfuscated variant when
if (spec->debugData && spec->debugLen > 0) { // one was produced. Number the OUTPUT densely with outIdx rather
rc |= dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, spec->debugData, (uint32_t)spec->debugLen); // 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 resources. Callers pre-strip / pre-obfuscate as needed; we just // form.
// 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.
int32_t outIdx = 0; int32_t outIdx = 0;
for (int32_t i = 0; i < spec->formCount; i++) { for (int32_t i = 0; i < frmCount; i++) {
if (!spec->formData || !spec->formData[i] || !spec->formLens || spec->formLens[i] <= 0) { 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; continue;
} }
@ -154,11 +306,105 @@ int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec) {
break; 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); 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++; outIdx++;
} }
return rc; 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;
}

View file

@ -20,65 +20,68 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE. // 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 // Both the DVX BASIC IDE (ideMain.c, Make Executable) and the standalone
// compiler (bascomp.c) need to attach the same group of resources to // command-line compiler (bascomp.c) turn a compiled module plus a set of
// an output .app file after writing the stub DXE. This header exposes // .frm texts into a finished .app. The whole back half of that job --
// a single function that takes the metadata, bytecode, debug info and // release stripping, form/control name obfuscation, bytecode compaction,
// form texts and appends all resources in the canonical order. // 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: // Callers own every buffer they pass in via BasBuildSpecT; the module is
// - writing the stub to outPath before calling us // never modified (release builds work on a private copy).
// - copying the .hlp file next to outPath (if any)
// - freeing any buffers they passed in via BasBuildSpecT
//
// Resource names use BAS_RES_* from basRes.h so both callers stay in
// sync automatically.
#ifndef BAS_BUILD_H #ifndef BAS_BUILD_H
#define BAS_BUILD_H #define BAS_BUILD_H
#include "runtime/vm.h"
#include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
// Progress callback (may be NULL); receives one line per pipeline stage.
typedef void (*BasBuildLogFnT)(const char *msg);
typedef struct { 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 *projName;
const char *author; const char *author;
const char *publisher; const char *publisher;
const char *version; const char *version;
const char *copyright; const char *copyright;
const char *description; const char *description;
const char *helpFile; // just the filename, not a path
// Icon: either a file to load and embed, OR pre-loaded bytes. // Project directory; iconPath and helpFile are relative to it.
// If iconPath is set (non-NULL and non-empty) it is read from disk. const char *projectDir;
// Otherwise iconData / iconSize are used (may themselves be NULL/0). const char *iconPath; // NULL/empty: embed the NOICON fallback
const char *iconPath; const char *helpFile; // NULL/empty: no help file
const void *iconData;
int32_t iconSize;
// Bytecode module and optional debug info. // Path of the running executable; it carries the STUB and NOICON
const void *moduleData; // resources.
int32_t moduleLen; const char *selfPath;
const void *debugData; // may be NULL
int32_t debugLen;
// Forms: parallel arrays of formCount entries, each a text blob. // Compiled module. Never modified.
// formData[i] points at formLens[i] bytes of (already stripped / const BasModuleT *module;
// possibly obfuscated) form source to embed as resource FORMi.
int32_t formCount; // Raw .frm texts (NUL-terminated), one per form. Comments are
const uint8_t *const *formData; // stripped here; release builds additionally obfuscate them.
const int32_t *formLens; const char *const *frmSources;
int32_t frmCount;
bool release;
BasBuildLogFnT log; // may be NULL
} BasBuildSpecT; } BasBuildSpecT;
// Append the full resource set to outPath. Returns 0 on success, non-zero // Build outPath from spec. Returns NULL on success, otherwise a static
// on failure. The stub must already have been written to outPath. // error message. On failure outPath may be left partially written.
int32_t basBuildEmitResources(const char *outPath, const BasBuildSpecT *spec); const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec);
#ifdef __cplusplus #ifdef __cplusplus
} }

View file

@ -87,11 +87,13 @@ const char *dvxSkipWs(const char *s);
// Stub DXE embedded in the IDE app for release builds // Stub DXE embedded in the IDE app for release builds
#define BAS_RES_STUB "STUB" #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 // 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 // basBuild writer and the basstub reader so the naming cannot drift; %ld is
// the dense form index. // the dense form index.
#define BAS_RES_FORM_FMT "FORM%ld" #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 // Form resource scan limit
@ -105,8 +107,11 @@ const char *dvxSkipWs(const char *s);
// Begin-Form name extraction (shared by bascomp and basstub) // Begin-Form name extraction (shared by bascomp and basstub)
// ------------------------------------------------------------ // ------------------------------------------------------------
// Length of the "Begin Form " keyword prefix scanned for below. // Lengths of the .frm keywords scanned for below.
#define BAS_BEGIN_FORM_PREFIX_LEN 11 #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 <name>" line in .frm text. // Extract the form name from a "Begin Form <name>" line in .frm text.
// Writes a NUL-terminated name into nameBuf (max nameBufSize bytes, // 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; 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 #endif // BAS_RES_H

View file

@ -178,8 +178,6 @@ typedef struct {
basStringSub(s, start, len) Extract a substring. 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). basStringCompare(a, b) Compare. Returns <0, 0, >0 (like strcmp).
basStringCompareCI(a, b) Case-insensitive compare. basStringCompareCI(a, b) Case-insensitive compare.
basStringSystemInit() Initialize the string system and empty string singleton.
basStringSystemShutdown() Shut down the string system.
.endtable .endtable
The global basEmptyString is a singleton that is never freed. 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_OK 0 Program completed normally.
BAS_VM_HALTED 1 HALT instruction reached. BAS_VM_HALTED 1 HALT instruction reached.
BAS_VM_YIELDED 2 DoEvents yielded control. BAS_VM_ERROR 2 Runtime error.
BAS_VM_ERROR 3 Runtime error. BAS_VM_STACK_OVERFLOW 3 Evaluation stack overflow.
BAS_VM_STACK_OVERFLOW 4 Evaluation stack overflow. BAS_VM_STACK_UNDERFLOW 4 Evaluation stack underflow.
BAS_VM_STACK_UNDERFLOW 5 Evaluation stack underflow. BAS_VM_CALL_OVERFLOW 5 Call stack overflow.
BAS_VM_CALL_OVERFLOW 6 Call stack overflow. BAS_VM_DIV_BY_ZERO 6 Division by zero.
BAS_VM_DIV_BY_ZERO 7 Division by zero. BAS_VM_TYPE_MISMATCH 7 Type mismatch in operation.
BAS_VM_TYPE_MISMATCH 8 Type mismatch in operation. BAS_VM_OUT_OF_MEMORY 8 Memory allocation failed.
BAS_VM_OUT_OF_MEMORY 9 Memory allocation failed. BAS_VM_BAD_OPCODE 9 Unknown opcode encountered.
BAS_VM_BAD_OPCODE 10 Unknown opcode encountered. BAS_VM_FILE_ERROR 10 File I/O error.
BAS_VM_FILE_ERROR 11 File I/O error. BAS_VM_SUBSCRIPT_RANGE 11 Array subscript out of range.
BAS_VM_SUBSCRIPT_RANGE 12 Array subscript out of range. BAS_VM_STEP_LIMIT 12 Step limit reached (not an error).
BAS_VM_USER_ERROR 13 ON ERROR raised by program. BAS_VM_BREAKPOINT 13 Breakpoint or step completed (not an error).
BAS_VM_STEP_LIMIT 14 Step limit reached (not an error).
BAS_VM_BREAKPOINT 15 Breakpoint or step completed (not an error).
.endtable .endtable
.h2 Lifecycle .h2 Lifecycle

View file

@ -26,16 +26,94 @@
// event handler (Ctrl_Load, Form_Click, etc). Referenced by the stripper // event handler (Ctrl_Load, Form_Click, etc). Referenced by the stripper
// (to retain handlers in release builds), the obfuscator (to preserve event // (to retain handlers in release builds), the obfuscator (to preserve event
// naming in rewritten forms), and the IDE (to populate the Object/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 // dropdowns and generate handler stubs). Keep them in one place so adding
// one file. // 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 #ifndef BAS_EVENTS_H
#define BAS_EVENTS_H #define BAS_EVENTS_H
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h>
// NULL-terminated list of event suffixes. Case-insensitive match. typedef enum {
// Declared extern here, defined once in strip.c. 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[]; extern const char *basEventSuffixes[];
// True when suffix case-insensitively equals one of basEventSuffixes. // True when suffix case-insensitively equals one of basEventSuffixes.

View file

@ -50,11 +50,18 @@ void basEmitFloat(BasCodeGenT *cg, float v);
void basEmitU16(BasCodeGenT *cg, uint16_t v); void basEmitU16(BasCodeGenT *cg, uint16_t v);
const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name); const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val); 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) { 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++) { 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; 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; uint16_t idx = (uint16_t)cg->constCount;
BasStringT *s = basStringNew(text, len); BasStringT *s = basStringNew(text, len);
arrput(cg->constants, s); arrput(cg->constants, s);
arrput(cg->constHashes, h);
cg->constCount = (int32_t)arrlen(cg->constants); cg->constCount = (int32_t)arrlen(cg->constants);
return idx; 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) { void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex, const char *formName) {
BasDebugVarT dv; BasDebugVarT dv;
memset(&dv, 0, sizeof(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.scope = scope;
dv.dataType = dataType; dv.dataType = dataType;
dv.index = index; dv.index = index;
dv.procIndex = procIndex; dv.procIndex = procIndex;
if (formName && formName[0]) { 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); arrput(cg->debugVars, dv);
cg->debugVarCount = (int32_t)arrlen(cg->debugVars); 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) { 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)); BasModuleT *mod = (BasModuleT *)calloc(1, sizeof(BasModuleT));
if (!mod) { if (!mod) {
@ -232,11 +247,11 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
for (int32_t i = 0; i < tab->count; i++) { for (int32_t i = 0; i < tab->count; i++) {
BasSymbolT *s = tab->symbols[i]; BasSymbolT *s = tab->symbols[i];
if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) { if (isEmittableProc(s)) {
procCount++; procCount++;
} }
if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) { if (needsGlobalInit(s)) {
globalInitCount++; globalInitCount++;
} }
} }
@ -250,7 +265,7 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
for (int32_t i = 0; i < tab->count; i++) { for (int32_t i = 0; i < tab->count; i++) {
BasSymbolT *s = tab->symbols[i]; BasSymbolT *s = tab->symbols[i];
if (s->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].index = s->index;
mod->globalInits[gi].dataType = s->dataType; mod->globalInits[gi].dataType = s->dataType;
gi++; gi++;
@ -276,12 +291,12 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
for (int32_t i = 0; i < tab->count; i++) { for (int32_t i = 0; i < tab->count; i++) {
BasSymbolT *s = tab->symbols[i]; BasSymbolT *s = tab->symbols[i];
if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) { if (isEmittableProc(s)) {
BasProcEntryT *p = &mod->procs[idx++]; BasProcEntryT *p = &mod->procs[idx++];
strncpy(p->name, s->name, BAS_MAX_PROC_NAME - 1); strncpy(p->name, s->name, BAS_MAX_IDENT - 1);
p->name[BAS_MAX_PROC_NAME - 1] = '\0'; p->name[BAS_MAX_IDENT - 1] = '\0';
strncpy(p->formName, s->formName, BAS_MAX_PROC_NAME - 1); strncpy(p->formName, s->formName, BAS_MAX_IDENT - 1);
p->formName[BAS_MAX_PROC_NAME - 1] = '\0'; p->formName[BAS_MAX_IDENT - 1] = '\0';
p->codeAddr = s->codeAddr; p->codeAddr = s->codeAddr;
p->paramCount = s->paramCount; p->paramCount = s->paramCount;
p->localCount = s->localCount; p->localCount = s->localCount;
@ -324,6 +339,7 @@ void basCodeGenFree(BasCodeGenT *cg) {
arrfree(cg->code); arrfree(cg->code);
arrfree(cg->constants); arrfree(cg->constants);
arrfree(cg->constHashes);
arrfree(cg->dataPool); arrfree(cg->dataPool);
arrfree(cg->formVarInfo); arrfree(cg->formVarInfo);
arrfree(cg->debugVars); arrfree(cg->debugVars);
@ -333,18 +349,10 @@ void basCodeGenFree(BasCodeGenT *cg) {
} }
arrfree(cg->debugUdtDefs); arrfree(cg->debugUdtDefs);
cg->code = NULL;
cg->constants = NULL; // Leave the generator in the same state as basCodeGenInit so a reuse
cg->dataPool = NULL; // after Free starts clean (no stale counts or overflow flag).
cg->formVarInfo = NULL; basCodeGenInit(cg);
cg->debugVars = NULL;
cg->debugUdtDefs = NULL;
cg->constCount = 0;
cg->dataCount = 0;
cg->codeLen = 0;
cg->formVarInfoCount = 0;
cg->debugVarCount = 0;
cg->debugUdtDefCount = 0;
} }
@ -415,3 +423,30 @@ void basPatch16(BasCodeGenT *cg, int32_t pos, int16_t val) {
memcpy(&cg->code[pos], &val, 2); 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;
}

View file

@ -40,11 +40,6 @@
// Constant pool index is emitted as uint16_t; pool cannot exceed this. // Constant pool index is emitted as uint16_t; pool cannot exceed this.
#define BAS_MAX_CONSTANTS 0x10000 #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 // Code generator state
// ============================================================ // ============================================================
@ -53,6 +48,7 @@ typedef struct {
uint8_t *code; // stb_ds dynamic array uint8_t *code; // stb_ds dynamic array
int32_t codeLen; int32_t codeLen;
BasStringT **constants; // stb_ds dynamic array 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 constCount;
int32_t globalCount; int32_t globalCount;
BasValueT *dataPool; // stb_ds dynamic array BasValueT *dataPool; // stb_ds dynamic array

View file

@ -26,6 +26,11 @@
// each), and rewrites all code-address references so control flow // each), and rewrites all code-address references so control flow
// still lands on the correct instructions. // 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: // Address references:
// - BasProcEntryT::codeAddr (absolute) // - BasProcEntryT::codeAddr (absolute)
// - BasFormVarInfoT::initCodeAddr (absolute, negative = no init) // - BasFormVarInfoT::initCodeAddr (absolute, negative = no init)
@ -48,14 +53,15 @@
#define BAS_OPERAND_U16_MAX 0xFFFF // max absolute address encodable in a uint16 operand #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) // Function prototypes (alphabetical)
int32_t basCompactBytecode(BasModuleT *mod); int32_t basCompactBytecode(BasModuleT *mod);
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);
static bool isGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos);
static int32_t opOperandSize(uint8_t op);
static int16_t readI16LE(const uint8_t *p); static int16_t readI16LE(const uint8_t *p);
static int32_t readI32LE(const uint8_t *p);
static uint16_t readU16LE(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 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); 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; const uint8_t *oldCode = mod->code;
int32_t oldCodeLen = mod->codeLen; 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; int32_t lineCount = 0;
bool keepStmt = false;
{ {
int32_t pc = 0; int32_t pc = 0;
while (pc < oldCodeLen) { while (pc < oldCodeLen) {
uint8_t op = oldCode[pc]; uint8_t op = oldCode[pc];
int32_t operand = opOperandSize(op); int32_t operand = basOpcodeOperandSize(op);
if (operand < 0 || pc + 1 + operand > oldCodeLen) { if (operand < 0 || pc + 1 + operand > oldCodeLen) {
return 0; // unknown opcode -- skip compaction return 0; // unknown opcode -- skip compaction
@ -88,6 +96,10 @@ int32_t basCompactBytecode(BasModuleT *mod) {
lineCount++; lineCount++;
} }
if (op == OP_ON_ERROR || op == OP_RESUME || op == OP_RESUME_NEXT) {
keepStmt = true;
}
pc += 1 + operand; pc += 1 + operand;
} }
@ -101,7 +113,7 @@ int32_t basCompactBytecode(BasModuleT *mod) {
} }
int32_t newCodeLen = 0; int32_t newCodeLen = 0;
int32_t *remap = buildRemap(oldCode, oldCodeLen, &newCodeLen); int32_t *remap = buildRemap(oldCode, oldCodeLen, keepStmt, &newCodeLen);
if (!remap) { if (!remap) {
return 0; return 0;
@ -114,22 +126,25 @@ int32_t basCompactBytecode(BasModuleT *mod) {
return 0; return 0;
} }
// Copy bytes (skipping OP_LINE) and rewrite address operands. // Copy bytes (dropping or shrinking OP_LINE) and rewrite address operands.
bool ok = true; bool ok = true;
int32_t oldPc = 0; int32_t oldPc = 0;
while (oldPc < oldCodeLen && ok) { while (oldPc < oldCodeLen && ok) {
uint8_t op = oldCode[oldPc]; uint8_t op = oldCode[oldPc];
int32_t operand = opOperandSize(op); int32_t operand = basOpcodeOperandSize(op);
int32_t instSize = 1 + operand; int32_t instSize = 1 + operand;
int32_t newPc = remap[oldPc];
if (op == OP_LINE) { if (op == OP_LINE) {
if (keepStmt) {
newCode[newPc] = OP_STMT;
}
oldPc += instSize; oldPc += instSize;
continue; continue;
} }
int32_t newPc = remap[oldPc];
// Copy the instruction verbatim first; we'll overwrite operands that // Copy the instruction verbatim first; we'll overwrite operands that
// need remapping below. // need remapping below.
memcpy(newCode + newPc, oldCode + oldPc, instSize); memcpy(newCode + newPc, oldCode + oldPc, instSize);
@ -198,8 +213,8 @@ int32_t basCompactBytecode(BasModuleT *mod) {
case OP_PUSH_INT32: { case OP_PUSH_INT32: {
// Detect GOSUB return-address push and remap the absolute address. // Detect GOSUB return-address push and remap the absolute address.
if (isGosubPush(oldCode, oldCodeLen, oldPc)) { if (basIsGosubPush(oldCode, oldCodeLen, oldPc)) {
int32_t oldAddr = readI32LE(oldCode + oldPc + 1); int32_t oldAddr = basReadI32LE(oldCode + oldPc + 1);
if (oldAddr < 0 || oldAddr > oldCodeLen) { if (oldAddr < 0 || oldAddr > oldCodeLen) {
ok = false; ok = false;
@ -295,12 +310,13 @@ int32_t basCompactBytecode(BasModuleT *mod) {
// ============================================================ // ============================================================
// //
// remap[oldPos] = newPos for every byte position in [0, oldCodeLen]. // remap[oldPos] = newPos for every byte position in [0, oldCodeLen].
// For OP_LINE bytes (removed): remap points at where the NEXT instruction // For OP_LINE bytes: when keepStmt is set the opcode byte maps to the
// starts in the new code. // 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. // Final entry remap[oldCodeLen] = newCodeLen.
// //
// Returns malloc'd array of size (oldCodeLen + 1), or NULL on failure. // 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)); int32_t *remap = (int32_t *)malloc((codeLen + 1) * sizeof(int32_t));
if (!remap) { if (!remap) {
@ -312,7 +328,7 @@ static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, int32_t *outNew
while (oldPc < codeLen) { while (oldPc < codeLen) {
uint8_t op = code[oldPc]; uint8_t op = code[oldPc];
int32_t operand = opOperandSize(op); int32_t operand = basOpcodeOperandSize(op);
if (operand < 0) { if (operand < 0) {
free(remap); free(remap);
@ -327,8 +343,16 @@ static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, int32_t *outNew
} }
if (op == OP_LINE) { if (op == OP_LINE) {
// These bytes are removed; they map to where the next instruction starts. // Dropped entirely, or shrunk to OP_STMT: the opcode byte maps
for (int32_t i = 0; i < instSize; i++) { // 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; remap[oldPc + i] = newPc;
} }
} else { } 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: <next instruction>
// The invariant is V == oldPc + 8 (the pushed return address).
//
// Returns true if the given position is the start of such a pattern.
static bool isGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos) {
if (pos + 8 > codeLen) {
return false;
}
if (code[pos] != OP_PUSH_INT32) {
return false;
}
if (code[pos + 5] != OP_JMP) {
return false;
}
int32_t value = readI32LE(code + pos + 1);
return value == pos + 8;
}
// ============================================================
// Opcode operand size table
// ============================================================
// Returns operand byte count (excluding the 1-byte opcode), or -1 if unknown.
// 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) // 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) { static uint16_t readU16LE(const uint8_t *p) {
return (uint16_t)p[0] | ((uint16_t)p[1] << 8); return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
} }

View file

@ -28,8 +28,10 @@
// are handled transparently. // are handled transparently.
#include "lexer.h" #include "lexer.h"
#include "opcodes.h"
#include <ctype.h> #include <ctype.h>
#include <errno.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -60,11 +62,11 @@ static const KeywordEntryT sKeywords[] = {
KW("CHDIR", TOK_CHDIR), KW("CHDIR", TOK_CHDIR),
KW("CHDRIVE", TOK_CHDRIVE), KW("CHDRIVE", TOK_CHDRIVE),
KW("CLOSE", TOK_CLOSE), KW("CLOSE", TOK_CLOSE),
KW("CONST", TOK_CONST),
KW("CREATECONTROL", TOK_CREATECONTROL), KW("CREATECONTROL", TOK_CREATECONTROL),
KW("CREATEFORM", TOK_CREATEFORM), KW("CREATEFORM", TOK_CREATEFORM),
KW("CURDIR", TOK_CURDIR), KW("CURDIR", TOK_CURDIR),
KW("CURDIR$", TOK_CURDIR), KW("CURDIR$", TOK_CURDIR),
KW("CONST", TOK_CONST),
KW("DATA", TOK_DATA), KW("DATA", TOK_DATA),
KW("DECLARE", TOK_DECLARE), KW("DECLARE", TOK_DECLARE),
KW("DEF", TOK_DEF), KW("DEF", TOK_DEF),
@ -87,8 +89,8 @@ static const KeywordEntryT sKeywords[] = {
KW("ERASE", TOK_ERASE), KW("ERASE", TOK_ERASE),
KW("ERR", TOK_ERR), KW("ERR", TOK_ERR),
KW("ERROR", TOK_ERROR_KW), KW("ERROR", TOK_ERROR_KW),
KW("EXPLICIT", TOK_EXPLICIT),
KW("EXIT", TOK_EXIT), KW("EXIT", TOK_EXIT),
KW("EXPLICIT", TOK_EXPLICIT),
KW("FALSE", TOK_FALSE_KW), KW("FALSE", TOK_FALSE_KW),
KW("FILECOPY", TOK_FILECOPY), KW("FILECOPY", TOK_FILECOPY),
KW("FILELEN", TOK_FILELEN), KW("FILELEN", TOK_FILELEN),
@ -105,6 +107,8 @@ static const KeywordEntryT sKeywords[] = {
KW("INIREAD$", TOK_INIREAD), KW("INIREAD$", TOK_INIREAD),
KW("INIWRITE", TOK_INIWRITE), KW("INIWRITE", TOK_INIWRITE),
KW("INPUT", TOK_INPUT), KW("INPUT", TOK_INPUT),
KW("INPUTBOX", TOK_INPUTBOX),
KW("INPUTBOX$", TOK_INPUTBOX),
KW("INTEGER", TOK_INTEGER), KW("INTEGER", TOK_INTEGER),
KW("IS", TOK_IS), KW("IS", TOK_IS),
KW("KILL", TOK_KILL), KW("KILL", TOK_KILL),
@ -117,8 +121,6 @@ static const KeywordEntryT sKeywords[] = {
KW("ME", TOK_ME), KW("ME", TOK_ME),
KW("MKDIR", TOK_MKDIR), KW("MKDIR", TOK_MKDIR),
KW("MOD", TOK_MOD), KW("MOD", TOK_MOD),
KW("INPUTBOX", TOK_INPUTBOX),
KW("INPUTBOX$", TOK_INPUTBOX),
KW("MSGBOX", TOK_MSGBOX), KW("MSGBOX", TOK_MSGBOX),
KW("NAME", TOK_NAME), KW("NAME", TOK_NAME),
KW("NEXT", TOK_NEXT), KW("NEXT", TOK_NEXT),
@ -126,8 +128,8 @@ static const KeywordEntryT sKeywords[] = {
KW("NOTHING", TOK_NOTHING), KW("NOTHING", TOK_NOTHING),
KW("ON", TOK_ON), KW("ON", TOK_ON),
KW("OPEN", TOK_OPEN), KW("OPEN", TOK_OPEN),
KW("OPTIONAL", TOK_OPTIONAL),
KW("OPTION", TOK_OPTION), KW("OPTION", TOK_OPTION),
KW("OPTIONAL", TOK_OPTIONAL),
KW("OR", TOK_OR), KW("OR", TOK_OR),
KW("OUTPUT", TOK_OUTPUT), KW("OUTPUT", TOK_OUTPUT),
KW("PRESERVE", TOK_PRESERVE), KW("PRESERVE", TOK_PRESERVE),
@ -178,12 +180,38 @@ static const KeywordEntryT sKeywords[] = {
#define KEYWORD_COUNT (sizeof(sKeywords) / sizeof(sKeywords[0]) - 1) #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) // Function prototypes (alphabetical)
static char advance(BasLexerT *lex); static char advance(BasLexerT *lex);
static void appendTokenChar(BasLexerT *lex, int32_t *idx, char c); static void appendTokenChar(BasLexerT *lex, int32_t *idx, char c);
static bool atEnd(const BasLexerT *lex); static bool atEnd(const BasLexerT *lex);
char basAsciiUpper(char c);
bool basIsIdentChar(char c);
bool basIsTypeSuffixChar(char c); bool basIsTypeSuffixChar(char c);
bool basIsValidIdent(const char *name);
void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen); void basLexerInit(BasLexerT *lex, const char *source, int32_t sourceLen);
const char *basLexerKeywordAt(int32_t i); const char *basLexerKeywordAt(int32_t i);
BasKeywordClassE basLexerKeywordClass(int32_t i); BasKeywordClassE basLexerKeywordClass(int32_t i);
@ -191,6 +219,8 @@ int32_t basLexerKeywordCount(void);
BasTokenTypeE basLexerNext(BasLexerT *lex); BasTokenTypeE basLexerNext(BasLexerT *lex);
BasTokenTypeE basLexerPeek(const BasLexerT *lex); BasTokenTypeE basLexerPeek(const BasLexerT *lex);
const char *basTokenName(BasTokenTypeE type); 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 lookupKeyword(const char *text, int32_t len);
static BasTokenTypeE makeNewlineToken(BasLexerT *lex); static BasTokenTypeE makeNewlineToken(BasLexerT *lex);
static char peek(const BasLexerT *lex); static char peek(const BasLexerT *lex);
@ -202,7 +232,6 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex);
static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex); static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex);
static BasTokenTypeE tokenizeNumber(BasLexerT *lex); static BasTokenTypeE tokenizeNumber(BasLexerT *lex);
static BasTokenTypeE tokenizeString(BasLexerT *lex); static BasTokenTypeE tokenizeString(BasLexerT *lex);
static char upperChar(char c);
static char advance(BasLexerT *lex) { static char advance(BasLexerT *lex) {
if (atEnd(lex)) { if (atEnd(lex)) {
@ -211,7 +240,9 @@ static char advance(BasLexerT *lex) {
char c = lex->source[lex->pos++]; 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->line++;
lex->col = 1; lex->col = 1;
} else { } 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) { 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 // extension beyond classic QBASIC; it's convenient for bitmask
// work in the widget/graphics code. // work in the widget/graphics code.
if (c == '&') { if (c == '&') {
char n = upperChar(peekNext(lex)); char n = basAsciiUpper(peekNext(lex));
if (n == 'H' || n == 'O' || n == 'B') { if (n == 'H' || n == 'O' || n == 'B') {
lex->token.type = tokenizeHexLiteral(lex); lex->token.type = tokenizeHexLiteral(lex);
@ -354,7 +414,7 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) {
} }
// Identifier or keyword // Identifier or keyword
if (isalpha((unsigned char)c) || c == '_') { if (basIsIdentChar(c) && !isdigit((unsigned char)c)) {
lex->token.type = tokenizeIdentOrKeyword(lex); lex->token.type = tokenizeIdentOrKeyword(lex);
return lex->token.type; 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) { static BasTokenTypeE lookupKeyword(const char *text, int32_t len) {
// Case-insensitive keyword lookup. Short-circuits on length mismatch // Case-insensitive keyword lookup. Short-circuits on length mismatch
// (via cached keyword length) and on the very first character, both of // (via cached keyword length) and on the very first character, both of
// which reject the vast majority of entries before doing a full scan. // 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++) { for (int32_t i = 0; i < (int32_t)KEYWORD_COUNT; i++) {
const KeywordEntryT *kw = &sKeywords[i]; const KeywordEntryT *kw = &sKeywords[i];
@ -536,7 +629,7 @@ static BasTokenTypeE lookupKeyword(const char *text, int32_t len) {
bool match = true; bool match = true;
for (int32_t j = 1; j < len; j++) { for (int32_t j = 1; j < len; j++) {
if (upperChar(text[j]) != kw->text[j]) { if (basAsciiUpper(text[j]) != kw->text[j]) {
match = false; match = false;
break; break;
} }
@ -642,7 +735,7 @@ static void skipWhitespace(BasLexerT *lex) {
static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) { static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
advance(lex); // skip & advance(lex); // skip &
char base = upperChar(peek(lex)); char base = basAsciiUpper(peek(lex));
advance(lex); // skip H/O/B advance(lex); // skip H/O/B
int32_t shift; int32_t shift;
@ -705,8 +798,10 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
return TOK_ERROR; return TOK_ERROR;
} }
// Runtime INTEGER and LONG are both 32-bit; silently truncating a // The widest runtime integer (LONG) is 32 bits; silently truncating a
// wider literal would miscompile the constant, so reject it. // 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) { if (overflow) {
setError(lex, "Hexadecimal/octal/binary literal exceeds 32 bits"); setError(lex, "Hexadecimal/octal/binary literal exceeds 32 bits");
lex->token.type = TOK_ERROR; lex->token.type = TOK_ERROR;
@ -731,18 +826,20 @@ static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) { static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
int32_t idx = 0; 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)); appendTokenChar(lex, &idx, advance(lex));
} }
lex->token.text[idx] = '\0'; lex->token.text[idx] = '\0';
lex->token.textLen = idx; 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)) { if (!atEnd(lex)) {
char c = peek(lex); char c = peek(lex);
if (basIsTypeSuffixChar(c)) { if (basIsTypeSuffixChar(c) && (c == '$' || lookupKeyword(lex->token.text, idx) == TOK_IDENT)) {
advance(lex); advance(lex);
appendTokenChar(lex, &idx, c); appendTokenChar(lex, &idx, c);
lex->token.text[idx] = '\0'; 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 // always store 'E' in the buffer so atof() can parse it. Require at
// least one digit after the marker (and optional sign); otherwise the // 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). // 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')) { if (!atEnd(lex) && (marker == 'E' || marker == 'D')) {
int32_t savedIdx = idx; int32_t savedIdx = idx;
@ -851,16 +948,27 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
// Check for type suffix // Check for type suffix
if (!atEnd(lex)) { if (!atEnd(lex)) {
char c = peek(lex); char c = peek(lex);
int64_t val;
if (c == '%') { if (c == '%') {
advance(lex); 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; return TOK_INT_LIT;
} }
if (c == '&') { if (c == '&') {
advance(lex); 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; return TOK_LONG_LIT;
} }
@ -877,14 +985,21 @@ static BasTokenTypeE tokenizeNumber(BasLexerT *lex) {
return TOK_FLOAT_LIT; 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) { if (val >= INT16_MIN && val <= INT16_MAX) {
lex->token.intVal = (int32_t)val; lex->token.intVal = (int32_t)val;
return TOK_INT_LIT; return TOK_INT_LIT;
} }
lex->token.longVal = (int64_t)val; lex->token.longVal = val;
return TOK_LONG_LIT; return TOK_LONG_LIT;
} }
@ -927,12 +1042,3 @@ static BasTokenTypeE tokenizeString(BasLexerT *lex) {
return TOK_STRING_LIT; return TOK_STRING_LIT;
} }
static char upperChar(char c) {
if (c >= 'a' && c <= 'z') {
return c - 32;
}
return c;
}

View file

@ -212,6 +212,10 @@ typedef enum {
#define BAS_MAX_STRING_LEN (BAS_MAX_TOKEN_LEN - 1) #define BAS_MAX_STRING_LEN (BAS_MAX_TOKEN_LEN - 1)
#define BAS_LEX_ERROR_LEN 256 #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 { typedef struct {
BasTokenTypeE type; BasTokenTypeE type;
int32_t line; // 1-based source line number int32_t line; // 1-based source line number
@ -240,6 +244,7 @@ typedef struct {
int32_t col; // current column (1-based) int32_t col; // current column (1-based)
BasTokenT token; // current token BasTokenT token; // current token
char error[BAS_LEX_ERROR_LEN]; 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; } BasLexerT;
// ============================================================ // ============================================================
@ -260,9 +265,26 @@ BasTokenTypeE basLexerPeek(const BasLexerT *lex);
// Return human-readable name for a token type. // Return human-readable name for a token type.
const char *basTokenName(BasTokenTypeE 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 (% & ! # $). // True when c is a BASIC type-suffix character (% & ! # $).
bool basIsTypeSuffixChar(char c); 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 // Keyword iteration
// ============================================================ // ============================================================

View file

@ -26,13 +26,19 @@
#include "obfuscate.h" #include "obfuscate.h"
#include "basEvents.h" #include "basEvents.h"
#include "lexer.h"
#include "../basRes.h"
#include "../runtime/values.h" #include "../runtime/values.h"
#include <ctype.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#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 // Name map
// ============================================================ // ============================================================
@ -52,10 +58,8 @@ typedef struct {
// Function prototypes (alphabetical) // Function prototypes (alphabetical)
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms); 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); 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 void collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved);
static int32_t findFormEndPos(const char *text, int32_t len); static void emitBytes(uint8_t *out, int32_t outCap, int32_t *outLen, const char *src, int32_t len);
static bool isIdentChar(int c);
static bool isValidIdent(const char *name);
static const char *nameMapAdd(NameMapT *m, const char *name); static const char *nameMapAdd(NameMapT *m, const char *name);
static void nameMapFree(NameMapT *m); static void nameMapFree(NameMapT *m);
static void nameMapInit(NameMapT *m); static void nameMapInit(NameMapT *m);
@ -76,17 +80,36 @@ void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr
return; 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; NameMapT map;
nameMapInit(&names);
nameMapInit(&reserved);
nameMapInit(&map); nameMapInit(&map);
// Pass 1: collect all names from all .frm texts
for (int32_t i = 0; i < frmCount; i++) { for (int32_t i = 0; i < frmCount; i++) {
if (frmTexts[i] && frmLens[i] > 0) { 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++) { for (int32_t i = 0; i < frmCount; i++) {
outFrms[i].data = NULL; outFrms[i].data = NULL;
outFrms[i].len = 0; outFrms[i].len = 0;
@ -95,11 +118,8 @@ void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *fr
continue; continue;
} }
int32_t strippedLen = findFormEndPos(frmTexts[i], frmLens[i]); int32_t strippedLen = basFindFormEndPos(frmTexts[i], frmLens[i]);
int32_t outCap = rewriteFrmText(frmTexts[i], strippedLen, &map, NULL, 0) + 1;
// 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); uint8_t *outBuf = malloc(outCap);
if (!outBuf) { if (!outBuf) {
@ -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); int32_t outLen = rewriteFrmText(frmTexts[i], strippedLen, &map, outBuf, outCap);
// Ensure trailing newline // Ensure trailing newline
if (outLen > 0 && outBuf[outLen - 1] != '\n' && outLen < outCap) { if (outLen > 0 && outBuf[outLen - 1] != '\n') {
outBuf[outLen++] = '\n'; outBuf[outLen++] = '\n';
} }
@ -175,11 +195,11 @@ int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, in
firstNonWs++; firstNonWs++;
} }
if (contentEnd - firstNonWs >= 3 && if (contentEnd - firstNonWs >= BAS_REM_KEYWORD_LEN &&
strncasecmp(src + firstNonWs, "REM", 3) == 0 && strncasecmp(src + firstNonWs, "REM", BAS_REM_KEYWORD_LEN) == 0 &&
(contentEnd - firstNonWs == 3 || (contentEnd - firstNonWs == BAS_REM_KEYWORD_LEN ||
src[firstNonWs + 3] == ' ' || src[firstNonWs + BAS_REM_KEYWORD_LEN] == ' ' ||
src[firstNonWs + 3] == '\t')) { src[firstNonWs + BAS_REM_KEYWORD_LEN] == '\t')) {
contentEnd = firstNonWs; 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 <Type> <Name>" names to the map. // Scan a .frm text: every "Begin <Type> <Name>" adds Name to names, and
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *map) { // Type plus every "<Key> = ..." property key goes into reserved.
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved) {
const char *p = text; const char *p = text;
const char *end = text + len; const char *end = text + len;
@ -239,124 +261,55 @@ static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *map) {
// Trim leading whitespace // Trim leading whitespace
const char *l = skipWhitespace(lineStart, lineEnd); const char *l = skipWhitespace(lineStart, lineEnd);
char token[BAS_OBF_TOKEN_LEN];
// Check "Begin " if ((lineEnd - l) >= BAS_BEGIN_PREFIX_LEN && strncasecmp(l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) {
if ((lineEnd - l) < 6 || strncasecmp(l, "Begin ", 6) != 0) { l = skipWhitespace(l + BAS_BEGIN_PREFIX_LEN, lineEnd);
l = readToken(l, lineEnd, token, sizeof(token));
if (token[0] == '\0') {
continue; continue;
} }
l += 6; nameMapAdd(reserved, token);
l = skipWhitespace(l, lineEnd); l = skipWhitespace(l, lineEnd);
readToken(l, lineEnd, token, sizeof(token));
// Read type name if (token[0] && basIsValidIdent(token)) {
char typeName[64]; nameMapAdd(names, token);
l = readToken(l, lineEnd, typeName, sizeof(typeName)); }
if (typeName[0] == '\0') {
continue; continue;
} }
// Read control name // "<Key> = value": the key is a property name.
l = readToken(l, lineEnd, token, sizeof(token));
l = skipWhitespace(l, lineEnd); l = skipWhitespace(l, lineEnd);
char ctrlName[64];
l = readToken(l, lineEnd, ctrlName, sizeof(ctrlName));
if (ctrlName[0] && isValidIdent(ctrlName)) { if (l < lineEnd && *l == '=' && token[0] && basIsValidIdent(token)) {
nameMapAdd(map, ctrlName); 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. // Appends len bytes of src to out (bounded by outCap) and always advances
// Returns len of the stripped .frm. If no Begin Form found, returns original len. // *outLen, so a NULL out measures the exact output size.
static int32_t findFormEndPos(const char *text, int32_t len) { static void emitBytes(uint8_t *out, int32_t outCap, int32_t *outLen, const char *src, int32_t len) {
int32_t nesting = 0; if (out && *outLen + len <= outCap) {
bool inForm = false; memcpy(out + *outLen, src, len);
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; *outLen += len;
if (p < end && *p == '\r') {
p++;
} }
if (p < end && *p == '\n') {
p++;
}
const char *l = skipWhitespace(lineStart, lineEnd);
if ((lineEnd - l) >= 6 && strncasecmp(l, "Begin ", 6) == 0) {
// Check for "Begin Form ..." to set inForm on outer open
if (!inForm) {
const char *r = l + 6;
r = skipWhitespace(r, lineEnd);
if ((lineEnd - r) >= 5 && strncasecmp(r, "Form ", 5) == 0) {
inForm = true;
}
}
nesting++;
} else if ((lineEnd - l) >= 3 && strncasecmp(l, "End", 3) == 0 &&
(lineEnd - l == 3 || l[3] == ' ' || l[3] == '\t' || l[3] == '\r')) {
nesting--;
if (inForm && nesting == 0) {
return (int32_t)(p - text);
}
}
}
return len;
}
// ============================================================
// Pass 3: rewrite .frm text with mapped names
// ============================================================
// Returns true if c is a valid identifier character. // 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) { static const char *nameMapAdd(NameMapT *m, const char *name) {
const char *existing = nameMapLookup(m, 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) { 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)); NameEntryT *newEntries = realloc(m->entries, newCap * sizeof(NameEntryT));
if (!newEntries) { if (!newEntries) {
@ -376,7 +329,7 @@ static const char *nameMapAdd(NameMapT *m, const char *name) {
m->cap = newCap; m->cap = newCap;
} }
char mapped[16]; char mapped[BAS_OBF_MAPPED_LEN];
snprintf(mapped, sizeof(mapped), "C%ld", (long)(m->count + 1)); snprintf(mapped, sizeof(mapped), "C%ld", (long)(m->count + 1));
m->entries[m->count].orig = strdup(name); 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 // Rewrites the .frm text line by line. Only positions that hold a
// the map, emit the mapped name instead. Output to out (returns bytes written). // form/control NAME are remapped: the name token of a "Begin <Type> <Name>"
// 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) { 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 outLen = 0;
int32_t i = 0; const char *p = src;
bool inStr = false; const char *end = src + srcLen;
while (i < srcLen) { while (p < end) {
char c = src[i]; const char *lineStart = p;
if (c == '"') { while (p < end && *p != '\n' && *p != '\r') {
inStr = !inStr; p++;
if (outLen < outCap) {
out[outLen++] = (uint8_t)c;
} }
i++; const char *lineEnd = p;
continue;
if (p < end && *p == '\r') {
p++;
} }
// Read identifier if (p < end && *p == '\n') {
if (!inStr && (isalpha((unsigned char)c) || c == '_')) { p++;
int32_t identStart = i;
while (i < srcLen && isIdentChar((unsigned char)src[i])) {
i++;
} }
int32_t identLen = i - identStart; const char *l = skipWhitespace(lineStart, lineEnd);
char ident[128]; 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 (identLen >= (int32_t)sizeof(ident)) { if ((lineEnd - l) >= BAS_BEGIN_PREFIX_LEN && strncasecmp(l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) {
identLen = (int32_t)sizeof(ident) - 1; // Begin <Type> <Name>: 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);
memcpy(ident, src + identStart, identLen); const char *nameStart = t;
ident[identLen] = '\0'; t = readToken(t, lineEnd, token, sizeof(token));
const char *mapped = nameMapLookup(map, ident); if (token[0] && t - nameStart == (int32_t)strlen(token)) {
mapped = nameMapLookup(map, token);
if (mapped) { replaceStart = nameStart;
int32_t mLen = (int32_t)strlen(mapped); replaceEnd = t;
for (int32_t k = 0; k < mLen && outLen < outCap; k++) {
out[outLen++] = (uint8_t)mapped[k];
} }
} else { } else {
for (int32_t k = 0; k < identLen && outLen < outCap; k++) { // <Key> = value: a value that is exactly a mapped name (bare or
out[outLen++] = (uint8_t)ident[k]; // quoted) refers to a control and follows the rename.
const char *t = readToken(l, lineEnd, token, sizeof(token));
t = skipWhitespace(t, lineEnd);
if (t < lineEnd && *t == '=') {
t = skipWhitespace(t + 1, lineEnd);
const char *valueStart = t;
const char *valueEnd = lineEnd;
while (valueEnd > valueStart && (valueEnd[-1] == ' ' || valueEnd[-1] == '\t')) {
valueEnd--;
}
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 (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));
} }
if (outLen < outCap) {
out[outLen++] = (uint8_t)c;
}
i++;
} }
return outLen; 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 // separating name references from literals would give name refs their own
// pool slots and change emitted bytecode for every release build, so it is // pool slots and change emitted bytecode for every release build, so it is
// deferred; avoid string literals that exactly match a control/form name // 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++) { for (int32_t i = 0; i < mod->constCount; i++) {
const BasStringT *s = mod->constants[i]; const BasStringT *s = mod->constants[i];
@ -536,6 +523,32 @@ static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) {
if (mapped) { if (mapped) {
replaceConstant(mod, i, mapped); replaceConstant(mod, i, mapped);
continue;
}
// "<Name>_<Event>" 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 // Split on underscore
int32_t prefixLen = (int32_t)(underscore - proc->name); 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)) { if (prefixLen >= (int32_t)sizeof(prefix)) {
prefixLen = (int32_t)sizeof(prefix) - 1; prefixLen = (int32_t)sizeof(prefix) - 1;
@ -598,7 +611,7 @@ static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
const char *mapped = nameMapLookup(map, prefix); const char *mapped = nameMapLookup(map, prefix);
if (mapped) { if (mapped) {
char newName[BAS_MAX_PROC_NAME]; char newName[BAS_MAX_IDENT];
snprintf(newName, sizeof(newName), "%s_%s", mapped, suffix); snprintf(newName, sizeof(newName), "%s_%s", mapped, suffix);
snprintf(proc->name, sizeof(proc->name), "%s", newName); snprintf(proc->name, sizeof(proc->name), "%s", newName);
} }

View file

@ -41,10 +41,13 @@ typedef struct {
// Obfuscate form/control names in the module and all .frm texts. // Obfuscate form/control names in the module and all .frm texts.
// //
// Reads original names from the Begin declarations in each .frm, // Reads original names from the Begin declarations in each .frm,
// generates C1..Cn, then rewrites: // generates C1..Cn (skipping any name that is also a control type or a
// - The .frm text (form/control name declarations, stripping the // property key, since bytecode cannot tell those apart), then rewrites:
// trailing BASIC code section after the outer form closes) // - The .frm text (Begin-line names and property values naming a
// - Module string constants matching any original name // control, stripping the trailing BASIC code section after the
// outer form closes)
// - Module string constants matching any original name, and
// <OrigName>_<Event> handler names used as SetEvent targets
// - Procedure names matching <OrigName>_<Event> // - Procedure names matching <OrigName>_<Event>
// - formVarInfo entries keyed by form name // - formVarInfo entries keyed by form name
// //

View file

@ -28,6 +28,9 @@
#ifndef DVXBASIC_OPCODES_H #ifndef DVXBASIC_OPCODES_H
#define DVXBASIC_OPCODES_H #define DVXBASIC_OPCODES_H
#include <stdbool.h>
#include <stdint.h>
// ============================================================ // ============================================================
// Variable scope tags // Variable scope tags
// Emitted in bytecode (e.g. OP_FOR scopeTag byte) and consumed // 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_UDT 7 // ref-counted user-defined type
#define BAS_TYPE_OBJECT 8 // opaque host object (form, control, etc.) #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_REF 9 // ByRef pointer to a BasValueT slot
#define BAS_TYPE_ELEM_REF 10 // ByRef array element: counted BasArrayT* + flat index
// ============================================================ // ============================================================
// Stack operations // Stack operations
@ -78,13 +82,13 @@ typedef enum {
#define OP_NOP 0x00 #define OP_NOP 0x00
#define OP_PUSH_INT16 0x01 // [int16] push 16-bit integer #define OP_PUSH_INT16 0x01 // [int16] push 16-bit integer
#define OP_PUSH_INT32 0x02 // [int32] push 32-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_FLT64 0x04 // [float64] push 64-bit float
#define OP_PUSH_STR 0x05 // [uint16 idx] push string from constant pool #define OP_PUSH_STR 0x05 // [uint16 idx] push string from constant pool
#define OP_PUSH_TRUE 0x06 // push boolean True (-1) #define OP_PUSH_TRUE 0x06 // push boolean True (-1)
#define OP_PUSH_FALSE 0x07 // push boolean False (0) #define OP_PUSH_FALSE 0x07 // push boolean False (0)
#define OP_POP 0x08 // discard top of stack #define OP_POP 0x08 // discard top of stack
#define OP_DUP 0x09 // duplicate 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 // Variable access
@ -94,8 +98,6 @@ typedef enum {
#define OP_STORE_LOCAL 0x11 // [uint16 idx] pop to local variable #define OP_STORE_LOCAL 0x11 // [uint16 idx] pop to local variable
#define OP_LOAD_GLOBAL 0x12 // [uint16 idx] push global variable #define OP_LOAD_GLOBAL 0x12 // [uint16 idx] push global variable
#define OP_STORE_GLOBAL 0x13 // [uint16 idx] pop to 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_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_STORE_ARRAY 0x17 // [uint8 dims] value, indices, array ref on stack
#define OP_LOAD_FIELD 0x18 // [uint16 fieldIdx] load UDT field #define OP_LOAD_FIELD 0x18 // [uint16 fieldIdx] load UDT field
@ -119,11 +121,7 @@ typedef enum {
// Arithmetic (float) // 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_DIV_FLT 0x29 // float divide (/)
#define OP_NEG_FLT 0x2A
#define OP_POW 0x2B // exponentiation (^) #define OP_POW 0x2B // exponentiation (^)
// ============================================================ // ============================================================
@ -181,7 +179,7 @@ typedef enum {
#define OP_RET 0x55 // return from subroutine #define OP_RET 0x55 // return from subroutine
#define OP_RET_VAL 0x56 // return from function (value on stack) #define OP_RET_VAL 0x56 // return from function (value on stack)
#define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 scope] [int16 skipOffset] init FOR, skip body if range empty #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) #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_FLT_INT 0x61 // float -> int (banker's rounding)
#define OP_CONV_INT_STR 0x62 // int -> string #define OP_CONV_INT_STR 0x62 // int -> string
#define OP_CONV_STR_INT 0x63 // string -> int (VAL) #define OP_CONV_STR_INT 0x63 // string -> int (VAL)
#define OP_CONV_FLT_STR 0x64 // float -> string #define OP_CONV_STR_FLT 0x65 // any -> double (VAL, CDBL)
#define OP_CONV_STR_FLT 0x65 // string -> float (VAL)
#define OP_CONV_INT_LONG 0x66 // int16 -> int32 #define OP_CONV_INT_LONG 0x66 // int16 -> int32
#define OP_CONV_LONG_INT 0x67 // int32 -> int16
// ============================================================ // ============================================================
// I/O // I/O
@ -204,9 +200,8 @@ typedef enum {
#define OP_PRINT 0x70 // print TOS to current output #define OP_PRINT 0x70 // print TOS to current output
#define OP_PRINT_NL 0x71 // print newline #define OP_PRINT_NL 0x71 // print newline
#define OP_PRINT_TAB 0x72 // print tab (14-column zones) #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_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_CLOSE 0x76 // channel# on stack
#define OP_FILE_PRINT 0x77 // channel#, value on stack #define OP_FILE_PRINT 0x77 // channel#, value on stack
#define OP_FILE_INPUT 0x78 // channel# on stack, push string #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_ME_REF 0x8A // push current form reference
#define OP_CREATE_CTRL 0x8B // pop name, pop typeName, pop formRef, push controlRef #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_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_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_LOAD_FORM_VAR 0x8F // [uint16 idx] push currentFormVars[idx]
#define OP_STORE_FORM_VAR 0x9B // [uint16 idx] pop, store to currentFormVars[idx]
#define OP_PUSH_FORM_ADDR 0x9C // [uint16 idx] push &currentFormVars[idx] (ByRef)
#define OP_CREATE_CTRL_EX 0x9D // pop parentRef, pop name, pop type, pop formRef, push ctrlRef
#define OP_CREATE_FORM 0xF0 // pop height, pop width, pop nameStr, push formRef
#define OP_SET_EVENT 0xF1 // pop handlerNameStr, pop eventNameStr, pop ctrlRef
#define OP_REMOVE_CTRL 0xF2 // pop ctrlNameStr, pop formRef
// ============================================================ // ============================================================
// Array / misc // Array / misc
// ============================================================ // ============================================================
#define OP_DIM_ARRAY 0x90 // [uint8 dims] [uint8 type] bounds on stack #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_ERASE 0x92 // array ref on stack
#define OP_LBOUND 0x93 // [uint8 dim] array ref on stack #define OP_LBOUND 0x93 // [uint8 dim] array ref on stack
#define OP_UBOUND 0x94 // [uint8 dim] array ref on stack #define OP_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_RESUME_NEXT 0x97 // resume at next statement
#define OP_RAISE_ERR 0x98 // error number on stack #define OP_RAISE_ERR 0x98 // error number on stack
#define OP_ERR_NUM 0x99 // push current error number #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 &currentFormVars[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) // Math built-ins (single opcode each for common functions)
@ -284,7 +274,7 @@ typedef enum {
#define OP_MATH_LOG 0xA9 #define OP_MATH_LOG 0xA9
#define OP_MATH_EXP 0xAA #define OP_MATH_EXP 0xAA
#define OP_MATH_RND 0xAB #define OP_MATH_RND 0xAB
#define OP_MATH_RANDOMIZE 0xAC // seed on stack (or TIMER if -1) #define OP_MATH_RANDOMIZE 0xAC // seed on stack (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_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_RED 0xAE // pop LONG color; push (color>>16) & 0xFF
#define OP_GET_GREEN 0xAF // pop LONG color; push (color>>8) & 0xFF #define OP_GET_GREEN 0xAF // pop LONG color; push (color>>8) & 0xFF
@ -330,7 +320,7 @@ typedef enum {
// Random/Binary file I/O // 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_PUT 0xBF // pop channel + recno + value, write record
#define OP_FILE_SEEK 0xC0 // pop channel + position, seek #define OP_FILE_SEEK 0xC0 // pop channel + position, seek
#define OP_FILE_LOF 0xC1 // pop channel, push file length #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_PRINT_SPC_N 0xC9 // pop count, print that many spaces
#define OP_FORMAT 0xCA // pop format string + value, push formatted string #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_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) // External library calls (DECLARE LIBRARY)
@ -402,6 +392,11 @@ typedef enum {
// Debug // Debug
#define OP_LINE 0xEF // [uint16 lineNum] set current source line for debugger #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 // Halt
// ============================================================ // ============================================================
@ -409,4 +404,169 @@ typedef enum {
#define OP_END 0xFE // explicit END statement -- terminates program #define OP_END 0xFE // explicit END statement -- terminates program
#define OP_HALT 0xFF // implicit end of module #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 <returnAddr> OP_JMP <offset>, 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 #endif // DVXBASIC_OPCODES_H

File diff suppressed because it is too large Load diff

View file

@ -49,6 +49,18 @@
#define BAS_PARSE_ERROR_LEN 1024 #define BAS_PARSE_ERROR_LEN 1024
#define BAS_PARSE_ERR_SCRATCH 512 #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. // Optional compile-time validator for CtrlName.Member references.
// The IDE populates this from the project's .frm files + widget DXE // The IDE populates this from the project's .frm files + widget DXE
// metadata so typos die at compile time instead of at event-click // 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 lastUdtTypeId; // index of last resolved UDT type from resolveTypeName
int32_t optionBase; // default array lower bound (0 or 1) int32_t optionBase; // default array lower bound (0 or 1)
bool optionExplicit; // true = variables must be declared with DIM 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 char currentProc[BAS_MAX_TOKEN_LEN]; // name of current SUB/FUNCTION
// Per-form init block tracking // Per-form init block tracking
int32_t formInitJmpAddr; // code position of JMP to patch (-1 = none) int32_t formInitJmpAddr; // code position of JMP to patch (-1 = none)
@ -104,6 +117,18 @@ typedef struct {
int32_t selectDepth; int32_t selectDepth;
int32_t forSelectBase; int32_t forSelectBase;
int32_t doSelectBase; 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). // Optional compile-time CtrlName.Member validator (IDE-only).
const BasCtrlValidatorT *validator; const BasCtrlValidatorT *validator;
} BasParserT; } BasParserT;

View file

@ -30,8 +30,7 @@
// and SetEvent looks up handlers by name at runtime, so those proc // and SetEvent looks up handlers by name at runtime, so those proc
// names must be preserved. Everything else becomes F1, F2, F3... // names must be preserved. Everything else becomes F1, F2, F3...
// //
// OP_LINE removal is deferred to a future version (requires // OP_LINE removal is a separate pass: see compact.c.
// bytecode compaction and offset rewriting).
#include "strip.h" #include "strip.h"
#include "basEvents.h" #include "basEvents.h"
@ -47,12 +46,7 @@
// find it. Declared in basEvents.h; defined here as the single source // find it. Declared in basEvents.h; defined here as the single source
// of truth. // of truth.
const char *basEventSuffixes[] = { const char *basEventSuffixes[] = {
"Load", "Unload", "QueryUnload", "Resize", "Activate", "Deactivate", BAS_EVENT_LIST(BAS_EVENT_SUFFIX)
"Click", "DblClick", "Change", "Timer",
"GotFocus", "LostFocus",
"KeyPress", "KeyDown", "KeyUp",
"MouseDown", "MouseUp", "MouseMove",
"Scroll", "Reposition", "Validate",
NULL NULL
}; };
@ -120,7 +114,7 @@ void basStripModule(BasModuleT *mod) {
// Skip any generated name that collides with a kept proc (event // Skip any generated name that collides with a kept proc (event
// handler or constant-pool-referenced) or a constant-pool entry, so // handler or constant-pool-referenced) or a constant-pool entry, so
// basModuleFindProc can't resolve the wrong procedure at runtime. // 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++); snprintf(cand, sizeof(cand), "F%ld", (long)nextMangled++);

View file

@ -25,7 +25,8 @@
// Removes debug information from a compiled module to hinder // Removes debug information from a compiled module to hinder
// decompilation. Clears debug variable info and debug UDT // decompilation. Clears debug variable info and debug UDT
// definitions, and mangles proc names that aren't needed for // 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 #ifndef DVXBASIC_STRIP_H
#define DVXBASIC_STRIP_H #define DVXBASIC_STRIP_H

View file

@ -29,13 +29,9 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
// 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) // Function prototypes (alphabetical)
static void basSymbolFree(BasSymbolT *sym);
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType); BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
int32_t basSymTabAllocGlobalSlot(BasSymTabT *tab); int32_t basSymTabAllocGlobalSlot(BasSymTabT *tab);
int32_t basSymTabAllocSlot(BasSymTabT *tab); int32_t basSymTabAllocSlot(BasSymTabT *tab);
@ -47,10 +43,19 @@ void basSymTabFree(BasSymTabT *tab);
void basSymTabInit(BasSymTabT *tab); void basSymTabInit(BasSymTabT *tab);
int32_t basSymTabLeaveFormScope(BasSymTabT *tab); int32_t basSymTabLeaveFormScope(BasSymTabT *tab);
void basSymTabLeaveLocal(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 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) { BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) {
// Determine scope: local > form > global. // Determine scope: local > form > global.
@ -65,6 +70,12 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
scope = SCOPE_GLOBAL; 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); uint32_t h = nameHashCI(name);
// Check for duplicate in current scope (skip ended form symbols) // 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; return NULL;
} }
strncpy(sym->name, name, BAS_MAX_SYMBOL_NAME - 1); strcpy(sym->name, name);
sym->name[BAS_MAX_SYMBOL_NAME - 1] = '\0';
sym->nameHash = h; sym->nameHash = h;
sym->kind = kind; sym->kind = kind;
sym->scope = scope; 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. // event handler for a different form's control.
if (tab->inFormScope && tab->formScopeName[0] && if (tab->inFormScope && tab->formScopeName[0] &&
(scope == SCOPE_FORM || kind == SYM_SUB || kind == SYM_FUNCTION)) { (scope == SCOPE_FORM || kind == SYM_SUB || kind == SYM_FUNCTION)) {
strncpy(sym->formName, tab->formScopeName, BAS_MAX_SYMBOL_NAME - 1); strncpy(sym->formName, tab->formScopeName, BAS_MAX_IDENT - 1);
sym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0'; sym->formName[BAS_MAX_IDENT - 1] = '\0';
} }
arrput(tab->symbols, sym); arrput(tab->symbols, sym);
@ -129,8 +139,8 @@ int32_t basSymTabAllocSlot(BasSymTabT *tab) {
void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName) { void basSymTabEnterFormScope(BasSymTabT *tab, const char *formName) {
tab->inFormScope = true; tab->inFormScope = true;
strncpy(tab->formScopeName, formName, BAS_MAX_SYMBOL_NAME - 1); strncpy(tab->formScopeName, formName, BAS_MAX_IDENT - 1);
tab->formScopeName[BAS_MAX_SYMBOL_NAME - 1] = '\0'; tab->formScopeName[BAS_MAX_IDENT - 1] = '\0';
tab->nextFormVarIdx = 0; tab->nextFormVarIdx = 0;
tab->formScopeSymStart = tab->count; 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. // Case-insensitive FNV-1a hash used to accelerate symbol-table lookups.
// Caller computes hash of search-name once; each entry stores its own // 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. // that nearly always rejects non-matches without calling namesEqual.
// ============================================================ // ============================================================
static uint32_t nameHashCI(const char *name) { static uint32_t nameHashCI(const char *name) {
uint32_t h = 0x811C9DC5u; uint32_t h = BAS_FNV1A_OFFSET;
while (*name) { while (*name) {
char c = basAsciiUpper(*name); char c = basAsciiUpper(*name);
h ^= (uint32_t)(uint8_t)c; h ^= (uint32_t)(uint8_t)c;
h *= 0x01000193u; h *= BAS_FNV1A_PRIME;
name++; name++;
} }

View file

@ -32,6 +32,8 @@
#define DVXBASIC_SYMTAB_H #define DVXBASIC_SYMTAB_H
#include "../compiler/opcodes.h" #include "../compiler/opcodes.h"
#include "../compiler/lexer.h"
#include "../runtime/vm.h"
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
@ -55,13 +57,20 @@ typedef enum {
// Symbol entry // 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_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 // UDT field definition
typedef struct { typedef struct {
char name[BAS_MAX_SYMBOL_NAME]; char name[BAS_MAX_IDENT];
uint8_t dataType; // BAS_TYPE_* uint8_t dataType; // BAS_TYPE_*
int32_t udtTypeId; // if dataType == BAS_TYPE_UDT, index of the TYPE_DEF symbol int32_t udtTypeId; // if dataType == BAS_TYPE_UDT, index of the TYPE_DEF symbol
} BasFieldDefT; } BasFieldDefT;
@ -105,9 +114,12 @@ typedef struct {
bool isShared; bool isShared;
bool isExtern; // true = external library function (DECLARE LIBRARY) bool isExtern; // true = external library function (DECLARE LIBRARY)
bool formScopeEnded; // true = form scope ended, invisible to lookups 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 name[BAS_MAX_IDENT];
char formName[BAS_MAX_SYMBOL_NAME]; // form name for SCOPE_FORM vars char formName[BAS_MAX_IDENT]; // form name for SCOPE_FORM vars
uint8_t paramTypes[BAS_MAX_PARAMS]; uint8_t paramTypes[BAS_MAX_PARAMS];
bool paramByVal[BAS_MAX_PARAMS]; bool paramByVal[BAS_MAX_PARAMS];
bool paramOptional[BAS_MAX_PARAMS]; // true = OPTIONAL parameter 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) int32_t nextLocalIdx; // next local variable slot (reset per SUB/FUNCTION)
bool inLocalScope; // true when inside SUB/FUNCTION bool inLocalScope; // true when inside SUB/FUNCTION
bool inFormScope; // true inside BEGINFORM...ENDFORM 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 nextFormVarIdx; // next form-level variable slot
int32_t formScopeSymStart; // symbol count at BEGINFORM (for marking ended) int32_t formScopeSymStart; // symbol count at BEGINFORM (for marking ended)
} BasSymTabT; } BasSymTabT;
@ -146,7 +158,9 @@ void basSymTabInit(BasSymTabT *tab);
void basSymTabFree(BasSymTabT *tab); void basSymTabFree(BasSymTabT *tab);
// Add a symbol. Returns the symbol pointer, or NULL if the name already // 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); BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType);
// Look up a symbol by name. Searches local scope first, then global. // Look up a symbol by name. Searches local scope first, then global.

File diff suppressed because it is too large Load diff

View file

@ -36,6 +36,7 @@
#include "../runtime/values.h" #include "../runtime/values.h"
#include "dvxApp.h" #include "dvxApp.h"
#include "dvxWgt.h" #include "dvxWgt.h"
#include "frmParser.h"
// ============================================================ // ============================================================
// Forward declarations // Forward declarations
@ -48,14 +49,74 @@ typedef struct BasControlT BasControlT;
// Limits // Limits
// ============================================================ // ============================================================
#define BAS_MAX_CTRL_NAME 32 // Form and control names share the single identifier length BAS_MAX_IDENT
// Form names use the full identifier length (must equal the compiler-side // (vm.h) with the compiler, so name-keyed lookups never truncate.
// BAS_MAX_PROC_NAME / BAS_MAX_SYMBOL_NAME in vm.h); otherwise a 32-63 char // Window size for a form that gives no Width/Height; the IDE designer
// form name truncates here and form-scope variable binding (keyed by name) // uses the same values for a new form.
// silently fails to match the module's 63-char formName. #define BAS_DEFAULT_FORM_W 400
#define BAS_MAX_FORM_NAME 64 #define BAS_DEFAULT_FORM_H 300
#define BAS_MAX_FRM_LINE_LEN 512
#define BAS_MAX_FRM_NESTING 16 // ============================================================
// 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 // Menu ID to name mapping for event dispatch
@ -63,13 +124,13 @@ typedef struct BasControlT BasControlT;
typedef struct { typedef struct {
int32_t id; 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) BasControlT *proxy; // heap-allocated proxy for property access (widget=NULL, menuId stored)
} BasMenuIdMapT; } BasMenuIdMapT;
// Named popup / context menu owned by a form. // Named popup / context menu owned by a form.
typedef struct { typedef struct {
char name[BAS_MAX_CTRL_NAME]; char name[BAS_MAX_IDENT];
MenuT *menu; // wmCreateMenu root, or a submenu owned by its root MenuT *menu; // wmCreateMenu root, or a submenu owned by its root
bool ownsMenu; // true only for the root that wmFreeMenu must free bool ownsMenu; // true only for the root that wmFreeMenu must free
} BasFrmPopupMenuT; } BasFrmPopupMenuT;
@ -82,20 +143,20 @@ typedef struct {
// Event handler override (SetEvent) // Event handler override (SetEvent)
typedef struct { typedef struct {
char eventName[BAS_MAX_CTRL_NAME]; // e.g. "Click" char eventName[BAS_MAX_IDENT]; // e.g. "Click"
char handlerName[BAS_MAX_CTRL_NAME]; // e.g. "HandleOkClick" char handlerName[BAS_MAX_IDENT]; // e.g. "HandleOkClick"
} BasEventOverrideT; } BasEventOverrideT;
typedef struct BasControlT { typedef struct BasControlT {
char name[BAS_MAX_CTRL_NAME]; // VB control name (e.g. "Command1") char name[BAS_MAX_IDENT]; // VB control name (e.g. "Command1")
char typeName[BAS_MAX_CTRL_NAME]; // VB type name (e.g. "CommandButton") char typeName[BAS_MAX_IDENT]; // VB type name (e.g. "CommandButton")
int32_t index; // control array index (-1 = not in array) int32_t index; // control array index (-1 = not in array)
WidgetT *widget; // the DVX widget WidgetT *widget; // the DVX widget
BasFormT *form; // owning form BasFormT *form; // owning form
const WgtIfaceT *iface; // interface descriptor (from .wgt) const WgtIfaceT *iface; // interface descriptor (from .wgt)
char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding char dataSource[BAS_MAX_IDENT]; // name of Data control for binding
char dataField[BAS_MAX_CTRL_NAME]; // column name for binding char dataField[BAS_MAX_IDENT]; // column name for binding
char helpTopic[BAS_MAX_CTRL_NAME]; // help topic ID for F1 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 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) int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls)
BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES]; BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES];
@ -108,7 +169,7 @@ typedef struct BasControlT {
// event is in flight so different-event delivery (e.g. LostFocus // event is in flight so different-event delivery (e.g. LostFocus
// while Click is still running) is allowed through. // while Click is still running) is allowed through.
bool eventFiring; bool eventFiring;
char firingEventName[BAS_MAX_CTRL_NAME]; char firingEventName[BAS_MAX_IDENT];
} BasControlT; } BasControlT;
// ============================================================ // ============================================================
@ -116,7 +177,7 @@ typedef struct BasControlT {
// ============================================================ // ============================================================
typedef struct BasFormT { 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 WindowT *window; // DVX window
WidgetT *root; // widget root (from wgtInitWindow) WidgetT *root; // widget root (from wgtInitWindow)
WidgetT *contentBox; // VBox/HBox for user controls WidgetT *contentBox; // VBox/HBox for user controls
@ -134,7 +195,7 @@ typedef struct BasFormT {
bool frmCentered; bool frmCentered;
bool frmAutoSize; bool frmAutoSize;
char frmLayout[32]; // "VBox", "HBox", or "WrapBox" 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) // Per-form variable storage (allocated at load, freed at unload)
BasValueT *formVars; BasValueT *formVars;
int32_t formVarCount; int32_t formVarCount;
@ -168,29 +229,20 @@ typedef struct BasFormT {
// Cached .frm source for reload after unload // Cached .frm source for reload after unload
typedef struct { typedef struct {
char formName[BAS_MAX_FORM_NAME]; char formName[BAS_MAX_IDENT];
char *frmSource; // malloc'd copy of .frm text char *frmSource; // malloc'd copy of .frm text
int32_t frmSourceLen; int32_t frmSourceLen;
} BasFrmCacheT; } 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 { typedef struct {
AppContextT *ctx; // DVX app context AppContextT *ctx; // DVX app context
BasVmT *vm; // shared VM instance BasVmT *vm; // shared VM instance
BasModuleT *module; // compiled module BasModuleT *module; // compiled module
BasFormT **forms; // stb_ds array of heap-allocated pointers BasFormT **forms; // stb_ds array of heap-allocated pointers
BasFormT *currentForm; // form currently dispatching events 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 BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources
int32_t frmCacheCount; 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 // 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 // loop exits at the next pump so the app doesn't stumble forward
// with a halted VM. // with a halted VM.
@ -221,7 +273,7 @@ typedef struct {
// ctrl we'd otherwise have no way to tell the user WHICH control // ctrl we'd otherwise have no way to tell the user WHICH control
// was missing. This is the last requested name; if the lookup // was missing. This is the last requested name; if the lookup
// succeeded this value is still the name of that control. // succeeded this value is still the name of that control.
char lastLookupName[BAS_MAX_CTRL_NAME]; char lastLookupName[BAS_MAX_IDENT];
} BasFormRtT; } BasFormRtT;
// ============================================================ // ============================================================
@ -231,8 +283,8 @@ typedef struct {
// Initialize the form runtime with a DVX context and a compiled module. // Initialize the form runtime with a DVX context and a compiled module.
BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module); BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module);
// Load all cached forms (.frm text and compiled binaries) and show the // Load all cached .frm forms and show the startup form. Called before
// startup form. Called before bytecode execution begins. // bytecode execution begins.
void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName); void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName);
// VB-style event loop: pump DVX events until all forms are closed. // 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. // Register .frm source text for lazy loading when bytecode calls Load.
void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen); 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 ---- // ---- Widget creation ----
// Create a widget by resolved (DVX) type name. Returns NULL if the type // Create a widget by resolved (DVX) type name. Returns NULL if the type

View file

@ -31,15 +31,13 @@
#include <string.h> #include <string.h>
#include <strings.h> #include <strings.h>
#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) #define FRM_MAX_VB_VERSION 2.0 // highest VB form VERSION we can import (VB4+ rejected)
// Prototypes (alphabetical) // Prototypes (alphabetical)
bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb); bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb);
bool frmParseBool(const char *val); 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 frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax);
void frmStripQuotes(char *val); void frmStripQuotes(char *val);
static bool readToken(const char **p, char *buf, int32_t bufMax); 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; BlkTypeE blkType;
bool doPush = false; bool doPush = false;
readToken(&rest, typeName, FRM_MAX_TOKEN_LEN); if (!readToken(&rest, typeName, FRM_MAX_TOKEN_LEN)) {
rest = dvxSkipWs(rest);
readToken(&rest, ctrlName, FRM_MAX_TOKEN_LEN);
if (typeName[0] == '\0') {
continue; continue;
} }
rest = dvxSkipWs(rest);
readToken(&rest, ctrlName, FRM_MAX_TOKEN_LEN);
// Classify the block first so overflow is checked once and all // Classify the block first so overflow is checked once and all
// begin side-effects stay in lockstep with the stack push. // begin side-effects stay in lockstep with the stack push.
if (strcasecmp(typeName, "Form") == 0) { 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; blkType = BLK_FORM;
doPush = true; doPush = true;
} else if (strcasecmp(typeName, "Menu") == 0 && inForm) { } 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) { bool frmParseBool(const char *val) {
return frmParseBoolDefault(val, false);
}
bool frmParseBoolDefault(const char *val, bool defaultValue) {
if (!val) { 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 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 // 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) { static bool readToken(const char **p, char *buf, int32_t bufMax) {
const char *cur = *p; const char *cur = *p;
int32_t len = 0; int32_t len = 0;
while (*cur && *cur != ' ' && *cur != '\t' && *cur != '\r' && *cur != '\n' && len < bufMax - 1) { while (*cur && *cur != ' ' && *cur != '\t' && *cur != '\r' && *cur != '\n') {
buf[len++] = *cur++; if (len < bufMax - 1) {
buf[len++] = *cur;
}
cur++;
} }
buf[len] = '\0'; buf[len] = '\0';

View file

@ -35,6 +35,12 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
// 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. // Callbacks supplied by the consumer. Any field may be NULL.
typedef struct FrmParserCbsT { typedef struct FrmParserCbsT {
void *userData; void *userData;
@ -85,4 +91,9 @@ void frmStripQuotes(char *val);
// anything else -> false. // anything else -> false.
bool frmParseBool(const char *val); 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 #endif // DVXBASIC_FRMPARSER_H

File diff suppressed because it is too large Load diff

View file

@ -32,24 +32,32 @@
#include "dvxApp.h" #include "dvxApp.h"
#include "dvxWgt.h" #include "dvxWgt.h"
#include "canvas/canvas.h" #include "canvas/canvas.h"
#include "../compiler/lexer.h"
#include "../formrt/formrt.h" #include "../formrt/formrt.h"
#include "../formrt/frmParser.h"
#include "stb_ds_wrap.h" #include "stb_ds_wrap.h"
#include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
// ============================================================ // ============================================================
// Limits // 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_MAX_NAME BAS_MAX_IDENT
#define DSGN_NAME_FMT "%.31s" // keep in sync with DSGN_MAX_NAME #define DSGN_MENU_STACK_DEPTH FRM_MAX_NESTING // max menu nesting (editor and preview bar)
#define DSGN_MENU_STACK_DEPTH 8 // max menu nesting in designer parent/menu stacks #define DSGN_MAX_TEXT FRM_MAX_LINE_LEN
#define DSGN_MAX_TEXT 256
#define DSGN_MAX_PROPS 32 #define DSGN_MAX_PROPS 32
#define DSGN_HANDLE_SIZE 6 #define DSGN_HANDLE_SIZE 6
#define DSGN_MENU_ID_BASE 20000 // base ID for designer preview menu items #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) // Design-time property (stored as key=value strings)
@ -77,13 +85,19 @@ typedef struct {
// ============================================================ // ============================================================
// Design-time control // 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 { typedef struct {
char name[DSGN_MAX_NAME]; char name[DSGN_MAX_NAME];
char typeName[DSGN_MAX_NAME]; char typeName[DSGN_MAX_NAME];
char parentName[DSGN_MAX_NAME]; // empty = top-level (child of form) char parentName[DSGN_MAX_NAME]; // empty = top-level (child of form)
int32_t index; // control array index (-1 = not in array) 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 top;
int32_t width; int32_t width;
int32_t height; int32_t height;
@ -98,13 +112,33 @@ typedef struct {
WidgetT *widget; // live widget (created at design time for WYSIWYG) WidgetT *widget; // live widget (created at design time for WYSIWYG)
} DsgnControlT; } 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 // Design-time form
// ============================================================ // ============================================================
typedef struct { typedef struct {
// Fields are ordered by alignment to minimize struct padding. // Fields are ordered by alignment to minimize struct padding.
char name[DSGN_MAX_NAME]; char name[BAS_MAX_IDENT];
char caption[DSGN_MAX_TEXT]; char caption[DSGN_MAX_TEXT];
int32_t width; int32_t width;
int32_t height; int32_t height;
@ -181,18 +215,29 @@ void dsgnInit(DsgnStateT *ds, AppContextT *ctx);
// Call after dsgnLoadFrm or dsgnNewForm, with the form window's contentBox. // Call after dsgnLoadFrm or dsgnNewForm, with the form window's contentBox.
void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *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. // Load a .frm file into the designer.
bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen); bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen);
// Save the designer form to .frm text format. // 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); 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. // Create a new blank form.
void dsgnNewForm(DsgnStateT *ds, const char *name); void dsgnNewForm(DsgnStateT *ds, const char *name);
// Draw selection handles over the painted window surface. // 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. // Handle mouse click on the design surface.
void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag); 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. // 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); 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. // Get the default event name for a control type.
const char *dsgnDefaultEvent(const char *typeName); 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). // Check if a control type is a container (can hold children).
bool dsgnIsContainer(const char *typeName); 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 // 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. // the type has no such interface property. Shared IDE-internal helper.
const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName); 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. // Used in the form designer to preview the menu layout.
void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form); 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) // Code rename support (implemented in ideMain.c)
// ============================================================ // ============================================================

File diff suppressed because it is too large Load diff

View file

@ -45,9 +45,22 @@
// Constants // Constants
// ============================================================ // ============================================================
#define MAX_MENU_LEVEL 5 #define MAX_MENU_LEVEL (DSGN_MENU_STACK_DEPTH - 1) // deepest level the preview bar can show
#define ARROW_STR "-> " #define ARROW_STR "-> "
#define MED_MSG_BUF 128 // error-message scratch buffer #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 // Dialog state
@ -134,8 +147,8 @@ static void applyFields(void) {
int32_t itemCount = (int32_t)arrlen(sMed.items); int32_t itemCount = (int32_t)arrlen(sMed.items);
for (int32_t i = 0; i < itemCount; i++) { for (int32_t i = 0; i < itemCount; i++) {
if (i != sMed.selectedIdx && strncasecmp(sMed.items[i].name, "mnuSep", 6) == 0) { 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 + 6); int32_t n = atoi(sMed.items[i].name + MED_SEP_PREFIX_LEN);
if (n >= sepNum) { if (n >= sepNum) {
sepNum = n + 1; 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 { } else {
// Normal item: strip & and non-alphanumeric, prefix "mnu" // Normal item: strip & and non-alphanumeric, prefix "mnu"
int32_t p = 0; int32_t p = snprintf(autoName, DSGN_MAX_NAME, "%s", MED_NAME_PREFIX);
autoName[p++] = 'm';
autoName[p++] = 'n';
autoName[p++] = 'u';
for (const char *c = mi->caption; *c && p < DSGN_MAX_NAME - 1; c++) { for (const char *c = mi->caption; *c && p < DSGN_MAX_NAME - 1; c++) {
if (*c == '&') { if (*c == '&') {
@ -174,7 +184,7 @@ static void applyFields(void) {
mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb); mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb);
// Popup checkbox only meaningful on top-level items; for nested // Popup checkbox only meaningful on top-level items; for nested
// items `visible` stays true (the field is ignored there). // items `visible` stays true (the field is ignored there).
if (mi->level == 0 && sMed.popupCb) { if (mi->level == 0) {
mi->visible = !wgtCheckboxIsChecked(sMed.popupCb); mi->visible = !wgtCheckboxIsChecked(sMed.popupCb);
} else { } else {
mi->visible = true; mi->visible = true;
@ -208,10 +218,8 @@ static void loadFields(void) {
wgtCheckboxSetChecked(sMed.checkedCb, false); wgtCheckboxSetChecked(sMed.checkedCb, false);
wgtCheckboxSetChecked(sMed.radioCheckCb, false); wgtCheckboxSetChecked(sMed.radioCheckCb, false);
wgtCheckboxSetChecked(sMed.enabledCb, true); wgtCheckboxSetChecked(sMed.enabledCb, true);
if (sMed.popupCb) {
wgtCheckboxSetChecked(sMed.popupCb, false); wgtCheckboxSetChecked(sMed.popupCb, false);
wgtSetEnabled(sMed.popupCb, false); wgtSetEnabled(sMed.popupCb, false);
}
sMed.nameAutoGen = true; // new blank item -- auto-gen eligible sMed.nameAutoGen = true; // new blank item -- auto-gen eligible
return; return;
} }
@ -223,12 +231,11 @@ static void loadFields(void) {
wgtCheckboxSetChecked(sMed.checkedCb, mi->checked); wgtCheckboxSetChecked(sMed.checkedCb, mi->checked);
wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck); wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck);
wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled); wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled);
if (sMed.popupCb) {
// Popup (Visible=False) only applies to top-level menus. // Popup (Visible=False) only applies to top-level menus.
wgtCheckboxSetChecked(sMed.popupCb, mi->level == 0 && !mi->visible); wgtCheckboxSetChecked(sMed.popupCb, mi->level == 0 && !mi->visible);
wgtSetEnabled(sMed.popupCb, mi->level == 0); wgtSetEnabled(sMed.popupCb, mi->level == 0);
} }
}
bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) { bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
@ -247,14 +254,12 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
// If empty, start with one blank item so the user can type immediately // If empty, start with one blank item so the user can type immediately
if (arrlen(sMed.items) == 0) { if (arrlen(sMed.items) == 0) {
DsgnMenuItemT mi; DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi)); dsgnMenuItemInit(&mi);
mi.enabled = true;
mi.visible = true;
arrput(sMed.items, mi); arrput(sMed.items, mi);
} }
// Create modal dialog // 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) { if (!win) {
arrfree(sMed.items); arrfree(sMed.items);
@ -265,29 +270,30 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
win->maxH = win->h; win->maxH = win->h;
WidgetT *root = wgtInitWindow(ctx, win); 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); WidgetT *capRow = wgtHBox(root);
capRow->spacing = wgtPixels(4); capRow->spacing = wgtPixels(MED_ROW_SPACING);
WidgetT *capLbl = wgtLabel(capRow, "Caption:"); WidgetT *capLbl = wgtLabel(capRow, "Caption:");
capLbl->minW = wgtPixels(60); capLbl->minW = wgtPixels(MED_LABEL_W);
sMed.captionInput = wgtTextInput(capRow, DSGN_MAX_TEXT); sMed.captionInput = wgtTextInput(capRow, DSGN_MAX_TEXT - 1);
sMed.captionInput->weight = WGT_WEIGHT_FILL; sMed.captionInput->weight = WGT_WEIGHT_FILL;
sMed.captionInput->onChange = onCaptionChange; sMed.captionInput->onChange = onCaptionChange;
// Name row // Name row
WidgetT *namRow = wgtHBox(root); WidgetT *namRow = wgtHBox(root);
namRow->spacing = wgtPixels(4); namRow->spacing = wgtPixels(MED_ROW_SPACING);
WidgetT *namLbl = wgtLabel(namRow, "Name:"); WidgetT *namLbl = wgtLabel(namRow, "Name:");
namLbl->minW = wgtPixels(60); namLbl->minW = wgtPixels(MED_LABEL_W);
sMed.nameInput = wgtTextInput(namRow, DSGN_MAX_NAME); sMed.nameInput = wgtTextInput(namRow, DSGN_MAX_NAME - 1);
sMed.nameInput->weight = WGT_WEIGHT_FILL; sMed.nameInput->weight = WGT_WEIGHT_FILL;
sMed.nameInput->onChange = onNameChange; sMed.nameInput->onChange = onNameChange;
// Check row // Check row
WidgetT *chkRow = wgtHBox(root); WidgetT *chkRow = wgtHBox(root);
chkRow->spacing = wgtPixels(12); chkRow->spacing = wgtPixels(MED_CHECK_SPACING);
sMed.checkedCb = wgtCheckbox(chkRow, "Checked"); sMed.checkedCb = wgtCheckbox(chkRow, "Checked");
sMed.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck"); sMed.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck");
sMed.enabledCb = wgtCheckbox(chkRow, "Enabled"); sMed.enabledCb = wgtCheckbox(chkRow, "Enabled");
@ -301,27 +307,27 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
// Arrow buttons // Arrow buttons
WidgetT *arrowRow = wgtHBox(root); WidgetT *arrowRow = wgtHBox(root);
arrowRow->spacing = wgtPixels(4); arrowRow->spacing = wgtPixels(MED_ROW_SPACING);
WidgetT *btnOut = wgtButton(arrowRow, "<-"); WidgetT *btnOut = wgtButton(arrowRow, "<-");
btnOut->onClick = onOutdent; btnOut->onClick = onOutdent;
btnOut->minW = wgtPixels(32); btnOut->minW = wgtPixels(MED_ARROW_BTN_W);
WidgetT *btnIn = wgtButton(arrowRow, "->"); WidgetT *btnIn = wgtButton(arrowRow, "->");
btnIn->onClick = onIndent; btnIn->onClick = onIndent;
btnIn->minW = wgtPixels(32); btnIn->minW = wgtPixels(MED_ARROW_BTN_W);
WidgetT *btnUp = wgtButton(arrowRow, "Up"); WidgetT *btnUp = wgtButton(arrowRow, "Up");
btnUp->onClick = onMoveUp; btnUp->onClick = onMoveUp;
btnUp->minW = wgtPixels(32); btnUp->minW = wgtPixels(MED_ARROW_BTN_W);
WidgetT *btnDn = wgtButton(arrowRow, "Dn"); WidgetT *btnDn = wgtButton(arrowRow, "Dn");
btnDn->onClick = onMoveDown; btnDn->onClick = onMoveDown;
btnDn->minW = wgtPixels(32); btnDn->minW = wgtPixels(MED_ARROW_BTN_W);
// Action buttons // Action buttons
WidgetT *actRow = wgtHBox(root); WidgetT *actRow = wgtHBox(root);
actRow->spacing = wgtPixels(4); actRow->spacing = wgtPixels(MED_ROW_SPACING);
WidgetT *btnNext = wgtButton(actRow, "&Next"); WidgetT *btnNext = wgtButton(actRow, "&Next");
btnNext->onClick = onNext; btnNext->onClick = onNext;
@ -334,15 +340,15 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
// OK / Cancel // OK / Cancel
WidgetT *okRow = wgtHBox(root); WidgetT *okRow = wgtHBox(root);
okRow->spacing = wgtPixels(8); okRow->spacing = wgtPixels(MED_OK_SPACING);
WidgetT *btnOk = wgtButton(okRow, "OK"); WidgetT *btnOk = wgtButton(okRow, "OK");
btnOk->onClick = onOk; btnOk->onClick = onOk;
btnOk->minW = wgtPixels(60); btnOk->minW = wgtPixels(MED_OK_BTN_W);
WidgetT *btnCancel = wgtButton(okRow, "Cancel"); WidgetT *btnCancel = wgtButton(okRow, "Cancel");
btnCancel->onClick = onCancel; btnCancel->onClick = onCancel;
btnCancel->minW = wgtPixels(60); btnCancel->minW = wgtPixels(MED_OK_BTN_W);
// Populate // Populate
rebuildList(); rebuildList();
@ -463,9 +469,7 @@ static void onInsert(WidgetT *w) {
applyFields(); applyFields();
DsgnMenuItemT mi; DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi)); dsgnMenuItemInit(&mi);
mi.enabled = true;
mi.visible = true;
// Insert after the current item's subtree, at the same level // Insert after the current item's subtree, at the same level
int32_t insertAt; int32_t insertAt;
@ -620,9 +624,7 @@ static void onNext(WidgetT *w) {
} else { } else {
// Append new item // Append new item
DsgnMenuItemT mi; DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi)); dsgnMenuItemInit(&mi);
mi.enabled = true;
mi.visible = true;
if (count > 0) { if (count > 0) {
mi.level = sMed.items[count - 1].level; mi.level = sMed.items[count - 1].level;
@ -649,32 +651,47 @@ static void onOk(WidgetT *w) {
} }
} }
// Validate: check names are non-empty and unique // 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 count = (int32_t)arrlen(sMed.items);
int32_t badIdx = -1;
for (int32_t i = 0; i < count; i++) { const char *reason = NULL;
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 j = i + 1; j < count; j++) {
if (strcasecmp(sMed.items[i].name, sMed.items[j].name) == 0) {
char msg[MED_MSG_BUF]; char msg[MED_MSG_BUF];
snprintf(msg, sizeof(msg), "Duplicate menu name: %s", sMed.items[i].name);
dvxErrorBox(sMed.ctx, "Menu Editor", msg); for (int32_t i = 0; i < count && badIdx < 0; i++) {
sMed.selectedIdx = j; 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 && 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(); rebuildList();
loadFields(); loadFields();
wgtSetFocused(sMed.captionInput); wgtSetFocused(sMed.captionInput);
return; return;
} }
}
}
// Copy working items back to form // Copy working items back to form
arrfree(sMed.form->menuItems); arrfree(sMed.form->menuItems);
@ -727,7 +744,7 @@ static void rebuildList(void) {
arrsetlen(sLabels, 0); arrsetlen(sLabels, 0);
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
char buf[DSGN_MAX_TEXT + 32]; char buf[MED_LABEL_BUF];
int32_t pos = 0; int32_t pos = 0;
buf[0] = '\0'; buf[0] = '\0';

View file

@ -71,7 +71,11 @@
#define PRJ_WIN_W 180 #define PRJ_WIN_W 180
#define PRJ_WIN_H 300 #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_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_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 #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_W 70
#define PPD_BTN_H 24 #define PPD_BTN_H 24
#define PPD_DESC_H 60 #define PPD_DESC_H 60
#define PPD_ROOT_SPACING 2
#define PPD_ROW_SPACING 4
// ============================================================ // ============================================================
// Module state // Module state
@ -121,7 +127,7 @@ static struct {
static void onPrjWinClose(WindowT *win); static void onPrjWinClose(WindowT *win);
static void onTreeItemDblClick(WidgetT *w); static void onTreeItemDblClick(WidgetT *w);
static void onTreeSelChanged(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 ppdLoadIconPreview(void);
static void ppdOnBrowseHelp(WidgetT *w); static void ppdOnBrowseHelp(WidgetT *w);
static void ppdOnBrowseIcon(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); WidgetT *row = wgtHBox(parent);
row->spacing = wgtPixels(4); row->spacing = wgtPixels(PPD_ROW_SPACING);
WidgetT *lbl = wgtLabel(row, labelText); WidgetT *lbl = wgtLabel(row, labelText);
lbl->minW = wgtPixels(PPD_LABEL_W); lbl->minW = wgtPixels(PPD_LABEL_W);
WidgetT *input = wgtTextInput(row, maxLen); WidgetT *input = wgtTextInput(row, fieldSize - 1);
input->weight = WGT_WEIGHT_FILL; input->weight = WGT_WEIGHT_FILL;
wgtSetText(input, value); wgtSetText(input, value);
@ -217,7 +226,7 @@ static void ppdOnBrowseHelp(WidgetT *w) {
char path[DVX_MAX_PATH]; 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; return;
} }
@ -238,7 +247,7 @@ static void ppdOnBrowseIcon(WidgetT *w) {
char path[DVX_MAX_PATH]; 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)) { if (!validateIcon(path, true)) {
return; return;
} }
@ -268,7 +277,7 @@ static void ppdOnBrowseIcon(WidgetT *w) {
if (existing) { if (existing) {
fclose(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); snprintf(msg, sizeof(msg), "%s already exists.\nOverwrite it?", fname);
int32_t ow = dvxMessageBox(sPpd.ctx, "Overwrite", msg, MB_YESNO | MB_ICONQUESTION); int32_t ow = dvxMessageBox(sPpd.ctx, "Overwrite", msg, MB_YESNO | MB_ICONQUESTION);
@ -277,42 +286,7 @@ static void ppdOnBrowseIcon(WidgetT *w) {
} }
} }
// Copy the file if (!platformCopyFile(path, destPath)) {
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);
dvxErrorBox(sPpd.ctx, NULL, "Failed to copy icon into the project directory."); dvxErrorBox(sPpd.ctx, NULL, "Failed to copy icon into the project directory.");
return; return;
} }
@ -386,7 +360,7 @@ WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onCli
sOnClick = onClick; sOnClick = onClick;
sOnSelChange = onSelChange; 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) { if (!sPrjWin) {
return NULL; return NULL;
@ -438,6 +412,7 @@ void prjDestroyWindow(AppContextT *ctx, WindowT *win) {
sTree = NULL; sTree = NULL;
sPrj = NULL; sPrj = NULL;
sOnClick = NULL; sOnClick = NULL;
sOnSelChange = NULL;
} }
@ -479,7 +454,9 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
return false; 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); snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath);
// Derive project directory // Derive project directory
@ -551,7 +528,7 @@ void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
// Extract form name from .frm files // Extract form name from .frm files
if (prj->files[i].isForm) { 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 // 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 // Apply defaults from preferences
if (prefs) { if (prefs) {
snprintf(prj->author, sizeof(prj->author), "%s", prefsGetString(prefs, "defaults", "author", "")); 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, "defaults", "publisher", "")); 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, "defaults", "version", "1.0")); 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, "defaults", "copyright", "")); 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, "defaults", "description", "")); snprintf(prj->description, sizeof(prj->description), "%s", prefsGetString(prefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_DESCRIPTION, ""));
prj->optionExplicit = prefsGetBool(prefs, "editor", "optionExplicit", false); 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; return false;
} }
root->spacing = wgtPixels(2); root->spacing = wgtPixels(PPD_ROOT_SPACING);
sPpd.name = ppdAddRow(root, "Name:", prj->name, PRJ_MAX_NAME); sPpd.name = ppdAddRow(root, "Name:", prj->name, sizeof(prj->name));
sPpd.author = ppdAddRow(root, "Author:", prj->author, PRJ_MAX_STRING); sPpd.author = ppdAddRow(root, "Author:", prj->author, sizeof(prj->author));
sPpd.publisher = ppdAddRow(root, "Publisher:", prj->publisher, PRJ_MAX_STRING); sPpd.publisher = ppdAddRow(root, "Publisher:", prj->publisher, sizeof(prj->publisher));
sPpd.version = ppdAddRow(root, "Version:", prj->version, PRJ_MAX_NAME); sPpd.version = ppdAddRow(root, "Version:", prj->version, sizeof(prj->version));
sPpd.copyright = ppdAddRow(root, "Copyright:", prj->copyright, PRJ_MAX_STRING); sPpd.copyright = ppdAddRow(root, "Copyright:", prj->copyright, sizeof(prj->copyright));
// Startup form dropdown // Startup form dropdown
{ {
WidgetT *sfRow = wgtHBox(root); WidgetT *sfRow = wgtHBox(root);
sfRow->spacing = wgtPixels(4); sfRow->spacing = wgtPixels(PPD_ROW_SPACING);
WidgetT *sfLbl = wgtLabel(sfRow, "Startup Form:"); WidgetT *sfLbl = wgtLabel(sfRow, "Startup Form:");
sfLbl->minW = wgtPixels(PPD_LABEL_W); 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 // Icon row: label + preview + Browse button
{ {
WidgetT *iconRow = wgtHBox(root); WidgetT *iconRow = wgtHBox(root);
iconRow->spacing = wgtPixels(4); iconRow->spacing = wgtPixels(PPD_ROW_SPACING);
WidgetT *iconLbl = wgtLabel(iconRow, "Icon:"); WidgetT *iconLbl = wgtLabel(iconRow, "Icon:");
iconLbl->minW = wgtPixels(PPD_LABEL_W); iconLbl->minW = wgtPixels(PPD_LABEL_W);
@ -728,8 +705,8 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
if (noIconData) { if (noIconData) {
sPpd.iconPreview = wgtImage(iconRow, noIconData, niW, niH, niP); sPpd.iconPreview = wgtImage(iconRow, noIconData, niW, niH, niP);
} else { } else {
uint8_t *placeholder = (uint8_t *)calloc(4, 1); uint8_t *placeholder = (uint8_t *)calloc(PRJ_PLACEHOLDER_BPP, 1);
sPpd.iconPreview = wgtImage(iconRow, placeholder, 1, 1, 4); sPpd.iconPreview = wgtImage(iconRow, placeholder, 1, 1, PRJ_PLACEHOLDER_BPP);
} }
WidgetT *browseBtn = wgtButton(iconRow, "Browse..."); WidgetT *browseBtn = wgtButton(iconRow, "Browse...");
@ -742,12 +719,12 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
// Help file row // Help file row
{ {
WidgetT *hlpRow = wgtHBox(root); WidgetT *hlpRow = wgtHBox(root);
hlpRow->spacing = wgtPixels(4); hlpRow->spacing = wgtPixels(PPD_ROW_SPACING);
WidgetT *hlpLbl = wgtLabel(hlpRow, "Help File:"); WidgetT *hlpLbl = wgtLabel(hlpRow, "Help File:");
hlpLbl->minW = wgtPixels(PPD_LABEL_W); 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; sPpd.helpFileInput->weight = WGT_WEIGHT_FILL;
wgtSetText(sPpd.helpFileInput, prj->helpFile); 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) // Description: label above, textarea below (matches Preferences layout)
wgtLabel(root, "Description:"); wgtLabel(root, "Description:");
sPpd.description = wgtTextArea(root, PRJ_MAX_DESC); sPpd.description = wgtTextArea(root, (int32_t)sizeof(prj->description) - 1);
sPpd.description->weight = WGT_WEIGHT_FILL; sPpd.description->weight = WGT_WEIGHT_FILL;
sPpd.description->minH = wgtPixels(PPD_DESC_H); sPpd.description->minH = wgtPixels(PPD_DESC_H);
wgtSetText(sPpd.description, prj->description); wgtSetText(sPpd.description, prj->description);
@ -886,8 +863,8 @@ void prjRebuildTree(PrjStateT *prj) {
wgtTreeItemSetExpanded(modsNode, true); wgtTreeItemSetExpanded(modsNode, true);
for (int32_t i = 0; i < prj->fileCount; i++) { for (int32_t i = 0; i < prj->fileCount; i++) {
char buf[DVX_MAX_PATH + 4]; char buf[DVX_MAX_PATH + sizeof(PRJ_MODIFIED_SUFFIX)];
snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? " *" : ""); snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? PRJ_MODIFIED_SUFFIX : "");
char *label = strdup(buf); char *label = strdup(buf);
arrput(sLabels, label); arrput(sLabels, label);
WidgetT *item = wgtTreeItem(prj->files[i].isForm ? formsNode : modsNode, 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 // Write the project files whose isForm flag matches into an INI section as
// File0, File1, ... plus a Count entry. // File0, File1, ... plus a Count entry.
static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, const char *section, bool isForm) { 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. // Returns true if valid. Shows an error dialog and returns false if not.
static bool validateIcon(const char *fullPath, bool showErrors) { static bool validateIcon(const char *fullPath, bool showErrors) {
int32_t infoW = 0; int32_t infoW = 0;
@ -995,10 +962,10 @@ static bool validateIcon(const char *fullPath, bool showErrors) {
return false; return false;
} }
if (infoW != 32 || infoH != 32) { if (infoW != PRJ_ICON_SIZE || infoH != PRJ_ICON_SIZE) {
if (showErrors) { if (showErrors) {
char msg[128]; char msg[PRJ_MSG_BUF];
snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH); 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); dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING);
} }
return false; return false;

View file

@ -28,6 +28,7 @@
#include "dvxApp.h" #include "dvxApp.h"
#include "dvxPrefs.h" #include "dvxPrefs.h"
#include "dvxTypes.h" #include "dvxTypes.h"
#include "../formrt/formrt.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
@ -40,6 +41,18 @@
#define PRJ_MAX_STRING 128 #define PRJ_MAX_STRING 128
#define PRJ_MAX_DESC 512 #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 // Project file entry
// ============================================================ // ============================================================
@ -48,7 +61,7 @@ typedef struct {
// Fields are ordered by alignment to minimize struct padding. // Fields are ordered by alignment to minimize struct padding.
char *buffer; // in-memory edit buffer (malloc'd, NULL = not loaded) char *buffer; // in-memory edit buffer (malloc'd, NULL = not loaded)
char path[DVX_MAX_PATH]; // relative path (8.3 DOS name) char path[DVX_MAX_PATH]; // relative path (8.3 DOS name)
char formName[PRJ_MAX_NAME]; // form object name (from "Begin Form <name>") char formName[BAS_MAX_IDENT]; // form object name (from "Begin Form <name>")
bool isForm; // true = .frm, false = .bas bool isForm; // true = .frm, false = .bas
bool modified; // true = buffer has unsaved changes bool modified; // true = buffer has unsaved changes
} PrjFileT; } PrjFileT;
@ -77,7 +90,7 @@ typedef struct {
char name[PRJ_MAX_NAME]; char name[PRJ_MAX_NAME];
char projectPath[DVX_MAX_PATH]; // full path to .dbp file char projectPath[DVX_MAX_PATH]; // full path to .dbp file
char projectDir[DVX_MAX_PATH]; // directory containing .dbp char projectDir[DVX_MAX_PATH]; // directory containing .dbp
char startupForm[PRJ_MAX_NAME]; char startupForm[BAS_MAX_IDENT];
// Project metadata (for binary generation) // Project metadata (for binary generation)
char author[PRJ_MAX_STRING]; char author[PRJ_MAX_STRING];
char publisher[PRJ_MAX_STRING]; char publisher[PRJ_MAX_STRING];
@ -97,7 +110,6 @@ typedef struct {
void prjInit(PrjStateT *prj); void prjInit(PrjStateT *prj);
bool prjLoad(PrjStateT *prj, const char *dbpPath); bool prjLoad(PrjStateT *prj, const char *dbpPath);
bool prjSave(const PrjStateT *prj); 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 prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandleT *prefs);
void prjClose(PrjStateT *prj); void prjClose(PrjStateT *prj);
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm); int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm);

View file

@ -28,6 +28,7 @@
// property value to edit it via an InputBox dialog. // property value to edit it via an InputBox dialog.
#include "ideProperties.h" #include "ideProperties.h"
#include "../formrt/formrt.h"
#include "../formrt/frmParser.h" #include "../formrt/frmParser.h"
#include "dvxDlg.h" #include "dvxDlg.h"
#include "dvxWm.h" #include "dvxWm.h"
@ -48,6 +49,9 @@
#define PRP_WIN_W 220 #define PRP_WIN_W 220
#define PRP_WIN_H 400 #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_QUERY_BUF 512 // SQL probe scratch buffer
#define PRP_PROMPT_BUF 128 // input-dialog prompt buffer #define PRP_PROMPT_BUF 128 // input-dialog prompt buffer
#define PRP_TITLE_SUFFIX_PAD 16 // pad over DSGN_MAX_TEXT for " [Design]" suffix #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_CELL_COLUMNS 2 // property grid: name column + value column
#define PRP_MAX_LAYOUT_NAMES 32 // designer dropdown: max layout container types #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_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 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 addPropRow(const char *name, const char *value);
static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled, int32_t depth); 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 WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index);
static void freeCellData(void); static void freeCellData(void);
static void freeTreeLabels(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 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 void resolveDbPath(const char *dbName, char *out, int32_t outSize);
static bool treeOrderMatches(void); 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) { 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. // recursion always terminates.
if (dsgnIsContainer(child->typeName) && if (dsgnIsContainer(child->typeName) &&
strcasecmp(child->name, parentName) != 0 && strcasecmp(child->name, parentName) != 0 &&
depth < PRP_MAX_NEST_DEPTH) { depth < DSGN_MAX_NEST_DEPTH) {
cascadeToChildren(ds, child->name, visible, enabled, depth + 1); cascadeToChildren(ds, child->name, visible, enabled, depth + 1);
} }
} }
} }
// Walk tree items recursively, collecting control names in order. // Walk tree items recursively, collecting controls in tree order together
static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, const char *parentName) { // 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) { for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
const char *label = (const char *)item->userData; 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++) { for (int32_t i = 0; i < srcCount; i++) {
if (strcmp(srcArr[i]->name, itemName) == 0 && srcArr[i]->index == itemIndex) { 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(*outArr, srcArr[i]);
arrput(*outParents, tp);
// Recurse into children (for containers) // Recurse into children (for containers)
if (item->firstChild) { if (item->firstChild) {
collectTreeOrder(item, srcArr, srcCount, outArr, itemName); collectTreeOrder(item, srcArr, srcCount, outArr, outParents, itemName);
} }
break; 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. // Walk tree items recursively to find the one matching a control name.
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index) { static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index) {
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) { 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 // Query with LIMIT 0 to get column names without fetching rows
char query[PRP_QUERY_BUF]; 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); snprintf(query, sizeof(query), "%s LIMIT 0", recSrc);
} else { } else {
snprintf(query, sizeof(query), "SELECT * FROM %s LIMIT 0", recSrc); snprintf(query, sizeof(query), "SELECT * FROM %s LIMIT 0", recSrc);
@ -363,33 +362,21 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
} }
// Determine the data type of a property by name. Checks built-in // Determine the editor type of a property by name. Designer-only rows
// properties first, then looks up the widget's interface descriptor. // and the special editors come first, then the widget's own interface
// Returns WGT_IFACE_STRING, WGT_IFACE_INT, or WGT_IFACE_BOOL. // (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) { 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, "Type") == 0) { return PROP_TYPE_READONLY; }
if (strcasecmp(propName, "Index") == 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 // 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, "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, "Layout") == 0) { return PROP_TYPE_LAYOUT; }
if (strcasecmp(propName, "HelpTopic") == 0) { return PROP_TYPE_STRING; }
// Special editors
if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; } if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; }
if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; } if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; }
if (strcasecmp(propName, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; } if (strcasecmp(propName, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; }
@ -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, "MasterField") == 0) { return PROP_TYPE_DATAFIELD; }
if (strcasecmp(propName, "DetailField") == 0) { return PROP_TYPE_DATAFIELD; } if (strcasecmp(propName, "DetailField") == 0) { return PROP_TYPE_DATAFIELD; }
// Look up in the widget's interface descriptor const WgtPropDescT *p = findIfaceProp(typeName, propName);
if (typeName && typeName[0]) {
const char *wgtName = wgtFindByBasName(typeName);
if (wgtName) {
const WgtIfaceT *iface = wgtGetIface(wgtName);
const WgtPropDescT *p = wgtIfaceFindProp(iface, propName);
if (p) { if (p) {
return p->type; 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 { } else {
DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx]; 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++) { for (int32_t pi = 0; pi < ctrl->propCount; pi++) {
if (strcasecmp(ctrl->props[pi].name, "Layout") == 0) { if (strcasecmp(ctrl->props[pi].name, "Layout") == 0) {
layoutField = ctrl->props[pi].value; layoutField = ctrl->props[pi].value;
@ -574,20 +569,10 @@ static void onPropDblClick(WidgetT *w) {
// layoutField points at (it points at a selected container's // layoutField points at (it points at a selected container's
// Layout prop when one is selected). Per-container layouts are // Layout prop when one is selected). Per-container layouts are
// applied separately inside dsgnCreateWidgets. // applied separately inside dsgnCreateWidgets.
WidgetT *contentBox = dsgnCreateContentBox(root, sDs->form->layout); sDs->form->contentBox = basFormRtCreateContentBox(root, sDs->form->layout);
dsgnRebuildWidgets(sDs);
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); dvxInvalidateWindow(sPrpCtx, sDs->formWin);
} }
}
prpRefresh(sDs); prpRefresh(sDs);
return; return;
@ -763,12 +748,16 @@ static void onPropDblClick(WidgetT *w) {
char oldName[DSGN_MAX_NAME]; char oldName[DSGN_MAX_NAME];
snprintf(oldName, sizeof(oldName), "%s", ctrl->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 // Rename all members of a control array, not just the selected one
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
DsgnControlT *c = sDs->form->controls[i]; DsgnControlT *c = sDs->form->controls[i];
if (strcasecmp(c->name, oldName) == 0) { 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) { if (c->widget) {
wgtSetName(c->widget, c->name); wgtSetName(c->widget, c->name);
@ -784,7 +773,7 @@ static void onPropDblClick(WidgetT *w) {
DsgnControlT *c = sDs->form->controls[i]; DsgnControlT *c = sDs->form->controls[i];
if (strcasecmp(c->parentName, oldName) == 0) { 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); ideRenameInCode(oldName, newValue);
prpRebuildTree(sDs); prpRebuildTree(sDs);
} else if (strcasecmp(propName, "MinWidth") == 0) { } else if (dsgnFindIntProp(propName)) {
ctrl->width = atoi(newValue); const DsgnIntPropT *ip = dsgnFindIntProp(propName);
if (ctrl->widget) { *(int32_t *)((char *)ctrl + ip->offset) = atoi(newValue);
ctrl->widget->minW = wgtPixels(ctrl->width); dsgnSyncWidgetGeom(ctrl);
}
} 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;
}
} else if (strcasecmp(propName, "Visible") == 0 && !dsgnIfaceHasProp(ctrl->typeName, "Visible")) { } else if (strcasecmp(propName, "Visible") == 0 && !dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
bool val = frmParseBool(newValue); bool val = frmParseBool(newValue);
ctrl->visible = val; ctrl->visible = val;
@ -874,43 +838,16 @@ static void onPropDblClick(WidgetT *w) {
const WgtPropDescT *p = wgtIfaceFindProp(iface, propName); const WgtPropDescT *p = wgtIfaceFindProp(iface, propName);
if (p && p->setFn) { if (p && p->setFn) {
if (p->type == WGT_IFACE_STRING) { // props[] is the design-time store (it survives a
// Strings must outlive this function, so the // widget rebuild and is what gets saved); the live
// ctrl->props[] copy is what we pass to setFn // widget mirrors it. Strings are passed from the
// (not the newValue buffer). // props[] copy so they outlive this function.
bool found = false; dsgnSetPropValue(ctrl, propName, newValue);
for (int32_t j = 0; j < ctrl->propCount; j++) { const char *stored = dsgnControlGetPropValue(ctrl, propName);
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;
}
}
if (!found && ctrl->propCount < DSGN_MAX_PROPS) { if (stored) {
snprintf(ctrl->props[ctrl->propCount].name, DSGN_MAX_NAME, "%s", propName); wgtApplyPropFromString(ctrl->widget, p, stored);
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;
}
}
} }
ifaceHandled = true; ifaceHandled = true;
@ -921,29 +858,14 @@ static void onPropDblClick(WidgetT *w) {
if (!ifaceHandled) { if (!ifaceHandled) {
// Custom prop storage // Custom prop storage
bool found = false; dsgnSetPropValue(ctrl, propName, newValue);
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++;
}
// Update widget text from the persistent props array // Update widget text from the persistent props array
if (ctrl->widget && (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0)) { if (ctrl->widget && (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0)) {
for (int32_t i = 0; i < ctrl->propCount; i++) { const char *stored = dsgnControlGetPropValue(ctrl, propName);
if (strcasecmp(ctrl->props[i].name, propName) == 0) {
wgtSetText(ctrl->widget, ctrl->props[i].value); if (stored) {
break; wgtSetText(ctrl->widget, stored);
}
} }
} }
} }
@ -956,19 +878,14 @@ static void onPropDblClick(WidgetT *w) {
} }
} else { } else {
if (strcasecmp(propName, "Name") == 0) { if (strcasecmp(propName, "Name") == 0) {
char oldName[DSGN_MAX_NAME]; char oldName[BAS_MAX_IDENT];
snprintf(oldName, sizeof(oldName), "%s", sDs->form->name); snprintf(oldName, sizeof(oldName), "%s", sDs->form->name);
// Length-clamped memcpy instead of strncpy/snprintf because if (!validateNewName(newValue, BAS_MAX_IDENT, oldName)) {
// GCC warns about both when source exceeds the buffer. return;
int32_t nl = (int32_t)strlen(newValue);
if (nl >= DSGN_MAX_NAME) {
nl = DSGN_MAX_NAME - 1;
} }
memcpy(sDs->form->name, newValue, nl); snprintf(sDs->form->name, sizeof(sDs->form->name), "%s", newValue);
sDs->form->name[nl] = '\0';
ideRenameInCode(oldName, sDs->form->name); ideRenameInCode(oldName, sDs->form->name);
prpRebuildTree(sDs); prpRebuildTree(sDs);
} else if (strcasecmp(propName, "Caption") == 0) { } else if (strcasecmp(propName, "Caption") == 0) {
@ -1049,37 +966,55 @@ static void onTreeChange(WidgetT *w) {
// Actual reorder happened -- rebuild the controls array from tree order. // Actual reorder happened -- rebuild the controls array from tree order.
int32_t count = (int32_t)arrlen(sDs->form->controls); int32_t count = (int32_t)arrlen(sDs->form->controls);
DsgnControlT **newArr = NULL; DsgnControlT **newArr = NULL;
PrpParentT *newParents = NULL;
WidgetT *formItem = sTree->firstChild; WidgetT *formItem = sTree->firstChild;
if (!formItem) { if (!formItem) {
return; return;
} }
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, ""); collectTreeOrder(formItem, sDs->form->controls, count, &newArr, &newParents, "");
// If we lost items (dragged above form), revert // Revert if items were lost (dragged above the form) or a control was
if ((int32_t)arrlen(newArr) != count) { // 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(newArr);
arrfree(newParents);
prpRebuildTree(sDs); prpRebuildTree(sDs);
return; return;
} }
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); arrfree(sDs->form->controls);
sDs->form->controls = newArr; sDs->form->controls = newArr;
sDs->form->dirty = true; sDs->form->dirty = true;
if (sDs->form->contentBox) { dsgnRebuildWidgets(sDs);
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);
}
prpRebuildTree(sDs); prpRebuildTree(sDs);
if (sDs->formWin) { if (sDs->formWin) {
@ -1220,8 +1155,8 @@ WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) {
sDs = ds; sDs = ds;
sPrpCtx = ctx; sPrpCtx = ctx;
int32_t winX = ctx->display.width - PRP_WIN_W - 10; int32_t winX = ctx->display.width - PRP_WIN_W - PRP_WIN_RIGHT_MARGIN;
WindowT *win = dvxCreateWindow(ctx, "Properties", winX, 30, PRP_WIN_W, PRP_WIN_H, true); WindowT *win = dvxCreateWindow(ctx, PRP_DIALOG_TITLE, winX, PRP_WIN_Y, PRP_WIN_W, PRP_WIN_H, true);
if (!win) { if (!win) {
return NULL; return NULL;
@ -1246,12 +1181,12 @@ WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) {
sPropList = wgtListView(splitter); sPropList = wgtListView(splitter);
sPropList->onDblClick = onPropDblClick; sPropList->onDblClick = onPropDblClick;
static const ListViewColT cols[2] = { static const ListViewColT cols[PRP_CELL_COLUMNS] = {
{ "Property", 0, ListViewAlignLeftE }, { "Property", 0, ListViewAlignLeftE },
{ "Value", 0, ListViewAlignLeftE } { "Value", 0, ListViewAlignLeftE }
}; };
wgtListViewSetColumns(sPropList, cols, 2); wgtListViewSetColumns(sPropList, cols, PRP_CELL_COLUMNS);
prpRebuildTree(ds); prpRebuildTree(ds);
prpRefresh(ds); prpRefresh(ds);
@ -1326,7 +1261,7 @@ void prpRebuildTree(DsgnStateT *ds) {
if (ctrl->parentName[0]) { if (ctrl->parentName[0]) {
for (int32_t j = 0; j < i; j++) { 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]; treeParent = treeItems[j];
break; break;
} }
@ -1390,7 +1325,7 @@ void prpRefresh(DsgnStateT *ds) {
if (ds->selectedIdx >= 0 && ds->selectedIdx < count) { if (ds->selectedIdx >= 0 && ds->selectedIdx < count) {
DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx]; DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
char buf[32]; char buf[PRP_INT_BUF];
addPropRow("Name", ctrl->name); addPropRow("Name", ctrl->name);
@ -1401,20 +1336,12 @@ void prpRefresh(DsgnStateT *ds) {
addPropRow("Type", ctrl->typeName); addPropRow("Type", ctrl->typeName);
snprintf(buf, sizeof(buf), "%d", (int)ctrl->width); for (int32_t i = 0; dsgnIntPropAt(i); i++) {
addPropRow("MinWidth", buf); const DsgnIntPropT *ip = dsgnIntPropAt(i);
snprintf(buf, sizeof(buf), "%d", (int)ctrl->height); snprintf(buf, sizeof(buf), "%d", (int)*(const int32_t *)((const char *)ctrl + ip->offset));
addPropRow("MinHeight", buf); addPropRow(ip->name, 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);
if (!dsgnIfaceHasProp(ctrl->typeName, "Visible")) { if (!dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
addPropRow("Visible", ctrl->visible ? "True" : "False"); addPropRow("Visible", ctrl->visible ? "True" : "False");
@ -1430,6 +1357,12 @@ void prpRefresh(DsgnStateT *ds) {
addPropRow(ctrl->props[i].name, ctrl->props[i].value); 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) // Widget interface properties (from the .wgt descriptor)
const char *wgtName = wgtFindByBasName(ctrl->typeName); const char *wgtName = wgtFindByBasName(ctrl->typeName);
@ -1474,28 +1407,19 @@ void prpRefresh(DsgnStateT *ds) {
} }
} }
} else { } 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); for (int32_t i = 0; i < formPropCount; i++) {
addPropRow("Caption", ds->form->caption); char valBuf[DSGN_MAX_TEXT];
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");
snprintf(buf, sizeof(buf), "%d", (int)ds->form->left); if (dsgnFormPropValue(ds->form, formProps[i].name, valBuf, sizeof(valBuf))) {
addPropRow("Left", buf); addPropRow(formProps[i].name, valBuf);
}
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);
} }
wgtListViewSetData(sPropList, (const char **)sCellData, (int32_t)arrlen(sCellData) / PRP_CELL_COLUMNS); wgtListViewSetData(sPropList, (const char **)sCellData, (int32_t)arrlen(sCellData) / PRP_CELL_COLUMNS);
@ -1529,38 +1453,19 @@ static bool treeOrderMatches(void) {
} }
int32_t count = (int32_t)arrlen(sDs->form->controls); int32_t count = (int32_t)arrlen(sDs->form->controls);
// 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; DsgnControlT **newArr = NULL;
PrpParentT *newParents = 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); bool match = ((int32_t)arrlen(newArr) == count);
if (match) { if (match) {
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
if (newArr[i] != sDs->form->controls[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; match = false;
break; break;
} }
@ -1568,6 +1473,33 @@ static bool treeOrderMatches(void) {
} }
arrfree(newArr); arrfree(newArr);
free(oldParents); arrfree(newParents);
return match; 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;
}

View file

@ -46,6 +46,14 @@
#define TBX_COLS 4 #define TBX_COLS 4
#define TBX_WIN_W 120 #define TBX_WIN_W 120
#define TBX_WIN_H 250 #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 // Per-tool entry
@ -53,7 +61,7 @@
typedef struct { typedef struct {
char typeName[DSGN_MAX_NAME]; char typeName[DSGN_MAX_NAME];
char tooltip[64]; char tooltip[TBX_TOOLTIP_LEN];
} TbxToolEntryT; } TbxToolEntryT;
// ============================================================ // ============================================================
@ -139,15 +147,15 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
if (wgtPath) { if (wgtPath) {
// Build suffixed resource names: "icon24", "icon24-2", etc. // Build suffixed resource names: "icon24", "icon24-2", etc.
char iconResName[32]; char iconResName[TBX_RES_NAME_LEN];
char nameResName[32]; char nameResName[TBX_RES_NAME_LEN];
if (pathIdx <= 1) { if (pathIdx <= 1) {
snprintf(iconResName, sizeof(iconResName), "icon24"); snprintf(iconResName, sizeof(iconResName), "%s", TBX_RES_ICON);
snprintf(nameResName, sizeof(nameResName), "name"); snprintf(nameResName, sizeof(nameResName), "%s", TBX_RES_NAME);
} else { } else {
snprintf(iconResName, sizeof(iconResName), "icon24-%d", (int)pathIdx); snprintf(iconResName, sizeof(iconResName), TBX_RES_SUFFIX_FMT, TBX_RES_ICON, (int)pathIdx);
snprintf(nameResName, sizeof(nameResName), "name-%d", (int)pathIdx); snprintf(nameResName, sizeof(nameResName), TBX_RES_SUFFIX_FMT, TBX_RES_NAME, (int)pathIdx);
} }
iconData = dvxResLoadIcon(ctx, wgtPath, iconResName, &iconW, &iconH, &iconPitch); 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; int32_t toolIdx = (int32_t)arrlen(sTbxTools) - 1;
// Start a new row every TBX_COLS buttons // Start a new row every TBX_COLS buttons
if (col == 0 || !row) { if (col == 0) {
row = wgtHBox(root); row = wgtHBox(root);
row->spacing = 0; row->spacing = 0;
} }

View file

@ -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$). 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 .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. 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 .endcode
.note info .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 .endnote
Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n. 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 ERROR n ' Raise a runtime error with error number n
.endcode .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 .code
On Error GoTo ErrorHandler On Error GoTo ErrorHandler
@ -952,16 +963,20 @@ ErrorHandler:
------ ------- ------ -------
1 FOR loop error (NEXT without FOR, NEXT variable mismatch, FOR stack underflow) 1 FOR loop error (NEXT without FOR, NEXT variable mismatch, FOR stack underflow)
4 Out of DATA 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 7 Out of memory
9 Subscript out of range / invalid variable or field index 9 Subscript out of range / invalid variable or field index
11 Division by zero 11 Division by zero
13 Type mismatch / not an array / not a TYPE instance 13 Type mismatch / not an array / not a TYPE instance
20 RESUME without error
26 FOR loop nesting too deep 26 FOR loop nesting too deep
51 Internal error (bad opcode) 51 Internal error (bad opcode)
52 Bad file number or file not open 52 Bad file number or file not open
53 File not found 53 File not found
54 Bad file mode 54 Bad file mode
58 File already exists or rename failed 58 File already exists or rename failed
59 Bad record length (OPEN ... LEN must be 1 to 32767)
67 Too many files open 67 Too many files open
75 Path/file access error 75 Path/file access error
76 Path not found 76 Path not found
@ -1045,7 +1060,7 @@ OPEN filename$ FOR BINARY AS #channel
INPUT Open for sequential reading. File must exist. INPUT Open for sequential reading. File must exist.
OUTPUT Open for sequential writing. Creates or truncates. OUTPUT Open for sequential writing. Creates or truncates.
APPEND Open for sequential writing at end of file. 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. BINARY Open for raw binary I/O.
.endtable .endtable
@ -1067,10 +1082,16 @@ PRINT #channel, expression
.h2 INPUT # .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 .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 .endcode
.h2 LINE INPUT # .h2 LINE INPUT #
@ -1096,13 +1117,26 @@ Write #1, "Scott", 42, 3.14
.h2 GET / PUT .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 .code
GET #channel, [recordNum], variable GET #channel, [recordNum], variable
PUT #channel, [recordNum], variable PUT #channel, [recordNum], variable
.endcode .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 .h2 SEEK
Sets the file position. As a function, returns the current position. 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 CHR$(n) String Character with ASCII code n
FORMAT$(value, fmt$) String Formats a numeric value using a format string FORMAT$(value, fmt$) String Formats a numeric value using a format string
HEX$(n) String Hexadecimal representation of n (uppercase, no leading &H) 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(s$, find$) Long Position of find$ in s$ (1-based), 0 if not found
INSTR(start, s$, find$) Integer Search starting at position start (1-based) INSTR(start, s$, find$) Long Search starting at position start (1-based)
LCASE$(s$) String Converts s$ to lowercase LCASE$(s$) String Converts s$ to lowercase
LEFT$(s$, n) String Leftmost n characters of s$ 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$ LTRIM$(s$) String Removes leading spaces from s$
MID$(s$, start) String Substring from start (1-based) to end of string 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 MID$(s$, start, length) String Substring of length characters starting at start
OCT$(n) String Octal representation of n (no leading &O) OCT$(n) String Octal representation of n (no leading &O)
RIGHT$(s$, n) String Rightmost n characters of s$ RIGHT$(s$, n) String Rightmost n characters of s$
RTRIM$(s$) String Removes trailing spaces from 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) 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$ TRIM$(s$) String Removes leading and trailing spaces from s$
UCASE$(s$) String Converts s$ to uppercase UCASE$(s$) String Converts s$ to uppercase
VAL(s$) Double Converts string s$ to a numeric value; stops at first non-numeric character VAL(s$) Double Converts string s$ to a numeric value; stops at first non-numeric character
.endtable .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$ .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. 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 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 CDBL(n) Double Converts n to Double
CINT(n) Integer Converts n to Integer (rounds half away from zero) 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 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 CSNG(n) Single Converts n to Single
CSTR(n) String Converts n to its String representation CSTR(n) String Converts n to its String representation
.endtable .endtable

View file

@ -38,16 +38,19 @@
#define BAS_ERR_NEXT_WITHOUT_FOR 1 #define BAS_ERR_NEXT_WITHOUT_FOR 1
#define BAS_ERR_OUT_OF_DATA 4 #define BAS_ERR_OUT_OF_DATA 4
#define BAS_ERR_ILLEGAL_FUNC_CALL 5 #define BAS_ERR_ILLEGAL_FUNC_CALL 5
#define BAS_ERR_OVERFLOW 6
#define BAS_ERR_OUT_OF_MEMORY 7 #define BAS_ERR_OUT_OF_MEMORY 7
#define BAS_ERR_SUBSCRIPT_RANGE 9 #define BAS_ERR_SUBSCRIPT_RANGE 9
#define BAS_ERR_DIV_BY_ZERO 11 #define BAS_ERR_DIV_BY_ZERO 11
#define BAS_ERR_TYPE_MISMATCH 13 #define BAS_ERR_TYPE_MISMATCH 13
#define BAS_ERR_RESUME_WITHOUT_ERR 20
#define BAS_ERR_FOR_NESTING 26 #define BAS_ERR_FOR_NESTING 26
#define BAS_ERR_BAD_OPCODE 51 #define BAS_ERR_BAD_OPCODE 51
#define BAS_ERR_BAD_FILE_NUM 52 #define BAS_ERR_BAD_FILE_NUM 52
#define BAS_ERR_FILE_NOT_FOUND 53 #define BAS_ERR_FILE_NOT_FOUND 53
#define BAS_ERR_BAD_FILE_MODE 54 #define BAS_ERR_BAD_FILE_MODE 54
#define BAS_ERR_FILE_EXISTS 58 #define BAS_ERR_FILE_EXISTS 58
#define BAS_ERR_BAD_RECORD_LEN 59
#define BAS_ERR_TOO_MANY_FILES 67 #define BAS_ERR_TOO_MANY_FILES 67
#define BAS_ERR_PATH_FILE_ACCESS 75 #define BAS_ERR_PATH_FILE_ACCESS 75
#define BAS_ERR_PATH_NOT_FOUND 76 #define BAS_ERR_PATH_NOT_FOUND 76

View file

@ -48,7 +48,18 @@
// localCount int32 // localCount int32
// returnType uint8 // returnType uint8
// isFunction 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 "serialize.h"
#include "../compiler/opcodes.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++) { for (int32_t i = 0; i < mod->debugVarCount; i++) {
BasDebugVarT *v = &mod->debugVars[i]; BasDebugVarT *v = &mod->debugVars[i];
rStrInto(&r, v->name, BAS_MAX_PROC_NAME); rStrInto(&r, v->name, BAS_MAX_IDENT);
rStrInto(&r, v->formName, BAS_MAX_PROC_NAME); rStrInto(&r, v->formName, BAS_MAX_IDENT);
v->scope = rU8(&r); v->scope = rU8(&r);
v->dataType = rU8(&r); v->dataType = rU8(&r);
v->index = rI32(&r); v->index = rI32(&r);
@ -372,8 +383,8 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
p->returnType = rU8(&r); p->returnType = rU8(&r);
p->isFunction = rU8(&r) != 0; p->isFunction = rU8(&r) != 0;
rStrInto(&r, p->name, BAS_MAX_PROC_NAME); rStrInto(&r, p->name, BAS_MAX_IDENT);
rStrInto(&r, p->formName, BAS_MAX_PROC_NAME); 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++) { for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
BasFormVarInfoT *fv = &mod->formVarInfo[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->varCount = rI32(&r);
fv->initCodeAddr = rI32(&r); fv->initCodeAddr = rI32(&r);
fv->initCodeLen = rI32(&r); fv->initCodeLen = rI32(&r);

View file

@ -36,6 +36,11 @@
#include <string.h> #include <string.h>
#define BAS_STRING_IMMORTAL_REFCOUNT 999999 // sentinel refCount; empty string is never freed (see basStringUnref) #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 // 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 *basArrayNew(int32_t dims, int32_t *lbounds, int32_t *ubounds, uint8_t elementType);
BasArrayT *basArrayRef(BasArrayT *arr); BasArrayT *basArrayRef(BasArrayT *arr);
void basArrayUnref(BasArrayT *arr); void basArrayUnref(BasArrayT *arr);
static int32_t basClampToRange(double n, int32_t lo, int32_t hi);
BasStringT *basStringAlloc(int32_t cap); BasStringT *basStringAlloc(int32_t cap);
int32_t basStringCompare(const BasStringT *a, const BasStringT *b); int32_t basStringCompare(const BasStringT *a, const BasStringT *b);
int32_t basStringCompareCI(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 *basStringNew(const char *text, int32_t len);
BasStringT *basStringRef(BasStringT *s); BasStringT *basStringRef(BasStringT *s);
BasStringT *basStringSub(const BasStringT *s, int32_t start, int32_t len); BasStringT *basStringSub(const BasStringT *s, int32_t start, int32_t len);
void basStringSystemInit(void);
void basStringSystemShutdown(void);
void basStringUnref(BasStringT *s); void basStringUnref(BasStringT *s);
BasUdtT *basUdtClone(const BasUdtT *udt);
void basUdtFree(BasUdtT *udt); void basUdtFree(BasUdtT *udt);
BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount); BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount);
BasUdtT *basUdtRef(BasUdtT *udt); BasUdtT *basUdtRef(BasUdtT *udt);
@ -82,18 +85,18 @@ BasValueT basValInteger(int16_t v);
bool basValIsTruthy(BasValueT v); bool basValIsTruthy(BasValueT v);
BasValueT basValLong(int32_t v); BasValueT basValLong(int32_t v);
BasValueT basValObject(void *obj); BasValueT basValObject(void *obj);
uint8_t basValPromoteType(uint8_t a, uint8_t b); double basParseNumber(const char *s);
void basValRelease(BasValueT *v); void basValRelease(BasValueT *v);
bool basValRoundToInt32(BasValueT v, int32_t lo, int32_t hi, int32_t *out);
BasValueT basValSingle(float v); BasValueT basValSingle(float v);
BasValueT basValString(BasStringT *s); BasValueT basValString(BasStringT *s);
BasValueT basValStringFromC(const char *text); BasValueT basValStringFromC(const char *text);
BasValueT basValToBool(BasValueT v); BasValueT basValToBool(BasValueT v);
BasValueT basValToDouble(BasValueT v); BasValueT basValToDouble(BasValueT v);
BasValueT basValToInteger(BasValueT v); int32_t basValToInt32(BasValueT v);
BasValueT basValToLong(BasValueT v);
double basValToNumber(BasValueT v); double basValToNumber(BasValueT v);
BasValueT basValToSingle(BasValueT v);
BasValueT basValToString(BasValueT v); BasValueT basValToString(BasValueT v);
static int32_t formatDouble(double n, int32_t sigDigits, char *buf, int32_t bufSize);
// ============================================================ // ============================================================
// Array system // 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) { BasStringT *basStringAlloc(int32_t cap) {
if (cap < 1) { if (cap < 1) {
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) { void basStringUnref(BasStringT *s) {
if (!s || s == basEmptyString) { if (!s || s == basEmptyString) {
return; return;
@ -392,6 +371,35 @@ void basStringUnref(BasStringT *s) {
// ============================================================ // ============================================================
// UDT system // 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) { void basUdtFree(BasUdtT *udt) {
if (!udt) { if (!udt) {
return; return;
@ -455,6 +463,78 @@ void basUdtUnref(BasUdtT *udt) {
// ============================================================ // ============================================================
// Value constructors / refcount helpers // 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 basValBool(bool v) {
BasValueT val; BasValueT val;
val.type = BAS_TYPE_BOOLEAN; val.type = BAS_TYPE_BOOLEAN;
@ -504,6 +584,8 @@ BasValueT basValCopy(BasValueT v) {
basArrayRef(v.arrVal); basArrayRef(v.arrVal);
} else if (v.type == BAS_TYPE_UDT && v.udtVal) { } else if (v.type == BAS_TYPE_UDT && v.udtVal) {
basUdtRef(v.udtVal); basUdtRef(v.udtVal);
} else if (v.type == BAS_TYPE_ELEM_REF) {
basArrayRef(v.elemRef.arr);
} }
return v; return v;
@ -519,7 +601,7 @@ BasValueT basValDouble(double v) {
BasStringT *basValFormatString(BasValueT v) { BasStringT *basValFormatString(BasValueT v) {
char buf[64]; char buf[BAS_NUM_FORMAT_BUF_LEN];
switch (v.type) { switch (v.type) {
case BAS_TYPE_INTEGER: case BAS_TYPE_INTEGER:
@ -530,14 +612,11 @@ BasStringT *basValFormatString(BasValueT v) {
snprintf(buf, sizeof(buf), "%ld", (long)v.longVal); snprintf(buf, sizeof(buf), "%ld", (long)v.longVal);
return basStringNew(buf, (int32_t)strlen(buf)); return basStringNew(buf, (int32_t)strlen(buf));
case BAS_TYPE_SINGLE: { case BAS_TYPE_SINGLE:
snprintf(buf, sizeof(buf), "%g", (double)v.sngVal); return basStringNew(buf, formatDouble((double)v.sngVal, BAS_SINGLE_SIG_DIGITS, buf, sizeof(buf)));
return basStringNew(buf, (int32_t)strlen(buf));
}
case BAS_TYPE_DOUBLE: case BAS_TYPE_DOUBLE:
snprintf(buf, sizeof(buf), "%g", v.dblVal); return basStringNew(buf, formatDouble(v.dblVal, BAS_DOUBLE_SIG_DIGITS, buf, sizeof(buf)));
return basStringNew(buf, (int32_t)strlen(buf));
case BAS_TYPE_BOOLEAN: case BAS_TYPE_BOOLEAN:
return basStringNew(v.boolVal ? "True" : "False", v.boolVal ? 4 : 5); 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) { void basValRelease(BasValueT *v) {
if (v->type == BAS_TYPE_STRING) { if (v->type == BAS_TYPE_STRING) {
basStringUnref(v->strVal); basStringUnref(v->strVal);
@ -636,10 +690,33 @@ void basValRelease(BasValueT *v) {
} else if (v->type == BAS_TYPE_UDT) { } else if (v->type == BAS_TYPE_UDT) {
basUdtUnref(v->udtVal); basUdtUnref(v->udtVal);
v->udtVal = NULL; 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 basValSingle(float v) {
BasValueT val; BasValueT val;
val.type = BAS_TYPE_SINGLE; 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); 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. if (isnan(n)) {
int32_t rounded = basClampToRange(n, INT16_MIN, INT16_MAX); return 0;
return basValInteger((int16_t)rounded);
} }
if (n >= (double)INT32_MAX) {
return INT32_MAX;
}
BasValueT basValToLong(BasValueT v) { if (n <= (double)INT32_MIN) {
double n = basValToNumber(v); return INT32_MIN;
// 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 (int32_t)n;
return basValLong(rounded);
} }
@ -714,7 +792,7 @@ double basValToNumber(BasValueT v) {
case BAS_TYPE_STRING: case BAS_TYPE_STRING:
if (v.strVal && v.strVal->len > 0) { if (v.strVal && v.strVal->len > 0) {
return atof(v.strVal->data); return basParseNumber(v.strVal->data);
} }
return 0.0; return 0.0;
@ -725,11 +803,6 @@ double basValToNumber(BasValueT v) {
} }
BasValueT basValToSingle(BasValueT v) {
return basValSingle((float)basValToNumber(v));
}
BasValueT basValToString(BasValueT v) { BasValueT basValToString(BasValueT v) {
if (v.type == BAS_TYPE_STRING) { if (v.type == BAS_TYPE_STRING) {
// Normalize a NULL strVal to the empty string so callers can // Normalize a NULL strVal to the empty string so callers can
@ -748,3 +821,23 @@ BasValueT basValToString(BasValueT v) {
result.strVal = s; result.strVal = s;
return result; 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;
}

View file

@ -76,10 +76,6 @@ int32_t basStringCompareCI(const BasStringT *a, const BasStringT *b);
// The empty string singleton (never freed). // The empty string singleton (never freed).
extern BasStringT *basEmptyString; extern BasStringT *basEmptyString;
// Initialize/shutdown the string system.
void basStringSystemInit(void);
void basStringSystemShutdown(void);
// ============================================================ // ============================================================
// Forward declarations // Forward declarations
// ============================================================ // ============================================================
@ -140,6 +136,11 @@ BasUdtT *basUdtRef(BasUdtT *udt);
// Decrement reference count. Frees if count reaches zero. // Decrement reference count. Frees if count reaches zero.
void basUdtUnref(BasUdtT *udt); 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 // Tagged value
// ============================================================ // ============================================================
@ -157,6 +158,10 @@ struct BasValueTag {
BasUdtT *udtVal; // BAS_TYPE_UDT (ref-counted) BasUdtT *udtVal; // BAS_TYPE_UDT (ref-counted)
void *objVal; // BAS_TYPE_OBJECT (opaque host pointer) void *objVal; // BAS_TYPE_OBJECT (opaque host pointer)
BasValueT *refVal; // BAS_TYPE_REF (ByRef pointer to variable slot) 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). // Get the numeric value as a double (for mixed-type arithmetic).
double basValToNumber(BasValueT v); 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. // Get the string representation. Returns a new ref-counted string.
BasStringT *basValFormatString(BasValueT v); BasStringT *basValFormatString(BasValueT v);
@ -202,8 +222,4 @@ int32_t basValCompare(BasValueT a, BasValueT b);
// Compare two values case-insensitively (for OPTION COMPARE TEXT). // Compare two values case-insensitively (for OPTION COMPARE TEXT).
int32_t basValCompareCI(BasValueT a, BasValueT b); 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 #endif // DVXBASIC_VALUES_H

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,7 @@
// BasVmT *vm = basVmCreate(); // BasVmT *vm = basVmCreate();
// basVmSetPrintCallback(vm, myPrintFn, myCtx); // basVmSetPrintCallback(vm, myPrintFn, myCtx);
// basVmSetInputCallback(vm, myInputFn, myCtx); // basVmSetInputCallback(vm, myInputFn, myCtx);
// basVmLoadModule(vm, compiledCode, codeLen, constants, numConsts); // basVmLoadModule(vm, module);
// BasVmResultE result = basVmRun(vm); // BasVmResultE result = basVmRun(vm);
// basVmDestroy(vm); // basVmDestroy(vm);
@ -53,7 +53,8 @@
#define BAS_VM_MAX_LOCALS 64 // locals per stack frame #define BAS_VM_MAX_LOCALS 64 // locals per stack frame
#define BAS_VM_MAX_FOR_DEPTH 32 // nested FOR loops #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_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_DEFAULT_STEP_SLICE 10000 // bytecode steps per yield
#define BAS_VM_MAX_CALL_ARGS 16 // max args to OP_CALL_METHOD / OP_CALL_EXTERN #define BAS_VM_MAX_CALL_ARGS 16 // max args to OP_CALL_METHOD / OP_CALL_EXTERN
@ -68,7 +69,6 @@
typedef enum { typedef enum {
BAS_VM_OK, // program completed normally BAS_VM_OK, // program completed normally
BAS_VM_HALTED, // HALT instruction reached BAS_VM_HALTED, // HALT instruction reached
BAS_VM_YIELDED, // DoEvents yielded control
BAS_VM_ERROR, // runtime error BAS_VM_ERROR, // runtime error
BAS_VM_STACK_OVERFLOW, BAS_VM_STACK_OVERFLOW,
BAS_VM_STACK_UNDERFLOW, BAS_VM_STACK_UNDERFLOW,
@ -79,7 +79,6 @@ typedef enum {
BAS_VM_BAD_OPCODE, BAS_VM_BAD_OPCODE,
BAS_VM_FILE_ERROR, BAS_VM_FILE_ERROR,
BAS_VM_SUBSCRIPT_RANGE, BAS_VM_SUBSCRIPT_RANGE,
BAS_VM_USER_ERROR, // ON ERROR raised
BAS_VM_STEP_LIMIT, // step limit reached (not an error) BAS_VM_STEP_LIMIT, // step limit reached (not an error)
BAS_VM_BREAKPOINT // hit breakpoint or step completed (not an error) BAS_VM_BREAKPOINT // hit breakpoint or step completed (not an error)
} BasVmResultE; } BasVmResultE;
@ -262,7 +261,6 @@ typedef struct {
typedef struct { typedef struct {
int32_t returnPc; // instruction to return to int32_t returnPc; // instruction to return to
int32_t baseSlot; // base index in locals array
int32_t localCount; // number of locals in this frame int32_t localCount; // number of locals in this frame
int32_t errorHandler; // ON ERROR GOTO target in this SUB (0 = none) 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) 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 { typedef struct {
void *handle; // FILE* or platform-specific void *handle; // FILE* or platform-specific
int32_t mode; // BasFileModeE value int32_t mode; // BasFileModeE value
int32_t recLen; // RANDOM-mode record length (OPEN ... LEN=)
} BasFileChannelT; } BasFileChannelT;
// ============================================================ // ============================================================
// Procedure table entry (retained from symbol table for runtime) // 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 { typedef struct {
char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved) char name[BAS_MAX_IDENT]; // SUB/FUNCTION name (case-preserved)
char formName[BAS_MAX_PROC_NAME]; // owning form (for form-scope vars), "" if global char formName[BAS_MAX_IDENT]; // owning form (for form-scope vars), "" if global
int32_t codeAddr; // entry point in code[] int32_t codeAddr; // entry point in code[]
int32_t paramCount; // number of parameters int32_t paramCount; // number of parameters
int32_t localCount; // number of local variables (for debugger) int32_t localCount; // number of local variables (for debugger)
@ -310,13 +312,13 @@ typedef struct {
// Debug UDT field definition (preserved for debugger watch) // Debug UDT field definition (preserved for debugger watch)
typedef struct { typedef struct {
char name[BAS_MAX_PROC_NAME]; char name[BAS_MAX_IDENT];
uint8_t dataType; uint8_t dataType;
} BasDebugFieldT; } BasDebugFieldT;
// Debug UDT type definition (preserved for debugger watch) // Debug UDT type definition (preserved for debugger watch)
typedef struct { typedef struct {
char name[BAS_MAX_PROC_NAME]; char name[BAS_MAX_IDENT];
int32_t typeId; // matches BasUdtT.typeId int32_t typeId; // matches BasUdtT.typeId
BasDebugFieldT *fields; // malloc'd array BasDebugFieldT *fields; // malloc'd array
int32_t fieldCount; int32_t fieldCount;
@ -324,8 +326,8 @@ typedef struct {
// Debug variable info (preserved in module for debugger display) // Debug variable info (preserved in module for debugger display)
typedef struct { typedef struct {
char name[BAS_MAX_PROC_NAME]; char name[BAS_MAX_IDENT];
char formName[BAS_MAX_PROC_NAME]; // form name for SCOPE_FORM vars (empty for others) 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 scope; // SCOPE_GLOBAL, SCOPE_LOCAL, SCOPE_FORM
uint8_t dataType; // BAS_TYPE_* uint8_t dataType; // BAS_TYPE_*
int32_t index; // variable slot index int32_t index; // variable slot index
@ -337,7 +339,7 @@ typedef struct {
// ============================================================ // ============================================================
typedef struct { typedef struct {
char formName[BAS_MAX_PROC_NAME]; char formName[BAS_MAX_IDENT];
int32_t varCount; int32_t varCount;
int32_t initCodeAddr; // offset in module->code for per-form init (-1 = none) int32_t initCodeAddr; // offset in module->code for per-form init (-1 = none)
int32_t initCodeLen; // length of init bytecode int32_t initCodeLen; // length of init bytecode
@ -347,16 +349,16 @@ typedef struct {
// Compiled module (output of the compiler) // Compiled module (output of the compiler)
// ============================================================ // ============================================================
// Runtime-required global init entry. STRING and SINGLE/DOUBLE // Runtime-required global init entry. STRING globals need to start
// globals need to start with the correct slot type even when debug // with an empty string in the slot even when debug info has been
// info has been stripped, or operators that switch on slot type // stripped, or operators that switch on slot type (STRING concat)
// (e.g. STRING concat) break on first use. // break on first use. Only BAS_TYPE_STRING entries are emitted and
// acted on; other types keep the zero default.
typedef struct { typedef struct {
int32_t index; // global slot index int32_t index; // global slot index
uint8_t dataType; // BAS_TYPE_* uint8_t dataType; // BAS_TYPE_*
} BasGlobalInitT; } BasGlobalInitT;
typedef struct { typedef struct {
uint8_t *code; // p-code bytecode uint8_t *code; // p-code bytecode
int32_t codeLen; int32_t codeLen;
@ -390,7 +392,6 @@ typedef struct {
int32_t pc; // program counter int32_t pc; // program counter
bool running; bool running;
bool ended; // END statement executed -- program should terminate bool ended; // END statement executed -- program should terminate
bool yielded;
bool badOperand; // operand read ran past code[] -- raise BAS_VM_BAD_OPCODE next step 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 stepLimit; // max steps per basVmRun (0 = unlimited)
int32_t stepCount; // steps executed in last basVmRun int32_t stepCount; // steps executed in last basVmRun
@ -410,7 +411,7 @@ typedef struct {
BasValueT stack[BAS_VM_STACK_SIZE]; BasValueT stack[BAS_VM_STACK_SIZE];
int32_t sp; // stack pointer (index of next free slot) 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 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 // Call stack
BasCallFrameT callStack[BAS_VM_CALL_STACK_SIZE]; BasCallFrameT callStack[BAS_VM_CALL_STACK_SIZE];
@ -440,8 +441,9 @@ typedef struct {
// String comparison mode // String comparison mode
bool compareTextMode; // true = case-insensitive comparisons bool compareTextMode; // true = case-insensitive comparisons
// Error handling // Error handling. The active ON ERROR handler lives on the call frame
int32_t errorHandler; // PC of ON ERROR GOTO handler (0 = none) // that installed it (BasCallFrameT.errorHandler); the dispatcher walks
// frames to find it.
int32_t errorNumber; // current Err number int32_t errorNumber; // current Err number
int32_t errorPc; // PC of the instruction that caused the error (for RESUME) 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) 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. // Destroy a VM instance and free all resources.
void basVmDestroy(BasVmT *vm); 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); void basVmLoadModule(BasVmT *vm, BasModuleT *module);
// Execute the loaded module. Returns when the program ends, // Execute the loaded module. Returns when the program ends,
@ -508,9 +512,6 @@ BasVmResultE basVmRun(BasVmT *vm);
// Useful for stepping/debugging. // Useful for stepping/debugging.
BasVmResultE basVmStep(BasVmT *vm); BasVmResultE basVmStep(BasVmT *vm);
// Reset the VM to initial state (clear stack, globals, PC).
void basVmReset(BasVmT *vm);
// Set I/O callbacks. // Set I/O callbacks.
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx); void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
void basVmSetInputCallback(BasVmT *vm, BasInputFnT 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. // The VM remains in a runnable state -- call basVmRun again to continue.
void basVmSetStepLimit(BasVmT *vm, int32_t limit); 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. // Get the current error message.
const char *basVmGetError(const BasVmT *vm); const char *basVmGetError(const BasVmT *vm);
@ -556,9 +553,6 @@ void basVmStepOut(BasVmT *vm);
// Run to cursor: break when reaching the specified source line. // Run to cursor: break when reaching the specified source line.
void basVmRunToCursor(BasVmT *vm, int32_t 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. // Call a SUB by code address from the host.
// Pushes a call frame, runs until the SUB returns, then restores // Pushes a call frame, runs until the SUB returns, then restores
// the previous execution state. Returns true if the SUB was called // the previous execution state. Returns true if the SUB was called

View file

@ -31,17 +31,9 @@
// The stub (basstub.app) is read from the same directory as the // The stub (basstub.app) is read from the same directory as the
// compiler executable. // compiler executable.
#include "../compiler/compact.h"
#include "../compiler/lexer.h"
#include "../compiler/obfuscate.h"
#include "../compiler/parser.h" #include "../compiler/parser.h"
#include "../compiler/strip.h"
#include "../compiler/symtab.h" #include "../compiler/symtab.h"
#include "../compiler/opcodes.h"
#include "../runtime/vm.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/dvxPrefs.h"
#include "../../../../libs/kpunch/libdvx/dvxTypes.h" #include "../../../../libs/kpunch/libdvx/dvxTypes.h"
#include "../../../../libs/kpunch/libdvx/platform/dvxPlat.h" #include "../../../../libs/kpunch/libdvx/platform/dvxPlat.h"
@ -54,6 +46,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <strings.h> #include <strings.h>
#include <unistd.h>
// Initial capacity of the source-concatenation buffer, and the hard // Initial capacity of the source-concatenation buffer, and the hard
// ceiling its capacity may reach. The ceiling keeps the int32 capacity // ceiling its capacity may reach. The ceiling keeps the int32 capacity
@ -61,11 +54,20 @@
#define CONCAT_INITIAL_CAP 8192 #define CONCAT_INITIAL_CAP 8192
#define CONCAT_MAX_CAP 0x40000000 // 1 GiB cap; keeps int32 doubling safe #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) // Function prototypes (alphabetical)
static void buildLog(const char *msg);
static bool concatGrow(char **buf, int32_t *cap, int32_t need); static bool concatGrow(char **buf, int32_t *cap, int32_t need);
static const char *extractFormCode(const char *frmText); static const char *selfPath(const char *argv0);
int main(int argc, char **argv);
static void usage(void); 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) { 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) { // Path of this executable, which carries the embedded STUB and NOICON
if (!frmText) { // resources. On a hosted OS argv[0] is whatever the shell typed (a bare
return NULL; // 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; return argv0;
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;
} }
@ -158,7 +133,7 @@ int main(int argc, char **argv) {
const char *outputPath = NULL; const char *outputPath = NULL;
bool release = false; 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) { if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
outputPath = argv[++i]; outputPath = argv[++i];
} else if (strcmp(argv[i], "-release") == 0) { } 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 *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 *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 *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); bool optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
// Derive output path // Derive output path
@ -283,8 +256,9 @@ int main(int argc, char **argv) {
// Pass 0: .bas modules, Pass 1: .frm code sections // Pass 0: .bas modules, Pass 1: .frm code sections
for (int32_t pass = 0; pass < 2; pass++) { for (int32_t pass = 0; pass < 2; pass++) {
for (int32_t i = 0; i < fileCount; i++) { for (int32_t i = 0; i < fileCount; i++) {
if (pass == 0 && files[i].isForm) { continue; } if (files[i].isForm != (pass == 1)) {
if (pass == 1 && !files[i].isForm) { continue; } continue;
}
int32_t srcLen = 0; int32_t srcLen = 0;
char *srcBuf = platformReadFile(files[i].path, &srcLen); char *srcBuf = platformReadFile(files[i].path, &srcLen);
@ -298,14 +272,11 @@ int main(int argc, char **argv) {
if (files[i].isForm) { if (files[i].isForm) {
// Extract form name from "Begin Form <name>" // Extract form name from "Begin Form <name>"
char formName[BAS_MAX_SYMBOL_NAME] = ""; char formName[BAS_MAX_IDENT] = "";
basExtractFormName(srcBuf, formName, BAS_MAX_SYMBOL_NAME); basExtractFormName(srcBuf, formName, BAS_MAX_IDENT);
code = extractFormCode(srcBuf); // The BASIC code section follows the outer Begin Form block.
code = srcBuf + basFindFormEndPos(srcBuf, srcLen);
if (!code) {
code = "";
}
int32_t codeLen = (int32_t)strlen(code); 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); printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount);
// Strip for release // Raw .frm texts; the shared build pipeline strips comments and, for
if (release) { // release builds, obfuscates them.
basStripModule(mod); char **frmSources = NULL; // stb_ds: owned form text
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
for (int32_t i = 0; i < fileCount; i++) { for (int32_t i = 0; i < fileCount; i++) {
if (!files[i].isForm) { if (!files[i].isForm) {
continue; continue;
} }
int32_t flen = 0; char *fdata = platformReadFile(files[i].path, NULL);
char *fdata = platformReadFile(files[i].path, &flen);
if (!fdata) { if (fdata) {
continue; arrput(frmSources, fdata);
}
// 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);
} }
} }
// 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; BasBuildSpecT spec;
memset(&spec, 0, sizeof(spec)); memset(&spec, 0, sizeof(spec));
spec.projName = projName; spec.projName = projName;
@ -560,94 +382,34 @@ int main(int argc, char **argv) {
spec.version = version; spec.version = version;
spec.copyright = copyright; spec.copyright = copyright;
spec.description = description; spec.description = description;
spec.projectDir = projectDir;
spec.iconPath = iconPath;
spec.helpFile = helpFile; spec.helpFile = helpFile;
spec.iconPath = iconDiskPath; spec.selfPath = selfPath(argv[0]);
spec.moduleData = modData; spec.module = mod;
spec.moduleLen = modLen; spec.frmSources = (const char *const *)frmSources;
spec.debugData = dbgData; spec.frmCount = (int32_t)arrlen(frmSources);
spec.debugLen = dbgLen; spec.release = release;
spec.formCount = frmCount; spec.log = buildLog;
spec.formData = emitFormData;
spec.formLens = emitFormLens;
int32_t emitRc = basBuildEmitResources(outputPath, &spec); const char *failure = basBuildApp(outputPath, &spec);
free(modData); for (int32_t i = 0; i < (int32_t)arrlen(frmSources); i++) {
free(dbgData); free(frmSources[i]);
if (emitRc != 0) {
fprintf(stderr, "Error: failed writing resources to %s\n", outputPath);
for (int32_t i = 0; i < frmCount; i++) {
free(frmData[i]);
free(obfFrms[i].data);
} }
arrfree(frmSources);
basModuleFree(mod);
arrfree(files); arrfree(files);
arrfree(frmData);
arrfree(frmLens);
arrfree(obfFrms);
arrfree(emitFormData);
arrfree(emitFormLens);
prefsClose(prefs); prefsClose(prefs);
if (failure) {
fprintf(stderr, "Error: %s\n", failure);
return 1; return 1;
} }
// Copy help file to output directory (the HELPFILE resource itself was // Report the true on-disk size of the finished app (stub, bytecode,
// written by basBuildEmitResources). // debug info, icon, metadata and every FORMn resource).
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.
int32_t outBytes = 0; int32_t outBytes = 0;
FILE *szf = fopen(outputPath, "rb"); FILE *szf = fopen(outputPath, "rb");
@ -666,21 +428,8 @@ int main(int argc, char **argv) {
return 0; return 0;
// ----- Error cleanup paths ----- // ----- Error cleanup paths -----
// failForms: frm arrays already exist; free their elements and the // failFiles: only the files array exists. Each early error frees its
// arrays, then fall through to free files. // own raw malloc'd buffers (concatBuf, parser) before jumping here.
// 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: failFiles:
arrfree(files); arrfree(files);
prefsClose(prefs); prefsClose(prefs);

View file

@ -68,12 +68,24 @@ AppDescriptorT appDescriptor = {
static AppContextT *sAc = NULL; static AppContextT *sAc = NULL;
// Function prototypes (alphabetical; main/appMain last) // Function prototypes (alphabetical; appMain last)
void appShutdown(void); void appShutdown(void);
static bool stubDoEvents(void *ctx); static bool stubDoEvents(void *ctx);
static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize); static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize);
static void stubPrint(void *ctx, const char *text, bool newline); static void stubPrint(void *ctx, const char *text, bool newline);
int32_t appMain(DxeAppContextT *ctx); 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) { static bool stubDoEvents(void *ctx) {
(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) { int32_t appMain(DxeAppContextT *ctx) {
sAc = ctx->shellCtx; sAc = ctx->shellCtx;
@ -123,7 +127,9 @@ int32_t appMain(DxeAppContextT *ctx) {
return 1; 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; uint32_t nameSize = 0;
char *appName = (char *)dvxResRead(res, BAS_RES_NAME, &nameSize); char *appName = (char *)dvxResRead(res, BAS_RES_NAME, &nameSize);
@ -131,7 +137,7 @@ int32_t appMain(DxeAppContextT *ctx) {
ShellAppT *app = shellGetApp(ctx->appId); ShellAppT *app = shellGetApp(ctx->appId);
if (app) { if (app) {
snprintf(app->name, SHELL_APP_NAME_MAX, "%s", appName); snprintf(app->name, SHELL_APP_NAME_MAX, "%.*s", (int)nameSize, appName);
} }
free(appName); free(appName);
@ -142,7 +148,7 @@ int32_t appMain(DxeAppContextT *ctx) {
char *helpName = (char *)dvxResRead(res, BAS_RES_HELPFILE, &helpNameSize); char *helpName = (char *)dvxResRead(res, BAS_RES_HELPFILE, &helpNameSize);
if (helpName) { 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); free(helpName);
} }
@ -176,6 +182,14 @@ int32_t appMain(DxeAppContextT *ctx) {
// Create VM // Create VM
BasVmT *vm = basVmCreate(); 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); basVmLoadModule(vm, mod);
basVmSetPrintCallback(vm, stubPrint, NULL); basVmSetPrintCallback(vm, stubPrint, NULL);
basVmSetInputCallback(vm, stubInput, NULL); basVmSetInputCallback(vm, stubInput, NULL);
@ -201,9 +215,17 @@ int32_t appMain(DxeAppContextT *ctx) {
// Create form runtime // Create form runtime
BasFormRtT *rt = basFormRtCreate(sAc, vm, mod); 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 // Register .frm source text for lazy loading
for (int32_t i = 0; i < BAS_MAX_FORM_RESOURCES; i++) { 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); snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)i);
uint32_t frmSize = 0; uint32_t frmSize = 0;
@ -230,11 +252,9 @@ int32_t appMain(DxeAppContextT *ctx) {
frmText = frmTextZ; frmText = frmTextZ;
frmText[frmSize] = '\0'; frmText[frmSize] = '\0';
// Extract form name from "Begin Form <name>" line. Use the full // Extract form name from "Begin Form <name>" line.
// form-name length so a 32-63 char form name is not truncated here char frmName[BAS_MAX_IDENT] = "";
// (which would break name-keyed form-scope variable binding). basExtractFormName(frmText, frmName, BAS_MAX_IDENT);
char frmName[BAS_MAX_FORM_NAME] = "";
basExtractFormName(frmText, frmName, BAS_MAX_FORM_NAME);
if (frmName[0]) { if (frmName[0]) {
basFormRtRegisterFrm(rt, frmName, frmText, (int32_t)frmSize); basFormRtRegisterFrm(rt, frmName, frmText, (int32_t)frmSize);

View file

@ -161,8 +161,6 @@ int main(void) {
printf("DVX BASIC Bytecode Compaction Tests\n"); printf("DVX BASIC Bytecode Compaction Tests\n");
printf("====================================\n\n"); printf("====================================\n\n");
basStringSystemInit();
// ---- Basic control flow ---- // ---- Basic control flow ----
testCompact("FOR loop", testCompact("FOR loop",
@ -355,6 +353,120 @@ int main(void) {
"END SUB\n" "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); printf("\n%d/%d tests passed\n", (int)(sTotal - sFailed), (int)sTotal);
return sFailed > 0 ? 1 : 0; return sFailed > 0 ? 1 : 0;
} }

File diff suppressed because it is too large Load diff

View file

@ -31,8 +31,6 @@
#include <string.h> #include <string.h>
int main(void) { int main(void) {
basStringSystemInit();
const char *source = "PRINT \"Hello, World!\"\n"; const char *source = "PRINT \"Hello, World!\"\n";
printf("Source: [%s]\n", source); printf("Source: [%s]\n", source);
printf("Source len: %d\n", (int)strlen(source)); printf("Source len: %d\n", (int)strlen(source));

View file

@ -30,7 +30,11 @@
// Add new tests with TEST_EQ(name, source, expected) -- other helpers // Add new tests with TEST_EQ(name, source, expected) -- other helpers
// cover compile errors and runtime errors. // cover compile errors and runtime errors.
#include "compiler/compact.h"
#include "compiler/obfuscate.h"
#include "compiler/parser.h" #include "compiler/parser.h"
#include "compiler/strip.h"
#include "runtime/serialize.h"
#include "runtime/vm.h" #include "runtime/vm.h"
#include "runtime/values.h" #include "runtime/values.h"
@ -46,6 +50,9 @@
#define CAPTURE_MAX 8192 #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 { typedef struct {
char buf[CAPTURE_MAX]; char buf[CAPTURE_MAX];
int32_t len; int32_t len;
@ -315,7 +322,6 @@ static int32_t runSubAndCapture(const char *source, const char *subName,
vm->errorNumber = 0; vm->errorNumber = 0;
vm->errorMsg[0] = '\0'; vm->errorMsg[0] = '\0';
vm->inErrorHandler = false; vm->inErrorHandler = false;
vm->errorHandler = 0;
bool ok = basVmCallSub(vm, subAddr); bool ok = basVmCallSub(vm, subAddr);
rc = ok ? 0 : 1; 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)) #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 // Widget-level event-dispatch harness
// ============================================================ // ============================================================
@ -753,8 +966,6 @@ static void testDispatchEq(const char *name, const TestDispatchT *d, const char
// ============================================================ // ============================================================
int main(void) { int main(void) {
basStringSystemInit();
printf("DVX BASIC Regression Suite\n"); printf("DVX BASIC Regression Suite\n");
printf("==========================\n"); printf("==========================\n");
@ -1041,7 +1252,7 @@ int main(void) {
// --- CAST/conversion --- // --- CAST/conversion ---
TEST_EQ("cint", "PRINT CINT(3.6)\n", "4 \n"); TEST_EQ("cint", "PRINT CINT(3.6)\n", "4 \n");
TEST_EQ("clng", "PRINT CLNG(100000)\n", "100000 \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 --- // --- Float arithmetic preserves fractional results ---
// Regression: the VM was promoting OP_ADD_INT results back to int16 // 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", "SUB later\n PRINT \"ok\"\nEND SUB\n",
"ok\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 --- // --- FUNCTION without explicit return returns default value ---
TEST_EQ("function-default-return", TEST_EQ("function-default-return",
"FUNCTION zero AS INTEGER\nEND FUNCTION\n" "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-tiny", "PRINT 0.001\n", "0.001 \n");
TEST_EQ("float-neg", "PRINT -3.14\n", "-3.14 \n"); TEST_EQ("float-neg", "PRINT -3.14\n", "-3.14 \n");
// DVX uses %g formatting (~6 digits of precision) for doubles. // 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-via-div", "PRINT 22 / 7\n", "3.14285714285714 \n");
TEST_EQ("float-large", "PRINT 1000000.5\n", "1e+06 \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 // Integer division and MOD edge cases
@ -1755,10 +1980,10 @@ int main(void) {
// ============================================================ // ============================================================
// DVX rounds half-away-from-zero (not VB banker's rounding). // 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-round-half-up2","PRINT CINT(3.5)\n", "4 \n");
TEST_EQ("cint-truncate", "PRINT CINT(2.49)\n", "2 \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("clng-from-double", "PRINT CLNG(3.9)\n", "4 \n");
TEST_EQ("csng", "PRINT CSNG(1.5)\n", "1.5 \n"); TEST_EQ("csng", "PRINT CSNG(1.5)\n", "1.5 \n");
TEST_EQ("cstr-int", "PRINT CSTR(42)\n", "42\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-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-cos-zero", "PRINT COS(0)\n", "1 \n");
TEST_EQ("math-sin-zero", "PRINT SIN(0)\n", "0 \n"); TEST_EQ("math-sin-zero", "PRINT SIN(0)\n", "0 \n");
TEST_EQ("math-exp-zero", "PRINT EXP(0)\n", "1 \n"); TEST_EQ("math-exp-zero", "PRINT EXP(0)\n", "1 \n");
@ -2316,15 +2541,69 @@ int main(void) {
// Type conversion through assignment // Type conversion through assignment
// ============================================================ // ============================================================
// DVX uses dynamic typing: assigning a float to a DIM AS INTEGER // Stores into an explicitly typed variable coerce to the declared
// slot stores the float value (the DIM only sets the INITIAL type). // type: INTEGER/LONG round (banker's) and range-check, SINGLE
// This differs from QBASIC but is consistent across the language. // narrows. Untyped variables keep the value's own type.
TEST_EQ("dim-int-assign-float-preserves-type", TEST_EQ("typed-store-int-rounds",
"DIM n AS INTEGER\n" "DIM n AS INTEGER\n"
"n = 3.7\n" "n = 3.7\n"
"PRINT n\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"); "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", TEST_EQ("assign-string-from-num",
"DIM s AS STRING\n" "DIM s AS STRING\n"
"s = STR$(42)\n" "s = STR$(42)\n"
@ -2865,6 +3144,211 @@ int main(void) {
"IF NOT (x < 0 OR x > 100) THEN PRINT \"in-range\"\n", "IF NOT (x < 0 OR x > 100) THEN PRINT \"in-range\"\n",
"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("\n--------------------------\n");
printf("PASS: %d FAIL: %d\n", (int)sPassCount, (int)sFailCount); printf("PASS: %d FAIL: %d\n", (int)sPassCount, (int)sFailCount);

View file

@ -201,7 +201,7 @@ static void test4(void) {
// FOR_NEXT: increment i, test, jump back // FOR_NEXT: increment i, test, jump back
emit8(OP_FOR_NEXT); emit8(OP_FOR_NEXT);
emitU16(0); // local index emitU16(0); // local index
emit8(1); // isLocal=1 emit8(SCOPE_LOCAL);
int16_t offset = (int16_t)(loopBody - (sCodeLen + 2)); int16_t offset = (int16_t)(loopBody - (sCodeLen + 2));
emit16(offset); emit16(offset);
@ -232,8 +232,6 @@ int main(void) {
printf("DVX BASIC VM Tests\n"); printf("DVX BASIC VM Tests\n");
printf("==================\n\n"); printf("==================\n\n");
basStringSystemInit();
test1(); test1();
test2(); test2();
test3(); test3();

View file

@ -63,7 +63,7 @@ DECLARE LIBRARY "basrt"
' Set the I/O base address for a COM port before opening. ' Set the I/O base address for a COM port before opening.
' Default bases: COM1=&H3F8, COM2=&H2F8, COM3=&H3E8, COM4=&H2E8 ' 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. ' Set the IRQ for a COM port before opening.
' Default IRQs: COM1=4, COM2=3, COM3=4, COM4=3 ' Default IRQs: COM1=4, COM2=3, COM3=4, COM4=3
@ -86,7 +86,7 @@ DECLARE LIBRARY "basrt"
DECLARE SUB SerClose(BYVAL com AS INTEGER) DECLARE SUB SerClose(BYVAL com AS INTEGER)
' Write a string to the serial port. Returns True on success. ' 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. ' Read pending data from the serial port. Returns "" if none available.
DECLARE FUNCTION SerRead$(BYVAL com AS INTEGER) AS STRING DECLARE FUNCTION SerRead$(BYVAL com AS INTEGER) AS STRING
@ -115,7 +115,7 @@ DECLARE LIBRARY "basrt"
' Send data on a logical channel (0-127). ' Send data on a logical channel (0-127).
' encrypt: True to encrypt (requires CommHandshake first). ' 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. ' Receive pending data from a specific channel. Returns "" if none.
DECLARE FUNCTION CommRecv$(BYVAL handle AS INTEGER, BYVAL channel AS INTEGER) AS STRING DECLARE FUNCTION CommRecv$(BYVAL handle AS INTEGER, BYVAL channel AS INTEGER) AS STRING

View file

@ -38,6 +38,9 @@
#include <strings.h> #include <strings.h>
#include "dvxMem.h" #include "dvxMem.h"
// File copy chunk size for multi-instance temp copies
#define COPY_BUF_SIZE 32768
// ============================================================ // ============================================================
// Module state // 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 // small as TS_DEFAULT_STACK_SIZE (32KB), which a 32KB stack frame would
// overflow. The cooperative switcher is single-threaded, so a shared // overflow. The cooperative switcher is single-threaded, so a shared
// copy buffer is safe. // copy buffer is safe.
static char buf[32768]; static char buf[COPY_BUF_SIZE];
size_t n; size_t n;
while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { 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]) { 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 { } else {
snprintf(out, outSize, "_dvx%02ld%s", (long)id, dot); 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) { 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 // Allocate a slot
int32_t id = allocSlot(); int32_t id = allocSlot();
@ -413,30 +434,16 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
return -1; 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}; char tempPath[DVX_MAX_PATH] = {0};
ShellAppT *existing = findLoadedPath(path);
if (existing) { 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 // Multi-instance allowed: copy to a temp file so dlopen gets
// an independent code+data image. // an independent code+data image.
makeTempPath(path, id, tempPath, sizeof(tempPath)); makeTempPath(path, id, tempPath, sizeof(tempPath));
if (copyFile(path, tempPath) != 0) { 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)); snprintf(msg, sizeof(msg), "Failed to create instance copy of %s.", platformPathBaseName(path));
dvxErrorBox(ctx, NULL, msg); dvxErrorBox(ctx, NULL, msg);
return -1; return -1;
@ -445,9 +452,6 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
loadPath = tempPath; loadPath = tempPath;
} }
// Snapshot free memory before loading so we can estimate app usage
dvxMemSnapshotLoad(id);
// Show hourglass during the load (dlopen + symbol resolution + appMain) // Show hourglass during the load (dlopen + symbol resolution + appMain)
dvxSetBusy(ctx, true); dvxSetBusy(ctx, true);
@ -455,7 +459,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
void *handle = dlopen(loadPath, RTLD_GLOBAL); void *handle = dlopen(loadPath, RTLD_GLOBAL);
if (!handle) { if (!handle) {
char msg[512]; char msg[SHELL_MSG_MAX];
snprintf(msg, sizeof(msg), "Failed to load %s:\n%s", platformPathBaseName(path), dlerror()); snprintf(msg, sizeof(msg), "Failed to load %s:\n%s", platformPathBaseName(path), dlerror());
dvxLog("DXE load failed: %s", msg); dvxLog("DXE load failed: %s", msg);
dvxSetBusy(ctx, false); dvxSetBusy(ctx, false);
@ -467,7 +471,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
AppDescriptorT *desc = (AppDescriptorT *)dlsym(handle, "_appDescriptor"); AppDescriptorT *desc = (AppDescriptorT *)dlsym(handle, "_appDescriptor");
if (!desc) { if (!desc) {
char msg[256]; char msg[SHELL_MSG_MAX];
snprintf(msg, sizeof(msg), "%s: missing appDescriptor", platformPathBaseName(path)); snprintf(msg, sizeof(msg), "%s: missing appDescriptor", platformPathBaseName(path));
dvxLog("DXE symbol error: %s", msg); dvxLog("DXE symbol error: %s", msg);
dvxSetBusy(ctx, false); 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"); int32_t (*entry)(DxeAppContextT *) = (int32_t (*)(DxeAppContextT *))dlsym(handle, "_appMain");
if (!entry) { if (!entry) {
char msg[256]; char msg[SHELL_MSG_MAX];
snprintf(msg, sizeof(msg), "%s: missing appMain", platformPathBaseName(path)); snprintf(msg, sizeof(msg), "%s: missing appMain", platformPathBaseName(path));
dvxSetBusy(ctx, false); dvxSetBusy(ctx, false);
dvxErrorBox(ctx, NULL, msg); dvxErrorBox(ctx, NULL, msg);

View file

@ -57,6 +57,12 @@
#define SHELL_APP_NAME_MAX 64 #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". // Every DXE app exports a global AppDescriptorT named "appDescriptor".
// The shell reads it at load time to determine how to launch the app. // 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 appPath[DVX_MAX_PATH]; // full path to the .app file
char appDir[DVX_MAX_PATH]; // directory containing the .app file char appDir[DVX_MAX_PATH]; // directory containing the .app file
char configDir[DVX_MAX_PATH]; // writable config directory (CONFIG/<apppath>/) char configDir[DVX_MAX_PATH]; // writable config directory (CONFIG/<apppath>/)
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 helpFile[DVX_MAX_PATH]; // help file path (for F1 context help)
char helpTopic[128]; // current help topic ID (updated by app) char helpTopic[128]; // current help topic ID (updated by app)
void (*onHelpQuery)(void *ctx); // called on F1 to refresh helpTopic void (*onHelpQuery)(void *ctx); // called on F1 to refresh helpTopic

View file

@ -58,6 +58,7 @@
#include "dvxPlat.h" #include "dvxPlat.h"
#include "stb_ds_wrap.h" #include "stb_ds_wrap.h"
#include <inttypes.h>
#include <stdarg.h> #include <stdarg.h>
#include <setjmp.h> #include <setjmp.h>
#include <stdio.h> #include <stdio.h>
@ -146,7 +147,7 @@ static void f1HelpHandler(void *ctx) {
AppContextT *ac = (AppContextT *)ctx; AppContextT *ac = (AppContextT *)ctx;
// Find the focused window's owning app // Find the focused window's owning app
char args[1024] = {0}; char args[SHELL_ARGS_MAX] = {0};
int32_t focusedAppId = 0; int32_t focusedAppId = 0;
if (ac->stack.focusedIdx >= 0) { if (ac->stack.focusedIdx >= 0) {
@ -344,7 +345,7 @@ int shellMain(int argc, char *argv[]) {
// initialization are caught and recovered from gracefully. // initialization are caught and recovered from gracefully.
platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, dvxLog); 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. // so the VGA loading splash stays visible through all the init above.
{ {
int32_t videoW = prefsGetInt(sPrefs, "video", "width", DVX_DEFAULT_VIDEO_W); 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); const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", MOUSE_ACCEL_DEFAULT);
int32_t accelVal = 0; int32_t accelVal = 0;
if (strcmp(accelStr, "off") == 0) { accelVal = MOUSE_ACCEL_OFF; } if (strcmp(accelStr, "off") == 0) {
else if (strcmp(accelStr, "low") == 0) { accelVal = MOUSE_ACCEL_LOW; } accelVal = MOUSE_ACCEL_OFF;
else if (strcmp(accelStr, "medium") == 0) { accelVal = MOUSE_ACCEL_MEDIUM; } } else if (strcmp(accelStr, "low") == 0) {
else if (strcmp(accelStr, "high") == 0) { accelVal = MOUSE_ACCEL_HIGH; } 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 speed = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT);
int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_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); const char *val = prefsGetString(sPrefs, "colors", dvxColorName((ColorIdE)i), NULL);
if (val) { 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][0] = (uint8_t)r;
sCtx.colorRgb[i][1] = (uint8_t)g; sCtx.colorRgb[i][1] = (uint8_t)g;
sCtx.colorRgb[i][2] = (uint8_t)b; sCtx.colorRgb[i][2] = (uint8_t)b;
@ -499,7 +507,7 @@ int shellMain(int argc, char *argv[]) {
ShellAppT *app = shellGetApp(crashedAppId); ShellAppT *app = shellGetApp(crashedAppId);
if (app) { 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); snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name);
shellForceKillApp(&sCtx, app); shellForceKillApp(&sCtx, app);
sCtx.currentAppId = 0; sCtx.currentAppId = 0;

View file

@ -1096,7 +1096,6 @@ Draw a vertical line (1px wide).
.index dirtyListClear .index dirtyListClear
.index flushRect .index flushRect
.index rectIntersect .index rectIntersect
.index rectIsEmpty
.h1 dvxComp.h -- Layer 3: Dirty Rectangle Compositor .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. 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 .topic api.wm
.title dvxWm.h -- Layer 4: Window Manager .title dvxWm.h -- Layer 4: Window Manager
.toc 1 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 .h3 wmScrollbarClick
.code .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 .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. 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 stack Window stack
dl Dirty list dl Dirty list
idx Stack index of window idx Stack index of window
orient SCROLL_VERTICAL or SCROLL_HORIZONTAL orient ScrollbarVerticalE or ScrollbarHorizontalE
mx, my Click screen coordinates mx, my Click screen coordinates
.endtable .endtable
@ -4610,7 +4593,6 @@ Remove a key from a section. No-op if the key does not exist.
.index dvxStrdup .index dvxStrdup
.index dvxMemGetAppUsage .index dvxMemGetAppUsage
.index dvxMemResetApp .index dvxMemResetApp
.index dvxMemSnapshotLoad
.h1 dvxMem.h -- Per-App Memory Tracking .h1 dvxMem.h -- Per-App Memory Tracking
@ -4672,20 +4654,6 @@ Tracked strdup.
.h2 Accounting .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 .h3 dvxMemGetAppUsage
.code .code
@ -5380,6 +5348,14 @@ const char *platformPathBaseName(const char *path);
Return a pointer to the leaf (basename) portion of 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 .h3 platformReadFile
.code .code

View file

@ -511,7 +511,6 @@ Explicit use (e.g. in the Task Manager) can include dvxMem.h to call:
.table .table
Function Purpose Function Purpose
-------- ------- -------- -------
dvxMemSnapshotLoad Baseline a newly-loaded app's memory state
dvxMemGetAppUsage Query current bytes allocated for an app dvxMemGetAppUsage Query current bytes allocated for an app
dvxMemResetApp Free every tracked allocation charged to an app dvxMemResetApp Free every tracked allocation charged to an app
.endtable .endtable

File diff suppressed because it is too large Load diff

View file

@ -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 // Separating-axis test with a gap tolerance. Two rects merge if they
// overlap OR if the gap between them is <= DIRTY_MERGE_GAP pixels. // overlap OR if the gap between them is <= DIRTY_MERGE_GAP pixels.
// The gap tolerance is the key tuning parameter for the merge algorithm: // 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. // the inner loop of dirtyListMerge.
static inline bool rectsOverlapOrAdjacent(const RectT *a, const RectT *b, int32_t gap) { static inline bool rectsOverlapOrAdjacent(const RectT *a, const RectT *b, int32_t gap) {
if (a->x + a->w + gap < b->x) { return false; } if (a->x + a->w + gap < b->x) {
if (b->x + b->w + gap < a->x) { return false; } return false;
if (a->y + a->h + gap < b->y) { return false; } }
if (b->y + b->h + gap < a->y) { 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; return true;
} }

View file

@ -72,7 +72,4 @@ void flushRect(DisplayT *d, const RectT *r);
// compositing to clip window content to dirty regions. // compositing to clip window content to dirty regions.
bool rectIntersect(const RectT *a, const RectT *b, RectT *result); 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 #endif // DVX_COMP_H

View file

@ -483,7 +483,11 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
if (initialDir && initialDir[0]) { if (initialDir && initialDir[0]) {
strncpy(sFd.curDir, initialDir, DVX_MAX_PATH - 1); strncpy(sFd.curDir, initialDir, DVX_MAX_PATH - 1);
} else { } 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'; sFd.curDir[DVX_MAX_PATH - 1] = '\0';
@ -553,12 +557,9 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
wgtDropdownSetItems(sFd.filterDd, filterLabels, fc); wgtDropdownSetItems(sFd.filterDd, filterLabels, fc);
wgtDropdownSetSelected(sFd.filterDd, 0); wgtDropdownSetSelected(sFd.filterDd, 0);
if (sFd.filterDd) {
sFd.filterDd->onChange = fdOnFilterChange; sFd.filterDd->onChange = fdOnFilterChange;
} }
} }
}
// Filename row // Filename row
WidgetT *nameRow = wgtHBox(root); WidgetT *nameRow = wgtHBox(root);
@ -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) { 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; return;
} }
// Sort: build index array, sort, reorder // 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)); 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) { if (!sortIdx || !tmpNames || !tmpIsDir) {
dvxLog("Dialog: failed to allocate sort index"); dvxLog("Dialog: failed to allocate sort arrays");
return; } else {
}
for (int32_t i = 0; i < sFd.entryCount; i++) { for (int32_t i = 0; i < sFd.entryCount; i++) {
sortIdx[i] = 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++) { for (int32_t i = 0; i < sFd.entryCount; i++) {
tmpNames[i] = sFd.entryNames[sortIdx[i]]; tmpNames[i] = sFd.entryNames[sortIdx[i]];
tmpIsDir[i] = sFd.entryIsDir[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) { static void fdOnPathKey(WidgetT *w, int32_t keyCode, int32_t shift) {
(void)shift; (void)shift;
if (keyCode != KEY_ASCII_ENTER) { if (keyCode != KEY_ENTER) {
return; return;
} }

View file

@ -43,7 +43,6 @@ void *dvxCalloc(size_t nmemb, size_t size);
void *dvxRealloc(void *ptr, size_t size); void *dvxRealloc(void *ptr, size_t size);
void dvxFree(void *ptr); void dvxFree(void *ptr);
char *dvxStrdup(const char *s); char *dvxStrdup(const char *s);
void dvxMemSnapshotLoad(int32_t appId);
uint32_t dvxMemGetAppUsage(int32_t appId); uint32_t dvxMemGetAppUsage(int32_t appId);
void dvxMemResetApp(int32_t appId); void dvxMemResetApp(int32_t appId);

View file

@ -475,9 +475,28 @@ typedef struct {
#define KEY_LEFT (0x4B | KEY_EXT_FLAG) #define KEY_LEFT (0x4B | KEY_EXT_FLAG)
#define KEY_RIGHT (0x4D | 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). // ASCII control / printable ranges (non-extended codes).
#define KEY_ESCAPE 0x1B #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_ASCII_DEL 0x7F // not the same as KEY_DELETE (scancode 0x53)
#define KEY_BACKSPACE 0x08 // ASCII backspace (not KEY_DELETE scancode) #define KEY_BACKSPACE 0x08 // ASCII backspace (not KEY_DELETE scancode)
#define KEY_ASCII_PRINT_FIRST 0x20 // space #define KEY_ASCII_PRINT_FIRST 0x20 // space
@ -560,10 +579,6 @@ typedef struct {
#define HIT_MAXIMIZE 8 #define HIT_MAXIMIZE 8
#define HIT_NONE (-1) #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, // Minimized windows display as icons at the bottom of the screen,
// similar to a classic desktop icon bar. // similar to a classic desktop icon bar.
#define ICON_SIZE 64 #define ICON_SIZE 64
@ -625,6 +640,11 @@ typedef struct WindowT {
// Widget tree root (NULL if no widgets) // Widget tree root (NULL if no widgets)
struct WidgetT *widgetRoot; 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) // Context menu (NULL if none, caller owns the MenuT allocation)
MenuT *contextMenu; MenuT *contextMenu;
@ -681,7 +701,7 @@ typedef struct {
int32_t resizeWindow; int32_t resizeWindow;
int32_t resizeEdge; int32_t resizeEdge;
int32_t scrollWindow; // window being scroll-dragged (HIT_NONE = none) 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 int32_t scrollDragOff; // mouse offset from thumb start
} WindowStackT; } WindowStackT;
@ -849,6 +869,7 @@ typedef struct {
// ============================================================ // ============================================================
#define DVX_MIN(a, b) ((a) < (b) ? (a) : (b)) #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)) #define DVX_MAX(a, b) ((a) > (b) ? (a) : (b))
#endif // DVX_TYPES_H #endif // DVX_TYPES_H

View file

@ -552,10 +552,22 @@ void wgtSetEnabled(WidgetT *w, bool enabled);
// Set read-only mode (allows scrolling/selection but blocks editing) // Set read-only mode (allows scrolling/selection but blocks editing)
void wgtSetReadOnly(WidgetT *w, bool readOnly); void wgtSetReadOnly(WidgetT *w, bool readOnly);
// Set/get keyboard focus // Set/get keyboard focus. wgtSetFocused refuses disabled or hidden widgets.
void wgtSetFocused(WidgetT *w); void wgtSetFocused(WidgetT *w);
WidgetT *wgtGetFocused(void); 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 // Show/hide a widget
void wgtSetVisible(WidgetT *w, bool visible); void wgtSetVisible(WidgetT *w, bool visible);

View file

@ -166,7 +166,8 @@ void widgetAllocRollback(WidgetT *w);
// struct remain zeroed. Returns NULL on allocation failure. // struct remain zeroed. Returns NULL on allocation failure.
WidgetT *widgetAllocWithText(WidgetT *parent, int32_t type, size_t dataSize, const char *text); 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 *widgetFindNextFocusable(WidgetT *root, WidgetT *after);
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before); WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before);
WidgetT *widgetFindByAccel(WidgetT *root, char key); WidgetT *widgetFindByAccel(WidgetT *root, char key);
@ -176,6 +177,7 @@ int32_t widgetCountVisibleChildren(const WidgetT *w);
int32_t widgetFrameBorderWidth(const WidgetT *w); int32_t widgetFrameBorderWidth(const WidgetT *w);
bool widgetIsFocusable(int32_t type); bool widgetIsFocusable(int32_t type);
bool widgetIsHorizContainer(int32_t type); bool widgetIsHorizContainer(int32_t type);
bool widgetIsShown(const WidgetT *w);
int32_t multiClickDetect(int32_t vx, int32_t vy); int32_t multiClickDetect(int32_t vx, int32_t vy);
// Clipboard // Clipboard
@ -185,7 +187,9 @@ const char *clipboardGet(int32_t *outLen);
// Hit testing // Hit testing
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y); 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); 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); 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) // 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 widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font);
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font); void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font);
void widgetLayoutBox(WidgetT *w, const BitmapFontT *font); void widgetLayoutBox(WidgetT *w, const BitmapFontT *font);

View file

@ -64,7 +64,6 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdio.h>
#include "dvxMem.h" #include "dvxMem.h"
// ============================================================ // ============================================================
@ -86,6 +85,11 @@
#define MINIMIZE_ICON_SIZE 4 // filled square size for minimize icon #define MINIMIZE_ICON_SIZE 4 // filled square size for minimize icon
#define DRAG_DEADZONE 4 // wmDragMove: pixels before drag activates #define DRAG_DEADZONE 4 // wmDragMove: pixels before drag activates
#define CONTENT_CLEAR_BYTE 0xFF // content buffer fill: clean white background #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 // Title bar gadget geometry
@ -114,8 +118,11 @@ typedef struct {
// Prototypes // 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 computeMenuBarPositions(WindowT *win, const BitmapFontT *font);
static void computeTitleGeom(const WindowT *win, TitleGeomT *g); 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 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 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); 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 freeMenuItemSubMenu(MenuItemT *item);
static void freeMenuRecursive(MenuT *menu); static void freeMenuRecursive(MenuT *menu);
static bool menuGrowItems(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 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 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); 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 wmAdjustIndexForRaise(int32_t index, int32_t raisedSlot, int32_t newTop);
static int32_t wmAdjustIndexForRemoval(int32_t index, int32_t removedSlot); 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 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 *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu);
static MenuItemT *wmMenuFindItemRecursive(MenuT *menu, 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 // 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. // padding (CHROME_TITLE_PAD) on both sides and MENU_BAR_GAP between labels.
// Positions are cached and only recomputed when positionsDirty is set (after // Positions are cached and only recomputed when positionsDirty is set (after
@ -156,7 +180,7 @@ static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) {
for (int32_t i = 0; i < win->menuBar->menuCount; i++) { for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
MenuT *menu = win->menuBar->menus[i]; MenuT *menu = win->menuBar->menus[i];
int32_t labelW = textWidthAccel(font, menu->label) + CHROME_TITLE_PAD * 2; int32_t labelW = menuLabelWidth(font, menu->label);
menu->barX = x; menu->barX = x;
menu->barW = labelW; menu->barW = labelW;
@ -184,7 +208,7 @@ static void computeTitleGeom(const WindowT *win, TitleGeomT *g) {
g->titleY = win->y + CHROME_BORDER_WIDTH; g->titleY = win->y + CHROME_BORDER_WIDTH;
g->titleW = win->w - CHROME_BORDER_WIDTH * 2; g->titleW = win->w - CHROME_BORDER_WIDTH * 2;
g->titleH = CHROME_TITLE_HEIGHT; g->titleH = CHROME_TITLE_HEIGHT;
g->gadgetS = g->titleH - GADGET_INSET * 2; g->gadgetS = GADGET_SIZE;
g->gadgetY = g->titleY + GADGET_INSET; g->gadgetY = g->titleY + GADGET_INSET;
g->closeX = g->titleX + GADGET_PAD; 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: // 4px raised Motif-style border using 3 shades:
// highlight (outer top/left), face (middle), shadow (outer bottom/right) // highlight (outer top/left), face (middle), shadow (outer bottom/right)
// with inner crease lines for 3D depth. // with inner crease lines for 3D depth.
@ -295,12 +332,8 @@ static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fon
// region. setClipRect only clamps to screen bounds, it does not intersect // region. setClipRect only clamps to screen bounds, it does not intersect
// with the prior clip, so we intersect against it with the shared // with the prior clip, so we intersect against it with the shared
// rectIntersect primitive. // rectIntersect primitive.
int32_t savedClipX = d->clipX; RectT savedClip = clipSave(d);
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 barRect = { win->x + CHROME_BORDER_WIDTH, barY, win->w - CHROME_BORDER_WIDTH * 2, barH };
RectT savedClip = { savedClipX, savedClipY, savedClipW, savedClipH };
RectT menuClip; RectT menuClip;
if (rectIntersect(&barRect, &savedClip, &menuClip)) { if (rectIntersect(&barRect, &savedClip, &menuClip)) {
@ -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 // Menu bar lies fully outside the dirty rect: an empty clip rejects
// every label span while still letting the separator line paint once // every label span while still letting the separator line paint once
// the clip is restored below. // 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++) { 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, drawHLine(d, ops, win->x + CHROME_BORDER_WIDTH, barY + barH - 1,
win->w - CHROME_BORDER_WIDTH * 2, colors->windowShadow); 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 colStart = 0;
int32_t colEnd = dstW; int32_t colEnd = dstW;
if (dstY < d->clipY) { rowStart = d->clipY - dstY; } if (dstY < d->clipY) {
if (dstY + dstH > d->clipY + d->clipH) { rowEnd = d->clipY + d->clipH - dstY; } rowStart = d->clipY - dstY;
if (dstX < d->clipX) { colStart = d->clipX - dstX; } }
if (dstX + dstW > d->clipX + d->clipW) { colEnd = d->clipX + d->clipW - dstX; }
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) { if (rowStart >= rowEnd || colStart >= colEnd) {
return; return;
@ -626,7 +670,7 @@ static bool menuGrowItems(MenuT *menu) {
return true; 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)); MenuItemT *newBuf = (MenuItemT *)realloc(menu->items, newCap * sizeof(MenuItemT));
if (!newBuf) { 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 // Grows the item array, takes the next slot, and initializes the common
// fields (zeroed, label copied + NUL-terminated, enabled, accelerator). // fields (zeroed, label copied + NUL-terminated, enabled, accelerator).
// Returns the new item, or NULL if the array could not grow. // 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++]; MenuItemT *item = &menu->items[menu->itemCount++];
memset(item, 0, sizeof(*item)); memset(item, 0, sizeof(*item));
strncpy(item->label, label, MAX_MENU_LABEL - 1); copyLabel(item->label, label, MAX_MENU_LABEL);
item->label[MAX_MENU_LABEL - 1] = '\0';
item->enabled = true; item->enabled = true;
item->accelKey = accelParse(label); item->accelKey = accelParse(item->label);
return item; return item;
} }
@ -694,11 +744,7 @@ static MenuItemT *menuNewItem(MenuT *menu, const char *label) {
// field init, and content-rect update live in one place. // 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) { 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. // Free any prior scrollbar so a second add on the same window does not leak.
if (*slot) {
free(*slot); free(*slot);
*slot = NULL;
}
*slot = (ScrollbarT *)malloc(sizeof(ScrollbarT)); *slot = (ScrollbarT *)malloc(sizeof(ScrollbarT));
if (!*slot) { if (!*slot) {
@ -706,6 +752,12 @@ static ScrollbarT *scrollbarAdd(WindowT *win, ScrollbarT **slot, ScrollbarOrient
return NULL; 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)->orient = orient;
(*slot)->min = min; (*slot)->min = min;
(*slot)->max = max; (*slot)->max = max;
@ -769,7 +821,7 @@ static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32
return trackLen; 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) { if (*thumbSize < SCROLLBAR_WIDTH) {
*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, // 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 // 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 // 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) { MenuT *wmAddMenu(MenuBarT *bar, const char *label) {
if (bar->menuCount >= bar->menuCap) { 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 *)); MenuT **newBuf = (MenuT **)realloc(bar->menus, newCap * sizeof(MenuT *));
if (!newBuf) { if (!newBuf) {
@ -894,9 +955,8 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label) {
return NULL; return NULL;
} }
strncpy(menu->label, label, MAX_MENU_LABEL - 1); copyLabel(menu->label, label, MAX_MENU_LABEL);
menu->label[MAX_MENU_LABEL - 1] = '\0'; menu->accelKey = accelParse(menu->label);
menu->accelKey = accelParse(label);
bar->menus[bar->menuCount] = menu; bar->menus[bar->menuCount] = menu;
bar->menuCount++; bar->menuCount++;
bar->positionsDirty = true; bar->positionsDirty = true;
@ -930,12 +990,12 @@ MenuBarT *wmAddMenuBar(WindowT *win) {
// before any repaint can observe it. // before any repaint can observe it.
wmDestroyMenuBar(win, NULL); wmDestroyMenuBar(win, NULL);
memset(bar, 0, sizeof(MenuBarT));
bar->activeIdx = -1;
win->menuBar = bar; win->menuBar = bar;
memset(win->menuBar, 0, sizeof(MenuBarT));
win->menuBar->activeIdx = -1;
wmUpdateContentRect(win); 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) { 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) { 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 *)); WindowT **newBuf = (WindowT **)realloc(stack->windows, newCap * sizeof(WindowT *));
if (!newBuf) { if (!newBuf) {
fprintf(stderr, "WM: Failed to grow window stack\n"); dvxLog("WM: failed to grow window stack");
return NULL; return NULL;
} }
@ -1126,7 +1186,7 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int
WindowT *win = (WindowT *)malloc(sizeof(WindowT)); WindowT *win = (WindowT *)malloc(sizeof(WindowT));
if (!win) { if (!win) {
fprintf(stderr, "WM: Failed to allocate window\n"); dvxLog("WM: failed to allocate window");
return NULL; return NULL;
} }
@ -1139,16 +1199,11 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int
win->w = w; win->w = w;
win->h = h; win->h = h;
win->visible = true; win->visible = true;
win->focused = false;
win->minimized = false;
win->maximized = false;
win->resizable = resizable; win->resizable = resizable;
win->destroyPending = false;
win->maxW = WM_MAX_FROM_SCREEN; win->maxW = WM_MAX_FROM_SCREEN;
win->maxH = WM_MAX_FROM_SCREEN; win->maxH = WM_MAX_FROM_SCREEN;
strncpy(win->title, title, MAX_TITLE_LEN - 1); copyLabel(win->title, title, MAX_TITLE_LEN);
win->title[MAX_TITLE_LEN - 1] = '\0';
wmUpdateContentRect(win); wmUpdateContentRect(win);
@ -1252,9 +1307,8 @@ void wmDestroyWindow(WindowStackT *stack, WindowT *win) {
win->contentBuf = NULL; win->contentBuf = NULL;
wmDestroyMenuBar(win, NULL); wmDestroyMenuBar(win, NULL);
wmRemoveScrollbars(win);
free(win->vScroll);
free(win->hScroll);
free(win->iconData); free(win->iconData);
free(win); 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. // 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) { void wmDrawChrome(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo) {
int32_t savedClipX = d->clipX; RectT savedClip = clipSave(d);
int32_t savedClipY = d->clipY;
int32_t savedClipW = d->clipW;
int32_t savedClipH = d->clipH;
setClipRect(d, clipTo->x, clipTo->y, clipTo->w, clipTo->h); 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 // GEOS Motif-style resize break indicators
drawResizeBreaks(d, ops, colors, win); drawResizeBreaks(d, ops, colors, win);
// Restore clip rect clipRestore(d, &savedClip);
setClipRect(d, savedClipX, savedClipY, savedClipW, savedClipH);
} }
@ -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) { void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win, const RectT *clipTo) {
int32_t savedClipX = d->clipX; RectT savedClip = clipSave(d);
int32_t savedClipY = d->clipY;
int32_t savedClipW = d->clipW;
int32_t savedClipH = d->clipH;
setClipRect(d, clipTo->x, clipTo->y, clipTo->w, clipTo->h); 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); 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. // Frees a standalone context menu and all its submenus recursively.
// Unlike freeMenuRecursive (which only frees submenu children because the // freeMenuRecursive tears down the submenu tree and item array; this also
// top-level struct is embedded), this also frees the root MenuT itself. // frees the root MenuT itself, which the caller owns.
void wmFreeMenu(MenuT *menu) { void wmFreeMenu(MenuT *menu) {
if (!menu) { if (!menu) {
return; return;
} }
// Free submenus recursively freeMenuRecursive(menu);
for (int32_t i = 0; i < menu->itemCount; i++) {
if (menu->items[i].subMenu) {
wmFreeMenu(menu->items[i].subMenu);
}
}
free(menu->items);
free(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); 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 newW;
int32_t newH = (win->maxH == WM_MAX_FROM_SCREEN) ? d->height : DVX_MIN(win->maxH, d->height); int32_t newH;
wmEffectiveMaxSize(win, d, &newW, &newH);
win->x = 0; win->x = 0;
win->y = 0; win->y = 0;
@ -1804,15 +1846,11 @@ void wmMenuItemSetCheckedInMenu(MenuT *menu, int32_t id, bool checked) {
return; return;
} }
for (int32_t i = 0; i < menu->itemCount; i++) { MenuT *owner = NULL;
if (menu->items[i].id == id) { MenuItemT *item = wmMenuFindItemRecursive(menu, id, &owner);
menuItemApplyChecked(menu, i, checked);
return;
}
if (menu->items[i].subMenu) { if (item) {
wmMenuItemSetCheckedInMenu(menu->items[i].subMenu, id, checked); menuItemApplyChecked(owner, (int32_t)(item - owner->items), checked);
}
} }
} }
@ -1846,6 +1884,12 @@ void wmMinimize(WindowStackT *stack, DirtyListT *dl, WindowT *win) {
win->minimized = true; 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--) { for (int32_t i = stack->count - 1; i >= 0; i--) {
if (stack->windows[i]->visible && !stack->windows[i]->minimized) { if (stack->windows[i]->visible && !stack->windows[i]->minimized) {
wmSetFocus(stack, dl, i); 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. // widths (when a menu bar is present) to size the minimum width.
void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) { void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) {
int32_t gadgetS = CHROME_TITLE_HEIGHT - GADGET_INSET * 2; int32_t titleMinW = GADGET_PAD + GADGET_SIZE + GADGET_PAD + FONT_CHAR_WIDTH + GADGET_PAD + GADGET_SIZE + GADGET_PAD;
int32_t gadgetPad = GADGET_PAD;
int32_t charW = FONT_CHAR_WIDTH;
int32_t titleMinW = gadgetPad + gadgetS + gadgetPad + charW + gadgetPad + gadgetS + gadgetPad;
if (win->resizable) { if (win->resizable) {
titleMinW += gadgetS + gadgetPad; titleMinW += GADGET_SIZE + GADGET_PAD;
} }
*minW = titleMinW + CHROME_BORDER_WIDTH * 2; *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; int32_t menuW = CHROME_TOTAL_SIDE;
for (int32_t i = 0; i < win->menuBar->menuCount; i++) { for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
// Use the same width calc as computeMenuBarPositions so the menuW += menuLabelWidth(&dvxFont8x16, win->menuBar->menus[i]->label);
// 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;
if (i < win->menuBar->menuCount - 1) { if (i < win->menuBar->menuCount - 1) {
menuW += MENU_BAR_GAP; 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. // paint will show a clean background rather than garbage.
int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) { int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) {
if (win->contentBuf) {
free(win->contentBuf); free(win->contentBuf);
win->contentBuf = NULL; win->contentBuf = NULL;
}
win->contentPitch = win->contentW * d->format.bytesPerPixel; win->contentPitch = win->contentW * d->format.bytesPerPixel;
int32_t bufSize = win->contentPitch * win->contentH; 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 // Initiates a window resize. Unlike drag (which stores mouse-to-origin
// offset), resize stores the absolute mouse position. wmResizeMove computes // offset), resize stores the absolute mouse position. wmResizeMove computes
// delta from this position each frame, then conditionally resets it only // the delta from this position each frame and then resets it to the
// on axes where the resize was applied. When clamped, the delta accumulates // (possibly clamped) edge position, warping the cursor to match.
// so the border sticks to the mouse when the user reverses direction.
void wmResizeBegin(WindowStackT *stack, int32_t idx, int32_t edge, int32_t mouseX, int32_t mouseY) { void wmResizeBegin(WindowStackT *stack, int32_t idx, int32_t edge, int32_t mouseX, int32_t mouseY) {
stack->resizeWindow = idx; stack->resizeWindow = idx;
@ -2267,12 +2311,10 @@ void wmResizeEnd(WindowStackT *stack) {
// clamped to [minW/minH, maxW/maxH]. // clamped to [minW/minH, maxW/maxH].
// //
// After resizing, the content buffer is reallocated and the app is notified // After resizing, the content buffer is reallocated and the app is notified
// via onResize + onPaint. dragOffX/Y are reset to the current mouse position // via onResize + onPaint. dragOffX/Y are always reset to the clamped edge
// only on axes where the resize was actually applied. If clamped (window at // position, and that position is reported back so the caller can warp the
// min/max size), dragOff is NOT updated on that axis, so the accumulated // cursor onto the edge. Keeping the cursor pinned to the border is what
// delta tracks how far the mouse moved past the border. When the user // prevents a dead zone when the user reverses direction at a size limit.
// reverses direction, the border immediately follows -- it "sticks" to
// the mouse pointer instead of creating a dead zone.
// //
// If the user resizes while maximized, the maximized flag is cleared. // If the user resizes while maximized, the maximized flag is cleared.
// This prevents wmRestore from snapping back to the pre-maximize geometry, // 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); wmMinWindowSize(win, &minW, &minH);
// Compute effective maximum size // Compute effective maximum size
int32_t maxW = (win->maxW == WM_MAX_FROM_SCREEN) ? d->width : DVX_MIN(win->maxW, d->width); int32_t maxW;
int32_t maxH = (win->maxH == WM_MAX_FROM_SCREEN) ? d->height : DVX_MIN(win->maxH, d->height); int32_t maxH;
wmEffectiveMaxSize(win, d, &maxW, &maxH);
// Mark old position dirty // Mark old position dirty
dirtyListAdd(dl, win->x, win->y, win->w, win->h); 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 // dirtied only if the value actually changed, avoiding unnecessary
// repaints when clicking at the min/max limit. // 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) { if (idx < 0 || idx >= stack->count) {
return; return;
} }
WindowT *win = stack->windows[idx]; WindowT *win = stack->windows[idx];
ScrollbarT *sb = (orient == SCROLL_VERTICAL) ? win->vScroll : win->hScroll; ScrollbarT *sb = (orient == ScrollbarVerticalE) ? win->vScroll : win->hScroll;
if (!sb) { if (!sb) {
return; return;
@ -2531,63 +2575,31 @@ void wmScrollbarClick(WindowStackT *stack, DirtyListT *dl, int32_t idx, int32_t
return; return;
} }
// Reduce to one axis: the mouse offset along the bar from its origin.
int32_t oldValue = sb->value; 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) { if (rel < SCROLLBAR_WIDTH) {
int32_t relY = my - sbScreenY; // Decrement arrow button
// Up arrow button
if (relY < SCROLLBAR_WIDTH) {
sb->value -= 1; sb->value -= 1;
} } else if (rel >= sb->length - SCROLLBAR_WIDTH) {
// Down arrow button // Increment arrow button
else if (relY >= sb->length - SCROLLBAR_WIDTH) {
sb->value += 1; sb->value += 1;
} } else if (rel >= thumbStart && rel < thumbStart + thumbSize) {
// Thumb // Thumb: begin drag
else if (relY >= SCROLLBAR_WIDTH + thumbPos &&
relY < SCROLLBAR_WIDTH + thumbPos + thumbSize) {
stack->scrollWindow = idx; stack->scrollWindow = idx;
stack->scrollOrient = SCROLL_VERTICAL; stack->scrollOrient = sb->orient;
stack->scrollDragOff = my - (sbScreenY + SCROLLBAR_WIDTH + thumbPos); stack->scrollDragOff = rel - thumbStart;
return; return;
} } else if (rel < thumbStart) {
// Trough above thumb // Trough before thumb
else if (relY < SCROLLBAR_WIDTH + thumbPos) {
sb->value -= sb->pageSize; sb->value -= sb->pageSize;
}
// Trough below thumb
else {
sb->value += sb->pageSize;
}
} else { } else {
int32_t relX = mx - sbScreenX; // Trough after thumb
// 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; sb->value += sb->pageSize;
} }
}
scrollbarCommitValue(win, sb, dl, sbScreenX, sbScreenY, oldValue); 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]; 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) { if (!sb) {
wmScrollbarEnd(stack); 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. // avoiding a full-window repaint for what is purely a chrome change.
void wmSetTitle(WindowT *win, DirtyListT *dl, const char *title) { void wmSetTitle(WindowT *win, DirtyListT *dl, const char *title) {
strncpy(win->title, title, MAX_TITLE_LEN - 1); copyLabel(win->title, title, MAX_TITLE_LEN);
win->title[MAX_TITLE_LEN - 1] = '\0';
// Dirty the title bar area // Dirty the title bar area
dirtyListAdd(dl, win->x + CHROME_BORDER_WIDTH, dirtyListAdd(dl, win->x + CHROME_BORDER_WIDTH,

View file

@ -90,6 +90,10 @@ void wmDestroyMenuBar(WindowT *win, const DisplayT *d);
// Get the minimum window size (accounts for chrome, gadgets, and menu bar). // Get the minimum window size (accounts for chrome, gadgets, and menu bar).
void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH); 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 // Append a dropdown menu to the menu bar. Returns the MenuT to populate
// with items. The label supports & accelerator markers (e.g. "&File"). // with items. The label supports & accelerator markers (e.g. "&File").
MenuT *wmAddMenu(MenuBarT *bar, const char *label); 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 // Handle an initial click on a scrollbar. Determines what was hit (up/down
// arrows, page trough area, or thumb) and either adjusts the value // arrows, page trough area, or thumb) and either adjusts the value
// immediately (arrows, trough) or begins a thumb drag operation. // 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 // Update the scroll value during an active thumb drag. Maps the mouse
// position along the track to a scroll value proportional to the range. // position along the track to a scroll value proportional to the range.

View file

@ -43,6 +43,7 @@
#define DVX_PLAT_H #define DVX_PLAT_H
#include "dvxTypes.h" #include "dvxTypes.h"
#include "dvxMem.h"
#include <stddef.h> #include <stddef.h>
@ -259,19 +260,9 @@ bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb);
// Calls to dvxFree on non-tracked pointers (magic mismatch) fall through // Calls to dvxFree on non-tracked pointers (magic mismatch) fall through
// to the real free() safely. // 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 // The DXE export table maps malloc/free/calloc/realloc/strdup to
// these wrappers. DXE code is tracked transparently. // those 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);
// Create a directory and all parent directories (like mkdir -p). // Create a directory and all parent directories (like mkdir -p).
// Returns 0 on success, -1 on failure. Existing directories are not // 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. // NULL -- if the path has no separator, the whole path is the basename.
const char *platformPathBaseName(const char *path); 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. // Slurp a whole file into a freshly-malloc'd, NUL-terminated buffer.
// Returns NULL on error (file missing, OOM, read truncated). On success, // Returns NULL on error (file missing, OOM, read truncated). On success,
// *outLen (if non-NULL) receives the byte length (not counting the NUL). // *outLen (if non-NULL) receives the byte length (not counting the NUL).

View file

@ -413,11 +413,6 @@ void dvxMemResetApp(int32_t appId) {
} }
void dvxMemSnapshotLoad(int32_t appId) {
(void)appId;
}
void *dvxRealloc(void *ptr, size_t size) { void *dvxRealloc(void *ptr, size_t size) {
if (!ptr) { if (!ptr) {
return dvxMalloc(size); return dvxMalloc(size);
@ -2565,7 +2560,6 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(dvxMemAppIdPtr) DXE_EXPORT(dvxMemAppIdPtr)
DXE_EXPORT(dvxMemGetAppUsage) DXE_EXPORT(dvxMemGetAppUsage)
DXE_EXPORT(dvxMemResetApp) DXE_EXPORT(dvxMemResetApp)
DXE_EXPORT(dvxMemSnapshotLoad)
DXE_EXPORT(dvxReadDir) DXE_EXPORT(dvxReadDir)
DXE_EXPORT(dvxReadDirFree) DXE_EXPORT(dvxReadDirFree)
DXE_EXPORT(dvxRealloc) DXE_EXPORT(dvxRealloc)
@ -2577,6 +2571,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
// --- platform --- // --- platform ---
DXE_EXPORT(platformAltScanToChar) DXE_EXPORT(platformAltScanToChar)
DXE_EXPORT(platformChdir) DXE_EXPORT(platformChdir)
DXE_EXPORT(platformCopyFile)
DXE_EXPORT(platformFlushRect) DXE_EXPORT(platformFlushRect)
DXE_EXPORT(platformGetMemoryInfo) DXE_EXPORT(platformGetMemoryInfo)
DXE_EXPORT(platformGetSystemInfo) DXE_EXPORT(platformGetSystemInfo)

View file

@ -49,6 +49,9 @@
// Permission bits for directories created by dvxMakeDirs (rwxr-xr-x). // Permission bits for directories created by dvxMakeDirs (rwxr-xr-x).
#define MKDIR_MODE 0755 #define MKDIR_MODE 0755
// Transfer block used by platformCopyFile.
#define COPY_FILE_CHUNK 4096
const char *dvxSkipWs(const char *s) { const char *dvxSkipWs(const char *s) {
if (!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) { const char *platformPathBaseName(const char *path) {
if (!path) { if (!path) {
return ""; return "";

View file

@ -43,9 +43,11 @@
// wgtRegisterClass() call. Index = type ID. // wgtRegisterClass() call. Index = type ID.
const WidgetClassT **widgetClassTable = NULL; 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 { 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; const void *value;
} ApiMapEntryT; } ApiMapEntryT;
@ -232,6 +234,10 @@ void wgtRegisterApi(const char *name, const void *api) {
return; return;
} }
if (!sApiMap) {
sh_new_strdup(sApiMap);
}
shput(sApiMap, name, api); shput(sApiMap, name, api);
} }
@ -263,5 +269,9 @@ void wgtRegisterIface(const char *name, const WgtIfaceT *iface) {
IfaceEntryT entry; IfaceEntryT entry;
memset(&entry, 0, sizeof(entry)); memset(&entry, 0, sizeof(entry));
entry.iface = iface; entry.iface = iface;
if (!sIfaceMap) {
sh_new_strdup(sIfaceMap);
}
shput(sIfaceMap, name, entry); shput(sIfaceMap, name, entry);
} }

View file

@ -102,6 +102,7 @@ static int32_t sClickCount = 0;
// Prototypes // Prototypes
// ============================================================ // ============================================================
static void collectFocusable(WidgetT *w, WidgetT ***list);
static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter); 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 // Implements Tab-order navigation: finds the next focusable widget
// after 'after' in depth-first tree order. The two-pass approach // after 'after' in depth-first tree order. The two-pass approach
// (search from 'after' to end, then wrap to start) ensures circular // (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. // just to find the next one -- the common case returns quickly.
static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter) { 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) { if (!w->visible || !w->enabled) {
return NULL; return NULL;
} }
if (after == NULL) { if (w != after && *pastAfter && widgetIsFocusable(w->type)) {
*pastAfter = true;
}
if (w == after) {
*pastAfter = true;
} else if (*pastAfter && widgetIsFocusable(w->type)) {
return w; return w;
} }
@ -332,6 +351,10 @@ void widgetClearReferences(WidgetT *w) {
sKeyPressedBtn = NULL; 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 // The on-screen tooltip may borrow w->tooltip, which dies with the
// widget; hide it so the compositor stops drawing a freed string. // widget; hide it so the compositor stops drawing a freed string.
// The context lives in the window root's userData (see wgtInitWindow); // The context lives in the window root's userData (see wgtInitWindow);
@ -431,6 +454,8 @@ void widgetDetachWindowReferences(WindowT *win) {
if (sKeyPressedBtn && sKeyPressedBtn->window == win) { if (sKeyPressedBtn && sKeyPressedBtn->window == win) {
sKeyPressedBtn = NULL; sKeyPressedBtn = NULL;
} }
win->lastFocusWidget = NULL;
} }
@ -489,42 +514,15 @@ WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) {
// Shift+Tab navigation: finds the previous focusable widget. // Shift+Tab navigation: finds the previous focusable widget.
// Collects all focusable widgets via DFS, then returns the one // Collects all focusable widgets via a recursive DFS into one
// before 'before' (with wraparound). Uses stb_ds dynamic arrays // stb_ds array, then returns the one before 'before' (with
// so there's no fixed limit on widget count or tree depth. // 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 *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
WidgetT **list = NULL; WidgetT **list = NULL;
WidgetT **stack = NULL;
arrput(stack, root); collectFocusable(root, &list);
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);
}
WidgetT *result = NULL; WidgetT *result = NULL;
int32_t count = arrlen(list); int32_t count = arrlen(list);
@ -543,22 +541,51 @@ WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
} }
arrfree(list); arrfree(list);
arrfree(stack);
return result; return result;
} }
int32_t widgetFrameBorderWidth(const WidgetT *w) { // Fires the focus-transition callbacks for a change that has ALREADY been
if (!wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS)) { // recorded in sFocusedWidget: the class-level blur (commit/clamp edits)
return 0; // 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);
} }
int32_t pad = 0; if (sWidgetGen != gen) {
int32_t gap = 0; return false;
int32_t extraTop = 0; }
int32_t borderW = 0; }
wclsGetLayoutMetrics(w, NULL, &pad, &gap, &extraTop, &borderW); if (next && next != prev && next->onFocus) {
next->onFocus(next);
}
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; 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 // Register a widget-destroy subscriber. Idempotent: a callback already
// present is not added twice, so a module can register on every attach // present is not added twice, so a module can register on every attach
// without tracking its own state. Fired by widgetClearReferences for // 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. // 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) { 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; *thumbSize = (trackLen * visibleSize) / totalSize;
if (*thumbSize < SB_MIN_THUMB) { 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; int32_t maxScroll = totalSize - visibleSize;
if (maxScroll > 0) { if (maxScroll > 0) {
*thumbPos = ((trackLen - *thumbSize) * scrollPos) / maxScroll; *thumbPos = ((trackLen - *thumbSize) * clampInt(scrollPos, 0, maxScroll)) / maxScroll;
} else { } else {
*thumbPos = 0; *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 // Remove a widget-destroy subscriber. No-op if the callback is absent, so
// a double-unregister is safe. // a double-unregister is safe.
void widgetUnregisterDestroyFn(void (*fn)(WidgetT *w)) { void widgetUnregisterDestroyFn(void (*fn)(WidgetT *w)) {

View file

@ -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 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 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. // 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); bool hadHScroll = (win->hScroll != NULL);
// Remove existing scrollbars to measure full available area // Remove existing scrollbars to measure full available area
if (hadVScroll) { wmRemoveScrollbars(win);
free(win->vScroll);
win->vScroll = NULL;
}
if (hadHScroll) {
free(win->hScroll);
win->hScroll = NULL;
}
wmUpdateContentRect(win); wmUpdateContentRect(win);
int32_t availW = win->contentW; int32_t availW = win->contentW;
@ -218,13 +209,12 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
// Install scroll handler // Install scroll handler
win->onScroll = widgetOnScroll; win->onScroll = widgetOnScroll;
// Layout at the virtual content size (the larger of content area and min size) // Arrange at the virtual content size using the measure pass already
int32_t layoutW; // done above (wgtLayout would re-measure the whole tree). The next
int32_t layoutH; // PAINT_FULL arranges again at the same geometry; arranging here too
// keeps widget positions valid for callers that read them before then.
widgetVirtualSize(win, root, &layoutW, &layoutH); widgetSetRootGeometry(win, root);
widgetLayoutChildren(root, &ctx->font);
wgtLayout(root, layoutW, layoutH, &ctx->font);
} }
@ -241,22 +231,16 @@ void widgetOnBlur(WindowT *win) {
// selection a menu Copy/Cut command is about to act on), and keeping // selection a menu Copy/Cut command is about to act on), and keeping
// the selection visible in an inactive window matches convention. // the selection visible in an inactive window matches convention.
// Selection clears happen on widget-to-widget focus transitions. // 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) { if (sFocusedWidget && sFocusedWidget->window == win) {
WidgetT *prev = sFocusedWidget; WidgetT *prev = sFocusedWidget;
sFocusedWidget = NULL; sFocusedWidget = NULL;
win->lastFocusWidget = prev;
wgtInvalidatePaint(prev); wgtInvalidatePaint(prev);
widgetFireFocusChange(prev, NULL);
// 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);
}
} }
} }
@ -294,8 +278,8 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
return; return;
} }
// Don't dispatch keys to disabled widgets // Don't dispatch keys to disabled or hidden widgets
if (!focus->enabled) { if (!focus->enabled || !widgetIsShown(focus)) {
return; return;
} }
@ -342,7 +326,7 @@ void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod) {
return; return;
} }
if (!focus->enabled) { if (!focus->enabled || !widgetIsShown(focus)) {
return; 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) { 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 // Close popups from other windows
if (sOpenPopup && sOpenPopup->window != win) { if (sOpenPopup && sOpenPopup->window != win) {
wclsClosePopup(sOpenPopup); wclsClosePopup(sOpenPopup);
@ -531,9 +507,16 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
uint32_t upGen = sWidgetGen; uint32_t upGen = sWidgetGen;
dispatchButtonEdges(upHit, upGen, buttons, sPrevMouseButtons, relX, relY); 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; sPrevMouseButtons = buttons;
sPrevMouseX = x;
sPrevMouseY = y;
return; return;
} }
@ -640,24 +623,11 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
sPrevMouseX = vx; sPrevMouseX = vx;
sPrevMouseY = vy; sPrevMouseY = vy;
// sFocusedWidget is now set directly by the widget's mouse handler // sFocusedWidget is now set directly by the widget's mouse handler.
// Fire the blur/focus callbacks for that transition. Skipped entirely
// Fire focus/blur callbacks on transitions. Skipped entirely if a // if a callback destroyed widgets -- prevFocus may be dangling then.
// callback destroyed widgets -- prevFocus may be dangling then.
if (sWidgetGen == gen) { if (sWidgetGen == gen) {
if (prevFocus && prevFocus != sFocusedWidget) { widgetFireFocusChange(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);
}
} }
} }
@ -716,31 +686,31 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
} }
// Apply scroll offset and re-layout at virtual size // Apply scroll offset and re-layout at virtual size
int32_t scrollX = win->hScroll ? win->hScroll->value : 0; widgetSetRootGeometry(win, root);
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;
if (full) { if (full) {
widgetLayoutChildren(root, &ctx->font); widgetLayoutChildren(root, &ctx->font);
} }
// Auto-focus first focusable widget if nothing has focus yet, but // Restore widget focus if nothing has focus yet, but only for the
// only for the window that actually holds WM focus. Background and // window that actually holds WM focus. Background and just-blurred
// just-blurred windows are repainted by the deferred-paint loop too, // windows are repainted by the deferred-paint loop too, and must not
// and must not steal global widget focus from the active window. // 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) { if (!sFocusedWidget && win->focused) {
WidgetT *first = widgetFindNextFocusable(root, NULL); WidgetT *target = win->lastFocusWidget;
if (first) { win->lastFocusWidget = NULL;
sFocusedWidget = first;
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 // Positions the root at the negative scroll offset and sizes it to the
// and the widget tree's measured minimum. Used both to drive scroll ranges // virtual content size: the larger of the available content area and the
// (widgetManageScrollbars) and the paint-time layout (widgetOnPaint) so the // widget tree's measured minimum. Shared by widgetManageScrollbars and
// two never diverge. // widgetOnPaint so the geometry the two arrange at never diverges.
static void widgetVirtualSize(const WindowT *win, const WidgetT *root, int32_t *outW, int32_t *outH) { static void widgetSetRootGeometry(const WindowT *win, WidgetT *root) {
*outW = DVX_MAX(win->contentW, root->calcMinW); root->x = -(win->hScroll ? win->hScroll->value : 0);
*outH = DVX_MAX(win->contentH, root->calcMinH); root->y = -(win->vScroll ? win->vScroll->value : 0);
root->w = DVX_MAX(win->contentW, root->calcMinW);
root->h = DVX_MAX(win->contentH, root->calcMinH);
} }

View file

@ -52,6 +52,35 @@
#include "dvxWgtP.h" #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, // Measure pass for box containers (VBox, HBox, RadioGroup, StatusBar,
// Toolbar, Frame, TabPage). Recursively measures all visible children, // Toolbar, Frame, TabPage). Recursively measures all visible children,
// then computes this container's minimum size as: // then computes this container's minimum size as:
@ -63,28 +92,15 @@
void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) { void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
bool horiz = widgetIsHorizContainer(w->type); 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 mainSize = 0;
int32_t crossSize = 0; int32_t crossSize = 0;
int32_t count = 0; int32_t count = 0;
int32_t pad;
int32_t gap;
int32_t frameExtraTop;
int32_t metricBorderW;
if (pad == 0) { widgetBoxMetrics(w, font, &pad, &gap, &frameExtraTop, &metricBorderW);
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);
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) { if (!c->visible) {
@ -121,14 +137,10 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
w->calcMinH = mainSize + frameExtraTop; w->calcMinH = mainSize + frameExtraTop;
} }
// Border (Frame and similar containers with getLayoutMetrics). // Border (Frame and similar containers with getLayoutMetrics; 0 otherwise)
// 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->calcMinW += metricBorderW * 2;
w->calcMinH += metricBorderW * 2; w->calcMinH += metricBorderW * 2;
} }
}
// Top-level measure dispatcher. Routes to the appropriate measure // Top-level measure dispatcher. Routes to the appropriate measure
@ -196,32 +208,25 @@ void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) { void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
bool horiz = widgetIsHorizContainer(w->type); bool horiz = widgetIsHorizContainer(w->type);
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth); int32_t pad;
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth); int32_t gap;
int32_t frameExtraTop;
int32_t fb;
if (pad == 0) { widgetBoxMetrics(w, font, &pad, &gap, &frameExtraTop, &fb);
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);
}
int32_t innerX = w->x + pad + fb; int32_t innerX = w->x + pad + fb;
int32_t innerY = w->y + pad + fb + frameExtraTop; int32_t innerY = w->y + pad + fb + frameExtraTop;
int32_t innerW = w->w - pad * 2 - fb * 2; int32_t innerW = w->w - pad * 2 - fb * 2;
int32_t innerH = w->h - pad * 2 - fb * 2 - frameExtraTop; int32_t innerH = w->h - pad * 2 - fb * 2 - frameExtraTop;
if (innerW < 0) { innerW = 0; } if (innerW < 0) {
if (innerH < 0) { innerH = 0; } innerW = 0;
}
if (innerH < 0) {
innerH = 0;
}
int32_t count = widgetCountVisibleChildren(w); int32_t count = widgetCountVisibleChildren(w);
@ -268,8 +273,12 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
} }
} }
// Second pass: assign positions and sizes // 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 pos = (horiz ? innerX : innerY) + alignOffset;
int32_t weightSeen = 0;
int32_t extraGiven = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) { if (!c->visible) {
@ -281,7 +290,18 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
// Distribute extra space by weight // Distribute extra space by weight
if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) { 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 // 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 // The root widget is positioned at (0,0) and given the full available
// area, then the arrange pass distributes space to its children. // area, then the arrange pass distributes space to its children.
// //
// This is called from widgetManageScrollbars() and widgetOnPaint(), // The window paths (widgetManageScrollbars, widgetOnPaint) do not use
// which may pass a virtual content size larger than the physical // this: they measure once and arrange at the scrolled root geometry
// window if scrolling is needed. // 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) { void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font) {
if (!root) { if (!root) {

View file

@ -40,6 +40,9 @@
static bool sFullRepaint = false; static bool sFullRepaint = false;
// Knuth multiplicative hash constant (2^32 / golden ratio).
#define KNUTH_HASH_MUL 2654435761u
// ============================================================ // ============================================================
// Prototypes // Prototypes
@ -48,11 +51,12 @@ static bool sFullRepaint = false;
static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops); 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 bool pressableHitTest(const WidgetT *w, const WidgetT *root, int32_t x, int32_t y);
static WidgetT *wgtFindImpl(WidgetT *w, const char *name); static WidgetT *wgtFindImpl(WidgetT *w, const char *name);
static bool widgetDropFocusWithin(WidgetT *w);
static void widgetNotifyChildChanged(WidgetT *w); static void widgetNotifyChildChanged(WidgetT *w);
// Draws a 1px border in a neon color derived from the widget pointer. // 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. // across the palette evenly so adjacent containers get different colors.
// This is only active when sDebugLayout is true (toggled via // This is only active when sDebugLayout is true (toggled via
// wgtSetDebugLayout), used during development to visualize container // 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 {255, 128, 255}, // orchid
}; };
uint32_t h = (uint32_t)(uintptr_t)w * 2654435761u; uint32_t h = (uint32_t)(uintptr_t)w * KNUTH_HASH_MUL;
int32_t idx = (int32_t)((h >> 16) % 12); 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]); 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); drawRectOutline(d, ops, w->x, w->y, w->w, w->h, color);
@ -270,14 +274,7 @@ void wgtInvalidate(WidgetT *w) {
return; return;
} }
// Find the root AppContextT *ctx = wgtGetContext(w);
WidgetT *root = w;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) { if (!ctx) {
return; return;
@ -353,49 +350,31 @@ void wgtSetDebugLayout(AppContextT *ctx, bool enabled) {
void wgtSetEnabled(WidgetT *w, bool enabled) { void wgtSetEnabled(WidgetT *w, bool enabled) {
if (w) { if (!w) {
return;
}
w->enabled = enabled; w->enabled = enabled;
// A disabled subtree must not keep keyboard focus. Bail if the blur
// callback destroyed widgets -- w may be gone.
if (!enabled && !widgetDropFocusWithin(w)) {
return;
}
wgtInvalidatePaint(w); wgtInvalidatePaint(w);
} }
}
// 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) { void wgtSetFocused(WidgetT *w) {
if (!w || !w->enabled) { if (!w || !w->enabled || !widgetIsShown(w)) {
return; return;
} }
WidgetT *prev = sFocusedWidget; widgetTransferFocus(w);
if (prev && prev != w) {
wclsClearSelection(prev);
wgtInvalidatePaint(prev);
}
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;
}
}
if (w->onFocus) {
w->onFocus(w);
}
} }
@ -446,14 +425,36 @@ void wgtSetTooltip(WidgetT *w, const char *text) {
void wgtSetVisible(WidgetT *w, bool visible) { void wgtSetVisible(WidgetT *w, bool visible) {
if (w) { if (!w) {
return;
}
w->visible = visible; 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 // Notify parent chain of child visibility change via onChildChanged vtable
widgetNotifyChildChanged(w); widgetNotifyChildChanged(w);
wgtInvalidate(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 widgetPressableOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod; (void)mod;
if (key == ' ' || key == 0x0D) { if (key == KEY_SPACE || key == KEY_ENTER) {
w->pressed = true; w->pressed = true;
sKeyPressedBtn = w; sKeyPressedBtn = w;
wgtInvalidatePaint(w); wgtInvalidatePaint(w);
@ -661,15 +662,27 @@ const char *widgetTextGet(const WidgetT *w) {
// Shared text setter for widgets whose DataT first field is // Shared text setter for widgets whose DataT first field is
// `const char *text`. Replaces the owned strdup'd string and // `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) { void widgetTextSet(WidgetT *w, const char *text) {
if (!w || !w->data) { if (!w || !w->data) {
return; return;
} }
const char **slot = (const char **)w->data; 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); free((void *)*slot);
*slot = text ? strdup(text) : NULL; *slot = copy;
w->accelKey = accelParse(text); w->accelKey = accelParse(copy);
} }

View file

@ -60,14 +60,10 @@ void widgetDrawScrollbarHEx(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT
return; return;
} }
int32_t trackLen = sbW - barW * 2; int32_t thumbPos;
int32_t thumbPos = 0; int32_t thumbSize;
int32_t thumbSize = 0;
if (trackLen > 0 && totalSize > 0) {
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
}
widgetScrollbarThumb(sbW - barW * 2, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
drawScrollbar(d, ops, colors, ScrollbarHorizontalE, sbX, sbY, sbW, barW, 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; return;
} }
int32_t trackLen = sbH - barW * 2; int32_t thumbPos;
int32_t thumbPos = 0; int32_t thumbSize;
int32_t thumbSize = 0;
if (trackLen > 0 && totalSize > 0) {
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
}
widgetScrollbarThumb(sbH - barW * 2, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
drawScrollbar(d, ops, colors, ScrollbarVerticalE, sbX, sbY, sbH, barW, thumbPos, thumbSize); drawScrollbar(d, ops, colors, ScrollbarVerticalE, sbX, sbY, sbH, barW, thumbPos, thumbSize);
} }
@ -109,12 +101,14 @@ ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSi
return ScrollHitArrowIncE; return ScrollHitArrowIncE;
} }
int32_t trackLen = sbLen - WGT_SB_W * 2;
if (trackLen > 0 && totalSize > 0) {
int32_t thumbPos; int32_t thumbPos;
int32_t thumbSize; int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
widgetScrollbarThumb(sbLen - WGT_SB_W * 2, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
if (thumbSize <= 0) {
return ScrollHitNoneE;
}
int32_t trackRel = relPos - WGT_SB_W; int32_t trackRel = relPos - WGT_SB_W;
@ -128,6 +122,3 @@ ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSi
return ScrollHitThumbE; return ScrollHitThumbE;
} }
return ScrollHitNoneE;
}

View file

@ -49,6 +49,9 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
// ABI-required stack alignment at function entry (bytes)
#define STACK_ALIGN 16
// ============================================================================ // ============================================================================
// Internal types // Internal types
// ============================================================================ // ============================================================================
@ -380,6 +383,7 @@ static void taskTrampoline(void) {
tsExit(); tsExit();
} }
uint32_t tsActiveCount(void) { uint32_t tsActiveCount(void) {
if (!initialized) { if (!initialized) {
return 0; 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 // which switches away without returning, but it satisfies debuggers
// and ABI checkers that expect a return address at the bottom of each frame. // and ABI checkers that expect a return address at the bottom of each frame.
uintptr_t top = (uintptr_t)(task->stack + stackSize); uintptr_t top = (uintptr_t)(task->stack + stackSize);
top &= ~(uintptr_t)0xF; top &= ~(uintptr_t)(STACK_ALIGN - 1);
top -= sizeof(uintptr_t); top -= sizeof(uintptr_t);
*(uintptr_t *)top = 0; // dummy return address; trampoline never returns *(uintptr_t *)top = 0; // dummy return address; trampoline never returns

View file

@ -352,6 +352,8 @@ static void dpmiUnlockMemory(void);
static int findIrq(int com); static int findIrq(int com);
static void freeIrq(int com); static void freeIrq(int com);
static int installIrqHandler(int irq); static int installIrqHandler(int irq);
static void irqRestore(uint32_t flags);
static uint32_t irqSave(void);
static uint8_t picReadIrr(uint16_t port); static uint8_t picReadIrr(uint16_t port);
static void removeIrqHandler(int irq); static void removeIrqHandler(int irq);
int rs232ClearRxBuffer(int com); int rs232ClearRxBuffer(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) { static uint8_t picReadIrr(uint16_t port) {
PIC_WRITE_OCW3(port, PIC_RR); PIC_WRITE_OCW3(port, PIC_RR);
return inportb(port); 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 // 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. // data register access hits the divisor latch instead of RBR/THR.
asm("CLI"); uint32_t flags = irqSave();
UART_READ_BPS(port, divisor); UART_READ_BPS(port, divisor);
asm("STI"); irqRestore(flags);
return divisorToBps(divisor); 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). // 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); UART_WRITE_BPS(port, (uint16_t)divisor);
asm("STI"); irqRestore(flags);
return RS232_SUCCESS; return RS232_SUCCESS;
} }

View file

@ -2490,12 +2490,11 @@ void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorScheme
} }
int32_t trackLen = len - thick * 2; int32_t trackLen = len - thick * 2;
if (trackLen > 0) {
int32_t thumbPos; int32_t thumbPos;
int32_t thumbSize; int32_t thumbSize;
widgetScrollbarThumb(trackLen, total, visible, scroll, &thumbPos, &thumbSize); widgetScrollbarThumb(trackLen, total, visible, scroll, &thumbPos, &thumbSize);
if (thumbSize > 0) {
if (vertical) { if (vertical) {
drawBevel(d, ops, x, y + thick + thumbPos, thick, thumbSize, &btnBevel); drawBevel(d, ops, x, y + thick + thumbPos, thick, thumbSize, &btnBevel);
} else { } else {

View file

@ -728,7 +728,7 @@ static void processHcf(const char *hcfPath, const char *hcfDir) {
dvxLog("helpRecompile: %s -> %s (%d files)", hcfPath, ctx.outputFile, (int)inputCount); dvxLog("helpRecompile: %s -> %s (%d files)", hcfPath, ctx.outputFile, (int)inputCount);
int32_t rc = hlpcCompile((const char **)ctx.inputFiles, inputCount, ctx.outputFile, 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); hlpcProgressCallback, NULL);
if (rc != 0) { if (rc != 0) {

View file

@ -47,6 +47,10 @@ typedef void (*HlpcProgressFnT)(void *ctx, int32_t current, int32_t total);
// progressFn: progress callback (NULL = no progress reporting) // progressFn: progress callback (NULL = no progress reporting)
// progressCtx: opaque context passed to progressFn // 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. // 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); 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);

View file

@ -682,7 +682,7 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
sp->scrollPosV -= font->charHeight; sp->scrollPosV -= font->charHeight;
} else if (relY >= sbH - SP_SB_W) { } else if (relY >= sbH - SP_SB_W) {
sp->scrollPosV += font->charHeight; sp->scrollPosV += font->charHeight;
} else if (trackLen > 0) { } else {
int32_t thumbPos; int32_t thumbPos;
int32_t thumbSize; int32_t thumbSize;
widgetScrollbarThumb(trackLen, contentMinH, innerH, sp->scrollPosV, &thumbPos, &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; sp->scrollPosH -= font->charWidth;
} else if (relX >= sbW - SP_SB_W) { } else if (relX >= sbW - SP_SB_W) {
sp->scrollPosH += font->charWidth; sp->scrollPosH += font->charWidth;
} else if (trackLen > 0) { } else {
int32_t thumbPos; int32_t thumbPos;
int32_t thumbSize; int32_t thumbSize;
widgetScrollbarThumb(trackLen, contentMinW, innerW, sp->scrollPosH, &thumbPos, &thumbSize); widgetScrollbarThumb(trackLen, contentMinW, innerW, sp->scrollPosH, &thumbPos, &thumbSize);