Many fixes and greatly increased test coverage.

This commit is contained in:
Scott Duensing 2026-08-31 14:15:53 -05:00
parent dc8be04ae9
commit 2c170d263f
440 changed files with 70158 additions and 6506 deletions

1
.gitignore vendored
View file

@ -10,3 +10,4 @@ lib/
.claude/ .claude/
capture/ capture/
just-stuff/ just-stuff/
dvx.log

View file

@ -32,7 +32,7 @@
# src/loader/ -> bin/DVX.EXE # src/loader/ -> bin/DVX.EXE
# src/tools/ -> bin/host/<tool> # src/tools/ -> bin/host/<tool>
.PHONY: all clean libdvx libtasks loader texthelp listhelp widgets dvxshell taskmgr serial sql apps tools deploy-helpsrc compile-help deploy-sdk .PHONY: all clean libdvx libtasks loader texthelp listhelp widgets dvxshell taskmgr serial sql apps tools deploy-helpsrc compile-help deploy-sdk test test-quick test-asan test-host test-samples coverage
all: libdvx libtasks loader texthelp listhelp tools widgets dvxshell taskmgr serial sql apps deploy-helpsrc compile-help deploy-sdk all: libdvx libtasks loader texthelp listhelp tools widgets dvxshell taskmgr serial sql apps deploy-helpsrc compile-help deploy-sdk
@ -75,6 +75,48 @@ apps: libdvx libtasks dvxshell tools
deploy-helpsrc: deploy-helpsrc:
$(MAKE) -C src/tools deploy-helpsrc $(MAKE) -C src/tools deploy-helpsrc
# Host-side tests. Every suite is native gcc; the GUI suites build
# obj/host/libdvx.a, the widget .so files and libdvxtest.a themselves
# (sanitizers on by default, SAN=1).
# make test -- everything: BASIC harnesses + fuzz, sample goldens,
# proxy build, all host suites, tools goldens, lint gate
# make test-quick -- same minus the fuzzers, static analyzer and tools
# make test-asan -- BASIC harnesses rebuilt under ASan/UBSan plus the
# host suites (which are already sanitized)
DVXBASIC_DIR = src/apps/kpunch/dvxbasic
HOST_SUITES = src/test/dvxtest src/test/sql src/test/unit src/test/wm src/test/widgets src/test/dialogs src/test/formrt src/test/ide src/test/shell src/test/serial
test: apps
$(MAKE) -C $(DVXBASIC_DIR) tests
$(MAKE) -C src/tools/proxy
$(MAKE) test-samples
$(MAKE) test-host
src/test/tools/runTools.sh
$(MAKE) -C src/test/lint
test-quick: apps
$(MAKE) -C $(DVXBASIC_DIR) tests FUZZ_N=0
$(MAKE) test-samples
$(MAKE) test-host
$(MAKE) -C src/test/lint style ascii
test-asan: apps
$(MAKE) -C $(DVXBASIC_DIR) tests TESTSAN=1
$(MAKE) test-host
test-host:
@for d in $(HOST_SUITES); do $(MAKE) -C $$d || exit 1; done
test-samples:
src/test/samples/runSamples.sh
# Line coverage of the host suites: rebuilds everything with gcc
# --coverage (no sanitizers) under obj/hostcov, runs the BASIC
# harnesses, sample goldens and every host suite, then writes the
# report to obj/coverage/html and prints a per-directory summary.
coverage: tools
$(MAKE) -C src/test/coverage
HLPC = bin/host/dvxhlpc HLPC = bin/host/dvxhlpc
SYSTEM_DHS = src/libs/kpunch/libdvx/sysdoc.dhs \ SYSTEM_DHS = src/libs/kpunch/libdvx/sysdoc.dhs \

View file

@ -273,6 +273,7 @@ img { max-width: 100%; }
<li><a href="#lang.func.fileio">EOF</a></li> <li><a href="#lang.func.fileio">EOF</a></li>
<li><a href="#lang.operators">EQV</a></li> <li><a href="#lang.operators">EQV</a></li>
<li><a href="#lang.declarations">ERASE</a></li> <li><a href="#lang.declarations">ERASE</a></li>
<li><a href="#lang.misc">ERL</a></li>
<li><a href="#lang.misc">ERR</a></li> <li><a href="#lang.misc">ERR</a></li>
<li><a href="#lang.misc">ERROR</a></li> <li><a href="#lang.misc">ERROR</a></li>
<li><a href="#lang.forms">Event Handlers</a></li> <li><a href="#lang.forms">Event Handlers</a></li>
@ -1225,11 +1226,15 @@ name$ = &quot;Hello&quot; ' String</code></pre>
<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> <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> <p>Storing a value into a variable declared as Integer, Long, Single or Boolean (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, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has.</p>
<pre><code>Dim n As Integer <pre><code>Dim n As Integer
n = 3.7 ' n is 4 n = 3.7 ' n is 4
n = 40000 ' Error 6: Overflow n = 40000 ' Error 6: Overflow
x = 3.7 ' x has no declared type and keeps 3.7</code></pre> Dim b As Boolean
b = 3 ' b is True
Dim s
s = 1 / 3 ' s is Single: prints 0.3333333
x = 1 / 3 ' x was never declared and keeps the Double: prints 0.333333333333333</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>
@ -1319,9 +1324,11 @@ REM This is a comment
DIM variable(upperBound) AS type DIM variable(upperBound) AS type
DIM variable(lower TO upper) AS type DIM variable(lower TO upper) AS type
DIM variable(dim1, dim2, ...) AS type DIM variable(dim1, dim2, ...) AS type
DIM variable() AS type
DIM variable AS UdtName DIM variable AS UdtName
DIM variable AS STRING * n DIM variable AS STRING * n
DIM SHARED variable AS type</code></pre> DIM SHARED variable AS type</code></pre>
<p>An array declared with bounds is a fixed array. DIM variable() with empty parentheses declares a dynamic array that has no storage until a REDIM gives it bounds; using it before then raises error 13. An array that is first created by REDIM, and an array parameter, are dynamic too (see ERASE for the difference).</p>
<p>Multiple variables can be declared at module level so their values persist between procedure calls. Arrays can have up to 8 dimensions. A variable declared with type suffix does not need the AS clause:</p> <p>Multiple variables can be declared at module level so their values persist between procedure calls. Arrays can have up to 8 dimensions. A variable declared with type suffix does not need the AS clause:</p>
<pre><code>Dim name$, count%, total&amp; ' suffixes infer the types</code></pre> <pre><code>Dim name$, count%, total&amp; ' suffixes infer the types</code></pre>
<p>Examples:</p> <p>Examples:</p>
@ -1335,7 +1342,7 @@ 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). 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> <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 an array, optionally preserving existing data. The array may have been declared with DIM name() or with bounds, or not declared at all (REDIM then declares a dynamic array).</p>
<pre><code>REDIM array(newBounds) AS type <pre><code>REDIM array(newBounds) AS type
REDIM PRESERVE array(newBounds) AS type</code></pre> REDIM PRESERVE array(newBounds) AS type</code></pre>
<pre><code>ReDim items(newSize) As String <pre><code>ReDim items(newSize) As String
@ -1354,18 +1361,19 @@ Const HEADER As String = &quot;=== Report ===&quot;</code></pre>
<p>Defines a user-defined type (record/structure).</p> <p>Defines a user-defined type (record/structure).</p>
<pre><code>TYPE TypeName <pre><code>TYPE TypeName
fieldName AS type fieldName AS type
fieldName AS STRING * n
... ...
END TYPE</code></pre> END TYPE</code></pre>
<pre><code>Type PersonType <pre><code>Type PersonType
firstName As String firstName As String
lastName As String lastName As String * 20
age As Integer age As Integer
End Type End Type
Dim p As PersonType Dim p As PersonType
p.firstName = &quot;Scott&quot; p.firstName = &quot;Scott&quot;
p.age = 30</code></pre> p.age = 30</code></pre>
<p>UDT fields can themselves be UDTs (nested types).</p> <p>UDT fields can themselves be UDTs (nested types), and field access chains through array elements too: shapes(i).origin.x = 5. A new instance starts with numeric fields at 0, Boolean fields False, variable strings empty and STRING * n fields padded to n spaces. TYPE variables can be written to and read from RANDOM and BINARY files with PUT and GET (see GET / PUT).</p>
<h2>DECLARE</h2> <h2>DECLARE</h2>
<p>Forward-declares a SUB or FUNCTION. This is rarely required because the compiler supports forward references within a module, but it is still accepted for compatibility:</p> <p>Forward-declares a SUB or FUNCTION. This is rarely required because the compiler supports forward references within a module, but it is still accepted for compatibility:</p>
<pre><code>DECLARE SUB name ([BYVAL] [OPTIONAL] param AS type, ...) <pre><code>DECLARE SUB name ([BYVAL] [OPTIONAL] param AS type, ...)
@ -1413,9 +1421,11 @@ udt.field = expression
LET variable = expression</code></pre> LET variable = expression</code></pre>
<p>The LET keyword is optional and supported for compatibility.</p> <p>The LET keyword is optional and supported for compatibility.</p>
<h2>SWAP</h2> <h2>SWAP</h2>
<p>Exchanges the values of two variables. The variables must be the same type.</p> <p>Exchanges the values of two variables or array elements. The two operands should be the same type.</p>
<pre><code>SWAP variable1, variable2</code></pre> <pre><code>SWAP variable1, variable2
<pre><code>Swap a, b</code></pre> SWAP array(index), variable</code></pre>
<pre><code>Swap a, b
Swap names(i), names(j)</code></pre>
<h2>SET</h2> <h2>SET</h2>
<p>Assigns an object reference (form or control) to a variable. Required when creating a form or control with CreateForm / CreateControl so that the returned reference is stored in the variable:</p> <p>Assigns an object reference (form or control) to a variable. Required when creating a form or control with CreateForm / CreateControl so that the returned reference is stored in the variable:</p>
<pre><code>SET variable = objectExpression</code></pre> <pre><code>SET variable = objectExpression</code></pre>
@ -1424,7 +1434,7 @@ Set frm = CreateForm(&quot;MyForm&quot;, 320, 240)
frm.Caption = &quot;Built in code&quot;</code></pre> frm.Caption = &quot;Built in code&quot;</code></pre>
<p>For ordinary numeric or string assignment, SET is not used.</p> <p>For ordinary numeric or string assignment, SET is not used.</p>
<h2>ERASE</h2> <h2>ERASE</h2>
<p>Frees the memory of a dynamic array and resets it to undimensioned state. Fixed-size arrays (declared with constant bounds) reset their elements but keep their shape.</p> <p>Frees the memory of a dynamic array (declared with DIM name(), created by REDIM, or an array parameter) and resets it to the undimensioned state; using it again before a REDIM raises error 13. A fixed array (declared with bounds) keeps its bounds and has every element reset to zero, an empty string, False or a fresh TYPE instance.</p>
<pre><code>ERASE arrayName</code></pre> <pre><code>ERASE arrayName</code></pre>
</div> </div>
<div class="topic" id="lang.conditionals"> <div class="topic" id="lang.conditionals">
@ -1528,16 +1538,26 @@ Wend</code></pre>
<h1>Procedures</h1> <h1>Procedures</h1>
<h2>SUB...END SUB</h2> <h2>SUB...END SUB</h2>
<p>Defines a subroutine (no return value).</p> <p>Defines a subroutine (no return value).</p>
<pre><code>SUB name ([BYVAL] [OPTIONAL] param AS type, ...) <pre><code>SUB name ([BYVAL | BYREF] [OPTIONAL] param AS type, ...)
SUB name (arrayParam() AS type, ...)
statements statements
END SUB</code></pre> END SUB</code></pre>
<pre><code>Sub Greet(ByVal name As String) <pre><code>Sub Greet(ByVal name As String)
Print &quot;Hello, &quot; &amp; name Print &quot;Hello, &quot; &amp; name
End Sub</code></pre> End Sub</code></pre>
<p>Parameters are passed by reference by default. Use ByVal for value semantics; there is no separate ByRef keyword (omitting ByVal is the by-reference form). Writes to a by-reference parameter update the caller's variable, including individual array elements: `bump a(3)` passed to a by-reference parameter will modify `a(3)` in place. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:</p> <p>Parameters are passed by reference by default; BYREF may be written out to say so, and BYVAL selects value semantics. Writes to a by-reference parameter update the caller's variable, including individual array elements: `bump a(3)` passed to a by-reference parameter will modify `a(3)` in place. A parameter written as name() receives a whole array by reference; the caller passes `arr()` (or just `arr`), and the SUB may index, REDIM or ERASE it. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:</p>
<pre><code>Greet &quot;World&quot; <pre><code>Greet &quot;World&quot;
Greet(&quot;World&quot;) Greet(&quot;World&quot;)
Call Greet(&quot;World&quot;)</code></pre> Call Greet(&quot;World&quot;)</code></pre>
<pre><code>Sub Fill(v() As Integer, ByRef count As Integer)
ReDim v(9) As Integer
v(9) = 7
count = count + 1
End Sub
Dim a(3) As Integer
Dim n As Integer
Fill a(), n</code></pre>
<h3>Optional Parameters</h3> <h3>Optional Parameters</h3>
<p>Mark a parameter OPTIONAL to allow callers to omit it. An optional parameter must be positioned after all required parameters and receives an empty/zero default when not supplied.</p> <p>Mark a parameter OPTIONAL to allow callers to omit it. An optional parameter must be positioned after all required parameters and receives an empty/zero default when not supplied.</p>
<pre><code>Sub Announce(ByVal msg As String, Optional ByVal loud As Integer) <pre><code>Sub Announce(ByVal msg As String, Optional ByVal loud As Integer)
@ -1617,16 +1637,17 @@ PRINT USING format$; expression [; expression] ...</code></pre>
<p>Special functions inside PRINT:</p> <p>Special functions inside PRINT:</p>
<ul> <ul>
<li>SPC(n) -- print n spaces</li> <li>SPC(n) -- print n spaces</li>
<li>TAB(n) -- advance to column n (first column is 1)</li> <li>TAB(n) -- advance to column n (first column is 1); when the cursor is already past column n, a new line is started first</li>
</ul> </ul>
<pre><code>Print &quot;Name:&quot;; Tab(20); name$ <pre><code>Print &quot;Name:&quot;; Tab(20); name$
Print &quot;X=&quot;; x, &quot;Y=&quot;; y ' comma = next 14-column zone Print &quot;X=&quot;; x, &quot;Y=&quot;; y ' comma = next 14-column zone
Print #1, &quot;Written to file&quot;</code></pre> Print #1, &quot;Written to file&quot;</code></pre>
<h3>PRINT USING</h3> <h3>PRINT USING</h3>
<p>PRINT USING formats each following expression according to a format string and prints the result. If more expressions are supplied than the format string consumes, the format string is reused from the start for each.</p> <p>PRINT USING formats each following expression according to a format string and prints the result. The format string is a mix of fields and literal text: each expression consumes the next field, the literal text before it is printed as it is, and the literal text after the last field is printed at the end. If more expressions are supplied than there are fields, the format string is reused from the start.</p>
<pre> Format character Meaning <pre> Format character Meaning
---------------- ------- ---------------- -------
# Digit (replaced with a digit or space to pad on the left) # Digit position; unused positions on the left become spaces
0 Digit position that is always printed (zero padded); the first 0 sets the minimum number of digits
. Decimal-point position . Decimal-point position
, Insert thousands separator , Insert thousands separator
+ At start or end: always show a sign character + At start or end: always show a sign character
@ -1636,13 +1657,19 @@ Print #1, &quot;Written to file&quot;</code></pre>
^^^^ Anywhere: format the value in scientific notation ^^^^ Anywhere: format the value in scientific notation
! First character of a string only ! First character of a string only
&amp; Entire string (variable length) &amp; Entire string (variable length)
\ ... \ Fixed-length string field (width = 2 + number of spaces between backslashes)</pre> \ ... \ Fixed-length string field (width = 2 + number of spaces between backslashes)
<pre><code>Print Using &quot;###.##&quot;; 3.14159 ' &quot; 3.14&quot; _ The next character is printed literally</pre>
Print Using &quot;$$#,##0.00&quot;; 1234567.89 ' &quot;$1,234,567.89&quot; <p>A numeric field is as wide as its format characters; the value is right-aligned in it and a negative sign (or a leading +) floats directly in front of the first digit. A value that does not fit is printed in full with a leading %. A string expression must meet a string field and a numeric expression a numeric field, otherwise error 13 (Type mismatch) is raised.</p>
<pre><code>Print Using &quot;###.##&quot;; -3.14159 ' &quot; -3.14&quot;
Print Using &quot;+###&quot;; 42 ' &quot; +42&quot;
Print Using &quot;$$#,##0.00&quot;; 1234.5 ' &quot; $1,234.50&quot;
Print Using &quot;**#,##0.00&quot;; 42.5 ' &quot;*****42.50&quot; Print Using &quot;**#,##0.00&quot;; 42.5 ' &quot;*****42.50&quot;
Print Using &quot;###&quot;; 12345 ' &quot;%12345&quot;
Print Using &quot;####.####^^^^&quot;; 0.000123 ' scientific notation Print Using &quot;####.####^^^^&quot;; 0.000123 ' scientific notation
Print Using &quot;\ \&quot;; &quot;Hello&quot; ' fixed-width 4-char: &quot;Hell&quot; Print Using &quot;\ \&quot;; &quot;Hello&quot; ' fixed-width 4-char: &quot;Hell&quot;
Print Using &quot;&amp; likes &amp;&quot;; name$; food$</code></pre> Print Using &quot;&amp; likes &amp;&quot;; name$; food$ ' &quot;Scott likes pizza&quot;
Print Using &quot;Total: ###.## units&quot;; 7.5 ' &quot;Total: 7.50 units&quot;
Print Using &quot;## and ##&quot;; 1; 2; 3 ' &quot; 1 and 2 3 and &quot;</code></pre>
<h2>INPUT</h2> <h2>INPUT</h2>
<p>Reads a line of text from the user or from a file channel.</p> <p>Reads a line of text from the user or from a file channel.</p>
<pre><code>INPUT variable <pre><code>INPUT variable
@ -1669,13 +1696,13 @@ 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). RESUME or RESUME NEXT executed while no error is active raises error 20 (RESUME without error).</p> <p>The ERR keyword returns the current error number in expressions and ERL the source line the error was raised on (both are 0 when no error is active, and both reset after RESUME). 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
ErrorHandler: ErrorHandler:
Print &quot;Error number:&quot;; Err Print &quot;Error number:&quot;; Err; &quot;on line&quot;; Erl
Resume Next</code></pre> Resume Next</code></pre>
<h3>Common Error Numbers</h3> <h3>Common Error Numbers</h3>
<pre> Number Meaning <pre> Number Meaning
@ -1692,13 +1719,14 @@ ErrorHandler:
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 (OPEN for INPUT, KILL, FILELEN, ...)
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) 59 Bad record length (OPEN ... LEN must be 1 to 32767; GET/PUT data longer than LEN)
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
1000 No form runtime (a form or control statement ran without the form runtime, e.g. under a headless runner)</pre>
<h2>SHELL</h2> <h2>SHELL</h2>
<p>Executes an operating-system command.</p> <p>Executes an operating-system command.</p>
<pre><code>SHELL &quot;command&quot;</code></pre> <pre><code>SHELL &quot;command&quot;</code></pre>
@ -1756,8 +1784,21 @@ Close #1</code></pre>
<p>Read and write records in RANDOM or BINARY mode files. When recordNum is omitted the transfer starts at the current file position.</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 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 and Boolean 2 bytes, Long 4, Single 4, Double 8); a String is stored as a 2-byte length followed by its characters. PUT always writes a whole record, padding the unused part with zero bytes, and GET always advances to the next record; data longer than LEN raises error 59.</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> <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>
<p>The variable may be a TYPE variable. Its fields are packed in declaration order with the sizes above; a STRING * n field takes exactly n bytes (padded with spaces), a variable-length String field is stored with its 2-byte length prefix in both modes, and nested TYPE fields are packed inline. GET fills the fields of the existing variable in place.</p>
<p>The variable may also be an array element, arr(i), of any type including a TYPE: PUT writes that element and GET reads into it. The index expression of a GET target is evaluated twice (once to fetch the element, once to store it back), so keep it free of side effects.</p>
<pre><code>Type RecT
id As Integer
score As Double
nm As String * 6
End Type
Dim r As RecT
Open &quot;recs.dat&quot; For Random As #1 Len = 32
r.id = 1
Put #1, 1, r ' 16 bytes of fields, padded to a 32-byte record
Get #1, 1, r
Close #1</code></pre>
<pre><code>Dim buf As String <pre><code>Dim buf As String
Open &quot;raw.bin&quot; For Binary As #1 Open &quot;raw.bin&quot; For Binary As #1
buf = Space$(16) buf = Space$(16)
@ -1822,7 +1863,7 @@ WHILE first$ &lt;&gt; &quot;&quot;
first$ = DIR$ ' next match, no argument first$ = DIR$ ' next match, no argument
WEND</code></pre> WEND</code></pre>
<h2>FILELEN</h2> <h2>FILELEN</h2>
<p>Returns the length of a file in bytes.</p> <p>Returns the length of a file in bytes. A file that does not exist raises error 53 (File not found).</p>
<pre><code>bytes = FILELEN(filename$)</code></pre> <pre><code>bytes = FILELEN(filename$)</code></pre>
</div> </div>
<div class="topic" id="lang.func.string"> <div class="topic" id="lang.func.string">
@ -1855,10 +1896,12 @@ WEND</code></pre>
<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>
<pre><code>Print Format$(3.14159, &quot;###.##&quot;) ' &quot; 3.14&quot; <pre><code>Print Format$(3.14159, &quot;###.##&quot;) ' &quot; 3.14&quot;
Print Format$(-7, &quot;+###&quot;) ' &quot; -7&quot;
Print Format$(42, &quot;00000&quot;) ' &quot;00042&quot; Print Format$(42, &quot;00000&quot;) ' &quot;00042&quot;
Print Format$(1234.5, &quot;#,##0.00&quot;) ' &quot;1,234.50&quot; Print Format$(1234.5, &quot;#,##0.00&quot;) ' &quot;1,234.50&quot;
Print Format$(5, &quot;#,##0.00&quot;) ' &quot; 5.00&quot;
Print Format$(0.5, &quot;PERCENT&quot;) ' &quot;50%&quot;</code></pre> Print Format$(0.5, &quot;PERCENT&quot;) ' &quot;50%&quot;</code></pre>
<p>The accepted format characters are # (digit or pad space), 0 (digit or pad zero), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), and the literal word PERCENT (multiplies by 100 and appends %). See PRINT USING for details on each.</p> <p>The whole format string is one numeric field. The accepted format characters are # (digit or pad space), 0 (digit that is always printed; zero pads up to the first 0), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), ^^^^ (scientific notation) and the literal word PERCENT (multiplies by 100 and appends %). Unlike PRINT USING, a value wider than the field is printed in full without a % marker. See PRINT USING for details on each character.</p>
<h2>MID$ Assignment</h2> <h2>MID$ Assignment</h2>
<p>MID$ can also be used on the left side of an assignment to replace a portion of a string without changing its length:</p> <p>MID$ can also be used on the left side of an assignment to replace a portion of a string without changing its length:</p>
<pre><code>Mid$(s$, start [, length]) = replacement$</code></pre> <pre><code>Mid$(s$, start [, length]) = replacement$</code></pre>
@ -1917,10 +1960,10 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background</code></pre>
EOF(channel) Boolean True if the file pointer is at end of file EOF(channel) Boolean True if the file pointer is at end of file
FREEFILE Integer Next available file channel number (1..16) FREEFILE Integer Next available file channel number (1..16)
INPUT$(n, #channel) String Reads exactly n characters from the file INPUT$(n, #channel) String Reads exactly n characters from the file
LOC(channel) Long Current read/write position in the file LOC(channel) Long BINARY: position of the last byte read or written (0 at the start); RANDOM: number of the last record read or written; sequential: number of 128-byte blocks read or written
LOF(channel) Long Length of the file in bytes LOF(channel) Long Length of the file in bytes
SEEK(channel) Long Current file position (function form) SEEK(channel) Long 1-based position of the next byte to be read or written
FILELEN(path$) Long Length of the named file in bytes (no OPEN needed) FILELEN(path$) Long Length of the named file in bytes (no OPEN needed; error 53 when missing)
GETATTR(path$) Integer File attribute bits (see vbReadOnly, vbHidden, etc.) GETATTR(path$) Integer File attribute bits (see vbReadOnly, vbHidden, etc.)
CURDIR$ String Current working directory CURDIR$ String Current working directory
DIR$(pattern$) String First filename matching pattern, or &quot;&quot; DIR$(pattern$) String First filename matching pattern, or &quot;&quot;
@ -1944,8 +1987,9 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background</code></pre>
<p>DVX BASIC supports Visual Basic-style forms and controls for building graphical user interfaces. A form is normally designed visually in the IDE and saved as a .frm file, but forms and their controls can also be built entirely in code.</p> <p>DVX BASIC supports Visual Basic-style forms and controls for building graphical user interfaces. A form is normally designed visually in the IDE and saved as a .frm file, but forms and their controls can also be built entirely in code.</p>
<h2>Loading and Unloading Forms</h2> <h2>Loading and Unloading Forms</h2>
<pre><code>LOAD FormName <pre><code>LOAD FormName
UNLOAD FormName</code></pre> UNLOAD FormName
<p>LOAD creates the form and its controls in memory. It fires Form_Load when the form is first loaded. UNLOAD destroys the form, firing Form_QueryUnload (which can cancel the close) and then Form_Unload. The form name here is the literal name of the form as it appears in its .frm file.</p> UNLOAD Me</code></pre>
<p>LOAD creates the form and its controls in memory. It fires Form_Load when the form is first loaded. UNLOAD destroys the form, firing Form_QueryUnload (which can cancel the close) and then Form_Unload. The form name here is the literal name of the form as it appears in its .frm file; UNLOAD Me unloads the form whose event handler is running.</p>
<h2>Showing and Hiding Forms</h2> <h2>Showing and Hiding Forms</h2>
<pre><code>FormName.Show [mode] <pre><code>FormName.Show [mode]
FormName.Hide FormName.Hide
@ -1961,6 +2005,10 @@ value = ControlName.Property</code></pre>
<pre><code>Text1.Text = &quot;Hello&quot; <pre><code>Text1.Text = &quot;Hello&quot;
Label1.Caption = &quot;Name: &quot; &amp; name$ Label1.Caption = &quot;Name: &quot; &amp; name$
x = Text1.Left</code></pre> x = Text1.Left</code></pre>
<p>A control on another form is reached through that form's name, or through a variable holding a form reference: FormName.ControlName.Property works in both assignments and expressions, and so does FormName.ControlName.Method. Me.ControlName.Property does the same for the current form.</p>
<pre><code>Form2.Label1.Caption = &quot;From Form1&quot;
total = Form2.List1.ListCount
Form2.List1.Clear</code></pre>
<h2>Method Calls</h2> <h2>Method Calls</h2>
<pre><code>ControlName.Method [args]</code></pre> <pre><code>ControlName.Method [args]</code></pre>
<pre><code>List1.AddItem &quot;New entry&quot; <pre><code>List1.AddItem &quot;New entry&quot;

View file

@ -48,6 +48,11 @@ all: $(C_APPS) dvxbasic $(BASIC_APPS)
dvxbasic: dvxbasic:
$(MAKE) -C dvxbasic $(MAKE) -C dvxbasic
# bascomp is produced by the dvxbasic sub-make; this rule gives the BASIC app
# rules a real target to depend on so parallel builds order correctly.
$(BASCOMP): | dvxbasic
@:
cpanel: $(BINDIR)/kpunch/cpanel/cpanel.app cpanel: $(BINDIR)/kpunch/cpanel/cpanel.app
progman: $(BINDIR)/kpunch/progman/progman.app progman: $(BINDIR)/kpunch/progman/progman.app
clock: $(BINDIR)/kpunch/clock/clock.app clock: $(BINDIR)/kpunch/clock/clock.app
@ -125,6 +130,57 @@ widshow: $(BINDIR)/kpunch/widshow/widshow.app
$(BINDIR)/kpunch/widshow/widshow.app: widshow/widshow.dbp widshow/widshow.frm widshow/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/widshow dvxbasic $(BINDIR)/kpunch/widshow/widshow.app: widshow/widshow.dbp widshow/widshow.frm widshow/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/widshow dvxbasic
$(BASCOMP) widshow/widshow.dbp -o $@ -release $(BASCOMP) widshow/widshow.dbp -o $@ -release
# Native host build (test harness): each C app becomes a plain ELF shared
# object under obj/host/apps/<name>.so with its resources appended by
# dvxres exactly as on DOS (the resource footer trails the ELF image, which
# the dynamic loader ignores). SAN=1 adds ASan/UBSan.
HOSTCC = gcc
HOSTOBJDIR = ../../../obj/host/apps
HOSTCFLAGS = -O1 -g -fPIC -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -D_GNU_SOURCE -I../../libs/kpunch/libdvx -I../../libs/kpunch/libdvx/platform -I../../libs/kpunch/libdvx/thirdparty -I../../widgets/kpunch -I../../libs/kpunch/libtasks -I../../libs/kpunch/dvxshell
HOSTLDFLAGS = -shared
ifeq ($(SAN),1)
HOSTCFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer
HOSTLDFLAGS += -fsanitize=address,undefined
endif
# COV=1 (make coverage): gcov instrumentation under obj/hostcov.
ifeq ($(COV),1)
HOSTOBJDIR = ../../../obj/hostcov/apps
HOSTCFLAGS += --coverage
HOSTLDFLAGS += --coverage
endif
HOST_SOS = $(foreach n,$(C_APPS),$(HOSTOBJDIR)/$(n).so)
.PHONY: host host-clean
host: $(HOST_SOS)
$(HOSTOBJDIR)/progman.so: progman/progman.c $(COMMON_H) ../../libs/kpunch/dvxshell/shellInf.h
@mkdir -p $(HOSTOBJDIR)
$(HOSTCC) $(HOSTCFLAGS) $(HOSTLDFLAGS) -o $@ $<
$(HOSTOBJDIR)/clock.so: clock/clock.c clock/clock.res clock/icon32.bmp $(COMMON_H)
@mkdir -p $(HOSTOBJDIR)
$(HOSTCC) $(HOSTCFLAGS) $(HOSTLDFLAGS) -o $@ $<
cd clock && ../$(DVXRES) build ../$@ clock.res
$(HOSTOBJDIR)/cpanel.so: cpanel/cpanel.c cpanel/cpanel.res cpanel/icon32.bmp $(COMMON_H)
@mkdir -p $(HOSTOBJDIR)
$(HOSTCC) $(HOSTCFLAGS) $(HOSTLDFLAGS) -o $@ $<
cd cpanel && ../$(DVXRES) build ../$@ cpanel.res
$(HOSTOBJDIR)/dvxdemo.so: dvxdemo/dvxdemo.c dvxdemo/dvxdemo.res dvxdemo/icon32.bmp $(COMMON_H) $(WIDGET_H)
@mkdir -p $(HOSTOBJDIR)
$(HOSTCC) $(HOSTCFLAGS) $(HOSTLDFLAGS) -o $@ $<
cd dvxdemo && ../$(DVXRES) build ../$@ dvxdemo.res
$(HOSTOBJDIR)/dvxhelp.so: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h dvxhelp/dvxhelp.res dvxhelp/icon32.bmp $(COMMON_H)
@mkdir -p $(HOSTOBJDIR)
$(HOSTCC) $(HOSTCFLAGS) $(HOSTLDFLAGS) -o $@ $<
cd dvxhelp && ../$(DVXRES) build ../$@ dvxhelp.res
host-clean:
rm -rf $(HOSTOBJDIR)
$(OBJDIR): $(OBJDIR):
mkdir -p $(OBJDIR) mkdir -p $(OBJDIR)

View file

@ -219,6 +219,24 @@ AppDescriptorT appDescriptor = {
}; };
static void applyMouseConfig(void) {
int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1;
int32_t dbl = wgtSliderGetValue(sDblClickSldr);
const char *accelName = mapAccelValue(wgtDropdownGetSelected(sAccelDrop));
int32_t accelVal = mapAccelName(accelName);
// Slider is inverted vs the mickey ratio (see CP_SPEED_INVERT):
// slider 2 -> 32 mickeys/8px (slowest)
// slider 26 -> 8 mickeys/8px (default)
// slider 32 -> 2 mickeys/8px (fastest)
int32_t speedVal = CP_SPEED_INVERT - wgtSliderGetValue(sSpeedSldr);
int32_t wheelStep = wgtSliderGetValue(sWheelStepSldr);
dvxSetMouseConfig(sAc, dir, dbl, accelVal, speedVal, wheelStep);
}
// Cleanup hook the shell calls before dlclose: these module-static stb_ds // Cleanup hook the shell calls before dlclose: these module-static stb_ds
// arrays (and any still-open prefs handle) would otherwise leak into the // arrays (and any still-open prefs handle) would otherwise leak into the
// long-lived shell heap on every open/close, since the DXE unload doesn't // long-lived shell heap on every open/close, since the DXE unload doesn't
@ -244,24 +262,6 @@ void appShutdown(void) {
} }
static void applyMouseConfig(void) {
int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1;
int32_t dbl = wgtSliderGetValue(sDblClickSldr);
const char *accelName = mapAccelValue(wgtDropdownGetSelected(sAccelDrop));
int32_t accelVal = mapAccelName(accelName);
// Slider is inverted vs the mickey ratio (see CP_SPEED_INVERT):
// slider 2 -> 32 mickeys/8px (slowest)
// slider 26 -> 8 mickeys/8px (default)
// slider 32 -> 2 mickeys/8px (fastest)
int32_t speedVal = CP_SPEED_INVERT - wgtSliderGetValue(sSpeedSldr);
int32_t wheelStep = wgtSliderGetValue(sWheelStepSldr);
dvxSetMouseConfig(sAc, dir, dbl, accelVal, speedVal, wheelStep);
}
static void buildColorsTab(WidgetT *page) { static void buildColorsTab(WidgetT *page) {
// Color list on the left, sliders on the right // Color list on the left, sliders on the right
WidgetT *hbox = wgtHBox(page); WidgetT *hbox = wgtHBox(page);
@ -518,12 +518,24 @@ static void buildVideoTab(WidgetT *page) {
const char *depthName; const char *depthName;
switch (sVideoModes[i].bpp) { switch (sVideoModes[i].bpp) {
case 8: depthName = "256 colors"; break; case 8:
case 15: depthName = "32 thousand colors"; break; depthName = "256 colors";
case 16: depthName = "65 thousand colors"; break; break;
case 24: depthName = "16 million colors"; break; case 15:
case 32: depthName = "16 million colors+"; break; depthName = "32 thousand colors";
default: depthName = ""; break; break;
case 16:
depthName = "65 thousand colors";
break;
case 24:
depthName = "16 million colors";
break;
case 32:
depthName = "16 million colors+";
break;
default:
depthName = "";
break;
} }
snprintf(sLabelBufs[i], sizeof(sLabelBufs[i]), "%ldx%ld %s", (long)sVideoModes[i].w, (long)sVideoModes[i].h, depthName); snprintf(sLabelBufs[i], sizeof(sLabelBufs[i]), "%ldx%ld %s", (long)sVideoModes[i].w, (long)sVideoModes[i].h, depthName);

View file

@ -45,7 +45,7 @@ HOSTDIR = ../../../../bin/host
DVXRES = $(HOSTDIR)/dvxres DVXRES = $(HOSTDIR)/dvxres
# Runtime library objects (VM + values + form runtime + serialization) # Runtime library objects (VM + values + form runtime + serialization)
RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/frmParser.o $(OBJDIR)/serialize.o RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/frmParser.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o
RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt
RT_TARGET = $(RT_TARGETDIR)/basrt.lib RT_TARGET = $(RT_TARGETDIR)/basrt.lib
@ -61,34 +61,51 @@ APP_TARGET = $(APPDIR)/dvxbasic.app
STUB_OBJS = $(OBJDIR)/basstub.o STUB_OBJS = $(OBJDIR)/basstub.o
STUB_TARGET = $(OBJDIR)/basstub.app STUB_TARGET = $(OBJDIR)/basstub.app
# Native test programs (host gcc, not cross-compiled) # Native test programs (host gcc, not cross-compiled).
# TESTSAN=1 rebuilds the harnesses under ASan/UBSan (make tests TESTSAN=1).
HOSTCC = gcc HOSTCC = gcc
HOSTCFLAGS = -O2 $(WARNFLAGS) -Wno-stringop-truncation -D_GNU_SOURCE $(DVX_INCLUDES) HOSTCFLAGS = -O2 $(WARNFLAGS) -Wno-stringop-truncation -D_GNU_SOURCE $(DVX_INCLUDES)
ifeq ($(TESTSAN),1)
HOSTCFLAGS += -g -fno-omit-frame-pointer -fsanitize=address,undefined
endif
# COV=1 (make coverage): every host build (harnesses, bascomp, basrun,
# basrt.a) gets gcov instrumentation and lands under obj/hostcov so it
# never mixes with the sanitized or release host binaries. dvxres is
# still the release tool from bin/host. Must precede the rules below
# because target names expand when they are read.
ifeq ($(COV),1)
HOSTDIR = ../../../../obj/hostcov/basic
DVXRES = ../../../../bin/host/dvxres
HOSTOBJDIR = ../../../../obj/hostcov
HOSTCFLAGS += -g --coverage
HOSTRT_COVFLAGS = --coverage
endif
# Every header a host harness can pull in; listed as a prerequisite so a # 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. # header edit rebuilds the harness instead of leaving a stale binary.
HEADERS = $(wildcard *.h compiler/*.h runtime/*.h formrt/*.h) 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_LEX = $(HOSTDIR)/test_lex
TEST_QUICK = $(HOSTDIR)/test_quick
TEST_COMPACT = $(HOSTDIR)/test_compact TEST_COMPACT = $(HOSTDIR)/test_compact
TEST_SUITE = $(HOSTDIR)/test_suite TEST_SUITE = $(HOSTDIR)/test_suite
STB_DS_IMPL = ../../../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c STB_DS_IMPL = ../../../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c
PLATFORM_UTIL = ../../../libs/kpunch/libdvx/platform/dvxPlatformUtil.c PLATFORM_UTIL = ../../../libs/kpunch/libdvx/platform/dvxPlatformUtil.c
TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL) TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_LEX_SRCS = test_lex.c compiler/lexer.c
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
TEST_COMPACT_SRCS = test_compact.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL) TEST_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 compiler/strip.c compiler/obfuscate.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL) TEST_SUITE_SRCS = test_suite.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)
# 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)
BASCOMP_TARGET = $(HOSTDIR)/bascomp BASCOMP_TARGET = $(HOSTDIR)/bascomp
# Headless host runner (host tool; runs .app/.bas/.dbp with PRINT on stdout)
BASRUN_SRCS = stub/basrun.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)
BASRUN_TARGET = $(HOSTDIR)/basrun
# Host-side BASIC test directory (basrun fixtures, fuzzers, compaction diff)
BASIC_TESTDIR = ../../../test/basic
# 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 $(WARNFLAGS) -march=i486 -mtune=i586 $(DVX_INCLUDES) DOSCFLAGS = -O2 $(WARNFLAGS) -march=i486 -mtune=i586 $(DVX_INCLUDES)
@ -98,37 +115,37 @@ SYSTEMDIR = ../../../../bin/system
.PHONY: all clean tests .PHONY: all clean tests
all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(BASRUN_TARGET) $(SYSTEMDIR)/BASCOMP.EXE
# Run every built harness and fail the build if any returns non-zero. # Run every built harness and fail the build if any returns non-zero.
# test_vm/test_lex/test_quick are inspection harnesses with no pass/fail
# of their own, so they run output-suppressed purely for crash detection;
# test_compiler, test_compact and test_suite each report a failure count # test_compiler, test_compact and test_suite each report a failure count
# and return non-zero on failure. make stops at the first one that fails. # and return non-zero on failure. make stops at the first one that fails.
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(TEST_SUITE) # The harness binaries depend on the sanitizer stamp so switching TESTSAN
$(TEST_VM) > /dev/null # on or off forces a rebuild instead of running a stale binary.
$(TEST_LEX) > /dev/null TESTSAN_STAMP = $(HOSTDIR)/.testsan-$(if $(filter 1,$(TESTSAN)),on,off)
$(TEST_QUICK) > /dev/null
# The src/test/basic suite (sample runs, fixtures, fuzzers, compaction
# differential) needs bascomp and basrun; it inherits TESTSAN.
tests: $(TEST_COMPILER) $(TEST_COMPACT) $(TEST_SUITE) $(BASCOMP_TARGET) $(BASRUN_TARGET)
$(TEST_COMPILER) $(TEST_COMPILER)
$(TEST_COMPACT) $(TEST_COMPACT)
$(TEST_SUITE) $(TEST_SUITE)
$(MAKE) -C $(BASIC_TESTDIR) test TESTSAN=$(TESTSAN)
$(TEST_COMPILER): $(TEST_COMPILER_SRCS) $(HEADERS) | $(HOSTDIR) $(BASRUN_TARGET): $(BASRUN_SRCS) $(HEADERS) $(TESTSAN_STAMP) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(BASRUN_SRCS) -lm
$(TEST_COMPILER): $(TEST_COMPILER_SRCS) $(HEADERS) $(TESTSAN_STAMP) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm $(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm
$(TEST_SUITE): $(TEST_SUITE_SRCS) $(HEADERS) | $(HOSTDIR) $(TEST_SUITE): $(TEST_SUITE_SRCS) $(HEADERS) $(TESTSAN_STAMP) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_SUITE_SRCS) -lm $(HOSTCC) $(HOSTCFLAGS) -I../../../tools -DBAS_TEST_SRC_DIR='"$(abspath .)"' -o $@ $(TEST_SUITE_SRCS) -lm
$(TEST_VM): $(TEST_VM_SRCS) $(HEADERS) | $(HOSTDIR) $(TESTSAN_STAMP): | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_VM_SRCS) -lm rm -f $(HOSTDIR)/.testsan-on $(HOSTDIR)/.testsan-off
touch $@
$(TEST_LEX): $(TEST_LEX_SRCS) $(HEADERS) | $(HOSTDIR) $(TEST_COMPACT): $(TEST_COMPACT_SRCS) $(HEADERS) $(TESTSAN_STAMP) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -w -o $@ $(TEST_LEX_SRCS) -lm
$(TEST_QUICK): $(TEST_QUICK_SRCS) $(HEADERS) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_QUICK_SRCS) -lm
$(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
@ -211,4 +228,49 @@ $(APPDIR):
clean: clean:
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 -rf $(RT_OBJS) $(COMP_OBJS) $(IDE_OBJS) $(STUB_OBJS) $(OBJDIR)/*.d $(RT_TARGET) $(APP_TARGET) $(STUB_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR)
rm -f $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(TEST_SUITE) rm -f $(TEST_COMPILER) $(TEST_COMPACT) $(TEST_SUITE) $(BASRUN_TARGET) $(HOSTDIR)/.testsan-on $(HOSTDIR)/.testsan-off
$(MAKE) -C $(BASIC_TESTDIR) clean
# ============================================================
# Host basrt archive (src/test/formrt suite) -- T5 block, keep last
# ============================================================
#
# Compiler + runtime + form runtime built with the native toolchain into
# obj/host/basrt.a, linked by the form-runtime test suite against the
# host libdvx.a and the widget .so files. basNativeHost.c stands in for
# the i386 basNativeDos.c trampoline (Makefile object-list selection, no
# preprocessor conditionals). SAN must match the rest of the host build.
SAN ?= 1
HOSTOBJDIR ?= ../../../../obj/host
HOSTRT_DIR = $(HOSTOBJDIR)/basrt
HOSTRT_TARGET = $(HOSTOBJDIR)/basrt.a
HOSTRT_SRCS = 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 formrt/formrt.c formrt/frmParser.c formrt/basNativeHost.c
HOSTRT_OBJS = $(patsubst %.c,$(HOSTRT_DIR)/%.o,$(notdir $(HOSTRT_SRCS)))
HOSTRT_CFLAGS = -O1 -g $(WARNFLAGS) -Wno-stringop-truncation -D_GNU_SOURCE $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks $(HOSTRT_COVFLAGS)
ifeq ($(SAN),1)
HOSTRT_CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer
endif
.PHONY: host-basrt host-basrt-clean
host-basrt: $(HOSTRT_TARGET)
$(HOSTRT_TARGET): $(HOSTRT_OBJS)
rm -f $@
ar rcs $@ $(HOSTRT_OBJS)
$(HOSTRT_DIR)/%.o: compiler/%.c $(HEADERS)
@mkdir -p $(HOSTRT_DIR)
$(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $<
$(HOSTRT_DIR)/%.o: runtime/%.c $(HEADERS)
@mkdir -p $(HOSTRT_DIR)
$(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $<
$(HOSTRT_DIR)/%.o: formrt/%.c $(HEADERS)
@mkdir -p $(HOSTRT_DIR)
$(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $<
host-basrt-clean:
rm -rf $(HOSTRT_DIR) $(HOSTRT_TARGET)

View file

@ -72,9 +72,18 @@
// DJGPP as well as every host OS bascomp runs on; DVX_PATH_SEP is not. // DJGPP as well as every host OS bascomp runs on; DVX_PATH_SEP is not.
#define BAS_BUILD_SEP "/" #define BAS_BUILD_SEP "/"
// Growth unit of the project source buffer.
#define BAS_BUILD_SOURCE_CHUNK 8192
// Room for a BEGINFORM "name" / ENDFORM wrapper around a form's code.
#define BAS_BUILD_WRAP_SLACK (BAS_MAX_IDENT + 32)
// Function prototypes (alphabetical) // Function prototypes (alphabetical)
static bool appendSource(BasProjectSourceT *ps, const char *text, int32_t len);
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);
const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec); const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec);
bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, const char *formName, int32_t *textLine, int32_t *textLines);
bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out);
void basBuildFreeProject(BasProjectSourceT *out);
static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...); static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...);
static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec); static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec);
static int32_t emitIcon(const char *outPath, const BasBuildSpecT *spec); static int32_t emitIcon(const char *outPath, const BasBuildSpecT *spec);
@ -83,6 +92,48 @@ static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, i
static const char *writeStub(const char *outPath, const BasBuildSpecT *spec); static const char *writeStub(const char *outPath, const BasBuildSpecT *spec);
// Appends text to the project source and makes sure it ends on a newline
// so the next file starts on a fresh line.
static bool appendSource(BasProjectSourceT *ps, const char *text, int32_t len) {
int32_t need = ps->sourceLen + len + 2;
if (need > ps->sourceCap) {
int32_t newCap = ps->sourceCap ? ps->sourceCap : BAS_BUILD_SOURCE_CHUNK;
while (newCap < need) {
newCap *= 2;
}
char *grown = (char *)realloc(ps->source, (size_t)newCap);
if (!grown) {
snprintf(ps->error, sizeof(ps->error), "Out of memory.");
return false;
}
ps->source = grown;
ps->sourceCap = newCap;
}
memcpy(ps->source + ps->sourceLen, text, (size_t)len);
ps->sourceLen += len;
for (int32_t i = 0; i < len; i++) {
if (text[i] == '\n') {
ps->lineCount++;
}
}
if (ps->sourceLen > 0 && ps->source[ps->sourceLen - 1] != '\n') {
ps->source[ps->sourceLen++] = '\n';
ps->lineCount++;
}
ps->source[ps->sourceLen] = '\0';
return true;
}
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) {
if (!value || !value[0]) { if (!value || !value[0]) {
return 0; return 0;
@ -161,6 +212,121 @@ const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) {
} }
bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, const char *formName, int32_t *textLine, int32_t *textLines) {
bool isForm = (formName && formName[0]);
if (isForm) {
char wrap[BAS_BUILD_WRAP_SLACK];
int32_t n = snprintf(wrap, sizeof(wrap), "BEGINFORM \"%s\"\n", formName);
if (!appendSource(ps, wrap, n)) {
return false;
}
}
int32_t startLine = ps->lineCount + 1;
if (!appendSource(ps, text, len)) {
return false;
}
if (textLine) {
*textLine = startLine;
}
if (textLines) {
*textLines = ps->lineCount + 1 - startLine;
}
if (isForm) {
return appendSource(ps, "ENDFORM\n", (int32_t)strlen("ENDFORM\n"));
}
return true;
}
bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out) {
static const char *const sections[2] = { BAS_INI_SECTION_MODULES, BAS_INI_SECTION_FORMS };
memset(out, 0, sizeof(*out));
out->optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
// Pass 0: .bas modules, pass 1: .frm code sections.
for (int32_t pass = 0; pass < 2; pass++) {
bool isForm = (pass == 1);
for (int32_t i = 0; ; i++) {
char key[BAS_BUILD_LOG_BUF];
snprintf(key, sizeof(key), "File%d", (int)i);
const char *val = prefsGetString(prefs, sections[pass], key, NULL);
if (!val) {
break;
}
char filePath[DVX_MAX_PATH];
snprintf(filePath, sizeof(filePath), "%s%s%s", projectDir, BAS_BUILD_SEP, val);
int32_t srcLen = 0;
char *src = platformReadFile(filePath, &srcLen);
if (!src) {
snprintf(out->error, sizeof(out->error), "Cannot read %s", filePath);
return false;
}
bool ok;
if (isForm) {
char formName[BAS_MAX_IDENT] = "";
basExtractFormName(src, formName, BAS_MAX_IDENT);
// The BASIC code section follows the outer Begin Form block.
const char *code = src + basFindFormEndPos(src, srcLen);
ok = basBuildAppendFile(out, code, (int32_t)strlen(code), formName, NULL, NULL);
char *frmPath = (char *)malloc(strlen(filePath) + 1);
if (frmPath) {
strcpy(frmPath, filePath);
arrput(out->frmPaths, frmPath);
}
} else {
ok = basBuildAppendFile(out, src, (int32_t)strlen(src), NULL, NULL, NULL);
}
free(src);
if (!ok) {
return false;
}
out->fileCount++;
}
}
if (out->fileCount == 0) {
snprintf(out->error, sizeof(out->error), "Project has no source files.");
return false;
}
return true;
}
void basBuildFreeProject(BasProjectSourceT *out) {
for (int32_t i = 0; i < (int32_t)arrlen(out->frmPaths); i++) {
free(out->frmPaths[i]);
}
arrfree(out->frmPaths);
free(out->source);
memset(out, 0, sizeof(*out));
}
static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...) { static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...) {
if (!spec->log) { if (!spec->log) {
return; return;

View file

@ -37,6 +37,8 @@
#define BAS_BUILD_H #define BAS_BUILD_H
#include "runtime/vm.h" #include "runtime/vm.h"
#include "dvxPrefs.h"
#include "dvxTypes.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
@ -83,6 +85,40 @@ typedef struct {
// error message. On failure outPath may be left partially written. // error message. On failure outPath may be left partially written.
const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec); const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec);
// Longest error text basBuildConcatProject reports (a path plus a reason).
#define BAS_BUILD_ERROR_LEN (DVX_MAX_PATH + 64)
// A project's BASIC source as the compiler consumes it: every [Modules]
// file in order, then the code section of every [Forms] file wrapped in
// BEGINFORM "name" / ENDFORM, each file ending on a newline. frmPaths
// lists the .frm files in the same order for the resource step.
typedef struct {
char *source; // NUL-terminated, malloc'd
int32_t sourceLen;
int32_t sourceCap;
int32_t lineCount; // newlines in source (next text starts on lineCount + 1)
char **frmPaths; // stb_ds array of malloc'd paths
int32_t fileCount; // modules + forms
bool optionExplicit; // [Settings] OptionExplicit
char error[BAS_BUILD_ERROR_LEN];
} BasProjectSourceT;
// Appends one file's text to *ps (a zeroed struct is a valid empty
// project) and makes sure it ends on a newline. A non-empty formName
// wraps the text in BEGINFORM "name" / ENDFORM. textLine and textLines
// (either may be NULL) receive the 1-based line the text starts on and
// the number of lines it occupies, wrapper lines excluded -- what an IDE
// source map needs. Returns false with ps->error set on failure.
bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, const char *formName, int32_t *textLine, int32_t *textLines);
// Reads the source files named by an open .dbp (paths relative to
// projectDir) into *out. Returns false with out->error set on failure;
// call basBuildFreeProject either way.
bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out);
// Releases everything basBuildConcatProject allocated.
void basBuildFreeProject(BasProjectSourceT *out);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View file

@ -46,7 +46,6 @@ void basEmit16(BasCodeGenT *cg, int16_t v);
void basEmit8(BasCodeGenT *cg, uint8_t b); void basEmit8(BasCodeGenT *cg, uint8_t b);
static void basEmitBytes(BasCodeGenT *cg, const void *data, int32_t len); static void basEmitBytes(BasCodeGenT *cg, const void *data, int32_t len);
void basEmitDouble(BasCodeGenT *cg, double v); void basEmitDouble(BasCodeGenT *cg, double v);
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);
@ -393,10 +392,6 @@ void basEmitDouble(BasCodeGenT *cg, double v) {
} }
void basEmitFloat(BasCodeGenT *cg, float v) {
basEmitBytes(cg, &v, (int32_t)sizeof(v));
}
void basEmitU16(BasCodeGenT *cg, uint16_t v) { void basEmitU16(BasCodeGenT *cg, uint16_t v) {
basEmitBytes(cg, &v, (int32_t)sizeof(v)); basEmitBytes(cg, &v, (int32_t)sizeof(v));

View file

@ -67,8 +67,8 @@ typedef struct {
// API // API
// ============================================================ // ============================================================
void basCodeGenInit(BasCodeGenT *cg);
void basCodeGenFree(BasCodeGenT *cg); void basCodeGenFree(BasCodeGenT *cg);
void basCodeGenInit(BasCodeGenT *cg);
// Emit single byte // Emit single byte
void basEmit8(BasCodeGenT *cg, uint8_t b); void basEmit8(BasCodeGenT *cg, uint8_t b);
@ -80,7 +80,6 @@ void basEmit16(BasCodeGenT *cg, int16_t v);
void basEmitU16(BasCodeGenT *cg, uint16_t v); void basEmitU16(BasCodeGenT *cg, uint16_t v);
// Emit 32-bit float // Emit 32-bit float
void basEmitFloat(BasCodeGenT *cg, float v);
// Emit 64-bit double // Emit 64-bit double
void basEmitDouble(BasCodeGenT *cg, double v); void basEmitDouble(BasCodeGenT *cg, double v);

View file

@ -100,6 +100,13 @@ int32_t basCompactBytecode(BasModuleT *mod) {
keepStmt = true; keepStmt = true;
} }
// ERL reports the line of the current error, which only the
// OP_LINE stream provides: a module that uses it keeps every
// line marker.
if (op == OP_ERL) {
return 0;
}
pc += 1 + operand; pc += 1 + operand;
} }
@ -326,22 +333,14 @@ static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, bool keepStmt,
int32_t oldPc = 0; int32_t oldPc = 0;
int32_t newPc = 0; int32_t newPc = 0;
// basCompactBytecode's pre-scan already validated every opcode and
// instruction boundary in this stream, so the walk needs no checks
// of its own -- one source of truth for what a well-formed stream is.
while (oldPc < codeLen) { while (oldPc < codeLen) {
uint8_t op = code[oldPc]; uint8_t op = code[oldPc];
int32_t operand = basOpcodeOperandSize(op); int32_t operand = basOpcodeOperandSize(op);
if (operand < 0) {
free(remap);
return NULL;
}
int32_t instSize = 1 + operand; int32_t instSize = 1 + operand;
if (oldPc + instSize > codeLen) {
free(remap);
return NULL;
}
if (op == OP_LINE) { if (op == OP_LINE) {
// Dropped entirely, or shrunk to OP_STMT: the opcode byte maps // Dropped entirely, or shrunk to OP_STMT: the opcode byte maps
// to the boundary marker (if kept) and the operand bytes to the // to the boundary marker (if kept) and the operand bytes to the
@ -366,11 +365,6 @@ static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, bool keepStmt,
oldPc += instSize; oldPc += instSize;
} }
if (oldPc != codeLen) {
free(remap);
return NULL;
}
remap[codeLen] = newPc; remap[codeLen] = newPc;
*outNewLen = newPc; *outNewLen = newPc;
return remap; return remap;

View file

@ -56,6 +56,7 @@ static const KeywordEntryT sKeywords[] = {
KW("BASE", TOK_BASE), KW("BASE", TOK_BASE),
KW("BINARY", TOK_BINARY), KW("BINARY", TOK_BINARY),
KW("BOOLEAN", TOK_BOOLEAN), KW("BOOLEAN", TOK_BOOLEAN),
KW("BYREF", TOK_BYREF),
KW("BYVAL", TOK_BYVAL), KW("BYVAL", TOK_BYVAL),
KW("CALL", TOK_CALL), KW("CALL", TOK_CALL),
KW("CASE", TOK_CASE), KW("CASE", TOK_CASE),
@ -87,6 +88,7 @@ static const KeywordEntryT sKeywords[] = {
KW("EOF", TOK_EOF_KW), KW("EOF", TOK_EOF_KW),
KW("EQV", TOK_EQV), KW("EQV", TOK_EQV),
KW("ERASE", TOK_ERASE), KW("ERASE", TOK_ERASE),
KW("ERL", TOK_ERL),
KW("ERR", TOK_ERR), KW("ERR", TOK_ERR),
KW("ERROR", TOK_ERROR_KW), KW("ERROR", TOK_ERROR_KW),
KW("EXIT", TOK_EXIT), KW("EXIT", TOK_EXIT),

View file

@ -76,6 +76,7 @@ typedef enum {
TOK_AS, TOK_AS,
TOK_BASE, TOK_BASE,
TOK_BOOLEAN, TOK_BOOLEAN,
TOK_BYREF,
TOK_BYVAL, TOK_BYVAL,
TOK_CALL, TOK_CALL,
TOK_CASE, TOK_CASE,
@ -101,6 +102,7 @@ typedef enum {
TOK_EOF_KW, // EOF (keyword, not end-of-file) TOK_EOF_KW, // EOF (keyword, not end-of-file)
TOK_EQV, TOK_EQV,
TOK_ERASE, TOK_ERASE,
TOK_ERL,
TOK_ERR, TOK_ERR,
TOK_ERROR_KW, TOK_ERROR_KW,
TOK_EXPLICIT, TOK_EXPLICIT,

View file

@ -246,7 +246,7 @@ typedef enum {
#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] [uint8 type] bounds on stack (UDT: typeId, fieldCount after bounds) #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 // [uint8 mode] pop array ref, push replacement (BAS_ERASE_ZERO keeps the shape, BAS_ERASE_FREE undimensions)
#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
#define OP_ON_ERROR 0x95 // [int16 handler] set error handler (0 = disable) #define OP_ON_ERROR 0x95 // [int16 handler] set error handler (0 = disable)
@ -254,6 +254,7 @@ 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_ERL 0x9A // push the source line of the current error (0 when none)
#define OP_STORE_FORM_VAR 0x9B // [uint16 idx] pop, store to 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_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_CTRL_EX 0x9D // pop parentRef, pop name, pop type, pop formRef, push ctrlRef
@ -324,7 +325,7 @@ typedef enum {
#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
#define OP_FILE_LOC 0xC2 // pop channel, push current position #define OP_FILE_LOC 0xC2 // pop channel, push LOC (last byte / record / block, by mode)
#define OP_FILE_FREEFILE 0xC3 // push next free channel number #define OP_FILE_FREEFILE 0xC3 // push next free channel number
#define OP_FILE_INPUT_N 0xC4 // pop channel + n, read n chars, push string #define OP_FILE_INPUT_N 0xC4 // pop channel + n, read n chars, push string
@ -339,7 +340,7 @@ typedef enum {
// PRINT USING // PRINT USING
// ============================================================ // ============================================================
#define OP_PRINT_USING 0xC7 // pop format + value, push formatted string #define OP_PRINT_USING 0xC7 // [uint8 flags] pop format + value, push format + formatted string (BAS_USING_FIRST / BAS_USING_LAST)
// ============================================================ // ============================================================
// SPC(n) and TAB(n) with stack-based argument // SPC(n) and TAB(n) with stack-based argument
@ -396,6 +397,8 @@ typedef enum {
#define OP_CREATE_FORM 0xF0 // pop height, pop width, pop nameStr, push formRef #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_SET_EVENT 0xF1 // pop handlerNameStr, pop eventNameStr, pop ctrlRef
#define OP_REMOVE_CTRL 0xF2 // pop ctrlNameStr, pop formRef #define OP_REMOVE_CTRL 0xF2 // pop ctrlNameStr, pop formRef
#define OP_FILE_PUT_UDT 0xF6 // pop layoutStr, pop udt, pop recno, pop channel; write packed fields
#define OP_FILE_SEEK_GET 0xF7 // pop channel, push 1-based file position (SEEK function)
// ============================================================ // ============================================================
// Halt // Halt
@ -404,6 +407,27 @@ 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
// OP_PRINT_USING flag bits: FIRST rewinds the format scan to the start
// of the format string, LAST appends the literal text that follows the
// consumed field.
#define BAS_USING_FIRST 0x01
#define BAS_USING_LAST 0x02
// OP_ERASE modes.
#define BAS_ERASE_ZERO 0 // fixed array: reset every element, keep the bounds
#define BAS_ERASE_FREE 1 // dynamic array: release the storage (undimensioned until REDIM)
// TYPE layout descriptors (built by the compiler as a string constant,
// walked by the VM): one BAS_TYPE_* byte per field in declaration order.
// A STRING entry is followed by a little-endian uint16 fixed length (0 =
// variable length); a BAS_TYPE_UDT entry by the nested type's uint16
// typeId and uint16 fieldCount and then its own entries. DIM/REDIM use
// the layout to build fully initialised instances, GET/PUT to pack and
// unpack the fields (variable strings carry an int16 length prefix).
#define BAS_LAYOUT_MAX_ENTRY_LEN 5
#define BAS_LAYOUT_MAX_LEN 1024
#define BAS_LAYOUT_MAX_DEPTH 16 // nesting cap for TYPE fields inside a layout
// ============================================================ // ============================================================
// Operand-size table and bytecode pattern helpers // Operand-size table and bytecode pattern helpers
// ============================================================ // ============================================================
@ -437,6 +461,25 @@ typedef enum {
#define BAS_GOSUB_PATTERN_LEN 8 // PUSH_INT32(1+4) + JMP(1+2) #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 #define BAS_GOSUB_JMP_OFFSET 5 // byte offset of the OP_JMP inside the pattern
static inline bool basIsGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos);
static inline int32_t basOpcodeOperandSize(uint8_t op);
static inline int32_t basReadI32LE(const uint8_t *p);
// 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;
}
// Returns operand byte count for op, or BAS_OPERAND_UNKNOWN. // Returns operand byte count for op, or BAS_OPERAND_UNKNOWN.
static inline int32_t basOpcodeOperandSize(uint8_t op) { static inline int32_t basOpcodeOperandSize(uint8_t op) {
switch (op) { switch (op) {
@ -471,7 +514,6 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
case OP_MSGBOX: case OP_INPUTBOX: case OP_ME_REF: 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: case OP_FIND_CTRL: case OP_FIND_CTRL_IDX:
case OP_CREATE_CTRL_EX: case OP_CREATE_CTRL_EX:
case OP_ERASE:
case OP_RESUME: case OP_RESUME_NEXT: case OP_RESUME: case OP_RESUME_NEXT:
case OP_RAISE_ERR: case OP_ERR_NUM: case OP_RAISE_ERR: case OP_ERR_NUM:
case OP_MATH_ABS: case OP_MATH_INT: case OP_MATH_FIX: case OP_MATH_ABS: case OP_MATH_INT: case OP_MATH_FIX:
@ -490,7 +532,7 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
case OP_FILE_GET: case OP_FILE_PUT: case OP_FILE_SEEK: 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_LOF: case OP_FILE_LOC: case OP_FILE_FREEFILE:
case OP_FILE_INPUT_N: case OP_FILE_INPUT_N:
case OP_STR_MID_ASGN: case OP_PRINT_USING: case OP_STR_MID_ASGN:
case OP_PRINT_TAB_N: case OP_PRINT_SPC_N: case OP_PRINT_TAB_N: case OP_PRINT_SPC_N:
case OP_FORMAT: case OP_SHELL: case OP_FORMAT: case OP_SHELL:
case OP_APP_PATH: case OP_APP_CONFIG: case OP_APP_DATA: case OP_APP_PATH: case OP_APP_CONFIG: case OP_APP_DATA:
@ -501,6 +543,7 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
case OP_FS_DIR_NEXT: case OP_FS_FILELEN: case OP_FS_DIR_NEXT: case OP_FS_FILELEN:
case OP_FS_GETATTR: case OP_FS_SETATTR: case OP_FS_GETATTR: case OP_FS_SETATTR:
case OP_CREATE_FORM: case OP_SET_EVENT: case OP_REMOVE_CTRL: case OP_CREATE_FORM: case OP_SET_EVENT: case OP_REMOVE_CTRL:
case OP_FILE_PUT_UDT: case OP_ERL: case OP_FILE_SEEK_GET:
case OP_END: case OP_HALT: case OP_END: case OP_HALT:
return BAS_OPERAND_NONE; return BAS_OPERAND_NONE;
@ -509,7 +552,8 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
case OP_FILE_OPEN: case OP_FILE_OPEN:
case OP_CALL_METHOD: case OP_SHOW_FORM: case OP_CALL_METHOD: case OP_SHOW_FORM:
case OP_LBOUND: case OP_UBOUND: case OP_LBOUND: case OP_UBOUND:
case OP_COMPARE_MODE: case OP_COMPARE_MODE: case OP_PRINT_USING:
case OP_ERASE:
return BAS_OPERAND_U8; return BAS_OPERAND_U8;
case OP_PUSH_INT16: case OP_PUSH_STR: case OP_PUSH_INT16: case OP_PUSH_STR:
@ -556,17 +600,4 @@ static inline int32_t basReadI32LE(const uint8_t *p) {
} }
// 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

@ -97,6 +97,7 @@ typedef struct {
int32_t errorLine; int32_t errorLine;
int32_t prevLine; // line of the previous token (for error reporting) int32_t prevLine; // line of the previous token (for error reporting)
int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName
int32_t tempCount; // compiler temporaries allocated so far (SWAP of array elements)
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
bool currentProcIsFunction; // true while parsing a FUNCTION body (EXIT FUNCTION / return-value assignment) bool currentProcIsFunction; // true while parsing a FUNCTION body (EXIT FUNCTION / return-value assignment)

View file

@ -73,6 +73,7 @@ typedef struct {
char name[BAS_MAX_IDENT]; 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
int32_t fixedLen; // for STRING * n fields: fixed length (0 = variable-length)
} BasFieldDefT; } BasFieldDefT;
typedef struct { typedef struct {
@ -111,6 +112,7 @@ typedef struct {
uint8_t dataType; // BAS_TYPE_* for variables/functions uint8_t dataType; // BAS_TYPE_* for variables/functions
bool isDefined; // false = forward-declared bool isDefined; // false = forward-declared
bool isArray; bool isArray;
bool isDynamic; // array declared without bounds (DIM a(), array parameter, or created by REDIM): ERASE frees it
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

View file

@ -0,0 +1,90 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// basNativeDos.c -- DECLARE LIBRARY native call trampoline (i386 / DJGPP)
//
// Pushes the marshalled 32-bit argument words right-to-left, calls the
// resolved symbol with the cdecl convention, and returns EAX (or the x87
// ST(0) result for floating-point returns). The host build links
// basNativeHost.c instead; the two are selected by the Makefile object
// list, never by preprocessor conditionals.
#include "formrt.h"
// Push the already-marshalled cdecl argument words and call a native
// function via funcPtr. Returns the EAX result word; when fpReturn is
// true the x87 ST(0) result is also stored through fpResult via fstpl.
//
// The ESP save, the entire push loop, the call, and the ESP restore
// live inside ONE __asm__ block per return path: a saved-ESP value held
// in a scratch register cannot survive across separate asm statements
// (GCC is free to reuse the register), so everything that depends on it
// must stay in the same block. EDI holds the saved ESP, ESI the args
// base, ECX the down-counter (an in/out operand, pre-loaded with the
// count) and EBX the target -- fixed registers, because with EAX, ECX,
// EDX, ESI and EDI all spoken for there is nothing left to allocate;
// the rest are clobber-listed so GCC reloads anything it kept there.
// "memory" stops GCC caching the string args the callee may
// dereference. fstpl is gated strictly to fpReturn so
// the integer path never pops the x87 stack.
uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult) {
uint32_t rawResult = 0;
int32_t cnt = nativeCount;
if (fpReturn) {
__asm__ __volatile__(
"movl %%esp, %%edi\n\t" // save ESP
"1:\n\t"
"testl %%ecx, %%ecx\n\t"
"jle 2f\n\t"
"decl %%ecx\n\t"
"pushl (%%esi, %%ecx, 4)\n\t" // push nativeArgs[ECX]
"jmp 1b\n\t"
"2:\n\t"
"call *%[fn]\n\t"
"fstpl %[fpres]\n\t" // pop x87 ST(0) -> *fpResult
"movl %%edi, %%esp\n\t" // restore ESP
: "=a"(rawResult), [fpres] "=m"(*fpResult), [cnt] "+c"(cnt)
: [fn] "b"(funcPtr), "S"(nativeArgs)
: "edi", "edx", "cc", "memory"
);
} else {
__asm__ __volatile__(
"movl %%esp, %%edi\n\t" // save ESP
"1:\n\t"
"testl %%ecx, %%ecx\n\t"
"jle 2f\n\t"
"decl %%ecx\n\t"
"pushl (%%esi, %%ecx, 4)\n\t" // push nativeArgs[ECX]
"jmp 1b\n\t"
"2:\n\t"
"call *%[fn]\n\t"
"movl %%edi, %%esp\n\t" // restore ESP
: "=a"(rawResult), [cnt] "+c"(cnt)
: [fn] "b"(funcPtr), "S"(nativeArgs)
: "edi", "edx", "cc", "memory"
);
}
return rawResult;
}

View file

@ -0,0 +1,41 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// basNativeHost.c -- DECLARE LIBRARY native call trampoline (host build)
//
// The i386 cdecl trampoline in basNativeDos.c cannot run on a 64-bit
// host and the test harness has no libffi, so extern calls are refused
// here: nothing is called and the result reads as zero. Selected by the
// Makefile object list of the host basrt archive.
#include "formrt.h"
uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult) {
(void)funcPtr;
(void)nativeArgs;
(void)nativeCount;
(void)fpReturn;
*fpResult = 0.0;
return 0;
}

View file

@ -77,7 +77,7 @@
#define BAS_ERR_SUMMARY_LEN 128 #define BAS_ERR_SUMMARY_LEN 128
#define BAS_ERR_BOX_LEN (BAS_ERR_SUMMARY_LEN + BAS_ERR_DETAIL_LEN) #define BAS_ERR_BOX_LEN (BAS_ERR_SUMMARY_LEN + BAS_ERR_DETAIL_LEN)
// dlsym name buffer: "_" + the longest BASIC identifier. // dlsym name buffer: DVX_SYM_PREFIX + the longest BASIC identifier.
#define BAS_MANGLED_NAME_LEN (BAS_MAX_IDENT + 2) #define BAS_MANGLED_NAME_LEN (BAS_MAX_IDENT + 2)
// Comma-separated property-name list for "Valid: ..." diagnostics. // Comma-separated property-name list for "Valid: ..." diagnostics.
@ -238,10 +238,21 @@ typedef struct {
} BasFrmMenuItemT; } BasFrmMenuItemT;
// ContextMenu assignment held back until the parse ends: the popup it
// names is built from the accumulated menu items only after every
// control exists, so applying it inline would find no menu and bind
// nothing. ctrl == NULL is the form-level property.
typedef struct { typedef struct {
BasFormRtT *rt; BasControlT *ctrl;
BasFormT *form; char menuName[BAS_MAX_IDENT];
BasControlT *current; } BasFrmPendingMenuT;
typedef struct {
BasFormRtT *rt;
BasFormT *form;
BasControlT *current;
BasFrmPendingMenuT *pendingMenus; // stb_ds array
WidgetT *parentStack[FRM_MAX_NESTING]; WidgetT *parentStack[FRM_MAX_NESTING];
int32_t nestDepth; int32_t nestDepth;
bool containerStack[FRM_MAX_NESTING]; bool containerStack[FRM_MAX_NESTING];
@ -415,12 +426,12 @@ void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const ch
void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value); void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value);
void basFormRtShowForm(void *ctx, void *formRef, bool modal); void basFormRtShowForm(void *ctx, void *formRef, bool modal);
static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form); static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form);
void basFormRtTestReset(void);
void basFormRtUnloadForm(void *ctx, void *formRef); void basFormRtUnloadForm(void *ctx, void *formRef);
const char *basInputBox2(const char *title, const char *prompt, const char *defaultText); const char *basInputBox2(const char *title, const char *prompt, const char *defaultText);
int32_t basInputCancelled(void); int32_t basInputCancelled(void);
int32_t basIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal); int32_t basIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal);
static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name); static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
static uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult);
int32_t basPromptSave(const char *title); int32_t basPromptSave(const char *title);
static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc); static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc);
void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt); void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt);
@ -470,6 +481,7 @@ static void frmLoadOnFormProp(void *userData, const char *key, const char *value
static void frmLoadOnMenuBegin(void *userData, const char *name, int32_t level); static void frmLoadOnMenuBegin(void *userData, const char *name, int32_t level);
static void frmLoadOnMenuEnd(void *userData); static void frmLoadOnMenuEnd(void *userData);
static void frmLoadOnMenuProp(void *userData, const char *key, const char *value); static void frmLoadOnMenuProp(void *userData, const char *key, const char *value);
static void frmLoadQueueContextMenu(BasFrmLoadCtxT *ctx, BasControlT *ctrl, const char *menuName);
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled); static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled);
static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName); static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName);
static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled); static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled);
@ -510,6 +522,7 @@ int32_t ResExtract(const char *path, const char *name, const char *outFile);
const char *ResGetText(const char *path, const char *name); const char *ResGetText(const char *path, const char *name);
static void resizeIfaceBuffer(BasControlT *ctrl); static void resizeIfaceBuffer(BasControlT *ctrl);
const char *ResName(int32_t handle, int32_t index); const char *ResName(int32_t handle, int32_t index);
static BasControlT *resolveCtrlRef(BasFormRtT *rt, void *ref);
static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId, bool *fromBar); static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId, bool *fromBar);
static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc); static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc);
static bool resolveShellIdle(void); static bool resolveShellIdle(void);
@ -734,7 +747,7 @@ void *basExternResolve(void *ctx, const char *libName, const char *funcName) {
(void)libName; (void)libName;
char mangledName[BAS_MANGLED_NAME_LEN]; char mangledName[BAS_MANGLED_NAME_LEN];
snprintf(mangledName, sizeof(mangledName), "_%s", funcName); snprintf(mangledName, sizeof(mangledName), DVX_SYM_PREFIX "%s", funcName);
return dlsym(NULL, mangledName); return dlsym(NULL, mangledName);
} }
@ -815,8 +828,8 @@ void basFormRtBindVm(BasFormRtT *rt) {
BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc) { BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc) {
BasFormRtT *rt = (BasFormRtT *)ctx; BasFormRtT *rt = (BasFormRtT *)ctx;
BasControlT *ctrl = (BasControlT *)ctrlRef; BasControlT *ctrl = resolveCtrlRef(rt, ctrlRef);
if (!ctrl) { if (!ctrl) {
basFormRtRuntimeError(rt, basFormRtRuntimeError(rt,
@ -1337,16 +1350,15 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
snprintf(rt->lastLookupName, sizeof(rt->lastLookupName), "%s", ctrlName); snprintf(rt->lastLookupName, sizeof(rt->lastLookupName), "%s", ctrlName);
} }
if (!form) {
return NULL;
}
// The current form first (a local miss is expected for cross-form // The current form first (a local miss is expected for cross-form
// access; don't spam the log), then every other loaded form: dynamic // access; don't spam the log), then every other loaded form: dynamic
// forms created by CreateForm/CreateControl hold controls that the // forms created by CreateForm/CreateControl hold controls that the
// calling SUB references by name (e.g. a mnuXxx_Click on the main // calling SUB references by name (e.g. a mnuXxx_Click on the main
// form sets properties on a control just created on a new form). // form sets properties on a control just created on a new form).
BasControlT *hit = findNamedObjectInForm(form, ctrlName); // Module-level code runs with no current form at all (NULL); it can
// still name any loaded form or control, so the scan covers every
// form in that case.
BasControlT *hit = form ? findNamedObjectInForm(form, ctrlName) : NULL;
if (hit || !rt) { if (hit || !rt) {
return hit; return hit;
@ -1379,13 +1391,19 @@ void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32
"%s(%d)", ctrlName, (int)index); "%s(%d)", ctrlName, (int)index);
} }
if (!form) { // Same scan order as basFormRtFindCtrl: the current form (when the
return NULL; // caller has one), then every other loaded form.
} for (int32_t f = -1; f < (int32_t)arrlen(rt->forms); f++) {
BasFormT *scan = (f < 0) ? form : rt->forms[f];
for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) { if (!scan || (f >= 0 && scan == form)) {
if (form->controls[i]->index == index && strcasecmp(form->controls[i]->name, ctrlName) == 0) { continue;
return form->controls[i]; }
for (int32_t i = 0; i < (int32_t)arrlen(scan->controls); i++) {
if (scan->controls[i]->index == index && strcasecmp(scan->controls[i]->name, ctrlName) == 0) {
return scan->controls[i];
}
} }
} }
@ -1429,8 +1447,8 @@ const BasPropDescT *basFormRtFormProps(int32_t *count) {
BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
BasFormRtT *rt = (BasFormRtT *)ctx; BasFormRtT *rt = (BasFormRtT *)ctx;
BasControlT *ctrl = (BasControlT *)ctrlRef; BasControlT *ctrl = resolveCtrlRef(rt, ctrlRef);
if (!ctrl) { if (!ctrl) {
basFormRtRuntimeError(rt, basFormRtRuntimeError(rt,
@ -1624,6 +1642,17 @@ void *basFormRtLoadForm(void *ctx, const char *formName) {
} }
} }
// A form whose Unload is committed but deferred (a handler is still
// on the stack) keeps answering to its name until the frees flush:
// 'Unload X' compiles to a Load-then-Unload pair, and re-parsing
// the cached .frm here would resurrect the form and unload it a
// second time. Unload on the returned form is a no-op (unloading).
for (int32_t i = 0; i < (int32_t)arrlen(rt->pendingUnload); i++) {
if (strcasecmp(rt->pendingUnload[i]->name, formName) == 0) {
return rt->pendingUnload[i];
}
}
// Check the .frm cache for reload after unload. sLoadingFrm is set // Check the .frm cache for reload after unload. sLoadingFrm is set
// only while basFormRtLoadFrm's frmParse is running: the parser // only while basFormRtLoadFrm's frmParse is running: the parser
// re-enters this function via frmLoadOnFormBegin, and that nested // re-enters this function via frmLoadOnFormBegin, and that nested
@ -1728,6 +1757,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
} }
arrfree(ctx.menuItems); arrfree(ctx.menuItems);
arrfree(ctx.pendingMenus);
return NULL; return NULL;
} }
@ -1844,6 +1874,15 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
arrfree(menuItems); arrfree(menuItems);
menuItems = NULL; menuItems = NULL;
// Bind the ContextMenu assignments now that the popups exist.
for (int32_t i = 0; i < (int32_t)arrlen(ctx.pendingMenus); i++) {
BasFrmPendingMenuT *pending = &ctx.pendingMenus[i];
BasValueT name = basValStringFromC(pending->menuName);
basFormRtSetProp(rt, pending->ctrl ? pending->ctrl : &form->formCtrl, "ContextMenu", name);
basValRelease(&name);
}
// Call Resize on widgets that need it (e.g. PictureBox) so the // Call Resize on widgets that need it (e.g. PictureBox) so the
// internal bitmap matches the layout size. Width/Height aren't // internal bitmap matches the layout size. Width/Height aren't
// known until properties are parsed, so this must happen after // known until properties are parsed, so this must happen after
@ -1873,6 +1912,8 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
} }
} }
arrfree(ctx.pendingMenus);
// Cache the .frm source for reload after unload // Cache the .frm source for reload after unload
if (form) { if (form) {
bool cached = false; bool cached = false;
@ -2013,15 +2054,17 @@ void basFormRtRunSimple(BasFormRtT *rt) {
if (!dvxUpdate(rt->ctx)) { if (!dvxUpdate(rt->ctx)) {
break; break;
} }
} else if (result == BAS_VM_ERROR) { } else if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
// basFormRtRuntimeError already surfaces its own error // basFormRtRuntimeError already surfaces its own error
// dialog and sets rt->terminated, so only show this generic // dialog and sets rt->terminated, so only show this generic
// box for VM-internal errors (division by zero, type // box for VM-internal errors. Every non-HALTED/OK result is
// mismatch, etc.) that didn't come through that path. // an error here: BAS_VM_ERROR carries a message, and the
// specific codes (division by zero, type mismatch, bad
// opcode, ...) must not end the program silently.
if (!rt->terminated) { if (!rt->terminated) {
const char *errMsg = basVmGetError(vm); const char *errMsg = basVmGetError(vm);
char buf[BAS_ERR_BOX_LEN]; char buf[BAS_ERR_BOX_LEN];
snprintf(buf, sizeof(buf), "Runtime error:\n%s", errMsg ? errMsg : "Unknown error"); snprintf(buf, sizeof(buf), "Runtime error:\n%s", errMsg[0] ? errMsg : "Unknown error");
dvxMessageBox(rt->ctx, "Error", buf, 0); dvxMessageBox(rt->ctx, "Error", buf, 0);
} }
break; break;
@ -2188,8 +2231,7 @@ void basFormRtSerialShutdown(void) {
void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName) { void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName) {
(void)ctx; BasControlT *ctrl = resolveCtrlRef((BasFormRtT *)ctx, ctrlRef);
BasControlT *ctrl = (BasControlT *)ctrlRef;
if (!ctrl || !eventName || !handlerName) { if (!ctrl || !eventName || !handlerName) {
return; return;
@ -2213,8 +2255,8 @@ void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const ch
void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value) { void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value) {
BasFormRtT *rt = (BasFormRtT *)ctx; BasFormRtT *rt = (BasFormRtT *)ctx;
BasControlT *ctrl = (BasControlT *)ctrlRef; BasControlT *ctrl = resolveCtrlRef(rt, ctrlRef);
if (!ctrl) { if (!ctrl) {
basFormRtRuntimeError(rt, basFormRtRuntimeError(rt,
@ -2328,9 +2370,18 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) {
// re-arming the input gate on a window the unload just hid. // re-arming the input gate on a window the unload just hid.
rtEventEnter(rt); rtEventEnter(rt);
// The window manager fires onFocus (Activate) only on a focus
// CHANGE. A form created hidden is already the focused window, so
// its first Show would never activate; fire it here in that case.
bool activateHere = !form->window->visible && form->window->focused;
dvxShowWindow(rt->ctx, form->window); dvxShowWindow(rt->ctx, form->window);
dvxRaiseWindow(rt->ctx, form->window); dvxRaiseWindow(rt->ctx, form->window);
if (activateHere && !form->unloading && form->window->focused) {
basFormRtFireEvent(rt, form, form->name, BAS_EVT_ACTIVATE);
}
if (!form->unloading) { if (!form->unloading) {
if (form->frmAutoSize) { if (form->frmAutoSize) {
dvxFitWindow(rt->ctx, form->window); dvxFitWindow(rt->ctx, form->window);
@ -2405,6 +2456,39 @@ static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form) {
} }
// Test-harness hook: return every module-level static to its load-time
// state so the next basFormRtCreate in the same process starts clean.
// Serial/comm state goes through the normal shutdown path first so open
// ports and idle pollers are released rather than leaked.
void basFormRtTestReset(void) {
basFormRtSerialShutdown();
for (int32_t i = 0; i < RES_MAX_HANDLES; i++) {
if (sResHandles[i]) {
dvxResClose(sResHandles[i]);
sResHandles[i] = NULL;
}
}
memset(sSerAttach, 0, sizeof(sSerAttach));
memset(&sSerApi, 0, sizeof(sSerApi));
memset(&sCommApi, 0, sizeof(sCommApi));
sSerApiResolved = false;
sCommApiResolved = false;
sHlpcCompile = NULL;
sHlpcResolved = false;
sShellLoadApp = NULL;
sShellLoadResolved = false;
sShellRegisterIdle = NULL;
sShellUnregisterIdle = NULL;
sShellIdleResolved = false;
sFormRt = NULL;
sLoadingFrm = false;
sLastInputBoxCancelled = false;
}
void basFormRtUnloadForm(void *ctx, void *formRef) { void basFormRtUnloadForm(void *ctx, void *formRef) {
BasFormRtT *rt = (BasFormRtT *)ctx; BasFormRtT *rt = (BasFormRtT *)ctx;
BasFormT *form = (BasFormT *)formRef; BasFormT *form = (BasFormT *)formRef;
@ -2493,63 +2577,6 @@ static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char
} }
// Push the already-marshalled cdecl argument words and call a native
// function via funcPtr. Returns the EAX result word; when fpReturn is
// true the x87 ST(0) result is also stored through fpResult via fstpl.
//
// The ESP save, the entire push loop, the call, and the ESP restore
// live inside ONE __asm__ block per return path: a saved-ESP value held
// in a scratch register cannot survive across separate asm statements
// (GCC is free to reuse the register), so everything that depends on it
// must stay in the same block. EDI holds the saved ESP, ESI the args
// base, ECX the down-counter; all are clobber-listed so GCC reloads
// anything it kept there. "memory" stops GCC caching the string args
// the callee may dereference. fstpl is gated strictly to fpReturn so
// the integer path never pops the x87 stack.
static uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult) {
uint32_t rawResult = 0;
if (fpReturn) {
__asm__ __volatile__(
"movl %%esp, %%edi\n\t" // save ESP
"movl %[cnt], %%ecx\n\t" // ECX = arg count
"1:\n\t"
"testl %%ecx, %%ecx\n\t"
"jle 2f\n\t"
"decl %%ecx\n\t"
"pushl (%%esi, %%ecx, 4)\n\t" // push nativeArgs[ECX]
"jmp 1b\n\t"
"2:\n\t"
"call *%[fn]\n\t"
"fstpl %[fpres]\n\t" // pop x87 ST(0) -> *fpResult
"movl %%edi, %%esp\n\t" // restore ESP
: "=a"(rawResult), [fpres] "=m"(*fpResult)
: [fn] "r"(funcPtr), "S"(nativeArgs), [cnt] "r"(nativeCount)
: "ecx", "edi", "edx", "cc", "memory"
);
} else {
__asm__ __volatile__(
"movl %%esp, %%edi\n\t" // save ESP
"movl %[cnt], %%ecx\n\t" // ECX = arg count
"1:\n\t"
"testl %%ecx, %%ecx\n\t"
"jle 2f\n\t"
"decl %%ecx\n\t"
"pushl (%%esi, %%ecx, 4)\n\t" // push nativeArgs[ECX]
"jmp 1b\n\t"
"2:\n\t"
"call *%[fn]\n\t"
"movl %%edi, %%esp\n\t" // restore ESP
: "=a"(rawResult)
: [fn] "r"(funcPtr), "S"(nativeArgs), [cnt] "r"(nativeCount)
: "ecx", "edi", "edx", "cc", "memory"
);
}
return rawResult;
}
int32_t basPromptSave(const char *title) { int32_t basPromptSave(const char *title) {
if (!sFormRt) { if (!sFormRt) {
return DVX_SAVE_NO; return DVX_SAVE_NO;
@ -3185,14 +3212,14 @@ static bool commResolveApi(void) {
} }
sCommApiResolved = true; sCommApiResolved = true;
sCommApi.open = dlsym(NULL, "_secLinkOpen"); sCommApi.open = dlsym(NULL, DVX_SYM("secLinkOpen"));
sCommApi.close = dlsym(NULL, "_secLinkClose"); sCommApi.close = dlsym(NULL, DVX_SYM("secLinkClose"));
sCommApi.handshake = dlsym(NULL, "_secLinkHandshake"); sCommApi.handshake = dlsym(NULL, DVX_SYM("secLinkHandshake"));
sCommApi.isReady = dlsym(NULL, "_secLinkIsReady"); sCommApi.isReady = dlsym(NULL, DVX_SYM("secLinkIsReady"));
sCommApi.poll = dlsym(NULL, "_secLinkPoll"); sCommApi.poll = dlsym(NULL, DVX_SYM("secLinkPoll"));
sCommApi.send = dlsym(NULL, "_secLinkSend"); sCommApi.send = dlsym(NULL, DVX_SYM("secLinkSend"));
sCommApi.sendBuf = dlsym(NULL, "_secLinkSendBuf"); sCommApi.sendBuf = dlsym(NULL, DVX_SYM("secLinkSendBuf"));
sCommApi.getPending = dlsym(NULL, "_secLinkGetPending"); sCommApi.getPending = dlsym(NULL, DVX_SYM("secLinkGetPending"));
return sCommApi.open != NULL; return sCommApi.open != NULL;
} }
@ -3952,6 +3979,11 @@ static void frmLoadOnCtrlProp(void *userData, const char *key, const char *value
snprintf(scratch, sizeof(scratch), "%s", value); snprintf(scratch, sizeof(scratch), "%s", value);
frmStripQuotes(scratch); frmStripQuotes(scratch);
if (strcasecmp(key, "ContextMenu") == 0) {
frmLoadQueueContextMenu(ctx, ctrl, scratch);
return;
}
// Layout property on a container: replace the parentStack entry // Layout property on a container: replace the parentStack entry
// with a layout box inside the container widget. VBox is the // with a layout box inside the container widget. VBox is the
// default for Frame, so no wrapper needed. // default for Frame, so no wrapper needed.
@ -4084,12 +4116,14 @@ static void frmLoadOnFormProp(void *userData, const char *key, const char *value
snprintf(frm->helpTopic, sizeof(frm->helpTopic), "%s", text); snprintf(frm->helpTopic, sizeof(frm->helpTopic), "%s", text);
break; break;
case FORM_PROP_CONTEXTMENU:
frmLoadQueueContextMenu(ctx, NULL, text);
break;
case FORM_PROP_NAME: case FORM_PROP_NAME:
case FORM_PROP_VISIBLE: case FORM_PROP_VISIBLE:
case FORM_PROP_CONTEXTMENU:
case FORM_PROP_COUNT: case FORM_PROP_COUNT:
// Name comes from the Begin line; Visible and ContextMenu are // Name comes from the Begin line; Visible is runtime-only.
// runtime-only (the menu does not exist until the parse ends).
break; break;
} }
} }
@ -4152,6 +4186,18 @@ static void frmLoadOnMenuProp(void *userData, const char *key, const char *value
} }
// Remember a ContextMenu assignment for basFormRtLoadFrm to apply once
// the popup menus exist (see BasFrmPendingMenuT).
static void frmLoadQueueContextMenu(BasFrmLoadCtxT *ctx, BasControlT *ctrl, const char *menuName) {
BasFrmPendingMenuT pending;
memset(&pending, 0, sizeof(pending));
pending.ctrl = ctrl;
snprintf(pending.menuName, sizeof(pending.menuName), "%s", menuName);
arrput(ctx->pendingMenus, pending);
}
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled) { static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled) {
const BasPropDescT *pd = basFormRtFindCommonProp(propName); const BasPropDescT *pd = basFormRtFindCommonProp(propName);
WidgetT *w = ctrl->widget; WidgetT *w = ctrl->widget;
@ -4355,7 +4401,7 @@ static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *pr
int32_t HelpCompile(const char *inputFile, const char *outputFile) { int32_t HelpCompile(const char *inputFile, const char *outputFile) {
if (!sHlpcResolved) { if (!sHlpcResolved) {
sHlpcResolved = true; sHlpcResolved = true;
sHlpcCompile = dlsym(NULL, "_hlpcCompile"); sHlpcCompile = dlsym(NULL, DVX_SYM("hlpcCompile"));
} }
if (!sHlpcCompile || !inputFile || !outputFile) { if (!sHlpcCompile || !inputFile || !outputFile) {
@ -4373,7 +4419,7 @@ int32_t HelpCompile(const char *inputFile, const char *outputFile) {
void HelpView(const char *hlpFile) { void HelpView(const char *hlpFile) {
if (!sShellLoadResolved) { if (!sShellLoadResolved) {
sShellLoadResolved = true; sShellLoadResolved = true;
sShellLoadApp = dlsym(NULL, "_shellLoadAppWithArgs"); sShellLoadApp = dlsym(NULL, DVX_SYM("shellLoadAppWithArgs"));
} }
if (!sShellLoadApp || !sFormRt || !sFormRt->ctx || !hlpFile) { if (!sShellLoadApp || !sFormRt || !sFormRt->ctx || !hlpFile) {
@ -5100,6 +5146,32 @@ const char *ResName(int32_t handle, int32_t index) {
} }
// Object references reach the property/method bridges as either a
// BasControlT* (FindCtrl) or a BasFormT* (Me, CreateForm, LoadForm).
// Map a form reference onto its synthetic form control so both kinds
// take the same path. Detached (pending-unload) forms are included:
// a handler may still touch Me after Unload Me.
static BasControlT *resolveCtrlRef(BasFormRtT *rt, void *ref) {
if (!rt || !ref) {
return (BasControlT *)ref;
}
for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
if (rt->forms[i] == ref) {
return &rt->forms[i]->formCtrl;
}
}
for (int32_t i = 0; i < (int32_t)arrlen(rt->pendingUnload); i++) {
if (rt->pendingUnload[i] == ref) {
return &rt->pendingUnload[i]->formCtrl;
}
}
return (BasControlT *)ref;
}
// Resolve a menu item by command id across both the form's menu bar AND its // Resolve a menu item by command id across both the form's menu bar AND its
// popup-only menus, so .Checked/.Enabled work on Visible=False popup items // popup-only menus, so .Checked/.Enabled work on Visible=False popup items
// (which have no menu-bar entry) as well as bar items. When fromBar is // (which have no menu-bar entry) as well as bar items. When fromBar is
@ -5170,8 +5242,8 @@ static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc) {
static bool resolveShellIdle(void) { static bool resolveShellIdle(void) {
if (!sShellIdleResolved) { if (!sShellIdleResolved) {
sShellIdleResolved = true; sShellIdleResolved = true;
sShellRegisterIdle = dlsym(NULL, "_shellRegisterIdle"); sShellRegisterIdle = dlsym(NULL, DVX_SYM("shellRegisterIdle"));
sShellUnregisterIdle = dlsym(NULL, "_shellUnregisterIdle"); sShellUnregisterIdle = dlsym(NULL, DVX_SYM("shellUnregisterIdle"));
} }
return sShellRegisterIdle != NULL; return sShellRegisterIdle != NULL;
@ -5514,18 +5586,18 @@ static bool serResolveApi(void) {
} }
sSerApiResolved = true; sSerApiResolved = true;
sSerApi.open = dlsym(NULL, "_rs232Open"); sSerApi.open = dlsym(NULL, DVX_SYM("rs232Open"));
sSerApi.close = dlsym(NULL, "_rs232Close"); sSerApi.close = dlsym(NULL, DVX_SYM("rs232Close"));
sSerApi.read = dlsym(NULL, "_rs232Read"); sSerApi.read = dlsym(NULL, DVX_SYM("rs232Read"));
sSerApi.write = dlsym(NULL, "_rs232Write"); sSerApi.write = dlsym(NULL, DVX_SYM("rs232Write"));
sSerApi.writeBuf = dlsym(NULL, "_rs232WriteBuf"); sSerApi.writeBuf = dlsym(NULL, DVX_SYM("rs232WriteBuf"));
sSerApi.getRxBuffered = dlsym(NULL, "_rs232GetRxBuffered"); sSerApi.getRxBuffered = dlsym(NULL, DVX_SYM("rs232GetRxBuffered"));
sSerApi.clearRxBuffer = dlsym(NULL, "_rs232ClearRxBuffer"); sSerApi.clearRxBuffer = dlsym(NULL, DVX_SYM("rs232ClearRxBuffer"));
sSerApi.getUartType = dlsym(NULL, "_rs232GetUartType"); sSerApi.getUartType = dlsym(NULL, DVX_SYM("rs232GetUartType"));
sSerApi.getBase = dlsym(NULL, "_rs232GetBase"); sSerApi.getBase = dlsym(NULL, DVX_SYM("rs232GetBase"));
sSerApi.getIrq = dlsym(NULL, "_rs232GetIrq"); sSerApi.getIrq = dlsym(NULL, DVX_SYM("rs232GetIrq"));
sSerApi.setBase = dlsym(NULL, "_rs232SetBase"); sSerApi.setBase = dlsym(NULL, DVX_SYM("rs232SetBase"));
sSerApi.setIrq = dlsym(NULL, "_rs232SetIrq"); sSerApi.setIrq = dlsym(NULL, DVX_SYM("rs232SetIrq"));
return sSerApi.open != NULL; return sSerApi.open != NULL;
} }

View file

@ -82,8 +82,8 @@ const BasPropDescT *basFormRtFormProps(int32_t *count);
const BasPropDescT *basFormRtCommonProps(int32_t *count); const BasPropDescT *basFormRtCommonProps(int32_t *count);
// Case-insensitive lookups; NULL when the name is not in the table. // Case-insensitive lookups; NULL when the name is not in the table.
const BasPropDescT *basFormRtFindFormProp(const char *name);
const BasPropDescT *basFormRtFindCommonProp(const char *name); const BasPropDescT *basFormRtFindCommonProp(const char *name);
const BasPropDescT *basFormRtFindFormProp(const char *name);
// ============================================================ // ============================================================
// Common methods // Common methods
@ -302,27 +302,41 @@ void basFormRtDestroy(BasFormRtT *rt);
// Called by basFormRtDestroy and by the stub's _appShutdown force-kill hook. // Called by basFormRtDestroy and by the stub's _appShutdown force-kill hook.
void basFormRtSerialShutdown(void); void basFormRtSerialShutdown(void);
// Test-only: clear every file-scope static so a host test process can run
// many create/destroy cycles. Not used by the DOS build.
void basFormRtTestReset(void);
// Wire up the VM's UI callbacks to this form runtime. // Wire up the VM's UI callbacks to this form runtime.
void basFormRtBindVm(BasFormRtT *rt); void basFormRtBindVm(BasFormRtT *rt);
// Report a non-recoverable runtime error: log, halt the VM, mark the
// runtime terminated and (unless suppressErrorDialog) show the modal
// error box. Follow-on errors while terminated are swallowed.
void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...);
// ---- UI callback implementations (match BasUiCallbacksT) ---- // ---- UI callback implementations (match BasUiCallbacksT) ----
BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName);
void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value);
BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc); BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc);
void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const char *ctrlName); void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const char *ctrlName);
void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName); void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName);
void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32_t index); void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32_t index);
void *basFormRtLoadForm(void *ctx, const char *formName); BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName);
void basFormRtUnloadForm(void *ctx, void *formRef);
void basFormRtShowForm(void *ctx, void *formRef, bool modal);
void basFormRtHideForm(void *ctx, void *formRef); void basFormRtHideForm(void *ctx, void *formRef);
void *basFormRtLoadForm(void *ctx, const char *formName);
int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title); int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title);
void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value);
void basFormRtShowForm(void *ctx, void *formRef, bool modal);
void basFormRtUnloadForm(void *ctx, void *formRef);
// ---- Extern call callbacks (shared by IDE and stub) ---- // ---- Extern call callbacks (shared by IDE and stub) ----
void *basExternResolve(void *ctx, const char *libName, const char *funcName); void *basExternResolve(void *ctx, const char *libName, const char *funcName);
// Native cdecl trampoline behind basExternCall. basNativeDos.c carries
// the i386 asm; basNativeHost.c is the 64-bit host stub that calls
// nothing. Selected by the Makefile object list.
BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType); BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType);
uint32_t basNativeCall(void *funcPtr, const uint32_t *nativeArgs, int32_t nativeCount, bool fpReturn, double *fpResult);
// ---- Form caching ---- // ---- Form caching ----
@ -361,10 +375,10 @@ WindowT *basFormRtCreateFormWindow(AppContextT *ctx, const char *title, const c
// ---- Dynamic form/control API ---- // ---- Dynamic form/control API ----
void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_t height);
void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef); void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef);
void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName); void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_t height);
void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName); void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName);
void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName);
// ---- Event dispatch ---- // ---- Event dispatch ----

View file

@ -0,0 +1 @@
.h1 Help

View file

@ -265,8 +265,8 @@ void dsgnSyncWidgetGeom(DsgnControlT *ctrl);
// Built-in integer property table: entry by index (NULL past the end) and // Built-in integer property table: entry by index (NULL past the end) and
// lookup by .frm key (name or alias). // lookup by .frm key (name or alias).
const DsgnIntPropT *dsgnIntPropAt(int32_t idx);
const DsgnIntPropT *dsgnFindIntProp(const char *name); const DsgnIntPropT *dsgnFindIntProp(const char *name);
const DsgnIntPropT *dsgnIntPropAt(int32_t idx);
// Text value of a runtime form property (basFormRtFormProps) as the designer // Text value of a runtime form property (basFormRtFormProps) as the designer
// stores it; false when the designer has no field for it (runtime-only). // stores it; false when the designer has no field for it (runtime-only).

View file

@ -446,7 +446,9 @@ static void onEditorChange(WidgetT *w);
static void onEvtDropdownChange(WidgetT *w); static void onEvtDropdownChange(WidgetT *w);
static void onFindClose(WindowT *win); static void onFindClose(WindowT *win);
static void onFindCloseBtn(WidgetT *w); static void onFindCloseBtn(WidgetT *w);
static void onFindDirDown(WidgetT *w, int32_t button, int32_t x, int32_t y);
static void onFindNext(WidgetT *w); static void onFindNext(WidgetT *w);
static void onFindScopeDown(WidgetT *w, int32_t button, int32_t x, int32_t y);
static void onFormWinClose(WindowT *win); static void onFormWinClose(WindowT *win);
static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y); static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y);
static void onFormWinKey(WindowT *win, int32_t key, int32_t mod); static void onFormWinKey(WindowT *win, int32_t key, int32_t mod);
@ -456,9 +458,11 @@ static void onFormWinPaint(WindowT *win, RectT *dirtyArea);
static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH); static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH);
static void onGutterClick(WidgetT *w, int32_t lineNum); static void onGutterClick(WidgetT *w, int32_t lineNum);
static void onImmediateChange(WidgetT *w); static void onImmediateChange(WidgetT *w);
static void onImmWinClose(WindowT *win);
static void onLocalsClose(WindowT *win); static void onLocalsClose(WindowT *win);
static void onMenu(WindowT *win, int32_t menuId); static void onMenu(WindowT *win, int32_t menuId);
static void onObjDropdownChange(WidgetT *w); static void onObjDropdownChange(WidgetT *w);
static void onOutWinClose(WindowT *win);
static void onPrefsCancel(WidgetT *w); static void onPrefsCancel(WidgetT *w);
static void onPrefsOk(WidgetT *w); static void onPrefsOk(WidgetT *w);
static void onPrjFileDblClick(int32_t fileIdx, bool isForm); static void onPrjFileDblClick(int32_t fileIdx, bool isForm);
@ -830,6 +834,10 @@ static WidgetT *sBtnReplAll = NULL;
static WidgetT *sCaseCheck = NULL; static WidgetT *sCaseCheck = NULL;
static WidgetT *sScopeGroup = NULL; // radio group: 0=Func, 1=Obj, 2=File, 3=Proj static WidgetT *sScopeGroup = NULL; // radio group: 0=Func, 1=Obj, 2=File, 3=Proj
static WidgetT *sDirGroup = NULL; // radio group: 0=Fwd, 1=Back static WidgetT *sDirGroup = NULL; // radio group: 0=Fwd, 1=Back
// The radio-group widget exposes no selection getter, so the dialog
// tracks the picks itself through each radio's onMouseDown.
static int32_t sFindScopeIdx = ScopeProjE;
static int32_t sFindDirIdx = 0;
static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array (rebuilt by joinProcBufs) static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array (rebuilt by joinProcBufs)
static const char **sObjItems = NULL; // stb_ds dynamic array static const char **sObjItems = NULL; // stb_ds dynamic array
@ -1792,26 +1800,20 @@ static bool compileProject(void) {
dvxUpdate(sAc); dvxUpdate(sAc);
// Build source: either concatenate project files or use editor contents // Build source: either concatenate project files or use editor contents
char *concatBuf = NULL; BasProjectSourceT project;
const char *src = NULL; const char *src = NULL;
int32_t srcLen = 0; int32_t srcLen = 0;
memset(&project, 0, sizeof(project));
if (hasProject() && sProject.fileCount > 0) { if (hasProject() && sProject.fileCount > 0) {
// Stash current editor/designer state into project buffers // Stash current editor/designer state into project buffers
// (for a form this also writes the editor back to form->code) // (for a form this also writes the editor back to form->code)
stashCurrentFile(); stashCurrentFile();
// Concatenate all files (each normalized to the canonical proc layout) // Concatenate all files (each normalized to the canonical proc
concatBuf = (char *)malloc(IDE_MAX_SOURCE); // layout) with the same BEGINFORM/ENDFORM wrapping bascomp and
// basrun use, recording where each file lands for the source map.
if (!concatBuf) {
setStatus("Out of memory.");
dvxSetBusy(sAc, false);
return false;
}
int32_t pos = 0;
int32_t line = 1;
arrfree(sProject.sourceMap); arrfree(sProject.sourceMap);
sProject.sourceMap = NULL; sProject.sourceMap = NULL;
sProject.sourceMapCount = 0; sProject.sourceMapCount = 0;
@ -1830,70 +1832,46 @@ static bool compileProject(void) {
if (!fileSrc) { if (!fileSrc) {
sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[i].path, (int)IDE_MAX_SOURCE); sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[i].path, (int)IDE_MAX_SOURCE);
showCompileError("Compilation failed: cannot read file."); showCompileError("Compilation failed: cannot read file.");
free(concatBuf); basBuildFreeProject(&project);
return false; return false;
} }
int32_t fileLen = (int32_t)strlen(fileSrc); int32_t fileLen = (int32_t)strlen(fileSrc);
// BEGINFORM/ENDFORM directives plus a trailing newline // BEGINFORM/ENDFORM directives plus a trailing newline
if (pos + fileLen + IDE_SOURCE_MARGIN >= IDE_MAX_SOURCE) { if (project.sourceLen + fileLen + IDE_SOURCE_MARGIN >= IDE_MAX_SOURCE) {
sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[i].path); sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[i].path);
showCompileError("Compilation failed: source too large."); showCompileError("Compilation failed: source too large.");
free(fileSrc); free(fileSrc);
free(concatBuf); basBuildFreeProject(&project);
return false; return false;
} }
// Inject BEGINFORM directive for .frm code sections // startLine is recorded AFTER the injected BEGINFORM so the
if (sProject.files[i].isForm && sProject.files[i].formName[0]) { // source map lines match what the editor shows.
pos = emitClamped(concatBuf, IDE_MAX_SOURCE, pos, "BEGINFORM \"%s\"\n", sProject.files[i].formName); const char *formName = sProject.files[i].isForm ? sProject.files[i].formName : NULL;
line++; PrjSourceMapT mapEntry;
} bool ok = basBuildAppendFile(&project, fileSrc, fileLen, formName, &mapEntry.startLine, &mapEntry.lineCount);
// Record startLine AFTER injected directives so the source
// map lines match what the editor shows (not the synthetic lines).
int32_t startLine = line;
memcpy(concatBuf + pos, fileSrc, fileLen);
pos += fileLen;
// Count lines
for (int32_t j = 0; j < fileLen; j++) {
if (fileSrc[j] == '\n') {
line++;
}
}
free(fileSrc); free(fileSrc);
// Ensure a trailing newline between files if (!ok) {
if (fileLen > 0 && concatBuf[pos - 1] != '\n') { setStatus(project.error);
concatBuf[pos++] = '\n'; dvxSetBusy(sAc, false);
line++; basBuildFreeProject(&project);
return false;
} }
// Record source map BEFORE injected ENDFORM directive mapEntry.fileIdx = i;
PrjSourceMapT mapEntry;
mapEntry.fileIdx = i;
mapEntry.startLine = startLine;
mapEntry.lineCount = line - startLine;
arrput(sProject.sourceMap, mapEntry); arrput(sProject.sourceMap, mapEntry);
sProject.sourceMapCount = (int32_t)arrlen(sProject.sourceMap); sProject.sourceMapCount = (int32_t)arrlen(sProject.sourceMap);
// Inject ENDFORM directive
if (sProject.files[i].isForm && sProject.files[i].formName[0]) {
pos = emitClamped(concatBuf, IDE_MAX_SOURCE, pos, "ENDFORM\n");
line++;
}
dvxUpdate(sAc); dvxUpdate(sAc);
} }
} }
concatBuf[pos] = '\0'; src = project.source;
src = concatBuf; srcLen = project.sourceLen;
srcLen = pos;
} else { } else {
// No project files -- compile the full source // No project files -- compile the full source
src = getFullSource(); src = getFullSource();
@ -1911,7 +1889,7 @@ static bool compileProject(void) {
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT)); BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
if (!parser) { if (!parser) {
free(concatBuf); basBuildFreeProject(&project);
setStatus("Out of memory."); setStatus("Out of memory.");
dvxSetBusy(sAc, false); dvxSetBusy(sAc, false);
return false; return false;
@ -1948,7 +1926,7 @@ static bool compileProject(void) {
showCompileError("Compilation failed: unknown widget type."); showCompileError("Compilation failed: unknown widget type.");
basParserFree(parser); basParserFree(parser);
free(parser); free(parser);
free(concatBuf); basBuildFreeProject(&project);
arrfree(validatorCtx.entries); arrfree(validatorCtx.entries);
arrfree(validatorCtx.badTypes); arrfree(validatorCtx.badTypes);
return false; return false;
@ -2023,13 +2001,13 @@ static bool compileProject(void) {
showCompileError("Compilation failed."); showCompileError("Compilation failed.");
basParserFree(parser); basParserFree(parser);
free(parser); free(parser);
free(concatBuf); basBuildFreeProject(&project);
arrfree(validatorCtx.entries); arrfree(validatorCtx.entries);
arrfree(validatorCtx.badTypes); arrfree(validatorCtx.badTypes);
return false; return false;
} }
free(concatBuf); basBuildFreeProject(&project);
arrfree(validatorCtx.entries); arrfree(validatorCtx.entries);
arrfree(validatorCtx.badTypes); arrfree(validatorCtx.badTypes);
@ -3327,7 +3305,7 @@ static bool getFindForward(void) {
return true; return true;
} }
return wgtRadioGetIndex(sDirGroup) == 0; return sFindDirIdx == 0;
} }
@ -3341,9 +3319,7 @@ static FindScopeE getFindScope(void) {
return ScopeProjE; return ScopeProjE;
} }
int32_t idx = wgtRadioGetIndex(sScopeGroup); switch (sFindScopeIdx) {
switch (idx) {
case 0: case 0:
return ScopeFuncE; return ScopeFuncE;
@ -4045,10 +4021,15 @@ void ideRenameInCode(const char *oldName, const char *newName) {
} }
} }
// Update form->code from the renamed buffers (only if editor has this form's code) // Update form->code: from the renamed buffers when the editor holds
if (sDesigner.form && sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount && // this form's code, otherwise directly -- with the code window closed
sProject.files[sEditorFileIdx].isForm && // the proc buffers were released, and the designer's copy of the code
strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) == 0) { // is the only one that exists.
bool editorHasForm = sDesigner.form && sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount &&
sProject.files[sEditorFileIdx].isForm &&
strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) == 0;
if (editorHasForm) {
char *code = strdup(getFullSource()); char *code = strdup(getFullSource());
if (code) { if (code) {
@ -4057,6 +4038,14 @@ void ideRenameInCode(const char *oldName, const char *newName) {
} }
sDesigner.form->dirty = true; sDesigner.form->dirty = true;
} else if (sDesigner.form && sDesigner.form->code) {
char *replaced = renameInBuffer(sDesigner.form->code, oldName, newName);
if (replaced) {
free(sDesigner.form->code);
sDesigner.form->code = replaced;
sDesigner.form->dirty = true;
}
} }
// Update cached formName if the active file is a form being renamed // Update cached formName if the active file is a form being renamed
@ -4184,9 +4173,9 @@ static void immPrintCallback(void *ctx, const char *text, bool newline) {
const char *cur = wgtGetText(sImmediate); const char *cur = wgtGetText(sImmediate);
const char *keep = cur ? cur : ""; const char *keep = cur ? cur : "";
int32_t curLen = (int32_t)strlen(keep); size_t curLen = strlen(keep);
int32_t textLen = text ? (int32_t)strlen(text) : 0; size_t textLen = text ? strlen(text) : 0;
int32_t extra = newline ? 1 : 0; size_t extra = newline ? 1 : 0;
// A single oversize chunk is truncated to what can ever fit // A single oversize chunk is truncated to what can ever fit
if (textLen + extra + 1 >= IDE_MAX_IMM) { if (textLen + extra + 1 >= IDE_MAX_IMM) {
@ -4203,10 +4192,16 @@ static void immPrintCallback(void *ctx, const char *text, bool newline) {
break; break;
} }
curLen -= (int32_t)(nl + 1 - keep); curLen -= (size_t)(nl + 1 - keep);
keep = nl + 1; keep = nl + 1;
} }
// Everything above keeps curLen + textLen + extra below IDE_MAX_IMM;
// the clamp makes that visible to the compiler as well.
if (curLen >= IDE_MAX_IMM) {
curLen = IDE_MAX_IMM - 1;
}
memmove(immBuf, keep, curLen); memmove(immBuf, keep, curLen);
if (textLen > 0) { if (textLen > 0) {
@ -5229,6 +5224,11 @@ static void onClose(WindowT *win) {
prefsClose(sPrefs); prefsClose(sPrefs);
sPrefs = NULL; sPrefs = NULL;
// The accelerator table is shared by every IDE window; all of them
// are gone by now, so release it before the main window goes.
dvxFreeAccelTable(win->accelTable);
win->accelTable = NULL;
dvxDestroyWindow(sAc, win); dvxDestroyWindow(sAc, win);
} }
@ -5418,6 +5418,16 @@ static void onFindCloseBtn(WidgetT *w) {
} }
// Direction radio picked: remember the index (the group widget offers
// no way to read it back).
static void onFindDirDown(WidgetT *w, int32_t button, int32_t x, int32_t y) {
(void)button;
(void)x;
(void)y;
sFindDirIdx = (int32_t)(intptr_t)w->userData;
}
static void onFindNext(WidgetT *w) { static void onFindNext(WidgetT *w) {
(void)w; (void)w;
@ -5450,6 +5460,15 @@ static void onFindNext(WidgetT *w) {
} }
// Scope radio picked: remember the index.
static void onFindScopeDown(WidgetT *w, int32_t button, int32_t x, int32_t y) {
(void)button;
(void)x;
(void)y;
sFindScopeIdx = (int32_t)(intptr_t)w->userData;
}
// onFormWinClose -- shell callback when user clicks X on the form window. // onFormWinClose -- shell callback when user clicks X on the form window.
static void onFormWinClose(WindowT *win) { static void onFormWinClose(WindowT *win) {
dvxDestroyWindow(sAc, win); dvxDestroyWindow(sAc, win);
@ -5710,6 +5729,13 @@ static void onImmediateChange(WidgetT *w) {
} }
// The IDE keeps sImmWin/sImmediate for its whole lifetime; destroying
// the window here would leave them dangling.
static void onImmWinClose(WindowT *win) {
dvxHideWindow(sAc, win);
}
static void onLocalsClose(WindowT *win) { static void onLocalsClose(WindowT *win) {
dvxHideWindow(sAc, win); dvxHideWindow(sAc, win);
} }
@ -6009,6 +6035,12 @@ static void onObjDropdownChange(WidgetT *w) {
} }
// Hide (not destroy): sOutWin/sOutput stay bound for PRINT output.
static void onOutWinClose(WindowT *win) {
dvxHideWindow(sAc, win);
}
static void onPrefsCancel(WidgetT *w) { static void onPrefsCancel(WidgetT *w) {
(void)w; (void)w;
sPrefsDlg.done = true; sPrefsDlg.done = true;
@ -6379,19 +6411,35 @@ static void openFindDialog(bool showReplace) {
WidgetT *scopeFrame = wgtFrame(optRow, "Scope"); WidgetT *scopeFrame = wgtFrame(optRow, "Scope");
WidgetT *scopeBox = wgtVBox(scopeFrame); WidgetT *scopeBox = wgtVBox(scopeFrame);
sScopeGroup = wgtRadioGroup(scopeBox); sScopeGroup = wgtRadioGroup(scopeBox);
wgtRadio(sScopeGroup, "Function");
wgtRadio(sScopeGroup, "Object"); static const char *scopeNames[] = { "Function", "Object", "File", "Project" };
wgtRadio(sScopeGroup, "File");
wgtRadio(sScopeGroup, "Project"); for (int32_t i = 0; i < (int32_t)(sizeof(scopeNames) / sizeof(scopeNames[0])); i++) {
WidgetT *scopeRadio = wgtRadio(sScopeGroup, scopeNames[i]);
scopeRadio->userData = (void *)(intptr_t)i;
scopeRadio->onMouseDown = onFindScopeDown;
}
wgtRadioGroupSetSelected(sScopeGroup, ScopeProjE); wgtRadioGroupSetSelected(sScopeGroup, ScopeProjE);
sFindScopeIdx = ScopeProjE;
// Direction // Direction
WidgetT *dirFrame = wgtFrame(optRow, "Direction"); WidgetT *dirFrame = wgtFrame(optRow, "Direction");
WidgetT *dirBox = wgtVBox(dirFrame); WidgetT *dirBox = wgtVBox(dirFrame);
sDirGroup = wgtRadioGroup(dirBox); sDirGroup = wgtRadioGroup(dirBox);
wgtRadio(sDirGroup, "Forward");
wgtRadio(sDirGroup, "Backward"); static const char *dirNames[] = { "Forward", "Backward" };
for (int32_t i = 0; i < (int32_t)(sizeof(dirNames) / sizeof(dirNames[0])); i++) {
WidgetT *dirRadio = wgtRadio(sDirGroup, dirNames[i]);
dirRadio->userData = (void *)(intptr_t)i;
dirRadio->onMouseDown = onFindDirDown;
}
wgtRadioGroupSetSelected(sDirGroup, 0); // Forward wgtRadioGroupSetSelected(sDirGroup, 0); // Forward
sFindDirIdx = 0;
// Match Case // Match Case
WidgetT *caseBox = wgtVBox(optRow); WidgetT *caseBox = wgtVBox(optRow);
@ -6444,6 +6492,18 @@ static void openProject(void) {
// openProjectPath -- load a .dbp (the previous project is already closed) // openProjectPath -- load a .dbp (the previous project is already closed)
static void openProjectPath(const char *path) { static void openProjectPath(const char *path) {
// prefsLoad treats a missing file as an empty INI, so prjLoad alone
// cannot reject a stale recent entry; probe the file first or a
// deleted .dbp silently "opens" as a phantom empty project.
FILE *probe = fopen(path, "rb");
if (!probe) {
dvxErrorBox(sAc, NULL, "Could not open project file.");
return;
}
fclose(probe);
if (!prjLoad(&sProject, path)) { if (!prjLoad(&sProject, path)) {
dvxErrorBox(sAc, NULL, "Could not open project file."); dvxErrorBox(sAc, NULL, "Could not open project file.");
return; return;
@ -7339,7 +7399,12 @@ static void runModule(BasModuleT *mod) {
if (result == BAS_VM_HALTED && (int32_t)arrlen(formRt->forms) > 0) { if (result == BAS_VM_HALTED && (int32_t)arrlen(formRt->forms) > 0) {
setStatus("Running (event loop)..."); setStatus("Running (event loop)...");
while (sWin && sAc->running && (int32_t)arrlen(formRt->forms) > 0 && !sStopRequested && !vm->ended) { // vm->errorMsg outliving an event dispatch means an unhandled
// runtime error escaped a handler (ON ERROR clears the message
// when it takes over); without this test the program kept
// running after the error and the report below never fired
// until the user pressed Stop.
while (sWin && sAc->running && (int32_t)arrlen(formRt->forms) > 0 && !sStopRequested && !vm->ended && vm->errorMsg[0] == '\0') {
dvxUpdate(sAc); dvxUpdate(sAc);
} }
} }
@ -8032,6 +8097,7 @@ static void showCompileError(const char *status) {
static void showImmediateWindow(void) { static void showImmediateWindow(void) {
if (sImmWin) { if (sImmWin) {
dvxShowWindow(sAc, sImmWin);
return; return;
} }
@ -8041,6 +8107,7 @@ static void showImmediateWindow(void) {
sImmWin = dvxCreateWindow(sAc, "Immediate", sAc->display.width / 2, outY, sAc->display.width / 2, outH, true); sImmWin = dvxCreateWindow(sAc, "Immediate", sAc->display.width / 2, outY, sAc->display.width / 2, outH, true);
if (sImmWin) { if (sImmWin) {
sImmWin->onClose = onImmWinClose;
sImmWin->onFocus = onContentFocus; sImmWin->onFocus = onContentFocus;
sImmWin->onMenu = onMenu; sImmWin->onMenu = onMenu;
sImmWin->accelTable = sWin ? sWin->accelTable : NULL; sImmWin->accelTable = sWin ? sWin->accelTable : NULL;
@ -8111,6 +8178,7 @@ static void showLocalsWindow(void) {
static void showOutputWindow(void) { static void showOutputWindow(void) {
if (sOutWin) { if (sOutWin) {
dvxShowWindow(sAc, sOutWin);
return; return;
} }
@ -8120,6 +8188,7 @@ static void showOutputWindow(void) {
sOutWin = dvxCreateWindow(sAc, "Output", 0, outY, sAc->display.width / 2, outH, true); sOutWin = dvxCreateWindow(sAc, "Output", 0, outY, sAc->display.width / 2, outH, true);
if (sOutWin) { if (sOutWin) {
sOutWin->onClose = onOutWinClose;
sOutWin->onFocus = onContentFocus; sOutWin->onFocus = onContentFocus;
sOutWin->onMenu = onMenu; sOutWin->onMenu = onMenu;
sOutWin->accelTable = sWin ? sWin->accelTable : NULL; sOutWin->accelTable = sWin ? sWin->accelTable : NULL;
@ -8370,15 +8439,18 @@ static void showProc(int32_t procIdx) {
// If a buffer was deleted (empty skeleton discard), adjust the // If a buffer was deleted (empty skeleton discard), adjust the
// target index since arrdel shifts everything after it. // target index since arrdel shifts everything after it.
if (sCurProcIdx >= -1) { if (sCurProcIdx >= -1) {
int32_t deletedIdx = sCurProcIdx; int32_t deletedIdx = sCurProcIdx;
bool listChanged = saveCurProc();
if (saveCurProc()) { // Every later procedure's line number depends on the buffer just
// The proc list changed; keep sProcTable in step with it // saved (lines added or removed above it shift them), so the proc
joinProcBufs(); // table is rebuilt after any save -- not only when the list
// changed -- or breakpoints toggled in the next proc land on the
// wrong compile line.
joinProcBufs();
if (deletedIdx >= 0 && procIdx > deletedIdx) { if (listChanged && deletedIdx >= 0 && procIdx > deletedIdx) {
procIdx--; procIdx--;
}
} }
} }
@ -9459,10 +9531,13 @@ static bool validatorIsMethodValid(void *ctx, const char *wgtType, const char *m
const WgtIfaceT *iface = wgtGetIface(wgtName); const WgtIfaceT *iface = wgtGetIface(wgtName);
if (!iface || !iface->methods) { if (!iface) {
return true; return true;
} }
// A known type with no interface methods accepts only the common
// set (already checked above); being permissive here let typos
// like btnGo.Boggle compile.
for (int32_t i = 0; i < iface->methodCount; i++) { for (int32_t i = 0; i < iface->methodCount; i++) {
if (strcasecmp(iface->methods[i].name, methodName) == 0) { if (strcasecmp(iface->methods[i].name, methodName) == 0) {
return true; return true;
@ -9509,10 +9584,12 @@ static bool validatorIsPropValid(void *ctx, const char *wgtType, const char *pro
const WgtIfaceT *iface = wgtGetIface(wgtName); const WgtIfaceT *iface = wgtGetIface(wgtName);
if (!iface || !iface->props) { if (!iface) {
return true; return true;
} }
// Same reasoning as validatorIsMethodValid: an empty interface
// property table must not turn validation off for the whole type.
return wgtIfaceFindProp(iface, propName) != NULL; return wgtIfaceFindProp(iface, propName) != NULL;
} }

View file

@ -107,15 +107,15 @@ typedef struct {
// Project management // Project management
// ============================================================ // ============================================================
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm);
void prjClose(PrjStateT *prj);
void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize);
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);
void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandleT *prefs);
void prjClose(PrjStateT *prj);
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm);
void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx); void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx);
void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandleT *prefs);
void prjRemoveFile(PrjStateT *prj, int32_t idx); void prjRemoveFile(PrjStateT *prj, int32_t idx);
void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize); bool prjSave(const PrjStateT *prj);
// ============================================================ // ============================================================
// Source map -- translate concatenated line to file + local line // Source map -- translate concatenated line to file + local line
@ -134,8 +134,8 @@ typedef void (*PrjSelChangeFnT)(void);
WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onClick, PrjSelChangeFnT onSelChange); WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onClick, PrjSelChangeFnT onSelChange);
void prjDestroyWindow(AppContextT *ctx, WindowT *win); void prjDestroyWindow(AppContextT *ctx, WindowT *win);
void prjRebuildTree(PrjStateT *prj);
int32_t prjGetSelectedFileIdx(void); int32_t prjGetSelectedFileIdx(void);
void prjRebuildTree(PrjStateT *prj);
// ============================================================ // ============================================================
// Project properties dialog // Project properties dialog

View file

@ -293,12 +293,12 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
} }
// Resolve SQL functions via dlsym // Resolve SQL functions via dlsym
SqlOpenFnT sqlOpen = (SqlOpenFnT)dlsym(NULL, "_dvxSqlOpen"); SqlOpenFnT sqlOpen = (SqlOpenFnT)dlsym(NULL, DVX_SYM("dvxSqlOpen"));
SqlCloseFnT sqlClose = (SqlCloseFnT)dlsym(NULL, "_dvxSqlClose"); SqlCloseFnT sqlClose = (SqlCloseFnT)dlsym(NULL, DVX_SYM("dvxSqlClose"));
SqlQueryFnT sqlQuery = (SqlQueryFnT)dlsym(NULL, "_dvxSqlQuery"); SqlQueryFnT sqlQuery = (SqlQueryFnT)dlsym(NULL, DVX_SYM("dvxSqlQuery"));
SqlFieldCountFnT sqlFieldCount = (SqlFieldCountFnT)dlsym(NULL, "_dvxSqlFieldCount"); SqlFieldCountFnT sqlFieldCount = (SqlFieldCountFnT)dlsym(NULL, DVX_SYM("dvxSqlFieldCount"));
SqlFieldNameFnT sqlFieldName = (SqlFieldNameFnT)dlsym(NULL, "_dvxSqlFieldName"); SqlFieldNameFnT sqlFieldName = (SqlFieldNameFnT)dlsym(NULL, DVX_SYM("dvxSqlFieldName"));
SqlFreeResultFnT sqlFreeResult = (SqlFreeResultFnT)dlsym(NULL, "_dvxSqlFreeResult"); SqlFreeResultFnT sqlFreeResult = (SqlFreeResultFnT)dlsym(NULL, DVX_SYM("dvxSqlFreeResult"));
if (!sqlOpen || !sqlClose || !sqlQuery || !sqlFieldCount || !sqlFieldName || !sqlFreeResult) { if (!sqlOpen || !sqlClose || !sqlQuery || !sqlFieldCount || !sqlFieldName || !sqlFreeResult) {
return 0; return 0;
@ -417,12 +417,12 @@ static int32_t getTableNames(const char *dbName, char (**outNames)[DSGN_MAX_NAME
return 0; return 0;
} }
SqlOpenFnT sqlOpen = (SqlOpenFnT)dlsym(NULL, "_dvxSqlOpen"); SqlOpenFnT sqlOpen = (SqlOpenFnT)dlsym(NULL, DVX_SYM("dvxSqlOpen"));
SqlCloseFnT sqlClose = (SqlCloseFnT)dlsym(NULL, "_dvxSqlClose"); SqlCloseFnT sqlClose = (SqlCloseFnT)dlsym(NULL, DVX_SYM("dvxSqlClose"));
SqlQueryFnT sqlQuery = (SqlQueryFnT)dlsym(NULL, "_dvxSqlQuery"); SqlQueryFnT sqlQuery = (SqlQueryFnT)dlsym(NULL, DVX_SYM("dvxSqlQuery"));
SqlNextFnT sqlNext = (SqlNextFnT)dlsym(NULL, "_dvxSqlNext"); SqlNextFnT sqlNext = (SqlNextFnT)dlsym(NULL, DVX_SYM("dvxSqlNext"));
SqlFieldTextFnT sqlFieldText = (SqlFieldTextFnT)dlsym(NULL, "_dvxSqlFieldText"); SqlFieldTextFnT sqlFieldText = (SqlFieldTextFnT)dlsym(NULL, DVX_SYM("dvxSqlFieldText"));
SqlFreeResultFnT sqlFreeResult = (SqlFreeResultFnT)dlsym(NULL, "_dvxSqlFreeResult"); SqlFreeResultFnT sqlFreeResult = (SqlFreeResultFnT)dlsym(NULL, DVX_SYM("dvxSqlFreeResult"));
if (!sqlOpen || !sqlClose || !sqlQuery || !sqlNext || !sqlFieldText || !sqlFreeResult) { if (!sqlOpen || !sqlClose || !sqlQuery || !sqlNext || !sqlFieldText || !sqlFreeResult) {
return 0; return 0;

View file

@ -100,13 +100,17 @@ When mixing types in expressions, values are automatically promoted to a common
.h2 Assignment Conversion .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. Storing a value into a variable declared as Integer, Long, Single or Boolean (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, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has.
.code .code
Dim n As Integer Dim n As Integer
n = 3.7 ' n is 4 n = 3.7 ' n is 4
n = 40000 ' Error 6: Overflow n = 40000 ' Error 6: Overflow
x = 3.7 ' x has no declared type and keeps 3.7 Dim b As Boolean
b = 3 ' b is True
Dim s
s = 1 / 3 ' s is Single: prints 0.3333333
x = 1 / 3 ' x was never declared and keeps the Double: prints 0.333333333333333
.endcode .endcode
.h2 Boolean Values .h2 Boolean Values
@ -275,11 +279,14 @@ DIM variable AS type
DIM variable(upperBound) AS type DIM variable(upperBound) AS type
DIM variable(lower TO upper) AS type DIM variable(lower TO upper) AS type
DIM variable(dim1, dim2, ...) AS type DIM variable(dim1, dim2, ...) AS type
DIM variable() AS type
DIM variable AS UdtName DIM variable AS UdtName
DIM variable AS STRING * n DIM variable AS STRING * n
DIM SHARED variable AS type DIM SHARED variable AS type
.endcode .endcode
An array declared with bounds is a fixed array. DIM variable() with empty parentheses declares a dynamic array that has no storage until a REDIM gives it bounds; using it before then raises error 13. An array that is first created by REDIM, and an array parameter, are dynamic too (see ERASE for the difference).
Multiple variables can be declared at module level so their values persist between procedure calls. Arrays can have up to 8 dimensions. A variable declared with type suffix does not need the AS clause: Multiple variables can be declared at module level so their values persist between procedure calls. Arrays can have up to 8 dimensions. A variable declared with type suffix does not need the AS clause:
.code .code
@ -306,7 +313,7 @@ Fixed-length strings (STRING * n) are padded with spaces and truncated when assi
.h2 REDIM .h2 REDIM
Reallocates a dynamic array, optionally preserving existing data. Reallocates an array, optionally preserving existing data. The array may have been declared with DIM name() or with bounds, or not declared at all (REDIM then declares a dynamic array).
.code .code
REDIM array(newBounds) AS type REDIM array(newBounds) AS type
@ -344,6 +351,7 @@ Defines a user-defined type (record/structure).
.code .code
TYPE TypeName TYPE TypeName
fieldName AS type fieldName AS type
fieldName AS STRING * n
... ...
END TYPE END TYPE
.endcode .endcode
@ -351,7 +359,7 @@ END TYPE
.code .code
Type PersonType Type PersonType
firstName As String firstName As String
lastName As String lastName As String * 20
age As Integer age As Integer
End Type End Type
@ -360,7 +368,7 @@ p.firstName = "Scott"
p.age = 30 p.age = 30
.endcode .endcode
UDT fields can themselves be UDTs (nested types). UDT fields can themselves be UDTs (nested types), and field access chains through array elements too: shapes(i).origin.x = 5. A new instance starts with numeric fields at 0, Boolean fields False, variable strings empty and STRING * n fields padded to n spaces. TYPE variables can be written to and read from RANDOM and BINARY files with PUT and GET (see GET / PUT).
.h2 DECLARE .h2 DECLARE
@ -451,14 +459,16 @@ The LET keyword is optional and supported for compatibility.
.h2 SWAP .h2 SWAP
Exchanges the values of two variables. The variables must be the same type. Exchanges the values of two variables or array elements. The two operands should be the same type.
.code .code
SWAP variable1, variable2 SWAP variable1, variable2
SWAP array(index), variable
.endcode .endcode
.code .code
Swap a, b Swap a, b
Swap names(i), names(j)
.endcode .endcode
.h2 SET .h2 SET
@ -479,7 +489,7 @@ For ordinary numeric or string assignment, SET is not used.
.h2 ERASE .h2 ERASE
Frees the memory of a dynamic array and resets it to undimensioned state. Fixed-size arrays (declared with constant bounds) reset their elements but keep their shape. Frees the memory of a dynamic array (declared with DIM name(), created by REDIM, or an array parameter) and resets it to the undimensioned state; using it again before a REDIM raises error 13. A fixed array (declared with bounds) keeps its bounds and has every element reset to zero, an empty string, False or a fresh TYPE instance.
.code .code
ERASE arrayName ERASE arrayName
@ -673,7 +683,8 @@ Wend
Defines a subroutine (no return value). Defines a subroutine (no return value).
.code .code
SUB name ([BYVAL] [OPTIONAL] param AS type, ...) SUB name ([BYVAL | BYREF] [OPTIONAL] param AS type, ...)
SUB name (arrayParam() AS type, ...)
statements statements
END SUB END SUB
.endcode .endcode
@ -684,7 +695,7 @@ Sub Greet(ByVal name As String)
End Sub End Sub
.endcode .endcode
Parameters are passed by reference by default. Use ByVal for value semantics; there is no separate ByRef keyword (omitting ByVal is the by-reference form). Writes to a by-reference parameter update the caller's variable, including individual array elements: `bump a(3)` passed to a by-reference parameter will modify `a(3)` in place. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional: Parameters are passed by reference by default; BYREF may be written out to say so, and BYVAL selects value semantics. Writes to a by-reference parameter update the caller's variable, including individual array elements: `bump a(3)` passed to a by-reference parameter will modify `a(3)` in place. A parameter written as name() receives a whole array by reference; the caller passes `arr()` (or just `arr`), and the SUB may index, REDIM or ERASE it. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:
.code .code
Greet "World" Greet "World"
@ -692,6 +703,18 @@ Greet("World")
Call Greet("World") Call Greet("World")
.endcode .endcode
.code
Sub Fill(v() As Integer, ByRef count As Integer)
ReDim v(9) As Integer
v(9) = 7
count = count + 1
End Sub
Dim a(3) As Integer
Dim n As Integer
Fill a(), n
.endcode
.h3 Optional Parameters .h3 Optional Parameters
Mark a parameter OPTIONAL to allow callers to omit it. An optional parameter must be positioned after all required parameters and receives an empty/zero default when not supplied. Mark a parameter OPTIONAL to allow callers to omit it. An optional parameter must be positioned after all required parameters and receives an empty/zero default when not supplied.
@ -846,7 +869,7 @@ Special functions inside PRINT:
.list .list
.item SPC(n) -- print n spaces .item SPC(n) -- print n spaces
.item TAB(n) -- advance to column n (first column is 1) .item TAB(n) -- advance to column n (first column is 1); when the cursor is already past column n, a new line is started first
.endlist .endlist
.code .code
@ -857,12 +880,13 @@ Print #1, "Written to file"
.h3 PRINT USING .h3 PRINT USING
PRINT USING formats each following expression according to a format string and prints the result. If more expressions are supplied than the format string consumes, the format string is reused from the start for each. PRINT USING formats each following expression according to a format string and prints the result. The format string is a mix of fields and literal text: each expression consumes the next field, the literal text before it is printed as it is, and the literal text after the last field is printed at the end. If more expressions are supplied than there are fields, the format string is reused from the start.
.table .table
Format character Meaning Format character Meaning
---------------- ------- ---------------- -------
# Digit (replaced with a digit or space to pad on the left) # Digit position; unused positions on the left become spaces
0 Digit position that is always printed (zero padded); the first 0 sets the minimum number of digits
. Decimal-point position . Decimal-point position
, Insert thousands separator , Insert thousands separator
+ At start or end: always show a sign character + At start or end: always show a sign character
@ -873,15 +897,22 @@ PRINT USING formats each following expression according to a format string and p
! First character of a string only ! First character of a string only
& Entire string (variable length) & Entire string (variable length)
\ ... \ Fixed-length string field (width = 2 + number of spaces between backslashes) \ ... \ Fixed-length string field (width = 2 + number of spaces between backslashes)
_ The next character is printed literally
.endtable .endtable
A numeric field is as wide as its format characters; the value is right-aligned in it and a negative sign (or a leading +) floats directly in front of the first digit. A value that does not fit is printed in full with a leading %. A string expression must meet a string field and a numeric expression a numeric field, otherwise error 13 (Type mismatch) is raised.
.code .code
Print Using "###.##"; 3.14159 ' " 3.14" Print Using "###.##"; -3.14159 ' " -3.14"
Print Using "$$#,##0.00"; 1234567.89 ' "$1,234,567.89" Print Using "+###"; 42 ' " +42"
Print Using "$$#,##0.00"; 1234.5 ' " $1,234.50"
Print Using "**#,##0.00"; 42.5 ' "*****42.50" Print Using "**#,##0.00"; 42.5 ' "*****42.50"
Print Using "###"; 12345 ' "%12345"
Print Using "####.####^^^^"; 0.000123 ' scientific notation Print Using "####.####^^^^"; 0.000123 ' scientific notation
Print Using "\ \"; "Hello" ' fixed-width 4-char: "Hell" Print Using "\ \"; "Hello" ' fixed-width 4-char: "Hell"
Print Using "& likes &"; name$; food$ Print Using "& likes &"; name$; food$ ' "Scott likes pizza"
Print Using "Total: ###.## units"; 7.5 ' "Total: 7.50 units"
Print Using "## and ##"; 1; 2; 3 ' " 1 and 2 3 and "
.endcode .endcode
.h2 INPUT .h2 INPUT
@ -926,6 +957,7 @@ Restore
.index RESUME NEXT .index RESUME NEXT
.index ERROR .index ERROR
.index ERR .index ERR
.index ERL
.index SHELL .index SHELL
.index SLEEP .index SLEEP
.index RANDOMIZE .index RANDOMIZE
@ -944,7 +976,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). RESUME or RESUME NEXT executed while no error is active raises error 20 (RESUME without error). The ERR keyword returns the current error number in expressions and ERL the source line the error was raised on (both are 0 when no error is active, and both reset after RESUME). 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,7 +984,7 @@ Open "missing.txt" For Input As #1
Exit Sub Exit Sub
ErrorHandler: ErrorHandler:
Print "Error number:"; Err Print "Error number:"; Err; "on line"; Erl
Resume Next Resume Next
.endcode .endcode
@ -973,13 +1005,14 @@ ErrorHandler:
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 (OPEN for INPUT, KILL, FILELEN, ...)
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) 59 Bad record length (OPEN ... LEN must be 1 to 32767; GET/PUT data longer than LEN)
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
1000 No form runtime (a form or control statement ran without the form runtime, e.g. under a headless runner)
.endtable .endtable
.h2 SHELL .h2 SHELL
@ -1124,10 +1157,28 @@ 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 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 and Boolean 2 bytes, Long 4, Single 4, Double 8); a String is stored as a 2-byte length followed by its characters. PUT always writes a whole record, padding the unused part with zero bytes, and GET always advances to the next record; data longer than LEN raises error 59.
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). 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).
The variable may be a TYPE variable. Its fields are packed in declaration order with the sizes above; a STRING * n field takes exactly n bytes (padded with spaces), a variable-length String field is stored with its 2-byte length prefix in both modes, and nested TYPE fields are packed inline. GET fills the fields of the existing variable in place.
The variable may also be an array element, arr(i), of any type including a TYPE: PUT writes that element and GET reads into it. The index expression of a GET target is evaluated twice (once to fetch the element, once to store it back), so keep it free of side effects.
.code
Type RecT
id As Integer
score As Double
nm As String * 6
End Type
Dim r As RecT
Open "recs.dat" For Random As #1 Len = 32
r.id = 1
Put #1, 1, r ' 16 bytes of fields, padded to a 32-byte record
Get #1, 1, r
Close #1
.endcode
.code .code
Dim buf As String Dim buf As String
Open "raw.bin" For Binary As #1 Open "raw.bin" For Binary As #1
@ -1271,7 +1322,7 @@ WEND
.h2 FILELEN .h2 FILELEN
Returns the length of a file in bytes. Returns the length of a file in bytes. A file that does not exist raises error 53 (File not found).
.code .code
bytes = FILELEN(filename$) bytes = FILELEN(filename$)
@ -1341,12 +1392,14 @@ s$ = FORMAT$(value, fmt$)
.code .code
Print Format$(3.14159, "###.##") ' " 3.14" Print Format$(3.14159, "###.##") ' " 3.14"
Print Format$(-7, "+###") ' " -7"
Print Format$(42, "00000") ' "00042" Print Format$(42, "00000") ' "00042"
Print Format$(1234.5, "#,##0.00") ' "1,234.50" Print Format$(1234.5, "#,##0.00") ' "1,234.50"
Print Format$(5, "#,##0.00") ' " 5.00"
Print Format$(0.5, "PERCENT") ' "50%" Print Format$(0.5, "PERCENT") ' "50%"
.endcode .endcode
The accepted format characters are # (digit or pad space), 0 (digit or pad zero), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), and the literal word PERCENT (multiplies by 100 and appends %). See PRINT USING for details on each. The whole format string is one numeric field. The accepted format characters are # (digit or pad space), 0 (digit that is always printed; zero pads up to the first 0), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), ^^^^ (scientific notation) and the literal word PERCENT (multiplies by 100 and appends %). Unlike PRINT USING, a value wider than the field is printed in full without a % marker. See PRINT USING for details on each character.
.h2 MID$ Assignment .h2 MID$ Assignment
@ -1468,10 +1521,10 @@ Me.BackColor = RGB(0, 0, 128) ' dark blue background
EOF(channel) Boolean True if the file pointer is at end of file EOF(channel) Boolean True if the file pointer is at end of file
FREEFILE Integer Next available file channel number (1..16) FREEFILE Integer Next available file channel number (1..16)
INPUT$(n, #channel) String Reads exactly n characters from the file INPUT$(n, #channel) String Reads exactly n characters from the file
LOC(channel) Long Current read/write position in the file LOC(channel) Long BINARY: position of the last byte read or written (0 at the start); RANDOM: number of the last record read or written; sequential: number of 128-byte blocks read or written
LOF(channel) Long Length of the file in bytes LOF(channel) Long Length of the file in bytes
SEEK(channel) Long Current file position (function form) SEEK(channel) Long 1-based position of the next byte to be read or written
FILELEN(path$) Long Length of the named file in bytes (no OPEN needed) FILELEN(path$) Long Length of the named file in bytes (no OPEN needed; error 53 when missing)
GETATTR(path$) Integer File attribute bits (see vbReadOnly, vbHidden, etc.) GETATTR(path$) Integer File attribute bits (see vbReadOnly, vbHidden, etc.)
CURDIR$ String Current working directory CURDIR$ String Current working directory
DIR$(pattern$) String First filename matching pattern, or "" DIR$(pattern$) String First filename matching pattern, or ""
@ -1533,9 +1586,10 @@ DVX BASIC supports Visual Basic-style forms and controls for building graphical
.code .code
LOAD FormName LOAD FormName
UNLOAD FormName UNLOAD FormName
UNLOAD Me
.endcode .endcode
LOAD creates the form and its controls in memory. It fires Form_Load when the form is first loaded. UNLOAD destroys the form, firing Form_QueryUnload (which can cancel the close) and then Form_Unload. The form name here is the literal name of the form as it appears in its .frm file. LOAD creates the form and its controls in memory. It fires Form_Load when the form is first loaded. UNLOAD destroys the form, firing Form_QueryUnload (which can cancel the close) and then Form_Unload. The form name here is the literal name of the form as it appears in its .frm file; UNLOAD Me unloads the form whose event handler is running.
.h2 Showing and Hiding Forms .h2 Showing and Hiding Forms
@ -1568,6 +1622,14 @@ Label1.Caption = "Name: " & name$
x = Text1.Left x = Text1.Left
.endcode .endcode
A control on another form is reached through that form's name, or through a variable holding a form reference: FormName.ControlName.Property works in both assignments and expressions, and so does FormName.ControlName.Method. Me.ControlName.Property does the same for the current form.
.code
Form2.Label1.Caption = "From Form1"
total = Form2.List1.ListCount
Form2.List1.Clear
.endcode
.h2 Method Calls .h2 Method Calls
.code .code

View file

@ -55,5 +55,6 @@
#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
#define BAS_ERR_EXTERNAL_NOT_FOUND 453 #define BAS_ERR_EXTERNAL_NOT_FOUND 453
#define BAS_ERR_NO_FORM_RUNTIME 1000 // form/control statement with no form runtime attached (headless run)
#endif #endif

View file

@ -61,6 +61,7 @@ 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);
double basParseNumber(const char *s);
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);
@ -85,7 +86,6 @@ 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);
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); bool basValRoundToInt32(BasValueT v, int32_t lo, int32_t hi, int32_t *out);
BasValueT basValSingle(float v); BasValueT basValSingle(float v);
@ -228,6 +228,81 @@ void basArrayUnref(BasArrayT *arr) {
} }
// ============================================================
// 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);
}
BasStringT *basStringAlloc(int32_t cap) { BasStringT *basStringAlloc(int32_t cap) {
if (cap < 1) { if (cap < 1) {
cap = 1; cap = 1;
@ -460,81 +535,6 @@ void basUdtUnref(BasUdtT *udt) {
} }
// ============================================================
// Value constructors / refcount helpers
// ============================================================
double basParseNumber(const char *s) {
if (!s) {
return 0.0;
}
while (*s == ' ' || *s == '\t') {
s++;
}
// &H / &O prefixed integers (VAL("&HFF") = 255).
if (s[0] == '&' && (s[1] == 'H' || s[1] == 'h' || s[1] == 'O' || s[1] == 'o')) {
int32_t radix = (s[1] == 'H' || s[1] == 'h') ? BAS_RADIX_HEX : BAS_RADIX_OCT;
return (double)(int32_t)strtoul(s + 2, NULL, radix);
}
// Decimal: hand-scan the accepted span so atof's extra syntax (hex
// floats, inf, nan) is never reached, and 'D' exponents are accepted.
char buf[BAS_NUM_FORMAT_BUF_LEN];
int32_t len = 0;
const char *p = s;
if (*p == '+' || *p == '-') {
buf[len++] = *p++;
}
bool sawDigit = false;
while (isdigit((unsigned char)*p) && len < BAS_NUM_FORMAT_BUF_LEN - 1) {
buf[len++] = *p++;
sawDigit = true;
}
if (*p == '.' && len < BAS_NUM_FORMAT_BUF_LEN - 1) {
buf[len++] = *p++;
while (isdigit((unsigned char)*p) && len < BAS_NUM_FORMAT_BUF_LEN - 1) {
buf[len++] = *p++;
sawDigit = true;
}
}
if (!sawDigit) {
return 0.0;
}
if ((*p == 'E' || *p == 'e' || *p == 'D' || *p == 'd') && len < BAS_NUM_FORMAT_BUF_LEN - 3) {
const char *q = p + 1;
char expBuf[BAS_NUM_FORMAT_BUF_LEN];
int32_t expLen = 0;
if (*q == '+' || *q == '-') {
expBuf[expLen++] = *q++;
}
if (isdigit((unsigned char)*q)) {
while (isdigit((unsigned char)*q) && expLen < BAS_NUM_FORMAT_BUF_LEN - 1) {
expBuf[expLen++] = *q++;
}
if (len + 1 + expLen < BAS_NUM_FORMAT_BUF_LEN) {
buf[len++] = 'e';
memcpy(buf + len, expBuf, expLen);
len += expLen;
}
}
}
buf[len] = '\0';
return atof(buf);
}
BasValueT basValBool(bool v) { BasValueT basValBool(bool v) {
BasValueT val; BasValueT val;
val.type = BAS_TYPE_BOOLEAN; val.type = BAS_TYPE_BOOLEAN;
@ -773,6 +773,29 @@ int32_t basValToInt32(BasValueT v) {
} }
BasValueT basValToInteger(BasValueT v) {
int32_t n = 0;
if (!basValRoundToInt32(v, INT16_MIN, INT16_MAX, &n)) {
double d = basValToNumber(v);
n = isnan(d) ? 0 : ((d < 0) ? INT16_MIN : INT16_MAX);
}
return basValInteger((int16_t)n);
}
BasValueT basValToLong(BasValueT v) {
int32_t n = 0;
if (!basValRoundToInt32(v, INT32_MIN, INT32_MAX, &n)) {
n = basValToInt32(v);
}
return basValLong(n);
}
double basValToNumber(BasValueT v) { double basValToNumber(BasValueT v) {
switch (v.type) { switch (v.type) {
case BAS_TYPE_INTEGER: case BAS_TYPE_INTEGER:
@ -803,6 +826,11 @@ 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

View file

@ -167,12 +167,12 @@ struct BasValueTag {
// Create values. Out-of-line so they can be resolved via the DXE // Create values. Out-of-line so they can be resolved via the DXE
// dynamic-symbol table when basrt.lib is loaded. // dynamic-symbol table when basrt.lib is loaded.
BasValueT basValBool(bool v);
BasValueT basValDouble(double v);
BasValueT basValInteger(int16_t v); BasValueT basValInteger(int16_t v);
BasValueT basValLong(int32_t v); BasValueT basValLong(int32_t v);
BasValueT basValSingle(float v);
BasValueT basValDouble(double v);
BasValueT basValBool(bool v);
BasValueT basValObject(void *obj); BasValueT basValObject(void *obj);
BasValueT basValSingle(float v);
BasValueT basValString(BasStringT *s); BasValueT basValString(BasStringT *s);
BasValueT basValStringFromC(const char *text); BasValueT basValStringFromC(const char *text);
@ -184,12 +184,12 @@ void basValRelease(BasValueT *v);
// Convert a value to a specific type. Returns the converted value. // Convert a value to a specific type. Returns the converted value.
// The original is NOT released -- caller manages lifetime. // The original is NOT released -- caller manages lifetime.
BasValueT basValToBool(BasValueT v);
BasValueT basValToDouble(BasValueT v);
BasValueT basValToInteger(BasValueT v); BasValueT basValToInteger(BasValueT v);
BasValueT basValToLong(BasValueT v); BasValueT basValToLong(BasValueT v);
BasValueT basValToSingle(BasValueT v); BasValueT basValToSingle(BasValueT v);
BasValueT basValToDouble(BasValueT v);
BasValueT basValToString(BasValueT v); BasValueT basValToString(BasValueT v);
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);

File diff suppressed because it is too large Load diff

View file

@ -441,6 +441,11 @@ typedef struct {
// String comparison mode // String comparison mode
bool compareTextMode; // true = case-insensitive comparisons bool compareTextMode; // true = case-insensitive comparisons
// PRINT state: output column (0-based, for comma zones and TAB) and
// the scan position inside the active PRINT USING format string.
int32_t printColumn;
int32_t usingPos;
// Error handling. The active ON ERROR handler lives on the call frame // Error handling. The active ON ERROR handler lives on the call frame
// that installed it (BasCallFrameT.errorHandler); the dispatcher walks // that installed it (BasCallFrameT.errorHandler); the dispatcher walks
// frames to find it. // frames to find it.
@ -513,13 +518,13 @@ BasVmResultE basVmRun(BasVmT *vm);
BasVmResultE basVmStep(BasVmT *vm); BasVmResultE basVmStep(BasVmT *vm);
// Set I/O callbacks. // Set I/O callbacks.
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx);
void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx); void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx);
void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx);
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
// Set UI callbacks (for form/control system). // Set UI callbacks (for form/control system).
void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui);
void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql); void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql);
void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui);
// Set external library callbacks (for DECLARE LIBRARY support). // Set external library callbacks (for DECLARE LIBRARY support).
void basVmSetExternCallbacks(BasVmT *vm, const BasExternCallbacksT *ext); void basVmSetExternCallbacks(BasVmT *vm, const BasExternCallbacksT *ext);

View file

@ -48,18 +48,12 @@
#include <strings.h> #include <strings.h>
#include <unistd.h> #include <unistd.h>
// Initial capacity of the source-concatenation buffer, and the hard
// ceiling its capacity may reach. The ceiling keeps the int32 capacity
// doubling in concatGrow from overflowing.
#define CONCAT_INITIAL_CAP 8192
#define CONCAT_MAX_CAP 0x40000000 // 1 GiB cap; keeps int32 doubling safe
// Kernel view of the running image on hosted OSes. // Kernel view of the running image on hosted OSes.
#define SELF_EXE_PATH "/proc/self/exe" #define SELF_EXE_PATH "/proc/self/exe"
// Function prototypes (alphabetical) // Function prototypes (alphabetical)
static void buildLog(const char *msg); static void buildLog(const char *msg);
static bool concatGrow(char **buf, int32_t *cap, int32_t need);
static const char *selfPath(const char *argv0); static const char *selfPath(const char *argv0);
static void usage(void); static void usage(void);
int main(int argc, char **argv); int main(int argc, char **argv);
@ -70,35 +64,6 @@ static void buildLog(const char *msg) {
} }
static bool concatGrow(char **buf, int32_t *cap, int32_t need) {
if (need < 0 || need > CONCAT_MAX_CAP) {
return false;
}
int32_t newCap = *cap;
while (newCap < need) {
if (newCap > CONCAT_MAX_CAP / 2) {
// Doubling would overflow int32; jump straight to need.
newCap = need;
break;
}
newCap *= 2;
}
char *grown = (char *)realloc(*buf, (size_t)newCap);
if (grown == NULL) {
return false;
}
*buf = grown;
*cap = newCap;
return true;
}
// Path of this executable, which carries the embedded STUB and NOICON // Path of this executable, which carries the embedded STUB and NOICON
// resources. On a hosted OS argv[0] is whatever the shell typed (a bare // resources. On a hosted OS argv[0] is whatever the shell typed (a bare
// name when invoked through PATH), so the kernel's view of the running // name when invoked through PATH), so the kernel's view of the running
@ -153,7 +118,18 @@ int main(int argc, char **argv) {
return 1; return 1;
} }
// Load the project file // Load the project file. prefsLoad treats a missing file as an
// empty INI, so probe it first or a bad path would surface as a
// confusing "no modules" build error.
FILE *dbpProbe = fopen(dbpPath, "rb");
if (!dbpProbe) {
fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath);
return 1;
}
fclose(dbpProbe);
PrefsHandleT *prefs = prefsLoad(dbpPath); PrefsHandleT *prefs = prefsLoad(dbpPath);
if (!prefs) { if (!prefs) {
@ -182,7 +158,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, "");
bool optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
// Derive output path // Derive output path
char outBuf[DVX_MAX_PATH]; char outBuf[DVX_MAX_PATH];
@ -195,135 +170,17 @@ int main(int argc, char **argv) {
printf("Project: %s\n", projName); printf("Project: %s\n", projName);
printf("Output: %s (%s)\n", outputPath, release ? "release" : "debug"); printf("Output: %s (%s)\n", outputPath, release ? "release" : "debug");
// Collect source files (dynamic array; grows as needed) // Concatenate the project's sources (modules first, then form code)
typedef struct { BasProjectSourceT project;
char path[DVX_MAX_PATH];
bool isForm;
} SrcFileT;
SrcFileT *files = NULL; if (!basBuildConcatProject(prefs, projectDir, &project)) {
fprintf(stderr, "Error: %s\n", project.error);
// Modules basBuildFreeProject(&project);
for (int32_t i = 0; ; i++) { prefsClose(prefs);
char key[16]; return 1;
snprintf(key, sizeof(key), "File%d", (int)i);
const char *val = prefsGetString(prefs, BAS_INI_SECTION_MODULES, key, NULL);
if (!val) {
break;
}
SrcFileT entry;
snprintf(entry.path, DVX_MAX_PATH, "%s/%s", projectDir, val);
entry.isForm = false;
arrput(files, entry);
} }
// Forms int32_t fileCount = project.fileCount;
for (int32_t i = 0; ; i++) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)i);
const char *val = prefsGetString(prefs, BAS_INI_SECTION_FORMS, key, NULL);
if (!val) {
break;
}
SrcFileT entry;
snprintf(entry.path, DVX_MAX_PATH, "%s/%s", projectDir, val);
entry.isForm = true;
arrput(files, entry);
}
int32_t fileCount = (int32_t)arrlen(files);
if (fileCount == 0) {
fprintf(stderr, "Error: project has no source files.\n");
goto failFiles;
}
// Concatenate sources (modules first, then form code)
int32_t concatCap = CONCAT_INITIAL_CAP;
char *concatBuf = (char *)malloc(concatCap);
if (!concatBuf) {
fprintf(stderr, "Error: out of memory.\n");
goto failFiles;
}
int32_t pos = 0;
// Pass 0: .bas modules, Pass 1: .frm code sections
for (int32_t pass = 0; pass < 2; pass++) {
for (int32_t i = 0; i < fileCount; i++) {
if (files[i].isForm != (pass == 1)) {
continue;
}
int32_t srcLen = 0;
char *srcBuf = platformReadFile(files[i].path, &srcLen);
if (!srcBuf) {
fprintf(stderr, "Error: cannot read %s\n", files[i].path);
goto concatFail;
}
const char *code = srcBuf;
if (files[i].isForm) {
// Extract form name from "Begin Form <name>"
char formName[BAS_MAX_IDENT] = "";
basExtractFormName(srcBuf, formName, BAS_MAX_IDENT);
// The BASIC code section follows the outer Begin Form block.
code = srcBuf + basFindFormEndPos(srcBuf, srcLen);
int32_t codeLen = (int32_t)strlen(code);
if (!concatGrow(&concatBuf, &concatCap, pos + codeLen + 128)) {
fprintf(stderr, "Error: out of memory.\n");
free(srcBuf);
goto concatFail;
}
// Inject BEGINFORM directive before form code
if (formName[0]) {
pos += snprintf(concatBuf + pos, concatCap - pos, "BEGINFORM \"%s\"\n", formName);
}
memcpy(concatBuf + pos, code, codeLen);
pos += codeLen;
if (pos > 0 && concatBuf[pos - 1] != '\n') {
concatBuf[pos++] = '\n';
}
// Inject ENDFORM directive after form code
if (formName[0]) {
pos += snprintf(concatBuf + pos, concatCap - pos, "ENDFORM\n");
}
} else {
int32_t codeLen = (int32_t)strlen(code);
if (!concatGrow(&concatBuf, &concatCap, pos + codeLen + 2)) {
fprintf(stderr, "Error: out of memory.\n");
free(srcBuf);
goto concatFail;
}
memcpy(concatBuf + pos, code, codeLen);
pos += codeLen;
if (pos > 0 && concatBuf[pos - 1] != '\n') {
concatBuf[pos++] = '\n';
}
}
free(srcBuf);
}
}
concatBuf[pos] = '\0';
// Compile // Compile
printf("Compiling %d file(s)...\n", (int)fileCount); printf("Compiling %d file(s)...\n", (int)fileCount);
@ -332,28 +189,26 @@ int main(int argc, char **argv) {
if (!parser) { if (!parser) {
fprintf(stderr, "Error: out of memory.\n"); fprintf(stderr, "Error: out of memory.\n");
goto concatFail; goto failProject;
} }
basParserInit(parser, concatBuf, pos); basParserInit(parser, project.source, project.sourceLen);
parser->optionExplicit = optionExplicit; parser->optionExplicit = project.optionExplicit;
if (!basParse(parser)) { if (!basParse(parser)) {
fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error); fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error);
basParserFree(parser); basParserFree(parser);
free(parser); free(parser);
goto concatFail; goto failProject;
} }
free(concatBuf);
BasModuleT *mod = basParserBuildModule(parser); BasModuleT *mod = basParserBuildModule(parser);
basParserFree(parser); basParserFree(parser);
free(parser); free(parser);
if (!mod) { if (!mod) {
fprintf(stderr, "Error: failed to build module.\n"); fprintf(stderr, "Error: failed to build module.\n");
goto failFiles; goto failProject;
} }
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);
@ -362,12 +217,8 @@ int main(int argc, char **argv) {
// release builds, obfuscates them. // release builds, obfuscates them.
char **frmSources = NULL; // stb_ds: owned form text char **frmSources = NULL; // stb_ds: owned form text
for (int32_t i = 0; i < fileCount; i++) { for (int32_t i = 0; i < (int32_t)arrlen(project.frmPaths); i++) {
if (!files[i].isForm) { char *fdata = platformReadFile(project.frmPaths[i], NULL);
continue;
}
char *fdata = platformReadFile(files[i].path, NULL);
if (fdata) { if (fdata) {
arrput(frmSources, fdata); arrput(frmSources, fdata);
@ -400,7 +251,7 @@ int main(int argc, char **argv) {
arrfree(frmSources); arrfree(frmSources);
basModuleFree(mod); basModuleFree(mod);
arrfree(files); basBuildFreeProject(&project);
prefsClose(prefs); prefsClose(prefs);
if (failure) { if (failure) {
@ -427,18 +278,10 @@ int main(int argc, char **argv) {
printf("Created %s (%d bytes)\n", outputPath, (int)outBytes); printf("Created %s (%d bytes)\n", outputPath, (int)outBytes);
return 0; return 0;
// ----- Error cleanup paths ----- // ----- Error cleanup path -----
// failFiles: only the files array exists. Each early error frees its // The caller has already printed an appropriate error message.
// own raw malloc'd buffers (concatBuf, parser) before jumping here. failProject:
failFiles: basBuildFreeProject(&project);
arrfree(files);
prefsClose(prefs); prefsClose(prefs);
return 1; return 1;
// The concat buffer is owned by paths that run before the frm arrays
// exist; free it here and route to the files-only cleanup. The caller
// has already printed an appropriate error message.
concatFail:
free(concatBuf);
goto failFiles;
} }

View file

@ -0,0 +1,400 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// basrun.c -- DVX BASIC headless host runner
//
// Runs a BASIC program without the DVX shell or form runtime so the
// compiler and VM can be exercised end-to-end from a test script.
//
// basrun [options] program.app | program.bas | project.dbp
//
// A .app is opened through dvxResOpen and its MODULE (and optional
// DEBUG) resource is deserialized exactly as basstub.c does. A .bas
// or .dbp is compiled on the fly (a .dbp is concatenated by the shared
// basBuildConcatProject: modules first, then the code section of each
// form wrapped in BEGINFORM/ENDFORM).
//
// PRINT goes to stdout, INPUT reads a line from stdin. No UI, SQL or
// extern callbacks are installed, so form/control opcodes are no-ops
// and DECLARE LIBRARY calls raise error 453.
//
// Options:
// --steps N abort after N bytecode steps (0 = unlimited)
// --data DIR directory for App.Config / App.Data (default: program dir)
// --compact .bas/.dbp only: strip + compact the module before running
// --quiet do not report runtime errors on stderr
//
// Exit code: 0 on normal completion, the BASIC error number on a
// runtime error (capped at BASRUN_EXIT_ERR_MAX), BASRUN_EXIT_STEPS when
// the step cap is hit, BASRUN_EXIT_USAGE for bad arguments or a program
// that could not be loaded or compiled.
#include "../compiler/compact.h"
#include "../compiler/parser.h"
#include "../compiler/strip.h"
#include "../runtime/vm.h"
#include "../runtime/serialize.h"
#include "../basBuild.h"
#include "../basRes.h"
#include "dvxPrefs.h"
#include "dvxRes.h"
#include "dvxTypes.h"
#include "dvxPlat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#define BASRUN_EXIT_OK 0
#define BASRUN_EXIT_ERR_MAX 250 // runtime error numbers above this are clamped
#define BASRUN_EXIT_STEPS 253 // --steps cap reached
#define BASRUN_EXIT_USAGE 255 // bad arguments, unreadable or uncompilable program
#define BASRUN_STEP_SLICE BAS_VM_DEFAULT_STEP_SLICE
#define BASRUN_INPUT_LINE_LEN 1024
typedef struct {
int32_t steps;
bool compact;
bool quiet;
const char *dataDir;
const char *program;
} OptionsT;
// Function prototypes (alphabetical; main last)
static BasModuleT *compileSource(const char *source, int32_t len, bool optionExplicit, const char *label);
static bool hostDoEvents(void *ctx);
static bool hostInput(void *ctx, const char *prompt, char *buf, int32_t bufSize);
static void hostPrint(void *ctx, const char *text, bool newline);
static BasModuleT *loadApp(const char *path);
static BasModuleT *loadBas(const char *path);
static BasModuleT *loadDbp(const char *path);
static bool parseArgs(int argc, char **argv, OptionsT *opt);
static void programDir(const char *path, char *out, int32_t outSize);
static int32_t runModule(BasModuleT *mod, const OptionsT *opt);
static void usage(void);
int main(int argc, char **argv);
static BasModuleT *compileSource(const char *source, int32_t len, bool optionExplicit, const char *label) {
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
if (!parser) {
fprintf(stderr, "basrun: out of memory\n");
return NULL;
}
basParserInit(parser, source, len);
parser->optionExplicit = optionExplicit;
if (!basParse(parser)) {
fprintf(stderr, "basrun: %s: compile error at line %d: %s\n", label, (int)parser->errorLine, parser->error);
basParserFree(parser);
free(parser);
return NULL;
}
BasModuleT *mod = basParserBuildModule(parser);
basParserFree(parser);
free(parser);
if (!mod) {
fprintf(stderr, "basrun: %s: failed to build module\n", label);
}
return mod;
}
static bool hostDoEvents(void *ctx) {
(void)ctx;
return true;
}
static bool hostInput(void *ctx, const char *prompt, char *buf, int32_t bufSize) {
(void)ctx;
(void)prompt;
fflush(stdout);
if (!fgets(buf, bufSize, stdin)) {
buf[0] = '\0';
return false;
}
// Drop the line terminator; platformStripLineEndings only removes CRs.
int32_t len = (int32_t)strlen(buf);
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
len--;
}
buf[len] = '\0';
return true;
}
static void hostPrint(void *ctx, const char *text, bool newline) {
(void)ctx;
fputs(text, stdout);
if (newline) {
fputc('\n', stdout);
}
}
static BasModuleT *loadApp(const char *path) {
DvxResHandleT *res = dvxResOpen(path);
if (!res) {
fprintf(stderr, "basrun: %s: no resource directory\n", path);
return NULL;
}
uint32_t modSize = 0;
uint8_t *modData = (uint8_t *)dvxResRead(res, BAS_RES_MODULE, &modSize);
if (!modData) {
fprintf(stderr, "basrun: %s: MODULE resource not found\n", path);
dvxResClose(res);
return NULL;
}
BasModuleT *mod = basModuleDeserialize(modData, (int32_t)modSize);
free(modData);
if (!mod) {
fprintf(stderr, "basrun: %s: failed to deserialize module\n", path);
dvxResClose(res);
return NULL;
}
uint32_t dbgSize = 0;
uint8_t *dbgData = (uint8_t *)dvxResRead(res, BAS_RES_DEBUG, &dbgSize);
if (dbgData) {
basDebugDeserialize(mod, dbgData, (int32_t)dbgSize);
free(dbgData);
}
dvxResClose(res);
return mod;
}
static BasModuleT *loadBas(const char *path) {
int32_t len = 0;
char *src = platformReadFile(path, &len);
if (!src) {
fprintf(stderr, "basrun: cannot read %s\n", path);
return NULL;
}
BasModuleT *mod = compileSource(src, len, false, path);
free(src);
return mod;
}
static BasModuleT *loadDbp(const char *path) {
// prefsLoad opens a missing file as an empty INI; probe first so a
// bad path is reported as such.
FILE *probe = fopen(path, "rb");
if (!probe) {
fprintf(stderr, "basrun: cannot open project %s\n", path);
return NULL;
}
fclose(probe);
PrefsHandleT *prefs = prefsLoad(path);
if (!prefs) {
fprintf(stderr, "basrun: cannot open project %s\n", path);
return NULL;
}
char projectDir[DVX_MAX_PATH];
programDir(path, projectDir, sizeof(projectDir));
BasProjectSourceT project;
BasModuleT *mod = NULL;
if (basBuildConcatProject(prefs, projectDir, &project)) {
mod = compileSource(project.source, project.sourceLen, project.optionExplicit, path);
} else {
fprintf(stderr, "basrun: %s: %s\n", path, project.error);
}
basBuildFreeProject(&project);
prefsClose(prefs);
return mod;
}
static bool parseArgs(int argc, char **argv, OptionsT *opt) {
memset(opt, 0, sizeof(*opt));
for (int32_t i = 1; i < argc; i++) {
if (strcmp(argv[i], "--steps") == 0 && i + 1 < argc) {
opt->steps = (int32_t)strtol(argv[++i], NULL, 10);
} else if (strcmp(argv[i], "--data") == 0 && i + 1 < argc) {
opt->dataDir = argv[++i];
} else if (strcmp(argv[i], "--compact") == 0) {
opt->compact = true;
} else if (strcmp(argv[i], "--quiet") == 0) {
opt->quiet = true;
} else if (argv[i][0] == '-') {
fprintf(stderr, "basrun: unknown option %s\n", argv[i]);
return false;
} else if (opt->program) {
fprintf(stderr, "basrun: only one program may be given\n");
return false;
} else {
opt->program = argv[i];
}
}
return opt->program != NULL;
}
static void programDir(const char *path, char *out, int32_t outSize) {
snprintf(out, outSize, "%s", path);
char *sep = platformPathDirEnd(out);
if (sep) {
*sep = '\0';
} else {
out[0] = '.';
out[1] = '\0';
}
}
static int32_t runModule(BasModuleT *mod, const OptionsT *opt) {
BasVmT *vm = basVmCreate();
if (!vm) {
fprintf(stderr, "basrun: out of memory creating the VM\n");
return BASRUN_EXIT_USAGE;
}
basVmLoadModule(vm, mod);
basVmSetPrintCallback(vm, hostPrint, NULL);
basVmSetInputCallback(vm, hostInput, NULL);
basVmSetDoEventsCallback(vm, hostDoEvents, NULL);
char dir[DVX_MAX_PATH];
programDir(opt->program, dir, sizeof(dir));
const char *dataDir = opt->dataDir ? opt->dataDir : dir;
snprintf(vm->appPath, DVX_MAX_PATH, "%s", dir);
snprintf(vm->appConfig, DVX_MAX_PATH, "%s", dataDir);
snprintf(vm->appData, DVX_MAX_PATH, "%s", dataDir);
// Run in slices so a --steps cap can be enforced across the whole run.
basVmSetStepLimit(vm, BASRUN_STEP_SLICE);
int32_t total = 0;
int32_t exit = BASRUN_EXIT_OK;
BasVmResultE result = basVmRun(vm);
while (result == BAS_VM_STEP_LIMIT) {
total += vm->stepCount;
if (opt->steps > 0 && total >= opt->steps) {
if (!opt->quiet) {
fprintf(stderr, "basrun: step cap of %d reached at line %d\n", (int)opt->steps, (int)vm->currentLine);
}
exit = BASRUN_EXIT_STEPS;
break;
}
result = basVmRun(vm);
}
if (result != BAS_VM_HALTED && result != BAS_VM_OK && result != BAS_VM_STEP_LIMIT) {
int32_t errNum = vm->errorNumber;
if (!opt->quiet) {
fprintf(stderr, "basrun: runtime error %d at line %d: %s\n", (int)errNum, (int)vm->errorLine, basVmGetError(vm));
}
if (errNum <= 0) {
errNum = 1;
}
exit = errNum > BASRUN_EXIT_ERR_MAX ? BASRUN_EXIT_ERR_MAX : errNum;
}
fflush(stdout);
basVmDestroy(vm);
return exit;
}
static void usage(void) {
fprintf(stderr, "DVX BASIC headless runner\n\n");
fprintf(stderr, "Usage: basrun [--steps N] [--data DIR] [--compact] [--quiet] program.app|program.bas|project.dbp\n");
}
int main(int argc, char **argv) {
OptionsT opt;
if (!parseArgs(argc, argv, &opt)) {
usage();
return BASRUN_EXIT_USAGE;
}
BasModuleT *mod = NULL;
bool compiled = false;
if (dvxHasExt(opt.program, ".app")) {
mod = loadApp(opt.program);
} else if (dvxHasExt(opt.program, ".dbp")) {
mod = loadDbp(opt.program);
compiled = true;
} else {
mod = loadBas(opt.program);
compiled = true;
}
if (!mod) {
return BASRUN_EXIT_USAGE;
}
if (opt.compact && compiled) {
basStripModule(mod);
basCompactBytecode(mod);
}
int32_t exit = runModule(mod, &opt);
basModuleFree(mod);
return exit;
}

View file

@ -30,6 +30,7 @@
#include "compiler/strip.h" #include "compiler/strip.h"
#include "compiler/compact.h" #include "compiler/compact.h"
#include "runtime/vm.h" #include "runtime/vm.h"
#include "compiler/opcodes.h"
#include "runtime/values.h" #include "runtime/values.h"
#include <stdbool.h> #include <stdbool.h>
@ -40,6 +41,12 @@
#define MAX_OUT 65536 #define MAX_OUT 65536
// Every possible opcode byte, for the operand metadata sweep.
#define TEST_OPCODE_SPACE 256
// A byte no OP_* define claims (verified against opcodes.h).
#define TEST_UNDEFINED_OPCODE 0xFD
typedef struct { typedef struct {
char *buf; char *buf;
@ -49,9 +56,17 @@ typedef struct {
// Function prototypes // Function prototypes
static void captureCallback(void *ctx, const char *text, bool newline); static void captureCallback(void *ctx, const char *text, bool newline);
static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int32_t outCap); static void expectNoCompact(const char *name, const uint8_t *code, int32_t codeLen);
static void testCompact(const char *name, const char *source); static BasModuleT *makeRawModule(const uint8_t *code, int32_t codeLen);
static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int32_t outCap);
static void testCompact(const char *name, const char *source);
static void testCraftedTables(void);
static void testOperandKinds(void);
static int32_t sTotal = 0;
static int32_t sFailed = 0;
static void captureCallback(void *ctx, const char *text, bool newline) { static void captureCallback(void *ctx, const char *text, bool newline) {
@ -71,6 +86,34 @@ static void captureCallback(void *ctx, const char *text, bool newline) {
} }
static void expectNoCompact(const char *name, const uint8_t *code, int32_t codeLen) {
sTotal++;
BasModuleT *mod = makeRawModule(code, codeLen);
int32_t removed = basCompactBytecode(mod);
if (removed != 0 || mod->codeLen != codeLen || memcmp(mod->code, code, codeLen) != 0) {
printf("FAIL: %s (removed %d)\n", name, (int32_t)removed);
sFailed++;
} else {
printf("PASS: %s\n", name);
}
basModuleFree(mod);
}
static BasModuleT *makeRawModule(const uint8_t *code, int32_t codeLen) {
BasModuleT *mod = (BasModuleT *)calloc(1, sizeof(BasModuleT));
mod->code = (uint8_t *)malloc(codeLen);
memcpy(mod->code, code, codeLen);
mod->codeLen = codeLen;
mod->entryPoint = 0;
return mod;
}
static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int32_t outCap) { static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int32_t outCap) {
BasParserT parser; BasParserT parser;
basParserInit(&parser, source, (int32_t)strlen(source)); basParserInit(&parser, source, (int32_t)strlen(source));
@ -126,10 +169,17 @@ static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int
} }
static int32_t sTotal = 0;
static int32_t sFailed = 0;
// A module built directly from raw bytes, exercising validation paths a
// compiled program can never produce.
// Compaction must refuse the stream (return 0) and leave it untouched.
// Crafted proc / form-var tables: out-of-range entries must abort
// compaction, a negative init address (no init code) must not.
static void testCompact(const char *name, const char *source) { static void testCompact(const char *name, const char *source) {
sTotal++; sTotal++;
@ -157,6 +207,114 @@ static void testCompact(const char *name, const char *source) {
} }
static void testCraftedTables(void) {
static const uint8_t okCode[] = { OP_LINE, 1, 0, OP_HALT };
{
sTotal++;
BasModuleT *mod = makeRawModule(okCode, (int32_t)sizeof(okCode));
mod->procs = (BasProcEntryT *)calloc(1, sizeof(BasProcEntryT));
mod->procCount = 1;
mod->procs[0].codeAddr = 999;
if (basCompactBytecode(mod) != 0) {
printf("FAIL: proc codeAddr out of range compacted\n");
sFailed++;
} else {
printf("PASS: proc codeAddr out of range refused\n");
}
basModuleFree(mod);
}
{
sTotal++;
BasModuleT *mod = makeRawModule(okCode, (int32_t)sizeof(okCode));
mod->formVarInfo = (BasFormVarInfoT *)calloc(1, sizeof(BasFormVarInfoT));
mod->formVarInfoCount = 1;
mod->formVarInfo[0].initCodeAddr = -1;
mod->formVarInfo[0].initCodeLen = 0;
if (basCompactBytecode(mod) <= 0) {
printf("FAIL: negative init addr blocked compaction\n");
sFailed++;
} else {
printf("PASS: negative init addr skipped\n");
}
basModuleFree(mod);
}
{
sTotal++;
BasModuleT *mod = makeRawModule(okCode, (int32_t)sizeof(okCode));
mod->formVarInfo = (BasFormVarInfoT *)calloc(1, sizeof(BasFormVarInfoT));
mod->formVarInfoCount = 1;
mod->formVarInfo[0].initCodeAddr = 999;
mod->formVarInfo[0].initCodeLen = 1;
if (basCompactBytecode(mod) != 0) {
printf("FAIL: init addr out of range compacted\n");
sFailed++;
} else {
printf("PASS: init addr out of range refused\n");
}
basModuleFree(mod);
}
}
// Operand metadata: every opcode byte must report a well-formed size,
// and known opcodes their documented one.
static void testOperandKinds(void) {
sTotal++;
bool ok = true;
for (int32_t op = 0; op < TEST_OPCODE_SPACE; op++) {
int32_t size = basOpcodeOperandSize((uint8_t)op);
switch (size) {
case BAS_OPERAND_NONE:
case BAS_OPERAND_U8:
case BAS_OPERAND_U16:
case BAS_OPERAND_U8_U16:
case BAS_OPERAND_I32:
case BAS_OPERAND_FOR:
case BAS_OPERAND_CALL_EXTERN:
case BAS_OPERAND_F64:
case BAS_OPERAND_UNKNOWN:
break;
default:
printf("FAIL: opcode 0x%02X reports operand size %d\n", (int32_t)op, (int32_t)size);
ok = false;
break;
}
}
if (basOpcodeOperandSize(OP_LINE) != BAS_OPERAND_U16 || basOpcodeOperandSize(OP_CALL) != BAS_OPERAND_I32 || basOpcodeOperandSize(OP_FOR_NEXT) != BAS_OPERAND_FOR || basOpcodeOperandSize(TEST_UNDEFINED_OPCODE) != BAS_OPERAND_UNKNOWN) {
printf("FAIL: known opcode operand sizes\n");
ok = false;
}
if (ok) {
printf("PASS: operand size table well-formed\n");
} else {
sFailed++;
}
}
int main(void) { int main(void) {
printf("DVX BASIC Bytecode Compaction Tests\n"); printf("DVX BASIC Bytecode Compaction Tests\n");
printf("====================================\n\n"); printf("====================================\n\n");
@ -467,6 +625,40 @@ int main(void) {
"PRINT\n" "PRINT\n"
); );
// ---- Crafted modules: compaction must refuse invalid streams ----
{
static const uint8_t badCall[] = { OP_LINE, 1, 0, OP_CALL, 0xFF, 0xFF, 0, 0, OP_HALT };
static const uint8_t badJmp[] = { OP_LINE, 1, 0, OP_JMP, 0x00, 0x70, OP_HALT };
static const uint8_t badForNext[] = { OP_LINE, 1, 0, OP_FOR_NEXT, 0, 0, 0, 0x00, 0x70, OP_HALT };
static const uint8_t badForInit[] = { OP_LINE, 1, 0, OP_FOR_INIT, 0, 0, 0, 0x00, 0x70, OP_HALT };
static const uint8_t truncated[] = { OP_LINE, 1, 0, OP_JMP, 0 };
static const uint8_t unknownOp[] = { OP_LINE, 1, 0, TEST_UNDEFINED_OPCODE, OP_HALT };
expectNoCompact("OP_CALL target out of range refused", badCall, (int32_t)sizeof(badCall));
expectNoCompact("OP_JMP target out of range refused", badJmp, (int32_t)sizeof(badJmp));
expectNoCompact("OP_FOR_NEXT target out of range refused", badForNext, (int32_t)sizeof(badForNext));
expectNoCompact("OP_FOR_INIT target out of range refused", badForInit, (int32_t)sizeof(badForInit));
expectNoCompact("Truncated instruction refused", truncated, (int32_t)sizeof(truncated));
expectNoCompact("Unknown opcode refused", unknownOp, (int32_t)sizeof(unknownOp));
}
testCraftedTables();
testOperandKinds();
// ---- Strip: generated proc name colliding with a pool constant ----
testCompact("Strip mangled name collision",
"DIM s AS STRING\n"
"s = \"F0\"\n"
"CALL Foo\n"
"PRINT s\n"
"SUB Foo\n"
" PRINT \"hi\"\n"
"END SUB\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

@ -1,85 +0,0 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// test_quick.c -- Quick single-program test
//
// Build: make -C dvxbasic tests
#include "compiler/parser.h"
#include "runtime/vm.h"
#include "runtime/values.h"
#include <stdio.h>
#include <string.h>
int main(void) {
const char *source = "PRINT \"Hello, World!\"\n";
printf("Source: [%s]\n", source);
printf("Source len: %d\n", (int)strlen(source));
int32_t len = (int32_t)strlen(source);
BasParserT parser;
basParserInit(&parser, source, len);
if (!basParse(&parser)) {
printf("COMPILE ERROR: %s\n", parser.error);
basParserFree(&parser);
return 1;
}
printf("Compiled OK (%d bytes of p-code)\n", parser.cg.codeLen);
// Dump p-code
for (int i = 0; i < parser.cg.codeLen; i++) {
printf("%02X ", parser.cg.code[i]);
}
printf("\n");
BasModuleT *mod = basParserBuildModule(&parser);
basParserFree(&parser);
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
vm->callDepth = 1;
// Step limit
int steps = 0;
vm->running = true;
while (vm->running && steps < 1000) {
BasVmResultE r = basVmStep(vm);
steps++;
if (r != BAS_VM_OK) {
printf("[Result: %d after %d steps: %s]\n", r, steps, basVmGetError(vm));
break;
}
}
if (steps >= 1000) {
printf("[TIMEOUT after %d steps, PC=%d]\n", steps, vm->pc);
}
basVmDestroy(vm);
basModuleFree(mod);
return 0;
}

File diff suppressed because it is too large Load diff

View file

@ -1,242 +0,0 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// test_vm.c -- Quick test for the DVX BASIC VM
//
// Hand-assembles a small p-code program and executes it.
// Tests: PRINT "Hello, World!", arithmetic, FOR loop, string ops.
//
// Build: make -C dvxbasic tests
#include "compiler/opcodes.h"
#include "runtime/vm.h"
#include "runtime/values.h"
#include <stdio.h>
#include <string.h>
// ============================================================
// Helper: emit bytes into a code buffer
// ============================================================
static uint8_t sCode[4096];
static int32_t sCodeLen = 0;
// Function prototypes
static void emit16(int16_t v);
static void emit8(uint8_t b);
static void emitU16(uint16_t v);
static void test1(void);
static void test2(void);
static void test3(void);
static void test4(void);
static void emit16(int16_t v) {
memcpy(&sCode[sCodeLen], &v, 2);
sCodeLen += 2;
}
static void emit8(uint8_t b) {
sCode[sCodeLen++] = b;
}
static void emitU16(uint16_t v) {
emit16((int16_t)v);
}
static void test1(void) {
printf("--- Test 1: PRINT \"Hello, World!\" ---\n");
sCodeLen = 0;
// String constant pool
BasStringT *consts[1];
consts[0] = basStringNew("Hello, World!", 13);
// Code: PUSH_STR 0; PRINT; PRINT_NL; HALT
emit8(OP_PUSH_STR);
emitU16(0);
emit8(OP_PRINT);
emit8(OP_PRINT_NL);
emit8(OP_HALT);
BasModuleT module;
memset(&module, 0, sizeof(module));
module.code = sCode;
module.codeLen = sCodeLen;
module.constants = consts;
module.constCount = 1;
module.entryPoint = 0;
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, &module);
BasVmResultE result = basVmRun(vm);
printf("Result: %d (expected %d = HALTED)\n\n", result, BAS_VM_HALTED);
basVmDestroy(vm);
basStringUnref(consts[0]);
}
static void test2(void) {
printf("--- Test 2: PRINT 2 + 3 * 4 (expect 14) ---\n");
sCodeLen = 0;
// Code: PUSH 3; PUSH 4; MUL; PUSH 2; ADD; PRINT; PRINT_NL; HALT
emit8(OP_PUSH_INT16);
emit16(3);
emit8(OP_PUSH_INT16);
emit16(4);
emit8(OP_MUL_INT);
emit8(OP_PUSH_INT16);
emit16(2);
emit8(OP_ADD_INT);
emit8(OP_PRINT);
emit8(OP_PRINT_NL);
emit8(OP_HALT);
BasModuleT module;
memset(&module, 0, sizeof(module));
module.code = sCode;
module.codeLen = sCodeLen;
module.entryPoint = 0;
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, &module);
basVmRun(vm);
basVmDestroy(vm);
printf("\n");
}
static void test3(void) {
printf("--- Test 3: PRINT \"Hello\" & \" \" & \"BASIC\" ---\n");
sCodeLen = 0;
BasStringT *consts[3];
consts[0] = basStringNew("Hello", 5);
consts[1] = basStringNew(" ", 1);
consts[2] = basStringNew("BASIC", 5);
// Code: PUSH consts[0]; PUSH consts[1]; CONCAT; PUSH consts[2]; CONCAT; PRINT; PRINT_NL; HALT
emit8(OP_PUSH_STR); emitU16(0);
emit8(OP_PUSH_STR); emitU16(1);
emit8(OP_STR_CONCAT);
emit8(OP_PUSH_STR); emitU16(2);
emit8(OP_STR_CONCAT);
emit8(OP_PRINT);
emit8(OP_PRINT_NL);
emit8(OP_HALT);
BasModuleT module;
memset(&module, 0, sizeof(module));
module.code = sCode;
module.codeLen = sCodeLen;
module.constants = consts;
module.constCount = 3;
module.entryPoint = 0;
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, &module);
basVmRun(vm);
basVmDestroy(vm);
printf("\n");
basStringUnref(consts[0]);
basStringUnref(consts[1]);
basStringUnref(consts[2]);
}
static void test4(void) {
printf("--- Test 4: FOR i = 1 TO 5: PRINT i: NEXT ---\n");
sCodeLen = 0;
// We need a call frame with at least 1 local (the loop variable)
// For module-level code, we use callStack[0] as implicit frame
// Setup: store initial value in local 0
// PUSH 1; STORE_LOCAL 0 -- i = 1
emit8(OP_PUSH_INT16); emit16(1);
emit8(OP_STORE_LOCAL); emitU16(0);
// Push limit and step for FOR_INIT
// PUSH 5 (limit); PUSH 1 (step)
emit8(OP_PUSH_INT16); emit16(5);
emit8(OP_PUSH_INT16); emit16(1);
emit8(OP_FOR_INIT); emitU16(0); emit8(1); emit16(0); // scope=local, skipOffset patched below
// Loop body start (record PC for FOR_NEXT offset)
int32_t loopBody = sCodeLen;
// LOAD_LOCAL 0; PRINT; PRINT " "
emit8(OP_LOAD_LOCAL); emitU16(0);
emit8(OP_PRINT);
// FOR_NEXT: increment i, test, jump back
emit8(OP_FOR_NEXT);
emitU16(0); // local index
emit8(SCOPE_LOCAL);
int16_t offset = (int16_t)(loopBody - (sCodeLen + 2));
emit16(offset);
// After loop
emit8(OP_PRINT_NL);
emit8(OP_HALT);
BasModuleT module;
memset(&module, 0, sizeof(module));
module.code = sCode;
module.codeLen = sCodeLen;
module.entryPoint = 0;
BasVmT *vm = basVmCreate();
// Initialize the implicit main frame with 1 local
vm->callStack[0].localCount = 1;
vm->callDepth = 1;
basVmLoadModule(vm, &module);
basVmRun(vm);
basVmDestroy(vm);
printf("\n");
}
int main(void) {
printf("DVX BASIC VM Tests\n");
printf("==================\n\n");
test1();
test2();
test3();
test4();
printf("All tests complete.\n");
return 0;
}

View file

View file

@ -166,7 +166,7 @@ static int32_t sHistoryPos = -1;
static int32_t sHistoryCount = 0; static int32_t sHistoryCount = 0;
static int32_t sCurrentTopic = -1; static int32_t sCurrentTopic = -1;
// Viewport wrap width the width text should wrap at, derived from the // Viewport wrap width -- the width text should wrap at, derived from the
// ScrollPane's inner area. Updated when the window is first laid out and // ScrollPane's inner area. Updated when the window is first laid out and
// on resize. Wrapping widgets use this instead of w->parent->w so that // on resize. Wrapping widgets use this instead of w->parent->w so that
// wide tables/code don't inflate the wrap width. // wide tables/code don't inflate the wrap width.
@ -186,6 +186,7 @@ static int32_t sHelpListItemTypeId = -1;
// ============================================================ // ============================================================
int32_t appMain(DxeAppContextT *ctx); int32_t appMain(DxeAppContextT *ctx);
void appShutdown(void);
static void buildContentWidgets(void); static void buildContentWidgets(void);
static void closeHelpFile(void); static void closeHelpFile(void);
static int32_t countLines(const char *text); static int32_t countLines(const char *text);
@ -218,10 +219,10 @@ static void historyPush(int32_t topicIdx);
static const char *hlpString(uint32_t offset); static const char *hlpString(uint32_t offset);
static bool isFirstChild(WidgetT *w); static bool isFirstChild(WidgetT *w);
static int32_t maxLineWidth(const BitmapFontT *font, const char *text); static int32_t maxLineWidth(const BitmapFontT *font, const char *text);
static bool nextSiblingHasOwnBorder(WidgetT *w);
static void navigateBack(void); static void navigateBack(void);
static void navigateForward(void); static void navigateForward(void);
static void navigateToTopic(int32_t topicIdx); static void navigateToTopic(int32_t topicIdx);
static bool nextSiblingHasOwnBorder(WidgetT *w);
static void onClose(WindowT *win); static void onClose(WindowT *win);
static void onIndex(WidgetT *w); static void onIndex(WidgetT *w);
static void onMenu(WindowT *win, int32_t menuId); static void onMenu(WindowT *win, int32_t menuId);
@ -325,6 +326,13 @@ AppDescriptorT appDescriptor = {
}; };
// Shell force-kill/reap hook: release the help file data even when onClose
// never ran (the shell destroys the window itself).
void appShutdown(void) {
closeHelpFile();
}
// Re-wrap all text content widgets at the current content box width. // Re-wrap all text content widgets at the current content box width.
static void buildContentWidgets(void) { static void buildContentWidgets(void) {
@ -523,7 +531,7 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
const HlpImageRefT *imgRef = (const HlpImageRefT *)payload; const HlpImageRefT *imgRef = (const HlpImageRefT *)payload;
uint32_t absOffset = sHeader.imagePoolOffset + imgRef->imageOffset; uint32_t absOffset = sHeader.imagePoolOffset + imgRef->imageOffset;
// Save file position buildContentWidgets reads records // Save file position -- buildContentWidgets reads records
// sequentially and we must not disturb its position. // sequentially and we must not disturb its position.
long savedPos = ftell(sHlpFile); long savedPos = ftell(sHlpFile);
@ -650,7 +658,7 @@ static void displayRecord(const HlpRecordHdrT *hdr, const char *payload) {
// Ensure a HelpText-style widget's wrapped text is up to date for the given pixel width. // Ensure a HelpText-style widget's wrapped text is up to date for the given pixel width.
// Returns the wrapped text and updates lineCount. Uses cached result if width unchanged. // Returns the wrapped text and updates lineCount. Uses cached result if width unchanged.
// Wrap text at the given pixel width. Caches the result only re-wraps // Wrap text at the given pixel width. Caches the result -- only re-wraps
// when pixelW changes. Returns the wrapped string. // when pixelW changes. Returns the wrapped string.
static const char *doWrap(char **wrappedPtr, int32_t *wrapWidthPtr, int32_t *lineCountPtr, const char *text, int32_t pixelW, int32_t charW, int32_t padPixels) { static const char *doWrap(char **wrappedPtr, int32_t *wrapWidthPtr, int32_t *lineCountPtr, const char *text, int32_t pixelW, int32_t charW, int32_t padPixels) {
int32_t cols = (pixelW - padPixels) / charW; int32_t cols = (pixelW - padPixels) / charW;

View file

@ -133,10 +133,11 @@ static int32_t sAppCount = 0;
// ============================================================ // ============================================================
int32_t appMain(DxeAppContextT *ctx); int32_t appMain(DxeAppContextT *ctx);
void appShutdown(void);
static int32_t appEntryCmpName(const void *a, const void *b); static int32_t appEntryCmpName(const void *a, const void *b);
void appShutdown(void);
static void buildPmWindow(void); static void buildPmWindow(void);
static void desktopUpdate(void); static void desktopUpdate(void);
static void freeAppFiles(void);
static void onAppButtonClick(WidgetT *w); static void onAppButtonClick(WidgetT *w);
static void onPmClose(WindowT *win); static void onPmClose(WindowT *win);
static void onPmMenu(WindowT *win, int32_t menuId); static void onPmMenu(WindowT *win, int32_t menuId);
@ -163,12 +164,6 @@ AppDescriptorT appDescriptor = {
}; };
void appShutdown(void) {
shellUnregisterDesktopUpdate(desktopUpdate);
dvxQuit(sAc);
}
// qsort comparator for AppEntryT -- case-insensitive on display name. // qsort comparator for AppEntryT -- case-insensitive on display name.
static int32_t appEntryCmpName(const void *a, const void *b) { static int32_t appEntryCmpName(const void *a, const void *b) {
const AppEntryT *ea = (const AppEntryT *)a; const AppEntryT *ea = (const AppEntryT *)a;
@ -177,6 +172,15 @@ static int32_t appEntryCmpName(const void *a, const void *b) {
} }
void appShutdown(void) {
shellUnregisterDesktopUpdate(desktopUpdate);
freeAppFiles();
prefsClose(sPrefs);
sPrefs = NULL;
dvxQuit(sAc);
}
// Build the main Program Manager window with app buttons, menus, and status bar. // Build the main Program Manager window with app buttons, menus, and status bar.
// Window is centered horizontally and placed in the upper quarter vertically // Window is centered horizontally and placed in the upper quarter vertically
// so spawned app windows don't hide behind it. // so spawned app windows don't hide behind it.
@ -313,6 +317,18 @@ static void desktopUpdate(void) {
// Widget click handler for app grid buttons. userData was set to the // Widget click handler for app grid buttons. userData was set to the
// AppEntryT pointer during window construction, giving us the .app path. // AppEntryT pointer during window construction, giving us the .app path.
// Release the scanned app list and its icon bitmaps.
static void freeAppFiles(void) {
for (int32_t i = 0; i < sAppCount; i++) {
free(sAppFiles[i].iconData);
}
arrfree(sAppFiles);
sAppFiles = NULL;
sAppCount = 0;
}
static void onAppButtonClick(WidgetT *w) { static void onAppButtonClick(WidgetT *w) {
AppEntryT *entry = (AppEntryT *)w->userData; AppEntryT *entry = (AppEntryT *)w->userData;
@ -448,14 +464,7 @@ static void scanAppsDir(void) {
sScanning = true; sScanning = true;
// Free icons from previous scan freeAppFiles();
for (int32_t i = 0; i < sAppCount; i++) {
free(sAppFiles[i].iconData);
}
arrfree(sAppFiles);
sAppFiles = NULL;
sAppCount = 0;
scanAppsDirRecurse("apps"); scanAppsDirRecurse("apps");
if (sAppCount > 1) { if (sAppCount > 1) {
@ -607,7 +616,7 @@ int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx; sCtx = ctx;
sAc = ctx->shellCtx; sAc = ctx->shellCtx;
// Set help file for F1 system help lives in progman's app directory // Set help file for F1 -- system help lives in progman's app directory
snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%s", ctx->appDir, "dvxhelp.hlp"); snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%s", ctx->appDir, "dvxhelp.hlp");
// Load saved preferences // Load saved preferences
@ -625,6 +634,5 @@ int32_t appMain(DxeAppContextT *ctx) {
shellRegisterDesktopUpdate(desktopUpdate); shellRegisterDesktopUpdate(desktopUpdate);
return 0; return 0;
} }

View file

@ -71,6 +71,7 @@ static void makeTempPath(const char *origPath, int32_t id, char *out, int
static void releaseAppResources(ShellAppT *app); static void releaseAppResources(ShellAppT *app);
void shellAppInit(void); void shellAppInit(void);
int32_t shellAppSlotCount(void); int32_t shellAppSlotCount(void);
void shellAppTestReset(void);
void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize); void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize);
int32_t shellEnsureConfigDir(const DxeAppContextT *ctx); int32_t shellEnsureConfigDir(const DxeAppContextT *ctx);
void shellForceKillApp(AppContextT *ctx, ShellAppT *app); void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
@ -288,10 +289,12 @@ static void makeTempPath(const char *origPath, int32_t id, char *out, int32_t ou
tmpDir = getenv("TMP"); tmpDir = getenv("TMP");
} }
// The fallback is anchored to the working directory: a bare file name
// would make dlopen search the library path instead of opening it.
if (tmpDir && tmpDir[0]) { if (tmpDir && tmpDir[0]) {
snprintf(out, outSize, "%s" DVX_PATH_SEP "_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_PATH_SEP "_dvx%02ld%s", (long)id, dot);
} }
} }
@ -327,6 +330,18 @@ int32_t shellAppSlotCount(void) {
} }
// Test seam: release every slot (the caller has already terminated the
// apps) so the next shellAppInit starts from an empty table.
void shellAppTestReset(void) {
for (int32_t i = 0; i < arrlen(sApps); i++) {
free(sApps[i]);
}
arrfree(sApps);
sApps = NULL;
}
void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize) { void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize) {
snprintf(outPath, outSize, "%s" DVX_PATH_SEP "%s", ctx->configDir, filename); snprintf(outPath, outSize, "%s" DVX_PATH_SEP "%s", ctx->configDir, filename);
} }
@ -393,7 +408,8 @@ ShellAppT *shellGetApp(int32_t appId) {
// DXE3 is DJGPP's dynamic linking system -- similar to dlopen/dlsym on Unix. // DXE3 is DJGPP's dynamic linking system -- similar to dlopen/dlsym on Unix.
// Each .app file is a DXE3 shared object that exports _appDescriptor and // Each .app file is a DXE3 shared object that exports _appDescriptor and
// _appMain (and optionally _appShutdown). The leading underscore is the // _appMain (and optionally _appShutdown). The leading underscore is the
// COFF symbol convention; DJGPP's dlsym expects it. // COFF symbol convention; DJGPP's dlsym expects it, ELF hosts use the
// bare name -- DVX_SYM() supplies whichever the platform needs.
// //
// Multi-instance support: DXE3's dlopen returns the same handle for the // Multi-instance support: DXE3's dlopen returns the same handle for the
// same path (reference-counted), so two loads of the same .dxe share all // same path (reference-counted), so two loads of the same .dxe share all
@ -416,7 +432,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
if (existing) { if (existing) {
// Read multiInstance from the already-loaded descriptor // Read multiInstance from the already-loaded descriptor
AppDescriptorT *existDesc = (AppDescriptorT *)dlsym(existing->dxeHandle, "_appDescriptor"); AppDescriptorT *existDesc = (AppDescriptorT *)dlsym(existing->dxeHandle, DVX_SYM("appDescriptor"));
if (!existDesc || !existDesc->multiInstance) { if (!existDesc || !existDesc->multiInstance) {
char msg[SHELL_MSG_MAX]; char msg[SHELL_MSG_MAX];
@ -456,7 +472,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
dvxSetBusy(ctx, true); dvxSetBusy(ctx, true);
// Load the DXE // Load the DXE
void *handle = dlopen(loadPath, RTLD_GLOBAL); void *handle = dlopen(loadPath, RTLD_NOW | RTLD_GLOBAL);
if (!handle) { if (!handle) {
char msg[SHELL_MSG_MAX]; char msg[SHELL_MSG_MAX];
@ -468,7 +484,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
} }
// Look up required symbols // Look up required symbols
AppDescriptorT *desc = (AppDescriptorT *)dlsym(handle, "_appDescriptor"); AppDescriptorT *desc = (AppDescriptorT *)dlsym(handle, DVX_SYM("appDescriptor"));
if (!desc) { if (!desc) {
char msg[SHELL_MSG_MAX]; char msg[SHELL_MSG_MAX];
@ -479,7 +495,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
return shellLoadFail(handle, tempPath); return shellLoadFail(handle, tempPath);
} }
int32_t (*entry)(DxeAppContextT *) = (int32_t (*)(DxeAppContextT *))dlsym(handle, "_appMain"); int32_t (*entry)(DxeAppContextT *) = (int32_t (*)(DxeAppContextT *))dlsym(handle, DVX_SYM("appMain"));
if (!entry) { if (!entry) {
char msg[SHELL_MSG_MAX]; char msg[SHELL_MSG_MAX];
@ -489,7 +505,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
return shellLoadFail(handle, tempPath); return shellLoadFail(handle, tempPath);
} }
void (*shutdown)(void) = (void (*)(void))dlsym(handle, "_appShutdown"); void (*shutdown)(void) = (void (*)(void))dlsym(handle, DVX_SYM("appShutdown"));
// Fill in the app slot // Fill in the app slot
ShellAppT *app = sApps[id]; ShellAppT *app = sApps[id];
@ -579,7 +595,17 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
// The app creates its windows and returns. From this point on, // The app creates its windows and returns. From this point on,
// the app lives entirely through event callbacks dispatched by // the app lives entirely through event callbacks dispatched by
// the shell's dvxUpdate loop. No separate task or stack needed. // the shell's dvxUpdate loop. No separate task or stack needed.
app->entryFn(app->dxeCtx); // A non-zero return is the app declining to start: unload it the
// same way a fault would, without running its shutdown hook.
int32_t entryResult = app->entryFn(app->dxeCtx);
if (entryResult != 0) {
dvxLog("Shell: '%s' appMain returned %ld, unloading", app->name, (long)entryResult);
ctx->currentAppId = 0;
dvxSetBusy(ctx, false);
shellForceKillApp(ctx, app);
return -1;
}
} }
// The slot pointer is stable across the callback-only entry point above // The slot pointer is stable across the callback-only entry point above

View file

@ -187,6 +187,13 @@ ShellAppT *shellGetApp(int32_t appId);
// Total number of app slots (for iteration bounds) // Total number of app slots (for iteration bounds)
int32_t shellAppSlotCount(void); int32_t shellAppSlotCount(void);
// Test seams (host harness only): drop every app slot / rebind the
// shell-side callback tables and task hooks to a harness-owned context.
void shellAppTestReset(void);
int shellMain(int argc, char *argv[]);
void shellTestBind(AppContextT *ctx);
// Count running apps (not counting the shell itself) // Count running apps (not counting the shell itself)
int32_t shellRunningAppCount(void); int32_t shellRunningAppCount(void);

View file

@ -70,6 +70,7 @@
// ============================================================ // ============================================================
static AppContextT sCtx; static AppContextT sCtx;
static AppContextT *sCtxRef = &sCtx; // context the shell-side callbacks tag app ids from
static PrefsHandleT *sPrefs = NULL; static PrefsHandleT *sPrefs = NULL;
// setjmp buffer for crash recovery. The crash handler longjmps here to // setjmp buffer for crash recovery. The crash handler longjmps here to
// return control to the shell's main loop after an app crashes. // return control to the shell's main loop after an app crashes.
@ -117,6 +118,7 @@ int shellMain(int argc, char *argv[]);
void shellPurgeAppCallbacks(int32_t appId); void shellPurgeAppCallbacks(int32_t appId);
void shellRegisterDesktopUpdate(void (*updateFn)(void)); void shellRegisterDesktopUpdate(void (*updateFn)(void));
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx); void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);
void shellTestBind(AppContextT *ctx);
void shellUnregisterDesktopUpdate(void (*updateFn)(void)); void shellUnregisterDesktopUpdate(void (*updateFn)(void));
void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx); void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx);
static void titleChangeHandler(void *ctx); static void titleChangeHandler(void *ctx);
@ -127,12 +129,12 @@ static void titleChangeHandler(void *ctx);
// save/restore on every switch, the first yield back from an app left // save/restore on every switch, the first yield back from an app left
// the shell's allocations and windows attributed to that app. // the shell's allocations and windows attributed to that app.
static void appIdRestore(int32_t value) { static void appIdRestore(int32_t value) {
sCtx.currentAppId = value; sCtxRef->currentAppId = value;
} }
static int32_t appIdSave(void) { static int32_t appIdSave(void) {
return sCtx.currentAppId; return sCtxRef->currentAppId;
} }
@ -221,88 +223,6 @@ static void shellIdleDispatch(void *ctx) {
} }
// Drop every idle and desktop-update callback registered by the given app,
// matched on the appId captured when each was registered. shellForceKillApp
// (and, defensively, shellReapApp) calls this before dlclose so a force or
// crash kill reclaims an app's shell-side registrations WITHOUT running the
// app's own -- possibly hung or faulting -- shutdownFn. Otherwise the
// pointers would dangle into unmapped DXE code and the next shellDesktopUpdate
// or idle frame would jump into it. appId < 1 (the shell's own and
// persistent-DXE registrations) is never purged. Both lists are walked
// back-to-front so arrdel's index shifts cannot skip a matching entry.
void shellPurgeAppCallbacks(int32_t appId) {
if (appId < 1) {
return;
}
for (int32_t i = arrlen(sDesktopUpdateFns) - 1; i >= 0; i--) {
if (sDesktopUpdateFns[i].appId == appId) {
arrdel(sDesktopUpdateFns, i);
}
}
for (int32_t i = arrlen(sIdleHandlers) - 1; i >= 0; i--) {
if (sIdleHandlers[i].appId == appId) {
arrdel(sIdleHandlers, i);
}
}
}
void shellRegisterDesktopUpdate(void (*updateFn)(void)) {
DesktopUpdateHandlerT handler;
handler.fn = updateFn;
handler.appId = sCtx.currentAppId;
arrput(sDesktopUpdateFns, handler);
}
// Idempotent: a (fn, ctx) pair already present is not added twice, so a
// producer can call this on every attach without tracking its own state.
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx) {
IdleHandlerT handler;
for (int32_t i = 0; i < arrlen(sIdleHandlers); i++) {
if (sIdleHandlers[i].fn == fn && sIdleHandlers[i].ctx == ctx) {
return;
}
}
handler.fn = fn;
handler.ctx = ctx;
handler.appId = sCtx.currentAppId;
arrput(sIdleHandlers, handler);
}
void shellUnregisterDesktopUpdate(void (*updateFn)(void)) {
for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) {
if (sDesktopUpdateFns[i].fn == updateFn) {
arrdel(sDesktopUpdateFns, i);
return;
}
}
}
// No-op if the (fn, ctx) pair is absent, so double-unregister is safe.
void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx) {
for (int32_t i = 0; i < arrlen(sIdleHandlers); i++) {
if (sIdleHandlers[i].fn == fn && sIdleHandlers[i].ctx == ctx) {
arrdel(sIdleHandlers, i);
return;
}
}
}
static void titleChangeHandler(void *ctx) {
(void)ctx;
shellDesktopUpdate();
}
int shellMain(int argc, char *argv[]) { int shellMain(int argc, char *argv[]) {
(void)argc; (void)argc;
(void)argv; (void)argv;
@ -316,6 +236,8 @@ int shellMain(int argc, char *argv[]) {
if (tsInit() != TS_OK) { if (tsInit() != TS_OK) {
dvxLog("Failed to initialize task system"); dvxLog("Failed to initialize task system");
// dvxInit has not run yet at this point, so there is nothing to shut down. // dvxInit has not run yet at this point, so there is nothing to shut down.
prefsClose(sPrefs);
sPrefs = NULL;
return 1; return 1;
} }
@ -358,6 +280,8 @@ int shellMain(int argc, char *argv[]) {
if (result != 0) { if (result != 0) {
dvxLog("Failed to initialize DVX GUI (error %ld)", (long)result); dvxLog("Failed to initialize DVX GUI (error %ld)", (long)result);
tsShutdown(); tsShutdown();
prefsClose(sPrefs);
sPrefs = NULL;
return 1; return 1;
} }
@ -453,6 +377,8 @@ int shellMain(int argc, char *argv[]) {
dvxLog("Failed to load desktop app '%s'", desktopApp); dvxLog("Failed to load desktop app '%s'", desktopApp);
tsShutdown(); tsShutdown();
dvxShutdown(&sCtx); dvxShutdown(&sCtx);
prefsClose(sPrefs);
sPrefs = NULL;
return 1; return 1;
} }
@ -557,3 +483,109 @@ int shellMain(int argc, char *argv[]) {
dvxLog("DVX Shell exited."); dvxLog("DVX Shell exited.");
return 0; return 0;
} }
// Drop every idle and desktop-update callback registered by the given app,
// matched on the appId captured when each was registered. shellForceKillApp
// (and, defensively, shellReapApp) calls this before dlclose so a force or
// crash kill reclaims an app's shell-side registrations WITHOUT running the
// app's own -- possibly hung or faulting -- shutdownFn. Otherwise the
// pointers would dangle into unmapped DXE code and the next shellDesktopUpdate
// or idle frame would jump into it. appId < 1 (the shell's own and
// persistent-DXE registrations) is never purged. Both lists are walked
// back-to-front so arrdel's index shifts cannot skip a matching entry.
void shellPurgeAppCallbacks(int32_t appId) {
if (appId < 1) {
return;
}
for (int32_t i = arrlen(sDesktopUpdateFns) - 1; i >= 0; i--) {
if (sDesktopUpdateFns[i].appId == appId) {
arrdel(sDesktopUpdateFns, i);
}
}
for (int32_t i = arrlen(sIdleHandlers) - 1; i >= 0; i--) {
if (sIdleHandlers[i].appId == appId) {
arrdel(sIdleHandlers, i);
}
}
}
void shellRegisterDesktopUpdate(void (*updateFn)(void)) {
DesktopUpdateHandlerT handler;
handler.fn = updateFn;
handler.appId = sCtxRef->currentAppId;
arrput(sDesktopUpdateFns, handler);
}
// Idempotent: a (fn, ctx) pair already present is not added twice, so a
// producer can call this on every attach without tracking its own state.
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx) {
IdleHandlerT handler;
for (int32_t i = 0; i < arrlen(sIdleHandlers); i++) {
if (sIdleHandlers[i].fn == fn && sIdleHandlers[i].ctx == ctx) {
return;
}
}
handler.fn = fn;
handler.ctx = ctx;
handler.appId = sCtxRef->currentAppId;
arrput(sIdleHandlers, handler);
}
// Test seam: run the shell-side app machinery against a harness-owned
// context instead of the shell's private one. Mirrors the wiring shellMain
// performs (slot table, memory attribution, task context hooks, idle
// dispatch) and drops every handler and slot left by the previous case.
// Pass NULL to return to the shell's own context.
void shellTestBind(AppContextT *ctx) {
arrfree(sDesktopUpdateFns);
arrfree(sIdleHandlers);
sDesktopUpdateFns = NULL;
sIdleHandlers = NULL;
sCtxRef = ctx ? ctx : &sCtx;
shellAppTestReset();
if (ctx) {
shellAppInit();
dvxMemAppIdPtr = &ctx->currentAppId;
tsSetContextHooks(appIdSave, appIdRestore);
ctx->idleCallback = shellIdleDispatch;
ctx->idleCtx = ctx;
}
}
void shellUnregisterDesktopUpdate(void (*updateFn)(void)) {
for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) {
if (sDesktopUpdateFns[i].fn == updateFn) {
arrdel(sDesktopUpdateFns, i);
return;
}
}
}
// No-op if the (fn, ctx) pair is absent, so double-unregister is safe.
void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx) {
for (int32_t i = 0; i < arrlen(sIdleHandlers); i++) {
if (sIdleHandlers[i].fn == fn && sIdleHandlers[i].ctx == ctx) {
arrdel(sIdleHandlers, i);
return;
}
}
}
static void titleChangeHandler(void *ctx) {
(void)ctx;
shellDesktopUpdate();
}

View file

@ -44,10 +44,45 @@ OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGETDIR = $(LIBSDIR)/kpunch/libdvx TARGETDIR = $(LIBSDIR)/kpunch/libdvx
TARGET = $(TARGETDIR)/libdvx.lib TARGET = $(TARGETDIR)/libdvx.lib
.PHONY: all clean # Native host build (test harness): same core sources plus the host
# platform backend and the shared platform pieces the DOS loader normally
# supplies (memory tracking, util, stb_ds). Produces a static archive the
# test binaries link with --whole-archive so widget .so files can resolve
# every core symbol through -rdynamic. SAN=1 adds ASan/UBSan.
HOSTCC = gcc
HOSTOBJDIR = ../../../../obj/host
HOSTTARGET = $(HOSTOBJDIR)/libdvx.a
HOSTCFLAGS = -O1 -g -fPIC -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -D_GNU_SOURCE -I. -Iplatform -I../libtasks -Ithirdparty
ifeq ($(SAN),1)
HOSTCFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer
endif
# COV=1 (make coverage): gcov instrumentation, objects kept apart from
# the sanitized build under obj/hostcov.
ifeq ($(COV),1)
HOSTOBJDIR = ../../../../obj/hostcov
HOSTCFLAGS += --coverage
endif
HOST_SRCS = $(SRCS) platform/dvxPlatformHost.c platform/dvxMemTrack.c platform/dvxPlatformUtil.c thirdparty/stb_ds_impl.c
HOST_OBJS = $(patsubst %.c,$(HOSTOBJDIR)/libdvx/%.o,$(HOST_SRCS))
HOST_HDRS = $(wildcard *.h platform/*.h thirdparty/*.h)
.PHONY: all clean host host-clean
all: $(TARGET) $(TARGETDIR)/libdvx.dep all: $(TARGET) $(TARGETDIR)/libdvx.dep
host: $(HOSTTARGET)
$(HOSTTARGET): $(HOST_OBJS)
rm -f $@
ar rcs $@ $(HOST_OBJS)
$(HOSTOBJDIR)/libdvx/%.o: %.c $(HOST_HDRS)
@mkdir -p $(dir $@)
$(HOSTCC) $(HOSTCFLAGS) -c -o $@ $<
host-clean:
rm -rf $(HOSTOBJDIR)/libdvx $(HOSTTARGET)
$(TARGETDIR)/libdvx.dep: libdvx.dep | $(TARGETDIR) $(TARGETDIR)/libdvx.dep: libdvx.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@ sed 's/$$/\r/' $< > $@

File diff suppressed because it is too large Load diff

View file

@ -43,7 +43,6 @@
#include "dvxComp.h" #include "dvxComp.h"
#include "dvxWm.h" #include "dvxWm.h"
#include <time.h>
// ============================================================ // ============================================================
// Application context // Application context
@ -87,17 +86,17 @@ typedef struct AppContextT {
// Double-click detection for minimized window icons: timestamps and // Double-click detection for minimized window icons: timestamps and
// window IDs track whether two clicks land on the same icon within // window IDs track whether two clicks land on the same icon within
// the system double-click interval. // the system double-click interval.
clock_t lastIconClickTime; PlatformTicksT lastIconClickTime;
int32_t lastIconClickId; // window ID of last-clicked minimized icon (-1 = none) int32_t lastIconClickId; // window ID of last-clicked minimized icon (-1 = none)
clock_t lastCloseClickTime; PlatformTicksT lastCloseClickTime;
int32_t lastCloseClickId; // window ID of last-clicked close gadget (-1 = none) int32_t lastCloseClickId; // window ID of last-clicked close gadget (-1 = none)
clock_t lastTitleClickTime; PlatformTicksT lastTitleClickTime;
int32_t lastTitleClickId; // window ID of last-clicked title bar (-1 = none) int32_t lastTitleClickId; // window ID of last-clicked title bar (-1 = none)
// Minimized icon thumbnails are refreshed one per frame (round-robin) // Minimized icon thumbnails are refreshed one per frame (round-robin)
// rather than all at once, to amortize the cost of scaling the content // rather than all at once, to amortize the cost of scaling the content
// buffer down to icon size. // buffer down to icon size.
int32_t iconRefreshIdx; // next minimized icon to refresh (staggered) int32_t iconRefreshIdx; // next minimized icon to refresh (staggered)
int32_t frameCount; // frame counter for periodic tasks uint32_t frameCount; // frame counter for periodic tasks
// The idle callback allows the host application (e.g. the DVX shell) // The idle callback allows the host application (e.g. the DVX shell)
// to do background work (poll serial ports, service DXE apps) during // to do background work (poll serial ports, service DXE apps) during
// frames where no GUI events occurred, instead of just yielding the CPU. // frames where no GUI events occurred, instead of just yielding the CPU.
@ -126,7 +125,7 @@ typedef struct AppContextT {
// Tooltip state -- tooltip appears after the mouse hovers over a widget // Tooltip state -- tooltip appears after the mouse hovers over a widget
// with a tooltip string for a brief delay. Pre-computing W/H avoids // with a tooltip string for a brief delay. Pre-computing W/H avoids
// re-measuring on every paint frame. // re-measuring on every paint frame.
clock_t tooltipHoverStart; // when mouse stopped moving PlatformTicksT tooltipHoverStart; // when mouse stopped moving
const char *tooltipText; // text to show (NULL = hidden) const char *tooltipText; // text to show (NULL = hidden)
int32_t tooltipX; // screen position int32_t tooltipX; // screen position
int32_t tooltipY; int32_t tooltipY;
@ -140,7 +139,7 @@ typedef struct AppContextT {
// Mouse configuration (loaded from preferences) // Mouse configuration (loaded from preferences)
int32_t wheelDirection; // 1 = normal, -1 = reversed int32_t wheelDirection; // 1 = normal, -1 = reversed
int32_t wheelStep; // lines per wheel notch (1-10, default 3) int32_t wheelStep; // lines per wheel notch (1-10, default 3)
clock_t dblClickTicks; // double-click speed in clock() ticks PlatformTicksT dblClickTicks; // double-click speed in platformClock() ticks
int32_t accelThreshold; // last applied pointer-accel threshold int32_t accelThreshold; // last applied pointer-accel threshold
int32_t mickeyRatio; // last applied mickeys-to-pixels ratio int32_t mickeyRatio; // last applied mickeys-to-pixels ratio
// Color scheme source RGB values (unpacked, for theme save/get) // Color scheme source RGB values (unpacked, for theme save/get)
@ -271,8 +270,8 @@ void dvxShowContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t scr
// (plus chrome). Used for dialog boxes and other fixed-layout windows // (plus chrome). Used for dialog boxes and other fixed-layout windows
// where the window should shrink-wrap its content. // where the window should shrink-wrap its content.
void dvxFitWindow(AppContextT *ctx, WindowT *win); void dvxFitWindow(AppContextT *ctx, WindowT *win);
void dvxFitWindowW(AppContextT *ctx, WindowT *win);
void dvxFitWindowH(AppContextT *ctx, WindowT *win); void dvxFitWindowH(AppContextT *ctx, WindowT *win);
void dvxFitWindowW(AppContextT *ctx, WindowT *win);
void dvxResizeWindow(AppContextT *ctx, WindowT *win, int32_t newW, int32_t newH); void dvxResizeWindow(AppContextT *ctx, WindowT *win, int32_t newW, int32_t newH);
// Mark a sub-region of a window's content area as needing repaint. The // Mark a sub-region of a window's content area as needing repaint. The

View file

@ -57,9 +57,11 @@
#include <ctype.h> #include <ctype.h>
#include <dirent.h> #include <dirent.h>
#include <limits.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <strings.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
@ -104,6 +106,25 @@
#define CB_LIST_HEIGHT 120 #define CB_LIST_HEIGHT 120
#define CB_PADDING 8 #define CB_PADDING 8
// Widgets per dialog that keep Enter for themselves (see dialogOnKey)
#define DLG_ENTER_OWNERS_MAX 3
// ============================================================
// Keyboard hook shared by every dialog
// ============================================================
//
// The widget layer only routes keys to the focused widget, so a dialog
// gets Escape/Enter semantics from a window-level onKey wrapper that
// runs before/after widgetOnKey. Each dialog's state struct starts
// with this header and is installed as the window's userData.
typedef struct {
WidgetT *defaultBtn; // clicked on Enter when no button/owner has focus
WidgetT *cancelBtn; // clicked on Escape
WidgetT *enterOwners[DLG_ENTER_OWNERS_MAX]; // focused widgets that consume Enter themselves
} DialogKeysT;
// ============================================================ // ============================================================
// Word-wrap iterator types // Word-wrap iterator types
@ -136,6 +157,7 @@ typedef struct {
// breaks the nested dvxUpdate loop in each dvx*Dialog function. // breaks the nested dvxUpdate loop in each dvx*Dialog function.
typedef struct { typedef struct {
DialogKeysT keys; // must be first (window userData)
AppContextT *ctx; AppContextT *ctx;
int32_t result; // ID_OK, ID_CANCEL, ID_YES, ID_NO, etc. int32_t result; // ID_OK, ID_CANCEL, ID_YES, ID_NO, etc.
bool done; // set true to break the modal loop bool done; // set true to break the modal loop
@ -148,6 +170,7 @@ typedef struct {
} MsgBoxStateT; } MsgBoxStateT;
typedef struct { typedef struct {
DialogKeysT keys; // must be first (window userData)
AppContextT *ctx; AppContextT *ctx;
bool done; bool done;
bool accepted; // true = OK, false = Cancel bool accepted; // true = OK, false = Cancel
@ -157,20 +180,23 @@ typedef struct {
} InputBoxStateT; } InputBoxStateT;
typedef struct { typedef struct {
bool done; DialogKeysT keys; // must be first (window userData)
bool accepted; bool done;
int32_t *outVal; bool accepted;
WidgetT *spinner; int32_t *outVal;
WidgetT *spinner;
} IntBoxStateT; } IntBoxStateT;
typedef struct { typedef struct {
bool done; DialogKeysT keys; // must be first (window userData)
bool accepted; bool done;
int32_t *outIdx; bool accepted;
WidgetT *listBox; int32_t *outIdx;
WidgetT *listBox;
} ChoiceBoxStateT; } ChoiceBoxStateT;
typedef struct { typedef struct {
DialogKeysT keys; // must be first (window userData)
AppContextT *ctx; AppContextT *ctx;
bool done; // set true to break modal loop bool done; // set true to break modal loop
bool accepted; // true if user clicked OK/Open/Save bool accepted; // true if user clicked OK/Open/Save
@ -205,6 +231,8 @@ static void cbOnCancel(WidgetT *w);
static void cbOnClose(WindowT *win); static void cbOnClose(WindowT *win);
static void cbOnDblClick(WidgetT *w); static void cbOnDblClick(WidgetT *w);
static void cbOnOk(WidgetT *w); static void cbOnOk(WidgetT *w);
static void dialogCenter(AppContextT *ctx, WindowT *win);
static void dialogOnKey(WindowT *win, int32_t key, int32_t mod);
static void drawIconCircle(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t color); static void drawIconCircle(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t color);
static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color); static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color);
static bool fdAcceptFile(const char *name); static bool fdAcceptFile(const char *name);
@ -266,7 +294,14 @@ static void cbOnOk(WidgetT *w) {
// listbox failed to create, returning true would hand the caller // listbox failed to create, returning true would hand the caller
// an unwritten *outIdx. // an unwritten *outIdx.
if (sChoiceBox.listBox && sChoiceBox.outIdx) { if (sChoiceBox.listBox && sChoiceBox.outIdx) {
*sChoiceBox.outIdx = wgtListBoxGetSelected(sChoiceBox.listBox); int32_t sel = wgtListBoxGetSelected(sChoiceBox.listBox);
// No highlighted item: OK is a no-op, the dialog stays up.
if (sel < 0) {
return;
}
*sChoiceBox.outIdx = sel;
sChoiceBox.accepted = true; sChoiceBox.accepted = true;
} }
@ -274,6 +309,70 @@ static void cbOnOk(WidgetT *w) {
} }
// Center a dialog's frame on the screen once its final size is known.
// dvxCreateWindow auto-cascades a window that lands on another's origin
// and dvxFitWindow changes the size after creation; both would leave a
// dialog off-center.
static void dialogCenter(AppContextT *ctx, WindowT *win) {
int32_t x = (ctx->display.width - win->w) / 2;
int32_t y = (ctx->display.height - win->h) / 2;
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
win->x = x;
win->y = y;
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
}
// Window-level key hook installed on every dialog. Escape activates
// the cancel button (the last button of a message box). Enter runs the
// widget layer first, then activates the default button unless the
// focused widget is itself a button (it already pressed itself) or one
// of the dialog's Enter owners (path input, file list, ...).
static void dialogOnKey(WindowT *win, int32_t key, int32_t mod) {
DialogKeysT *keys = (DialogKeysT *)win->userData;
WidgetT *focus = wgtGetFocused();
if (key == KEY_ESCAPE) {
if (keys->cancelBtn && keys->cancelBtn->onClick) {
keys->cancelBtn->onClick(keys->cancelBtn);
}
return;
}
widgetOnKey(win, key, mod);
if (key != KEY_ENTER || !keys->defaultBtn || !keys->defaultBtn->onClick) {
return;
}
if (focus) {
if (focus->wclass == keys->defaultBtn->wclass) {
return;
}
for (int32_t i = 0; i < DLG_ENTER_OWNERS_MAX; i++) {
if (keys->enterOwners[i] == focus) {
return;
}
}
}
keys->defaultBtn->onClick(keys->defaultBtn);
}
// Draws the shared 2-pixel ring used by the circular message-box icons // Draws the shared 2-pixel ring used by the circular message-box icons
// (info, error, question) via the integer distance-squared test. // (info, error, question) via the integer distance-squared test.
static void drawIconCircle(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t color) { static void drawIconCircle(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t color) {
@ -370,14 +469,7 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co
int32_t contentH = CB_PADDING + promptH + CB_PADDING + CB_LIST_HEIGHT + CB_PADDING + BUTTON_HEIGHT + CB_PADDING; int32_t contentH = CB_PADDING + promptH + CB_PADDING + CB_LIST_HEIGHT + CB_PADDING + BUTTON_HEIGHT + CB_PADDING;
int32_t contentW = CB_DIALOG_WIDTH; int32_t contentW = CB_DIALOG_WIDTH;
int32_t winX = (ctx->display.width - contentW) / 2 - CHROME_TOTAL_SIDE; WindowT *win = dvxCreateWindowCentered(ctx, title ? title : "Choose", contentW + CHROME_TOTAL_SIDE * 2, contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM, false);
int32_t winY = (ctx->display.height - contentH) / 2 - CHROME_TOTAL_TOP;
WindowT *win = dvxCreateWindow(ctx, title ? title : "Choose",
winX, winY,
contentW + CHROME_TOTAL_SIDE * 2,
contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM,
false);
if (!win) { if (!win) {
return false; return false;
@ -388,13 +480,14 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co
win->maxW = win->w; win->maxW = win->w;
win->maxH = win->h; win->maxH = win->h;
sChoiceBox.done = false; memset(&sChoiceBox, 0, sizeof(sChoiceBox));
sChoiceBox.accepted = false; sChoiceBox.outIdx = outIdx;
sChoiceBox.outIdx = outIdx;
sChoiceBox.listBox = NULL;
WidgetT *root = wgtInitWindow(ctx, win); WidgetT *root = wgtInitWindow(ctx, win);
win->userData = &sChoiceBox;
win->onKey = dialogOnKey;
if (root) { if (root) {
if (prompt && prompt[0]) { if (prompt && prompt[0]) {
wgtLabel(root, prompt); wgtLabel(root, prompt);
@ -412,7 +505,8 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co
wgtListBoxSetSelected(lb, defaultIdx); wgtListBoxSetSelected(lb, defaultIdx);
} }
sChoiceBox.listBox = lb; sChoiceBox.listBox = lb;
sChoiceBox.keys.enterOwners[0] = lb;
} }
WidgetT *btnRow = wgtHBox(root); WidgetT *btnRow = wgtHBox(root);
@ -423,22 +517,25 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co
WidgetT *okBtn = wgtButton(btnRow, "&OK"); WidgetT *okBtn = wgtButton(btnRow, "&OK");
if (okBtn) { if (okBtn) {
okBtn->minW = wgtPixels(BUTTON_WIDTH); okBtn->minW = wgtPixels(BUTTON_WIDTH);
okBtn->minH = wgtPixels(BUTTON_HEIGHT); okBtn->minH = wgtPixels(BUTTON_HEIGHT);
okBtn->onClick = cbOnOk; okBtn->onClick = cbOnOk;
sChoiceBox.keys.defaultBtn = okBtn;
} }
WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
if (cancelBtn) { if (cancelBtn) {
cancelBtn->minW = wgtPixels(BUTTON_WIDTH); cancelBtn->minW = wgtPixels(BUTTON_WIDTH);
cancelBtn->minH = wgtPixels(BUTTON_HEIGHT); cancelBtn->minH = wgtPixels(BUTTON_HEIGHT);
cancelBtn->onClick = cbOnCancel; cancelBtn->onClick = cbOnCancel;
sChoiceBox.keys.cancelBtn = cancelBtn;
} }
} }
} }
dvxFitWindow(ctx, win); dvxFitWindow(ctx, win);
dialogCenter(ctx, win);
WindowT *prevModal = ctx->modalWindow; WindowT *prevModal = ctx->modalWindow;
ctx->modalWindow = win; ctx->modalWindow = win;
@ -455,6 +552,35 @@ bool dvxChoiceDialog(AppContextT *ctx, const char *title, const char *prompt, co
} }
void dvxDialogTestReset(void) {
fdFreeEntries();
memset(&sMsgBox, 0, sizeof(sMsgBox));
memset(&sInputBox, 0, sizeof(sInputBox));
memset(&sIntBox, 0, sizeof(sIntBox));
memset(&sChoiceBox, 0, sizeof(sChoiceBox));
memset(&sFd, 0, sizeof(sFd));
}
// Creates and runs a modal message box. The flags parameter is a bitmask:
// low nibble selects button set (MB_OK, MB_YESNO, etc.), next nibble
// selects icon type (MB_ICONINFO, MB_ICONERROR, etc.). This is the same
// flag encoding Windows MessageBox uses, which makes porting code easier.
//
// The dialog is auto-sized to fit the word-wrapped message text plus the
// button row. Non-resizable (maxW/maxH clamped to initial size) because
// resizing a message box serves no purpose.
//
// Button labels use '&' to mark accelerator keys (e.g., "&OK" makes
// Alt+O activate the button). Button IDs are stored in widget->userData
// via intptr_t cast -- a common pattern when you need to associate a
// small integer with a widget without allocating a separate struct.
int32_t dvxErrorBox(AppContextT *ctx, const char *title, const char *message) {
return dvxMessageBox(ctx, title ? title : "Error", message, MB_OK | MB_ICONERROR);
}
// Creates a modal file dialog using the widget system. The layout is: // Creates a modal file dialog using the widget system. The layout is:
// - Path input (with Enter-to-navigate) // - Path input (with Enter-to-navigate)
// - File listbox (single-click selects, double-click opens/accepts) // - File listbox (single-click selects, double-click opens/accepts)
@ -493,12 +619,7 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
sFd.curDir[DVX_MAX_PATH - 1] = '\0'; sFd.curDir[DVX_MAX_PATH - 1] = '\0';
// Create dialog window // Create dialog window
int32_t dlgW = FD_DIALOG_WIDTH; WindowT *win = dvxCreateWindowCentered(ctx, title ? title : ((flags & FD_SAVE) ? "Save As" : "Open"), FD_DIALOG_WIDTH, FD_DIALOG_HEIGHT, false);
int32_t dlgH = FD_DIALOG_HEIGHT;
int32_t winX = (ctx->display.width - dlgW) / 2;
int32_t winY = (ctx->display.height - dlgH) / 2;
WindowT *win = dvxCreateWindow(ctx, title ? title : ((flags & FD_SAVE) ? "Save As" : "Open"), winX, winY, dlgW, dlgH, false);
if (!win) { if (!win) {
return false; return false;
@ -514,6 +635,9 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
return false; return false;
} }
win->userData = &sFd;
win->onKey = dialogOnKey;
// Path row // Path row
WidgetT *pathRow = wgtHBox(root); WidgetT *pathRow = wgtHBox(root);
@ -527,6 +651,7 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
// mid-edit (and stat() on each key). // mid-edit (and stat() on each key).
if (sFd.pathInput) { if (sFd.pathInput) {
sFd.pathInput->onKeyDown = fdOnPathKey; sFd.pathInput->onKeyDown = fdOnPathKey;
sFd.keys.enterOwners[0] = sFd.pathInput;
} }
} }
@ -537,6 +662,7 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
sFd.fileList->weight = WGT_WEIGHT_FILL; sFd.fileList->weight = WGT_WEIGHT_FILL;
sFd.fileList->onChange = fdOnListClick; sFd.fileList->onChange = fdOnListClick;
sFd.fileList->onDblClick = fdOnListDblClick; sFd.fileList->onDblClick = fdOnListDblClick;
sFd.keys.enterOwners[1] = sFd.fileList;
} }
// Filter row (if filters provided) // Filter row (if filters provided)
@ -557,7 +683,8 @@ 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);
sFd.filterDd->onChange = fdOnFilterChange; sFd.filterDd->onChange = fdOnFilterChange;
sFd.keys.enterOwners[2] = sFd.filterDd;
} }
} }
@ -578,8 +705,9 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
WidgetT *okBtn = wgtButton(btnRow, (flags & FD_SAVE) ? "&Save" : "&Open"); WidgetT *okBtn = wgtButton(btnRow, (flags & FD_SAVE) ? "&Save" : "&Open");
if (okBtn) { if (okBtn) {
okBtn->onClick = fdOnOk; okBtn->onClick = fdOnOk;
okBtn->minW = wgtPixels(BUTTON_WIDTH); okBtn->minW = wgtPixels(BUTTON_WIDTH);
sFd.keys.defaultBtn = okBtn;
} }
WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
@ -587,12 +715,14 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
if (cancelBtn) { if (cancelBtn) {
cancelBtn->onClick = fdOnCancel; cancelBtn->onClick = fdOnCancel;
cancelBtn->minW = wgtPixels(BUTTON_WIDTH); cancelBtn->minW = wgtPixels(BUTTON_WIDTH);
sFd.keys.cancelBtn = cancelBtn;
} }
} }
// Load initial directory // Load initial directory
fdLoadDir(); fdLoadDir();
wgtInvalidate(root); wgtInvalidate(root);
dialogCenter(ctx, win);
// Modal loop. Save/restore the previous modal so a file dialog // Modal loop. Save/restore the previous modal so a file dialog
// opened from inside another modal (e.g. a Browse button) does not // opened from inside another modal (e.g. a Browse button) does not
@ -634,6 +764,11 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
} }
int32_t dvxInfoBox(AppContextT *ctx, const char *title, const char *message) {
return dvxMessageBox(ctx, title ? title : "Information", message, MB_OK | MB_ICONINFO);
}
// Modal input dialog with prompt, text field, OK/Cancel buttons. // Modal input dialog with prompt, text field, OK/Cancel buttons.
// Follows the same nested-event-loop pattern as dvxMessageBox. // Follows the same nested-event-loop pattern as dvxMessageBox.
@ -652,14 +787,7 @@ bool dvxInputBox(AppContextT *ctx, const char *title, const char *prompt, const
int32_t contentH = IB_PADDING + promptH + IB_PADDING + ctx->font.charHeight + 4 + IB_PADDING + BUTTON_HEIGHT + IB_PADDING; int32_t contentH = IB_PADDING + promptH + IB_PADDING + ctx->font.charHeight + 4 + IB_PADDING + BUTTON_HEIGHT + IB_PADDING;
int32_t contentW = IB_DIALOG_WIDTH; int32_t contentW = IB_DIALOG_WIDTH;
int32_t winX = (ctx->display.width - contentW) / 2 - CHROME_TOTAL_SIDE; WindowT *win = dvxCreateWindowCentered(ctx, title ? title : "Input", contentW + CHROME_TOTAL_SIDE * 2, contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM, false);
int32_t winY = (ctx->display.height - contentH) / 2 - CHROME_TOTAL_TOP;
WindowT *win = dvxCreateWindow(ctx, title ? title : "Input",
winX, winY,
contentW + CHROME_TOTAL_SIDE * 2,
contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM,
false);
if (!win) { if (!win) {
return false; return false;
@ -670,14 +798,16 @@ bool dvxInputBox(AppContextT *ctx, const char *title, const char *prompt, const
win->maxW = win->w; win->maxW = win->w;
win->maxH = win->h; win->maxH = win->h;
sInputBox.ctx = ctx; memset(&sInputBox, 0, sizeof(sInputBox));
sInputBox.done = false; sInputBox.ctx = ctx;
sInputBox.accepted = false; sInputBox.outBuf = outBuf;
sInputBox.outBuf = outBuf;
sInputBox.outBufSize = outBufSize; sInputBox.outBufSize = outBufSize;
WidgetT *root = wgtInitWindow(ctx, win); WidgetT *root = wgtInitWindow(ctx, win);
win->userData = &sInputBox;
win->onKey = dialogOnKey;
if (root) { if (root) {
// Prompt label // Prompt label
if (prompt && prompt[0]) { if (prompt && prompt[0]) {
@ -706,22 +836,25 @@ bool dvxInputBox(AppContextT *ctx, const char *title, const char *prompt, const
WidgetT *okBtn = wgtButton(btnRow, "&OK"); WidgetT *okBtn = wgtButton(btnRow, "&OK");
if (okBtn) { if (okBtn) {
okBtn->minW = wgtPixels(BUTTON_WIDTH); okBtn->minW = wgtPixels(BUTTON_WIDTH);
okBtn->minH = wgtPixels(BUTTON_HEIGHT); okBtn->minH = wgtPixels(BUTTON_HEIGHT);
okBtn->onClick = ibOnOk; okBtn->onClick = ibOnOk;
sInputBox.keys.defaultBtn = okBtn;
} }
WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
if (cancelBtn) { if (cancelBtn) {
cancelBtn->minW = wgtPixels(BUTTON_WIDTH); cancelBtn->minW = wgtPixels(BUTTON_WIDTH);
cancelBtn->minH = wgtPixels(BUTTON_HEIGHT); cancelBtn->minH = wgtPixels(BUTTON_HEIGHT);
cancelBtn->onClick = ibOnCancel; cancelBtn->onClick = ibOnCancel;
sInputBox.keys.cancelBtn = cancelBtn;
} }
} }
} }
dvxFitWindow(ctx, win); dvxFitWindow(ctx, win);
dialogCenter(ctx, win);
WindowT *prevModal = ctx->modalWindow; WindowT *prevModal = ctx->modalWindow;
ctx->modalWindow = win; ctx->modalWindow = win;
@ -753,14 +886,7 @@ bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int
int32_t contentH = IB_PADDING + promptH + IB_PADDING + ctx->font.charHeight + 8 + IB_PADDING + BUTTON_HEIGHT + IB_PADDING; int32_t contentH = IB_PADDING + promptH + IB_PADDING + ctx->font.charHeight + 8 + IB_PADDING + BUTTON_HEIGHT + IB_PADDING;
int32_t contentW = IB_DIALOG_WIDTH; int32_t contentW = IB_DIALOG_WIDTH;
int32_t winX = (ctx->display.width - contentW) / 2 - CHROME_TOTAL_SIDE; WindowT *win = dvxCreateWindowCentered(ctx, title ? title : "Input", contentW + CHROME_TOTAL_SIDE * 2, contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM, false);
int32_t winY = (ctx->display.height - contentH) / 2 - CHROME_TOTAL_TOP;
WindowT *win = dvxCreateWindow(ctx, title ? title : "Input",
winX, winY,
contentW + CHROME_TOTAL_SIDE * 2,
contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM,
false);
if (!win) { if (!win) {
return false; return false;
@ -771,13 +897,14 @@ bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int
win->maxW = win->w; win->maxW = win->w;
win->maxH = win->h; win->maxH = win->h;
sIntBox.done = false; memset(&sIntBox, 0, sizeof(sIntBox));
sIntBox.accepted = false; sIntBox.outVal = outVal;
sIntBox.outVal = outVal;
sIntBox.spinner = NULL;
WidgetT *root = wgtInitWindow(ctx, win); WidgetT *root = wgtInitWindow(ctx, win);
win->userData = &sIntBox;
win->onKey = dialogOnKey;
if (root) { if (root) {
if (prompt && prompt[0]) { if (prompt && prompt[0]) {
wgtLabel(root, prompt); wgtLabel(root, prompt);
@ -798,22 +925,25 @@ bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int
WidgetT *okBtn = wgtButton(btnRow, "&OK"); WidgetT *okBtn = wgtButton(btnRow, "&OK");
if (okBtn) { if (okBtn) {
okBtn->minW = wgtPixels(BUTTON_WIDTH); okBtn->minW = wgtPixels(BUTTON_WIDTH);
okBtn->minH = wgtPixels(BUTTON_HEIGHT); okBtn->minH = wgtPixels(BUTTON_HEIGHT);
okBtn->onClick = iibOnOk; okBtn->onClick = iibOnOk;
sIntBox.keys.defaultBtn = okBtn;
} }
WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
if (cancelBtn) { if (cancelBtn) {
cancelBtn->minW = wgtPixels(BUTTON_WIDTH); cancelBtn->minW = wgtPixels(BUTTON_WIDTH);
cancelBtn->minH = wgtPixels(BUTTON_HEIGHT); cancelBtn->minH = wgtPixels(BUTTON_HEIGHT);
cancelBtn->onClick = iibOnCancel; cancelBtn->onClick = iibOnCancel;
sIntBox.keys.cancelBtn = cancelBtn;
} }
} }
} }
dvxFitWindow(ctx, win); dvxFitWindow(ctx, win);
dialogCenter(ctx, win);
WindowT *prevModal = ctx->modalWindow; WindowT *prevModal = ctx->modalWindow;
ctx->modalWindow = win; ctx->modalWindow = win;
@ -830,30 +960,6 @@ bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int
} }
// Creates and runs a modal message box. The flags parameter is a bitmask:
// low nibble selects button set (MB_OK, MB_YESNO, etc.), next nibble
// selects icon type (MB_ICONINFO, MB_ICONERROR, etc.). This is the same
// flag encoding Windows MessageBox uses, which makes porting code easier.
//
// The dialog is auto-sized to fit the word-wrapped message text plus the
// button row. Non-resizable (maxW/maxH clamped to initial size) because
// resizing a message box serves no purpose.
//
// Button labels use '&' to mark accelerator keys (e.g., "&OK" makes
// Alt+O activate the button). Button IDs are stored in widget->userData
// via intptr_t cast -- a common pattern when you need to associate a
// small integer with a widget without allocating a separate struct.
int32_t dvxErrorBox(AppContextT *ctx, const char *title, const char *message) {
return dvxMessageBox(ctx, title ? title : "Error", message, MB_OK | MB_ICONERROR);
}
int32_t dvxInfoBox(AppContextT *ctx, const char *title, const char *message) {
return dvxMessageBox(ctx, title ? title : "Information", message, MB_OK | MB_ICONINFO);
}
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags) { int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags) {
if (!ctx || !message) { if (!ctx || !message) {
return ID_CANCEL; return ID_CANCEL;
@ -938,24 +1044,19 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message,
int32_t contentH = msgAreaH + BUTTON_HEIGHT + MSG_PADDING * 3; int32_t contentH = msgAreaH + BUTTON_HEIGHT + MSG_PADDING * 3;
// Create the dialog window (non-resizable) // Create the dialog window (non-resizable)
int32_t winX = (ctx->display.width - contentW) / 2 - CHROME_TOTAL_SIDE; WindowT *win = dvxCreateWindowCentered(ctx, title, contentW + CHROME_TOTAL_SIDE * 2, contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM, false);
int32_t winY = (ctx->display.height - contentH) / 2 - CHROME_TOTAL_TOP;
WindowT *win = dvxCreateWindow(ctx, title, winX, winY,
contentW + CHROME_TOTAL_SIDE * 2,
contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM,
false);
if (!win) { if (!win) {
return ID_CANCEL; return ID_CANCEL;
} }
dialogCenter(ctx, win);
win->modal = true; win->modal = true;
// Set up state // Set up state
memset(&sMsgBox, 0, sizeof(sMsgBox));
sMsgBox.ctx = ctx; sMsgBox.ctx = ctx;
sMsgBox.result = ID_CANCEL; sMsgBox.result = ID_CANCEL;
sMsgBox.done = false;
sMsgBox.message = message; sMsgBox.message = message;
sMsgBox.iconType = iconFlags; sMsgBox.iconType = iconFlags;
sMsgBox.textX = MSG_PADDING + (hasIcon ? ICON_AREA_WIDTH : 0); sMsgBox.textX = MSG_PADDING + (hasIcon ? ICON_AREA_WIDTH : 0);
@ -970,6 +1071,7 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message,
// Override onPaint with our custom handler, set window-level state // Override onPaint with our custom handler, set window-level state
win->userData = &sMsgBox; win->userData = &sMsgBox;
win->onPaint = onMsgBoxPaint; win->onPaint = onMsgBoxPaint;
win->onKey = dialogOnKey;
win->onClose = onMsgBoxClose; win->onClose = onMsgBoxClose;
win->maxW = win->w; win->maxW = win->w;
win->maxH = win->h; win->maxH = win->h;
@ -996,6 +1098,13 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message,
btn->minH = wgtPixels(BUTTON_HEIGHT); btn->minH = wgtPixels(BUTTON_HEIGHT);
btn->userData = (void *)(intptr_t)btnIds[i]; btn->userData = (void *)(intptr_t)btnIds[i];
btn->onClick = onButtonClick; btn->onClick = onButtonClick;
// Enter = first button, Escape = last (OK / Cancel / No)
if (i == 0) {
sMsgBox.keys.defaultBtn = btn;
}
sMsgBox.keys.cancelBtn = btn;
} }
} }
} }
@ -1123,7 +1232,7 @@ static int fdEntryCompare(const void *a, const void *b) {
return sFd.entryIsDir[ia] ? -1 : 1; return sFd.entryIsDir[ia] ? -1 : 1;
} }
return stricmp(sFd.entryNames[ia], sFd.entryNames[ib]); return strcasecmp(sFd.entryNames[ia], sFd.entryNames[ib]);
} }
@ -1433,8 +1542,10 @@ static void fdNavigate(const char *path) {
return; return;
} }
// Canonicalize the path // Canonicalize the path. realpath needs a PATH_MAX buffer (glibc's
char canon[DVX_MAX_PATH]; // fortified build aborts on anything smaller); the result is then
// clipped into curDir.
char canon[PATH_MAX];
if (realpath(resolved, canon) != NULL) { if (realpath(resolved, canon) != NULL) {
strncpy(sFd.curDir, canon, DVX_MAX_PATH - 1); strncpy(sFd.curDir, canon, DVX_MAX_PATH - 1);

View file

@ -195,6 +195,27 @@ char accelParse(const char *text) {
} }
// ============================================================
// calcCenteredText
// ============================================================
//
// Shared by every widget that centers a text label in its own rect
// (buttons, checkbox labels, etc.). Centralising the arithmetic
// avoids tiny off-by-one drift from copy-paste variations.
void calcCenteredText(const BitmapFontT *font, int32_t rectX, int32_t rectY, int32_t rectW, int32_t rectH, const char *text, int32_t *outX, int32_t *outY) {
int32_t textW = textWidthAccel(font, text);
if (outX) {
*outX = rectX + (rectW - textW) / 2;
}
if (outY) {
*outY = rectY + (rectH - font->charHeight) / 2;
}
}
// ============================================================ // ============================================================
// clipRect // clipRect
// ============================================================ // ============================================================
@ -633,65 +654,6 @@ void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w
} }
// ============================================================
// calcCenteredText
// ============================================================
//
// Shared by every widget that centers a text label in its own rect
// (buttons, checkbox labels, etc.). Centralising the arithmetic
// avoids tiny off-by-one drift from copy-paste variations.
void calcCenteredText(const BitmapFontT *font, int32_t rectX, int32_t rectY, int32_t rectW, int32_t rectH, const char *text, int32_t *outX, int32_t *outY) {
int32_t textW = textWidthAccel(font, text);
if (outX) {
*outX = rectX + (rectW - textW) / 2;
}
if (outY) {
*outY = rectY + (rectH - font->charHeight) / 2;
}
}
// ============================================================
// drawPressableBevel
// ============================================================
//
// Standard 2px button / toggle bevel. Swaps highlight and shadow when
// pressed to create the sunken look. face fills the interior (pass 0
// to leave the underlying content alone, e.g. for transparent toggle
// buttons).
void drawPressableBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, bool pressed, uint32_t face, const ColorSchemeT *colors) {
BevelStyleT bevel;
bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight;
bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow;
bevel.face = face;
bevel.width = 2;
drawBevel(d, ops, x, y, w, h, &bevel);
}
// ============================================================
// drawWidgetTextAccel
// ============================================================
//
// Every text-bearing widget paints this same enabled/disabled branch.
// Centralising it keeps the embossed-disabled appearance consistent
// and removes ~5 lines of boilerplate per widget.
void drawWidgetTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque, bool enabled, const ColorSchemeT *colors) {
if (!enabled) {
drawTextAccel(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
drawTextAccel(d, ops, font, x, y, text, colors->windowShadow, 0, false);
return;
}
drawTextAccel(d, ops, font, x, y, text, fg, bg, opaque);
}
// ============================================================ // ============================================================
// drawInit // drawInit
// ============================================================ // ============================================================
@ -839,6 +801,25 @@ void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, in
} }
// ============================================================
// drawPressableBevel
// ============================================================
//
// Standard 2px button / toggle bevel. Swaps highlight and shadow when
// pressed to create the sunken look. face fills the interior (pass 0
// to leave the underlying content alone, e.g. for transparent toggle
// buttons).
void drawPressableBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, bool pressed, uint32_t face, const ColorSchemeT *colors) {
BevelStyleT bevel;
bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight;
bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow;
bevel.face = face;
bevel.width = 2;
drawBevel(d, ops, x, y, w, h, &bevel);
}
// Draws a 1px rectangle outline (top/bottom HLine, left/right VLine) in a // Draws a 1px rectangle outline (top/bottom HLine, left/right VLine) in a
// single color. Factors the repeated 4-line outline idiom. // single color. Factors the repeated 4-line outline idiom.
void drawRectOutline(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color) { void drawRectOutline(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color) {
@ -1393,6 +1374,25 @@ void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h
} }
// ============================================================
// drawWidgetTextAccel
// ============================================================
//
// Every text-bearing widget paints this same enabled/disabled branch.
// Centralising it keeps the embossed-disabled appearance consistent
// and removes ~5 lines of boilerplate per widget.
void drawWidgetTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque, bool enabled, const ColorSchemeT *colors) {
if (!enabled) {
drawTextAccel(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
drawTextAccel(d, ops, font, x, y, text, colors->windowShadow, 0, false);
return;
}
drawTextAccel(d, ops, font, x, y, text, fg, bg, opaque);
}
// ============================================================ // ============================================================
// putPixel // putPixel
// ============================================================ // ============================================================

View file

@ -38,12 +38,12 @@
extern int32_t *dvxMemAppIdPtr; extern int32_t *dvxMemAppIdPtr;
void *dvxMalloc(size_t size);
void *dvxCalloc(size_t nmemb, size_t size); void *dvxCalloc(size_t nmemb, size_t size);
void *dvxRealloc(void *ptr, size_t size);
void dvxFree(void *ptr); void dvxFree(void *ptr);
char *dvxStrdup(const char *s); void *dvxMalloc(size_t size);
uint32_t dvxMemGetAppUsage(int32_t appId); uint32_t dvxMemGetAppUsage(int32_t appId);
void dvxMemResetApp(int32_t appId); void dvxMemResetApp(int32_t appId);
void *dvxRealloc(void *ptr, size_t size);
char *dvxStrdup(const char *s);
#endif // DVX_MEM_H #endif // DVX_MEM_H

View file

@ -59,8 +59,7 @@
#define PAL_CHROME_WHITE 239 #define PAL_CHROME_WHITE 239
// Generate the default 8-bit palette into a 768-byte buffer (256 * 3, RGB) // Generate the default 8-bit palette into a 768-byte buffer (256 * 3, RGB)
static inline void dvxGeneratePalette(uint8_t *pal) static inline void dvxGeneratePalette(uint8_t *pal) {
{
int32_t idx = 0; int32_t idx = 0;
// Entries 0-215: 6x6x6 color cube // Entries 0-215: 6x6x6 color cube
@ -133,6 +132,7 @@ static inline void dvxGeneratePalette(uint8_t *pal)
} }
} }
// Find the nearest palette entry for an RGB color using minimum Euclidean // Find the nearest palette entry for an RGB color using minimum Euclidean
// distance in RGB space. The two-phase approach avoids a full 256-entry // distance in RGB space. The two-phase approach avoids a full 256-entry
// linear scan in the common case: // linear scan in the common case:
@ -142,8 +142,7 @@ static inline void dvxGeneratePalette(uint8_t *pal)
// (indices 216-239) to see if any is closer than the cube match. // (indices 216-239) to see if any is closer than the cube match.
// Entries 240-255 are reserved (black) and skipped to avoid false matches. // Entries 240-255 are reserved (black) and skipped to avoid false matches.
// This is called by packColor() in 8-bit mode, so it needs to be fast. // This is called by packColor() in 8-bit mode, so it needs to be fast.
static inline uint8_t dvxNearestPalEntry(const uint8_t *pal, uint8_t r, uint8_t g, uint8_t b) static inline uint8_t dvxNearestPalEntry(const uint8_t *pal, uint8_t r, uint8_t g, uint8_t b) {
{
// Snap to nearest cube vertex: +25 rounds to nearest 51-step level // Snap to nearest cube vertex: +25 rounds to nearest 51-step level
int32_t ri = (r + 25) / 51; int32_t ri = (r + 25) / 51;
int32_t gi = (g + 25) / 51; int32_t gi = (g + 25) / 51;

View file

@ -133,27 +133,6 @@ static void freeEntry(PrefsEntryT *e) {
} }
static int strcmpci(const char *a, const char *b) {
for (;;) {
int d = tolower((unsigned char)*a) - tolower((unsigned char)*b);
if (d != 0 || !*a) {
return d;
}
a++;
b++;
}
}
static char *trimInPlace(char *buf) {
char *p = (char *)dvxSkipWs(buf);
dvxTrimRight(p);
return p;
}
// ============================================================ // ============================================================
// Public API // Public API
// ============================================================ // ============================================================
@ -445,3 +424,25 @@ void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const
newEntry.value = dupStr(value); newEntry.value = dupStr(value);
arrins(h->entries, insertAt, newEntry); arrins(h->entries, insertAt, newEntry);
} }
static int strcmpci(const char *a, const char *b) {
for (;;) {
int d = tolower((unsigned char)*a) - tolower((unsigned char)*b);
if (d != 0 || !*a) {
return d;
}
a++;
b++;
}
}
static char *trimInPlace(char *buf) {
char *p = (char *)dvxSkipWs(buf);
dvxTrimRight(p);
return p;
}

View file

@ -312,34 +312,6 @@ typedef struct WidgetT {
} WidgetT; } WidgetT;
// ============================================================
// Typed dispatch helpers
// ============================================================
//
// Each wclsFoo() inline function extracts a handler by stable method ID,
// casts it to the correct function pointer type, and calls it with a
// NULL check. This gives callers type-safe dispatch with the same
// codegen as a direct struct field call.
static inline bool wclsHas(const WidgetT *w, int32_t methodId) {
return w->wclass && w->wclass->handlers[methodId] != NULL;
}
static inline void wclsPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
typedef void (*FnT)(WidgetT *, DisplayT *, const BlitOpsT *, const BitmapFontT *, const ColorSchemeT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_PAINT] : NULL;
if (fn) { fn(w, d, ops, font, colors); }
}
static inline void wclsPaintOverlay(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
typedef void (*FnT)(WidgetT *, DisplayT *, const BlitOpsT *, const BitmapFontT *, const ColorSchemeT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_PAINT_OVERLAY] : NULL;
if (fn) { fn(w, d, ops, font, colors); }
}
static inline void wclsCalcMinSize(WidgetT *w, const BitmapFontT *font) { static inline void wclsCalcMinSize(WidgetT *w, const BitmapFontT *font) {
typedef void (*FnT)(WidgetT *, const BitmapFontT *); typedef void (*FnT)(WidgetT *, const BitmapFontT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CALC_MIN_SIZE] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CALC_MIN_SIZE] : NULL;
@ -347,52 +319,20 @@ static inline void wclsCalcMinSize(WidgetT *w, const BitmapFontT *font) {
} }
static inline void wclsLayout(WidgetT *w, const BitmapFontT *font) { static inline bool wclsClearSelection(WidgetT *w) {
typedef void (*FnT)(WidgetT *, const BitmapFontT *); typedef bool (*FnT)(WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_LAYOUT] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CLEAR_SELECTION] : NULL;
if (fn) { fn(w, font); } return fn ? fn(w) : false;
} }
static inline void wclsGetLayoutMetrics(const WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW) { static inline void wclsClosePopup(WidgetT *w) {
typedef void (*FnT)(const WidgetT *, const BitmapFontT *, int32_t *, int32_t *, int32_t *, int32_t *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_LAYOUT_METRICS] : NULL;
if (fn) { fn(w, font, pad, gap, extraTop, borderW); }
}
static inline void wclsOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
typedef void (*FnT)(WidgetT *, WidgetT *, int32_t, int32_t);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_MOUSE] : NULL;
if (fn) { fn(w, root, vx, vy); }
}
static inline void wclsOnKey(WidgetT *w, int32_t key, int32_t mod) {
typedef void (*FnT)(WidgetT *, int32_t, int32_t);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_KEY] : NULL;
if (fn) { fn(w, key, mod); }
}
// Dispatched on the widget LOSING keyboard focus, before the app-level
// onBlur callback. Editing widgets (e.g. the spinner) commit/clamp their
// in-progress text here. Must be called from every focus-loss transition
// (mouse focus change, Tab/accel navigation, window blur, wgtSetFocused).
static inline void wclsOnBlur(WidgetT *w) {
typedef void (*FnT)(WidgetT *); typedef void (*FnT)(WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_BLUR] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CLOSE_POPUP] : NULL;
if (fn) { fn(w); } if (fn) { fn(w); }
} }
static inline void wclsOnAccelActivate(WidgetT *w, WidgetT *root) {
typedef void (*FnT)(WidgetT *, WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_ACCEL_ACTIVATE] : NULL;
if (fn) { fn(w, root); }
}
// Default destroy behavior: if the widget's class does not supply an // Default destroy behavior: if the widget's class does not supply an
// explicit WGT_METHOD_DESTROY handler, free w->data automatically (and // explicit WGT_METHOD_DESTROY handler, free w->data automatically (and
// the text-at-offset-0 first, if WCLASS_HAS_TEXT is set). This removes // the text-at-offset-0 first, if WCLASS_HAS_TEXT is set). This removes
@ -416,38 +356,17 @@ static inline void wclsDestroy(WidgetT *w) {
} }
static inline void wclsOnChildChanged(WidgetT *parent, WidgetT *child) { static inline int32_t wclsGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy) {
typedef void (*FnT)(WidgetT *, WidgetT *); typedef int32_t (*FnT)(const WidgetT *, int32_t, int32_t);
FnT fn = parent->wclass ? (FnT)parent->wclass->handlers[WGT_METHOD_ON_CHILD_CHANGED] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_CURSOR_SHAPE] : NULL;
if (fn) { fn(parent, child); } return fn ? fn(w, vx, vy) : 0;
} }
static inline const char *wclsGetText(const WidgetT *w) { static inline void wclsGetLayoutMetrics(const WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW) {
typedef const char *(*FnT)(const WidgetT *); typedef void (*FnT)(const WidgetT *, const BitmapFontT *, int32_t *, int32_t *, int32_t *, int32_t *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_TEXT] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_LAYOUT_METRICS] : NULL;
return fn ? fn(w) : ""; if (fn) { fn(w, font, pad, gap, extraTop, borderW); }
}
static inline void wclsSetText(WidgetT *w, const char *text) {
typedef void (*FnT)(WidgetT *, const char *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_SET_TEXT] : NULL;
if (fn) { fn(w, text); }
}
static inline bool wclsClearSelection(WidgetT *w) {
typedef bool (*FnT)(WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CLEAR_SELECTION] : NULL;
return fn ? fn(w) : false;
}
static inline void wclsClosePopup(WidgetT *w) {
typedef void (*FnT)(WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CLOSE_POPUP] : NULL;
if (fn) { fn(w); }
} }
@ -458,10 +377,56 @@ static inline void wclsGetPopupRect(const WidgetT *w, const BitmapFontT *font, i
} }
static inline void wclsOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { static inline const char *wclsGetText(const WidgetT *w) {
typedef void (*FnT)(WidgetT *, WidgetT *, int32_t, int32_t); typedef const char *(*FnT)(const WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_DRAG_UPDATE] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_TEXT] : NULL;
if (fn) { fn(w, root, x, y); } return fn ? fn(w) : "";
}
// ============================================================
// Typed dispatch helpers
// ============================================================
//
// Each wclsFoo() inline function extracts a handler by stable method ID,
// casts it to the correct function pointer type, and calls it with a
// NULL check. This gives callers type-safe dispatch with the same
// codegen as a direct struct field call.
static inline bool wclsHas(const WidgetT *w, int32_t methodId) {
return w->wclass && w->wclass->handlers[methodId] != NULL;
}
static inline void wclsLayout(WidgetT *w, const BitmapFontT *font) {
typedef void (*FnT)(WidgetT *, const BitmapFontT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_LAYOUT] : NULL;
if (fn) { fn(w, font); }
}
static inline void wclsOnAccelActivate(WidgetT *w, WidgetT *root) {
typedef void (*FnT)(WidgetT *, WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_ACCEL_ACTIVATE] : NULL;
if (fn) { fn(w, root); }
}
// Dispatched on the widget LOSING keyboard focus, before the app-level
// onBlur callback. Editing widgets (e.g. the spinner) commit/clamp their
// in-progress text here. Must be called from every focus-loss transition
// (mouse focus change, Tab/accel navigation, window blur, wgtSetFocused).
static inline void wclsOnBlur(WidgetT *w) {
typedef void (*FnT)(WidgetT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_BLUR] : NULL;
if (fn) { fn(w); }
}
static inline void wclsOnChildChanged(WidgetT *parent, WidgetT *child) {
typedef void (*FnT)(WidgetT *, WidgetT *);
FnT fn = parent->wclass ? (FnT)parent->wclass->handlers[WGT_METHOD_ON_CHILD_CHANGED] : NULL;
if (fn) { fn(parent, child); }
} }
@ -472,10 +437,38 @@ static inline void wclsOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y
} }
static inline int32_t wclsGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy) { static inline void wclsOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
typedef int32_t (*FnT)(const WidgetT *, int32_t, int32_t); typedef void (*FnT)(WidgetT *, WidgetT *, int32_t, int32_t);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_CURSOR_SHAPE] : NULL; FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_DRAG_UPDATE] : NULL;
return fn ? fn(w, vx, vy) : 0; if (fn) { fn(w, root, x, y); }
}
static inline void wclsOnKey(WidgetT *w, int32_t key, int32_t mod) {
typedef void (*FnT)(WidgetT *, int32_t, int32_t);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_KEY] : NULL;
if (fn) { fn(w, key, mod); }
}
static inline void wclsOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
typedef void (*FnT)(WidgetT *, WidgetT *, int32_t, int32_t);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_ON_MOUSE] : NULL;
if (fn) { fn(w, root, vx, vy); }
}
static inline void wclsPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
typedef void (*FnT)(WidgetT *, DisplayT *, const BlitOpsT *, const BitmapFontT *, const ColorSchemeT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_PAINT] : NULL;
if (fn) { fn(w, d, ops, font, colors); }
}
static inline void wclsPaintOverlay(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
typedef void (*FnT)(WidgetT *, DisplayT *, const BlitOpsT *, const BitmapFontT *, const ColorSchemeT *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_PAINT_OVERLAY] : NULL;
if (fn) { fn(w, d, ops, font, colors); }
} }
@ -500,6 +493,13 @@ static inline void wclsScrollChildIntoView(WidgetT *parent, const WidgetT *child
} }
static inline void wclsSetText(WidgetT *w, const char *text) {
typedef void (*FnT)(WidgetT *, const char *);
FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_SET_TEXT] : NULL;
if (fn) { fn(w, text); }
}
// ============================================================ // ============================================================
// Window integration // Window integration
// ============================================================ // ============================================================
@ -543,8 +543,8 @@ void wgtInvalidate(WidgetT *w);
void wgtInvalidatePaint(WidgetT *w); void wgtInvalidatePaint(WidgetT *w);
// Set/get widget text (label, button, textInput, etc.) // Set/get widget text (label, button, textInput, etc.)
void wgtSetText(WidgetT *w, const char *text);
const char *wgtGetText(const WidgetT *w); const char *wgtGetText(const WidgetT *w);
void wgtSetText(WidgetT *w, const char *text);
// Enable/disable a widget // Enable/disable a widget
void wgtSetEnabled(WidgetT *w, bool enabled); void wgtSetEnabled(WidgetT *w, bool enabled);
@ -553,8 +553,8 @@ void wgtSetEnabled(WidgetT *w, bool enabled);
void wgtSetReadOnly(WidgetT *w, bool readOnly); void wgtSetReadOnly(WidgetT *w, bool readOnly);
// Set/get keyboard focus. wgtSetFocused refuses disabled or hidden widgets. // Set/get keyboard focus. wgtSetFocused refuses disabled or hidden widgets.
void wgtSetFocused(WidgetT *w);
WidgetT *wgtGetFocused(void); WidgetT *wgtGetFocused(void);
void wgtSetFocused(WidgetT *w);
// Move keyboard focus to w (NULL clears it): clears the previous widget's // Move keyboard focus to w (NULL clears it): clears the previous widget's
// selection, repaints both, and fires the blur/focus callbacks. Returns // selection, repaints both, and fires the blur/focus callbacks. Returns
@ -652,8 +652,8 @@ void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT
// This replaces the monolithic WidgetApiT -- adding a new widget // This replaces the monolithic WidgetApiT -- adding a new widget
// requires zero changes to dvxWidget.h. // requires zero changes to dvxWidget.h.
void wgtRegisterApi(const char *name, const void *api);
const void *wgtGetApi(const char *name); const void *wgtGetApi(const char *name);
void wgtRegisterApi(const char *name, const void *api);
// ============================================================ // ============================================================
// Widget interface descriptors // Widget interface descriptors
@ -755,8 +755,8 @@ typedef struct {
} WgtIfaceT; } WgtIfaceT;
// Register/retrieve interface descriptors by widget type name. // Register/retrieve interface descriptors by widget type name.
void wgtRegisterIface(const char *name, const WgtIfaceT *iface);
const WgtIfaceT *wgtGetIface(const char *name); const WgtIfaceT *wgtGetIface(const char *name);
void wgtRegisterIface(const char *name, const WgtIfaceT *iface);
// Case-insensitive lookup of a property descriptor on iface by name. // Case-insensitive lookup of a property descriptor on iface by name.
// Single source of truth for the runtime and IDE property-override rule. // Single source of truth for the runtime and IDE property-override rule.
@ -768,8 +768,8 @@ const WgtPropDescT *wgtIfaceFindProp(const WgtIfaceT *iface, const char *propNam
const char *wgtFindByBasName(const char *basName); const char *wgtFindByBasName(const char *basName);
// Enumerate all registered widget interfaces. // Enumerate all registered widget interfaces.
int32_t wgtIfaceCount(void);
const WgtIfaceT *wgtIfaceAt(int32_t idx, const char **outName); const WgtIfaceT *wgtIfaceAt(int32_t idx, const char **outName);
int32_t wgtIfaceCount(void);
// Get/set the .wgt file path for a registered widget (set by loader). // Get/set the .wgt file path for a registered widget (set by loader).
const char *wgtIfaceGetPath(const char *name); const char *wgtIfaceGetPath(const char *name);

View file

@ -94,23 +94,24 @@ static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) {
} }
static inline void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
drawTextAccel(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
drawTextAccel(d, ops, font, x, y, text, colors->windowShadow, 0, false);
}
static inline void drawTextEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) { static inline void drawTextEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
drawText(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false); drawText(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
drawText(d, ops, font, x, y, text, colors->windowShadow, 0, false); drawText(d, ops, font, x, y, text, colors->windowShadow, 0, false);
} }
static inline void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) {
drawTextAccel(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
drawTextAccel(d, ops, font, x, y, text, colors->windowShadow, 0, false);
}
// ============================================================ // ============================================================
// Shared interaction state (defined in widgetCore.c) // Shared interaction state (defined in widgetCore.c)
// ============================================================ // ============================================================
extern bool sCursorBlinkOn; extern bool sCursorBlinkOn;
extern clock_t sDblClickTicks; extern PlatformTicksT sDblClickTicks;
extern bool sDebugLayout; extern bool sDebugLayout;
extern WidgetT *sClosedPopup; extern WidgetT *sClosedPopup;
extern WidgetT *sFocusedWidget; extern WidgetT *sFocusedWidget;
@ -125,14 +126,31 @@ extern WidgetT **sPollWidgets; // stb_ds dynamic array
extern uint32_t sWidgetGen; extern uint32_t sWidgetGen;
extern void (*sCursorBlinkFn)(void); extern void (*sCursorBlinkFn)(void);
// ============================================================
// Test-harness reset hooks
// ============================================================
//
// The host test runner brings the GUI stack up and down many times in
// one process. dvxShutdown tears down the windows but never routes
// through widgetDetachWindowReferences, so every module-level pointer
// above (plus the per-module statics behind the helpers below) would
// dangle into the next dvxInit. wgtTestReset returns all of it to its
// load-time state; the per-module helpers exist only because the
// statics they clear are file-local. Never called on DOS.
void dvxAppTestReset(void);
void dvxDialogTestReset(void);
void wgtTestReset(void);
void widgetEventTestReset(void);
void widgetOpsTestReset(void);
// ============================================================ // ============================================================
// Core widget functions (widgetCore.c) // Core widget functions (widgetCore.c)
// ============================================================ // ============================================================
// Tree manipulation // Tree manipulation
void widgetAddChild(WidgetT *parent, WidgetT *child); void widgetAddChild(WidgetT *parent, WidgetT *child);
void widgetRemoveChild(WidgetT *parent, WidgetT *child);
void widgetDestroyChildren(WidgetT *w); void widgetDestroyChildren(WidgetT *w);
void widgetRemoveChild(WidgetT *parent, WidgetT *child);
// Clears every global interaction pointer that references w and removes w // Clears every global interaction pointer that references w and removes w
// from the poll list. Call for every destroyed node. // from the poll list. Call for every destroyed node.
@ -168,17 +186,17 @@ WidgetT *widgetAllocWithText(WidgetT *parent, int32_t type, size_t dataSize, con
// Focus management. widgetTransferFocus / widgetFireFocusChange are // Focus management. widgetTransferFocus / widgetFireFocusChange are
// declared in dvxWgt.h (the app layer's Tab/accelerator handlers use them). // declared in dvxWgt.h (the app layer's Tab/accelerator handlers use them).
WidgetT *widgetFindByAccel(WidgetT *root, char key);
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);
// Utility queries // Utility queries
int32_t multiClickDetect(int32_t vx, int32_t vy);
int32_t widgetCountVisibleChildren(const WidgetT *w); 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); bool widgetIsShown(const WidgetT *w);
int32_t multiClickDetect(int32_t vx, int32_t vy);
// Clipboard // Clipboard
void clipboardCopy(const char *text, int32_t len); void clipboardCopy(const char *text, int32_t len);
@ -262,6 +280,17 @@ 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 widgetPressableOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetPressableOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
// ============================================================
// Change notification (widgetOps.c)
// ============================================================
//
// Fires w->onChange (when set) and reports whether the widget tree
// survived: false means the handler destroyed widgets (sWidgetGen moved)
// and w must not be touched again. Widgets call this wherever code
// follows the notification.
bool widgetFireChange(WidgetT *w);
// ============================================================ // ============================================================
// Text helpers (widgetOps.c) // Text helpers (widgetOps.c)
// ============================================================ // ============================================================

View file

@ -118,8 +118,8 @@ typedef struct {
// Prototypes // Prototypes
// ============================================================ // ============================================================
static RectT clipSave(const DisplayT *d);
static void clipRestore(DisplayT *d, const RectT *saved); static void clipRestore(DisplayT *d, const RectT *saved);
static RectT clipSave(const DisplayT *d);
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 copyLabel(char *dst, const char *src, size_t cap);
@ -317,10 +317,6 @@ static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT
// the bottom visually separates the menu bar from the content area. // the bottom visually separates the menu bar from the content area.
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) {
if (!win->menuBar) {
return;
}
int32_t barY = win->y + CHROME_TITLEBAR_BOTTOM; int32_t barY = win->y + CHROME_TITLEBAR_BOTTOM;
int32_t barH = CHROME_MENU_HEIGHT; int32_t barH = CHROME_MENU_HEIGHT;
@ -835,91 +831,6 @@ static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32
} }
// Renders a window-level scrollbar by delegating to the shared draw-layer
// painter. Computes the screen position from the window origin plus the
// scrollbar's window-relative x/y, derives the thumb geometry from the
// scroll range, and hands everything to drawScrollbar().
//
// winX/winY are the window's screen position; sb->x/y are relative to the
// window origin. This split lets scrollbar positions survive window drags
// without recalculation -- only winX/winY change.
static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY) {
int32_t x = winX + sb->x;
int32_t y = winY + sb->y;
int32_t thumbPos;
int32_t thumbSize;
scrollbarThumbInfo(sb, &thumbPos, &thumbSize);
drawScrollbar(d, ops, colors, sb->orient, x, y, sb->length, SCROLLBAR_WIDTH, thumbPos, thumbSize);
}
// Effective maximum frame size for a window: its maxW/maxH constraint
// clamped to the screen, or the full screen when WM_MAX_FROM_SCREEN.
// Shared by wmMaximize and wmResizeMove.
static void wmEffectiveMaxSize(const WindowT *win, const DisplayT *d, int32_t *maxW, int32_t *maxH) {
*maxW = (win->maxW == WM_MAX_FROM_SCREEN) ? d->width : DVX_MIN(win->maxW, d->width);
*maxH = (win->maxH == WM_MAX_FROM_SCREEN) ? d->height : DVX_MIN(win->maxH, d->height);
}
// wmMenuFindItem -- find a menu item by command ID anywhere in a menu bar,
// descending into nested submenus to any depth. When outMenu is non-NULL it
// receives the menu that directly contains the matched item so callers can
// locate radio-group siblings and compute the item index.
static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu) {
if (!bar) {
return NULL;
}
for (int32_t m = 0; m < bar->menuCount; m++) {
MenuItemT *found = wmMenuFindItemRecursive(bar->menus[m], id, outMenu);
if (found) {
return found;
}
}
return NULL;
}
// wmMenuFindItemRecursive -- find a menu item by command ID within a single
// menu tree (the menu plus every nested submenu, to any depth). When outMenu
// is non-NULL it receives the menu that directly contains the matched item.
// Shared id-search used by wmMenuFindItem (menu bars) and wmMenuFindItemInMenu
// (standalone popup/context menus).
static MenuItemT *wmMenuFindItemRecursive(MenuT *menu, int32_t id, MenuT **outMenu) {
if (!menu) {
return NULL;
}
for (int32_t i = 0; i < menu->itemCount; i++) {
if (menu->items[i].id == id) {
if (outMenu) {
*outMenu = menu;
}
return &menu->items[i];
}
if (menu->items[i].subMenu) {
MenuItemT *found = wmMenuFindItemRecursive(menu->items[i].subMenu, id, outMenu);
if (found) {
return found;
}
}
}
return NULL;
}
// Adds a horizontal scrollbar to a window. The scrollbar steals // Adds a horizontal scrollbar to a window. The scrollbar steals
// SCROLLBAR_WIDTH pixels from the bottom of the content area (handled by // SCROLLBAR_WIDTH pixels from the bottom of the content area (handled by
// wmUpdateContentRect). The caller specifies the logical scroll range; the // wmUpdateContentRect). The caller specifies the logical scroll range; the
@ -1565,6 +1476,27 @@ void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
} }
// Renders a window-level scrollbar by delegating to the shared draw-layer
// painter. Computes the screen position from the window origin plus the
// scrollbar's window-relative x/y, derives the thumb geometry from the
// scroll range, and hands everything to drawScrollbar().
//
// winX/winY are the window's screen position; sb->x/y are relative to the
// window origin. This split lets scrollbar positions survive window drags
// without recalculation -- only winX/winY change.
static void wmDrawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const ScrollbarT *sb, int32_t winX, int32_t winY) {
int32_t x = winX + sb->x;
int32_t y = winY + sb->y;
int32_t thumbPos;
int32_t thumbSize;
scrollbarThumbInfo(sb, &thumbPos, &thumbSize);
drawScrollbar(d, ops, colors, sb->orient, x, y, sb->length, SCROLLBAR_WIDTH, thumbPos, thumbSize);
}
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) {
RectT savedClip = clipSave(d); RectT savedClip = clipSave(d);
@ -1604,6 +1536,15 @@ void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *co
} }
// 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);
}
// Frees a standalone context menu and all its submenus recursively. // Frees a standalone context menu and all its submenus recursively.
// freeMenuRecursive tears down the submenu tree and item array; this also // freeMenuRecursive tears down the submenu tree and item array; this also
// frees the root MenuT itself, which the caller owns. // frees the root MenuT itself, which the caller owns.
@ -1811,6 +1752,28 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT
} }
// wmMenuFindItem -- find a menu item by command ID anywhere in a menu bar,
// descending into nested submenus to any depth. When outMenu is non-NULL it
// receives the menu that directly contains the matched item so callers can
// locate radio-group siblings and compute the item index.
static MenuItemT *wmMenuFindItem(MenuBarT *bar, int32_t id, MenuT **outMenu) {
if (!bar) {
return NULL;
}
for (int32_t m = 0; m < bar->menuCount; m++) {
MenuItemT *found = wmMenuFindItemRecursive(bar->menus[m], id, outMenu);
if (found) {
return found;
}
}
return NULL;
}
// wmMenuFindItemInMenu -- recursively find an item by command ID within a // wmMenuFindItemInMenu -- recursively find an item by command ID within a
// single menu tree (the menu itself plus every nested submenu). Used for // single menu tree (the menu itself plus every nested submenu). Used for
// popup/context menus that are not attached to a menu bar, so the menu-bar // popup/context menus that are not attached to a menu bar, so the menu-bar
@ -1820,6 +1783,39 @@ MenuItemT *wmMenuFindItemInMenu(MenuT *menu, int32_t id) {
} }
// wmMenuFindItemRecursive -- find a menu item by command ID within a single
// menu tree (the menu plus every nested submenu, to any depth). When outMenu
// is non-NULL it receives the menu that directly contains the matched item.
// Shared id-search used by wmMenuFindItem (menu bars) and wmMenuFindItemInMenu
// (standalone popup/context menus).
static MenuItemT *wmMenuFindItemRecursive(MenuT *menu, int32_t id, MenuT **outMenu) {
if (!menu) {
return NULL;
}
for (int32_t i = 0; i < menu->itemCount; i++) {
if (menu->items[i].id == id) {
if (outMenu) {
*outMenu = menu;
}
return &menu->items[i];
}
if (menu->items[i].subMenu) {
MenuItemT *found = wmMenuFindItemRecursive(menu->items[i].subMenu, id, outMenu);
if (found) {
return found;
}
}
}
return NULL;
}
bool wmMenuItemIsChecked(MenuBarT *bar, int32_t id) { bool wmMenuItemIsChecked(MenuBarT *bar, int32_t id) {
MenuItemT *item = wmMenuFindItem(bar, id, NULL); MenuItemT *item = wmMenuFindItem(bar, id, NULL);
return item ? item->checked : false; return item ? item->checked : false;

View file

@ -0,0 +1,255 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// dvxMemTrack.c -- per-app memory tracking shared by every platform backend
//
// Wraps malloc/free/calloc/realloc/strdup with a small header per
// allocation that records the owning app ID and size, so DVX can report
// per-app memory usage in the Task Manager and detect leaks at app
// termination. Pure portable C: linked into dvx.exe on DOS (where the
// DXE export table maps libc's malloc family onto these wrappers) and into
// the host test libraries.
//
// The allocator reads *dvxMemAppIdPtr to determine which app to charge.
// The shell sets this pointer to &ctx->currentAppId during init.
//
// Cross-boundary safety: when dvxFree receives a pointer that was
// allocated by libc (not our wrapper), the magic check at ptr-16 fails
// and we fall through to libc free. On DJGPP the heap is a contiguous
// sbrk region so reading 16 bytes before any heap pointer is always
// mapped memory. On hosts with a sanitizer the same read is flagged
// only for untracked pointers, which DVX never hands to dvxFree.
#include "dvxPlat.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// Per-app memory tracking header magic
#define DVX_ALLOC_MAGIC 0xDEADBEEFUL
// Growth slack when the per-app usage table has to expand for a new appId.
#define APP_MEM_GROW_SLACK 16
// Per-app memory tracking header prepended to each allocation
typedef struct {
uint32_t magic;
int32_t appId;
uint32_t size;
uint32_t pad;
} DvxAllocHeaderT;
// ============================================================
// Prototypes
// ============================================================
static void dvxMemGrow(int32_t appId);
static void memUncharge(int32_t appId, uint32_t size);
// ============================================================
// Module state
// ============================================================
int32_t *dvxMemAppIdPtr = NULL;
static uint32_t *sAppMemUsed = NULL;
static int32_t sAppMemCap = 0;
void *dvxCalloc(size_t nmemb, size_t size) {
// Guard against nmemb*size overflowing size_t (calloc contract).
if (size != 0 && nmemb > SIZE_MAX / size) {
return NULL;
}
size_t total = nmemb * size;
void *ptr = dvxMalloc(total);
if (ptr) {
memset(ptr, 0, total);
}
return ptr;
}
void dvxFree(void *ptr) {
if (!ptr) {
return;
}
DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)ptr - 1;
if (hdr->magic != DVX_ALLOC_MAGIC) {
// Not a tracked allocation -- pass through to real free
free(ptr);
return;
}
int32_t appId = hdr->appId;
if (appId >= 0 && appId < sAppMemCap) {
memUncharge(appId, hdr->size);
}
hdr->magic = 0;
free(hdr);
}
void *dvxMalloc(size_t size) {
int32_t appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
// Guard the header addition against size_t wrap, which would otherwise
// hand back a non-NULL pointer over an undersized block.
if (size > SIZE_MAX - sizeof(DvxAllocHeaderT)) {
return NULL;
}
DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)malloc(sizeof(DvxAllocHeaderT) + size);
if (!hdr) {
return NULL;
}
hdr->magic = DVX_ALLOC_MAGIC;
hdr->appId = appId;
hdr->size = (uint32_t)size;
hdr->pad = 0;
if (appId >= 0) {
dvxMemGrow(appId);
if (appId < sAppMemCap) {
sAppMemUsed[appId] += (uint32_t)size;
}
}
return hdr + 1;
}
uint32_t dvxMemGetAppUsage(int32_t appId) {
if (appId < 0 || appId >= sAppMemCap) {
return 0;
}
return sAppMemUsed[appId];
}
static void dvxMemGrow(int32_t appId) {
if (appId < sAppMemCap) {
return;
}
int32_t newCap = appId + APP_MEM_GROW_SLACK;
uint32_t *newArr = (uint32_t *)realloc(sAppMemUsed, newCap * sizeof(uint32_t));
if (!newArr) {
return;
}
memset(newArr + sAppMemCap, 0, (newCap - sAppMemCap) * sizeof(uint32_t));
sAppMemUsed = newArr;
sAppMemCap = newCap;
}
void dvxMemResetApp(int32_t appId) {
if (appId >= 0 && appId < sAppMemCap) {
sAppMemUsed[appId] = 0;
}
}
void *dvxRealloc(void *ptr, size_t size) {
if (!ptr) {
return dvxMalloc(size);
}
if (size == 0) {
dvxFree(ptr);
return NULL;
}
DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)ptr - 1;
if (hdr->magic != DVX_ALLOC_MAGIC) {
// Not tracked -- pass through to real realloc
return realloc(ptr, size);
}
// Guard the header addition against size_t wrap, which would otherwise
// hand back a non-NULL pointer over an undersized block.
if (size > SIZE_MAX - sizeof(DvxAllocHeaderT)) {
return NULL;
}
int32_t appId = hdr->appId;
uint32_t oldSize = hdr->size;
DvxAllocHeaderT *newHdr = (DvxAllocHeaderT *)realloc(hdr, sizeof(DvxAllocHeaderT) + size);
if (!newHdr) {
return NULL;
}
if (appId >= 0 && appId < sAppMemCap) {
memUncharge(appId, oldSize);
sAppMemUsed[appId] += (uint32_t)size;
}
newHdr->size = (uint32_t)size;
return newHdr + 1;
}
char *dvxStrdup(const char *s) {
if (!s) {
return NULL;
}
size_t len = strlen(s) + 1;
char *dup = (char *)dvxMalloc(len);
if (dup) {
memcpy(dup, s, len);
}
return dup;
}
// Subtract a freed block from an app's usage, clamping at zero so a block
// freed after dvxMemResetApp (which already zeroed the counter) cannot wrap.
static void memUncharge(int32_t appId, uint32_t size) {
if (sAppMemUsed[appId] >= size) {
sAppMemUsed[appId] -= size;
} else {
sAppMemUsed[appId] = 0;
}
}

View file

@ -26,9 +26,15 @@
// interface. To port DVX to a new platform, implement a new // interface. To port DVX to a new platform, implement a new
// dvxPlatformXxx.c against this header. // dvxPlatformXxx.c against this header.
// //
// Currently one implementation exists: // Two implementations exist:
// dvxPlatformDos.c -- DJGPP/DPMI: real VESA VBE, INT 33h mouse, // dvxPlatformDos.c -- DJGPP/DPMI: real VESA VBE, INT 33h mouse,
// INT 16h keyboard, rep movsd/stosl asm spans // INT 16h keyboard, rep movsd/stosl asm spans
// dvxPlatformHost.c -- native Linux test backend: offscreen
// framebuffer, queue-driven input, fake clock
//
// Code shared by every backend lives in dvxPlatformUtil.c (logging,
// path/file helpers, glob matching, Alt scancode table) and
// dvxMemTrack.c (per-app allocation tracking).
// //
// The abstraction covers five areas: video mode setup, framebuffer // The abstraction covers five areas: video mode setup, framebuffer
// flushing, optimized memory spans, mouse input, and keyboard input. // flushing, optimized memory spans, mouse input, and keyboard input.
@ -69,9 +75,37 @@ typedef struct {
// Logging // Logging
// ============================================================ // ============================================================
// Append a line to dvx.log. Lives in dvx.exe, exported to all modules. // Append a line to the DVX log file. Lives in dvxPlatformUtil.c, so it
// is linked into dvx.exe (and exported to all modules) as well as into
// every host-side tool and test binary.
void dvxLog(const char *fmt, ...); void dvxLog(const char *fmt, ...);
// Set the path dvxLog appends to. The pointer is retained, not copied,
// so it must outlive every subsequent dvxLog call (a string literal is
// the intended argument). Defaults to "dvx.log" in the working
// directory until this is called.
void platformSetLogPath(const char *path);
// ============================================================
// Timing
// ============================================================
// Monotonic tick counter for UI timing (double-click, tooltip delay,
// cursor blink, timer widgets). Ticks advance at platformClockHz()
// per second. On DOS this wraps clock()/CLOCKS_PER_SEC; on the host
// test backend it is a fake clock advanced only by hostClockAdvance()
// so timing-dependent behaviour is deterministic under test.
//
// Unsigned so that "now - then" is wrap-safe; always compare intervals
// via subtraction, never compare raw tick values with < or >.
typedef uint32_t PlatformTicksT;
PlatformTicksT platformClock(void);
PlatformTicksT platformClockHz(void);
// Convert a millisecond interval to ticks at the platform rate.
#define PLATFORM_MS_TO_TICKS(ms) ((PlatformTicksT)(((uint64_t)(ms) * platformClockHz()) / 1000))
// ============================================================ // ============================================================
// System lifecycle // System lifecycle
// ============================================================ // ============================================================
@ -133,12 +167,12 @@ void platformFlushRect(const DisplayT *d, const RectT *r);
// differ only in the byte count computation (count * bytesPerPixel). // differ only in the byte count computation (count * bytesPerPixel).
// drawInit() selects the right function pointers into BlitOpsT at startup. // drawInit() selects the right function pointers into BlitOpsT at startup.
void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count);
void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count);
void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count);
void platformSpanCopy8(uint8_t *dst, const uint8_t *src, int32_t count);
void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count); void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count);
void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count); void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count);
void platformSpanCopy8(uint8_t *dst, const uint8_t *src, int32_t count);
void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count);
void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count);
void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count);
// ============================================================ // ============================================================
// Input -- Mouse // Input -- Mouse
@ -336,7 +370,12 @@ void dvxReadDirFree(char **entries);
// #define SOMEDIR "CONFIG" DVX_PATH_SEP "WPAPER" // #define SOMEDIR "CONFIG" DVX_PATH_SEP "WPAPER"
// DJGPP accepts both '/' and '\\' for fopen/stat/etc., so either would // DJGPP accepts both '/' and '\\' for fopen/stat/etc., so either would
// work, but '\\' is what DOS users expect to see in displayed paths. // work, but '\\' is what DOS users expect to see in displayed paths.
// Every other platform uses '/'.
#ifdef __DJGPP__
#define DVX_PATH_SEP "\\" #define DVX_PATH_SEP "\\"
#else
#define DVX_PATH_SEP "/"
#endif
// Simple glob pattern matching for filenames. Case-insensitive. // Simple glob pattern matching for filenames. Case-insensitive.
// Supports * (zero or more chars) and ? (one char). // Supports * (zero or more chars) and ? (one char).
@ -354,6 +393,18 @@ int32_t platformStripLineEndings(char *buf, int32_t len);
// DXE module support // DXE module support
// ============================================================ // ============================================================
// Symbol-name decoration for dlsym(). DJGPP's COFF toolchain prefixes
// every C symbol with an underscore and its dlsym() expects the
// decorated name; ELF platforms use the bare name. DVX_SYM("appMain")
// yields the correct string literal for the current platform. Code that
// builds names at runtime (snprintf) uses DVX_SYM_PREFIX instead.
#ifdef __DJGPP__
#define DVX_SYM_PREFIX "_"
#else
#define DVX_SYM_PREFIX ""
#endif
#define DVX_SYM(name) DVX_SYM_PREFIX name
// Register platform and C runtime symbols with the dynamic module // Register platform and C runtime symbols with the dynamic module
// loader so that DXE modules can resolve them at load time. On DOS // loader so that DXE modules can resolve them at load time. On DOS
// this calls dlregsym() with the full DJGPP libc/libm/libgcc/platform // this calls dlregsym() with the full DJGPP libc/libm/libgcc/platform

View file

@ -0,0 +1,100 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// dvxPlatHost.h -- test-harness hooks into the native host platform backend
//
// dvxPlatformHost.c implements dvxPlat.h for native Linux with an offscreen
// framebuffer, queue-driven input and a fake clock. Nothing in DVX proper
// calls the functions declared here; they are the injection points a test
// runner uses to feed input, control time and inspect output.
//
// Input model: hostMousePush/hostKeyPush append to queues that the
// platform poll functions drain one entry per call, so one pushed event
// corresponds to one event-loop frame. When the mouse queue is empty
// platformMousePoll keeps reporting the last state, exactly like a real
// driver whose mouse is not moving.
#ifndef DVX_PLAT_HOST_H
#define DVX_PLAT_HOST_H
#include "dvxTypes.h"
// ============================================================
// Mouse
// ============================================================
// Queue a mouse state sample. buttons is the platform bitmask: bit 0
// left, bit 1 right, bit 2 middle. Coordinates are clamped to the
// screen range set by platformMouseInit.
void hostMousePush(int32_t x, int32_t y, int32_t buttons);
// Accumulate a wheel delta (positive = scroll down) for the next
// platformMouseWheelPoll.
void hostWheelPush(int32_t delta);
// ============================================================
// Keyboard
// ============================================================
// Queue a key press. ascii is 0 for extended keys (arrows, F-keys),
// scancode is the PC set-1 make code, modifiers is the BIOS shift-state
// byte (bits 0-1 shift, bit 2 ctrl, bit 3 alt) that
// platformKeyboardGetModifiers will report from this point on.
void hostKeyPush(int32_t ascii, int32_t scancode, int32_t modifiers);
// Queue a key release for platformKeyUpRead.
void hostKeyUpPush(int32_t scancode);
// Set the modifier byte without queueing a key (e.g. to hold Shift down
// across a mouse drag).
void hostSetModifiers(int32_t modifiers);
// ============================================================
// Clock
// ============================================================
// Advance the fake clock by ms milliseconds.
void hostClockAdvance(int32_t ms);
// Set the fake clock to an absolute millisecond value.
void hostClockSet(uint32_t ms);
// Switch between the fake clock (default, deterministic) and wall time
// for interactive use. Entering real-time mode continues from the
// current fake value; leaving it freezes at the current wall value.
void hostClockSetRealTime(bool realTime);
// ============================================================
// Framebuffer
// ============================================================
// The flushed display surface (what a monitor would show): pixels reach
// it only through platformFlushRect, so it reflects the compositor's
// dirty-rect output rather than the raw backbuffer. Same pitch and
// pixel format as the DisplayT.
const uint8_t *hostFramebuffer(const DisplayT *d);
// Write the flushed display surface of the most recently initialised
// display as an RGB PNG. Returns false if no display is active or the
// file could not be written.
bool hostSavePng(const char *path);
#endif // DVX_PLAT_HOST_H

View file

@ -92,9 +92,6 @@
// RDTSC calibration: measure over this many BIOS timer ticks // RDTSC calibration: measure over this many BIOS timer ticks
#define CLOCK_MEAS_TICKS 3 #define CLOCK_MEAS_TICKS 3
// Per-app memory tracking header magic
#define DVX_ALLOC_MAGIC 0xDEADBEEFUL
// Key-up detection ring buffer size // Key-up detection ring buffer size
#define KEYUP_BUF_SIZE 16 #define KEYUP_BUF_SIZE 16
@ -153,20 +150,10 @@
#define MOUSE_BUTTON_MASK 0x07 #define MOUSE_BUTTON_MASK 0x07
// Per-app memory tracking header prepended to each allocation
typedef struct {
uint32_t magic;
int32_t appId;
uint32_t size;
uint32_t pad;
} DvxAllocHeaderT;
// ============================================================ // ============================================================
// Prototypes // Prototypes
// ============================================================ // ============================================================
static void dvxMemGrow(int32_t appId);
static uint32_t estimateClockMhz(void); static uint32_t estimateClockMhz(void);
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, uint16_t *outMode, DisplayT *d); static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, uint16_t *outMode, DisplayT *d);
static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requestedW, int32_t requestedH, int32_t preferredBpp); static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
@ -227,47 +214,15 @@ static int32_t sCurY = 0;
static MouseModeE sMouseMode = MouseModeMickeyE; static MouseModeE sMouseMode = MouseModeMickeyE;
static int32_t sPromoteCounter = 0; static int32_t sPromoteCounter = 0;
// Alt+key scan code to ASCII lookup table (indexed by BIOS scan code).
// INT 16h returns these scan codes with ascii=0 for Alt+key combos.
// Using a 256-byte lookup table instead of a switch or if-chain because
// this is called on every keypress and the table fits in a single cache
// line cluster. The designated initializer syntax leaves all other
// entries as zero, which is the "no mapping" sentinel.
static const char sAltScanToAscii[256] = {
// Alt+letters
[0x10] = 'q', [0x11] = 'w', [0x12] = 'e', [0x13] = 'r',
[0x14] = 't', [0x15] = 'y', [0x16] = 'u', [0x17] = 'i',
[0x18] = 'o', [0x19] = 'p', [0x1E] = 'a', [0x1F] = 's',
[0x20] = 'd', [0x21] = 'f', [0x22] = 'g', [0x23] = 'h',
[0x24] = 'j', [0x25] = 'k', [0x26] = 'l', [0x2C] = 'z',
[0x2D] = 'x', [0x2E] = 'c', [0x2F] = 'v', [0x30] = 'b',
[0x31] = 'n', [0x32] = 'm',
// Alt+digits
[0x78] = '1', [0x79] = '2', [0x7A] = '3', [0x7B] = '4',
[0x7C] = '5', [0x7D] = '6', [0x7E] = '7', [0x7F] = '8',
[0x80] = '9', [0x81] = '0',
};
// System information static buffer // System information static buffer
static char sSysInfoBuf[PLATFORM_SYSINFO_MAX]; static char sSysInfoBuf[PLATFORM_SYSINFO_MAX];
static int32_t sSysInfoPos = 0; static int32_t sSysInfoPos = 0;
// Per-app memory tracking -- tracks every allocation made by DXE code // Per-app memory tracking (dvxMalloc & co.) lives in dvxMemTrack.c. The
// via a 16-byte header prepended to each allocation. The DXE export // DXE export table below maps malloc/free/calloc/realloc/strdup to those
// table maps malloc/free/calloc/realloc/strdup to these wrappers so // wrappers so all DXE code is transparently tracked without #define
// all DXE code is transparently tracked without #define macros. // macros. stb_ds is also tracked: the loader overrides STBDS_REALLOC/
// // FREE to call dvxRealloc/dvxFree before including stb_ds.h.
// stb_ds is also tracked: the loader overrides STBDS_REALLOC/FREE
// to call dvxRealloc/dvxFree before including stb_ds.h.
//
// Cross-boundary safety: when dvxFree receives a pointer that was
// allocated by libc (not our wrapper), the magic check at ptr-16
// fails and we fall through to libc free. The DJGPP heap is a
// contiguous sbrk region so reading 16 bytes before any heap
// pointer is always valid memory (never unmapped).
int32_t *dvxMemAppIdPtr = NULL;
static uint32_t *sAppMemUsed = NULL;
static int32_t sAppMemCap = 0;
// Key-up detection via INT 9 hook. The BIOS keyboard interrupt (INT 16h) // Key-up detection via INT 9 hook. The BIOS keyboard interrupt (INT 16h)
// only reports key presses. To detect key releases we chain INT 9 (the // only reports key presses. To detect key releases we chain INT 9 (the
@ -306,171 +261,6 @@ static uint32_t sLfbLinearAddr = LFB_NOT_MAPPED;
static uint32_t sLfbMappedSize = 0; static uint32_t sLfbMappedSize = 0;
void *dvxCalloc(size_t nmemb, size_t size) {
// Guard against nmemb*size overflowing size_t (calloc contract).
if (size != 0 && nmemb > SIZE_MAX / size) {
return NULL;
}
size_t total = nmemb * size;
void *ptr = dvxMalloc(total);
if (ptr) {
memset(ptr, 0, total);
}
return ptr;
}
void dvxFree(void *ptr) {
if (!ptr) {
return;
}
DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)ptr - 1;
if (hdr->magic != DVX_ALLOC_MAGIC) {
// Not a tracked allocation -- pass through to real free
free(ptr);
return;
}
int32_t appId = hdr->appId;
if (appId >= 0 && appId < sAppMemCap) {
sAppMemUsed[appId] -= hdr->size;
}
hdr->magic = 0;
free(hdr);
}
void *dvxMalloc(size_t size) {
int32_t appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
// Guard the header addition against size_t wrap, which would otherwise
// hand back a non-NULL pointer over an undersized block.
if (size > SIZE_MAX - sizeof(DvxAllocHeaderT)) {
return NULL;
}
DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)malloc(sizeof(DvxAllocHeaderT) + size);
if (!hdr) {
return NULL;
}
hdr->magic = DVX_ALLOC_MAGIC;
hdr->appId = appId;
hdr->size = (uint32_t)size;
hdr->pad = 0;
if (appId >= 0) {
dvxMemGrow(appId);
if (appId < sAppMemCap) {
sAppMemUsed[appId] += (uint32_t)size;
}
}
return hdr + 1;
}
uint32_t dvxMemGetAppUsage(int32_t appId) {
if (appId < 0 || appId >= sAppMemCap) {
return 0;
}
return sAppMemUsed[appId];
}
static void dvxMemGrow(int32_t appId) {
if (appId < sAppMemCap) {
return;
}
int32_t newCap = appId + 16;
uint32_t *newArr = (uint32_t *)realloc(sAppMemUsed, newCap * sizeof(uint32_t));
if (!newArr) {
return;
}
memset(newArr + sAppMemCap, 0, (newCap - sAppMemCap) * sizeof(uint32_t));
sAppMemUsed = newArr;
sAppMemCap = newCap;
}
void dvxMemResetApp(int32_t appId) {
if (appId >= 0 && appId < sAppMemCap) {
sAppMemUsed[appId] = 0;
}
}
void *dvxRealloc(void *ptr, size_t size) {
if (!ptr) {
return dvxMalloc(size);
}
if (size == 0) {
dvxFree(ptr);
return NULL;
}
DvxAllocHeaderT *hdr = (DvxAllocHeaderT *)ptr - 1;
if (hdr->magic != DVX_ALLOC_MAGIC) {
// Not tracked -- pass through to real realloc
return realloc(ptr, size);
}
// Guard the header addition against size_t wrap, which would otherwise
// hand back a non-NULL pointer over an undersized block.
if (size > SIZE_MAX - sizeof(DvxAllocHeaderT)) {
return NULL;
}
int32_t appId = hdr->appId;
uint32_t oldSize = hdr->size;
DvxAllocHeaderT *newHdr = (DvxAllocHeaderT *)realloc(hdr, sizeof(DvxAllocHeaderT) + size);
if (!newHdr) {
return NULL;
}
if (appId >= 0 && appId < sAppMemCap) {
sAppMemUsed[appId] -= oldSize;
sAppMemUsed[appId] += (uint32_t)size;
}
newHdr->size = (uint32_t)size;
return newHdr + 1;
}
char *dvxStrdup(const char *s) {
if (!s) {
return NULL;
}
size_t len = strlen(s) + 1;
char *dup = (char *)dvxMalloc(len);
if (dup) {
memcpy(dup, s, len);
}
return dup;
}
// RDTSC calibration via BIOS timer. // RDTSC calibration via BIOS timer.
// Measures TSC ticks over 3 BIOS timer ticks (~165 ms). The BIOS timer // Measures TSC ticks over 3 BIOS timer ticks (~165 ms). The BIOS timer
// at 0040:006C increments at 18.2065 Hz (1193182 / 65536 Hz per tick). // at 0040:006C increments at 18.2065 Hz (1193182 / 65536 Hz per tick).
@ -921,12 +711,16 @@ static int32_t mapLfb(DisplayT *d, uint32_t physAddr) {
} }
char platformAltScanToChar(int32_t scancode) { // UI tick source. DJGPP's clock() is driven by the PIT (CLOCKS_PER_SEC
if (scancode < 0 || scancode > 255) { // is typically 91) and counts wall time, which is exactly what UI timing
return 0; // wants. glibc's clock() would be CPU time -- hence the seam.
} PlatformTicksT platformClock(void) {
return (PlatformTicksT)clock();
}
return sAltScanToAscii[scancode];
PlatformTicksT platformClockHz(void) {
return (PlatformTicksT)CLOCKS_PER_SEC;
} }
@ -2519,33 +2313,30 @@ extern unsigned char __dj_ctype_toupper[];
extern void *__emutls_get_address(void *); extern void *__emutls_get_address(void *);
// stb_ds internals (implementation compiled into dvx.exe via loaderMain.c) // stb_ds internals (implementation compiled into dvx.exe via loaderMain.c)
// dvxLog lives in dvx.exe (loaderMain.c)
extern void dvxLog(const char *fmt, ...);
// DPMI/go32 internals needed by the serial driver (rs232) // DPMI/go32 internals needed by the serial driver (rs232)
#include <dpmi.h> #include <dpmi.h>
#include <go32.h> #include <go32.h>
extern void *stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap);
extern void stbds_arrfreef(void *a); extern void stbds_arrfreef(void *a);
extern void *stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap);
extern size_t stbds_hash_bytes(void *p, size_t len, size_t seed);
extern size_t stbds_hash_string(char *str, size_t seed);
extern void *stbds_hmdel_key(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode);
extern void stbds_hmfree_func(void *a, size_t elemsize); extern void stbds_hmfree_func(void *a, size_t elemsize);
extern void *stbds_hmget_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); extern void *stbds_hmget_key(void *a, size_t elemsize, void *key, size_t keysize, int mode);
extern void *stbds_hmget_key_ts(void *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode); extern void *stbds_hmget_key_ts(void *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode);
extern void *stbds_hmput_default(void *a, size_t elemsize); extern void *stbds_hmput_default(void *a, size_t elemsize);
extern void *stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); extern void *stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode);
extern void *stbds_hmdel_key(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode);
extern void *stbds_shmode_func(size_t elemsize, int mode);
extern size_t stbds_hash_string(char *str, size_t seed);
extern size_t stbds_hash_bytes(void *p, size_t len, size_t seed);
extern void stbds_rand_seed(size_t seed); extern void stbds_rand_seed(size_t seed);
extern void *stbds_shmode_func(size_t elemsize, int mode);
extern char *stbds_stralloc(void *a, char *str); extern char *stbds_stralloc(void *a, char *str);
extern void stbds_strreset(void *a); extern void stbds_strreset(void *a);
// GCC runtime helpers (64-bit math, float conversion) // GCC runtime helpers (64-bit math, float conversion)
extern unsigned long long __fixunsdfdi(double);
extern unsigned long long __fixunssfdi(float);
extern long long __fixdfdi(double); extern long long __fixdfdi(double);
extern long long __fixsfdi(float); extern long long __fixsfdi(float);
extern unsigned long long __fixunsdfdi(double);
extern unsigned long long __fixunssfdi(float);
extern double __floatdidf(long long); extern double __floatdidf(long long);
extern float __floatdisf(long long); extern float __floatdisf(long long);
@ -2571,6 +2362,8 @@ DXE_EXPORT_TABLE(sDxeExportTable)
// --- platform --- // --- platform ---
DXE_EXPORT(platformAltScanToChar) DXE_EXPORT(platformAltScanToChar)
DXE_EXPORT(platformChdir) DXE_EXPORT(platformChdir)
DXE_EXPORT(platformClock)
DXE_EXPORT(platformClockHz)
DXE_EXPORT(platformCopyFile) DXE_EXPORT(platformCopyFile)
DXE_EXPORT(platformFlushRect) DXE_EXPORT(platformFlushRect)
DXE_EXPORT(platformGetMemoryInfo) DXE_EXPORT(platformGetMemoryInfo)
@ -2598,6 +2391,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformReadFile) DXE_EXPORT(platformReadFile)
DXE_EXPORT(platformRegisterDxeExports) DXE_EXPORT(platformRegisterDxeExports)
DXE_EXPORT(platformRegisterSymOverrides) DXE_EXPORT(platformRegisterSymOverrides)
DXE_EXPORT(platformSetLogPath)
DXE_EXPORT(platformSpanCopy8) DXE_EXPORT(platformSpanCopy8)
DXE_EXPORT(platformSpanCopy16) DXE_EXPORT(platformSpanCopy16)
DXE_EXPORT(platformSpanCopy32) DXE_EXPORT(platformSpanCopy32)

View file

@ -0,0 +1,857 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// dvxPlatformHost.c -- native Linux platform backend for DVX GUI
//
// Implements dvxPlat.h without any display, mouse or keyboard hardware so
// the whole GUI stack can run inside an ordinary process under the test
// harness (and sanitizers). The five platform domains map to:
// 1. Video: an offscreen backbuffer plus a separate "LFB" shadow that
// only platformFlushRect writes, so tests observe exactly
// what the compositor's dirty-rect logic flushed.
// 2. Flush: memcpy per scanline.
// 3. Spans: plain C fills/copies (16/32-bit fills replicate the value).
// 4. Mouse: a queue fed by hostMousePush; one sample per poll.
// 5. Keyboard: queues fed by hostKeyPush/hostKeyUpPush; the modifier
// byte is whatever the harness last set.
//
// Time comes from a fake clock (platformClock) that advances only when
// the harness says so, which makes double-click, tooltip, blink and timer
// behaviour deterministic. hostClockSetRealTime switches to wall time
// for interactive runs.
//
// Everything test-facing is declared in dvxPlatHost.h; everything DVX
// proper sees is the dvxPlat.h contract.
#include "dvxPlat.h"
#include "dvxPlatHost.h"
#include "dvxPal.h"
#include "thirdparty/stb_image_write_wrap.h"
#include <ctype.h>
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sysinfo.h>
#include <time.h>
// Default mode when the caller passes zero for a dimension.
#define HOST_DEFAULT_W 640
#define HOST_DEFAULT_H 480
#define HOST_DEFAULT_BPP 32
// Bits per pixel for each of the three supported formats.
#define HOST_BPP_8 8
#define HOST_BPP_16 16
#define HOST_BPP_32 32
// 8-bit palette: 256 entries of 3 bytes (RGB) each.
#define PALETTE_ENTRIES 256
#define PALETTE_BYTES (PALETTE_ENTRIES * 3)
// Fake clock resolution: one tick per millisecond.
#define HOST_CLOCK_HZ 1000
#define MS_PER_SEC 1000
#define NS_PER_MS 1000000
// Input queue depths. A frame consumes one mouse sample and every
// queued key, so these only need to absorb what a test pushes between
// two dvxUpdate calls.
#define MOUSE_QUEUE_SIZE 256
#define KEY_QUEUE_SIZE 256
// Mouse button bitmask (bits 0-2 = L/R/M), matching the DOS backend.
#define MOUSE_BUTTON_MASK 0x07
// BIOS shift-state byte: only the low 8 bits are meaningful.
#define MODIFIER_MASK 0xFF
// PNG output is 3 bytes per pixel (RGB).
#define PNG_COMPONENTS 3
// Kilobytes per byte divisor for platformGetMemoryInfo.
#define BYTES_PER_KB 1024
// Sentinel for "clock not yet started" in real-time mode.
#define WALL_CLOCK_UNSET 0
// Ring buffer of mouse state samples.
typedef struct {
int32_t x;
int32_t y;
int32_t buttons;
} HostMouseSampleT;
// Fixed-size FIFO used for both the mouse and keyboard queues.
typedef struct {
int32_t head;
int32_t tail;
int32_t capacity;
} HostQueueT;
// ============================================================
// Prototypes
// ============================================================
static void clampMouse(int32_t *x, int32_t *y);
static void crashHandler(int sig);
static void fillFormat(DisplayT *d, int32_t bpp);
static bool queueIsEmpty(const HostQueueT *q);
static bool queueIsFull(const HostQueueT *q);
static int32_t queuePop(HostQueueT *q);
static int32_t queuePush(HostQueueT *q);
static uint32_t wallClockMs(void);
// ============================================================
// Module state
// ============================================================
// Fixed mode list reported by platformVideoEnumModes.
static const int32_t sModeList[][2] = {
{ 640, 480 },
{ 800, 600 },
{ 1024, 768 }
};
static const int32_t sBppList[] = { HOST_BPP_8, HOST_BPP_16, HOST_BPP_32 };
// Most recently initialised display, for hostSavePng.
static DisplayT *sDisplay = NULL;
// Palette as last programmed by platformVideoSetPalette (8-bit modes).
static uint8_t sPalette[PALETTE_BYTES];
// Fake clock.
static uint32_t sTicks = 0;
static bool sRealTime = false;
static uint32_t sRealTimeBase = WALL_CLOCK_UNSET; // wall ms when real-time mode began
static uint32_t sRealTimeStart = 0; // fake ticks when real-time mode began
// Mouse.
static HostMouseSampleT sMouseQueue[MOUSE_QUEUE_SIZE];
static HostQueueT sMouseQ = { 0, 0, MOUSE_QUEUE_SIZE };
static HostMouseSampleT sMouseState = { 0, 0, 0 };
static int32_t sMouseRangeW = HOST_DEFAULT_W;
static int32_t sMouseRangeH = HOST_DEFAULT_H;
static int32_t sWheelDelta = 0;
// Keyboard.
static PlatformKeyEventT sKeyQueue[KEY_QUEUE_SIZE];
static HostQueueT sKeyQ = { 0, 0, KEY_QUEUE_SIZE };
static PlatformKeyEventT sKeyUpQueue[KEY_QUEUE_SIZE];
static HostQueueT sKeyUpQ = { 0, 0, KEY_QUEUE_SIZE };
static int32_t sModifiers = 0;
// Crash recovery.
static jmp_buf *sCrashJmp = NULL;
static volatile int *sCrashSignal = NULL;
static PlatformLogFnT sCrashLogFn = NULL;
// System information static buffer.
static char sSysInfoBuf[PLATFORM_SYSINFO_MAX];
// ============================================================
// Internal helpers
// ============================================================
static void clampMouse(int32_t *x, int32_t *y) {
if (*x < 0) {
*x = 0;
}
if (*y < 0) {
*y = 0;
}
if (*x >= sMouseRangeW) {
*x = sMouseRangeW - 1;
}
if (*y >= sMouseRangeH) {
*y = sMouseRangeH - 1;
}
}
static void crashHandler(int sig) {
if (sCrashLogFn) {
platformLogCrashDetail(sig, sCrashLogFn);
}
if (sCrashSignal) {
*sCrashSignal = sig;
}
if (sCrashJmp) {
longjmp(*sCrashJmp, 1);
}
// No recovery point: fall back to the default action.
signal(sig, SIG_DFL);
raise(sig);
}
// Fill in the PixelFormatT for one of the three supported depths.
static void fillFormat(DisplayT *d, int32_t bpp) {
PixelFormatT *f = &d->format;
memset(f, 0, sizeof(*f));
f->bitsPerPixel = bpp;
f->bytesPerPixel = bpp / 8;
switch (bpp) {
case HOST_BPP_16:
f->redBits = 5;
f->greenBits = 6;
f->blueBits = 5;
f->redShift = 11;
f->greenShift = 5;
f->blueShift = 0;
break;
case HOST_BPP_32:
f->redBits = 8;
f->greenBits = 8;
f->blueBits = 8;
f->redShift = 16;
f->greenShift = 8;
f->blueShift = 0;
break;
default:
// 8-bit palette mode: no colour fields.
break;
}
f->redMask = ((1U << f->redBits) - 1) << f->redShift;
f->greenMask = ((1U << f->greenBits) - 1) << f->greenShift;
f->blueMask = ((1U << f->blueBits) - 1) << f->blueShift;
}
void hostClockAdvance(int32_t ms) {
sTicks += (uint32_t)ms;
}
void hostClockSet(uint32_t ms) {
sTicks = ms;
}
void hostClockSetRealTime(bool realTime) {
if (realTime == sRealTime) {
return;
}
if (realTime) {
sRealTimeBase = wallClockMs();
sRealTimeStart = sTicks;
} else {
sTicks = platformClock();
}
sRealTime = realTime;
}
const uint8_t *hostFramebuffer(const DisplayT *d) {
return d->lfb;
}
void hostKeyPush(int32_t ascii, int32_t scancode, int32_t modifiers) {
sModifiers = modifiers & MODIFIER_MASK;
int32_t idx = queuePush(&sKeyQ);
if (idx < 0) {
return;
}
sKeyQueue[idx].ascii = ascii;
sKeyQueue[idx].scancode = scancode;
}
void hostKeyUpPush(int32_t scancode) {
int32_t idx = queuePush(&sKeyUpQ);
if (idx < 0) {
return;
}
sKeyUpQueue[idx].ascii = 0;
sKeyUpQueue[idx].scancode = scancode;
}
void hostMousePush(int32_t x, int32_t y, int32_t buttons) {
int32_t idx = queuePush(&sMouseQ);
if (idx < 0) {
return;
}
clampMouse(&x, &y);
sMouseQueue[idx].x = x;
sMouseQueue[idx].y = y;
sMouseQueue[idx].buttons = buttons & MOUSE_BUTTON_MASK;
}
bool hostSavePng(const char *path) {
const DisplayT *d = sDisplay;
if (!d || !d->lfb || !path) {
return false;
}
size_t rowBytes = (size_t)d->width * PNG_COMPONENTS;
uint8_t *rgb = (uint8_t *)malloc(rowBytes * (size_t)d->height);
if (!rgb) {
return false;
}
const PixelFormatT *f = &d->format;
for (int32_t y = 0; y < d->height; y++) {
const uint8_t *src = d->lfb + (size_t)y * (size_t)d->pitch;
uint8_t *dst = rgb + (size_t)y * rowBytes;
for (int32_t x = 0; x < d->width; x++) {
uint32_t pixel;
switch (f->bytesPerPixel) {
case 1:
pixel = src[x];
break;
case 2:
pixel = ((const uint16_t *)src)[x];
break;
default:
pixel = ((const uint32_t *)src)[x];
break;
}
if (f->bitsPerPixel == HOST_BPP_8) {
const uint8_t *entry = sPalette + (pixel & (PALETTE_ENTRIES - 1)) * 3;
dst[0] = entry[0];
dst[1] = entry[1];
dst[2] = entry[2];
} else {
uint32_t r = (pixel & f->redMask) >> f->redShift;
uint32_t g = (pixel & f->greenMask) >> f->greenShift;
uint32_t b = (pixel & f->blueMask) >> f->blueShift;
// Scale each field up to 8 bits by replicating its top bits.
dst[0] = (uint8_t)((r << (8 - f->redBits)) | (r >> (2 * f->redBits - 8)));
dst[1] = (uint8_t)((g << (8 - f->greenBits)) | (g >> (2 * f->greenBits - 8)));
dst[2] = (uint8_t)((b << (8 - f->blueBits)) | (b >> (2 * f->blueBits - 8)));
}
dst += PNG_COMPONENTS;
}
}
bool ok = stbi_write_png(path, d->width, d->height, PNG_COMPONENTS, rgb, (int)rowBytes) != 0;
free(rgb);
return ok;
}
void hostSetModifiers(int32_t modifiers) {
sModifiers = modifiers & MODIFIER_MASK;
}
void hostWheelPush(int32_t delta) {
sWheelDelta += delta;
}
PlatformTicksT platformClock(void) {
if (sRealTime) {
return sRealTimeStart + (wallClockMs() - sRealTimeBase);
}
return sTicks;
}
PlatformTicksT platformClockHz(void) {
return HOST_CLOCK_HZ;
}
void platformFlushRect(const DisplayT *d, const RectT *r) {
if (r->w <= 0 || r->h <= 0) {
return;
}
int32_t bpp = d->format.bytesPerPixel;
size_t rowBytes = (size_t)r->w * (size_t)bpp;
size_t offset = (size_t)r->y * (size_t)d->pitch + (size_t)r->x * (size_t)bpp;
const uint8_t *src = d->backBuf + offset;
uint8_t *dst = d->lfb + offset;
for (int32_t y = 0; y < r->h; y++) {
memcpy(dst, src, rowBytes);
src += d->pitch;
dst += d->pitch;
}
}
bool platformGetMemoryInfo(uint32_t *totalKb, uint32_t *freeKb) {
struct sysinfo si;
if (sysinfo(&si) != 0) {
*totalKb = 0;
*freeKb = 0;
return false;
}
*totalKb = (uint32_t)(((uint64_t)si.totalram * si.mem_unit) / BYTES_PER_KB);
*freeKb = (uint32_t)(((uint64_t)si.freeram * si.mem_unit) / BYTES_PER_KB);
return true;
}
const char *platformGetSystemInfo(const DisplayT *display) {
uint32_t totalKb = 0;
uint32_t freeKb = 0;
platformGetMemoryInfo(&totalKb, &freeKb);
snprintf(sSysInfoBuf, sizeof(sSysInfoBuf),
"=== Platform ===\n"
"Native host test backend\n"
"\n"
"=== Memory ===\n"
"Total: %lu KB\n"
"Free: %lu KB\n"
"\n"
"=== Video ===\n"
"Offscreen %ldx%ld, %ld bpp\n",
(unsigned long)totalKb, (unsigned long)freeKb,
(long)display->width, (long)display->height, (long)display->format.bitsPerPixel);
return sSysInfoBuf;
}
void platformInit(void) {
// Nothing to set up: no console modes or break handling on the host.
}
void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal, PlatformLogFnT logFn) {
struct sigaction sa;
sCrashJmp = recoveryBuf;
sCrashSignal = crashSignal;
sCrashLogFn = logFn;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = crashHandler;
sigemptyset(&sa.sa_mask);
// SA_NODEFER keeps the signal unblocked after we longjmp out of the
// handler, so a second crash is still caught.
sa.sa_flags = SA_NODEFER;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGFPE, &sa, NULL);
sigaction(SIGILL, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
}
int32_t platformKeyboardGetModifiers(void) {
return sModifiers;
}
bool platformKeyboardRead(PlatformKeyEventT *evt) {
if (queueIsEmpty(&sKeyQ)) {
return false;
}
*evt = sKeyQueue[queuePop(&sKeyQ)];
return true;
}
void platformKeyUpInit(void) {
sKeyUpQ.head = 0;
sKeyUpQ.tail = 0;
}
bool platformKeyUpRead(PlatformKeyEventT *evt) {
if (queueIsEmpty(&sKeyUpQ)) {
return false;
}
*evt = sKeyUpQueue[queuePop(&sKeyUpQ)];
return true;
}
void platformKeyUpShutdown(void) {
sKeyUpQ.head = 0;
sKeyUpQ.tail = 0;
}
const char *platformLineEnding(void) {
return "\n";
}
void platformLogCrashDetail(int sig, PlatformLogFnT logFn) {
const char *sigName = "UNKNOWN";
if (sig == SIGSEGV) {
sigName = "SIGSEGV (segmentation fault)";
} else if (sig == SIGFPE) {
sigName = "SIGFPE (floating point exception)";
} else if (sig == SIGILL) {
sigName = "SIGILL (illegal instruction)";
} else if (sig == SIGBUS) {
sigName = "SIGBUS (bus error)";
}
logFn("=== CRASH ===");
logFn("Signal: %d (%s)", sig, sigName);
}
void platformMouseInit(int32_t screenW, int32_t screenH) {
sMouseRangeW = screenW;
sMouseRangeH = screenH;
sMouseState.x = screenW / 2;
sMouseState.y = screenH / 2;
sMouseState.buttons = 0;
sMouseQ.head = 0;
sMouseQ.tail = 0;
sWheelDelta = 0;
}
void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) {
if (!queueIsEmpty(&sMouseQ)) {
sMouseState = sMouseQueue[queuePop(&sMouseQ)];
}
*mx = sMouseState.x;
*my = sMouseState.y;
*buttons = sMouseState.buttons;
}
void platformMouseSetAccel(int32_t threshold) {
(void)threshold;
}
void platformMouseSetMickeys(int32_t horizMickeys, int32_t vertMickeys) {
(void)horizMickeys;
(void)vertMickeys;
}
void platformMouseWarp(int32_t x, int32_t y) {
clampMouse(&x, &y);
sMouseState.x = x;
sMouseState.y = y;
}
bool platformMouseWheelInit(void) {
return true;
}
int32_t platformMouseWheelPoll(void) {
int32_t delta = sWheelDelta;
sWheelDelta = 0;
return delta;
}
void platformRegisterDxeExports(void) {
// ELF: symbols resolve through the executable's dynamic table
// (-rdynamic) and RTLD_GLOBAL, no registration needed.
}
void platformRegisterSymOverrides(const PlatformSymOverrideT *entries) {
(void)entries;
}
void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count) {
memcpy(dst, src, (size_t)count * 2);
}
void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count) {
memcpy(dst, src, (size_t)count * 4);
}
void platformSpanCopy8(uint8_t *dst, const uint8_t *src, int32_t count) {
memcpy(dst, src, (size_t)count);
}
void platformSpanFill16(uint8_t *dst, uint32_t color, int32_t count) {
uint16_t *p = (uint16_t *)dst;
uint16_t c = (uint16_t)color;
for (int32_t i = 0; i < count; i++) {
p[i] = c;
}
}
void platformSpanFill32(uint8_t *dst, uint32_t color, int32_t count) {
uint32_t *p = (uint32_t *)dst;
for (int32_t i = 0; i < count; i++) {
p[i] = color;
}
}
void platformSpanFill8(uint8_t *dst, uint32_t color, int32_t count) {
memset(dst, (int)(color & 0xFF), (size_t)count);
}
void platformSplashFillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint8_t color) {
(void)x;
(void)y;
(void)w;
(void)h;
(void)color;
}
void platformSplashInit(void) {
}
bool platformSplashLoadRaw(const char *path) {
(void)path;
return false;
}
void platformSplashShutdown(void) {
}
// Host filesystems accept nearly anything; keep the checks that matter
// for a name typed into a save dialog: non-empty, no directory separators,
// no control characters, and not "." or "..".
const char *platformValidateFilename(const char *name) {
if (!name || name[0] == '\0') {
return "Filename must not be empty.";
}
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
return "That name is reserved.";
}
for (const char *p = name; *p; p++) {
if (*p == '/' || *p == '\\') {
return "Filename must not contain a directory separator.";
}
if (iscntrl((unsigned char)*p)) {
return "Filename contains invalid characters.";
}
}
return NULL;
}
void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *userData), void *userData) {
int32_t modeCount = (int32_t)(sizeof(sModeList) / sizeof(sModeList[0]));
int32_t bppCount = (int32_t)(sizeof(sBppList) / sizeof(sBppList[0]));
for (int32_t m = 0; m < modeCount; m++) {
for (int32_t b = 0; b < bppCount; b++) {
cb(sModeList[m][0], sModeList[m][1], sBppList[b], userData);
}
}
}
void platformVideoFreeBuffers(DisplayT *d) {
free(d->backBuf);
d->backBuf = NULL;
free(d->palette);
d->palette = NULL;
free(d->lfb);
d->lfb = NULL;
}
int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
int32_t bpp = preferredBpp;
memset(d, 0, sizeof(*d));
if (bpp != HOST_BPP_8 && bpp != HOST_BPP_16 && bpp != HOST_BPP_32) {
bpp = HOST_DEFAULT_BPP;
}
d->width = requestedW > 0 ? requestedW : HOST_DEFAULT_W;
d->height = requestedH > 0 ? requestedH : HOST_DEFAULT_H;
fillFormat(d, bpp);
d->pitch = d->width * d->format.bytesPerPixel;
size_t fbSize = (size_t)d->pitch * (size_t)d->height;
d->backBuf = (uint8_t *)calloc(1, fbSize);
d->lfb = (uint8_t *)calloc(1, fbSize);
if (!d->backBuf || !d->lfb) {
platformVideoFreeBuffers(d);
return -1;
}
if (bpp == HOST_BPP_8) {
d->palette = (uint8_t *)malloc(PALETTE_BYTES);
if (!d->palette) {
platformVideoFreeBuffers(d);
return -1;
}
dvxGeneratePalette(d->palette);
platformVideoSetPalette(d->palette, 0, PALETTE_ENTRIES);
}
d->clipX = 0;
d->clipY = 0;
d->clipW = d->width;
d->clipH = d->height;
sDisplay = d;
return 0;
}
void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count) {
for (int32_t i = 0; i < count; i++) {
int32_t entry = firstEntry + i;
if (entry < 0 || entry >= PALETTE_ENTRIES) {
continue;
}
memcpy(sPalette + entry * 3, pal + entry * 3, 3);
}
}
void platformVideoShutdown(DisplayT *d) {
platformVideoFreeBuffers(d);
if (sDisplay == d) {
sDisplay = NULL;
}
}
void platformYield(void) {
// Nothing else is running in a test process.
}
static bool queueIsEmpty(const HostQueueT *q) {
return q->head == q->tail;
}
static bool queueIsFull(const HostQueueT *q) {
return ((q->head + 1) % q->capacity) == q->tail;
}
// Return the slot index to read, advancing tail. Caller checks empty.
static int32_t queuePop(HostQueueT *q) {
int32_t idx = q->tail;
q->tail = (q->tail + 1) % q->capacity;
return idx;
}
// Return the slot index to write, advancing head, or -1 when full.
static int32_t queuePush(HostQueueT *q) {
if (queueIsFull(q)) {
return -1;
}
int32_t idx = q->head;
q->head = (q->head + 1) % q->capacity;
return idx;
}
static uint32_t wallClockMs(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint32_t)((uint64_t)ts.tv_sec * MS_PER_SEC + (uint64_t)ts.tv_nsec / NS_PER_MS);
}
// ============================================================
// Harness hooks (dvxPlatHost.h)
// ============================================================
// ============================================================
// Platform contract (dvxPlat.h)
// ============================================================

View file

@ -52,6 +52,120 @@
// Transfer block used by platformCopyFile. // Transfer block used by platformCopyFile.
#define COPY_FILE_CHUNK 4096 #define COPY_FILE_CHUNK 4096
// Log file used by dvxLog until platformSetLogPath overrides it.
#define DEFAULT_LOG_PATH "dvx.log"
// Number of BIOS scan codes covered by the Alt+key lookup table.
#define ALT_SCAN_TABLE_SIZE 256
// ============================================================
// Module state
// ============================================================
// Current dvxLog destination (see platformSetLogPath).
static const char *sLogPath = DEFAULT_LOG_PATH;
// Alt+key scan code to ASCII lookup table (indexed by BIOS scan code).
// INT 16h returns these scan codes with ascii=0 for Alt+key combos.
// Using a 256-byte lookup table instead of a switch or if-chain because
// this is called on every keypress and the table fits in a single cache
// line cluster. The designated initializer syntax leaves all other
// entries as zero, which is the "no mapping" sentinel.
static const char sAltScanToAscii[ALT_SCAN_TABLE_SIZE] = {
// Alt+letters
[0x10] = 'q', [0x11] = 'w', [0x12] = 'e', [0x13] = 'r',
[0x14] = 't', [0x15] = 'y', [0x16] = 'u', [0x17] = 'i',
[0x18] = 'o', [0x19] = 'p', [0x1E] = 'a', [0x1F] = 's',
[0x20] = 'd', [0x21] = 'f', [0x22] = 'g', [0x23] = 'h',
[0x24] = 'j', [0x25] = 'k', [0x26] = 'l', [0x2C] = 'z',
[0x2D] = 'x', [0x2E] = 'c', [0x2F] = 'v', [0x30] = 'b',
[0x31] = 'n', [0x32] = 'm',
// Alt+digits
[0x78] = '1', [0x79] = '2', [0x7A] = '3', [0x7B] = '4',
[0x7C] = '5', [0x7D] = '6', [0x7E] = '7', [0x7F] = '8',
[0x80] = '9', [0x81] = '0',
};
bool dvxHasExt(const char *name, const char *ext) {
if (!name || !ext) {
return false;
}
size_t nameLen = strlen(name);
size_t extLen = strlen(ext);
if (nameLen < extLen) {
return false;
}
return strcasecmp(name + nameLen - extLen, ext) == 0;
}
// Append one formatted line to the log file. Opens/closes the file
// per-write so it is never held open across a crash or a task switch.
void dvxLog(const char *fmt, ...) {
FILE *f = fopen(sLogPath, "a");
if (!f) {
return;
}
va_list ap;
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
fprintf(f, "\n");
fclose(f);
}
char **dvxReadDir(const char *dirPath) {
if (!dirPath) {
return NULL;
}
DIR *dir = opendir(dirPath);
if (!dir) {
return NULL;
}
char **entries = NULL;
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
// Skip "." and ".."
if (ent->d_name[0] == '.' &&
(ent->d_name[1] == '\0' ||
(ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
continue;
}
arrput(entries, strdup(ent->d_name));
}
closedir(dir);
return entries;
}
void dvxReadDirFree(char **entries) {
if (!entries) {
return;
}
int32_t n = (int32_t)arrlen(entries);
for (int32_t i = 0; i < n; i++) {
free(entries[i]);
}
arrfree(entries);
}
const char *dvxSkipWs(const char *s) { const char *dvxSkipWs(const char *s) {
if (!s) { if (!s) {
@ -109,64 +223,12 @@ int32_t dvxTrimRight(char *buf) {
} }
bool dvxHasExt(const char *name, const char *ext) { char platformAltScanToChar(int32_t scancode) {
if (!name || !ext) { if (scancode < 0 || scancode >= ALT_SCAN_TABLE_SIZE) {
return false; return 0;
} }
size_t nameLen = strlen(name); return sAltScanToAscii[scancode];
size_t extLen = strlen(ext);
if (nameLen < extLen) {
return false;
}
return strcasecmp(name + nameLen - extLen, ext) == 0;
}
char **dvxReadDir(const char *dirPath) {
if (!dirPath) {
return NULL;
}
DIR *dir = opendir(dirPath);
if (!dir) {
return NULL;
}
char **entries = NULL;
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
// Skip "." and ".."
if (ent->d_name[0] == '.' &&
(ent->d_name[1] == '\0' ||
(ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
continue;
}
arrput(entries, strdup(ent->d_name));
}
closedir(dir);
return entries;
}
void dvxReadDirFree(char **entries) {
if (!entries) {
return;
}
int32_t n = (int32_t)arrlen(entries);
for (int32_t i = 0; i < n; i++) {
free(entries[i]);
}
arrfree(entries);
} }
@ -216,37 +278,42 @@ bool platformCopyFile(const char *srcPath, const char *dstPath) {
} }
const char *platformPathBaseName(const char *path) { // Simple glob pattern matching for filenames. Supports:
if (!path) { // * matches zero or more characters
return ""; // ? matches exactly one character
// Case-insensitive. Exported for use by DXE modules.
bool platformGlobMatch(const char *pattern, const char *name) {
while (*pattern && *name) {
if (*pattern == '*') {
pattern++;
if (!*pattern) {
return true;
}
while (*name) {
if (platformGlobMatch(pattern, name)) {
return true;
}
name++;
}
return false;
} else if (*pattern == '?' || tolower((unsigned char)*pattern) == tolower((unsigned char)*name)) {
pattern++;
name++;
} else {
return false;
}
} }
const char *sep = platformPathDirEnd(path); // Skip trailing *
return sep ? sep + 1 : path; while (*pattern == '*') {
} pattern++;
// Return the last path separator in `path`, or NULL if none.
//
// Platform-specific, because what counts as a separator varies:
// - DJGPP/DOS and Win32 accept both '/' and '\\' -- we have to check
// both and return whichever appears last.
// - Unix-like systems use only '/'. '\\' is a legal filename
// character there, so treating it as a separator would incorrectly
// split paths containing literal backslashes.
char *platformPathDirEnd(const char *path) {
#if defined(__DJGPP__) || defined(_WIN32) || defined(_WIN64)
char *fwd = strrchr(path, '/');
char *back = strrchr(path, '\\');
if (back > fwd) {
return back;
} }
return fwd; return *pattern == '\0' && *name == '\0';
#else
return strrchr(path, '/');
#endif
} }
@ -278,28 +345,51 @@ int32_t platformMkdirRecursive(const char *path) {
} }
} }
// Create the final directory // Create the final directory. EEXIST is only success when what exists
if (mkdir(buf, MKDIR_MODE) != 0 && errno != EEXIST) { // is actually a directory.
return -1; if (mkdir(buf, MKDIR_MODE) != 0) {
struct stat st;
if (errno != EEXIST || stat(buf, &st) != 0 || !S_ISDIR(st.st_mode)) {
return -1;
}
} }
return 0; return 0;
} }
// Remove carriage returns in-place. Used when reading DOS-format const char *platformPathBaseName(const char *path) {
// text files on hosts that don't auto-strip them. if (!path) {
int32_t platformStripLineEndings(char *buf, int32_t len) { return "";
int32_t dst = 0;
for (int32_t src = 0; src < len; src++) {
if (buf[src] != '\r') {
buf[dst++] = buf[src];
}
} }
buf[dst] = '\0'; const char *sep = platformPathDirEnd(path);
return dst; return sep ? sep + 1 : path;
}
// Return the last path separator in `path`, or NULL if none.
//
// Platform-specific, because what counts as a separator varies:
// - DJGPP/DOS and Win32 accept both '/' and '\\' -- we have to check
// both and return whichever appears last.
// - Unix-like systems use only '/'. '\\' is a legal filename
// character there, so treating it as a separator would incorrectly
// split paths containing literal backslashes.
char *platformPathDirEnd(const char *path) {
#if defined(__DJGPP__) || defined(_WIN32) || defined(_WIN64)
char *fwd = strrchr(path, '/');
char *back = strrchr(path, '\\');
if (back > fwd) {
return back;
}
return fwd;
#else
return strrchr(path, '/');
#endif
} }
@ -354,3 +444,25 @@ char *platformReadFile(const char *path, int32_t *outLen) {
return buf; return buf;
} }
void platformSetLogPath(const char *path) {
sLogPath = path ? path : DEFAULT_LOG_PATH;
}
// Remove carriage returns in-place. Used when reading DOS-format
// text files on hosts that don't auto-strip them.
int32_t platformStripLineEndings(char *buf, int32_t len) {
int32_t dst = 0;
for (int32_t src = 0; src < len; src++) {
if (buf[src] != '\r') {
buf[dst++] = buf[src];
}
}
buf[dst] = '\0';
return dst;
}

View file

@ -44,10 +44,10 @@
#include "dvxDraw.h" #include "dvxDraw.h"
#include "dvxPlat.h" #include "dvxPlat.h"
#include "stb_ds_wrap.h" #include "stb_ds_wrap.h"
#include "../../../widgets/kpunch/timer/timer.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h>
// ============================================================ // ============================================================
// Global state for drag and popup tracking // Global state for drag and popup tracking
@ -68,7 +68,7 @@
// and wgtDestroy() handle this cleanup. // and wgtDestroy() handle this cleanup.
bool sCursorBlinkOn = true; // text cursor blink phase (toggled by wgtUpdateCursorBlink) bool sCursorBlinkOn = true; // text cursor blink phase (toggled by wgtUpdateCursorBlink)
clock_t sDblClickTicks = 0; // set from ctx->dblClickTicks during first paint PlatformTicksT sDblClickTicks = 0; // set from ctx->dblClickTicks during first paint
bool sDebugLayout = false; bool sDebugLayout = false;
WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk) WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk)
WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list
@ -92,7 +92,7 @@ static int32_t sClipboardCap = 0;
#define DBLCLICK_TOLERANCE 4 #define DBLCLICK_TOLERANCE 4
// Multi-click state (used by widgetEvent.c for universal dbl-click detection) // Multi-click state (used by widgetEvent.c for universal dbl-click detection)
static clock_t sLastClickTime = 0; static PlatformTicksT sLastClickTime = 0;
static int32_t sLastClickX = -1; static int32_t sLastClickX = -1;
static int32_t sLastClickY = -1; static int32_t sLastClickY = -1;
static int32_t sClickCount = 0; static int32_t sClickCount = 0;
@ -194,7 +194,7 @@ static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfte
int32_t multiClickDetect(int32_t vx, int32_t vy) { int32_t multiClickDetect(int32_t vx, int32_t vy) {
clock_t now = clock(); PlatformTicksT now = platformClock();
// Guard against multiple calls in the same frame (e.g. widget onMouse // Guard against multiple calls in the same frame (e.g. widget onMouse
// calls it, then widgetEvent.c calls it again). If coords and time // calls it, then widgetEvent.c calls it again). If coords and time
@ -218,6 +218,40 @@ int32_t multiClickDetect(int32_t vx, int32_t vy) {
} }
void wgtTestReset(void) {
sFocusedWidget = NULL;
sOpenPopup = NULL;
sDragWidget = NULL;
sCursorBlinkOn = true;
sDebugLayout = false;
sDblClickTicks = 0;
sLastClickTime = 0;
sLastClickX = -1;
sLastClickY = -1;
sClickCount = 0;
arrfree(sPollWidgets);
sPollWidgets = NULL;
arrfree(sWidgetDestroyFns);
sWidgetDestroyFns = NULL;
free(sClipboard);
sClipboard = NULL;
sClipboardLen = 0;
sClipboardCap = 0;
widgetEventTestReset();
widgetOpsTestReset();
dvxAppTestReset();
const TimerApiT *timerApi = dvxTimerApi();
if (timerApi && timerApi->testReset) {
timerApi->testReset();
}
}
// Appends a child to the end of the parent's child list. O(1) // Appends a child to the end of the parent's child list. O(1)
// thanks to the lastChild tail pointer. The child list is singly- // thanks to the lastChild tail pointer. The child list is singly-
// linked (nextSibling), which saves 4 bytes per widget vs doubly- // linked (nextSibling), which saves 4 bytes per widget vs doubly-

View file

@ -110,6 +110,14 @@ static void dispatchButtonEdges(WidgetT *hit, uint32_t snapGen, int32_t buttons,
} }
void widgetEventTestReset(void) {
sClosedPopup = NULL;
sPrevMouseButtons = 0;
sPrevMouseX = -1;
sPrevMouseY = -1;
}
// Manages automatic scrollbar addition/removal for widget-based windows. // Manages automatic scrollbar addition/removal for widget-based windows.
// Called on every invalidation to ensure scrollbars match the current // Called on every invalidation to ensure scrollbars match the current
// widget tree's minimum size requirements. // widget tree's minimum size requirements.
@ -248,10 +256,17 @@ void widgetOnBlur(WindowT *win) {
// widgetOnFocus -- window gained focus // widgetOnFocus -- window gained focus
// //
// Mark the window's content dirty so the compositor knows to // Mark the window's content dirty so the compositor knows to
// refresh its minimized icon thumbnail if needed. // refresh its minimized icon thumbnail if needed, and request a paint
// when no widget holds focus: widgetOnPaint is what hands focus back
// (win->lastFocusWidget or the first focusable widget), and nothing
// else is guaranteed to dirty the content after an Alt+Tab.
void widgetOnFocus(WindowT *win) { void widgetOnFocus(WindowT *win) {
win->iconNeedsRefresh = true; win->iconNeedsRefresh = true;
if (win->widgetRoot && !sFocusedWidget) {
wgtInvalidatePaint(win->widgetRoot);
}
} }
@ -394,9 +409,23 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
// Handle drag release // Handle drag release
if (sDragWidget && !(buttons & MOUSE_LEFT)) { if (sDragWidget && !(buttons & MOUSE_LEFT)) {
wclsOnDragEnd(sDragWidget, root, x, y); WidgetT *drag = sDragWidget;
uint32_t gen = sWidgetGen;
// The drag widget captured the press (its onMouseDown fired on the
// press path), so it owns the matching left onMouseUp -- delivered
// before the drag-end handler so a pressable widget sees
// onMouseDown, onMouseUp, onClick in that order. The callback may
// destroy widgets (widgetClearReferences nulls sDragWidget then).
if (drag->enabled && drag->onMouseUp) {
drag->onMouseUp(drag, 1, x - drag->x - drag->contentOffX, y - drag->y - drag->contentOffY);
}
if (sWidgetGen == gen && sDragWidget) {
wclsOnDragEnd(drag, root, x, y);
wgtInvalidatePaint(sDragWidget);
}
wgtInvalidatePaint(sDragWidget);
sDragWidget = NULL; sDragWidget = NULL;
// Record the release edge. Leaving a stale LEFT bit here would // Record the release edge. Leaving a stale LEFT bit here would
@ -484,9 +513,9 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
// Left button is up. If it was just released, deliver the left-button // Left button is up. If it was just released, deliver the left-button
// MouseUp to the widget under the cursor (the remainder of this handler // MouseUp to the widget under the cursor (the remainder of this handler
// is press-time logic and is skipped on release). This is the only path // is press-time logic and is skipped on release). A release that ends
// that reaches a left onMouseUp, which the early return otherwise left // a drag capture is delivered to the drag widget by the drag-release
// unreachable. // path above instead and never reaches here.
if (!(buttons & MOUSE_LEFT)) { if (!(buttons & MOUSE_LEFT)) {
// Deliver the left-button MouseUp (release edge) and any right/middle // Deliver the left-button MouseUp (release edge) and any right/middle
// press/release edges to the widget under the cursor, then return -- // press/release edges to the widget under the cursor, then return --

View file

@ -52,6 +52,72 @@
#include "dvxWgtP.h" #include "dvxWgtP.h"
// Public entry point: runs both passes on the entire widget tree.
// The root widget is positioned at (0,0) and given the full available
// area, then the arrange pass distributes space to its children.
//
// The window paths (widgetManageScrollbars, widgetOnPaint) do not use
// this: they measure once and arrange at the scrolled root geometry
// themselves. This entry point serves callers that need a complete
// layout on demand (e.g. dvxFitWindow).
void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font) {
if (!root) {
return;
}
// Measure pass
widgetCalcMinSizeTree(root, font);
// Layout pass
root->x = 0;
root->y = 0;
root->w = availW;
root->h = availH;
widgetLayoutChildren(root, font);
}
// Decodes a tagged size value into an actual pixel count.
//
// The tagged integer format uses the high 2 bits as a type tag:
// 00 = pixels (value is used directly)
// 01 = characters (value * charWidth, for text-relative sizing)
// 10 = percent (value% of parentSize, for responsive layouts)
//
// This encoding allows a single int32_t field to represent any of
// three unit types without needing a separate struct or enum.
// The tradeoff is that pixel values are limited to 30 bits (~1 billion),
// which is far more than any supported display resolution.
//
// A value of 0 means "auto" (use intrinsic/calculated size) -- the
// caller checks for 0 before calling this function.
int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth) {
if (taggedSize == 0) {
return 0;
}
uint32_t sizeType = (uint32_t)taggedSize & WGT_SIZE_TYPE_MASK;
int32_t value = taggedSize & WGT_SIZE_VAL_MASK;
switch (sizeType) {
case WGT_SIZE_PIXELS:
return value;
case WGT_SIZE_CHARS:
return value * charWidth;
case WGT_SIZE_PERCENT:
return (parentSize * value) / 100;
default:
return value;
}
}
// Resolves the layout metrics of a box container: padding and gap come // Resolves the layout metrics of a box container: padding and gap come
// from the widget's tagged sizes (falling back to DEFAULT_PADDING / // from the widget's tagged sizes (falling back to DEFAULT_PADDING /
// DEFAULT_SPACING when unset), then a class-supplied getLayoutMetrics // DEFAULT_SPACING when unset), then a class-supplied getLayoutMetrics
@ -388,68 +454,3 @@ void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
} }
} }
// Public entry point: runs both passes on the entire widget tree.
// The root widget is positioned at (0,0) and given the full available
// area, then the arrange pass distributes space to its children.
//
// The window paths (widgetManageScrollbars, widgetOnPaint) do not use
// this: they measure once and arrange at the scrolled root geometry
// themselves. This entry point serves callers that need a complete
// layout on demand (e.g. dvxFitWindow).
void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font) {
if (!root) {
return;
}
// Measure pass
widgetCalcMinSizeTree(root, font);
// Layout pass
root->x = 0;
root->y = 0;
root->w = availW;
root->h = availH;
widgetLayoutChildren(root, font);
}
// Decodes a tagged size value into an actual pixel count.
//
// The tagged integer format uses the high 2 bits as a type tag:
// 00 = pixels (value is used directly)
// 01 = characters (value * charWidth, for text-relative sizing)
// 10 = percent (value% of parentSize, for responsive layouts)
//
// This encoding allows a single int32_t field to represent any of
// three unit types without needing a separate struct or enum.
// The tradeoff is that pixel values are limited to 30 bits (~1 billion),
// which is far more than any supported display resolution.
//
// A value of 0 means "auto" (use intrinsic/calculated size) -- the
// caller checks for 0 before calling this function.
int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth) {
if (taggedSize == 0) {
return 0;
}
uint32_t sizeType = (uint32_t)taggedSize & WGT_SIZE_TYPE_MASK;
int32_t value = taggedSize & WGT_SIZE_VAL_MASK;
switch (sizeType) {
case WGT_SIZE_PIXELS:
return value;
case WGT_SIZE_CHARS:
return value * charWidth;
case WGT_SIZE_PERCENT:
return (parentSize * value) / 100;
default:
return value;
}
}

View file

@ -286,7 +286,7 @@ void wgtInvalidate(WidgetT *w) {
widgetManageScrollbars(w->window, ctx); widgetManageScrollbars(w->window, ctx);
} }
// Full repaint layout changed, all widgets need redrawing // Full repaint -- layout changed, all widgets need redrawing
w->window->paintNeeded = PAINT_FULL; w->window->paintNeeded = PAINT_FULL;
dvxInvalidateWindow(ctx, w->window); dvxInvalidateWindow(ctx, w->window);
} }
@ -316,7 +316,7 @@ void wgtInvalidatePaint(WidgetT *w) {
} }
} }
// Defer the actual paint it will happen once in the main loop // Defer the actual paint -- it will happen once in the main loop
// before compositing, batching multiple invalidations into one // before compositing, batching multiple invalidations into one
// tree walk instead of one per call. Don't downgrade FULL to PARTIAL. // tree walk instead of one per call. Don't downgrade FULL to PARTIAL.
if (w->window->paintNeeded < PAINT_PARTIAL) { if (w->window->paintNeeded < PAINT_PARTIAL) {
@ -458,6 +458,17 @@ static bool widgetDropFocusWithin(WidgetT *w) {
} }
bool widgetFireChange(WidgetT *w) {
uint32_t gen = sWidgetGen;
if (w->onChange) {
w->onChange(w);
}
return sWidgetGen == gen;
}
// Walks the ancestor chain and notifies the nearest ancestor that implements // Walks the ancestor chain and notifies the nearest ancestor that implements
// onChildChanged of a structural change to w (destroy, visibility toggle). // onChildChanged of a structural change to w (destroy, visibility toggle).
// Stops at the first ancestor that handles it. // Stops at the first ancestor that handles it.
@ -475,6 +486,11 @@ static void widgetNotifyChildChanged(WidgetT *w) {
} }
void widgetOpsTestReset(void) {
sFullRepaint = false;
}
// Recursive paint walker. For each visible widget: // Recursive paint walker. For each visible widget:
// 1. Call the widget's paint function (if any) via vtable. // 1. Call the widget's paint function (if any) via vtable.
// 2. If the widget has WCLASS_PAINTS_CHILDREN, stop recursion -- // 2. If the widget has WCLASS_PAINTS_CHILDREN, stop recursion --
@ -541,7 +557,7 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
wclsPaint(w, d, ops, font, colors); wclsPaint(w, d, ops, font, colors);
} }
// Always recurse into children a clean parent may have dirty children // Always recurse into children -- a clean parent may have dirty children
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
widgetPaintOne(c, d, ops, font, colors); widgetPaintOne(c, d, ops, font, colors);
} }

View file

@ -37,10 +37,38 @@ OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGETDIR = $(LIBSDIR)/kpunch/libtasks TARGETDIR = $(LIBSDIR)/kpunch/libtasks
TARGET = $(TARGETDIR)/libtasks.lib TARGET = $(TARGETDIR)/libtasks.lib
.PHONY: all clean # Native host build (test harness). SAN=1 adds ASan/UBSan.
HOSTCC = gcc
HOSTOBJDIR = ../../../../obj/host
HOSTTARGET = $(HOSTOBJDIR)/libtasks.a
HOSTCFLAGS = -O1 -g -fPIC -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -D_GNU_SOURCE -I../libdvx -I../libdvx/thirdparty
ifeq ($(SAN),1)
HOSTCFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer
endif
# COV=1 (make coverage): gcov instrumentation under obj/hostcov.
ifeq ($(COV),1)
HOSTOBJDIR = ../../../../obj/hostcov
HOSTCFLAGS += --coverage
endif
HOST_OBJS = $(patsubst %.c,$(HOSTOBJDIR)/libtasks/%.o,$(SRCS))
.PHONY: all clean host host-clean
all: $(TARGET) all: $(TARGET)
host: $(HOSTTARGET)
$(HOSTTARGET): $(HOST_OBJS)
rm -f $@
ar rcs $@ $(HOST_OBJS)
$(HOSTOBJDIR)/libtasks/%.o: %.c taskSwch.h ../libdvx/thirdparty/stb_ds.h
@mkdir -p $(dir $@)
$(HOSTCC) $(HOSTCFLAGS) -c -o $@ $<
host-clean:
rm -rf $(HOSTOBJDIR)/libtasks $(HOSTTARGET)
$(TARGET): $(OBJS) | $(TARGETDIR) $(TARGET): $(OBJS) | $(TARGETDIR)
$(DXE3GEN) -o $(TARGETDIR)/libtasks.dxe -U $(OBJS) $(DXE3GEN) -o $(TARGETDIR)/libtasks.dxe -U $(OBJS)
mv $(TARGETDIR)/libtasks.dxe $@ mv $(TARGETDIR)/libtasks.dxe $@

View file

@ -49,6 +49,14 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
// Under AddressSanitizer every stack switch is announced so the runtime
// tracks the task stacks (and longjmp back to main) instead of guessing.
// The guard is a compiler feature test, not a platform test; DJGPP never
// defines it.
#if defined(__SANITIZE_ADDRESS__)
#include <sanitizer/common_interface_defs.h>
#endif
// ABI-required stack alignment at function entry (bytes) // ABI-required stack alignment at function entry (bytes)
#define STACK_ALIGN 16 #define STACK_ALIGN 16
@ -105,6 +113,9 @@ typedef struct {
TaskEntryT entry; TaskEntryT entry;
void *arg; void *arg;
int32_t ctxValue; // per-task value saved/restored via tsSetContextHooks int32_t ctxValue; // per-task value saved/restored via tsSetContextHooks
void *asanFake; // sanitizer fake-stack handle (unused outside ASan)
const void *asanBottom;
size_t asanSize;
bool isMain; bool isMain;
bool allocated; // true if slot is in use, false if free for reuse bool allocated; // true if slot is in use, false if free for reuse
} TaskBlockT; } TaskBlockT;
@ -136,14 +147,18 @@ static TsCtxRestoreFnT sCtxRestoreFn = NULL;
// switcher cannot re-enter tsExit until control resumes on another task and // switcher cannot re-enter tsExit until control resumes on another task and
// clears this first. // clears this first.
static uint8_t *sStackPendingFree = NULL; static uint8_t *sStackPendingFree = NULL;
static uint32_t sAsanFrom = 0; // task we last switched away from
// ============================================================================ // ============================================================================
// Forward declarations (alphabetical) // Forward declarations (alphabetical)
// ============================================================================ // ============================================================================
static void asanAfterSwitch(void);
static void asanBeforeSwitch(uint32_t prev, uint32_t next, bool exiting);
static void contextSwitch(TaskContextT *save, TaskContextT *restore); static void contextSwitch(TaskContextT *save, TaskContextT *restore);
static int32_t findFreeSlot(void); static int32_t findFreeSlot(void);
static void freePendingStack(void); static void freePendingStack(void);
static bool isRunnable(uint32_t idx);
static int32_t scanReady(void); static int32_t scanReady(void);
static uint32_t scheduleNext(void); static uint32_t scheduleNext(void);
static void switchContextValue(uint32_t prev, uint32_t next); static void switchContextValue(uint32_t prev, uint32_t next);
@ -169,6 +184,28 @@ void tsYield(void);
// Functions (alphabetical) // Functions (alphabetical)
// ============================================================================ // ============================================================================
// Runs first on the destination stack: hands the sanitizer this task's
// fake stack and records the bounds of the stack we came from.
static void asanAfterSwitch(void) {
#if defined(__SANITIZE_ADDRESS__)
__sanitizer_finish_switch_fiber(tasks[currentIdx].asanFake, &tasks[sAsanFrom].asanBottom, &tasks[sAsanFrom].asanSize);
#endif
}
// Runs last on the departing stack. An exiting task passes no save slot
// so the sanitizer releases its fake stack.
static void asanBeforeSwitch(uint32_t prev, uint32_t next, bool exiting) {
sAsanFrom = prev;
#if defined(__SANITIZE_ADDRESS__)
__sanitizer_start_switch_fiber(exiting ? NULL : &tasks[prev].asanFake, tasks[next].asanBottom, tasks[next].asanSize);
#else
(void)next;
(void)exiting;
#endif
}
// Switch execution from the current task to another by saving and restoring // Switch execution from the current task to another by saving and restoring
// callee-saved registers and the stack pointer. The return address is // callee-saved registers and the stack pointer. The return address is
// captured as a local label so that when another task switches back to us, // captured as a local label so that when another task switches back to us,
@ -285,13 +322,23 @@ static void freePendingStack(void) {
// Scan ready tasks (round-robin starting after currentIdx) for one that // Scan ready tasks (round-robin starting after currentIdx) for one that
// still has credits. On a hit, consume one credit and return its index. // still has credits. On a hit, consume one credit and return its index.
// Returns -1 when no ready task has credits left. // Returns -1 when no ready task has credits left.
// Ready, or the task currently on the CPU (which is Running, not Ready).
static bool isRunnable(uint32_t idx) {
return tasks[idx].state == TaskStateReady || (idx == currentIdx && tasks[idx].state == TaskStateRunning);
}
static int32_t scanReady(void) { static int32_t scanReady(void) {
uint32_t count = (uint32_t)arrlen(tasks); uint32_t count = (uint32_t)arrlen(tasks);
for (uint32_t i = 1; i <= count; i++) { for (uint32_t i = 1; i <= count; i++) {
uint32_t idx = (currentIdx + i) % count; uint32_t idx = (currentIdx + i) % count;
if (tasks[idx].allocated && tasks[idx].state == TaskStateReady && tasks[idx].credits > 0) { // The current task is Running rather than Ready; it may keep the
// CPU while it still has credits, otherwise the remainder of its
// budget is thrown away at the next refill and the documented
// (priority + 1) share never materialises.
if (tasks[idx].allocated && isRunnable(idx) && tasks[idx].credits > 0) {
tasks[idx].credits--; tasks[idx].credits--;
return (int32_t)idx; return (int32_t)idx;
} }
@ -334,7 +381,7 @@ static uint32_t scheduleNext(void) {
bool anyReady = false; bool anyReady = false;
for (uint32_t i = 0; i < count; i++) { for (uint32_t i = 0; i < count; i++) {
if (tasks[i].allocated && tasks[i].state == TaskStateReady) { if (tasks[i].allocated && isRunnable(i)) {
tasks[i].credits = tasks[i].priority + 1; tasks[i].credits = tasks[i].priority + 1;
anyReady = true; anyReady = true;
} }
@ -378,6 +425,8 @@ static void switchContextValue(uint32_t prev, uint32_t next) {
// The trampoline ensures clean task termination even if the app forgets // The trampoline ensures clean task termination even if the app forgets
// to call tsExit() explicitly. // to call tsExit() explicitly.
static void taskTrampoline(void) { static void taskTrampoline(void) {
asanAfterSwitch();
TaskBlockT *task = &tasks[currentIdx]; TaskBlockT *task = &tasks[currentIdx];
task->entry(task->arg); task->entry(task->arg);
tsExit(); tsExit();
@ -435,15 +484,17 @@ int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSi
task->name[TS_NAME_MAX - 1] = '\0'; task->name[TS_NAME_MAX - 1] = '\0';
} }
task->stackSize = stackSize; task->stackSize = stackSize;
task->state = TaskStateReady; task->asanBottom = task->stack;
task->priority = priority; task->asanSize = stackSize;
task->credits = priority + 1; task->state = TaskStateReady;
task->entry = entry; task->priority = priority;
task->arg = arg; task->credits = priority + 1;
task->ctxValue = 0; task->entry = entry;
task->isMain = false; task->arg = arg;
task->allocated = true; task->ctxValue = 0;
task->isMain = false;
task->allocated = true;
// Set up initial stack (grows downward, 16-byte aligned). // Set up initial stack (grows downward, 16-byte aligned).
// The ABI requires 16-byte stack alignment at function entry. We align // The ABI requires 16-byte stack alignment at function entry. We align
@ -511,6 +562,7 @@ void tsExit(void) {
tasks[next].state = TaskStateRunning; tasks[next].state = TaskStateRunning;
switchContextValue(prev, next); switchContextValue(prev, next);
asanBeforeSwitch(prev, next, true);
contextSwitch(&tasks[prev].context, &tasks[next].context); contextSwitch(&tasks[prev].context, &tasks[next].context);
// Terminated task never resumes here // Terminated task never resumes here
} }
@ -649,7 +701,9 @@ int32_t tsPause(uint32_t taskId) {
tasks[next].state = TaskStateRunning; tasks[next].state = TaskStateRunning;
switchContextValue(prev, next); switchContextValue(prev, next);
asanBeforeSwitch(prev, next, false);
contextSwitch(&tasks[prev].context, &tasks[next].context); contextSwitch(&tasks[prev].context, &tasks[next].context);
asanAfterSwitch();
} }
} }
@ -676,6 +730,15 @@ void tsRecoverToMain(void) {
currentIdx = 0; currentIdx = 0;
tasks[0].state = TaskStateRunning; tasks[0].state = TaskStateRunning;
#if defined(__SANITIZE_ADDRESS__)
// The longjmp landed on main's stack without a switch; re-announce it
// (bounds were captured the first time main switched away).
if (tasks[0].asanBottom) {
__sanitizer_start_switch_fiber(NULL, tasks[0].asanBottom, tasks[0].asanSize);
__sanitizer_finish_switch_fiber(NULL, NULL, NULL);
}
#endif
// Crash recovery longjmps here without a normal switch, so restore // Crash recovery longjmps here without a normal switch, so restore
// the main task's context value explicitly -- otherwise the crashed // the main task's context value explicitly -- otherwise the crashed
// task's value (e.g. its app id) leaks into the shell. // task's value (e.g. its app id) leaks into the shell.
@ -781,5 +844,7 @@ void tsYield(void) {
tasks[next].state = TaskStateRunning; tasks[next].state = TaskStateRunning;
switchContextValue(prev, next); switchContextValue(prev, next);
asanBeforeSwitch(prev, next, false);
contextSwitch(&tasks[prev].context, &tasks[next].context); contextSwitch(&tasks[prev].context, &tasks[next].context);
asanAfterSwitch();
} }

View file

@ -259,7 +259,7 @@ bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY,
(*scrollPos)++; (*scrollPos)++;
} }
} else { } else {
// Trough page up/down based on which side of thumb // Trough -- page up/down based on which side of thumb
int32_t trackLen = popH - POPUP_SCROLLBAR_W * 2; int32_t trackLen = popH - POPUP_SCROLLBAR_W * 2;
int32_t thumbSize = (int32_t)(((int64_t)visibleItems * trackLen) / itemCount); int32_t thumbSize = (int32_t)(((int64_t)visibleItems * trackLen) / itemCount);

View file

@ -395,7 +395,7 @@ int rs232WriteBuf(int com, const char *data, int len);
// ======================================================================== // ========================================================================
// Interrupt service routine // Baud rate helpers and interrupt service routine
// ======================================================================== // ========================================================================
static int32_t bpsToDivisor(int32_t bps) { static int32_t bpsToDivisor(int32_t bps) {
@ -410,18 +410,6 @@ static int32_t bpsToDivisor(int32_t bps) {
} }
static int32_t divisorToBps(uint16_t divisor) {
uint32_t i;
for (i = 0; i < BPS_MAP_COUNT; i++) {
if (sBpsMap[i].divisor == divisor) {
return sBpsMap[i].bps;
}
}
return RS232_ERR_INVALID_BPS;
}
// Single shared ISR for all COM ports. ISR sharing is necessary because // Single shared ISR for all COM ports. ISR sharing is necessary because
// COM1/COM3 typically share IRQ4 and COM2/COM4 share IRQ3. Having one ISR // COM1/COM3 typically share IRQ4 and COM2/COM4 share IRQ3. Having one ISR
// that polls all ports avoids the complexity of per-IRQ handlers. // that polls all ports avoids the complexity of per-IRQ handlers.
@ -551,6 +539,18 @@ static void comGeneralIsr(void) {
} }
static int32_t divisorToBps(uint16_t divisor) {
uint32_t i;
for (i = 0; i < BPS_MAP_COUNT; i++) {
if (sBpsMap[i].divisor == divisor) {
return sBpsMap[i].bps;
}
}
return RS232_ERR_INVALID_BPS;
}
// ======================================================================== // ========================================================================
// DPMI utility functions // DPMI utility functions
// ======================================================================== // ========================================================================
@ -781,6 +781,11 @@ static int installIrqHandler(int irq) {
} }
static void irqRestore(uint32_t flags) {
asm volatile("pushl %0\n\tpopfl" : : "r"(flags) : "memory", "cc");
}
// Disable interrupts, returning the previous EFLAGS so the caller can // Disable interrupts, returning the previous EFLAGS so the caller can
// restore the interrupt state it was entered with instead of blindly // restore the interrupt state it was entered with instead of blindly
// re-enabling with STI. // re-enabling with STI.
@ -793,11 +798,6 @@ static uint32_t irqSave(void) {
} }
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);
@ -1340,10 +1340,18 @@ int rs232SetData(int com, int dataBits) {
} }
switch (dataBits) { switch (dataBits) {
case 5: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_5); return RS232_SUCCESS; case 5:
case 6: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_6); return RS232_SUCCESS; UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_5);
case 7: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_7); return RS232_SUCCESS; return RS232_SUCCESS;
case 8: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_8); return RS232_SUCCESS; case 6:
UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_6);
return RS232_SUCCESS;
case 7:
UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_7);
return RS232_SUCCESS;
case 8:
UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~DATA_MASK) | DATA_8);
return RS232_SUCCESS;
} }
return RS232_ERR_INVALID_DATA; return RS232_ERR_INVALID_DATA;
} }
@ -1483,11 +1491,26 @@ int rs232SetParity(int com, char parity) {
} }
switch (parity) { switch (parity) {
case 'n': case 'N': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_NONE); return RS232_SUCCESS; case 'n':
case 'e': case 'E': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_EVEN); return RS232_SUCCESS; case 'N':
case 'o': case 'O': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_ODD); return RS232_SUCCESS; UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_NONE);
case 'm': case 'M': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_MARK); return RS232_SUCCESS; return RS232_SUCCESS;
case 's': case 'S': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_SPACE); return RS232_SUCCESS; case 'e':
case 'E':
UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_EVEN);
return RS232_SUCCESS;
case 'o':
case 'O':
UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_ODD);
return RS232_SUCCESS;
case 'm':
case 'M':
UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_MARK);
return RS232_SUCCESS;
case 's':
case 'S':
UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_SPACE);
return RS232_SUCCESS;
} }
return RS232_ERR_INVALID_PARITY; return RS232_ERR_INVALID_PARITY;
} }
@ -1524,8 +1547,12 @@ int rs232SetStop(int com, int stopBits) {
} }
switch (stopBits) { switch (stopBits) {
case 1: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~STOP_MASK) | STOP_1); return RS232_SUCCESS; case 1:
case 2: UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~STOP_MASK) | STOP_2); return RS232_SUCCESS; UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~STOP_MASK) | STOP_1);
return RS232_SUCCESS;
case 2:
UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~STOP_MASK) | STOP_2);
return RS232_SUCCESS;
} }
return RS232_ERR_INVALID_STOP; return RS232_ERR_INVALID_STOP;
} }

View file

@ -124,21 +124,13 @@ static void completeHandshake(SecLinkT *link) {
uint8_t txKey[SEC_XTEA_KEY_SIZE]; uint8_t txKey[SEC_XTEA_KEY_SIZE];
uint8_t rxKey[SEC_XTEA_KEY_SIZE]; uint8_t rxKey[SEC_XTEA_KEY_SIZE];
bool weAreLower; bool weAreLower;
int rc;
// An invalid remote key (out of range, e.g. all zeros) must fail the // An invalid remote key (out of range, e.g. all zeros) must fail the
// handshake -- ignoring these return codes would leave masterKey as // handshake -- ignoring these return codes would leave masterKey as
// uninitialized stack memory and enter READY with garbage key material. // uninitialized stack memory and enter READY with garbage key material.
rc = secDhComputeSecret(link->dh, link->remoteKey, SEC_DH_KEY_SIZE); // The derive call cannot fail once the secret is computed, but it is
if (rc != SEC_SUCCESS) { // kept under the same check so a future error path stays covered.
secDhDestroy(link->dh); if (secDhComputeSecret(link->dh, link->remoteKey, SEC_DH_KEY_SIZE) != SEC_SUCCESS || secDhDeriveKey(link->dh, masterKey, SEC_XTEA_KEY_SIZE) != SEC_SUCCESS) {
link->dh = 0;
link->state = STATE_ERROR;
return;
}
rc = secDhDeriveKey(link->dh, masterKey, SEC_XTEA_KEY_SIZE);
if (rc != SEC_SUCCESS) {
secDhDestroy(link->dh); secDhDestroy(link->dh);
link->dh = 0; link->dh = 0;
link->state = STATE_ERROR; link->state = STATE_ERROR;
@ -282,12 +274,9 @@ int secLinkHandshake(SecLinkT *link) {
return SECLINK_ERR_ALLOC; return SECLINK_ERR_ALLOC;
} }
rc = secDhGenerateKeys(link->dh); // Cannot fail for a non-NULL context (the only failure is a NULL dh,
if (rc != SEC_SUCCESS) { // which was checked above).
secDhDestroy(link->dh); secDhGenerateKeys(link->dh);
link->dh = 0;
return SECLINK_ERR_HANDSHAKE;
}
// Export our public key // Export our public key
len = SEC_DH_KEY_SIZE; len = SEC_DH_KEY_SIZE;

View file

@ -56,6 +56,8 @@ typedef char secDhKeySizeMatch[(BN_BYTES == SEC_DH_KEY_SIZE) ? 1 : -1];
#define DH_PRIVATE_BITS 256 #define DH_PRIVATE_BITS 256
#define DH_PRIVATE_BYTES (DH_PRIVATE_BITS / 8) #define DH_PRIVATE_BYTES (DH_PRIVATE_BITS / 8)
#define DH_PRIVATE_WORDS (DH_PRIVATE_BITS / 32)
#define DH_PRIVATE_TOP_BIT 0x80000000u
#define XTEA_ROUNDS 32 #define XTEA_ROUNDS 32
#define XTEA_DELTA 0x9E3779B9 #define XTEA_DELTA 0x9E3779B9
@ -580,10 +582,12 @@ int secDhGenerateKeys(SecDhT *dh) {
bnClear(&dh->privateKey); bnClear(&dh->privateKey);
secRngBytes((uint8_t *)dh->privateKey.w, DH_PRIVATE_BYTES); secRngBytes((uint8_t *)dh->privateKey.w, DH_PRIVATE_BYTES);
// Ensure private key >= 2 // Force the top bit of the 256-bit exponent. This guarantees the
if (bnBitLength(&dh->privateKey) <= 1) { // private key is >= 2 (a zero or one exponent would make the public
dh->privateKey.w[0] = 2; // value trivial) and fixes the exponent bit length at DH_PRIVATE_BITS,
} // so the square-and-multiply loop in bnModExp always runs the same
// number of iterations regardless of the random draw.
dh->privateKey.w[DH_PRIVATE_WORDS - 1] |= DH_PRIVATE_TOP_BIT;
// public = g^private mod p // public = g^private mod p
bnModExp(&dh->publicKey, &sDhGenerator, &dh->privateKey, &sDhPrime, sDhM0Inv, &sDhR2); bnModExp(&dh->publicKey, &sDhGenerator, &dh->privateKey, &sDhPrime, sDhM0Inv, &sDhR2);

View file

@ -69,14 +69,14 @@ typedef struct SecCipherS SecCipherT;
// RNG -- seed before generating keys. Hardware entropy is weak (~20 bits); // RNG -- seed before generating keys. Hardware entropy is weak (~20 bits);
// callers should supplement with keyboard timing, mouse jitter, etc. // callers should supplement with keyboard timing, mouse jitter, etc.
int secRngGatherEntropy(uint8_t *buf, int len);
void secRngAddEntropy(const uint8_t *data, int len); void secRngAddEntropy(const uint8_t *data, int len);
void secRngBytes(uint8_t *buf, int len); void secRngBytes(uint8_t *buf, int len);
int secRngGatherEntropy(uint8_t *buf, int len);
void secRngSeed(const uint8_t *entropy, int len); void secRngSeed(const uint8_t *entropy, int len);
// Diffie-Hellman key exchange (1024-bit, RFC 2409 Group 2) // Diffie-Hellman key exchange (1024-bit, RFC 2409 Group 2)
SecDhT *secDhCreate(void);
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
SecDhT *secDhCreate(void);
int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen); int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen);
void secDhDestroy(SecDhT *dh); void secDhDestroy(SecDhT *dh);
int secDhGenerateKeys(SecDhT *dh); int secDhGenerateKeys(SecDhT *dh);

View file

@ -75,10 +75,68 @@ MKKEYWORDHASH = $(OBJDIR)/mkkeywordhash
OBJS = $(OBJDIR)/dvxSql.o $(SQLITE_OBJS) $(OBJDIR)/sqlite_parse.o $(OBJDIR)/sqlite_opcodes.o OBJS = $(OBJDIR)/dvxSql.o $(SQLITE_OBJS) $(OBJDIR)/sqlite_parse.o $(OBJDIR)/sqlite_opcodes.o
.PHONY: all clean # ---- Native host build (test harness) ----
#
# Builds the same SQLite sources plus the wrapper with the host gcc into
# a static archive the test executables link with -rdynamic so the
# datactrl widget .so resolves dvxSql* at load time. SAN=1 (default)
# matches the dvxtest sanitizer flags; COV=1 instruments for gcov under
# obj/hostcov. The generated sources (phase 1) are shared with the DOS
# build.
SAN ?= 1
ifeq ($(COV),1)
HOSTSUB = hostcov
else
HOSTSUB = host
endif
HOSTOBJDIR = ../../../../obj/$(HOSTSUB)/sql
HOSTTARGET = ../../../../obj/$(HOSTSUB)/dvxsql.a
HOSTCFLAGS = -O1 -g -fPIC -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -D_GNU_SOURCE \
-DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION \
-I$(SQLITE_SRC) -I$(SQLITE_DIR)/examples -I$(OBJDIR)
# Vendored SQLite 3.5 predates modern gcc diagnostics; it is third party
# code and is compiled without -Werror and without UBSan (its hash
# function relies on signed shift wraparound).
HOSTSQLITEFLAGS = -O1 -g -fPIC -w -D_GNU_SOURCE -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -I$(SQLITE_SRC) -I$(SQLITE_DIR)/examples -I$(OBJDIR)
ifeq ($(SAN),1)
HOSTCFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer
HOSTSQLITEFLAGS+= -fsanitize=address -fno-omit-frame-pointer
endif
ifeq ($(COV),1)
HOSTCFLAGS += --coverage
endif
HOST_SQLITE_OBJS = $(SQLITE_SRCS:%=$(HOSTOBJDIR)/sqlite_%.o) $(HOSTOBJDIR)/sqlite_parse.o $(HOSTOBJDIR)/sqlite_opcodes.o
HOST_OBJS = $(HOSTOBJDIR)/dvxSql.o $(HOST_SQLITE_OBJS)
.PHONY: all clean host host-clean
all: $(TARGET) $(TARGETDIR)/dvxsql.dep all: $(TARGET) $(TARGETDIR)/dvxsql.dep
host: $(HOSTTARGET)
$(HOSTTARGET): $(HOST_OBJS)
rm -f $@
ar rcs $@ $(HOST_OBJS)
$(HOSTOBJDIR)/dvxSql.o: dvxSql.c dvxSql.h | $(HOSTOBJDIR)
$(HOSTCC) $(HOSTCFLAGS) -c -o $@ $<
$(HOSTOBJDIR)/sqlite_%.o: $(SQLITE_SRC)/%.c $(GEN_OPCODES_H) $(GEN_KEYWORDHASH) $(GEN_PARSE_H) | $(HOSTOBJDIR)
$(HOSTCC) $(HOSTSQLITEFLAGS) -c -o $@ $<
$(HOSTOBJDIR)/sqlite_parse.o: $(GEN_PARSE_C) $(GEN_OPCODES_H) | $(HOSTOBJDIR)
$(HOSTCC) $(HOSTSQLITEFLAGS) -c -o $@ $<
$(HOSTOBJDIR)/sqlite_opcodes.o: $(GEN_OPCODES_C) | $(HOSTOBJDIR)
$(HOSTCC) $(HOSTSQLITEFLAGS) -c -o $@ $<
$(HOSTOBJDIR):
mkdir -p $(HOSTOBJDIR)
host-clean:
rm -rf $(HOSTOBJDIR) $(HOSTTARGET)
$(TARGETDIR)/dvxsql.dep: dvxsql.dep | $(TARGETDIR) $(TARGETDIR)/dvxsql.dep: dvxsql.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@ sed 's/$$/\r/' $< > $@

View file

@ -30,7 +30,6 @@
#include "textHelp.h" #include "textHelp.h"
#include <ctype.h> #include <ctype.h>
#include <time.h>
// ============================================================ // ============================================================
// Static state // Static state
@ -38,7 +37,7 @@
// Cursor blink state // Cursor blink state
#define CURSOR_BLINK_MS 250 #define CURSOR_BLINK_MS 250
static clock_t sCursorBlinkTime = 0; static PlatformTicksT sCursorBlinkTime = 0;
// Slack added to line cache capacity when growing. Bigger values // Slack added to line cache capacity when growing. Bigger values
// mean fewer reallocs at the cost of wasted space; 256 empirically // mean fewer reallocs at the cost of wasted space; 256 empirically
@ -62,8 +61,8 @@ static WidgetT *sLastSelectedWidget = NULL;
// ============================================================ // ============================================================
void clearOtherSelections(WidgetT *except); void clearOtherSelections(WidgetT *except);
void textEditClearWidgetRef(WidgetT *w);
bool isWordChar(char c); bool isWordChar(char c);
void textEditClearWidgetRef(WidgetT *w);
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd); static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd);
void textEditDrawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors); void textEditDrawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors);
void textEditEnsureVisible(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, int32_t tabW, int32_t visRows, int32_t visCols, int32_t *pScrollRow, int32_t *pScrollCol); void textEditEnsureVisible(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, int32_t tabW, int32_t visRows, int32_t visCols, int32_t *pScrollRow, int32_t *pScrollCol);
@ -78,12 +77,12 @@ void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char
void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen); void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen);
static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, int32_t len); static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, int32_t len);
void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth); void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth);
void textEditMultiFree(TextEditMultiT *te);
bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth);
int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len); int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len);
int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row); int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row); int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len); int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len);
void textEditMultiFree(TextEditMultiT *te);
bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth);
void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col); void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col);
int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor); int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
bool textEditReplaceSelectionMulti(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor); bool textEditReplaceSelectionMulti(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
@ -93,6 +92,7 @@ uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defa
int32_t textEditVisualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW); int32_t textEditVisualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW);
int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW); int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW);
static void textHelpInit(void) __attribute__((constructor)); static void textHelpInit(void) __attribute__((constructor));
void textHelpTestReset(void);
void wgtUpdateCursorBlink(void); void wgtUpdateCursorBlink(void);
void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd); void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd);
void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect); void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect);
@ -100,11 +100,11 @@ void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const c
void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor); void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor);
void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts); void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts);
void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t textX, int32_t textY, int32_t innerW, int32_t visCols, int32_t visRows, int32_t scrollRow, int32_t scrollCol, int32_t cursorRow, int32_t cursorCol, int32_t selAnchor, int32_t selCursor, int32_t tabW, uint32_t fg, uint32_t bg, bool showCursor, const TextEditPaintHooksT *hooks); void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t textX, int32_t textY, int32_t innerW, int32_t visCols, int32_t visRows, int32_t scrollRow, int32_t scrollCol, int32_t cursorRow, int32_t cursorCol, int32_t selAnchor, int32_t selCursor, int32_t tabW, uint32_t fg, uint32_t bg, bool showCursor, const TextEditPaintHooksT *hooks);
void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll);
int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff);
int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff);
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t fieldWidth); void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t fieldWidth);
void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX); void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX);
int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff);
void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll);
int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff);
int32_t wordBoundaryLeft(const char *buf, int32_t pos); int32_t wordBoundaryLeft(const char *buf, int32_t pos);
int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);
int32_t wordEnd(const char *buf, int32_t len, int32_t pos); int32_t wordEnd(const char *buf, int32_t len, int32_t pos);
@ -191,12 +191,6 @@ static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor,
hi = *pLen; hi = *pLen;
} }
if (lo >= hi) {
*pSelStart = -1;
*pSelEnd = -1;
return;
}
memmove(buf + lo, buf + hi, *pLen - hi + 1); memmove(buf + lo, buf + hi, *pLen - hi + 1);
*pLen -= (hi - lo); *pLen -= (hi - lo);
*pCursor = lo; *pCursor = lo;
@ -302,11 +296,21 @@ bool textEditFindNext(TextEditLineCacheT *lc, const char *buf, int32_t len, cons
} }
if (match) { if (match) {
*pSelAnchor = pos; // Park the cursor on the far side of the match relative to the
*pSelCursor = pos + needleLen; // search direction (start when backward, start when forward via
// the anchor/cursor split below), so repeating the search in the
// same direction advances past this match instead of re-finding
// it. The selection always covers the match; only which end is
// the live cursor differs.
if (forward) {
*pSelAnchor = pos;
*pSelCursor = pos + needleLen;
} else {
*pSelAnchor = pos + needleLen;
*pSelCursor = pos;
}
int32_t cursorOff = forward ? pos : pos + needleLen; textEditOffToRowCol(lc, buf, len, pos, pCursorRow, pCursorCol);
textEditOffToRowCol(lc, buf, len, cursorOff, pCursorRow, pCursorCol);
*pDesiredCol = *pCursorCol; *pDesiredCol = *pCursorCol;
return true; return true;
} }
@ -627,46 +631,6 @@ void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth) {
} }
void textEditMultiFree(TextEditMultiT *te) {
free(te->buf);
free(te->undoBuf);
textEditLineCacheFree(&te->lines);
te->buf = NULL;
te->undoBuf = NULL;
te->bufSize = 0;
te->len = 0;
}
// Allocates the buf/undoBuf pair sized for maxLen characters plus
// NUL, and initializes cursor/scroll/selection/undo state. Returns
// false (leaving te->buf == NULL) if allocation fails.
bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth) {
memset(te, 0, sizeof(*te));
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
te->buf = (char *)malloc(bufSize);
te->undoBuf = (char *)malloc(bufSize);
te->bufSize = bufSize;
if (!te->buf || !te->undoBuf) {
free(te->buf);
free(te->undoBuf);
te->buf = NULL;
te->undoBuf = NULL;
return false;
}
te->buf[0] = '\0';
te->undoBuf[0] = '\0';
te->selAnchor = -1;
te->selCursor = -1;
textEditLineCacheInit(&te->lines, tabWidth);
return true;
}
int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len) { int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len) {
textEditLineCacheEnsure(lc, buf, len); textEditLineCacheEnsure(lc, buf, len);
return lc->cachedLines; return lc->cachedLines;
@ -725,6 +689,46 @@ int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len)
} }
void textEditMultiFree(TextEditMultiT *te) {
free(te->buf);
free(te->undoBuf);
textEditLineCacheFree(&te->lines);
te->buf = NULL;
te->undoBuf = NULL;
te->bufSize = 0;
te->len = 0;
}
// Allocates the buf/undoBuf pair sized for maxLen characters plus
// NUL, and initializes cursor/scroll/selection/undo state. Returns
// false (leaving te->buf == NULL) if allocation fails.
bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth) {
memset(te, 0, sizeof(*te));
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
te->buf = (char *)malloc(bufSize);
te->undoBuf = (char *)malloc(bufSize);
te->bufSize = bufSize;
if (!te->buf || !te->undoBuf) {
free(te->buf);
free(te->undoBuf);
te->buf = NULL;
te->undoBuf = NULL;
return false;
}
te->buf[0] = '\0';
te->undoBuf[0] = '\0';
te->selAnchor = -1;
te->selCursor = -1;
textEditLineCacheInit(&te->lines, tabWidth);
return true;
}
void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col) { void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col) {
textEditLineCacheEnsure(lc, buf, len); textEditLineCacheEnsure(lc, buf, len);
@ -746,8 +750,11 @@ void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, i
return; return;
} }
// Search rows [0, cachedLines-1] only: lineOffsets[cachedLines] is the
// buffer-end sentinel, and an offset at the very end of a buffer that
// does not end in a newline must map to the LAST line, not one past it.
int32_t lo = 0; int32_t lo = 0;
int32_t hi = lc->cachedLines; int32_t hi = lc->cachedLines - 1;
while (lo < hi) { while (lo < hi) {
int32_t mid = (lo + hi + 1) / 2; int32_t mid = (lo + hi + 1) / 2;
@ -818,15 +825,11 @@ int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, i
*pSelAnchor = -1; *pSelAnchor = -1;
*pSelCursor = -1; *pSelCursor = -1;
// Dirty the cache BEFORE converting the cursor: replaceAll changed the // Dirty the cache BEFORE converting the cursor: replaceAll changed the
// buffer, so the row/col conversions below must rebuild from it. // buffer, so the row/col conversions below must rebuild from it. The
// cursor is re-derived from its clamped byte offset so a row that no
// longer exists (a replaced newline) cannot linger in *pCursorRow.
textEditLineCacheDirty(lc); textEditLineCacheDirty(lc);
textEditOffToRowCol(lc, buf, *pLen, textEditRowColToOff(lc, buf, *pLen, *pCursorRow, *pCursorCol), pCursorRow, pCursorCol);
int32_t cursorOff = textEditRowColToOff(lc, buf, *pLen, *pCursorRow, *pCursorCol);
if (cursorOff > *pLen) {
textEditOffToRowCol(lc, buf, *pLen, *pLen, pCursorRow, pCursorCol);
}
*pDesiredCol = *pCursorCol; *pDesiredCol = *pCursorCol;
} }
@ -996,9 +999,20 @@ static void textHelpInit(void) {
} }
// Test-harness hook: wgtTestReset drops the blink hook and the destroy
// subscriber list this module registered from its constructor, so the
// harness calls this after every reset to clear the selection tracker
// and register again.
void textHelpTestReset(void) {
sLastSelectedWidget = NULL;
sCursorBlinkTime = 0;
textHelpInit();
}
void wgtUpdateCursorBlink(void) { void wgtUpdateCursorBlink(void) {
clock_t now = clock(); PlatformTicksT now = platformClock();
clock_t interval = (clock_t)CURSOR_BLINK_MS * CLOCKS_PER_SEC / 1000; PlatformTicksT interval = PLATFORM_MS_TO_TICKS(CURSOR_BLINK_MS);
if ((now - sCursorBlinkTime) >= interval) { if ((now - sCursorBlinkTime) >= interval) {
sCursorBlinkTime = now; sCursorBlinkTime = now;
@ -1234,14 +1248,12 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
#define SEL_LO() (*pSA < *pSC ? *pSA : *pSC) #define SEL_LO() (*pSA < *pSC ? *pSA : *pSC)
#define SEL_HI() (*pSA < *pSC ? *pSC : *pSA) #define SEL_HI() (*pSA < *pSC ? *pSC : *pSA)
if (HAS_SEL()) { // A mouse click leaves a degenerate [p,p] range behind as the drag
if (*pSA > *pLen) { // anchor; treat it as "no selection" so SEL_BEGIN anchors at the
*pSA = *pLen; // cursor instead of the stale click position.
} if (*pSA >= 0 && *pSA == *pSC) {
*pSA = -1;
if (*pSC > *pLen) { *pSC = -1;
*pSC = *pLen;
}
} }
// Ctrl+A -- select all // Ctrl+A -- select all
@ -1302,8 +1314,8 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
*pDesiredCol = *pCol; *pDesiredCol = *pCol;
} }
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
textEditLineCacheDirty(lc); textEditLineCacheDirty(lc);
@ -1329,8 +1341,8 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
*pSC = -1; *pSC = -1;
*pDesiredCol = *pCol; *pDesiredCol = *pCol;
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
textEditLineCacheDirty(lc); textEditLineCacheDirty(lc);
@ -1380,8 +1392,8 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
*pSA = -1; *pSA = -1;
*pSC = -1; *pSC = -1;
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
textEditLineCacheDirty(lc); textEditLineCacheDirty(lc);
@ -1431,8 +1443,8 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
*pDesiredCol = indent; *pDesiredCol = indent;
} }
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
textEditLineCacheDirty(lc); textEditLineCacheDirty(lc);
@ -1457,8 +1469,8 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
*pDesiredCol = *pCol; *pDesiredCol = *pCol;
textEditLineCacheDirty(lc); textEditLineCacheDirty(lc);
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} else { } else {
int32_t off = CUR_OFF(); int32_t off = CUR_OFF();
@ -1477,8 +1489,8 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
textEditLineCacheNotifyDelete(lc, buf, *pLen, off - 1, 1); textEditLineCacheNotifyDelete(lc, buf, *pLen, off - 1, 1);
} }
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} }
} }
@ -1502,8 +1514,8 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
*pDesiredCol = *pCol; *pDesiredCol = *pCol;
textEditLineCacheDirty(lc); textEditLineCacheDirty(lc);
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} else { } else {
int32_t off = CUR_OFF(); int32_t off = CUR_OFF();
@ -1520,8 +1532,8 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
textEditLineCacheNotifyDelete(lc, buf, *pLen, off, 1); textEditLineCacheNotifyDelete(lc, buf, *pLen, off, 1);
} }
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} }
} }
@ -1784,12 +1796,10 @@ navigation: {
(*pCol)++; (*pCol)++;
*pDesiredCol = *pCol; *pDesiredCol = *pCol;
textEditLineCacheNotifyInsert(lc, buf, *pLen, off, 1); textEditLineCacheNotifyInsert(lc, buf, *pLen, off, 1);
} else {
textEditLineCacheDirty(lc);
} }
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} }
@ -2011,6 +2021,14 @@ void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const Bitmap
// This is the core single-line text editing engine, parameterized by // This is the core single-line text editing engine, parameterized by
// pointer to allow reuse across TextInput, Spinner, and ComboBox. // pointer to allow reuse across TextInput, Spinner, and ComboBox.
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t fieldWidth) { void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t fieldWidth) {
// A mouse click leaves a degenerate [p,p] range behind as the drag
// anchor. Treat it as "no selection" here so a later Shift+arrow
// anchors at the cursor, not at the stale click position.
if (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelStart == *pSelEnd) {
*pSelStart = -1;
*pSelEnd = -1;
}
bool shift = (mod & KEY_MOD_SHIFT) != 0; bool shift = (mod & KEY_MOD_SHIFT) != 0;
bool hasSel = (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelEnd >= 0 && *pSelStart != *pSelEnd); bool hasSel = (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelEnd >= 0 && *pSelStart != *pSelEnd);
int32_t selLo = hasSel ? (*pSelStart < *pSelEnd ? *pSelStart : *pSelEnd) : -1; int32_t selLo = hasSel ? (*pSelStart < *pSelEnd ? *pSelStart : *pSelEnd) : -1;
@ -2091,8 +2109,8 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
*pCursor += paste; *pCursor += paste;
} }
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} }
@ -2110,8 +2128,8 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} }
@ -2153,8 +2171,8 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
*pSelEnd = -1; *pSelEnd = -1;
} }
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
goto adjustScroll; goto adjustScroll;
@ -2179,8 +2197,8 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
(*pLen)++; (*pLen)++;
(*pCursor)++; (*pCursor)++;
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} }
} else if (key == 8) { } else if (key == 8) {
@ -2192,8 +2210,8 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} else if (*pCursor > 0) { } else if (*pCursor > 0) {
if (undoBuf) { if (undoBuf) {
@ -2205,8 +2223,8 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
(*pLen)--; (*pLen)--;
(*pCursor)--; (*pCursor)--;
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} }
} else if (key == KEY_LEFT) { } else if (key == KEY_LEFT) {
@ -2352,8 +2370,8 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} else if (*pCursor < *pLen) { } else if (*pCursor < *pLen) {
if (undoBuf) { if (undoBuf) {
@ -2364,8 +2382,8 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_
memmove(buf + pos, buf + pos + 1, *pLen - pos); memmove(buf + pos, buf + pos + 1, *pLen - pos);
(*pLen)--; (*pLen)--;
if (w->onChange) { if (!widgetFireChange(w)) {
w->onChange(w); return;
} }
} }
} else { } else {
@ -2438,6 +2456,18 @@ void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT
} }
// Converts a mouse coordinate (during an active thumb drag) to a
// clamped scroll value. dragOff is the thumb-relative offset captured
// when the drag started (from the hit-test's *pDragOff).
int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff) {
(void)vertical;
int32_t maxScroll = total - visible;
int32_t trackLen = len - thick * 2;
int32_t rel = mouseCoord - sbStart - thick - dragOff;
return widgetScrollbarThumbDragScroll(trackLen, total, visible, rel, maxScroll);
}
// Paints a single vertical or horizontal text-grid scrollbar: // Paints a single vertical or horizontal text-grid scrollbar:
// trough, end-arrow buttons with direction triangles, and thumb. // trough, end-arrow buttons with direction triangles, and thumb.
// All items are in logical units (lines/cols). // All items are in logical units (lines/cols).
@ -2504,18 +2534,6 @@ void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorScheme
} }
// Converts a mouse coordinate (during an active thumb drag) to a
// clamped scroll value. dragOff is the thumb-relative offset captured
// when the drag started (from the hit-test's *pDragOff).
int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff) {
(void)vertical;
int32_t maxScroll = total - visible;
int32_t trackLen = len - thick * 2;
int32_t rel = mouseCoord - sbStart - thick - dragOff;
return widgetScrollbarThumbDragScroll(trackLen, total, visible, rel, maxScroll);
}
// Classifies a mouse click against a text-grid scrollbar. Returns // Classifies a mouse click against a text-grid scrollbar. Returns
// one of the TEXT_SB_HIT_* codes. Callers typically translate UP/ // one of the TEXT_SB_HIT_* codes. Callers typically translate UP/
// DOWN to single-step scrolls and PAGE_UP/PAGE_DOWN to page-sized // DOWN to single-step scrolls and PAGE_UP/PAGE_DOWN to page-sized
@ -2538,11 +2556,6 @@ int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_
} }
int32_t trackLen = len - thick * 2; int32_t trackLen = len - thick * 2;
if (trackLen <= 0) {
return TEXT_SB_HIT_NONE;
}
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);

View file

@ -40,6 +40,9 @@
void wgtUpdateCursorBlink(void); void wgtUpdateCursorBlink(void);
// Test-only: reset module statics and re-register the blink hook.
void textHelpTestReset(void);
// ============================================================ // ============================================================
// Selection management // Selection management
// ============================================================ // ============================================================
@ -89,13 +92,13 @@ typedef struct TextEditLineCacheT {
int32_t tabWidth; // tab stop width used to compute lineVisLens int32_t tabWidth; // tab stop width used to compute lineVisLens
} TextEditLineCacheT; } TextEditLineCacheT;
void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth);
void textEditLineCacheFree(TextEditLineCacheT *lc);
void textEditLineCacheDirty(TextEditLineCacheT *lc); void textEditLineCacheDirty(TextEditLineCacheT *lc);
void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth);
void textEditLineCacheEnsure(TextEditLineCacheT *lc, const char *buf, int32_t len); void textEditLineCacheEnsure(TextEditLineCacheT *lc, const char *buf, int32_t len);
void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen); void textEditLineCacheFree(TextEditLineCacheT *lc);
void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth);
void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen); void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen);
void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen);
void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth);
int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len); int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len);
int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row); int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row); int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
@ -120,8 +123,8 @@ int32_t textEditRowColToOff(TextEditLineCacheT *lc, const char *buf, int32_t len
#define TEXT_SYNTAX_TYPE 6 #define TEXT_SYNTAX_TYPE 6
#define TEXT_SYNTAX_MAX 7 #define TEXT_SYNTAX_MAX 7
uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom);
void textEditDrawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors); void textEditDrawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors);
uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom);
// ============================================================ // ============================================================
// Scroll adjustment // Scroll adjustment
@ -172,8 +175,8 @@ void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const Bitmap
// //
// widgetTextEditMultiDragUpdateArea is called during a drag to // widgetTextEditMultiDragUpdateArea is called during a drag to
// auto-scroll the view and extend the selection. // auto-scroll the view and extend the selection.
void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor);
void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t *pScrollRow, int32_t scrollCol, int32_t visRows, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelCursor); void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t *pScrollRow, int32_t scrollCol, int32_t visRows, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelCursor);
void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor);
// ============================================================ // ============================================================
// Multi-line key handler // Multi-line key handler
@ -231,8 +234,8 @@ void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLine
#define TEXT_SB_HIT_PAGE_DOWN 4 #define TEXT_SB_HIT_PAGE_DOWN 4
#define TEXT_SB_HIT_THUMB 5 #define TEXT_SB_HIT_THUMB 5
void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll);
int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff); int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff);
void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll);
int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff); int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff);
// ============================================================ // ============================================================
@ -269,8 +272,8 @@ typedef struct TextEditMultiT {
TextEditLineCacheT lines; TextEditLineCacheT lines;
} TextEditMultiT; } TextEditMultiT;
bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth);
void textEditMultiFree(TextEditMultiT *te); void textEditMultiFree(TextEditMultiT *te);
bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth);
// ============================================================ // ============================================================
// Multi-line text operations // Multi-line text operations
@ -282,7 +285,8 @@ void textEditMultiFree(TextEditMultiT *te);
// //
// textEditFindNext: searches forward/backward from the current // textEditFindNext: searches forward/backward from the current
// cursor position. If found, selects the match and moves the cursor // cursor position. If found, selects the match and moves the cursor
// to the match's start (forward) or end (backward). Returns true // to the match's start, so repeating the search in either direction
// advances past the match instead of re-finding it. Returns true
// on match, false on no-match (no wrap-around). // on match, false on no-match (no wrap-around).
// //
// textEditReplaceAll: replaces every occurrence of needle with // textEditReplaceAll: replaces every occurrence of needle with

View file

@ -24,7 +24,9 @@
# #
# Builds the bootstrap loader (dvx.exe) that loads DXE modules. # Builds the bootstrap loader (dvx.exe) that loads DXE modules.
# Links dvxPlatformDos.c directly -- the platform layer provides # Links dvxPlatformDos.c directly -- the platform layer provides
# the DXE export table via platformRegisterDxeExports(). # the DXE export table via platformRegisterDxeExports(). dvxPlatformUtil.c
# (logging, glob, path helpers) and dvxMemTrack.c (per-app allocation
# tracking) are the backend-independent halves of the platform layer.
DJGPP_PREFIX = $(HOME)/djgpp/djgpp DJGPP_PREFIX = $(HOME)/djgpp/djgpp
DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib
@ -40,7 +42,7 @@ BINDIR = ../../bin
SRCS = loaderMain.c SRCS = loaderMain.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
POBJS = $(POBJDIR)/dvxPlatformDos.o $(POBJDIR)/dvxPlatformUtil.o $(POBJDIR)/dvxPrefs.o $(OBJDIR)/dvxhlpc.o POBJS = $(POBJDIR)/dvxPlatformDos.o $(POBJDIR)/dvxPlatformUtil.o $(POBJDIR)/dvxMemTrack.o $(POBJDIR)/dvxPrefs.o $(OBJDIR)/dvxhlpc.o
TARGET = $(BINDIR)/dvx.exe TARGET = $(BINDIR)/dvx.exe
.PHONY: all clean .PHONY: all clean
@ -62,6 +64,9 @@ $(POBJDIR)/dvxPlatformDos.o: ../libs/kpunch/libdvx/platform/dvxPlatformDos.c | $
$(POBJDIR)/dvxPlatformUtil.o: ../libs/kpunch/libdvx/platform/dvxPlatformUtil.c | $(POBJDIR) $(POBJDIR)/dvxPlatformUtil.o: ../libs/kpunch/libdvx/platform/dvxPlatformUtil.c | $(POBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<
$(POBJDIR)/dvxMemTrack.o: ../libs/kpunch/libdvx/platform/dvxMemTrack.c | $(POBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(POBJDIR)/dvxPrefs.o: ../libs/kpunch/libdvx/dvxPrefs.c | $(POBJDIR) $(POBJDIR)/dvxPrefs.o: ../libs/kpunch/libdvx/dvxPrefs.c | $(POBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<
@ -81,6 +86,7 @@ $(BINDIR):
$(OBJDIR)/loaderMain.o: loaderMain.c ../libs/kpunch/libdvx/platform/dvxPlat.h ../libs/kpunch/libdvx/dvxTypes.h $(OBJDIR)/loaderMain.o: loaderMain.c ../libs/kpunch/libdvx/platform/dvxPlat.h ../libs/kpunch/libdvx/dvxTypes.h
$(POBJDIR)/dvxPlatformDos.o: ../libs/kpunch/libdvx/platform/dvxPlatformDos.c ../libs/kpunch/libdvx/platform/dvxPlat.h ../libs/kpunch/libdvx/dvxTypes.h ../libs/kpunch/libdvx/dvxPal.h $(POBJDIR)/dvxPlatformDos.o: ../libs/kpunch/libdvx/platform/dvxPlatformDos.c ../libs/kpunch/libdvx/platform/dvxPlat.h ../libs/kpunch/libdvx/dvxTypes.h ../libs/kpunch/libdvx/dvxPal.h
$(POBJDIR)/dvxPlatformUtil.o: ../libs/kpunch/libdvx/platform/dvxPlatformUtil.c ../libs/kpunch/libdvx/platform/dvxPlat.h ../libs/kpunch/libdvx/dvxTypes.h $(POBJDIR)/dvxPlatformUtil.o: ../libs/kpunch/libdvx/platform/dvxPlatformUtil.c ../libs/kpunch/libdvx/platform/dvxPlat.h ../libs/kpunch/libdvx/dvxTypes.h
$(POBJDIR)/dvxMemTrack.o: ../libs/kpunch/libdvx/platform/dvxMemTrack.c ../libs/kpunch/libdvx/platform/dvxPlat.h ../libs/kpunch/libdvx/dvxMem.h
$(POBJDIR)/dvxPrefs.o: ../libs/kpunch/libdvx/dvxPrefs.c ../libs/kpunch/libdvx/dvxPrefs.h $(POBJDIR)/dvxPrefs.o: ../libs/kpunch/libdvx/dvxPrefs.c ../libs/kpunch/libdvx/dvxPrefs.h
clean: clean:

View file

@ -46,7 +46,7 @@
#include <strings.h> #include <strings.h>
#include <sys/stat.h> #include <sys/stat.h>
// The loader is not a DXE use plain realloc/free for stb_ds so that // The loader is not a DXE -- use plain realloc/free for stb_ds so that
// all translation units (loaderMain.o, dvxPrefs.o) share the same heap. // all translation units (loaderMain.o, dvxPrefs.o) share the same heap.
#define STB_DS_IMPLEMENTATION #define STB_DS_IMPLEMENTATION
#include "stb_ds_wrap.h" #include "stb_ds_wrap.h"
@ -86,7 +86,7 @@ typedef struct {
void *handle; void *handle;
} ModuleT; } ModuleT;
// Scratch state for processHcf callback collects output=, imagedir=, and // Scratch state for processHcf callback -- collects output=, imagedir=, and
// the accumulated source= glob expansions in one pass. // the accumulated source= glob expansions in one pass.
typedef struct { typedef struct {
const char *hcfDir; const char *hcfDir;
@ -275,24 +275,6 @@ static int32_t countTotalHelpSteps(const char *dirPath) {
} }
// Global logging function exported to all DXE modules.
// Opens/closes the file per-write so it's never held open.
void dvxLog(const char *fmt, ...) {
FILE *f = fopen(LOG_PATH, "a");
if (!f) {
return;
}
va_list ap;
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
fprintf(f, "\n");
fclose(f);
}
static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize) { static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize) {
// Find last directory separator // Find last directory separator
const char *start = path; const char *start = path;
@ -486,7 +468,7 @@ static void helpRecompileIfNeeded(void) {
} }
// Progress callback for hlpcCompile updates the splash progress bar. // Progress callback for hlpcCompile -- updates the splash progress bar.
static void hlpcProgressCallback(void *ctx, int32_t current, int32_t total) { static void hlpcProgressCallback(void *ctx, int32_t current, int32_t total) {
(void)ctx; (void)ctx;
(void)total; (void)total;
@ -563,7 +545,7 @@ static void loadInOrder(ModuleT *mods) {
} }
dvxLog("Loading: %s", mods[i].path); dvxLog("Loading: %s", mods[i].path);
mods[i].handle = dlopen(mods[i].path, RTLD_GLOBAL); mods[i].handle = dlopen(mods[i].path, RTLD_NOW | RTLD_GLOBAL);
if (!mods[i].handle) { if (!mods[i].handle) {
const char *err = dlerror(); const char *err = dlerror();
@ -573,7 +555,7 @@ static void loadInOrder(ModuleT *mods) {
exit(1); exit(1);
} }
RegFnT regFn = (RegFnT)dlsym(mods[i].handle, "_wgtRegister"); RegFnT regFn = (RegFnT)dlsym(mods[i].handle, DVX_SYM("wgtRegister"));
if (regFn) { if (regFn) {
regFn(); regFn();
@ -584,10 +566,10 @@ static void loadInOrder(ModuleT *mods) {
typedef const char *(*IfaceGetPathFnT)(const char *); typedef const char *(*IfaceGetPathFnT)(const char *);
typedef void (*IfaceSetPathFnT)(const char *, const char *); typedef void (*IfaceSetPathFnT)(const char *, const char *);
IfaceCountFnT countFn = (IfaceCountFnT)dlsym(NULL, "_wgtIfaceCount"); IfaceCountFnT countFn = (IfaceCountFnT)dlsym(NULL, DVX_SYM("wgtIfaceCount"));
IfaceAtFnT atFn = (IfaceAtFnT)dlsym(NULL, "_wgtIfaceAt"); IfaceAtFnT atFn = (IfaceAtFnT)dlsym(NULL, DVX_SYM("wgtIfaceAt"));
IfaceGetPathFnT getPathFn = (IfaceGetPathFnT)dlsym(NULL, "_wgtIfaceGetPath"); IfaceGetPathFnT getPathFn = (IfaceGetPathFnT)dlsym(NULL, DVX_SYM("wgtIfaceGetPath"));
IfaceSetPathFnT setPathFn = (IfaceSetPathFnT)dlsym(NULL, "_wgtIfaceSetPath"); IfaceSetPathFnT setPathFn = (IfaceSetPathFnT)dlsym(NULL, DVX_SYM("wgtIfaceSetPath"));
if (countFn && atFn && getPathFn && setPathFn) { if (countFn && atFn && getPathFn && setPathFn) {
int32_t ic = countFn(); int32_t ic = countFn();
@ -656,60 +638,6 @@ static void logAndReadDeps(ModuleT *mods) {
} }
// Simple glob pattern matching for filenames. Supports:
// * matches zero or more characters
// ? matches exactly one character
// !pat at the start means "exclude files matching pat"
// Case-insensitive. Exported for use by DXE modules.
bool platformGlobMatch(const char *pattern, const char *name) {
while (*pattern && *name) {
if (*pattern == '*') {
pattern++;
if (!*pattern) {
return true;
}
while (*name) {
if (platformGlobMatch(pattern, name)) {
return true;
}
name++;
}
return false;
} else if (*pattern == '?' || tolower((unsigned char)*pattern) == tolower((unsigned char)*name)) {
pattern++;
name++;
} else {
return false;
}
}
// Skip trailing *
while (*pattern == '*') {
pattern++;
}
return *pattern == '\0' && *name == '\0';
}
// Callback for processHcf: accumulate output=, imagedir=, and expanded source= entries.
static void processHcfCallback(const char *key, const char *val, const char *exclude, void *user) {
ProcessHcfCtxT *ctx = (ProcessHcfCtxT *)user;
if (strcasecmp(key, "output") == 0) {
snprintf(ctx->outputFile, sizeof(ctx->outputFile), "%s" DVX_PATH_SEP "%s", ctx->hcfDir, val);
} else if (strcasecmp(key, "imagedir") == 0) {
snprintf(ctx->imgDir, sizeof(ctx->imgDir), "%s", val);
} else if (strcasecmp(key, "source") == 0) {
collectGlobFiles(&ctx->inputFiles, val, exclude);
}
}
static void processHcf(const char *hcfPath, const char *hcfDir) { static void processHcf(const char *hcfPath, const char *hcfDir) {
ProcessHcfCtxT ctx; ProcessHcfCtxT ctx;
memset(&ctx, 0, sizeof(ctx)); memset(&ctx, 0, sizeof(ctx));
@ -743,6 +671,20 @@ static void processHcf(const char *hcfPath, const char *hcfDir) {
} }
// Callback for processHcf: accumulate output=, imagedir=, and expanded source= entries.
static void processHcfCallback(const char *key, const char *val, const char *exclude, void *user) {
ProcessHcfCtxT *ctx = (ProcessHcfCtxT *)user;
if (strcasecmp(key, "output") == 0) {
snprintf(ctx->outputFile, sizeof(ctx->outputFile), "%s" DVX_PATH_SEP "%s", ctx->hcfDir, val);
} else if (strcasecmp(key, "imagedir") == 0) {
snprintf(ctx->imgDir, sizeof(ctx->imgDir), "%s", val);
} else if (strcasecmp(key, "source") == 0) {
collectGlobFiles(&ctx->inputFiles, val, exclude);
}
}
// Recursively scan a directory for .hcf files and process each one. // Recursively scan a directory for .hcf files and process each one.
static void processHcfDir(const char *dirPath) { static void processHcfDir(const char *dirPath) {
char **names = dvxReadDir(dirPath); char **names = dvxReadDir(dirPath);
@ -977,6 +919,7 @@ int main(int argc, char *argv[]) {
} }
// Suppress Ctrl+C before anything else // Suppress Ctrl+C before anything else
platformSetLogPath(LOG_PATH);
platformInit(); platformInit();
// Switch to VGA mode 13h and show graphical splash // Switch to VGA mode 13h and show graphical splash
@ -1007,7 +950,7 @@ int main(int argc, char *argv[]) {
// Find and call shellMain from whichever module exports it // Find and call shellMain from whichever module exports it
typedef int (*ShellMainFnT)(int, char **); typedef int (*ShellMainFnT)(int, char **);
ShellMainFnT shellMain = (ShellMainFnT)findSymbol(handles, "_shellMain"); ShellMainFnT shellMain = (ShellMainFnT)findSymbol(handles, DVX_SYM("shellMain"));
if (!shellMain) { if (!shellMain) {
dvxLog("ERROR: No module exports shellMain"); dvxLog("ERROR: No module exports shellMain");

124
src/test/basic/Makefile Normal file
View file

@ -0,0 +1,124 @@
# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# src/test/basic -- host-side BASIC end-to-end tests (suite T7)
#
# make test headless sample/fixture runs, compaction
# differential, compiler fuzz, bytecode fuzz
# make test TESTSAN=1 same with the fuzzers built under ASan/UBSan
# (bin/host/basrun and bascomp come from the
# dvxbasic Makefile, which honours TESTSAN too)
# make fuzz COV=1 fuzzers built with gcov instrumentation under
# obj/hostcov (same split as the dvxbasic Makefile)
#
# Tunables: FUZZ_N (mutations per seed), FUZZ_SEED, FUZZ_STEPS.
HOSTCC = gcc
WARNFLAGS = -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -Wno-stringop-truncation
DVXBASIC = ../../apps/kpunch/dvxbasic
LIBDVX = ../../libs/kpunch/libdvx
INCLUDES = -I$(DVXBASIC) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty
HOSTCFLAGS = -O2 -g $(WARNFLAGS) -D_GNU_SOURCE $(INCLUDES)
ifeq ($(TESTSAN),1)
HOSTCFLAGS += -fno-omit-frame-pointer -fsanitize=address,undefined
endif
REPO = ../../..
# COV=1: instrumented binaries and their scratch tree live under
# obj/hostcov so they never mix with the sanitized or release builds.
ifeq ($(COV),1)
HOSTCFLAGS += --coverage
HOSTDIR = $(REPO)/obj/hostcov/basic
OUTDIR = $(REPO)/obj/hostcov/test/basic
else
HOSTDIR = $(REPO)/bin/host
OUTDIR = $(REPO)/obj/test/basic
endif
CORPUS = $(abspath $(OUTDIR)/corpus)
FUZZWORK = $(OUTDIR)/fuzzwork
GOLDEN = $(abspath $(REPO)/src/test/samples/golden)
FUZZ_N = 10
FUZZ_SEED = 12345
FUZZ_STEPS = 200000
# Corrupt length fields legitimately ask for huge allocations; the
# deserializer must handle a NULL from malloc, so let ASan return one
# instead of aborting the run.
ASAN_ENV = ASAN_OPTIONS=allocator_may_return_null=1:detect_leaks=1
COMPILER_SRCS = $(DVXBASIC)/compiler/lexer.c $(DVXBASIC)/compiler/parser.c $(DVXBASIC)/compiler/codegen.c $(DVXBASIC)/compiler/symtab.c
RUNTIME_SRCS = $(DVXBASIC)/runtime/vm.c $(DVXBASIC)/runtime/values.c $(DVXBASIC)/runtime/serialize.c
SUPPORT_SRCS = $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c
HARNESS_HDRS = $(wildcard $(DVXBASIC)/*.h $(DVXBASIC)/compiler/*.h $(DVXBASIC)/runtime/*.h) fuzzCommon.h
FUZZ_COMPILE_SRCS = fuzzCompile.c fuzzCommon.c $(COMPILER_SRCS) $(RUNTIME_SRCS) $(SUPPORT_SRCS)
FUZZ_MODULE_SRCS = fuzzModule.c fuzzCommon.c $(COMPILER_SRCS) $(RUNTIME_SRCS) $(LIBDVX)/dvxResource.c $(SUPPORT_SRCS)
FUZZ_COMPILE = $(abspath $(HOSTDIR)/fuzzCompile)
FUZZ_MODULE = $(abspath $(HOSTDIR)/fuzzModule)
HARNESS_SRCS = $(DVXBASIC)/test_suite.c $(DVXBASIC)/test_compiler.c $(DVXBASIC)/test_compact.c
TESTSAN_STAMP = $(OUTDIR)/.testsan-$(if $(filter 1,$(TESTSAN)),on,off)
.PHONY: all build test corpus runs fuzz clean
all: build
build: $(FUZZ_COMPILE) $(FUZZ_MODULE)
test: runs fuzz
# basrun-driven sample / fixture / compaction runs
runs:
./runBasic.sh
# Regenerate the seed corpus from the C harnesses every time: cheap,
# and it can never go stale.
corpus: | $(OUTDIR)
rm -rf $(CORPUS)
./extractCorpus.py $(CORPUS) $(HARNESS_SRCS)
# Fuzzers run inside a scratch directory so file-system statements in
# mutated programs only ever touch files there.
fuzz: build corpus
rm -rf $(FUZZWORK)
mkdir -p $(FUZZWORK)
cd $(FUZZWORK) && $(ASAN_ENV) $(FUZZ_COMPILE) -n $(FUZZ_N) -seed $(FUZZ_SEED) -steps $(FUZZ_STEPS) $(CORPUS)
cd $(FUZZWORK) && $(ASAN_ENV) $(FUZZ_MODULE) -n $(FUZZ_N) -seed $(FUZZ_SEED) -steps $(FUZZ_STEPS) -corpus $(CORPUS) $(wildcard $(GOLDEN)/*.app)
$(FUZZ_COMPILE): $(FUZZ_COMPILE_SRCS) $(HARNESS_HDRS) $(TESTSAN_STAMP) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(FUZZ_COMPILE_SRCS) -lm
$(FUZZ_MODULE): $(FUZZ_MODULE_SRCS) $(HARNESS_HDRS) $(TESTSAN_STAMP) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(FUZZ_MODULE_SRCS) -lm
$(TESTSAN_STAMP): | $(OUTDIR)
rm -f $(OUTDIR)/.testsan-on $(OUTDIR)/.testsan-off
touch $@
$(HOSTDIR) $(OUTDIR):
mkdir -p $@
clean:
rm -rf $(OUTDIR) $(FUZZ_COMPILE) $(FUZZ_MODULE)

226
src/test/basic/extractCorpus.py Executable file
View file

@ -0,0 +1,226 @@
#!/usr/bin/env python3
# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
"""extractCorpus.py -- pull BASIC test programs out of the C harnesses.
Every TEST_EQ / TEST_COMPILE_ERROR / TEST_RUNTIME_ERROR / TEST_SUB_EQ /
runProgram / expectCompileError / testCompact call whose source argument
is a plain (possibly concatenated) C string literal becomes one .bas
file in the output directory. The corpus is the seed set for
fuzzCompile and fuzzModule; it is generated at test time so the C
harnesses stay the single source of truth.
Usage: extractCorpus.py OUTDIR HARNESS.c [HARNESS.c ...]
"""
import os
import re
import sys
CALL_NAMES = {
"TEST_EQ", "TEST_COMPILE_ERROR", "TEST_RUNTIME_ERROR", "TEST_SUB_EQ",
"runProgram", "expectCompileError", "testCompact",
}
CALL_RE = re.compile(r"\b(" + "|".join(sorted(CALL_NAMES)) + r")\s*\(")
ESCAPES = {
"n": "\n", "t": "\t", "r": "\r", "\\": "\\", '"': '"', "'": "'",
"0": "\0", "a": "\a", "b": "\b", "f": "\f", "v": "\v", "?": "?",
}
def unescape(body):
out = []
i = 0
while i < len(body):
c = body[i]
if c != "\\":
out.append(c)
i += 1
continue
i += 1
e = body[i]
if e == "x":
j = i + 1
while j < len(body) and body[j] in "0123456789abcdefABCDEF":
j += 1
out.append(chr(int(body[i + 1:j], 16)))
i = j
elif e in "01234567":
j = i
while j < len(body) and j < i + 3 and body[j] in "01234567":
j += 1
out.append(chr(int(body[i:j], 8)))
i = j
else:
out.append(ESCAPES.get(e, e))
i += 1
return "".join(out)
def splitArgs(text, start):
"""Split the argument list starting after '(' at text[start].
Returns (list of raw argument strings, index after ')')."""
depth = 0
args = []
cur = []
i = start
inStr = False
while i < len(text):
c = text[i]
if inStr:
cur.append(c)
if c == "\\":
cur.append(text[i + 1])
i += 2
continue
if c == '"':
inStr = False
i += 1
continue
if c == '"':
inStr = True
cur.append(c)
elif c == "(":
depth += 1
cur.append(c)
elif c == ")":
if depth == 0:
args.append("".join(cur))
return args, i + 1
depth -= 1
cur.append(c)
elif c == "," and depth == 0:
args.append("".join(cur))
cur = []
else:
cur.append(c)
i += 1
return None, i
LITERAL_RE = re.compile(r'"((?:[^"\\]|\\.)*)"')
def literalValue(arg):
"""Concatenate adjacent string literals; None if arg is not purely literals."""
rest = arg.strip()
parts = []
while rest:
m = LITERAL_RE.match(rest)
if not m:
return None
parts.append(unescape(m.group(1)))
rest = rest[m.end():].lstrip()
if not parts:
return None
return "".join(parts)
def stripComments(text):
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
return re.sub(r"//[^\n]*", "", text)
def safeName(name):
return re.sub(r"[^A-Za-z0-9]+", "_", name).strip("_")[:48] or "case"
def main():
if len(sys.argv) < 3:
print(__doc__)
return 1
outDir = sys.argv[1]
os.makedirs(outDir, exist_ok=True)
count = 0
seen = set()
for path in sys.argv[2:]:
text = stripComments(open(path, encoding="ascii", errors="replace").read())
prefix = os.path.splitext(os.path.basename(path))[0]
pos = 0
while True:
m = CALL_RE.search(text, pos)
if not m:
break
args, pos = splitArgs(text, m.end())
if args is None:
break
# Skip the #define lines: their "arguments" are macro params.
if len(args) < 2:
continue
name = literalValue(args[0])
source = literalValue(args[1])
if name is None or source is None or not source.strip():
continue
key = source
if key in seen:
continue
seen.add(key)
count += 1
fileName = "%s-%04d-%s.bas" % (prefix, count, safeName(name))
with open(os.path.join(outDir, fileName), "w", encoding="latin-1") as f:
f.write(source)
print("extractCorpus: %d programs written to %s" % (count, outDir))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,35 @@
Dim m(2, 3) As Integer
Dim i As Integer
Dim j As Integer
For i = 0 To 2
For j = 0 To 3
m(i, j) = i * 10 + j
Next j
Next i
Print m(2, 3); m(0, 1); m(1, 2)
Print LBound(m, 1); UBound(m, 1); LBound(m, 2); UBound(m, 2)
Dim c(1 To 2, 1 To 2, 1 To 2) As Long
c(2, 2, 2) = 888
c(1, 2, 1) = 121
Print c(2, 2, 2); c(1, 2, 1); c(1, 1, 1)
ReDim d(3) As String
d(3) = "three"
ReDim Preserve d(6)
d(6) = "six"
Print "[" & d(3) & "][" & d(6) & "]"; UBound(d)
ReDim Preserve d(1)
Print UBound(d); "[" & d(1) & "]"
ReDim d(2)
Print "[" & d(2) & "]"
Dim n(5) As Integer
n(5) = 5
On Error GoTo Oops
Erase n
Print n(5)
Print "after erase"
Print m(3, 0)
Print "after bounds"
End
Oops:
Print "err"; Err
Resume Next

View file

@ -0,0 +1,10 @@
23 1 12
0 2 0 3
888 121 0
[three][six]6
1 []
[]
0
after erase
err9
after bounds

View file

@ -0,0 +1,18 @@
Print CInt(2.5); CInt(3.5); CInt(-2.5); CInt(-3.5); CInt(0.5); CInt(1.5)
Print CInt(2.4999); CInt(2.5001); CInt(-0.5)
Print CLng(2.5); CLng(3.5); CLng(2147483646.5)
Print CInt(32767.4); CInt(-32768.4)
Print Int(-2.5); Fix(-2.5); Int(2.5); Fix(2.5)
Print CDbl(1) / 3
Print CSng(1) / 3
Print CBool(0); CBool(5); CBool(""); CBool("x")
Print CStr(12) & CStr(-3.5)
On Error GoTo Bad
Print CInt(32768)
Print CInt(-32769)
Print CLng(3000000000#)
Print "done"
End
Bad:
Print "err"; Err
Resume Next

View file

@ -0,0 +1,13 @@
2 4 -2 -4 0 2
2 3 0
2 4 2147483646
32767 -32768
-3 -2 2 2
0.333333333333333
0.333333333333333
False True False True
12-3.5
err6
err6
err6
done

View file

@ -0,0 +1,8 @@
Option Compare Text
Print "abc" = "ABC"; "a" < "B"; Instr("Hello", "L")
Select Case "HELLO"
Case "hello"
Print "matched"
Case Else
Print "missed"
End Select

View file

@ -0,0 +1,2 @@
True True 0
matched

View file

@ -0,0 +1,13 @@
Const MAXV = 100
Const PI = 3.14159
Const APPNAME = "DVX"
Const FLAG = True
Const HDR As String = "== Report =="
Dim x As Integer
Let x = MAXV \ 3
Print x; MAXV; APPNAME; FLAG; HDR
Print Int(PI * 100)
Sub ShowIt()
Print APPNAME & " in sub"; MAXV
End Sub
ShowIt

View file

@ -0,0 +1,3 @@
33 100 DVXTrue == Report ==
314
DVX in sub100

View file

@ -0,0 +1,25 @@
Dim a As Integer
Dim b As Double
Dim s As String
Dim i As Integer
Read a, b, s
Print a; b; s
For i = 1 To 3
Read a
Print a;
Next i
Print
Restore
Read a
Print a
Read b, s, a, a, a
Print a
On Error GoTo Done
Read a
Print "not here"
End
Done:
Print "err"; Err
End
Data 1, 2.5, "text", 10, 20
Data 30

View file

@ -0,0 +1,5 @@
1 2.5 text
10 20 30
1
30
err4

View file

@ -0,0 +1,15 @@
DefInt I-K
DefLng L
DefSng S
DefDbl D
DefStr T
i = 7 / 2
Print i
l = 2147483647
Print l
s = 1 / 3
Print s
d = 1 / 3
Print d
t = "text"
Print t

View file

@ -0,0 +1,5 @@
4
2147483647
0.3333333
0.333333333333333
text

View file

@ -0,0 +1,3 @@
Print "before"
Print 1 \ 0
Print "after"

View file

@ -0,0 +1 @@
11

View file

@ -0,0 +1 @@
before

View file

@ -0,0 +1,11 @@
' ERL reports the line of the current error and resets after RESUME.
On Error GoTo Handler
Dim v As Integer
v = 1 / 0
Print "after"; ERL
Error 7
Print "end"; ERL
End
Handler:
Print Err; ERL
Resume Next

View file

@ -0,0 +1,4 @@
11 4
after0
7 6
end0

View file

@ -0,0 +1,38 @@
Dim f As String
Dim buf As String
Dim i As Integer
Dim l As Long
Dim d As Double
f = App.Data & "/raw.bin"
Open f For Binary As #1
Put #1, 1, "ABCDEFGH"
i = 4660
Put #1, 9, i
l = 305419896
Put #1, , l
d = 2.5
Put #1, , d
Close #1
Print "size:"; FileLen(f)
Open f For Binary As #2
buf = Space$(4)
Get #2, 1, buf
Print buf
Get #2, 5, buf
Print buf
i = 0
Get #2, 9, i
Print i
l = 0
Get #2, , l
Print l
d = 0
Get #2, , d
Print d
Print "loc:"; Loc(2); "lof:"; Lof(2); "eof:"; Eof(2)
Seek #2, 3
buf = Space$(2)
Get #2, , buf
Print buf; Seek(2)
Close #2
Kill f

View file

@ -0,0 +1,8 @@
size:22
ABCD
EFGH
4660
305419896
2.5
loc:22 lof:22 eof:True
CD5

View file

@ -0,0 +1,34 @@
Dim f As String
Dim a As String
Dim b As Integer
Dim c As Double
Dim d As String
f = App.Data & "/data.txt"
Open f For Output As #1
Write #1, "Scott", 42, 3.25
Write #1, "Jane, Doe", -7, 0.5
Print #1, "plain,12,1.75"
Close #1
Open f For Input As #1
Input #1, a, b, c
Print a; b; c
Input #1, a, b, c
Print a; b; c
Input #1, a, b, c
Print a; b; c
Print "eof:"; Eof(1)
Close #1
Open f For Input As #1
Line Input #1, d
Print d
Line Input #1, d
Print d
Close #1
Open f For Append As #2
Print #2, "tail"
Close #2
Open f For Input As #3
Print Input$(5, #3)
Close #3
Print "free:"; FreeFile
Kill f

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