diff --git a/.gitattributes b/.gitattributes index 458c6be88..4b96a41bd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -26,6 +26,10 @@ add-in/Resources/** -text bugs/** -text *.twinproj binary +# The twinBASIC program in scripts/imagestrip/ is laid out as a reproducer is, and is +# checked out byte for byte for the same reason: its .twinproj is packed from its src/. +scripts/imagestrip/** -text + # The VB6 template `bug_repro.mjs new --with-vb6` copies into a reproducer's vb6/ # is CRLF in every checkout, as the bugs/** files it becomes are, and VB6 needs it. test/repro-templates/vb6/** -text diff --git a/.github/actions/run-gates/action.yml b/.github/actions/run-gates/action.yml index 28ecb0aec..209791d56 100644 --- a/.github/actions/run-gates/action.yml +++ b/.github/actions/run-gates/action.yml @@ -16,6 +16,28 @@ description: The gate steps both workflows share, checked against test.bat and c runs: using: composite steps: + # check_a11y runs FIRST. The deploy workflow backgrounds the book PDF + # render just before this composite action and joins it after, so the + # 24 non-a11y gates below mostly run in parallel with the book: they are + # Node scripts, CPU-bound, and the only two Chromium workloads (axe here + # and paged.js in the book render) overlap for just the ~40 s a11y + # takes, at the START of the run. a11y last would mean the two Chromiums + # fought over the runner's 2 vCPUs at the END, when the book render + # cannot amortise the slowdown. + # + # In check.bat, check_a11y now sits right after check_tree_fresh and + # before check_dot_fit / pick_a11y_sample --- the gate roster check + # requires check.bat's order to match the composite action's order after + # filtering to the gates both have. + # + # Matches check.bat: the build's own link check gates this, and a link + # failure short-circuits before the (slower) browser scan runs. Scans + # _site-offline/, whose relative asset paths actually resolve -- _site/ + # uses root-absolute URLs that render unstyled here, making every + # colour-contrast result meaningless. + - name: Accessibility check (check_a11y.mjs) + shell: bash + run: node scripts/check_a11y.mjs # The build is the only pass over the site's HTML. This step is NOT a # second one: it runs the differential harness against a nine-file # synthetic tree that carries one fault of every kind, so @@ -90,6 +112,21 @@ runs: - name: Unit-test the bug reproducers' pictures (test/png.test.mjs) shell: bash run: node --test test/png.test.mjs + # What an embedded image keeps (scripts/lib/compact-image.mjs's + # compactImage): an editor's metadata goes, and a notice of whose the + # picture is and on what terms stays, over images built in the test. The + # half that runs scripts/imagestrip/ needs its exe and Windows, which CI + # has not, and is skipped. No browser, no built tree, well under a second. + - name: Unit-test what an embedded image keeps (test/compact-image.test.mjs) + shell: bash + run: node --test test/compact-image.test.mjs + # An SVG screenshot is written on one line (scripts/svgshot/snapshot-svg.mjs's + # oneLine), so that a CRLF checkout changes only its last line ending. What + # it keeps of an image it embeds is test/compact-image.test.mjs's, above. + # No browser, no built tree, well under a second. + - name: Unit-test that SVG screenshots are one line (test/svgshot.test.mjs) + shell: bash + run: node --test test/svgshot.test.mjs # check_examples.mjs's probes: batching, crash isolation through a fake # lane, the canaries and the fence classifier. check_examples.mjs needs a # twinBASIC install, which CI has not; its probes need none. @@ -258,11 +295,3 @@ runs: - name: Verify a11y sample coverage (pick_a11y_sample.mjs) shell: bash run: node scripts/pick_a11y_sample.mjs --check - # Matches check.bat: the build's own link check gates this, and a link - # failure short-circuits before the (slower) browser scan runs. Scans - # _site-offline/, whose relative asset paths actually resolve -- _site/ - # uses root-absolute URLs that render unstyled here, making every - # colour-contrast result meaningless. - - name: Accessibility check (check_a11y.mjs) - shell: bash - run: node scripts/check_a11y.mjs diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 121ade6dd..3455a7870 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -106,7 +106,11 @@ jobs: # The gates both workflows run, in one list: see # .github/actions/run-gates/action.yml, which check_ci_workflows.mjs # checks against test.bat and check.bat. Each gate is its own group in - # this step's log. + # this step's log. check_a11y.mjs is the FIRST step inside the + # composite action -- the deploy workflow backgrounds the book PDF + # render just before it, and a11y running first means the two Chromium + # workloads overlap for ~40 s at the start instead of fighting at the + # end over the book's PDF-generation phase. - name: Run the gates uses: ./.github/actions/run-gates # The action's first step compares the standalone link checker's two diff --git a/.github/workflows/tbdocs-gh-pages.yml b/.github/workflows/tbdocs-gh-pages.yml index 3135a2d09..b0a951554 100644 --- a/.github/workflows/tbdocs-gh-pages.yml +++ b/.github/workflows/tbdocs-gh-pages.yml @@ -98,19 +98,78 @@ jobs: # offers. Never pass --no-help-archive here: the download would lose # the archive (check_ci_workflows.mjs refuses the flag). run: node builder/tbdocs.mjs --src docs --url '${{ steps.pages.outputs.origin }}' --baseurl '${{ steps.pages.outputs.base_path }}' --no-fetch-assets --check-audit-index + # The book PDF render runs in parallel with the gates. This step starts + # it in the background and returns as soon as the process is up, so the + # gates begin immediately; the "Wait for the book PDF render" step + # below joins the backgrounded process, prints its captured output, + # and exits with its code. On the deploy workflow this saves ~90 s + # against the previous sequential layout (the book render takes about + # as long as the gates). checks.yml is unaffected -- it has no PDF to + # render -- and the gates' composite-action rendering stays as it was. + # + # nohup makes the child ignore SIGHUP and redirected stdio decouples + # it from this step's shell; together they keep the child alive after + # the shell exits -- the runner reparents it and leaves it running + # until the join step reads /tmp/book-render.log and + # /tmp/book-render.exit. + # + # No `setsid`: when bash backgrounds `setsid cmd`, the backgrounded + # setsid is a process-group leader and forks, the parent exits, and + # the real command runs under the child's PID -- so `$!` captures a + # dead PID and the join step's `kill -0` returns false in ~0 s, + # usually before the render has finished. nohup does not fork; `$!` + # captures the real PID, which stays live until the render completes. + - name: Start the book PDF render in the background + working-directory: ./docs + run: | + mkdir -p _pdf + nohup bash -c ' + node ../book/render-book.mjs _site-pdf/book.html -o "_pdf/twinBASIC Book.pdf" --outline-tags h1,h2,h3,h4 --additional-script ../perf/detach-pages.js + echo $? > /tmp/book-render.exit + ' >/tmp/book-render.log 2>&1 /tmp/book-render.pid + echo "started book render as PID $BOOK_PID" # The gates both workflows run, in one list: see # .github/actions/run-gates/action.yml, which check_ci_workflows.mjs # checks against test.bat and check.bat. checks.yml runs only on pull # requests, so this is the one place a change pushed straight to # `staging`, or a manual dispatch, meets them. Each gate is its own - # group in this step's log. + # step in the composite action's rendering, with its own duration. + # + # check_a11y.mjs is the FIRST step of the composite action, so its + # Chromium work overlaps with the backgrounded book render's early + # Chromium-heavy phase for ~40 s. The 24 Node gates below it are + # CPU-bound, so they run through the rest of the book render without + # Chromium contention. - name: Run the gates uses: ./.github/actions/run-gates - - name: Render book PDF + # Join the backgrounded book render, print its output, and exit with + # its code. This step's own duration shows how much the book overlapped + # with the gates: 0 s if the book finished during them, or + # book_time - gates_time if the gates finished first. The 15-minute + # step timeout is a backstop against a wedged render; a missing PID + # or exit file aborts rather than hanging. + - name: Wait for the book PDF render + timeout-minutes: 15 run: | - mkdir -p _pdf - node ../book/render-book.mjs _site-pdf/book.html -o "_pdf/twinBASIC Book.pdf" --outline-tags h1,h2,h3,h4 --additional-script ../perf/detach-pages.js - working-directory: ./docs + if [ ! -f /tmp/book-render.pid ]; then + echo "::error::no PID file; the start step did not run" + exit 2 + fi + BOOK_PID=$(cat /tmp/book-render.pid) + echo "waiting for book render PID $BOOK_PID" + while kill -0 "$BOOK_PID" 2>/dev/null; do + sleep 2 + done + cat /tmp/book-render.log + if [ ! -f /tmp/book-render.exit ]; then + echo "::error::the book render process ended without writing its exit status" + exit 1 + fi + BOOK_EXIT=$(cat /tmp/book-render.exit) + echo "book render exit code: $BOOK_EXIT" + exit "$BOOK_EXIT" - name: Upload Pages artifact uses: actions/upload-pages-artifact@v5 with: diff --git a/.gitignore b/.gitignore index d38d8daea..ea9960691 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,10 @@ indexer/.packages/ # A bug reproducer's zip is only for attaching to its GitHub issue (see # BUGS-TO-REPORT.md); the .twinproj beside it is committed. /bugs/**/*.zip + +# The IDE builds scripts/imagestrip/imagestrip.twinproj into Build\ beside it. +/scripts/imagestrip/Build/ + +# shoot_docs.mjs --svg: each picture's bundle (scripts/svgshot/bundle.mjs), what its SVG +# is drawn from, for scripts/svgshot/replay.mjs. Bundles hold the Windows user name. +/.svgshot-bundles/ diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 3c931e13c..ca6a3bf79 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -70,6 +70,15 @@ How the four duties above fit it: - **Additional context** gives what did not reproduce it, the severity, and the builds it was checked on besides the one above. +**A follow-up to a closed issue** --- one whose fix left part of the defect, or a case its +report named only in passing --- opens its **Describe the bug** with "Follow-up to #NNNN, +which is fixed in BETA for ; this is ." Its **To Reproduce** +ends with regression tests the twinBASIC developers can adapt (owner, 2026-10-10): a +`[TestFixture]` module, introduced by one sentence, with one `[TestCase]` per behaviour that +asserts the correct result, covering what the old issue described (these pass) and what is +left (these fail), or compile cases for a diagnostic, or a scripted check where no unit test +reaches. Its reproducer is a `test` reproducer that holds that module (see below). + **An entry's paragraphs and list items are not wrapped**, one line each, unlike the rest of this file. A GitHub issue renders a single newline as a line break, so a wrapped paragraph pastes with a break in mid-sentence. Entries are separated by a `---` line. @@ -102,7 +111,7 @@ what VB6 drew, the pixels that differ in red. `test/png.test.mjs` fails when an `REPORT.md` does not embed one. `expect.imagesDiffer` lets `verify` judge by them. `scripts/bug_repro.mjs` makes and checks them (the tool's page is -[Tools and Scripts](docs/Documentation/Tools.md#bug-repro)): +[Tools and Scripts](docs/Documentation/Tools-Defects.md#bug-repro)): ```sh node scripts/bug_repro.mjs new "" # bugs//src/ and repro.json @@ -126,7 +135,8 @@ node scripts/impexp.mjs import bugs//.twinproj bugs//src --ove ``` `bugs//repro.json` says how to ask the compiler about the entry, and is committed with -the reproducer: a `mode` of `compile`, `build`, `run`, `cli` (the compiler executable's own +the reproducer: a `mode` of `compile`, `build`, `run`, `test` (its `[TestCase]` Subs and +compile cases, run by `tbrun --tests`), `cli` (the compiler executable's own command line), `lane` (a lane of `ide-test.bat` or `addin-test.bat` and the tests in it that assert the bug), `probe` (a script under `scripts/`) or `manual`, and what a reproduction looks like in `expect`, such as the exit code of `tbbuild`, the diagnostic codes, or a @@ -755,10 +765,10 @@ What was tried: --- -## Calling a method of `stdole.IDispatch` is a late-bound call by name and fails with &H80020006 +## Calling a method of `stdole.IDispatch` is a late-bound call by name and fails with error 438 **Describe the bug** -A variable declared `As stdole.IDispatch` does not call the four `IDispatch` methods through the interface. `d.GetTypeInfoCount count` is compiled as a late-bound call: the compiler accepts any arguments, whatever their number and type, and at run time the call looks `GetTypeInfoCount` up by name in the object, which does not have it, and raises `&H80020006` (*Unknown name.*). With no error handler the error ends the run (in the IDE's run, with no message). A call of `GetTypeInfo`, `GetIDsOfNames` or `Invoke` ends the run the same way when unhandled. Members of the object itself can be called through the variable, as through an `Object`. +A variable declared `As stdole.IDispatch` does not call the four `IDispatch` methods through the interface. `d.GetTypeInfoCount count` is compiled as a late-bound call: the compiler accepts any arguments, whatever their number and type, and at run time the call looks `GetTypeInfoCount` up by name in the object, which does not have it, and raises error 438 (*Object doesn't support this property or method*). With no error handler the error ends the run (in the IDE's run, with no message). A call of `GetTypeInfo`, `GetIDsOfNames` or `Invoke` ends the run the same way when unhandled. Members of the object itself can be called through the variable, as through an `Object`. **To Reproduce** Steps to reproduce the behavior: @@ -775,9 +785,9 @@ Steps to reproduce the behavior: 2. See no compile error, and this output (each line prints `Err.Number` after the statement): ``` d.Hello: error 0, Hello ran 1 time(s) - d.GetTypeInfoCount: error -2147352570 (80020006) Unknown name. - d.GetTypeInfoCount "a", "b", "c": error -2147352570 - d.NoSuchMethod: error -2147352570 + d.GetTypeInfoCount: error 438 (1B6) Object doesn't support this property or method + d.GetTypeInfoCount "a", "b", "c": error 438 + d.NoSuchMethod: error 438 ``` 3. Remove the `On Error Resume Next` line and run again: the output stops at the first `d.GetTypeInfoCount`, and the run ends with no message. @@ -788,18 +798,86 @@ VB6 refuses the declaration itself. The same program in VB6 (attached as `stdole ``` Compile Error in File '...\Module1.bas', Line 16 : Function or interface marked as restricted, or the function uses an Automation type not supported in Visual Basic ``` -`Dim d As IDispatch` is refused the same way. With `Dim d As Object` in its place, VB6 runs `d.Hello` and raises 438 for the other three statements, so twinBASIC's `stdole.IDispatch` behaves as VB6's `Object` does, apart from the error number (see the entry about `&H80020006` and 438). +`Dim d As IDispatch` is refused the same way. With `Dim d As Object` in its place, VB6 runs `d.Hello` and raises 438 for the other three statements, so twinBASIC's `stdole.IDispatch` behaves as VB6's `Object` does. **Desktop:** - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 995 + - twinBASIC compiler version: BETA 1005 **Additional context** Severity: low, since a project can declare its own copy of the interface, but the failure is silent without a handler and the compiler gives no sign that the call is not what it appears to be. +On BETA 995 and 997 the error was `&H80020006` (*Unknown name.*), the number every late-bound call to a member an object does not have raised then (#2490); BETA 1005 raises 438 for those calls, as VB6 does, and so for these. + What was tried: `GetIDsOfNames` and `Invoke` with zero arguments, and `GetTypeInfo` with a null pointer, each end the run the same way; calls with arguments of the wrong type compile (`d.GetTypeInfoCount "a"`, `d.GetTypeInfo "a", "b", "c"`, `d.Invoke "a", "b", "c", "d", "e", "f", "g", "h"` all compile). An unhandled `Err.Raise 5` in the same harness ends the run the same way, so the silent end is how an unhandled error shows there, not a separate fault. Running the built exe with `--exe` was not possible on this machine (the harness could not start it). - + + +--- + +## A module-level `Single` array `Const` whose element overflows holds infinity, and a later array `Const` that calls `Sqr`, `Timer` or `Rnd` is refused with TB5001 + +**Describe the bug** +Follow-up to #2475, which is fixed in BETA 1005 for the element types it reported: an element too large for a `Long`, `Integer`, `Byte` or `LongLong` array `Const` is now TB5001 on its own line, where BETA 997 crashed the compiler. This is the `Single` case, which #2475 gave as a second crash under Additional context: it no longer crashes, but it is still wrong. At module level, an array `Const` of `Single` whose element is too large for a `Single` compiles without a diagnostic, and the element reads as infinity. An array `Const` declared after it, whose element calls `Sqr`, `Timer` or `Rnd`, is then refused with TB5001, *Type mismatch*, although it is correct and compiles on its own. Observed in the IDE's diagnostics and in a run. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `single-array-const-overflow.twinproj` (attached as `single-array-const-overflow.zip`). Its module `Startup` begins: + ``` + Const Big() As Single = Array(1E+39) + Const Root() As Double = Array(Sqr(4)) + ``` +2. Compile it. See TB5001 *Type mismatch* on the declaration of `Root`, and nothing on the declaration of `Big`. +3. Delete the declaration of `Root` and print `Big(0)` in `Main`: the project compiles, and `Big(0)` prints `1.#INF`. + +The module below is the reproducer's, and is meant to be adapted as a regression test. Because what it tests is a diagnostic, each case is a line of source that ends in the comment `' CASE : `: the diagnostic code the line must get, `error` for any error, or `none`. On BETA 1005 the nine cases from #2475 and the scalar control pass, and `SingleElementOverflow` and `SqrAfterSingleOverflow` fail. +```vb +' Regression tests for #2475 and its follow-up, as compile cases: each line that +' ends in a ' CASE : comment must give that diagnostic on that +' line (a code, "error" for any error, or "none"). tbrun --tests checks them. +' Big overflows a Single and is accepted; Root, correct, is then refused with +' TB5001. Declared alone, Root compiles, and Big(0) reads 1.#INF. +Module Startup + ' Left in BETA 1005: a Single element that overflows is accepted, and the + ' array constant after it, which calls Sqr, is refused. + Const Big() As Single = Array(1E+39) ' CASE SingleElementOverflow: error + Const Root() As Double = Array(Sqr(4)) ' CASE SqrAfterSingleOverflow: none + + ' Fixed in #2475 (BETA 1005): an element too large for its type is TB5001, + ' where BETA 997 crashed the compiler. + Const L1() As Long = Array(2147483648) ' CASE LongElementOverflow: TB5001 + Const L2() As Long = Array(1, 2147483648) ' CASE LongSecondElementOverflow: TB5001 + Const L3() As Long = Array(3000000000.5) ' CASE LongFractionOverflow: TB5001 + Const I1() As Integer = Array(40000.5) ' CASE IntegerElementOverflow: TB5001 + Const Y1() As Byte = Array(300.5) ' CASE ByteElementOverflow: TB5001 + Const Y2() As Byte = Array(-1.5) ' CASE ByteNegativeElement: TB5001 + Const Q1() As LongLong = Array(9223372036854775808#) ' CASE LongLongElementOverflow: TB5001 + Const D1() As Double = Array(1E+300 * 1E+300) ' CASE DoubleElementOverflow: TB5001 + + ' Control: a scalar Single constant that overflows is refused where it is used. + Const S As Single = 1E+39 + + Public Sub Main() + Debug.Print S ' CASE ScalarSingleOverflowUse: TB5002 + End Sub +End Module +``` + +**Expected behavior** +An error on the declaration of `Big`, TB5001 as for every other element type: the `Long`, `Integer`, `Byte`, `LongLong` and `Double` arrays above are each refused on their own line. The declaration of `Root` compiles, as it does without `Big`, and `Root(0)` is 2. A scalar `Const S As Single = 1E+39` is refused too, with TB5002 where it is used. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +VB6 has no array constants, so there is no VB6 project. + +What reproduces it: `Array(3.5E+38)` in place of `Array(1E+39)`; `Timer` or `Rnd` in place of `Sqr(4)`, when that constant is the first array `Const` after `Big` (a second one after it compiles). What does not: `Root` declared before `Big`; an element of `Abs(-4)` or a literal after `Big`; the same `Single` constant declared inside a procedure, which is not accepted silently (the run ends with an error when the procedure is reached). The order and the three functions suggest an overflow flag left set while `Big` is evaluated, which the next constant's call then reads as an error; that is an inference. + +Severity: a silent wrong value, an element that holds infinity where a compile error belongs, and a compile error on a correct constant that names neither the cause nor the line that has it. + + --- @@ -1014,3 +1092,1490 @@ What did not reproduce it: `Host.CurrentProject.RootFolder.Parent`, the parent o Severity: crash. The crash is in the read, so no `On Error` handler helps, and an add-in that walks up the tree with `Parent` until `Is Nothing` crashes the compiler on every start, until its DLL is removed. The workaround is to stop at the folder whose `Path` is `twinbasic:/` without reading its `Parent`. + +--- + +## File statements and functions raise *Unspecified error* (-2147467259) in place of VB6's error numbers + +**Describe the bug** +When a file statement or function fails, twinBASIC raises run-time error -2147467259 (`&H80004005`, *Unspecified error*) where VB6 raises a numbered error: 52 *Bad file name or number*, 53 *File not found*, 55 *File already open*, 58 *File already exists*, 59 *Bad record length*, 63 *Bad record number*, 75 *Path/File access error* or 76 *Path not found*. Of 42 failing file operations, 37 raise -2147467259, and the other 5 match VB6. An `On Error` handler written for VB6, which tests `Err.Number` for one of those numbers, does not recognise the error. Observed in a run of the reproducer project, in the IDE and in a built exe alike. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `file-errors-unspecified.twinproj` (attached as `file-errors-unspecified.zip`) and run it (F5). Its `Sub Main` makes a folder in `%TEMP%`, makes 42 file operations fail one at a time under `On Error Resume Next`, and prints `: ` after each. The smallest of them: + ``` + On Error Resume Next + Kill Environ$("TEMP") & "\no-such-file.txt" + Debug.Print Err.Number, Err.Description + ``` +2. See `-2147467259 Unspecified error` in each of these 37 cases, where the same program built in VB6 (attached as `file-errors-unspecified-vb6.zip`) prints the number shown: + + | operation that fails | VB6 | + |---|---| + | `Open` a file that does not exist, `For Input` | 53 | + | `Open` a file in a folder that does not exist, `For Output`, `Append`, `Binary` and `Random` (4 cases) | 76 | + | `Open` a folder `For Input` | 75 | + | `Open` a read-only file `For Output` | 75 | + | `Open` a file name that is not valid | 52 | + | `Open` with a file number that is in use | 55 | + | `GetAttr`, `FileLen`, `FileDateTime` and `SetAttr` of a file that does not exist (4 cases) | 53 | + | `GetAttr` of a file in a folder that does not exist | 76 | + | `Kill` of a file that does not exist, and of a pattern that matches no file (2 cases) | 53 | + | `Kill` of a read-only file | 75 | + | `Kill` of a file that is open | 55 | + | `Name` of a file that does not exist | 53 | + | `Name` onto a file that exists | 58 | + | `Name` of a file that is open | 55 | + | `MkDir` of a folder that exists | 75 | + | `MkDir` in a folder that does not exist | 76 | + | `RmDir` of a folder that does not exist | 76 | + | `RmDir` of a folder that holds a file | 75 | + | `ChDir` to a folder that does not exist | 76 | + | `EOF`, `LOF`, `Loc`, `Seek`, `FileAttr`, `Get #`, `Put #`, `Print #` and `Line Input #` with file number 99, which is not open (9 cases) | 52 | + | `Seek #f, 0` | 63 | + | `Put` of a 22-character string to a `Random` file opened with `Len = 4` | 59 | + +3. See the 5 cases that match VB6: `FileCopy` of a file that does not exist (53) and into a folder that does not exist (76), `Line Input #` past the end of the file (62), and `Close` of a file number that is not open and `SetAttr` of a file open `For Input`, which raise no error in either. + +**Expected behavior** +The numbers in the table, with VB6's descriptions, as for the 5 cases that already match. A VB6 program that handles a missing file by testing `Err.Number = 53` takes its other branch in twinBASIC. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +This extends #2345, which reports the first row of the table on BETA 979, and is part of #56. `FileCopy` raised -2147467259 as well up to BETA 995, and raises VB6's numbers from BETA 997, the build that fixed #2451. + +The same on BETA 997 and 957, apart from `FileCopy`. The same in a built exe as in the IDE, and the same with the project setting *Use Unicode Standard Library* set to No. In code compiled with LLVM the number is the same, and the description is *Application-defined or object-defined error*. + +What does not reproduce it: the `DirListBox` and `FileListBox` controls trap the error themselves, and raise 76 for a `Path` that does not exist, as VB6 does. + +Severity: moderate. An error is raised wherever VB6 raises one, so no failure goes unreported. But a handler that tests for the number does not recognise the error, a handler that shows `Err.Description` shows *Unspecified error*, and code that decides a file exists from `Err.Number <> 53` decides wrongly. + + + +--- + +## `SetAttr` changes the attributes of a file open for writing, where VB6 raises error 55 + +**Describe the bug** +`SetAttr` on a file that the program has open `For Output`, `Append`, `Binary` or `Random` sets the attributes without an error. VB6 raises run-time error 55, *File already open*, and leaves the attributes as they were. So a twinBASIC program can make a file read-only while it still has it open for writing, and a VB6 program's handler for error 55 never runs. Observed in a run of the reproducer project. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `setattr-open-file-no-error.twinproj` (attached as `setattr-open-file-no-error.zip`) and run it (F5). Its `Sub Main` opens one file in each mode in turn and, while the file is open, runs `SetAttr` on it with `vbReadOnly` under `On Error Resume Next`, then prints `Err.Number` and `GetAttr` of the file. +2. See, beside what the same program prints when built in VB6 (attached as `setattr-open-file-no-error-vb6.zip`): + + | file open `For` | twinBASIC | VB6 | + |---|---|---| + | `Input` | no error; attributes 1 (read-only) | no error; attributes 1 | + | `Output` | no error; attributes 1 | 55 *File already open*; attributes 32 | + | `Append` | no error; attributes 1 | 55 *File already open*; attributes 0 | + | `Binary` | no error; attributes 1 | 55 *File already open*; attributes 0 | + | `Random` | no error; attributes 1 | 55 *File already open*; attributes 0 | + +**Expected behavior** +Error 55 for a file open `For Output`, `Append`, `Binary` or `Random`, with its attributes unchanged, as in VB6. VBA-Docs states that a run-time error occurs when setting the attributes of an open file; VB6 raises none for a file open `For Input`, and twinBASIC agrees with it there. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +The same on BETA 997 and 957. + +Severity: low, but silent. No error is raised, and the file's attributes change where VB6 leaves them alone: the file stays read-only until its attributes are set back. + + + +--- + +## `Dim ... As New` on an `Interface` compiles, where `New` on one is refused with TB5074 + +**Describe the bug** +Follow-up to #2486, which is fixed in BETA 1005 for the `New` expression: `New IParent`, on an `Interface` declared in a project or a package, is now refused with TB5074, *Cannot construct 'IParent': expected a class type.*, as `New stdole.IUnknown` is. This is the declaration form, which is still accepted: `Dim p As New IParent` compiles without a diagnostic, in a procedure, at module level and as a field of a class. At run time it builds the object #2486 described, with nothing behind its members: a call on a member the interface declares returns 0, and a call on a member it inherits from another `Interface` ends the run with an access violation. Observed in the IDE's diagnostics, and in a run. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `dim-as-new-interface.twinproj` (attached as `dim-as-new-interface.zip`). Its module `NewOnInterfaceTests`, below, declares `Interface IParent`, `Interface IChild Extends IParent`, a class that implements `IParent`, and each way of constructing one of them. +2. Compile it. Each of the four `New` lines on an interface gets TB5074. The five lines below get no diagnostic: + ``` + Public Item As New IParent ' a field of a class + Private ModuleLevel As New IParent ' at module level + Dim p As New IParent ' in a procedure + Dim q As New IChild + Dim r As New ErrorContext ' an interface of the VBRUN package + ``` +3. To see what such an object does, run this in a project of its own, with the two interfaces above: + ``` + Dim p As New IParent + Debug.Print (p Is Nothing), p.F() ' False 0 + Dim c As New IChild + Debug.Print c.G() ' 0 + Debug.Print c.F() ' NATIVE EXCEPTION: ACCESS_VIOLATION, and the run ends + ``` + +The module below is the reproducer's, and is meant to be adapted as a regression test. Because what it tests is a diagnostic, each case is a line of source that ends in the comment `' CASE : `: the diagnostic code the line must get, `error` for any error, or `none`. On BETA 1005 the four `New_` cases from #2486 and the two controls that construct a class pass, and the five `AsNew_` cases on an interface fail. +```vb +' Regression tests for #2486 and its follow-up, as compile cases: each line that +' ends in a ' CASE : comment must give that diagnostic on that +' line (a code, "error" for any error, or "none"). tbrun --tests checks them. +' New needs a class; an Interface has none, so every way of constructing one is +' a compile error, as New stdole.IUnknown is. + +[InterfaceId("E5B8C0A1-0000-4000-8000-0000000000D1")] +Interface IParent + Function F() As Long +End Interface + +[InterfaceId("E5B8C0A1-0000-4000-8000-0000000000D2")] +Interface IChild Extends IParent + Function G() As Long +End Interface + +Class Implementer + Implements IParent + Private Function IParent_F() As Long Implements IParent.F + Return 1 + End Function +End Class + +Class Holder + Public Item As New IParent ' CASE AsNew_ClassField: error +End Class + +Module NewOnInterfaceTests + + Private ModuleLevel As New IParent ' CASE AsNew_ModuleVariable: error + + ' Fixed in #2486 (BETA 1005): New on an Interface is refused with TB5074. + Private Sub NewExpressions() + Dim a As IParent = New IParent ' CASE New_ProjectInterface: TB5074 + Dim b As IChild = New IChild ' CASE New_DerivedInterface: TB5074 + Dim c As ErrorContext = New ErrorContext ' CASE New_PackageInterface: TB5074 + Dim d As stdole.IUnknown = New stdole.IUnknown ' CASE New_TypeLibraryInterface: TB5074 + Dim e As IParent = New Implementer ' CASE New_ClassThatImplements: none + End Sub + + ' Left in BETA 1005: Dim ... As New on an Interface compiles. + Private Sub AsNewDeclarations() + Dim p As New IParent ' CASE AsNew_ProjectInterface: error + Dim q As New IChild ' CASE AsNew_DerivedInterface: error + Dim r As New ErrorContext ' CASE AsNew_PackageInterface: error + Dim s As New Implementer ' CASE AsNew_Class: none + End Sub + +End Module +``` + +**Expected behavior** +A compile error on each `As New` of an `Interface`, TB5074 as for `New` on one: `As New` constructs the object when it is first used, and an interface has no class to construct. `As New` on a class that implements the interface compiles, as it does now. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +VB6 has no comparison: a VB6 project cannot declare an `Interface`, and the interfaces of the type libraries it references are hidden from it, as #2486 said. + +The object `As New` builds is the one #2486 described: `TypeName` gives the interface's name, `ObjPtr` is not 0, `ErrorContext.Number` reads 0, and an inherited member ends the run with `NATIVE EXCEPTION: ACCESS_VIOLATION` (step 3, checked in a run on BETA 1005). + +Severity: low in practice, since few programs write `As New` on an interface, but the compile-time check #2486 added does not cover it, and the result is a silent wrong value or a crash. + + + +--- + +## A twinBASIC class's connection point and its container end in an access violation when given a null pointer + +**Describe the bug** +Follow-up to #2488, which is fixed in BETA 1005 for `Advise`: given `Nothing` as the sink, or an unassigned `IUnknown` variable, it now fails with `E_POINTER`. This is the other methods of the same two objects that take a pointer: given a null one, each still ends the run with an access violation, which the DEBUG CONSOLE reports as `NATIVE EXCEPTION: ACCESS_VIOLATION`. They are `IConnectionPoint::GetConnectionInterface` (a null *pIID*), `IConnectionPoint::GetConnectionPointContainer` (a null *ppCPC*), `IConnectionPointContainer::FindConnectionPoint` (a null *riid*, or a null *ppCP*) and `IConnectionPointContainer::EnumConnectionPoints` (a null *ppEnum*), on the connectable object of a class that declares an `Event`. Observed by running the reproducer's test cases. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `connection-point-null-pointer-crash.twinproj` (attached as `connection-point-null-pointer-crash.zip`). Its module `ConnectionPointTests`, below, declares the project's own copies of `IConnectionPointContainer`, `IEnumConnectionPoints` and `IConnectionPoint`, with every out parameter `ByVal LongPtr` so that a null one can be passed, a class `EventSource` with one `Event`, and one `[TestCase]` Sub for each call. +2. Call one of the null-pointer cases from `Sub Main`, such as `ConnectionPointTests.GetConnectionInterface_NullIid_ReturnsEPointer`, and press F5. +3. See a line beginning `NATIVE EXCEPTION: ACCESS_VIOLATION` in the DEBUG CONSOLE: the run ends at the call, although it is made under `On Error Resume Next`. + +The module below is the reproducer's, and is meant to be adapted as a regression test. On BETA 1005 the three `Advise_` cases from #2488 pass, and the five null-pointer cases fail with the access violation. +```vb +' Regression tests for #2488 and its follow-up. A class with an Event is a +' connectable object; these call its IConnectionPointContainer and its +' IConnectionPoint with a null pointer, where the COM contract (the Windows SDK +' pages for each method) says the method returns E_POINTER. Every out parameter +' is declared ByVal LongPtr, so that a null one can be passed. + +[InterfaceId("B196B284-BAB4-101A-B69C-00AA00341D07")] +Interface IConnectionPointContainerRaw Extends stdole.IUnknown + Sub EnumConnectionPoints(ByVal ppEnum As LongPtr) + Sub FindConnectionPoint(ByVal riid As LongPtr, ByVal ppCP As LongPtr) +End Interface + +[InterfaceId("B196B285-BAB4-101A-B69C-00AA00341D07")] +Interface IEnumConnectionPointsRaw Extends stdole.IUnknown + Sub Next(ByVal cConnections As Long, ByRef ppCP As IConnectionPointRaw, ByRef pcFetched As Long) +End Interface + +[InterfaceId("B196B286-BAB4-101A-B69C-00AA00341D07")] +Interface IConnectionPointRaw Extends stdole.IUnknown + Sub GetConnectionInterface(ByVal piid As LongPtr) + Sub GetConnectionPointContainer(ByVal ppCPC As LongPtr) + Function Advise(ByVal pUnkSink As stdole.IUnknown) As Long + Sub Unadvise(ByVal dwCookie As Long) +End Interface + +Class EventSource + Public Event Ping() +End Class + +Class NotASink + Public Value As Long +End Class + +[TestFixture] +Module ConnectionPointTests + + Private Const E_POINTER As Long = &H80004003 ' the Windows SDK, for a null pointer argument + Private Const E_NOINTERFACE As Long = &H80004002 + + Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _ + ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As LongPtr) + + ' The connection point of a new EventSource, through EnumConnectionPoints and Next. + Private Function PointOf(ByVal container As IConnectionPointContainerRaw) As IConnectionPointRaw + Dim pEnum As LongPtr + container.EnumConnectionPoints VarPtr(pEnum) + Dim points As IEnumConnectionPointsRaw + CopyMemory VarPtr(points), VarPtr(pEnum), LenB(pEnum) ' takes over the reference + Dim fetched As Long + points.Next 1, PointOf, fetched + End Function + + ' Fixed in #2488 (BETA 1005): Advise with a null sink returns E_POINTER. + + [TestCase] + Public Sub Advise_Nothing_ReturnsEPointer() + Dim point As IConnectionPointRaw = PointOf(New EventSource) + On Error Resume Next + point.Advise Nothing + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub Advise_UnassignedIUnknown_ReturnsEPointer() + Dim point As IConnectionPointRaw = PointOf(New EventSource) + Dim sink As stdole.IUnknown + On Error Resume Next + point.Advise sink + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub Advise_NotASink_ReturnsENoInterface() + Dim point As IConnectionPointRaw = PointOf(New EventSource) + On Error Resume Next + point.Advise New NotASink + Assert.Strict.AreEqual E_NOINTERFACE, Err.Number ' as VB6 returns, too + End Sub + + ' Left in BETA 1005: every other null pointer ends in an access violation. + + [TestCase] + Public Sub GetConnectionInterface_NullIid_ReturnsEPointer() + Dim point As IConnectionPointRaw = PointOf(New EventSource) + On Error Resume Next + point.GetConnectionInterface 0 + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub GetConnectionPointContainer_NullOut_ReturnsEPointer() + Dim point As IConnectionPointRaw = PointOf(New EventSource) + On Error Resume Next + point.GetConnectionPointContainer 0 + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub FindConnectionPoint_NullIid_ReturnsEPointer() + Dim container As IConnectionPointContainerRaw = New EventSource + Dim p As LongPtr + On Error Resume Next + container.FindConnectionPoint 0, VarPtr(p) + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub FindConnectionPoint_NullOut_ReturnsEPointer() + Dim container As IConnectionPointContainerRaw = New EventSource + Dim point As IConnectionPointRaw = PointOf(container) + Dim iid(15) As Byte + point.GetConnectionInterface VarPtr(iid(0)) + On Error Resume Next + container.FindConnectionPoint VarPtr(iid(0)), 0 + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub EnumConnectionPoints_NullOut_ReturnsEPointer() + Dim container As IConnectionPointContainerRaw = New EventSource + On Error Resume Next + container.EnumConnectionPoints 0 + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + +End Module +``` + +**Expected behavior** +Each call fails with `E_POINTER` (`&H80004003`), which twinBASIC raises as a run-time error that `On Error` can handle, as `Advise` now does for a null sink. The Windows SDK documentation of each of these methods lists `E_POINTER` for a pointer argument that is not valid. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +VB6 crashes too. The same calls on the connection point of a VB6 class with an event, made through `DispCallFunc` (the VB6 project is attached as `connection-point-null-pointer-crash-vb6.zip`; the environment variable `CASE`, 1 to 8, picks the call, and 2 is `GetConnectionInterface` with a null pointer), each end the exe with an access violation, `0xC0000005` in `MSVBVM60.DLL`. As for #2488, a fix costs no compatibility: no program depends on the crash. + +What does not crash: each call given a valid pointer. `FindConnectionPoint` with the point's own interface identifier returns it, and with `GUID_NULL` it fails with `CONNECT_E_NOCONNECTION` (`&H80040200`), as the contract says (VB6 returns `E_NOINTERFACE` there). + +Severity: low in practice, since a program rarely passes a null pointer, but a COM client written in any language can, and the object crashes the process that hosts the class instead of refusing the call. + + + +--- + +## A failure code from a late-bound call is raised as the HRESULT, where VB6 raises its own error number, such as 450 for DISP_E_BADPARAMCOUNT + +**Describe the bug** +Follow-up to #2490, which is fixed in BETA 1005 for a name the object does not have: a late-bound statement and `CallByName` now raise error 438, as VB6 does. This is the rest of the conversion VB6 makes when `IDispatch::Invoke` fails. For ten failure codes VB6 raises one of its own error numbers, and twinBASIC raises the `HRESULT` itself, so a handler written for VB6 or VBA does not recognise the error. The common one is a call with the wrong number of arguments to an object such as a `Collection`, a `Scripting.Dictionary` or a `Scripting.FileSystemObject`: VB6 raises 450, *Wrong number of arguments or invalid property assignment*, and twinBASIC raises `&H8002000E` (`DISP_E_BADPARAMCOUNT`), *Invalid number of parameters.* Observed by running the reproducer's test cases. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `latebound-hresult-mapping.twinproj` (attached as `latebound-hresult-mapping.zip`). Its module, below, holds `HResultObject`, a `NotDispatchable` class that implements the project's own copy of `IDispatch`: a member name of the form `Hr` fails with that `HRESULT`, so `o.Hr8002000E` is a late-bound call whose `Invoke` returns `DISP_E_BADPARAMCOUNT`. +2. Run, with `On Error Resume Next`, `x = c.Item(1, 2)` where `c` is an `Object` holding a `Collection`. See `Err.Number` -2147352562 (`&H8002000E`); VB6 gives 450. +3. Run each case of the module below. Each prints the number twinBASIC raises; the VB6 project (attached as `latebound-hresult-mapping-vb6.zip`) prints VB6's: + + | `Invoke` returns | VB6 | twinBASIC | + |---|---|---| + | `DISP_E_BADPARAMCOUNT` `&H8002000E` | 450 | -2147352562 | + | `DISP_E_PARAMNOTOPTIONAL` `&H8002000F` | 449 | -2147352561 | + | `E_NOTIMPL` `&H80004001` | 445 | -2147467263 | + | `E_OUTOFMEMORY` `&H8007000E` | 7 | -2147024882 | + | `DISP_E_BADINDEX` `&H8002000B` | 9 | -2147352565 | + | `DISP_E_NONAMEDARGS` `&H80020007` | 446 | -2147352569 | + | `DISP_E_ARRAYISLOCKED` `&H8002000D` | 10 | -2147352563 | + | `E_NOINTERFACE` `&H80004002` | 430 | -2147467262 | + | `E_ACCESSDENIED` `&H80070005` | 70 | -2147024891 | + | `E_ABORT` `&H80004004` | 287 | -2147467260 | + +The module below is the reproducer's, and is meant to be adapted as a regression test. Each expected number is VB6's, from the VB6 project. On BETA 1005 the three cases from #2490 and the three codes twinBASIC already converts pass, and the ten codes of the table and the `Collection` call of step 2 fail. +```vb +' Regression tests for #2490 and its follow-up: the error number a late-bound +' call raises when the object fails it. The expected numbers are what VB6 6.0 +' raises for the same failure (the VB6 project beside this one prints them). +' +' HResultObject answers any name of the form Hr and fails the call with that +' HRESULT, so o.Hr8002000E is a late-bound call whose Invoke returns +' DISP_E_BADPARAMCOUNT. + +Module DispatchTypes + Public Type GUID + Data1 As Long + Data2 As Integer + Data3 As Integer + Data4(0 To 7) As Byte + End Type + + Public Type DISPPARAMS + rgvarg As LongPtr + rgdispidNamedArgs As LongPtr + cArgs As Long + cNamedArgs As Long + End Type + + Public Type EXCEPINFO + wCode As Integer + wReserved As Integer + bstrSource As LongPtr + bstrDescription As LongPtr + bstrHelpFile As LongPtr + dwHelpContext As Long + pvReserved As LongPtr + pfnDeferredFillIn As LongPtr + scode As Long + End Type + + Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _ + ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As LongPtr) + Private Declare PtrSafe Function lstrlenW Lib "kernel32" (ByVal lpString As LongPtr) As Long + + Public Function PtrToString(ByVal p As LongPtr) As String + Dim n As Long = lstrlenW(p) + Dim s As String = Space$(n) + If n > 0 Then CopyMemory StrPtr(s), p, n * 2 + Return s + End Function +End Module + +[InterfaceId("00020400-0000-0000-C000-000000000046")] +Interface IDispatchOwn Extends stdole.IUnknown + Sub GetTypeInfoCount(ByRef pctinfo As Long) + Sub GetTypeInfo(ByVal iTInfo As Long, ByVal lcid As Long, ByRef ppTInfo As LongPtr) + Sub GetIDsOfNames(ByRef riid As GUID, ByRef rgszNames As LongPtr, ByVal cNames As Long, ByVal lcid As Long, ByRef rgDispId As Long) + Sub Invoke(ByVal dispIdMember As Long, ByRef riid As GUID, ByVal lcid As Long, ByVal wFlags As Integer, ByRef pDispParams As DISPPARAMS, ByRef pVarResult As Variant, ByRef pExcepInfo As EXCEPINFO, ByRef puArgErr As Long) +End Interface + +' Dispatched through its own IDispatchOwn methods, since it is NotDispatchable. +NotDispatchable Class HResultObject + Implements IDispatchOwn + + Private Sub GetTypeInfoCount(ByRef pctinfo As Long) Implements IDispatchOwn.GetTypeInfoCount + pctinfo = 0 + End Sub + + Private Sub GetTypeInfo(ByVal iTInfo As Long, ByVal lcid As Long, ByRef ppTInfo As LongPtr) Implements IDispatchOwn.GetTypeInfo + Err.ReturnHResult = &H80004001 ' E_NOTIMPL + End Sub + + ' "Hr8002000E" gets the DISPID &H8002000E. + Private Sub GetIDsOfNames(ByRef riid As GUID, ByRef rgszNames As LongPtr, ByVal cNames As Long, ByVal lcid As Long, ByRef rgDispId As Long) Implements IDispatchOwn.GetIDsOfNames + rgDispId = CLng("&H" & Mid$(PtrToString(rgszNames), 3)) + End Sub + + ' Fails with the HRESULT that is the DISPID. + Private Sub Invoke(ByVal dispIdMember As Long, ByRef riid As GUID, ByVal lcid As Long, ByVal wFlags As Integer, ByRef pDispParams As DISPPARAMS, ByRef pVarResult As Variant, ByRef pExcepInfo As EXCEPINFO, ByRef puArgErr As Long) Implements IDispatchOwn.Invoke + Err.ReturnHResult = dispIdMember + End Sub +End Class + +Class Widget + Public Sub Hello() + End Sub +End Class + +[TestFixture] +Module LateBoundErrorTests + + ' Fixed in #2490 (BETA 1005): a name the object does not have is error 438. + + [TestCase] + Public Sub UnknownMember_Class_Raises438() + Dim o As Object = New Widget + On Error Resume Next + o.Nope + Assert.Strict.AreEqual 438, Err.Number ' VB6: 438 + End Sub + + [TestCase] + Public Sub UnknownMember_Collection_Raises438() + Dim o As Object = New Collection + On Error Resume Next + o.Nope + Assert.Strict.AreEqual 438, Err.Number ' VB6: 438 + End Sub + + [TestCase] + Public Sub UnknownMember_CallByName_Raises438() + Dim o As Object = New Widget + On Error Resume Next + CallByName o, "Nope", VbMethod + Assert.Strict.AreEqual 438, Err.Number ' VB6: 438 + End Sub + + ' Already as in VB6 in BETA 1005: these failure codes are raised as VB6 raises them. + + [TestCase] + Public Sub MemberNotFound_Raises438() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80020003 ' DISP_E_MEMBERNOTFOUND + Assert.Strict.AreEqual 438, Err.Number ' VB6: 438 + End Sub + + [TestCase] + Public Sub TypeMismatch_Raises13() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80020005 ' DISP_E_TYPEMISMATCH + Assert.Strict.AreEqual 13, Err.Number ' VB6: 13 + End Sub + + [TestCase] + Public Sub InvalidArg_Raises5() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80070057 ' E_INVALIDARG + Assert.Strict.AreEqual 5, Err.Number ' VB6: 5 + End Sub + + ' Left in BETA 1005: these are raised as the HRESULT itself. + + [TestCase] + Public Sub BadParamCount_Collection_Raises450() + Dim o As Object = New Collection + Dim x As Variant + On Error Resume Next + x = o.Item(1, 2) ' Item takes one argument + Assert.Strict.AreEqual 450, Err.Number ' VB6: 450, Wrong number of arguments or invalid property assignment + End Sub + + [TestCase] + Public Sub BadParamCount_Raises450() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr8002000E ' DISP_E_BADPARAMCOUNT + Assert.Strict.AreEqual 450, Err.Number ' VB6: 450, Wrong number of arguments or invalid property assignment + End Sub + + [TestCase] + Public Sub ParamNotOptional_Raises449() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr8002000F ' DISP_E_PARAMNOTOPTIONAL + Assert.Strict.AreEqual 449, Err.Number ' VB6: 449, Argument not optional + End Sub + + [TestCase] + Public Sub NotImpl_Raises445() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80004001 ' E_NOTIMPL + Assert.Strict.AreEqual 445, Err.Number ' VB6: 445, Object doesn't support this action + End Sub + + [TestCase] + Public Sub OutOfMemory_Raises7() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr8007000E ' E_OUTOFMEMORY + Assert.Strict.AreEqual 7, Err.Number ' VB6: 7, Out of memory + End Sub + + [TestCase] + Public Sub BadIndex_Raises9() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr8002000B ' DISP_E_BADINDEX + Assert.Strict.AreEqual 9, Err.Number ' VB6: 9, Subscript out of range + End Sub + + [TestCase] + Public Sub NoNamedArgs_Raises446() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80020007 ' DISP_E_NONAMEDARGS + Assert.Strict.AreEqual 446, Err.Number ' VB6: 446, Object doesn't support named arguments + End Sub + + [TestCase] + Public Sub ArrayIsLocked_Raises10() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr8002000D ' DISP_E_ARRAYISLOCKED + Assert.Strict.AreEqual 10, Err.Number ' VB6: 10, This array is fixed or temporarily locked + End Sub + + [TestCase] + Public Sub NoInterface_Raises430() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80004002 ' E_NOINTERFACE + Assert.Strict.AreEqual 430, Err.Number ' VB6: 430, Class does not support Automation or does not support expected interface + End Sub + + [TestCase] + Public Sub AccessDenied_Raises70() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80070005 ' E_ACCESSDENIED + Assert.Strict.AreEqual 70, Err.Number ' VB6: 70, Permission denied + End Sub + + [TestCase] + Public Sub Abort_Raises287() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80004004 ' E_ABORT + Assert.Strict.AreEqual 287, Err.Number ' VB6: 287, Application-defined or object-defined error + End Sub + +End Module +``` + +**Expected behavior** +The error number VB6 raises for each code in the table, with VB6's description, as twinBASIC already does for `DISP_E_MEMBERNOTFOUND` (438), `DISP_E_TYPEMISMATCH` (13) and the others listed below. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +twinBASIC already raises VB6's number for `DISP_E_MEMBERNOTFOUND` (438), `DISP_E_TYPEMISMATCH` (13), `E_INVALIDARG` (5), `DISP_E_OVERFLOW` (6), `DISP_E_DIVBYZERO` (11), `DISP_E_BADVARTYPE` (458), `DISP_E_PARAMNOTFOUND` (448) and an `HRESULT` of the form `&H800A....` (its low word, 424 for `&H800A01A8`). `E_POINTER`, `E_FAIL`, `E_UNEXPECTED` and `&H8007000D` are raised as themselves by both, from a statement. + +The wrong number of arguments, on objects that are not twinBASIC's: `Dictionary.Add "k"` (one argument of two), `Dictionary.Add "k", 1, 2`, `Dictionary.Count = 5`, `x = Dictionary.Exists()`, `x = FileSystemObject.GetFileName()`, `x = FileSystemObject.BuildPath("a")` and `x = Collection.Item(1, 2)` each raise -2147352562 in twinBASIC and 450 in VB6. + +Not part of this report: VB6's `CallByName` raises 440, *Automation error*, for the four codes that a statement leaves as they are, where twinBASIC's raises the code itself. The numbers in the table are the same for a statement, for `x = o.Name` and for `CallByName` in VB6. + +Severity: code ported from VB6 that tests `Err.Number` for 450, 449 or 9 after a late-bound call does not recognise the error, and the description is the system's text for a COM code instead of VB's. + + + +--- + +## An error raised with a constant number, in a member called late-bound, reaches the caller as &H80020009, *Exception occurred.* + +**Describe the bug** +Follow-up to #2490, which is fixed in BETA 1005 for a name the object does not have, now error 438 as in VB6. This is another late-bound error number that is not VB6's: when the member called raises an error itself, with `Err.Raise` and a constant number (`Err.Raise 380`, with or without a source and a description) or with the `Error` statement, the late-bound caller gets error -2147352567 (`&H80020009`, `DISP_E_EXCEPTION`), *Exception occurred.*, instead of 380. Called early-bound, the same member raises 380. `Err.Raise` with a number held in a variable reaches a late-bound caller correctly. The VB package's own properties are affected: a colour property set late-bound, or with `CallByName`, to a value that is not a colour raises `&H80020009`, where the same assignment early-bound raises 380 (see #2484). Observed by running the reproducer's test cases, in an IDE run and in the built exe alike. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `latebound-constant-raise-exception.twinproj` (attached as `latebound-constant-raise-exception.zip`). Its class `Thing`, in the module below, raises error 380 each way. +2. Run, with `On Error Resume Next`: + ``` + Dim o As Object = New Thing + o.RaiseConstant ' Err.Raise 380 + Debug.Print Err.Number, Err.Description + ``` +3. See `-2147352567 Exception occurred.` Through `Dim t As Thing`, `t.RaiseConstant` gives `380 Invalid property value`. + +The module below is the reproducer's, and is meant to be adapted as a regression test. Each expected number is VB6's, from the VB6 project attached as `latebound-constant-raise-exception-vb6.zip`, which raises 380 for each of `Thing`'s members; the last case's is the early-bound result. On BETA 1005 the case from #2490 and the two that already raise 380 pass, and the seven others fail with -2147352567. +```vb +' Regression tests for #2490 and its follow-up: the error a late-bound caller +' sees when the member it calls raises one. Thing's members each raise error 380 +' (Invalid property value) a different way. Called early-bound, every one raises +' 380; called late-bound, VB6 6.0 raises 380 for every one (the VB6 project beside +' this one prints them). In BETA 1005 a member that raises with a constant number +' and nothing else reaches the late-bound caller as &H80020009 (DISP_E_EXCEPTION, +' "Exception occurred."). + +Class Thing + Private mValue As Long + + Public Sub RaiseConstant() + Err.Raise 380 + End Sub + + Public Sub RaiseVariable(ByVal Number As Long) + Err.Raise Number + End Sub + + Public Sub RaiseConstantWithSource() + Err.Raise 380, "Thing" + End Sub + + Public Sub RaiseConstantWithDescription() + Err.Raise 380, "Thing", "the value is out of range" + End Sub + + Public Sub ErrorStatement() + Error 380 + End Sub + + Public Property Get Value() As Long + Return mValue + End Property + + Public Property Let Value(ByVal NewValue As Long) + If NewValue < 0 Then Err.Raise 380 + mValue = NewValue + End Property +End Class + +[TestFixture] +Module RaiseThroughLateBindingTests + + ' Fixed in #2490 (BETA 1005): a name the object does not have is error 438. + + [TestCase] + Public Sub UnknownMember_Raises438() + Dim o As Object = New Thing + On Error Resume Next + o.Nope + Assert.Strict.AreEqual 438, Err.Number ' VB6: 438 + End Sub + + ' Already as in VB6 in BETA 1005. + + [TestCase] + Public Sub EarlyBound_RaiseConstant_Raises380() + Dim t As Thing = New Thing + On Error Resume Next + t.RaiseConstant + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_RaiseVariable_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.RaiseVariable 380 + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + ' Left in BETA 1005: &H80020009 in place of 380. + + [TestCase] + Public Sub LateBound_RaiseConstantWithSource_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.RaiseConstantWithSource + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_RaiseConstantWithDescription_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.RaiseConstantWithDescription + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_RaiseConstant_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.RaiseConstant + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_ErrorStatement_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.ErrorStatement + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_PropertyLet_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.Value = -1 + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub CallByName_RaiseConstant_Raises380() + Dim o As Object = New Thing + On Error Resume Next + CallByName o, "RaiseConstant", VbMethod + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_FormColour_Raises380() + ' The VB package's own colour properties raise 380 the same way (#2484). + Dim o As Object = New PictureBox + On Error Resume Next + o.BackColor = -1 + Assert.Strict.AreEqual 380, Err.Number ' early-bound: 380 + End Sub + +End Module +``` + +**Expected behavior** +380, *Invalid property value*, in every case, as the same member raises early-bound, as `Err.Raise` with a variable number gives late-bound, and as VB6 raises: the VB6 project prints `380` for each of the late-bound calls, `CallByName` included, and 380 with the description given where one is given. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +Also on BETA 997. + +What does not reproduce it: `Err.Raise Number` with the number in a variable or a parameter, with or without a source and a description; a run-time error the member does not raise itself, such as a division by zero (11) or `Collection.Item` with an index past its end (9), which arrive as themselves; any early-bound call. What does: `Err.Raise` with a constant number, with or without a constant source and description (`Err.Raise 5`, `Err.Raise 1234` as well as 380); the `Error` statement; a `Property Let`; a late-bound statement and `CallByName` alike; the built exe as well as an IDE run. The `&H80020009` is what `IDispatch::Invoke` returns when the member raised an error, with the error in the `EXCEPINFO`; so the constant-number form seems to leave the `EXCEPINFO` without the number the caller reads, which is an inference. + +Severity: moderate. Code that uses an `Object` variable or `CallByName`, as code ported from VB6 or written for scripting often does, cannot tell one error from another when the member raises with a constant number, which is the usual way to write `Err.Raise`, and the description it gets is *Exception occurred.* + + + +--- + +## `Data.ForeColor`, `Data.BackColor`, `OLE.BackColor` and `MultiFrame.BackColor` store a value that is not a colour, where VB6 raises error 380 + +**Describe the bug** +Follow-up to #2484, which is fixed in BETA 1005 for every colour property of Form, PictureBox, UserControl, Printer, MDIForm, Label, TextBox, CommandButton and the other controls it named: each now raises error 380, *Invalid property value*, and keeps its colour, as VB6 does. This is the four properties that still store the value: `ForeColor` and `BackColor` of the Data control, `BackColor` of the OLE control and `BackColor` of the MultiFrame. Assigning `-1` to any of them raises no error, and the property then reads `FFFFFFFF`. Observed by running the reproducer's test cases. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `colour-validation-data-ole-multiframe.twinproj` (attached as `colour-validation-data-ole-multiframe.zip`). Its `Form1` holds a PictureBox, a Label, a TextBox, a CommandButton, a user control (`UC1`), a Data control, an OLE control and a MultiFrame; `MyMDIForm` is an MDIForm. +2. Run, with `On Error Resume Next`: + ``` + Load Form1 + Form1.Data1.ForeColor = vbGreen + Form1.Data1.ForeColor = -1 + Debug.Print Err.Number, Hex$(Form1.Data1.ForeColor) + ``` +3. See `0 FFFFFFFF`. The same with `Form1.Label1.ForeColor` prints `380 FF00`. + +The module below is the reproducer's, and is meant to be adapted as a regression test. It loads the forms and never shows them. On BETA 1005 the seventeen cases from #2484 pass, and the four cases for the Data control, the OLE control and the MultiFrame fail with `Expected: 380, Actual: 0`. +```vb +' Regression tests for #2484 and its follow-up. Assigning a value that is not a +' colour to a colour property raises error 380, Invalid property value, and +' leaves the colour it had: VB6 6.0 does this for each property it has (the VB6 +' project beside this one prints them), and twinBASIC does it for the other +' controls. Form1 holds one control of each class; it is loaded, never shown. +' Each case sets the colour to vbGreen, assigns the bad value early-bound under +' On Error Resume Next, and checks the error and the colour read back. +[TestFixture] +Module ColourPropertyTests + + Private Const NotAColour As Long = -1 + Private Const SystemColourOutOfRange As Long = &H8000001F + + Private Sub Expect380(ByVal Number As Long, ByVal After As Long) + Assert.Strict.AreEqual 380, Number, "the error" ' VB6: 380, Invalid property value + Assert.Strict.AreEqual CLng(vbGreen), After, "the colour it had" ' VB6: the old colour is kept + End Sub + + ' Fixed in #2484 (BETA 1005). + + [TestCase] + Public Sub Form_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.ForeColor = vbGreen + On Error Resume Next: Form1.ForeColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Form_ForeColor_SystemColourOutOfRange() + Dim number As Long, after As Long + Load Form1: Form1.ForeColor = vbGreen + On Error Resume Next: Form1.ForeColor = SystemColourOutOfRange: number = Err.Number: On Error GoTo 0 + after = Form1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Form_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.BackColor = vbGreen + On Error Resume Next: Form1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Form_FillColor() + Dim number As Long, after As Long + Load Form1: Form1.FillColor = vbGreen + On Error Resume Next: Form1.FillColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.FillColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub PictureBox_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.PictureBox1.ForeColor = vbGreen + On Error Resume Next: Form1.PictureBox1.ForeColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.PictureBox1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub PictureBox_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.PictureBox1.BackColor = vbGreen + On Error Resume Next: Form1.PictureBox1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.PictureBox1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub PictureBox_FillColor() + Dim number As Long, after As Long + Load Form1: Form1.PictureBox1.FillColor = vbGreen + On Error Resume Next: Form1.PictureBox1.FillColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.PictureBox1.FillColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Label_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.Label1.ForeColor = vbGreen + On Error Resume Next: Form1.Label1.ForeColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.Label1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Label_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.Label1.BackColor = vbGreen + On Error Resume Next: Form1.Label1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.Label1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub TextBox_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.TextBox1.ForeColor = vbGreen + On Error Resume Next: Form1.TextBox1.ForeColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.TextBox1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub TextBox_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.TextBox1.BackColor = vbGreen + On Error Resume Next: Form1.TextBox1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.TextBox1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub CommandButton_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.CommandButton1.BackColor = vbGreen + On Error Resume Next: Form1.CommandButton1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.CommandButton1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub UserControl_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.UC1.TrySet "ForeColor", NotAColour, number, after: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub UserControl_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.UC1.TrySet "BackColor", NotAColour, number, after: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub UserControl_FillColor() + Dim number As Long, after As Long + Load Form1: Form1.UC1.TrySet "FillColor", NotAColour, number, after: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub MDIForm_BackColor() + Dim number As Long, after As Long + Load MyMDIForm: MyMDIForm.BackColor = vbGreen + On Error Resume Next: MyMDIForm.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = MyMDIForm.BackColor: Unload MyMDIForm + Expect380 number, after + End Sub + + [TestCase] + Public Sub MDIForm_BackColor_SystemColourOutOfRange() + Dim number As Long, after As Long + Load MyMDIForm: MyMDIForm.BackColor = vbGreen + On Error Resume Next: MyMDIForm.BackColor = SystemColourOutOfRange: number = Err.Number: On Error GoTo 0 + after = MyMDIForm.BackColor: Unload MyMDIForm + Expect380 number, after + End Sub + + ' Left in BETA 1005: these store the value and raise no error. + + [TestCase] + Public Sub Data_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.Data1.ForeColor = vbGreen + On Error Resume Next: Form1.Data1.ForeColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.Data1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Data_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.Data1.BackColor = vbGreen + On Error Resume Next: Form1.Data1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.Data1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub OLE_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.OLE1.BackColor = vbGreen + On Error Resume Next: Form1.OLE1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.OLE1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub MultiFrame_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.MultiFrame1.BackColor = vbGreen + On Error Resume Next: Form1.MultiFrame1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.MultiFrame1.BackColor: Unload Form1 + Expect380 number, after + End Sub + +End Module +``` +`UC1`, the user control on `Form1`, sets its own colour properties for the three `UserControl_` cases: +```vb + ' Sets one of the user control's own colour properties to vbGreen, then to + ' Value; returns the error the second assignment raised, and the colour after it. + Public Sub TrySet(ByVal Prop As String, ByVal Value As Long, ByRef Number As Long, ByRef After As Long) + Select Case Prop + Case "ForeColor": ForeColor = vbGreen + Case "BackColor": BackColor = vbGreen + Case "FillColor": FillColor = vbGreen + End Select + On Error Resume Next + Select Case Prop + Case "ForeColor": ForeColor = Value + Case "BackColor": BackColor = Value + Case "FillColor": FillColor = Value + End Select + Number = Err.Number + On Error GoTo 0 + Select Case Prop + Case "ForeColor": After = ForeColor + Case "BackColor": After = BackColor + Case "FillColor": After = FillColor + End Select + End Sub +``` + +**Expected behavior** +Error 380 for each assignment, and the old colour kept, as twinBASIC now does for the other controls, and as VB6 does for `OLE.BackColor`. The VB6 project, attached as `colour-validation-data-ole-multiframe-vb6.zip`, prints `OLE.BackColor = -1: Err 380, reads FF00`, and 380 for each of the other properties of the test. It has no Data control (none could be loaded in VB6 on the machine), and VB6 has no MultiFrame. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +`&H8000001F` and `&HFF0000FF`, which are not colours either, are stored by the same four properties with no error. In the BETA 1005 package source, the Data control, the OLE control and the MultiFrame declare these as plain `Public ... As OLE_COLOR` fields, with no `OnPropertyLet` handler that calls `CommonValidateColorOrRevert`, unlike the other controls. `OLE.BackColor` also gets the compile warning TB0009, *[BackColor] has not yet been implemented*, wherever it is used, and stores the value all the same. + +Not tested here: `Printer.ForeColor` and `Printer.FillColor`, which #2484 named, need a default printer; on BETA 1005 they raise 380 and keep the colour when there is one (#2484's reproducer prints them). + +Severity: low. The stored value is what the control then paints with, and no error tells the program that the colour it was given is not one. + + + +--- + +## After `Reset`, `Clone` of the WebView2 response headers enumerator skips the headers read before the `Reset` + +**Describe the bug** +Follow-up to #2496, which is fixed in BETA 1005 for `Clone` after `Next` and after `Skip`, on the enumerators of the request headers and the response headers alike, and after `Reset` on the request headers'. This is the response headers' enumerator after a `Reset`: after two `Next` calls and a `Reset`, the enumerator's `Next` returns the first header, `Ha`, and its clone's `Next` returns the third, `Hc`. Observed by running the reproducer's test cases. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `webview2-response-headers-reset-clone.twinproj` (attached as `webview2-response-headers-reset-clone.zip`). Its `Form1` holds one WebView2 control, which navigates to `https://example.com/page` and answers that request itself in `WebResourceRequested`, so nothing goes to the network. The handler appends four headers, `Ha` to `Hd`, to the response, and records what three fresh enumerators of `Request.Headers`, then three of `Response.Headers`, return (module `HeaderEnumeration`, below). +2. Call `HeaderEnumerationTests.Response_CloneAfterReset` from `Sub Main` and press F5. The form shows until its request is answered, and closes. +3. See the assertion fail with `Expected: "Ha|Ha" Actual: "Ha|Hc"`: the enumerator's `Next`, then the clone's. + +The module below is the reproducer's, and is meant to be adapted as a regression test. It shows `Form1` while the control answers its request, and unloads it. On BETA 1005 the five cases from #2496 pass, and `Response_CloneAfterReset` fails. +```vb +' Regression tests for #2496 and its follow-up. IEnumVARIANT::Clone returns an +' enumerator with the same state as the one it is called on (the COM +' documentation of IEnumVARIANT::Clone), so the clone's Next returns what the +' enumerator's own Next returns next. Each case compares the two after one Next, +' after Skip 2, and after two Next and a Reset, for the request headers and for +' the response headers, which hold Ha, Hb, Hc and Hd. + +' IEnumVARIANT, declared here because the package's own declaration is private. +[InterfaceId("00020404-0000-0000-C000-000000000046")] +Interface IEnumVARIANTOwn Extends stdole.IUnknown + Type CArrayOfVariants + Elements(1) As Variant + End Type + + Sub Next(ByVal celt As Long, ByRef rgVar As CArrayOfVariants, ByRef pCeltFetched As Long) + Sub Skip(ByVal celt As Long) + Sub Reset() + Function Clone() As stdole.IUnknown +End Interface + +' What the enumerators returned, recorded by Form1 while it answers its request. +Module HeaderEnumeration + + Public Recorded As Boolean + Public Results As New Collection ' " " -> "|" + + Private Function NextName(ByVal e As IEnumVARIANTOwn) As String + Dim arr As IEnumVARIANTOwn.CArrayOfVariants + Dim fetched As Long + e.Next 1, arr, fetched + If fetched = 0 Then Return "(end)" + Dim hd As WebView2Header = arr.Elements(0) + Return hd.Name + End Function + + ' Three fresh enumerators of one collection. + Public Sub Record(ByVal Side As String, ByVal u0 As stdole.IUnknown, ByVal u1 As stdole.IUnknown, ByVal u2 As stdole.IUnknown) + Dim e As IEnumVARIANTOwn, c As IEnumVARIANTOwn, first As String + + Set e = u0 + first = NextName(e) + Set c = e.Clone() + Results.Add NextName(e) & "|" & NextName(c), Side & " after one Next" + + Set e = u1 + NextName e + NextName e + e.Reset + Set c = e.Clone() + Results.Add NextName(e) & "|" & NextName(c), Side & " after Reset" + + Set e = u2 + e.Skip 2 + Set c = e.Clone() + Results.Add NextName(e) & "|" & NextName(c), Side & " after Skip 2" + End Sub + +End Module + +[TestFixture] +Module HeaderEnumerationTests + + Private Declare PtrSafe Function SetEnvironmentVariableW Lib "kernel32" (ByVal lpName As LongPtr, ByVal lpValue As LongPtr) As Long + + ' Shows Form1 until its request has been answered, once. + Private Sub Collect() + If HeaderEnumeration.Recorded Then Exit Sub + ' Only for an IDE that was started with these variables, as this repository's + ' test harness starts it: WebView2 would read them, and the control would fail. + Dim folder As String = Environ$("WEBVIEW2_USER_DATA_FOLDER") + Dim args As String = Environ$("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS") + SetEnvironmentVariableW StrPtr("WEBVIEW2_USER_DATA_FOLDER"), 0 + SetEnvironmentVariableW StrPtr("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS"), 0 + Form1.Show + Dim started As Double = Timer + Do Until HeaderEnumeration.Recorded Or Timer - started > 30 + DoEvents + Loop + Unload Form1 + If Len(folder) Then SetEnvironmentVariableW StrPtr("WEBVIEW2_USER_DATA_FOLDER"), StrPtr(folder) + If Len(args) Then SetEnvironmentVariableW StrPtr("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS"), StrPtr(args) + End Sub + + ' "|" + Private Function Pair(ByVal Key As String) As String + Collect + Assert.Strict.IsTrue HeaderEnumeration.Recorded, "the request was answered" + Return HeaderEnumeration.Results(Key) + End Function + + ' Fixed in #2496 (BETA 1005): the clone continues from the enumerator's position. + + [TestCase] + Public Sub Request_CloneAfterNext() + Assert.Strict.AreEqual "Upgrade-Insecure-Requests|Upgrade-Insecure-Requests", Pair("request after one Next") + End Sub + + [TestCase] + Public Sub Request_CloneAfterSkip() + Assert.Strict.AreEqual "User-Agent|User-Agent", Pair("request after Skip 2") + End Sub + + [TestCase] + Public Sub Request_CloneAfterReset() + Assert.Strict.AreEqual "Accept|Accept", Pair("request after Reset") + End Sub + + [TestCase] + Public Sub Response_CloneAfterNext() + Assert.Strict.AreEqual "Hb|Hb", Pair("response after one Next") + End Sub + + [TestCase] + Public Sub Response_CloneAfterSkip() + Assert.Strict.AreEqual "Hc|Hc", Pair("response after Skip 2") + End Sub + + ' Left in BETA 1005: Reset does not rewind the position Clone copies. + + [TestCase] + Public Sub Response_CloneAfterReset() + Assert.Strict.AreEqual "Ha|Ha", Pair("response after Reset") + End Sub + +End Module +``` +`Form1`'s handlers, which call `Record`: +```vb + Private Sub WebView21_Ready() Handles WebView21.Ready + WebView21.AddWebResourceRequestedFilter "*", wv2All + WebView21.Navigate "https://example.com/page" + End Sub + + Private Sub WebView21_WebResourceRequested(ByVal Request As WebView2Request, ByVal Response As WebView2Response) _ + Handles WebView21.WebResourceRequested + Response.Headers.AppendHeader "Ha", "1" + Response.Headers.AppendHeader "Hb", "2" + Response.Headers.AppendHeader "Hc", "3" + Response.Headers.AppendHeader "Hd", "4" + If Not HeaderEnumeration.Recorded Then + Dim rq As WebView2RequestHeaders = Request.Headers + HeaderEnumeration.Record "request", rq._NewEnum(), rq._NewEnum(), rq._NewEnum() + Dim rs As WebView2ResponseHeaders = Response.Headers + HeaderEnumeration.Record "response", rs._NewEnum(), rs._NewEnum(), rs._NewEnum() + HeaderEnumeration.Recorded = True + End If + Response.StatusCode = 200 + Response.ReasonPhrase = "OK" + Response.ContentUTF8 = "probe" + End Sub +``` + +**Expected behavior** +`Ha|Ha`: `IEnumVARIANT::Clone` creates an enumerator with the same state as the current one, so after a `Reset` the clone starts at the first header, as the request headers' clone does. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +In BETA 1005's package source, `WebView2HeadersCollection.twin`, `Next` and `Skip` advance `IterationIdx`, which `Clone` passes to the new enumerator. The two request headers classes set `IterationIdx = 0` in their `Reset`; the two response headers classes in `WebView2ResponseHeaders.twin`, `WebView2HeadersCollection_ResponseGetHeaders` and `WebView2HeadersCollection_ResponseEnumerator`, do not, so after a `Reset` their clone skips as many headers as were read before it. Setting `IterationIdx = 0` in those two `Reset`s, as the request side does, would fix it. Only `_NewEnum` (the `ResponseEnumerator` class) was run; `GetHeaders` (the other class) has the same `Reset`. + +Severity: low. `For Each` does not call `Clone`, so only a COM client that clones the enumerator after resetting it gets the wrong headers. + + + +--- + +## `import` and `export` exit with code 0 when they end `... FAILED` + +**Describe the bug** +Follow-up to #2493, which is fixed in BETA 1005 for the message: `import` of a folder with no `Settings` file now prints `ERROR: missing Settings file` before `... FAILED`. #2493's Expected behavior also said that a failure should not exit with 0, and that is left: `import` and `export` exit with code 0 after every failure they report, so a script that tests the exit code takes a failed run for a successful one. Observed with the compiler executable run from a script, its output and exit code captured. + +**To Reproduce** +Steps to reproduce the behavior: +1. Unzip `import-export-exit-zero.zip` (it holds `import-export-exit-zero.twinproj`, an ordinary console project), and export it once: `twinBASIC_win32.exe export C:\p\import-export-exit-zero.twinproj C:\p\src\`. +2. Run each of these, and look at the exit code (`echo %ERRORLEVEL%` in a Command Prompt): + ``` + twinBASIC_win32.exe import C:\p\x.twinproj C:\p\src\Sources\ + twinBASIC_win32.exe import C:\p\y.twinproj C:\p\no-such-folder\ + twinBASIC_win32.exe export C:\p\no-such-project.twinproj C:\p\out\ + twinBASIC_win32.exe export C:\p\import-export-exit-zero.twinproj C:\p\src\ + twinBASIC_win32.exe import C:\p\import-export-exit-zero.twinproj C:\p\src\ + ``` +3. See each end `... FAILED`, after `ERROR: missing Settings file`, `ERROR: input folder does not exist`, `ERROR: input twinproj file does not exist`, `[EXPORT] ERROR: output file already exists and --overwrite not set: ...Settings` and `ERROR: failed to create output file: ...import-export-exit-zero.twinproj (already exists and --overwrite not set)` in turn, and each exit with code 0. + +The script below is meant to be adapted as a regression test. A unit test cannot run the compiler's command line, so it is a Command Prompt script: each case runs one command and asserts the exit code, `PASS` for not 0. On BETA 1005 every case prints `FAIL`. It expects the export of step 1 in `%P%\src\` and the project in `%P%\`. +```bat +@echo off +rem Regression tests for #2493 and its follow-up: a run of import or export that +rem ends "... FAILED" exits with a code other than 0. Set TB to the compiler +rem executable and P to a folder holding the project and its export in src\. +call :expect_failure ImportNoSettings import "%P%\x.twinproj" "%P%\src\Sources\" +call :expect_failure ImportNoFolder import "%P%\y.twinproj" "%P%\no-such-folder\" +call :expect_failure ExportNoProject export "%P%\no-such-project.twinproj" "%P%\out\" +call :expect_failure ExportNoOverwrite export "%P%\import-export-exit-zero.twinproj" "%P%\src\" +call :expect_failure ImportNoOverwrite import "%P%\import-export-exit-zero.twinproj" "%P%\src\" +exit /b + +:expect_failure +set NAME=%1 +shift +"%TB%" %1 %2 %3 > nul 2>&1 < nul +if errorlevel 1 (echo PASS %NAME%) else (echo FAIL %NAME%: exit code 0) +exit /b +``` + +**Expected behavior** +A code other than 0 from every run that ends `... FAILED`, as the last line of #2493's Expected behavior asked, so that a script or a build step can tell a failure without reading the output. Each run keeps its `ERROR:` line, as BETA 1005 prints it. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +The same exit code on BETA 995 and 997 for each of these failures (there, the first prints no `ERROR:` line, which is what #2493 reported). A successful `export` and `import` end `... DONE` and exit 0, as they should. + +Severity: minor in an interactive session, where the `ERROR:` line is read; in a build script that checks the exit code, a project that was never written, or a tree that was never exported, passes for a success. + + + +--- + +## `? p` in the Debug Console, where `p` is a user-defined type, fails with a codegen error and a linker error + +**Describe the bug** +Follow-up to #2485, which is fixed in BETA 1005 for the watch: a watch on a variable of a user-defined type now shows `{user defined type, 8 bytes}`, of its type, at this stop and the next, with no linker error. This is the same expression typed in the Debug Console: `? p` still fails with `(compile error: codegen error; check for compilation errors)`, and the Debug Console prints `[LINKER] compilation (codegen) error detected in 'Startup.{temp_procedure}' at line #1`. Observed in the IDE, stopped at a breakpoint. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `console-print-udt-linker-error.twinproj` (attached as `console-print-udt-linker-error.zip`). `Startup.twin` declares a type, and `Main` passes the line marked `BREAK` twice: + ``` + Private Type Point + X As Long + Y As Long + End Type + + Public Sub Main() + Dim p As Point + Dim i As Long + p.X = 7 + For i = 1 To 2 + Debug.Print "pass " & i ' BREAK + Next + End Sub + ``` +2. Put a breakpoint on the line marked `BREAK` (F9) and press F5. The run stops there. +3. Type `? p` in the Debug Console and press Enter. +4. See `(compile error: codegen error; check for compilation errors)`, and `[LINKER] compilation (codegen) error detected in 'Startup.{temp_procedure}' at line #1` in the Debug Console. + +A unit test cannot reach the Debug Console, so the check below is a scripted one, meant to be adapted as a regression test; the documentation's IDE test harness runs it by driving the IDE (its `watches` scenario). At the stop of step 2, with the Debug Console's output marked: +- **Fixed in #2485 (BETA 1005), passes:** a watch on `p` shows the value `{user defined type, 8 bytes}` and the type `Point`; a watch on `p.X` shows `7` and `Long`; after F5 to the second stop, both watches show the same again; and no `[LINKER]` line has been printed since the mark. +- **Left in BETA 1005, fails:** after `? p` and Enter, the Debug Console holds no `[LINKER]` line, and no `codegen error`; it holds an error about the expression, such as that `p` has no single value to print. + +**Expected behavior** +An expression that cannot be shown, such as `? p`, which has no single value to print, is refused with a message about the expression, and no `[LINKER]` line, as the watch on the same expression now shows it without one. A linker error about generated code reads as though the project had failed to build, which it has not. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +Also on BETA 987, 995 and 997, where the watch on `p` failed in the same way. + +What does not reproduce it, on BETA 1005: a watch on `p`, a watch on `p.X`, and **Variables**, which shows `p` and its fields. Seen with a `Private Type` declared in the module; other declarations of the type were not tried. + +Severity: low. The console reports a failed build for a line that only asks to print a structure. + + + +--- + +## A `ParamArray` element that is a `Variant` variable holding an array cannot be indexed or assigned to an array: error -2147467259 + +**Describe the bug** +When the argument passed to a `ParamArray` is a `Variant` variable that holds an array, the procedure cannot use the element as an array. Assigning it to an array variable of any type, indexing it to read or to write an element, and passing it on to another `ParamArray` that indexes it raise run-time error -2147467259, *Unspecified error*; `Join` raises error 13. VB6 does all of these without an error. `LBound`, `UBound`, `IsArray`, `VarType` and `For Each` work on the same element. Observed in the reproducer project's regression tests and in a run of its `Sub Main`. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `paramarray-variant-array.twinproj` (attached as `paramarray-variant-array.zip`). Its module `Cases` holds 28 cases: each passes a small array to a `ParamArray` function in one way, does one thing with `parts(0)`, and returns what that gives, or `error `. Its module `ParamArrayTests` holds one `[TestCase]` per case, asserting what the case gives in VB6. +2. Run it (F5). `Sub Main` prints what each case gives; the same program built in VB6 (attached as `paramarray-variant-array-vb6.zip`) prints what VB6 gives: + + | the argument | what the function does with `parts(0)` | twinBASIC | VB6 | + |---|---|---|---| + | `b`, a `Byte()` variable | `a = parts(0)`, `a() As Byte` | `2 98` | `2 98` | + | `v`, a `Variant` variable holding a `Byte()` | `a = parts(0)`, `a() As Byte` | error -2147467259 | `2 98` | + | a `Variant` variable holding a `Long()`, `String()`, `Double()` or `Variant()` | `a = parts(0)`, `a()` of that type | error -2147467259 | `2 7`, `2 y`, `2 2.5`, `2 y` | + | `v` | `x = parts(0)(1)` | error -2147467259 | `98` | + | a `Variant` variable holding a two-dimensional `Long()` | `x = parts(0)(1, 2)` | error -2147467259 | `12` | + | a `Variant` variable holding `Array(1, 2, 3)` | `parts(0)(1) = 99` | error -2147467259, and the caller's element 1 stays 2 | no error, and the caller's element 1 is 99 | + | a `Variant` variable holding a `String()` | `Join(parts(0), "+")` | error 13 | `x+y+z` | + | `v` | passed on to another `ParamArray` function, which indexes its element | error -2147467259 | `98` | + | a `Variant` variable holding `Array(1, -2, 300, 20.3)` | the `Max` and `UnpackParamArray` of #1660 | error -2147467259 | `300` | + | a module-level `Variant`, an element of a `Variant()` array, a `Variant` field of a UDT, or a `ByVal` or `ByRef` `Variant` parameter passed on | `x = parts(0)(1)` | error -2147467259 | `98` | + | `v`, to a class's method that takes the `ParamArray`, called early- or late-bound | `x = parts(0)(1)` | error -2147467259 | `98` | + | `v` | `LBound` and `UBound`; `IsArray` and `VarType`; `For Each` | `0 to 2`; `True 8209`; the sum | the same | + | `v` | passed on to a `ByVal x As Variant` parameter, which indexes it | `98` | `98` | + | `v` | the whole `ParamArray` passed on to another, which reads `q(0)(0)(1)` | `98` | `98` | + | `v` | `x = parts(0)`, then `x(1)` | `98` | `98` | + | `(v)`, `CVar(b)`, or a function's `Variant` result | `x = parts(0)(1)` | `98` | `98` | + +**Expected behavior** +What VB6 gives, in every case. The element refers to the caller's `Variant`, which holds an array: assigning the element to an array variable copies that array, indexing it reads or writes the array's elements, and `Join` joins them. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +The same on BETA 997 and 957, and in a built exe. In an exe compiled with LLVM every case gives the same, but for the case of #1660, which ends the program with an access violation: its error is raised in `UnpackParamArray`, which has no error handler, and is not passed on to the handler of its caller, a separate defect of LLVM-compiled code. + +What does not reproduce it: an argument that is an array variable, or an expression such as `(v)`, `CVar(b)` or a function's result, which passes a copy rather than a reference to a `Variant`; the element read into a local `Variant` first, or passed to a `ByVal Variant` parameter; the whole `ParamArray` passed on to another; and outside a `ParamArray`, a `ByRef Variant` parameter (`a = v` works) or an element of a `Variant()` array assigned in the procedure the array is passed to. + +#1660 (open) is this defect reached another way: its unpacking routine is one of the cases, and fails as reported there. This entry narrows it to the element of a `ParamArray` that refers to a `Variant` variable, and shows what else fails with it. + +Severity: moderate. An error is always raised, so nothing goes wrong silently, but VB6 code that forwards a `Variant` array through a `ParamArray`, such as a `Max` or `Concat` helper or the unpacking routine of #1660, fails, and *Unspecified error* gives no hint why. Reading the element into a `Variant` first avoids it. + + + +--- + +## The IDE's `styles.css` labels a GIF and a JPEG `image/png`, and two PNGs `/png` + +**Describe the bug** +Four of the images that the IDE's `ide\styles.css` embeds as base64 `data:` URIs are labelled with a media type their bytes do not have: two PNGs are labelled `/png`, which is not a media type, and a GIF and a JPEG are labelled `image/png`. The other 171 images embedded in the `.css` files under `ide\` are labelled with the type they have. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `ide\styles.css`, in the folder of the twinBASIC installation, in a text editor. There is no project to attach: the defect is in the installation's own file. +2. Line 2947, in the rule of `.treeExpanded` and `.sectionExpanderIcon`, reads `background-image: url('data:/png;base64,iVBORw0KGgo...')`. Decode the base64: its first eight bytes are `89 50 4E 47 0D 0A 1A 0A`, the signature of a PNG. Line 2952, in the rule of `.treeExpanded1`, is labelled `/png` in the same way, and is also a PNG. +3. Line 3917, in the rule of `#progressSpinner` and `.progressSpinnerIcon`, reads `url('data:image/png;base64,R0lGODlh...')`. Decode the base64: it starts with `GIF89a`, the signature of a GIF. +4. Line 7412, in the rule of `.vipGold1_autoWeighingSystemsLogo`, reads `url('data:image/png;base64,/9j/4AAQ...')`. Decode the base64: it starts with `FF D8 FF E0`, the start of a JPEG. + +**Expected behavior** +Each `data:` URI names the type of its bytes, as the other 171 do: `image/png` for the two PNGs, `image/gif` for the GIF and `image/jpeg` for the JPEG. A media type is `type/subtype`, so `/png` is not one. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +BETA 997 and 957 have the same four images, at lines 2947, 2952, 3917 and 7389 in BETA 997, and at lines 2935, 2940, 3905 and 7323 in BETA 957. No other `.css` file under `ide\` has a mislabelled image, the Monaco editor's `editor.main.css` included. Images embedded in the IDE's `.js` and `.htm` files were not checked. + +Severity: low. Chromium identifies an image by its bytes, so all four pictures show: in headless Chrome 148, each of the four URIs loads as an `` at its own size, and as a `background-image` draws the same pixels as the URI with the correct label. The IDE's own WebView2 was not tested. + + + +--- + +## An error is not passed to the caller's `On Error` handler when the caller or the procedure that raised it is compiled with LLVM + +**Describe the bug** +An error raised in a procedure that has no error handler goes to the `On Error` handler of its caller. When either of the two procedures is compiled with LLVM, by `[CompilerOptions("+llvm")]` or with LLVM turned on for the whole project, the caller's handler does not run. Run in the IDE (F5), the program ends at the call without a message. In a built exe, a caller compiled with LLVM shows the unhandled-error box and ends; a callee compiled with LLVM shows a box titled `_TB_ERROR_HANDLER` that names the procedure but no error, and the program then ends with an access violation (0xC0000005 at address 0). With neither procedure compiled with LLVM, the caller's handler catches the error, in the IDE and in an exe, as it does in VB6. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `llvm-error-not-passed-to-caller.twinproj` (attached as `llvm-error-not-passed-to-caller.zip`). It needs an LLVM licence (Ultimate): its procedures are compiled with LLVM by their `[CompilerOptions("+llvm")]` attribute. Its module `Cases` holds 12 cases. Each calls a procedure that raises an error and has no error handler, from a procedure with `On Error GoTo` (cases 10 and 11: `On Error Resume Next`), and returns what the handler caught. The error is `Err.Raise 5000` or a division by zero. Case 4 is: + ``` + [CompilerOptions("+llvm")] + Private Sub ErrRaise_Llvm() + Err.Raise 5000, "Callee", "raised by Err.Raise" + End Sub + + Public Function CallerPlain_CalleeLlvm_ErrRaise() As String + On Error GoTo Handler + ErrRaise_Llvm + CallerPlain_CalleeLlvm_ErrRaise = "no error" + Exit Function + Handler: + CallerPlain_CalleeLlvm_ErrRaise = "caught " & Err.Number + End Function + ``` +2. Run the project in the IDE (F5). `Sub Main` runs the cases in order, and writes each case's name to the DEBUG CONSOLE, then what its handler caught. +3. See `caught 5000`, `caught 11` and `caught 11` for the first three cases. Then see the name `CallerPlain_CalleeLlvm_ErrRaise` and nothing after it: the run has ended without a message, and `end of Main` is not written. +4. Set `FirstCase` in the module `Startup` to 5, 6 and so on to run the cases after the first one that ends the run. Each of them ends the run in the same way. What each case does in the IDE, in an exe built from the project (which shows only its box and how it ends, since `Debug.Print` writes nothing in an exe), and in VB6 (the same procedures without the attributes, attached as `llvm-error-not-passed-to-caller-vb6.zip`, which writes each case's result to `out.txt`): + + | cases | the caller, which has the handler | the callee, which raises the error | twinBASIC, run in the IDE | twinBASIC, built exe | VB6 | + |---|---|---|---|---|---| + | 1, 2 | ordinary | ordinary | `caught 5000`, `caught 11` | the same | the same | + | 3 | LLVM, which also raises the error (a division by zero) | the same procedure | `caught 11` | `caught 11` | `caught 11` | + | 4, 7 | ordinary | LLVM | the run ends | the box `_TB_ERROR_HANDLER` (`unhandled error in Cases.ErrRaise_Llvm`; case 7: `Cases.Divide_Llvm`), then an access violation | `caught 5000`, `caught 11` | + | 5, 8 | LLVM | ordinary | the run ends | the unhandled-error box (`Run-time error '5000' raised by Err.Raise`; case 8: `Run-time error '11' Division by zero`), then the program ends | `caught 5000`, `caught 11` | + | 6, 9 | LLVM | LLVM | the run ends | as 4 and 7 | `caught 5000`, `caught 11` | + | 10 | ordinary, with `On Error Resume Next` | LLVM | the run ends | as 4 | `continued, Err.Number 5000` | + | 11 | LLVM, with `On Error Resume Next` | ordinary | the run ends | as 5 | `continued, Err.Number 5000` | + | 12 | ordinary, with an ordinary procedure between it and the callee | LLVM | the run ends | as 4 | `caught 5000` | + +**Expected behavior** +The error is passed to the caller's handler whichever of the two procedures is compiled with LLVM, as it is without LLVM and in VB6: an `On Error GoTo` handler runs with `Err.Number` set to 5000 (or 11), and under `On Error Resume Next` the caller continues with the statement after the call, with the same `Err.Number`. With no handler in any caller, the error is unhandled, and the box shows its number and description. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +On BETA 1005 every case of the table was run on its own, in the IDE and in a 32-bit exe. The run ends at case 4 in the IDE on BETA 997, 995 and 987 as well. A 64-bit exe of cases 4 and 5 on BETA 1005 does what the 32-bit one does, and so does a BETA 997 exe, but for the box of case 5, which is titled `shutdown` there. BETA 957 refuses the project's LLVM procedures ("a feature used in your code is not yet supported with the LLVM compiler"). + +What does not reproduce it: neither procedure compiled with LLVM (cases 1 and 2), and an LLVM-compiled procedure that handles an error its own statement causes (case 3). An error raised by `Err.Raise` and one caused by a division by zero behave the same; so do `On Error GoTo` and `On Error Resume Next`, and a callee reached through an ordinary procedure (case 12). With LLVM turned on for the whole project, every procedure is compiled with LLVM, so cases 1 and 2 end the run as well, in the IDE and in an exe; case 3 still works. + +Severity: high for a program compiled with LLVM. An error that the caller handles ends the program, and with a callee compiled with LLVM it ends with an access violation. In the IDE the run ends without a message, so the cause is hard to find. No wrong result is produced. Compiling both procedures without LLVM avoids it. + + diff --git a/WIP.A11y.md b/WIP.A11y.md index d2c708c6e..b8aae7312 100644 --- a/WIP.A11y.md +++ b/WIP.A11y.md @@ -10,7 +10,7 @@ which refuses a tree older than the sources that produced it, then [scripts/pick_a11y_sample.mjs --check](scripts/pick_a11y_sample.mjs) --- see [Choosing the sample](#choosing-the-sample) --- and then [scripts/check_a11y.mjs](scripts/check_a11y.mjs), which runs puppeteer + -axe-core over thirteen sample pages against WCAG 2.0/2.1/2.2 at Level A + AA +axe-core over fourteen sample pages against WCAG 2.0/2.1/2.2 at Level A + AA (plus the `heading-order` best-practice rule) and exits non-zero on any violation. It requires `build.bat` to have produced an up-to-date `_site/`. @@ -55,7 +55,7 @@ axe excludes `aria-hidden` subtrees from rule evaluation wholesale, so the icon **A closed `
` is invisible to the scan.** Its subtree is `notRendered`, so axe never walks it -- the same property that makes the section-links disclosure cheap is what hides it. A defect inside it (for example `target-size` on every link) would pass `check.bat`, because the state a reader sees after one click is never audited. -`STATE_AUDITS` in [scripts/lib/axe-scan.mjs](scripts/lib/axe-scan.mjs) closes that: entries layered onto the page x theme x viewport matrix that apply a DOM mutation from `PAGE_STATES` after navigation and before the audit. Two entries, each run across both themes and both viewports --- eight extra audits on top of the 52 the thirteen sample pages produce, for the 60 the scan reports. +`STATE_AUDITS` in [scripts/lib/axe-scan.mjs](scripts/lib/axe-scan.mjs) closes that: entries layered onto the page x theme x viewport matrix that apply a DOM mutation from `PAGE_STATES` after navigation and before the audit. Two entries, each run across both themes and both viewports --- eight extra audits on top of the 56 the fourteen sample pages produce, for the 64 the scan reports. Three things about it are essential: @@ -69,7 +69,7 @@ Syntax-highlight token colours are kept above 4.5:1 automatically: [builder/high ## Choosing the sample -The scan audits thirteen pages out of ~1,160, so its page list decides what it can report at all -- and a page list that stops being representative fails *silently*. A hand-picked list goes stale as the site grows around it: a sample with no table, image, `
` widget or video card leaves `image-alt`, the table rules and `scrollable-region-focusable` in the run options with nothing to run on, and the gate reports a clean pass. +The scan audits fourteen pages out of ~1,160, so its page list decides what it can report at all -- and a page list that stops being representative fails *silently*. A hand-picked list goes stale as the site grows around it: a sample with no table, image, `
` widget or video card leaves `image-alt`, the table rules and `scrollable-region-focusable` in the run options with nothing to run on, and the gate reports a clean pass. The full-site sweep ([scripts/sweep_a11y.mjs](scripts/sweep_a11y.mjs) — every page, both themes, both viewports, 3,476 audits, ~20 min) is the check on the sample. It shows which constructs the sample has to contain: table wrappers (`scrollable-region-focusable`), footnote back-links (`link-in-text-block`), the FAQ's `` elements at the phone viewport (`target-size`), heading structure (`heading-order`), diagrams (`role-img-alt`) and code-block colours (`color-contrast`). @@ -79,7 +79,7 @@ So the sample is derived rather than maintained by hand. [scripts/pick_a11y_samp - `--propose`: greedy set cover, seeded from the current `SAMPLE_PAGES`, so it prints what to *add*. `--fresh` ignores the current set and covers from scratch, which is how to ask whether the existing pages still earn their place. - `--census`: what each family is, how many pages use it, which page uses it most. -Cost is part of the choice: audit cost is super-linear in element count (k = 2.73), so the cheapest cover is not the smallest one, and `--propose` ranks candidates by measured per-page cost from the sweep's JSONL when one has been run. Across the full matrix, the thirteen-page sample costs 18.7 s of audit against 6.3 s for a small six-page sample -- 2.96x for a bit over twice the pages, because cost tracks element count rather than page count. `Pipeline-Stages.html` alone is 26 % of it, and earns that as the site's largest and most table-dense page. +Cost is part of the choice: audit cost is super-linear in element count (k = 2.73), so the cheapest cover is not the smallest one, and `--propose` ranks candidates by measured per-page cost from the sweep's JSONL when one has been run. Across the full matrix, a thirteen-page sample costs 18.7 s of audit against 6.3 s for a small six-page sample -- 2.96x for a bit over twice the pages, because cost tracks element count rather than page count. `Pipeline-Stages.html` alone is 26 % of it, and earns that as the site's largest and most table-dense page. **When the docs start using a construct they have not used before** — a new admonition shape, a figure, a widget — add a family for it in `FAMILIES` and let `--check` say whether the sample already covers it. Leaving it out is not neutral: it means the rule for that construct never runs anywhere. @@ -98,8 +98,8 @@ It has one blind spot: it compares a candidate against a baseline produced by th It also compares *which* findings axe produces, never their shape. The `no-html` scheme passes the gate on every audit and would still crash the reporter, because `noHtml: true` makes `node.html` null and `check_a11y.mjs` calls `.slice()` on it. Treat the gate as necessary, not sufficient. -Cost attribution for the scan lives in [perf/ab-axe.mjs](perf/ab-axe.mjs), with [perf/probe-axe-dom.mjs](perf/probe-axe-dom.mjs) for DOM operation counts and [perf/probe-axe-scaling.mjs](perf/probe-axe-scaling.mjs) for the size curve. The investigation, [builder/PLAN-axe-perf.md](builder/PLAN-axe-perf.md), is **complete**. Its conclusion: one vendored source patch, `SOURCE_PATCHES['plain-color-fields']` in `axe-scan.mjs` (see above), is worth **26 %** across a realistic page set and **30 %** on large pages. It is gated audit-by-audit by [check_a11y_fingerprint.mjs](scripts/check_a11y_fingerprint.mjs) and verified value-by-value by [check_axe_patch_equiv.mjs](scripts/check_axe_patch_equiv.mjs) -- both are needed, because the fingerprint gate compares `incomplete` as a rule-id set and would not notice a colour error that shifted ratios without flipping a pass/fail. The patch asserts an exact occurrence count at each substitution point, so an axe-core bump fails loudly rather than silently reverting. Before touching the scan: +Cost attribution for the scan lives in [perf/ab-axe.mjs](perf/ab-axe.mjs), with [perf/probe-axe-dom.mjs](perf/probe-axe-dom.mjs) for DOM operation counts and [perf/probe-axe-scaling.mjs](perf/probe-axe-scaling.mjs) for the size curve. The investigation, [builder/PLAN-axe-perf.md](builder/PLAN-axe-perf.md), is complete. Its conclusion: one vendored source patch, `SOURCE_PATCHES['plain-color-fields']` in `axe-scan.mjs` (see above), is worth **26 %** across a realistic page set and **30 %** on large pages. It is gated audit-by-audit by [check_a11y_fingerprint.mjs](scripts/check_a11y_fingerprint.mjs) and verified value-by-value by [check_axe_patch_equiv.mjs](scripts/check_axe_patch_equiv.mjs) -- both are needed, because the fingerprint gate compares `incomplete` as a rule-id set and would not notice a colour error that shifted ratios without flipping a pass/fail. The patch asserts an exact occurrence count at each substitution point, so an axe-core bump fails loudly rather than silently reverting. Before touching the scan: - **Audit cost is super-linear in page size** -- `k = 2.73` on real pages, almost entirely inside `color-contrast`. The four largest pages in the site cost ~5.9x an average sample page each. - **Every axe-core upgrade needs the fingerprint gate run across it** -- axe ships new and revised WCAG rules between minors, and the source patches are pinned to the bundle's current text. -- **`ab-axe.mjs` cannot resolve an effect under ~20 %** -- its tracer overhead plus GC timing swamps the signal, and it has mis-read a real ~10 % effect as noise twice (`plain-color-fields`, then `config-only`). Use [perf/ab-axe-pages.mjs](perf/ab-axe-pages.mjs) to decide anything modest, and run `--scheme production` as an A/A control before believing a small delta. `config-only` measures **-10.7 %** there against a **-0.4 %** A/A, and is declined anyway: `noHtml` and `selectors: false` remove the `html` and `target` fields that say which element failed. +- **`ab-axe.mjs` cannot resolve an effect under ~20 %** -- its tracer overhead plus GC timing swamps the signal, and it mis-reads a real ~10 % effect as noise. Use [perf/ab-axe-pages.mjs](perf/ab-axe-pages.mjs) to decide anything modest, and run `--scheme production` as an A/A control before believing a small delta. `config-only` measures **-10.7 %** there against a **-0.4 %** A/A, and is declined anyway: `noHtml` and `selectors: false` remove the `html` and `target` fields that say which element failed. diff --git a/WIP.Authoring.md b/WIP.Authoring.md index ba18cc303..e043bff6e 100644 --- a/WIP.Authoring.md +++ b/WIP.Authoring.md @@ -75,6 +75,7 @@ Formatting conventions: - Code blocks use ` ```tb ` (highlighted via Shiki using the vendored `builder/twinbasic.tmLanguage.json` grammar). - Parameter lists use the deflist `term` + `: definition` indentation pattern (NOT the MS-style markdown table). - Set `vba_attribution: true` in the frontmatter on any page derived from VBA-Docs; omit it on fully original content (e.g. VB package pages). The flag drives an extra line in the site footer. +- An image names the dark `X.png` only, never `X.light.png`, `X.svg` or `X.light.svg`: the build pairs the two themes and shows each theme's SVG where `shoot_docs --svg` wrote one. `png` in the attribute block (`{:width="190" height="337" png}`) keeps that picture's PNG in every output. The rules are in [Authoring, A picture in both themes](docs/Documentation/Authoring.md#a-picture-in-both-themes) and the section after it. ### Attribution policy @@ -110,10 +111,6 @@ The six packages carrying `indexed_from` are `AppGlobalClassObject`, `CustomCont commit as the content it adds. A stale value is worse than none: it asserts a completeness check was run against a build that no longer matches the package. -> **These keys are also in active use by package documentation work that has not landed -> yet**, covering packages beyond the six above. If you are about to document a package -> from scratch, ask first --- it may already be written and waiting to merge. - One other inert key is a different thing: `has_children` on four pages is a just-the-docs leftover. tbdocs derives the nav tree itself and never reads it. Nobody needs to maintain it. diff --git a/WIP.Build.md b/WIP.Build.md index ba32fab7c..7302c2543 100644 --- a/WIP.Build.md +++ b/WIP.Build.md @@ -91,7 +91,7 @@ yet designed; the plan holds measurements and open questions for them. ## Tooling policy -### Tooling is JavaScript, and the two remaining `.py` files each have a reason +### Tooling is JavaScript, and the three remaining `.py` files each have a reason Everything under `scripts/`, `builder/`, `lib/`, `book/`, `eval/` and `wisdom/` is Node.js. One trap: **a tool that rewrites a file must preserve its line endings @@ -100,7 +100,7 @@ universal-newline translation, rewriting any LF file it touches to CRLF on Windo --- a whole-file diff for a one-character fix. Some of the tree's markdown files are LF, so the trap is live. -Two `.py` files stay, and neither is an oversight: +Three `.py` files stay, and none is an oversight: - **`scripts/impexp.py`** is not tooling. It is a published download, declared in `_config.yml`'s `bundle_extra` beside `impexp.mjs` and offered to readers on @@ -110,6 +110,14 @@ Two `.py` files stay, and neither is an oversight: use produces wrong CFF2 metrics --- a one-line build-configuration defect in harfbuzzjs, documented with the evidence in [WIP.Fonts.md](WIP.Fonts.md), which also holds the full account of the blocked JavaScript port. +- **`scripts/subset_font.py`** cuts each font an SVG screenshot embeds down to the + characters it draws (`shoot_docs --svg`, [WIP.Screenshots.md](WIP.Screenshots.md)). It is + fontTools, the subsetter `build_fonts.py` already relies on. harfbuzzjs was tried first and + removed (owner: "don't mess with harfbuzz"); it is the library whose CFF2 defect keeps + `build_fonts.py` in Python. It loads a font without recalculating glyph bounds, because the + IDE's icon font stores loose glyph boxes and recalculating them moved icons a pixel, and it + keeps the hinting (owner's decision) and the font's timestamp, so a picture's SVG is the same + bytes from one run to the next. One `.ps1` exists for a third kind of reason. **`scripts/lib/tb-launch.ps1`** is Win32 calls --- `CreateDesktop`, `CreateProcess` with `STARTUPINFO.lpDesktop`, and the job object @@ -120,6 +128,58 @@ without a native FFI addon. Adding one for a few calls would mean execution policy. See [Compiling a twinBASIC project without the IDE in front of you](WIP.Harness.md#compiling-a-twinbasic-project-without-the-ide-in-front-of-you). +One folder holds a program written in twinBASIC itself, for a fourth reason. +**`scripts/imagestrip/`** strips the metadata from an SVG file and from the images a CSS or +SVG file embeds as `data:` URIs, at any depth, for the IDE's own stylesheets. Its users are +the twinBASIC developers, who run it in their own build, which has twinBASIC and need not +have Node, so a Node edition of the program would serve nobody. Nothing in this repository +runs it but the half of `test/compact-image.test.mjs` that is skipped unless `IMAGESTRIP_EXE` +names its exe, because no gate can build it. It is laid out as a reproducer under `bugs/` +is, an exported tree in `src/` with `imagestrip.twinproj` packed from it beside it, and +both are checked out byte for byte so that the two agree; nothing checks that they do, so +a change to `src/` is packed again by hand. See [Tools and +Scripts](docs/Documentation/Tools-IDE.md#imagestrip). + +**Its own unit tests are twinBASIC `[TestFixture]` modules in `src/Sources/Tests/`**, with +the images they build in `TestImages`, run by `tbrun --tests` (owner, 2026-10-10), which reads +every `.twin` under `Sources/`. `TestImages.Cat` copies each `ParamArray` element through a +`Variant` before assigning it to an array, around a twinBASIC defect queued in +BUGS-TO-REPORT.md. + +**Its reference is `stripFile` in `scripts/lib/compact-image.mjs`.** The test runs the program +over a corpus of CSS and SVG files and the newest install's stylesheets, and compares its +output, summary line and reports with `stripFile`'s byte for byte; the negative control (a +build without one rule) fails it. The two are kept in step by hand: a change to what one +strips is a change to both. `compactImage` in the same module is what the SVG screenshots +(`scripts/svgshot/snapshot-svg.mjs`, [Pictures as SVG](#pictures-as-svg)) embed their images +through, so a change to what it strips reaches their SVGs on the next `shoot_docs --svg` run. +What both strip was decided by the owner, 2026-10-10: + +- **An embedded image keeps its pixels and any notice of whose it is, and nothing else.** + The IDE's icons carry GIMP's EXIF, IPTC and XMP profiles (12 KB around a 28-pixel icon), + "Created with GIMP", Paint.NET's and ImageReady's names, CorelDRAW's and Illustrator's + comments and metadata, "Optimized with ezgif" in a GIF; its lock icon, from fileformat.info, + carries a CC BY-NC-SA attribution (Title, Author, Description with the source, Copyright) + that must stay with it. So a PNG keeps its image and colour chunks and the text chunks that + name a picture, its author, source or terms; its colour chunks go when they say only sRGB + (GIMP's built-in profile, 672 bytes in every PNG it saves, known by its colourants and + curves, with `gAMA` and `cHRM`). A JPEG keeps its coding segments, JFIF, ICC and Adobe's; a + GIF all but its comments; an SVG all but its declaration, an empty DOCTYPE, comments and + metadata; and any profile, comment or metadata that gives a value to an author or a rights + field stays whole, as does an EXIF that turns the picture. +- **An image is named by its bytes.** The IDE labels a JPEG and a GIF `image/png`, and two + PNGs `/png` (BETA 1005). +- **Recursive, so that CSS and SVG are handled alike.** An SVG image is stripped as an SVG + file is, images inside it included; one of the IDE's Illustrator icons holds a PNG with a + 2.6 KB ICC profile in base64 broken by CR LF, which is why base64 is read as a browser + reads it. + +Two differences between the program and `compactImage` are deliberate and are `stripFile`'s +too: a BMP is only relabelled, not converted to a PNG (that is compaction, and would need a +PNG encoder in twinBASIC), and an image keeps its encoding --- base64 rewritten only when its +image changed, a text SVG cut in place, escapes and all --- where `compactImage` writes every +SVG as percent-encoded text. + Two `.mjs` files also run a little PowerShell inline, for Windows state Node has no API for, and neither adds a file: `scripts/tbrun.mjs` takes a process snapshot with `Get-Process`, and `scripts/lib/tb-registry.mjs` reads and restores the IDE's registry @@ -726,7 +786,7 @@ The decisions, each with what it rules out: `scripts/` (`biome.jsonc`), because `check_tree_fresh.mjs` would not see a change to what the build imports. `impexp.mjs` is the one exception, by name in the rule: it imports nothing, the build already publishes it through `bundle_extra`, and - `check_tree_fresh.mjs` now watches it and `add-in/`. Not the compiler executable, + `check_tree_fresh.mjs` watches it and `add-in/`. Not the compiler executable, which CI does not have, and not `impexp.py`, which CI's Node job need not have. - **`--serve` writes it, `--dry-run` does not.** The preview has the page that links to it, and packing takes about 100 ms; the help archive is skipped under `--serve` because @@ -743,8 +803,8 @@ The decisions, each with what it rules out: ### Build-time counts as named values `{{tbdocs:pages}}` in a page renders as the number of pages the build -discovered. Designed in [builder/PLAN-counts.md](builder/PLAN-counts.md), -implemented in [builder/counts.mjs](builder/counts.mjs), documented for +discovered. Design: [builder/PLAN-counts.md](builder/PLAN-counts.md); +code: [builder/counts.mjs](builder/counts.mjs); documented for contributors at [Authoring Pages](docs/Documentation/Authoring.md#counts-the-build-fills-in). Twelve names are live, and most of the prose is still hand-written. @@ -1000,3 +1060,92 @@ are what say it does not. **CI never downloads.** `process.env.CI` selects offline mode (`--fetch-assets` / `--no-fetch-assets` override it), and in offline mode a referenced-but-uncommitted asset throws rather than fetching. If CI could fetch, an author who wrote the markdown but forgot to commit the image would get a green build while the published site went on hotlinking a third party -- the exact failure the whole mechanism exists to prevent. A fetch *failure* in dev mode is softer: warn, keep building, and flip the exit code, so one dead video doesn't block a local preview. The render-side halves are `videoLinkPlugin` (marked link -> poster frame + outbound link) and `remoteImagePlugin` (user-attachment `` -> the vendored copy), both in [builder/render.mjs](builder/render.mjs). Both emit **root-absolute** paths, because the PDF book flattens every page into one document and a page-relative src resolves against the book root there. + +### Pictures in two themes + +**The rule:** `X.png` is the dark picture, `X.light.png` beside it the light one, and a page +names only `X.png`. `themePairPlugin` ([builder/render.mjs](builder/render.mjs)) writes an +image whose file has a light sibling as two ``, light then dark, with the same alt and +attributes (the light one's width and height scaled, below), classes `pic-light` / `pic-dark` and both `loading="lazy"`; a link to such a picture +becomes two links the same way (the feature map's "Full size" opens the picture the reader +sees). The online and offline trees, and so the help archive, carry both; the book carries the +light one alone, a plain `` (`lightOnly` in +[builder/theme-pictures.mjs](builder/theme-pictures.mjs), step 0 of `bookChapterTransform`). +`scripts/shoot_docs.mjs` writes the pairs ([WIP.Screenshots.md](WIP.Screenshots.md)). Why: + +- **Not `` with a media query.** It follows the OS only, never the toggle. +- **Lazy, because a hidden lazy image is never fetched.** Checked with puppeteer in all four modes (light and + dark stored, system with each OS scheme): only the shown theme's images are requested, and + toggling requests the other theme's images in view, and nothing else. The book's must not be + lazy: paged.js raises on an image that has not loaded. **Anything that waits for a page's + images must skip the hidden one**, whose `decode()` never settles: `gotoPage` in + `scripts/lib/axe-scan.mjs` waits only for images that are laid out, made eager (else + `check.bat` hangs until the protocol timeout). +- **CSS, no script.** The head script sets `data-theme` before first paint, so there is no + flash. `custom.scss` hides `.pic-light` under `dark-theme` (both of its selectors, so both + dark modes) and `.pic-dark` under `light-theme`, its exact complement in + `docs/_sass/custom/_theme.scss`. No rule gives the shown image a `display` of its own. +- **The sibling comes from the static-file list**, the set `relativeLinksPlugin` already + resolves against, never from the disk: a worker has the list and no business reading the tree. +- **A page that names a `.light.png` fails the build** (markdown image or link, or raw HTML + `src`/`href`): it would show the light picture in both themes. **A `.light.png` with no + `X.png` beside it fails `discover`**: it would never be shown. Nothing else needs to know: a + `.light.png` is an ordinary static file to the publish allowlist, the link check and the index + audit, and each one adds one to the page baseline's static-file count. +- **Each is shown at its own size, at one scale.** The page's `{:width height}` is the dark + picture's; the light `` gets it scaled by the two files' pixel sizes, which `discover` + reads from their PNG headers on the main thread (`pairSizes`) and the workers receive with the + shared render data. The Light theme lays the IDE out a few pixels differently (the toolbar + 4 CSS px shorter, a panel's title bar 3, some buttons narrower), so a light screenshot is up to + a few pixels off the dark one's size. Forcing the dark size on it instead either pulls + neighbouring UI into the frame (a line of the blue title bar over every light toolbar) or + crops the subject. + +### Pictures as SVG + +**The rule:** `X.svg` beside `X.png`, and `X.light.svg` beside `X.light.png`, is the picture +drawn as SVG by `shoot_docs --svg` ([WIP.Screenshots.md](WIP.Screenshots.md#pictures-as-svg)). +A page still names `X.png`. `themePairPlugin` names each theme's SVG in its place when that is +among the static files, an image and a link alike, unless the attribute block holds `png`, +which it takes out of the output. `picture_svg` in `_config.yml` says which outputs show the +SVGs: online and book do, offline (and so the help archive) does not. Why: + +- **The page names the PNG.** The PNG stays as the picture the SVG was checked against, and as + the picture of an output that shows PNGs; and `shoot_docs` does not write an SVG that falls + short of its PNG, so a page cannot count on one existing. +- **The renderer always writes the SVG, and an output that shows PNGs takes it back after.** + `pngPictures` ([builder/theme-pictures.mjs](builder/theme-pictures.mjs)) names each picture's + SVG back as its PNG in the rendered HTML: a `src` or `href` naming an `.svg` with its `.png` + beside it can only be one the renderer swapped, since a diagram's SVG has no PNG; it rewrites + outside code only (`replaceOutsideCode`). `cpu-worker.mjs` makes the offline page from the + SVG HTML and swaps it when offline shows PNGs, and the online page the same way; + `book.mjs` swaps before `pdf.mjs` collects the book's images. One rendering, three outputs. +- **An offline tree that shows PNGs holds no picture SVG.** `tbdocs.mjs` adds each one's path + to `offline_exclude` in memory, so the offline copy, the help archive, the offline link check + and the index audit agree without knowing about pictures. That adds a few hundred literal + patterns, so `offlineExcluded` (and its copy in `check-tree.mjs`) compares a pattern with no + wildcard as a string instead of compiling it. The archive is a stored zip, 37 MB without + the SVGs, and the 310 of them (2026-10-10) would add 18 MB. +- **A picture's SVG is never inlined.** `figurePlugin` inlines a diagram's SVG in its figure; a + picture's (`isPictureSvg`: an `.svg` with its `.png` beside it) stays an `` in the + figure's container, and `tbdocs.mjs` does not put it in the `svgContentsMap` the workers + receive. +- **What becomes a figure.** An image that is its paragraph's only content (or the two images of + a light and dark pair), of any kind: a diagram, a picture, a raster. `figurePlugin`'s `figures` + core rule decides it on the token stream, so an image with text beside it or inside a link + stays a plain ``. The bar's groups (vector, raster, zoom) are chosen when `figure_open` + renders, from the image's `src` as `remoteImagePlugin` will write it. +- **`data-tall` is decided at build time, so nothing moves after load.** The wrapper gets it when + the figure at the 736 px content column is at least as tall as its bar stood on end (220 px + with both groups, 126 px with the raster group only); the size is the page's `{:width height}` + for a picture and the root ``'s width and height (pt) for a diagram, and a picture with + no stated size counts as short. Measuring in the browser instead would reflow the page after + the first paint, and the sticky bar needs to know its side before layout. +- **On `file://` a picture's bar keeps only Zoom.** The browser can neither fetch a `file://` + URL to put on the clipboard nor honour `download` on a `file://` link, so `figure.js` hides + a picture's download and copy groups there; the offline tree shows pictures as PNG, so its + vector group is hidden anyway. A diagram's actions serialise the SVG already in the page and + work offline. +- **No `` with a PNG fallback** (owner): every browser the site supports shows an SVG + in an ``. The fallback a page needs is the per-picture `png`. +- **A page that names `X.light.svg` fails the build**, as one that names `X.light.png` does. diff --git a/WIP.ExamplesBuild.md b/WIP.ExamplesBuild.md index fec20fa10..42428cbf0 100644 --- a/WIP.ExamplesBuild.md +++ b/WIP.ExamplesBuild.md @@ -7,7 +7,7 @@ this sample actually build*. [scripts/lib/tb-fences.mjs](scripts/lib/tb-fences.mjs) holds the half that needs no compiler, and [test/example-projects/](test/example-projects/) holds the template trees. Reader-facing documentation is the [`check_examples.mjs` -entry](docs/Documentation/Tools.md) in Tools.md and [Checking that a sample +entry](docs/Documentation/Tools-Samples.md#check-examples) in Tools-Samples.md and [Checking that a sample compiles](docs/Documentation/Authoring.md) in Authoring.md. ## State @@ -375,9 +375,8 @@ form with a `TextBox` on it, and impossible to compile alone, because the design than the code declares `Text1`. Declaring those instances lets the compiler check what the sample asserts: that the member exists, that it takes those arguments, that the types line up. -Measured on the 263 `Reference/Built-In` fences: **99% fail against a bare template, 78% -with every package referenced, 67% with the stage set**. Per package, CEF goes 85% → 15%, -WebView2 73% → 27%, WinNativeCommonCtls 100% → 45%. +Against a bare template 99% of the 263 `Reference/Built-In` fences fail; with every +package referenced 78%; with the stage set 67%. **The list is written out rather than inferred, and that is the point.** A rule over "an identifier ending in a digit" would also declare `Var1`, `Arg1`, `Line2`, `SQLITE3`, `VBA7`, @@ -386,8 +385,8 @@ deliberate fiction, so it stays explicit and greppable. **Stage entries are the biggest lever.** A flat tail of undeclared *names* is not a flat tail of *work*: 45 undeclared VB control instances (`lblName`, `mnuFileSaveAs`, `optPlain`, -`dlgOptions`, `fraLeft`), each on one or two pages, bought 22 samples in `Default/VB` for -one file edit. +`dlgOptions`, `fraLeft`), each on one or two pages, cost one file edit and fixed 22 samples +in `Default/VB`. ## Batching, which is the whole cost question @@ -396,8 +395,7 @@ project size**: what is paid for is startup, not compilation. **Fill the lanes, not the batches.** Filling each batch to `--batch` before opening another put 120, 55, 4 and 3 samples on four lanes, and a run takes as long as its biggest batch. The -batcher sizes to `ceil(total / jobs)` instead. On the VBA reference that was 28 s → 10.7 s -for the same result. +batcher sizes to `ceil(total / jobs)` instead. ### A sample can pass on its neighbour's declarations @@ -453,8 +451,8 @@ All are forced by putting unrelated samples in one compilation unit: `project.name = "ProbeWS"`, a `Module ProbeWS` or a `Module probews` stops the `[RunAfterBuild]` Sub from running, even when it is empty and the Sub is in another module. Nothing reports it: the build is green, and the DEBUG CONSOLE ends at `[BUILD] Executing - '..'...` (BETA 995 and 997; the BUGS-TO-REPORT.md entry with the - reproducer `bugs/module-named-like-project/`). The tool names each project `DocSamples` + '..'...` (BETA 995 and 997, twinbasic/twinbasic#2494). BETA 1005 runs + the Sub, so the rule matters only to an older build. The tool names each project `DocSamples` and every module it generates `tbx...`, so the two cannot meet; a sample that declared `Module DocSamples3` would. @@ -589,10 +587,6 @@ the Form page's `True`). crashes the compiler returns no rows at all, canary included; `runBatch` hands a crash to `isolateCrash` before it reads the canary, so that case never reaches it. - Ten failing samples across the console and `[implicit]` templates (among them an - unterminated `Sub`, garbage after a stray `End Sub` and an `If` with no `End If`) were each - reported with their own errors, with no canary event and no split, in about 13 s. - **The canary is required only by a read with no errors in it.** It proves that the IDE published something, never that it published everything: a read that includes it and misses a later file's diagnostics would still pass that file. So a read holding real @@ -630,12 +624,10 @@ the Form page's `True`). them, and when neither does, each half is searched with the other held. The members are blamed --- each has its own result, and none is a pass --- and one finding names them all. **No real crash of that shape is known**, so its tests are probes against a fake lane - whose builds crash on the sample sets a probe chooses. Four two-file candidates were - tried for a real one: inheritance cycles through interfaces, classes and UDTs, and the - fixture's placeholder name with a base declared in the other file. None crashes only as a - pair: the class and UDT cycles are diagnosed, the interface cycle is accepted without a - word, which is queued in [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md), and the placeholder - crashes from its own file. + whose builds crash on the sample sets a probe chooses. Inheritance cycles and the + fixture's placeholder name are not such a crash: class and UDT cycles are diagnosed, the + interface cycle is accepted without a word (queued in + [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md)), and the placeholder crashes from its own file. ### A diagnostic that lands in a package's own source @@ -663,8 +655,6 @@ Three parts to the handling, each forced: plain halving takes a group apart and drops one half's context, manufacturing the failure it then reports. The crash bisect has the same rule. -Cost on this corpus: one blame in 1,103 samples, three extra builds. - **`--only` cuts a `projname` group.** Narrowing to `WinServicesLib/ServiceCreator` leaves the page that declares `MyService` out of the run while keeping three pages that instantiate `ServiceCreator(Of MyService)`, so the isolation blames a sample the full gate passes. @@ -723,8 +713,7 @@ that fails only the second is one LLVM cannot generate code for. - **A unit declaring its own `Sub Main` gets a project of its own, without `tbxMain`.** Two Mains compile, but binding the startup object fails the build ("'Main' is ambiguous"), as `tbxMain.twin`'s header says. `makeBatches`' `alone` picks the unit, - the batch carries `noMain`, and `stageBatch` leaves out `tbxMain.twin`. (HelpFile, - PrevInstance, Project-Types and the two WinServicesLib groups are such units.) + the batch carries `noMain`, and `stageBatch` leaves out `tbxMain.twin`. - **A `[DllExport]` name counts among a sample's names.** Two samples exporting `MyExportedFunction` (API-Declarations, Classes-and-Modules) compile together, and the linker refuses them ("duplicate [DLLExport] functions detected"); as names, the batcher @@ -751,8 +740,7 @@ that fails only the second is one LLVM cannot generate code for. changing" as suspect on this compiler.** - **The classifier refuses a `` as a declaration name**, so a syntax skeleton such as `Interface Extends ` / `End Interface` (in - `Reference/Attributes.md`) takes an explicit `slot=` to reach the compiler. Through BETA - 995 that skeleton crashes the compiler (twinbasic/twinbasic#2453); BETA 997 does not. + `Reference/Attributes.md`) takes an explicit `slot=` to reach the compiler. - **`project.buildPath` must be an explicit file.** The default `${SourcePath}\Build\...` template opens a native Save dialog, which on the private desktop is invisible and unreachable, so the build silently never happens --- and the WebView2 renderer stays @@ -796,8 +784,6 @@ that fails only the second is one LLVM cannot generate code for. ## Rules from the survey passes -What a maintainer needs from the survey work follows. - **Count diagnostics, then try the fix.** A guess from counting diagnostics is not a result. "`[Me]` wants a fourth slot" (27 samples, TB5025): a bare `Class` wrapper fixes **5**, because `Me` becoming legal does not make `Me.Caption` resolve: the @@ -871,7 +857,7 @@ Decide shown versus hidden by one question: *would a reader of the page need to `Toolbar1`), stand-ins for an external library's declarations (`ChooseColorFlags`), and the class boilerplate around a method the page is about. -Language rules this work found: +Language rules: - **`Dim x As New Foo(args)` is not valid twinBASIC.** Constructor arguments need the expression form, `Set x = New Foo(args)`. `twinBASIC-Additions.md`'s **Parameterised New** @@ -930,12 +916,11 @@ a reader writes. - A `projname` is global, so two pages choosing `demo` would merge without saying so. Scoping it to the page would prevent that and would also prevent a group spanning pages, which a multi-page tutorial wants. Left global and documented; revisit if a collision happens. -- **Should the remaining single-page stage entries move into `hidden` fences?** About a - dozen control instances are used by exactly one page --- `picCanvas`, `hsbVolume`, - `mfPanels`, `lblCoords`. They are one line each in a list whose stated purpose is - "the control instances the samples assume", so they stay. The invented *classes* go in - `hidden` fences, because a class is not a control instance and a page is where it belongs. The line between - the two is a judgement, not a rule. +- **Single-page stage entries.** About a dozen control instances are used by exactly one + page --- `picCanvas`, `hsbVolume`, `mfPanels`, `lblCoords`. They stay in the stage set, + one line each, because its purpose is "the control instances the samples assume". The + invented *classes* go in `hidden` fences, because a class is not a control instance and a + page is where it belongs. The line between the two is a judgement, not a rule. - Which pages should carry `check_run` (33 samples do)? A sample states its output in its own text, as a trailing comment on a `Debug.Print` line or as the comment lines under `' Output:`; its comparison is only as useful as the pages that state a printed value. diff --git a/WIP.Fonts.md b/WIP.Fonts.md index ce8e06f99..be97efef4 100644 --- a/WIP.Fonts.md +++ b/WIP.Fonts.md @@ -122,10 +122,8 @@ success, and why the same missing function also leaves `head`'s bbox at its sent Why upstream does not catch it: HarfBuzz *has* a regression test for exactly this (`instantiate_cff2_update_metrics.tests`, PR #4189), but its CFF2 goldens are copied from hb-subset's own output (`no_fonttools`), and the suite never runs an `HB_NO_DRAW` build. -harfbuzzjs's tests do not exercise pinning at all. harfbuzzjs 1.2.1--1.4.0 had no CFF2 -subsetting at all; 1.5.0 onward has it, silently wrong. (HarfBuzz PR #6009 added the guard -and a `#define HB_NO_CFF` that made the breakage loud; commit `38976fba` removed the loud -half and kept the guard.) +harfbuzzjs's tests do not exercise pinning at all. harfbuzzjs has subset CFF2 since 1.5.0, +silently wrong. **The fix is one line in harfbuzzjs**: `#undef HB_NO_DRAW` in `config-override-subset.h` (or restore `-DHB_CONFIG_OVERRIDE_LAST_H`, which is what 1.2.0 used and why it was correct). @@ -135,9 +133,7 @@ invalid `head` --- but the actionable report is harfbuzzjs's. **Not yet reported upstream (last checked 2026-09-21).** A full sweep of both trackers (every open issue and PR, closed-issue term searches, Discussions, and the `subset-font` / -`subfont` / `cn-font-split` wrappers) found nothing. The closest prior art is a review -comment on harfbuzz PR #6009 that named `config-override-subset.h` and the missing -`#undef HB_NO_DRAW`, for the loud predecessor. Two look-alikes are different bugs: #6068 +`subfont` / `cn-font-split` wrappers) found nothing. Two look-alikes are different bugs: #6068 (HVAR advances frozen on partial instancing; fixed) and the open #4029 (glyf `hhea` bounds not recomputed when the metrics map is empty --- the same shape, a different table). Both repos' `main` still carried the defect when checked. diff --git a/WIP.Harness.md b/WIP.Harness.md index 2b5c4b977..63036f2bf 100644 --- a/WIP.Harness.md +++ b/WIP.Harness.md @@ -171,7 +171,7 @@ short: an entry written from the two targets someone tried, a Sub and a Const, s name (the page's, the compiler's token table's, `--names`) at each of about 60 sites in [scripts/lib/attribute-sites.mjs](scripts/lib/attribute-sites.mjs), in each argument shape --- and lays the answers against the page. Its reader-facing description is -[Tools and Scripts](docs/Documentation/Tools.md#sweep-attributes); this is why it is built as +[Tools and Scripts](docs/Documentation/Tools-Samples.md#sweep-attributes); this is why it is built as it is. **Against BETA 987: 34,526 probes in 134 builds, 8 to 16 minutes on four lanes.** That is @@ -217,9 +217,7 @@ other 193 are refused at every site. run cut short. **It finds compiler crashes the documentation never would:** a crash in a batch is -isolated by halving down to one probe beside the canaries, which is how a bare -`[PopulateFrom]` on an Enum is found to kill the compiler (twinbasic/twinbasic#2450; BETA 997 -does not crash). +isolated by halving down to one probe beside the canaries. **Read the report's "accepted at most sites" section before believing an acceptance.** `[Description]` is taken at 53 of 61 sites and `[Hidden]` and `[Restricted]` at 42: either @@ -328,8 +326,8 @@ Running in one process gives four rules: a cleanup first; `sweep_attributes` uses it to write the report of what it had learned (`salvage`), and `tb-cdp` drops a frame that is not JSON instead of throwing from an event callback. -- **The name.** tb-ide already exports a `buildProject(c)` that builds an exe through an open - connection, and the notes below mean that one; the new function is `compileProject`. +- **The name.** tb-ide exports a `buildProject(c)` that builds an exe through an open + connection, and the notes below mean that one; `compileProject` is the compile-only function. Two further rules: @@ -521,7 +519,7 @@ Build, then reads the DEBUG CONSOLE back over CDP. The staging is [scripts/lib/tb-project.mjs](scripts/lib/tb-project.mjs), which the add-in harness shares. The probe is a module with a `[RunAfterBuild]` Sub, which the IDE runs once the exe is linked. Reader-facing documentation is the [`tbrun.mjs` entry in -Tools.md](docs/Documentation/Tools.md). +Tools-Compiler.md](docs/Documentation/Tools-Compiler.md#tbrun). **The script owns the tree because of a silent trap.** A project whose `project.buildPath` is still the default `${SourcePath}\Build\...` template opens a native *Save* dialog when @@ -643,6 +641,48 @@ on an image allowlist, *and* windowless --- a new one that has a window is repor alone, since that cannot be told from a copy the user opened. `--no-reap` turns it off, and concurrent runs driving the same server should use it and sweep once at the end. +### Running a project's test cases + +twinBASIC's own unit-test form is a Module marked `[TestFixture]` whose parameterless Subs +are marked `[TestCase]` and check with the Assert package. **BETA 1005 has no way for a +person to run one.** A `[TestCase]` Sub gets no CodeLens "run" link (an unmarked Sub in the +same module gets one), the IDE's handler for the compiler's `custom/provideTests` request is +an empty function in `main.js`, and the compiler never sent it in the runs here. Called from +`Sub Main` or a `[RunAfterBuild]` Sub, a failed assertion raises -353703420 (`&HEAEAEA04`, +*Assertion FAILED*) and stops the run at the error panel; `On Error` does not catch it, so a +probe cannot count failures itself. (Measured with `tbrun`: the run ends at the first failed +assertion, exit 5.) + +**The compiler has a test mode, and `tbrun --tests` uses it.** The CodeLens "run" link sends +the debugger socket `evaluate` with the procedure's name as the expression +(`twinbasic.runCode` in `main.js`). The compiler also reads `isTest` in that request (its +strings list `expression`, `context`, `isTest`, `isStartupExecution`), and with `isTest: true` +it runs the procedure as a test: the response is `success` with no `body.error` for a pass, +and for a failure `body.error` with `body.lineNumber`, the case ends there and nothing stops: + +| the case | `body.error` | +|---|---| +| a failed `AreEqual` | `ASSERTION FAILED: the two values are not equal. \r\n Expected: 3 [Integer]\r\n Actual: 2 [Integer]. ` | +| `Inconclusive "x"` | `ASSERTION FAILED: [INCONCLUSIVE] x` | +| an unhandled `Err.Raise 5` | `(runtime error 5: Invalid procedure call or argument)` | +| an access violation | `(runtime error -2147467259: NATIVE EXCEPTION: ACCESS_VIOLATION)`, line -1 | +| a failed assertion under `On Error Resume Next` | the assertion's message: `On Error` does not hide it | + +A case that fails at its first assertion runs no further. The next case runs normally, even +after an access violation. `Debug.Print` in a case writes to the DEBUG CONSOLE. A name that is +not there is `success: false` with a compile-error message. An unmarked Sub runs the same way, +so the attributes matter only to the runner that lists the cases: `scripts/lib/tb-tests.mjs` +finds them in the sources (`[TestFixture]` before `Module`, `[TestCase]` before a +parameterless `Sub`) and refuses one it cannot call. All measured on BETA 1005 over CDP. + +**`Assert.Exact` compares the type too**: `Assert.Exact.AreEqual 5, Err.Number` fails, 5 is +an `Integer` and `Err.Number` a `Long`. The tests here use `Assert.Strict`. + +**A compile-time defect is a compile case**: a line ending `' CASE : `, +`: error` or `: none`, judged against the diagnostics of the compile (`judgeCompiles`). `error` +is for a defect whose fix may choose its own diagnostic, so the case does not fail on a code it +cannot predict. A project with compile errors runs none of its `[TestCase]`s. + ### Measuring LLVM `tbrun --llvm` sets `compiler.debugOptions` and `compiler.buildOptions` to `+llvm` in the @@ -857,15 +897,15 @@ from outside the IDE. it started. So can an IDE of a run from another session, which is why a check of the registry waits until no other session's run is going. -**An association that names the temp folder is never put back.** A run that starts while -another run's IDE copy is open finds the association pointing at that copy. Put back at the -end, it would point `.twinproj` files at a folder that has been deleted, and other sessions -run `examples.bat` while add-in tests run copies, so the overlap is ordinary. `startTidy` -notes whether the association it recorded names the temp folder, and if it did, `finishTidy` -leaves the association as the IDEs set it and says so; the next IDE started from a real -install points it at that install. An association found naming a deleted copy makes -`.twinproj` files open nothing until an IDE is started from a real install. Fix it -by hand if you find it so. +**An association that names the temp folder is put back pointing at the newest install.** A +run that starts while another run's IDE copy is open finds the association pointing at that +copy. Put back as found, it would point `.twinproj` files at a folder that has been deleted, +and other sessions run `examples.bat` while add-in tests run copies, so the overlap is +ordinary. `startTidy` notes whether the association it recorded names the temp folder, and if +it did, `finishTidy` puts it back with every path to a copy's `twinBASIC.exe` replaced by +`findIde()`'s, the newest install on the Desktop (owner, 2026-10-10): what that install's IDE +writes when it is next started. With no install outside the temp folder it leaves the +association as the IDEs set it and says so. **The recent list is put back as it was found, not only swept.** On a real list the IDE changes it by itself while a run's projects are on it, in two ways, both in @@ -1382,10 +1422,9 @@ probe lanes: been ended. **Timing, BETA 995:** the thirteen lanes other than widgets (24 s alone on BETA 997) take about two minutes at the default two at a time -(the env lane is 16 s alone, the help lane 15 to 18 s, of which its sixteen cases are about 6 s). Typical lane costs (BETA 983): an add-in build about 10 s, a host IDE -about 9 s, scenario 2 s; the keys lane 28 s (about 9 s of it pressing keys), panes 23 s, -symbols and ideserver about 9 s each (they build nothing), appdata 18 s, arch 56 s (two builds -and two switches of target), reload 47 s (two builds and two restarts), entry 19 s. +(the env lane is 16 s alone, the help lane 15 to 18 s, of which its sixteen cases are about 6 s). Typical lane costs: an add-in build about 10 s, a host IDE +about 9 s, a scenario 2 s; the slowest lanes are arch (56 s: two builds and two switches of +target) and reload (47 s: two builds and two restarts). **Known gaps and behaviours:** diff --git a/WIP.HelpAddin.md b/WIP.HelpAddin.md index a75c496a0..9119e07e4 100644 --- a/WIP.HelpAddin.md +++ b/WIP.HelpAddin.md @@ -1,14 +1,13 @@ # The IDE Help Add-in -See [WIP.md](WIP.md) for the maintenance guide. This file covers the planned twinBASIC IDE -add-in that shows the documentation for the symbol under the cursor, and the harness that -tests IDE add-ins by machine, which the add-in is developed against. +See [WIP.md](WIP.md) for the maintenance guide. This file covers the twinBASIC IDE add-in +that shows the documentation for the symbol under the cursor, and the harness that tests IDE +add-ins by machine, which the add-in is developed against. -**Status.** Stages 1 to 3 stand. Stage 4, the add-in itself in [add-in/](add-in/), has -increments 1 to 9 built and tested: F1 to a page, the help pane, the compiler's hover to tell -which `Add`, a name with no page shown by its declaration and `[Description]`, offline -help served from an archive in the DLL, hover help, a detached window, a settings panel, -and only the packages the project references. +**What exists.** The add-in is in [add-in/](add-in/) (Stage 4, below): F1 to a page, the +help pane, the compiler's hover to tell which `Add`, a name with no page shown by its +declaration and `[Description]`, offline help served from an archive in the DLL, hover help, +a detached window, a settings panel, and only the packages the project references. - `addin-test.bat` operates Samples 10 and 15 end to end and leaves the registry as it found it. - All of Stage 2's questions, P1 to P21, are answered. Most are held by probe lanes that @@ -32,7 +31,7 @@ one of three kinds: ## Goals 1. **Help for the symbol under the cursor.** A key press opens the documentation page for - the identifier at the cursor, or for the selection. F1, unless P1/P2 rule it out. + the identifier at the cursor, or for the selection. F1 (P1 and P2 do not rule it out). 2. **A help pane.** A dockable tool window with search and a page view, so that reading the documentation does not mean leaving the IDE. 3. **Theme-aware.** It follows the IDE's light, dark and classic themes. @@ -143,14 +142,14 @@ the compiler what a symbol is.** Each of those gaps shapes a stage below. restart.** A window it adds again under the same id is the same window, emptied and shown again (below, under Tool windows). So the rebuild loop works without ending the IDE: rename the loaded DLL aside, which P8 allows, put the new build in its place, and restart - the compiler. With build A renamed aside, a restart loaded nothing, left no button or - shortcut, and left both of A's windows showing the text; the renamed file could be - deleted, since the compiler that held it had ended. With build B then copied in under A's - name, the next restart loaded B, whose first line came 1.0 s after the click (4.4 s with - another lane building at the same time); B had one button, one shortcut that fired B - alone, and A's two windows, filled with B's content. A restart's compile settled after - 6.6 s, against 7.6 s for a new IDE to open the project and settle its compile, so a - harness saves little by the loop; a person keeps the IDE, its open files and its layout. + the compiler. With the DLL renamed aside, a restart loads nothing and leaves no button or + shortcut, and the old windows show the text; the renamed file can be deleted, since the + compiler that held it has ended. With a new build copied in under the old name, the next + restart loads it, its first line coming about 1 s after the click (4.4 s with another + lane building at the same time), with one button, one shortcut, and the old windows + filled with the new build's content. A restart's compile settles after 6.6 s, against + 7.6 s for a new IDE to open the project and settle its compile, so a harness saves little + by the loop; a person keeps the IDE, its open files and its layout. - **The build target picks the compiler, and the compiler picks the folder (P7).** The IDE remembers the target of each project in the shared registry, as one JSON object in `IDESettings\targetArchitectureMemory` keyed by project path, and opens a project in the @@ -403,8 +402,8 @@ offline site itself instead (Stage 4, increment 5), so this route is not taken. does with the VB and tbIDE ones. A variable gives its declaration, `*local variable* Dim c As Collection`. `Debug`, `Debug.Print` and a statement such as `Dim` give nothing, and a type such as `Long` a line - about it. BETA 983 gives no hover for those three, and BETA 987 and 995 a hover whose text is - empty, so the add-in must treat both as nothing. Over a procedure's name in its own declaration, hover gives a debug block + about it. For those three a build gives no hover (BETA 983) or a hover whose text is empty + (BETA 987 and 995), so the add-in treats both as nothing. Over a procedure's name in its own declaration, hover gives a debug block instead, `TB-DEBUG CODEGEN SIZE: [NOT-READY]`; over a `ByVal` parameter of a class, `String`, `Variant` or `Object` it adds a wrong note about `Option Explicit` ([BUGS-TO-REPORT.md](BUGS-TO-REPORT.md)). @@ -495,7 +494,7 @@ loaded line. ### Stage 1: testing add-ins by machine -Built. Everything after this stage is developed against it. [WIP.Harness.md](WIP.Harness.md) +Everything after this stage is developed against it. [WIP.Harness.md](WIP.Harness.md) has the harness side of each item. 1. **One library for starting and driving the IDE:** @@ -600,8 +599,8 @@ has the harness side of each item. under the runner leaves its registry entries to the runner, which sweeps only the lanes' folders. -Stage 1's acceptance, met: both scenarios pass under `addin-test.bat`, and the whole -registry, `IDESettings` included through hashes, is identical around the run. +Stage 1's acceptance: both scenarios pass under `addin-test.bat`, and the whole registry, +`IDESettings` included through hashes, is identical around the run. - **Sample 10:** toolbar button, then its tool window, then a message box (the three-button box answered `button2`, the follow-up answered `ok`, then a notification and a DEBUG @@ -612,9 +611,8 @@ registry, `IDESettings` included through hashes, is identical around the run. ### Stage 2: probes that decide the design Most probes are a small add-in plus a scenario. P5, P11 and P13 need only CDP and the file -system. Record every answer in this file with the build number it was measured on. - -All twenty-one questions are answered. +system. Record every answer in this file with the build number it was measured on. A new +question gets the next number; all of P1 to P21 are answered. **A probe whose answer something else rests on becomes a lane**: its add-in in `test/addin/probes//`, its scenario beside the others, listed in `lanes.mjs`, with each @@ -639,7 +637,7 @@ run, and the failure says what to update. The lanes and what rests on each: - [env.test.mjs](test/addin/env.test.mjs) (P10): the rule that an add-in under test opens nothing, which reads `TB_ADDIN_TEST`. - [widgets.test.mjs](test/addin/widgets.test.mjs) (P16, P17): the two routes to hover help - increment 6 weighed; the mouse route is the one built. It prints the P16 timings with the tests. + (increment 6); the mouse route is the one built. It prints the P16 timings with the tests. - [references.test.mjs](test/addin/references.test.mjs) (P20): increment 9's route to the packages a project references. - [parent.test.mjs](test/addin/parent.test.mjs) (P21): the WARNING on the FileSystemItem @@ -662,15 +660,15 @@ The table gives each answer in brief; the sections above have the detail. | P7 | Which bitness does the compiler start in, and does switching the build target restart it in the other one and load the other `addins` folder? **Answered, BETA 983 and 995: yes, and yes.** The target a project opens in picks the compiler, and each compiler loads the folders of its own bitness alone, the install's and `%APPDATA%`'s. | building and testing both bitnesses --- `buildAddin` builds either, and a shipped add-in needs both | | P8 | Is a loaded add-in DLL locked against being overwritten? **Answered, BETA 983 and 995: yes.** While its IDE runs, overwriting fails (`EBUSY`) and deleting fails (`EPERM`), though renaming works; the hold outlasts the compiler's exit by a few tens of milliseconds. The P9 lane checks all three again. | the rebuild loop --- the DLL is built outside `addins`, and copied in once the IDE has ended, or renamed aside first (P9) | | P9 | Does a compiler restart reload add-ins from disk? **Answered, BETA 983 and 995: yes.** The restart removes every add-in's buttons and shortcuts, leaves its windows showing `(currently unavailable)`, kills the old compiler so that no `Class_Terminate` runs, and starts a compiler that loads whatever file is in the folders then. A window comes back by its id. | a rebuild loop without restarting the IDE --- it works: rename aside, copy in, restart | -| P10 | Does an environment variable set by the harness reach the add-in (`Environ$`)? **Answered, BETA 983 and 995: yes**, through the launcher, the IDE and the compiler the IDE starts, and again in a compiler a restart starts. With `TB_ADDIN_TEST=1` in `launchIde`'s environment, `Environ$` and `GetEnvironmentVariableW` both returned `1`; left out, `Environ$` was empty and the Win32 call said unset. `WEBVIEW2_USER_DATA_FOLDER`, which `launchIde` always sets, arrived with the lane's port in it. | the side-effect switch | -| P11 | Does the IDE write into its own install folder during a session? **Answered, BETA 983 and 995: no.** On 983 a compile, a compiler crash and a `tbrun` build-and-run left all 233 files byte-identical, mtimes included. On 995 the install (235 files) was byte-identical after exports, a `tbbuild` and two `tbrun` runs; `%APPDATA%` was not re-checked. | hardlinks or copies --- copies, for safety, at 380 ms | +| P10 | Does an environment variable set by the harness reach the add-in (`Environ$`)? **Answered, BETA 983 and 995: yes**, through the launcher, the IDE and the compiler the IDE starts, and again in a compiler a restart starts. With `TB_ADDIN_TEST=1` in `launchIde`'s environment, `Environ$` and `GetEnvironmentVariableW` both return `1`; left out, `Environ$` is empty and the Win32 call says unset. `WEBVIEW2_USER_DATA_FOLDER`, which `launchIde` always sets, arrives with the lane's port in it. | the side-effect switch | +| P11 | Does the IDE write into its own install folder during a session? **Answered, BETA 983 and 995: no.** A compile, a compiler crash, exports, a `tbbuild` and `tbrun` runs leave every file of the install byte-identical, mtimes included (233 files on 983, 235 on 995); `%APPDATA%` was not re-checked on 995. | hardlinks or copies --- copies, for safety, at 380 ms | | P12 | Does `raiseEvent` from plain tool-window HTML throw? **Answered, BETA 983 and 995: yes** --- `TypeError: Cannot read properties of null (reading 'rootEventHandler')`, and the listener is not called. An inline handler that calls the listener `AddEventListener` stored on its parent, `this.parentNode.(event)`, reaches the add-in. | how the pane's events are written | | P13 | Does the compiler's HTTP server serve any file placed under `ide\`? **Answered, BETA 983 and 995: yes**, and it is the page server, `twinBASIC_win32.exe --ide=`, not the compiler. A frame with a relative `src` shows the file on the IDE page's own origin. A query string makes a 404, and `.html` has no `Content-Type`. | an offline route --- it exists ([Offline](#ways-to-show-a-page)) | | P14 | What do `tbCreateCompilerAddin_v2` and `_v3` expect? **Answered, BETA 983 and 995: what `tbCreateCompilerAddin` does.** The names are version stamps: the linker exports a function named `tbCreateCompilerAddin` as `tbCreateCompilerAddin_v3` alone, and an IDE that knows none of a DLL's names refuses it as `compiled for a newer version of the twinBASIC IDE`, as a patched `_v4` was. | nothing in the design --- the add-in declares `tbCreateCompilerAddin` as the package says; the tbIDE page has a NOTE | | P15 | Does the compiler say anything about a name's `[Description]`, or a COM type library's help string? **Answered, BETA 995: hover gives it**, after the line naming where the name is declared, for a procedure, a member, a constant, a module variable, an enumeration, a `Type`, a class and a module, and a type library's member (`Scripting.IDictionary.Add`: "Add a new key and item to the dictionary."). Classes and modules, the project's and a type library's, are `in library `. A procedure with none gets the IDE's tip, `*no further info available. Tip: use [Description("")] ...*`. On a procedure's name where it is declared, hover gives a block of code-generation details (`TB-DEBUG CODEGEN SIZE: [NOT-READY]`) instead. Signature help gives the same text. A member of an enumeration has none: a `[Description(...)]` line inside an `Enum` is not an attribute but a member of its own, named by a bracketed identifier, as VB6 allows (`Debug.Print Shade.[Description("The light one.")]` prints its value), so hover lists it as a member of the enumeration (twinbasic/twinbasic#2465, closed as not a bug). | Stage 4, increment 4: a name with no page shows its declaration and description from the hover; no parser of the project's source is needed | | P16 | What does hover help through the public API cost, and how long does a widget live? **Answered, BETA 997:** `GetSelectionInfo` takes 0.5 ms a call, reading `Text` of a 152-character file 0.3 ms, `AddMonacoWidget` 2.8 ms and `Remove` 0.6 ms; a 100 ms `AddinTimer` ticks every 110 ms. A widget with a column is drawn above its line (below when there is no room), stays when the cursor moves, and is removed by the IDE when another file is shown; `Remove` on it after that raises no error. An add-in has no cursor or mouse event (only `OnProjectLoaded`, `OnChangedActiveEditor`, `OnChangedTheme`, a tool window's `OnClose` and a button's `OnClick`), so it can only poll the cursor, and cannot see the mouse | Stage 4, increment 6: polling every 250 ms costs nothing worth counting | | P17 | Can the page's Monaco take a second hover provider for `twinbasic`, as the add-in would register it through the page? **Answered, BETA 997: yes.** Its text is shown in the IDE's own hover, above the compiler's, from a synchronous answer and from a promise answered 500 ms later, and goes once the provider is disposed. Registered for `"*"`, which matches less closely than the IDE's `"twinbasic"`, its text is shown below the compiler's. A link in it is drawn as ``, and a click listener on the window in the capture phase receives the click, and with `preventDefault` and `stopPropagation` nothing is opened. The IDE's hover shows after 1,000 ms (`hover:{delay:1e3}`) and is off when the user turns off *showExtraInformationWhenHovering* | Stage 4, increment 6: the mouse route, the owner's second exception to page internals | -| P18 | What hides a hover once the mouse slides down it past the code editor's bottom? **Answered, BETA 997: the IDE's dock.** Its resizer along the editor's edge (`z-index: 1000`) and its drop targets lie over the hover, transparent, and take the mouse; Monaco's `_onEditorMouseLeave` then hides the hover. Monaco alone keeps a hover the mouse is over: its DOM stays inside the editor's, and the hover is sticky. CDP's mouse reproduces it only in steps small enough to land on the 6 px resizer; the owner's own mouse found it, under a watcher on the IDE's DevTools port that recorded `_hideWidgets`'s stack | Stage 4, increment 6: the add-in draws every hover above the dock, hover help on or off | +| P18 | What keeps a hover shown while the mouse slides down it past the code editor's bottom? **Answered, BETA 1005: the IDE.** It draws `.monaco-hover` at `z-index: 1000 !important`, above its dock resizer along the editor's edge (`.dockElementResizer`, `z-index: 1000`) and its drop targets, so they never take the mouse from the hover (Monaco's `_onEditorMouseLeave` would hide it). Monaco alone keeps a hover the mouse is over: its DOM stays inside the editor's, and the hover is sticky. CDP's mouse reaches the 6 px resizer only in steps small enough to land on it, so the test slides the mouse a pixel at a time | Stage 4, increment 6: nothing in the add-in; the `help` lanes check the IDE's behaviour | | P19 | Can an add-in show a window of its own, a Form holding the WebView2 package's control? **Answered, BETA 997: yes** ([detach.test.mjs](test/addin/detach.test.mjs), `probes/detach`). A minimal hand-written `.tbform` builds, given `_className` and `_clsid` on the form and the control. `Show vbModeless` gives a visible top-level window, owned by the compiler's process (`twinBASIC_win32_noDEP.exe`, a child of `twinBASIC.exe`), with no message loop of the add-in's own; the control is ready about 250 ms after the first Show, 60--80 ms for a later form. A page loaded with `NavigateToString` posts to the add-in and receives what it posts, and an iframe in it loads an `http://localhost` page. Hide and Show keep the page; Unload and a new form work; the IDE ending leaves no `msedgewebview2.exe` of the window's behind. **The harness gives the IDE `WEBVIEW2_USER_DATA_FOLDER` and `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`, the compiler inherits them, and they override the control's own options: with them left in place the control fails, error `8007139F`** (which of the two, not isolated). With no user data folder set, the control uses `bin\twinBASIC_win32_noDEP.exe.WebView2`, which the IDE has made already | Stage 4, increment 7: the detached window, and the variables it removes under the test switch | | P20 | Can an add-in learn, through the public API, which packages its project references? **Answered, BETA 995 and 997: yes** ([references.test.mjs](test/addin/references.test.mjs), `probes/references` and its three hosts `probes/references-*`). `/Packages`, reached with `For Each` from `Host.FileSystem.RootFolder` (`twinbasic:/`), holds a folder for each package the project references, newest first, then VBRUN and VBA, which are always there; a package's own references are folders inside its own `Packages`. A folder is named for the package's project, which is the key the symbol index's `packages` map uses (`VB`, not the reference's symbol `WindowsControlsPackage`; `Assert`, not `TwinBasicAssertions`); `VBComDlg`, which only WinNativeCommonCtls references, is the one name the map lacks. `IsPackagesFolder` is true for every `Packages` folder and false for a package's. A package is one folder shared by every list it is in, and its `Path` and `Parent` name the first one it was found in, so only `Name` identifies it. The list is complete when `Host_OnProjectLoaded` runs. Applying a reference added or removed on the References page restarts the compiler with no question asked, and the new add-in instance's list shows the change. A type library is a file in `References`, not a package. | Stage 4, increment 9: the packages the add-in offers | | P21 | What does reading `Parent` of the file system's root do? **Answered, BETA 983, 995 and 997: it crashes the compiler** ([parent.test.mjs](test/addin/parent.test.mjs), `probes/parent`, one IDE per case, the case passed in `TB_P21_CASE`). The read itself crashes, before the value is used: into an `Object` or a `Folder`, in `Is Nothing`, in `TypeName`. Each compiler the IDE restarts loads the add-in and crashes again. The project folder's `Parent` reads, and is the root (`twinbasic:/`). | no add-in here reads the root's `Parent` (P20's tells the root by its path); the WARNING on FileSystemItem's **Parent**; a BUGS entry | @@ -685,7 +683,7 @@ fixed from here on. Permanent Links documents the format for any reader; [WIP.Build.md](WIP.Build.md#the-symbol-index-and-the-drift-guard-on-its-urls) the build side. It has 5,536 entries at 4,086 distinct URLs; 782 KB, 52 KB gzipped, under 100 ms of the build. The file is emitted the way `assets/js/search-data.json` is, so an installed add-in can fetch -a newer index; a copy is also to be built into the add-in, for when the site cannot be reached. +a newer index; the add-in also embeds a copy (Stage 0), for when the site cannot be reached. The data model: @@ -794,12 +792,6 @@ has the complete list): **Not done, and why:** -- **The rest of an embedded mode for pages.** The theme is done: the site reads a - `theme=dark|light` query parameter (Stage 4, increment 2), since the add-in cannot reach - into a cross-site frame. Hiding the header and navigation is a separate, optional - question, untested. The IDE's own server answers any URL with a query string with a 404 - (P13); the add-in's server (increment 5) ignores the query, so the parameter works - offline too. - **Keywords with no page of their own** --- `ElseIf`, `Until`, `Step`, `To`, `In`, `ByVal`, `ByRef`, `Optional`, `As` --- are in the index only where a page's title gives them. Adding one is a `symbols:` line on the page that explains it, which is a content @@ -813,12 +805,15 @@ has the complete list): ### Stage 4: the add-in, in increments -Each increment is finished with its scenarios. Worked on by hand, a new build replaces the -loaded one without ending the IDE: rename the loaded DLL aside, put the new build in its -place, and click the compiler's restart button (P9). The lanes need not: a restart saves a -harness about a second against opening a new IDE. +Each increment has its scenarios in the lanes. The site reads `theme=dark|light` and `pane=1` +as query parameters, since the add-in cannot reach into a cross-site frame; the IDE's own +server answers a URL with a query string with a 404 (P13), and the add-in's server +(increment 5) ignores the query, so the parameters work offline too. Worked on by hand, a +new build replaces the loaded one without ending the IDE: rename the loaded DLL aside, put +the new build in its place, and click the compiler's restart button (P9). The lanes need +not: a restart saves a harness about a second against opening a new IDE. -1. **F1 to a page. Built**, tested by [help.test.mjs](test/addin/help.test.mjs) on BETA +1. **F1 to a page**, tested by [help.test.mjs](test/addin/help.test.mjs) on BETA 995. F1 takes the selection or the name under the cursor (below) and looks it up in the embedded index. One page is shown in the help pane (increment 2); a miss says `No help for ''` through `ShowNotification`; an empty spot puts the focus in the @@ -837,7 +832,7 @@ harness about a second against opening a new IDE. is on, and every scenario checks that line before it presses anything. An IDE build that stopped passing the variable on to the compiler then fails the run, instead of starting a browser on the private desktop. -2. **The help pane. Built**, tested by the same lane on BETA 995. The tool window +2. **The help pane**, tested by the same lane on BETA 995. The tool window `tbDocsHelpPane`, titled `TWINBASIC HELP`, holds a search box, an *Open in browser* button, a results list and the page in an iframe (P3), laid out by a flex wrapper inside the root, because showing the window resets the root's `display`. The toolbar's @@ -893,23 +888,19 @@ harness about a second against opening a new IDE. `data-theme`, and switches the theme with the IDE's own commands (`executeIdeCommand("tbTheme_SwitchToLightMode")`, `...DarkMode`, `main.js` BETA 995), which save it in `twinBASIC_IDE\IDESettings\GENERAL` (`colorTheme`), where the user's - own IDE reads it; the registry tidy puts that one entry back (`restoreTheme`). Fault - runs: without the handler only the theme-change case fails; with the site ignoring the - parameter every case that shows a page fails. + own IDE reads it; the registry tidy puts that one entry back (`restoreTheme`). **In the detached window the frame's storage throws** (`SecurityError`, both storages, BETA 997): the window's page is a `data:` URL (`NavigateToString`), so the docs frame is a frame of another site under an opaque top-level origin, with a DevTools target of its own - (type `iframe`). Until 2026-10-09 the head script applied `?theme=` there, and - `theme-toggle.js`, which read the choice back from storage alone, reset it to *system*, so - the page followed the window's WebView2, which is Windows' app mode; a link inside the - frame also lost `data-pane`. So the head script keeps both in the frame's `window.name` - too (`tbdocs::<1>`, written only by a page given either parameter, read only when - storage throws; a navigation inside the frame keeps it), and `theme-toggle.js` mirrors - the attribute the head script set. A pane frame is on the IDE page's own site and its + (type `iframe`). So the head script keeps the theme and `pane` in the frame's + `window.name` too (`tbdocs::<1>`, written only by a page given either parameter, + read only when storage throws; a navigation inside the frame keeps it), and + `theme-toggle.js` mirrors the attribute the head script set; without that the page + would follow the window's WebView2 (Windows' app mode) and a link inside the frame + would lose `data-pane`. A pane frame is on the IDE page's own site and its storage works. The lanes check both places in Light and Dark, the page given and a page after a link, with the frame's `prefers-color-scheme` emulated to the other scheme - (`Emulation.setEmulatedMedia`, on the frame's own target in the window): before the fix, - all four window cases failed and the pane's passed. + (`Emulation.setEmulatedMedia`, on the frame's own target in the window). - **The page shows without the site's chrome**: `FrameUrl` adds `pane=1` too, which the head script keeps in `sessionStorage` as it keeps the theme and turns into `data-pane` on the root. `custom.scss` then hides the sidebar and the header (search box, @@ -918,11 +909,9 @@ harness about a second against opening a new IDE. (0,2,0), because the dark compilation re-emits the header's `display: flex` at (0,4,1). *Open in browser* passes neither parameter. Every `showsPage` checks the computed `display` of the four, and a case follows a link inside the frame and checks that the - next page keeps the theme and the layout. Fault runs: the rules under `html[data-pane]` - failed every page case in the dark theme, with the sidebar back; the head script not - keeping `pane` failed only the link case. The lane's pane is narrower than the site's - `md` breakpoint, where the header is hidden anyway, so the wide layout was checked with - puppeteer over the offline tree at 500, 900 and 1400 px in both themes. + next page keeps the theme and the layout. The lane's pane is narrower than the site's + `md` breakpoint, where the header is hidden anyway, so the wide layout is not covered by + it; check it with puppeteer over the offline tree at 500, 900 and 1400 px in both themes. - F1 pressed while the focus is in the page goes to the page, not to the add-in, so a lookup from there goes through the search box. @@ -941,7 +930,7 @@ harness about a second against opening a new IDE. case loads the build's real page, and a 404 fails it. The frame is then same-site, and `Page.getFrameTree` reads its URL. The offline tree cannot be used: a page served over http cannot frame a `file://` URL. So the lane needs `build.bat` first. -3. **Context: which `Add`? Built**, tested by the same lane on BETA 995. **The route is the +3. **Context: which `Add`?** Tested by the same lane on BETA 995. **The route is the page's socket** (the owner, 2026-10-04): the add-in's own parser would have to be close to a whole twinBASIC parser, so it waits, and the socket stands until upstream gives the API a call for it. For the name under the cursor, F1 asks the compiler's hover about @@ -973,12 +962,12 @@ harness about a second against opening a new IDE. over AppGlobalClassObject's and `ErrObject.Number` over every other `Number`. No answer to read (a statement, `Debug.Print`, a comment, a late-bound `o.Add`) is the plain lookup. - - **The fault run** (the question made to answer `unavailable` every time) failed the - cases that need the compiler: `c.Add`, `.Add` inside `With`, a CheckBox's `Value`, - `App.Path`, `Err.Number` and the project's `Beep`. + - The cases that need the compiler (and so fail when the question answers `unavailable`) + are `c.Add`, `.Add` inside `With`, a CheckBox's `Value`, `App.Path`, `Err.Number` and + the project's `Beep`. - The socket code is in files of its own, `CompilerHover.twin` and `hover.js`, without comments, so that it can be replaced whole once the API has a call. -4. **A name with no page. Built**, tested by the same lane on BETA 995, at the owner's +4. **A name with no page**, tested by the same lane on BETA 995, at the owner's request (2026-10-04): a user who documents their own code with `[Description]` sees it, and is not sent to a page for another name. When the hover names a package the index does not document --- the project, another package, a type library --- F1 shows, in @@ -995,9 +984,7 @@ harness about a second against opening a new IDE. `f1Shows` marks the page before pressing F1 and waits for a page without the mark. A page showing again after a summary is waited for too, since its `src` can already be right. - - **The fault run** (`ShowPage` not putting the frame back) failed the four cases that - show a page after a summary. -5. **Offline help. Built**, tested by the lane `help-offline` on BETA 995, at the owner's +5. **Offline help**, tested by the lane `help-offline` on BETA 995, at the owner's choice (2026-10-05): the add-in's own server, written in twinBASIC, serving one archive embedded in the DLL. The owner's other choices: the archive is a resource; the documentation build makes it (owner, 2026-10-06); the pane uses @@ -1035,7 +1022,7 @@ harness about a second against opening a new IDE. (`PerformanceNavigationTiming.responseStatus`), so a 404 fails a case either way. An offline-only case asks the server from Node: a page's gzip body and a stored image byte for byte, a folder's 301, a 404 and `HEAD`. -6. **Hover help. Built**, tested by the `help` and `help-offline` lanes on BETA 997, with +6. **Hover help**, tested by the `help` and `help-offline` lanes on BETA 997, with the owner's choices (2026-10-06): links in the IDE's hover under the mouse, behind one *Hover help* checkbox in the settings panel (increment 8), off by default and kept with `SaveSetting` (`Hover`, `On`). It shows a link per page the name has, at most five, labelled with the @@ -1045,10 +1032,10 @@ harness about a second against opening a new IDE. declaration and description. `SymbolIndex.Pages` is the lookup F1 and hover help share. `HoverHelp` raises `Pick` with the page's path. - **At the cursor there is nothing** (owner, 2026-10-07). A widget above the line, added - through the public API once the cursor had rested on a name (P16), was built and then - removed: it covered the line above whenever typing paused on a name, did not look like - the IDE's own hover, and gave nothing the mouse hover and F1 do not. P16 says how to - build it again, should the page's provider ever stop working. + through the public API once the cursor had rested on a name (P16), is not used: it + covers the line above whenever typing pauses on a name, does not look like the IDE's own + hover, and gives nothing the mouse hover and F1 do not. P16 says what it costs, should + the page's provider ever stop working. - **Under the mouse**, through the page (P17): `hoverhelp.js`, sent through a sink (`helpAskMouse`) every time the pane is built, disposes the provider and click listener an earlier instance of the add-in left in `window.tbDocsHoverHelp`, and registers new @@ -1061,22 +1048,18 @@ harness about a second against opening a new IDE. click on one whose `data-href` starts with it is taken in the capture phase and sent to the add-in, which shows the page in the pane. - **The hover stays past the editor's edge** (P18). The editor is created with - `fixedOverflowWidgets`, so a long hover reaches past the code editor's edge, and the - IDE's dock resizer along that edge (`.dockElementResizer`, `z-index: 1000`) and the - dock's drop targets (`.dockGroupVerticalDockPointBottom`) lie over it: transparent, - so the hover shows, but they take the mouse. Monaco takes the mouse reaching them as - leaving the editor (`_onEditorMouseLeave`, its `relatedTarget` not in the hover) and - hides the hover, whether or not hover help is on. So `hoverhelp.js`, every time the pane - is built, adds a style (`#tbDocsHoverStay`) that draws `.monaco-hover` at - `z-index: 100000`, above them, with the box ticked or not (owner, 2026-10-07: it is - for every hover, and goes once the IDE is fixed, twinbasic/twinbasic#2506). Making the - resizer take no pointer events is not enough: the drop target under it takes the mouse - instead. + `fixedOverflowWidgets`, so a long hover reaches past the code editor's edge. Since + BETA 1005 the IDE draws `.monaco-hover` at `z-index: 1000 !important`, above the dock + resizer along that edge and the dock's drop targets, so the mouse sliding down the + hover past the edge keeps it. The add-in adds nothing for this, with hover help on or + off. The `help` and `help-offline` lanes check the IDE's behaviour: the test makes the + page short enough that the hover reaches past the editor's bottom, and slides the + mouse down it across the edge. - **In the lanes**, the floating pane covers the middle of the code editor, so the cases move it against the window's right edge before the mouse rests anywhere; and a hover left open covers the toolbar, so each mouse case moves the mouse away at its end. `test/addin/hover.mjs` holds the helpers both lanes use. -7. **A detached window. Built**, tested by the `help` and `help-offline` lanes on BETA 997, +7. **A detached window**, tested by the `help` and `help-offline` lanes on BETA 997, at the owner's request (2026-10-07): the pane can be moved to another monitor. The owner's choices: the add-in shows a window of its own, a twinBASIC Form holding the WebView2 package's control (P19), rather than a `window.open` from the page, which would @@ -1114,9 +1097,9 @@ harness about a second against opening a new IDE. form's (`SetWindowLongPtrW`, as `HelpServer` does for its message window) answers `WM_GETMINMAXINFO` with the work area exactly. A maximized window with no title bar has no frame taken off its client area (measured at 150%: the client area is the whole window - rectangle), so the window outside the work area by the frame's thickness, which an - earlier version assumed, was shifted up and left by it and left a strip at the right and - bottom; the procedure also answers `WM_NCCALCSIZE` for a maximized window whose proposed + rectangle), so a window placed outside the work area by the frame's thickness is + shifted up and left by it and leaves a strip at the right and bottom; the procedure + also answers `WM_NCCALCSIZE` for a maximized window whose proposed rectangle is the work area with the rectangle as it came, so that no Windows version takes a frame off it. (A window being restored is still zoomed when its `WM_NCCALCSIZE` comes, with its own smaller rectangle: only the work-area rectangle is @@ -1192,7 +1175,7 @@ harness about a second against opening a new IDE. caption (no read-back of the DWM attribute), Alt+F4, the registry read, maximizing on a monitor other than the primary one (the lab machine has one), and the move itself, whose loop needs the real mouse, which the harness's desktop has none of. -8. **Settings. Built**, tested by the `help` and `help-offline` lanes on BETA 997, at the +8. **Settings**, tested by the `help` and `help-offline` lanes on BETA 997, at the owner's choice (2026-10-07): a gear at the right end of the pane's bar, and on the detached window's title bar (in its bar under the Windows caption), opens a drop-down panel of settings, one label row each. A click outside it, Esc, or the gear closes it; changing a @@ -1202,7 +1185,7 @@ harness about a second against opening a new IDE. because the frame is cross-site there and runs in a process of its own, so the window's `blur` closes the panel as well. The lanes' pane frame is on the IDE page's site, so one case points it at `127.0.0.1` to check the shade with a cross-site frame. -9. **Only the packages the project references. Built**, tested by the `help` and +9. **Only the packages the project references**, tested by the `help` and `help-offline` lanes on BETA 997, at the owner's choice (2026-10-07): F1, the hover links and the search offer the language's own entries and the packages the project references; an *All packages* row in the settings panel, off by default and kept with `SaveSetting` @@ -1214,19 +1197,18 @@ harness about a second against opening a new IDE. `FindDeclared` is not limited, since the compiler's hover names the declaring package. F1 on a name only an unreferenced package documents says *No help for 'X' in the packages this project references*. -10. **Later:** how a user gets an add-in with the archive: the owner's plan (2026-10-07) is to - build it in CI with everything else, once the compiler runs on Linux there. - **Done meanwhile (2026-10-09): the project file is a download, and it is the offline - add-in.** Every docs build, CI's included, packs `add-in/` into - `tB/IDE/AddIns/downloads/tbDocsHelp.twinproj` in the online tree +10. **How a user gets an add-in with the archive.** The project file is a download, and it + is the offline add-in (owner, 2026-10-09). Every docs build, CI's included, packs + `add-in/` into `tB/IDE/AddIns/downloads/tbDocsHelp.twinproj` in the online tree (`builder/addin-project.mjs`, the `addinProject` task), and *Getting the add-in* links to it first, the source route second. It holds the files git tracks, the symbol index of the build that publishes it, and the archive that build wrote as - `Resources/HELP/site.zip` (the project is then about 28 MB), so the add-in built from it serves the pages - itself with no internet; CI passes no `--no-help-archive` for that reason. A build - that wrote no archive packs none. The offline tree holds no copy, and its links to the - file go to the website. The reader still builds the two DLLs in the IDE. What this - increment still adds is the DLLs themselves, built in CI. + `Resources/HELP/site.zip` (the project is then about 28 MB), so the add-in built from it + serves the pages itself with no internet; CI passes no `--no-help-archive` for that + reason. A build that wrote no archive packs none. The offline tree holds no copy, and + its links to the file go to the website. The reader still builds the two DLLs in the + IDE. **Not done:** the DLLs themselves, built in CI --- the owner's plan (2026-10-07) is + to build them there with everything else, once the compiler runs on Linux there. Why the pack is made the way it is: [WIP.Build.md](WIP.Build.md#the-help-add-ins-project-file). **Lookup:** @@ -1283,16 +1265,13 @@ the route once the socket is gone, if the API never gains a call for it: - Scanning on a background thread is possible. Measure the scan first; a second thread inside the IDE's process is the riskiest part of the whole design. -The add-in's code skeletons are written in Stage 4 against the compiler, with tests. - ### Stage 5: shipping - Build both bitnesses, which `buildAddin` does (P7). The documentation page is [docs/IDE/AddIns/Help.md](docs/IDE/AddIns/Help.md); its pictures are taken by `scripts/shoot_docs.mjs` from `test/addin/helpdemo`, so retake them after a change the - pictures show, and update the page's *Getting the add-in* once increment 10 settles how users - get it. Until then the page offers the project file the docs build publishes, then the - source route. + pictures show. The page's *Getting the add-in* offers the project file the docs build + publishes, then the source route; update it once the DLLs are built in CI (increment 10). - Distribution is upstream's decision: the community add-ins list, or bundled with the IDE. - Take to upstream, with the probe results as evidence: the shortcut bug; a call to open a URL; a way to ask the compiler about the symbol at a position, whose answer hover already @@ -1318,7 +1297,7 @@ Recommended, and not yet confirmed: ## Rules -Stage 1 is built, so the rules for testing add-ins bind every session and are in +The rules for testing add-ins bind every session and are in [WIP.md, Driving the twinBASIC compiler](WIP.md#driving-the-twinbasic-compiler): no test add-in in the real install's `addins\` or in `%APPDATA%\twinBASIC\addins\`, no real browser from a test, every `SaveSetting` application named in `lanes.mjs`, IDEs ended by pid, and diff --git a/WIP.Screenshots.md b/WIP.Screenshots.md index 89421fc9d..6cf91ff99 100644 --- a/WIP.Screenshots.md +++ b/WIP.Screenshots.md @@ -1,28 +1,35 @@ -# Screenshots — the plan for `shoot_docs.mjs` +# Screenshots — `shoot_docs.mjs` -The documentation's IDE screenshots go stale with every beta. `scripts/shoot_docs.mjs` (until -increment 1, `shoot_help_addin.mjs`, which took only the help add-in's eight pictures) retakes -them from a live IDE; this plan grows it to every picture of the IDE the documentation holds, -annotations included. The owner's decisions behind it are dated 2026-10-08. +The documentation's IDE screenshots go stale with every beta. `scripts/shoot_docs.mjs` retakes +them from a live IDE: every picture of the IDE the documentation holds, annotations included, +and the program windows and web pages that sit beside them. The owner's decisions behind it are +dated 2026-10-08 onward. ## What there is to retake -About 212 raster images under `docs/` besides the help add-in's eight. By what producing them -needs: +Every menu, dialog, panel and editor view of the IDE is HTML in the IDE's page, so all are +reachable over CDP; annotations (arrows, boxes, rings, numbers, labels) are drawn on top by the +tool, and a composite is assembled from several clips. Running programs' windows and a few web +pages are taken beside them. -| class | count | what | -|---|---|---| -| IDE UI, plain | ~150 | every menu, dialog, panel and editor view is HTML in the IDE's page, so all are reachable over CDP | -| IDE UI, annotated | ~43 | arrows, boxes, rings, numbers and labels drawn on top; 8 of them composites of several crops | -| not IDE UI | ~18 | running exe windows, GitHub pages, the splash window, one GIF: out of scope | +### Pictures still outside the tool -Stale already: About (BETA 953), New Project (BETA 950), `CallStack.png` (compiler v0.15.957), -`fafaloneIDEscreenshot1.png` (BETA 407), several package pictures (BETA 730). +Ten images under `docs/`, each for a stated reason: + +- **The six GitHub walkthrough pictures** of `Documentation/Building.md` (compare-changes, + create-pull-request, merge-pull-request, confirm-merge, choose-workflow, run-workflow): the + owner's to retake by hand, with a signed-in account (owner, 2026-10-09); no agent touches them + or their prose. They show base `main` and the old workflow name, and confirm-merge an e-mail + address, until then. +- **`packPublishPackage1`**: `packagePublisherPublish` opens the sign-in form when no publisher + is signed in, before the confirmation box, and a package is never actually published, so the + old file stays. **`packPublishComplete1`** is the result of a real publish, so it stays too. +- **`Tutorials/WebView2/Images/tbWebView2InAForm.gif`** is skipped (owner). +- **`favicon.png`** is never changed by this work (owner). ## Decisions (owner, 2026-10-08) -- **One tool.** `shoot_help_addin.mjs` is renamed `shoot_docs.mjs`; the help add-in's pictures - become one setup among the others. +- **One tool.** `shoot_docs.mjs`; the help add-in's pictures are one setup among the others. - **2x pictures, shown at half size**: every retaken picture gets `{:width="W" height="H"}` (half its pixel size) on its page, as `docs/IDE/AddIns/Help.md` has. A JPG retaken becomes a PNG. - **Menus are transparent cut-outs**, as today: the bar item and its drop-downs opaque, the rest @@ -35,22 +42,30 @@ Stale already: About (BETA 953), New Project (BETA 950), `CallStack.png` (compil ## The IDE the tool drives -Measured by the s103 probe (8 IDE runs, BETA 997): - -- **Fixed device scale.** The IDE page otherwise takes the host's scaling (1.5 here: - `body.scale150`, 22.67 px menu rows). `--force-device-scale-factor=1` added to the WebView2 - browser arguments gives `scale100` and integer sizes; `Emulation.setDeviceMetricsOverride` at - `deviceScaleFactor: 2` then gives a clean 2x. Menus and dialogs come out at the old pictures' - CSS sizes to the pixel (File 189x336, Manage Keyboard Shortcuts 1016x736). **The help add-in's - pictures change once when this goes in**: until now they followed the host's scaling. -- **No-project start.** `Lane.open` always opens a project. Started with no argument, the IDE - shows its splash and then New / Open Project, with every menu in its no-project state. Needs a - `Lane.openNoProject()` (`launchOnDesktop` with no argument, then `attachIde`). The lane's copy - of the install leaves out `projects\` (29 MB): New Project then shows 2 templates and no - samples, so a setup that shows them copies `projects\` into the lane's copy. -- **Theme.** The pictures are of the dark theme (`tbTheme_SwitchToDarkMode`, as now). The menu - bar and dialog title bars follow the theme; drop-downs and dialog bodies are light in every - theme. +Measured on BETA 997: + +- **Fixed device scale.** The IDE page otherwise takes the host's scaling (1.5 on the dev box: + `body.scale150`, 22.67 px menu rows). `--force-device-scale-factor=1` in the WebView2 browser + arguments (`Lane`'s `browserArgs`) gives `scale100` and integer sizes; + `Emulation.setDeviceMetricsOverride` at `deviceScaleFactor: 2` then gives a clean 2x. Menus and + dialogs come out at the CSS sizes of the pictures taken before the fix to the pixel (File + 189x336, Manage Keyboard Shortcuts 1016x736). +- **No-project start.** `Lane.open` always opens a project. `Lane.openNoProject()` + (`launchOnDesktop` with no argument, then `attachIde`) starts the IDE as it shows its splash and + then New / Open Project, with every menu in its no-project state. The lane's copy of the + install leaves out `projects\` (29 MB): New Project then shows 2 templates and no samples, so + a setup that shows them copies `projects\` into the lane's copy. +- **Theme** (owner, 2026-10-09). Every picture is taken in the dark theme + (`tbTheme_SwitchToDarkMode`) as `X.png`, byte for byte the picture it always was, and in the + **Light** theme (`tbTheme_SwitchToLightMode`, not Classic) as `X.light.png` beside it. The + site shows the light one in its light theme and the dark one in its dark theme, following the + toggle and, with no choice stored, the OS; pages are not edited, the build pairs the files by + name ([WIP.Build.md, Pictures in two themes](WIP.Build.md#pictures-in-two-themes)); the PDF + has the light ones only. The menu bar and dialog title bars follow the theme; drop-downs and + dialog bodies are light in every theme. Light lays the window out a few pixels differently + (the toolbar 4 CSS px shorter, a panel's title bar 3, the Global Search button narrower), so + a light picture can be a few pixels off the dark one's size; the build shows each at its own + size, at one scale. - **Opening things.** Menus open on a real mouse press on `#rootMenu`; submenus on a real mouse move onto the item (~500 ms; wait for `#contextMenuSUB > .contextMenuItem`). Dialogs by `executeIdeCommand` (`tbHelp_ShowAboutWindow`, `tbIde_ShowIDEOptions`, @@ -65,7 +80,11 @@ Measured by the s103 probe (8 IDE runs, BETA 997): private APPDATA — see Machine state in the pictures; - the licence line on About (`LICENCE: tB Licence: NOT READY` when the licence check has not finished) — the same section; - - the Windows user name: the existing visible-text check stays and covers every picture. + - the Windows user name: the visible-text check (text boxes included) covers every picture, + run before the first capture and again when a capture is chosen, with the SVG under + `--svg`: the page can draw again in between, as Project Settings did, putting the real + build path back over the template a shot had set in the page only (a shot that sets a + value types it, `input` and `change`, so the page keeps it). ## Capture @@ -89,22 +108,30 @@ Measured by the s103 probe (8 IDE runs, BETA 997): grey level off and stay so), then takes up to 8, 150 ms apart: it keeps one equal to the file on disk, else the first two in a row that agree, and fails if none agree. So a picture is rewritten when the IDE draws something different, not when the noise flips. - **Left over, under the full parallel load:** a run now and then settles on the other version of - one or two pictures and keeps it through every capture, so a longer wait in `capture` does not - help: Diagnostics (34 pixels along an arrow's edge), 6ad7a172 (4 title-bar corner pixels), - Menu_Window_PanelLayouts (one submenu corner pixel), 1-3 grey levels each; each is unchanged - when its setup runs alone, and which one varies from run to run (`JOB_SECONDS` moves it, but is - not the whole cause). Put such a picture back rather than commit it; diffs in - `.claude/tooling-review-scratch/s103-shots/perf/diffs-final/`. + Under the full parallel load a run now and then settles on the other version of one or two + pictures (1-3 grey levels, a few pixels to a few hundred along an edge) and keeps it through + every capture, so a longer wait in `capture` does not help; each is unchanged when its setup + runs alone, and which one varies from run to run. So **a picture within a capture's noise of + its file is left as it is** (`nearly`: one size, no channel more than 16 levels apart, at most + 0.5% of the pixels apart), reported `unchanged but for a capture's noise`, with a `.noise` + picture under `--diffs`. The same test decides that a light picture is the dark one. +- **A shot may name where the page flickers of its own accord** (`unsteady` in `capture`'s + options): a list of `{on, pad}`, `on` an annotation anchor (see [Annotations](#annotations)) + and `pad` how far round its box, in CSS px (1 by default, an edge's anti-aliasing). Every + comparison `capture` makes leaves those pixels out (`same`, and `nearly`'s `rects`), and a + capture that matches the file or the dark picture outside them keeps that picture's bytes, + so the flicker never rewrites the file. Use it for a pixel no condition settles, as small as + the element allows: the WebView2 download page's Version arrow (`#version + .px-dropdown__icon`, 8 by 9) changes a pixel by more than the noise between any two + captures, animations and transitions off. A shot without it compares byte for byte. - **Settle by condition, not by sleep.** Fixed sleeps were most of a run (a closed menu leaves a 2x8 px empty `#contextMenu`, so a wait for it to vanish always ran out its 2 s; ~3,000 calls). A menu is closed when it holds no items; a dialog is still when a MutationObserver has seen no change and its images and fonts are loaded; a floated panel when the IDE's `.flashElement` class is gone; a help page when its fonts, images and body are settled. The page size is set only once `body` has its `scale100` class: the IDE picks 48 or 32 px New Project tiles from it. -- **A picture that shows state sets that state itself** rather than relying on the shots before it - (9eeffbcf, PackagePublishing_1, packLicenceFiles), so a picture is the same whichever IDE or - order takes it. +- **A picture that shows state sets that state itself** rather than relying on the shots before it, + so a picture is the same whichever IDE or order takes it. ## Annotations @@ -126,7 +153,11 @@ beside an anchor: `left`/`right`/`above`/`below`), `badge` (a number). 1 px white halo so a stroke reads on light and dark UI; one solid triangular head, 5x the stroke long; boxes and rings 3 px with 4 px padding; labels in the page's font (Segoe UI) 600 at 15 px on a pill (dark red with white text over dark UI, white with red text over light); badges a filled red circle of -22 px with a white bold numeral. No hollow block arrows, no hand-drawn strokes. +22 px with a white bold numeral. The dark pill is `#8E161A`. No hollow block arrows, no hand-drawn strokes. **A label's +tone follows the theme** unless the shot names one: dark in the dark theme, light in Light, +read from the page's panel colour. Every label today sits on what follows the theme (a panel's +ground, the window), so none names a tone; one over a drop-down or a dialog's body, light in +every theme, would say `tone: "light"`. **Composites** (the five CustomControls code-to-property pictures, `Editor.png`, `DebugConsole.png`): each part is captured as its own clip; the tool then lays the parts out in a full-window layer @@ -138,9 +169,9 @@ as the IDE's own pop-up list, beside the select. ## The tool -`scripts/shoot_docs.mjs [--only <regex>] [--port N] [--ide <path>] [--jobs N] [--diffs <dir>]`, -with the current tool's exit codes (0, 1 a picture failed, 2 the tool could not run, 3 not put -back). +`scripts/shoot_docs.mjs [--only <regex>] [--out <dir>] [--port N] [--ide <path>] [--jobs N] [--diffs <dir>] [--svg]`, +Exit codes: 0 every picture written or unchanged, 1 a picture failed, 2 the tool could not run, +3 not put back. - **Setups**: one IDE each --- `no-project` (menus, dialogs, panels), `help` (the add-in's eight, `test/addin/helpdemo`), `project`, `sample`, the `settings` setups, `glyphs`, `global-search` @@ -148,88 +179,286 @@ back). opened), `sample6` (Sample 6 exported and opened) and `designer` (the sample fixture with `test/shots/designer/` staged onto it, plus the Global Search add-in for Toolbar_3; it sets the page's `currentDPI` to the 2x ratio, or the IDE's 5 s DPI check covers every designer with - RESYNC), and `forms` (the same without the add-in). A setup is `{name, start, prepare}`. + RESYNC), and `forms` (the same without the add-in), `community` (the demo, its compiler + restarted on an empty licence key in the page) and `splash` (the demo, for the splash + alone), `programs`, `codelens`, `package` and `fusion` (pictures of a running program), + `package-server`, `settings-linked` and `package-check`, `import`, `webpage` and `web` (a + Chrome, not an IDE), `featuremap`, `customcontrols`, `code` and `sample9` (all described + below). A setup is `{name, start, prepare}`. Every add-in a setup loads has its `SaveSetting` key snapshotted, emptied and restored (`tbDocsHelp`, `GlobalSearchAddIn`). - **Jobs**: `--jobs N` (default 6) runs setups at once, each IDE on its own claimed ports and private desktop, and splits the big setups into parts, longest first; a full run is about - 110 s (it was ~765 s sequential; the jobs add up to over 600 s, so six IDEs cannot do much - better). `--jobs 1` runs one IDE per setup in table order. Per-run + 200 s in both themes (the jobs add up to about 1,250 s per theme, so six IDEs cannot do much better). + `--jobs 1` runs one IDE per setup in table order. Per-run state lives on the connection (`c.shot`), never in module variables. - **Diffs**: `--diffs <dir>` (never under `docs/`) writes, for each picture that differs from the file on disk, the committed picture, the new one and an amplified difference map side by side (`scripts/lib/shot-diff.mjs`, `composeComparison` with `amplify` in `scripts/lib/png.mjs`). - **Shots**: a table of `{out, setup, take, annotate?}`, `out` the picture's path under - `docs/`; `--only` matches it. `take(ctx)` brings the IDE to the state and returns the clip - (or the parts of a composite); `annotate` is a list of primitives. -- Shared helpers move from the probe kit (`.claude/tooling-review-scratch/s103-shots/ui.mjs`): - `openMenu`, `hoverItem`, `closeMenus`, `waitModal`, `closeModal`, the cut-out, the snap. - -## Increments - -1. **Rename and the no-project setup**: `shoot_docs.mjs`; the device-scale fix and - `Lane.openNoProject`; cut-out capture; the 19 menu pictures, `Menu.png` and the 13 dialogs - (About, IDE Options, the two Manage dialogs x2, New Project's tabs, Recent x2, Components - message, the FAQ's New Project Options and Samples, `llvmdoc2`); pages get their - `{:width height}`; alt text and prose checked against each new picture (the Tools menu has - two new entries; About's text changed). Retake the help add-in's eight with the scale fix. -2. **Done: the overlay** (`scripts/lib/shot-annotate.mjs`), on the annotated pictures of the - same setup (`7e1cb69c` New tab, `6ad7a172` and `ccSampleProject` Samples tab, - `tbWebView2Sample0`). Labels and badges take the IDE page's own font, Segoe UI, since the page - has no Inter and none is injected; the dark pill is `#8E161A`. The anchors search the main - document only, so increment 3's tool windows need shadow roots added. `label`, `box`, - `underline`, `bend`, forks and `{code}` anchors work in a probe but are in no picture yet. -3. **Project setups**: Project Settings with a filter, the panels, Project Explorer, the - Global Search add-in (Sample 15), and their annotated pictures. -4. **Designers and code**: a form with controls, the CustomControls sample project, Compiler - Constants; the composites and the replica select lists. -5. **The feature map**, replacing `fafaloneIDEscreenshot1.png` and `014a1d28`. - -6. **The rest**: the pictures left out of increments 1-5, by the owner's disposition below. - -## Increment 6 (owner, 2026-10-09) - -**Increment 5 is done**: `Features/Images/IDE-FeatureMap.png` (2880x1800, setup `featuremap`, -fixture `test/shots/featuremap/` FeatureTour) replaces `fafaloneIDEscreenshot1.png` and -`014a1d28`, both deleted. Twelve labels, each anchored to an element or code span; Problems is -labelled Diagnostics, its 997 title. Sticky scroll, advanced hover info, inline hints, folding -always shown and the debugger's memory figures are switched on in the page only; the memory -figures, which change from run to run, are fixed in the page once real ones are shown, as -History's times are. `Services_Operational` was retaken (setup `project`). Two of the three -pictures added to increment 5 were left: **`Services_Operational_Tooltip` is the badge's native -`title`**, which no capture holds; **Fusion's 569099635**: the `settings-fusion` project only ever -writes `[BUILD] failed to build Fusion server file '...'` (on load and after a compiler restart), -never the "successfully built" line -- a possible IDE defect, or the lane's staged layout; not -reproduced elsewhere. - -After increment 5, 53 images under `docs/` are outside the tool; the unreferenced -`Documentation/Images/environment-variables.png` was deleted (`7d5b5633`). The two left from -increment 5 join the groups below: the tooltip the special IDE states, Fusion the running -programs (it needs a project whose Fusion host really builds). The owner's -disposition, group by group: - -- **Special IDE states** (`IDE/Status Bar.md`'s `Services_Limited`, `Services_Unavailable` and - the three badges' tooltips -- native `title` tooltips, which no CDP capture holds, so these need - another route --, `Licence_CommunityEdition`; `IDE/Splash Screen.md`'s `Splash_Screen`): **inspect - the IDE page first to see how each state can be reproduced**, then generate them. A state is - reproduced as the IDE reaches it, never by editing what it draws. -- **Pictures of a running program** (`Anchoring-Docking.md`'s 3fa1cf2b, 0aeb25f6, fddbffa9, - 4829696d, bc9f3756, 599a66ad, 80185a8d; `GUI-Components/New.md`'s 4ad9c774, 5fc60b7b; - `Forms.md`'s 85f25aa2; `Inheritance.md`'s b0724fe2; `CodeLens.md`'s 351d0147; `Debugging.md`'s - 021f6cbf; `Toolbar_4`; `Creating a TWINPACK package.md`'s 8d74d820 and packPublishComplete1; - `Fusion.md`'s 569099635, a successful Fusion host build): - **write the program or programs that illustrate what each picture depicts, and run them while - the pictures are taken.** -- **Package server** (the other Features/Packages pictures: Importing from TWINSERV, Importing from - a TWINPACK file, Linked Packages, Updating a package, packPublishPackage1): **network access is - fine. Never actually publish a package**; everything short of that is allowed. The - byte-identical pair d9f1e4d9 / e749e10f is still the open question below. -- **Not of the IDE**: retake what can be reproduced. A `.vbp` import uses a **synthetic `.vbp` - project**, and the path is **scrubbed** from the picture (16833fae). The others in the group - (`Documentation/Building.md`'s GitHub and Affinity screenshots, `IDE/Webpage.md`'s `Webpage`, - `FAQs.md`'s 94490c87 and ac019c1a, `Package-Server.md`'s 5951dab6) get the same treatment where - they can be reproduced. **`Tutorials/WebView2/Images/tbWebView2InAForm.gif` is skipped**, and - **`favicon.png` is never changed** by this work. + `docs/`; `--only` matches it or its `.light.png`, and selects the shot in both themes. + `take(ctx)` brings the IDE to the state and returns the clip (or the parts of a composite); + `annotate` is a list of primitives. +- **Two themes**: each job takes its shots in dark, then `backToPrepared` (no menu + or dialog, the tabs opened since closed with their changes discarded, the Tabs List's + Recently Closed as it was, and the keys the takes added to `ctx` dropped), the IDE switched to + Light by `tbTheme_SwitchToLightMode` (`ensureTheme`, settled by the panels' colour, the + page's images and fonts), and the same shots again: no second IDE start. A shot that leaves + something only it can undo puts it back itself: `Help_Window` attaches the help again with + the window's Attach, `addReport` opens the report the dark pass added rather than adding + another. A light picture with no file yet prefers a capture equal to the dark picture while + its first is within a capture's noise of it; one with the dark picture's pixels is not kept (a light + file there is removed); one more than 32 device pixels off the dark one's size fails, since + that is a wrong state, not the theme. `--diffs` names a file `<path>.dark…` or + `<path>.light…`; the run ends with a count per theme of each state. +- Shared helpers: `openMenu`, `hoverItem`, `closeMenus`, `waitModal`, `closeModal`, the cut-out, + the snap. +- **Retry a failure once before looking into it.** These setups fail now and then and pass on + a second run of the same selection: `package-server` ("typing fmt. opened no completion + list", "OLE Guid and interface definitions stayed ticked"; it needs the network), `help` + ("the pane did not show /tB/Modules/Interaction/MsgBox", the setup taking minutes instead of + about 90 s), `sample6` ("the Project Explorer has no row Sources" or "Packages"), and `web` + and `webpage`, whose pages are live and change of their own accord. A picture that fails the + same way twice is a real fault. +- **`Features/Images/a6525b1d...png` changes width from run to run** (1512 or 1514 px); its + light picture is narrower again (1498). Each comes out within the 32-pixel tolerance, so the + run passes, but the file can be rewritten for the width alone. + +## Standing rules for what a picture shows (owner, 2026-10-09) + +- **A state is reproduced as the IDE reaches it, never by editing what it draws.** The tool + inspects the IDE's page first to see how a state can be reached. +- **A picture of a running program comes from a program written to illustrate what the picture + depicts, run while the picture is taken.** A running program's window is taken once, as the + program draws it. +- **Network access is fine for the package-server pictures. Never actually publish a package**; + everything short of that is allowed. +- **A picture not of the IDE is retaken where it can be reproduced.** A `.vbp` import uses a + synthetic `.vbp` project, and the path is scrubbed from the picture. +- **Licence and LIMITED states are reached only by a route that never touches the user's real + licence**, or the old pictures stay. +- **Byte-identical pictures are one file**, each page keeping its own alt text. +- `Documentation/Building.md`'s "Editing screenshots" section points at `shoot_docs.mjs`. + +## The feature map + +`Features/Images/IDE-FeatureMap.png` (2880x1800, setup `featuremap`, fixture +`test/shots/featuremap/` FeatureTour) is a project-made full-IDE feature map. Twelve labels, each +anchored to an element or code span; Problems is labelled Diagnostics, its 997 title. Sticky +scroll, advanced hover info, inline hints, folding always shown and the debugger's memory figures +are switched on in the page only; the memory figures, which change from run to run, are fixed in +the page once real ones are shown, as History's times are. + +## Special IDE states + +- **`Services_Unavailable`**: the badge in the `no-project` setup. With no project the badge's + title reads each service `Not initialized`; all four read `Disconnected` only for the 0.1 s + between a compiler ending and the IDE starting the next. +- **`Splash_Screen`** is not a window of its own: it is the IDE's modal dialog + (`showSplashWindow`, HTML in the page), shown only when the IDE starts with no project and + closed after 2.5 s by a timer, which then shows New / Open Project. The shot calls + `showSplashWindow` itself and holds the one 2.5 s timer the call starts until the capture, + then runs it. It has a setup of its own (`splash`): the sponsors' logos are scaled one of two + ways by what drew them earlier in the same IDE (8,795 pixels apart; in `no-project` by whether + the start-up splash came before or after the 2x override, and in `project` About's light + picture changed with the splash taken before it), and the New / Open Project dialog its close + shows leaves the status bar's badges a pixel shorter for the shots after it. +- **`Licence_CommunityEdition`** (setup `community`): the IDE reads the key from IDESettings + into the page's `licenceKey` and passes it to each compiler it starts; the compiler answers + with the edition and hands the key back, and the IDE saves it only when it differs. The tool + sets `licenceKey = ""` in the page and restarts the compiler with the toolbar's button: the + registry value is never read, changed or removed by the tool, and the save call is one + `PAGE_DEFAULTS` has made do nothing. `doCommunityNagScreens` runs; its only screen is dated + 1-7 January 2026. +- **`Services_Limited`** (setup `community`): LIMITED is never a lasting state. The page draws it + for about 0.6 s while a compiler starts (its own connection up, the page then connecting FS, + LSP and the debugger one after another, once the compiler has loaded the project). Closing one + connection from the page is no route: the browser takes 60 s to close it (the compiler never + answers the closing handshake), the IDE then draws LIMITED for 0.1 s, the compiler ends, and + the IDE starts another. `Network.setBlockedURLs` does not stop a WebSocket, and throttling the + page's CPU (`Emulation.setCPUThrottlingRate` 40) holds LIMITED but makes every evaluation time + out. The shot holds the page's `fs.connect` call of a restart (the toolbar's button) until the + capture and then makes it as the IDE did. The restart's DEBUG CONSOLE line names the project's + folder, under the user's, and Clear does nothing until the compiler is connected, so + `nameUserAsUserIn(c, ".debugConsoleOuter")` replaces the name there in the page; the console + is cleared with its button once the compiler is back. +- **The tooltips** (`Services_Unavailable_Tooltip`, `Services_Operational_Tooltip`) are + **replicas** (owner, 2026-10-10). A native + `title` tooltip never appears on a private desktop: neither a CDP mouse move + over the badge nor a `WM_MOUSEMOVE` posted or sent to any of the page's three windows + (`Chrome_WidgetWin_0`, `Chrome_WidgetWin_1`, `Chrome_RenderWidgetHostHWND`) made a new + top-level window appear in 3 s, and the page saw no hover from the messages. Real input + reaches only the desktop on screen. So `tooltipOn` (`shoot_docs.mjs`) puts one `div` into the + page, holding the badge's own `title` read live, styled by `TOOLTIP_CSS`, placed as Windows + places a tooltip by the cursor (cursor taken at the badge's middle; the badge is at the + window's bottom, so it goes above), captured alone and removed, in the badge's own setups + (`project` for OPERATIONAL, `no-project` for UNAVAILABLE). Calibrated against the January + pictures (154x73 and 158x73 px) by rendering the same text in puppeteer's Chrome at 1x: Segoe + UI 12 px (9 pt), 16 px lines, `#242424` for the 1 px border **and** the text (the January + pictures' darkest text pixel is 36 grey levels, not black), 7 px side padding, 3 above and 4 + below, the width rounded up to whole pixels. Result in a 1x render with the January text: the + same size to the pixel and a mean luminance difference of 0.4 grey levels (maximum 9). In the + IDE at 2x, shown at half size, OPERATIONAL is 158x73 like the January picture (its text a + little softer than a 1x ClearType picture: mean difference 8 after a box-filter to 1x). + **The text is the IDE's live one**: with no project the badge's title reads each service + `Not initialized` (the January picture says `Disconnected`, the state for 0.1 s between + compilers), so the UNAVAILABLE picture is 155x73 and its alt text says Not initialized. The + replica does not follow the theme, so the Light pass takes the dark pixels and keeps no + `.light.png` file. +- **Window capture, for the running programs and the Webpage pane**: `desktopWindows(desktop, {children})` and + `captureWindow(desktop, hwnd, file, {flags})` in `tb-ide.mjs`, through `tb-launch.ps1`'s + `TBBUILD_WINDOWS` mode (a thread put on the desktop with `SetThreadDesktop`, per-monitor DPI + aware). On a private desktop `PrintWindow` with `PW_RENDERFULLCONTENT` (2) gives the IDE's + window as drawn, WebView2 content included, and Notepad with its Windows 10 frame; flag 0 leaves + the WebView2 areas white and black and draws a Windows 7 style frame. The picture is the + window's rectangle at the desktop's DPI (144 here, so 1.5x, not 2x), including the invisible + resize borders (`DWMWA_EXTENDED_FRAME_BOUNDS` would trim them). It returns the window's and + its children's texts for the user-name check; text a window draws itself is not among them. + About 0.5 s a call (a PowerShell start). The IDE's window class is `ThunderForm`; the Webpage + pane's second WebView2 is a `Chrome_WidgetWin_1` child (title `Google`) of zero size until the + pane shows. The capture also returns the visible frame (`DWMWA_EXTENDED_FRAME_BOUNDS`), the + client area and a layered window's key and alpha. `tb-launch.ps1` is 28,576 characters of the + 30,000 its environment variable allows. + +## Pictures of a running program + +- **The program's window is captured by the harness** (`captureWindow`), not by the program: + no fixture needs a capture module, and the frame is the system's. +- **Sizing.** The picture is the window's pixels at the desktop's DPI (144 here, 1.5x), cropped + to its visible frame (the invisible resize borders are left out), and shown at its size at + 96 DPI: `keep` takes a `scale` from the shot (`{png, scale}`), 2 for every page picture. + Not upscaled to 2x, which would add nothing. A run on a display at 100% writes 1x pictures + with the same `{:width height}`. A shot is `once: true`: taken in the dark pass only, any + `.light.png` removed; a job of them has no light pass. +- **Setup `programs`**: `test/shots/programs`, ShotPrograms, opened and built in the IDE; each + picture starts the exe on a private desktop of its own (`launchOnDesktop`) with its command + line naming the form, and the two take turns through files (`TB_SHOT_SIGNALS`: `shown`, + `grow`, `grown`, `end`). An anchoring picture is one window before and after the program makes + it 100 by 80 pixels larger (96 DPI), side by side 16 pixels apart on a transparent ground: no + size pill, no hatched desktop. **The foreground is not dependable on a private desktop**: a program started on its own takes + it and loses it again now and then, and one run from the IDE never gets it, so the form draws + its frame as active (`WM_NCACTIVATE`) whenever it is not the foreground window; and no control + takes the focus (`TabStop` off), since a focused one drew a caret or, now and then, a focus + rectangle. With both, repeated runs are unchanged. +- **85f25aa2** (Opacity 75, TransparencyKey red): `PW_RENDERFULLCONTENT` draws the key colour + black and every pixel opaque, so the tool reads the key's pixels from a client-only capture + (flags 1) and gives the picture the window's own alpha (191) and alpha 0 there: the page is + what lies under the form, a plain background in each site theme. +- **021f6cbf** is the project run from the IDE (F5) with `debugger.immediateMemoryInvalidation` + and `debugger.runtimeCommandLineArguments` "listview" in the fixture's Settings: a Win32 + ListView whose column headers get the pointer of a String freed on return (`DanglingPtr`) + shows rows of warning signs; built as an exe, the same code shows the old text. +- **CodeLens** (`b0724fe2`, `351d0147`; setup `codelens`, `test/shots/codelens`, CodeLensDemo): + the bar clicked for real, the editor above the DEBUG CONSOLE in a layout set in the page. The + line times (the page's clock for some lines, the compiler's for `Debug.Print`) and the time + taken are set in the page once shown. `Scale factor` prints what the lane's compiler reads, + 1.50 on this machine. +- **`Toolbar_4`**: setup `designer`, the sample run with Start (`context.codeExecuting`) and + stopped after. +- **`8d74d820`** (setup `package`) and **Fusion's `569099635`** (setup `fusion`): the paths the + console names are on `PACKAGE_ROOT`, `\tbshots` at the root of the temp folder's drive + (`Lane.open`'s `buildPath`; the fixture's `project.fusionBuildPath` in the staged copy), + removed at the end unless it was there before. +- **A Fusion host needs a Fusion reference that holds an ActiveX control.** `settings-fusion`'s + three Fusion references (Windows Script Host Object Model, Shell Controls and Automation, ADO + 6.1) hold no ActiveX controls; each on its own makes the host build fail, with no cause + given, in the lane's staging, with the IDE's placeholders, with the `Build` folder made first, + with `projects\` copied, as a package or a Standard EXE, win32 or win64. A reference to + MSCOMCTL.OCX (32-bit only, registered by VB6) with `fusionAllTo32` builds the host, win32 and + win64: `test/shots/fusion` uses it, so the shot needs MSCOMCTL.OCX registered. Not queued: + Fusion is documented for controls; a failure message that names no cause may be worth an + issue. + +## Package server + +Taken on BETA 1005, in both themes where the theme shows. + +- **Setup `package-server`**: the sample with `test/shots/packages` (Greeting, a class to type + `fmt.` into), signed out, TWINSERV's live list. Its APPDATA is `PACKAGE_ROOT\AppData\Roaming`, + a neutral root for the path the two linked-package questions name (`f48a7254`, `8cf72685`); + removed at the end. Every shot ticks what it shows and unticks it again before it leaves the + dialog (the IDE deletes the project's copy of an embedded package at once, BETA 1005 asks + nothing), and `packagesClean` empties the local packages folder (never the folder itself: the + IDE writes into it and does not make it, and an export fails with an alert) and fails a shot + when an earlier one left a package. Only `e2a65dfe` applies (the completion list needs the + compiler to have the package). +- **The linked-package sequence is one project**, not two: the IDE asks "Local Package exists" + whenever a package with a local copy is ticked (`g_UserPackagesInfo`), so tick, untick + Embedded (the IDE writes the copy), untick, tick again gives `f48a7254`, and Download it from + TWINSERV (EMBED) then Embedded unticked gives `8cf72685`. One IDE per setup could not run two + projects in order anyway. +- **`f66fb240`** (setup `settings-linked`): the sample with `test/shots/settings/linked.json`, + linking OLEGuids, and an APPDATA with no copy: as a project opened on a machine without the + package. Opening reaches it; Restart the compiler (from MEMORY) with the copy deleted hangs at + 5% then UNAVAILABLE for good, Apply Changes (from FILE) does not: an IDE defect, not yet in + BUGS-TO-REPORT.md. +- **`db4636f6`** (setup `package-check`): the IDE downloads only the newest build, and a package + imported from a file has no publisher, which the check needs. The tool downloads OLEGuids + 1.0.0.9 from TWINSERV by the IDE's own address, unpacks it into the staged project's + `Packages\OLEGuids` and adds the reference the IDE writes for a TWINSERV package: the check + on load says 1.0.0.10 is available. OLEGuids replaces WinDevLib in the page (WinDevLib + publishes weekly, so its line would change every run, and it is 23 MB). +- **`a1331a0e`** is the Enabled Libraries tab with the embedded package's tick ringed: BETA 1005 + no longer asks "Also remove the imported package ...", and `Updating a package.md` says the copy + is deleted at once. +- **`packPublishPackage1`** kept: `packagePublisherPublish` opens the sign-in form when no + publisher is signed in, before the confirmation box. +- **`e749e10f`**: the gear is the Project Explorer's (the old picture's toolbar gear is gone); + the dialog is shown whole with the gear beside it (2324 px wide; a smaller page does not make + the dialog narrower, it covers the gear). The full dialog fills Available COM References + (hidden, its text in the page), whose paths name the user: the shot waits for that list and + then renames the user in the page. +- **`f48a7254`** starts at the question's left edge: the row being ticked shows an animated + spinner no two captures hold alike. **`e2a65dfe`**: the completion list is the IDE's own + (`.debugConsoleEntryIntellisenseBox`), shows types as `...` until the compiler gives them + (now and then never: the dot is retyped), and is placed by the list the name before the dot + opened unless that is closed first. +- **Compiler crash, not narrowed**: about 3 runs in 10 of the setup, an Apply that embeds or + removes CSharpishStringFormater meets `NATIVE EXCEPTION: ACCESS_VIOLATION {no-basic-code} + [twinBASIC_win32.dll+0010BD3F]`; ten probe cycles of the same steps did not reproduce it. Not + queued. `applyChanges` clears the DEBUG CONSOLE first, so one crash does not fail every later + Apply, and prints the crash lines. +- All pictures are page captures, so the page-text check for the user name covers them. + +## Not of the IDE + +Taken on BETA 1005. + +- **`16833fae`** (setup `import`): the sample open, and `test/shots/vbp` (ImportDemo: a form, + a module, `..\Shared\modShared.bas` for `{PARENT-FOLDER}`) staged with CRLF on + `PACKAGE_ROOT\Projects` (the path the dialog shows, `C:\tbshots\Projects\ImportDemo\ImportDemo.vbp`), + with a `.res` the tool writes (`scripts/lib/res-file.mjs`: three icons and the fixture's + manifest). The IDE's Open dialog is native, so the take calls `importFileByPath` with the + file, what the IDE calls with the file chosen; the form's two files are ticked with real + clicks. Cancel ends the import's compiler and the IDE then restarts its own, writing a + DEBUG CONSOLE line that names the work folder: the take waits for the restart and clears the + console, or the light pass's page fails the user-name check. Both themes (the title bar). +- **`IDE/Images/Webpage.png`** (setup `webpage`): the pane's page is a second WebView2 on the + IDE's DevTools port (its own `page` target, title the page's). The take sets + `liveWebpanelSettings` in the page (the IDE's defaults, zoom 0.7, with + `https://docs.twinbasic.com/`; the user's saved settings are read by the IDE but never shown + or written), floats the pane at 340x500, captures the pane's target with + `setDeviceMetricsOverride` at the body's size less 2 (the host puts the WebView2 one pixel + inside it, `syncPosition`) at 2x, and pastes it into the page's capture of the pane. The page's + `prefers-color-scheme` follows the picture's theme, so the dark picture shows the docs dark. + The IDE itself draws a host screenshot into the pane when something covers it + (`AskForAdditionalWebview2Screenshot`, `checkObscured`), at the host's 1.5x: not used. + The target's text is searched for the user name with the page's. +- **`94490c87`, `ac019c1a`** (setup `web`): no IDE; puppeteer's Chrome started by + `launchOnDesktop` on a private desktop (not headless, so its user agent is the ordinary one) + with `CalculateNativeWinOcclusion` and the backgrounding of occluded windows off: on a desktop + nobody sees, Chrome otherwise draws no frame and a capture waits forever or holds a blank page. + Both `once`, at 2x, light. A clip is in document coordinates (`pageClip`): nothing else in the + tool scrolls. Microsoft's page rebuilds itself after `load` and puts its scroll back to the + top, so `scrolledTo` waits for the page to stay still and scrolls until the element stays + put. Its cookie banner is at the top of the page and outside the crop; nothing is clicked on + it. The arrow and ring are the overlay's. The GitHub page is the repository's release list, + whose first release is the newest (`releases/latest` skips pre-releases); Assets is opened + with a real click. Its numbers (size, "yesterday", the version) are the day's, so the FAQ's alt + does not name them; its install-size entry says about 40 MB and 120 MB (the BETA 1005 folder + less the runtime's `.WebView2` data folder). +- `test/addin/help.test.mjs` fetches `/favicon.png` as its sample PNG, so `favicon.png` must stay + a PNG. ## Machine state in the pictures (owner, 2026-10-08) @@ -240,49 +469,42 @@ disposition, group by group: - **About's licence line**: the tool waits for `licenceIsSet` before opening About, so it shows the edition, never NOT READY. - **The language tick** stays on whatever language the IDE runs in. -- **`llvmdoc2`** shows the default thread count, 1 (the old picture's 10 was its author's - setting); the LLVM page's prose is checked for any reliance on 10. +- **`llvmdoc2`** shows the default thread count, 1 (the picture's earlier 10 was its author's + setting). -## Increment 3 (owner, 2026-10-08) +## Project-setup pictures (owner, 2026-10-08) -Survey of its 61 pictures and 17 glyph crops: `.claude/tooling-review-scratch/s103-shots/INC3-SURVEY.md`. -Built in three batches, one after another: no-project panels; a Standard EXE fixture with Project -Settings; Sample 6, Sample 15 and Global Search. **All three batches are done** (`b958f18a`, -`079185d0`, `74c6ed4d`, and batch 3 after `f916a99c`). In BETA 997 Sample 6's control images -are in the CustomControls package's Miscellaneous folder, not the project's, and the tutorial -says so. +Project Settings, the panels, Project Explorer, Sample 6, Sample 15 and Global Search. In +BETA 997 Sample 6's control images are in the CustomControls package's Miscellaneous folder, not +the project's, and the tutorial says so. - **History's times are fixed in the page**, as the recent lists are, and its project is a - project-made fixture; left out only if the times cannot be overridden. -- **The glyph crops are retaken** (element clips, 2x, shown at half size). + project-made fixture. +- **The glyph crops** are element clips, 2x, shown at half size. - **A machine or account value becomes a neutral one** (Publisher blank or a project-made name; the COM list shows this machine's, under the username check), and prose that names the old value is changed to match. -- **Old-design pictures are retaken in today's look**, with alt text and prose rechecked. - -## Increment 4 (owner, 2026-10-09) +- **An old-design picture is retaken in today's look**, with alt text and prose rechecked. -Survey: `.claude/tooling-review-scratch/s103-shots/INC4-SURVEY.md`, four batches (designers and -Format menu; Properties and Toolbox; replicas and composites; code views). +## Designers, code and composites (owner, 2026-10-09) -- **Composites** lay their parts out on the IDE's dark background (not white), with a gap, code - on the left and the panel on the right, one red arrow across. +- **Composites** lay their parts out on the IDE's background in the picture's theme, with a gap, + code on the left and the panel on the right, one red arrow across. - **Replica select lists** take one style: a white list, 1 px grey border, the IDE's font, the current option on mid-grey `#6E6E6E` with white text. - **The CustomControls tutorial's pictures** come from a project-made fixture, `test/shots/customcontrols/`, referencing the CustomControls package, with a class using the tutorial's names (`MyField`, `MyEnum`); the Toolbox may list two grids. - **No mouse pointer** is drawn in any picture. -- (Main session) The designer pictures come from a `designer` setup that stages files onto the - sample fixture at run time; controls are put on forms by hand-written `.tbform` files. -- **Batch 1 is done** (designers, the Format menu, Toolbar_3). The report is made in the page by - Sources > Add > Add Windows Report. Toolbox_Report is the *docked* Toolbox: a floating one - lists every tool even with a report active (`reportDesignerMode` is set on `#bodyInner`, which - a floating panel is outside) -- a possible IDE defect, not yet reproduced or queued. After the - Format shots the designer marks frmControls changed with no control moved (cause unknown), so - those shots close it with Discard Changes. -- **Batch 2 is done** (the Anchors pair, the QR code beside the Toolbox, the FAQ's ICON crop, the - WebView2 tutorial's Toolbox and PROPERTIES). Setup `forms` is `designer` without the add-in; +- The designer pictures come from a `designer` setup that stages files onto the sample fixture at + run time; controls are put on forms by hand-written `.tbform` files. +- **Designers and the Format menu.** The report is made in the page by Sources > Add > Add + Windows Report. Toolbox_Report is the *docked* Toolbox: a floating one lists every tool even + with a report active (`reportDesignerMode` is set on `#bodyInner`, which a floating panel is + outside) -- a possible IDE defect, not yet reproduced or queued. After the Format shots the + designer marks frmControls changed with no control moved (cause unknown), so those shots close + it with Discard Changes. +- **Properties and Toolbox.** Setup `forms` is `designer` without the add-in; `settings-webview2` also stages `test/shots/designer/webview2/` (frmWeb, a WebView2 `Web1` with no DocumentURL, so the designer loads nothing), since only that setup references the package. `MyOwnIcon.ico` is the template's icon copied at run time. 8611d12a's panels are put @@ -291,10 +513,9 @@ Format menu; Properties and Toolbox; replicas and composites; code views). In 997 PROPERTIES is grouped by category only (Anchors in LAYOUT, WebView2's own properties in GENERAL), and **a property's description is never shown at the panel's foot**: it goes to the designer's own `.proprtiesBoxFooter` inside the code panel, not the tool window -- a possible - IDE defect, not reproduced or queued. The WebView2 tutorial now points to the class reference + IDE defect, not reproduced or queued. The WebView2 tutorial points to the class reference instead. A floating Toolbox for a form lists what the docked one does. -- **Batch 3 is done** (Editor.png, FindReplace_Direction, 2a1c71fd, 4c8b881e and the six - CustomControls property-sheet pictures). **Replicas** are the overlay's `list` primitive: the +- **Replicas and composites.** **Replicas** are the overlay's `list` primitive: the select's options, current index and font, drawn as SVG at its bottom-left in the owner's style, a scroll bar when only a window of rows shows; its rows are anchors. **Composites** are `scripts/lib/shot-composite.mjs`: each part captured alone (a code part is the real editor @@ -306,9 +527,7 @@ Format menu; Properties and Toolbox; replicas and composites; code views). ` = 42` in the page and reopens the form: an open designer never takes a new default and shows no RESYNC (the page's TIP says it does; only a default value was tested). A custom control's Left/Top/Width/Height read in twips, its `PixelCount` fields in pixels. -- **Batch 4 is done** (the Compiler Constants pair, flags-attribute, the CustomControls code - crops, ClassId insert, the ICustomControl hover, the custom property, the form JSON, - tbWebView2CreateEvent). `codePart` wraps `codeLines` (any open file; `include`, `extraRight`, +- **Code views.** `codePart` wraps `codeLines` (any open file; `include`, `extraRight`, `annotate`, `hover`, `view`, `bare`). Setup `code` stages `test/shots/code/` (ConstantsDemo, FlagsDemo); `buildConfiguration` changes `buildConfigSelector`, waits for the greying to move, and clears the DEBUG CONSOLE, whose compiler-restart line names the user's folder. Inline @@ -318,7 +537,262 @@ Format menu; Properties and Toolbox; replicas and composites; code views). the file's text, so `frmCustomJson.tbform` holds `"MyField": 0` (in `frmCustom` it would override `= 42` in 1b). Setup `sample9` opens Sample 9, whose `WebView_Create` is unchanged. -## Open questions - -1. **The byte-identical pair** `d9f1e4d9` / `e749e10f` (Features/Packages/Images): merge into one - file when it is retaken (recommendation). +## Pictures as SVG + +`--svg` writes each picture a second time, as `X.svg` (and `X.light.svg`), drawn from the page +the PNG was just captured from, and the build shows it in place of the PNG +([WIP.Build.md, Pictures as SVG](WIP.Build.md#pictures-as-svg)). + +**Owner's decisions (2026-10-09 and 10):** + +- Text is real text, in per-picture subsets of the fonts the IDE drew it with, cut by fontTools + (`scripts/subset_font.py`), hinting kept. Glyph outlines were rejected (they render poorly); + harfbuzzjs was tried and removed ("don't mess with harfbuzz"). +- The PNG and the SVG are both committed. One SVG per theme: a single SVG switching on CSS + variables and a `#light` fragment was proposed and dropped. +- Pages name `X.png` and the build substitutes; `png` in the attribute block keeps the PNG. +- `picture_svg` in `_config.yml` per output: online and book on, offline (and so the help + archive) off. +- No `<picture>` fallback for old browsers; the per-picture `png` instead. + +**How a picture is drawn** (`scripts/svgshot/`): + +- `capture.mjs`'s `svgOfPage` reads, while the page is as captured: a DOM snapshot (layout, text + fragments, computed styles, paint order); then, from a measuring layer added after it, each + font's ascent and the fonts Chromium drew each run with (`CSS.getPlatformFontsForNode`); the + canvases' pixels, the web fonts' bytes, the annotation layer as XML; and the styles of each + scrolling box's scrollbar parts. +- `snapshot-svg.mjs`'s `renderSvg` makes one paint item per layout object, in the snapshot's + paint order. Boxes are snapped to whole CSS px, since the IDE lays out at 1x and is only drawn + at 2x; text, and the text a control draws, keeps its exact position. What the snapshot lacks is + drawn as replicas of what Chromium paints: a closed `<select>`, a checkbox and a radio button, + an input's and a textarea's text (scrolled), and scrollbars styled with `::-webkit-scrollbar`. +- **An embedded image keeps its pixels and any notice of whose it is, and nothing else** + (owner, 2026-10-10). `renderSvg` passes each through `compactImage` from + `scripts/lib/compact-image.mjs`, which `scripts/imagestrip/` matches; what it keeps and why + is in [WIP.Build.md](WIP.Build.md) beside `imagestrip`, and `test/compact-image.test.mjs` + tests it. The icons whose sRGB profile it drops decode to the same pixels in Edge 155 and + Chromium 148; a `gAMA` 1.0 control does not. The 318 by 346 pixels of a 16-pixel icon such + as tB-Red are the IDE's own asset at its own size, and stay so (owner, 2026-10-10). +- `fonts.mjs` gives each run's characters to the font files Chromium used for them, and + `subset_font.py` cuts each file to the characters drawn, as WOFF2. +- `diff.mjs` renders the SVG as an `<img>` at 2x and compares it with the PNG by luminance and + alpha, a shift of one device pixel allowed, so that the PNGs' ClearType fringes mostly cancel + out. +- **The SVG's size is the PNG's.** A clip half a CSS pixel past a whole one (a menu 168.5 wide) + is captured a device pixel short of it, 168 wide, from the same origin; an SVG of the whole + clip shows the half of the edge the PNG cuts off. That column alone put a small picture past + the bar (a 16-pixel icon at 11%, a menu at 0.5%), so `capture` gives `svgOfPage` the clip + with the PNG's width and height. +- **A converter change is tried with `replay.mjs`, not a retake** (owner, 2026-10-10). + `capture.mjs`'s `collectBundle` reads everything the converter needs from the page into a + *bundle*: the snapshot, its style names (positional, so a bundle carries its own list), and + per document the measuring layer's answers, the runs' platform fonts, the web fonts, the + scrollbar styles, each inline `<svg>`'s markup and a frame's viewport and zoom. A web font is + kept as the `@font-face` rules of its family that some run was drawn with (`webFonts[key]`, + in the page's order, each `{family, range, lo, hi}`) and the files' bytes once by name + (`fontFiles`), so `webRulesFor` picks a character's file at replay as it does at capture; + an inline `<svg>` is `svgs`, pairs of a layout object's key and what `inlineSvg` read. The + annotation layer's box is already in document coordinates in its markup, so a scrolled page + needs nothing more. `bundle.mjs`'s `renderBundle` draws the + SVG from it alone, and `svgOfPage` is the one then the other, so a replay with the converter + unchanged gives the same bytes. `--svg` saves every picture's bundle, faithful or not, in + `.svgshot-bundles/` (gitignored, never under `docs/`: a bundle holds the user name), **one + file per picture holding both themes** (`X.bundle.json.br`, `{dark, light}`, the dark one's + text first), **Brotli** at quality 9 with a 16 MB window: the light bundle is mostly the dark + one, which a long window finds and gzip's 32 KB does not, so the 370 bundles went from 57.7 + MB gzipped to 21.8 MB in 189 files, about 115 KB a picture. A light picture not kept (the + same as the dark) has its slot dropped (`dropBundle`). `trimSnapshot` cuts the snapshot to the clip (the layout objects near + it, every ancestor's, a frame's document only when its `<iframe>` is kept, `<style>` and + `<script>` text emptied, unused strings dropped; every DOM node kept), 97 MB to 58 MB for + the 370, and all 370 draw the same SVG trimmed as whole. A trimmed layout object keeps its + old index in `layout.originalIndex`, which its key is made of: the scrollbar styles + measured at the capture are found by it. **A replay takes about 20 s for the 370** (it + took 3.4 min at first): an SVG that comes out byte-identical to its file beside the PNG its + bundle names is not compared again (`--recheck` compares all); the fonts' cuts, about two + thirds of an SVG's time, are kept in `.svgshot-bundles/subsets/` under a hash of the font + (file, face, size, time, or a web font's bytes), its weight, the characters, the hinting and + `subset_font.py` (`fonts.mjs` `useSubsetCache`; `shoot_docs` uses it too); and `--jobs` + are processes, each with a browser started with `--disable-gpu`, half the logical + processors and at most 8 by default. Drawing is synchronous, so one process gains nothing + from more pictures in flight; with the GPU on, every browser shared it and four processes + were as fast as fourteen, and fourteen timed a screenshot out. A bundle answers only what the converter asked at capture; a question + it has no answer for is a *miss*, counted by `renderBundle`, and replay lists a picture whose + misses are new since its capture, whose PNG hash changed, or whose bundle lacks a style now + read, with the `--only` command that retakes them. **A change to what is read from the page + that no miss shows raises `BUNDLE_VERSION`**: version 2 (the web font rules, `fontFiles`, + `svgs`) does not read a version 1 bundle, and replay lists every one as needing a retake + (the 370 of this cache did). Reading version 1 would replay a split font with the one file it + kept, and a picture with an inline `<svg>` without it, and nothing in a version 1 bundle + says which pictures have either. Answering misses offline in headless + Chromium, and drawing bundles in `tbdocs`, are later steps, not done: the second needs the + bundles committed, so cleaned of the user name, and complete, since CI + has no Windows fonts to measure with. +- **A filter of colour functions is baked, not drawn** (`colour-filter.mjs`; owner, 2026-10-10). + Chromium's PDF backend cannot express an SVG `<filter>` and draws the whole filtered group as a + 300 dpi bitmap, text included: the book lost the text of 44 of its 110 SVGs that way. Every CSS + filter function but `blur()` and `drop-shadow()` is an affine colour map, which commutes with + antialiasing, so mapping each fill, stroke, gradient stop and text class under the group paints + the same picture on screen, to a channel's rounding; `opacity()` becomes the group's opacity. + A real filter stays only round an embedded image (a `<g filter>` def wrapping it) and over a + group that blends or already holds a real filter group. A `drop-shadow()` last in the list + (the IDE's toolbar codicons glow with one) is split the same way: the shadow alone, from a + copy of the group under a filter that blurs its alpha, then the group over it unfiltered, + which is what the filter's own merge paints. One SVG for screen and book; two + variants were considered and dropped, since the bake is exact. Blurred shadows stay bitmaps in + the PDF, which loses nothing. +- **The bar** (`SVG_FAITHFUL` in `shoot_docs.mjs`): an SVG with more than 0.25% of its pixels 96 + grey levels or more off is not written, and an older one is removed. Anything not drawn falls + to it. +- **One line** (`oneLine`). Git checks a picture's SVG out with CRLF where `core.autocrlf` says + so, since no `.gitattributes` rule can tell it from a diagram's SVG. An SVG icon embedded as + text and the annotation layer's XML hold line breaks; so each is written as the space an XML + parser makes of a break inside a tag, or ` ` in text, the same character. A checkout then + changes only the last line ending, which `keepSvg` ignores when it compares. + +**Scrollbars.** The IDE styles every scrollbar with `::-webkit-scrollbar` rules +(`ide/styles.css`, coloured by theme variables), a second family under `.modalDialogContainer`. +`getComputedStyle` cannot name a scrollbar part or a state such as `:vertical`, and a rule's text +cannot be copied onto an element of the tool's own: Chromium drops a `var()` shorthand from +`cssText` once a longhand overrides part of it, as the thumb's `background-clip` does its +`background: var(...)`. So `scrollbarParts` gives each rule that styles this element's +scrollbars, for a moment, one more selector, matching a probe element per part with the +specificity of the selector it stands for; Chromium cascades and resolves the `var()`s, and the +rules are put back. The parts are laid out as Chromium's custom scrollbar theme does: a button's +length its `height` (vertical) or `width`, `auto` being the platform's scrollbar thickness, which +the page measures; the thumb as long as its share of the content, never shorter than its own +length, whose `auto` is 17 px (Windows' thumb length at 96 DPI; no page property shows it, and +the bench checks it); its place from the snapshot's scroll offset. A corner rule left with no +declaration (the IDE's `background-color: transaprent`) still gives the corner a style of its +own, transparent. The buttons' arrows blend into the track with `background-blend-mode: +hard-light`, drawn as an isolated group with `mix-blend-mode`. + +**Drop-down lists.** The label is set from the box's exact position: a table cell can put a +select at x.5, where Chromium snaps the border to the pixel grid but not the text. It sits in the +content box less 4 px at the start and the arrow's 16 px box at the end, aligned by +`text-align-last`, else `text-align` (a select does not take it from its cell), and is cut at +that box. The chevron is 8 by 4 px, stroked 2 px wide, centred in the 16 px box at the padding +box's right edge whatever the padding, its top 2 px above the box's middle rounded down: fitted +against Chromium's own arrow in Edge. + +**Underlines** (`decoratingBoxes`, `underlineBand`, `inkBand` in `snapshot-svg.mjs`). A text +decoration is not inherited: the converter walks up from the text and takes the line of every box +that sets one, through blocks, stopping after an atomic inline box, a float or a positioned box. +Fitted in Edge 155 on `bench/decoration.html`, in whole CSS px since Chromium snaps the line at +1x: the baseline rounded; an auto thickness `max(1, floor(size/10))`, a given one rounded; an auto +offset `max(1, ceil(size/10/2))` below the baseline, a given one rounded with no gap of its own +(the package-server tip is 13 px with the IDE's `text-underline-offset: 2px`, so 2 px down, 1 +thick); the sizes and the colour (`text-decoration-color`) from the box that sets the line. Chromium +cuts a gap round each descender, as wide as the unrounded thickness on each side; `capture.mjs` +finds the ink by drawing the run 16 times as large on a canvas in the measuring layer +(`convert.mjs`, which runs no measuring layer, draws lines whole). Reported as not drawn: +overline, line-through, every style but solid, and `text-underline-position` other than auto. +`test/svgshot.test.mjs` holds the fitted figures. + +**A background image is sized by its bytes** (`naturalSize`), not by the type its URL names: +the IDE calls a JPEG and a BMP `image/png`. The About dialog's EXCALIBUR logo is such a JPEG, +drawn with `background-size: contain`; read as a PNG, its JFIF header gave 4,718,592 by about 4.3 +billion pixels, and the logo would come out 0.054 px wide. + +**Frames** (owner, 2026-10-10: cross-origin frames in general). A frame of the same site (the +docked help pane: the documentation on another `localhost` port) is in the IDE's process, and +`DOMSnapshot.captureSnapshot` returns its document beside the page's: the `<iframe>` node's +`contentDocumentIndex` names it, and its boxes are in its own coordinates. `decodeSnapshot` +decodes every document and hangs a frame's on its `<iframe>` node (`node.frame`); `renderSvg` +draws it at the iframe's place in paint order, as one group moved to the iframe's content box +less the frame's scroll, clipped to it, scaled when the frame is zoomed (its `innerWidth` against +the box; `unzoom` puts boxes Chromium gives in the parent's px back into the frame's), over its +canvas colour (the root's background, else the body's, else none) and under its viewport's +scrollbars. `svgOfPage` measures each frame in an isolated world of its own +(`Page.createIsolatedWorld`), so its `@font-face` rules and styles apply; one `PictureFonts` +serves the whole picture. A frame of another site is a target of its own and is not in the +snapshot: it is reported as "a frame of another process" (Help_Window's, the Webpage pane's). +Not done yet, and the way to do it: attach to that frame's own target (`Target.setAutoAttach` +with `flatten`, or the targets `shoot_docs` already finds for the PNGs), take its snapshot and +measure it there, and nest it at its `<iframe>`'s content box as a same-site frame is +(`DOM.getFrameOwner` names the `<iframe>` of a frame id). A bench page gets such a frame from +an `<iframe>` on `127.0.0.1` while the page is on `localhost`: another site, so its own process. + +**Web fonts** are found by CSS font matching on the run's `font-family` list, style and weight +(`webFontFiles`, in the page), not by the face's own name: the site's Inter calls itself "Inter +Variable", and the last rule of a family was its italic file. A family split by +`unicode-range` (Microsoft's Segoe UI Variable: five files per weight, Latin, Latin Extended, +Cyrillic, Greek, symbols) gives a run every rule whose range holds one of its characters +(`webRulesFor`, the last such rule per character, as CSS does), only those files are read, and +`PictureFonts.css` gives each file only the characters in its own range even where its cmap +has more. A variable WOFF2 is read in `fonts.mjs` and cut at each weight the picture uses, +held to the weights its rule declares (`subset_font.py`'s `weight`); its other axes stay +variable. A font fontTools cannot cut gets no face and its text falls back, reported +as "text in a font that could not be cut" (`subset_font.py` answers that job with an error +instead of failing the run). + +**fontTools is patched** for that weight cut (`patch_gvar_subset` in `subset_font.py`). Pinning +one axis of a font that keeps another (Segoe UI Variable: `wght` and `opsz`) leaves `gvar` +without entries for the glyphs left with no variation, which the rest of fontTools reads as +none; the subsetter's `gvar` step (4.65.0) looks every kept glyph up and fails with `KeyError +'.notdef'`. The patch reads a missing glyph as none; with no axis pinned it writes the same +bytes as fontTools' own. Delete it once fontTools does the same (no issue filed upstream yet). + +**The platform's own scrollbar** (`paintNativeScrollbars`) is the Fluent one Edge and the +IDE's WebView2 draw (on this Windows 10 machine too): 15 px, a round-ended 9 px thumb, arrows +9.5 by 4.75, `scrollbar-color` where it is set, else the +light or dark default by `color-scheme`; the docs site's dark theme sets `scrollbar-color`, which +turns its own `::-webkit-scrollbar` rules off. **A focus ring** (`outline-style: auto`, +`paintFocusRing`) is a 2 px band in the outline colour and a 1 px white one outside it. Both were +fitted in Edge on bench pages of their own. + +**An inline `<svg>`** in the page is drawn as a nested `<svg>` in its box's content box, in its +place in paint order (`paintInlineSvg`, from `paintBox`), so a CSS transform, opacity, border and +padding apply as to any box. `inlineSvg` (`capture.mjs`) reads it in the page, in a frame's +document too: its subtree with each element's styles resolved (paint, stroke, opacity, +visibility, transform with its origin and box, clip, mask, filter, markers, text and font; a +paint written `currentColor` stays so, and an inherited property is written only where it +changes, so a `<use>`'s target inherits from the `<use>`), what it refers to outside itself +(sprite symbols, gradients, clip paths, masks, markers) copied into a `<defs>`, its ids +scoped per svg (`scopeIds`), its `<image>`s through `compactImage`, its `<text>` in the picture's +faces. As in Chromium, a gradient, clip path, mask or marker inside a `display: none` tree is +not found by `url()`, and a `<use>` still finds its target there. **The annotation layer** is +`position: fixed`, so on a scrolled page its box is moved by the scroll into the document's +coordinates, which the picture's `viewBox` is in. + +**The bench** (`node scripts/svgshot/bench.mjs`, outside every gate, needs Edge and fontTools) +draws the pages under `scripts/svgshot/bench/` (scrollbars, native scrollbars, drop-down lists, +underlines, focus rings, frames, a web font split by range, inline SVG, a scrolled page with an +annotation layer, SVG in frames), served from `127.0.0.1` with the site's fonts under `/fonts/` +(a `file:` page cannot `fetch` its font bytes), with the converter +and compares them with the browser's own picture: in Edge, whose Chromium is the IDE's WebView2's +(Puppeteer's trails it, and draws the select's arrow otherwise), laid out at 1x and drawn at 2x +as the IDE is, text without ClearType. Each page is held to the figures recorded in +`bench/baseline.json` (`--update` records them): no page comes out exact, and a fixed limit +missed a fault fixed while the bench was written (arrows a quarter too tall moved a page from +0.025% to 0.064%). `node scripts/svgshot/pixels.mjs <png> x y w h --beside <png>` prints a +rectangle of two pictures as letters, one per colour: how an edge, a stroke or a blend came out. +A new replica gets a page there. + +**Not drawn yet:** a thin platform scrollbar (`scrollbar-width: thin`), a zoomed frame's +scrollbars, a textarea's resize grip, a number input's spin buttons, an indeterminate checkbox, +conic gradients, the 2011 `-webkit-radial-gradient`, `filter: hue-rotate`, an inset shadow's +blur, a search input's cancel button (the help pane's `#helpSearch`), and a frame of another +process: Help_Window's and the Webpage pane's (which `webpage` pastes into the PNG from its own +capture), so those pictures stay PNG. In an inline `<svg>`: a `<foreignObject>`, an `<image>` +the page cannot read, a `<use>` of another file, a CSS `mask` or `clip-path` on the root, text +decoration, geometry set by CSS (`d`, `width`, `x`), and animation (the state at the snapshot +is drawn). A duplicate id in one document resolves inside the svg first, where the browser +takes the document's first. The WebView2 download page's Version arrow is an `unsteady` +region. Drawn wrongly: a collapsed table +border, doubled (the IDE has none); text is set on its exact baseline, where Chromium rounds +the baseline to a whole CSS pixel (a device pixel off at most; an underline uses the rounded +one). A canvas is a raster (the editor's minimap is drawn with +`putImageData`). The run reports each under "not drawn". + +**Open, the owner's** (2026-10-11): +- The SVGs are to be checked in browsers other than Chromium's, with an online tool; leave + them as they are until then. +- fontTools' `gvar` subsetting bug, worked round by `patch_gvar_subset`, is to be reported + upstream: one line in `subset/__init__.py` (a glyph missing from `gvar.variations` read as + having no variation), with a reproducer on a font with two axes under a free licence, such as + Inter 4's variable file, never a Microsoft font. +- Whether the licences of the Microsoft fonts the pictures embed subsets of allow publishing + them in the SVGs. Their embedding flags (OS/2 `fsType`) allow it: Segoe UI (regular, bold, + semilight), Consolas, Segoe MDL2 Assets, Segoe UI Symbol and the web's Segoe UI Variable are + 0x8, editable embedding; Cascadia Mono is 0x0, installable. The flags are not the licence. diff --git a/WIP.Search.md b/WIP.Search.md index d4f1e9c4a..4dad4cf8c 100644 --- a/WIP.Search.md +++ b/WIP.Search.md @@ -469,7 +469,7 @@ the token `do`, which existed nowhere in the index. Many twinBASIC keywords are English stop words, so every keyword query missed its own entry (`Do` ranked 18th, `For` 17th, `With` 55th, `Is` 177th). The fix is one line inside the `lunr(function(){...})` builder: `this.pipeline.remove(lunr.stopWordFilter);`. -Every keyword query it fixes goes to rank 1, and the fix isn't specific to +Every keyword query goes to rank 1, and the fix isn't specific to twinBASIC's keyword list. The cost: a few short, extremely common tokens (mostly other `vbXxx` constant name prefixes and `Index`/`Lock`/`Column`/`Time$`) crowd into more results; see "Known regressions" above. @@ -614,7 +614,7 @@ experiments' copy is `eval/search_quality.mjs` records its ground truth in the baseline as a label (`intent-1` to `intent-5`), so `--compare` can tell a changed ground truth from -a changed ranking. Each set below was added to the label in turn. +a changed ranking. Each set below names the label it counts under. - **Bare names** (`intent-1`), judged by the tiers above, and **qualified names** (only that symbol's URL counts), both derived from @@ -1022,9 +1022,8 @@ behaves here". set's elements plus the second's, an element in both counting twice), which `intersect()` uses to choose the set it walks, so even the order of a set's elements is lunr's. Results, refs and scores are unchanged, checked over - 10,292 queries, one per page or section title; they took 718 s in the - replica before (worst 4,068 ms) and 86 s after (7 over 200 ms, worst 331 - ms), and `a page` fell from 809 ms to 87 ms. The token-set guard's install + 10,292 queries, one per page or section title; they take 86 s in the + replica (7 over 200 ms, worst 331 ms), and `a page` takes 87 ms. The token-set guard's install check allows a second install after `separateTokenSetKeys()`. - **`pinIndexFieldLengths()`** pins the `index` field's average length at 1. - **`buildIndexInSlices()`** builds the index in slices (Design §5 C). @@ -1205,13 +1204,12 @@ it was measured while a lone clause's boost cancelled out, so it never had a fair test. Done with field boosts, as the `primary` field, it took tier-order violations to zero. Don't rebuild X1t without a new idea. -The measurement also blamed the CheckBox control's `names` field for not -listing every member, which was false: both entries' `names` are the single -word `CheckBox`. The real difference: the CheckBox page's top entry -(`/tB/Packages/VB/CheckBox/`) has 1 character of content, since a class's -introduction sits under its own heading (`#checkbox-class`), while -`DTPicker › CheckBox` has 467 characters that mention "checkbox" several -times. +The explanation that CheckBox's `names` field lists too few members was false: +both entries' `names` are the single word `CheckBox`. The difference is the +content: the CheckBox page's top entry (`/tB/Packages/VB/CheckBox/`) has 1 +character, since a class's introduction sits under its own heading +(`#checkbox-class`), while `DTPicker › CheckBox` has 467 characters that +mention "checkbox" several times. ### Known misses and limits @@ -1255,8 +1253,8 @@ The counts are in "Resuming this work"; the diagnoses are here. setting without linking it, and twinBASIC-Additions' changelog anchors outrank the Features pages for `inline assembly`. - **The builder docs are in the reader's search**, and may crowd other - site-tooling words: `dark mode`, before its entry, put the site's own build - docs (Documentation/Development) first; `compile to exe` puts + site-tooling words: `dark mode` needed an entry to keep the site's own build + docs (Documentation/Development) off rank 1; `compile to exe` puts `TbExpressionService.Compile` first. Noted, not changed. - **Open content gaps:** `COM interop` (no clear target: Categories' COM and Automation list, Interfaces-CoClasses, ActiveX Registration), subclassing diff --git a/WIP.Typography.md b/WIP.Typography.md index b9c75b22c..1d7d95b7e 100644 --- a/WIP.Typography.md +++ b/WIP.Typography.md @@ -35,7 +35,7 @@ centre far enough to intersect a neighbour. The link is sized outright clearance. Two rules follow. **Run the full sweep after any change that moves type -metrics**: the thirteen-page sample stays clean through such a change and would +metrics**: the fourteen-page sample stays clean through such a change and would ship the regression. And **treat a target that passes only on the spacing exception as unfixed**: the result depends on the font, the surrounding layout and the viewport at once, and any of the three can move. @@ -82,7 +82,7 @@ Two rules that are easy to get wrong: ## Diagrams Inline SVG inherits the page's font environment, so the self-hosted faces apply -to diagram labels too --- `svg-inline.js` puts the SVG in the DOM rather than +to diagram labels too --- `figure.js` and `figurePlugin` put the SVG in the DOM rather than behind an `<img src>`, which would isolate it from the document's `@font-face` rules. The DOT sources --- `docs/assets/images/dot/` for shared diagrams, `Images/` beside a page for one that belongs to it --- and `builder/gantt.mjs` @@ -177,7 +177,7 @@ stale but correct SVG beats a freshly wrong one. ### Diagram scale on the web: never stretch a diagram up -The rule is `.svg-container svg { width: auto; max-width: 100% }`. A +The rule is `.fig-container > svg { width: auto; max-width: 100% }`. A `width: 100%` rule would stretch every diagram to the column whatever its own size, and label size would become an accident of how wide Graphviz drew the diagram. Label size against 16px body text in a 736px column under `width: 100%`: @@ -219,7 +219,7 @@ and on the printed page alike. ### Diagram scale in print: `zoom: 0.875`, never a text-only shrink **Do not shrink diagram text alone in `print.css`** (a rule such as -`.svg-container text { font-size: 75% }`). Graphviz sizes each box to the text it +`.fig-container text { font-size: 75% }`). Graphviz sizes each box to the text it measured and positions the label with `text-anchor="start"` at an x computed for that size, so shrinking only the text leaves it undersized *and* left of centre in a box that did not move. Across the book's five diagrams the worst off-centre @@ -277,13 +277,15 @@ palette --- the same thing `admonitions.scss` does for its icons. Contrast: The selector is scoped to **direct** children, because node text must keep the dark ink its own light fill needs. Do not widen it to all `text`: the node labels would go light on a light fill in dark mode, the same defect with the -themes swapped. +themes swapped. A cluster or edge that links (a `URL` in the DOT) has its label +at `g > a > text` instead, which the rule also names; inside the link, +`currentColor` is the theme's link colour. ## Diagram exports carry their own font -The four buttons above each inlined diagram (Download / Copy, SVG / PNG) all +The four export buttons of each inlined diagram (Download / Copy, SVG / PNG) all route through `serializeWithFonts` in -[docs/assets/js/svg-inline.js](docs/assets/js/svg-inline.js), which embeds the +[docs/assets/js/figure.js](docs/assets/js/figure.js), which embeds the faces the diagram paints with as `data:` URIs in an inline `@font-face`. This is necessary because **an exported diagram is cut off from the page's @@ -294,6 +296,12 @@ stylesheet either. Both fall back to whatever the viewer has installed. On a machine with no local Inter, an exported PNG asking for `font-family="Inter"` rasterises *pixel-identical* to one asking for a font that does not exist. +The stylesheet's colours are lost the same way. A cluster or edge label's +`currentColor` (see above) does not reach the export, so it would be black, and +on a dark theme's export, unreadable. `asTheReaderSeesIt` writes each label's +computed fill into the copy and puts the page's background behind it: an export +is the diagram as the reader sees it, in their theme. + A `data:` URI is not an external fetch, so secure static mode permits it, and the bytes come from the HTTP cache because the page already downloaded them. It costs ~25 ms per click and nothing over the wire. Two details are essential: diff --git a/WIP.md b/WIP.md index 9a71551d6..a9f87e341 100644 --- a/WIP.md +++ b/WIP.md @@ -122,7 +122,8 @@ node scripts/tbrun.mjs <exported-source-dir> # what does it print - **It runs the IDE on a private Windows desktop**, so it cannot seize focus. Set `TBBUILD_SHOW=1` while working interactively and leave it unset for unattended runs: a wedged IDE nobody can see is expensive to diagnose. - **One project per IDE**, 6--8 seconds each and flat in project size. Reusing a live IDE for a second project wedges it, so the cold start is the unit of work, not overhead to optimise away. Run concurrently to go faster: distinct `--port` values give distinct DevTools ports, user-data folders, temp folders (`%TEMP%\tbbuild-tmp-<port>`; IDEs building in one temp folder fail now and then to write the type library) and desktops. - **Keep a probe that might crash the compiler in a project of its own.** twinBASIC runs the compiler in the same process as user code, so one bad probe can take the run down and cost the other probes their answer. -- **`tbrun` takes an exported tree, not a `.twinproj`**, because it has to pin `project.buildPath` in its own staged copy. A project still on the default template opens a native Save dialog that is invisible on the private desktop: the build never happens, while every health check says the IDE is fine. The probe is a module with a `[RunAfterBuild]` Sub, and must start with `Debug.Cls`. Its exit codes: 0 the probe ran and its output was captured, 1 the project has compile errors, 2 the harness failed or the build did after a clean compile, or the probe's module holds a procedure named like the module, which stops the `[RunAfterBuild]` Sub from running (BETA 997; `--allow-name-clash` runs it anyway), 3 no output, 4 the compiler crashed, as `tbbuild` reports it, 5 the probe ended before it returned (`End`, or an error raised with no handler in LLVM-compiled code, which ends the run silently), 6 with `--exe`, the exe exited with a code other than 0, the event log records that it faulted, it opened a box (an unhandled error opens one, and `tbrun` closes it), or it was still running at `--timeout`. +- **`tbrun` takes an exported tree, not a `.twinproj`**, because it has to pin `project.buildPath` in its own staged copy. A project still on the default template opens a native Save dialog that is invisible on the private desktop: the build never happens, while every health check says the IDE is fine. The probe is a module with a `[RunAfterBuild]` Sub, and must start with `Debug.Cls`. Its exit codes: 0 the probe ran and its output was captured, 1 the project has compile errors, 2 the harness failed or the build did after a clean compile, or, on a build before 1005, the probe's module holds a procedure named like the module, which stops the `[RunAfterBuild]` Sub from running there (`--allow-name-clash` runs it anyway), 3 no output, 4 the compiler crashed, as `tbbuild` reports it, 5 the probe ended before it returned (`End`, or an error raised with no handler in LLVM-compiled code, which ends the run silently), 6 with `--exe`, the exe exited with a code other than 0, the event log records that it faulted, it opened a box (an unhandled error opens one, and `tbrun` closes it), or it was still running at `--timeout`, 7 with `--tests`, a case failed. +- **A project's own unit tests: `tbrun --tests`.** It builds nothing: it runs each `[TestCase]` Sub of each `[TestFixture]` Module in the compiler's test mode, the debugger's `evaluate` request with `isTest`, which the IDE itself never sends (BETA 1005 has no test runner a person can use), and prints `PASS` or `FAIL` for each. A failed assertion, an unhandled error and an access violation each end only their case. A defect that is a diagnostic is a compile case instead, a line ending `' CASE <Name>: <codes | error | none>`. A follow-up issue's regression tests are written this way: [WIP.Harness.md](WIP.Harness.md#running-a-projects-test-cases). - **Measuring LLVM: `tbrun --llvm`** (or `--compiler-options "<s>"`) compiles the whole probe with LLVM, by setting `compiler.debugOptions` --- what a `[RunAfterBuild]` run is compiled with; `compiler.buildOptions` alone does not reach it (BETA 995) --- and `compiler.buildOptions`, for the exe. It refuses a Community or Personal licence. `--exe` also runs the built exe on a private desktop; the exe runs `Sub Main` and prints with `TbRun.Out`, a module `tbrun` adds, because `Debug.Print` writes nothing in an exe. - **A census is evidence, not applicability.** The corpus not using an attribute somewhere does not mean the compiler refuses it there, and the reverse also holds. Only a probe settles that. - **The sweep is the probe for a whole attribute.** Before writing or changing an attribute's `Applicable to:` line, run `sweep_attributes.mjs --names <Name>` (a minute or a few; the once-per-project ones, `RunAfterBuild` and `RunBeforeStartupObject`, take several); it tries every declaration site, not only the ones already claimed. **Always pass `--out` and `--dump-results`**: a run piped through `tail` keeps one line of a report that takes ten minutes. A clean build there means the compiler accepts the attribute, not that it does anything, and an Enum member cannot be tested at all (see [WIP.Harness.md](WIP.Harness.md#sweeping-every-attribute-at-every-site)). @@ -364,9 +365,10 @@ is a build artifact. - **Edit the `.dot`, never the `.svg`**, and never set a diagram's `font-family` anywhere else. Graphviz sizes every box to the text *it* measured, and its WASM build has no font machinery at all --- it falls back to Times for any face it does not know. A face the layout never saw leaves labels painted outside their boxes. - `node scripts/check_dot_fit.mjs` is the gate on exactly that, and runs inside `check.bat`. Run it after touching any diagram. +- **A diagram whose boxes link** (a `URL` on a node or cluster, relative to the page) declares its nodes in reading order and sets `outputorder=nodesfirst`: that order is the Tab order. `builder/inline-diagram.mjs` makes it a group of links when it is inlined; `Tools-Groups.dot` is the example. - **Scale a diagram whole, or not at all.** Stretching an SVG up inflates its labels; shrinking the text alone unmoors the labels from their boxes. - **A `@font-face` added to [docs/_sass/custom/_fonts.scss](docs/_sass/custom/_fonts.scss) needs its stack added to [docs/_sass/modules-dark.scss](docs/_sass/modules-dark.scss) as well**, and must stay inside the `emit-font-faces` mixin. The dark compilation re-emits its whole payload under two selectors at raised specificity: a face declared there would be invalid, and a stack left out there applies in light mode and silently does not in dark. -- **Run the full a11y sweep after any change that moves type metrics.** An inline element's measured height is its font's content area, so `target-size` results move with the face. The thirteen-page sample can stay clean while the full sweep regresses. +- **Run the full a11y sweep after any change that moves type metrics.** An inline element's measured height is its font's content area, so `target-size` results move with the face. The fourteen-page sample can stay clean while the full sweep regresses. Why Graphviz needs Inter's widths patched into its Times table and what breaks that patch, why the web rule is `width: auto` and the PDF's is `zoom: 0.875`, @@ -381,15 +383,21 @@ must never do: [WIP.Typography.md](WIP.Typography.md). ### Tooling is JavaScript Everything under `scripts/`, `builder/`, `lib/`, `book/`, `eval/` and `wisdom/` is -Node.js, and a new tool joins them there. Three files are not, each for a stated -reason rather than by oversight: `scripts/impexp.py` is a published download +Node.js, and a new tool joins them there. Four files and one folder are not, each +for a stated reason rather than by oversight: `scripts/impexp.py` is a published download offered to readers rather than tooling, `scripts/build_fonts.py` stays Python because the JavaScript HarfBuzz build produces wrong CFF2 metrics -([WIP.Fonts.md](WIP.Fonts.md)), and `scripts/lib/tb-launch.ps1` is Win32 calls -Node cannot make without a native FFI addon --- a private desktop, and the job -object the IDE runs in --- and is never run as a file, so the execution policy -never comes into it. The full accounting, and what the two ports gained, is in -[WIP.Build.md](WIP.Build.md). +([WIP.Fonts.md](WIP.Fonts.md)), `scripts/subset_font.py` cuts an SVG screenshot's +fonts with fontTools, the subsetter the repository already trusts, `scripts/lib/tb-launch.ps1` is Win32 calls +Node cannot make without a native FFI addon --- a private desktop, the job +object the IDE runs in, and the windows on that desktop listed and captured +(`desktopWindows`, `captureWindow` in `tb-ide.mjs`) --- and is never run as a file, so the execution policy +never comes into it, and `scripts/imagestrip/` is a twinBASIC program, because the +twinBASIC developers run it in their own build, which has twinBASIC and need not have +Node. After a change to its `src/`, pack `imagestrip.twinproj` again with `impexp`, run its own +`[TestCase]`s with `tbrun --tests`, and run the half of `test/compact-image.test.mjs` that tests +it with `IMAGESTRIP_EXE` set to its exe: no gate can build it, so nothing else will. +The full accounting, and what the two ports gained, is in [WIP.Build.md](WIP.Build.md). ### The published docs assume manual work @@ -431,7 +439,7 @@ line into its description** --- the contributor-facing statement is **[WIP.ExamplesBuild.md](WIP.ExamplesBuild.md) is the file for this** --- the markup, the slots, the template projects and their stage sets, the batching and bisect-on-crash rules, -what actually collides inside one project, and what the first full run found. Two results +and what actually collides inside one project. Two facts from it concern the harness rather than the samples: `[RunAfterBuild]` is **one per project** (TB5114), which `check_run` has to be designed around; and **a project that crashes the compiler can look like a clean build** @@ -457,17 +465,17 @@ Why the report separates the wedged task from the merely blocked ones, and why - `build.bat` — runs `node builder\tbdocs.mjs --src docs --check-audit-index` (which implies `--check`) and produces three trees in one pass: the online copy at `_site/`, a `file://`-browsable copy at `_site-offline/`, and the sparse pagedjs source at `_site-pdf/`. The offline pass adds ~700 ms and the PDF pass adds ~150 ms on top of the ~2 s online build. Toggle `also_build_offline` / `also_build_pdf` in `_config.yml` (or pass `--no-offline` / `--no-pdf`) to skip a sibling output. `--check` adds ~1.7 s and runs the link + integrity check over the HTML while it is still in worker memory; `build.bat --no-check` gets a plain build. A build also writes the help add-in's archive, `add-in/Resources/HELP/site.zip` (see the help-archive bullet below); `--no-help-archive` skips it, and CI never passes it. - `serve.bat` — runs `tbdocs --serve`: initial build, then a long-lived process with watcher, debounced rebuilds, and SSE-driven browser auto-reload. Writes to `docs/_serve/` (disjoint from `build.bat`'s `_site*/`) and skips the offline + PDF passes — so a one-off `build.bat` for the PDF or offline mirror doesn't disturb the live preview. Ctrl+C to stop. - `check.bat` — the gates that read the built site: a freshness check that refuses a stale tree (`scripts/check_tree_fresh.mjs`), the DOT diagram fit check (`scripts/check_dot_fit.mjs`), the a11y sample-coverage check (`scripts/pick_a11y_sample.mjs --check`), then the accessibility check (`scripts/check_a11y.mjs`). The link + integrity check runs in `build.bat`. ~37 s. -- `test.bat` — the tests the *toolchain* has to pass: the publish-allowlist self-test (`scripts/check_publish_policy.mjs`), the gate-list check (`scripts/check_gate_lists.mjs`), the CI-workflow roster check (`scripts/check_ci_workflows.mjs`), the lint gate (`scripts/check_lint.mjs`), the site-search unit tests (`node --test test/search.test.mjs`), the markdown-plugin unit tests (`node --test test/render.test.mjs`), the date-formatter unit tests (`node --test test/strftime.test.mjs`), the bug-reproducer picture tests (`node --test test/png.test.mjs`), `check_examples.mjs`'s probes (`node --test test/example-batches.test.mjs`), the port-claim unit tests (`node --test test/ports.test.mjs`), the add-in project-file tests (`node --test test/addin-project.test.mjs`), the regex-safety gate (`scripts/check_regex_safety.mjs`), the code-region gate (`scripts/check_code_regions.mjs`), the page-count drift-guard probes (`scripts/check_page_baseline.mjs`), the book-coverage probes (`scripts/check_book_coverage.mjs`), the symbol-index probes (`scripts/check_symbol_index.mjs`), the twinBASIC-scanner probes (`scripts/check_twin_parsers.mjs`), the attribute-sweep probes (`scripts/check_attribute_sweep.mjs`), the command-line probes and cases (`scripts/check_cli.mjs`), the pdf-lib shim comparison (`scripts/check_pdf_shims_equiv.mjs`), the impexp parity check (`scripts/check_impexp_parity.mjs`), and the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`). ~23 s, of which the regex-safety gate is ~10 s and the impexp check ~4 s. See [What belongs in test.bat rather than check.bat](WIP.Build.md#what-belongs-in-testbat-rather-than-checkbat). +- `test.bat` — the tests the *toolchain* has to pass: the publish-allowlist self-test (`scripts/check_publish_policy.mjs`), the gate-list check (`scripts/check_gate_lists.mjs`), the CI-workflow roster check (`scripts/check_ci_workflows.mjs`), the lint gate (`scripts/check_lint.mjs`), the site-search unit tests (`node --test test/search.test.mjs`), the markdown-plugin unit tests (`node --test test/render.test.mjs`), the date-formatter unit tests (`node --test test/strftime.test.mjs`), the bug-reproducer picture tests (`node --test test/png.test.mjs`), the embedded-image tests (`node --test test/compact-image.test.mjs`), the SVG screenshots' one-line test (`node --test test/svgshot.test.mjs`), `check_examples.mjs`'s probes (`node --test test/example-batches.test.mjs`), the port-claim unit tests (`node --test test/ports.test.mjs`), the add-in project-file tests (`node --test test/addin-project.test.mjs`), the regex-safety gate (`scripts/check_regex_safety.mjs`), the code-region gate (`scripts/check_code_regions.mjs`), the page-count drift-guard probes (`scripts/check_page_baseline.mjs`), the book-coverage probes (`scripts/check_book_coverage.mjs`), the symbol-index probes (`scripts/check_symbol_index.mjs`), the twinBASIC-scanner probes (`scripts/check_twin_parsers.mjs`), the attribute-sweep probes (`scripts/check_attribute_sweep.mjs`), the command-line probes and cases (`scripts/check_cli.mjs`), the pdf-lib shim comparison (`scripts/check_pdf_shims_equiv.mjs`), the impexp parity check (`scripts/check_impexp_parity.mjs`), and the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`). ~23 s, of which the regex-safety gate is ~10 s and the impexp check ~4 s. See [What belongs in test.bat rather than check.bat](WIP.Build.md#what-belongs-in-testbat-rather-than-checkbat). - `book.bat` — renders the PDF from `docs\_site-pdf\book.html` via `node book\render-book.mjs` into `docs\_pdf\twinBASIC Book.pdf`. Run `build.bat` first to populate `_site-pdf/`; `book.bat` refuses a tree older than its sources rather than rendering the previous book (see [The book refuses a stale source tree](WIP.Build.md#the-book-refuses-a-stale-source-tree)). - `examples.bat` — compiles the documentation's own twinBASIC code samples, every `tb` fence marked `check_build`, and reports the ones the compiler refuses against the line in the page they came from; a statement sample also marked `check_run` is built and run, and what it prints is checked against the page. Needs a twinBASIC install and Windows, so it is outside every gate and outside CI; ~110 s over the 1,197 samples marked (33 of them `check_run`). `--build` also builds each project that compiles clean, and `--llvm` builds it with LLVM (see [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md)). Two modes need no compiler at all: `--census` classifies every fence and says how many classifiable ones are still unmarked, and `--report <survey.json>` groups a saved `--propose --json` survey by diagnostic, section and unresolved name. `--propose` itself does compile. See [Compiling the reference's own code samples](#compiling-the-references-own-code-samples) and [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md). - `addin-test.bat` — tests IDE add-ins by operating an IDE: every lane in `test/addin/lanes.mjs` builds the add-ins it tests into a private copy of the install, opens a project and checks what the add-in does. Outside every gate and outside CI for the same reasons as `examples.bat`; about two minutes for the seventeen lanes today (BETA 997): Samples 10 and 15, the thirteen probe lanes behind Stage 2's answers in [WIP.HelpAddin.md](WIP.HelpAddin.md), and the help add-in in `add-in/` twice, built with its committed copy of the symbol index, `add-in/Resources/SYMBOLS/symbols.json`, which a local build rewrites whenever the index changes. The `help` lane serves the built `docs/_site` on localhost to the add-in's pane; `help-offline` builds the add-in with a help archive of `docs/_site-offline` and the pane's pages come from the add-in's own server. So run `build.bat` first. Exit 0 every lane passed and the registry is as it was found, 1 a lane failed, 2 the harness failed, 3 the registry or a work folder was not put back. See [Driving the twinBASIC compiler](#driving-the-twinbasic-compiler) for its rules. - `ide-test.bat` --- the same runner for scenarios that operate the IDE itself rather than an add-in: every lane in `test/ide/lanes.mjs`, base port 9660. Same exit codes, same standing outside every gate and outside CI, same rules. - `try-help-addin.bat` --- opens one IDE on the user's desktop with the help add-in built and loaded, to try it by hand, and puts the registry back once it is closed (see [Driving the twinBASIC compiler](#driving-the-twinbasic-compiler)). Needs `build.bat` first. Exit 0 closed and put back, 1 the add-in or the project did not compile, 2 the tool could not run, 3 not put back. -- `node scripts/shoot_docs.mjs` --- writes the documentation's IDE screenshots (plan and owner's decisions: [WIP.Screenshots.md](WIP.Screenshots.md)). Each picture is a shot in a table, `out` its path under `docs/` (`--only` matches it), in a setup (the main three below; the full list is in WIP.Screenshots.md), each one IDE on a **private** desktop started only when a shot in it is selected: `help` (builds the add-in as `try-help-addin.bat` does, opens `test/addin/helpdemo`, `TB_DOCS_HELP_REAL_TITLEBAR=0`: the eight `docs/IDE/AddIns/Images/Help_<Name>.png`), `no-project` (`Lane.openNoProject` with `includeProjects`: the menus, cut out with alpha, the dialogs that need no project, and the empty panels, each floated on its own, after the dialogs) and `project` (the demo without the add-in: About, which needs the compiler's licence answer, and IDE Options, whose font lists need a project). Every IDE gets `--force-device-scale-factor=1` (`Lane`'s `browserArgs`), else the page follows the display's scaling; the page is overridden to 1280x880 at 2x, dark theme, and `PAGE_DEFAULTS` puts the IDE's options, panel layouts and keyboard groups to their defaults and the recent lists to the tool's own **in the page only**, its save calls replaced first so nothing is written. Each picture is a `Page.captureScreenshot` with a `clip`; a page showing it gives it `{:width height}` of half its pixel size. A shot's `annotate` list (arrows, rings, badges...) is drawn by `scripts/lib/shot-annotate.mjs` as one SVG layer over the page before the capture and removed after it, the clip growing to hold it. A picture is rewritten only when its bytes differ, and a second run reports every one `unchanged`; a capture is not byte-stable, so `capture` takes up to eight and keeps one equal to the file on disk. `--jobs N` (default 6) runs the setups at once on their own ports and desktops, a full run in about 110 s (`--jobs 1`: one at a time); `--diffs <dir>` writes the old picture, the new one and an amplified difference map for each that changed. Settle by a condition, never a fixed sleep, and a picture that shows some state sets it itself rather than relying on the shots before it. The page's animations and carets are switched off through a style sheet put into the document and every shadow root; no picture holds the DEBUG CONSOLE (its lines carry a time). **The Windows user name must never be in a published picture**: the visible text of the page is searched for it before a picture is kept, and the run fails when it is there. The tool finds each name it presses F1 on by searching the demo's source, not by line. The add-in is built on a port of its own (`Lane.addAddin`'s `port`): the building IDE's WebView2 process can hold its port after the IDE ends for longer than `launchIde`'s ten seconds. No wrapper; outside every gate and outside CI, as `try-help-addin.bat` is. Exit 0 every picture written or unchanged, 1 a picture failed, 2 the tool could not run, 3 not put back. Run `build.bat` first. +- `node scripts/shoot_docs.mjs` --- writes the documentation's IDE screenshots (plan and owner's decisions: [WIP.Screenshots.md](WIP.Screenshots.md)). Each picture is a shot in a table, `out` its path under `docs/` (`--only` matches it), in a setup (the main three below; the full list is in WIP.Screenshots.md), each one IDE on a **private** desktop started only when a shot in it is selected: `help` (builds the add-in as `try-help-addin.bat` does, opens `test/addin/helpdemo`, `TB_DOCS_HELP_REAL_TITLEBAR=0`: the eight `docs/IDE/AddIns/Images/Help_<Name>.png`), `no-project` (`Lane.openNoProject` with `includeProjects`: the menus, cut out with alpha, the dialogs that need no project, and the empty panels, each floated on its own, after the dialogs) and `project` (the demo without the add-in: About, which needs the compiler's licence answer, and IDE Options, whose font lists need a project). One setup is not an IDE: `web` starts puppeteer's Chrome through `launchOnDesktop` on a private desktop of its own (never headless, never on the user's desktop; native-window occlusion and occluded-window backgrounding switched off, or a capture waits forever or comes back blank) for the two public pages the FAQ shows, taken once; it needs the network, as do `webpage` (the Webpage pane on `https://docs.twinbasic.com/`, its page captured from the pane's own DevTools target and pasted into the pane's place) and the package-server setups, which never publish. Every IDE gets `--force-device-scale-factor=1` (`Lane`'s `browserArgs`), else the page follows the display's scaling; the page is overridden to 1280x880 at 2x, and `PAGE_DEFAULTS` puts the IDE's options, panel layouts and keyboard groups to their defaults and the recent lists to the tool's own **in the page only**, its save calls replaced first so nothing is written. Each picture is a `Page.captureScreenshot` with a `clip`; a page showing it gives it `{:width height}` of half its pixel size. **Every picture is taken in two themes by one IDE**: dark as `X.png`, then, after `backToPrepared` and `tbTheme_SwitchToLightMode`, Light (not Classic) as `X.light.png` beside it, which the build pairs by name ([WIP.Build.md](WIP.Build.md#pictures-in-two-themes)); a light picture with the dark one's pixels is not kept, and one more than 32 device pixels off the dark one's size fails (a wrong state, not the theme). A label's pill follows the theme unless the shot names a `tone`. A shot's `annotate` list (arrows, rings, badges...) is drawn by `scripts/lib/shot-annotate.mjs` as one SVG layer over the page before the capture and removed after it, the clip growing to hold it. A picture is rewritten only when its bytes differ, and a second run reports every one `unchanged`; a capture is not byte-stable, so `capture` takes up to eight and keeps one equal to the file on disk, and a picture that differs from the file only by a capture's noise (16 grey levels, 0.5% of its pixels) is left as it is. `--jobs N` (default 6) runs the setups at once on their own ports and desktops, a full run in about 200 s (`--jobs 1`: one at a time); `--diffs <dir>` writes the old picture, the new one and an amplified difference map for each that changed, named with its theme. Settle by a condition, never a fixed sleep, and a picture that shows some state sets it itself rather than relying on the shots before it. The page's animations and carets are switched off through a style sheet put into the document and every shadow root; a picture that holds DEBUG CONSOLE lines has their times (and a CodeLens run's time taken) set in the page once the real ones show. **A running program's window** (setup `programs`, fixture `test/shots/programs`) is taken **once** (`once: true`, the dark pass only) with `captureWindow`, cropped to its visible frame, at the display's DPI, shown at its size at 96 DPI. **The Windows user name must never be in a published picture**: the visible text of the page, text boxes included, is searched for it before the first capture and again when a capture is chosen, with the picture's SVG under `--svg` (a page can draw again in between: Project Settings once put a real path back over the one the shot set), and for a program's window the texts of the window and its children, and the run fails when it is there. The tool finds each name it presses F1 on by searching the demo's source, not by line. The add-in is built on a port of its own (`Lane.addAddin`'s `port`): the building IDE's WebView2 process can hold its port after the IDE ends for longer than `launchIde`'s ten seconds. **`--svg`** also draws each picture as SVG from the page as it stands at the capture (`scripts/svgshot/`: a DOM snapshot converted, its text set in per-picture subsets of the fonts the IDE drew it with, cut by `scripts/subset_font.py`, which needs Python with fontTools), compares it with the PNG, and writes `X.svg` or `X.light.svg` beside it only when no more than 0.25% of its pixels are far off; otherwise an older SVG is removed and the page shows the PNG ([WIP.Build.md](WIP.Build.md#pictures-as-svg)). With `--svg` each picture's *bundle*, what its SVG is drawn from, is saved in the gitignored `.svgshot-bundles/` (`--bundles`; never under `docs/`, a bundle holds the user name), and **after a change to the converter, `node scripts/svgshot/replay.mjs` draws every SVG again from the bundles, with no IDE**, in about 20 s for the 370 (an SVG the same as its file is not compared again, `--recheck` compares all; the fonts' cuts are kept in `.svgshot-bundles/subsets/`; one browser per process, GPU off); it lists the pictures whose bundle cannot answer what the converter now asks, with the `--only` command that retakes them ([WIP.Screenshots.md](WIP.Screenshots.md#pictures-as-svg)). `node scripts/svgshot/bench.mjs` tests the converter's replicas of what Chromium draws outside the DOM (scrollbars, drop-down lists) in Edge, with no IDE ([WIP.Screenshots.md](WIP.Screenshots.md#pictures-as-svg)). No wrapper; outside every gate and outside CI, as `try-help-addin.bat` is. Exit 0 every picture written or unchanged, 1 a picture failed, 2 the tool could not run, 3 not put back. Run `build.bat` first. - **The help add-in's project file is written by every build, CI's included.** The `addinProject` task (`builder/addin-project.mjs`) packs `add-in/` into `tB/IDE/AddIns/downloads/tbDocsHelp.twinproj` in the **online tree only**, which the Help Add-In page offers as a download; `_config.yml`'s `addin_project` declares both ends, so a fixture config gets none. It waits for the `helpArchive` task. It packs the files `git ls-files` lists there, read from the working folder, never a `Resources/HELP/` file from the disk, with CRLF made LF outside `Resources/` (so a Windows and a Linux build write the same bytes), the `tB/symbols.json` the same build wrote in place of the committed copy, and **the zip that build's archive task wrote** as `Resources/HELP/site.zip`, passed in memory (a build that wrote none --- `--no-help-archive`, `--serve`, another `--dest`, a fixture --- packs without it, and the summary line says `without the offline archive`). The download is about 28 MB with the archive, 1 MB without. The offline tree holds no copy (the project carries that tree's archive, so a copy there would be circular), and its links to the file are rewritten to the website by the general rule in `offline-rewrite.mjs`: a URL the offline tree lacks but the online tree has becomes `site.url` + path, and is no miss (`computeWebsiteUrl`; the offline link check's `forbidExempt` excuses exactly those links, `isWebsiteOnlyLink`). The pack is `importProject` from `scripts/impexp.mjs`, the one module under `scripts/` that `builder/` may import (`biome.jsonc`); `check_tree_fresh.mjs` watches it and `add-in/` for that reason. The publish allowlist exempts the one path in the trees only (`publishPolicyFor`'s `generated`); a `.twinproj` anywhere else, or in `docs/`, is refused. `--serve` writes it into its one tree; `--dry-run` writes nothing. A failure (no git) fails the build with exit 1 and the index audit names the missing file. **Don't build the DLLs in CI from it**: that needs a twinBASIC install; the download is the project, and the reader builds it. Why each of these: [WIP.Build.md](WIP.Build.md#the-help-add-ins-project-file); `test/addin-project.test.mjs` is the gate. - **The help archive is the `helpArchive` task of the build's graph.** `builder/help-archive-step.mjs` writes `add-in/Resources/HELP/site.zip` from `docs/_site-offline` through `lib/help-archive.mjs`, once every task that writes into the offline tree has finished (`flushJoin`, `scss`, `writeOffline`; the archive task lists exactly those) and **before** the Gantt injection, so the zip holds `BuildInfo.html` without the chart and `assets/images/gantt.svg` as the 105-byte placeholder the sources hold (it is a tracked static file, so the file list already has it). It reads the file back to verify it. A mismatch fails the build (exit 1). After the Gantt injection `recheckHelpArchive` checks that the offline tree on disk lists the same files as the archive and differs from it only in `BuildInfo.html` and `gantt.svg`; a task that wrote into the offline tree late fails the build there. The help add-in embeds the zip as a resource and serves the pages from it with its own server; a build of the add-in without it uses the live site. The file is gitignored (about 26 MB), so a build of the add-in from a clean clone is the online one until `build.bat` has run. The download carries the same zip (previous bullet). **`--no-help-archive` turns the step off, and neither CI workflow may pass it** (`check_ci_workflows.mjs` refuses it, in `ALLOWED.forbiddenBuildFlags`), because CI's published download would then hold no archive. It runs only for the guarded source tree (`docs`) built into the default `docs/_site-offline` (not another `--dest`, so `compare_trees` and `check_links_diff` leave it alone), never under `--serve` or `--dry-run`, never without an offline tree, and not keyed on `$CI`. There is no command-line tool for the archive: the `help-offline` lane makes its own with `writeHelpArchive`. The zip's format is fixed by the add-in's reader, which does no inflating ([WIP.HelpAddin.md](WIP.HelpAddin.md), Stage 4, increment 5). -- `node scripts/bug_repro.mjs` --- the reproducer projects under `bugs/<slug>/` for the entries of [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md): `new`, `pack` (impexp, then the zip, written in Node), `compile`, `build` and `run` through `tbbuild` and `tbrun`, `vb6` (builds the optional `vb6/` VB6 project beside the twinBASIC one in a temp copy, runs its exe on a private desktop, and prints its `out.txt`; a project with a form or a user control is built without Unattended Execution, which VB6 refuses for it; `pack` zips its sources into `<slug>-vb6.zip`; VB6 is started only by `scripts/lib/vb6.mjs`, never from a shell; a graphical defect's `images` key makes `run` and `vb6` keep what the `PngDump` modules of `test/repro-templates/png/` draw as `images/<name>-tb.png` and `-vb6.png`, with a `-compare.png` of both, tested by `test/png.test.mjs`), `file` (moves a filed entry and its reproducer to `bugs/filed/<slug>/`; `file --marked` does every marked entry), and `verify`, which reads each `bugs/*/repro.json` and `bugs/filed/*/repro.json` and says whether the bug still reproduces on the newest beta; a `lane` reproducer names the `ide-test` or `addin-test` lane and tests that assert its bug, which `verify` runs last through that runner, and a `probe` one a script under `scripts/`. **No reproducer should need a person at the IDE** (owner, 2026-10-05): a bug the IDE has to be operated for gets a lane test, and `manual` is left for what cannot be automated at all. It has no wrapper. **`verify` is run by a person only**, never by a gate or CI: it needs a twinBASIC install, like `examples.bat`. Default port 9440, and `--jobs N` takes N ports from there. Its command-line cases are in `scripts/lib/cli-cases.mjs`, and need no IDE. +- `node scripts/bug_repro.mjs` --- the reproducer projects under `bugs/<slug>/` for the entries of [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md): `new`, `pack` (impexp, then the zip, written in Node), `compile`, `build` and `run` through `tbbuild` and `tbrun`, `vb6` (builds the optional `vb6/` VB6 project beside the twinBASIC one in a temp copy, runs its exe on a private desktop, and prints its `out.txt`; a project with a form or a user control is built without Unattended Execution, which VB6 refuses for it; `pack` zips its sources into `<slug>-vb6.zip`; VB6 is started only by `scripts/lib/vb6.mjs`, never from a shell; a graphical defect's `images` key makes `run` and `vb6` keep what the `PngDump` modules of `test/repro-templates/png/` draw as `images/<name>-tb.png` and `-vb6.png`, with a `-compare.png` of both, tested by `test/png.test.mjs`), `file` (moves a filed entry and its reproducer to `bugs/filed/<slug>/`; `file --marked` does every marked entry), and `verify`, which reads each `bugs/*/repro.json` and `bugs/filed/*/repro.json` and says whether the bug still reproduces on the newest beta; a `lane` reproducer names the `ide-test` or `addin-test` lane and tests that assert its bug, which `verify` runs last through that runner, a `probe` one a script under `scripts/`, and a `test` one its own `[TestCase]` Subs and compile cases through `tbrun --tests`, judged by the `PASS` lines of what is fixed and the `FAIL` lines of the bug (`new --template tests` starts one). **No reproducer should need a person at the IDE** (owner, 2026-10-05): a bug the IDE has to be operated for gets a lane test, and `manual` is left for what cannot be automated at all. It has no wrapper. **`verify` is run by a person only**, never by a gate or CI: it needs a twinBASIC install, like `examples.bat`. Default port 9440, and `--jobs N` takes N ports from there. Its command-line cases are in `scripts/lib/cli-cases.mjs`, and need no IDE. - `node scripts/vb6run.mjs <file | ->` and `node scripts/vb6run.mjs --docs [--only <regex>]` --- build and run VB6 code, so that what a sample prints in twinBASIC can be compared with VB6: a stand-alone file of statements or a `.bas` module, or `--docs`, which builds the documentation's `check_run` fences in VB6 and says for each whether it prints what its page says (`same`, `differs`, `not VB6`, `error`, `refused`; a `projname=` group is built as a project of its own, its `slot=file` fences translated into VB6 `.cls` and `.bas` components). It has no wrapper and is outside every gate and outside CI: it needs VB6 (`--vb6`, `VB6_EXE`, or the standard install folder). **Never start `VB6.EXE` from a shell**: in Git Bash `/make` is rewritten as a path and VB6 answers with a modal box on the desktop; the tool spawns it from Node with an argument array, and a sample that calls `MsgBox` or `InputBox` or contains `End` is refused. Its `Debug.Print` rewrite and its file translation have probes in `test/example-batches.test.mjs`, and its command-line cases are in `scripts/lib/cli-cases.mjs`; both need no VB6. What it relies on is in [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md). Three generators sit outside that loop and produce committed artifacts rather than build output — none runs during a build, and none is needed for one. `python scripts/build_fonts.py` rebuilds the subset webfaces under `docs/assets/fonts/` and needs a network connection; `node scripts/build_dot_metrics.mjs` regenerates `builder/inter-metrics.json` from those webfaces and needs only a browser. See [Typography](#typography). `node scripts/build_package_api.mjs` regenerates `builder/package-api.json`, the packages' declared API that the build's symbol index (`tB/symbols.json`, for the IDE help add-in) is annotated from; it needs a twinBASIC install, so **run it when the reference is re-indexed against a newer build** and commit it with the pages. See [WIP.HelpAddin.md, Stage 3](WIP.HelpAddin.md#stage-3-the-symbol-index-generated-by-the-docs-build). @@ -482,7 +490,7 @@ build.bat && check.bat On the dev box that is ~4 s of build against ~37 s of check, of which the axe scan is ~20 s. [builder/PLAN-checks.md](builder/PLAN-checks.md) records how the link checker sits in the build's task graph and its cost; the axe follow-ons are designed there but not implemented. -**If the change touched `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `test/`, `add-in/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~23 s. Nineteen of its twenty-two gates cannot be affected by an edit under `docs/` at all. **Three can.** `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `check_gate_lists.mjs` reads `README.md` and every page under `docs/Documentation/`, so an edit to any developer page that states a gate count can fail it. **`check_code_regions.mjs` has two halves, and a content edit reaches only one.** Its corpus sweep reads `DOCS_DIR`, which is `<repo>/docs`, and tokenises every markdown file in it, so a page that provokes a rewrite into *altering* a code region fails it --- that half is content-dependent. Its fixed probes are not: they run against their own sources whatever the tree holds, and they cover the **mirror** fault, where a rewrite silently stops firing. The sweep cannot see that fault, because text the rewrite skipped is masked and restored unchanged and every region still matches. So run `test.bat` after adding an unusual code construct --- a fence whose contents include a fence marker, a 4-space indented block, an admonition wrapping a fence --- and read the built page as well, because for the mirror fault the gate is asserting that the mask and the admonition scan still work rather than checking your page: +**If the change touched `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `test/`, `add-in/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~23 s. Twenty-one of its twenty-four gates cannot be affected by an edit under `docs/` at all. **Three can.** `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `check_gate_lists.mjs` reads `README.md` and every page under `docs/Documentation/`, so an edit to any developer page that states a gate count can fail it. **`check_code_regions.mjs` has two halves, and a content edit reaches only one.** Its corpus sweep reads `DOCS_DIR`, which is `<repo>/docs`, and tokenises every markdown file in it, so a page that provokes a rewrite into *altering* a code region fails it --- that half is content-dependent. Its fixed probes are not: they run against their own sources whatever the tree holds, and they cover the **mirror** fault, where a rewrite silently stops firing. The sweep cannot see that fault, because text the rewrite skipped is masked and restored unchanged and every region still matches. So run `test.bat` after adding an unusual code construct --- a fence whose contents include a fence marker, a 4-space indented block, an admonition wrapping a fence --- and read the built page as well, because for the mirror fault the gate is asserting that the mask and the admonition scan still work rather than checking your page: ```sh build.bat && check.bat && test.bat @@ -496,6 +504,19 @@ it meant to and nowhere else. See [WIP.Build.md](WIP.Build.md#the-pipeline). ### The gates, and where their internals are +**The Tools and Scripts page is a landing page and seven group pages.** +`Tools.md` holds the batch wrappers (and with them the two numbered gate lists +`check_gate_lists.mjs` reads from that file and no other), the command-line +conventions and the configuration files. Every other tool's entry is on one of +`docs/Documentation/Tools-<Group>.md` --- Build, Site-Checks, Toolchain-Tests, +Compiler, Samples, IDE, Defects --- published at +`/Documentation/Development/Tools-<Group>`. The permalinks are flat, as the +tbdocs Builder's children are, because a `Tools/` folder beside `Tools.html` +makes the link check resolve `Tools#anchor` to the folder. **A new tool gets an +entry on the group page it fits and a node in +`docs/Documentation/Images/Tools-Groups.dot`**, the map on the landing page; a +link to a tool's entry names the group page, never `Tools#<tool>`. + [Tools and Scripts](docs/Documentation/Tools.md) owns the authoritative lists, and `scripts/check_gate_lists.mjs` fails the run if `README.md` or any page under `docs/Documentation/` disagrees with them --- **so do not state a gate @@ -524,6 +545,8 @@ wrapper: | `test.bat` | `test/search.test.mjs` | the search entries `builder/search.mjs` writes hold what they should, and the copies of the search client still agree. Run by `node --test`; the gate roster reads such a line as a gate, named by its path | | `test.bat` | `test/render.test.mjs` | `builder/render.mjs`'s markdown-it plugins, one at a time, on inputs no page holds --- today the ellipsis plugin's dot counting. Run by `node --test` | | `test.bat` | `test/strftime.test.mjs` | `builder/strftime.mjs`'s `formatDate`, which no build calls because no page sets `last_modified_date`. Run by `node --test` | +| `test.bat` | `test/compact-image.test.mjs` | what an embedded image keeps (`compactImage` and `stripFile` in `scripts/lib/compact-image.mjs`): an editor's metadata goes, a notice of whose the picture is and on what terms stays (the IDE's lock icon carries a CC BY-NC-SA attribution), an sRGB profile goes and any other stays, a JPEG or a GIF is labelled by its bytes, and an image inside an SVG inside a CSS file is reached and keeps its encoding. Its second half runs `scripts/imagestrip/` over a CSS file and SVG files of every image the test builds and over the IDE's own stylesheets, and compares output, summary and reports with `stripFile` byte for byte; it is skipped unless `IMAGESTRIP_EXE` names the built exe. Run by `node --test` | +| `test.bat` | `test/svgshot.test.mjs` | an SVG screenshot is written on one line (`oneLine` in `scripts/svgshot/snapshot-svg.mjs`), a break in a tag as a space and one in text as a character reference, so that a CRLF checkout changes only its last line ending; an underline is placed and sized by the figures fitted in Edge (`underlineBand`) under the boxes that set one (`decoratingBoxes`); a background image is sized by its bytes, a JPEG the IDE calls `image/png` included (`naturalSize`); an iframe's document is decoded and placed at the iframe's content box, less its scroll, scaled when zoomed, over its canvas colour (`frameGeometry`, `frameView`, `unzoom`, `viewportScroller`, `canvasColor`); a run's web font is the `@font-face` rule CSS matching picks (`webFontFiles`), and a variable WOFF2 is read and cut per weight; a family split by `unicode-range` gives each character to the rule whose range holds it (`parseUnicodeRange`, `webRulesFor`); an inline `<svg>` is found and its ids scoped (`inlineSvgsIn`, `scopeIds`); a filter of colour functions is baked into the colours under it, in order and clamped, its pictures wrapped in a real filter and its opacity kept apart (`colour-filter.mjs`); a transformed element's boxes and text are put back from the snapshot's after-transform boxes (`elementTransform`, `untransformBox`); a closed select shows its option's whole text (`selectLabel`); a bundle draws the same SVG from its file as in memory, and counts each question it cannot answer (`renderBundle`); its web font rules and files draw each character from the file whose range holds it, at the weight its rule allows, and its inline `<svg>` markup is drawn in its box, from the file and when trimmed (`drawBundle`); a bundle of version 1 is refused; one trimmed to its view draws the same (`trimSnapshot`); a font's cut comes from the subset cache with no Python run (`subsetKey`, `useSubsetCache`). What it keeps of an image it embeds is `compactImage`'s, which the row above tests. Run by `node --test` | | `test.bat` | `test/example-batches.test.mjs` | `check_examples.mjs`'s probes, from `scripts/lib/example-batches.mjs`: batching, crash isolation through a fake lane, the canaries, the fence classifier and `check_run`'s refusals, expected output, dispatcher and marker reading, with no IDE. Run by `node --test` | | `test.bat` | `test/ports.test.mjs` | `scripts/lib/tb-ports.mjs` claims ports as the lane runners need: two claims at once are disjoint, a port in use and a live lock are passed over, a stale lock is taken over, a lock goes when its process exits. Run by `node --test` | | `test.bat` | `test/addin-project.test.mjs` | `builder/addin-project.mjs` packs the help add-in's project file from exactly the files git tracks in `add-in/`, as the working folder holds them, never `Resources/HELP/`, with the index it is given, the same bytes from a CRLF and an LF checkout, into every tree it is given. Needs git. Run by `node --test` | @@ -556,6 +579,16 @@ whitespace inside inline `<code>` is content. Favor concise one-line git commit messages. +**A piece of work goes in a short-lived worktree unless the owner says not to** (owner, +2026-10-11): ask when starting, and without a no, proceed. `git worktree add -b <name> +.claude/worktrees/<name> staging` branches from the local `staging`, which holds commits +`origin` may not have yet; give it a `node_modules` junction to the main checkout's +(`New-Item -ItemType Junction`) and run `build.bat` once. When the work is done: rebase on +`staging`, fast-forward `staging` in the main checkout (git refuses while a file the branch +changes has uncommitted changes there), then remove the junction on its own (`cmd /c rmdir +<worktree>\node_modules`; a recursive delete would follow it into the main checkout's +`node_modules`), then the worktree and the branch. + **Lint before every commit:** `node scripts/check_lint.mjs`, a fraction of a second. It runs Biome's linter and formatter check over the tooling and the site's two scripts, and fails on a warning as well as an error, because Biome reports an unused import as a warning, diff --git a/add-in/Resources/SCRIPTS/hoverhelp.js b/add-in/Resources/SCRIPTS/hoverhelp.js index 23098f4b8..3e99194c3 100644 --- a/add-in/Resources/SCRIPTS/hoverhelp.js +++ b/add-in/Resources/SCRIPTS/hoverhelp.js @@ -6,16 +6,6 @@ if (old) { for (var k in old.pending) old.pending[k](""); window.tbDocsHoverHelp = null; } -// The IDE's dock resizers and drop targets lie over the part of a hover that -// reaches past the code editor, transparent but taking the mouse, so that -// Monaco takes the mouse reaching them as leaving the editor and hides the -// hover. Drawn above them, the hover keeps the mouse. This is for every hover, -// with hover help on or off, until the IDE is fixed (twinbasic/twinbasic#2506). -if (!document.getElementById("tbDocsHoverStay")) { - var stay = document.head.appendChild(document.createElement("style")); - stay.id = "tbDocsHoverStay"; - stay.textContent = ".monaco-editor .monaco-hover { z-index: 100000 !important; }"; -} if (on) { var pending = {}, seq = 0; var provider = monaco.languages.registerHoverProvider("twinbasic", { diff --git a/add-in/Sources/HoverHelp.twin b/add-in/Sources/HoverHelp.twin index 395f4d695..14062c6ca 100644 --- a/add-in/Sources/HoverHelp.twin +++ b/add-in/Sources/HoverHelp.twin @@ -2,9 +2,7 @@ ' hover provider the page runs (hoverhelp.js) adds the links above the ' compiler's text in the IDE's own hover, next to the name. A name with no ' page shows nothing, since the IDE's hover already shows its declaration and -' description. A click on a link raises Pick. On or off, the script also draws -' every hover above the IDE's dock, which would otherwise take the mouse from -' a hover reaching past the editor (twinbasic/twinbasic#2506). +' description. A click on a link raises Pick. Private Class HoverHelp Public Event Pick(ByVal url As String) diff --git a/bugs/colour-validation-data-ole-multiframe/colour-validation-data-ole-multiframe.twinproj b/bugs/colour-validation-data-ole-multiframe/colour-validation-data-ole-multiframe.twinproj new file mode 100644 index 000000000..d7e0bec5c Binary files /dev/null and b/bugs/colour-validation-data-ole-multiframe/colour-validation-data-ole-multiframe.twinproj differ diff --git a/bugs/colour-validation-data-ole-multiframe/repro.json b/bugs/colour-validation-data-ole-multiframe/repro.json new file mode 100644 index 000000000..927d67f4d --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/repro.json @@ -0,0 +1,29 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS ColourPropertyTests\\.Form_ForeColor$", + "^PASS ColourPropertyTests\\.Form_ForeColor_SystemColourOutOfRange$", + "^PASS ColourPropertyTests\\.Form_BackColor$", + "^PASS ColourPropertyTests\\.Form_FillColor$", + "^PASS ColourPropertyTests\\.PictureBox_ForeColor$", + "^PASS ColourPropertyTests\\.PictureBox_BackColor$", + "^PASS ColourPropertyTests\\.PictureBox_FillColor$", + "^PASS ColourPropertyTests\\.Label_ForeColor$", + "^PASS ColourPropertyTests\\.Label_BackColor$", + "^PASS ColourPropertyTests\\.TextBox_ForeColor$", + "^PASS ColourPropertyTests\\.TextBox_BackColor$", + "^PASS ColourPropertyTests\\.CommandButton_BackColor$", + "^PASS ColourPropertyTests\\.UserControl_ForeColor$", + "^PASS ColourPropertyTests\\.UserControl_BackColor$", + "^PASS ColourPropertyTests\\.UserControl_FillColor$", + "^PASS ColourPropertyTests\\.MDIForm_BackColor$", + "^PASS ColourPropertyTests\\.MDIForm_BackColor_SystemColourOutOfRange$", + "^FAIL ColourPropertyTests\\.Data_ForeColor: .*Expected: 380 .*Actual: 0 ", + "^FAIL ColourPropertyTests\\.Data_BackColor: .*Expected: 380 .*Actual: 0 ", + "^FAIL ColourPropertyTests\\.OLE_BackColor: .*Expected: 380 .*Actual: 0 ", + "^FAIL ColourPropertyTests\\.MultiFrame_BackColor: .*Expected: 380 .*Actual: 0 " + ] + } +} diff --git a/bugs/colour-validation-data-ole-multiframe/src/Settings b/bugs/colour-validation-data-ole-multiframe/src/Settings new file mode 100644 index 000000000..44cdcc15c --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ColourValidationDataOleMultiframe", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `Data.ForeColor`, `Data.BackColor`, `OLE.BackColor` and `MultiFrame.BackColor` store a value that is not a colour, where VB6 raises error 380", + "project.exportPathIsV2": true, + "project.id": "{9DBA139B-563C-4523-BA6F-075477A0FC90}", + "project.name": "ColourValidationDataOleMultiframe", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/colour-validation-data-ole-multiframe/src/Sources/ColourPropertyTests.twin b/bugs/colour-validation-data-ole-multiframe/src/Sources/ColourPropertyTests.twin new file mode 100644 index 000000000..a904a6882 --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/src/Sources/ColourPropertyTests.twin @@ -0,0 +1,206 @@ +' Regression tests for #2484 and its follow-up. Assigning a value that is not a +' colour to a colour property raises error 380, Invalid property value, and +' leaves the colour it had: VB6 6.0 does this for each property it has (the VB6 +' project beside this one prints them), and twinBASIC does it for the other +' controls. Form1 holds one control of each class; it is loaded, never shown. +' Each case sets the colour to vbGreen, assigns the bad value early-bound under +' On Error Resume Next, and checks the error and the colour read back. +[TestFixture] +Module ColourPropertyTests + + Private Const NotAColour As Long = -1 + Private Const SystemColourOutOfRange As Long = &H8000001F + + Private Sub Expect380(ByVal Number As Long, ByVal After As Long) + Assert.Strict.AreEqual 380, Number, "the error" ' VB6: 380, Invalid property value + Assert.Strict.AreEqual CLng(vbGreen), After, "the colour it had" ' VB6: the old colour is kept + End Sub + + ' Fixed in #2484 (BETA 1005). + + [TestCase] + Public Sub Form_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.ForeColor = vbGreen + On Error Resume Next: Form1.ForeColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Form_ForeColor_SystemColourOutOfRange() + Dim number As Long, after As Long + Load Form1: Form1.ForeColor = vbGreen + On Error Resume Next: Form1.ForeColor = SystemColourOutOfRange: number = Err.Number: On Error GoTo 0 + after = Form1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Form_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.BackColor = vbGreen + On Error Resume Next: Form1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Form_FillColor() + Dim number As Long, after As Long + Load Form1: Form1.FillColor = vbGreen + On Error Resume Next: Form1.FillColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.FillColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub PictureBox_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.PictureBox1.ForeColor = vbGreen + On Error Resume Next: Form1.PictureBox1.ForeColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.PictureBox1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub PictureBox_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.PictureBox1.BackColor = vbGreen + On Error Resume Next: Form1.PictureBox1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.PictureBox1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub PictureBox_FillColor() + Dim number As Long, after As Long + Load Form1: Form1.PictureBox1.FillColor = vbGreen + On Error Resume Next: Form1.PictureBox1.FillColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.PictureBox1.FillColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Label_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.Label1.ForeColor = vbGreen + On Error Resume Next: Form1.Label1.ForeColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.Label1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Label_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.Label1.BackColor = vbGreen + On Error Resume Next: Form1.Label1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.Label1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub TextBox_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.TextBox1.ForeColor = vbGreen + On Error Resume Next: Form1.TextBox1.ForeColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.TextBox1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub TextBox_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.TextBox1.BackColor = vbGreen + On Error Resume Next: Form1.TextBox1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.TextBox1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub CommandButton_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.CommandButton1.BackColor = vbGreen + On Error Resume Next: Form1.CommandButton1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.CommandButton1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub UserControl_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.UC1.TrySet "ForeColor", NotAColour, number, after: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub UserControl_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.UC1.TrySet "BackColor", NotAColour, number, after: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub UserControl_FillColor() + Dim number As Long, after As Long + Load Form1: Form1.UC1.TrySet "FillColor", NotAColour, number, after: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub MDIForm_BackColor() + Dim number As Long, after As Long + Load MyMDIForm: MyMDIForm.BackColor = vbGreen + On Error Resume Next: MyMDIForm.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = MyMDIForm.BackColor: Unload MyMDIForm + Expect380 number, after + End Sub + + [TestCase] + Public Sub MDIForm_BackColor_SystemColourOutOfRange() + Dim number As Long, after As Long + Load MyMDIForm: MyMDIForm.BackColor = vbGreen + On Error Resume Next: MyMDIForm.BackColor = SystemColourOutOfRange: number = Err.Number: On Error GoTo 0 + after = MyMDIForm.BackColor: Unload MyMDIForm + Expect380 number, after + End Sub + + ' Left in BETA 1005: these store the value and raise no error. + + [TestCase] + Public Sub Data_ForeColor() + Dim number As Long, after As Long + Load Form1: Form1.Data1.ForeColor = vbGreen + On Error Resume Next: Form1.Data1.ForeColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.Data1.ForeColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub Data_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.Data1.BackColor = vbGreen + On Error Resume Next: Form1.Data1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.Data1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub OLE_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.OLE1.BackColor = vbGreen + On Error Resume Next: Form1.OLE1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.OLE1.BackColor: Unload Form1 + Expect380 number, after + End Sub + + [TestCase] + Public Sub MultiFrame_BackColor() + Dim number As Long, after As Long + Load Form1: Form1.MultiFrame1.BackColor = vbGreen + On Error Resume Next: Form1.MultiFrame1.BackColor = NotAColour: number = Err.Number: On Error GoTo 0 + after = Form1.MultiFrame1.BackColor: Unload Form1 + Expect380 number, after + End Sub + +End Module diff --git a/bugs/colour-validation-data-ole-multiframe/src/Sources/Form1.tbform b/bugs/colour-validation-data-ole-multiframe/src/Sources/Form1.tbform new file mode 100644 index 000000000..6152d3179 --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/src/Sources/Form1.tbform @@ -0,0 +1,165 @@ +[ + { + "AlwaysShowKeyboardCues": false, + "Appearance": "vbAppear3d", + "AutoRedraw": false, + "BackColor": -2147483633, + "BorderStyle": "vbSizable", + "Caption": "Form1", + "ClipControls": true, + "ControlBox": true, + "DrawMode": "vbCopyPen", + "DrawStyle": "vbSolid", + "DrawWidth": 1, + "Enabled": true, + "FillColor": 0, + "FillStyle": "vbFSTransparent", + "FontBold": false, + "FontItalic": false, + "FontName": "Segoe UI", + "FontSize": 8, + "FontStrikethru": false, + "FontTransparent": true, + "FontUnderline": false, + "ForeColor": -2147483630, + "FormDesignerId": "{EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA01}", + "HasDC": true, + "Height": 300, + "HelpContextID": 0, + "Icon": [], + "KeyPreview": false, + "Left": 0, + "LinkMode": "vbLinkNone", + "LinkTopic": null, + "MDIChild": false, + "MaxButton": true, + "MaxHeight": 0, + "MaxWidth": 0, + "MinButton": true, + "MinHeight": 0, + "MinWidth": 0, + "MouseIcon": [], + "MousePointer": "vbDefault", + "Moveable": true, + "Name": "Form1", + "NegotiateMenus": false, + "OLEDropMode": "vbOLEDropNone", + "Opacity": 100, + "Palette": [], + "PaletteMode": "vbPaletteModeHalftone", + "Picture": [], + "PictureDpiScaling": false, + "RightToLeft": false, + "ScaleHeight": 4500, + "ScaleLeft": 0, + "ScaleMode": "vbTwips", + "ScaleTop": 0, + "ScaleWidth": 4500, + "ShowInTaskbar": true, + "StartUpPosition": "vbStartUpScreen", + "TabFocusAutoSelect": false, + "Tag": null, + "Top": 0, + "TopMost": false, + "TransparencyKey": -1, + "Visible": true, + "WhatsThisButton": false, + "WhatsThisHelp": false, + "Width": 300, + "WindowState": "vbNormal", + "__IDEOptions": { + "alignToGrid": false, + "gridHeight": 10, + "gridWidth": 10, + "lockedControls": [], + "multiColoredGrabbers": false, + "showGrid": true, + "showOutlines": false + }, + "__lastUpdateMarker": 2061908507, + "_className": "Form", + "_clsid": "{33AD4F38-6699-11CF-B70C-00AA0060D393}", + "_children": [ + { + "Index": -1, + "Name": "PictureBox1", + "Left": 20, + "Top": 20, + "Width": 100, + "Height": 30, + "_className": "PictureBox", + "_clsid": "{33AD4ED0-6699-11CF-B70C-00AA0060D393}" + }, + { + "Index": -1, + "Name": "Label1", + "Left": 20, + "Top": 60, + "Width": 100, + "Height": 30, + "_className": "Label", + "_clsid": "{33AD4ED8-6699-11CF-B70C-00AA0060D393}" + }, + { + "Index": -1, + "Name": "TextBox1", + "Left": 20, + "Top": 100, + "Width": 100, + "Height": 30, + "_className": "TextBox", + "_clsid": "{33AD4EE0-6699-11CF-B70C-00AA0060D393}" + }, + { + "Index": -1, + "Name": "CommandButton1", + "Left": 20, + "Top": 140, + "Width": 100, + "Height": 30, + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}" + }, + { + "Index": -1, + "Name": "UC1", + "Left": 140, + "Top": 20, + "Width": 100, + "Height": 100, + "_className": "UC1", + "_clsid": "{EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA10}" + }, + { + "Index": -1, + "Name": "Data1", + "Left": 260, + "Top": 20, + "Width": 100, + "Height": 30, + "_className": "Data", + "_clsid": "{33AD4FF8-6699-11CF-B70C-00AA0060D393}" + }, + { + "Index": -1, + "Name": "OLE1", + "Left": 260, + "Top": 60, + "Width": 100, + "Height": 30, + "_className": "OLE", + "_clsid": "{33AD5000-6699-11CF-B70C-00AA0060D393}" + }, + { + "Index": -1, + "Name": "MultiFrame1", + "Left": 260, + "Top": 100, + "Width": 100, + "Height": 30, + "_className": "MultiFrame", + "_clsid": "{5724D906-E1EE-455E-9FE3-BB322399766F}" + } + ] + } +] \ No newline at end of file diff --git a/bugs/colour-validation-data-ole-multiframe/src/Sources/Form1.twin b/bugs/colour-validation-data-ole-multiframe/src/Sources/Form1.twin new file mode 100644 index 000000000..fa6a1c3a9 --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/src/Sources/Form1.twin @@ -0,0 +1,9 @@ +[Description("")] +[FormDesignerId("EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA01")] +[PredeclaredId] +Class Form1 + + Sub New() + End Sub + +End Class diff --git a/bugs/colour-validation-data-ole-multiframe/src/Sources/MyMDIForm.tbform b/bugs/colour-validation-data-ole-multiframe/src/Sources/MyMDIForm.tbform new file mode 100644 index 000000000..904a66f2f --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/src/Sources/MyMDIForm.tbform @@ -0,0 +1,59 @@ +[ + { + "AlwaysShowKeyboardCues": false, + "Appearance": "vbAppear3d", + "AutoShowChildren": true, + "BackColor": -2147483636, + "Caption": "Window", + "Enabled": true, + "FormDesignerId": "{EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA02}", + "Form_AlwaysShowKeyboardCues": false, + "Form_Opacity": 100, + "Form_TopMost": false, + "Height": 571, + "HelpContextID": 0, + "Icon": "", + "Index": -1, + "Left": 0, + "LinkMode": "vbLinkNone", + "LinkTopic": null, + "MaxHeight": 0, + "MaxWidth": 0, + "MinHeight": 0, + "MinWidth": 0, + "MouseIcon": "", + "MousePointer": "vbDefault", + "Moveable": true, + "Name": "MyMDIForm", + "NegotiateToolbars": true, + "OLEDropMode": "vbOLEDropNone", + "Opacity": 100, + "Picture": "", + "PictureDpiScaling": true, + "RightToLeft": false, + "ScrollBars": true, + "StartUpPosition": "vbStartUpWindowsDefault", + "TabFocusAutoSelect": false, + "Tag": null, + "Top": 0, + "TopMost": false, + "TransparencyKey": -1, + "Visible": true, + "WhatsThisHelp": false, + "Width": 912, + "WindowState": "vbNormal", + "__IDEOptions": { + "alignToGrid": false, + "gridHeight": 10, + "gridWidth": 10, + "lockedControls": [], + "multiColoredGrabbers": true, + "showGrid": true, + "showOutlines": false + }, + "__lastUpdateMarker": 3416063361, + "_children": [], + "_className": "MDIForm", + "_clsid": "{33AD4F70-6699-11CF-B70C-00AA0060D393}" + } +] diff --git a/bugs/colour-validation-data-ole-multiframe/src/Sources/MyMDIForm.twin b/bugs/colour-validation-data-ole-multiframe/src/Sources/MyMDIForm.twin new file mode 100644 index 000000000..ee52cbbad --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/src/Sources/MyMDIForm.twin @@ -0,0 +1,9 @@ +[Description("")] +[FormDesignerId("EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA02")] +[PredeclaredId] +Class MyMDIForm + + Sub New() + End Sub + +End Class diff --git a/bugs/colour-validation-data-ole-multiframe/src/Sources/Startup.twin b/bugs/colour-validation-data-ole-multiframe/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/colour-validation-data-ole-multiframe/src/Sources/UC1.tbcontrol b/bugs/colour-validation-data-ole-multiframe/src/Sources/UC1.tbcontrol new file mode 100644 index 000000000..e962717bb --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/src/Sources/UC1.tbcontrol @@ -0,0 +1,78 @@ +[ + { + "AccessKeys": null, + "Alignable": false, + "Appearance": "vbAppear3d", + "AutoRedraw": false, + "BackColor": 11065270, + "BackStyle": "vbBFOpaque", + "BorderStyle": "vbNoBorder", + "CanGetFocus": true, + "ClipBehavior": "vbClipUseRegion", + "ClipControls": true, + "ControlContainer": false, + "DataBindingBehavior": "vbDataBindingNone", + "DataSourceBehavior": "vbDataSourceNone", + "DefaultCancel": false, + "DrawMode": "vbCopyPen", + "DrawStyle": "vbSolid", + "DrawWidth": 1, + "EditAtDesignTime": false, + "Enabled": true, + "FillColor": 0, + "FillStyle": "vbFSTransparent", + "FontBold": false, + "FontItalic": false, + "FontName": "Segoe UI", + "FontSize": 8, + "FontStrikethru": false, + "FontTransparent": true, + "FontUnderline": false, + "ForceResizeToContainer": false, + "ForeColor": -2147483630, + "FormDesignerId": "{EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA11}", + "ForwardFocus": false, + "HasDC": true, + "Height": 100, + "HitBehavior": "vbHitTestUseRegion", + "Index": -1, + "InvisibleAtRuntime": false, + "KeyPreview": false, + "Left": 0, + "MaskColor": -2147483633, + "MaskPicture": "", + "MouseIcon": "", + "MousePointer": "vbDefault", + "Name": "UC1", + "OLEDropMode": "vbOLEDropNone", + "OverrideMiscStatus": 0, + "Palette": "", + "PaletteMode": "vbPaletteModeContainer", + "Picture": "", + "PictureDpiScaling": true, + "Public": true, + "RightToLeft": false, + "ScaleHeight": 0, + "ScaleLeft": 0, + "ScaleMode": "vbTwips", + "ScaleTop": 0, + "ScaleWidth": 0, + "Tag": null, + "ToolboxBitmap": "", + "Top": 0, + "Width": 100, + "Windowless": false, + "__IDEOptions": { + "alignToGrid": false, + "gridHeight": 10, + "gridWidth": 10, + "lockedControls": [], + "showGrid": true, + "showOutlines": false + }, + "__lastUpdateMarker": 714827664, + "_children": [], + "_className": "UserControl", + "_clsid": "{33AD5010-6699-11CF-B70C-00AA0060D393}" + } +] \ No newline at end of file diff --git a/bugs/colour-validation-data-ole-multiframe/src/Sources/UC1.twin b/bugs/colour-validation-data-ole-multiframe/src/Sources/UC1.twin new file mode 100644 index 000000000..56a1b3e23 --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/src/Sources/UC1.twin @@ -0,0 +1,31 @@ +[FormDesignerId("EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA11")] +[ClassId("EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA10")] +[InterfaceId("EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA12")] +[EventInterfaceId("EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA13")] +[COMControl] +Public Class UC1 + + ' Sets one of the user control's own colour properties to vbGreen, then to + ' Value; returns the error the second assignment raised, and the colour after it. + Public Sub TrySet(ByVal Prop As String, ByVal Value As Long, ByRef Number As Long, ByRef After As Long) + Select Case Prop + Case "ForeColor": ForeColor = vbGreen + Case "BackColor": BackColor = vbGreen + Case "FillColor": FillColor = vbGreen + End Select + On Error Resume Next + Select Case Prop + Case "ForeColor": ForeColor = Value + Case "BackColor": BackColor = Value + Case "FillColor": FillColor = Value + End Select + Number = Err.Number + On Error GoTo 0 + Select Case Prop + Case "ForeColor": After = ForeColor + Case "BackColor": After = BackColor + Case "FillColor": After = FillColor + End Select + End Sub + +End Class diff --git a/bugs/colour-validation-data-ole-multiframe/vb6/Form1.frm b/bugs/colour-validation-data-ole-multiframe/vb6/Form1.frm new file mode 100644 index 000000000..5db10f220 --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/vb6/Form1.frm @@ -0,0 +1,62 @@ +VERSION 5.00 +Begin VB.Form Form1 + Caption = "Form1" + ClientHeight = 3000 + ClientLeft = 60 + ClientTop = 345 + ClientWidth = 4000 + LinkTopic = "Form1" + ScaleHeight = 3000 + ScaleWidth = 4000 + StartUpPosition = 3 'Windows Default + Visible = 0 'False + Begin VB.OLE OLE1 + Height = 315 + Left = 1200 + TabIndex = 8 + Top = 2280 + Width = 855 + End + Begin Probe.UC1 UC1 + Height = 1215 + Left = 3360 + TabIndex = 7 + Top = 120 + Width = 1215 + End + Begin VB.CommandButton CommandButton1 + Height = 255 + Left = 240 + TabIndex = 3 + Top = 1800 + Width = 855 + End + Begin VB.TextBox TextBox1 + Height = 255 + Left = 240 + TabIndex = 2 + Top = 1320 + Width = 855 + End + Begin VB.Label Label1 + Height = 255 + Left = 240 + Top = 840 + Width = 855 + End + Begin VB.PictureBox PictureBox1 + Height = 255 + Left = 240 + ScaleHeight = 195 + ScaleWidth = 795 + TabIndex = 0 + Top = 360 + Width = 855 + End +End +Attribute VB_Name = "Form1" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = False +Attribute VB_PredeclaredId = True +Attribute VB_Exposed = False +Option Explicit diff --git a/bugs/filed/exe-error-box-title/vb6/Form1.frm b/bugs/colour-validation-data-ole-multiframe/vb6/MDIForm1.frm similarity index 63% rename from bugs/filed/exe-error-box-title/vb6/Form1.frm rename to bugs/colour-validation-data-ole-multiframe/vb6/MDIForm1.frm index 4a68c9ecb..b922d4917 100644 --- a/bugs/filed/exe-error-box-title/vb6/Form1.frm +++ b/bugs/colour-validation-data-ole-multiframe/vb6/MDIForm1.frm @@ -1,16 +1,14 @@ VERSION 5.00 -Begin VB.Form Form1 - Caption = "Form1" +Begin VB.MDIForm MDIForm1 + Caption = "MDIForm1" ClientHeight = 3000 ClientLeft = 60 ClientTop = 345 ClientWidth = 4000 - LinkTopic = "Form1" - ScaleHeight = 3000 - ScaleWidth = 4000 + LinkTopic = "MDIForm1" StartUpPosition = 3 'Windows Default End -Attribute VB_Name = "Form1" +Attribute VB_Name = "MDIForm1" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = True diff --git a/bugs/colour-validation-data-ole-multiframe/vb6/Module1.bas b/bugs/colour-validation-data-ole-multiframe/vb6/Module1.bas new file mode 100644 index 000000000..63f9b3cd2 --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/vb6/Module1.bas @@ -0,0 +1,83 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The VB6 side of the reproducer: node scripts/bug_repro.mjs vb6 colour-validation-data-ole-multiframe + +Private Sub Cases() + Load Form1 + On Error Resume Next + Form1.ForeColor = vbGreen + Form1.ForeColor = -1 + Print #9, "Form.ForeColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.ForeColor): Err.Clear + Form1.ForeColor = vbGreen + Form1.ForeColor = &H8000001F + Print #9, "Form.ForeColor = &H8000001F: Err " & Err.Number & ", reads " & Hex$(Form1.ForeColor): Err.Clear + Form1.BackColor = vbGreen + Form1.BackColor = -1 + Print #9, "Form.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.BackColor): Err.Clear + Form1.FillColor = vbGreen + Form1.FillColor = -1 + Print #9, "Form.FillColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.FillColor): Err.Clear + Form1.PictureBox1.ForeColor = vbGreen + Form1.PictureBox1.ForeColor = -1 + Print #9, "PictureBox.ForeColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.PictureBox1.ForeColor): Err.Clear + Form1.PictureBox1.BackColor = vbGreen + Form1.PictureBox1.BackColor = -1 + Print #9, "PictureBox.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.PictureBox1.BackColor): Err.Clear + Form1.PictureBox1.FillColor = vbGreen + Form1.PictureBox1.FillColor = -1 + Print #9, "PictureBox.FillColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.PictureBox1.FillColor): Err.Clear + Form1.Label1.ForeColor = vbGreen + Form1.Label1.ForeColor = -1 + Print #9, "Label.ForeColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.Label1.ForeColor): Err.Clear + Form1.Label1.BackColor = vbGreen + Form1.Label1.BackColor = -1 + Print #9, "Label.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.Label1.BackColor): Err.Clear + Form1.TextBox1.ForeColor = vbGreen + Form1.TextBox1.ForeColor = -1 + Print #9, "TextBox.ForeColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.TextBox1.ForeColor): Err.Clear + Form1.TextBox1.BackColor = vbGreen + Form1.TextBox1.BackColor = -1 + Print #9, "TextBox.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.TextBox1.BackColor): Err.Clear + Form1.CommandButton1.BackColor = vbGreen + Form1.CommandButton1.BackColor = -1 + Print #9, "CommandButton.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.CommandButton1.BackColor): Err.Clear + Printer.ForeColor = vbGreen + Printer.ForeColor = -1 + Print #9, "Printer.ForeColor = -1: Err " & Err.Number & ", reads " & Hex$(Printer.ForeColor): Err.Clear + Printer.FillColor = vbGreen + Printer.FillColor = -1 + Print #9, "Printer.FillColor = -1: Err " & Err.Number & ", reads " & Hex$(Printer.FillColor): Err.Clear + Form1.UC1.RunCases + Form1.OLE1.BackColor = vbGreen + Form1.OLE1.BackColor = -1 + Print #9, "OLE.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.OLE1.BackColor): Err.Clear + Unload Form1 + Load MDIForm1 + MDIForm1.BackColor = vbGreen + MDIForm1.BackColor = -1 + Print #9, "MDIForm.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(MDIForm1.BackColor): Err.Clear + MDIForm1.BackColor = vbGreen + MDIForm1.BackColor = &H8000001F + Print #9, "MDIForm.BackColor = &H8000001F: Err " & Err.Number & ", reads " & Hex$(MDIForm1.BackColor): Err.Clear + MDIForm1.Show + MDIForm1.BackColor = vbGreen + MDIForm1.BackColor = -1 + Print #9, "MDIForm (shown).BackColor = -1: Err " & Err.Number & ", reads " & Hex$(MDIForm1.BackColor): Err.Clear + MDIForm1.BackColor = vbGreen + MDIForm1.BackColor = &H8000001F + Print #9, "MDIForm (shown).BackColor = &H8000001F: Err " & Err.Number & ", reads " & Hex$(MDIForm1.BackColor): Err.Clear + Unload MDIForm1 +End Sub + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Cases + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub diff --git a/bugs/filed/exe-error-box-title/vb6/Probe.vbp b/bugs/colour-validation-data-ole-multiframe/vb6/Probe.vbp similarity index 74% rename from bugs/filed/exe-error-box-title/vb6/Probe.vbp rename to bugs/colour-validation-data-ole-multiframe/vb6/Probe.vbp index fec2b8dc1..950884b68 100644 --- a/bugs/filed/exe-error-box-title/vb6/Probe.vbp +++ b/bugs/colour-validation-data-ole-multiframe/vb6/Probe.vbp @@ -1,9 +1,10 @@ Type=Exe Form=Form1.frm +Form=MDIForm1.frm +UserControl=UC1.ctl Module=Module1; Module1.bas Startup="Sub Main" ExeName32="Probe.exe" Name="Probe" -Title="ExeErrorBoxTitle" CompilationType=0 OptimizationType=0 diff --git a/bugs/colour-validation-data-ole-multiframe/vb6/UC1.ctl b/bugs/colour-validation-data-ole-multiframe/vb6/UC1.ctl new file mode 100644 index 000000000..20846f82c --- /dev/null +++ b/bugs/colour-validation-data-ole-multiframe/vb6/UC1.ctl @@ -0,0 +1,28 @@ +VERSION 5.00 +Begin VB.UserControl UC1 + ClientHeight = 1000 + ClientLeft = 0 + ClientTop = 0 + ClientWidth = 1000 + ScaleHeight = 1000 + ScaleWidth = 1000 +End +Attribute VB_Name = "UC1" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = False +Option Explicit + +Public Sub RunCases() + On Error Resume Next + UserControl.ForeColor = vbGreen + UserControl.ForeColor = -1 + Print #9, "UserControl.ForeColor = -1: Err " & Err.Number & ", reads " & Hex$(UserControl.ForeColor): Err.Clear + UserControl.BackColor = vbGreen + UserControl.BackColor = -1 + Print #9, "UserControl.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(UserControl.BackColor): Err.Clear + UserControl.FillColor = vbGreen + UserControl.FillColor = -1 + Print #9, "UserControl.FillColor = -1: Err " & Err.Number & ", reads " & Hex$(UserControl.FillColor): Err.Clear +End Sub diff --git a/bugs/connection-point-null-pointer-crash/connection-point-null-pointer-crash.twinproj b/bugs/connection-point-null-pointer-crash/connection-point-null-pointer-crash.twinproj new file mode 100644 index 000000000..1bbda6ee3 Binary files /dev/null and b/bugs/connection-point-null-pointer-crash/connection-point-null-pointer-crash.twinproj differ diff --git a/bugs/connection-point-null-pointer-crash/repro.json b/bugs/connection-point-null-pointer-crash/repro.json new file mode 100644 index 000000000..0cdc8cbc3 --- /dev/null +++ b/bugs/connection-point-null-pointer-crash/repro.json @@ -0,0 +1,16 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS ConnectionPointTests\\.Advise_Nothing_ReturnsEPointer$", + "^PASS ConnectionPointTests\\.Advise_UnassignedIUnknown_ReturnsEPointer$", + "^PASS ConnectionPointTests\\.Advise_NotASink_ReturnsENoInterface$", + "^FAIL ConnectionPointTests\\.GetConnectionInterface_NullIid_ReturnsEPointer: .*ACCESS_VIOLATION", + "^FAIL ConnectionPointTests\\.GetConnectionPointContainer_NullOut_ReturnsEPointer: .*ACCESS_VIOLATION", + "^FAIL ConnectionPointTests\\.FindConnectionPoint_NullIid_ReturnsEPointer: .*ACCESS_VIOLATION", + "^FAIL ConnectionPointTests\\.FindConnectionPoint_NullOut_ReturnsEPointer: .*ACCESS_VIOLATION", + "^FAIL ConnectionPointTests\\.EnumConnectionPoints_NullOut_ReturnsEPointer: .*ACCESS_VIOLATION" + ] + } +} diff --git a/bugs/connection-point-null-pointer-crash/src/Settings b/bugs/connection-point-null-pointer-crash/src/Settings new file mode 100644 index 000000000..efdc22b66 --- /dev/null +++ b/bugs/connection-point-null-pointer-crash/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ConnectionPointNullPointerCrash", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: A twinBASIC class's connection point and its container end in an access violation when given a null pointer", + "project.exportPathIsV2": true, + "project.id": "{E3B68C9D-C95D-43DB-9F2E-B6DEE35A2058}", + "project.name": "ConnectionPointNullPointerCrash", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/connection-point-null-pointer-crash/src/Sources/ConnectionPointTests.twin b/bugs/connection-point-null-pointer-crash/src/Sources/ConnectionPointTests.twin new file mode 100644 index 000000000..41ca7b365 --- /dev/null +++ b/bugs/connection-point-null-pointer-crash/src/Sources/ConnectionPointTests.twin @@ -0,0 +1,126 @@ +' Regression tests for #2488 and its follow-up. A class with an Event is a +' connectable object; these call its IConnectionPointContainer and its +' IConnectionPoint with a null pointer, where the COM contract (the Windows SDK +' pages for each method) says the method returns E_POINTER. Every out parameter +' is declared ByVal LongPtr, so that a null one can be passed. + +[InterfaceId("B196B284-BAB4-101A-B69C-00AA00341D07")] +Interface IConnectionPointContainerRaw Extends stdole.IUnknown + Sub EnumConnectionPoints(ByVal ppEnum As LongPtr) + Sub FindConnectionPoint(ByVal riid As LongPtr, ByVal ppCP As LongPtr) +End Interface + +[InterfaceId("B196B285-BAB4-101A-B69C-00AA00341D07")] +Interface IEnumConnectionPointsRaw Extends stdole.IUnknown + Sub Next(ByVal cConnections As Long, ByRef ppCP As IConnectionPointRaw, ByRef pcFetched As Long) +End Interface + +[InterfaceId("B196B286-BAB4-101A-B69C-00AA00341D07")] +Interface IConnectionPointRaw Extends stdole.IUnknown + Sub GetConnectionInterface(ByVal piid As LongPtr) + Sub GetConnectionPointContainer(ByVal ppCPC As LongPtr) + Function Advise(ByVal pUnkSink As stdole.IUnknown) As Long + Sub Unadvise(ByVal dwCookie As Long) +End Interface + +Class EventSource + Public Event Ping() +End Class + +Class NotASink + Public Value As Long +End Class + +[TestFixture] +Module ConnectionPointTests + + Private Const E_POINTER As Long = &H80004003 ' the Windows SDK, for a null pointer argument + Private Const E_NOINTERFACE As Long = &H80004002 + + Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _ + ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As LongPtr) + + ' The connection point of a new EventSource, through EnumConnectionPoints and Next. + Private Function PointOf(ByVal container As IConnectionPointContainerRaw) As IConnectionPointRaw + Dim pEnum As LongPtr + container.EnumConnectionPoints VarPtr(pEnum) + Dim points As IEnumConnectionPointsRaw + CopyMemory VarPtr(points), VarPtr(pEnum), LenB(pEnum) ' takes over the reference + Dim fetched As Long + points.Next 1, PointOf, fetched + End Function + + ' Fixed in #2488 (BETA 1005): Advise with a null sink returns E_POINTER. + + [TestCase] + Public Sub Advise_Nothing_ReturnsEPointer() + Dim point As IConnectionPointRaw = PointOf(New EventSource) + On Error Resume Next + point.Advise Nothing + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub Advise_UnassignedIUnknown_ReturnsEPointer() + Dim point As IConnectionPointRaw = PointOf(New EventSource) + Dim sink As stdole.IUnknown + On Error Resume Next + point.Advise sink + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub Advise_NotASink_ReturnsENoInterface() + Dim point As IConnectionPointRaw = PointOf(New EventSource) + On Error Resume Next + point.Advise New NotASink + Assert.Strict.AreEqual E_NOINTERFACE, Err.Number ' as VB6 returns, too + End Sub + + ' Left in BETA 1005: every other null pointer ends in an access violation. + + [TestCase] + Public Sub GetConnectionInterface_NullIid_ReturnsEPointer() + Dim point As IConnectionPointRaw = PointOf(New EventSource) + On Error Resume Next + point.GetConnectionInterface 0 + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub GetConnectionPointContainer_NullOut_ReturnsEPointer() + Dim point As IConnectionPointRaw = PointOf(New EventSource) + On Error Resume Next + point.GetConnectionPointContainer 0 + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub FindConnectionPoint_NullIid_ReturnsEPointer() + Dim container As IConnectionPointContainerRaw = New EventSource + Dim p As LongPtr + On Error Resume Next + container.FindConnectionPoint 0, VarPtr(p) + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub FindConnectionPoint_NullOut_ReturnsEPointer() + Dim container As IConnectionPointContainerRaw = New EventSource + Dim point As IConnectionPointRaw = PointOf(container) + Dim iid(15) As Byte + point.GetConnectionInterface VarPtr(iid(0)) + On Error Resume Next + container.FindConnectionPoint VarPtr(iid(0)), 0 + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + + [TestCase] + Public Sub EnumConnectionPoints_NullOut_ReturnsEPointer() + Dim container As IConnectionPointContainerRaw = New EventSource + On Error Resume Next + container.EnumConnectionPoints 0 + Assert.Strict.AreEqual E_POINTER, Err.Number + End Sub + +End Module diff --git a/bugs/connection-point-null-pointer-crash/src/Sources/Startup.twin b/bugs/connection-point-null-pointer-crash/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/connection-point-null-pointer-crash/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/connection-point-null-pointer-crash/vb6/Module1.bas b/bugs/connection-point-null-pointer-crash/vb6/Module1.bas new file mode 100644 index 000000000..c94b6723b --- /dev/null +++ b/bugs/connection-point-null-pointer-crash/vb6/Module1.bas @@ -0,0 +1,156 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' VB6 cannot call IConnectionPoint directly, so the vtable is called with DispCallFunc. +' IUnknown: 0 QueryInterface, 1 AddRef, 2 Release. +' IConnectionPointContainer: 3 EnumConnectionPoints, 4 FindConnectionPoint. +' IEnumConnectionPoints: 3 Next. IConnectionPoint: 3 GetConnectionInterface, +' 4 GetConnectionPointContainer, 5 Advise, 6 Unadvise. +' The case to run comes from the environment variable CASE (1 to 8), and is 2 when +' it is not set: 1, 3 and 4 are controls, and 2 and 5 to 8 pass a null pointer. +' Build and run it with: node scripts/bug_repro.mjs vb6 connection-point-null-pointer-crash + +Private Declare Function DispCallFunc Lib "oleaut32.dll" (ByVal pvInstance As Long, ByVal oVft As Long, ByVal cc As Long, ByVal vtReturn As Integer, ByVal cActuals As Long, prgvt As Integer, prgpvarg As Long, pvargResult As Variant) As Long +Private Declare Function IIDFromString Lib "ole32.dll" (ByVal lpsz As Long, lpiid As Any) As Long +Private Declare Function StringFromGUID2 Lib "ole32.dll" (rguid As Any, ByVal lpsz As Long, ByVal cchMax As Long) As Long +Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long) + +Private Const CC_STDCALL As Long = 4 +Private Const VT_I4 As Integer = 3 + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Cases + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub + +' Calls slot Slot of the COM object pObj with up to four 32-bit arguments; returns its HRESULT. +Function CallSlot(ByVal pObj As Long, ByVal Slot As Long, ByVal nArgs As Long, ByVal a0 As Long, ByVal a1 As Long, ByVal a2 As Long, ByVal a3 As Long) As Long + Dim vals(0 To 3) As Variant + Dim vts(0 To 3) As Integer + Dim ptrs(0 To 3) As Long + Dim vRet As Variant + Dim hr As Long + Dim i As Long + vals(0) = a0: vals(1) = a1: vals(2) = a2: vals(3) = a3 + For i = 0 To 3 + vts(i) = VT_I4 + ptrs(i) = VarPtr(vals(i)) + Next + vRet = CLng(0) + hr = DispCallFunc(pObj, Slot * 4, CC_STDCALL, VT_I4, nArgs, vts(0), ptrs(0), vRet) + If hr <> 0 Then + CallSlot = hr + Else + CallSlot = CLng(vRet) + End If +End Function + +Function GuidText(buf() As Byte) As String + Dim s As String + Dim n As Long + s = String$(80, vbNullChar) + n = StringFromGUID2(buf(0), StrPtr(s), 80) + If n > 1 Then GuidText = Left$(s, n - 1) Else GuidText = "(StringFromGUID2 failed)" +End Function + +Sub Cases() + Dim src As New Source + Dim iidContainer(0 To 15) As Byte + Dim iidOut(0 To 15) As Byte + Dim iidNull(0 To 15) As Byte + Dim iidSentinel(0 To 15) As Byte + Dim pContainer As Long, pEnum As Long, pPoint As Long, fetched As Long + Dim pOut As Long + Dim hr As Long + Dim which As String + Dim i As Long + + which = Environ$("CASE") + If which = "" Then which = "2" + Print #9, "case=" & which + + IIDFromString StrPtr("{B196B284-BAB4-101A-B69C-00AA00341D07}"), iidContainer(0) + hr = CallSlot(ObjPtr(src), 0, 2, VarPtr(iidContainer(0)), VarPtr(pContainer), 0, 0) + Print #9, "QueryInterface IConnectionPointContainer: hr=" & Hex$(hr) + If hr <> 0 Then Exit Sub + + hr = CallSlot(pContainer, 3, 1, VarPtr(pEnum), 0, 0, 0) + Print #9, "EnumConnectionPoints: hr=" & Hex$(hr) + If hr <> 0 Then Exit Sub + + hr = CallSlot(pEnum, 3, 3, 1, VarPtr(pPoint), VarPtr(fetched), 0) + Print #9, "Next: hr=" & Hex$(hr) & " fetched=" & fetched + If hr <> 0 Or pPoint = 0 Then Exit Sub + + ' Preliminary, a valid GetConnectionInterface: gives the IID the other cases need. + For i = 0 To 15: iidSentinel(i) = &HAA: iidOut(i) = &HAA: Next + hr = CallSlot(pPoint, 3, 1, VarPtr(iidOut(0)), 0, 0, 0) + + Select Case which + Case "1" + Print #9, "GetConnectionInterface(valid buffer): hr=" & Hex$(hr) & " iid=" & GuidText(iidOut) & " (buffer started as AA bytes)" + + Case "2" + Print #9, "valid GetConnectionInterface first: hr=" & Hex$(hr) & " iid=" & GuidText(iidOut) + Print #9, "before GetConnectionInterface(NULL)" + Close #9 + hr = CallSlot(pPoint, 3, 1, 0, 0, 0, 0) + Open App.Path & "\out.txt" For Append As #9 + Print #9, "after GetConnectionInterface(NULL): hr=" & Hex$(hr) + + Case "3" + Print #9, "valid GetConnectionInterface first: hr=" & Hex$(hr) & " iid=" & GuidText(iidOut) + pOut = 0 + hr = CallSlot(pContainer, 4, 2, VarPtr(iidOut(0)), VarPtr(pOut), 0, 0) + Print #9, "FindConnectionPoint(right IID, valid out): hr=" & Hex$(hr) & " pointer nonzero=" & (pOut <> 0) & " same as enumerated=" & (pOut = pPoint) + + Case "4" + pOut = 0 + hr = CallSlot(pContainer, 4, 2, VarPtr(iidNull(0)), VarPtr(pOut), 0, 0) + Print #9, "FindConnectionPoint(GUID_NULL, valid out): hr=" & Hex$(hr) & " pointer nonzero=" & (pOut <> 0) + + Case "5" + pOut = 0 + Print #9, "before FindConnectionPoint(NULL riid, valid out)" + Close #9 + hr = CallSlot(pContainer, 4, 2, 0, VarPtr(pOut), 0, 0) + Open App.Path & "\out.txt" For Append As #9 + Print #9, "after FindConnectionPoint(NULL riid, valid out): hr=" & Hex$(hr) & " pointer nonzero=" & (pOut <> 0) + + Case "6" + Print #9, "valid GetConnectionInterface first: hr=" & Hex$(hr) & " iid=" & GuidText(iidOut) + Print #9, "before FindConnectionPoint(right IID, NULL out)" + Close #9 + hr = CallSlot(pContainer, 4, 2, VarPtr(iidOut(0)), 0, 0, 0) + Open App.Path & "\out.txt" For Append As #9 + Print #9, "after FindConnectionPoint(right IID, NULL out): hr=" & Hex$(hr) + + Case "7" + Print #9, "before GetConnectionPointContainer(NULL out)" + Close #9 + hr = CallSlot(pPoint, 4, 1, 0, 0, 0, 0) + Open App.Path & "\out.txt" For Append As #9 + Print #9, "after GetConnectionPointContainer(NULL out): hr=" & Hex$(hr) + + Case "8" + Print #9, "before EnumConnectionPoints(NULL out)" + Close #9 + hr = CallSlot(pContainer, 3, 1, 0, 0, 0, 0) + Open App.Path & "\out.txt" For Append As #9 + Print #9, "after EnumConnectionPoints(NULL out): hr=" & Hex$(hr) + + Case Else + Print #9, "unknown case" + End Select + + CallSlot pPoint, 2, 0, 0, 0, 0, 0 + CallSlot pEnum, 2, 0, 0, 0, 0, 0 + CallSlot pContainer, 2, 0, 0, 0, 0, 0 +End Sub diff --git a/bugs/filed/advise-nothing-crashes/vb6/Probe.vbp b/bugs/connection-point-null-pointer-crash/vb6/Probe.vbp similarity index 100% rename from bugs/filed/advise-nothing-crashes/vb6/Probe.vbp rename to bugs/connection-point-null-pointer-crash/vb6/Probe.vbp diff --git a/bugs/filed/advise-nothing-crashes/vb6/Source.cls b/bugs/connection-point-null-pointer-crash/vb6/Source.cls similarity index 100% rename from bugs/filed/advise-nothing-crashes/vb6/Source.cls rename to bugs/connection-point-null-pointer-crash/vb6/Source.cls diff --git a/bugs/console-print-udt-linker-error/repro.json b/bugs/console-print-udt-linker-error/repro.json new file mode 100644 index 000000000..d6b93c445 --- /dev/null +++ b/bugs/console-print-udt-linker-error/repro.json @@ -0,0 +1,11 @@ +{ + "mode": "lane", + "lane": "ide:watches", + "tests": [ + "a watch on p shows it as VARIABLES does", + "a watch on p.X works", + "the next stop shows the watches again", + "? p in the DEBUG CONSOLE fails with a codegen error, and the console reports a linker error" + ], + "steps": "1. Open console-print-udt-linker-error.twinproj in the twinBASIC IDE, put a breakpoint on the line marked BREAK (F9) and press F5. The run stops there.\n2. Add a watch on p and one on p.X. Fixed in #2485 (BETA 1005): the watch on p shows {user defined type, 8 bytes}, of type Point; the watch on p.X shows 7, of type Long; at the next stop (F5) both show again, and the DEBUG CONSOLE has no linker error.\n3. Type ? p in the DEBUG CONSOLE and press Enter.\n4. Expected: an error about the expression, such as that p has no single value to print, and no [LINKER] line. Actual (BETA 1005): (compile error: codegen error; check for compilation errors), and the DEBUG CONSOLE prints [LINKER] compilation (codegen) error detected in 'Startup.{temp_procedure}' at line #1.\nAsserted by `ide-test.bat --only watches` (the project is test/ide/probes/watches): its first three tests assert the fixed watch, and its last asserts the ? p defect, so the lane passes while the bug is there." +} diff --git a/bugs/filed/array-const-in-array-const-crash/src/Settings b/bugs/console-print-udt-linker-error/src/Settings similarity index 83% rename from bugs/filed/array-const-in-array-const-crash/src/Settings rename to bugs/console-print-udt-linker-error/src/Settings index 7059415f1..b450226f6 100644 --- a/bugs/filed/array-const-in-array-const-crash/src/Settings +++ b/bugs/console-print-udt-linker-error/src/Settings @@ -1,12 +1,12 @@ { "configuration.inherits": "Defaults", - "project.appTitle": "ArrayConstInArrayConstCrash", + "project.appTitle": "ConsolePrintUdtLinkerError", "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", "project.buildType": "Standard EXE", - "project.description": "Reproduces: A module-level array Const initialised from an element of another array Const crashes the compiler", + "project.description": "Reproduces: `? p` in the Debug Console, where `p` is a user-defined type, fails with a codegen error and a linker error", "project.exportPathIsV2": true, - "project.id": "{155E2A64-CA94-4EBE-B050-FAC1C69B8ED9}", - "project.name": "ArrayConstInArrayConstCrash", + "project.id": "{B5995E31-57BA-45B4-B638-9BB216A73D59}", + "project.name": "ConsolePrintUdtLinkerError", "project.optionExplicit": true, "project.references": [ { diff --git a/bugs/console-print-udt-linker-error/src/Sources/Startup.twin b/bugs/console-print-udt-linker-error/src/Sources/Startup.twin new file mode 100644 index 000000000..35edec61f --- /dev/null +++ b/bugs/console-print-udt-linker-error/src/Sources/Startup.twin @@ -0,0 +1,21 @@ +' Put a breakpoint on the line marked BREAK (F9) and press F5. At the stop, a watch +' on p shows it, and a watch on p.X shows 7 (fixed in #2485). Type ? p in the +' Debug Console: it fails with a codegen error, and the console reports a linker +' error. +Module Startup + + Private Type Point + X As Long + Y As Long + End Type + + Public Sub Main() + Dim p As Point + Dim i As Long + p.X = 7 + For i = 1 To 2 + Debug.Print "pass " & i ' BREAK + Next + End Sub + +End Module diff --git a/bugs/dim-as-new-interface/dim-as-new-interface.twinproj b/bugs/dim-as-new-interface/dim-as-new-interface.twinproj new file mode 100644 index 000000000..525cc1bb7 Binary files /dev/null and b/bugs/dim-as-new-interface/dim-as-new-interface.twinproj differ diff --git a/bugs/dim-as-new-interface/repro.json b/bugs/dim-as-new-interface/repro.json new file mode 100644 index 000000000..fe5135bae --- /dev/null +++ b/bugs/dim-as-new-interface/repro.json @@ -0,0 +1,19 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS New_ProjectInterface$", + "^PASS New_DerivedInterface$", + "^PASS New_PackageInterface$", + "^PASS New_TypeLibraryInterface$", + "^PASS New_ClassThatImplements$", + "^PASS AsNew_Class$", + "^FAIL AsNew_ClassField: ", + "^FAIL AsNew_ModuleVariable: ", + "^FAIL AsNew_ProjectInterface: ", + "^FAIL AsNew_DerivedInterface: ", + "^FAIL AsNew_PackageInterface: " + ] + } +} diff --git a/bugs/dim-as-new-interface/src/Settings b/bugs/dim-as-new-interface/src/Settings new file mode 100644 index 000000000..e58f882bc --- /dev/null +++ b/bugs/dim-as-new-interface/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "DimAsNewInterface", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `Dim ... As New` on an `Interface` compiles, where `New` on one is refused with TB5074", + "project.exportPathIsV2": true, + "project.id": "{CB907B1B-174C-468E-A4D9-4D02DC86ADBC}", + "project.name": "DimAsNewInterface", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/dim-as-new-interface/src/Sources/NewOnInterfaceTests.twin b/bugs/dim-as-new-interface/src/Sources/NewOnInterfaceTests.twin new file mode 100644 index 000000000..e179cd889 --- /dev/null +++ b/bugs/dim-as-new-interface/src/Sources/NewOnInterfaceTests.twin @@ -0,0 +1,49 @@ +' Regression tests for #2486 and its follow-up, as compile cases: each line that +' ends in a ' CASE <Name>: <expected> comment must give that diagnostic on that +' line (a code, "error" for any error, or "none"). tbrun --tests checks them. +' New needs a class; an Interface has none, so every way of constructing one is +' a compile error, as New stdole.IUnknown is. + +[InterfaceId("E5B8C0A1-0000-4000-8000-0000000000D1")] +Interface IParent + Function F() As Long +End Interface + +[InterfaceId("E5B8C0A1-0000-4000-8000-0000000000D2")] +Interface IChild Extends IParent + Function G() As Long +End Interface + +Class Implementer + Implements IParent + Private Function IParent_F() As Long Implements IParent.F + Return 1 + End Function +End Class + +Class Holder + Public Item As New IParent ' CASE AsNew_ClassField: error +End Class + +Module NewOnInterfaceTests + + Private ModuleLevel As New IParent ' CASE AsNew_ModuleVariable: error + + ' Fixed in #2486 (BETA 1005): New on an Interface is refused with TB5074. + Private Sub NewExpressions() + Dim a As IParent = New IParent ' CASE New_ProjectInterface: TB5074 + Dim b As IChild = New IChild ' CASE New_DerivedInterface: TB5074 + Dim c As ErrorContext = New ErrorContext ' CASE New_PackageInterface: TB5074 + Dim d As stdole.IUnknown = New stdole.IUnknown ' CASE New_TypeLibraryInterface: TB5074 + Dim e As IParent = New Implementer ' CASE New_ClassThatImplements: none + End Sub + + ' Left in BETA 1005: Dim ... As New on an Interface compiles. + Private Sub AsNewDeclarations() + Dim p As New IParent ' CASE AsNew_ProjectInterface: error + Dim q As New IChild ' CASE AsNew_DerivedInterface: error + Dim r As New ErrorContext ' CASE AsNew_PackageInterface: error + Dim s As New Implementer ' CASE AsNew_Class: none + End Sub + +End Module diff --git a/bugs/dim-as-new-interface/src/Sources/Startup.twin b/bugs/dim-as-new-interface/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/dim-as-new-interface/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/file-errors-unspecified/file-errors-unspecified.twinproj b/bugs/file-errors-unspecified/file-errors-unspecified.twinproj new file mode 100644 index 000000000..bd08df406 Binary files /dev/null and b/bugs/file-errors-unspecified/file-errors-unspecified.twinproj differ diff --git a/bugs/file-errors-unspecified/repro.json b/bugs/file-errors-unspecified/repro.json new file mode 100644 index 000000000..8a98b7f3a --- /dev/null +++ b/bugs/file-errors-unspecified/repro.json @@ -0,0 +1,45 @@ +{ + "mode": "run", + "expect": { + "exit": 0, + "output": [ + "^Open missing file For Input: -2147467259 ", + "^Open in missing folder For Output: -2147467259 ", + "^Open in missing folder For Append: -2147467259 ", + "^Open in missing folder For Binary: -2147467259 ", + "^Open in missing folder For Random: -2147467259 ", + "^Open a folder For Input: -2147467259 ", + "^Open read-only file For Output: -2147467259 ", + "^Open invalid name For Output: -2147467259 ", + "^Open with a file number in use: -2147467259 ", + "^GetAttr missing file: -2147467259 ", + "^GetAttr in missing folder: -2147467259 ", + "^FileLen missing file: -2147467259 ", + "^FileDateTime missing file: -2147467259 ", + "^SetAttr missing file: -2147467259 ", + "^Kill missing file: -2147467259 ", + "^Kill pattern with no match: -2147467259 ", + "^Kill read-only file: -2147467259 ", + "^Kill open file: -2147467259 ", + "^Name missing file: -2147467259 ", + "^Name onto an existing file: -2147467259 ", + "^Name open file: -2147467259 ", + "^MkDir existing folder: -2147467259 ", + "^MkDir in missing folder: -2147467259 ", + "^RmDir missing folder: -2147467259 ", + "^RmDir folder not empty: -2147467259 ", + "^ChDir missing folder: -2147467259 ", + "^EOF of file number 99, not open: -2147467259 ", + "^LOF of file number 99, not open: -2147467259 ", + "^Loc of file number 99, not open: -2147467259 ", + "^Seek of file number 99, not open: -2147467259 ", + "^FileAttr of file number 99, not open: -2147467259 ", + "^Get from file number 99, not open: -2147467259 ", + "^Put to file number 99, not open: -2147467259 ", + "^Print to file number 99, not open: -2147467259 ", + "^Line Input from file number 99, not open: -2147467259 ", + "^Seek to position 0: -2147467259 ", + "^Put a record longer than Len: -2147467259 " + ] + } +} diff --git a/bugs/filed/gettypeinfo-badindex/src/Settings b/bugs/file-errors-unspecified/src/Settings similarity index 84% rename from bugs/filed/gettypeinfo-badindex/src/Settings rename to bugs/file-errors-unspecified/src/Settings index d49cf8a15..5ba650917 100644 --- a/bugs/filed/gettypeinfo-badindex/src/Settings +++ b/bugs/file-errors-unspecified/src/Settings @@ -1,12 +1,12 @@ { "configuration.inherits": "Defaults", - "project.appTitle": "GettypeinfoBadindex", + "project.appTitle": "FileErrorsUnspecified", "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", "project.buildType": "Standard EXE", - "project.description": "Reproduces: A twinBASIC class's GetTypeInfo with an iTInfo of 1 returns E_UNEXPECTED, not DISP_E_BADINDEX", + "project.description": "Reproduces: File statements and functions raise *Unspecified error* (-2147467259) in place of VB6's error numbers", "project.exportPathIsV2": true, - "project.id": "{A75D7620-90A1-4F6D-B0DB-449E47391782}", - "project.name": "GettypeinfoBadindex", + "project.id": "{60A6D83A-605C-4D59-B856-0AF42191C139}", + "project.name": "FileErrorsUnspecified", "project.optionExplicit": true, "project.references": [ { diff --git a/bugs/file-errors-unspecified/src/Sources/Startup.twin b/bugs/file-errors-unspecified/src/Sources/Startup.twin new file mode 100644 index 000000000..956ec0df1 --- /dev/null +++ b/bugs/file-errors-unspecified/src/Sources/Startup.twin @@ -0,0 +1,242 @@ +' Each case makes one file statement or function fail and prints the error it raises. +Module Startup + + Private Sub Show(ByVal Label As String) + Debug.Print Label & ": " & Err.Number & " " & Err.Description + End Sub + + Private Sub MakeFile(ByVal FilePath As String) + Dim f As Integer + f = FreeFile + Open FilePath For Output As #f + Print #f, "x" + Close #f + End Sub + + Public Sub Main() + Dim d As String, f As Integer, n As Variant, s As String, b As Byte + d = Environ$("TEMP") & "\FileErrorsUnspecified-" & Hex$(Timer * 100) + MkDir d + MakeFile d & "\exists.txt" + MakeFile d & "\readonly.txt" + SetAttr d & "\readonly.txt", vbReadOnly + MkDir d & "\full" + MakeFile d & "\full\a.txt" + + On Error Resume Next + + Err.Clear + f = FreeFile + Open d & "\missing.txt" For Input As #f + Show "Open missing file For Input" + Close #f + + Err.Clear + f = FreeFile + Open d & "\nofolder\x.txt" For Output As #f + Show "Open in missing folder For Output" + Close #f + + Err.Clear + f = FreeFile + Open d & "\nofolder\x.txt" For Append As #f + Show "Open in missing folder For Append" + Close #f + + Err.Clear + f = FreeFile + Open d & "\nofolder\x.txt" For Binary As #f + Show "Open in missing folder For Binary" + Close #f + + Err.Clear + f = FreeFile + Open d & "\nofolder\x.txt" For Random As #f + Show "Open in missing folder For Random" + Close #f + + Err.Clear + f = FreeFile + Open d For Input As #f + Show "Open a folder For Input" + Close #f + + Err.Clear + f = FreeFile + Open d & "\readonly.txt" For Output As #f + Show "Open read-only file For Output" + Close #f + + Err.Clear + f = FreeFile + Open d & "\a|b.txt" For Output As #f + Show "Open invalid name For Output" + Close #f + + Err.Clear + f = FreeFile + Open d & "\exists.txt" For Input As #f + Open d & "\readonly.txt" For Input As #f + Show "Open with a file number in use" + Close #f + + Err.Clear + n = GetAttr(d & "\missing.txt") + Show "GetAttr missing file" + + Err.Clear + n = GetAttr(d & "\nofolder\x.txt") + Show "GetAttr in missing folder" + + Err.Clear + n = FileLen(d & "\missing.txt") + Show "FileLen missing file" + + Err.Clear + n = FileDateTime(d & "\missing.txt") + Show "FileDateTime missing file" + + Err.Clear + SetAttr d & "\missing.txt", vbNormal + Show "SetAttr missing file" + + Err.Clear + f = FreeFile + Open d & "\exists.txt" For Input As #f + SetAttr d & "\exists.txt", vbNormal + Show "SetAttr open file" + Close #f + + Err.Clear + Kill d & "\missing.txt" + Show "Kill missing file" + + Err.Clear + Kill d & "\*.zzz" + Show "Kill pattern with no match" + + Err.Clear + Kill d & "\readonly.txt" + Show "Kill read-only file" + + Err.Clear + f = FreeFile + Open d & "\open1.txt" For Output As #f + Kill d & "\open1.txt" + Show "Kill open file" + Close #f + + Err.Clear + Name d & "\missing.txt" As d & "\new.txt" + Show "Name missing file" + + Err.Clear + Name d & "\exists.txt" As d & "\readonly.txt" + Show "Name onto an existing file" + + Err.Clear + f = FreeFile + Open d & "\open2.txt" For Output As #f + Name d & "\open2.txt" As d & "\renamed.txt" + Show "Name open file" + Close #f + + Err.Clear + MkDir d & "\full" + Show "MkDir existing folder" + + Err.Clear + MkDir d & "\nofolder\sub" + Show "MkDir in missing folder" + + Err.Clear + RmDir d & "\nofolder" + Show "RmDir missing folder" + + Err.Clear + RmDir d & "\full" + Show "RmDir folder not empty" + + Err.Clear + ChDir d & "\nofolder" + Show "ChDir missing folder" + + Err.Clear + FileCopy d & "\missing.txt", d & "\copy.txt" + Show "FileCopy missing source" + + Err.Clear + FileCopy d & "\exists.txt", d & "\nofolder\copy.txt" + Show "FileCopy into missing folder" + + Err.Clear + n = EOF(99) + Show "EOF of file number 99, not open" + + Err.Clear + n = LOF(99) + Show "LOF of file number 99, not open" + + Err.Clear + n = Loc(99) + Show "Loc of file number 99, not open" + + Err.Clear + n = Seek(99) + Show "Seek of file number 99, not open" + + Err.Clear + n = FileAttr(99, 1) + Show "FileAttr of file number 99, not open" + + Err.Clear + Get #99, 1, b + Show "Get from file number 99, not open" + + Err.Clear + Put #99, 1, b + Show "Put to file number 99, not open" + + Err.Clear + Print #99, "x" + Show "Print to file number 99, not open" + + Err.Clear + Line Input #99, s + Show "Line Input from file number 99, not open" + + Err.Clear + Close #99 + Show "Close file number 99, not open" + + Err.Clear + f = FreeFile + Open d & "\exists.txt" For Input As #f + Line Input #f, s + Line Input #f, s + Show "Line Input past the end of the file" + Close #f + + Err.Clear + f = FreeFile + Open d & "\exists.txt" For Binary As #f + Seek #f, 0 + Show "Seek to position 0" + Close #f + + Err.Clear + f = FreeFile + Open d & "\record.dat" For Random As #f Len = 4 + Put #f, 1, "longer than four bytes" + Show "Put a record longer than Len" + Close #f + + Err.Clear + SetAttr d & "\readonly.txt", vbNormal + Kill d & "\*.*" + Kill d & "\full\*.*" + RmDir d & "\full" + RmDir d + End Sub + +End Module diff --git a/bugs/file-errors-unspecified/vb6/Module1.bas b/bugs/file-errors-unspecified/vb6/Module1.bas new file mode 100644 index 000000000..03573bc5d --- /dev/null +++ b/bugs/file-errors-unspecified/vb6/Module1.bas @@ -0,0 +1,256 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The VB6 side of bugs/file-errors-unspecified: the same cases as src/Sources/Startup.twin. +' Build and run it with: node scripts/bug_repro.mjs vb6 file-errors-unspecified +' File number 9 is out.txt, so no case closes every file or uses number 9. + +Sub Show(ByVal Label As String) + Print #9, Label & ": " & Err.Number & " " & Err.Description +End Sub + +Sub MakeFile(ByVal FilePath As String) + Dim f As Integer + f = FreeFile + Open FilePath For Output As #f + Print #f, "x" + Close #f +End Sub + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Cases + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub + +Sub Cases() + Dim d As String, f As Integer, n As Variant, s As String, b As Byte + d = Environ$("TEMP") & "\FileErrorsUnspecified-vb6-" & Hex$(Timer * 100) + MkDir d + MakeFile d & "\exists.txt" + MakeFile d & "\readonly.txt" + SetAttr d & "\readonly.txt", vbReadOnly + MkDir d & "\full" + MakeFile d & "\full\a.txt" + + On Error Resume Next + + Err.Clear + f = FreeFile + Open d & "\missing.txt" For Input As #f + Show "Open missing file For Input" + Close #f + + Err.Clear + f = FreeFile + Open d & "\nofolder\x.txt" For Output As #f + Show "Open in missing folder For Output" + Close #f + + Err.Clear + f = FreeFile + Open d & "\nofolder\x.txt" For Append As #f + Show "Open in missing folder For Append" + Close #f + + Err.Clear + f = FreeFile + Open d & "\nofolder\x.txt" For Binary As #f + Show "Open in missing folder For Binary" + Close #f + + Err.Clear + f = FreeFile + Open d & "\nofolder\x.txt" For Random As #f + Show "Open in missing folder For Random" + Close #f + + Err.Clear + f = FreeFile + Open d For Input As #f + Show "Open a folder For Input" + Close #f + + Err.Clear + f = FreeFile + Open d & "\readonly.txt" For Output As #f + Show "Open read-only file For Output" + Close #f + + Err.Clear + f = FreeFile + Open d & "\a|b.txt" For Output As #f + Show "Open invalid name For Output" + Close #f + + Err.Clear + f = FreeFile + Open d & "\exists.txt" For Input As #f + Open d & "\readonly.txt" For Input As #f + Show "Open with a file number in use" + Close #f + + Err.Clear + n = GetAttr(d & "\missing.txt") + Show "GetAttr missing file" + + Err.Clear + n = GetAttr(d & "\nofolder\x.txt") + Show "GetAttr in missing folder" + + Err.Clear + n = FileLen(d & "\missing.txt") + Show "FileLen missing file" + + Err.Clear + n = FileDateTime(d & "\missing.txt") + Show "FileDateTime missing file" + + Err.Clear + SetAttr d & "\missing.txt", vbNormal + Show "SetAttr missing file" + + Err.Clear + f = FreeFile + Open d & "\exists.txt" For Input As #f + SetAttr d & "\exists.txt", vbNormal + Show "SetAttr open file" + Close #f + + Err.Clear + Kill d & "\missing.txt" + Show "Kill missing file" + + Err.Clear + Kill d & "\*.zzz" + Show "Kill pattern with no match" + + Err.Clear + Kill d & "\readonly.txt" + Show "Kill read-only file" + + Err.Clear + f = FreeFile + Open d & "\open1.txt" For Output As #f + Kill d & "\open1.txt" + Show "Kill open file" + Close #f + + Err.Clear + Name d & "\missing.txt" As d & "\new.txt" + Show "Name missing file" + + Err.Clear + Name d & "\exists.txt" As d & "\readonly.txt" + Show "Name onto an existing file" + + Err.Clear + f = FreeFile + Open d & "\open2.txt" For Output As #f + Name d & "\open2.txt" As d & "\renamed.txt" + Show "Name open file" + Close #f + + Err.Clear + MkDir d & "\full" + Show "MkDir existing folder" + + Err.Clear + MkDir d & "\nofolder\sub" + Show "MkDir in missing folder" + + Err.Clear + RmDir d & "\nofolder" + Show "RmDir missing folder" + + Err.Clear + RmDir d & "\full" + Show "RmDir folder not empty" + + Err.Clear + ChDir d & "\nofolder" + Show "ChDir missing folder" + + Err.Clear + FileCopy d & "\missing.txt", d & "\copy.txt" + Show "FileCopy missing source" + + Err.Clear + FileCopy d & "\exists.txt", d & "\nofolder\copy.txt" + Show "FileCopy into missing folder" + + Err.Clear + n = EOF(99) + Show "EOF of file number 99, not open" + + Err.Clear + n = LOF(99) + Show "LOF of file number 99, not open" + + Err.Clear + n = Loc(99) + Show "Loc of file number 99, not open" + + Err.Clear + n = Seek(99) + Show "Seek of file number 99, not open" + + Err.Clear + n = FileAttr(99, 1) + Show "FileAttr of file number 99, not open" + + Err.Clear + Get #99, 1, b + Show "Get from file number 99, not open" + + Err.Clear + Put #99, 1, b + Show "Put to file number 99, not open" + + Err.Clear + Print #99, "x" + Show "Print to file number 99, not open" + + Err.Clear + Line Input #99, s + Show "Line Input from file number 99, not open" + + Err.Clear + Close #99 + Show "Close file number 99, not open" + + Err.Clear + f = FreeFile + Open d & "\exists.txt" For Input As #f + Line Input #f, s + Line Input #f, s + Show "Line Input past the end of the file" + Close #f + + Err.Clear + f = FreeFile + Open d & "\exists.txt" For Binary As #f + Seek #f, 0 + Show "Seek to position 0" + Close #f + + Err.Clear + f = FreeFile + Open d & "\record.dat" For Random As #f Len = 4 + Put #f, 1, "longer than four bytes" + Show "Put a record longer than Len" + Close #f + + Err.Clear + SetAttr d & "\readonly.txt", vbNormal + Kill d & "\*.*" + Kill d & "\full\*.*" + RmDir d & "\full" + RmDir d +End Sub diff --git a/bugs/filed/first-conversion-spurious-overflow/vb6/Probe.vbp b/bugs/file-errors-unspecified/vb6/Probe.vbp similarity index 100% rename from bugs/filed/first-conversion-spurious-overflow/vb6/Probe.vbp rename to bugs/file-errors-unspecified/vb6/Probe.vbp diff --git a/bugs/filed/advise-nothing-crashes/REPORT.md b/bugs/filed/advise-nothing-crashes/REPORT.md deleted file mode 100644 index 100249983..000000000 --- a/bugs/filed/advise-nothing-crashes/REPORT.md +++ /dev/null @@ -1,44 +0,0 @@ -Filed as [twinbasic/twinbasic#2488](https://github.com/twinbasic/twinbasic/issues/2488). - -## `IConnectionPoint.Advise(Nothing)` on a twinBASIC class's connection point ends in an access violation - -**Describe the bug** -A class that declares an `Event` is a connectable object, and its connection point is reached through `IConnectionPointContainer`. Calling `Advise` on that connection point with a null sink raises a native `ACCESS_VIOLATION` and ends the run, instead of returning an error. It is the same crash whether the argument is `Nothing` or an unassigned `stdole.IUnknown` variable. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `advise-nothing-crashes.twinproj` (attached as `advise-nothing-crashes.zip`). Its one source file, `Startup.twin`, declares the project's own copies of `IConnectionPointContainer`, `IEnumConnectionPoints` and `IConnectionPoint` (`stdole` has none of them), a class with one event, and this: - ``` - Private Class Source - Public Event Ping() - End Class - - Public Sub Main() - Dim src As New Source - Dim container As IConnectionPointContainer = src - Dim points As IEnumConnectionPoints = container.EnumConnectionPoints() - Dim point As IConnectionPoint, fetched As Long - points.Next 1, point, fetched - Debug.Print "before" - Dim cookie As Long = point.Advise(Nothing) - Debug.Print "after " & cookie - End Sub - ``` -2. Run the project in the IDE (F5). -3. See `before` in the DEBUG CONSOLE, and then `NATIVE EXCEPTION: ACCESS_VIOLATION /Startup.twin; Startup.Main LINE 000033 [...twinBASIC_win32.dll+00389F3E]`. `after` is never printed. - -**Expected behavior** -`Advise` returns `E_POINTER`, which twinBASIC raises as run-time error `&H80004003` that `On Error` can handle. The Windows SDK page for [IConnectionPoint::Advise](https://learn.microsoft.com/en-us/windows/win32/api/ocidl/nf-ocidl-iconnectionpoint-advise) lists `E_POINTER` for "The value in *pUnkSink* or *pdwCookie* is not valid. For example, either pointer may be **NULL**." - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 995 - -**Additional context** -Severity: low in practice, since a program rarely advises a null sink, but a COM client written in any language can send one, and the connection point crashes the process that hosts the class instead of refusing the call. - -What was tried: an object that is not a sink gives an ordinary error (`E_NOINTERFACE`, `&H80004002`), so only a null pointer crashes. The address of the crash is the same in the standalone probe and in this project. - -VB6 crashes too. The same call on the connection point of a VB6 class with an event, made through `DispCallFunc` (the VB6 project is attached as `advise-nothing-crashes-vb6.zip`), ends the exe with an access violation (`0xC0000005` in `MSVBVM60.DLL`, in the Application event log) after it writes `before`. A fix therefore costs no compatibility: no program depends on the crash. - -<!-- Reproducer: bugs/advise-nothing-crashes/ (mode run, expects tbrun exit 5, the output `before` and the native exception); verified on 995. VB6 side in bugs/advise-nothing-crashes/vb6/ (VB6 6.0): out.txt ends at `before`, and `vb6` reports the crash. Stated in docs/Reference/COM-Interfaces/IConnectionPoint.md, section Advise, the last bullet of "In twinBASIC" and the WARNING after it, which names BETA 995 ("Advise with Nothing as the sink ends the program with an access violation"): when fixed, remove the WARNING, state what error it raises, and add Nothing to the example if it fits. --> diff --git a/bugs/filed/advise-nothing-crashes/advise-nothing-crashes.twinproj b/bugs/filed/advise-nothing-crashes/advise-nothing-crashes.twinproj deleted file mode 100644 index 474577671..000000000 Binary files a/bugs/filed/advise-nothing-crashes/advise-nothing-crashes.twinproj and /dev/null differ diff --git a/bugs/filed/advise-nothing-crashes/repro.json b/bugs/filed/advise-nothing-crashes/repro.json deleted file mode 100644 index 6d9e3116b..000000000 --- a/bugs/filed/advise-nothing-crashes/repro.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "mode": "run", - "expect": { - "exit": 5, - "output": [ - "^before$", - "NATIVE EXCEPTION: ACCESS_VIOLATION" - ] - }, - "issue": 2488 -} diff --git a/bugs/filed/advise-nothing-crashes/src/Sources/Startup.twin b/bugs/filed/advise-nothing-crashes/src/Sources/Startup.twin deleted file mode 100644 index caacaee96..000000000 --- a/bugs/filed/advise-nothing-crashes/src/Sources/Startup.twin +++ /dev/null @@ -1,37 +0,0 @@ -' Advise(Nothing) on the connection point of a class that has an Event ends in an ACCESS_VIOLATION. -' The contract returns E_POINTER. -[InterfaceId("B196B284-BAB4-101A-B69C-00AA00341D07")] -Private Interface IConnectionPointContainer Extends stdole.IUnknown - Function EnumConnectionPoints() As IEnumConnectionPoints -End Interface - -[InterfaceId("B196B285-BAB4-101A-B69C-00AA00341D07")] -Private Interface IEnumConnectionPoints Extends stdole.IUnknown - Sub Next(ByVal cConnections As Long, ByRef ppCP As IConnectionPoint, ByRef pcFetched As Long) -End Interface - -[InterfaceId("B196B286-BAB4-101A-B69C-00AA00341D07")] -Private Interface IConnectionPoint Extends stdole.IUnknown - Sub GetConnectionInterface(ByVal piid As LongPtr) - Function GetConnectionPointContainer() As IConnectionPointContainer - Function Advise(ByVal pUnkSink As stdole.IUnknown) As Long -End Interface - -Private Class Source - Public Event Ping() -End Class - -Module Startup - - Public Sub Main() - Dim src As New Source - Dim container As IConnectionPointContainer = src - Dim points As IEnumConnectionPoints = container.EnumConnectionPoints() - Dim point As IConnectionPoint, fetched As Long - points.Next 1, point, fetched - Debug.Print "before" - Dim cookie As Long = point.Advise(Nothing) - Debug.Print "after " & cookie - End Sub - -End Module diff --git a/bugs/filed/advise-nothing-crashes/vb6/Module1.bas b/bugs/filed/advise-nothing-crashes/vb6/Module1.bas deleted file mode 100644 index a9f005544..000000000 --- a/bugs/filed/advise-nothing-crashes/vb6/Module1.bas +++ /dev/null @@ -1,89 +0,0 @@ -Attribute VB_Name = "Module1" -Option Explicit - -' VB6 cannot call IConnectionPoint directly, so the vtable is called with DispCallFunc. -' IUnknown: 0 QueryInterface, 1 AddRef, 2 Release. -' IConnectionPointContainer: 3 EnumConnectionPoints, 4 FindConnectionPoint. -' IEnumConnectionPoints: 3 Next. IConnectionPoint: 3 GetConnectionInterface, -' 4 GetConnectionPointContainer, 5 Advise, 6 Unadvise. - -Private Declare Function DispCallFunc Lib "oleaut32.dll" (ByVal pvInstance As Long, ByVal oVft As Long, ByVal cc As Long, ByVal vtReturn As Integer, ByVal cActuals As Long, prgvt As Integer, prgpvarg As Long, pvargResult As Variant) As Long -Private Declare Function IIDFromString Lib "ole32.dll" (ByVal lpsz As Long, lpiid As Any) As Long -Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long) - -Private Const CC_STDCALL As Long = 4 -Private Const VT_I4 As Integer = 3 - -Sub Main() - On Error GoTo Fail - Open App.Path & "\out.txt" For Output As #9 - Cases - Close #9 - Exit Sub -Fail: - On Error Resume Next - Print #9, "unhandled error " & Err.Number & ": " & Err.Description - Close #9 -End Sub - -' Calls slot Slot of the COM object pObj with up to four 32-bit arguments; returns its HRESULT. -Function CallSlot(ByVal pObj As Long, ByVal Slot As Long, ByVal nArgs As Long, ByVal a0 As Long, ByVal a1 As Long, ByVal a2 As Long, ByVal a3 As Long) As Long - Dim pVft As Long - Dim vals(0 To 3) As Variant - Dim vts(0 To 3) As Integer - Dim ptrs(0 To 3) As Long - Dim vRet As Variant - Dim hr As Long - Dim i As Long - vals(0) = a0: vals(1) = a1: vals(2) = a2: vals(3) = a3 - For i = 0 To 3 - vts(i) = VT_I4 - ptrs(i) = VarPtr(vals(i)) - Next - CopyMemory pVft, ByVal pObj, 4 - vRet = CLng(0) - hr = DispCallFunc(pObj, Slot * 4, CC_STDCALL, VT_I4, nArgs, vts(0), ptrs(0), vRet) - If hr <> 0 Then - CallSlot = hr - Else - CallSlot = CLng(vRet) - End If -End Function - -Sub Cases() - Dim src As New Source - Dim iidContainer(0 To 15) As Byte - Dim pContainer As Long, pEnum As Long, pPoint As Long, fetched As Long - Dim cookie As Long - Dim hr As Long - - IIDFromString StrPtr("{B196B284-BAB4-101A-B69C-00AA00341D07}"), iidContainer(0) - hr = CallSlot(ObjPtr(src), 0, 2, VarPtr(iidContainer(0)), VarPtr(pContainer), 0, 0) - Print #9, "QueryInterface IConnectionPointContainer: error=" & Hex$(hr) - If hr <> 0 Then Exit Sub - - hr = CallSlot(pContainer, 3, 1, VarPtr(pEnum), 0, 0, 0) - Print #9, "EnumConnectionPoints: error=" & Hex$(hr) - If hr <> 0 Then Exit Sub - - hr = CallSlot(pEnum, 3, 3, 1, VarPtr(pPoint), VarPtr(fetched), 0) - Print #9, "Next: error=" & Hex$(hr) & " fetched=" & fetched - If hr <> 0 Or pPoint = 0 Then Exit Sub - - ' Advise with a null sink. The file is closed around the call so the line survives a crash. - Print #9, "before" - Close #9 - cookie = -1 - hr = CallSlot(pPoint, 5, 2, 0, VarPtr(cookie), 0, 0) - Open App.Path & "\out.txt" For Append As #9 - Print #9, "after Advise(Nothing): error=" & Hex$(hr) & " cookie=" & cookie - - If hr = 0 And cookie <> 0 Then - hr = CallSlot(pPoint, 6, 1, cookie, 0, 0, 0) - Print #9, "Unadvise " & cookie & ": error=" & Hex$(hr) - End If - - CallSlot pPoint, 2, 0, 0, 0, 0, 0 - CallSlot pEnum, 2, 0, 0, 0, 0, 0 - CallSlot pContainer, 2, 0, 0, 0, 0, 0 -End Sub diff --git a/bugs/filed/advise-unadvise-hresults/REPORT.md b/bugs/filed/advise-unadvise-hresults/REPORT.md deleted file mode 100644 index 22e07b371..000000000 --- a/bugs/filed/advise-unadvise-hresults/REPORT.md +++ /dev/null @@ -1,44 +0,0 @@ -Filed as [twinbasic/twinbasic#2487](https://github.com/twinbasic/twinbasic/issues/2487). - -## `Unadvise` with a cookie that names no connection succeeds, where VB6 returns CONNECT_E_NOCONNECTION - -**Describe the bug** -On the connection point of a twinBASIC class that has an `Event`, `Unadvise` with a cookie that names no connection (0 and 99 were tried) succeeds silently and does nothing. The COM contract returns an error, and VB6 returns `CONNECT_E_NOCONNECTION` (`&H80040200`). - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `advise-unadvise-hresults.twinproj` (attached as `advise-unadvise-hresults.zip`). Its one source file, `Startup.twin`, declares the project's own copies of the connection-point interfaces (`stdole` has none of them), a class `Source` with one event, and a class `NotASink` with one field. `Sub Main` runs, with `On Error Resume Next`: - ``` - cookie = point.Advise(sink) ' sink is a NotASink - point.Unadvise 0 - point.Unadvise 99 - ``` -2. Run the project in the IDE (F5). -3. See in the DEBUG CONSOLE: - ``` - Advise, a sink without the outgoing interface: error=80004002 - Unadvise 0: error=0 - Unadvise 99: error=0 - ``` - -**Expected behavior** -`Unadvise` reports the bad cookie, as VB6 does. The page for [IConnectionPoint::Unadvise](https://learn.microsoft.com/en-us/windows/win32/api/ocidl/nf-ocidl-iconnectionpoint-unadvise) lists `E_POINTER` for "The value in *dwCookie* does not represent a valid connection", and `CONNECT_E_NOCONNECTION` is the code the `CONNECT_E_` range has for it. It is not `S_OK`. - -The same calls on the connection point of a VB6 class with an event, made through `DispCallFunc` (the VB6 project is attached as `advise-unadvise-hresults-vb6.zip`): -``` -Advise, a sink without the outgoing interface: error=80004002 - cookie=-1 -Unadvise 99: error=80040200 -before Unadvise 0 -``` -`Unadvise 0` comes last because it ends the VB6 exe with an access violation (`0xC0000005` in `MSVBVM60.DLL`). twinBASIC's 0 for that cookie is no worse than VB6's crash; the cookie 99 is the clean comparison. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 995 - -**Additional context** -Severity: low. A client that releases a connection twice, or with a wrong cookie, is told it worked. - -`Advise` with a sink that does not answer `QueryInterface` for the outgoing interface fails with `E_NOINTERFACE` (`&H80004002`), where the Windows SDK page for [IConnectionPoint::Advise](https://learn.microsoft.com/en-us/windows/win32/api/ocidl/nf-ocidl-iconnectionpoint-advise) says the method "must return CONNECT_E_CANNOTCONNECT" (`&H80040201`). VB6 returns `E_NOINTERFACE` too, so that is not reported here; the reproducer prints it as a control. A sink that is an ordinary twinBASIC class gives the same `E_NOINTERFACE` whether it has no members or implements `IDispatch` (the outgoing interface's identifier is generated for each build, so a twinBASIC class cannot answer for it). Nothing is connected afterwards, and no event reaches the sink. Calling `Unadvise` a second time with the cookie of the failed `Advise`, which is 0, also returns without an error. -<!-- Reproducer: bugs/advise-unadvise-hresults/ (mode run, expects tbrun exit 0 and the three lines above); verified on 995. VB6 side in bugs/advise-unadvise-hresults/vb6/ (VB6 6.0), the output above; it ends at `before Unadvise 0` because that call crashes the exe. Narrowed to Unadvise at the owner's word (2026-10-04), since VB6 also returns E_NOINTERFACE from Advise. Stated in docs/Reference/COM-Interfaces/IConnectionPoint.md, sections Advise (the second bullet of "In twinBASIC", which says VB6 returns E_NOINTERFACE too: no change when this is fixed) and Unadvise (the WARNING, which names BETA 995, about a cookie that names no connection) and the last example, whose comments show 80004002 and 0: when fixed, remove the WARNING, and change the rest. When the page is updated, state the code twinBASIC returns for a bad cookie. --> diff --git a/bugs/filed/advise-unadvise-hresults/advise-unadvise-hresults.twinproj b/bugs/filed/advise-unadvise-hresults/advise-unadvise-hresults.twinproj deleted file mode 100644 index e74ff1737..000000000 Binary files a/bugs/filed/advise-unadvise-hresults/advise-unadvise-hresults.twinproj and /dev/null differ diff --git a/bugs/filed/advise-unadvise-hresults/repro.json b/bugs/filed/advise-unadvise-hresults/repro.json deleted file mode 100644 index a7a46e940..000000000 --- a/bugs/filed/advise-unadvise-hresults/repro.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mode": "run", - "expect": { - "exit": 0, - "output": [ - "^Advise, a sink without the outgoing interface: error=80004002$", - "^Unadvise 0: error=0$", - "^Unadvise 99: error=0$" - ] - }, - "issue": 2487 -} diff --git a/bugs/filed/advise-unadvise-hresults/src/Sources/Startup.twin b/bugs/filed/advise-unadvise-hresults/src/Sources/Startup.twin deleted file mode 100644 index adf74c54e..000000000 --- a/bugs/filed/advise-unadvise-hresults/src/Sources/Startup.twin +++ /dev/null @@ -1,55 +0,0 @@ -' Advise with a sink that does not support the outgoing interface fails with E_NOINTERFACE, -' where CONNECT_E_CANNOTCONNECT (&H80040201) is expected. Unadvise with a cookie that names -' no connection succeeds, where an error is expected. -[InterfaceId("B196B284-BAB4-101A-B69C-00AA00341D07")] -Private Interface IConnectionPointContainer Extends stdole.IUnknown - Function EnumConnectionPoints() As IEnumConnectionPoints -End Interface - -[InterfaceId("B196B285-BAB4-101A-B69C-00AA00341D07")] -Private Interface IEnumConnectionPoints Extends stdole.IUnknown - Sub Next(ByVal cConnections As Long, ByRef ppCP As IConnectionPoint, ByRef pcFetched As Long) -End Interface - -[InterfaceId("B196B286-BAB4-101A-B69C-00AA00341D07")] -Private Interface IConnectionPoint Extends stdole.IUnknown - Sub GetConnectionInterface(ByVal piid As LongPtr) - Function GetConnectionPointContainer() As IConnectionPointContainer - Function Advise(ByVal pUnkSink As stdole.IUnknown) As Long - Sub Unadvise(ByVal dwCookie As Long) -End Interface - -Private Class Source - Public Event Ping() -End Class - -Private Class NotASink - Public Value As Long -End Class - -Module Startup - - Private Sub Report(ByVal What As String) - Debug.Print What & ": error=" & Hex$(Err.Number) - Err.Clear - End Sub - - Public Sub Main() - Dim src As New Source - Dim container As IConnectionPointContainer = src - Dim points As IEnumConnectionPoints = container.EnumConnectionPoints() - Dim point As IConnectionPoint, fetched As Long - points.Next 1, point, fetched - - Dim sink As New NotASink - Dim cookie As Long - On Error Resume Next - cookie = point.Advise(sink) - Report "Advise, a sink without the outgoing interface" - point.Unadvise 0 - Report "Unadvise 0" - point.Unadvise 99 - Report "Unadvise 99" - End Sub - -End Module diff --git a/bugs/filed/advise-unadvise-hresults/vb6/Module1.bas b/bugs/filed/advise-unadvise-hresults/vb6/Module1.bas deleted file mode 100644 index 2f3b37f1d..000000000 --- a/bugs/filed/advise-unadvise-hresults/vb6/Module1.bas +++ /dev/null @@ -1,97 +0,0 @@ -Attribute VB_Name = "Module1" -Option Explicit - -' VB6 cannot call IConnectionPoint directly, so the vtable is called with DispCallFunc. -' IUnknown: 0 QueryInterface, 1 AddRef, 2 Release. -' IConnectionPointContainer: 3 EnumConnectionPoints, 4 FindConnectionPoint. -' IEnumConnectionPoints: 3 Next. IConnectionPoint: 3 GetConnectionInterface, -' 4 GetConnectionPointContainer, 5 Advise, 6 Unadvise. - -Private Declare Function DispCallFunc Lib "oleaut32.dll" (ByVal pvInstance As Long, ByVal oVft As Long, ByVal cc As Long, ByVal vtReturn As Integer, ByVal cActuals As Long, prgvt As Integer, prgpvarg As Long, pvargResult As Variant) As Long -Private Declare Function IIDFromString Lib "ole32.dll" (ByVal lpsz As Long, lpiid As Any) As Long - -Private Const CC_STDCALL As Long = 4 -Private Const VT_I4 As Integer = 3 - -Sub Main() - On Error GoTo Fail - Open App.Path & "\out.txt" For Output As #9 - Cases - Close #9 - Exit Sub -Fail: - On Error Resume Next - Print #9, "unhandled error " & Err.Number & ": " & Err.Description - Close #9 -End Sub - -' Calls slot Slot of the COM object pObj with up to four 32-bit arguments; returns its HRESULT. -Function CallSlot(ByVal pObj As Long, ByVal Slot As Long, ByVal nArgs As Long, ByVal a0 As Long, ByVal a1 As Long, ByVal a2 As Long, ByVal a3 As Long) As Long - Dim vals(0 To 3) As Variant - Dim vts(0 To 3) As Integer - Dim ptrs(0 To 3) As Long - Dim vRet As Variant - Dim hr As Long - Dim i As Long - vals(0) = a0: vals(1) = a1: vals(2) = a2: vals(3) = a3 - For i = 0 To 3 - vts(i) = VT_I4 - ptrs(i) = VarPtr(vals(i)) - Next - vRet = CLng(0) - hr = DispCallFunc(pObj, Slot * 4, CC_STDCALL, VT_I4, nArgs, vts(0), ptrs(0), vRet) - If hr <> 0 Then - CallSlot = hr - Else - CallSlot = CLng(vRet) - End If -End Function - -Sub Report(ByVal What As String, ByVal hr As Long) - Note What & ": error=" & Hex$(hr) -End Sub - -Sub Note(ByVal Text As String) - Print #9, Text - ' Close and reopen so every line is on disk even if the next call crashes the exe. - Close #9 - Open App.Path & "\out.txt" For Append As #9 -End Sub - -Sub Cases() - Dim src As New Source - Dim sink As New NotASink - Dim iidContainer(0 To 15) As Byte - Dim pContainer As Long, pEnum As Long, pPoint As Long, fetched As Long - Dim cookie As Long - Dim hr As Long - - IIDFromString StrPtr("{B196B284-BAB4-101A-B69C-00AA00341D07}"), iidContainer(0) - hr = CallSlot(ObjPtr(src), 0, 2, VarPtr(iidContainer(0)), VarPtr(pContainer), 0, 0) - Report "QueryInterface IConnectionPointContainer", hr - If hr <> 0 Then Exit Sub - - hr = CallSlot(pContainer, 3, 1, VarPtr(pEnum), 0, 0, 0) - Report "EnumConnectionPoints", hr - If hr <> 0 Then Exit Sub - - hr = CallSlot(pEnum, 3, 3, 1, VarPtr(pPoint), VarPtr(fetched), 0) - Report "Next (fetched=" & fetched & ")", hr - If hr <> 0 Or pPoint = 0 Then Exit Sub - - cookie = -1 - hr = CallSlot(pPoint, 5, 2, ObjPtr(sink), VarPtr(cookie), 0, 0) - Report "Advise, a sink without the outgoing interface", hr - Print #9, " cookie=" & cookie - - hr = CallSlot(pPoint, 6, 1, 99, 0, 0, 0) - Report "Unadvise 99", hr - ' Unadvise 0 ends the VB6 exe on the spot, so it is last and flagged first. - Note "before Unadvise 0" - hr = CallSlot(pPoint, 6, 1, 0, 0, 0, 0) - Report "Unadvise 0", hr - - CallSlot pPoint, 2, 0, 0, 0, 0, 0 - CallSlot pEnum, 2, 0, 0, 0, 0, 0 - CallSlot pContainer, 2, 0, 0, 0, 0, 0 -End Sub diff --git a/bugs/filed/advise-unadvise-hresults/vb6/NotASink.cls b/bugs/filed/advise-unadvise-hresults/vb6/NotASink.cls deleted file mode 100644 index 7be037462..000000000 --- a/bugs/filed/advise-unadvise-hresults/vb6/NotASink.cls +++ /dev/null @@ -1,16 +0,0 @@ -VERSION 1.0 CLASS -BEGIN - MultiUse = -1 'True - Persistable = 0 'NotPersistable - DataBindingBehavior = 0 'vbNone - DataSourceBehavior = 0 'vbNone - MTSTransactionMode = 0 'NotAnMTSObject -END -Attribute VB_Name = "NotASink" -Attribute VB_GlobalNameSpace = False -Attribute VB_Creatable = True -Attribute VB_PredeclaredId = False -Attribute VB_Exposed = True -Option Explicit - -Public Value As Long diff --git a/bugs/filed/advise-unadvise-hresults/vb6/Source.cls b/bugs/filed/advise-unadvise-hresults/vb6/Source.cls deleted file mode 100644 index 2ed814eab..000000000 --- a/bugs/filed/advise-unadvise-hresults/vb6/Source.cls +++ /dev/null @@ -1,16 +0,0 @@ -VERSION 1.0 CLASS -BEGIN - MultiUse = -1 'True - Persistable = 0 'NotPersistable - DataBindingBehavior = 0 'vbNone - DataSourceBehavior = 0 'vbNone - MTSTransactionMode = 0 'NotAnMTSObject -END -Attribute VB_Name = "Source" -Attribute VB_GlobalNameSpace = False -Attribute VB_Creatable = True -Attribute VB_PredeclaredId = False -Attribute VB_Exposed = True -Option Explicit - -Public Event Ping() diff --git a/bugs/filed/array-const-element-overflow-crash/REPORT.md b/bugs/filed/array-const-element-overflow-crash/REPORT.md deleted file mode 100644 index 27a02d03b..000000000 --- a/bugs/filed/array-const-element-overflow-crash/REPORT.md +++ /dev/null @@ -1,36 +0,0 @@ -Filed as [twinbasic/twinbasic#2475](https://github.com/twinbasic/twinbasic/issues/2475). - -## A module-level array `Const` with an element that does not fit its type crashes the compiler - -**Describe the bug** -An array `Const` at module level whose element is too large for the element type takes the compiler down: `Const Big() As Long = Array(2147483648)`. The IDE's compile restarts the compiler, which crashes again, and the project can neither be compiled nor run. The crash happens whether or not the constant is used. Observed with `tbbuild`, which reports "the compiler crashed 3x". - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `array-const-element-overflow-crash.twinproj` (attached as `array-const-element-overflow-crash.zip`). Its one module is: - ``` - Module Startup - Const Big() As Long = Array(2147483648) - Public Sub Main() - End Sub - End Module - ``` -2. Compile it. The compiler crashes. - -**Expected behavior** -A compile error, as for a scalar constant: `Const K As Long = 2147483648` is refused with TB5002, and `Const Small() As Integer = Array(32768)` with TB5001. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 997 - -**Additional context** -BETA 995 has no array constants: every array `Const` is refused there with TB5245, *array cannot be initialized here*. VB6 has no array constants either (`Const A() As Long = Array(1)` is a syntax error), so there is no VB6 project. - -What crashes, each in a module of its own, unused or used: `Array(2147483648)`, `Array(2147483648#)`, `Array(3000000000.5)` and `Array(1, 2147483648)` for `Long`; `Array(40000.5)` for `Integer`; `Array(300.5)` and `Array(-1.5)` for `Byte`; `Array(9223372036854775808#)` for `LongLong`. What does not: the same constant declared inside a procedure compiles clean; a `Single` or `Double` array with a huge element (`Array(1E+300)`) compiles; an `Integer` or `Byte` element that is an integer literal too large (`Array(32768)`) is the ordinary TB5001. - -A second crash seems to belong with this one, and it is not in the reproducer. A `Single` array `Const` whose element overflows, `Const A() As Single = Array(1E+39)`, compiles alone, but crashes the compiler when a later array `Const` of `Double` calls `Sqr`, `Timer` or `Rnd`, for example `Const B() As Double = Array(Sqr(4))` on the next line. The reverse order, `Abs` or `Len` in place of those, `Now` or `Date`, and a `Single` array of `Timer` do not crash. The cause is not established; it could be an overflow flag left by the first constant, which the second one's call then trips over. - -Severity: a compiler crash, and the project cannot be opened for compiling until the constant is removed. - -<!-- Reproducer: bugs/array-const-element-overflow-crash/ (mode compile, expects tbbuild exit 4); verified on BETA 997 by `bug_repro.mjs verify`; on 995 the same project gives TB5245, as every array Const does there. The Sqr/Timer/Rnd variant was checked with `tbbuild` on BETA 997 in scratch projects (beta997-probes c24_KJ_KTm and the t_* cases) and is not in the reproducer. docs/Reference/Core/Const.md (the "Array constants" section) carries a `> [!WARNING]` for it, naming BETA 997 (a crash, owner's rule), removed once a fixed build is released. --> diff --git a/bugs/filed/array-const-element-overflow-crash/array-const-element-overflow-crash.twinproj b/bugs/filed/array-const-element-overflow-crash/array-const-element-overflow-crash.twinproj deleted file mode 100644 index 01c0c8fc2..000000000 Binary files a/bugs/filed/array-const-element-overflow-crash/array-const-element-overflow-crash.twinproj and /dev/null differ diff --git a/bugs/filed/array-const-element-overflow-crash/repro.json b/bugs/filed/array-const-element-overflow-crash/repro.json deleted file mode 100644 index 5be4e77bf..000000000 --- a/bugs/filed/array-const-element-overflow-crash/repro.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mode": "compile", - "expect": { - "exit": 4 - }, - "issue": 2475 -} diff --git a/bugs/filed/array-const-element-overflow-crash/src/Sources/Startup.twin b/bugs/filed/array-const-element-overflow-crash/src/Sources/Startup.twin deleted file mode 100644 index f3ad96fac..000000000 --- a/bugs/filed/array-const-element-overflow-crash/src/Sources/Startup.twin +++ /dev/null @@ -1,9 +0,0 @@ -' Crashes the compiler: an array Const whose element does not fit the element type. -Module Startup - - Const Big() As Long = Array(2147483648) - - Public Sub Main() - End Sub - -End Module diff --git a/bugs/filed/array-const-in-array-const-crash/REPORT.md b/bugs/filed/array-const-in-array-const-crash/REPORT.md deleted file mode 100644 index 1a2ba6f71..000000000 --- a/bugs/filed/array-const-in-array-const-crash/REPORT.md +++ /dev/null @@ -1,37 +0,0 @@ -Filed as [twinbasic/twinbasic#2474](https://github.com/twinbasic/twinbasic/issues/2474). - -## A module-level array `Const` initialised from an element of another array `Const` crashes the compiler - -**Describe the bug** -At module level, an array `Const` whose `Array(...)` list names an element of another array `Const` crashes the compiler: `Const A() As Long = Array(1, 2, 3)` followed by `Const B() As Long = Array(A(0))`. The compiler crashes again each time the IDE restarts it, so the project cannot be compiled or run. Observed with `tbbuild`, which reports "the compiler crashed 3x". - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `array-const-in-array-const-crash.twinproj` (attached as `array-const-in-array-const-crash.zip`). Its one module is: - ``` - Module Startup - Const A() As Long = Array(1, 2, 3) - Const B() As Long = Array(A(0)) - Public Sub Main() - End Sub - End Module - ``` -2. Compile it. The compiler crashes. - -**Expected behavior** -Either `B` is `Array(1)`, as it is when the two constants are declared in a procedure, or the compiler refuses the element with an error, as it does for a scalar constant (`Const S As Long = A(0)` is TB5002). - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 997 - -**Additional context** -BETA 995 has no array constants: every array `Const` is refused there with TB5245. VB6 has none either, so there is no VB6 project. - -It crashes with `B` used or unused, with `Private` constants, with `Double` arrays, with the two in the opposite order, and when an array refers to its own element (`Const B() As Long = Array(1, B(0))`). - -What does not crash: the same two constants inside a procedure compile clean; an array whose element is a scalar `Const` compiles; `Array(A(0) + 1)` is an ordinary TB5001, *Type mismatch*. - -Severity: a compiler crash, and the project cannot be compiled until the constant is removed. - -<!-- Reproducer: bugs/array-const-in-array-const-crash/ (mode compile, expects tbbuild exit 4); verified on BETA 997 by `bug_repro.mjs verify`; on 995 the project gives TB5245. The variants above were checked with `tbbuild` on 997 in scratch projects. docs/Reference/Core/Const.md (the "Array constants" section) carries a `> [!WARNING]` for it, naming BETA 997 (a crash, owner's rule), removed once a fixed build is released. Related: array-const-element-overflow-crash. --> diff --git a/bugs/filed/array-const-in-array-const-crash/array-const-in-array-const-crash.twinproj b/bugs/filed/array-const-in-array-const-crash/array-const-in-array-const-crash.twinproj deleted file mode 100644 index a474a5412..000000000 Binary files a/bugs/filed/array-const-in-array-const-crash/array-const-in-array-const-crash.twinproj and /dev/null differ diff --git a/bugs/filed/array-const-in-array-const-crash/repro.json b/bugs/filed/array-const-in-array-const-crash/repro.json deleted file mode 100644 index b9926dae5..000000000 --- a/bugs/filed/array-const-in-array-const-crash/repro.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mode": "compile", - "expect": { - "exit": 4 - }, - "issue": 2474 -} diff --git a/bugs/filed/array-const-in-array-const-crash/src/Sources/Startup.twin b/bugs/filed/array-const-in-array-const-crash/src/Sources/Startup.twin deleted file mode 100644 index b046b14ee..000000000 --- a/bugs/filed/array-const-in-array-const-crash/src/Sources/Startup.twin +++ /dev/null @@ -1,10 +0,0 @@ -' Crashes the compiler: an array Const initialised from an element of another array Const. -Module Startup - - Const A() As Long = Array(1, 2, 3) - Const B() As Long = Array(A(0)) - - Public Sub Main() - End Sub - -End Module diff --git a/bugs/filed/array-const-paramarray-codegen-error/REPORT.md b/bugs/filed/array-const-paramarray-codegen-error/REPORT.md deleted file mode 100644 index 56f4ed276..000000000 --- a/bugs/filed/array-const-paramarray-codegen-error/REPORT.md +++ /dev/null @@ -1,41 +0,0 @@ -Filed as [twinbasic/twinbasic#2470](https://github.com/twinbasic/twinbasic/issues/2470). - -## Passing a module-level array `Const` to a `ParamArray` compiles clean and then fails with a codegen error - -**Describe the bug** -A module-level array `Const` passed as the argument of a `ParamArray` parameter compiles without a diagnostic, and the build then fails: the linker reports *compilation (codegen) error detected* at the call. The same constant passed to a parameter declared `a() As Long` is refused at compile time with TB5001, and to a `Variant` parameter with TB5077, so the `ParamArray` case slips through the check. Observed with `tbbuild --build`. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `array-const-paramarray-codegen-error.twinproj` (attached as `array-const-paramarray-codegen-error.zip`). Its module: - ``` - Module Startup - Const Values() As Long = Array(10, 20, 30) - Public Sub Main() - Count Values - End Sub - Private Sub Count(ParamArray Items() As Variant) - Debug.Print UBound(Items) - End Sub - End Module - ``` -2. Compile it: no errors. Build it (or press F5). -3. The build log reads `[LINKER] compilation (codegen) error detected in 'Startup.Mainrootmain' at line #7` and `[BUILD] failed`. - -**Expected behavior** -A compile error at the call, as for the other whole-array uses of a constant array (TB5001 or TB5077). Documentation says the array "cannot be assigned to an array variable or a Variant, or passed as an array argument". - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 997 - -**Additional context** -BETA 995 has no array constants (TB5245). VB6 has none either, so there is no VB6 project. - -With LLVM the same call gives *a feature used in your code is not yet supported with the LLVM compiler*. - -What does not reproduce it: the constant passed after another argument (`Count 1, Values`) fails the same way, but `Count Values(0)`, an element, builds clean, an ordinary array variable passed to a `ParamArray` builds clean, and a constant array declared inside the procedure builds clean. - -Severity: low. The build fails with a message that gives the line, but not the cause. - -<!-- Reproducer: bugs/array-const-paramarray-codegen-error/ (mode build, expects tbbuild exit 5 and "codegen" in the message); verified on BETA 997 by `bug_repro.mjs verify`; on 995 TB5245. The LLVM message is from the probe c15_paramarray run with `tbrun --llvm` on 997. The quoted documentation sentence is docs/Reference/Core/Const.md, "Array constants". That page carries a `> [!NOTE]` for it, naming BETA 997; removed once a fixed build is released. --> diff --git a/bugs/filed/array-const-paramarray-codegen-error/array-const-paramarray-codegen-error.twinproj b/bugs/filed/array-const-paramarray-codegen-error/array-const-paramarray-codegen-error.twinproj deleted file mode 100644 index a0a6bcd9d..000000000 Binary files a/bugs/filed/array-const-paramarray-codegen-error/array-const-paramarray-codegen-error.twinproj and /dev/null differ diff --git a/bugs/filed/array-const-paramarray-codegen-error/repro.json b/bugs/filed/array-const-paramarray-codegen-error/repro.json deleted file mode 100644 index 4946aa4be..000000000 --- a/bugs/filed/array-const-paramarray-codegen-error/repro.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mode": "build", - "expect": { - "exit": 5, - "message": "codegen" - }, - "issue": 2470 -} diff --git a/bugs/filed/array-const-paramarray-codegen-error/src/Settings b/bugs/filed/array-const-paramarray-codegen-error/src/Settings deleted file mode 100644 index 0c521c124..000000000 --- a/bugs/filed/array-const-paramarray-codegen-error/src/Settings +++ /dev/null @@ -1,61 +0,0 @@ -{ - "configuration.inherits": "Defaults", - "project.appTitle": "ArrayConstParamarrayCodegenError", - "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", - "project.buildType": "Standard EXE", - "project.description": "Reproduces: Passing a module-level array Const to a ParamArray compiles clean and then fails with a codegen error", - "project.exportPathIsV2": true, - "project.id": "{905FF907-B511-47BF-9641-9C0CE6049E83}", - "project.name": "ArrayConstParamarrayCodegenError", - "project.optionExplicit": true, - "project.references": [ - { - "id": "{00020430-0000-0000-C000-000000000046}", - "lcid": 0, - "name": "OLE Automation", - "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", - "path64": "C:\\Windows\\System32\\stdole2.tlb", - "symbolId": "stdole", - "versionMajor": 2, - "versionMinor": 0 - }, - { - "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "VB", - "versionBuild": 0, - "versionMajor": 0, - "versionMinor": 0, - "versionRevision": 31 - }, - { - "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - App global class object", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "AppGlobalClassProject", - "versionBuild": 0, - "versionMajor": 1, - "versionMinor": 0, - "versionRevision": 0 - } - ], - "project.settingsVersion": 1, - "project.startupObject": "Sub Main", - "project.warnings": { - "errors": [], - "hints": [], - "ignored": [], - "info": [], - "warnings": [] - }, - "runtime.useUnicodeStandardLibrary": true -} diff --git a/bugs/filed/array-const-paramarray-codegen-error/src/Sources/Startup.twin b/bugs/filed/array-const-paramarray-codegen-error/src/Sources/Startup.twin deleted file mode 100644 index c491c6764..000000000 --- a/bugs/filed/array-const-paramarray-codegen-error/src/Sources/Startup.twin +++ /dev/null @@ -1,14 +0,0 @@ -' A module-level array Const passed to a ParamArray compiles clean, and the build fails with a codegen error. -Module Startup - - Const Values() As Long = Array(10, 20, 30) - - Public Sub Main() - Count Values - End Sub - - Private Sub Count(ParamArray Items() As Variant) - Debug.Print UBound(Items) - End Sub - -End Module diff --git a/bugs/filed/class-public-array-const-unreachable/REPORT.md b/bugs/filed/class-public-array-const-unreachable/REPORT.md deleted file mode 100644 index 16ef603db..000000000 --- a/bugs/filed/class-public-array-const-unreachable/REPORT.md +++ /dev/null @@ -1,33 +0,0 @@ -Filed as [twinbasic/twinbasic#2471](https://github.com/twinbasic/twinbasic/issues/2471). - -## A `Public` array `Const` in a class is accepted but cannot be reached from outside, where a `Public` scalar `Const` is refused - -**Describe the bug** -A class may not have a `Public` constant: `Public Const Num As Long = 5` in a class is refused with TB5250, *Constants in a class cannot be Public*. For an array constant the declaration `Public Const Arr() As Long = Array(1, 2, 3)` is accepted without a diagnostic, and then `Arr` cannot be reached from outside the class: `obj.Arr(1)` is TB5027, *Unrecognized member*. The declaration is accepted and the constant is private in practice. Observed in the compiler's diagnostics. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `class-public-array-const-unreachable.twinproj` (attached as `class-public-array-const-unreachable.zip`). Class `WithArray` declares `Public Const Arr() As Long = Array(1, 2, 3)` and class `WithScalar` declares `Public Const Num As Long = 5`. `Sub Main` reads `a.Arr(1)` and `s.Num` from instances. -2. Compile it. The diagnostics are: - ``` - Startup.twin [7,21]: TB5027 Unrecognized member 'Arr' on type '_WithArray' [non-extensible object] - Startup.twin [8,21]: TB5027 Unrecognized member 'Num' on type '_WithScalar' [non-extensible object] - WithScalar.twin [3,18]: TB5250 Constants in a class cannot be Public - ``` - The declaration in `WithArray` has no diagnostic of its own. - -**Expected behavior** -The same refusal for both: TB5250 on the declaration of `Arr`, so that the cause is named at the declaration and not as an unrecognized member at each use. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 997 - -**Additional context** -BETA 995 has no array constants (TB5245). VB6 has none either, so there is no VB6 project; a `Public Const` in a VB6 class is refused. - -Inside the class, the array constant reads correctly (`Arr(1)` in `WithArray.Show`). A `Private` array `Const` in a class works, and `Friend`, `Protected` and `Static` are accepted as well. - -Severity: low. The error appears at the use, with a message that does not mention the declaration. - -<!-- Reproducer: bugs/class-public-array-const-unreachable/ (mode compile, expects TB5027 and TB5250); verified on BETA 997 by `bug_repro.mjs verify`; on 995 TB5245. The VB6 remark is from knowledge of VB6, not run. docs/Reference/Core/Const.md (the "Array constants" section) carries a `> [!NOTE]` for it, naming BETA 997; removed once a fixed build is released. --> diff --git a/bugs/filed/class-public-array-const-unreachable/class-public-array-const-unreachable.twinproj b/bugs/filed/class-public-array-const-unreachable/class-public-array-const-unreachable.twinproj deleted file mode 100644 index eb95df1b5..000000000 Binary files a/bugs/filed/class-public-array-const-unreachable/class-public-array-const-unreachable.twinproj and /dev/null differ diff --git a/bugs/filed/class-public-array-const-unreachable/repro.json b/bugs/filed/class-public-array-const-unreachable/repro.json deleted file mode 100644 index e45459bdc..000000000 --- a/bugs/filed/class-public-array-const-unreachable/repro.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "mode": "compile", - "expect": { - "exit": 1, - "diagnostics": [ - "TB5027", - "TB5250" - ] - }, - "issue": 2471 -} diff --git a/bugs/filed/class-public-array-const-unreachable/src/Settings b/bugs/filed/class-public-array-const-unreachable/src/Settings deleted file mode 100644 index 145b727d8..000000000 --- a/bugs/filed/class-public-array-const-unreachable/src/Settings +++ /dev/null @@ -1,61 +0,0 @@ -{ - "configuration.inherits": "Defaults", - "project.appTitle": "ClassPublicArrayConstUnreachable", - "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", - "project.buildType": "Standard EXE", - "project.description": "Reproduces: A Public array Const in a class is accepted but cannot be reached from outside, where a Public scalar Const is refused", - "project.exportPathIsV2": true, - "project.id": "{5E544EA9-CBDF-4369-AE04-F88DCF5047A5}", - "project.name": "ClassPublicArrayConstUnreachable", - "project.optionExplicit": true, - "project.references": [ - { - "id": "{00020430-0000-0000-C000-000000000046}", - "lcid": 0, - "name": "OLE Automation", - "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", - "path64": "C:\\Windows\\System32\\stdole2.tlb", - "symbolId": "stdole", - "versionMajor": 2, - "versionMinor": 0 - }, - { - "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "VB", - "versionBuild": 0, - "versionMajor": 0, - "versionMinor": 0, - "versionRevision": 31 - }, - { - "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - App global class object", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "AppGlobalClassProject", - "versionBuild": 0, - "versionMajor": 1, - "versionMinor": 0, - "versionRevision": 0 - } - ], - "project.settingsVersion": 1, - "project.startupObject": "Sub Main", - "project.warnings": { - "errors": [], - "hints": [], - "ignored": [], - "info": [], - "warnings": [] - }, - "runtime.useUnicodeStandardLibrary": true -} diff --git a/bugs/filed/class-public-array-const-unreachable/src/Sources/Startup.twin b/bugs/filed/class-public-array-const-unreachable/src/Sources/Startup.twin deleted file mode 100644 index 2ec58f224..000000000 --- a/bugs/filed/class-public-array-const-unreachable/src/Sources/Startup.twin +++ /dev/null @@ -1,11 +0,0 @@ -' A Public array Const in a class is accepted but cannot be reached; a Public scalar Const is refused. -Module Startup - - Public Sub Main() - Dim a As New WithArray - Dim s As New WithScalar - Debug.Print a.Arr(1) - Debug.Print s.Num - End Sub - -End Module diff --git a/bugs/filed/class-public-array-const-unreachable/src/Sources/WithArray.twin b/bugs/filed/class-public-array-const-unreachable/src/Sources/WithArray.twin deleted file mode 100644 index 174556bef..000000000 --- a/bugs/filed/class-public-array-const-unreachable/src/Sources/WithArray.twin +++ /dev/null @@ -1,9 +0,0 @@ -Class WithArray - - Public Const Arr() As Long = Array(1, 2, 3) - - Public Sub Show() - Debug.Print Arr(1) - End Sub - -End Class diff --git a/bugs/filed/class-public-array-const-unreachable/src/Sources/WithScalar.twin b/bugs/filed/class-public-array-const-unreachable/src/Sources/WithScalar.twin deleted file mode 100644 index dcf91595c..000000000 --- a/bugs/filed/class-public-array-const-unreachable/src/Sources/WithScalar.twin +++ /dev/null @@ -1,5 +0,0 @@ -Class WithScalar - - Public Const Num As Long = 5 - -End Class diff --git a/bugs/filed/clear-all-breakpoints-restart/REPORT.md b/bugs/filed/clear-all-breakpoints-restart/REPORT.md deleted file mode 100644 index 6731b9440..000000000 --- a/bugs/filed/clear-all-breakpoints-restart/REPORT.md +++ /dev/null @@ -1,39 +0,0 @@ -Filed as [twinbasic/twinbasic#2502](https://github.com/twinbasic/twinbasic/issues/2502). - -## Clear All Breakpoints is undone by restarting the compiler, unless the project was saved in between - -**Describe the bug** -Breakpoints removed with **Debug > Clear All Breakpoints** come back when the compiler restarts. After **Restart the compiler** on the toolbar the margin shows them again, as they were just before they were cleared, and the next run stops at them. A breakpoint removed with F9 stays removed, and saving the project between the two keeps the effect of **Clear All Breakpoints**. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `clear-all-breakpoints-restart.twinproj` (attached as `clear-all-breakpoints-restart.zip`) and open `Startup.twin`. `Main` prints three lines: - ``` - Public Sub Main() - Debug.Print "main start" - Debug.Print "the breakpoint's line" ' BREAK - Debug.Print "main end" - End Sub - ``` -2. Put the cursor on the line marked `BREAK` and press F9. A breakpoint appears in the margin. -3. Choose **Debug > Clear All Breakpoints** (Ctrl+Shift+F9). The breakpoint goes. -4. Click **Restart the compiler** on the toolbar, and wait for the compile to end. The breakpoint is back in the margin. -5. Press F5. The run stops at the breakpoint, having printed only `main start`. - -**Expected behavior** -Breakpoints cleared with **Clear All Breakpoints** stay cleared, as a breakpoint removed with F9 does: after the restart the margin shows none, and F5 prints all three lines. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 995 - -**Additional context** -Also on BETA 987, identically. - -What does not reproduce it: removing the breakpoint with F9 instead, which stays removed across the restart; and saving the project (Ctrl+S) after **Clear All Breakpoints** and before the restart, which keeps them cleared. A breakpoint saved with the project survives a restart, as it should. Not tried: whether the IDE's other ways of starting a new compiler, such as switching between win32 and win64, do the same. - -Where it seems to come from, in BETA 995's `ide/main.js`: F9 records each change in `g_SessionFilesystemTransactions`, which the IDE replays into a new compiler and which a save empties; **Clear All Breakpoints** (`tbDebug_BreakpointsClear`) clears the breakpoints in the compiler and records nothing there, so the replay sets them again. - -Severity: breakpoints a person has removed stop the program again after a restart, with nothing to connect the two. - -<!-- Asserted by `ide-test.bat --only breakpoints` (test/ide/breakpoints.test.mjs: the fault, then F9, saving after Clear All, and a saved breakpoint across a restart); passes on BETA 995 and 987. The reproducer's Startup.twin is test/ide/probes/breakpoints/Sources/Startup.twin with a different header comment. When fixed: update that test and this entry. --> diff --git a/bugs/filed/clear-all-breakpoints-restart/clear-all-breakpoints-restart.twinproj b/bugs/filed/clear-all-breakpoints-restart/clear-all-breakpoints-restart.twinproj deleted file mode 100644 index 48abb9764..000000000 Binary files a/bugs/filed/clear-all-breakpoints-restart/clear-all-breakpoints-restart.twinproj and /dev/null differ diff --git a/bugs/filed/clear-all-breakpoints-restart/repro.json b/bugs/filed/clear-all-breakpoints-restart/repro.json deleted file mode 100644 index 5d47db571..000000000 --- a/bugs/filed/clear-all-breakpoints-restart/repro.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "mode": "lane", - "lane": "ide:breakpoints", - "tests": [ - "Clear All Breakpoints is undone by a restart: the breakpoint is back, and the run stops at it" - ], - "steps": "1. Open clear-all-breakpoints-restart.twinproj in the twinBASIC IDE and open Startup.twin.\n2. Put the cursor on the line marked BREAK and press F9. A breakpoint appears in the margin.\n3. Choose Debug > Clear All Breakpoints (Ctrl+Shift+F9). The breakpoint goes.\n4. Click Restart the compiler on the toolbar, and wait for the compile to end.\n5. Expected: no breakpoint. Actual: the breakpoint is back in the margin.\n6. Press F5. Expected: the DEBUG CONSOLE shows main start, the breakpoint's line and main end. Actual: the run stops at the breakpoint.\nControls: a breakpoint removed with F9 instead stays removed after a restart, and saving the project (Ctrl+S) between Clear All Breakpoints and the restart keeps them cleared. A breakpoint saved with the project survives a restart, as it should.\nAsserted on BETA 995 by `ide-test.bat --only breakpoints` (the project is test/ide/probes/breakpoints).", - "issue": 2502 -} diff --git a/bugs/filed/clear-all-breakpoints-restart/src/Settings b/bugs/filed/clear-all-breakpoints-restart/src/Settings deleted file mode 100644 index a0adc78b8..000000000 --- a/bugs/filed/clear-all-breakpoints-restart/src/Settings +++ /dev/null @@ -1,61 +0,0 @@ -{ - "configuration.inherits": "Defaults", - "project.appTitle": "ClearAllBreakpointsRestart", - "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", - "project.buildType": "Standard EXE", - "project.description": "Reproduces: Clear All Breakpoints is undone by restarting the compiler, unless the project was saved in between", - "project.exportPathIsV2": true, - "project.id": "{8C486AF4-90AD-4407-AB35-C54960FD6D7B}", - "project.name": "ClearAllBreakpointsRestart", - "project.optionExplicit": true, - "project.references": [ - { - "id": "{00020430-0000-0000-C000-000000000046}", - "lcid": 0, - "name": "OLE Automation", - "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", - "path64": "C:\\Windows\\System32\\stdole2.tlb", - "symbolId": "stdole", - "versionMajor": 2, - "versionMinor": 0 - }, - { - "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "VB", - "versionBuild": 0, - "versionMajor": 0, - "versionMinor": 0, - "versionRevision": 31 - }, - { - "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - App global class object", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "AppGlobalClassProject", - "versionBuild": 0, - "versionMajor": 1, - "versionMinor": 0, - "versionRevision": 0 - } - ], - "project.settingsVersion": 1, - "project.startupObject": "Sub Main", - "project.warnings": { - "errors": [], - "hints": [], - "ignored": [], - "info": [], - "warnings": [] - }, - "runtime.useUnicodeStandardLibrary": true -} diff --git a/bugs/filed/clear-all-breakpoints-restart/src/Sources/Startup.twin b/bugs/filed/clear-all-breakpoints-restart/src/Sources/Startup.twin deleted file mode 100644 index 33f3cd97f..000000000 --- a/bugs/filed/clear-all-breakpoints-restart/src/Sources/Startup.twin +++ /dev/null @@ -1,13 +0,0 @@ -' Put a breakpoint on the line marked BREAK (F9), remove it with Debug > Clear All -' Breakpoints, click the toolbar's Restart the compiler, and press F5: the run -' stops at the breakpoint that was cleared. With no breakpoint, it prints all -' three lines. -Module Startup - - Public Sub Main() - Debug.Print "main start" - Debug.Print "the breakpoint's line" ' BREAK - Debug.Print "main end" - End Sub - -End Module diff --git a/bugs/filed/concurrent-builds-shared-temp/REPORT.md b/bugs/filed/concurrent-builds-shared-temp/REPORT.md index 53f61f1a9..de478bbb1 100644 --- a/bugs/filed/concurrent-builds-shared-temp/REPORT.md +++ b/bugs/filed/concurrent-builds-shared-temp/REPORT.md @@ -30,4 +30,4 @@ Each build writes its type library and succeeds, however many IDEs share the `TE **Additional context** Severity: low; the build passes when repeated, and an IDE run by a person rarely builds at the same moment as another. It affects tools that build several projects at once. The compiler imports `GetTempFileNameW`, and its messages show that it writes the type library to a file and reads it back (`[TYPELIB] failed to read in generated type library file` is the message beside this one). A temporary file name that two processes both use would explain the counts; that is an inference, not observed. The temp folder is empty after the builds, so whatever is written there is deleted. -<!-- Reproducer: bugs/concurrent-builds-shared-temp/ (mode probe: scripts/probe_shared_temp.mjs builds it in eight IDEs at once with one TEMP, and with --control a TEMP each; on BETA 997, 3 of 8 failed in the first round shared, 0 of 48 with a TEMP each). The first measurement used the same console template with check_examples' two staging modules, not this reproducer itself: a Sonnet agent's 904 builds through tbbuild-style lanes, data in %TEMP%/claude/typelib-probe/results.jsonl (not kept), 2026-10-02, BETA 995 only; no 983 control. scripts/lib/tb-ide.mjs's launchIde gives every IDE %TEMP%/tbbuild-tmp-<port> since 7a716388, so no harness of this repository reproduces it today: to measure it again, pass TEMP and TMP to launchIde's env. When fixed, the comment in launchIde and WIP.ExamplesBuild.md's per-IDE temp folder note may say so; the folders can stay. --> +<!-- Reproducer: bugs/concurrent-builds-shared-temp/ (mode probe: scripts/probe_shared_temp.mjs builds it in eight IDEs at once with one TEMP, and with --control a TEMP each; on BETA 997, 3 of 8 failed in the first round shared, 0 of 48 with a TEMP each; on BETA 1005, 1 of 32 shared, in round 4, with `[TYPELIB] failed to read in generated type library file` in place of `failed to finalize`. The probe finds the reproducer under bugs/ or bugs/filed/). The first measurement used the same console template with check_examples' two staging modules, not this reproducer itself: a Sonnet agent's 904 builds through tbbuild-style lanes, data in %TEMP%/claude/typelib-probe/results.jsonl (not kept), 2026-10-02, BETA 995 only; no 983 control. scripts/lib/tb-ide.mjs's launchIde gives every IDE %TEMP%/tbbuild-tmp-<port> since 7a716388, so no harness of this repository reproduces it today: to measure it again, pass TEMP and TMP to launchIde's env. When fixed, the comment in launchIde and WIP.ExamplesBuild.md's per-IDE temp folder note may say so; the folders can stay. --> diff --git a/bugs/filed/exe-error-box-title/REPORT.md b/bugs/filed/exe-error-box-title/REPORT.md deleted file mode 100644 index 524951e1b..000000000 --- a/bugs/filed/exe-error-box-title/REPORT.md +++ /dev/null @@ -1,29 +0,0 @@ -Filed as [twinbasic/twinbasic#2501](https://github.com/twinbasic/twinbasic/issues/2501). - -## The box for an unhandled error in a built exe is titled `shutdown`, not with the program's title - -**Describe the bug** -In a built exe, the message box for an unhandled run-time error has the title `shutdown`. The project's title (`App.Title`) is not used, and neither is a title assigned to `App.Title` at run time. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `exe-error-box-title.twinproj` (attached as `exe-error-box-title.zip`). Its title, in the project settings, is `ExeErrorBoxTitle`. - ``` - Public Sub Main() - Err.Raise 5, "MySrc", "my text" - End Sub - ``` -2. Build the project and run the exe. -3. See a box titled `shutdown`, with the text `Run-time error '5'` and `my text`. - -**Expected behavior** -The box is titled with the program's title, `ExeErrorBoxTitle`, as VB6 titles it with `App.Title`. The VB6 project is attached as `exe-error-box-title-vb6.zip`; its exe shows a box titled `ExeErrorBoxTitle` with the text `Run-time error '5':` and `my text`. (Its `Form1` is never shown; it is there because a VB6 project without a form may be built with Unattended Execution, which writes the error to the event log instead of showing a box.) - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 997 - -**Additional context** -Severity: low; the box does not say which program it comes from. The same when `App.Title` is assigned before the error, for an error raised in a form's event procedure, and on BETA 983 and 995. #2017 mentions the same title for an error raised in a COM interface implementation. - -<!-- No docs page states the title; when fixed, nothing to update. Checked with scripts/bug_repro.mjs (run mode with "exe": tbrun exit 6 and the box's title; verify) on 997, and with `run --exe` on BETA 983 and 995 (with a form, see form-event-error-continues); `App.Title` assigned at run time with `run --exe` on 997; the VB6 side with `bug_repro vb6` (exit 8, the box). --> diff --git a/bugs/filed/exe-error-box-title/exe-error-box-title.twinproj b/bugs/filed/exe-error-box-title/exe-error-box-title.twinproj deleted file mode 100644 index f1ad87947..000000000 Binary files a/bugs/filed/exe-error-box-title/exe-error-box-title.twinproj and /dev/null differ diff --git a/bugs/filed/exe-error-box-title/repro.json b/bugs/filed/exe-error-box-title/repro.json deleted file mode 100644 index 10c8e48b7..000000000 --- a/bugs/filed/exe-error-box-title/repro.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "mode": "run", - "exe": true, - "expect": { - "exit": 6, - "output": [ - "^built$", - "^--- exe: exit 0$", - "^--- exe: a box, closed: shutdown: Run-time error '5' my text$" - ] - }, - "issue": 2501 -} diff --git a/bugs/filed/exe-error-box-title/src/Sources/Startup.twin b/bugs/filed/exe-error-box-title/src/Sources/Startup.twin deleted file mode 100644 index 546a2ce9c..000000000 --- a/bugs/filed/exe-error-box-title/src/Sources/Startup.twin +++ /dev/null @@ -1,16 +0,0 @@ -' An unhandled error in a built exe opens a box titled "shutdown", where VB6 titles it with -' App.Title. Build the exe and run it. The project's title is ExeErrorBoxTitle. -Module Startup - - Public Sub Main() - Err.Raise 5, "MySrc", "my text" - End Sub - - ' For scripts/bug_repro.mjs only: tbrun runs the exe after the IDE has run a [RunAfterBuild] Sub. - [RunAfterBuild] - Private Sub Built() - Debug.Cls - Debug.Print "built" - End Sub - -End Module diff --git a/bugs/filed/exe-error-box-title/vb6/Module1.bas b/bugs/filed/exe-error-box-title/vb6/Module1.bas deleted file mode 100644 index 2f2b40781..000000000 --- a/bugs/filed/exe-error-box-title/vb6/Module1.bas +++ /dev/null @@ -1,15 +0,0 @@ -Attribute VB_Name = "Module1" -Option Explicit - -' The VB6 side of a bug reproducer: what VB6 does where the report says twinBASIC differs. -' Build and run it with: node scripts/bug_repro.mjs vb6 exe-error-box-title -' The error is left unhandled on purpose: that is the case the report is about. Form1 is never -' shown; it is there so that bug_repro.mjs builds the exe without Unattended Execution, which -' would write the error to the event log instead of showing the box. - -Sub Main() - Open App.Path & "\out.txt" For Output As #9 - Print #9, "main start" - Close #9 - Err.Raise 5, "MySrc", "my text" -End Sub diff --git a/bugs/filed/first-conversion-spurious-overflow/REPORT.md b/bugs/filed/first-conversion-spurious-overflow/REPORT.md deleted file mode 100644 index fdf7011d7..000000000 --- a/bugs/filed/first-conversion-spurious-overflow/REPORT.md +++ /dev/null @@ -1,44 +0,0 @@ -Filed as [twinbasic/twinbasic#2497](https://github.com/twinbasic/twinbasic/issues/2497). - -## In an IDE run, the first conversion of a Single or Double to an integer raises error 6 when the code also converts a constant out of the Long range - -**Describe the bug** -When the code a run compiles holds a conversion of a constant that does not fit a `Long`, such as `CLng(1E+20)`, the first conversion of a `Single` or `Double` value to an integer that the run makes raises error 6 (*Overflow*), and its target is left at 0. The value converted is in range, and the conversion that overflows need never run: `If False Then t = CLng(1E+20)` is enough. Only the first conversion of the run fails; the same statement run again gives the right value. Observed in IDE runs (F5 and a `[RunAfterBuild]` procedure); the built exe is not affected. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `first-conversion-spurious-overflow.twinproj` (attached as `first-conversion-spurious-overflow.zip`). Its `Sub Main`: - ``` - Dim s As Single = 4.5 - Dim t As Long - On Error Resume Next - t = CLng(s) - Debug.Print "first CLng(s): t=" & t & " Err=" & Err.Number - Err.Clear - t = CLng(s) - Debug.Print "second CLng(s): t=" & t & " Err=" & Err.Number - Err.Clear - If False Then t = CLng(1E+20) ' never runs; without this line both lines print t=4 Err=0 - ``` -2. Run it (F5) and read the DEBUG CONSOLE: - ``` - first CLng(s): t=0 Err=6 - second CLng(s): t=4 Err=0 - ``` -3. Without `On Error Resume Next`, the run stops at the first `t = CLng(s)` with error 6. - -**Expected behavior** -`t=4 Err=0` on both lines, as the built exe of the same project prints, and as VB6 prints for the same statements (attached as `first-conversion-spurious-overflow-vb6.zip`). - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 997 - -**Additional context** -Also on BETA 995. Each of these, anywhere in code the run compiles, causes it: `CLng(1E+20)`, `CInt(1E+10)`, and `t = 1E+20` assigned to a `Long`, in the same procedure or in one it calls (even after the failing conversion, or behind `If False`). These do not: the same conversion in a `Private` procedure that nothing references, `CLng(1E+9)` (which fits), `CByte(300.5)`, and `1E+300 * 1E+300`. Every value that causes it lies outside the 32-bit integer range; `CByte(300.5)` does not. The first conversion fails whether it is `CLng` or the count of `ShiftRotateLeft`, of a `Single` or of a `Double`, and whether the value is 4.5 or 4. If a conversion that overflows at run time (`CLng(d)` with `d` holding 1E+20) runs first, it raises 6 as it should, and the conversions after it are correct. Compiled with LLVM, nothing fails. - -This looks like a floating-point exception flag set while the compiler evaluates the constant conversion, in the IDE's process, which is also the process the run executes in; the run's first checked conversion then reads the flag and reports an overflow. - -Severity: a silent wrong result under `On Error Resume Next` (0 for 4), and otherwise a spurious error 6 that stops the run, in the IDE only, so a program that works as an exe fails while being debugged. - -<!-- Reproducer: bugs/first-conversion-spurious-overflow/ (mode run, expects the two lines above); verified on BETA 997 with bug_repro run and verify. VB6: bug_repro vb6 prints t=4 Err=0 twice. The variants (cause and non-cause lists, the exe column with tbrun --exe, LLVM with tbrun --llvm) were measured on 997 with one tbrun project each; the trees and outputs were in the session scratchpad, not kept. 995: .claude/tooling-review-scratch/beta997-probes/probes/m24_round/run_995.txt (case A1); the first sightings are m17_early and m16_with there. Stated in a `> [!WARNING]` naming BETA 997 on docs/Reference/Default/VBA/Conversion/CLng.md and CInt.md (after the rounding paragraph; owner's choice of pages); remove both once a fixed build is released. --> diff --git a/bugs/filed/first-conversion-spurious-overflow/first-conversion-spurious-overflow.twinproj b/bugs/filed/first-conversion-spurious-overflow/first-conversion-spurious-overflow.twinproj deleted file mode 100644 index 1a5f0b786..000000000 Binary files a/bugs/filed/first-conversion-spurious-overflow/first-conversion-spurious-overflow.twinproj and /dev/null differ diff --git a/bugs/filed/first-conversion-spurious-overflow/repro.json b/bugs/filed/first-conversion-spurious-overflow/repro.json deleted file mode 100644 index d64c4d38c..000000000 --- a/bugs/filed/first-conversion-spurious-overflow/repro.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "mode": "run", - "expect": { - "exit": 0, - "output": [ - "^first CLng\\(s\\): t=0 Err=6$", - "^second CLng\\(s\\): t=4 Err=0$" - ] - }, - "issue": 2497 -} diff --git a/bugs/filed/first-conversion-spurious-overflow/src/Settings b/bugs/filed/first-conversion-spurious-overflow/src/Settings deleted file mode 100644 index 57fa5cbd4..000000000 --- a/bugs/filed/first-conversion-spurious-overflow/src/Settings +++ /dev/null @@ -1,61 +0,0 @@ -{ - "configuration.inherits": "Defaults", - "project.appTitle": "FirstConversionSpuriousOverflow", - "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", - "project.buildType": "Standard EXE", - "project.description": "Reproduces: In an IDE run, the first conversion of a Single or Double to an integer raises error 6 when the code also converts a constant out of the Long range", - "project.exportPathIsV2": true, - "project.id": "{E81F33B6-DE3A-48E7-95CE-A00CAFEF8A46}", - "project.name": "FirstConversionSpuriousOverflow", - "project.optionExplicit": true, - "project.references": [ - { - "id": "{00020430-0000-0000-C000-000000000046}", - "lcid": 0, - "name": "OLE Automation", - "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", - "path64": "C:\\Windows\\System32\\stdole2.tlb", - "symbolId": "stdole", - "versionMajor": 2, - "versionMinor": 0 - }, - { - "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "VB", - "versionBuild": 0, - "versionMajor": 0, - "versionMinor": 0, - "versionRevision": 31 - }, - { - "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - App global class object", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "AppGlobalClassProject", - "versionBuild": 0, - "versionMajor": 1, - "versionMinor": 0, - "versionRevision": 0 - } - ], - "project.settingsVersion": 1, - "project.startupObject": "Sub Main", - "project.warnings": { - "errors": [], - "hints": [], - "ignored": [], - "info": [], - "warnings": [] - }, - "runtime.useUnicodeStandardLibrary": true -} diff --git a/bugs/filed/first-conversion-spurious-overflow/src/Sources/Startup.twin b/bugs/filed/first-conversion-spurious-overflow/src/Sources/Startup.twin deleted file mode 100644 index 33db431d9..000000000 --- a/bugs/filed/first-conversion-spurious-overflow/src/Sources/Startup.twin +++ /dev/null @@ -1,18 +0,0 @@ -' In an IDE run, the first conversion of a Single to a Long raises error 6, because the -' procedure also holds a conversion of a constant out of the Long range, which never runs. -Module Startup - - Public Sub Main() - Dim s As Single = 4.5 - Dim t As Long - On Error Resume Next - t = CLng(s) - Debug.Print "first CLng(s): t=" & t & " Err=" & Err.Number - Err.Clear - t = CLng(s) - Debug.Print "second CLng(s): t=" & t & " Err=" & Err.Number - Err.Clear - If False Then t = CLng(1E+20) ' never runs; without this line both lines print t=4 Err=0 - End Sub - -End Module diff --git a/bugs/filed/first-conversion-spurious-overflow/vb6/Module1.bas b/bugs/filed/first-conversion-spurious-overflow/vb6/Module1.bas deleted file mode 100644 index a36e2fd82..000000000 --- a/bugs/filed/first-conversion-spurious-overflow/vb6/Module1.bas +++ /dev/null @@ -1,22 +0,0 @@ -Attribute VB_Name = "Module1" -Option Explicit - -' The VB6 side of first-conversion-spurious-overflow: the same statements as the -' twinBASIC project's Sub Main, writing to out.txt. -' Build and run it with: node scripts/bug_repro.mjs vb6 first-conversion-spurious-overflow - -Sub Main() - Dim s As Single - Dim t As Long - Open App.Path & "\out.txt" For Output As #9 - s = 4.5 - On Error Resume Next - t = CLng(s) - Print #9, "first CLng(s): t=" & t & " Err=" & Err.Number - Err.Clear - t = CLng(s) - Print #9, "second CLng(s): t=" & t & " Err=" & Err.Number - Err.Clear - If False Then t = CLng(1E+20) - Close #9 -End Sub diff --git a/bugs/filed/forecolor-invalid-value-stored/REPORT.md b/bugs/filed/forecolor-invalid-value-stored/REPORT.md index 8c8787e7a..3e83668c2 100644 --- a/bugs/filed/forecolor-invalid-value-stored/REPORT.md +++ b/bugs/filed/forecolor-invalid-value-stored/REPORT.md @@ -3,38 +3,44 @@ Filed as [twinbasic/twinbasic#2484](https://github.com/twinbasic/twinbasic/issue ## Setting a colour property to a value that is not a colour stores it, and raises error 5 or nothing, where VB6 raises error 380 and keeps the old value **Describe the bug** -Assigning a value that is not a colour to a colour property does not leave the property as it was, on a form, on a PictureBox or UserControl, on the Printer, and on every control tried. `Form.ForeColor = -1` and `Form.ForeColor = &H8000001F` raise error 5 (*Invalid procedure call or argument*) and still store the value, so `ForeColor` reads back `FFFFFFFF` and `8000001F` afterwards. `Form.BackColor = -1` and `Form.FillColor = -1` raise nothing and store the value. `ForeColor` raises error 5 on the surfaces that draw with a device context (Form, PictureBox, UserControl, Printer); every other colour property tried raises nothing, for `-1`, `&H8000001F` and `&HFF0000FF` alike. VB6 raises error 380 (*Invalid property value*) for every one of these assignments and keeps the colour the property had. Observed by assigning with `On Error Resume Next` and printing `Err.Number` and the value read back. +Assigning a value that is not a colour to `ForeColor` or `BackColor` of a Data control, or to `BackColor` of an OLE control or a MultiFrame, raises no error and stores the value, so the property reads back `FFFFFFFF` after `-1`. VB6 raises error 380 (*Invalid property value*) for such an assignment and keeps the colour the property had, and so does twinBASIC for every other colour property tried. Observed by assigning with `On Error Resume Next` and printing `Err.Number` and the value read back. + +On BETA 995 and 997 every colour property tried stored the value, a form's, a PictureBox's, a UserControl's and the Printer's `ForeColor` with error 5 and the rest with no error, which is what this issue first reported. BETA 1005 raises error 380 and keeps the old colour for all of them except the four above. **To Reproduce** Steps to reproduce the behavior: -1. Open `forecolor-invalid-value-stored.twinproj` (attached as `forecolor-invalid-value-stored.zip`). Its `Sub Main` loads `Form1`, which holds a PictureBox, a Label, a TextBox, a CommandButton and a user control, UC1, whose `RunCases` assigns its own colour properties; it then loads `MyMDIForm`, an MDIForm, assigns the same values to its `BackColor`, shows it and assigns them again. For each property tried it sets `vbGreen`, then assigns `-1` (and `&H8000001F` to the form's `ForeColor`) under `On Error Resume Next` and prints the error number and the value read back. The Printer lines need a default printer; nothing is printed. +1. Open `forecolor-invalid-value-stored.twinproj` (attached as `forecolor-invalid-value-stored.zip`). Its `Sub Main` loads `Form1`, which holds a PictureBox, a Label, a TextBox, a CommandButton, a user control, UC1, whose `RunCases` assigns its own colour properties, a Data control, an OLE control and a MultiFrame; it then loads `MyMDIForm`, an MDIForm, assigns the same values to its `BackColor`, shows it and assigns them again. For each property tried it sets `vbGreen`, then assigns `-1` (and `&H8000001F` to the form's `ForeColor`) under `On Error Resume Next` and prints the error number and the value read back. The Printer lines need a default printer; nothing is printed. 2. Run it (F5) and read the DEBUG CONSOLE: ``` - Form.ForeColor = -1: Err 5, reads FFFFFFFF - Form.ForeColor = &H8000001F: Err 5, reads 8000001F - Form.BackColor = -1: Err 0, reads FFFFFFFF - Form.FillColor = -1: Err 0, reads FFFFFFFF - PictureBox.ForeColor = -1: Err 5, reads FFFFFFFF - PictureBox.BackColor = -1: Err 0, reads FFFFFFFF - PictureBox.FillColor = -1: Err 0, reads FFFFFFFF - Label.ForeColor = -1: Err 0, reads FFFFFFFF - Label.BackColor = -1: Err 0, reads FFFFFFFF - TextBox.ForeColor = -1: Err 0, reads FFFFFFFF - TextBox.BackColor = -1: Err 0, reads FFFFFFFF - CommandButton.BackColor = -1: Err 0, reads FFFFFFFF - Printer.ForeColor = -1: Err 5, reads FFFFFFFF - Printer.FillColor = -1: Err 0, reads FFFFFFFF - UserControl.ForeColor = -1: Err 5, reads FFFFFFFF - UserControl.BackColor = -1: Err 0, reads FFFFFFFF - UserControl.FillColor = -1: Err 0, reads FFFFFFFF - MDIForm.BackColor = -1: Err 0, reads FFFFFFFF - MDIForm.BackColor = &H8000001F: Err 0, reads 8000001F - MDIForm (shown).BackColor = -1: Err 5, reads FFFFFFFF - MDIForm (shown).BackColor = &H8000001F: Err 5, reads 8000001F + Form.ForeColor = -1: Err 380, reads FF00 + Form.ForeColor = &H8000001F: Err 380, reads FF00 + Form.BackColor = -1: Err 380, reads FF00 + Form.FillColor = -1: Err 380, reads FF00 + PictureBox.ForeColor = -1: Err 380, reads FF00 + PictureBox.BackColor = -1: Err 380, reads FF00 + PictureBox.FillColor = -1: Err 380, reads FF00 + Label.ForeColor = -1: Err 380, reads FF00 + Label.BackColor = -1: Err 380, reads FF00 + TextBox.ForeColor = -1: Err 380, reads FF00 + TextBox.BackColor = -1: Err 380, reads FF00 + CommandButton.BackColor = -1: Err 380, reads FF00 + Printer.ForeColor = -1: Err 380, reads FF00 + Printer.FillColor = -1: Err 380, reads FF00 + UserControl.ForeColor = -1: Err 380, reads FF00 + UserControl.BackColor = -1: Err 380, reads FF00 + UserControl.FillColor = -1: Err 380, reads FF00 + Data.ForeColor = -1: Err 0, reads FFFFFFFF + Data.BackColor = -1: Err 0, reads FFFFFFFF + OLE.BackColor = -1: Err 0, reads FFFFFFFF + MultiFrame.BackColor = -1: Err 0, reads FFFFFFFF + MDIForm.BackColor = -1: Err 380, reads FF00 + MDIForm.BackColor = &H8000001F: Err 380, reads FF00 + MDIForm (shown).BackColor = -1: Err 380, reads FF00 + MDIForm (shown).BackColor = &H8000001F: Err 380, reads FF00 ``` **Expected behavior** -Error 380 for each assignment, and the old value kept, as VB6 does (the VB6 project, attached as `forecolor-invalid-value-stored-vb6.zip`, prints): +Error 380 for each assignment, and the old value kept, as VB6 does and as twinBASIC now does for the other lines. The VB6 project, attached as `forecolor-invalid-value-stored-vb6.zip`, has no Data control (none could be loaded in VB6 on the machine) and VB6 has no MultiFrame; it prints: ``` Form.ForeColor = -1: Err 380, reads FF00 Form.ForeColor = &H8000001F: Err 380, reads FF00 @@ -53,6 +59,7 @@ Printer.FillColor = -1: Err 380, reads FF00 UserControl.ForeColor = -1: Err 380, reads FF00 UserControl.BackColor = -1: Err 380, reads FF00 UserControl.FillColor = -1: Err 380, reads FF00 +OLE.BackColor = -1: Err 380, reads FF00 MDIForm.BackColor = -1: Err 380, reads FF00 MDIForm.BackColor = &H8000001F: Err 380, reads FF00 MDIForm (shown).BackColor = -1: Err 380, reads FF00 @@ -62,11 +69,13 @@ MDIForm (shown).BackColor = &H8000001F: Err 380, reads FF00 **Desktop:** - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 995 + - twinBASIC compiler version: BETA 1005 **Additional context** -Severity: low. `ForeColor` raises the error on the surfaces and the other properties do not, and the stored value is what a later `Line`, `Circle`, `PSet` or `Print` then uses (or what the control paints with), so a handled error leaves the invalid colour in force. +Severity: low. The stored value is what the control then paints with, and no error tells the program that the colour it was given is not one. + +On BETA 1005 the wider sweep below gives error 380 and the old value for every colour property of every class it covers, with `-1`, `&H8000001F` and `&HFF0000FF`, except `Data.ForeColor`, `Data.BackColor`, `OLE.BackColor` and `MultiFrame.BackColor`, which store all three with no error. -What was tried: the same lines on BETA 983 print the same, so this is not a change of BETA 984. A value that is a valid colour, such as `&H100FF00` or `&H20000FF`, is accepted by `ForeColor` with no error, as in VB6. Besides the attached project, each of `-1`, `&H8000001F` and `&HFF0000FF` was assigned to every colour property of a UserControl (placed on the form), CheckBox, Frame, ListBox, Shape, ComboBox, DirListBox, DriveListBox, FileListBox, OptionButton and OLE in a project of its own, on BETA 995 and 983 and in VB6: the results are the same as above, error 5 for the UserControl's `ForeColor` and none for the rest in twinBASIC, error 380 and the old value kept in VB6. The twinBASIC-only controls behave the same (`QRCode.ForeColor` raises error 5, `CheckMark.BackColor` and `MultiFrame.BackColor` raise nothing), and so do `CommandButton.ForeColor` and the Data control's colours, which VB6 has no counterpart or no loadable project for. The package source explains the pattern: the colour properties are plain stored fields (`Public ForeColor As OLE_COLOR` and so on, in `GraphicsBase` for Form, PictureBox, UserControl, PropertyPage and Report, and declared again in each control), with nothing that validates the value; the error 5 comes from the `ForeColor` change handler of `GraphicsBase`, which runs after the value is stored. +What was tried, up to BETA 997: the same lines on BETA 983 print the same, so this is not a change of BETA 984. A value that is a valid colour, such as `&H100FF00` or `&H20000FF`, is accepted by `ForeColor` with no error, as in VB6. Besides the attached project, each of `-1`, `&H8000001F` and `&HFF0000FF` was assigned to every colour property of a UserControl (placed on the form), CheckBox, Frame, ListBox, Shape, ComboBox, DirListBox, DriveListBox, FileListBox, OptionButton and OLE in a project of its own, on BETA 995 and 983 and in VB6: the results are the same as above, error 5 for the UserControl's `ForeColor` and none for the rest in twinBASIC, error 380 and the old value kept in VB6. The twinBASIC-only controls behave the same (`QRCode.ForeColor` raises error 5, `CheckMark.BackColor` and `MultiFrame.BackColor` raise nothing), and so do `CommandButton.ForeColor` and the Data control's colours, which VB6 has no counterpart or no loadable project for. The package source explains the pattern: the colour properties are plain stored fields (`Public ForeColor As OLE_COLOR` and so on, in `GraphicsBase` for Form, PictureBox, UserControl, PropertyPage and Report, and declared again in each control), with nothing that validates the value; the error 5 comes from the `ForeColor` change handler of `GraphicsBase`, which runs after the value is stored. -<!-- Reproducer: bugs/forecolor-invalid-value-stored/ (mode run, expects the twenty-one lines above in twinBASIC); verified on 995 and 983 for the first seventeen lines, and the four MDIForm lines were run on 997 (MyMDIForm needs a FormDesignerId of its own: given Form1's, its lines test Form1). The VB6 project is in bugs/forecolor-invalid-value-stored/vb6/ (`bug_repro.mjs vb6 forecolor-invalid-value-stored` prints the twenty-one lines above). The wider sweep (every colour property of the classes named under "What was tried", three values each, on 995, 983 and VB6) is in .claude/tooling-review-scratch/beta995-probes/s73/colours/ (c4.out995.txt, c4.out983.txt, c5.outvb6.txt, c6.Data.outvb6.txt, c3.out*.txt for the UserControl), local scratch files that are not in the repository. Stated in a WARNING naming BETA 995 under each of the colour properties on the pages under docs/Reference/Default/VB/ for: Form, PictureBox, UserControl, PropertyPage, Report, Printer, Label, TextBox, CommandButton, CheckBox, OptionButton, Frame, ListBox, ComboBox, DirListBox, DriveListBox, FileListBox, Shape, OLE (the VB6 comparison is in the WARNING), MDIForm (BackColor only; its WARNING names BETA 997) and Data, CheckMark, MultiFrame, QRCode (twinBASIC behaviour only; CommandButton.ForeColor likewise). PropertyPage and Report were not run: they inherit GraphicsBase, as Form does. MDIForm's BackColor was run on 997 with a real MDIForm and stores the value, raising no error while the form is only loaded and error 5 once it is shown; its ForeColor and FillColor are not on its interface (a compile error early-bound; 380 through As Form). When fixed, remove each WARNING. --> +<!-- BETA 1005: the reproducer gained a Data control, an OLE control and a MultiFrame on Form1 (and an OLE control on the VB6 side's Form1), and repro.json expects their four lines, which are what still reproduces; `verify` reproduces, and `vb6` prints 380 for OLE. The sweep trees c3, c4 and c5 below were run again with tbrun on 1005 (outputs in the session scratchpad, not kept). The pages under docs/Reference/Default/VB/ keep a WARNING naming BETA 1005 for those four properties only (Data/index.md under ForeColor and BackColor, OLE/index.md under BackColor, MultiFrame/index.md under BackColor); the other WARNINGs were removed and their properties say that a value that is not a colour raises error 380. Before 1005: Reproducer: bugs/forecolor-invalid-value-stored/ (mode run, expected the twenty-one lines then printed in twinBASIC); verified on 995 and 983 for the first seventeen lines, and the four MDIForm lines were run on 997 (MyMDIForm needs a FormDesignerId of its own: given Form1's, its lines test Form1). The VB6 project is in bugs/forecolor-invalid-value-stored/vb6/ (`bug_repro.mjs vb6 forecolor-invalid-value-stored` prints the twenty-one lines above). The wider sweep (every colour property of the classes named under "What was tried", three values each, on 995, 983 and VB6) is in .claude/tooling-review-scratch/beta995-probes/s73/colours/ (c4.out995.txt, c4.out983.txt, c5.outvb6.txt, c6.Data.outvb6.txt, c3.out*.txt for the UserControl), local scratch files that are not in the repository. Stated in a WARNING naming BETA 995 under each of the colour properties on the pages under docs/Reference/Default/VB/ for: Form, PictureBox, UserControl, PropertyPage, Report, Printer, Label, TextBox, CommandButton, CheckBox, OptionButton, Frame, ListBox, ComboBox, DirListBox, DriveListBox, FileListBox, Shape, OLE (the VB6 comparison is in the WARNING), MDIForm (BackColor only; its WARNING names BETA 997) and Data, CheckMark, MultiFrame, QRCode (twinBASIC behaviour only; CommandButton.ForeColor likewise). PropertyPage and Report were not run: they inherit GraphicsBase, as Form does. MDIForm's BackColor was run on 997 with a real MDIForm and stores the value, raising no error while the form is only loaded and error 5 once it is shown; its ForeColor and FillColor are not on its interface (a compile error early-bound; 380 through As Form). When fixed, remove each WARNING. --> diff --git a/bugs/filed/forecolor-invalid-value-stored/forecolor-invalid-value-stored.twinproj b/bugs/filed/forecolor-invalid-value-stored/forecolor-invalid-value-stored.twinproj index e0fa6e340..ad8963a54 100644 Binary files a/bugs/filed/forecolor-invalid-value-stored/forecolor-invalid-value-stored.twinproj and b/bugs/filed/forecolor-invalid-value-stored/forecolor-invalid-value-stored.twinproj differ diff --git a/bugs/filed/forecolor-invalid-value-stored/repro.json b/bugs/filed/forecolor-invalid-value-stored/repro.json index 5387b988c..d172e69cb 100644 --- a/bugs/filed/forecolor-invalid-value-stored/repro.json +++ b/bugs/filed/forecolor-invalid-value-stored/repro.json @@ -3,27 +3,10 @@ "expect": { "exit": 0, "output": [ - "^Form\\.ForeColor = -1: Err 5, reads FFFFFFFF$", - "^Form\\.ForeColor = &H8000001F: Err 5, reads 8000001F$", - "^Form\\.BackColor = -1: Err 0, reads FFFFFFFF$", - "^Form\\.FillColor = -1: Err 0, reads FFFFFFFF$", - "^PictureBox\\.ForeColor = -1: Err 5, reads FFFFFFFF$", - "^PictureBox\\.BackColor = -1: Err 0, reads FFFFFFFF$", - "^PictureBox\\.FillColor = -1: Err 0, reads FFFFFFFF$", - "^Label\\.ForeColor = -1: Err 0, reads FFFFFFFF$", - "^Label\\.BackColor = -1: Err 0, reads FFFFFFFF$", - "^TextBox\\.ForeColor = -1: Err 0, reads FFFFFFFF$", - "^TextBox\\.BackColor = -1: Err 0, reads FFFFFFFF$", - "^CommandButton\\.BackColor = -1: Err 0, reads FFFFFFFF$", - "^Printer\\.ForeColor = -1: Err 5, reads FFFFFFFF$", - "^Printer\\.FillColor = -1: Err 0, reads FFFFFFFF$", - "^UserControl\\.ForeColor = -1: Err 5, reads FFFFFFFF$", - "^UserControl\\.BackColor = -1: Err 0, reads FFFFFFFF$", - "^UserControl\\.FillColor = -1: Err 0, reads FFFFFFFF$", - "^MDIForm\\.BackColor = -1: Err 0, reads FFFFFFFF$", - "^MDIForm\\.BackColor = &H8000001F: Err 0, reads 8000001F$", - "^MDIForm \\(shown\\)\\.BackColor = -1: Err 5, reads FFFFFFFF$", - "^MDIForm \\(shown\\)\\.BackColor = &H8000001F: Err 5, reads 8000001F$" + "^Data\\.ForeColor = -1: Err 0, reads FFFFFFFF$", + "^Data\\.BackColor = -1: Err 0, reads FFFFFFFF$", + "^OLE\\.BackColor = -1: Err 0, reads FFFFFFFF$", + "^MultiFrame\\.BackColor = -1: Err 0, reads FFFFFFFF$" ] }, "issue": 2484 diff --git a/bugs/filed/forecolor-invalid-value-stored/src/Sources/Form1.tbform b/bugs/filed/forecolor-invalid-value-stored/src/Sources/Form1.tbform index e5d0e4107..6152d3179 100644 --- a/bugs/filed/forecolor-invalid-value-stored/src/Sources/Form1.tbform +++ b/bugs/filed/forecolor-invalid-value-stored/src/Sources/Form1.tbform @@ -129,6 +129,36 @@ "Height": 100, "_className": "UC1", "_clsid": "{EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA10}" + }, + { + "Index": -1, + "Name": "Data1", + "Left": 260, + "Top": 20, + "Width": 100, + "Height": 30, + "_className": "Data", + "_clsid": "{33AD4FF8-6699-11CF-B70C-00AA0060D393}" + }, + { + "Index": -1, + "Name": "OLE1", + "Left": 260, + "Top": 60, + "Width": 100, + "Height": 30, + "_className": "OLE", + "_clsid": "{33AD5000-6699-11CF-B70C-00AA0060D393}" + }, + { + "Index": -1, + "Name": "MultiFrame1", + "Left": 260, + "Top": 100, + "Width": 100, + "Height": 30, + "_className": "MultiFrame", + "_clsid": "{5724D906-E1EE-455E-9FE3-BB322399766F}" } ] } diff --git a/bugs/filed/forecolor-invalid-value-stored/src/Sources/Startup.twin b/bugs/filed/forecolor-invalid-value-stored/src/Sources/Startup.twin index 4ae73f0f1..7b267c57f 100644 --- a/bugs/filed/forecolor-invalid-value-stored/src/Sources/Startup.twin +++ b/bugs/filed/forecolor-invalid-value-stored/src/Sources/Startup.twin @@ -46,6 +46,18 @@ Module Startup Printer.FillColor = -1 Debug.Print "Printer.FillColor = -1: Err " & Err.Number & ", reads " & Hex$(Printer.FillColor): Err.Clear Form1.UC1.RunCases + Form1.Data1.ForeColor = vbGreen + Form1.Data1.ForeColor = -1 + Debug.Print "Data.ForeColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.Data1.ForeColor): Err.Clear + Form1.Data1.BackColor = vbGreen + Form1.Data1.BackColor = -1 + Debug.Print "Data.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.Data1.BackColor): Err.Clear + Form1.OLE1.BackColor = vbGreen + Form1.OLE1.BackColor = -1 + Debug.Print "OLE.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.OLE1.BackColor): Err.Clear + Form1.MultiFrame1.BackColor = vbGreen + Form1.MultiFrame1.BackColor = -1 + Debug.Print "MultiFrame.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.MultiFrame1.BackColor): Err.Clear Unload Form1 Load MyMDIForm MyMDIForm.BackColor = vbGreen diff --git a/bugs/filed/forecolor-invalid-value-stored/vb6/Form1.frm b/bugs/filed/forecolor-invalid-value-stored/vb6/Form1.frm index 373c3c018..5db10f220 100644 --- a/bugs/filed/forecolor-invalid-value-stored/vb6/Form1.frm +++ b/bugs/filed/forecolor-invalid-value-stored/vb6/Form1.frm @@ -10,6 +10,13 @@ Begin VB.Form Form1 ScaleWidth = 4000 StartUpPosition = 3 'Windows Default Visible = 0 'False + Begin VB.OLE OLE1 + Height = 315 + Left = 1200 + TabIndex = 8 + Top = 2280 + Width = 855 + End Begin Probe.UC1 UC1 Height = 1215 Left = 3360 diff --git a/bugs/filed/forecolor-invalid-value-stored/vb6/Module1.bas b/bugs/filed/forecolor-invalid-value-stored/vb6/Module1.bas index 8f1e9a3f9..0e325253e 100644 --- a/bugs/filed/forecolor-invalid-value-stored/vb6/Module1.bas +++ b/bugs/filed/forecolor-invalid-value-stored/vb6/Module1.bas @@ -49,6 +49,9 @@ Private Sub Cases() Printer.FillColor = -1 Print #9, "Printer.FillColor = -1: Err " & Err.Number & ", reads " & Hex$(Printer.FillColor): Err.Clear Form1.UC1.RunCases + Form1.OLE1.BackColor = vbGreen + Form1.OLE1.BackColor = -1 + Print #9, "OLE.BackColor = -1: Err " & Err.Number & ", reads " & Hex$(Form1.OLE1.BackColor): Err.Clear Unload Form1 Load MDIForm1 MDIForm1.BackColor = vbGreen diff --git a/bugs/filed/form-event-error-continues/REPORT.md b/bugs/filed/form-event-error-continues/REPORT.md index 2ea9795f1..787d1fdb2 100644 --- a/bugs/filed/form-event-error-continues/REPORT.md +++ b/bugs/filed/form-event-error-continues/REPORT.md @@ -32,4 +32,4 @@ The program ends when the error's box is closed, and `Form_Activate` never runs. **Additional context** Severity: medium; code goes on running after an error that the program did not handle, with whatever state the error left. The same for an error in `Form_Activate` (the form stays open), and on BETA 983 and 995. An unhandled error in `Sub Main` does end the exe, with exit code 0. -<!-- docs/Reference/Core/On-Error.md carries a NOTE under "Without an On Error statement ... execution stops" (owner, 2026-10-06); when fixed, remove the NOTE. Checked with scripts/bug_repro.mjs (run mode with "exe": tbrun exit 6 and both boxes; verify) on 997, and the Form_Activate case with `run --exe` on BETA 983, 995 and 997; the VB6 sides of Form_Load and Form_Activate with `bug_repro vb6` (exit 8, the box, and the exe ended). --> +<!-- docs/Reference/Core/On-Error.md carries a NOTE under "Without an On Error statement ... execution stops" (owner, 2026-10-06); when fixed, remove the NOTE. Checked with scripts/bug_repro.mjs (run mode with "exe": tbrun exit 6 and both boxes; verify) on 997 and 1005 (on 1005 the error's box is titled with the project's title, FormEventErrorContinues, rather than `shutdown`, #2501, so repro.json matches any title), and the Form_Activate case with `run --exe` on BETA 983, 995 and 997; the VB6 sides of Form_Load and Form_Activate with `bug_repro vb6` (exit 8, the box, and the exe ended). --> diff --git a/bugs/filed/form-event-error-continues/repro.json b/bugs/filed/form-event-error-continues/repro.json index 362b5970e..9e8837d3a 100644 --- a/bugs/filed/form-event-error-continues/repro.json +++ b/bugs/filed/form-event-error-continues/repro.json @@ -5,7 +5,7 @@ "exit": 6, "output": [ "^built$", - "^--- exe: a box, closed: shutdown: Run-time error '5' my text in Form_Load$", + "^--- exe: a box, closed: [^:]+: Run-time error '5' my text in Form_Load$", "^--- exe: a box, closed: FormEventErrorContinues: Form_Activate ran after the error$" ] }, diff --git a/bugs/filed/gettypeinfo-badindex/REPORT.md b/bugs/filed/gettypeinfo-badindex/REPORT.md deleted file mode 100644 index 8732f3689..000000000 --- a/bugs/filed/gettypeinfo-badindex/REPORT.md +++ /dev/null @@ -1,40 +0,0 @@ -Filed as [twinbasic/twinbasic#2489](https://github.com/twinbasic/twinbasic/issues/2489). - -## A twinBASIC class's `GetTypeInfo` with an `iTInfo` of 1 returns E_UNEXPECTED, not DISP_E_BADINDEX - -**Describe the bug** -`IDispatch::GetTypeInfo` on an object of a twinBASIC class returns `E_UNEXPECTED` (`&H8000FFFF`) for any `iTInfo` other than 0. The object's `GetTypeInfoCount` returns 1, so 0 is the only valid index, and the contract gives `DISP_E_BADINDEX` for a bad one. Observed in a run of the reproducer project, calling the method through a project's own declaration of the interface. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `gettypeinfo-badindex.twinproj` (attached as `gettypeinfo-badindex.zip`). Its one source file, `Startup.twin`, declares a copy of `IDispatch` with the same `[InterfaceId]` (the `stdole` declaration cannot be called), a class `Widget` with one `Sub`, and a `Sub Main` that calls `GetTypeInfoCount` and `GetTypeInfo` on a `Widget` through the copy. -2. Run it and read the DEBUG CONSOLE: - ``` - GetTypeInfoCount = 1 - GetTypeInfo(0): HRESULT 0, pointer returned True - GetTypeInfo(1): HRESULT 8000FFFF, error -2147418113, pointer returned False - GetTypeInfo(2): HRESULT 8000FFFF, error -2147418113, pointer returned False - GetTypeInfo(-1): HRESULT 8000FFFF, error -2147418113 - ``` - -**Expected behavior** -`DISP_E_BADINDEX` (`&H8002000B`), which the `IDispatch::GetTypeInfo` documentation gives for an index that is not valid, for an index the object does not have. `E_UNEXPECTED` (*Catastrophic failure*) is the code for a call made at a time when the object cannot take it, and it reads as a fault in the object. - -VB6 does not return `E_UNEXPECTED`. The same calls on a VB6 class, made through `DispCallFunc` (the VB6 project is attached as `gettypeinfo-badindex-vb6.zip`), return `TYPE_E_ELEMENTNOTFOUND` (`&H8002802B`) for every index but 0: -``` -GetTypeInfoCount = 1 (HRESULT 0) -GetTypeInfo(0): HRESULT 0, pointer returned True -GetTypeInfo(1): HRESULT 8002802B, pointer returned False -GetTypeInfo(2): HRESULT 8002802B, pointer returned False -GetTypeInfo(-1): HRESULT 8002802B, pointer returned False -``` -Either `DISP_E_BADINDEX` or VB6's `TYPE_E_ELEMENTNOTFOUND` says that the index names nothing; `E_UNEXPECTED` does not. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 995 - -**Additional context** -Severity: minor. A caller that checks for `DISP_E_BADINDEX` to learn that an object has no more type information gets a different code; `GetTypeInfo(0)` and `GetTypeInfoCount` are right. - -<!-- Reproducer: bugs/gettypeinfo-badindex/ (mode run, expects the GetTypeInfo(0) and GetTypeInfo(1) lines above); verified on 995. VB6 output from bugs/gettypeinfo-badindex/vb6/ (VB6 6.0, Probe.exe). Stated in docs/Reference/COM-Interfaces/IDispatch.md, the GetTypeInfo section ("For 1 it fails with E_UNEXPECTED ..., not with DISP_E_BADINDEX"); when fixed, say that it fails with DISP_E_BADINDEX. --> diff --git a/bugs/filed/gettypeinfo-badindex/gettypeinfo-badindex.twinproj b/bugs/filed/gettypeinfo-badindex/gettypeinfo-badindex.twinproj deleted file mode 100644 index ec3f6f6fd..000000000 Binary files a/bugs/filed/gettypeinfo-badindex/gettypeinfo-badindex.twinproj and /dev/null differ diff --git a/bugs/filed/gettypeinfo-badindex/repro.json b/bugs/filed/gettypeinfo-badindex/repro.json deleted file mode 100644 index 5162a076e..000000000 --- a/bugs/filed/gettypeinfo-badindex/repro.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "mode": "run", - "expect": { - "exit": 0, - "output": [ - "^GetTypeInfo\\(0\\): HRESULT 0, pointer returned True$", - "^GetTypeInfo\\(1\\): HRESULT 8000FFFF," - ] - }, - "issue": 2489 -} diff --git a/bugs/filed/gettypeinfo-badindex/src/Sources/Startup.twin b/bugs/filed/gettypeinfo-badindex/src/Sources/Startup.twin deleted file mode 100644 index 2695bf4ce..000000000 --- a/bugs/filed/gettypeinfo-badindex/src/Sources/Startup.twin +++ /dev/null @@ -1,38 +0,0 @@ -[InterfaceId("00020400-0000-0000-C000-000000000046")] -Private Interface IDispatchCopy Extends stdole.IUnknown - Sub GetTypeInfoCount(ByRef pctinfo As Long) - Sub GetTypeInfo(ByVal iTInfo As Long, ByVal lcid As Long, ByRef ppTInfo As LongPtr) - Sub GetIDsOfNames(ByVal riid As LongPtr, ByVal rgszNames As LongPtr, ByVal cNames As Long, ByVal lcid As Long, ByVal rgDispId As LongPtr) - Sub Invoke(ByVal dispIdMember As Long, ByVal riid As LongPtr, ByVal lcid As Long, ByVal wFlags As Integer, ByVal pDispParams As LongPtr, ByVal pVarResult As LongPtr, ByVal pExcepInfo As LongPtr, ByVal puArgErr As LongPtr) -End Interface - -Class Widget - Public Sub Hello() - End Sub -End Class - -Module Startup - - Public Sub Main() - Dim d As IDispatchCopy = New Widget - Dim count As Long - d.GetTypeInfoCount count - Debug.Print "GetTypeInfoCount = " & count - Dim ti As LongPtr - On Error Resume Next - d.GetTypeInfo 0, 0, ti - Debug.Print "GetTypeInfo(0): HRESULT " & Hex(Err.LastHresult) & ", pointer returned " & (ti <> 0) - Err.Clear - ti = 0 - d.GetTypeInfo 1, 0, ti - Debug.Print "GetTypeInfo(1): HRESULT " & Hex(Err.LastHresult) & ", error " & Err.Number & ", pointer returned " & (ti <> 0) - Err.Clear - ti = 0 - d.GetTypeInfo 2, 0, ti - Debug.Print "GetTypeInfo(2): HRESULT " & Hex(Err.LastHresult) & ", error " & Err.Number & ", pointer returned " & (ti <> 0) - Err.Clear - d.GetTypeInfo -1, 0, ti - Debug.Print "GetTypeInfo(-1): HRESULT " & Hex(Err.LastHresult) & ", error " & Err.Number - End Sub - -End Module diff --git a/bugs/filed/gettypeinfo-badindex/vb6/Module1.bas b/bugs/filed/gettypeinfo-badindex/vb6/Module1.bas deleted file mode 100644 index a9c779b29..000000000 --- a/bugs/filed/gettypeinfo-badindex/vb6/Module1.bas +++ /dev/null @@ -1,73 +0,0 @@ -Attribute VB_Name = "Module1" -Option Explicit - -' IDispatch::GetTypeInfoCount and GetTypeInfo called by vtable slot (3 and 4) on a -' VB6 class, through DispCallFunc. The HRESULT is the call's own result. - -Private Declare Function DispCallFunc Lib "oleaut32.dll" (ByVal pvInstance As Long, ByVal oVft As Long, ByVal cc As Long, ByVal vtReturn As Integer, ByVal cActuals As Long, ByRef prgvt As Integer, ByRef prgpvarg As Long, ByRef pvargResult As Variant) As Long - -Private Const CC_STDCALL As Long = 4 -Private Const VT_I4 As Integer = 3 - -Sub Main() - On Error GoTo Fail - Open App.Path & "\out.txt" For Output As #9 - Cases - Close #9 - Exit Sub -Fail: - On Error Resume Next - Print #9, "unhandled error " & Err.Number & ": " & Err.Description - Close #9 -End Sub - -' Calls slot SlotNo of the interface at Ptr with Count Long arguments (at most 3); -' returns the HRESULT the method returned, or DispCallFunc's own failure. -Private Function CallSlot(ByVal Ptr As Long, ByVal SlotNo As Long, ByVal Count As Long, ByVal Arg0 As Long, ByVal Arg1 As Long, ByVal Arg2 As Long) As Long - Dim types(0 To 2) As Integer - Dim vars(0 To 2) As Variant - Dim ptrs(0 To 2) As Long - Dim result As Variant - Dim hr As Long - Dim i As Long - vars(0) = Arg0 - vars(1) = Arg1 - vars(2) = Arg2 - For i = 0 To 2 - types(i) = VT_I4 - ptrs(i) = VarPtr(vars(i)) - Next - result = CLng(0) - hr = DispCallFunc(Ptr, SlotNo * 4, CC_STDCALL, VT_I4, Count, types(0), ptrs(0), result) - If hr <> 0 Then - CallSlot = hr - Else - CallSlot = CLng(result) - End If -End Function - -Private Sub TryIndex(ByVal Ptr As Long, ByVal Index As Long) - Dim ti As Long - Dim hr As Long - ti = 0 - hr = CallSlot(Ptr, 4, 3, Index, 0, VarPtr(ti)) - Print #9, "GetTypeInfo(" & Index & "): HRESULT " & Hex(hr) & ", pointer returned " & (ti <> 0) - If ti <> 0 Then hr = CallSlot(ti, 2, 0, 0, 0, 0) -End Sub - -Private Sub Cases() - Dim w As Widget - Dim o As Object - Dim p As Long - Dim count As Long - Dim hr As Long - Set w = New Widget - Set o = w - p = ObjPtr(o) - hr = CallSlot(p, 3, 1, VarPtr(count), 0, 0) - Print #9, "GetTypeInfoCount = " & count & " (HRESULT " & Hex(hr) & ")" - TryIndex p, 0 - TryIndex p, 1 - TryIndex p, 2 - TryIndex p, -1 -End Sub diff --git a/bugs/filed/hover-hidden-over-dock-resizer/REPORT.md b/bugs/filed/hover-hidden-over-dock-resizer/REPORT.md deleted file mode 100644 index 767cb2c52..000000000 --- a/bugs/filed/hover-hidden-over-dock-resizer/REPORT.md +++ /dev/null @@ -1,29 +0,0 @@ -Filed as [twinbasic/twinbasic#2506](https://github.com/twinbasic/twinbasic/issues/2506). - -## A tall hover in the code editor disappears when the mouse slides down it onto the dock's resizer - -**Describe the bug** -A hover in the code editor that reaches past the editor's bottom edge disappears when the mouse, sliding down inside it, crosses that edge, although the mouse is still over the hover. Monaco draws the hover outside the editor (the IDE creates the editor with `fixedOverflowWidgets: true`), and a long hover, such as the one over `Collection`, which lists its members, reaches over the IDE's dock. The dock has two transparent elements there: the dock resizer along the editor's bottom edge (`DIV.dockElementResizer`, `z-index: 1000`, 6 px tall) and, beneath it, the dock's drop target (`DIV.dockGroupVerticalDockPointBottom`). They are drawn under the hover, so the hover is visible, but they take the mouse: `document.elementFromPoint` returns the resizer where the hover is drawn. When the mouse reaches them, the page gets `mouseout` from the hover to `.dockElementResizer`, and Monaco 0.35.1's `ModesHoverController._onEditorMouseLeave` sees a `relatedTarget` that is not inside the hover and calls `_hideWidgets()`. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `hover-hidden-over-dock-resizer.twinproj` (attached as `hover-hidden-over-dock-resizer.zip`), a console project whose `Startup.twin` has `Dim c As Collection` on line 3. -2. Rest the mouse on `Collection` in line 3 and wait a second. A tall hover opens below the line, listing the class's members, and reaches past the code editor's bottom edge. -3. Slide the mouse slowly down inside the hover, past the code editor's bottom edge. -4. The hover disappears as soon as the pointer crosses the edge, with the mouse still over the part of the hover that is drawn below it. - -**Expected behavior** -The hover stays while the mouse is over it, as Monaco does by itself: the hover's DOM stays inside the editor's, and `hover.sticky` is true by default (the IDE passes only `hover: { delay: 1e3 }`). Nothing in the dock should take the mouse from a hover drawn over it. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 997 - -**Additional context** -A person sees it by sliding the mouse down the hover at an ordinary speed. A script has to move the mouse 1 px at a time: a larger step jumps over the 6 px resizer and the hover stays. In the scripted run, in a window of 1093 by 784 px, the editor's bottom edge was at y 580, the hover reached y 673 and went at y 575, where the page has `DIV.dockElementResizer`. - -What does not reproduce it: the mouse sliding down the same hover above the editor's bottom edge, where nothing of the dock lies under it. With a style `.monaco-editor .monaco-hover { z-index: 100000 !important; }` added to the page, the hover stays at every pixel across the edge, so the hover's position in the page is the cause. Making only the resizer `pointer-events: none` is not enough: the drop target under it takes the mouse instead. - -Severity: low. Any hover that reaches the dock goes at the edge, which is the usual case for a hover over a class with many members, and the part of it that shows below the edge cannot be reached to read or scroll. - -<!-- Asserted by `ide-test.bat --only hover-past-editor` (test/ide/hover-past-editor.test.mjs: the hover over Collection on line 3 reaches 12 px past the editor's bottom edge; sliding down it in steps of 1 px from 12 px above that edge to 12 px below, the hover goes where the page has the dock resizer, with the mouse above the hover's bottom edge; the controls are the same slide above the dock, and across the edge with the z-index style added); passes on BETA 997. Reproducer: bugs/hover-hidden-over-dock-resizer/; its Startup.twin is test/ide/probes/hover-past-editor/Sources/Startup.twin with a different trailing comment. No documentation page carries a callout for this defect. The help add-in works around it in add-in/Resources/SCRIPTS/hoverhelp.js (commit 246b95e9), by adding the style above to the page while hover help is on, so remove that workaround once a fixed build is released, and then this entry, its reproducer and the lane. --> diff --git a/bugs/filed/hover-hidden-over-dock-resizer/hover-hidden-over-dock-resizer.twinproj b/bugs/filed/hover-hidden-over-dock-resizer/hover-hidden-over-dock-resizer.twinproj deleted file mode 100644 index fde8f4292..000000000 Binary files a/bugs/filed/hover-hidden-over-dock-resizer/hover-hidden-over-dock-resizer.twinproj and /dev/null differ diff --git a/bugs/filed/hover-hidden-over-dock-resizer/repro.json b/bugs/filed/hover-hidden-over-dock-resizer/repro.json deleted file mode 100644 index a888eccc8..000000000 --- a/bugs/filed/hover-hidden-over-dock-resizer/repro.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "mode": "lane", - "lane": "ide:hover-past-editor", - "tests": [ - "the hover over Collection reaches past the code editor's bottom edge", - "the hover disappears when the mouse slides down it onto the dock's resizer, though the mouse is still over the hover" - ], - "steps": "1. Open hover-hidden-over-dock-resizer.twinproj in the twinBASIC IDE, with a window large enough to show Startup.twin and the panels beneath the code editor. Line 3 of Startup.twin reads Dim c As Collection.\n2. Rest the mouse on Collection in line 3 and wait a second. A tall hover opens below the line, listing the class's members, and reaches past the code editor's bottom edge.\n3. Slide the mouse slowly down inside the hover, past the code editor's bottom edge.\n4. Expected: the hover stays while the mouse is over it, and scrolls or shows its end. Actual: it disappears as soon as the pointer crosses the editor's bottom edge, with the mouse still over the part of the hover that is drawn there.\nA step of more than a few pixels skips the 6 px strip where it happens, so a script moves the mouse 1 px at a time. With the mouse over that strip, document.elementFromPoint returns DIV.dockElementResizer, the IDE's dock resizer, which is transparent and takes the mouse; beneath it lies the dock's drop target, DIV.dockGroupVerticalDockPointBottom.\nControl: the mouse sliding down the same hover above the editor's bottom edge keeps it; and with a style .monaco-editor .monaco-hover { z-index: 100000 !important; } added to the page, the hover stays at every pixel across the edge.\nAsserted on BETA 997 by `ide-test.bat --only hover-past-editor` (the project is test/ide/probes/hover-past-editor).", - "issue": 2506 -} diff --git a/bugs/filed/hover-hidden-over-dock-resizer/src/Settings b/bugs/filed/hover-hidden-over-dock-resizer/src/Settings deleted file mode 100644 index 9b087bce3..000000000 --- a/bugs/filed/hover-hidden-over-dock-resizer/src/Settings +++ /dev/null @@ -1,61 +0,0 @@ -{ - "configuration.inherits": "Defaults", - "project.appTitle": "HoverHiddenOverDockResizer", - "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", - "project.buildType": "Standard EXE", - "project.description": "Reproduces: A tall hover in the code editor disappears when the mouse slides down it onto the dock's resizer", - "project.exportPathIsV2": true, - "project.id": "{9883BEA8-F8E7-403D-AECF-2C91D69A5FE7}", - "project.name": "HoverHiddenOverDockResizer", - "project.optionExplicit": true, - "project.references": [ - { - "id": "{00020430-0000-0000-C000-000000000046}", - "lcid": 0, - "name": "OLE Automation", - "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", - "path64": "C:\\Windows\\System32\\stdole2.tlb", - "symbolId": "stdole", - "versionMajor": 2, - "versionMinor": 0 - }, - { - "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "VB", - "versionBuild": 0, - "versionMajor": 0, - "versionMinor": 0, - "versionRevision": 31 - }, - { - "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - App global class object", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "AppGlobalClassProject", - "versionBuild": 0, - "versionMajor": 1, - "versionMinor": 0, - "versionRevision": 0 - } - ], - "project.settingsVersion": 1, - "project.startupObject": "Sub Main", - "project.warnings": { - "errors": [], - "hints": [], - "ignored": [], - "info": [], - "warnings": [] - }, - "runtime.useUnicodeStandardLibrary": true -} diff --git a/bugs/filed/hover-hidden-over-dock-resizer/src/Sources/Startup.twin b/bugs/filed/hover-hidden-over-dock-resizer/src/Sources/Startup.twin deleted file mode 100644 index 4cb5aa2bb..000000000 --- a/bugs/filed/hover-hidden-over-dock-resizer/src/Sources/Startup.twin +++ /dev/null @@ -1,10 +0,0 @@ -Module Startup - Public Sub Main() - Dim c As Collection ' HOVER: the hover lists the members and reaches past the editor - Set c = New Collection - c.Add 1 - End Sub -End Module -' Hover over Collection on line 3, wait a second for the hover, and slide the mouse -' down inside it. The hover disappears when the pointer crosses the code editor's -' bottom edge, though the mouse is still over the hover. diff --git a/bugs/filed/import-no-settings-silent/REPORT.md b/bugs/filed/import-no-settings-silent/REPORT.md deleted file mode 100644 index 0dc8d044f..000000000 --- a/bugs/filed/import-no-settings-silent/REPORT.md +++ /dev/null @@ -1,32 +0,0 @@ -Filed as [twinbasic/twinbasic#2493](https://github.com/twinbasic/twinbasic/issues/2493). - -## `import` of a folder with no `Settings` file fails without saying why - -**Describe the bug** -Given a folder with no `Settings` file at its top, `import` lists the files it read, ends `... FAILED` with no `ERROR:` line, and writes nothing. Every other failure measured names its cause. The refusal is right, and the silence is not. - -**To Reproduce** -Steps to reproduce the behavior: -1. Unzip `import-no-settings-silent.zip` (it holds `import-no-settings-silent.twinproj`, an ordinary project) and export it: `twinBASIC_win32.exe export C:\p\import-no-settings-silent.twinproj C:\p\src\`. -2. Import only the folder `Sources`, which has no `Settings` file at its top: `twinBASIC_win32.exe import C:\p\x.twinproj C:\p\src\Sources\`. -3. See: - ``` - importing into "C:\p\x.twinproj" from "C:\p\src\Sources\"... - IMPORTED: C:\p\src\Sources\\Startup.twin - ... FAILED - ``` -4. See that there is no `ERROR:` line, the exit code is 0, and `C:\p\x.twinproj` was not written. - -**Expected behavior** -An `ERROR:` line that names the cause, such as `ERROR: no Settings file in the input folder`, before `... FAILED`, as the other failures print. A failure should also not exit with 0. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 995 - -**Additional context** -Severity: minor, because the refusal is right. - -Also on BETA 983, where a copy of the HelloWorld sample's tree with `Settings` deleted behaves the same: it lists `Resources\ICON\twinBASIC.ico` and `Sources\HelloWorld.twin`, ends `... FAILED` and writes no project. - -<!-- Automated: bugs/import-no-settings-silent/repro.json is a cli reproducer, import of {src}\Sources\, expecting the IMPORTED line for Startup.twin, `... FAILED` and exit 0. A repro.json cannot assert that a line is absent, so it checks the IMPORTED line, `... FAILED` and exit 0 only: after a fix that adds an ERROR line, read the output by hand. Nothing in scripts/ or WIP.md states the compiler's behaviour; scripts/impexp.mjs import refuses a folder with no Settings file by name ("has no Settings file", with a self-test). Measured on BETA 983 and 995 (cli995/log1-*.txt, ENTRY 9) and by bug_repro verify on 995. --> diff --git a/bugs/filed/import-no-settings-silent/import-no-settings-silent.twinproj b/bugs/filed/import-no-settings-silent/import-no-settings-silent.twinproj deleted file mode 100644 index f4b8fa495..000000000 Binary files a/bugs/filed/import-no-settings-silent/import-no-settings-silent.twinproj and /dev/null differ diff --git a/bugs/filed/import-no-settings-silent/repro.json b/bugs/filed/import-no-settings-silent/repro.json deleted file mode 100644 index ea5367f1d..000000000 --- a/bugs/filed/import-no-settings-silent/repro.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "mode": "cli", - "cli": [ - "import", - "{tmp}\\packed.twinproj", - "{src}\\Sources\\" - ], - "expect": { - "exit": 0, - "output": [ - "IMPORTED: .*Startup\\.twin", - "^\\.\\.\\. FAILED" - ], - "absent": [ - "ERROR" - ] - }, - "issue": 2493 -} diff --git a/bugs/filed/import-no-settings-silent/src/Sources/Startup.twin b/bugs/filed/import-no-settings-silent/src/Sources/Startup.twin deleted file mode 100644 index 48b35cb87..000000000 --- a/bugs/filed/import-no-settings-silent/src/Sources/Startup.twin +++ /dev/null @@ -1,7 +0,0 @@ -' An ordinary project. Nothing to run: import of its Sources folder alone, which has no Settings file, fails without a reason. -Module Startup - - Public Sub Main() - End Sub - -End Module diff --git a/bugs/filed/latebound-unknown-member-error/REPORT.md b/bugs/filed/latebound-unknown-member-error/REPORT.md deleted file mode 100644 index 5b2271373..000000000 --- a/bugs/filed/latebound-unknown-member-error/REPORT.md +++ /dev/null @@ -1,41 +0,0 @@ -Filed as [twinbasic/twinbasic#2490](https://github.com/twinbasic/twinbasic/issues/2490). - -## A late-bound call to a member that does not exist raises &H80020006, and `CallByName` raises &H80004005, where VB6 raises 438 - -**Describe the bug** -Calling a member that an object does not have, through an `Object` variable, raises error `&H80020006` (`DISP_E_UNKNOWNNAME`, *Unknown name.*) instead of 438, *Object doesn't support this property or method*. `CallByName` with the same name raises `&H80004005`, *Unspecified error*. An `On Error` handler written for VB6 or VBA, which tests for 438, does not catch either. Observed in a run of the reproducer project. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `latebound-unknown-member-error.twinproj` (attached as `latebound-unknown-member-error.zip`). Its one source file, `Startup.twin`, holds a class `Widget` with a `Sub Hello()` and a `Sub Main` that calls members through `Object` variables, with `On Error Resume Next`. -2. Run it and read the DEBUG CONSOLE: - ``` - o.Nope -> error -2147352570 (80020006) [Unknown name.] - Collection.Nope -> error -2147352570 (80020006) [Unknown name.] - CallByName o, Nope -> error -2147467259 (80004005) [Unspecified error] - o.Hello (control) -> error 0 (0) [] - ``` - -**Expected behavior** -Error 438 for all three, as in VB6 and VBA. The same cases in a VB6 project (a class, a `Variant` holding it, a `Collection`, a `Scripting.Dictionary`, and `CallByName` on each of them), attached as `latebound-unknown-member-error-vb6.zip`: -``` -o.Nope (class) -> error 438 (1B6) [Object doesn't support this property or method] -v.Nope (class in a Variant) -> error 438 (1B6) [Object doesn't support this property or method] -Collection.Nope -> error 438 (1B6) [Object doesn't support this property or method] -Dictionary.Nope -> error 438 (1B6) [Object doesn't support this property or method] -CallByName class Nope -> error 438 (1B6) [Object doesn't support this property or method] -CallByName Dictionary Nope -> error 438 (1B6) [Object doesn't support this property or method] -CallByName Collection Nope -> error 438 (1B6) [Object doesn't support this property or method] -``` -twinBASIC already maps `DISP_E_MEMBERNOTFOUND` from `Invoke` to 438; `DISP_E_UNKNOWNNAME` from `GetIDsOfNames`, which is the answer for a name the object does not have, should be mapped the same way, and `CallByName` should not turn it into `E_FAIL`. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 995 - -**Additional context** -Severity: code ported from VB6 or VBA that probes an object for a member with `On Error` and 438 stops working, and the message names a COM code instead of the problem. - -The twinBASIC class's `GetIDsOfNames` itself is right: it returns `DISP_E_UNKNOWNNAME` for a name it does not know, as the contract says; the conversion into a run-time error is what differs. The same `&H80020006` came from a twinBASIC class, a `Collection`, a `Scripting.Dictionary` and a `Scripting.FileSystemObject`, and a `Variant` holding an object; `CallByName` gave `&H80004005` for a twinBASIC class and a `Dictionary`. Other failures are mapped as VB6 does: a call that `Invoke` rejects with `DISP_E_MEMBERNOTFOUND` raises 438, and a `Nothing` object raises 91. - -<!-- Reproducer: bugs/latebound-unknown-member-error/ (mode run, expects the three lines above); verified on 995. VB6 output from bugs/latebound-unknown-member-error/vb6/ (VB6 6.0, Probe.exe). Stated in docs/Reference/COM-Interfaces/IDispatch.md, the first two rows of the table under "Errors from a late-bound call" and the first NOTE below it (In VBA an unknown member raises error 438); when fixed, delete the rows and the NOTE, and check docs/Reference/Default/VBA/Interaction/CallByName.md, which does not mention either code today, and anywhere else that lists the errors CallByName raises. --> diff --git a/bugs/filed/latebound-unknown-member-error/latebound-unknown-member-error.twinproj b/bugs/filed/latebound-unknown-member-error/latebound-unknown-member-error.twinproj deleted file mode 100644 index c8087c1cd..000000000 Binary files a/bugs/filed/latebound-unknown-member-error/latebound-unknown-member-error.twinproj and /dev/null differ diff --git a/bugs/filed/latebound-unknown-member-error/repro.json b/bugs/filed/latebound-unknown-member-error/repro.json deleted file mode 100644 index dcdd2a4ca..000000000 --- a/bugs/filed/latebound-unknown-member-error/repro.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mode": "run", - "expect": { - "exit": 0, - "output": [ - "^o\\.Nope -> error -2147352570 \\(80020006\\) \\[Unknown name\\.\\]$", - "^Collection\\.Nope -> error -2147352570 \\(80020006\\)", - "^CallByName o, Nope -> error -2147467259 \\(80004005\\)" - ] - }, - "issue": 2490 -} diff --git a/bugs/filed/latebound-unknown-member-error/src/Settings b/bugs/filed/latebound-unknown-member-error/src/Settings deleted file mode 100644 index e781f2ab5..000000000 --- a/bugs/filed/latebound-unknown-member-error/src/Settings +++ /dev/null @@ -1,61 +0,0 @@ -{ - "configuration.inherits": "Defaults", - "project.appTitle": "LateboundUnknownMemberError", - "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", - "project.buildType": "Standard EXE", - "project.description": "Reproduces: A late-bound call to a member that does not exist raises &H80020006, and CallByName raises &H80004005, where VB6 and VBA raise 438", - "project.exportPathIsV2": true, - "project.id": "{97A93038-DDBD-4CFC-A02B-D93573BDF332}", - "project.name": "LateboundUnknownMemberError", - "project.optionExplicit": true, - "project.references": [ - { - "id": "{00020430-0000-0000-C000-000000000046}", - "lcid": 0, - "name": "OLE Automation", - "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", - "path64": "C:\\Windows\\System32\\stdole2.tlb", - "symbolId": "stdole", - "versionMajor": 2, - "versionMinor": 0 - }, - { - "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "VB", - "versionBuild": 0, - "versionMajor": 0, - "versionMinor": 0, - "versionRevision": 31 - }, - { - "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - App global class object", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "AppGlobalClassProject", - "versionBuild": 0, - "versionMajor": 1, - "versionMinor": 0, - "versionRevision": 0 - } - ], - "project.settingsVersion": 1, - "project.startupObject": "Sub Main", - "project.warnings": { - "errors": [], - "hints": [], - "ignored": [], - "info": [], - "warnings": [] - }, - "runtime.useUnicodeStandardLibrary": true -} diff --git a/bugs/filed/latebound-unknown-member-error/src/Sources/Startup.twin b/bugs/filed/latebound-unknown-member-error/src/Sources/Startup.twin deleted file mode 100644 index 90e3df934..000000000 --- a/bugs/filed/latebound-unknown-member-error/src/Sources/Startup.twin +++ /dev/null @@ -1,27 +0,0 @@ -Class Widget - Public Sub Hello() - End Sub -End Class - -Module Startup - - Private Sub Show(ByVal label As String) - Debug.Print label & " -> error " & Err.Number & " (" & Hex(Err.Number) & ") [" & Err.Description & "]" - Err.Clear - End Sub - - Public Sub Main() - Dim o As Object = New Widget - Dim c As Object = New Collection - On Error Resume Next - o.Nope - Show "o.Nope" - c.Nope - Show "Collection.Nope" - CallByName o, "Nope", vbMethod - Show "CallByName o, Nope" - o.Hello - Show "o.Hello (control)" - End Sub - -End Module diff --git a/bugs/filed/latebound-unknown-member-error/vb6/Module1.bas b/bugs/filed/latebound-unknown-member-error/vb6/Module1.bas deleted file mode 100644 index b3a3ef98d..000000000 --- a/bugs/filed/latebound-unknown-member-error/vb6/Module1.bas +++ /dev/null @@ -1,56 +0,0 @@ -Attribute VB_Name = "Module1" -Option Explicit - -Sub Main() - On Error GoTo Fail - Open App.Path & "\out.txt" For Output As #9 - Cases - Close #9 - Exit Sub -Fail: - On Error Resume Next - Print #9, "unhandled error " & Err.Number & ": " & Err.Description - Close #9 -End Sub - -Sub Show(ByVal label As String) - Print #9, label & " -> error " & Err.Number & " (" & Hex(Err.Number) & ") [" & Err.Description & "]" - Err.Clear -End Sub - -Sub Cases() - Dim w As New Widget - Dim o As Object - Dim v As Variant - Dim c As New Collection - Dim d As Object - Set o = w - v = Empty - Set v = w - On Error Resume Next - o.Nope - Show "o.Nope (class)" - v.Nope - Show "v.Nope (class in a Variant)" - Dim oc As Object - Set oc = c - oc.Nope - Show "Collection.Nope" - Set d = CreateObject("Scripting.Dictionary") - d.Nope - Show "Dictionary.Nope" - CallByName o, "Nope", VbMethod - Show "CallByName class Nope" - CallByName d, "Nope", VbMethod - Show "CallByName Dictionary Nope" - CallByName oc, "Nope", VbMethod - Show "CallByName Collection Nope" - w.Hits = 0 - o.Hello 1 - Show "o.Hello 1" - Print #9, " Hits=" & w.Hits - w.Hits = 0: w.Gets = 0 - o.Prop = 1 - Show "o.Prop = 1" - Print #9, " Hits=" & w.Hits & " Gets=" & w.Gets -End Sub diff --git a/bugs/filed/module-named-like-project/REPORT.md b/bugs/filed/module-named-like-project/REPORT.md deleted file mode 100644 index 55ebae973..000000000 --- a/bugs/filed/module-named-like-project/REPORT.md +++ /dev/null @@ -1,44 +0,0 @@ -Filed as [twinbasic/twinbasic#2494](https://github.com/twinbasic/twinbasic/issues/2494). - -## A `[RunAfterBuild]` Sub does not run, and nothing says so, when a module is named like the project - -**Describe the bug** -When a module has the same name as the project, a `[RunAfterBuild]` Sub does not run, and nothing says so. The build succeeds and the exe is written. The DEBUG CONSOLE's last line is `[BUILD] Executing '<project>.<module>.<Sub>'...`, and nothing follows it: no message box, no entry in the error panel, no diagnostic, and the status bar is as it is after a run that works. The names are compared without regard to letter case, so `ProbeWS` and `probews` clash as well. Even an empty module with the project's name, beside the module that holds the Sub, stops it. Observed in the DEBUG CONSOLE after pressing Build. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `module-named-like-project.twinproj` (attached as `module-named-like-project.zip`). The project is named `ModuleNamedLikeProject` and has one module of that name: - ``` - Module ModuleNamedLikeProject - Public Sub Main() - End Sub - [RunAfterBuild] - Public Sub Hello() - Debug.Cls - Debug.Print "RunAfterBuild ran" - End Sub - End Module - ``` -2. Build the project. -3. Read the DEBUG CONSOLE. It ends at `[BUILD] Executing 'ModuleNamedLikeProject.ModuleNamedLikeProject.Hello'...`. `RunAfterBuild ran` is never printed. -4. Rename the module, to `Runner` for example, and build again. The console is cleared and holds `RunAfterBuild ran`. - -**Expected behavior** -The Sub runs whatever the modules are called, as it does once the module is renamed. If a module may not have the project's name, the compiler should refuse it with an error, as VB6 does, and not build the project and skip the Sub in silence. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 997 - -**Additional context** -Also on BETA 995, identically. - -VB6 refuses a project whose module has the project's name. Its build stops with `Name conflicts with existing module, project, or object library`, for a module that holds `Sub Main` (the project is attached as `module-named-like-project-vb6.zip`) and for an empty module of that name beside the module that holds `Sub Main` (checked, not attached). So VB6 reports the clash, where twinBASIC accepts it and skips the Sub. - -What does not reproduce it: with any other module name, such as `Runner`, the Sub runs. A project with no module of the project's name runs it, whichever module holds it. - -What reproduces it besides the plain case: the clash in another letter case, such as a project `ProbeWS` with a module `probews`; and an empty module with the project's name in a project whose `[RunAfterBuild]` Sub is in a different module, `Runner`, where the console ends at `[BUILD] Executing 'ProbeWS.Runner.<Sub>'...`, which names an unambiguous Sub. So the cause is the module's name, not a call that cannot be told apart. - -Severity: moderate. A `[RunAfterBuild]` Sub runs after the exe is built, for example to sign it, and here it is skipped while the build reports success. Nothing points at the module's name as the cause, so the project's owner has to find it by renaming modules. - -<!-- Reproducer: bugs/module-named-like-project/ (mode run, exe true; expects exit 5, the console's `[BUILD] Executing 'ModuleNamedLikeProject.ModuleNamedLikeProject.<Sub>'...` line, and no `RunAfterBuild ran`); verified on BETA 995 and 997 by `bug_repro.mjs verify`, with a copy whose module is named Runner as the control (exit 0, `RunAfterBuild ran`, verify says NO LONGER REPRODUCES). `bug_repro.mjs run` needs --exe by hand, because a project may hold one [RunAfterBuild] (TB5114) and this one's own is the subject; verify takes exe from repro.json. The harness wraps the Sub, so run's console names `tbrun_RunProbe`; a build in the IDE names the Sub itself. Steps 3 and 4 were checked on BETA 995 and 997 by building the reproducer, and a copy with the module named Runner, with `tbbuild --build --keep` and reading the DEBUG CONSOLE. The other-case and empty-module variants were checked on BETA 995 and 997 in scratch trees that are not in the reproducer. VB6: `bug_repro.mjs vb6 module-named-like-project` prints the refusal; the empty-module form was built in a scratch copy. docs/Reference/Attributes.md (#runafterbuild) carries a WARNING callout for this; once a fixed build is released, its sentences about the module's name go. WIP.ExamplesBuild.md (the bullet "No generated module may be named like the project") and scripts/check_examples.mjs (the comment on its three collision rules) state the rule for generated modules; they called the clash an ambiguous call that the IDE refuses at execution time, and were corrected with this entry. When fixed: say in both since which build the rule is no longer needed, or remove it. --> diff --git a/bugs/filed/module-named-like-project/module-named-like-project.twinproj b/bugs/filed/module-named-like-project/module-named-like-project.twinproj deleted file mode 100644 index 0936cc3ac..000000000 Binary files a/bugs/filed/module-named-like-project/module-named-like-project.twinproj and /dev/null differ diff --git a/bugs/filed/module-named-like-project/repro.json b/bugs/filed/module-named-like-project/repro.json deleted file mode 100644 index 9615e9816..000000000 --- a/bugs/filed/module-named-like-project/repro.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "mode": "run", - "exe": true, - "expect": { - "exit": 5, - "output": [ - "^\\[BUILD\\] Executing 'ModuleNamedLikeProject\\.ModuleNamedLikeProject\\.\\w+'\\.\\.\\.$" - ], - "absent": [ - "^RunAfterBuild ran$" - ] - }, - "issue": 2494 -} diff --git a/bugs/filed/module-named-like-project/src/Settings b/bugs/filed/module-named-like-project/src/Settings deleted file mode 100644 index aabccfd71..000000000 --- a/bugs/filed/module-named-like-project/src/Settings +++ /dev/null @@ -1,61 +0,0 @@ -{ - "configuration.inherits": "Defaults", - "project.appTitle": "ModuleNamedLikeProject", - "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", - "project.buildType": "Standard EXE", - "project.description": "Reproduces: A [RunAfterBuild] Sub does not run, and nothing says so, when a module is named like the project", - "project.exportPathIsV2": true, - "project.id": "{36F80D98-2C72-4F38-80C0-45BBD3314CCC}", - "project.name": "ModuleNamedLikeProject", - "project.optionExplicit": true, - "project.references": [ - { - "id": "{00020430-0000-0000-C000-000000000046}", - "lcid": 0, - "name": "OLE Automation", - "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", - "path64": "C:\\Windows\\System32\\stdole2.tlb", - "symbolId": "stdole", - "versionMajor": 2, - "versionMinor": 0 - }, - { - "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "VB", - "versionBuild": 0, - "versionMajor": 0, - "versionMinor": 0, - "versionRevision": 31 - }, - { - "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - App global class object", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "AppGlobalClassProject", - "versionBuild": 0, - "versionMajor": 1, - "versionMinor": 0, - "versionRevision": 0 - } - ], - "project.settingsVersion": 1, - "project.startupObject": "Sub Main", - "project.warnings": { - "errors": [], - "hints": [], - "ignored": [], - "info": [], - "warnings": [] - }, - "runtime.useUnicodeStandardLibrary": true -} diff --git a/bugs/filed/module-named-like-project/src/Sources/ModuleNamedLikeProject.twin b/bugs/filed/module-named-like-project/src/Sources/ModuleNamedLikeProject.twin deleted file mode 100644 index 2611c83ae..000000000 --- a/bugs/filed/module-named-like-project/src/Sources/ModuleNamedLikeProject.twin +++ /dev/null @@ -1,12 +0,0 @@ -Module ModuleNamedLikeProject - - Public Sub Main() - End Sub - - [RunAfterBuild] - Public Sub Hello() - Debug.Cls - Debug.Print "RunAfterBuild ran" - End Sub - -End Module diff --git a/bugs/filed/module-named-like-project/vb6/Module1.bas b/bugs/filed/module-named-like-project/vb6/Module1.bas deleted file mode 100644 index 0d5e5dd16..000000000 --- a/bugs/filed/module-named-like-project/vb6/Module1.bas +++ /dev/null @@ -1,19 +0,0 @@ -Attribute VB_Name = "ModuleNamedLikeProject" -Option Explicit - -' The VB6 side of the reproducer: a project named ModuleNamedLikeProject with a -' standard module of the same name. VB6 has no [RunAfterBuild], so what is checked -' is whether it accepts the name at all. -' Build and run it with: node scripts/bug_repro.mjs vb6 module-named-like-project - -Sub Main() - On Error GoTo Fail - Open App.Path & "\out.txt" For Output As #9 - Print #9, "Sub Main ran, in a module named like the project" - Close #9 - Exit Sub -Fail: - On Error Resume Next - Print #9, "unhandled error " & Err.Number & ": " & Err.Description - Close #9 -End Sub diff --git a/bugs/filed/new-on-interface/REPORT.md b/bugs/filed/new-on-interface/REPORT.md deleted file mode 100644 index 91afeb431..000000000 --- a/bugs/filed/new-on-interface/REPORT.md +++ /dev/null @@ -1,35 +0,0 @@ -Filed as [twinbasic/twinbasic#2486](https://github.com/twinbasic/twinbasic/issues/2486). - -## `New` on an `Interface` compiles, and a call on the object returns a default value, or ends in an access violation for an inherited member - -**Describe the bug** -`New` is accepted on an `Interface`, although no class exists for it, and the statement returns an object that is not `Nothing` and has no implementation behind its members. A call on a member the interface declares itself returns the default for its type (0 for a `Long`, an empty string, `Nothing` for an interface) and raises no error. A call on a member the interface inherits from another `Interface` ends the run with a native `ACCESS_VIOLATION`. `New stdole.IUnknown` and `New stdole.IDispatch` are refused with TB5074 (*Class construction expected class datatype*), so the check exists for an interface from a type library, and is missing for one declared in twinBASIC source, in a project or a package. Observed in a run of the reproducer project; found with `ErrorContext` of the VBRUN package, an interface with no class, and `ErrorCallstack` does the same. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `new-on-interface.twinproj` (attached as `new-on-interface.zip`). Its one source file, `Startup.twin`, declares `Interface IParent` with one `Function F() As Long` and `Interface IChild Extends IParent` with one `Function G() As Long`, and a `Sub Main` that constructs `New IParent`, `New ErrorContext` and `New IChild` and calls their members. -2. Run it (F5) and read the DEBUG CONSOLE: - ``` - IParent: Nothing? False, F() = 0 - ErrorContext: Unknown, Number = 0, Callstack Nothing? True - IChild.G() = 0 - IChild.F(), inherited, next - NATIVE EXCEPTION: ACCESS_VIOLATION /Startup.twin; Startup.Main LINE 000029 [$00000000] - ``` - The project compiles without a diagnostic. `ObjPtr` of each object is not 0. `ErrorContext.Number` and `.State` read 0, `.Description` and `.Source` read an empty string, and `.Callstack` is `Nothing`, so `e.Callstack.Count` raises error 91, as any call on `Nothing` does. That error 91 is not a second defect: an unhandled one ends a run, as it does for a `Collection` variable that is `Nothing`, and under `On Error Resume Next` it skips the statement. - -**Expected behavior** -A compile error, TB5074, for `New` on any `Interface`, as for `stdole.IUnknown`: an interface is a contract, and `New` needs a class that implements it. If an object is built nevertheless, a call on any of its members should behave the same way, not return a default for one member and crash on another. Code written against the VBRUN documentation, which lists `ErrorContext` with its members, compiles and then reads zeros, where a refusal would say at once that nothing can create the object. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 995 - -**Additional context** -Severity: low in practice, since few programs write `New` on an interface, but it is a compile-time check that is missing, and the result is a silent wrong value or a crash. - -What was tried, each in a project of its own: `New` is accepted on an interface declared with `Extends stdole.IUnknown`, with no `Extends`, with `Extends stdole.IDispatch`, with `Extends` another interface of the project, and on a `Private` one, and `Dim x As New IFoo` is accepted as well. `TypeName` gives `Unknown` for an interface that extends `stdole.IUnknown` (and for `ErrorContext` and `ErrorCallstack`), and the interface's own name for one with no `Extends` or with `Extends stdole.IDispatch`. A `String` member returns an empty string, a `Property Get` returns 0, a `Sub` returns without a sign, and a member returning the interface itself returns `Nothing`. Only inherited members crash. `New stdole.IUnknown` and `New stdole.IDispatch` are refused. The same results on BETA 983. - -VB6 has no comparison: a project cannot declare an `Interface`, and the interfaces of the type libraries it references (`stdole.IUnknown`, `IPictureDisp`, `IFontDisp`) are hidden from it (*User-defined type not defined*). - -<!-- Reproducer: bugs/new-on-interface/ (mode run, expects tbrun exit 5, the four lines above and the native exception); verified on 995. Probes from the narrowing are local scratch files, not in the repository. docs/Reference/Default/VBRUN/ErrorContext/index.md, ErrorCallstack/index.md and ErrorStackFrame/index.md carry a NOTE that no code can obtain these objects in BETA 995 ("code that uses them compiles, but nothing returns an object that implements them"); when this is fixed, check that wording, since `New` on them compiles today and returns an object that has no implementation. --> diff --git a/bugs/filed/new-on-interface/new-on-interface.twinproj b/bugs/filed/new-on-interface/new-on-interface.twinproj deleted file mode 100644 index 64aeb57a3..000000000 Binary files a/bugs/filed/new-on-interface/new-on-interface.twinproj and /dev/null differ diff --git a/bugs/filed/new-on-interface/repro.json b/bugs/filed/new-on-interface/repro.json deleted file mode 100644 index 800e4bc91..000000000 --- a/bugs/filed/new-on-interface/repro.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "mode": "run", - "expect": { - "exit": 5, - "output": [ - "^IParent: Nothing\\? False, F\\(\\) = 0$", - "^ErrorContext: Unknown, Number = 0, Callstack Nothing\\? True$", - "^IChild\\.G\\(\\) = 0$", - "^IChild\\.F\\(\\), inherited, next$", - "NATIVE EXCEPTION: ACCESS_VIOLATION" - ] - }, - "issue": 2486 -} diff --git a/bugs/filed/new-on-interface/src/Settings b/bugs/filed/new-on-interface/src/Settings deleted file mode 100644 index b2be6a5c5..000000000 --- a/bugs/filed/new-on-interface/src/Settings +++ /dev/null @@ -1,61 +0,0 @@ -{ - "configuration.inherits": "Defaults", - "project.appTitle": "NewOnInterface", - "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", - "project.buildType": "Standard EXE", - "project.description": "Reproduces: `New` on an `Interface` compiles, and a call on the object returns a default value, or ends in an access violation for an inherited member", - "project.exportPathIsV2": true, - "project.id": "{9276A787-2DCB-4B14-8467-1CABF235C647}", - "project.name": "NewOnInterface", - "project.optionExplicit": true, - "project.references": [ - { - "id": "{00020430-0000-0000-C000-000000000046}", - "lcid": 0, - "name": "OLE Automation", - "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", - "path64": "C:\\Windows\\System32\\stdole2.tlb", - "symbolId": "stdole", - "versionMajor": 2, - "versionMinor": 0 - }, - { - "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "VB", - "versionBuild": 0, - "versionMajor": 0, - "versionMinor": 0, - "versionRevision": 31 - }, - { - "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - App global class object", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "AppGlobalClassProject", - "versionBuild": 0, - "versionMajor": 1, - "versionMinor": 0, - "versionRevision": 0 - } - ], - "project.settingsVersion": 1, - "project.startupObject": "Sub Main", - "project.warnings": { - "errors": [], - "hints": [], - "ignored": [], - "info": [], - "warnings": [] - }, - "runtime.useUnicodeStandardLibrary": true -} diff --git a/bugs/filed/new-on-interface/src/Sources/Startup.twin b/bugs/filed/new-on-interface/src/Sources/Startup.twin deleted file mode 100644 index 7515fa63f..000000000 --- a/bugs/filed/new-on-interface/src/Sources/Startup.twin +++ /dev/null @@ -1,33 +0,0 @@ -' New is accepted on an Interface, although no class exists for it. The object it returns has -' nothing behind its members: a call on a member of the interface itself returns 0, and a call on a -' member that the interface inherits from another Interface ends in an ACCESS_VIOLATION. -' New stdole.IUnknown and New stdole.IDispatch are refused with TB5074, so the check is on the -' interface being declared in the project, or in a package, rather than in a type library. -[InterfaceId("E5B8C0A1-0000-4000-8000-0000000000A1")] -Private Interface IParent - Function F() As Long -End Interface - -[InterfaceId("E5B8C0A1-0000-4000-8000-0000000000A2")] -Private Interface IChild Extends IParent - Function G() As Long -End Interface - -Module Startup - - Public Sub Main() - Dim p As IParent = New IParent - Debug.Print "IParent: Nothing? " & (p Is Nothing) & ", F() = " & p.F() - - ' ErrorContext is such an interface, in the VBRUN package, and has no class either. - Dim e As ErrorContext = New ErrorContext - Debug.Print "ErrorContext: " & TypeName(e) & ", Number = " & e.Number & ", Callstack Nothing? " & (e.Callstack Is Nothing) - - Dim c As IChild = New IChild - Debug.Print "IChild.G() = " & c.G() - Debug.Print "IChild.F(), inherited, next" - Debug.Print "IChild.F() = " & c.F() - Debug.Print "not reached" - End Sub - -End Module diff --git a/bugs/filed/sub-named-like-module/REPORT.md b/bugs/filed/sub-named-like-module/REPORT.md deleted file mode 100644 index 2fde3ba4e..000000000 --- a/bugs/filed/sub-named-like-module/REPORT.md +++ /dev/null @@ -1,47 +0,0 @@ -Filed as [twinbasic/twinbasic#2481](https://github.com/twinbasic/twinbasic/issues/2481). - -## A `[RunAfterBuild]` Sub does not run, and nothing says so, when its module holds a procedure named like the module - -**Describe the bug** -When a module holds a procedure with the same name as the module, a `[RunAfterBuild]` Sub in that module does not run, and nothing says so. The build succeeds and the exe is written. The DEBUG CONSOLE's last line is `[BUILD] Executing '<project>.<module>.<Sub>'...`, and nothing follows it: no message box, no entry in the error panel, no diagnostic. Not even the first statement of the Sub runs. The names are compared without regard to letter case, so `Runner` and `runner` clash. A Sub, a Function and a `Private` Sub all cause it, and the procedure does not have to be called. Observed in the DEBUG CONSOLE after pressing Build. - -This is a sibling of "A `[RunAfterBuild]` Sub does not run, and nothing says so, when a module is named like the project". There the module and the project share a name; here the project's name plays no part, and the clash is inside one module. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `sub-named-like-module.twinproj` (attached as `sub-named-like-module.zip`). The project is named `SubNamedLikeModule`. It has a `Startup` module with an empty `Sub Main`, and this module: - ``` - Module Probe - [RunAfterBuild] - Public Sub Hello() - Debug.Cls - Debug.Print "RunAfterBuild ran" - End Sub - - Public Sub Probe() - End Sub - End Module - ``` -2. Build the project. -3. Read the DEBUG CONSOLE. It ends at `[BUILD] Executing 'SubNamedLikeModule.Probe.Hello'...`. `RunAfterBuild ran` is never printed. -4. Rename `Sub Probe` to `NotProbe`, or move it to another module, and build again. The console is cleared and holds `RunAfterBuild ran`. - -**Expected behavior** -The Sub runs whatever the other procedures in its module are called, as it does once `Probe` is renamed. If a procedure may not have its module's name, the compiler should refuse it with an error, and not build the project and skip the Sub in silence. Writing `Probe.Probe` in code is already refused, with TB5027. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 997 - -**Additional context** -Also on BETA 983, identically, so this is not a recent regression. - -VB6 accepts the name. A project named `SubNamedLikeModule` with a standard module `Probe` (`Attribute VB_Name = "Probe"`) that holds `Public Sub Probe()` builds, and `Sub Main` calls `Probe` and it runs (the project is attached as `sub-named-like-module-vb6.zip`; it printed `Sub Probe ran, in a module named Probe`). VB6 has no `[RunAfterBuild]`, so what is checked is that the module and the Sub can coexist. - -What does not reproduce it: a procedure with another name, such as `NotProbe`. A procedure named like a module, when the module that holds it is a different one from the module that holds the `[RunAfterBuild]` Sub (a `Runner` module holding the Sub, and an `Other` module holding `Sub Other`). - -What reproduces it besides the plain case: a `Function Probe`, a `Private Sub Probe`, and the clash in another letter case (`Module Runner` with `Sub runner`). The failure needs no help from any tool: the Sub is the one marked `[RunAfterBuild]`, and its body never starts. - -Severity: moderate. A `[RunAfterBuild]` Sub runs after the exe is built, for example to sign it, and here it is skipped while the build reports success. Nothing points at the clash as the cause, so the project's owner has to find it by renaming procedures. - -<!-- Reproducer: bugs/sub-named-like-module/ (mode run, exe true; expects exit 5, the console's `[BUILD] Executing 'SubNamedLikeModule.Probe.<Sub>'...` line, and no `RunAfterBuild ran`); verified on BETA 997 by `bug_repro.mjs verify` (reproduces, run exit 5), with a scratch copy whose Sub is named NotProbe as the control (exit 0, `RunAfterBuild ran`, verify says NO LONGER REPRODUCES); the copy was removed from bugs/ afterwards. `bug_repro.mjs run` needs --exe by hand, because a project may hold one [RunAfterBuild] (TB5114) and this one's own is the subject; verify takes exe from repro.json. The harness wraps the Sub in a Sub of its own appended to the same module, so run's console names `tbrun_RunProbe`; a build in the IDE names the Sub itself. The wrapper is not needed: a tree whose attribute the wrapper does not match (`[Description("x")] [RunAfterBuild]` on one line) fails the same way, and a marker file written by the Sub's first statement is not written. The scratch trees for the Function, Private Sub, other-case, other-module and BETA 983 variants are not in the reproducer; the variants were checked on BETA 997, the plain case and its no-wrapper form on BETA 983 as well. VB6: `bug_repro.mjs vb6 sub-named-like-module` prints `Sub Probe ran, in a module named Probe` and `Sub Main ran`. Related entry: "A `[RunAfterBuild]` Sub does not run, and nothing says so, when a module is named like the project" (bugs/module-named-like-project/); the two may share one cause, a name lookup that finds the module where it wants the procedure. docs/Reference/Attributes.md (#runafterbuild) carries this in the same WARNING callout as that entry; once a fixed build is released, its sentences about a procedure named like its module go. --> diff --git a/bugs/filed/sub-named-like-module/repro.json b/bugs/filed/sub-named-like-module/repro.json deleted file mode 100644 index 4fe961d43..000000000 --- a/bugs/filed/sub-named-like-module/repro.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "mode": "run", - "exe": true, - "expect": { - "exit": 5, - "output": [ - "^\\[BUILD\\] Executing 'SubNamedLikeModule\\.Probe\\.\\w+'\\.\\.\\.$" - ], - "absent": [ - "^RunAfterBuild ran$" - ] - }, - "issue": 2481 -} diff --git a/bugs/filed/sub-named-like-module/src/Settings b/bugs/filed/sub-named-like-module/src/Settings deleted file mode 100644 index fb3a4cbc7..000000000 --- a/bugs/filed/sub-named-like-module/src/Settings +++ /dev/null @@ -1,61 +0,0 @@ -{ - "configuration.inherits": "Defaults", - "project.appTitle": "SubNamedLikeModule", - "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", - "project.buildType": "Standard EXE", - "project.description": "Reproduces: A [RunAfterBuild] Sub does not run, and nothing says so, when its module holds a procedure named like the module", - "project.exportPathIsV2": true, - "project.id": "{FA93C97C-3E20-473A-A1C5-06C4F673AB54}", - "project.name": "SubNamedLikeModule", - "project.optionExplicit": true, - "project.references": [ - { - "id": "{00020430-0000-0000-C000-000000000046}", - "lcid": 0, - "name": "OLE Automation", - "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", - "path64": "C:\\Windows\\System32\\stdole2.tlb", - "symbolId": "stdole", - "versionMajor": 2, - "versionMinor": 0 - }, - { - "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "VB", - "versionBuild": 0, - "versionMajor": 0, - "versionMinor": 0, - "versionRevision": 31 - }, - { - "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - App global class object", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "AppGlobalClassProject", - "versionBuild": 0, - "versionMajor": 1, - "versionMinor": 0, - "versionRevision": 0 - } - ], - "project.settingsVersion": 1, - "project.startupObject": "Sub Main", - "project.warnings": { - "errors": [], - "hints": [], - "ignored": [], - "info": [], - "warnings": [] - }, - "runtime.useUnicodeStandardLibrary": true -} diff --git a/bugs/filed/sub-named-like-module/src/Sources/Probe.twin b/bugs/filed/sub-named-like-module/src/Sources/Probe.twin deleted file mode 100644 index 0b8b86b66..000000000 --- a/bugs/filed/sub-named-like-module/src/Sources/Probe.twin +++ /dev/null @@ -1,13 +0,0 @@ -Module Probe - - [RunAfterBuild] - Public Sub Hello() - Debug.Cls - Debug.Print "RunAfterBuild ran" - End Sub - - ' Never called. Its name is the module's name. - Public Sub Probe() - End Sub - -End Module diff --git a/bugs/filed/sub-named-like-module/sub-named-like-module.twinproj b/bugs/filed/sub-named-like-module/sub-named-like-module.twinproj deleted file mode 100644 index a44afff91..000000000 Binary files a/bugs/filed/sub-named-like-module/sub-named-like-module.twinproj and /dev/null differ diff --git a/bugs/filed/sub-named-like-module/vb6/Module1.bas b/bugs/filed/sub-named-like-module/vb6/Module1.bas deleted file mode 100644 index c51ae261d..000000000 --- a/bugs/filed/sub-named-like-module/vb6/Module1.bas +++ /dev/null @@ -1,24 +0,0 @@ -Attribute VB_Name = "Probe" -Option Explicit - -' The VB6 side of the reproducer: a standard module named Probe that holds a -' Sub named Probe. VB6 has no [RunAfterBuild], so what is checked is whether it -' accepts the name, and whether the Sub can be called. -' Build and run it with: node scripts/bug_repro.mjs vb6 sub-named-like-module - -Public Sub Probe() - Print #9, "Sub Probe ran, in a module named Probe" -End Sub - -Sub Main() - On Error GoTo Fail - Open App.Path & "\out.txt" For Output As #9 - Probe - Print #9, "Sub Main ran" - Close #9 - Exit Sub -Fail: - On Error Resume Next - Print #9, "unhandled error " & Err.Number & ": " & Err.Description - Close #9 -End Sub diff --git a/bugs/filed/two-calling-conventions-accepted/REPORT.md b/bugs/filed/two-calling-conventions-accepted/REPORT.md deleted file mode 100644 index d57dd398b..000000000 --- a/bugs/filed/two-calling-conventions-accepted/REPORT.md +++ /dev/null @@ -1,46 +0,0 @@ -Filed as [twinbasic/twinbasic#2480](https://github.com/twinbasic/twinbasic/issues/2480). - -## A declaration that names two calling conventions compiles without a diagnostic - -**Describe the bug** -An API declaration, a procedure, a delegate or an interface member can name its calling convention after its name, and the compiler accepts two conventions on one declaration, in either order, with no error or warning. The call then uses one of them. In every pair tried on a `Declare`, `CDecl` is dropped and the other keyword is used, whichever comes first. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `two-calling-conventions-accepted.twinproj` (attached as `two-calling-conventions-accepted.zip`). Its `Startup.twin` declares `RtlUlongByteSwap`, which `ntdll` exports as fastcall on a 32-bit build (its one argument in ECX), under several conventions: - ``` - Private Declare PtrSafe Function SwapFast FastCall Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - Private Declare PtrSafe Function SwapCdecl CDecl Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - Private Declare PtrSafe Function SwapFastCdecl FastCall CDecl Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - Private Declare PtrSafe Function SwapCdeclFast CDecl FastCall Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - Private Declare PtrSafe Function SwapThisCdecl ThisCall CDecl Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - Private Declare PtrSafe Function SwapCdeclThis CDecl ThisCall Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - ``` - It also holds a delegate (`CDecl FastCall`), a procedure in a module (`CDecl ThisCall`) and an interface member (`ThisCall CDecl`), each naming two conventions. `Sub Main` prints the result of each `Declare` for `&H11223344`. -2. Compile it. The Problems panel reports 0 errors and 0 warnings. -3. Run it (win32). The DEBUG CONSOLE prints: - ``` - FastCall: 44332211 - FastCall CDecl: 44332211 - CDecl FastCall: 44332211 - ThisCall CDecl: 44332211 - CDecl ThisCall: 44332211 - CDecl: 1EEC6F00 - ``` - The last value changes from run to run: called as cdecl, the function swaps whatever ECX held. Every pair is called as fastcall or thiscall, which pass this one argument the same way. - -**Expected behavior** -A declaration has one calling convention, so the second keyword is refused with an error that names it. As it is, nothing says which of the two the call uses, and when one of them is `CDecl`, it is dropped without a word. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 997 - -**Additional context** -Also on BETA 995, identically apart from the cdecl value. - -Only the `Declare` pairs are observed at run time. The delegate, the procedure and the interface member are only compiled, so which convention they use is not known. Not tried: `FastCall` with `ThisCall`, which this function cannot tell apart, and a 64-bit build, where the conventions have no effect. - -Severity: low. It takes a mistake to trigger it, but the mistake goes unreported, and a declaration meant as cdecl that names a second convention by accident calls the function the wrong way. - -<!-- Reproducer: bugs/two-calling-conventions-accepted/ (mode run, expects exit 0 and the four pair lines with 44332211); verified on BETA 995 and 997 by `bug_repro.mjs verify` and `run`, and `compile` reports no diagnostic on 997. Stated in docs/Features/Advanced/API-Declarations.md, section "Calling Conventions", the sentences after the table that begin "Name at most one convention" (they name BETA 997): when fixed, say that a second convention is an error, with no mention of the defect. --> diff --git a/bugs/filed/two-calling-conventions-accepted/repro.json b/bugs/filed/two-calling-conventions-accepted/repro.json deleted file mode 100644 index d5a3497e9..000000000 --- a/bugs/filed/two-calling-conventions-accepted/repro.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "mode": "run", - "expect": { - "exit": 0, - "output": [ - "^FastCall CDecl: +44332211$", - "^CDecl FastCall: +44332211$", - "^ThisCall CDecl: +44332211$", - "^CDecl ThisCall: +44332211$" - ] - }, - "issue": 2480 -} diff --git a/bugs/filed/two-calling-conventions-accepted/src/Settings b/bugs/filed/two-calling-conventions-accepted/src/Settings deleted file mode 100644 index 5c385d9ca..000000000 --- a/bugs/filed/two-calling-conventions-accepted/src/Settings +++ /dev/null @@ -1,61 +0,0 @@ -{ - "configuration.inherits": "Defaults", - "project.appTitle": "TwoCallingConventionsAccepted", - "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", - "project.buildType": "Standard EXE", - "project.description": "Reproduces: A declaration that names two calling conventions compiles without a diagnostic", - "project.exportPathIsV2": true, - "project.id": "{01FC4406-FC9C-4EBB-A5F5-0E15C3B6F1D2}", - "project.name": "TwoCallingConventionsAccepted", - "project.optionExplicit": true, - "project.references": [ - { - "id": "{00020430-0000-0000-C000-000000000046}", - "lcid": 0, - "name": "OLE Automation", - "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", - "path64": "C:\\Windows\\System32\\stdole2.tlb", - "symbolId": "stdole", - "versionMajor": 2, - "versionMinor": 0 - }, - { - "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "VB", - "versionBuild": 0, - "versionMajor": 0, - "versionMinor": 0, - "versionRevision": 31 - }, - { - "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", - "isCompilerPackage": true, - "licence": "MIT", - "name": "[COMPILER PACKAGE] twinBASIC - App global class object", - "path32": "", - "path64": "", - "publisher": "TWINBASIC-COMPILER", - "symbolId": "AppGlobalClassProject", - "versionBuild": 0, - "versionMajor": 1, - "versionMinor": 0, - "versionRevision": 0 - } - ], - "project.settingsVersion": 1, - "project.startupObject": "Sub Main", - "project.warnings": { - "errors": [], - "hints": [], - "ignored": [], - "info": [], - "warnings": [] - }, - "runtime.useUnicodeStandardLibrary": true -} diff --git a/bugs/filed/two-calling-conventions-accepted/src/Sources/Startup.twin b/bugs/filed/two-calling-conventions-accepted/src/Sources/Startup.twin deleted file mode 100644 index 654341065..000000000 --- a/bugs/filed/two-calling-conventions-accepted/src/Sources/Startup.twin +++ /dev/null @@ -1,35 +0,0 @@ -' Every declaration below names two calling conventions, and the project -' compiles with no diagnostic. On a 32-bit build, ntdll exports -' RtlUlongByteSwap as fastcall: it takes its argument in ECX. - -[InterfaceId("6B0C2F61-3D1E-4C8A-9F0B-2A7E5D4C3B21")] -[OleAutomation(False)] -Interface ITwo Extends IUnknown - Function Add ThisCall CDecl(ByVal Amount As Long) As Long -End Interface - -Module Startup - - Private Declare PtrSafe Function SwapFast FastCall Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - Private Declare PtrSafe Function SwapCdecl CDecl Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - Private Declare PtrSafe Function SwapFastCdecl FastCall CDecl Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - Private Declare PtrSafe Function SwapCdeclFast CDecl FastCall Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - Private Declare PtrSafe Function SwapThisCdecl ThisCall CDecl Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - Private Declare PtrSafe Function SwapCdeclThis CDecl ThisCall Lib "ntdll" Alias "RtlUlongByteSwap" (ByVal Source As Long) As Long - - Private Delegate Function TwoDelegate CDecl FastCall (ByVal x As Long) As Long - - Public Function TwoProcedure CDecl ThisCall(ByVal x As Long) As Long - Return x - End Function - - Public Sub Main() - Debug.Print "FastCall: " & Hex$(SwapFast(&H11223344)) - Debug.Print "FastCall CDecl: " & Hex$(SwapFastCdecl(&H11223344)) - Debug.Print "CDecl FastCall: " & Hex$(SwapCdeclFast(&H11223344)) - Debug.Print "ThisCall CDecl: " & Hex$(SwapThisCdecl(&H11223344)) - Debug.Print "CDecl ThisCall: " & Hex$(SwapCdeclThis(&H11223344)) - Debug.Print "CDecl: " & Hex$(SwapCdecl(&H11223344)) - End Sub - -End Module diff --git a/bugs/filed/two-calling-conventions-accepted/two-calling-conventions-accepted.twinproj b/bugs/filed/two-calling-conventions-accepted/two-calling-conventions-accepted.twinproj deleted file mode 100644 index 78b72ee6c..000000000 Binary files a/bugs/filed/two-calling-conventions-accepted/two-calling-conventions-accepted.twinproj and /dev/null differ diff --git a/bugs/filed/udt-watch-codegen-error/REPORT.md b/bugs/filed/udt-watch-codegen-error/REPORT.md index 983f937ec..9b5539c10 100644 --- a/bugs/filed/udt-watch-codegen-error/REPORT.md +++ b/bugs/filed/udt-watch-codegen-error/REPORT.md @@ -3,7 +3,9 @@ Filed as [twinbasic/twinbasic#2485](https://github.com/twinbasic/twinbasic/issue ## A watch on a variable of a user-defined type fails with a codegen error, and the Debug Console reports a linker error at every stop **Describe the bug** -While the debugger is stopped, a watch on a variable of a user-defined type cannot be evaluated. **Watches** shows `(compile error: codegen error; check for compilation errors)`, of type `ERROR`, and the Debug Console prints `[LINKER] compilation (codegen) error detected in 'Startup.{temp_procedure}' at line #1`. The linker error comes again at every later stop while the watch exists, and `? p` in the Debug Console fails the same way. **Variables** shows the same variable and its fields without trouble, and a watch on one of its fields works. +While the debugger is stopped, `? p` in the Debug Console, where `p` is a variable of a user-defined type, fails with `(compile error: codegen error; check for compilation errors)`, and the Debug Console prints `[LINKER] compilation (codegen) error detected in 'Startup.{temp_procedure}' at line #1`. **Variables** and a watch on `p` show the same variable and its fields without trouble. + +On BETA 995 and 997 a watch on `p` failed in the same way, and repeated the linker error at every later stop; BETA 1005 fixed the watch, and left `? p` as it was. **To Reproduce** Steps to reproduce the behavior: @@ -24,22 +26,20 @@ Steps to reproduce the behavior: End Sub ``` 2. Put a breakpoint on the line marked `BREAK` (F9) and press F5. The run stops there, and **Variables** shows `p` as `{user defined type, 8 bytes}`, with `X` and `Y`. -3. Add a watch on `p` (**Debug > Add Watch...**, or the plus sign in **Watches**). The watch shows `(compile error: codegen error; check for compilation errors)`, of type `ERROR`, and the Debug Console prints `[LINKER] compilation (codegen) error detected in 'Startup.{temp_procedure}' at line #1`. -4. Press F5. The run stops at the breakpoint again, and the Debug Console prints the linker error again. -5. Type `? p` in the Debug Console and press Enter. It fails with the same error, and the Debug Console prints the same linker line. +3. Type `? p` in the Debug Console and press Enter. It prints `(compile error: codegen error; check for compilation errors)`, and the Debug Console prints `[LINKER] compilation (codegen) error detected in 'Startup.{temp_procedure}' at line #1`. **Expected behavior** -The watch shows `p` as **Variables** does, `{user defined type, 8 bytes}` opening to its fields, as VBA's Watch window shows a variable of a user-defined type. An expression that cannot be shown, such as `? p`, which has no single value to print, is refused with a message about the expression. A linker error about generated code reads as though the project had failed to build, which it has not. +An expression that cannot be shown, such as `? p`, which has no single value to print, is refused with a message about the expression. A linker error about generated code reads as though the project had failed to build, which it has not. **Desktop:** - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 995 + - twinBASIC compiler version: BETA 1005 **Additional context** -Also on BETA 987, identically. +Also on BETA 987, 995 and 997, where a watch on `p` failed in the same way. -What does not reproduce it: a watch on a field, `p.X`, which shows `7`, of type `Variant [Long]`; and **Variables**, which shows `p` and its fields. That type suggests a watch is evaluated as a `Variant`, which cannot hold a value of a user-defined type. Seen with a `Private Type` declared in the module; other declarations of the type were not tried. +What does not reproduce it, on BETA 1005: a watch on `p`, which shows `{user defined type, 8 bytes}`, of type `Point`, and again at the next stop with no linker error; a watch on a field, `p.X`, which shows `7`, of type `Long`; and **Variables**, which shows `p` and its fields. Seen with a `Private Type` declared in the module; other declarations of the type were not tried. -Severity: a watch on a structure is unusable, and while it exists every stop adds a linker error to the Debug Console. +Severity: low. The console reports a failed build for a line that only asks to print a structure. -<!-- Asserted by `ide-test.bat --only watches` (test/ide/watches.test.mjs: the watch on p, the watch on p.X, the next stop, and ? p); passes on BETA 995 and 987. The reproducer's Startup.twin is test/ide/probes/watches/Sources/Startup.twin with a different header comment. When fixed: update that test and this entry. --> +<!-- Asserted by `ide-test.bat --only watches` (test/ide/watches.test.mjs: the watch on p, the watch on p.X and the next stop, which now assert the fixed behaviour, and ? p, which asserts the defect); passes on BETA 1005, and its earlier form passed on 995 and 987. The reproducer's Startup.twin is test/ide/probes/watches/Sources/Startup.twin with a different header comment. When fixed: update that test and this entry. --> diff --git a/bugs/filed/udt-watch-codegen-error/repro.json b/bugs/filed/udt-watch-codegen-error/repro.json index 8f35928ff..d2d8967ea 100644 --- a/bugs/filed/udt-watch-codegen-error/repro.json +++ b/bugs/filed/udt-watch-codegen-error/repro.json @@ -2,10 +2,8 @@ "mode": "lane", "lane": "ide:watches", "tests": [ - "a watch on p fails with a codegen error, and the console reports a linker error", - "the next stop reports the linker error again", - "? p in the DEBUG CONSOLE fails the same way" + "? p in the DEBUG CONSOLE fails with a codegen error, and the console reports a linker error" ], - "steps": "1. Open udt-watch-codegen-error.twinproj in the twinBASIC IDE, put a breakpoint on the line marked BREAK (F9) and press F5. The run stops there; VARIABLES shows p as {user defined type, 8 bytes}, with X and Y.\n2. Add a watch on p (Debug > Add Watch..., or the plus sign in WATCHES).\n3. Expected: the watch shows p as VARIABLES does. Actual: its value is (compile error: codegen error; check for compilation errors) and its type ERROR, and the DEBUG CONSOLE prints [LINKER] compilation (codegen) error detected in 'Startup.{temp_procedure}' at line #1.\n4. Press F5. The run stops at the breakpoint again, on the loop's second pass, and the DEBUG CONSOLE prints the linker error again: it does so at every stop while the watch is there.\n5. Type ? p in the DEBUG CONSOLE and press Enter: the same error, and the same linker line.\nControl: a watch on p.X shows 7, of type Variant [Long].\nAsserted on BETA 995 by `ide-test.bat --only watches` (the project is test/ide/probes/watches).", + "steps": "1. Open udt-watch-codegen-error.twinproj in the twinBASIC IDE, put a breakpoint on the line marked BREAK (F9) and press F5. The run stops there; VARIABLES shows p as {user defined type, 8 bytes}, with X and Y.\n2. Type ? p in the DEBUG CONSOLE and press Enter.\n3. Expected: an error that says p has no single value to print. Actual: (compile error: codegen error; check for compilation errors), and the DEBUG CONSOLE prints [LINKER] compilation (codegen) error detected in 'Startup.{temp_procedure}' at line #1.\nControl: a watch on p shows {user defined type, 8 bytes}, of type Point, as VARIABLES does, and a watch on p.X shows 7, of type Long (BETA 1005; up to BETA 997 the watch on p failed as ? p does).\nAsserted on BETA 1005 by `ide-test.bat --only watches` (the project is test/ide/probes/watches).", "issue": 2485 } diff --git a/bugs/filed/webview2-headers-clone-restarts/REPORT.md b/bugs/filed/webview2-headers-clone-restarts/REPORT.md index 8cc72133e..5a72489ac 100644 --- a/bugs/filed/webview2-headers-clone-restarts/REPORT.md +++ b/bugs/filed/webview2-headers-clone-restarts/REPORT.md @@ -3,39 +3,41 @@ Filed as [twinbasic/twinbasic#2496](https://github.com/twinbasic/twinbasic/issue ## The WebView2 headers enumerator's Clone starts at the first header, not at the enumerator's position **Describe the bug** -`IEnumVARIANT::Clone` on the enumerator of a `WebView2RequestHeaders` object (`_NewEnum`, a `WebView2HeadersCollection`) returns an enumerator that starts at the first header, whatever the position of the enumerator it was cloned from. After `Next` has returned the first header, the clone's `Next` returns the first header again; after `Skip 2`, the clone still starts at the first. Observed in a run of the reproducer project. +`IEnumVARIANT::Clone` on the enumerator of a `WebView2ResponseHeaders` object (`_NewEnum`, a `WebView2HeadersCollection`) returns an enumerator at the wrong position once the enumerator has been `Reset`: after two `Next` calls and a `Reset`, the enumerator's `Next` returns the first header, and the clone's `Next` returns the third. Before a `Reset` the clone starts where the enumerator is, and the request headers' enumerator (`WebView2RequestHeaders`) is right after a `Reset` as well. Observed in a run of the reproducer project. + +On BETA 997 the clone of either enumerator started at the first header whatever the enumerator's position, which is what this issue first reported; BETA 1005 fixed that, and left the case after a `Reset` wrong for the response headers. **To Reproduce** Steps to reproduce the behavior: -1. Open `webview2-headers-clone-restarts.twinproj` (attached as `webview2-headers-clone-restarts.zip`). Its form holds one WebView2 control, which navigates to `https://example.com/page` and answers that request itself in `WebResourceRequested`. Before answering, the handler passes `Request.Headers` to `EnumProbe.Run`, which declares `IEnumVARIANT` (the package's own declaration is private) and calls it on `Headers._NewEnum()`: +1. Open `webview2-headers-clone-restarts.twinproj` (attached as `webview2-headers-clone-restarts.zip`). Its form holds one WebView2 control, which navigates to `https://example.com/page` and answers that request itself in `WebResourceRequested`. The handler appends four headers, `Ha` to `Hd`, to the response, and passes three fresh enumerators of `Request.Headers`, then three of `Response.Headers`, to `EnumProbe.Run`, which declares `IEnumVARIANT` (the package's own declaration is private) and calls it: ``` - Dim u As stdole.IUnknown = Headers._NewEnum() - Dim e As IEnumVARIANTProbe = u - Debug.Print "enumerator Next: " & NextName(e) - Dim c As IEnumVARIANTProbe = e.Clone() - Debug.Print "clone Next: " & NextName(c) - Debug.Print "enumerator Next: " & NextName(e) + Set e = u1 + NextName e + NextName e + e.Reset + Set c = e.Clone() + Debug.Print what & ": after two Next and Reset, enumerator Next: " & NextName(e) & ", clone Next: " & NextName(c) ``` 2. Run it (F5); the form closes by itself. The DEBUG CONSOLE shows: ``` - For Each: Accept Upgrade-Insecure-Requests User-Agent sec-ch-ua sec-ch-ua-mobile sec-ch-ua-platform - enumerator Next: Accept - clone Next: Accept - enumerator Next: Upgrade-Insecure-Requests - after Skip 2, enumerator Next: User-Agent - after Skip 2, clone Next: Accept + request: after one Next (Accept), enumerator Next: Upgrade-Insecure-Requests, clone Next: Upgrade-Insecure-Requests + request: after two Next and Reset, enumerator Next: Accept, clone Next: Accept + request: after Skip 2, enumerator Next: User-Agent, clone Next: User-Agent + response: after one Next (Ha), enumerator Next: Hb, clone Next: Hb + response: after two Next and Reset, enumerator Next: Ha, clone Next: Hc + response: after Skip 2, enumerator Next: Hc, clone Next: Hc ``` **Expected behavior** -`clone Next: Upgrade-Insecure-Requests` and `after Skip 2, clone Next: User-Agent`: `IEnumVARIANT::Clone` creates an enumerator with the same state as the current one, so the clone continues from the same position. +`response: after two Next and Reset, enumerator Next: Ha, clone Next: Ha`: `IEnumVARIANT::Clone` creates an enumerator with the same state as the current one, so the clone continues from the same position, as the request headers' clone does. **Desktop:** - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 997 + - twinBASIC compiler version: BETA 1005 **Additional context** -In the package's `WebView2HeadersCollection.twin`, the four derived classes' `Clone` pass `IterationIdx` to a new enumerator, whose constructor `Skip`s that many headers, but nothing ever assigns `IterationIdx`: `Next` and `Skip` call `Iterator.MoveNext` without advancing it, so it stays 0. Incrementing it in `Next` (once per header fetched) and in `Skip`, and setting it to 0 in each `Reset`, would fix it. The response headers classes (`WebView2ResponseHeaders`) have the same `Clone`; only the request headers were run. +In BETA 1005's package source, `WebView2HeadersCollection.twin`, `Next` and `Skip` now advance `IterationIdx`, which `Clone` passes to the new enumerator. The two request headers classes set `IterationIdx = 0` in their `Reset`, and the two response headers classes, `WebView2HeadersCollection_ResponseGetHeaders` and `WebView2HeadersCollection_ResponseEnumerator` in `WebView2ResponseHeaders.twin`, do not, so after a `Reset` their clone skips as many headers as were read before it. Setting `IterationIdx = 0` in those two `Reset`s, as the request side does, would fix it. Only `_NewEnum` (the `ResponseEnumerator` class) was run; `GetHeaders` (the other class) has the same `Reset`. -Severity: low. `For Each` does not call `Clone`, so only a COM client that clones the enumerator gets the wrong headers. +Severity: low. `For Each` does not call `Clone`, so only a COM client that clones the enumerator after resetting it gets the wrong headers. -<!-- Reproducer: bugs/webview2-headers-clone-restarts/ (mode run, expects the five Next lines above); verified on BETA 997 with bug_repro run and verify. Found by reading 997's package source (export cache %TEMP%/tb-census/beta-997) while updating the WebView2 headers pages; first confirmed by a Sonnet agent's probe, built as an exe through tbrun --exe (kit beta997-probes/wv2clone-brief.md; the tree was in the session scratchpad, not kept). The request goes to example.com but is answered in the handler. Stated in a `> [!WARNING]` naming BETA 997 on docs/Reference/Built-In/WebView2/WebView2HeadersCollection.md, after the paragraph on the enumerator operations; remove it once a fixed build is released. --> +<!-- Reproducer: bugs/webview2-headers-clone-restarts/ (mode run, expects the request's Reset line and the response's one-Next and Reset lines above); verified on BETA 1005 with bug_repro run and verify; on BETA 997 every clone line prints the first header (Accept, Ha). Found by reading 997's package source while updating the WebView2 headers pages; the response side and the Reset case were added on BETA 1005, from its exported package source. The request goes to example.com but is answered in the handler. Stated in a `> [!WARNING]` naming BETA 1005 on docs/Reference/Built-In/WebView2/WebView2HeadersCollection.md, after the paragraph on the enumerator operations; remove it once a fixed build is released. --> diff --git a/bugs/filed/webview2-headers-clone-restarts/repro.json b/bugs/filed/webview2-headers-clone-restarts/repro.json index 93366b989..983d4d21d 100644 --- a/bugs/filed/webview2-headers-clone-restarts/repro.json +++ b/bugs/filed/webview2-headers-clone-restarts/repro.json @@ -3,11 +3,9 @@ "expect": { "exit": 0, "output": [ - "^enumerator Next: Accept$", - "^clone Next:\\s+Accept$", - "^enumerator Next: Upgrade-Insecure-Requests$", - "^after Skip 2, enumerator Next: User-Agent$", - "^after Skip 2, clone Next:\\s+Accept$" + "^request: after two Next and Reset, enumerator Next: Accept, clone Next: Accept$", + "^response: after one Next \\(Ha\\), enumerator Next: Hb, clone Next: Hb$", + "^response: after two Next and Reset, enumerator Next: Ha, clone Next: Hc$" ] }, "issue": 2496 diff --git a/bugs/filed/webview2-headers-clone-restarts/src/Sources/EnumProbe.twin b/bugs/filed/webview2-headers-clone-restarts/src/Sources/EnumProbe.twin index 3265dd010..d9492c6b7 100644 --- a/bugs/filed/webview2-headers-clone-restarts/src/Sources/EnumProbe.twin +++ b/bugs/filed/webview2-headers-clone-restarts/src/Sources/EnumProbe.twin @@ -22,27 +22,25 @@ Module EnumProbe Return hd.Name End Function - Public Sub Run(ByVal Headers As WebView2RequestHeaders) - Dim h As WebView2Header - Dim names As String - For Each h In Headers - names &= " " & h.Name - Next - Debug.Print "For Each:" & names - - Dim u As stdole.IUnknown = Headers._NewEnum() - Dim e As IEnumVARIANTProbe = u - Debug.Print "enumerator Next: " & NextName(e) + ' Three fresh enumerators of one collection: Clone after one Next, Clone after + ' two Next and a Reset, and Clone after Skip 2. + Public Sub Run(ByVal what As String, ByVal u0 As stdole.IUnknown, ByVal u1 As stdole.IUnknown, ByVal u2 As stdole.IUnknown) + Dim e As IEnumVARIANTProbe = u0 + Dim first As String = NextName(e) Dim c As IEnumVARIANTProbe = e.Clone() - Debug.Print "clone Next: " & NextName(c) - Debug.Print "enumerator Next: " & NextName(e) + Debug.Print what & ": after one Next (" & first & "), enumerator Next: " & NextName(e) & ", clone Next: " & NextName(c) + + Set e = u1 + NextName e + NextName e + e.Reset + Set c = e.Clone() + Debug.Print what & ": after two Next and Reset, enumerator Next: " & NextName(e) & ", clone Next: " & NextName(c) - Set u = Headers._NewEnum() - Set e = u + Set e = u2 e.Skip 2 Set c = e.Clone() - Debug.Print "after Skip 2, enumerator Next: " & NextName(e) - Debug.Print "after Skip 2, clone Next: " & NextName(c) + Debug.Print what & ": after Skip 2, enumerator Next: " & NextName(e) & ", clone Next: " & NextName(c) End Sub End Module diff --git a/bugs/filed/webview2-headers-clone-restarts/src/Sources/Form1.twin b/bugs/filed/webview2-headers-clone-restarts/src/Sources/Form1.twin index d14bf80b9..925ddc5db 100644 --- a/bugs/filed/webview2-headers-clone-restarts/src/Sources/Form1.twin +++ b/bugs/filed/webview2-headers-clone-restarts/src/Sources/Form1.twin @@ -1,6 +1,7 @@ ' A form holding one WebView2 control. Once the control is ready it navigates to a ' page whose request it answers itself in WebResourceRequested, after enumerating the -' request's headers there; when that navigation completes the form closes. +' request's headers and the response's there; when that navigation completes the +' form closes. [FormDesignerId("7B247400-0000-4000-9000-7B2474000F01")] [PredeclaredId] @@ -15,9 +16,16 @@ Class Form1 Private Sub WebView21_WebResourceRequested(ByVal Request As WebView2Request, ByVal Response As WebView2Response) _ Handles WebView21.WebResourceRequested + Response.Headers.AppendHeader "Ha", "1" + Response.Headers.AppendHeader "Hb", "2" + Response.Headers.AppendHeader "Hc", "3" + Response.Headers.AppendHeader "Hd", "4" If Not Probed Then Probed = True - EnumProbe.Run Request.Headers + Dim rq As WebView2RequestHeaders = Request.Headers + EnumProbe.Run "request", rq._NewEnum(), rq._NewEnum(), rq._NewEnum() + Dim rs As WebView2ResponseHeaders = Response.Headers + EnumProbe.Run "response", rs._NewEnum(), rs._NewEnum(), rs._NewEnum() End If Response.StatusCode = 200 Response.ReasonPhrase = "OK" diff --git a/bugs/filed/webview2-headers-clone-restarts/webview2-headers-clone-restarts.twinproj b/bugs/filed/webview2-headers-clone-restarts/webview2-headers-clone-restarts.twinproj index dd63bce93..9baf76c90 100644 Binary files a/bugs/filed/webview2-headers-clone-restarts/webview2-headers-clone-restarts.twinproj and b/bugs/filed/webview2-headers-clone-restarts/webview2-headers-clone-restarts.twinproj differ diff --git a/bugs/ide-stylesheet-image-labels/repro.json b/bugs/ide-stylesheet-image-labels/repro.json new file mode 100644 index 000000000..6b7c5b6c9 --- /dev/null +++ b/bugs/ide-stylesheet-image-labels/repro.json @@ -0,0 +1,14 @@ +{ + "mode": "probe", + "probe": ["scripts/probe_ide_image_labels.mjs"], + "expect": { + "exit": 1, + "output": [ + "^ide/styles\\.css line \\d+: labelled /png, the bytes are a PNG$", + "^ide/styles\\.css line \\d+: labelled image/png, the bytes are a GIF$", + "^ide/styles\\.css line \\d+: labelled image/png, the bytes are a JPEG$", + "^[1-9]\\d* of \\d+ images in the IDE's stylesheets are mislabelled \\(BETA \\d+\\)$" + ] + }, + "steps": "1. Open ide\\styles.css, in the folder of the twinBASIC installation, in a text editor.\n2. Go to line 2947 (line 2935 in BETA 957). It is background-image: url('data:/png;base64,iVBORw0KGgo...'), the rule of .treeExpanded and .sectionExpanderIcon. The media type of the data: URI is /png, with no image in front of the slash. Decode the base64 after the comma: the first eight bytes are 89 50 4E 47 0D 0A 1A 0A, the signature of a PNG. Line 2952 (.treeExpanded1) is the same.\n3. Go to line 3917 (BETA 957: 3905), the rule of #progressSpinner and .progressSpinnerIcon. It is url('data:image/png;base64,R0lGODlh...'). Decode the base64: the first six bytes are 47 49 46 38 39 61, GIF89a, a GIF.\n4. Go to line 7412 (BETA 957: 7323), the rule of .vipGold1_autoWeighingSystemsLogo. It is url('data:image/png;base64,/9j/4AAQ...'). Decode the base64: the first three bytes are FF D8 FF, the start of a JPEG.\n5. Every other image the style sheets embed has the type its bytes have.\nscripts/probe_ide_image_labels.mjs does this for every image in the .css files of the installation (the newest twinBASIC_IDE_BETA_<n> folder on the Desktop, or --ide <twinBASIC.exe>) and prints one line for each that is mislabelled. On BETA 1005, 997 and 957 it prints the same four." +} diff --git a/bugs/import-export-exit-zero/import-export-exit-zero.twinproj b/bugs/import-export-exit-zero/import-export-exit-zero.twinproj new file mode 100644 index 000000000..132acf93c Binary files /dev/null and b/bugs/import-export-exit-zero/import-export-exit-zero.twinproj differ diff --git a/bugs/import-export-exit-zero/repro.json b/bugs/import-export-exit-zero/repro.json new file mode 100644 index 000000000..12a584056 --- /dev/null +++ b/bugs/import-export-exit-zero/repro.json @@ -0,0 +1,24 @@ +{ + "mode": "cli", + "cli": [ + ["import", "{tmp}\\no-settings.twinproj", "{src}\\Sources\\"], + ["import", "{tmp}\\no-folder.twinproj", "{tmp}\\no-such-folder\\"], + ["export", "{tmp}\\no-such-project.twinproj", "{tmp}\\out\\"], + ["export", "{project}", "{src}\\"], + ["import", "{project}", "{src}\\"] + ], + "expect": { + "exit": 0, + "output": [ + "^\\s*ERROR: missing Settings file$", + "^\\s*ERROR: input folder does not exist$", + "^\\s*ERROR: input twinproj file does not exist$", + "ERROR: output file already exists and --overwrite not set: .*Settings$", + "ERROR: failed to create output file: .*\\(already exists and --overwrite not set\\)$", + "^\\.\\.\\. FAILED" + ], + "absent": [ + "^\\.\\.\\. DONE" + ] + } +} diff --git a/bugs/filed/exe-error-box-title/src/Settings b/bugs/import-export-exit-zero/src/Settings similarity index 85% rename from bugs/filed/exe-error-box-title/src/Settings rename to bugs/import-export-exit-zero/src/Settings index 24ca19ee9..97427b5b5 100644 --- a/bugs/filed/exe-error-box-title/src/Settings +++ b/bugs/import-export-exit-zero/src/Settings @@ -1,12 +1,12 @@ { "configuration.inherits": "Defaults", - "project.appTitle": "ExeErrorBoxTitle", + "project.appTitle": "ImportExportExitZero", "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", "project.buildType": "Standard EXE", - "project.description": "Reproduces: An unhandled error in a built exe opens a box titled shutdown", + "project.description": "Reproduces: `import` and `export` exit with code 0 when they end `... FAILED`", "project.exportPathIsV2": true, - "project.id": "{7AE773DC-05F5-4F2D-AFC2-50D37A170ED6}", - "project.name": "ExeErrorBoxTitle", + "project.id": "{2C64CA9A-81D7-4968-9FE9-8F88FC647B70}", + "project.name": "ImportExportExitZero", "project.optionExplicit": true, "project.references": [ { diff --git a/bugs/filed/sub-named-like-module/src/Sources/Startup.twin b/bugs/import-export-exit-zero/src/Sources/Startup.twin similarity index 100% rename from bugs/filed/sub-named-like-module/src/Sources/Startup.twin rename to bugs/import-export-exit-zero/src/Sources/Startup.twin diff --git a/bugs/latebound-constant-raise-exception/latebound-constant-raise-exception.twinproj b/bugs/latebound-constant-raise-exception/latebound-constant-raise-exception.twinproj new file mode 100644 index 000000000..3cef85f20 Binary files /dev/null and b/bugs/latebound-constant-raise-exception/latebound-constant-raise-exception.twinproj differ diff --git a/bugs/latebound-constant-raise-exception/repro.json b/bugs/latebound-constant-raise-exception/repro.json new file mode 100644 index 000000000..dc77a8d08 --- /dev/null +++ b/bugs/latebound-constant-raise-exception/repro.json @@ -0,0 +1,18 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RaiseThroughLateBindingTests\\.UnknownMember_Raises438$", + "^PASS RaiseThroughLateBindingTests\\.EarlyBound_RaiseConstant_Raises380$", + "^PASS RaiseThroughLateBindingTests\\.LateBound_RaiseVariable_Raises380$", + "^FAIL RaiseThroughLateBindingTests\\.LateBound_RaiseConstantWithSource_Raises380: .*Actual: -2147352567 ", + "^FAIL RaiseThroughLateBindingTests\\.LateBound_RaiseConstantWithDescription_Raises380: .*Actual: -2147352567 ", + "^FAIL RaiseThroughLateBindingTests\\.LateBound_RaiseConstant_Raises380: .*Actual: -2147352567 ", + "^FAIL RaiseThroughLateBindingTests\\.LateBound_ErrorStatement_Raises380: .*Actual: -2147352567 ", + "^FAIL RaiseThroughLateBindingTests\\.LateBound_PropertyLet_Raises380: .*Actual: -2147352567 ", + "^FAIL RaiseThroughLateBindingTests\\.CallByName_RaiseConstant_Raises380: .*Actual: -2147352567 ", + "^FAIL RaiseThroughLateBindingTests\\.LateBound_FormColour_Raises380: .*Actual: -2147352567 " + ] + } +} diff --git a/bugs/latebound-constant-raise-exception/src/Settings b/bugs/latebound-constant-raise-exception/src/Settings new file mode 100644 index 000000000..3cc21d515 --- /dev/null +++ b/bugs/latebound-constant-raise-exception/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "LateboundConstantRaiseException", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: An error raised with a constant number, in a member called late-bound, reaches the caller as &H80020009, *Exception occurred.*", + "project.exportPathIsV2": true, + "project.id": "{C16D1ABC-02B9-4B4D-B630-87B606C43D5E}", + "project.name": "LateboundConstantRaiseException", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/latebound-constant-raise-exception/src/Sources/RaiseThroughLateBindingTests.twin b/bugs/latebound-constant-raise-exception/src/Sources/RaiseThroughLateBindingTests.twin new file mode 100644 index 000000000..cfdcaa3a3 --- /dev/null +++ b/bugs/latebound-constant-raise-exception/src/Sources/RaiseThroughLateBindingTests.twin @@ -0,0 +1,132 @@ +' Regression tests for #2490 and its follow-up: the error a late-bound caller +' sees when the member it calls raises one. Thing's members each raise error 380 +' (Invalid property value) a different way. Called early-bound, every one raises +' 380; called late-bound, VB6 6.0 raises 380 for every one (the VB6 project beside +' this one prints them). In BETA 1005 a member that raises with a constant number +' and nothing else reaches the late-bound caller as &H80020009 (DISP_E_EXCEPTION, +' "Exception occurred."). + +Class Thing + Private mValue As Long + + Public Sub RaiseConstant() + Err.Raise 380 + End Sub + + Public Sub RaiseVariable(ByVal Number As Long) + Err.Raise Number + End Sub + + Public Sub RaiseConstantWithSource() + Err.Raise 380, "Thing" + End Sub + + Public Sub RaiseConstantWithDescription() + Err.Raise 380, "Thing", "the value is out of range" + End Sub + + Public Sub ErrorStatement() + Error 380 + End Sub + + Public Property Get Value() As Long + Return mValue + End Property + + Public Property Let Value(ByVal NewValue As Long) + If NewValue < 0 Then Err.Raise 380 + mValue = NewValue + End Property +End Class + +[TestFixture] +Module RaiseThroughLateBindingTests + + ' Fixed in #2490 (BETA 1005): a name the object does not have is error 438. + + [TestCase] + Public Sub UnknownMember_Raises438() + Dim o As Object = New Thing + On Error Resume Next + o.Nope + Assert.Strict.AreEqual 438, Err.Number ' VB6: 438 + End Sub + + ' Already as in VB6 in BETA 1005. + + [TestCase] + Public Sub EarlyBound_RaiseConstant_Raises380() + Dim t As Thing = New Thing + On Error Resume Next + t.RaiseConstant + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_RaiseVariable_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.RaiseVariable 380 + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + ' Left in BETA 1005: &H80020009 in place of 380. + + [TestCase] + Public Sub LateBound_RaiseConstantWithSource_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.RaiseConstantWithSource + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_RaiseConstantWithDescription_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.RaiseConstantWithDescription + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_RaiseConstant_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.RaiseConstant + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_ErrorStatement_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.ErrorStatement + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_PropertyLet_Raises380() + Dim o As Object = New Thing + On Error Resume Next + o.Value = -1 + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub CallByName_RaiseConstant_Raises380() + Dim o As Object = New Thing + On Error Resume Next + CallByName o, "RaiseConstant", VbMethod + Assert.Strict.AreEqual 380, Err.Number ' VB6: 380 + End Sub + + [TestCase] + Public Sub LateBound_FormColour_Raises380() + ' The VB package's own colour properties raise 380 the same way (#2484). + Dim o As Object = New PictureBox + On Error Resume Next + o.BackColor = -1 + Assert.Strict.AreEqual 380, Err.Number ' early-bound: 380 + End Sub + +End Module diff --git a/bugs/latebound-constant-raise-exception/src/Sources/Startup.twin b/bugs/latebound-constant-raise-exception/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/latebound-constant-raise-exception/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/latebound-constant-raise-exception/vb6/Module1.bas b/bugs/latebound-constant-raise-exception/vb6/Module1.bas new file mode 100644 index 000000000..96feb7d47 --- /dev/null +++ b/bugs/latebound-constant-raise-exception/vb6/Module1.bas @@ -0,0 +1,46 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The VB6 side of latebound-constant-raise-exception: the error a late-bound caller +' sees when the member raises error 380, each way Thing raises it. +' Build and run it with: node scripts/bug_repro.mjs vb6 latebound-constant-raise-exception +' Probe.vbp builds Probe.exe, which writes what it finds to out.txt beside the exe. + +Private Sub Show(ByVal What As String) + Print #9, What & " -> " & Err.Number & " [" & Err.Description & "]" + Err.Clear +End Sub + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Dim t As Thing, o As Object + Set t = New Thing + Set o = t + On Error Resume Next + o.Nope + Show "late-bound Nope" + t.RaiseConstant + Show "early-bound RaiseConstant" + o.RaiseVariable 380 + Show "late-bound RaiseVariable 380" + o.RaiseConstantWithSource + Show "late-bound RaiseConstantWithSource" + o.RaiseConstantWithDescription + Show "late-bound RaiseConstantWithDescription" + o.RaiseConstant + Show "late-bound RaiseConstant" + o.ErrorStatement + Show "late-bound ErrorStatement" + o.Value = -1 + Show "late-bound Value = -1" + CallByName o, "RaiseConstant", VbMethod + Show "CallByName RaiseConstant" + On Error GoTo Fail + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub diff --git a/bugs/filed/advise-unadvise-hresults/vb6/Probe.vbp b/bugs/latebound-constant-raise-exception/vb6/Probe.vbp similarity index 67% rename from bugs/filed/advise-unadvise-hresults/vb6/Probe.vbp rename to bugs/latebound-constant-raise-exception/vb6/Probe.vbp index 1fee81151..78062a9ea 100644 --- a/bugs/filed/advise-unadvise-hresults/vb6/Probe.vbp +++ b/bugs/latebound-constant-raise-exception/vb6/Probe.vbp @@ -1,7 +1,6 @@ Type=Exe Module=Module1; Module1.bas -Class=Source; Source.cls -Class=NotASink; NotASink.cls +Class=Thing; Thing.cls Startup="Sub Main" ExeName32="Probe.exe" Name="Probe" diff --git a/bugs/latebound-constant-raise-exception/vb6/Thing.cls b/bugs/latebound-constant-raise-exception/vb6/Thing.cls new file mode 100644 index 000000000..3cb498cab --- /dev/null +++ b/bugs/latebound-constant-raise-exception/vb6/Thing.cls @@ -0,0 +1,45 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Thing" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = False +Option Explicit + +Private mValue As Long + +Public Sub RaiseConstant() + Err.Raise 380 +End Sub + +Public Sub RaiseVariable(ByVal Number As Long) + Err.Raise Number +End Sub + +Public Sub RaiseConstantWithSource() + Err.Raise 380, "Thing" +End Sub + +Public Sub RaiseConstantWithDescription() + Err.Raise 380, "Thing", "the value is out of range" +End Sub + +Public Sub ErrorStatement() + Error 380 +End Sub + +Public Property Get Value() As Long + Value = mValue +End Property + +Public Property Let Value(ByVal NewValue As Long) + If NewValue < 0 Then Err.Raise 380 + mValue = NewValue +End Property diff --git a/bugs/latebound-hresult-mapping/latebound-hresult-mapping.twinproj b/bugs/latebound-hresult-mapping/latebound-hresult-mapping.twinproj new file mode 100644 index 000000000..a42335059 Binary files /dev/null and b/bugs/latebound-hresult-mapping/latebound-hresult-mapping.twinproj differ diff --git a/bugs/latebound-hresult-mapping/repro.json b/bugs/latebound-hresult-mapping/repro.json new file mode 100644 index 000000000..22c85af28 --- /dev/null +++ b/bugs/latebound-hresult-mapping/repro.json @@ -0,0 +1,25 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS LateBoundErrorTests\\.UnknownMember_Class_Raises438$", + "^PASS LateBoundErrorTests\\.UnknownMember_Collection_Raises438$", + "^PASS LateBoundErrorTests\\.UnknownMember_CallByName_Raises438$", + "^PASS LateBoundErrorTests\\.MemberNotFound_Raises438$", + "^PASS LateBoundErrorTests\\.TypeMismatch_Raises13$", + "^PASS LateBoundErrorTests\\.InvalidArg_Raises5$", + "^FAIL LateBoundErrorTests\\.BadParamCount_Collection_Raises450: .*Actual: -2147352562 ", + "^FAIL LateBoundErrorTests\\.BadParamCount_Raises450: .*Actual: -2147352562 ", + "^FAIL LateBoundErrorTests\\.ParamNotOptional_Raises449: .*Actual: -2147352561 ", + "^FAIL LateBoundErrorTests\\.NotImpl_Raises445: .*Actual: -2147467263 ", + "^FAIL LateBoundErrorTests\\.OutOfMemory_Raises7: .*Actual: -2147024882 ", + "^FAIL LateBoundErrorTests\\.BadIndex_Raises9: .*Actual: -2147352565 ", + "^FAIL LateBoundErrorTests\\.NoNamedArgs_Raises446: .*Actual: -2147352569 ", + "^FAIL LateBoundErrorTests\\.ArrayIsLocked_Raises10: .*Actual: -2147352563 ", + "^FAIL LateBoundErrorTests\\.NoInterface_Raises430: .*Actual: -2147467262 ", + "^FAIL LateBoundErrorTests\\.AccessDenied_Raises70: .*Actual: -2147024891 ", + "^FAIL LateBoundErrorTests\\.Abort_Raises287: .*Actual: -2147467260 " + ] + } +} diff --git a/bugs/latebound-hresult-mapping/src/Settings b/bugs/latebound-hresult-mapping/src/Settings new file mode 100644 index 000000000..fe9bc7e06 --- /dev/null +++ b/bugs/latebound-hresult-mapping/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "LateboundHresultMapping", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: A failure code from a late-bound call is raised as the HRESULT, where VB6 raises its own error number, such as 450 for DISP_E_BADPARAMCOUNT", + "project.exportPathIsV2": true, + "project.id": "{9AB7BF99-42F6-47BD-98C7-9BD693CC02A5}", + "project.name": "LateboundHresultMapping", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/latebound-hresult-mapping/src/Sources/LateBoundErrorTests.twin b/bugs/latebound-hresult-mapping/src/Sources/LateBoundErrorTests.twin new file mode 100644 index 000000000..ca3356d4d --- /dev/null +++ b/bugs/latebound-hresult-mapping/src/Sources/LateBoundErrorTests.twin @@ -0,0 +1,230 @@ +' Regression tests for #2490 and its follow-up: the error number a late-bound +' call raises when the object fails it. The expected numbers are what VB6 6.0 +' raises for the same failure (the VB6 project beside this one prints them). +' +' HResultObject answers any name of the form Hr<hex> and fails the call with that +' HRESULT, so o.Hr8002000E is a late-bound call whose Invoke returns +' DISP_E_BADPARAMCOUNT. + +Module DispatchTypes + Public Type GUID + Data1 As Long + Data2 As Integer + Data3 As Integer + Data4(0 To 7) As Byte + End Type + + Public Type DISPPARAMS + rgvarg As LongPtr + rgdispidNamedArgs As LongPtr + cArgs As Long + cNamedArgs As Long + End Type + + Public Type EXCEPINFO + wCode As Integer + wReserved As Integer + bstrSource As LongPtr + bstrDescription As LongPtr + bstrHelpFile As LongPtr + dwHelpContext As Long + pvReserved As LongPtr + pfnDeferredFillIn As LongPtr + scode As Long + End Type + + Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _ + ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As LongPtr) + Private Declare PtrSafe Function lstrlenW Lib "kernel32" (ByVal lpString As LongPtr) As Long + + Public Function PtrToString(ByVal p As LongPtr) As String + Dim n As Long = lstrlenW(p) + Dim s As String = Space$(n) + If n > 0 Then CopyMemory StrPtr(s), p, n * 2 + Return s + End Function +End Module + +[InterfaceId("00020400-0000-0000-C000-000000000046")] +Interface IDispatchOwn Extends stdole.IUnknown + Sub GetTypeInfoCount(ByRef pctinfo As Long) + Sub GetTypeInfo(ByVal iTInfo As Long, ByVal lcid As Long, ByRef ppTInfo As LongPtr) + Sub GetIDsOfNames(ByRef riid As GUID, ByRef rgszNames As LongPtr, ByVal cNames As Long, ByVal lcid As Long, ByRef rgDispId As Long) + Sub Invoke(ByVal dispIdMember As Long, ByRef riid As GUID, ByVal lcid As Long, ByVal wFlags As Integer, ByRef pDispParams As DISPPARAMS, ByRef pVarResult As Variant, ByRef pExcepInfo As EXCEPINFO, ByRef puArgErr As Long) +End Interface + +' Dispatched through its own IDispatchOwn methods, since it is NotDispatchable. +NotDispatchable Class HResultObject + Implements IDispatchOwn + + Private Sub GetTypeInfoCount(ByRef pctinfo As Long) Implements IDispatchOwn.GetTypeInfoCount + pctinfo = 0 + End Sub + + Private Sub GetTypeInfo(ByVal iTInfo As Long, ByVal lcid As Long, ByRef ppTInfo As LongPtr) Implements IDispatchOwn.GetTypeInfo + Err.ReturnHResult = &H80004001 ' E_NOTIMPL + End Sub + + ' "Hr8002000E" gets the DISPID &H8002000E. + Private Sub GetIDsOfNames(ByRef riid As GUID, ByRef rgszNames As LongPtr, ByVal cNames As Long, ByVal lcid As Long, ByRef rgDispId As Long) Implements IDispatchOwn.GetIDsOfNames + rgDispId = CLng("&H" & Mid$(PtrToString(rgszNames), 3)) + End Sub + + ' Fails with the HRESULT that is the DISPID. + Private Sub Invoke(ByVal dispIdMember As Long, ByRef riid As GUID, ByVal lcid As Long, ByVal wFlags As Integer, ByRef pDispParams As DISPPARAMS, ByRef pVarResult As Variant, ByRef pExcepInfo As EXCEPINFO, ByRef puArgErr As Long) Implements IDispatchOwn.Invoke + Err.ReturnHResult = dispIdMember + End Sub +End Class + +Class Widget + Public Sub Hello() + End Sub +End Class + +[TestFixture] +Module LateBoundErrorTests + + ' Fixed in #2490 (BETA 1005): a name the object does not have is error 438. + + [TestCase] + Public Sub UnknownMember_Class_Raises438() + Dim o As Object = New Widget + On Error Resume Next + o.Nope + Assert.Strict.AreEqual 438, Err.Number ' VB6: 438 + End Sub + + [TestCase] + Public Sub UnknownMember_Collection_Raises438() + Dim o As Object = New Collection + On Error Resume Next + o.Nope + Assert.Strict.AreEqual 438, Err.Number ' VB6: 438 + End Sub + + [TestCase] + Public Sub UnknownMember_CallByName_Raises438() + Dim o As Object = New Widget + On Error Resume Next + CallByName o, "Nope", VbMethod + Assert.Strict.AreEqual 438, Err.Number ' VB6: 438 + End Sub + + ' Already as in VB6 in BETA 1005: these failure codes are raised as VB6 raises them. + + [TestCase] + Public Sub MemberNotFound_Raises438() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80020003 ' DISP_E_MEMBERNOTFOUND + Assert.Strict.AreEqual 438, Err.Number ' VB6: 438 + End Sub + + [TestCase] + Public Sub TypeMismatch_Raises13() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80020005 ' DISP_E_TYPEMISMATCH + Assert.Strict.AreEqual 13, Err.Number ' VB6: 13 + End Sub + + [TestCase] + Public Sub InvalidArg_Raises5() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80070057 ' E_INVALIDARG + Assert.Strict.AreEqual 5, Err.Number ' VB6: 5 + End Sub + + ' Left in BETA 1005: these are raised as the HRESULT itself. + + [TestCase] + Public Sub BadParamCount_Collection_Raises450() + Dim o As Object = New Collection + Dim x As Variant + On Error Resume Next + x = o.Item(1, 2) ' Item takes one argument + Assert.Strict.AreEqual 450, Err.Number ' VB6: 450, Wrong number of arguments or invalid property assignment + End Sub + + [TestCase] + Public Sub BadParamCount_Raises450() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr8002000E ' DISP_E_BADPARAMCOUNT + Assert.Strict.AreEqual 450, Err.Number ' VB6: 450, Wrong number of arguments or invalid property assignment + End Sub + + [TestCase] + Public Sub ParamNotOptional_Raises449() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr8002000F ' DISP_E_PARAMNOTOPTIONAL + Assert.Strict.AreEqual 449, Err.Number ' VB6: 449, Argument not optional + End Sub + + [TestCase] + Public Sub NotImpl_Raises445() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80004001 ' E_NOTIMPL + Assert.Strict.AreEqual 445, Err.Number ' VB6: 445, Object doesn't support this action + End Sub + + [TestCase] + Public Sub OutOfMemory_Raises7() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr8007000E ' E_OUTOFMEMORY + Assert.Strict.AreEqual 7, Err.Number ' VB6: 7, Out of memory + End Sub + + [TestCase] + Public Sub BadIndex_Raises9() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr8002000B ' DISP_E_BADINDEX + Assert.Strict.AreEqual 9, Err.Number ' VB6: 9, Subscript out of range + End Sub + + [TestCase] + Public Sub NoNamedArgs_Raises446() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80020007 ' DISP_E_NONAMEDARGS + Assert.Strict.AreEqual 446, Err.Number ' VB6: 446, Object doesn't support named arguments + End Sub + + [TestCase] + Public Sub ArrayIsLocked_Raises10() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr8002000D ' DISP_E_ARRAYISLOCKED + Assert.Strict.AreEqual 10, Err.Number ' VB6: 10, This array is fixed or temporarily locked + End Sub + + [TestCase] + Public Sub NoInterface_Raises430() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80004002 ' E_NOINTERFACE + Assert.Strict.AreEqual 430, Err.Number ' VB6: 430, Class does not support Automation or does not support expected interface + End Sub + + [TestCase] + Public Sub AccessDenied_Raises70() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80070005 ' E_ACCESSDENIED + Assert.Strict.AreEqual 70, Err.Number ' VB6: 70, Permission denied + End Sub + + [TestCase] + Public Sub Abort_Raises287() + Dim o As Object = New HResultObject + On Error Resume Next + o.Hr80004004 ' E_ABORT + Assert.Strict.AreEqual 287, Err.Number ' VB6: 287, Application-defined or object-defined error + End Sub + +End Module diff --git a/bugs/latebound-hresult-mapping/src/Sources/Startup.twin b/bugs/latebound-hresult-mapping/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/latebound-hresult-mapping/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/latebound-hresult-mapping/vb6/Module1.bas b/bugs/latebound-hresult-mapping/vb6/Module1.bas new file mode 100644 index 000000000..5703b6169 --- /dev/null +++ b/bugs/latebound-hresult-mapping/vb6/Module1.bas @@ -0,0 +1,247 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The VB6 side of latebound-hresult-mapping: the error number VB6 raises when a +' late-bound call fails. Build and run it with: +' node scripts/bug_repro.mjs vb6 latebound-hresult-mapping +' A lightweight COM object whose IDispatch::Invoke returns a chosen HRESULT: the +' name Cn fails with code n of the table in InitCodes. +' Memory block gObj: element 0 = pointer to the vtable, element 1 = reference count. + +Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long) +Private Declare Function lstrlenW Lib "kernel32.dll" (ByVal lpString As Long) As Long +Private Declare Function SysAllocString Lib "oleaut32.dll" (ByVal psz As Long) As Long + +Private Const NCODES As Long = 24 +Private gVtbl(0 To 6) As Long +Private gObj(0 To 1) As Long +Private gCodes(1 To NCODES) As Long +Private gNames(1 To NCODES) As String +Private gCalls(0 To NCODES) As Long +Private gFlags(0 To NCODES) As Long +Private gArgs(0 To NCODES) As Long +Private gHasExc(0 To NCODES) As Long +Private gGetIds As Long + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Cases + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub + +Private Sub SetCode(ByVal n As Long, ByVal code As Long, ByVal nm As String) + gCodes(n) = code + gNames(n) = nm +End Sub + +Private Sub InitCodes() + SetCode 1, &H80004001, "E_NOTIMPL" + SetCode 2, &H8007000E, "E_OUTOFMEMORY" + SetCode 3, &H80070057, "E_INVALIDARG" + SetCode 4, &H8002000E, "DISP_E_BADPARAMCOUNT" + SetCode 5, &H8002000F, "DISP_E_PARAMNOTOPTIONAL" + SetCode 6, &H8002000A, "DISP_E_OVERFLOW" + SetCode 7, &H80020005, "DISP_E_TYPEMISMATCH" + SetCode 8, &H8002000B, "DISP_E_BADINDEX" + SetCode 9, &H80020012, "DISP_E_DIVBYZERO" + SetCode 10, &H80020007, "DISP_E_NONAMEDARGS" + SetCode 11, &H80020008, "DISP_E_BADVARTYPE" + SetCode 12, &H80020004, "DISP_E_PARAMNOTFOUND" + SetCode 13, &H8002000D, "DISP_E_ARRAYISLOCKED" + SetCode 14, &H80004002, "E_NOINTERFACE" + SetCode 15, &H80070005, "E_ACCESSDENIED" + SetCode 16, &H80004003, "E_POINTER" + SetCode 17, &H80004004, "E_ABORT" + SetCode 18, &H80004005, "E_FAIL" + SetCode 19, &H8000FFFF, "E_UNEXPECTED" + SetCode 20, &H80020003, "DISP_E_MEMBERNOTFOUND" + SetCode 21, &H800A01A8, "800A01A8" + SetCode 22, &H8007000D, "HRESULT_FROM_WIN32(ERROR_INVALID_DATA) 8007000D" + SetCode 23, &H80020009, "DISP_E_EXCEPTION (excepinfo scode 80041234, wCode 0, description given)" + SetCode 24, &H80020009, "DISP_E_EXCEPTION (excepinfo scode 0, wCode 1234, description given)" +End Sub + +Private Function Fn(ByVal p As Long) As Long + Fn = p +End Function + +Private Sub InitObject() + gVtbl(0) = Fn(AddressOf Obj_QueryInterface) + gVtbl(1) = Fn(AddressOf Obj_AddRef) + gVtbl(2) = Fn(AddressOf Obj_Release) + gVtbl(3) = Fn(AddressOf Obj_GetTypeInfoCount) + gVtbl(4) = Fn(AddressOf Obj_GetTypeInfo) + gVtbl(5) = Fn(AddressOf Obj_GetIDsOfNames) + gVtbl(6) = Fn(AddressOf Obj_Invoke) + gObj(0) = VarPtr(gVtbl(0)) + gObj(1) = 1 +End Sub + +Public Function Obj_QueryInterface(ByVal pThis As Long, ByVal riid As Long, ByVal ppv As Long) As Long + Dim d1 As Long, d2 As Integer, d3 As Integer, b(0 To 7) As Byte + Dim isUnk As Boolean, isDisp As Boolean + If riid = 0 Or ppv = 0 Then + Obj_QueryInterface = &H80004003 + Exit Function + End If + CopyMemory d1, ByVal riid, 4 + CopyMemory d2, ByVal (riid + 4), 2 + CopyMemory d3, ByVal (riid + 6), 2 + CopyMemory b(0), ByVal (riid + 8), 8 + isUnk = (d1 = 0 And d2 = 0 And d3 = 0 And b(0) = &HC0 And b(7) = &H46 And b(1) = 0 And b(6) = 0) + isDisp = (d1 = &H20400 And d2 = 0 And d3 = 0 And b(0) = &HC0 And b(7) = &H46 And b(1) = 0 And b(6) = 0) + If isUnk Or isDisp Then + gObj(1) = gObj(1) + 1 + CopyMemory ByVal ppv, pThis, 4 + Obj_QueryInterface = 0 + Else + CopyMemory ByVal ppv, 0&, 4 + Obj_QueryInterface = &H80004002 + End If +End Function + +Public Function Obj_AddRef(ByVal pThis As Long) As Long + gObj(1) = gObj(1) + 1 + Obj_AddRef = gObj(1) +End Function + +Public Function Obj_Release(ByVal pThis As Long) As Long + gObj(1) = gObj(1) - 1 + Obj_Release = gObj(1) +End Function + +Public Function Obj_GetTypeInfoCount(ByVal pThis As Long, ByVal pctinfo As Long) As Long + If pctinfo <> 0 Then CopyMemory ByVal pctinfo, 0&, 4 + Obj_GetTypeInfoCount = 0 +End Function + +Public Function Obj_GetTypeInfo(ByVal pThis As Long, ByVal iTInfo As Long, ByVal lcid As Long, ByVal ppTInfo As Long) As Long + Obj_GetTypeInfo = &H80004001 +End Function + +Public Function Obj_GetIDsOfNames(ByVal pThis As Long, ByVal riid As Long, ByVal rgszNames As Long, ByVal cNames As Long, ByVal lcid As Long, ByVal rgDispId As Long) As Long + Dim pName As Long, n As Long, s As String, num As Long + gGetIds = gGetIds + 1 + CopyMemory pName, ByVal rgszNames, 4 + n = lstrlenW(pName) + s = String$(n, vbNullChar) + If n > 0 Then CopyMemory ByVal StrPtr(s), ByVal pName, n * 2 + If UCase$(Left$(s, 1)) = "C" And IsNumeric(Mid$(s, 2)) Then + num = CLng(Mid$(s, 2)) + CopyMemory ByVal rgDispId, num, 4 + Obj_GetIDsOfNames = 0 + Else + num = -1 + CopyMemory ByVal rgDispId, num, 4 + Obj_GetIDsOfNames = &H80020006 + End If +End Function + +Public Function Obj_Invoke(ByVal pThis As Long, ByVal dispIdMember As Long, ByVal riid As Long, ByVal lcid As Long, ByVal wFlags As Long, ByVal pDispParams As Long, ByVal pVarResult As Long, ByVal pExcepInfo As Long, ByVal puArgErr As Long) As Long + Dim cArgs As Long, bstr As Long, sc As Long, wc As Integer + If dispIdMember >= 1 And dispIdMember <= NCODES Then + gCalls(dispIdMember) = gCalls(dispIdMember) + 1 + gFlags(dispIdMember) = wFlags And &HFFFF& + If pDispParams <> 0 Then CopyMemory cArgs, ByVal (pDispParams + 8), 4 + gArgs(dispIdMember) = cArgs + gHasExc(dispIdMember) = IIf(pExcepInfo <> 0, 1, 0) + If gCodes(dispIdMember) = &H80020009 And pExcepInfo <> 0 Then + If dispIdMember = 23 Then + wc = 0: sc = &H80041234 + Else + wc = 1234: sc = 0 + End If + ' the caller need not have zeroed it: fill all 32 bytes + Dim z(0 To 31) As Byte + CopyMemory ByVal pExcepInfo, z(0), 32 + CopyMemory ByVal pExcepInfo, wc, 2 + bstr = SysAllocString(StrPtr("Source from excepinfo")) + CopyMemory ByVal (pExcepInfo + 4), bstr, 4 + bstr = SysAllocString(StrPtr("Description from excepinfo")) + CopyMemory ByVal (pExcepInfo + 8), bstr, 4 + CopyMemory ByVal (pExcepInfo + 28), sc, 4 + End If + Obj_Invoke = gCodes(dispIdMember) + Else + Obj_Invoke = &H80020003 + End If +End Function + +Private Sub Rec(ByVal n As Long, ByVal way As String, ByVal callsBefore As Long) + Dim num As Long, desc As String, src As String + num = Err.Number: desc = Err.Description: src = Err.Source + Print #9, gNames(n) & " (" & Hex$(gCodes(n)) & ") " & way & " -> " & num & " [" & desc & "] invokeCalls=" & (gCalls(n) - callsBefore) + Err.Clear +End Sub + +Private Sub Show(ByVal what As String) + Print #9, what & " -> " & Err.Number & " [" & Err.Description & "]" + Err.Clear +End Sub + +Sub Cases() + Dim o As Object, p As Long, n As Long, before As Long + InitCodes + InitObject + p = VarPtr(gObj(0)) + gObj(1) = gObj(1) + 1 + CopyMemory o, p, 4 + Print #9, "object ptr ok: " & (Not (o Is Nothing)) + + For n = 1 To NCODES + On Error Resume Next + Err.Clear + before = gCalls(n) + Select Case n + Case 1: o.C1 + Case 2: o.C2 + Case 3: o.C3 + Case 4: o.C4 + Case 5: o.C5 + Case 6: o.C6 + Case 7: o.C7 + Case 8: o.C8 + Case 9: o.C9 + Case 10: o.C10 + Case 11: o.C11 + Case 12: o.C12 + Case 13: o.C13 + Case 14: o.C14 + Case 15: o.C15 + Case 16: o.C16 + Case 17: o.C17 + Case 18: o.C18 + Case 19: o.C19 + Case 20: o.C20 + Case 21: o.C21 + Case 22: o.C22 + Case 23: o.C23 + Case 24: o.C24 + End Select + Rec n, "statement", before + On Error GoTo 0 + Next + + CopyMemory o, 0&, 4 + + ' Ordinary objects: a name they do not have, and a wrong number of arguments. + Dim w As Object, c As Object, x As Variant + Set w = New Widget + Set c = New Collection + On Error Resume Next + w.Nope + Show "Widget.Nope" + c.Nope + Show "Collection.Nope" + CallByName w, "Nope", VbMethod + Show "CallByName Widget, Nope" + x = c.Item(1, 2) + Show "x = Collection.Item(1, 2)" + On Error GoTo 0 +End Sub diff --git a/bugs/filed/gettypeinfo-badindex/vb6/Probe.vbp b/bugs/latebound-hresult-mapping/vb6/Probe.vbp similarity index 100% rename from bugs/filed/gettypeinfo-badindex/vb6/Probe.vbp rename to bugs/latebound-hresult-mapping/vb6/Probe.vbp diff --git a/bugs/filed/gettypeinfo-badindex/vb6/Widget.cls b/bugs/latebound-hresult-mapping/vb6/Widget.cls similarity index 74% rename from bugs/filed/gettypeinfo-badindex/vb6/Widget.cls rename to bugs/latebound-hresult-mapping/vb6/Widget.cls index 03afe78f1..c347c7b03 100644 --- a/bugs/filed/gettypeinfo-badindex/vb6/Widget.cls +++ b/bugs/latebound-hresult-mapping/vb6/Widget.cls @@ -15,3 +15,10 @@ Option Explicit Public Sub Hello() End Sub + +Public Sub Two(ByVal a As Long, ByVal b As Long) +End Sub + +Public Property Get RO() As Long + RO = 7 +End Property diff --git a/bugs/llvm-error-not-passed-to-caller/llvm-error-not-passed-to-caller.twinproj b/bugs/llvm-error-not-passed-to-caller/llvm-error-not-passed-to-caller.twinproj new file mode 100644 index 000000000..c889a2b95 Binary files /dev/null and b/bugs/llvm-error-not-passed-to-caller/llvm-error-not-passed-to-caller.twinproj differ diff --git a/bugs/llvm-error-not-passed-to-caller/repro.json b/bugs/llvm-error-not-passed-to-caller/repro.json new file mode 100644 index 000000000..52e6e7a9f --- /dev/null +++ b/bugs/llvm-error-not-passed-to-caller/repro.json @@ -0,0 +1,16 @@ +{ + "mode": "run", + "expect": { + "exit": 5, + "output": [ + "^CallerPlain_CalleePlain_ErrRaise\\r?\\n caught 5000$", + "^CallerPlain_CalleePlain_Divide\\r?\\n caught 11$", + "^Llvm_OwnHandler_Divide\\r?\\n caught 11$", + "^CallerPlain_CalleeLlvm_ErrRaise$" + ], + "absent": [ + "^CallerPlain_CalleeLlvm_ErrRaise\\r?\\n ", + "^end of Main$" + ] + } +} diff --git a/bugs/filed/array-const-element-overflow-crash/src/Settings b/bugs/llvm-error-not-passed-to-caller/src/Settings similarity index 82% rename from bugs/filed/array-const-element-overflow-crash/src/Settings rename to bugs/llvm-error-not-passed-to-caller/src/Settings index 9f9b64492..24665882d 100644 --- a/bugs/filed/array-const-element-overflow-crash/src/Settings +++ b/bugs/llvm-error-not-passed-to-caller/src/Settings @@ -1,12 +1,12 @@ { "configuration.inherits": "Defaults", - "project.appTitle": "ArrayConstElementOverflowCrash", + "project.appTitle": "LlvmErrorNotPassedToCaller", "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", "project.buildType": "Standard EXE", - "project.description": "Reproduces: A module-level array Const with an element that does not fit its type crashes the compiler", + "project.description": "Reproduces: An error is not passed to the caller's `On Error` handler when the caller or the procedure that raised it is compiled with LLVM", "project.exportPathIsV2": true, - "project.id": "{4401A4EA-F2FC-4E51-B2B1-5E7F888EC3AF}", - "project.name": "ArrayConstElementOverflowCrash", + "project.id": "{4A215D60-0A6F-4DE1-8C81-B84E9F7EF1B6}", + "project.name": "LlvmErrorNotPassedToCaller", "project.optionExplicit": true, "project.references": [ { diff --git a/bugs/llvm-error-not-passed-to-caller/src/Sources/Cases.twin b/bugs/llvm-error-not-passed-to-caller/src/Sources/Cases.twin new file mode 100644 index 000000000..83a930b70 --- /dev/null +++ b/bugs/llvm-error-not-passed-to-caller/src/Sources/Cases.twin @@ -0,0 +1,148 @@ +' Each case calls a procedure that raises an error and has no error handler, from a procedure that has +' one, and returns what that handler caught. [CompilerOptions("+llvm")] compiles a procedure with LLVM; +' the VB6 project in vb6/ has the same procedures without it. +Module Cases + + ' --- The callees: none of them has an error handler ---------------------------------------- + + Private Sub ErrRaise_Plain() + Err.Raise 5000, "Callee", "raised by Err.Raise" + End Sub + + [CompilerOptions("+llvm")] + Private Sub ErrRaise_Llvm() + Err.Raise 5000, "Callee", "raised by Err.Raise" + End Sub + + Private Sub Divide_Plain(ByVal divisor As Long) + Dim x As Long + x = 1 \ divisor + End Sub + + [CompilerOptions("+llvm")] + Private Sub Divide_Llvm(ByVal divisor As Long) + Dim x As Long + x = 1 \ divisor + End Sub + + Private Sub Middle_Plain() + ErrRaise_Llvm + End Sub + + ' --- The callers: each has an On Error GoTo handler and returns what it caught ------------- + + Public Function CallerPlain_CalleePlain_ErrRaise() As String + On Error GoTo Handler + ErrRaise_Plain + CallerPlain_CalleePlain_ErrRaise = "no error" + Exit Function + Handler: + CallerPlain_CalleePlain_ErrRaise = "caught " & Err.Number + End Function + + Public Function CallerPlain_CalleeLlvm_ErrRaise() As String + On Error GoTo Handler + ErrRaise_Llvm + CallerPlain_CalleeLlvm_ErrRaise = "no error" + Exit Function + Handler: + CallerPlain_CalleeLlvm_ErrRaise = "caught " & Err.Number + End Function + + [CompilerOptions("+llvm")] + Public Function CallerLlvm_CalleePlain_ErrRaise() As String + On Error GoTo Handler + ErrRaise_Plain + CallerLlvm_CalleePlain_ErrRaise = "no error" + Exit Function + Handler: + CallerLlvm_CalleePlain_ErrRaise = "caught " & Err.Number + End Function + + [CompilerOptions("+llvm")] + Public Function CallerLlvm_CalleeLlvm_ErrRaise() As String + On Error GoTo Handler + ErrRaise_Llvm + CallerLlvm_CalleeLlvm_ErrRaise = "no error" + Exit Function + Handler: + CallerLlvm_CalleeLlvm_ErrRaise = "caught " & Err.Number + End Function + + Public Function CallerPlain_CalleePlain_Divide() As String + On Error GoTo Handler + Divide_Plain 0 + CallerPlain_CalleePlain_Divide = "no error" + Exit Function + Handler: + CallerPlain_CalleePlain_Divide = "caught " & Err.Number + End Function + + Public Function CallerPlain_CalleeLlvm_Divide() As String + On Error GoTo Handler + Divide_Llvm 0 + CallerPlain_CalleeLlvm_Divide = "no error" + Exit Function + Handler: + CallerPlain_CalleeLlvm_Divide = "caught " & Err.Number + End Function + + [CompilerOptions("+llvm")] + Public Function CallerLlvm_CalleePlain_Divide() As String + On Error GoTo Handler + Divide_Plain 0 + CallerLlvm_CalleePlain_Divide = "no error" + Exit Function + Handler: + CallerLlvm_CalleePlain_Divide = "caught " & Err.Number + End Function + + [CompilerOptions("+llvm")] + Public Function CallerLlvm_CalleeLlvm_Divide() As String + On Error GoTo Handler + Divide_Llvm 0 + CallerLlvm_CalleeLlvm_Divide = "no error" + Exit Function + Handler: + CallerLlvm_CalleeLlvm_Divide = "caught " & Err.Number + End Function + + ' --- The same with On Error Resume Next, and with a procedure between caller and callee ------ + + Public Function CallerPlain_CalleeLlvm_ResumeNext() As String + On Error Resume Next + ErrRaise_Llvm + CallerPlain_CalleeLlvm_ResumeNext = "continued, Err.Number " & Err.Number + End Function + + [CompilerOptions("+llvm")] + Public Function CallerLlvm_CalleePlain_ResumeNext() As String + On Error Resume Next + ErrRaise_Plain + CallerLlvm_CalleePlain_ResumeNext = "continued, Err.Number " & Err.Number + End Function + + Public Function CallerPlain_MiddlePlain_CalleeLlvm() As String + On Error GoTo Handler + Middle_Plain + CallerPlain_MiddlePlain_CalleeLlvm = "no error" + Exit Function + Handler: + CallerPlain_MiddlePlain_CalleeLlvm = "caught " & Err.Number + End Function + + ' --- A procedure compiled with LLVM that handles its own error ------------------------------ + + [CompilerOptions("+llvm")] + Public Function Llvm_OwnHandler_Divide() As String + On Error GoTo Handler + Dim divisor As Long + Dim x As Long + x = 1 \ divisor + Llvm_OwnHandler_Divide = "no error" + Exit Function + Handler: + Llvm_OwnHandler_Divide = "caught " & Err.Number + End Function + +End Module diff --git a/bugs/llvm-error-not-passed-to-caller/src/Sources/Startup.twin b/bugs/llvm-error-not-passed-to-caller/src/Sources/Startup.twin new file mode 100644 index 000000000..df4f27e24 --- /dev/null +++ b/bugs/llvm-error-not-passed-to-caller/src/Sources/Startup.twin @@ -0,0 +1,31 @@ +' Runs the cases of Cases in order, writing each one's name and then what its handler caught. In a build +' that has the defect the run ends silently at the first case in which a procedure compiled with LLVM +' raises the error or catches it: the last line in the DEBUG CONSOLE is then that case's name. +Module Startup + + ' The first case to run. Set it to 5, 6 and so on to see the cases after the first one that ends the run. + Private Const FirstCase As Long = 1 + + Public Sub Main() + Debug.Cls + Dim n As Long + For n = FirstCase To 12 + Select Case n + Case 1: Debug.Print "CallerPlain_CalleePlain_ErrRaise": Debug.Print " " & Cases.CallerPlain_CalleePlain_ErrRaise() + Case 2: Debug.Print "CallerPlain_CalleePlain_Divide": Debug.Print " " & Cases.CallerPlain_CalleePlain_Divide() + Case 3: Debug.Print "Llvm_OwnHandler_Divide": Debug.Print " " & Cases.Llvm_OwnHandler_Divide() + Case 4: Debug.Print "CallerPlain_CalleeLlvm_ErrRaise": Debug.Print " " & Cases.CallerPlain_CalleeLlvm_ErrRaise() + Case 5: Debug.Print "CallerLlvm_CalleePlain_ErrRaise": Debug.Print " " & Cases.CallerLlvm_CalleePlain_ErrRaise() + Case 6: Debug.Print "CallerLlvm_CalleeLlvm_ErrRaise": Debug.Print " " & Cases.CallerLlvm_CalleeLlvm_ErrRaise() + Case 7: Debug.Print "CallerPlain_CalleeLlvm_Divide": Debug.Print " " & Cases.CallerPlain_CalleeLlvm_Divide() + Case 8: Debug.Print "CallerLlvm_CalleePlain_Divide": Debug.Print " " & Cases.CallerLlvm_CalleePlain_Divide() + Case 9: Debug.Print "CallerLlvm_CalleeLlvm_Divide": Debug.Print " " & Cases.CallerLlvm_CalleeLlvm_Divide() + Case 10: Debug.Print "CallerPlain_CalleeLlvm_ResumeNext": Debug.Print " " & Cases.CallerPlain_CalleeLlvm_ResumeNext() + Case 11: Debug.Print "CallerLlvm_CalleePlain_ResumeNext": Debug.Print " " & Cases.CallerLlvm_CalleePlain_ResumeNext() + Case 12: Debug.Print "CallerPlain_MiddlePlain_CalleeLlvm": Debug.Print " " & Cases.CallerPlain_MiddlePlain_CalleeLlvm() + End Select + Next + Debug.Print "end of Main" + End Sub + +End Module diff --git a/bugs/llvm-error-not-passed-to-caller/vb6/Module1.bas b/bugs/llvm-error-not-passed-to-caller/vb6/Module1.bas new file mode 100644 index 000000000..52d6651df --- /dev/null +++ b/bugs/llvm-error-not-passed-to-caller/vb6/Module1.bas @@ -0,0 +1,168 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The VB6 side of bugs/llvm-error-not-passed-to-caller: the cases of src/Sources/Cases.twin, each +' printed with what its handler catches in VB6. VB6 has no LLVM, so the procedures that the twinBASIC +' project compiles with [CompilerOptions("+llvm")] are ordinary procedures here, and every case is caught. +' Build and run it with: node scripts/bug_repro.mjs vb6 llvm-error-not-passed-to-caller + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Show "CallerPlain_CalleePlain_ErrRaise", CallerPlain_CalleePlain_ErrRaise() + Show "CallerPlain_CalleePlain_Divide", CallerPlain_CalleePlain_Divide() + Show "Llvm_OwnHandler_Divide", Llvm_OwnHandler_Divide() + Show "CallerPlain_CalleeLlvm_ErrRaise", CallerPlain_CalleeLlvm_ErrRaise() + Show "CallerLlvm_CalleePlain_ErrRaise", CallerLlvm_CalleePlain_ErrRaise() + Show "CallerLlvm_CalleeLlvm_ErrRaise", CallerLlvm_CalleeLlvm_ErrRaise() + Show "CallerPlain_CalleeLlvm_Divide", CallerPlain_CalleeLlvm_Divide() + Show "CallerLlvm_CalleePlain_Divide", CallerLlvm_CalleePlain_Divide() + Show "CallerLlvm_CalleeLlvm_Divide", CallerLlvm_CalleeLlvm_Divide() + Show "CallerPlain_CalleeLlvm_ResumeNext", CallerPlain_CalleeLlvm_ResumeNext() + Show "CallerLlvm_CalleePlain_ResumeNext", CallerLlvm_CalleePlain_ResumeNext() + Show "CallerPlain_MiddlePlain_CalleeLlvm", CallerPlain_MiddlePlain_CalleeLlvm() + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub + +Private Sub Show(ByVal name As String, ByVal result As String) + Print #9, name & ": " & result +End Sub + +' --- The callees: none of them has an error handler ------------------------------------------ + +Private Sub ErrRaise_Plain() + Err.Raise 5000, "Callee", "raised by Err.Raise" +End Sub + +Private Sub ErrRaise_Llvm() + Err.Raise 5000, "Callee", "raised by Err.Raise" +End Sub + +Private Sub Divide_Plain(ByVal divisor As Long) + Dim x As Long + x = 1 \ divisor +End Sub + +Private Sub Divide_Llvm(ByVal divisor As Long) + Dim x As Long + x = 1 \ divisor +End Sub + +Private Sub Middle_Plain() + ErrRaise_Llvm +End Sub + +' --- The callers: each has an On Error GoTo handler and returns what it caught --------------- + +Public Function CallerPlain_CalleePlain_ErrRaise() As String + On Error GoTo Handler + ErrRaise_Plain + CallerPlain_CalleePlain_ErrRaise = "no error" + Exit Function +Handler: + CallerPlain_CalleePlain_ErrRaise = "caught " & Err.Number +End Function + +Public Function CallerPlain_CalleeLlvm_ErrRaise() As String + On Error GoTo Handler + ErrRaise_Llvm + CallerPlain_CalleeLlvm_ErrRaise = "no error" + Exit Function +Handler: + CallerPlain_CalleeLlvm_ErrRaise = "caught " & Err.Number +End Function + +Public Function CallerLlvm_CalleePlain_ErrRaise() As String + On Error GoTo Handler + ErrRaise_Plain + CallerLlvm_CalleePlain_ErrRaise = "no error" + Exit Function +Handler: + CallerLlvm_CalleePlain_ErrRaise = "caught " & Err.Number +End Function + +Public Function CallerLlvm_CalleeLlvm_ErrRaise() As String + On Error GoTo Handler + ErrRaise_Llvm + CallerLlvm_CalleeLlvm_ErrRaise = "no error" + Exit Function +Handler: + CallerLlvm_CalleeLlvm_ErrRaise = "caught " & Err.Number +End Function + +Public Function CallerPlain_CalleePlain_Divide() As String + On Error GoTo Handler + Divide_Plain 0 + CallerPlain_CalleePlain_Divide = "no error" + Exit Function +Handler: + CallerPlain_CalleePlain_Divide = "caught " & Err.Number +End Function + +Public Function CallerPlain_CalleeLlvm_Divide() As String + On Error GoTo Handler + Divide_Llvm 0 + CallerPlain_CalleeLlvm_Divide = "no error" + Exit Function +Handler: + CallerPlain_CalleeLlvm_Divide = "caught " & Err.Number +End Function + +Public Function CallerLlvm_CalleePlain_Divide() As String + On Error GoTo Handler + Divide_Plain 0 + CallerLlvm_CalleePlain_Divide = "no error" + Exit Function +Handler: + CallerLlvm_CalleePlain_Divide = "caught " & Err.Number +End Function + +Public Function CallerLlvm_CalleeLlvm_Divide() As String + On Error GoTo Handler + Divide_Llvm 0 + CallerLlvm_CalleeLlvm_Divide = "no error" + Exit Function +Handler: + CallerLlvm_CalleeLlvm_Divide = "caught " & Err.Number +End Function + +' --- The same with On Error Resume Next, and with a procedure between caller and callee ------ + +Public Function CallerPlain_CalleeLlvm_ResumeNext() As String + On Error Resume Next + ErrRaise_Llvm + CallerPlain_CalleeLlvm_ResumeNext = "continued, Err.Number " & Err.Number +End Function + +Public Function CallerLlvm_CalleePlain_ResumeNext() As String + On Error Resume Next + ErrRaise_Plain + CallerLlvm_CalleePlain_ResumeNext = "continued, Err.Number " & Err.Number +End Function + +Public Function CallerPlain_MiddlePlain_CalleeLlvm() As String + On Error GoTo Handler + Middle_Plain + CallerPlain_MiddlePlain_CalleeLlvm = "no error" + Exit Function +Handler: + CallerPlain_MiddlePlain_CalleeLlvm = "caught " & Err.Number +End Function + +' --- A procedure that handles its own error --------------------------------------------------- + +Public Function Llvm_OwnHandler_Divide() As String + On Error GoTo Handler + Dim divisor As Long + Dim x As Long + x = 1 \ divisor + Llvm_OwnHandler_Divide = "no error" + Exit Function +Handler: + Llvm_OwnHandler_Divide = "caught " & Err.Number +End Function diff --git a/bugs/filed/sub-named-like-module/vb6/Probe.vbp b/bugs/llvm-error-not-passed-to-caller/vb6/Probe.vbp similarity index 62% rename from bugs/filed/sub-named-like-module/vb6/Probe.vbp rename to bugs/llvm-error-not-passed-to-caller/vb6/Probe.vbp index 33a0347fc..4d210b0e8 100644 --- a/bugs/filed/sub-named-like-module/vb6/Probe.vbp +++ b/bugs/llvm-error-not-passed-to-caller/vb6/Probe.vbp @@ -1,7 +1,7 @@ -Type=Exe -Module=Probe; Module1.bas -Startup="Sub Main" -ExeName32="Probe.exe" -Name="SubNamedLikeModule" -CompilationType=0 -OptimizationType=0 +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/paramarray-variant-array/paramarray-variant-array.twinproj b/bugs/paramarray-variant-array/paramarray-variant-array.twinproj new file mode 100644 index 000000000..afa837e02 Binary files /dev/null and b/bugs/paramarray-variant-array/paramarray-variant-array.twinproj differ diff --git a/bugs/paramarray-variant-array/repro.json b/bugs/paramarray-variant-array/repro.json new file mode 100644 index 000000000..c95859afb --- /dev/null +++ b/bugs/paramarray-variant-array/repro.json @@ -0,0 +1,36 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS ParamArrayTests\\.ArrayVariable_AssignedToByteArray$", + "^FAIL ParamArrayTests\\.VariantVariable_AssignedToByteArray: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.VariantVariable_AssignedToLongArray: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.VariantVariable_AssignedToStringArray: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.VariantVariable_AssignedToDoubleArray: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.VariantVariable_AssignedToVariantArray: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.VariantVariable_Indexed: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.VariantVariable_TwoDimensions_Indexed: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.VariantVariable_WrittenThrough: .*Actual: \"error -2147467259; the caller's element 1 is now 2\"", + "^PASS ParamArrayTests\\.VariantVariable_Bounds$", + "^PASS ParamArrayTests\\.VariantVariable_IsArrayAndVarType$", + "^PASS ParamArrayTests\\.VariantVariable_ForEach$", + "^FAIL ParamArrayTests\\.VariantVariable_Join: .*Actual: \"error 13\"", + "^PASS ParamArrayTests\\.VariantVariable_ThroughLocalVariant_Indexed$", + "^PASS ParamArrayTests\\.VariantVariable_PassedOnByVal_Indexed$", + "^FAIL ParamArrayTests\\.VariantVariable_ForwardedToParamArray_Indexed: .*Actual: \"error -2147467259\"", + "^PASS ParamArrayTests\\.VariantVariable_ParamArrayPassedWhole_Indexed$", + "^FAIL ParamArrayTests\\.VariantVariable_UnpackedAsInIssue1660: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.ModuleLevelVariant_Indexed: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.VariantArrayElement_Indexed: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.UdtVariantField_Indexed: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.ByValParameter_Forwarded_Indexed: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.ByRefParameter_Forwarded_Indexed: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.ClassMethod_EarlyBound_Indexed: .*Actual: \"error -2147467259\"", + "^FAIL ParamArrayTests\\.ClassMethod_LateBound_Indexed: .*Actual: \"error -2147467259\"", + "^PASS ParamArrayTests\\.VariantInParentheses_Indexed$", + "^PASS ParamArrayTests\\.CVarOfArray_Indexed$", + "^PASS ParamArrayTests\\.VariantFunctionResult_Indexed$" + ] + } +} diff --git a/bugs/paramarray-variant-array/src/Settings b/bugs/paramarray-variant-array/src/Settings new file mode 100644 index 000000000..13d377748 --- /dev/null +++ b/bugs/paramarray-variant-array/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ParamarrayVariantArray", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: A `ParamArray` element that is a `Variant` variable holding an array cannot be indexed or assigned to an array: error -2147467259", + "project.exportPathIsV2": true, + "project.id": "{AA31B749-2A9C-49A0-9B22-9230FDC10880}", + "project.name": "ParamarrayVariantArray", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/paramarray-variant-array/src/Sources/Cases.twin b/bugs/paramarray-variant-array/src/Sources/Cases.twin new file mode 100644 index 000000000..d9cd43f77 --- /dev/null +++ b/bugs/paramarray-variant-array/src/Sources/Cases.twin @@ -0,0 +1,387 @@ +' Each case passes an array to a ParamArray in one way and does one thing with the element, and +' returns what that gives, or "error <number>". The VB6 project in vb6/ holds the same cases and +' prints what each gives in VB6, which is what the [TestCase]s of ParamArrayTests assert. +Module Cases + + Private Type Holder + v As Variant + End Type + + Private mVariant As Variant + + Public Function ArrayVariable_AssignedToByteArray() As String + Dim b() As Byte + b = Abc() + ArrayVariable_AssignedToByteArray = ToBytes(b) + End Function + + Public Function VariantVariable_AssignedToByteArray() As String + Dim v As Variant + v = Abc() + VariantVariable_AssignedToByteArray = ToBytes(v) + End Function + + Public Function VariantVariable_AssignedToLongArray() As String + Dim a() As Long, v As Variant + ReDim a(0 To 2) + a(1) = 7 + v = a + VariantVariable_AssignedToLongArray = ToLongs(v) + End Function + + Public Function VariantVariable_AssignedToStringArray() As String + Dim v As Variant + v = Split("x,y,z", ",") + VariantVariable_AssignedToStringArray = ToStrings(v) + End Function + + Public Function VariantVariable_AssignedToDoubleArray() As String + Dim a() As Double, v As Variant + ReDim a(0 To 2) + a(1) = 2.5 + v = a + VariantVariable_AssignedToDoubleArray = ToDoubles(v) + End Function + + Public Function VariantVariable_AssignedToVariantArray() As String + Dim v As Variant + v = Array("x", "y", "z") + VariantVariable_AssignedToVariantArray = ToVariants(v) + End Function + + Public Function VariantVariable_Indexed() As String + Dim v As Variant + v = Abc() + VariantVariable_Indexed = Second(v) + End Function + + Public Function VariantVariable_TwoDimensions_Indexed() As String + Dim a() As Long, v As Variant + ReDim a(0 To 1, 0 To 2) + a(1, 2) = 12 + v = a + VariantVariable_TwoDimensions_Indexed = Cell12(v) + End Function + + Public Function VariantVariable_WrittenThrough() As String + Dim v As Variant, r As String + v = Array(1, 2, 3) + r = SetSecond(v) + VariantVariable_WrittenThrough = r & "; the caller's element 1 is now " & v(1) + End Function + + Public Function VariantVariable_Bounds() As String + Dim v As Variant + v = Abc() + VariantVariable_Bounds = Bounds(v) + End Function + + Public Function VariantVariable_IsArrayAndVarType() As String + Dim v As Variant + v = Abc() + VariantVariable_IsArrayAndVarType = Kind(v) + End Function + + Public Function VariantVariable_ForEach() As String + Dim v As Variant + v = Array(1, 2, 3) + VariantVariable_ForEach = Sum(v) + End Function + + Public Function VariantVariable_Join() As String + Dim v As Variant + v = Split("x,y,z", ",") + VariantVariable_Join = Joined(v) + End Function + + Public Function VariantVariable_PassedOnByVal_Indexed() As String + Dim v As Variant + v = Abc() + VariantVariable_PassedOnByVal_Indexed = PassOnByVal(v) + End Function + + Public Function VariantVariable_ForwardedToParamArray_Indexed() As String + Dim v As Variant + v = Abc() + VariantVariable_ForwardedToParamArray_Indexed = ForwardElement(v) + End Function + + Public Function VariantVariable_ParamArrayPassedWhole_Indexed() As String + Dim v As Variant + v = Abc() + VariantVariable_ParamArrayPassedWhole_Indexed = ForwardWhole(v) + End Function + + ' The function of #1660: a ParamArray passed whole, copied, and its element assigned to an array. + Public Function VariantVariable_UnpackedAsInIssue1660() As String + Dim v As Variant + v = Array(1, -2, 300, 20.3) + VariantVariable_UnpackedAsInIssue1660 = Unpacked(v) + End Function + + Public Function ModuleLevelVariant_Indexed() As String + mVariant = Abc() + ModuleLevelVariant_Indexed = Second(mVariant) + End Function + + Public Function VariantArrayElement_Indexed() As String + Dim va(0 To 0) As Variant + va(0) = Abc() + VariantArrayElement_Indexed = Second(va(0)) + End Function + + Public Function UdtVariantField_Indexed() As String + Dim t As Holder + t.v = Abc() + UdtVariantField_Indexed = Second(t.v) + End Function + + Public Function ByValParameter_Forwarded_Indexed() As String + ByValParameter_Forwarded_Indexed = ViaByVal(Abc()) + End Function + + Public Function ByRefParameter_Forwarded_Indexed() As String + Dim v As Variant + v = Abc() + ByRefParameter_Forwarded_Indexed = ViaByRef(v) + End Function + + Public Function ClassMethod_EarlyBound_Indexed() As String + Dim w As New Widget, v As Variant + v = Abc() + ClassMethod_EarlyBound_Indexed = w.Second(v) + End Function + + Public Function ClassMethod_LateBound_Indexed() As String + Dim o As Object, v As Variant + Set o = New Widget + v = Abc() + ClassMethod_LateBound_Indexed = o.Second(v) + End Function + + Public Function VariantInParentheses_Indexed() As String + Dim v As Variant + v = Abc() + VariantInParentheses_Indexed = Second((v)) + End Function + + Public Function CVarOfArray_Indexed() As String + Dim b() As Byte + b = Abc() + CVarOfArray_Indexed = Second(CVar(b)) + End Function + + Public Function VariantFunctionResult_Indexed() As String + VariantFunctionResult_Indexed = Second(AbcVariant()) + End Function + + Public Function VariantVariable_ThroughLocalVariant_Indexed() As String + Dim v As Variant + v = Abc() + VariantVariable_ThroughLocalVariant_Indexed = SecondThroughLocal(v) + End Function + + ' --- What each does with the element ------------------------------------------------------ + + Private Function ToBytes(ParamArray p() As Variant) As String + Dim a() As Byte + On Error GoTo Failed + a = p(0) + ToBytes = UBound(a) & " " & a(1) + Exit Function + Failed: + ToBytes = "error " & Err.Number + End Function + + Private Function ToLongs(ParamArray p() As Variant) As String + Dim a() As Long + On Error GoTo Failed + a = p(0) + ToLongs = UBound(a) & " " & a(1) + Exit Function + Failed: + ToLongs = "error " & Err.Number + End Function + + Private Function ToStrings(ParamArray p() As Variant) As String + Dim a() As String + On Error GoTo Failed + a = p(0) + ToStrings = UBound(a) & " " & a(1) + Exit Function + Failed: + ToStrings = "error " & Err.Number + End Function + + Private Function ToDoubles(ParamArray p() As Variant) As String + Dim a() As Double + On Error GoTo Failed + a = p(0) + ToDoubles = UBound(a) & " " & a(1) + Exit Function + Failed: + ToDoubles = "error " & Err.Number + End Function + + Private Function ToVariants(ParamArray p() As Variant) As String + Dim a() As Variant + On Error GoTo Failed + a = p(0) + ToVariants = UBound(a) & " " & a(1) + Exit Function + Failed: + ToVariants = "error " & Err.Number + End Function + + Private Function Second(ParamArray p() As Variant) As String + On Error GoTo Failed + Second = CStr(p(0)(1)) + Exit Function + Failed: + Second = "error " & Err.Number + End Function + + Private Function Cell12(ParamArray p() As Variant) As String + On Error GoTo Failed + Cell12 = CStr(p(0)(1, 2)) + Exit Function + Failed: + Cell12 = "error " & Err.Number + End Function + + Private Function SetSecond(ParamArray p() As Variant) As String + On Error GoTo Failed + p(0)(1) = 99 + SetSecond = "written" + Exit Function + Failed: + SetSecond = "error " & Err.Number + End Function + + Private Function Bounds(ParamArray p() As Variant) As String + On Error GoTo Failed + Bounds = LBound(p(0)) & " to " & UBound(p(0)) + Exit Function + Failed: + Bounds = "error " & Err.Number + End Function + + Private Function Kind(ParamArray p() As Variant) As String + On Error GoTo Failed + Kind = IsArray(p(0)) & " " & VarType(p(0)) + Exit Function + Failed: + Kind = "error " & Err.Number + End Function + + Private Function Sum(ParamArray p() As Variant) As String + Dim x As Variant, total As Long + On Error GoTo Failed + For Each x In p(0) + total = total + x + Next + Sum = CStr(total) + Exit Function + Failed: + Sum = "error " & Err.Number + End Function + + Private Function Joined(ParamArray p() As Variant) As String + On Error GoTo Failed + Joined = Join(p(0), "+") + Exit Function + Failed: + Joined = "error " & Err.Number + End Function + + Private Function PassOnByVal(ParamArray p() As Variant) As String + PassOnByVal = SecondOf(p(0)) + End Function + + Private Function SecondOf(ByVal x As Variant) As String + On Error GoTo Failed + SecondOf = CStr(x(1)) + Exit Function + Failed: + SecondOf = "error " & Err.Number + End Function + + Private Function ForwardElement(ParamArray p() As Variant) As String + ForwardElement = Second(p(0)) + End Function + + Private Function ForwardWhole(ParamArray p() As Variant) As String + ForwardWhole = SecondOfFirstOfFirst(p) + End Function + + Private Function SecondOfFirstOfFirst(ParamArray q() As Variant) As String + On Error GoTo Failed + SecondOfFirstOfFirst = CStr(q(0)(0)(1)) + Exit Function + Failed: + SecondOfFirstOfFirst = "error " & Err.Number + End Function + + ' As Max and UnpackParamArray in #1660: the largest value. + Private Function Unpacked(ParamArray vals() As Variant) As String + Dim t As Variant, i As Long, best As Double + On Error GoTo Failed + t = UnpackParamArray(vals) + best = t(LBound(t)) + For i = LBound(t) To UBound(t) + If t(i) > best Then best = t(i) + Next + Unpacked = CStr(best) + Exit Function + Failed: + Unpacked = "error " & Err.Number + End Function + + Private Function UnpackParamArray(ParamArray vParameters() As Variant) As Variant() + Dim vOut() As Variant, vTemp() As Variant, lUBound As Long + vOut() = vParameters() + lUBound = UBound(vOut) + Do While lUBound = 0 + If IsArray(vOut(0)) Then + vTemp() = vOut(0) + Erase vOut() + vOut() = vTemp() + Erase vTemp() + lUBound = UBound(vOut) + Else + lUBound = 1 + End If + Loop + UnpackParamArray = vOut + End Function + + Private Function ViaByVal(ByVal d As Variant) As String + ViaByVal = Second(d) + End Function + + Private Function ViaByRef(ByRef d As Variant) As String + ViaByRef = Second(d) + End Function + + Private Function SecondThroughLocal(ParamArray p() As Variant) As String + Dim x As Variant + On Error GoTo Failed + x = p(0) + SecondThroughLocal = CStr(x(1)) + Exit Function + Failed: + SecondThroughLocal = "error " & Err.Number + End Function + + Private Function Abc() As Byte() + Abc = StrConv("abc", vbFromUnicode) + End Function + + ' Through a Byte() variable: StrConv assigned to a Variant gives a String. + Private Function AbcVariant() As Variant + Dim b() As Byte + b = StrConv("abc", vbFromUnicode) + AbcVariant = b + End Function + +End Module diff --git a/bugs/paramarray-variant-array/src/Sources/ParamArrayTests.twin b/bugs/paramarray-variant-array/src/Sources/ParamArrayTests.twin new file mode 100644 index 000000000..73e61967d --- /dev/null +++ b/bugs/paramarray-variant-array/src/Sources/ParamArrayTests.twin @@ -0,0 +1,160 @@ +' Regression tests for "A `ParamArray` element that is a `Variant` variable holding an array +' cannot be indexed or assigned to an array: error -2147467259". Each case of Cases is asserted to +' give what it gives in VB6 6.0, which the VB6 project in vb6/ prints. The cases that pass today +' guard the neighbouring forms: an array variable, an expression, a copy, a Variant read first. +[TestFixture] +Module ParamArrayTests + + ' --- The element assigned to an array of each type --------------------------------------- + + [TestCase] + Public Sub ArrayVariable_AssignedToByteArray() + Assert.Strict.AreEqual "2 98", Cases.ArrayVariable_AssignedToByteArray() + End Sub + + [TestCase] + Public Sub VariantVariable_AssignedToByteArray() + Assert.Strict.AreEqual "2 98", Cases.VariantVariable_AssignedToByteArray() + End Sub + + [TestCase] + Public Sub VariantVariable_AssignedToLongArray() + Assert.Strict.AreEqual "2 7", Cases.VariantVariable_AssignedToLongArray() + End Sub + + [TestCase] + Public Sub VariantVariable_AssignedToStringArray() + Assert.Strict.AreEqual "2 y", Cases.VariantVariable_AssignedToStringArray() + End Sub + + [TestCase] + Public Sub VariantVariable_AssignedToDoubleArray() + Assert.Strict.AreEqual "2 " & CStr(2.5), Cases.VariantVariable_AssignedToDoubleArray() + End Sub + + [TestCase] + Public Sub VariantVariable_AssignedToVariantArray() + Assert.Strict.AreEqual "2 y", Cases.VariantVariable_AssignedToVariantArray() + End Sub + + ' --- The element read and written -------------------------------------------------------- + + [TestCase] + Public Sub VariantVariable_Indexed() + Assert.Strict.AreEqual "98", Cases.VariantVariable_Indexed() + End Sub + + [TestCase] + Public Sub VariantVariable_TwoDimensions_Indexed() + Assert.Strict.AreEqual "12", Cases.VariantVariable_TwoDimensions_Indexed() + End Sub + + [TestCase] + Public Sub VariantVariable_WrittenThrough() + Assert.Strict.AreEqual "written; the caller's element 1 is now 99", Cases.VariantVariable_WrittenThrough() + End Sub + + [TestCase] + Public Sub VariantVariable_Bounds() + Assert.Strict.AreEqual "0 to 2", Cases.VariantVariable_Bounds() + End Sub + + [TestCase] + Public Sub VariantVariable_IsArrayAndVarType() + Assert.Strict.AreEqual "True 8209", Cases.VariantVariable_IsArrayAndVarType() + End Sub + + [TestCase] + Public Sub VariantVariable_ForEach() + Assert.Strict.AreEqual "6", Cases.VariantVariable_ForEach() + End Sub + + [TestCase] + Public Sub VariantVariable_Join() + Assert.Strict.AreEqual "x+y+z", Cases.VariantVariable_Join() + End Sub + + [TestCase] + Public Sub VariantVariable_ThroughLocalVariant_Indexed() + Assert.Strict.AreEqual "98", Cases.VariantVariable_ThroughLocalVariant_Indexed() + End Sub + + ' --- The element passed on --------------------------------------------------------------- + + [TestCase] + Public Sub VariantVariable_PassedOnByVal_Indexed() + Assert.Strict.AreEqual "98", Cases.VariantVariable_PassedOnByVal_Indexed() + End Sub + + [TestCase] + Public Sub VariantVariable_ForwardedToParamArray_Indexed() + Assert.Strict.AreEqual "98", Cases.VariantVariable_ForwardedToParamArray_Indexed() + End Sub + + [TestCase] + Public Sub VariantVariable_ParamArrayPassedWhole_Indexed() + Assert.Strict.AreEqual "98", Cases.VariantVariable_ParamArrayPassedWhole_Indexed() + End Sub + + [TestCase] + Public Sub VariantVariable_UnpackedAsInIssue1660() + Assert.Strict.AreEqual "300", Cases.VariantVariable_UnpackedAsInIssue1660() + End Sub + + ' --- Where the Variant is ---------------------------------------------------------------- + + [TestCase] + Public Sub ModuleLevelVariant_Indexed() + Assert.Strict.AreEqual "98", Cases.ModuleLevelVariant_Indexed() + End Sub + + [TestCase] + Public Sub VariantArrayElement_Indexed() + Assert.Strict.AreEqual "98", Cases.VariantArrayElement_Indexed() + End Sub + + [TestCase] + Public Sub UdtVariantField_Indexed() + Assert.Strict.AreEqual "98", Cases.UdtVariantField_Indexed() + End Sub + + [TestCase] + Public Sub ByValParameter_Forwarded_Indexed() + Assert.Strict.AreEqual "98", Cases.ByValParameter_Forwarded_Indexed() + End Sub + + [TestCase] + Public Sub ByRefParameter_Forwarded_Indexed() + Assert.Strict.AreEqual "98", Cases.ByRefParameter_Forwarded_Indexed() + End Sub + + ' --- A class's method --------------------------------------------------------------------- + + [TestCase] + Public Sub ClassMethod_EarlyBound_Indexed() + Assert.Strict.AreEqual "98", Cases.ClassMethod_EarlyBound_Indexed() + End Sub + + [TestCase] + Public Sub ClassMethod_LateBound_Indexed() + Assert.Strict.AreEqual "98", Cases.ClassMethod_LateBound_Indexed() + End Sub + + ' --- Not a Variant variable: an expression or a copy ------------------------------------- + + [TestCase] + Public Sub VariantInParentheses_Indexed() + Assert.Strict.AreEqual "98", Cases.VariantInParentheses_Indexed() + End Sub + + [TestCase] + Public Sub CVarOfArray_Indexed() + Assert.Strict.AreEqual "98", Cases.CVarOfArray_Indexed() + End Sub + + [TestCase] + Public Sub VariantFunctionResult_Indexed() + Assert.Strict.AreEqual "98", Cases.VariantFunctionResult_Indexed() + End Sub + +End Module diff --git a/bugs/paramarray-variant-array/src/Sources/Startup.twin b/bugs/paramarray-variant-array/src/Sources/Startup.twin new file mode 100644 index 000000000..d995fe618 --- /dev/null +++ b/bugs/paramarray-variant-array/src/Sources/Startup.twin @@ -0,0 +1,40 @@ +' Prints what each case of Cases gives, as the VB6 project in vb6/ prints what it gives in VB6. +' The [TestCase]s of ParamArrayTests assert the VB6 results; tbrun --tests runs them. +Module Startup + + Public Sub Main() + Show "ArrayVariable_AssignedToByteArray", Cases.ArrayVariable_AssignedToByteArray() + Show "VariantVariable_AssignedToByteArray", Cases.VariantVariable_AssignedToByteArray() + Show "VariantVariable_AssignedToLongArray", Cases.VariantVariable_AssignedToLongArray() + Show "VariantVariable_AssignedToStringArray", Cases.VariantVariable_AssignedToStringArray() + Show "VariantVariable_AssignedToDoubleArray", Cases.VariantVariable_AssignedToDoubleArray() + Show "VariantVariable_AssignedToVariantArray", Cases.VariantVariable_AssignedToVariantArray() + Show "VariantVariable_Indexed", Cases.VariantVariable_Indexed() + Show "VariantVariable_TwoDimensions_Indexed", Cases.VariantVariable_TwoDimensions_Indexed() + Show "VariantVariable_WrittenThrough", Cases.VariantVariable_WrittenThrough() + Show "VariantVariable_Bounds", Cases.VariantVariable_Bounds() + Show "VariantVariable_IsArrayAndVarType", Cases.VariantVariable_IsArrayAndVarType() + Show "VariantVariable_ForEach", Cases.VariantVariable_ForEach() + Show "VariantVariable_Join", Cases.VariantVariable_Join() + Show "VariantVariable_PassedOnByVal_Indexed", Cases.VariantVariable_PassedOnByVal_Indexed() + Show "VariantVariable_ForwardedToParamArray_Indexed", Cases.VariantVariable_ForwardedToParamArray_Indexed() + Show "VariantVariable_ParamArrayPassedWhole_Indexed", Cases.VariantVariable_ParamArrayPassedWhole_Indexed() + Show "VariantVariable_UnpackedAsInIssue1660", Cases.VariantVariable_UnpackedAsInIssue1660() + Show "ModuleLevelVariant_Indexed", Cases.ModuleLevelVariant_Indexed() + Show "VariantArrayElement_Indexed", Cases.VariantArrayElement_Indexed() + Show "UdtVariantField_Indexed", Cases.UdtVariantField_Indexed() + Show "ByValParameter_Forwarded_Indexed", Cases.ByValParameter_Forwarded_Indexed() + Show "ByRefParameter_Forwarded_Indexed", Cases.ByRefParameter_Forwarded_Indexed() + Show "ClassMethod_EarlyBound_Indexed", Cases.ClassMethod_EarlyBound_Indexed() + Show "ClassMethod_LateBound_Indexed", Cases.ClassMethod_LateBound_Indexed() + Show "VariantInParentheses_Indexed", Cases.VariantInParentheses_Indexed() + Show "CVarOfArray_Indexed", Cases.CVarOfArray_Indexed() + Show "VariantFunctionResult_Indexed", Cases.VariantFunctionResult_Indexed() + Show "VariantVariable_ThroughLocalVariant_Indexed", Cases.VariantVariable_ThroughLocalVariant_Indexed() + End Sub + + Private Sub Show(ByVal name As String, ByVal result As String) + Debug.Print name & ": " & result + End Sub + +End Module diff --git a/bugs/paramarray-variant-array/src/Sources/Widget.twin b/bugs/paramarray-variant-array/src/Sources/Widget.twin new file mode 100644 index 000000000..9e6b31017 --- /dev/null +++ b/bugs/paramarray-variant-array/src/Sources/Widget.twin @@ -0,0 +1,12 @@ +' A method with a ParamArray, called early- and late-bound by the class cases of Cases. +Class Widget + + Public Function Second(ParamArray p() As Variant) As String + On Error GoTo Failed + Second = CStr(p(0)(1)) + Exit Function + Failed: + Second = "error " & Err.Number + End Function + +End Class diff --git a/bugs/paramarray-variant-array/vb6/Module1.bas b/bugs/paramarray-variant-array/vb6/Module1.bas new file mode 100644 index 000000000..a928ad551 --- /dev/null +++ b/bugs/paramarray-variant-array/vb6/Module1.bas @@ -0,0 +1,432 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The VB6 side of bugs/paramarray-variant-array: the cases of src/Sources/Cases.twin, each +' printed with what it gives in VB6, which is what the [TestCase]s of ParamArrayTests.twin assert. +' Build and run it with: node scripts/bug_repro.mjs vb6 paramarray-variant-array + +Private Type Holder + v As Variant +End Type + +Private mVariant As Variant + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Show "ArrayVariable_AssignedToByteArray", ArrayVariable_AssignedToByteArray() + Show "VariantVariable_AssignedToByteArray", VariantVariable_AssignedToByteArray() + Show "VariantVariable_AssignedToLongArray", VariantVariable_AssignedToLongArray() + Show "VariantVariable_AssignedToStringArray", VariantVariable_AssignedToStringArray() + Show "VariantVariable_AssignedToDoubleArray", VariantVariable_AssignedToDoubleArray() + Show "VariantVariable_AssignedToVariantArray", VariantVariable_AssignedToVariantArray() + Show "VariantVariable_Indexed", VariantVariable_Indexed() + Show "VariantVariable_TwoDimensions_Indexed", VariantVariable_TwoDimensions_Indexed() + Show "VariantVariable_WrittenThrough", VariantVariable_WrittenThrough() + Show "VariantVariable_Bounds", VariantVariable_Bounds() + Show "VariantVariable_IsArrayAndVarType", VariantVariable_IsArrayAndVarType() + Show "VariantVariable_ForEach", VariantVariable_ForEach() + Show "VariantVariable_Join", VariantVariable_Join() + Show "VariantVariable_PassedOnByVal_Indexed", VariantVariable_PassedOnByVal_Indexed() + Show "VariantVariable_ForwardedToParamArray_Indexed", VariantVariable_ForwardedToParamArray_Indexed() + Show "VariantVariable_ParamArrayPassedWhole_Indexed", VariantVariable_ParamArrayPassedWhole_Indexed() + Show "VariantVariable_UnpackedAsInIssue1660", VariantVariable_UnpackedAsInIssue1660() + Show "ModuleLevelVariant_Indexed", ModuleLevelVariant_Indexed() + Show "VariantArrayElement_Indexed", VariantArrayElement_Indexed() + Show "UdtVariantField_Indexed", UdtVariantField_Indexed() + Show "ByValParameter_Forwarded_Indexed", ByValParameter_Forwarded_Indexed() + Show "ByRefParameter_Forwarded_Indexed", ByRefParameter_Forwarded_Indexed() + Show "ClassMethod_EarlyBound_Indexed", ClassMethod_EarlyBound_Indexed() + Show "ClassMethod_LateBound_Indexed", ClassMethod_LateBound_Indexed() + Show "VariantInParentheses_Indexed", VariantInParentheses_Indexed() + Show "CVarOfArray_Indexed", CVarOfArray_Indexed() + Show "VariantFunctionResult_Indexed", VariantFunctionResult_Indexed() + Show "VariantVariable_ThroughLocalVariant_Indexed", VariantVariable_ThroughLocalVariant_Indexed() + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub + +Private Sub Show(ByVal name As String, ByVal result As String) + Print #9, name & ": " & result +End Sub + +' --- The cases: each passes an array to a ParamArray one way and does one thing with it --------- + +Public Function ArrayVariable_AssignedToByteArray() As String + Dim b() As Byte + b = Abc() + ArrayVariable_AssignedToByteArray = ToBytes(b) +End Function + +Public Function VariantVariable_AssignedToByteArray() As String + Dim v As Variant + v = Abc() + VariantVariable_AssignedToByteArray = ToBytes(v) +End Function + +Public Function VariantVariable_AssignedToLongArray() As String + Dim a() As Long, v As Variant + ReDim a(0 To 2) + a(1) = 7 + v = a + VariantVariable_AssignedToLongArray = ToLongs(v) +End Function + +Public Function VariantVariable_AssignedToStringArray() As String + Dim v As Variant + v = Split("x,y,z", ",") + VariantVariable_AssignedToStringArray = ToStrings(v) +End Function + +Public Function VariantVariable_AssignedToDoubleArray() As String + Dim a() As Double, v As Variant + ReDim a(0 To 2) + a(1) = 2.5 + v = a + VariantVariable_AssignedToDoubleArray = ToDoubles(v) +End Function + +Public Function VariantVariable_AssignedToVariantArray() As String + Dim v As Variant + v = Array("x", "y", "z") + VariantVariable_AssignedToVariantArray = ToVariants(v) +End Function + +Public Function VariantVariable_Indexed() As String + Dim v As Variant + v = Abc() + VariantVariable_Indexed = Second(v) +End Function + +Public Function VariantVariable_TwoDimensions_Indexed() As String + Dim a() As Long, v As Variant + ReDim a(0 To 1, 0 To 2) + a(1, 2) = 12 + v = a + VariantVariable_TwoDimensions_Indexed = Cell12(v) +End Function + +Public Function VariantVariable_WrittenThrough() As String + Dim v As Variant, r As String + v = Array(1, 2, 3) + r = SetSecond(v) + VariantVariable_WrittenThrough = r & "; the caller's element 1 is now " & v(1) +End Function + +Public Function VariantVariable_Bounds() As String + Dim v As Variant + v = Abc() + VariantVariable_Bounds = Bounds(v) +End Function + +Public Function VariantVariable_IsArrayAndVarType() As String + Dim v As Variant + v = Abc() + VariantVariable_IsArrayAndVarType = Kind(v) +End Function + +Public Function VariantVariable_ForEach() As String + Dim v As Variant + v = Array(1, 2, 3) + VariantVariable_ForEach = Sum(v) +End Function + +Public Function VariantVariable_Join() As String + Dim v As Variant + v = Split("x,y,z", ",") + VariantVariable_Join = Joined(v) +End Function + +Public Function VariantVariable_PassedOnByVal_Indexed() As String + Dim v As Variant + v = Abc() + VariantVariable_PassedOnByVal_Indexed = PassOnByVal(v) +End Function + +Public Function VariantVariable_ForwardedToParamArray_Indexed() As String + Dim v As Variant + v = Abc() + VariantVariable_ForwardedToParamArray_Indexed = ForwardElement(v) +End Function + +Public Function VariantVariable_ParamArrayPassedWhole_Indexed() As String + Dim v As Variant + v = Abc() + VariantVariable_ParamArrayPassedWhole_Indexed = ForwardWhole(v) +End Function + +' The function of #1660: a ParamArray passed whole, copied, and its element assigned to an array. +Public Function VariantVariable_UnpackedAsInIssue1660() As String + Dim v As Variant + v = Array(1, -2, 300, 20.3) + VariantVariable_UnpackedAsInIssue1660 = Unpacked(v) +End Function + +Public Function ModuleLevelVariant_Indexed() As String + mVariant = Abc() + ModuleLevelVariant_Indexed = Second(mVariant) +End Function + +Public Function VariantArrayElement_Indexed() As String + Dim va(0 To 0) As Variant + va(0) = Abc() + VariantArrayElement_Indexed = Second(va(0)) +End Function + +Public Function UdtVariantField_Indexed() As String + Dim t As Holder + t.v = Abc() + UdtVariantField_Indexed = Second(t.v) +End Function + +Public Function ByValParameter_Forwarded_Indexed() As String + ByValParameter_Forwarded_Indexed = ViaByVal(Abc()) +End Function + +Public Function ByRefParameter_Forwarded_Indexed() As String + Dim v As Variant + v = Abc() + ByRefParameter_Forwarded_Indexed = ViaByRef(v) +End Function + +Public Function ClassMethod_EarlyBound_Indexed() As String + Dim w As New Widget, v As Variant + v = Abc() + ClassMethod_EarlyBound_Indexed = w.Second(v) +End Function + +Public Function ClassMethod_LateBound_Indexed() As String + Dim o As Object, v As Variant + Set o = New Widget + v = Abc() + ClassMethod_LateBound_Indexed = o.Second(v) +End Function + +Public Function VariantInParentheses_Indexed() As String + Dim v As Variant + v = Abc() + VariantInParentheses_Indexed = Second((v)) +End Function + +Public Function CVarOfArray_Indexed() As String + Dim b() As Byte + b = Abc() + CVarOfArray_Indexed = Second(CVar(b)) +End Function + +Public Function VariantFunctionResult_Indexed() As String + VariantFunctionResult_Indexed = Second(AbcVariant()) +End Function + +Public Function VariantVariable_ThroughLocalVariant_Indexed() As String + Dim v As Variant + v = Abc() + VariantVariable_ThroughLocalVariant_Indexed = SecondThroughLocal(v) +End Function + +' --- What each does with the element ------------------------------------------------------------ + +Private Function ToBytes(ParamArray p() As Variant) As String + Dim a() As Byte + On Error GoTo Failed + a = p(0) + ToBytes = UBound(a) & " " & a(1) + Exit Function +Failed: + ToBytes = "error " & Err.Number +End Function + +Private Function ToLongs(ParamArray p() As Variant) As String + Dim a() As Long + On Error GoTo Failed + a = p(0) + ToLongs = UBound(a) & " " & a(1) + Exit Function +Failed: + ToLongs = "error " & Err.Number +End Function + +Private Function ToStrings(ParamArray p() As Variant) As String + Dim a() As String + On Error GoTo Failed + a = p(0) + ToStrings = UBound(a) & " " & a(1) + Exit Function +Failed: + ToStrings = "error " & Err.Number +End Function + +Private Function ToDoubles(ParamArray p() As Variant) As String + Dim a() As Double + On Error GoTo Failed + a = p(0) + ToDoubles = UBound(a) & " " & a(1) + Exit Function +Failed: + ToDoubles = "error " & Err.Number +End Function + +Private Function ToVariants(ParamArray p() As Variant) As String + Dim a() As Variant + On Error GoTo Failed + a = p(0) + ToVariants = UBound(a) & " " & a(1) + Exit Function +Failed: + ToVariants = "error " & Err.Number +End Function + +Private Function Second(ParamArray p() As Variant) As String + On Error GoTo Failed + Second = CStr(p(0)(1)) + Exit Function +Failed: + Second = "error " & Err.Number +End Function + +Private Function Cell12(ParamArray p() As Variant) As String + On Error GoTo Failed + Cell12 = CStr(p(0)(1, 2)) + Exit Function +Failed: + Cell12 = "error " & Err.Number +End Function + +Private Function SetSecond(ParamArray p() As Variant) As String + On Error GoTo Failed + p(0)(1) = 99 + SetSecond = "written" + Exit Function +Failed: + SetSecond = "error " & Err.Number +End Function + +Private Function Bounds(ParamArray p() As Variant) As String + On Error GoTo Failed + Bounds = LBound(p(0)) & " to " & UBound(p(0)) + Exit Function +Failed: + Bounds = "error " & Err.Number +End Function + +Private Function Kind(ParamArray p() As Variant) As String + On Error GoTo Failed + Kind = IsArray(p(0)) & " " & VarType(p(0)) + Exit Function +Failed: + Kind = "error " & Err.Number +End Function + +Private Function Sum(ParamArray p() As Variant) As String + Dim x As Variant, total As Long + On Error GoTo Failed + For Each x In p(0) + total = total + x + Next + Sum = CStr(total) + Exit Function +Failed: + Sum = "error " & Err.Number +End Function + +Private Function Joined(ParamArray p() As Variant) As String + On Error GoTo Failed + Joined = Join(p(0), "+") + Exit Function +Failed: + Joined = "error " & Err.Number +End Function + +Private Function PassOnByVal(ParamArray p() As Variant) As String + PassOnByVal = SecondOf(p(0)) +End Function + +Private Function SecondOf(ByVal x As Variant) As String + On Error GoTo Failed + SecondOf = CStr(x(1)) + Exit Function +Failed: + SecondOf = "error " & Err.Number +End Function + +Private Function ForwardElement(ParamArray p() As Variant) As String + ForwardElement = Second(p(0)) +End Function + +Private Function ForwardWhole(ParamArray p() As Variant) As String + ForwardWhole = SecondOfFirstOfFirst(p) +End Function + +Private Function SecondOfFirstOfFirst(ParamArray q() As Variant) As String + On Error GoTo Failed + SecondOfFirstOfFirst = CStr(q(0)(0)(1)) + Exit Function +Failed: + SecondOfFirstOfFirst = "error " & Err.Number +End Function + +' As Max and UnpackParamArray in #1660: the largest value. +Private Function Unpacked(ParamArray vals() As Variant) As String + Dim t As Variant, i As Long, best As Double + On Error GoTo Failed + t = UnpackParamArray(vals) + best = t(LBound(t)) + For i = LBound(t) To UBound(t) + If t(i) > best Then best = t(i) + Next + Unpacked = CStr(best) + Exit Function +Failed: + Unpacked = "error " & Err.Number +End Function + +Private Function UnpackParamArray(ParamArray vParameters() As Variant) As Variant() + Dim vOut() As Variant, vTemp() As Variant, lUBound As Long + vOut() = vParameters() + lUBound = UBound(vOut) + Do While lUBound = 0 + If IsArray(vOut(0)) Then + vTemp() = vOut(0) + Erase vOut() + vOut() = vTemp() + Erase vTemp() + lUBound = UBound(vOut) + Else + lUBound = 1 + End If + Loop + UnpackParamArray = vOut +End Function + +Private Function ViaByVal(ByVal d As Variant) As String + ViaByVal = Second(d) +End Function + +Private Function ViaByRef(ByRef d As Variant) As String + ViaByRef = Second(d) +End Function + +Private Function SecondThroughLocal(ParamArray p() As Variant) As String + Dim x As Variant + On Error GoTo Failed + x = p(0) + SecondThroughLocal = CStr(x(1)) + Exit Function +Failed: + SecondThroughLocal = "error " & Err.Number +End Function + +Private Function Abc() As Byte() + Abc = StrConv("abc", vbFromUnicode) +End Function + +' Through a Byte() variable: StrConv assigned to a Variant gives a String. +Private Function AbcVariant() As Variant + Dim b() As Byte + b = StrConv("abc", vbFromUnicode) + AbcVariant = b +End Function diff --git a/bugs/filed/latebound-unknown-member-error/vb6/Probe.vbp b/bugs/paramarray-variant-array/vb6/Probe.vbp similarity index 100% rename from bugs/filed/latebound-unknown-member-error/vb6/Probe.vbp rename to bugs/paramarray-variant-array/vb6/Probe.vbp diff --git a/bugs/filed/latebound-unknown-member-error/vb6/Widget.cls b/bugs/paramarray-variant-array/vb6/Widget.cls similarity index 51% rename from bugs/filed/latebound-unknown-member-error/vb6/Widget.cls rename to bugs/paramarray-variant-array/vb6/Widget.cls index 88b2786ef..291481074 100644 --- a/bugs/filed/latebound-unknown-member-error/vb6/Widget.cls +++ b/bugs/paramarray-variant-array/vb6/Widget.cls @@ -10,22 +10,14 @@ Attribute VB_Name = "Widget" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = True Attribute VB_PredeclaredId = False -Attribute VB_Exposed = True +Attribute VB_Exposed = False Option Explicit -Public Hits As Long -Public Gets As Long - -Public Sub Hello() - Hits = Hits + 1 -End Sub - -Public Property Get Prop() As Long - Gets = Gets + 1 - Prop = 1 -End Property - -Public Property Let Prop(ByVal Value As Long) - Hits = Hits + 1 - Err.Raise 5 -End Property +' A method with a ParamArray, called early- and late-bound by Module1's class cases. +Public Function Second(ParamArray p() As Variant) As String + On Error GoTo Failed + Second = CStr(p(0)(1)) + Exit Function +Failed: + Second = "error " & Err.Number +End Function diff --git a/bugs/setattr-open-file-no-error/repro.json b/bugs/setattr-open-file-no-error/repro.json new file mode 100644 index 000000000..9c6ed7d80 --- /dev/null +++ b/bugs/setattr-open-file-no-error/repro.json @@ -0,0 +1,12 @@ +{ + "mode": "run", + "expect": { + "exit": 0, + "output": [ + "^SetAttr vbReadOnly, file open For Output: error 0 ; attributes now 1$", + "^SetAttr vbReadOnly, file open For Append: error 0 ; attributes now 1$", + "^SetAttr vbReadOnly, file open For Binary: error 0 ; attributes now 1$", + "^SetAttr vbReadOnly, file open For Random: error 0 ; attributes now 1$" + ] + } +} diff --git a/bugs/setattr-open-file-no-error/setattr-open-file-no-error.twinproj b/bugs/setattr-open-file-no-error/setattr-open-file-no-error.twinproj new file mode 100644 index 000000000..e006b4a9a Binary files /dev/null and b/bugs/setattr-open-file-no-error/setattr-open-file-no-error.twinproj differ diff --git a/bugs/filed/import-no-settings-silent/src/Settings b/bugs/setattr-open-file-no-error/src/Settings similarity index 84% rename from bugs/filed/import-no-settings-silent/src/Settings rename to bugs/setattr-open-file-no-error/src/Settings index 224f710bd..bf8f7d86e 100644 --- a/bugs/filed/import-no-settings-silent/src/Settings +++ b/bugs/setattr-open-file-no-error/src/Settings @@ -1,12 +1,12 @@ { "configuration.inherits": "Defaults", - "project.appTitle": "ImportNoSettingsSilent", + "project.appTitle": "SetattrOpenFileNoError", "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", "project.buildType": "Standard EXE", - "project.description": "Reproduces: `import` of a folder with no `Settings` file fails without saying why", + "project.description": "Reproduces: `SetAttr` changes the attributes of a file open for writing, where VB6 raises error 55", "project.exportPathIsV2": true, - "project.id": "{6ADA554D-C646-459B-BE03-F459BC2393EB}", - "project.name": "ImportNoSettingsSilent", + "project.id": "{2F34E339-82F5-4C04-8AA8-DF6BEC6120EC}", + "project.name": "SetattrOpenFileNoError", "project.optionExplicit": true, "project.references": [ { diff --git a/bugs/setattr-open-file-no-error/src/Sources/Startup.twin b/bugs/setattr-open-file-no-error/src/Sources/Startup.twin new file mode 100644 index 000000000..9eff0a78c --- /dev/null +++ b/bugs/setattr-open-file-no-error/src/Sources/Startup.twin @@ -0,0 +1,36 @@ +' Opens one file in each mode, sets it read-only while it is open, and prints the error +' SetAttr raises and the attributes the file has afterwards. +Module Startup + + Public Sub Main() + Dim d As String, f As Integer, m As Variant, attr As Integer + d = Environ$("TEMP") & "\SetattrOpenFileNoError-" & Hex$(Timer * 100) + MkDir d + On Error Resume Next + For Each m In Array("Input", "Output", "Append", "Binary", "Random") + f = FreeFile + Select Case m + Case "Input" + Open d & "\a.txt" For Output As #f + Close #f + Open d & "\a.txt" For Input As #f + Case "Output" + Open d & "\a.txt" For Output As #f + Case "Append" + Open d & "\a.txt" For Append As #f + Case "Binary" + Open d & "\a.txt" For Binary As #f + Case "Random" + Open d & "\a.txt" For Random As #f + End Select + Err.Clear + SetAttr d & "\a.txt", vbReadOnly + Debug.Print "SetAttr vbReadOnly, file open For " & m & ": error " & Err.Number & " " & Err.Description & "; attributes now " & GetAttr(d & "\a.txt") + Close #f + SetAttr d & "\a.txt", vbNormal + Next + Kill d & "\a.txt" + RmDir d + End Sub + +End Module diff --git a/bugs/setattr-open-file-no-error/vb6/Module1.bas b/bugs/setattr-open-file-no-error/vb6/Module1.bas new file mode 100644 index 000000000..02338d83b --- /dev/null +++ b/bugs/setattr-open-file-no-error/vb6/Module1.bas @@ -0,0 +1,49 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The VB6 side of bugs/setattr-open-file-no-error: the same cases as src/Sources/Startup.twin. +' Build and run it with: node scripts/bug_repro.mjs vb6 setattr-open-file-no-error +' File number 9 is out.txt, so no case closes every file or uses number 9. + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Cases + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub + +Sub Cases() + Dim d As String, f As Integer, m As Variant + d = Environ$("TEMP") & "\SetattrOpenFileNoError-vb6-" & Hex$(Timer * 100) + MkDir d + On Error Resume Next + For Each m In Array("Input", "Output", "Append", "Binary", "Random") + f = FreeFile + Select Case m + Case "Input" + Open d & "\a.txt" For Output As #f + Close #f + Open d & "\a.txt" For Input As #f + Case "Output" + Open d & "\a.txt" For Output As #f + Case "Append" + Open d & "\a.txt" For Append As #f + Case "Binary" + Open d & "\a.txt" For Binary As #f + Case "Random" + Open d & "\a.txt" For Random As #f + End Select + Err.Clear + SetAttr d & "\a.txt", vbReadOnly + Print #9, "SetAttr vbReadOnly, file open For " & m & ": error " & Err.Number & " " & Err.Description & "; attributes now " & GetAttr(d & "\a.txt") + Close #f + SetAttr d & "\a.txt", vbNormal + Next + Kill d & "\a.txt" + RmDir d +End Sub diff --git a/bugs/filed/module-named-like-project/vb6/Probe.vbp b/bugs/setattr-open-file-no-error/vb6/Probe.vbp similarity index 54% rename from bugs/filed/module-named-like-project/vb6/Probe.vbp rename to bugs/setattr-open-file-no-error/vb6/Probe.vbp index 1b4e6c124..4d210b0e8 100644 --- a/bugs/filed/module-named-like-project/vb6/Probe.vbp +++ b/bugs/setattr-open-file-no-error/vb6/Probe.vbp @@ -1,7 +1,7 @@ -Type=Exe -Module=ModuleNamedLikeProject; Module1.bas -Startup="Sub Main" -ExeName32="Probe.exe" -Name="ModuleNamedLikeProject" -CompilationType=0 -OptimizationType=0 +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/single-array-const-overflow/repro.json b/bugs/single-array-const-overflow/repro.json new file mode 100644 index 000000000..f8be912ea --- /dev/null +++ b/bugs/single-array-const-overflow/repro.json @@ -0,0 +1,19 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS LongElementOverflow$", + "^PASS LongSecondElementOverflow$", + "^PASS LongFractionOverflow$", + "^PASS IntegerElementOverflow$", + "^PASS ByteElementOverflow$", + "^PASS ByteNegativeElement$", + "^PASS LongLongElementOverflow$", + "^PASS DoubleElementOverflow$", + "^PASS ScalarSingleOverflowUse$", + "^FAIL SingleElementOverflow: ", + "^FAIL SqrAfterSingleOverflow: " + ] + } +} diff --git a/bugs/single-array-const-overflow/single-array-const-overflow.twinproj b/bugs/single-array-const-overflow/single-array-const-overflow.twinproj new file mode 100644 index 000000000..1e4970cee Binary files /dev/null and b/bugs/single-array-const-overflow/single-array-const-overflow.twinproj differ diff --git a/bugs/filed/advise-nothing-crashes/src/Settings b/bugs/single-array-const-overflow/src/Settings similarity index 81% rename from bugs/filed/advise-nothing-crashes/src/Settings rename to bugs/single-array-const-overflow/src/Settings index 42817a24a..88cdd23af 100644 --- a/bugs/filed/advise-nothing-crashes/src/Settings +++ b/bugs/single-array-const-overflow/src/Settings @@ -1,12 +1,12 @@ { "configuration.inherits": "Defaults", - "project.appTitle": "AdviseNothingCrashes", + "project.appTitle": "SingleArrayConstOverflow", "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", "project.buildType": "Standard EXE", - "project.description": "Reproduces: t", + "project.description": "Reproduces: A module-level `Single` array `Const` whose element overflows holds infinity, and a later array `Const` that calls `Sqr`, `Timer` or `Rnd` is refused with TB5001", "project.exportPathIsV2": true, - "project.id": "{8A50AEC4-3E42-4FFF-8CBB-20995A8ECB02}", - "project.name": "AdviseNothingCrashes", + "project.id": "{C6D998DB-B6A1-49FA-BE6F-8B573B43A6F8}", + "project.name": "SingleArrayConstOverflow", "project.optionExplicit": true, "project.references": [ { diff --git a/bugs/single-array-const-overflow/src/Sources/Startup.twin b/bugs/single-array-const-overflow/src/Sources/Startup.twin new file mode 100644 index 000000000..d60640765 --- /dev/null +++ b/bugs/single-array-const-overflow/src/Sources/Startup.twin @@ -0,0 +1,29 @@ +' Regression tests for #2475 and its follow-up, as compile cases: each line that +' ends in a ' CASE <Name>: <expected> comment must give that diagnostic on that +' line (a code, "error" for any error, or "none"). tbrun --tests checks them. +' Big overflows a Single and is accepted; Root, correct, is then refused with +' TB5001. Declared alone, Root compiles, and Big(0) reads 1.#INF. +Module Startup + ' Left in BETA 1005: a Single element that overflows is accepted, and the + ' array constant after it, which calls Sqr, is refused. + Const Big() As Single = Array(1E+39) ' CASE SingleElementOverflow: error + Const Root() As Double = Array(Sqr(4)) ' CASE SqrAfterSingleOverflow: none + + ' Fixed in #2475 (BETA 1005): an element too large for its type is TB5001, + ' where BETA 997 crashed the compiler. + Const L1() As Long = Array(2147483648) ' CASE LongElementOverflow: TB5001 + Const L2() As Long = Array(1, 2147483648) ' CASE LongSecondElementOverflow: TB5001 + Const L3() As Long = Array(3000000000.5) ' CASE LongFractionOverflow: TB5001 + Const I1() As Integer = Array(40000.5) ' CASE IntegerElementOverflow: TB5001 + Const Y1() As Byte = Array(300.5) ' CASE ByteElementOverflow: TB5001 + Const Y2() As Byte = Array(-1.5) ' CASE ByteNegativeElement: TB5001 + Const Q1() As LongLong = Array(9223372036854775808#) ' CASE LongLongElementOverflow: TB5001 + Const D1() As Double = Array(1E+300 * 1E+300) ' CASE DoubleElementOverflow: TB5001 + + ' Control: a scalar Single constant that overflows is refused where it is used. + Const S As Single = 1E+39 + + Public Sub Main() + Debug.Print S ' CASE ScalarSingleOverflowUse: TB5002 + End Sub +End Module diff --git a/bugs/stdole-idispatch-late-bound/repro.json b/bugs/stdole-idispatch-late-bound/repro.json index edf14b304..4fd5ed9fd 100644 --- a/bugs/stdole-idispatch-late-bound/repro.json +++ b/bugs/stdole-idispatch-late-bound/repro.json @@ -4,7 +4,7 @@ "exit": 0, "output": [ "^d\\.Hello: error 0, Hello ran 1 time\\(s\\)$", - "^d\\.GetTypeInfoCount: error -2147352570 \\(80020006\\) Unknown name\\.$" + "^d\\.GetTypeInfoCount: error 438 \\(1B6\\) Object doesn't support this property or method$" ] } } diff --git a/bugs/webview2-response-headers-reset-clone/repro.json b/bugs/webview2-response-headers-reset-clone/repro.json new file mode 100644 index 000000000..b1645bbdf --- /dev/null +++ b/bugs/webview2-response-headers-reset-clone/repro.json @@ -0,0 +1,14 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS HeaderEnumerationTests\\.Request_CloneAfterNext$", + "^PASS HeaderEnumerationTests\\.Request_CloneAfterSkip$", + "^PASS HeaderEnumerationTests\\.Request_CloneAfterReset$", + "^PASS HeaderEnumerationTests\\.Response_CloneAfterNext$", + "^PASS HeaderEnumerationTests\\.Response_CloneAfterSkip$", + "^FAIL HeaderEnumerationTests\\.Response_CloneAfterReset: .*Expected: \"Ha\\|Ha\" Actual: \"Ha\\|Hc\"" + ] + } +} diff --git a/bugs/webview2-response-headers-reset-clone/src/Settings b/bugs/webview2-response-headers-reset-clone/src/Settings new file mode 100644 index 000000000..8e23fbc7b --- /dev/null +++ b/bugs/webview2-response-headers-reset-clone/src/Settings @@ -0,0 +1,89 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "Webview2ResponseHeadersResetClone", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: After `Reset`, `Clone` of the WebView2 response headers enumerator skips the headers read before the `Reset`", + "project.exportPathIsV2": true, + "project.id": "{C00329BA-9C82-4E94-8D31-D766BEC9AA88}", + "project.name": "Webview2ResponseHeadersResetClone", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + }, + { + "id": "{5D19653F-F49E-41F2-ABF8-16E473A89A7C}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] WebView2 Package", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "WebView2Package", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 3 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/webview2-response-headers-reset-clone/src/Sources/Form1.tbform b/bugs/webview2-response-headers-reset-clone/src/Sources/Form1.tbform new file mode 100644 index 000000000..5610cc552 --- /dev/null +++ b/bugs/webview2-response-headers-reset-clone/src/Sources/Form1.tbform @@ -0,0 +1,146 @@ +[ + { + "AlwaysShowKeyboardCues": false, + "Appearance": "vbAppear3d", + "AutoRedraw": false, + "BackColor": 13223074, + "BorderStyle": "vbSizable", + "Caption": "WebView2 form", + "ClipControls": true, + "ControlBox": true, + "DrawMode": "vbCopyPen", + "DrawStyle": "vbSolid", + "DrawWidth": 1, + "Enabled": true, + "FillColor": 0, + "FillStyle": "vbFSTransparent", + "FontBold": false, + "FontItalic": false, + "FontName": "Segoe UI", + "FontSize": 8, + "FontStrikethru": false, + "FontTransparent": true, + "FontUnderline": false, + "ForeColor": -2147483630, + "FormDesignerId": "{7B247400-0000-4000-9000-7B2474000F01}", + "HasDC": true, + "Height": 480, + "HelpContextID": 0, + "Icon": "", + "KeyPreview": false, + "Left": 0, + "LinkMode": "vbLinkNone", + "LinkTopic": null, + "MDIChild": false, + "MaxButton": true, + "MaxHeight": 0, + "MaxWidth": 0, + "MinButton": true, + "MinHeight": 0, + "MinWidth": 0, + "MouseIcon": "", + "MousePointer": "vbDefault", + "Moveable": true, + "Name": "Form1", + "NegotiateMenus": false, + "OLEDropMode": "vbOLEDropNone", + "Opacity": 100, + "Palette": "", + "PaletteMode": "vbPaletteModeHalftone", + "Picture": "", + "PictureDpiScaling": false, + "RightToLeft": false, + "ScaleHeight": 8560, + "ScaleLeft": 0, + "ScaleMode": "vbTwips", + "ScaleTop": 0, + "ScaleWidth": 11760, + "ShowInTaskbar": true, + "StartUpPosition": "vbStartUpScreen", + "TabFocusAutoSelect": false, + "Tag": null, + "Top": 0, + "TopMost": false, + "TransparencyKey": -1, + "Visible": true, + "WhatsThisButton": false, + "WhatsThisHelp": false, + "Width": 640, + "WindowState": "vbNormal", + "__IDEOptions": { + "alignToGrid": false, + "gridHeight": 10, + "gridWidth": 10, + "lockedControls": [], + "multiColoredGrabbers": true, + "showGrid": true, + "showOutlines": false + }, + "__lastUpdateMarker": 3573907440, + "_children": [ + { + "AdditionalAllowedFrameAncestors": null, + "Anchors": { + "Bottom": true, + "Left": true, + "Right": true, + "Top": true, + "_clsid": "{8524D4B5-72A9-40A9-A189-29E7905C40CA}" + }, + "AreBrowserAcceleratorKeysEnabled": true, + "AreDefaultContextMenusEnabled": true, + "AreDefaultScriptDialogsEnabled": true, + "AreDevToolsEnabled": true, + "AreHostObjectsAllowed": true, + "BackColor": 10534293, + "CausesValidation": true, + "Dock": "vbDockNone", + "DocumentURL": "about:blank", + "DragIcon": "", + "DragMode": "vbManual", + "Enabled": true, + "EnvironmentOptions": { + "AdditionalBrowserArguments": null, + "AllowSingleSignOnUsingOSPrimaryAccount": false, + "BrowserExecutableFolder": null, + "EnableTrackingPrevention": true, + "ExclusiveUserDataFolderAccess": false, + "Language": null, + "TargetCompatibleBrowserVersion": "86.0.616.0", + "UserDataFolder": "", + "_className": "WebView2EnvironmentOptions", + "_clsid": "{185E63C0-E641-47ED-B6A9-8565168A7FCF}" + }, + "Height": 440, + "Index": -1, + "IsBuiltInErrorPageEnabled": true, + "IsGeneralAutoFillEnabled": true, + "IsMuted": false, + "IsPasswordAutoSaveEnabled": false, + "IsPinchZoomEnabled": true, + "IsScriptEnabled": true, + "IsStatusBarEnabled": false, + "IsSwipeNavigationEnabled": true, + "IsWebMessageEnabled": true, + "IsZoomControlEnabled": true, + "Left": 0, + "MouseIcon": "", + "MousePointer": "vbDefault", + "Name": "WebView21", + "TabIndex": 7, + "TabStop": true, + "Tag": null, + "Top": 0, + "UseDeferredEvents": true, + "UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0", + "Visible": true, + "Width": 620, + "ZoomFactor": 1, + "_className": "WebView2", + "_clsid": "{93B7E07E-6C7F-4758-8E51-4B1B17EC0A62}" + } + ], + "_className": "Form", + "_clsid": "{33AD4F38-6699-11CF-B70C-00AA0060D393}" + } +] diff --git a/bugs/webview2-response-headers-reset-clone/src/Sources/Form1.twin b/bugs/webview2-response-headers-reset-clone/src/Sources/Form1.twin new file mode 100644 index 000000000..53581f1e3 --- /dev/null +++ b/bugs/webview2-response-headers-reset-clone/src/Sources/Form1.twin @@ -0,0 +1,32 @@ +' A form holding one WebView2 control. Once the control is ready it navigates to a +' page whose request it answers itself in WebResourceRequested, so nothing goes to +' the network; there it records what the headers enumerators do (HeaderEnumeration). + +[FormDesignerId("7B247400-0000-4000-9000-7B2474000F01")] +[PredeclaredId] +Class Form1 + + Private Sub WebView21_Ready() Handles WebView21.Ready + WebView21.AddWebResourceRequestedFilter "*", wv2All + WebView21.Navigate "https://example.com/page" + End Sub + + Private Sub WebView21_WebResourceRequested(ByVal Request As WebView2Request, ByVal Response As WebView2Response) _ + Handles WebView21.WebResourceRequested + Response.Headers.AppendHeader "Ha", "1" + Response.Headers.AppendHeader "Hb", "2" + Response.Headers.AppendHeader "Hc", "3" + Response.Headers.AppendHeader "Hd", "4" + If Not HeaderEnumeration.Recorded Then + Dim rq As WebView2RequestHeaders = Request.Headers + HeaderEnumeration.Record "request", rq._NewEnum(), rq._NewEnum(), rq._NewEnum() + Dim rs As WebView2ResponseHeaders = Response.Headers + HeaderEnumeration.Record "response", rs._NewEnum(), rs._NewEnum(), rs._NewEnum() + HeaderEnumeration.Recorded = True + End If + Response.StatusCode = 200 + Response.ReasonPhrase = "OK" + Response.ContentUTF8 = "<html><body>probe</body></html>" + End Sub + +End Class diff --git a/bugs/webview2-response-headers-reset-clone/src/Sources/HeaderEnumerationTests.twin b/bugs/webview2-response-headers-reset-clone/src/Sources/HeaderEnumerationTests.twin new file mode 100644 index 000000000..86a6c3c5e --- /dev/null +++ b/bugs/webview2-response-headers-reset-clone/src/Sources/HeaderEnumerationTests.twin @@ -0,0 +1,125 @@ +' Regression tests for #2496 and its follow-up. IEnumVARIANT::Clone returns an +' enumerator with the same state as the one it is called on (the COM +' documentation of IEnumVARIANT::Clone), so the clone's Next returns what the +' enumerator's own Next returns next. Each case compares the two after one Next, +' after Skip 2, and after two Next and a Reset, for the request headers and for +' the response headers, which hold Ha, Hb, Hc and Hd. + +' IEnumVARIANT, declared here because the package's own declaration is private. +[InterfaceId("00020404-0000-0000-C000-000000000046")] +Interface IEnumVARIANTOwn Extends stdole.IUnknown + Type CArrayOfVariants + Elements(1) As Variant + End Type + + Sub Next(ByVal celt As Long, ByRef rgVar As CArrayOfVariants, ByRef pCeltFetched As Long) + Sub Skip(ByVal celt As Long) + Sub Reset() + Function Clone() As stdole.IUnknown +End Interface + +' What the enumerators returned, recorded by Form1 while it answers its request. +Module HeaderEnumeration + + Public Recorded As Boolean + Public Results As New Collection ' "<side> <case>" -> "<enumerator's Next>|<clone's Next>" + + Private Function NextName(ByVal e As IEnumVARIANTOwn) As String + Dim arr As IEnumVARIANTOwn.CArrayOfVariants + Dim fetched As Long + e.Next 1, arr, fetched + If fetched = 0 Then Return "(end)" + Dim hd As WebView2Header = arr.Elements(0) + Return hd.Name + End Function + + ' Three fresh enumerators of one collection. + Public Sub Record(ByVal Side As String, ByVal u0 As stdole.IUnknown, ByVal u1 As stdole.IUnknown, ByVal u2 As stdole.IUnknown) + Dim e As IEnumVARIANTOwn, c As IEnumVARIANTOwn, first As String + + Set e = u0 + first = NextName(e) + Set c = e.Clone() + Results.Add NextName(e) & "|" & NextName(c), Side & " after one Next" + + Set e = u1 + NextName e + NextName e + e.Reset + Set c = e.Clone() + Results.Add NextName(e) & "|" & NextName(c), Side & " after Reset" + + Set e = u2 + e.Skip 2 + Set c = e.Clone() + Results.Add NextName(e) & "|" & NextName(c), Side & " after Skip 2" + End Sub + +End Module + +[TestFixture] +Module HeaderEnumerationTests + + Private Declare PtrSafe Function SetEnvironmentVariableW Lib "kernel32" (ByVal lpName As LongPtr, ByVal lpValue As LongPtr) As Long + + ' Shows Form1 until its request has been answered, once. + Private Sub Collect() + If HeaderEnumeration.Recorded Then Exit Sub + ' Only for an IDE that was started with these variables, as this repository's + ' test harness starts it: WebView2 would read them, and the control would fail. + Dim folder As String = Environ$("WEBVIEW2_USER_DATA_FOLDER") + Dim args As String = Environ$("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS") + SetEnvironmentVariableW StrPtr("WEBVIEW2_USER_DATA_FOLDER"), 0 + SetEnvironmentVariableW StrPtr("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS"), 0 + Form1.Show + Dim started As Double = Timer + Do Until HeaderEnumeration.Recorded Or Timer - started > 30 + DoEvents + Loop + Unload Form1 + If Len(folder) Then SetEnvironmentVariableW StrPtr("WEBVIEW2_USER_DATA_FOLDER"), StrPtr(folder) + If Len(args) Then SetEnvironmentVariableW StrPtr("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS"), StrPtr(args) + End Sub + + ' "<enumerator's Next>|<clone's Next>" + Private Function Pair(ByVal Key As String) As String + Collect + Assert.Strict.IsTrue HeaderEnumeration.Recorded, "the request was answered" + Return HeaderEnumeration.Results(Key) + End Function + + ' Fixed in #2496 (BETA 1005): the clone continues from the enumerator's position. + + [TestCase] + Public Sub Request_CloneAfterNext() + Assert.Strict.AreEqual "Upgrade-Insecure-Requests|Upgrade-Insecure-Requests", Pair("request after one Next") + End Sub + + [TestCase] + Public Sub Request_CloneAfterSkip() + Assert.Strict.AreEqual "User-Agent|User-Agent", Pair("request after Skip 2") + End Sub + + [TestCase] + Public Sub Request_CloneAfterReset() + Assert.Strict.AreEqual "Accept|Accept", Pair("request after Reset") + End Sub + + [TestCase] + Public Sub Response_CloneAfterNext() + Assert.Strict.AreEqual "Hb|Hb", Pair("response after one Next") + End Sub + + [TestCase] + Public Sub Response_CloneAfterSkip() + Assert.Strict.AreEqual "Hc|Hc", Pair("response after Skip 2") + End Sub + + ' Left in BETA 1005: Reset does not rewind the position Clone copies. + + [TestCase] + Public Sub Response_CloneAfterReset() + Assert.Strict.AreEqual "Ha|Ha", Pair("response after Reset") + End Sub + +End Module diff --git a/bugs/webview2-response-headers-reset-clone/src/Sources/Startup.twin b/bugs/webview2-response-headers-reset-clone/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/webview2-response-headers-reset-clone/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/webview2-response-headers-reset-clone/webview2-response-headers-reset-clone.twinproj b/bugs/webview2-response-headers-reset-clone/webview2-response-headers-reset-clone.twinproj new file mode 100644 index 000000000..35029800c Binary files /dev/null and b/bugs/webview2-response-headers-reset-clone/webview2-response-headers-reset-clone.twinproj differ diff --git a/builder/README.md b/builder/README.md index a4f540e06..a805001d6 100644 --- a/builder/README.md +++ b/builder/README.md @@ -92,8 +92,8 @@ drains SSE clients). The end-user-facing documentation about how the build pipeline works lives on the site itself under [Documentation Development](../docs/Documentation/): -[Tools and Scripts](../docs/Documentation/Tools.md) is the one-line -cheat sheet; [tbdocs Internals](../docs/Documentation/Builder.md) is +[Tools and Scripts](../docs/Documentation/Tools.md) and its seven +group pages are the cheat sheet; [tbdocs Internals](../docs/Documentation/Builder.md) is the architecture overview; [Extending the Builder](../docs/Documentation/Extending.md) is the how-to for adding a pipeline task, a markdown-it plugin or a render-worker sub-stage. diff --git a/builder/book.mjs b/builder/book.mjs index 2e65fae79..18bfd01b9 100644 --- a/builder/book.mjs +++ b/builder/book.mjs @@ -28,6 +28,7 @@ import { compressHtml } from "./compress.mjs"; import { normalizeBaseurl } from "./url.mjs"; import { escapeRegExp } from "./escape.mjs"; import { CODE_OR_PRE, replaceOutsideCode } from "./code-guard.mjs"; +import { lightOnly, pictureSvgOutputs, pngPictures } from "./theme-pictures.mjs"; // --------------------------------------------------------------------------- // §A Phase 2: chapter resolver + sort_by_nav_order @@ -297,7 +298,7 @@ const SUMMARY_OPEN_RE = /<summary[^>]*>/gi; const HEADING_SHIFT_RE = /<(\/?)h([1-6])\b/g; // PLAN-8 §6.3 / book-chapter-transform.rb#book_chapter_transform. Five -// logical passes: +// logical passes, after step 0 reduces a picture in two themes to its light one: // 1. strip `src="<baseurl>/` prefix (no-op when baseurl is empty) // 2. unwrap <details>/<summary> tags (FAQ-style collapsibles flatten // for print); summary id= attributes are preserved as inline spans @@ -307,7 +308,9 @@ const HEADING_SHIFT_RE = /<(\/?)h([1-6])\b/g; // 5. anchor-id prefix on all id= attributes and non-empty href="#..." export function bookChapterTransform(body, baseurl, headingShiftN, chapterAnchor) { if (!body) return body; - let result = body; + // Step 0: a picture in two themes is its light picture alone on paper, a plain image + // (theme-pictures.mjs). + let result = lightOnly(body); // Step 1: strip the baseurl-prefixed src. With baseurl="" the strip // is `src="/` -> `src="`, which removes the leading slash from @@ -425,6 +428,10 @@ function decodeUrlPath(p) { // skipBaseHeadingShift truthy. Skip the +1 1.5a base shift. // extraHeadingShift truthy. Apply an additional +1 shift (1.9 // chaptered-part extra). +// The book's view of a chapter's pictures: assembleBook sets it for the one synchronous +// assembly it does (the PNGs, when picture_svg.book is false) and puts it back after. +let bookPictures = (html) => html; + function emitChapter(out, chapter, opts, subPageState, baseurl, imagePaths) { // An empty body is a legitimate chapter -- a landing page that carries // only a title. An *absent* one means the page's render result never @@ -437,7 +444,7 @@ function emitChapter(out, chapter, opts, subPageState, baseurl, imagePaths) { `renderedContent; refusing to drop it from the book silently`, ); } - let body = chapter.renderedContent; + let body = bookPictures(chapter.renderedContent); if (!body.trim()) return; // book-chapter-body.html line 59-64: if content starts with `<`, use @@ -570,7 +577,7 @@ const MONTH_NAMES = [ // returned `imagePaths` is an array of every page-relative `<img // src=>` path referenced from the assembled body, decoded to the file's // own name and deduplicated in emit order (Set insertion order). -export function assembleBook(site, pages) { +export function assembleBook(site, pages, staticFiles = null) { const bookData = site.bookData; if (!bookData) { throw new Error("Phase 8: site.bookData is unset; Phase 2 didn't run."); @@ -580,15 +587,25 @@ export function assembleBook(site, pages) { const siteTitle = String(site.config?.title ?? ""); const baseurl = String(site.config?.baseurl ?? ""); + // A book that shows the pictures' PNGs (picture_svg.book false) names each picture's SVG + // back as its PNG, before the chapters' images are collected for the PDF tree. + if (staticFiles && !pictureSvgOutputs(site.config).book) { + const rels = new Set(staticFiles.map((s) => s.srcRel)); + bookPictures = (html) => pngPictures(html, rels, baseurl); + } const out = []; const imagePaths = new Set(); - out.push(renderBookHead(lang, siteTitle)); - out.push("\n<body>\n"); - out.push(renderTitlePage(site)); - emitFrontMatter(out, bookData, baseurl, imagePaths); - (bookData.parts ?? []).forEach((part, i) => { - emitPart(out, part, i, site, baseurl, imagePaths); - }); + try { + out.push(renderBookHead(lang, siteTitle)); + out.push("\n<body>\n"); + out.push(renderTitlePage(site)); + emitFrontMatter(out, bookData, baseurl, imagePaths); + (bookData.parts ?? []).forEach((part, i) => { + emitPart(out, part, i, site, baseurl, imagePaths); + }); + } finally { + bookPictures = (html) => html; + } out.push("\n</body>\n</html>\n"); let bookHtml = out.join(""); diff --git a/builder/check-tree.mjs b/builder/check-tree.mjs index a81256ebe..bf976af33 100644 --- a/builder/check-tree.mjs +++ b/builder/check-tree.mjs @@ -60,7 +60,7 @@ export function posix(p) { // is a much larger module and this is two functions. function excluded(rel, patterns) { if (!patterns || !patterns.length) return false; - return patterns.some((pat) => fnmatch(pat, rel)); + return patterns.some((pat) => (/[*?]/.test(pat) ? fnmatch(pat, rel) : pat === rel)); } // File.fnmatch(..., FNM_PATHNAME): `*` does not cross `/`, `**` does. diff --git a/builder/cpu-worker.mjs b/builder/cpu-worker.mjs index 489e5b97a..4e794489d 100644 --- a/builder/cpu-worker.mjs +++ b/builder/cpu-worker.mjs @@ -15,6 +15,7 @@ import { renderFullNav, templatePhase, withPartialNav } from "./template.mjs"; import { unpackShared } from "./sab-broadcast.mjs"; import { deriveSearchEntries } from "./search.mjs"; import { computeChunkSeo } from "./seo.mjs"; +import { pictureSvgOutputs, pngPictures } from "./theme-pictures.mjs"; import { deriveOfflinePage, isWebsiteOnlyLink, websiteOf } from "./offline-rewrite.mjs"; import { normalizeBaseurl } from "./url.mjs"; @@ -75,6 +76,7 @@ const handlers = { checkTrees, vendoredVideosObj, vendoredImagesObj, + pictureSizes, counts, } = unpackShared(_sharedSAB); @@ -90,6 +92,7 @@ const handlers = { linkTables, baseurl, staticFiles, + pictureSizes, svgContents, vendoredVideos, vendoredImages, @@ -125,7 +128,11 @@ const handlers = { } } - _renderEnv = { site, initData, offlineBase }; + // An output that shows the pictures' PNGs (picture_svg) gets the renderer's HTML with + // each picture's SVG named back as its PNG; the others get it as rendered. + const svgOutputs = pictureSvgOutputs(siteData.config); + const asPng = (html) => pngPictures(html, staticFiles, baseurl || ""); + _renderEnv = { site, initData, offlineBase, svgOutputs, asPng }; return {}; }, @@ -213,13 +220,17 @@ const handlers = { env.fullNav ??= renderFullNav(env.site); const writable = chunk.filter((p) => p.html !== undefined); for (const p of writable) { - const offlinePage = { ...p, html: withPartialNav(p.html, p, env.site, env.fullNav) }; + const source = env.svgOutputs.offline ? p.html : env.asPng(p.html); + const offlinePage = { ...p, html: withPartialNav(source, p, env.site, env.fullNav) }; const { html, misses } = deriveOfflinePage(offlinePage, offlineState); p.offlineHtml = html; p.offlineMisses = misses; } } + // The online page, once the offline one is taken from the HTML as rendered. + if (!env.svgOutputs.online) for (const p of chunk) if (p.html !== undefined) p.html = env.asPng(p.html); + // Stash writable pages for the matching flush:i (FIFO; one batch per // render:i, drained by exactly one flush:i on the same worker). const batch = []; diff --git a/builder/dot.mjs b/builder/dot.mjs index af312ae8d..7087a96bc 100644 --- a/builder/dot.mjs +++ b/builder/dot.mjs @@ -173,7 +173,7 @@ export async function listDotSources(srcRoot) { // the only record of which Graphviz produced the committed artifact -- so a // version bump shows up in the diff -- and a comment is valid in both XML and // HTML, which is the whole problem with the two lines above it. -function stripXmlPrologue(svgXml, label) { +export function stripXmlPrologue(svgXml, label) { const out = svgXml .replace(/^\uFEFF/, "") .replace(/^\s*<\?xml[\s\S]*?\?>\s*/i, "") diff --git a/builder/gantt.mjs b/builder/gantt.mjs index 9e2f6b83f..edfef537e 100644 --- a/builder/gantt.mjs +++ b/builder/gantt.mjs @@ -126,9 +126,11 @@ export function renderGantt(grouped) { for (let t = 0; t <= maxT + 0.5; t += tick) ticks.push(t); const o = []; - o.push( - `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${SVG_W} ${h}" style="width:100%;max-width:${SVG_W}px">`, - ); + // Its own width and height, in px, as a Graphviz diagram has them in pt: the + // figure it is shown in (render.mjs, figurePlugin) shrinks to its figure, so + // an SVG with only a percentage width has no size of its own to shrink to, + // and the stylesheet's `max-width: 100%` already fits it to the column. + o.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${SVG_W} ${h}" width="${SVG_W}" height="${h}">`); o.push(`<title>Build task timeline`); // Dark palette rule bodies (theme-neutral selectors); emitted below under diff --git a/builder/inline-diagram.mjs b/builder/inline-diagram.mjs new file mode 100644 index 000000000..4f1cd5e3c --- /dev/null +++ b/builder/inline-diagram.mjs @@ -0,0 +1,114 @@ +// A diagram's SVG made ready to be inlined into a page (render.mjs, figurePlugin). +// +// Graphviz writes a into every node, edge and cluster, holding its DOT identifier +// (`w1`, `cluster_form`, `CF->HI`): a pointer shows it as a tooltip, and the search index +// reads it as the page's text. An inlined Graphviz diagram drops them; the .svg file keeps +// them. An SVG Graphviz did not write keeps its titles. +// +// A diagram whose boxes link somewhere (URL attributes in its DOT) is inlined as a group +// of links, and takes two more changes: +// - `<a xlink:href>` becomes `<a href>`, the attribute the link check reads and figure.js +// finds a link by. The offline and book rewrites read either. +// - Each cluster is moved to just before the first node inside it. Graphviz emits every +// cluster ahead of every node, so a reader, and the Tab key, would meet all the group +// labels first and their boxes after. The nodes keep Graphviz's order, which is the +// order the DOT declares them in when it sets `outputorder=nodesfirst`: a DOT whose +// boxes link declares them in reading order and sets it. + +const GRAPHVIZ_MARK = "<!-- Generated by graphviz"; +const CLUSTER_OR_NODE_RE = /<g id="[^"]*" class="(cluster|node)">/g; +const G_TAG_RE = /<g[\s>]|<\/g>/g; +const POINT_RE = /(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/g; +const TEXT_AT_RE = /<text\b[^>]*?\bx="(-?[\d.]+)" y="(-?[\d.]+)"/; +const TITLE_RE = /<title>[^<]*<\/title>\r?\n?/g; +const XLINK_HREF_RE = /<a xlink:href=/g; + +// The SVG to inline, and whether it is a group of links. +export function inlineDiagram(svg) { + if (!svg.startsWith(GRAPHVIZ_MARK)) return { svg, linked: false }; + const linked = svg.includes("<a xlink:href="); + if (linked) svg = clustersBeforeTheirNodes(svg.replace(XLINK_HREF_RE, "<a href=")); + return { svg: svg.replace(TITLE_RE, ""), linked }; +} + +// The top-level cluster and node groups, each with where it starts and ends; a cluster +// with its box, a node with the point its label is drawn at (none for a node with no +// label). +function clusterAndNodeBlocks(svg) { + const blocks = []; + CLUSTER_OR_NODE_RE.lastIndex = 0; + for (let m = CLUSTER_OR_NODE_RE.exec(svg); m; m = CLUSTER_OR_NODE_RE.exec(svg)) { + let depth = 0; + G_TAG_RE.lastIndex = m.index; + let end = -1; + for (let t = G_TAG_RE.exec(svg); t; t = G_TAG_RE.exec(svg)) { + depth += t[0] === "</g>" ? -1 : 1; + if (depth === 0) { + end = G_TAG_RE.lastIndex; + break; + } + } + if (end < 0) break; + const text = svg.slice(m.index, end); + const block = { kind: m[1], start: m.index, end, text }; + if (block.kind === "cluster") block.box = boxOf(text); + else { + const at = TEXT_AT_RE.exec(text); + block.at = at ? [Number(at[1]), Number(at[2])] : null; + } + blocks.push(block); + CLUSTER_OR_NODE_RE.lastIndex = end; + } + return blocks; +} + +// The bounding box of the first shape in a cluster's group: its outline. +function boxOf(text) { + const shape = /\s(?:d|points)="([^"]*)"/.exec(text); + const xs = []; + const ys = []; + for (const [, x, y] of shape?.[1].matchAll(POINT_RE) ?? []) { + xs.push(Number(x)); + ys.push(Number(y)); + } + if (xs.length === 0) return null; + return { x0: Math.min(...xs), x1: Math.max(...xs), y0: Math.min(...ys), y1: Math.max(...ys) }; +} + +const inside = (box, [x, y]) => box && x >= box.x0 && x <= box.x1 && y >= box.y0 && y <= box.y1; +const area = (box) => (box.x1 - box.x0) * (box.y1 - box.y0); + +function clustersBeforeTheirNodes(svg) { + const blocks = clusterAndNodeBlocks(svg); + const clusters = blocks.filter((b) => b.kind === "cluster"); + const nodes = blocks.filter((b) => b.kind === "node"); + if (clusters.length === 0 || nodes.length === 0) return svg; + + // Each cluster goes before the first node inside it, an outer cluster before an inner + // one; a cluster with no node inside it stays where it is. + const before = new Map(); + for (const c of clusters) { + const first = nodes.find((n) => n.at && inside(c.box, n.at)); + if (!first) continue; + if (!before.has(first)) before.set(first, []); + before.get(first).push(c); + c.moved = true; + } + for (const list of before.values()) list.sort((a, b) => area(b.box) - area(a.box)); + + // Rebuilt from the pieces between the blocks: a moved cluster leaves its place empty + // (with the line break after it), and lands at the start of its first node's place. + let out = ""; + let pos = 0; + for (const b of blocks) { + out += svg.slice(pos, b.start); + if (b.kind === "node") for (const c of before.get(b) ?? []) out += `${c.text}\n`; + if (b.moved) { + pos = b.end + (/^\r?\n/.exec(svg.slice(b.end, b.end + 2))?.[0].length ?? 0); + continue; + } + out += b.text; + pos = b.end; + } + return out + svg.slice(pos); +} diff --git a/builder/offline-rewrite.mjs b/builder/offline-rewrite.mjs index b0024409f..dc13468ea 100644 --- a/builder/offline-rewrite.mjs +++ b/builder/offline-rewrite.mjs @@ -59,7 +59,9 @@ export function buildSitePathsSync(pages, staticFiles, excludePatterns, stubs, t // `*` does NOT cross `/`, `**` does. export function offlineExcluded(rel, patterns) { if (!patterns.length) return false; - return patterns.some((pat) => fnmatchPathname(pat, rel)); + // A pattern with no wildcard matches only itself; the build adds a few hundred such + // (the pictures' SVGs, when picture_svg.offline is false), so they skip the regex. + return patterns.some((pat) => (/[*?]/.test(pat) ? fnmatchPathname(pat, rel) : pat === rel)); } export function fnmatchPathname(pattern, str) { diff --git a/builder/page-baseline.json b/builder/page-baseline.json index 9524197f2..fcbd962c5 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 928, - "staticFiles": 253 + "pages": 935, + "staticFiles": 794 } diff --git a/builder/pdf.mjs b/builder/pdf.mjs index 98386b9b8..e2d26f99f 100644 --- a/builder/pdf.mjs +++ b/builder/pdf.mjs @@ -52,7 +52,7 @@ export async function writePdf( resolveBookPage(pages); // existence check; throws if missing or duplicated - const { bookHtml, imagePaths } = deriveBookOutputs(pages, site); + const { bookHtml, imagePaths } = deriveBookOutputs(pages, site, staticFiles); const staticByDestRel = new Map(staticFiles.map((s) => [s.destRel.replaceAll("\\", "/"), s])); const counters = { bookBytes: 0, html: 0, css: 0, fonts: 0, images: 0, missing: 0 }; @@ -94,8 +94,8 @@ export async function writePdf( // PLAN-9 §5.9: image-path collection is folded into the assembly // itself (book.mjs's emitChapter populates a Set as it goes), so there // is no separate pass over book.html looking for images. -function deriveBookOutputs(pages, site) { - return assembleBook(site, pages); +function deriveBookOutputs(pages, site, staticFiles) { + return assembleBook(site, pages, staticFiles); } // PLAN-8 §5.1: locate the one `layout: book-combined` page. Throws on diff --git a/builder/render.mjs b/builder/render.mjs index e32adc8d7..e9ba8cb52 100644 --- a/builder/render.mjs +++ b/builder/render.mjs @@ -21,6 +21,16 @@ import { countPlugin, findSurvivingPlaceholder } from "./counts.mjs"; import { replaceOutsideCode } from "./code-guard.mjs"; import { splitFragment } from "./url.mjs"; import { escapeMarkup, escapeMarkupAndQuotes, escapeRegExp } from "./escape.mjs"; +import { inlineDiagram } from "./inline-diagram.mjs"; +import { + isLightPicture, + isPictureSvg, + lightDimension, + lightSiblingOf, + PIC_DARK, + PIC_LIGHT, + svgOf, +} from "./theme-pictures.mjs"; export async function renderPhase(pages, site, staticFiles = []) { // Allow the orchestrator to pre-build the markdown-it instance (so @@ -445,11 +455,12 @@ export function createMarkdownIt(ctx) { md.use(headingLevelNormalizePlugin); md.use(tocPlugin); md.use(relativeLinksPlugin, ctx); + md.use(themePairPlugin, ctx); md.use(blockHtmlRecursionPlugin); md.use(kramdownDashesPlugin); md.use(kramdownEllipsisPlugin); md.use(flattenAdjacentStrongPlugin); - md.use(svgInlinePlugin, ctx); + md.use(figurePlugin, ctx); md.use(videoLinkPlugin, ctx); md.use(remoteImagePlugin, ctx); // Substitutes {{tbdocs:<name>}} in prose. Registered last so it runs after @@ -1599,6 +1610,125 @@ function walkTokens(tokens, fn) { } } +// ---------- pictures in two themes (theme-pictures.mjs) --------------------- +// +// An image whose file has a light sibling among the static files (`X.png` and `X.light.png`) +// becomes two images, the light one first, with the same alt text and attributes, each with +// its theme's class and `loading="lazy"`; the light one's width and height are the page's +// scaled by the two files' pixel sizes (`ctx.pictureSizes`, pairSizes in theme-pictures.mjs), +// since the Light theme lays the IDE out a few pixels differently. The stylesheet hides the one for the other theme, +// and a lazy image that is not displayed is never fetched. A link to such a picture (the +// feature map's "Full size") becomes two links the same way, so that it opens the picture +// the reader sees. The sibling is looked up in the static-file list the build already has, +// never on disk. Runs after relativeLinksPlugin, so a static file's src is root-absolute. +// +// A page never names a light picture itself: one that does, in markdown or raw HTML, fails +// the build, since a light picture shown in both themes is the mistake this exists to stop. +// The book keeps the light picture alone (book.mjs, lightOnly). +// +// Each of the two is then shown as its SVG (X.svg, X.light.svg; theme-pictures.mjs) when +// that is among the static files, an image and a link to the picture alike, unless the +// attribute block names `png` -- `![alt](X.png){:width="190" height="337" png}` -- which +// keeps the PNGs and is removed from the output. A picture with no light sibling gets its +// SVG all the same. The light one's sizes still come from the PNG pair. +const RAW_LIGHT_REF_RE = /\b(?:src|href)\s*=\s*["']?[^"'\s>]*\.light\.(?:png|svg)(?:[?#][^"'\s>]*)?(?:["'\s>]|$)/i; + +function themePairPlugin(md, ctx) { + const urlPath = (value) => { + const p = value.split(/[?#]/, 1)[0]; + try { + return decodeURIComponent(p); + } catch { + return p; + } + }; + const prefix = `${ctx.baseurl || ""}/`; + md.core.ruler.push("theme-pairs", (state) => { + const where = state.env?.page?.srcRel ?? "a page"; + const refuse = (what) => { + throw new Error( + `${where}: ${what} names a light picture directly. Name the dark one, X.png: ` + + "the build shows X.light.png beside it in the light theme", + ); + }; + const checkRaw = (html) => { + const m = RAW_LIGHT_REF_RE.exec(html); + if (m) refuse(`raw HTML ${m[0].trim()}`); + }; + const clone = (t) => { + const c = new state.Token(t.type, t.tag, t.nesting); + Object.assign(c, t); + c.attrs = t.attrs ? t.attrs.map((a) => [...a]) : null; + c.meta = t.meta ? { ...t.meta } : null; + return c; + }; + const mark = (t, name) => { + setClass(t, name); + if (t.type === "image") t.attrSet("loading", "lazy"); + t.meta = { ...(t.meta || {}), themePaired: true }; + }; + for (const block of state.tokens) { + if (block.type === "html_block") checkRaw(block.content); + if (block.type !== "inline" || !block.children) continue; + const kids = block.children; + for (let i = 0; i < kids.length; i++) { + const t = kids[i]; + if (t.type === "html_inline") checkRaw(t.content); + const attr = t.type === "image" ? "src" : t.type === "link_open" ? "href" : null; + if (!attr || t.meta?.themePaired) continue; + const keepPng = t.attrGet("png") !== null; + if (keepPng) t.attrs = t.attrs.filter(([name]) => name !== "png"); + const value = t.attrGet(attr); + if (!value) continue; + const target = urlPath(value); + if (isLightPicture(target)) refuse(`the ${t.type === "image" ? "image" : "link"} ${value}`); + if (!ctx.staticFiles || !target.startsWith(prefix)) continue; + const rel = target.slice(prefix.length); + // `v`, a URL naming the picture `pngRel`, as the URL of its SVG when there is one. + const asSvg = (v, pngRel) => + !keepPng && ctx.staticFiles.has(svgOf(pngRel) ?? "") ? v.replace(/\.png(?=[?#]|$)/, ".svg") : v; + const light = lightSiblingOf(rel); + if (!light || !ctx.staticFiles.has(light)) { + t.attrSet(attr, asSvg(value, rel)); + continue; + } + const lightValue = asSvg(value.replace(/\.png(?=[?#]|$)/, ".light.png"), light); + const darkValue = asSvg(value, rel); + if (t.type === "image") { + const copy = clone(t); + copy.attrSet("src", lightValue); + t.attrSet("src", darkValue); + const size = ctx.pictureSizes?.[light]; + if (size) { + for (const [attr, i] of [ + ["width", 0], + ["height", 1], + ]) { + const v = copy.attrGet(attr); + if (v !== null) copy.attrSet(attr, lightDimension(v, size[i], size[i + 2])); + } + } + mark(copy, PIC_LIGHT); + mark(t, PIC_DARK); + kids.splice(i, 0, copy); + i++; + continue; + } + const close = findLinkClose(kids, i); + if (close < 0) continue; + // The light link's contents are walked next (an image in it is paired in turn); + // both link_open tokens are marked, so neither is taken for a link to pair again. + const copies = kids.slice(i, close + 1).map(clone); + copies[0].attrSet("href", lightValue); + t.attrSet("href", darkValue); + mark(copies[0], PIC_LIGHT); + mark(t, PIC_DARK); + kids.splice(i, 0, ...copies); + } + } + }); +} + // ---------- §5.2 GFM admonitions (pre-render text rewrite) ------------------ // // Octicon SVG strings ported from the Ruby Octicons gem (v19.21.2) -- @@ -2043,85 +2173,166 @@ function remoteImagePlugin(md, ctx) { }); } -function svgInlinePlugin(md, ctx) { +// Figures. An image that is its paragraph's only content -- a diagram, a picture in two +// themes (the pair themePairPlugin wrote), or any other image -- is written as a figure: a +// wrapper holding a controls bar and the image, which docs/assets/js/figure.js runs. The +// bar has a group per format, each with Download and Copy, and Zoom; the stylesheet puts it +// above the figure, or beside it where the window leaves a margin for it. +// +// A diagram (a site .svg that is no picture's SVG) is inlined: its SVG text goes into the +// wrapper, so it draws in the page's own fonts and its labels take the theme's colour. Any +// other image stays an <img>, and figure.js reads what to download or copy from the one +// the reader sees. +// +// The wrapper is a <div>, and a lone `![alt](x.svg)` is a paragraph, so without hiding the +// paragraph tokens the output is `<p><div …></div></p>` -- invalid, because <p> takes +// phrasing content only. A browser repairs it by closing the paragraph early and leaving a +// stray empty <p> behind, which is why it went unnoticed. Only a paragraph whose ENTIRE +// content is the image (or the pair) qualifies: `See this: ![d](x.svg)` keeps its <p>, and +// its image stays a plain <img>. +// +// Which it is, is decided on the token stream; what the bar holds is decided when the +// figure_open token renders, from the image's src as remoteImagePlugin will write it (that +// plugin rewrites a vendored attachment's URL only when the image itself renders, after +// this). +const RASTER_FORMATS = { png: "PNG", jpg: "JPEG", jpeg: "JPEG", gif: "GIF", webp: "WebP" }; + +function figurePlugin(md, ctx) { const orig = md.renderer.rules.image; + const prefix = `${ctx.baseurl || ""}/`; - // The wrapper below is a <div>, and a lone `![alt](x.svg)` is a - // paragraph, so without this the output is `<p><div …></div></p>` -- - // invalid, because <p> takes phrasing content only. A browser repairs - // it by closing the paragraph early and leaving a stray empty <p> - // behind, which is why it went unnoticed. Hide the paragraph tokens - // instead, the same way markdown-it hides them inside tight lists. - // - // Only a paragraph whose ENTIRE content is the one image qualifies -- - // `See this: ![d](x.svg)` keeps its <p>, and its image is left as a - // plain <img> rather than the <div> wrapper, because unwrapping only - // the paragraph's own <p>/<p> pair can't rescue an inline image that - // still has text siblings either side of it. Tag the image via - // meta.svgInline so the renderer rule below -- which is the one place - // that actually emits the wrapper -- makes the same decision instead of - // re-deriving it from the src alone. - md.core.ruler.push("svg_inline_unwrap_paragraph", (state) => { + // The site-relative path of the SVG to inline for this image, or null: only a site .svg + // that is no picture's SVG (a picture's SVG has its PNG beside it) and whose text the + // build has. + function inlinableSvgRel(token) { + const src = token.attrGet("src"); + if (!src || !src.endsWith(".svg") || !src.startsWith(prefix)) return null; + const srcRel = src.slice(prefix.length); + if (ctx.staticFiles && isPictureSvg(srcRel, ctx.staticFiles)) return null; + return ctx.svgContents?.get(srcRel) ? srcRel : null; + } + + // One image, or the two of a picture pair, and nothing else. + const isFigure = (kids) => + kids.length === 1 + ? kids[0].type === "image" + : kids.length === 2 && kids.every((k) => k.type === "image" && k.meta?.themePaired); + + md.core.ruler.push("figures", (state) => { const toks = state.tokens; for (let i = 0; i + 2 < toks.length; i++) { if (toks[i].type !== "paragraph_open") continue; if (toks[i + 1].type !== "inline" || toks[i + 2].type !== "paragraph_close") continue; - const children = toks[i + 1].children; - if (!children || children.length !== 1 || children[0].type !== "image") continue; - if (!inlinableSvgRel(children[0])) continue; + const kids = toks[i + 1].children; + if (!kids || !isFigure(kids)) continue; toks[i].hidden = true; toks[i + 2].hidden = true; - children[0].meta = { ...(children[0].meta || {}), svgInline: true }; + const open = new state.Token("figure_open", "div", 1); + open.meta = { images: kids.slice() }; + if (kids.length === 1 && inlinableSvgRel(kids[0])) kids[0].meta = { ...(kids[0].meta || {}), figureInline: true }; + kids.unshift(open); + kids.push(new state.Token("figure_close", "div", -1)); } }); - // Whether the src qualifies for inlining at all -- used both by the - // core rule above (to decide whether to hide the paragraph) and by the - // renderer rule below (to fetch the SVG content once it knows it's - // rendering the wrapper). It says nothing about paragraph context, so - // it is NOT sufficient on its own to decide the wrapper gets emitted -- - // see meta.svgInline below. - function inlinableSvgRel(token) { - const srcIdx = token.attrIndex("src"); - if (srcIdx < 0) return null; - const src = token.attrs[srcIdx][1]; - if (!src.endsWith(".svg")) return null; - const prefix = (ctx.baseurl || "") + "/"; - if (!src.startsWith(prefix)) return null; - const srcRel = src.slice(prefix.length); - return ctx.svgContents?.get(srcRel) ? srcRel : null; - } + md.renderer.rules.figure_open = (tokens, idx, options, env, self) => { + const images = tokens[idx].meta.images; + if (env?.page) env.page.hasFigure = true; + const first = images[0]; + if (first.meta?.figureInline) { + const srcRel = inlinableSvgRel(first); + const alt = self.renderInlineAsText(first.children, options, env); + const stem = srcRel + .split("/") + .pop() + .replace(/\.svg$/, ""); + return diagramFigureOpen(ctx.svgContents.get(srcRel), alt, stem, srcRel); + } + // The dark picture of a pair is the last; any image of the pair names the same formats, + // and the page's width and height are the dark one's. + const dark = images[images.length - 1]; + const src = dark.attrGet("src") ?? ""; + return pictureFigureOpen(resolveVendoredAttachment(src, ctx) ?? src, dark.attrGet("width"), dark.attrGet("height")); + }; + md.renderer.rules.figure_close = () => `</div></div>`; md.renderer.rules.image = (tokens, idx, options, env, self) => { + // A diagram's SVG was written by figure_open. + if (tokens[idx].meta?.figureInline) return ""; + if (orig) return orig(tokens, idx, options, env, self); const token = tokens[idx]; - const srcRel = inlinableSvgRel(token); - // meta.svgInline is set only by the core rule above, only on an image - // that is its paragraph's sole content. Without this check here, a - // src match alone would inline the wrapper for a mixed paragraph too - // (`See this: ![d](x.svg)`) -- and that <p> is never hidden, so the - // <div> wrapper would end up nested inside it, which is invalid HTML. - if (srcRel === null || !token.meta?.svgInline) return fallback(); - const svgContent = ctx.svgContents.get(srcRel); - - const alt = self.renderInlineAsText(token.children, options, env); - const stem = srcRel - .split("/") - .pop() - .replace(/\.svg$/, ""); - - if (env?.page) env.page.hasSvg = true; - - return buildSvgWrapper(svgContent, alt, stem, srcRel); - - function fallback() { - if (orig) return orig(tokens, idx, options, env, self); - token.attrs[token.attrIndex("alt")][1] = self.renderInlineAsText(token.children, options, env); - return self.renderToken(tokens, idx, options); - } + token.attrs[token.attrIndex("alt")][1] = self.renderInlineAsText(token.children, options, env); + return self.renderToken(tokens, idx, options); }; } -function buildSvgWrapper(svgContent, alt, stem, srcRel) { +// A glyph of the figure sprite (template.mjs, FIGURE_SPRITE), by its name. +const figureGlyph = (name, cls = "") => + `<svg${cls ? ` class="${cls}"` : ""} aria-hidden="true" focusable="false"><use href="#fig-${name}"></use></svg>`; + +// One button of a figure's controls bar: an icon, named by its aria-label and, for a +// pointer, by its tooltip. +const figureButton = (action, label, glyph, attrs = "") => + `<button type="button" class="btn-reset" data-action="${action}"${attrs} aria-label="${label}" title="${label}">` + + figureGlyph(glyph) + + `</button>`; + +// A group of the bar, led by the sign of its format (`vector`, `raster`), or by nothing for +// Zoom. The sign is a label, hidden from assistive technology: each button's own label +// already names its format. `data-format` is what figure.js hides a group by. +const figureGroup = (format, buttons) => + `<span class="fig-group"${format ? ` data-format="${format}"` : ""}>` + + (format ? figureGlyph(format, "fig-format") : "") + + buttons.join("") + + `</span>`; + +const zoomGroup = () => figureGroup(null, [figureButton("zoom", "Zoom", "zoom")]); + +// Whether a figure is tall enough to have its bar stood on end beside it, in the margin +// (custom.scss, `.fig-wrap[data-tall]`): at least as tall as that bar, as the page shows it +// in the 736px content column a window that wide has. A shorter figure would leave the +// paragraph after it pushed down by the bar's height, so its bar lies flat, beside it or +// above it. Decided here, from sizes the page already states, so that nothing moves once +// the page has loaded; a figure whose size is not known is taken as short. +// +// The bar's height follows from custom.scss: a group is 2px of padding at each end, its +// format sign (18px and 8px of margin), and 28px buttons 1px apart; groups are 6px apart. +const FIGURE_COLUMN = 736; +const barHeight = (groups) => + groups.reduce((sum, g) => sum + 4 + (g.sign ? 26 : 0) + 28 * g.buttons + (g.buttons - 1 + (g.sign ? 1 : 0)), 0) + + 6 * (groups.length - 1); + +function tallAttr(width, height, groups) { + const w = Number(width); + const h = Number(height); + if (!(w > 0 && h > 0)) return ""; + return h * Math.min(1, FIGURE_COLUMN / w) >= barHeight(groups) ? " data-tall" : ""; +} + +// Whether a diagram's figure is tall (tallAttr), by its SVG. A diagram's bar has both +// format groups and Zoom. Exported for the Gantt chart, whose SVG replaces a placeholder +// only after the page is written (tbdocs.mjs, injectGanttChart). +export function diagramIsTall(svgContent) { + const groups = [ + { sign: true, buttons: 2 }, + { sign: true, buttons: 2 }, + { sign: false, buttons: 1 }, + ]; + return tallAttr(...svgPixelSize(svgContent), groups) !== ""; +} + +// A diagram's size, from its root <svg>'s width and height: Graphviz writes them in pt. +function svgPixelSize(svgContent) { + const root = /<svg\b[^>]*>/.exec(svgContent)?.[0] ?? ""; + const px = (name) => { + const m = new RegExp(String.raw`\s${name}="([\d.]+)(pt|px)?"`).exec(root); + return m ? Number(m[1]) * (m[2] === "pt" ? 4 / 3 : 1) : 0; + }; + return [px("width"), px("height")]; +} + +// The opening of a diagram's figure, its SVG included. +function diagramFigureOpen(svgContent, alt, stem, srcRel) { const esc = escapeMarkupAndQuotes; // `role="img"` with an empty `aria-label` is worse than no role at all: it @@ -2137,20 +2348,65 @@ function buildSvgWrapper(svgContent, alt, stem, srcRel) { `no accessible name. Add one: ![describe the diagram](...)`, ); } - const imgRole = labelled ? ` role="img" aria-label="${esc(alt)}"` : ""; + // A diagram whose boxes link somewhere (Graphviz's URL attribute) is a group of links, + // not an image: role="img" would make its links presentational, which axe reports as + // nested-interactive. inline-diagram.mjs says what else changes. + const { svg, linked } = inlineDiagram(svgContent); + svgContent = svg; + const imgRole = labelled ? ` role="${linked ? "group" : "img"}" aria-label="${esc(alt)}"` : ""; + const name = ` data-filename="${esc(stem)}"`; + const tall = diagramIsTall(svgContent) ? " data-tall" : ""; return ( - `<div class="svg-inline-wrap">` + - `<div class="svg-controls">` + - `<button type="button" class="btn-reset" data-action="download-svg" data-filename="${esc(stem)}">Download SVG</button>` + - `<button type="button" class="btn-reset" data-action="copy-svg">Copy SVG</button>` + - `<button type="button" class="btn-reset" data-action="download-png" data-filename="${esc(stem)}">Download PNG</button>` + - `<button type="button" class="btn-reset" data-action="copy-png" data-filename="${esc(stem)}">Copy PNG</button>` + - `<button type="button" class="btn-reset" data-action="zoom-svg" aria-label="Zoom diagram">Zoom</button>` + + `<div class="fig-wrap" data-kind="diagram"${tall}>` + + `<div class="fig-controls">` + + figureGroup("vector", [ + figureButton("download-svg", "Download SVG", "download", name), + figureButton("copy-svg", "Copy SVG", "copy"), + ]) + + figureGroup("raster", [ + figureButton("download-png", "Download PNG", "download", name), + figureButton("copy-png", "Copy PNG", "copy", name), + ]) + + zoomGroup() + `</div>` + - `<div class="svg-container" data-svg-src="${esc(srcRel)}"${imgRole}>` + - svgContent + + `<div class="fig-container" data-svg-src="${esc(srcRel)}"${imgRole}>` + + svgContent + ); +} + +// The opening of a picture's or an image's figure. A picture drawn as SVG offers both +// formats, its PNG being beside it; a raster image offers its own format, copied as PNG, +// the one image format a clipboard takes. An image of any other kind gets Zoom alone. +function pictureFigureOpen(src, width, height) { + const ext = /\.([a-z0-9]+)(?:[?#]|$)/i.exec(src)?.[1]?.toLowerCase() ?? ""; + const raster = ext === "svg" ? "PNG" : RASTER_FORMATS[ext]; + const groups = []; + const shape = [{ sign: false, buttons: 1 }]; + if (raster) shape.unshift({ sign: true, buttons: 2 }); + if (ext === "svg") shape.unshift({ sign: true, buttons: 2 }); + if (ext === "svg") { + groups.push( + figureGroup("vector", [ + figureButton("download-svg", "Download SVG", "download"), + figureButton("copy-svg", "Copy SVG", "copy"), + ]), + ); + } + if (raster) { + groups.push( + figureGroup("raster", [ + figureButton("download-png", `Download ${raster}`, "download"), + figureButton("copy-png", raster === "PNG" ? "Copy PNG" : "Copy as PNG", "copy"), + ]), + ); + } + groups.push(zoomGroup()); + return ( + `<div class="fig-wrap" data-kind="picture"${tallAttr(width, height, shape)}>` + + `<div class="fig-controls">` + + groups.join("") + `</div>` + - `</div>` + `<div class="fig-container">` ); } diff --git a/builder/tbdocs.mjs b/builder/tbdocs.mjs index 13f7b1c58..550e3fe74 100644 --- a/builder/tbdocs.mjs +++ b/builder/tbdocs.mjs @@ -70,7 +70,8 @@ import { vendorAssets } from "./vendor-assets.mjs"; import { computeSiteSeo } from "./seo.mjs"; import { resolveBookChapters } from "./book.mjs"; import { loadData } from "./data.mjs"; -import { createMarkdownIt, buildLinkTables, serializeLinkTables } from "./render.mjs"; +import { createMarkdownIt, buildLinkTables, diagramIsTall, serializeLinkTables } from "./render.mjs"; +import { isPictureSvg, pairSizes, pictureSvgOutputs, unpairedLightPictures } from "./theme-pictures.mjs"; import { loadHighlightTheme } from "./highlight-theme.mjs"; import { NAV_SCRIPT_REL, buildInitConfig, renderSidebar } from "./template.mjs"; import { @@ -447,12 +448,23 @@ const TASKS = { if (strays.length) { throw new Error(formatPublishRefusal(strays, { surface: "source", label: ctx.srcRoot })); } - return { pages, staticFiles, config }; + // A light picture is shown only in place of its dark one (theme-pictures.mjs), so one + // with no dark picture beside it would never be shown. + const unpaired = unpairedLightPictures(staticFiles.map((s) => s.srcRel)); + if (unpaired.length) { + throw new Error( + `${unpaired.length} light picture(s) with no dark picture beside them; ` + + `X.light.png needs X.png in the same folder:\n${unpaired.map((r) => ` ${r}`).join("\n")}`, + ); + } + const pictureSizes = await pairSizes(staticFiles); + return { pages, staticFiles, config, pictureSizes }; }, submit(out, state) { state.pages = out.pages; state.staticFiles = out.staticFiles; state.site.config = out.config; + state.site.pictureSizes = out.pictureSizes; for (const p of out.pages) state.pageByDest.set(p.destPath, p); }, }, @@ -537,6 +549,7 @@ const TASKS = { linkTables, baseurl, staticFiles: staticFileSet, + pictureSizes: state.site.pictureSizes, vendoredVideos: state.site.vendoredVideos, vendoredImages: state.site.vendoredImages, counts: state.site.counts, @@ -635,6 +648,18 @@ const TASKS = { void _dotSignal; // dependency signal only -- static files already appended in dot.submit void _markdownInitSignal; // dependency signal only -- markdown + linkTablesSerialized + seoSiteTitle/seoLogoUrl already on state.site const chunks = chunkPages(state.pages, ctx.workerCount); + // An offline tree that shows the pictures' PNGs (picture_svg.offline false) holds no + // picture SVG either: each is added to offline_exclude, on the config every consumer of + // that list reads -- the trees' inventories, the offline copy, the workers' payload -- + // so they all leave the same files out. + const staticSrcRels = new Set(state.staticFiles.map((f) => f.srcRel)); + if (!pictureSvgOutputs(state.site.config).offline) { + const pictureSvgs = state.staticFiles.filter((f) => isPictureSvg(f.srcRel, staticSrcRels)); + state.site.config.offline_exclude = [ + ...(Array.isArray(state.site.config.offline_exclude) ? state.site.config.offline_exclude : []), + ...pictureSvgs.map((f) => f.destRel.replaceAll("\\", "/")), + ]; + } const excludePatterns = Array.isArray(state.site.config?.offline_exclude) ? state.site.config.offline_exclude.map(String) : []; @@ -715,9 +740,11 @@ const TASKS = { } state.checkTrees = checkTrees; } + // The SVGs the renderer may inline: not the pictures' (isPictureSvg), which are images + // like their PNGs and would otherwise travel to every worker, a few hundred KB each. const svgContentsMap = Object.create(null); for (const f of state.staticFiles) { - if (f.srcRel.endsWith(".svg")) { + if (f.srcRel.endsWith(".svg") && !isPictureSvg(f.srcRel, staticSrcRels)) { try { svgContentsMap[f.srcRel] = await fs.readFile(path.join(ctx.srcRoot, f.srcRel), "utf8"); } catch {} @@ -745,6 +772,7 @@ const TASKS = { // Plain objects, not Maps -- packShared serialises to JSON. vendoredVideosObj: Object.fromEntries(state.site.vendoredVideos ?? []), vendoredImagesObj: Object.fromEntries(state.site.vendoredImages ?? []), + pictureSizes: state.site.pictureSizes ?? {}, counts: state.site.counts, }; const sharedSAB = packShared(shared); @@ -1338,7 +1366,16 @@ async function injectGanttChart(pages, destRoot, svgContent) { const svgStart = html.indexOf("<svg", idx); const svgEnd = html.indexOf("</svg>", svgStart); if (svgStart < 0 || svgEnd < 0) continue; - const patched = html.slice(0, svgStart) + svgContent + html.slice(svgEnd + 6); + // The figure around it was written for the placeholder, a pixel tall, so it says the + // figure is short; the chart decides that now (render.mjs, diagramIsTall). + let head = html.slice(0, svgStart); + const wrapStart = head.lastIndexOf('<div class="fig-wrap"', idx); + const wrapEnd = wrapStart < 0 ? -1 : head.indexOf(">", wrapStart); + if (wrapEnd >= 0 && wrapEnd < idx) { + const tag = head.slice(wrapStart, wrapEnd).replace(/ data-tall\b/, ""); + head = head.slice(0, wrapStart) + tag + (diagramIsTall(svgContent) ? " data-tall" : "") + head.slice(wrapEnd); + } + const patched = head + svgContent + html.slice(svgEnd + 6); await fs.writeFile(htmlPath, patched, "utf8"); await fs.writeFile(path.join(root, ...GANTT_SVG_REL.split("/")), svgContent, "utf8"); injected.push({ diff --git a/builder/template.mjs b/builder/template.mjs index 470a31f93..2500b33a2 100644 --- a/builder/template.mjs +++ b/builder/template.mjs @@ -95,6 +95,7 @@ function templatePage(page, site, init) { ` <div id="a11y-status" class="sr-only" aria-live="polite" aria-atomic="true"></div>\n` + init.svgSprites + `\n` + + (page.hasFigure ? FIGURE_SPRITE + `\n` : "") + init.sidebar + `\n` + ` <div class="main" id="page-top">\n` + @@ -193,8 +194,8 @@ function renderHead(page, site, init) { : "") + (bu ? ` <script>window.jtdBaseurl=${JSON.stringify(bu)};</script>\n` : "") + ` <script src="${escapeMarkupAndQuotes(relativeUrl("/assets/js/just-the-docs.js", bu))}"></script>\n` + - (page.hasSvg - ? ` <script src="${escapeMarkupAndQuotes(relativeUrl("/assets/js/svg-inline.js", bu))}" defer></script>\n` + (page.hasFigure + ? ` <script src="${escapeMarkupAndQuotes(relativeUrl("/assets/js/figure.js", bu))}" defer></script>\n` : "") + ` <meta name="viewport" content="width=device-width, initial-scale=1">\n` + headSeoBlock(page, site) + @@ -350,6 +351,22 @@ const SVG_SYMBOLS_COPY = `<!-- Bootstrap Icons. MIT License: https://github.com/ // Port of theme's _includes/icons/icons.html. The search icon is // conditional; the copy-code icons ship unconditionally. +// +// FIGURE_SPRITE is the figure controls bar's glyphs (render.mjs, figureButton), +// written only on a page with a figure. Feather's style on its 24 grid, at the +// 1.5 stroke of the theme toggle's icons; download, the clipboard and the +// magnifier follow Feather's own, and vector (a curve on two anchors, with its +// handles) and raster (pixels) are drawn for this bar. +const FIGURE_SPRITE = ` <svg xmlns="http://www.w3.org/2000/svg" class="d-none"> +<symbol id="fig-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol> +<symbol id="fig-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/></symbol> +<symbol id="fig-copied" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/><polyline points="8.5 14 11 16.5 15.5 11.5"/></symbol> +<symbol id="fig-zoom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16" y2="16"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></symbol> +<symbol id="fig-close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></symbol> +<symbol id="fig-vector" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="15" width="4" height="4" rx=".5"/><rect x="18" y="15" width="4" height="4" rx=".5"/><path d="M4 15C4 9 8 6 12 6s8 3 8 9"/><line x1="4" y1="6" x2="20" y2="6"/><circle cx="4" cy="6" r="1.3" fill="currentColor"/><circle cx="20" cy="6" r="1.3" fill="currentColor"/></symbol> +<symbol id="fig-raster" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><g fill="currentColor" stroke="none"><rect x="3.5" y="3.5" width="5.5" height="5.5"/><rect x="15" y="3.5" width="5.5" height="5.5"/><rect x="9" y="9" width="6" height="6"/><rect x="3.5" y="15" width="5.5" height="5.5"/><rect x="15" y="15" width="5.5" height="5.5"/></g></symbol> +</svg>`; + function buildSvgSprites(config) { const searchEnabled = config.search_enabled !== false; const parts = [ diff --git a/builder/theme-pictures.mjs b/builder/theme-pictures.mjs new file mode 100644 index 000000000..ff6b2c839 --- /dev/null +++ b/builder/theme-pictures.mjs @@ -0,0 +1,133 @@ +// A picture in two themes: `X.png`, the dark picture, and beside it `X.light.png`, the light +// one. A page names only `X.png`; the build pairs the two by name. Online and offline, the +// renderer (render.mjs's themePairPlugin) writes both, each with a class that the stylesheet +// hides in the other theme (custom.scss, `.pic-light` and `.pic-dark`), and both lazy, so the +// browser fetches only the one it shows; the light one's width and height are the page's +// scaled by the two files' sizes (pairSizes). The book keeps the light one alone (lightOnly, +// from book.mjs). WIP.Build.md says why it is done this way. +// +// A picture may also have been drawn as SVG (shoot_docs.mjs --svg): `X.svg` beside `X.png`, +// `X.light.svg` beside `X.light.png`. The renderer shows each theme's SVG where it exists, +// unless the page's attribute block says `png`; the PNGs stay, as the SVGs' reference. + +import { open } from "node:fs/promises"; +import { replaceOutsideCode } from "./code-guard.mjs"; + +export const PIC_LIGHT = "pic-light"; +export const PIC_DARK = "pic-dark"; + +const LIGHT_RE = /\.light\.png$/; +const DARK_RE = /\.png$/; + +// Whether a path (a source path, or a URL's path) names a light picture, PNG or SVG. +export const isLightPicture = (p) => /\.light\.(?:png|svg)$/.test(p); + +// The SVG of a picture's source path (X.svg for X.png), or null when it is no PNG. +export const svgOf = (rel) => (DARK_RE.test(rel) ? rel.replace(DARK_RE, ".svg") : null); + +// Whether a static .svg is a picture's SVG rather than a diagram: it has its PNG beside it. +export const isPictureSvg = (rel, staticFiles) => + rel.endsWith(".svg") && staticFiles.has(rel.replace(/\.svg$/, ".png")); + +// Which outputs show a picture's SVG in place of its PNG (_config.yml `picture_svg`): online, +// offline (which is also the help add-in's archive) and book, each true unless set false. +export function pictureSvgOutputs(config) { + const c = config?.picture_svg ?? {}; + return { online: c.online !== false, offline: c.offline !== false, book: c.book !== false }; +} + +const SVG_URL = /\b(src|href)="([^"?#]+)\.svg((?:[?#][^"]*)?)"/; + +// Rendered HTML with each picture's SVG named back as its PNG, for an output that shows the +// PNGs. The renderer shows the SVG wherever one exists and the page did not ask for `png`, +// and only then, so a src or href naming a picture's SVG (isPictureSvg) is one it swapped; +// a diagram's SVG has no PNG and is left as it is, and so is code. URLs are root-absolute +// under `baseurl`, as the renderer writes them. +export function pngPictures(html, staticFiles, baseurl = "") { + if (!html.includes(".svg")) return html; + const prefix = `${baseurl}/`; + return replaceOutsideCode(html, SVG_URL, (m, attr, stem, tail) => { + if (!stem.startsWith(prefix)) return m; + let rel = `${stem.slice(prefix.length)}.svg`; + try { + rel = decodeURIComponent(rel); + } catch {} + return isPictureSvg(rel, staticFiles) ? `${attr}="${stem}.png${tail}"` : m; + }); +} + +// The light sibling of a picture's source path, or null when it cannot have one. +export const lightSiblingOf = (rel) => + DARK_RE.test(rel) && !LIGHT_RE.test(rel) ? rel.replace(DARK_RE, ".light.png") : null; + +// The light pictures among `rels` (source paths) with no dark picture beside them. +export function unpairedLightPictures(rels) { + const all = new Set(rels); + return rels.filter((r) => LIGHT_RE.test(r) && !all.has(r.replace(LIGHT_RE, ".png"))).sort(); +} + +// A PNG file's width and height in pixels, from its header; null when it is no PNG. +async function pngSize(file) { + const fh = await open(file, "r"); + try { + const b = Buffer.alloc(24); + const { bytesRead } = await fh.read(b, 0, 24, 0); + if (bytesRead < 24 || b.toString("latin1", 12, 16) !== "IHDR") return null; + return [b.readUInt32BE(16), b.readUInt32BE(20)]; + } finally { + await fh.close(); + } +} + +// For each light picture among the static files ({ srcRel, srcPath }), its size and its dark +// picture's, in pixels: { "<light srcRel>": [lightW, lightH, darkW, darkH] }. The Light theme +// lays the IDE out a few pixels differently, so a light screenshot can be a little off the +// dark one's size; the renderer scales the page's width and height by it, so that each +// picture is shown at the same scale as the other. Read on the main thread, once, from the +// headers alone. +export async function pairSizes(staticFiles) { + const byRel = new Map(staticFiles.map((s) => [s.srcRel, s.srcPath])); + const out = Object.create(null); + for (const [rel, file] of byRel) { + if (!LIGHT_RE.test(rel)) continue; + const dark = byRel.get(rel.replace(LIGHT_RE, ".png")); + if (!dark) continue; + const [l, d] = await Promise.all([pngSize(file), pngSize(dark)]); + if (l && d) out[rel] = [...l, ...d]; + } + return out; +} + +// The light image's width and height, as the page's for the dark one scaled by the two +// pictures' sizes: `sizes` is pairSizes' entry. A value that is not a whole number of pixels +// (a percentage, an em) is left as it is. +export function lightDimension(value, light, dark) { + if (!/^\d+$/.test(String(value)) || !dark) return value; + return String(Math.round((Number(value) * light) / dark)); +} + +// A tag whose class list holds `name`. +const tagWithClass = (tag, name) => + new RegExp(String.raw`<${tag}\b[^>]*\sclass="(?:[^"]*\s)?${name}(?:\s[^"]*)?"[^>]*>`); + +const DARK_IMG = tagWithClass("img", PIC_DARK); +const DARK_LINK = new RegExp(String.raw`${tagWithClass("a", PIC_DARK).source}[\s\S]*?<\/a>`); +const LIGHT_TAG = new RegExp(`${tagWithClass("img", PIC_LIGHT).source}|${tagWithClass("a", PIC_LIGHT).source}`); + +// Takes `name` out of the class attribute of the tag `tag`, and the attribute with it when +// nothing is left. +const dropClass = (tag, name) => + tag.replace(/\sclass="([^"]*)"/, (_, list) => { + const kept = list.split(/\s+/).filter((c) => c && c !== name); + return kept.length ? ` class="${kept.join(" ")}"` : ""; + }); + +// Rendered HTML with each pair reduced to its light picture, as a plain image: the dark image +// and the dark link go, and the light ones lose their class and their `loading="lazy"` (the +// book's paged.js raises on an image that has not loaded when it breaks the pages). +export function lightOnly(html) { + if (!html.includes(PIC_DARK)) return html; + let out = replaceOutsideCode(html, DARK_LINK, () => ""); + out = replaceOutsideCode(out, DARK_IMG, () => ""); + return replaceOutsideCode(out, LIGHT_TAG, (tag) => dropClass(tag, PIC_LIGHT).replace(/\sloading="lazy"/, "")); +} diff --git a/check.bat b/check.bat index 6045f0753..60c6c6302 100644 --- a/check.bat +++ b/check.bat @@ -13,6 +13,14 @@ @rem passes. Refuse instead, naming build.bat. node scripts/check_tree_fresh.mjs @if errorlevel 1 goto :fail +@rem check_a11y runs here, before the lighter gates below, because the +@rem deploy workflow backgrounds the book PDF render and the composite +@rem action runs check_a11y as its FIRST step -- so the two Chromium +@rem workloads overlap just at the start. The gate roster check requires +@rem check.bat's order to match the composite action's order after filtering +@rem to the gates both have, so a11y lives here in both. +node scripts/check_a11y.mjs +@if errorlevel 1 goto :fail @rem Graphviz sizes its boxes from a width table; the browser paints the @rem text with a real font. Nothing in the build compares the two, so a @rem mismatch ships as a label hanging out of its box on a green build. @@ -22,8 +30,6 @@ node scripts/check_dot_fit.mjs @if errorlevel 1 goto :fail node scripts/pick_a11y_sample.mjs --check @if errorlevel 1 goto :fail -node scripts/check_a11y.mjs -@if errorlevel 1 goto :fail @popd @exit /b 0 :fail diff --git a/docs/Documentation/Authoring.md b/docs/Documentation/Authoring.md index d4c266eed..8f675ffc6 100644 --- a/docs/Documentation/Authoring.md +++ b/docs/Documentation/Authoring.md @@ -271,7 +271,7 @@ The repair is a re-levelling of the whole page, not a patch to one heading. The The repair is conditional, and the condition is easy to break without noticing. **The plugin runs only on a page that uses `#` and `###` and no `##` anywhere** --- a single `##` and it does not run at all. More than four hundred pages on this site are currently in that state, so on most of them, adding one `##` section disarms the normalizer for the whole page: every `###` that was already there stops being repaired and becomes a live heading-order defect, in the same edit that added a correctly-levelled section. -Nothing tells you. The build still succeeds --- heading order is not one of the things it checks --- and `check.bat`'s accessibility scan audits thirteen sample pages out of roughly 1,160, so unless you happened to edit one of those thirteen, the defect ships. +Nothing tells you. The build still succeeds --- heading order is not one of the things it checks --- and `check.bat`'s accessibility scan audits fourteen sample pages out of roughly 1,160, so unless you happened to edit one of those fourteen, the defect ships. So when you add a section to a page whose headings start at `###`, pick one of these and finish it: @@ -467,7 +467,7 @@ That failure is not hypothetical. The attribute reference once shipped all six o its admonitions as the literal text `[!NOTE]`, because the same pairing closed that page's opening fence on a marker in the middle of a line and every pairing after it was off by one. The mid-line case is fixed --- the rewrite now asks the -site's parser where the fences are --- and [`check_code_regions.mjs`](Tools#check-code-regions), which +site's parser where the fences are --- and [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions), which `test.bat` runs, is the only gate that sees this class of fault at all. ### Typography @@ -478,7 +478,7 @@ The site ships its own fonts, so a page renders the same on every platform: **In Those two example glyphs are, deliberately, the only characters on the whole site that fall back --- so the ✔ and ⋮ above are being drawn by your system's symbol font right now, next to text that is not. That difference in weight and shape is the effect this section is about. -Diagram exports carry the font with them. The Download / Copy SVG and PNG buttons above each diagram embed the typeface into the exported file, because an exported SVG has no access to the site's stylesheet and would otherwise render in whatever the viewer has installed. All four buttons work on every diagram. +Diagram exports carry the font with them. The Download / Copy SVG and PNG buttons of each diagram embed the typeface into the exported file, because an exported SVG has no access to the site's stylesheet and would otherwise render in whatever the viewer has installed. All four buttons work on every diagram. **Do not hand-edit a diagram's `.svg`.** It is a build artifact: the `.dot` beside it is the source, and the next build overwrites your edit. Changing the face is the edit that looks most harmless and is not --- Graphviz sizes each box to the text it measured, so a diagram whose labels are painted in a font the layout never saw has text hanging outside its boxes. `check.bat` fails on that; see [Diagrams](#diagrams) below. @@ -714,7 +714,7 @@ only half marked rather than reporting a missing symbol in the sample that is fi sample carrying one would simply never be compiled; the tool reports an unrecognised token rather than skipping it in silence. -[Tools and Scripts](Tools#check-examples) covers running it --- the census and survey +[Code Samples and Attributes](Tools-Samples#check-examples) covers running it --- the census and survey modes, the flags, and what the report means. ## Bullet lists, dashes, and parentheses @@ -836,6 +836,8 @@ Name the font as the existing diagrams do --- copy the `node`, `edge` and `graph Two things to leave alone. Node text is deliberately dark against each node's light fill in both themes; only the cluster and edge labels follow the page's text colour, because those are the ones sitting on the page background. And a diagram needs alt text like any other image --- the whole diagram gets one accessible name, so describe what it shows, not what shapes it contains, and never `![Diagram]`. Everything under [Images](#images) about what makes alt text worth having, and about nothing in the build being able to tell you, applies here unchanged. +A box can link somewhere: give its node, or its cluster, a `URL`, relative to the page that shows the diagram (`URL="Tools-Build#tbdocs"`). The diagram is then a group of links rather than one image, and the build moves each cluster's label just before the first box inside it, so that a reader, and the Tab key, takes each label and then its boxes. Declare the nodes in reading order and set `outputorder=nodesfirst` on the graph, so that the boxes come in that order too. `docs/Documentation/Images/Tools-Groups.dot` does both. The build's link check reports a link that leads nowhere, as it does for any other link. + ## Images Images live in an `Images/` folder beside the page that uses them, and are referenced by a relative path: @@ -846,7 +848,7 @@ Images live in an `Images/` folder beside the page that uses them, and are refer **The text in the square brackets is the image's alt text, and every image needs one.** Write what a reader who cannot see the picture would otherwise miss --- what the screenshot shows and which part of it the surrounding prose is pointing at --- rather than a label for the file. A caption-length phrase is usually right; the file name, the word *screenshot*, and the name of the button you happened to be looking for are not. -Nothing in the build will correct you, and it is worth knowing exactly why. The accessibility scan's `image-alt` rule asks only whether an accessible name *exists*, never whether it is the right one. `![image](…)` passes it. So does `![img](…)`. So does an empty `![](…)`, because an empty alt is the explicit signal that an image is decorative and a screen reader should skip it --- true of a spacer or a bullet glyph, never true of a screenshot. A green `check.bat` therefore says only that something is present in the brackets. Whether it describes the picture is a judgement no gate here makes, and the scan samples thirteen pages in any case, so most images are never looked at by it at all. +Nothing in the build will correct you, and it is worth knowing exactly why. The accessibility scan's `image-alt` rule asks only whether an accessible name *exists*, never whether it is the right one. `![image](…)` passes it. So does `![img](…)`. So does an empty `![](…)`, because an empty alt is the explicit signal that an image is decorative and a screen reader should skip it --- true of a spacer or a bullet glyph, never true of a screenshot. A green `check.bat` therefore says only that something is present in the brackets. Whether it describes the picture is a judgement no gate here makes, and the scan samples fourteen pages in any case, so most images are never looked at by it at all. One habit to avoid in particular: copy the *shape* of a neighbouring page's image markdown, never its alt text. The example above used to read `![Create Package]`, and that one string travelled from it onto ten further images across the site --- a references dialog, a toolbox, a properties window, an animated GIF --- describing none of them, on pages that passed every check the project has. They have since been rewritten, but nothing except a reader would ever have found them. @@ -862,6 +864,48 @@ There are three reasons, in increasing order of severity: [`build.bat`](Building#checking-link-integrity) enforces this. A remote `<img>` --- `http://`, `https://`, or protocol-relative `//host/...` --- is reported as `remote-asset:` and fails the run. +### A figure's buttons + +An image that stands alone in its paragraph is shown as a *figure*: the image with a bar of buttons to download it, copy it and zoom it. That holds for a diagram, a picture in both themes and any other PNG, JPEG, GIF or WebP, with no markup of yours. An image inside a sentence, or inside a link, is a plain image and gets no bar, so put an image you want to be zoomable on a line of its own. + +Give a picture its `{:width="..." height="..."}`. The build uses the size to decide where the bar goes: a figure at least as tall as the bar stood on end gets the bar beside it, in the margin of a wide window and in view while the figure scrolls past, and a shorter figure gets the bar above it or at its top right. A picture with no stated size is treated as short: its bar stays above it, however tall it is. The decision is made in the build, so nothing on the page moves after it loads. + +### A picture in both themes + +A screenshot of the IDE shows one of its themes, and the site has two. To give a picture a version for the site's light theme, save it beside the first one under the same name with `.light` before the extension: + +```text +docs/IDE/Images/Watches.png <-- the dark picture: the page names this one +docs/IDE/Images/Watches.light.png <-- the light picture: the page never names it +``` + +Keep the page as it is, naming `Watches.png` with its `{:width height}`. The build finds the light picture by its name and puts both into the page: a reader sees the light one in the light theme and the dark one in the dark theme, whether the theme was chosen with the toggle or follows the operating system, and the browser downloads only the one it shows. The PDF book has the light picture only. + +Three things to get right: + +- **Never name the `.light.png` in a page**, in an image, a link or raw HTML. The build stops and says which page did it, because that picture would then show in both themes. +- **Take the two pictures at the same scale.** The width and height in the page are for the dark picture, and the build shows the light one at the same scale: a light picture 8 pixels narrower is shown 4 pixels narrower when the page halves the dark one. The IDE's light theme lays some things out a few pixels smaller, so the two need not be exactly the same size. +- **A `.light.png` needs its dark picture beside it.** One without stops the build, because it would never be shown. + +The IDE's own screenshots come from `scripts/shoot_docs.mjs`, which takes each one in both themes and writes both files; it keeps no light picture that looks exactly like the dark one. Describe a picture in words that are true in both themes: its alt text and the prose around it are the same for both. "The Run button" is true of both, while "the white Run button" is not. + +### A picture as SVG + +Most of the IDE's screenshots are also kept as SVG, beside their PNGs under the same name: + +```text +docs/IDE/Images/Watches.png <-- the page names this one +docs/IDE/Images/Watches.svg <-- shown in its place +docs/IDE/Images/Watches.light.png +docs/IDE/Images/Watches.light.svg <-- shown in the light theme +``` + +`scripts/shoot_docs.mjs --svg` draws each SVG from the IDE's page at the moment it takes the PNG. Its text is real text, in the fonts the IDE drew it with, so the picture stays sharp at any zoom, and in the PDF book its text can be selected and searched. The page still names `Watches.png`: the build shows the SVG in its place wherever there is one, in each theme. + +- **Name the PNG, never the SVG.** The PNG stays in the repository. The SVG is checked against it when it is made, and an output that shows PNGs uses it. Which outputs show the SVGs is set by `picture_svg` in `docs/_config.yml`: the website and the PDF book do, and the offline copy and the IDE help add-in's archive show the PNGs. The SVGs would make that archive half as large again. +- **To keep one picture's PNG everywhere**, put `png` in its attribute block: `![The Watches panel](Images/Watches.png){:width="190" height="337" png}`. The build takes the word out of the page. +- **Do not edit a picture's SVG.** Take the picture again. `shoot_docs` writes an SVG only when it matches its PNG closely. One that has more than 0.25% of its pixels far from the PNG's is not written, and an older one is removed, so the page shows the PNG instead. + ### Videos Link the video and mark it `.video`: @@ -924,7 +968,7 @@ Three severities, used distinctly: A known defect in twinBASIC follows the same scale. A defect that silently gives a wrong result --- a function that returns the wrong value and raises no error --- is a `> [!WARNING]` until a fixed build is released, because nothing tells the reader that their data is wrong. A defect that crashes the program is a `> [!WARNING]` too, because whatever it had not saved is lost. So is a defect whose error hides a silent part, such as a procedure that runs twice before the error is raised. A defect the reader cannot miss and that loses nothing, such as an error raised or a statement the compiler refuses, is a `> [!NOTE]`. Either callout names the build it applies to. Once a fixed build is out, the callout is removed, and the page describes the correct behaviour with no mention of the defect: the documentation is not a record of past defects. -Where twinBASIC and VB6 behave differently, say what VB6 does. Check it in VB6 --- [`vb6run.mjs`](Tools#vb6run) builds and runs a sample there --- rather than writing it from memory. Where twinBASIC's behaviour is the intended one, the VB6 behaviour goes in a `> [!NOTE]`. Inside a defect's `> [!WARNING]` it is one sentence, such as "VB6 raises error 6, *Overflow*." +Where twinBASIC and VB6 behave differently, say what VB6 does. Check it in VB6 --- [`vb6run.mjs`](Tools-Defects#vb6run) builds and runs a sample there --- rather than writing it from memory. Where twinBASIC's behaviour is the intended one, the VB6 behaviour goes in a `> [!NOTE]`. Inside a defect's `> [!WARNING]` it is one sentence, such as "VB6 raises error 6, *Overflow*." Use one callout per concern, and reserve them for genuine notes --- plain "why this is useful" prose should stay a plain paragraph. diff --git a/docs/Documentation/Builder.md b/docs/Documentation/Builder.md index e1b04885e..c5a3e851d 100644 --- a/docs/Documentation/Builder.md +++ b/docs/Documentation/Builder.md @@ -76,6 +76,7 @@ Modules grouped by role. Each entry has one line; deep-dive in [Pipeline Stages] | [`url.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/url.mjs) | The URL helpers every other module shares: `absoluteUrl` and `relativeUrl` (ports of Jekyll's two URL filters), `normalizeBaseurl`, `encodeSpaces`, `splitFragment`. | | [`escape.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/escape.mjs) | The escapers every other module shares: `escapeMarkup` (`&`, `<`, `>`), `escapeMarkupAndQuotes` (those and both quotes), `escapeRegExp`. | | [`code-guard.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/code-guard.mjs) | The code guard for a rewrite of rendered HTML: `CODE_OR_PRE`, which consumes a `<code>` or `<pre>` element whole, and `replaceOutsideCode`. | +| [`theme-pictures.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/theme-pictures.mjs) | A picture in both themes, `X.png` with `X.light.png` beside it: the two class names the stylesheet hides by theme, `lightSiblingOf`, `unpairedLightPictures` (which `discover` refuses), and `lightOnly`, which reduces each pair to its light picture for the book. A picture drawn as SVG as well, `X.svg` beside `X.png`: `svgOf`, `isPictureSvg` (an `.svg` with its `.png` beside it, which is never inlined as a diagram), `pictureSvgOutputs` (which outputs show the SVGs, from `picture_svg` in `_config.yml`) and `pngPictures`, which names each picture's SVG back as its PNG in the HTML of an output that shows PNGs; see [A picture as SVG](Authoring#a-picture-as-svg). | | [`book.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/book.mjs) | Chapter selector resolution (Phase 2 half) + `book.html` assembly (Phase 8 half). | | [`build-info.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/build-info.mjs) | Git commit hash + date capture. Runs on a worker so the shell-outs hide behind the main spine. | | [`data.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/data.mjs) | Loads `_book.yml`. | @@ -93,7 +94,7 @@ Modules grouped by role. Each entry has one line; deep-dive in [Pipeline Stages] | File | Role | |---|---| -| [`render.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/render.mjs) | markdown-it configuration + plugin stack + `renderPhase`. Built once on main and once per worker. The plugins worth knowing by name: `svgInlinePlugin` (build-time SVG embedding), `videoLinkPlugin` (a `{: .video }` link becomes a locally vendored poster frame), `remoteImagePlugin` (a user-attachment URL becomes the vendored copy, in both markdown and raw `<img>` syntax), and `headingLevelNormalizePlugin` (renumbers a page that uses h1 and h3 but no h2, so the built page has no heading skip). | +| [`render.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/render.mjs) | markdown-it configuration + plugin stack + `renderPhase`. Built once on main and once per worker. The plugins worth knowing by name: `figurePlugin` (an image alone in its paragraph becomes a figure with a bar of download, copy and zoom buttons; a diagram's SVG is embedded at build time), `videoLinkPlugin` (a `{: .video }` link becomes a locally vendored poster frame), `remoteImagePlugin` (a user-attachment URL becomes the vendored copy, in both markdown and raw `<img>` syntax), `themePairPlugin` (an image or link whose `X.png` has an `X.light.png` beside it becomes two, the light one shown in the light theme and the dark one in the dark, and each names its theme's SVG where there is one and the attribute block does not say `png`; see [A picture in both themes](Authoring#a-picture-in-both-themes)), and `headingLevelNormalizePlugin` (renumbers a page that uses h1 and h3 but no h2, so the built page has no heading skip). | | [`highlight.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/highlight.mjs) | Shiki bootstrap + the bundled twinBASIC grammar. Emits the just-the-docs wrapper structure. | | [`highlight-theme.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/highlight-theme.mjs) | Loads `Light.theme` + `Dark.theme`, emits `tb-highlight.css` + scope-to-class lookup. Clamps any token colour that falls below 4.5:1 against the code-block background --- moving lightness away from the background while preserving hue and saturation --- so highlighted code meets WCAG AA; the emitted rule includes a `raised to 4.5:1` comment naming the original colour. | | [`template.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/template.mjs) | `templatePhase` (per-page layout wrap) + `buildInitConfig` + `renderSidebar` + `withPartialNav`, which cuts an offline page's nav down to the part of the tree the page opens with, + `navScript`, the text of the offline tree's `assets/js/nav.js`, which holds the whole tree. JS template literals; no template engine. Also `injectAnchorHeadings(html, headingsOut)`, which adds the permalink icon to each heading and collects the heading list as it goes, and `renderSectionLinks`, which spends that list on the per-page disclosure at the top of the footer. | @@ -117,20 +118,20 @@ Modules grouped by role. Each entry has one line; deep-dive in [Pipeline Stages] |---|---| | [`offline.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/offline.mjs) | Offline-tree writer + just-the-docs.js AST patcher + `search-data.js` wrapper. | | [`offline-rewrite.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/offline-rewrite.mjs) | Pure rewrite helpers (`deriveOfflinePage`, CSS url() rewrite, site-path set construction). Worker-safe; no node:fs dependency. | -| [`pdf.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/pdf.mjs) | `_site-pdf/` writer: `book.html` + `tb-highlight.css` + `print.css` + referenced images. | +| [`pdf.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/pdf.mjs) | `_site-pdf/` writer: `book.html` + `tb-highlight.css` + `print.css` + referenced images. A picture in both themes is its light picture alone in the book (`lightOnly` in `theme-pictures.mjs`, applied by `book.mjs`). | **Verification** | File | Role | |---|---| -| [`link-check.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/link-check.mjs) | The pure core: HTML in, findings out. No filesystem traversal and no CLI, which is what lets the build's fused check and the standalone [`scripts/check_links.mjs`](Tools#check-links) share one implementation. | +| [`link-check.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/link-check.mjs) | The pure core: HTML in, findings out. No filesystem traversal and no CLI, which is what lets the build's fused check and the standalone [`scripts/check_links.mjs`](Tools-Site-Checks#check-links) share one implementation. | | [`check.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/check.mjs) | The check over rendered pages, which `scripts/check_links.mjs` runs too: per-chunk `checkChunk`, the `joinChunks` merge and `findingsFor`. Also the build's `TREES` table, report formatting, and the `--check-audit-index` tree-index audit. | | [`check-tree.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/check-tree.mjs) | `deriveTreeRels` --- every relative path the build believes it emitted into a tree. The check's existence oracle is built from this rather than from a `readdir`, which is why the audit exists to keep it honest. | | [`counts.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/counts.mjs) | The registry behind `{{tbdocs:<name>}}` in prose. `deriveCounts(state)` computes every name from build state; `countPlugin` substitutes them as a core rule over the inline token stream, which is what makes code immune without a rule for it. `validateCountNames` rejects an unknown name on main before any worker renders, and `findSurvivingPlaceholder` rejects one that reached the output --- two checks because they fail differently. See [Authoring Pages](Authoring#counts). | | [`page-baseline.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/page-baseline.mjs) | The page-count drift guard, and the committed `page-baseline.json` it compares against. A rise rewrites the file, a fall fails the build, and neither CI nor `--serve` may write. See [Building and Deployment](Building#the-page-count-drift-guard). | | [`symbol-baseline.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/symbol-baseline.mjs) | The symbol index's drift guard, and the committed `symbol-baseline.json`, every URL `tB/symbols.json` has published. A new URL rewrites the file, a lost one fails the build. | | [`addin-index.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/addin-index.mjs) | The help add-in's committed copy of the index, `add-in/Resources/SYMBOLS/symbols.json`: a local build rewrites it when it differs from `tB/symbols.json`, and CI and `--serve` only say so. | -| [`addin-project.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/addin-project.mjs) | The help add-in's project file, a published download: packs the files git tracks in `add-in/`, with this build's symbol index, through `importProject` from `scripts/impexp.mjs`, the one module under `scripts/` the builder may import. See [Tools and Scripts](Tools#the-help-add-ins-project-file). | +| [`addin-project.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/addin-project.mjs) | The help add-in's project file, a published download: packs the files git tracks in `add-in/`, with this build's symbol index, through `importProject` from `scripts/impexp.mjs`, the one module under `scripts/` the builder may import. See [Build and Generation Tools](Tools-Build#the-help-add-ins-project-file). | | [`baseline.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/baseline.mjs) | The read, the write and the comparison the two drift guards share, and `GUARDED_SRC`, the one source tree they have figures for. | | [`publish-policy.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/publish-policy.mjs) | The allowlist of file types that may reach a published tree. Enforced twice, unconditionally: over the static-file inventory in `discover`, and over each tree's `deriveTreeRels` inventory in `dispatch`. A finding aborts the build. See [Drift guards](#drift-guards-and-failure-modes). | @@ -293,8 +294,8 @@ dispatch ┬→ render:0 ┬→ flush:0 ┐ - `writeAssets` (main) --- writes generated CSS, copies vendored theme JS, copies the project's static files. Page HTML is *not* written here --- the per-chunk `flush:i` tasks already did that. Depends on `prepPageDirs` so the directory tree exists. - `searchData` (main) --- concatenates `state.searchChunks` (already populated by each `render:i`'s `submit()`), joins `symbolIndex`'s symbols onto the matching entries by URL (adding the `names` / `qualified` / `primary` fields; see [Site search](Pipeline-Stages#searchdata-main) and `WIP.Search.md`'s "Design" §2), renumbers the global `i` index, and writes `search-data.json`. The heavy work (heading split, content sanitisation, URL encoding) ran on the workers; this task only joins and consolidates. Depends on `renderJoin`, `prepDest` and `symbolIndex` (for the join). - `symbolIndex` (main) --- writes `tB/symbols.json`, the [symbol index](Building#the-symbol-index) the IDE help add-in reads, and hands its `symbols` array to `searchData`'s join. Reads the heading ids out of every `/tB/` page's `renderedContent`, joins them with the committed `builder/package-api.json`, and returns the index's URLs for the drift guard that `runBuild` runs once the build is done. Depends on `renderJoin` and `prepDest`; `checkReport` waits for it, because the file is in the online tree's index; `searchData` and `addinProject` wait for it too. -- `helpArchive` (main) --- writes `add-in/Resources/HELP/site.zip`, the archive of the offline tree that the IDE help add-in embeds (see [Tools and Scripts](Tools#the-help-archive)). Depends on `flushJoin`, `scss` and `writeOffline`, which between them write everything in the offline tree, and runs before the Gantt chart is injected, so the archive holds `BuildInfo.html` without the chart. Does nothing under `--serve` or `--dry-run`, for a source tree other than `docs`, for a `--dest` other than `docs/_site`, without an offline tree, or with `--no-help-archive`. After the Gantt chart is injected, `runBuild` checks that the offline tree still lists the archive's files and differs from it in `BuildInfo.html` and `assets/images/gantt.svg` alone. -- `addinProject` (main) --- packs the help add-in's source folder, `add-in/`, into `tbDocsHelp.twinproj` and writes it into the online tree at the path `_config.yml`'s `addin_project` names (see [Tools and Scripts](Tools#the-help-add-ins-project-file)). The files are those git tracks there, the symbol index in it is the one `symbolIndex` wrote, and `Resources/HELP/site.zip` is the archive `helpArchive` wrote, if it wrote one. Depends on `symbolIndex`, `prepDest` and `helpArchive`; `dispatch` has already put the file in the online tree's index, so `checkReport` waits for it. Does nothing under `--dry-run`, or for a config that declares no download. +- `helpArchive` (main) --- writes `add-in/Resources/HELP/site.zip`, the archive of the offline tree that the IDE help add-in embeds (see [Build and Generation Tools](Tools-Build#the-help-archive)). Depends on `flushJoin`, `scss` and `writeOffline`, which between them write everything in the offline tree, and runs before the Gantt chart is injected, so the archive holds `BuildInfo.html` without the chart. Does nothing under `--serve` or `--dry-run`, for a source tree other than `docs`, for a `--dest` other than `docs/_site`, without an offline tree, or with `--no-help-archive`. After the Gantt chart is injected, `runBuild` checks that the offline tree still lists the archive's files and differs from it in `BuildInfo.html` and `assets/images/gantt.svg` alone. +- `addinProject` (main) --- packs the help add-in's source folder, `add-in/`, into `tbDocsHelp.twinproj` and writes it into the online tree at the path `_config.yml`'s `addin_project` names (see [Build and Generation Tools](Tools-Build#the-help-add-ins-project-file)). The files are those git tracks there, the symbol index in it is the one `symbolIndex` wrote, and `Resources/HELP/site.zip` is the archive `helpArchive` wrote, if it wrote one. Depends on `symbolIndex`, `prepDest` and `helpArchive`; `dispatch` has already put the file in the online tree's index, so `checkReport` waits for it. Does nothing under `--dry-run`, or for a config that declares no download. - `writeAux` (main) --- writes redirect stubs + sitemap + robots.txt. Depends on `writeAssets`, `searchData`, `flushJoin`, `deriveRedirects`, `deriveSitemap`. - `writeOffline` (main) --- produces `_site-offline/`. The per-page offline HTML was already computed inside `render:i` and written by `flush:i`, so this task only handles the cross-cutting work: CSS url() rewriting, the just-the-docs.js AST patch, the `search-data.js` wrapper, the offline `nav.js`, theme assets, redirect stubs. - `writePdf` (main) --- assembles `_site-pdf/book.html` and copies the images it references. Depends on `flushJoin` (so `renderedContent` is filled), `resolveBookChapters` (so `bookData._chapters` is wired), and `dot` (so diagram SVGs are in `staticFiles`). @@ -387,29 +388,39 @@ Three flags on the pool make the reuse safe: `serve.mjs` writes to `docs/_serve/` --- disjoint from `build.bat`'s `_site/` family. A one-off `build.bat` run during a serve session never touches the tree the live preview is showing. -## SVG inlining +## Figures -Markdown `![alt](/assets/images/foo.svg)` references to build-local SVGs are replaced at render time with the SVG content inlined directly in the HTML. The feature removes the browser round-trip for separate SVG files and adds interactive controls (zoom, download, clipboard copy) to every inlined diagram. +An image that is the only content of its paragraph is rendered as a *figure*: a wrapper holding the image and a bar of icon buttons for downloading, copying and zooming it. Three kinds of image qualify: a diagram, a picture in two themes (the light and dark pair that `themePairPlugin` writes), and any other image (PNG, JPEG, GIF, WebP, or a picture's SVG). An image with text beside it in its paragraph, or inside a link, stays a plain `<img>`. + +A diagram is a build-local `.svg` with no `.png` beside it. Its SVG is inlined directly in the HTML, which removes the browser round-trip for a separate file and lets the page's fonts and colours reach its text. A picture keeps its `<img>` (two for a light and dark pair). The pipeline: -1. **`dispatch.execute()`** reads every `.svg` static file into a `svgContentsMap` keyed by `srcRel`. The map is packed into the shared SAB and broadcast to every render worker. +1. **`dispatch.execute()`** reads every `.svg` static file into a `svgContentsMap` keyed by `srcRel`, except a picture's SVG: one with its `.png` beside it is a screenshot, shown as an image ([A picture as SVG](Authoring#a-picture-as-svg)), and at tens of kilobytes each would only make the map larger. The map is packed into the shared SAB and broadcast to every render worker. 2. **`renderEnvInit`** on each worker unpacks `svgContentsMap` and passes it as `svgContents` to `createMarkdownIt`. -3. **`svgInlinePlugin`** in `render.mjs` overrides the markdown-it image renderer. When the image is the only content of its paragraph, its `src` ends in `.svg` and the file's content exists in `ctx.svgContents`, the plugin replaces the `<img>` tag with a wrapper holding five control buttons (Download SVG, Copy SVG, Download PNG, Copy PNG, Zoom) and a container with the raw SVG, and hides the paragraph around it. The plugin also sets `page.hasSvg = true`. -4. **`templatePhase`** conditionally includes `<script defer src="/assets/js/svg-inline.js">` on pages where `page.hasSvg` is true. +3. **`figurePlugin`** in `render.mjs` adds a core rule named `figures`. For each paragraph whose only content is an image (or the two images of a light and dark pair), the rule hides the paragraph tokens and wraps the image or images in new `figure_open` and `figure_close` tokens, whose renderer rules write the wrapper. For a diagram, meaning an image whose `src` ends in `.svg`, has no `.png` beside it and has its content in `ctx.svgContents`, `figure_open` also writes the SVG, and the image renderer returns an empty string for that image. The plugin sets `page.hasFigure = true`. +4. **`templatePhase`** writes the sprite of button glyphs (`FIGURE_SPRITE`, the `fig-*` symbols: download, copy, copied, zoom, close, vector, raster) into the page and includes `<script defer src="/assets/js/figure.js">` on pages where `page.hasFigure` is true. -The wrapper HTML emitted by `buildSvgWrapper`: +The wrapper for a diagram, abbreviated: ```html -<div class="svg-inline-wrap"> - <div class="svg-controls"> - <button type="button" class="btn-reset" data-action="download-svg" data-filename="...">Download SVG</button> - <button type="button" class="btn-reset" data-action="copy-svg">Copy SVG</button> - <button type="button" class="btn-reset" data-action="download-png" data-filename="...">Download PNG</button> - <button type="button" class="btn-reset" data-action="copy-png" data-filename="...">Copy PNG</button> - <button type="button" class="btn-reset" data-action="zoom-svg" aria-label="Zoom diagram">Zoom</button> +<div class="fig-wrap" data-kind="diagram"> + <div class="fig-controls"> + <span class="fig-group" data-format="vector"> + <svg><use href="#fig-vector"/></svg> + <button type="button" class="btn-reset" data-action="download-svg" aria-label="Download SVG" title="Download SVG"> + <svg><use href="#fig-download"/></svg> + </button> + <button type="button" class="btn-reset" data-action="copy-svg" aria-label="Copy SVG" title="Copy SVG">...</button> + </span> + <span class="fig-group" data-format="raster"> + <svg><use href="#fig-raster"/></svg> + <button type="button" class="btn-reset" data-action="download-png" ...>...</button> + <button type="button" class="btn-reset" data-action="copy-png" ...>...</button> + </span> + <button type="button" class="btn-reset" data-action="zoom" aria-label="Zoom" title="Zoom">...</button> </div> - <div class="svg-container" data-svg-src="..." role="img" aria-label="..."> + <div class="fig-container" data-svg-src="..." role="img" aria-label="..."> <svg>...</svg> </div> </div> @@ -417,9 +428,17 @@ The wrapper HTML emitted by `buildSvgWrapper`: `role` and `aria-label` are emitted only when the image has alt text. An image without any logs a warning at build time, since a diagram is never decorative. -`svg-inline.js` (~320 lines, no dependencies) handles the controls on the client: zoom (a fullscreen overlay, opened by clicking the diagram or its Zoom button and closed with Escape), SVG download (serialises the `<svg>` to XML), SVG clipboard copy, and PNG download and copy (renders the SVG to a 2048 px-wide canvas via `Image` + `toBlob`). The controls are hidden in print CSS. +A picture's wrapper has `data-kind="picture"` and its container holds the `<img>` (two for a light and dark pair) in place of the SVG. A picture drawn as SVG offers the same five buttons as a diagram. A raster image offers its raster group (Download PNG, JPEG or GIF by its type, and Copy PNG, or "Copy as PNG" for a type other than PNG) and Zoom. + +**`data-tall` is decided at build time.** The wrapper gets `data-tall` when the figure, as shown in the 736 px content column, is at least as tall as its bar stood on end: 220 px with both groups, 126 px with the raster group only. The size comes from the `{:width height}` of the picture, or from the width and height (in points) on the root `<svg>` of a diagram. A picture with no stated size is taken as short. The stylesheet stands the bar of a `data-tall` figure on end beside it from a window 1120 px wide, and makes it sticky, so that it stays in view while the figure scrolls past. By default the bar lies flat beside the figure at its top right when the column has room for both on one line, and above the figure, against the column's right edge, when it has not. Because the decision is made in the build, nothing moves after the page loads. + +`figure.js` (no dependencies) handles the buttons on the client. For a diagram, it serialises the inline `<svg>` to XML with Inter and Cascadia Mono embedded for Download SVG and Copy SVG, and renders it to a 2048 px-wide canvas via `Image` + `toBlob` for Download PNG and Copy PNG. For a picture, Download is a link to the file of the theme being shown (its `.svg`, or the `.png` beside it), Copy SVG copies that file's text, and Copy PNG copies the PNG's bytes, or, for a raster of another type, the image drawn on a canvas and encoded as PNG. A successful copy swaps the clipboard glyph for a ticked one for 1.5 seconds and announces the copy through a `role="status"` element, `#fig-export-status`. + +Zoom turns the container into a fixed overlay over the window, in the body's background colour, with a close button (`.fig-zoom-close`). Escape, a click on the figure or the close button ends it, and focus returns to what opened it. The figure is shown at twice its size on the page, fitted to the window's width and never smaller than on the page. A raster is never stretched past its own pixels, and a figure taller than the window scrolls. + +On a page opened from disk (`file://`, as the offline copy is), the browser can neither fetch a `file://` URL for the clipboard nor honour `download` on a `file://` link, so a picture's bar keeps only Zoom. The offline tree shows a picture as PNG, so its vector group is hidden in any case. A diagram's buttons work offline, because they serialise the SVG already in the page. In print the bar is hidden (`print.css`). -Only a lone image whose content is present in `svgContents` is inlined; an external URL, a missing file, or an image with text beside it in its paragraph falls through to the default `<img>` renderer. The markdown-it instance built on the main thread has no `svgContents` and never inlines. +Only a lone image becomes a figure. A diagram needs its content present in `svgContents`: an external URL or a missing file falls through to the default `<img>` renderer. The markdown-it instance built on the main thread has no `svgContents` and never inlines a diagram. ## Diagram geometry @@ -441,7 +460,7 @@ The residual 0.5% is kerning, which a per-character table cannot express. It err Every build emits an inline-SVG Gantt chart of its task timeline. [`gantt.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/gantt.mjs)'s `groupGanttTimings` sorts the scheduler's timings into a `Map<section, taskTiming[]>`, and its `renderGantt(grouped)` renders one SVG row per main-thread task plus one row per worker lane. The main-thread rows are grouped in four bands --- Seeds, Spine, Write and Check --- with Render's main-thread tasks in the Spine band and the worker rows between Spine and Write; the `Join` barriers are not drawn. Each worker's row has one coloured rectangle per task it ran, in the order they started, and the colour encodes the task's section. The start-up bars share that row, labelled `cold`, `warm` and `env`: the worker's cold start, drawn on the first build only, since on a rebuild the workers are already running, then `warmInit` and `renderEnvInit`. -The Gantt chart flows through the same SVG inlining pipeline as other diagrams. The [Build Info](BuildInfo) page contains a standard markdown image reference to a placeholder `gantt.svg`; during the render pass it becomes an inline SVG wrapper with zoom and export controls. After `writeOffline` completes, `tbdocs.mjs:injectGanttChart` locates the wrapper's `data-svg-src` marker in the rendered HTML and swaps the placeholder SVG content for the real Gantt chart. Both the online and offline copies of the page are patched; the on-disk `gantt.svg` file is also updated so the offline mirror's fallback stays current. The help archive and the help add-in's project file are made before the injection, and hold the placeholder, because the chart holds the build's own timings and would make every build's download differ. +The Gantt chart flows through the same figure pipeline as other diagrams. The [Build Info](BuildInfo) page contains a standard markdown image reference to a placeholder `gantt.svg`; during the render pass it becomes a figure with an inline SVG, a zoom button and export buttons. After `writeOffline` completes, `tbdocs.mjs:injectGanttChart` locates the wrapper's `data-svg-src` marker in the rendered HTML and swaps the placeholder SVG content for the real Gantt chart. Both the online and offline copies of the page are patched; the on-disk `gantt.svg` file is also updated so the offline mirror's fallback stays current. The help archive and the help add-in's project file are made before the injection, and hold the placeholder, because the chart holds the build's own timings and would make every build's download differ. When adding a new task to `TASKS`, give it a section in `GANTT_SECTION` in `gantt.mjs`: `Seeds`, `Spine`, `Render`, `Write` or `Check`. The build fails, naming the task, if a task has no section or one the chart does not draw. Only a `unique_per_worker` task needs none: its timings are drawn in each worker's lane. @@ -495,7 +514,7 @@ The site's `/assets/` tree at deploy time is assembled from three sources: | Source on disk | What lives there | Phase that delivers it | |---|---|---| -| `docs/assets/` | Project-owned content: the two SCSS entry points, project JS (`theme-toggle.js`, `svg-inline.js`), hand-written stylesheets (`print.css`, `just-the-docs-head-nav.css`), Graphviz/DOT diagrams (`.dot` sources + `.svg` renders), the self-hosted webfaces under `fonts/` (subset `.woff2` plus their OFL licences), and any content images contributors add. | Discovered by [`discover.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/discover.mjs), copied by `writeAssets`. | +| `docs/assets/` | Project-owned content: the two SCSS entry points, project JS (`theme-toggle.js`, `figure.js`), hand-written stylesheets (`print.css`, `just-the-docs-head-nav.css`), Graphviz/DOT diagrams (`.dot` sources + `.svg` renders), the self-hosted webfaces under `fonts/` (subset `.woff2` plus their OFL licences), and any content images contributors add. | Discovered by [`discover.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/discover.mjs), copied by `writeAssets`. | | `builder/vendor/just-the-docs/` | Vendored from the just-the-docs theme (v0.10.1), and patched in tree rather than held pristine: `_sass/` (the theme's SCSS sources, fed into the compilation --- 30 of its files differ from the v0.10.1 originals) and `assets/js/just-the-docs.js` (also patched) + `assets/js/vendor/lunr.min.js` (unmodified); both JS files are copied verbatim into the output. See [`builder/vendor/just-the-docs/README.md`](https://github.com/twinbasic/documentation/blob/main/builder/vendor/just-the-docs/README.md) for the file-by-file inventory, the re-vendoring procedure, and the in-tree patches --- which cover `_sass/` as well as `just-the-docs.js`. | `_sass/` consumed by [`scss.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/scss.mjs); `assets/` copied by `writeAssets`. | | Generated in-process | `just-the-docs-combined.css` (from `scss.mjs`, over the two entry points plus every partial under `docs/_sass/` --- see [Project styling](#project-styling)) and `tb-highlight.css` (from `highlight-theme.mjs`). Neither is committed; both are rebuilt every run. | Written by `scss` (combined CSS) and `writeAssets` (highlight CSS). | @@ -503,7 +522,7 @@ The fonts are committed artifacts, like the DOT renders: `scripts/build_fonts.py CSS files in either copy path get a baseurl rewrite (`url("/path")` → `url("<baseurl>/path")`) when the deployment baseurl is non-empty; the same transform applies to generated CSS so the `url("/favicon.png")` the SCSS entry point emits resolves correctly under sub-path deployments. -The project JS is deliberately small. `theme-toggle.js` implements the three-state (system / light / dark) theme switch as a progressive enhancement over the no-JS `prefers-color-scheme` default: the correct palette renders even with scripting disabled, and the script only adds the manual override that persists a `data-theme` choice. A page opened with `?theme=light` or `?theme=dark` takes that theme instead, as do the pages reached from it in the same tab, without changing the reader's stored choice; the IDE's help add-in passes the IDE's theme this way. It also passes `?pane=1`, which the same early script keeps for the tab and which hides the sidebar and the header, so that its narrow pane shows the page alone. Where the browser refuses the page its storage, as it does to the frame in the add-in's detached window, the early script keeps both parameters in the frame's `window.name` instead, and `theme-toggle.js` takes the theme from the page rather than from storage. `svg-inline.js` powers the click-to-zoom overlay and the download / copy controls on inlined diagrams. +The project JS is deliberately small. `theme-toggle.js` implements the three-state (system / light / dark) theme switch as a progressive enhancement over the no-JS `prefers-color-scheme` default: the correct palette renders even with scripting disabled, and the script only adds the manual override that persists a `data-theme` choice. A page opened with `?theme=light` or `?theme=dark` takes that theme instead, as do the pages reached from it in the same tab, without changing the reader's stored choice; the IDE's help add-in passes the IDE's theme this way. It also passes `?pane=1`, which the same early script keeps for the tab and which hides the sidebar and the header, so that its narrow pane shows the page alone. Where the browser refuses the page its storage, as it does to the frame in the add-in's detached window, the early script keeps both parameters in the frame's `window.name` instead, and `theme-toggle.js` takes the theme from the page rather than from storage. `figure.js` powers the zoom overlay and the download / copy controls of every figure. ## Project styling @@ -571,21 +590,21 @@ Prefer a selector that wins on its own terms over one that wins by source order. Run `serve.bat` and look at the page **in both themes**. This is not a formality: a dark-mode specificity revert is usually cosmetic, produces no error and no warning, and no gate catches it. Use the theme toggle rather than the OS setting, so the `[data-theme]` half is what gets exercised. Do not judge styling by opening a built page as a `file://` URL --- the online tree references its assets root-absolutely, so a `file://` page loads unstyled and any conclusion about colour or spacing drawn from it is worthless. -Then `build.bat && check.bat`. A malformed rule surfaces as an SCSS compile failure, which warns with the source location and flips the exit code rather than aborting --- so the previous build's CSS lingers in `_site/` and the site appears to still work; read the build output, do not judge by the page. `check.bat`'s accessibility scan covers every sample page in both themes for exactly the reason above, and its `target-size` and `color-contrast` rules are where a geometry or palette change lands. If the new component introduces markup the site has not used before, also add a construct family to `scripts/pick_a11y_sample.mjs` --- see [Tools and Scripts](Tools#pick-a11y-sample) --- or no axe rule keyed on it will run anywhere. +Then `build.bat && check.bat`. A malformed rule surfaces as an SCSS compile failure, which warns with the source location and flips the exit code rather than aborting --- so the previous build's CSS lingers in `_site/` and the site appears to still work; read the build output, do not judge by the page. `check.bat`'s accessibility scan covers every sample page in both themes for exactly the reason above, and its `target-size` and `color-contrast` rules are where a geometry or palette change lands. If the new component introduces markup the site has not used before, also add a construct family to `scripts/pick_a11y_sample.mjs` --- see [Site Checks](Tools-Site-Checks#pick-a11y-sample) --- or no axe rule keyed on it will run anywhere. ## Changing a typeface This follows on from [Project styling](#project-styling). The site uses three faces --- Inter for text, Cascadia Mono for code, and Source Serif 4 for the body text of the PDF book --- and the build names them in far more places than the stylesheet. Only some of those places fail loudly when one is missed. In the order a change would go: -1. **The font files** (all three faces). [`scripts/build_fonts.py`](Tools#build-fonts) downloads each face's pinned release, verifies its SHA-256, subsets it, and writes the `.woff2` files and their licences into `docs/assets/fonts/`. A new face is an entry in its `SOURCES` table --- the release URL, the hash and the licence --- and one entry per file in `FACES`: the file inside the archive, the pinned axes, the Unicode ranges and the output name. Keep the output `.woff2`, which is the only font format the [publish allowlist](Building#what-the-build-refuses-to-publish) accepts. +1. **The font files** (all three faces). [`scripts/build_fonts.py`](Tools-Build#build-fonts) downloads each face's pinned release, verifies its SHA-256, subsets it, and writes the `.woff2` files and their licences into `docs/assets/fonts/`. A new face is an entry in its `SOURCES` table --- the release URL, the hash and the licence --- and one entry per file in `FACES`: the file inside the archive, the pinned axes, the Unicode ranges and the output name. Keep the output `.woff2`, which is the only font format the [publish allowlist](Building#what-the-build-refuses-to-publish) accepts. 2. **The web stylesheet** (Inter and Cascadia Mono). `docs/_sass/custom/_fonts.scss` holds the `@font-face` rules, inside the `emit-font-faces` mixin, and the `$tb-body-font-family` and `$tb-mono-font-family` stacks, which `docs/assets/css/just-the-docs-combined.scss` passes into the theme. `docs/_sass/modules-dark.scss` passes them again for the dark compilation, and `docs/assets/css/just-the-docs-dark.scss` uses the mono stack once more for `pre`, `kbd` and `samp`. All of these read the two variables, so replacing a face inside an existing stack changes nothing in them. A new stack has to be passed in both compilations, or the dark theme keeps the system fonts --- the [specificity trap](#the-specificity-trap) again. -3. **The preloads** (the two roman web faces). `fontPreloads()` in `builder/template.mjs` preloads `inter-variable.woff2` and `cascadia-mono-variable.woff2` by name, from its `PRELOAD_FONTS` list. It sets `crossorigin`, which a font preload needs even from the same origin: without it the browser downloads the file twice. A preload of a file that does not exist is a broken link on every page, and the build's link check reports it. The link-check fixture under `test/fixtures/check-src/assets/fonts/` holds stub files under the same two names, so renaming either file means renaming its stub too, or [`check_links_diff.mjs`](Tools#check-links-diff) fails on every pull request. +3. **The preloads** (the two roman web faces). `fontPreloads()` in `builder/template.mjs` preloads `inter-variable.woff2` and `cascadia-mono-variable.woff2` by name, from its `PRELOAD_FONTS` list. It sets `crossorigin`, which a font preload needs even from the same origin: without it the browser downloads the file twice. A preload of a file that does not exist is a broken link on every page, and the build's link check reports it. The link-check fixture under `test/fixtures/check-src/assets/fonts/` holds stub files under the same two names, so renaming either file means renaming its stub too, or [`check_links_diff.mjs`](Tools-Site-Checks#check-links-diff) fails on every pull request. 4. **The book** (all three faces). `docs/assets/css/print.css` is the whole design of the PDF and loads nothing from the Sass build, so it has its own `@font-face` block and its own stacks. `builder/pdf.mjs` reads that block for every file named as `url("../fonts/...")` and copies each into the sparse `_site-pdf/` tree, so there is no second list to keep in step. The PDF pass aborts on a named file that does not exist. -5. **Diagram exports** (Inter and Cascadia Mono). `FONT_FILES` in `docs/assets/js/svg-inline.js` maps each family name to its weight range and file names, and the Download and Copy buttons embed those files in an exported SVG or PNG. A family it does not list is exported without its face and without a message; a listed file that no longer exists costs one console warning. +5. **Diagram exports** (Inter and Cascadia Mono). `FONT_FILES` in `docs/assets/js/figure.js` maps each family name to its weight range and file names, and the Download and Copy buttons embed those files in an exported SVG or PNG. A family it does not list is exported without its face and without a message; a listed file that no longer exists costs one console warning. 6. **The Gantt chart** (Inter). `builder/gantt.mjs` writes a `<style>` of its own into the chart on the [Build Info](BuildInfo) page, with its own copy of the text stack. 7. **The diagram sources** (Inter). Every `.dot` source names the face in its `fontname` attributes, and the browser paints the rendered SVG in whatever those name. The new name must be one Graphviz does not recognise. The widths in the next step are written over the table Graphviz falls back to for an unknown name, so a name it does recognise --- one of its built-in PostScript families, such as Helvetica --- is measured from that family's own table instead. -8. **The diagram metrics** (Inter). Graphviz sizes the boxes from the width table in `builder/inter-metrics.json` --- see [Diagram geometry](#diagram-geometry) --- and the tools around that table are written for Inter by name. [`scripts/build_dot_metrics.mjs`](Tools#build-dot-metrics) measures the family `Inter` from Inter's two `.woff2` files, and [`scripts/check_dot_fit.mjs`](Tools#check-dot-fit) loads the same two files to paint what it measures, so a new face means editing both: rerunning the generator alone measures Inter again. `builder/dot-metrics.mjs` reads the table as `inter-metrics.json`, and `builder/dot.mjs` lists that file in `GENERATOR_FILES`, which is what makes a new table re-render every diagram; rename the file and both have to follow. Then run `check_dot_fit.mjs`. -9. **The accessibility sweep.** Run [`scripts/sweep_a11y.mjs`](Tools#sweep-a11y) over the whole site, not only `check.bat`'s sample. axe's `target-size` rule measures rendered boxes, and an inline element's height is its font's content area, so the results move with the face. Several rules in `custom/custom.scss` exist because of measured content-area heights, and their comments give the numbers for the current faces. +8. **The diagram metrics** (Inter). Graphviz sizes the boxes from the width table in `builder/inter-metrics.json` --- see [Diagram geometry](#diagram-geometry) --- and the tools around that table are written for Inter by name. [`scripts/build_dot_metrics.mjs`](Tools-Build#build-dot-metrics) measures the family `Inter` from Inter's two `.woff2` files, and [`scripts/check_dot_fit.mjs`](Tools-Site-Checks#check-dot-fit) loads the same two files to paint what it measures, so a new face means editing both: rerunning the generator alone measures Inter again. `builder/dot-metrics.mjs` reads the table as `inter-metrics.json`, and `builder/dot.mjs` lists that file in `GENERATOR_FILES`, which is what makes a new table re-render every diagram; rename the file and both have to follow. Then run `check_dot_fit.mjs`. +9. **The accessibility sweep.** Run [`scripts/sweep_a11y.mjs`](Tools-Site-Checks#sweep-a11y) over the whole site, not only `check.bat`'s sample. axe's `target-size` rule measures rendered boxes, and an inline element's height is its font's content area, so the results move with the face. Several rules in `custom/custom.scss` exist because of measured content-area heights, and their comments give the numbers for the current faces. Pages under `docs/Documentation/` also name the faces in prose, and nothing checks them. Search for the old face's name before committing. @@ -611,11 +630,11 @@ The build aborts or flips the exit code under a handful of conditions: - **DOT render failure.** Per-diagram failures retain the previous SVG and continue the batch so every broken diagram appears in one run; the orchestrator sets exit code 1 (`EXIT_FOUND`) from the failure count. - **SCSS compile failure.** The light/dark workers warn with the source location and continue with `failed: true`; the joiner sets exit code 1 (`EXIT_FOUND`). Existing `_site/` CSS lingers. - **Nav integrity.** Orphan or ambiguous `parent:` declarations throw inside `nav.execute()`, which aborts the build via `Scheduler._abort()`. -- **Unpublishable file type.** Every non-page under `docs/` is copied into the output verbatim, so `publish-policy.mjs` holds an allowlist of types that may be published and throws on anything else --- in `discover` over the static-file inventory, naming the source path before a byte is written, and again in `dispatch` over each tree's derived inventory, which is the only sweep that sees redirect stubs, vendored theme assets and the generated auxiliaries. Neither is behind `--check`: a build run with checks off is exactly when nothing else is looking. This one aborts rather than setting an exit code, on the opposite reasoning to the link check below --- a broken link leaves a tree worth inspecting, a tree carrying a private key does not. `SOURCE_EXTENSIONS` and `BUILD_EXTENSIONS` are separate sets so the build can emit `sitemap.xml` and `search-data.json` without blessing a stray `docs/secrets.json`; [`scripts/check_publish_policy.mjs`](Tools#check-publish-policy) asserts they stay separate. +- **Unpublishable file type.** Every non-page under `docs/` is copied into the output verbatim, so `publish-policy.mjs` holds an allowlist of types that may be published and throws on anything else --- in `discover` over the static-file inventory, naming the source path before a byte is written, and again in `dispatch` over each tree's derived inventory, which is the only sweep that sees redirect stubs, vendored theme assets and the generated auxiliaries. Neither is behind `--check`: a build run with checks off is exactly when nothing else is looking. This one aborts rather than setting an exit code, on the opposite reasoning to the link check below --- a broken link leaves a tree worth inspecting, a tree carrying a private key does not. `SOURCE_EXTENSIONS` and `BUILD_EXTENSIONS` are separate sets so the build can emit `sitemap.xml` and `search-data.json` without blessing a stray `docs/secrets.json`; [`scripts/check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy) asserts they stay separate. - **Redirect collision.** A `redirect_from:` entry becomes a stub page at that URL, so two ways of claiming one URL are refused in `deriveRedirectStubs()`: an entry pointing at a URL some page already publishes at, and two pages declaring the same entry. Both name the source file on each side --- a stub silently overwriting a real page, or one of two stubs silently winning, would be invisible in the output. - **Destination collision.** `assertNoDestinationCollisions()` runs before the write phase and throws if any static file's destination path equals a page's. The static-file copy and the page write run in parallel, so without the check which one survived would depend on I/O ordering. - **Missing PDF input.** Phase 8 aborts on three things the book cannot be assembled without: no page (or more than one) carrying `layout: book-combined`, a font `print.css` names absent from the source tree (naming `scripts/build_fonts.py`), and any image `book.html` references that is not under the source tree. The last is the one that fires in practice, and `--tolerate-missing-images` downgrades only that one to a warning. -- **A page the book manifest does not mention.** Not a failure: the book is complete for the manifest it was given. `bookCoverage()` compares every page with `_book.yml`'s entries and its `left_out:` list, and the summary prints a `book:` warning for a page in neither, a page in both, an entry that matches no page, and a landing or foreword URL that names no page. It runs only when the book is built, and [`check_book_coverage.mjs`](Tools#check-book-coverage) holds each finding to a probe. See [Book Configuration](Book-Configuration#pages-left-out-of-the-book). +- **A page the book manifest does not mention.** Not a failure: the book is complete for the manifest it was given. `bookCoverage()` compares every page with `_book.yml`'s entries and its `left_out:` list, and the summary prints a `book:` warning for a page in neither, a page in both, an entry that matches no page, and a landing or foreword URL that names no page. It runs only when the book is built, and [`check_book_coverage.mjs`](Tools-Toolchain-Tests#check-book-coverage) holds each finding to a probe. See [Book Configuration](Book-Configuration#pages-left-out-of-the-book). - **Worker crash.** A worker handler that throws posts `{ taskFailed, message, stack }` to main; the scheduler calls `_abort()`, the build rejects, and the orchestrator reports the error with the task name in the message. - **A worker that never returns at all.** The one failure with no error to report: a handler stuck in an unbounded loop, an exponentially backtracking regex or a promise that never settles posts nothing, so its successors' dependency counts never fall, `_remaining` never reaches zero, and the scheduler's promise never settles. Nothing in the SAB protocol can see it --- the scheduler is waiting on a message that is not coming. `Scheduler` therefore runs a `setInterval` watchdog: if no task completes for `--stall-timeout` seconds (default 120, `0` disables), it aborts with `{ stalled: true }` and prints the outstanding tasks split three ways --- claimed by a worker that never returned (the cause), runnable but unclaimed (including an `F_PIN_TO_PRED` task whose lane is the wedged one), and blocked on a predecessor (the consequence). A `render:` or `flush:` chunk additionally prints its source pages, through an optional `describe()` on the task def that nothing else reads. `Worker.terminate()` does end a thread spinning inside a regex, so the abort really ends the process. Under `--serve` the pool outlives a rebuild, so `serve.mjs` replaces the whole pool when it sees the `stalled` flag rather than identifying the wedged lane --- the SAB records the lane a task *completed* on, not the one that claimed it. See [when a build stops instead of failing](Building#when-a-build-stops) for the reader-facing form. - **Link and integrity check** (`--check`). Deliberately the one failure that does *not* abort: a broken link still produces a valid site you want on disk to inspect, unlike a nav ambiguity, where the output itself would be wrong. The check tasks collect findings and `runBuild()` sets the exit code afterwards --- 1 for any link or integrity failure, the same code as every other failed step the build reports. A refused command line or a crash exits 2 instead. diff --git a/docs/Documentation/Building.md b/docs/Documentation/Building.md index 9c538e3ec..fd19c4cb3 100644 --- a/docs/Documentation/Building.md +++ b/docs/Documentation/Building.md @@ -37,7 +37,7 @@ The documentation is rendered to HTML by `tbdocs`, a custom Node.js static site - **Node.js 22+** for `tbdocs` itself. - **`npm ci`** at the repository root installs everything: the static site generator's deps and the PDF renderer's deps. A single `package.json` at the repo root contains the whole dependency set. The `build.bat` / `serve.bat` wrappers assume the install has run. - **Chromium** is required for four things: rendering the PDF book (`book.bat`), two of `check.bat`'s steps --- the diagram-fit check (`scripts/check_dot_fit.mjs`), which re-renders each diagram with the real webfont, and the accessibility scan (`scripts/check_a11y.mjs`) --- and one of `test.bat`'s, the axe source-patch equivalence check (`scripts/check_axe_patch_equiv.mjs`). `npm install` normally brings it down as part of `puppeteer`'s own postinstall; `npx puppeteer browsers install chrome` fetches it on its own if that step was skipped. Add `--install-deps` only on Linux --- it installs system packages, needs root, and is not supported anywhere else. The day-to-day `build.bat` / `serve.bat` flow does not need it --- only `check.bat`, `test.bat` and `book.bat` do. -- **Windows and a twinBASIC IDE install**, for [`scripts/tbbuild.mjs`](Tools#tbbuild) alone. It compiles a `.twinproj` unattended by driving a real IDE, which is how a claim this documentation makes about the *language* gets checked against the compiler. Nothing that builds, serves, checks or publishes the site needs either, so skip this one unless you are writing or verifying a probe project. +- **Windows and a twinBASIC IDE install**, for [`scripts/tbbuild.mjs`](Tools-Compiler#tbbuild) alone. It compiles a `.twinproj` unattended by driving a real IDE, which is how a claim this documentation makes about the *language* gets checked against the compiler. Nothing that builds, serves, checks or publishes the site needs either, so skip this one unless you are writing or verifying a probe project. ### On macOS and Linux {: #posix-equivalents } @@ -59,12 +59,12 @@ Each `.bat` opens with `@pushd "%~dp0"`, which is what lets it be invoked from a `--check-audit-index` is the part most easily dropped in transcription, and dropping it is silent --- see [Building](#building) below for what it costs. Anyone who types `build.bat` gets the link check without thinking about it; anyone who types the underlying command has to include it themselves. -`check.bat` is a sequence of separate scripts rather than one command, each stopping the run if it fails. The order is cheapest-first, so a failure stops the run before the expensive gates have cost anything --- the accessibility scan at the end is by a wide margin the slowest: +`check.bat` is a sequence of separate scripts rather than one command, each stopping the run if it fails. [`check_tree_fresh`](Tools-Site-Checks#check-tree-fresh) runs first so a stale tree fails cheaply, before anything else looks at it; [`check_a11y`](Tools-Site-Checks#check-a11y) runs second even though it is the slowest, so the composite action can run it first in CI and overlap its Chromium work with the backgrounded book PDF render: node scripts/check_tree_fresh.mjs \ + && node scripts/check_a11y.mjs \ && node scripts/check_dot_fit.mjs \ - && node scripts/pick_a11y_sample.mjs --check \ - && node scripts/check_a11y.mjs + && node scripts/pick_a11y_sample.mjs --check `test.bat` is the same shape, in the same cheapest-first order: @@ -76,6 +76,8 @@ Each `.bat` opens with `@pushd "%~dp0"`, which is what lets it be invoked from a && node --test test/render.test.mjs \ && node --test test/strftime.test.mjs \ && node --test test/png.test.mjs \ + && node --test test/compact-image.test.mjs \ + && node --test test/svgshot.test.mjs \ && node --test test/example-batches.test.mjs \ && node --test test/ports.test.mjs \ && node --test test/addin-project.test.mjs \ @@ -117,11 +119,11 @@ or directly, from the repository root: A single `tbdocs` run produces all three trees. The `also_build_offline` and `also_build_pdf` keys in `_config.yml` toggle the sibling outputs; the `--no-offline` and `--no-pdf` flags do the same from the command line if you only want `_site/`. -Every build writes `add-in/Resources/HELP/site.zip`, an archive of the offline tree that the IDE help add-in embeds, and prints one line for it. The archive is made once the offline tree is complete and before the Gantt chart goes into the build-information page. The file is not committed. `--no-help-archive` turns the step off. [Tools and Scripts](Tools#the-help-archive) describes the archive. +Every build writes `add-in/Resources/HELP/site.zip`, an archive of the offline tree that the IDE help add-in embeds, and prints one line for it. The archive is made once the offline tree is complete and before the Gantt chart goes into the build-information page. The file is not committed. `--no-help-archive` turns the step off. [Build and Generation Tools](Tools-Build#the-help-archive) describes the archive. -Every build, CI's included, also packs the IDE help add-in's source folder, `add-in/`, into its project file, `tB/IDE/AddIns/downloads/tbDocsHelp.twinproj` in the online tree, which [Help Add-In](../../tB/IDE/AddIns/Help) offers as a download, and prints one line for it. Only the files git tracks there are packed, with the symbol index the same build wrote and the archive that build wrote; a build that wrote no archive packs none and says so. The offline tree holds no copy of the file, and its links to it open the website's copy. [Tools and Scripts](Tools#the-help-add-ins-project-file) describes the file. +Every build, CI's included, also packs the IDE help add-in's source folder, `add-in/`, into its project file, `tB/IDE/AddIns/downloads/tbDocsHelp.twinproj` in the online tree, which [Help Add-In](../../tB/IDE/AddIns/Help) offers as a download, and prints one line for it. Only the files git tracks there are packed, with the symbol index the same build wrote and the archive that build wrote; a build that wrote no archive packs none and says so. The offline tree holds no copy of the file, and its links to it open the website's copy. [Build and Generation Tools](Tools-Build#the-help-add-ins-project-file) describes the file. -The full set of `tbdocs` CLI flags --- every flag, what each one does, when to use it --- lives on the [Tools and Scripts](Tools#tbdocs) page. +The full set of `tbdocs` CLI flags --- every flag, what each one does, when to use it --- lives on the [Build and Generation Tools](Tools-Build#tbdocs) page. **A build that stops with `Nav-parent orphan detected` or `Nav-parent ambiguity detected`** has pages whose `parent:` names no page, or a title two pages share --- most often after a page's `title:` was changed. [Authoring Pages](Authoring#nav-parent-orphan) has the fix for each message. @@ -153,7 +155,7 @@ see [Book Configuration](Book-Configuration#pages-left-out-of-the-book). `check.bat` runs its four gates in order and ends on the scan's tally: - 13 pages x 2 theme(s) x 2 viewport(s) + 8 state audit(s) checked: 0 violation(s), 42 incomplete check(s) + 14 pages x 2 theme(s) x 2 viewport(s) + 8 state audit(s) checked: 0 violation(s), 46 incomplete check(s) **Incomplete is not a violation.** Those are checks axe declined to decide --- most often a colour-contrast node it could not resolve a background for --- and they are printed for a @@ -261,7 +263,7 @@ It covers all three trees --- `_site/` (the online tree), `_site-offline/` (the A failing check does not abort the build --- a broken link still produces a site worth looking at --- so it sets the exit code to 1 instead, whether the check found link failures, integrity failures or both. The summary lines say which. -[`scripts/check_links.mjs`](Tools#check-links) is still the tool for a tree this build did not produce: a release zip, a bisect, someone else's artifact. +[`scripts/check_links.mjs`](Tools-Site-Checks#check-links) is still the tool for a tree this build did not produce: a release zip, a bisect, someone else's artifact. ## What the build refuses to publish @@ -297,7 +299,7 @@ often than widening `SOURCE_EXTENSIONS`. A refusal from the second sweep gives different advice: nothing in `docs/` produced that file, so the message names `BUILD_EXTENSIONS` instead. -`test.bat` runs [`scripts/check_publish_policy.mjs`](Tools#check-publish-policy) +`test.bat` runs [`scripts/check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy) first, and it exists because a clean build proves only half of this. "Nothing in `docs/` is currently refused" is also what a list widened until it refuses nothing would report. The self-test asserts the other half against named probes @@ -362,7 +364,7 @@ below reads for its font metrics. baseline either. Because the file is a build output rather than a build input, -[`scripts/check_tree_fresh.mjs`](Tools#check-tree-fresh) skips it when it looks +[`scripts/check_tree_fresh.mjs`](Tools-Site-Checks#check-tree-fresh) skips it when it looks for sources newer than the built tree. Without that, the build writing a raised baseline at the end of its own run would make the tree it had just produced report as stale. @@ -381,7 +383,7 @@ gave a heading, read from the rendered HTML. What the pages cannot say --- the kind of a member, the values of an enumeration --- comes from `builder/package-api.json`, a committed snapshot of what the shipped packages declare. The build never regenerates it, because that needs a twinBASIC -install; [`build_package_api.mjs`](Tools#build-package-api) does, when the +install; [`build_package_api.mjs`](Tools-Build#build-package-api) does, when the reference is re-indexed against a newer build. The summary line says how many entries the index has, and how many public symbols no page documents. @@ -427,7 +429,7 @@ documents. check.bat -[`scripts/check_a11y.mjs`](Tools#check-a11y) drives `axe-core` inside headless Chromium (via `puppeteer`) over thirteen sample pages against WCAG 2.0/2.1/2.2 at Level A + AA (plus the `heading-order` best-practice rule), and exits non-zero on any violation. It runs last, because the cheaper gates ahead of it stop the run if they fail: a freshness check that refuses a stale tree, the [DOT diagram fit check](#diagram-fonts-and-why-checkbat-measures-them), and the sample-coverage check that says whether the thirteen pages still cover every markup construct the site uses. [Tools and Scripts](Tools#checkbat) has the list in order. +[`scripts/check_a11y.mjs`](Tools-Site-Checks#check-a11y) drives `axe-core` inside headless Chromium (via `puppeteer`) over fourteen sample pages against WCAG 2.0/2.1/2.2 at Level A + AA (plus the `heading-order` best-practice rule), and exits non-zero on any violation. It runs last, because the cheaper gates ahead of it stop the run if they fail: a freshness check that refuses a stale tree, the [DOT diagram fit check](#diagram-fonts-and-why-checkbat-measures-them), and the sample-coverage check that says whether the fourteen pages still cover every markup construct the site uses. [Tools and Scripts](Tools#checkbat) has the list in order. Each page is scanned in **both the light and dark themes** --- dark mode is a separate palette, so a light-mode pass says nothing about it --- and the scan runs against `_site-offline/` rather than `_site/`, because the online tree's root-absolute asset URLs do not resolve under `file://` and would leave every page unstyled. This stage needs the Chromium install from the [requirements](#requirements); the plain `build.bat` flow does not. @@ -449,12 +451,12 @@ when the change touches `builder/`, `scripts/`, `lib/`, `book/`, `eval/` or `wisdom/`. **Two of them do read `docs/`**, and the smaller one is easy to predict: -[`check_gate_lists.mjs`](Tools#check-gate-lists) reads this page, `README.md` +[`check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists) reads this page, `README.md` and every other page under `docs/Documentation/`, and fails on any gate count stated in prose that the wrappers do not run. So an edit to a developer page can fail it, and is meant to --- that is a count nothing else derives. -[`check_code_regions.mjs`](Tools#check-code-regions) is the other, and it is the +[`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions) is the other, and it is the one worth knowing about. It tokenises every markdown file under `docs/`, applies the build's real pre-render rewrite chain, tokenises again, and compares the code regions on either side --- so a page that introduces a code construct the corpus has not @@ -498,7 +500,7 @@ would still mean something against an empty `docs/` --- and that one would, on the probes it carries with it. One more is worth knowing about before you write a regex. -[`check_regex_safety.mjs`](Tools#check-regex-safety) refuses a pattern that can +[`check_regex_safety.mjs`](Tools-Toolchain-Tests#check-regex-safety) refuses a pattern that can backtrack exponentially, because that class of fault does not fail a build --- it stops one. The corpus passes for as long as no page contains the trigger, and then a worker sits inside `String.replace` and never returns. It has happened @@ -529,7 +531,7 @@ axe's `image-alt` asks only whether an accessible name exists. `tbdocs` regenerates each `.svg` from its `.dot` sibling when the SVG is missing or older than its source --- editing a `.dot` by one character regenerates the SVG on the next build. Both files belong in git; the `.dot` is the canonical source, the `.svg` is the build artifact. -At render time, any markdown image reference to a build-local `.svg` is replaced with the SVG content inlined directly in the HTML. Each inlined SVG gets a click-to-zoom overlay and four control links (Download SVG, Copy SVG, Download PNG, Copy PNG). The controls are hidden in print output. See the [SVG inlining](Builder#svg-inlining) section of the Builder page for the implementation details. +At render time, a markdown image reference to a build-local `.svg` that stands alone in its paragraph is replaced with the SVG content inlined directly in the HTML. Like every image that stands alone, it is shown as a figure, with a bar of buttons to download it and copy it as SVG or PNG and to zoom it; a click on the figure zooms it too. The bar is hidden in print output. See the [Figures](Builder#figures) section of the Builder page for the implementation details. The renderer calls `@hpcc-js/wasm-graphviz` directly: one WASM module load (~50 ms) covers the whole batch, then each diagram is a synchronous `gv.dot(src)` call. No headless browser and no Chromium dependency for diagrams. Two failure modes are handled distinctly: @@ -545,7 +547,7 @@ One thing does happen to Graphviz before it lays anything out. The WASM build ca Two consequences for anyone editing a diagram: - **Never hand-edit a `.svg`, and never set `font-family` anywhere but the `.dot`.** The SVG is a build artifact the next build overwrites, and Graphviz sizes every box to the text *it* measured --- a face the layout never saw leaves labels hanging outside their boxes. -- **`check.bat` runs [`scripts/check_dot_fit.mjs`](Tools#check-dot-fit)**, which re-renders every committed diagram with the real webfont and fails if a label sits outside the box Graphviz drew for it. Nothing in the build compares the two, and axe does not evaluate SVG `<text>` geometry, so without this gate a mismatch ships on a green build. +- **`check.bat` runs [`scripts/check_dot_fit.mjs`](Tools-Site-Checks#check-dot-fit)**, which re-renders every committed diagram with the real webfont and fails if a label sits outside the box Graphviz drew for it. Nothing in the build compares the two, and axe does not evaluate SVG `<text>` geometry, so without this gate a mismatch ships on a green build. ### When the diagram-fit check fails {: #dot-fit-remediation } @@ -599,7 +601,7 @@ The deploy workflow passes `--url` and `--baseurl` from the `configure-pages` ou ### The link-checker parity fixtures -[`scripts/check_links_diff.mjs`](Tools#check-links-diff) compares the check's two front ends: the standalone [`scripts/check_links.mjs`](Tools#check-links), still the tool for a tree this build did not produce, and the pass fused into the build. They run the same functions and differ only in how they read the tree, which is still enough to hide a fault, because **a checker that silently checks less reports a clean pass.** `check.bat` runs neither invocation; CI runs one or both. +[`scripts/check_links_diff.mjs`](Tools-Site-Checks#check-links-diff) compares the check's two front ends: the standalone [`scripts/check_links.mjs`](Tools-Site-Checks#check-links), still the tool for a tree this build did not produce, and the pass fused into the build. They run the same functions and differ only in how they read the tree, which is still enough to hide a fault, because **a checker that silently checks less reports a clean pass.** `check.bat` runs neither invocation; CI runs one or both. | Invocation | Where | What it holds to the comparison | |---|---|---| @@ -630,11 +632,11 @@ Self-hosting the body face settles the *text* at 17px on every machine, and does 4. Select your repository and branch to merge from, and set the base branch to **`staging`**. That is the repository's default branch, the branch the deploy workflow publishes from, and the one the pull-request checks run against. - ![GitHub's Compare changes page, with the compare across forks link marked, and below it the head repository and compare branch selectors](Images/compare-changes.png) + ![GitHub's Compare changes page, with the compare across forks link marked, and below it the head repository and compare branch selectors](Images/compare-changes.png){:width="926" height="170"} 5. Create the pull request. If it adds or changes a twinBASIC code sample, paste the `examples.bat` summary line into the description, as [Checking that a sample compiles](Authoring#checking-that-a-sample-compiles) describes --- no CI workflow compiles the samples. - ![The comparison after choosing the fork and branch, reporting Able to merge, with the green Create pull request button marked](Images/create-pull-request.png) + ![The comparison after choosing the fork and branch, reporting Able to merge, with the green Create pull request button marked](Images/create-pull-request.png){:width="1857" height="470"} A maintainer will merge the pull request into the documentation repository. You may wish to mention an outstanding request on the [#docs][hash-docs] channel, although the [#github-docs][hash-github-docs] channel provides automated notifications of pull requests. Normally, a maintainer will get a notification of a new pull request via Discord, and will merge it or comment with a request for changes. @@ -642,15 +644,17 @@ Self-hosting the body face settles the *text* at 17px on every machine, and does 6. Review, then merge the pull request or comment with required changes. - ![A pull request reporting no conflicts with the base branch, with the green Merge pull request button marked](Images/merge-pull-request.png) + ![A pull request reporting no conflicts with the base branch, with the green Merge pull request button marked](Images/merge-pull-request.png){:width="1371" height="416"} - ![The merge form's commit message, extended description and commit email fields, with the green Confirm merge button marked](Images/confirm-merge.png) + ![The merge form's commit message, extended description and commit email fields, with the green Confirm merge button marked](Images/confirm-merge.png){:width="1374" height="704"} 7. Select the **Build & deploy docs** action. - ![The repository's Actions tab, and the workflow list in the left sidebar where the deploy workflow is chosen](Images/choose-workflow.png){:width="75%"} + + ![The repository's Actions tab, and the workflow list in the left sidebar where the deploy workflow is chosen](Images/choose-workflow.png){:width="552" height="191"} 8. Manually run the build and deployment workflow if a release snapshot is needed. Pushes to `staging` deploy to Pages automatically; only the manual run additionally cuts a GitHub release. See [Cutting a release](#cutting-a-release) for what it attaches and the one input it takes. - ![The Run workflow dropdown open on its branch selector, with the green Run workflow button that starts the manual deployment](Images/run-workflow.png){:width="50%"} + + ![The Run workflow dropdown open on its branch selector, with the green Run workflow button that starts the manual deployment](Images/run-workflow.png){:width="368" height="231"} ### Cutting a release @@ -689,34 +693,16 @@ Afterwards, check the live site: ## Editing screenshots -One way to edit screenshots is to use an integrated vector / pixel program like [Affinity][af]<sup>1</sup>. A possible workflow: - -1. <kbd>PrtSc</kbd> to capture the screenshot. - -2. In Affinity, <kbd>Ctrl-Alt-Shift-N</kbd> (File, New from Clipboard) to get the entire screenshot into the program. - -3. Use the Vector Crop tool (from the Vector studio) to crop the screenshot down to the relevant part. - - ![Affinity's studio switcher, with Vector selected rather than Pixel](Images/af-vector-studio.png) ![The Vector studio's tool column, with the Vector Crop tool highlighted](Images/af-vector-crop-tool.png) +The documentation's pictures of the IDE are not edited by hand. [`scripts/shoot_docs.mjs`](Tools-IDE#shoot-docs) takes them from an IDE that it starts on a private desktop, so nothing appears on your screen. It draws the arrows, rings and numbered badges on them in one style, and it writes a picture only when the IDE draws something different, so one run brings every picture up to date after a new BETA. The same tool also takes the windows of running programs and the few web pages the documentation shows. -4. Select the cropped image and copy it to the clipboard with <kbd>Ctrl-C</kbd>. +Each picture is a *shot* in the tool's table: the file it writes, the IDE it is taken in, and the steps that bring that IDE to the state the picture shows. To change a picture, change its shot and run the tool for that picture alone: -5. Create a new file from clipboard again to open a document with just the cropped screenshot <kbd>Ctrl-Alt-Shift-N</kbd> (File, New from Clipboard). + node scripts/shoot_docs.mjs --only <part of the picture's path> -6. Close the file you opened in step 2. - -7. Add arrows and labels as needed. Those can be copy-pasted from other `.af` files in this repository. - -8. Export to PNG via <kbd>Ctrl-Alt-Shift-W</kbd> (File, Export, Export...). - -> [!NOTE] -> It is a convention to put the `.af` ("source") files in the `_Images` folder, and the exported `.png` files in the `Images` folder. Only the latter is published to the website. The former is preserved as the source for easy editing and updates. - ---- +A new picture of the IDE is a new shot, so that the next run takes it again. `node scripts/shoot_docs.mjs --help` lists the options, and [shoot_docs.mjs](Tools-IDE#shoot-docs) describes what the tool does to keep the pictures the same from one run to the next and your own settings and name out of them. -<sup>1</sup> Affinity is a free-as-in-beer suite that combines a vector editor, a bitmap editor, and a publishing layout editor. A Canva account is required to download; the accounts are free. +A picture the tool does not take, such as the GitHub pages under [Deploying to docs.twinbasic.com](#deploying-to-docstwinbasiccom), is a PNG file in the `Images` folder beside its page. The file an image editor saved it from, if you keep one, goes in the `_Images` folder beside that, which is not published. -[af]: https://www.affinity.studio/download [docs-pr]: https://github.com/twinbasic/documentation/compare [docs-repo]: https://github.com/twinbasic/documentation [hash-docs]: https://discord.com/channels/927638153546829845/1021635324809596988 diff --git a/docs/Documentation/Extending.md b/docs/Documentation/Extending.md index 5dc983925..54d5db2ab 100644 --- a/docs/Documentation/Extending.md +++ b/docs/Documentation/Extending.md @@ -69,7 +69,7 @@ starts with and names the file and line, the pattern, and a **witness** --- an input that makes that pattern take exponential time. -[The gate's own entry](Tools#check-regex-safety) explains the cause and the rewrite in +[The gate's own entry](Tools-Toolchain-Tests#check-regex-safety) explains the cause and the rewrite in full. The short form: 1. **Keep the witness.** Put the pattern and the witness in a scratch script and call @@ -184,7 +184,7 @@ If you changed the harness itself, or `check_links.mjs`, which is the harness's It runs `check_links.mjs`'s own regression guards, then compares the script with a deliberately corrupted copy of itself and fails unless the difference is reported. Without it, a harness that has stopped comparing anything prints the same agreement as one that works. -[The link-checker parity fixtures](Building#the-link-checker-parity-fixtures) explains what CI runs and why, and [`check_links_diff.mjs`](Tools#check-links-diff) is the full reference: every case, every side, and what each fixture holds the comparison to. +[The link-checker parity fixtures](Building#the-link-checker-parity-fixtures) explains what CI runs and why, and [`check_links_diff.mjs`](Tools-Site-Checks#check-links-diff) is the full reference: every case, every side, and what each fixture holds the comparison to. --- @@ -450,7 +450,7 @@ That is the full pattern: per-chunk compute on the render workers, merge into th `createMarkdownIt` in `render.mjs` builds the configured markdown-it instance. The plugins are applied in a fixed order: three from npm (`markdown-it-attrs`, `markdown-it-deflist`, `markdown-it-footnote`) interleaved with those defined in `render.mjs` itself, and `countPlugin` from `counts.mjs` registered last. A new plugin becomes part of that order. -Those in-tree plugins cover token-stream transforms --- `svgInlinePlugin` embeds SVG diagrams, `headingLevelNormalizePlugin` repairs legacy pages that skip from `h1` to `h3` --- alongside link, slug, and typography helpers, most of them closing a behavioural gap between markdown-it and the kramdown dialect the content was authored against. [Pipeline Stages](Pipeline-Stages#the-plugin-chain) tabulates the chain in registration order, with what each one does. +Those in-tree plugins cover token-stream transforms --- `figurePlugin` wraps a lone image in a figure and embeds a diagram's SVG, `headingLevelNormalizePlugin` repairs legacy pages that skip from `h1` to `h3` --- alongside link, slug, and typography helpers, most of them closing a behavioural gap between markdown-it and the kramdown dialect the content was authored against. [Pipeline Stages](Pipeline-Stages#the-plugin-chain) tabulates the chain in registration order, with what each one does. The same factory is called once on main (the shared site-level SEO instance, via `markdownInit`), once per render worker (via `renderEnvInit`), and once by each script that renders outside the build, such as `check_code_regions.mjs`. Plugins that reach for module-scope state must therefore work across worker boundaries --- in practice, that means no mutable closure-captured state, since each worker has its own module-scope instance. @@ -557,9 +557,9 @@ Then write a unit test for the plugin in [`test/render.test.mjs`](https://github Then run `test.bat`, which is where a `render.mjs` edit is judged. Two of its gates read this file: `check_regex_safety.mjs` refuses a regex that can backtrack exponentially, and `check_code_regions.mjs` refuses a pre-render rewrite that alters the contents of a code fence or code span. -**Assembling a pattern from string constants does not put it out of reach of the regex gate.** `new RegExp(`${A}${B}`)` is read if the source decides what `A` and `B` are; if it does not --- a function parameter, a `let` built in a loop --- `node scripts/check_regex_safety.mjs --census` says so by name, and the pattern is then yours to reason about. The resolvable shapes are the ones a reader can work out from that description --- a literal, a template, `+` concatenation, `String.raw`, a `const` declared once, `.source` of a `const` regex, a `join` over a `const` array, and a ternary checked both ways --- and the census is what tells you which bucket yours landed in; see [`check_regex_safety.mjs`](Tools#check-regex-safety). +**Assembling a pattern from string constants does not put it out of reach of the regex gate.** `new RegExp(`${A}${B}`)` is read if the source decides what `A` and `B` are; if it does not --- a function parameter, a `let` built in a loop --- `node scripts/check_regex_safety.mjs --census` says so by name, and the pattern is then yours to reason about. The resolvable shapes are the ones a reader can work out from that description --- a literal, a template, `+` concatenation, `String.raw`, a `const` declared once, `.source` of a `const` regex, a `join` over a `const` array, and a ternary checked both ways --- and the census is what tells you which bucket yours landed in; see [`check_regex_safety.mjs`](Tools-Toolchain-Tests#check-regex-safety). -**If the plugin emits markup the site has not carried before --- a new wrapper element, a widget, a figure, a control --- register a construct family for it in [`scripts/pick_a11y_sample.mjs`](Tools#pick-a11y-sample) in the same change.** The accessibility scan audits thirteen sample pages out of ~1,160, and a construct no sample page carries is a construct no axe rule keyed on it ever runs against. Leaving it out is not neutral: the gate goes on reporting a clean pass while covering less than it did before, which is the failure mode the derived sample exists to prevent. `node scripts/pick_a11y_sample.mjs --census` shows what the existing families are and which pages carry them; `--check` names the gaps and the cheapest page that closes each. The same obligation applies to a new task or sub-stage that changes the emitted HTML, and to a template change. +**If the plugin emits markup the site has not carried before --- a new wrapper element, a widget, a figure, a control --- register a construct family for it in [`scripts/pick_a11y_sample.mjs`](Tools-Site-Checks#pick-a11y-sample) in the same change.** The accessibility scan audits fourteen sample pages out of ~1,160, and a construct no sample page carries is a construct no axe rule keyed on it ever runs against. Leaving it out is not neutral: the gate goes on reporting a clean pass while covering less than it did before, which is the failure mode the derived sample exists to prevent. `node scripts/pick_a11y_sample.mjs --census` shows what the existing families are and which pages carry them; `--check` names the gaps and the cheapest page that closes each. The same obligation applies to a new task or sub-stage that changes the emitted HTML, and to a template change. --- @@ -637,14 +637,14 @@ A gate runs after the build and decides whether what the build produced is accep **A gate belongs in `test.bat` rather than `check.bat` if it would still mean something with no documentation in the tree.** -That is a rule about what the gate *interrogates*, not about what it happens to open. [`check_axe_patch_equiv.mjs`](Tools#check-axe-patch-equiv) loads a built page and needs Chromium, which makes it look like a `check.bat` gate. It is not. The page is there only because the probe needs some document to run inside, and it never reads that page's DOM; what the gate tests is the vendored axe source patch. Against an empty `docs/` it would still be worth running, so it goes in `test.bat`. +That is a rule about what the gate *interrogates*, not about what it happens to open. [`check_axe_patch_equiv.mjs`](Tools-Toolchain-Tests#check-axe-patch-equiv) loads a built page and needs Chromium, which makes it look like a `check.bat` gate. It is not. The page is there only because the probe needs some document to run inside, and it never reads that page's DOM; what the gate tests is the vendored axe source patch. Against an empty `docs/` it would still be worth running, so it goes in `test.bat`. Two worked applications of the rule: -- **A regression test for a build-time rewrite that has stopped firing** interrogates the rewrite, not the corpus, so it goes in `test.bat`. [`check_code_regions.mjs`](Tools#check-code-regions) is the gate of that shape already in the tree, and its admonition probes are the pattern to copy: each one asserts a defect that must stay fixed, in the normal run rather than behind a flag. +- **A regression test for a build-time rewrite that has stopped firing** interrogates the rewrite, not the corpus, so it goes in `test.bat`. [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions) is the gate of that shape already in the tree, and its admonition probes are the pattern to copy: each one asserts a defect that must stay fixed, in the normal run rather than behind a flag. - **A gate that compares this build's output against an earlier build's** interrogates the built tree, and says nothing at all about an empty `docs/`, so it goes in `check.bat`. -The split exists so that an edit confined to `docs/` usually has to pay for `check.bat` only. Two `test.bat` gates are the exception and are worth knowing before you assume a content edit cannot go red there: [`check_code_regions.mjs`](Tools#check-code-regions) tokenises every markdown file under `docs/`, and [`check_gate_lists.mjs`](Tools#check-gate-lists) reads `README.md` and every page under `docs/Documentation/`. Both CI workflows run every gate from both wrappers unconditionally, each as its own step, so the wrapper choice changes what a local edit costs and nothing about what reaches `staging`. +The split exists so that an edit confined to `docs/` usually has to pay for `check.bat` only. Two `test.bat` gates are the exception and are worth knowing before you assume a content edit cannot go red there: [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions) tokenises every markdown file under `docs/`, and [`check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists) reads `README.md` and every page under `docs/Documentation/`. Both CI workflows run every gate from both wrappers unconditionally, each as its own step, so the wrapper choice changes what a local edit costs and nothing about what reaches `staging`. ### Conventions @@ -657,7 +657,7 @@ The split exists so that an edit confined to `docs/` usually has to pay for `che - **Stop with `die(code, message)`** when the tool cannot go on: it prints the message to standard error and exits with the code. A tool that has something to put back first, as `sweep_attributes.mjs` has the registry, defines its own. - **End the usage text with an `Exit codes:` block,** one ` <code> <meaning>` line per code, a long meaning wrapped under itself. -[`scripts/check_cli.mjs`](Tools#check-cli) holds every tool to this, and its cases are in `scripts/lib/cli-cases.mjs`. A new tool is added to that file's `HELP_TOOLS` list, which makes the tool answer `--help` and `-h` with exit 0 and end its usage with exactly one `Exit codes:` table, and to its `REFUSALS` list, which gives it a case for an unknown flag and for an empty value; the gate throws on start if the two lists disagree. A tool that checks a value after the parse gets a case of its own in the same file's bad-value list. +[`scripts/check_cli.mjs`](Tools-Toolchain-Tests#check-cli) holds every tool to this, and its cases are in `scripts/lib/cli-cases.mjs`. A new tool is added to that file's `HELP_TOOLS` list, which makes the tool answer `--help` and `-h` with exit 0 and end its usage with exactly one `Exit codes:` table, and to its `REFUSALS` list, which gives it a case for an unknown flag and for an empty value; the gate throws on start if the two lists disagree. A tool that checks a value after the parse gets a case of its own in the same file's bad-value list. **Exit codes.** Three values, and a new gate in either wrapper uses them this way: @@ -690,7 +690,7 @@ The rule is not limited to gates. Every tool under `builder/`, `scripts/`, `book CI runs every gate from both wrappers, each as its own step of the action, so the wrapper choice does not change what CI does. Leaving a gate out of CI is a decision, not an omission, and is written down with its reason in `check_ci_workflows.mjs`'s list of allowed differences: `check_tree_fresh.mjs` is left out because CI builds in the same job and cannot have a stale tree. -Two checks enforce the registration, and neither reads the POSIX command blocks or `WIP.md`, so those two are a matter of care. [`check_gate_lists.mjs`](Tools#check-gate-lists) compares both wrappers against `Tools.md`'s two numbered lists --- membership, order, and the step count each section states --- so adding a gate without the entry fails `test.bat` naming the disagreement. It then sweeps `README.md` and every page under `docs/Documentation/` for a gate count stated anywhere in prose and fails on those too, which is what makes one edit enough: `Tools.md` owns the lists and nothing else restates them. If you find yourself writing a gate count into a second page, that is the thing not to do. [`check_ci_workflows.mjs`](Tools#check-ci-workflows) does the same for CI: both workflows, read through the action, must run every wrapper gate with the same arguments and in the same order, and any difference not on its list of allowed ones fails `test.bat`. +Two checks enforce the registration, and neither reads the POSIX command blocks or `WIP.md`, so those two are a matter of care. [`check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists) compares both wrappers against `Tools.md`'s two numbered lists --- membership, order, and the step count each section states --- so adding a gate without the entry fails `test.bat` naming the disagreement. It then sweeps `README.md` and every page under `docs/Documentation/` for a gate count stated anywhere in prose and fails on those too, which is what makes one edit enough: `Tools.md` owns the lists and nothing else restates them. If you find yourself writing a gate count into a second page, that is the thing not to do. [`check_ci_workflows.mjs`](Tools-Toolchain-Tests#check-ci-workflows) does the same for CI: both workflows, read through the action, must run every wrapper gate with the same arguments and in the same order, and any difference not on its list of allowed ones fails `test.bat`. ### It must be able to fail @@ -825,7 +825,7 @@ A clean run of all five is the bar for "ready to commit". Three gates are the ones a builder change is most likely to trip, and each fails for a reason worth reading rather than working around. -- `pick_a11y_sample.mjs --check`, in `check.bat`, fails when a construct family the site uses is covered by no page in the sample --- normally because a build added or moved pages and the cheapest page for some family is no longer in the list. The fix is to add the page it names, not to widen the sample by hand. **It cannot report a genuinely new construct.** The gate iterates the registered `FAMILIES` and nothing else, so markup no family describes produces silence, and that silence is the exact failure a derived sample exists to prevent: the axe rule keyed on that construct then runs nowhere. Adding a family is the deliberate step, and [Tools and Scripts](Tools#pick-a11y-sample) gives the shape of one. +- `pick_a11y_sample.mjs --check`, in `check.bat`, fails when a construct family the site uses is covered by no page in the sample --- normally because a build added or moved pages and the cheapest page for some family is no longer in the list. The fix is to add the page it names, not to widen the sample by hand. **It cannot report a genuinely new construct.** The gate iterates the registered `FAMILIES` and nothing else, so markup no family describes produces silence, and that silence is the exact failure a derived sample exists to prevent: the axe rule keyed on that construct then runs nowhere. Adding a family is the deliberate step, and [Site Checks](Tools-Site-Checks#pick-a11y-sample) gives the shape of one. - `check_publish_policy.mjs`, in `test.bat`, fails when a new emitted file type is not on the allowlist in `builder/publish-policy.mjs`. Add it to `BUILD_EXTENSIONS`, which is deliberately a separate set from `SOURCE_EXTENSIONS` so blessing a generated type does not also bless a stray one a contributor drops into `docs/`. - `check_code_regions.mjs`, also in `test.bat`, fails when a new pre-render rewrite alters the contents of a code fence, an indented code block or a code span. For a fence or a span, the fix is to move the rewrite inside `applyPreRenderRewrites` in `render.mjs`, between `maskCode` and its `restore`, rather than to widen the mask; an indented block is not masked at all. It also fails when a rewrite over rendered HTML changes a `<pre>` or `<code>` written as raw HTML. [When `test.bat` fails in `check_code_regions`](#code-regions-altered) has each case. diff --git a/docs/Documentation/Fixes-PDFLib.md b/docs/Documentation/Fixes-PDFLib.md index d78948f18..f1f464e00 100644 --- a/docs/Documentation/Fixes-PDFLib.md +++ b/docs/Documentation/Fixes-PDFLib.md @@ -17,7 +17,7 @@ Each patch was written against the source of pdf-lib 1.17.1, which `package.json The root cause of the need for all these patches is the same: pdf-lib is designed for general-purpose use in both browsers and Node, and optimises for generality rather than throughput on a single large document. -Each patch must leave the output unchanged. [`check_pdf_shims_equiv.mjs`](../Tools#check-pdf-shims-equiv), one of `test.bat`'s gates, saves one document with stock pdf-lib and with every patch `render-book.mjs` imports, compares the two files object by object, and fails if the patches are not the ones the gate lists, or if one never runs that the list does not mark as unreached. +Each patch must leave the output unchanged. [`check_pdf_shims_equiv.mjs`](../Tools-Toolchain-Tests#check-pdf-shims-equiv), one of `test.bat`'s gates, saves one document with stock pdf-lib and with every patch `render-book.mjs` imports, compares the two files object by object, and fails if the patches are not the ones the gate lists, or if one never runs that the list does not mark as unreached. * TOC goes here {:toc} diff --git a/docs/Documentation/Fixes.md b/docs/Documentation/Fixes.md index b175b7aeb..4638adfcc 100644 --- a/docs/Documentation/Fixes.md +++ b/docs/Documentation/Fixes.md @@ -12,7 +12,7 @@ permalink: /Documentation/Development/Fixes Two third-party libraries are modified in the tree itself. `book/lib/paged.browser.js` is a patched copy of paged.js v0.4.3 (MIT); the `fast-*.mjs` files there are side-effecting shims applied to pdf-lib's live exports before each PDF process phase. This section documents every change to those two: what the upstream behaviour was, why it was unsuitable for the build pipeline, and what was changed. -A third library is modified, but nowhere on disk. The accessibility scan rewrites the `axe-core` bundle as it injects it --- `SOURCE_PATCHES` in `scripts/lib/axe-scan.mjs` replaces `Color2`'s six WeakMap-emulated `#private` fields with plain own properties, worth about a quarter of the scan's running time. Nothing under `node_modules/` is touched, so the substitution has to be re-proved against each axe-core upgrade rather than surviving one. Two gates do that, and both are needed: [`check_axe_patch_equiv.mjs`](Tools#check-axe-patch-equiv) compares the colour values the patched and stock bundles produce, and [`check_a11y_fingerprint.mjs`](Tools#check-a11y-fingerprint) compares the findings across the whole scan matrix. +A third library is modified, but nowhere on disk. The accessibility scan rewrites the `axe-core` bundle as it injects it --- `SOURCE_PATCHES` in `scripts/lib/axe-scan.mjs` replaces `Color2`'s six WeakMap-emulated `#private` fields with plain own properties, worth about a quarter of the scan's running time. Nothing under `node_modules/` is touched, so the substitution has to be re-proved against each axe-core upgrade rather than surviving one. Two gates do that, and both are needed: [`check_axe_patch_equiv.mjs`](Tools-Toolchain-Tests#check-axe-patch-equiv) compares the colour values the patched and stock bundles produce, and [`check_a11y_fingerprint.mjs`](Tools-Site-Checks#check-a11y-fingerprint) compares the findings across the whole scan matrix. ## Sub-pages diff --git a/docs/Documentation/Images/Tools-Groups.dot b/docs/Documentation/Images/Tools-Groups.dot new file mode 100644 index 000000000..fa87f592b --- /dev/null +++ b/docs/Documentation/Images/Tools-Groups.dot @@ -0,0 +1,243 @@ +// Source for Tools-Groups.svg, the map of the Tools and Scripts pages: one +// cluster per group page, one node per tool. The SVG is a build artifact: +// tbdocs regenerates it whenever this file is newer. +// +// There are no real edges. The invisible ones only lay the nodes out: a chain +// inside a cluster stacks a column, a rank=same block holds a row of the +// cluster together, and an edge from the bottom of one row of clusters to the +// top of the next puts that row below it. A tool added to a page goes into its +// cluster's chains and rows; keep the columns of a cluster within one node of +// the same length. +// +// The clusters side by side in one row must have the same number of node rows. +// dot spaces its ranks for the whole graph, so where a shorter cluster ends, +// the gap its border needs opens inside its taller neighbour too. The +// invisible nodes (wp, cp1, cp2, ep, dp) pad the shorter ones. +// +// Every node and cluster label is a link, set in the block at the end so the +// layout above stays readable: a node to its tool's entry, by the id its +// heading carries on the built page, and a cluster to its group page. The +// URLs are relative to the Tools page, which inlines this diagram. A tool +// added to a page gets a line there too; the build's link check reports one +// whose entry has gone. The build moves each cluster's label just before its +// first node (builder/inline-diagram.mjs), so a cluster's nodes are declared +// together, row by row. +// +// Graphviz has no font machinery and would otherwise measure these labels +// with Times; builder/dot-metrics.mjs installs Inter's real widths before +// layout, and scripts/check_dot_fit.mjs asserts afterwards that no label +// outgrew its box. See WIP.md, "Diagrams". + +digraph tools_groups { + rankdir=TB + bgcolor="transparent" + nodesep=0.22 + ranksep=0.12 + newrank=true + // The nodes in the SVG in the order declared here, which is reading order: + // the order a reader and the Tab key take the links in. + outputorder=nodesfirst + + node [ + shape=box + style="rounded,filled" + fontname="Inter,system-ui,-apple-system,Segoe UI,sans-serif" + fontsize=11 + margin="0.10,0.04" + height=0.3 + fillcolor="#fef7e0" + color="#f9ab00" + ] + // Heavy, so that a column stays packed rather than stretching towards the + // light edges that place the next row of clusters. + edge [style=invis weight=20] + graph [ + color="#7a8090" + fontname="Inter,system-ui,-apple-system,Segoe UI,sans-serif" + fontsize=13 + style="rounded" + labeljust="l" + margin=0 + ] + + // ---- row 1 ---- + subgraph cluster_wrappers { + margin=12 + label="Batch wrappers" + w1 [label="build.bat"] w2 [label="serve.bat"] w3 [label="check.bat"] + w4 [label="test.bat"] w5 [label="book.bat"] w6 [label="examples.bat"] + w7 [label="addin-test.bat"] w8 [label="ide-test.bat"] w9 [label="try-help-addin.bat"] + wp [style=invis label=""] + w1 -> w4 -> w7 -> wp + w2 -> w5 -> w8 + w3 -> w6 -> w9 + { rank=same; w4 w5 w6 } + { rank=same; w7 w8 w9 } + } + subgraph cluster_build { + margin=12 + label="Build and Generation" + b1 [label="tbdocs"] b2 [label="render-book"] + b3 [label="compare_trees"] b4 [label="survey_tooling"] + b5 [label="convert_em_dash_separators"] b6 [label="build_fonts.py"] + b7 [label="build_dot_metrics"] b8 [label="build_package_api"] + b1 -> b3 -> b5 -> b7 + b2 -> b4 -> b6 -> b8 + { rank=same; b3 b4 } + { rank=same; b5 b6 } + { rank=same; b7 b8 } + } + + // ---- row 2 ---- + subgraph cluster_site { + margin=12 + label="Site Checks" + s1 [label="check_links"] s2 [label="crawl_check"] + s3 [label="check_links_diff"] s4 [label="check_tree_fresh"] + s5 [label="check_dot_fit"] s6 [label="check_a11y"] + s7 [label="pick_a11y_sample"] s8 [label="axe-scan"] + s9 [label="check_a11y_fingerprint"] s10 [label="sweep_a11y"] + s1 -> s3 -> s5 -> s7 -> s9 + s2 -> s4 -> s6 -> s8 -> s10 + { rank=same; s3 s4 } + { rank=same; s5 s6 } + { rank=same; s7 s8 } + { rank=same; s9 s10 } + } + subgraph cluster_compiler { + margin=12 + label="Compiler Harness" + c1 [label="tbbuild"] c2 [label="tbrun"] + c3 [label="impexp.mjs, impexp.py"] c4 [label="check_tb_registry"] + c5 [label="probe_shared_temp"] c6 [label="probe_build_twice"] + cp1 [style=invis label=""] cp2 [style=invis label=""] + c1 -> c3 -> c5 -> cp1 -> cp2 + c2 -> c4 -> c6 + { rank=same; c3 c4 } + { rank=same; c5 c6 } + } + + // ---- row 3 ---- + subgraph cluster_tests { + margin=12 + label="Toolchain Tests" + t1 [label="check_publish_policy"] t2 [label="check_regex_safety"] + t3 [label="check_code_regions"] t4 [label="check_gate_lists"] + t5 [label="check_ci_workflows"] t6 [label="check_lint"] + t7 [label="search.test"] t8 [label="render.test"] + t9 [label="strftime.test"] t10 [label="png.test"] + t11 [label="compact-image.test"] t12 [label="svgshot.test"] + t13 [label="example-batches.test"] t14 [label="ports.test"] + t15 [label="addin-project.test"] t16 [label="check_page_baseline"] + t17 [label="check_book_coverage"] t18 [label="check_symbol_index"] + t19 [label="check_twin_parsers"] t20 [label="check_attribute_sweep"] + t21 [label="check_cli"] t22 [label="check_pdf_shims_equiv"] + t23 [label="check_impexp_parity"] t24 [label="check_axe_patch_equiv"] + t1 -> t5 -> t9 -> t13 -> t17 -> t21 + t2 -> t6 -> t10 -> t14 -> t18 -> t22 + t3 -> t7 -> t11 -> t15 -> t19 -> t23 + t4 -> t8 -> t12 -> t16 -> t20 -> t24 + { rank=same; t5 t6 t7 t8 } + { rank=same; t9 t10 t11 t12 } + { rank=same; t13 t14 t15 t16 } + { rank=same; t17 t18 t19 t20 } + { rank=same; t21 t22 t23 t24 } + } + + // ---- row 4 ---- + subgraph cluster_samples { + margin=12 + label="Code Samples and Attributes" + e1 [label="check_examples"] e2 [label="gen_attribute_probes"] + e3 [label="census_attributes"] e4 [label="sweep_attributes"] + ep [style=invis label=""] + e1 -> e3 -> ep + e2 -> e4 + { rank=same; e3 e4 } + } + subgraph cluster_ide { + margin=12 + label="IDE Automation" + i1 [label="addin_test"] i2 [label="ide_test"] + i3 [label="try_help_addin"] i4 [label="shoot_docs"] + i5 [label="imagestrip"] i6 [label="probe_ide_image_labels"] + i7 [label="svgshot/replay"] + ip [style=invis label=""] + i1 -> i3 -> i5 -> i7 + i2 -> i4 -> i6 -> ip + { rank=same; i3 i4 } + { rank=same; i5 i6 } + { rank=same; i7 ip } + } + subgraph cluster_defects { + margin=12 + label="Defect Reproducers" + d1 [label="bug_repro"] + d2 [label="vb6run"] + dp [style=invis label=""] + d1 -> d2 -> dp + } + + // ---- rows ---- + { rank=same; w1 w2 w3 b1 b2 } + { rank=same; s1 s2 c1 c2 } + { rank=same; t1 t2 t3 t4 } + { rank=same; e1 e2 i1 i2 d1 } + edge [weight=1] + wp -> s1 b7 -> c1 + s9 -> t1 s10 -> t2 cp2 -> t3 c6 -> t4 + t21 -> e1 t22 -> i1 t24 -> d1 + + // ---- links ---- + // A subgraph named again is the same subgraph, so these set the clusters'. + subgraph cluster_wrappers { URL="#batch-wrappers" } + subgraph cluster_build { URL="Tools-Build" } + subgraph cluster_site { URL="Tools-Site-Checks" } + subgraph cluster_compiler { URL="Tools-Compiler" } + subgraph cluster_tests { URL="Tools-Toolchain-Tests" } + subgraph cluster_samples { URL="Tools-Samples" } + subgraph cluster_ide { URL="Tools-IDE" } + subgraph cluster_defects { URL="Tools-Defects" } + + w1 [URL="#buildbat"] w2 [URL="#servebat"] w3 [URL="#checkbat"] + w4 [URL="#testbat"] w5 [URL="#bookbat"] w6 [URL="#examplesbat"] + w7 [URL="#addin-testbat"] w8 [URL="#ide-testbat"] w9 [URL="#try-help-addinbat"] + + b1 [URL="Tools-Build#tbdocs"] b2 [URL="Tools-Build#bookrender-bookmjs"] + b3 [URL="Tools-Build#compare-trees"] b4 [URL="Tools-Build#survey-tooling"] + b5 [URL="Tools-Build#convert-em-dash-separators"] b6 [URL="Tools-Build#build-fonts"] + b7 [URL="Tools-Build#build-dot-metrics"] b8 [URL="Tools-Build#build-package-api"] + + s1 [URL="Tools-Site-Checks#check-links"] s2 [URL="Tools-Site-Checks#crawl-check"] + s3 [URL="Tools-Site-Checks#check-links-diff"] s4 [URL="Tools-Site-Checks#check-tree-fresh"] + s5 [URL="Tools-Site-Checks#check-dot-fit"] s6 [URL="Tools-Site-Checks#check-a11y"] + s7 [URL="Tools-Site-Checks#pick-a11y-sample"] s8 [URL="Tools-Site-Checks#axe-scan"] + s9 [URL="Tools-Site-Checks#check-a11y-fingerprint"] s10 [URL="Tools-Site-Checks#sweep-a11y"] + + c1 [URL="Tools-Compiler#tbbuild"] c2 [URL="Tools-Compiler#tbrun"] + c3 [URL="Tools-Compiler#impexp"] c4 [URL="Tools-Compiler#check-tb-registry"] + c5 [URL="Tools-Compiler#probe-shared-temp"] c6 [URL="Tools-Compiler#probe-build-twice"] + + t1 [URL="Tools-Toolchain-Tests#check-publish-policy"] t2 [URL="Tools-Toolchain-Tests#check-regex-safety"] + t3 [URL="Tools-Toolchain-Tests#check-code-regions"] t4 [URL="Tools-Toolchain-Tests#check-gate-lists"] + t5 [URL="Tools-Toolchain-Tests#check-ci-workflows"] t6 [URL="Tools-Toolchain-Tests#check-lint"] + t7 [URL="Tools-Toolchain-Tests#search-test"] t8 [URL="Tools-Toolchain-Tests#render-test"] + t9 [URL="Tools-Toolchain-Tests#strftime-test"] t10 [URL="Tools-Toolchain-Tests#png-test"] + t11 [URL="Tools-Toolchain-Tests#compact-image-test"] t12 [URL="Tools-Toolchain-Tests#svgshot-test"] + t13 [URL="Tools-Toolchain-Tests#example-batches-test"] t14 [URL="Tools-Toolchain-Tests#ports-test"] + t15 [URL="Tools-Toolchain-Tests#addin-project-test"] t16 [URL="Tools-Toolchain-Tests#check-page-baseline"] + t17 [URL="Tools-Toolchain-Tests#check-book-coverage"] t18 [URL="Tools-Toolchain-Tests#check-symbol-index"] + t19 [URL="Tools-Toolchain-Tests#check-twin-parsers"] t20 [URL="Tools-Toolchain-Tests#check-attribute-sweep"] + t21 [URL="Tools-Toolchain-Tests#check-cli"] t22 [URL="Tools-Toolchain-Tests#check-pdf-shims-equiv"] + t23 [URL="Tools-Toolchain-Tests#check-impexp-parity"] t24 [URL="Tools-Toolchain-Tests#check-axe-patch-equiv"] + + e1 [URL="Tools-Samples#check-examples"] e2 [URL="Tools-Samples#gen-attribute-probes"] + e3 [URL="Tools-Samples#census-attributes"] e4 [URL="Tools-Samples#sweep-attributes"] + + i1 [URL="Tools-IDE#addin-test"] i2 [URL="Tools-IDE#ide-test"] + i3 [URL="Tools-IDE#try-help-addin"] i4 [URL="Tools-IDE#shoot-docs"] + i5 [URL="Tools-IDE#imagestrip"] i6 [URL="Tools-IDE#probe-ide-image-labels"] + i7 [URL="Tools-IDE#replay"] + + d1 [URL="Tools-Defects#bug-repro"] d2 [URL="Tools-Defects#vb6run"] +} diff --git a/docs/Documentation/Images/Tools-Groups.svg b/docs/Documentation/Images/Tools-Groups.svg new file mode 100644 index 000000000..6967b37b0 --- /dev/null +++ b/docs/Documentation/Images/Tools-Groups.svg @@ -0,0 +1,776 @@ +<!-- Generated by graphviz version 16.1.0 (0) + --> +<!-- Title: tools_groups Pages: 1 --> +<svg width="672pt" height="795pt" + viewBox="0.00 0.00 672.00 795.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 790.8)"> +<title>tools_groups + +cluster_wrappers + + +Batch wrappers + + + + +cluster_build + + +Build and Generation + + + + +cluster_site + + +Site Checks + + + + +cluster_compiler + + +Compiler Harness + + + + +cluster_tests + + +Toolchain Tests + + + + +cluster_samples + + +Code Samples and Attributes + + + + +cluster_ide + + +IDE Automation + + + + +cluster_defects + + +Defect Reproducers + + + + + +w1 + + +build.bat + + + + + +w2 + + +serve.bat + + + + + +w3 + + +check.bat + + + + + +w4 + + +test.bat + + + + + +w5 + + +book.bat + + + + + +w6 + + +examples.bat + + + + + +w7 + + +addin-test.bat + + + + + +w8 + + +ide-test.bat + + + + + +w9 + + +try-help-addin.bat + + + + + + +b1 + + +tbdocs + + + + + +b2 + + +render-book + + + + + +b3 + + +compare_trees + + + + + +b4 + + +survey_tooling + + + + + +b5 + + +convert_em_dash_separators + + + + + +b6 + + +build_fonts.py + + + + + +b7 + + +build_dot_metrics + + + + + +b8 + + +build_package_api + + + + + +s1 + + +check_links + + + + + +s2 + + +crawl_check + + + + + +s3 + + +check_links_diff + + + + + +s4 + + +check_tree_fresh + + + + + +s5 + + +check_dot_fit + + + + + +s6 + + +check_a11y + + + + + +s7 + + +pick_a11y_sample + + + + + +s8 + + +axe-scan + + + + + +s9 + + +check_a11y_fingerprint + + + + + +s10 + + +sweep_a11y + + + + + +c1 + + +tbbuild + + + + + +c2 + + +tbrun + + + + + +c3 + + +impexp.mjs, impexp.py + + + + + +c4 + + +check_tb_registry + + + + + +c5 + + +probe_shared_temp + + + + + +c6 + + +probe_build_twice + + + + + + + +t1 + + +check_publish_policy + + + + + +t2 + + +check_regex_safety + + + + + +t3 + + +check_code_regions + + + + + +t4 + + +check_gate_lists + + + + + +t5 + + +check_ci_workflows + + + + + +t6 + + +check_lint + + + + + +t7 + + +search.test + + + + + +t8 + + +render.test + + + + + +t9 + + +strftime.test + + + + + +t10 + + +png.test + + + + + +t11 + + +compact-image.test + + + + + +t12 + + +svgshot.test + + + + + +t13 + + +example-batches.test + + + + + +t14 + + +ports.test + + + + + +t15 + + +addin-project.test + + + + + +t16 + + +check_page_baseline + + + + + +t17 + + +check_book_coverage + + + + + +t18 + + +check_symbol_index + + + + + +t19 + + +check_twin_parsers + + + + + +t20 + + +check_attribute_sweep + + + + + +t21 + + +check_cli + + + + + +t22 + + +check_pdf_shims_equiv + + + + + +t23 + + +check_impexp_parity + + + + + +t24 + + +check_axe_patch_equiv + + + + + +e1 + + +check_examples + + + + + +e2 + + +gen_attribute_probes + + + + + +e3 + + +census_attributes + + + + + +e4 + + +sweep_attributes + + + + + + +i1 + + +addin_test + + + + + +i2 + + +ide_test + + + + + +i3 + + +try_help_addin + + + + + +i4 + + +shoot_docs + + + + + +i5 + + +imagestrip + + + + + +i6 + + +probe_ide_image_labels + + + + + +i7 + + +svgshot/replay + + + + + + +d1 + + +bug_repro + + + + + +d2 + + +vb6run + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Documentation/Images/af-vector-crop-tool.png b/docs/Documentation/Images/af-vector-crop-tool.png deleted file mode 100644 index 13675b6f5..000000000 Binary files a/docs/Documentation/Images/af-vector-crop-tool.png and /dev/null differ diff --git a/docs/Documentation/Images/af-vector-studio.png b/docs/Documentation/Images/af-vector-studio.png deleted file mode 100644 index 2ab993076..000000000 Binary files a/docs/Documentation/Images/af-vector-studio.png and /dev/null differ diff --git a/docs/Documentation/PDF-Generation.md b/docs/Documentation/PDF-Generation.md index 91d068fd1..f377c6ad9 100644 --- a/docs/Documentation/PDF-Generation.md +++ b/docs/Documentation/PDF-Generation.md @@ -89,7 +89,7 @@ A paged.js stylesheet fetch that fails arrives as `error on LINK: `. paged. `_site-pdf/` is `build.bat`'s output, so a render that stops before Chromium starts is reporting on the previous command. -`book.bat`'s first action is [`check_tree_fresh.mjs`](Tools#check-tree-fresh) over `docs/_site-pdf`. It refuses two states, and neither one reaches the renderer. +`book.bat`'s first action is [`check_tree_fresh.mjs`](Tools-Site-Checks#check-tree-fresh) over `docs/_site-pdf`. It refuses two states, and neither one reaches the renderer. **The tree is absent.** Exit code 2: @@ -158,7 +158,7 @@ The same fork checks fonts on the same principle: **`book.bat` propagates both.** It copies `%ERRORLEVEL%` into a variable immediately after the renderer runs and exits with that variable once `popd` has restored the caller's directory --- the same pattern `build.bat` and `check.bat` already used. A batch file's exit code is otherwise its last command's, and an unguarded `popd` resets `ERRORLEVEL` to `0`; `book.bat` used to end on a bare `popd`, so a failed render always reported success to whatever launched it. A script can check `book.bat`'s own exit code directly now. Calling `node book\render-book.mjs` directly and reading its exit code, or watching for the `saved:` line, remain equally valid. -**`book.bat`'s own pre-flight refusals never reach the renderer, and they share the renderer's 2.** [`check_tree_fresh.mjs`](Tools#check-tree-fresh) exits 2 for an absent `_site-pdf/` and 1 for a stale one, and a failed `npm install` exits 1. So what a script sees from `book.bat` is: +**`book.bat`'s own pre-flight refusals never reach the renderer, and they share the renderer's 2.** [`check_tree_fresh.mjs`](Tools-Site-Checks#check-tree-fresh) exits 2 for an absent `_site-pdf/` and 1 for a stale one, and a failed `npm install` exits 1. So what a script sees from `book.bat` is: | Code | Sources | |---|---| diff --git a/docs/Documentation/Pipeline-Stages.md b/docs/Documentation/Pipeline-Stages.md index 8ca7af73f..de73d166c 100644 --- a/docs/Documentation/Pipeline-Stages.md +++ b/docs/Documentation/Pipeline-Stages.md @@ -48,7 +48,7 @@ The pipeline passes three pieces of mutable state through every task: the `pages | `seoIsHome` | `render:i` (`computeChunkSeo`) | `boolean` | `true` when the page's permalink is a known home-page URL. | | `html` | `render:i` (`templatePhase`) | `string` | Complete HTML document, ready to write to disk. Absent on `layout: book-combined` pages, which `writePdf` owns. | | `offlineHtml` | `render:i` (`deriveOfflinePage`) | `string\|undefined` | Pre-computed offline HTML for the page, with every absolute URL rewritten to a page-relative path. Set after `templatePhase` when `!skipOffline`. | -| `hasSvg` | `render:i` (`svgInlinePlugin`) | `boolean\|undefined` | `true` when the page contains at least one inlined SVG. `templatePhase` uses this to conditionally include the `svg-inline.js` script. | +| `hasFigure` | `render:i` (`figurePlugin`, in the `figure_open` renderer) | `boolean\|undefined` | `true` when the page contains at least one figure. `templatePhase` uses this to write the figure sprite into the page and to conditionally include the `figure.js` script. | | `offlineMisses` | `render:i` (`deriveOfflinePage`) | `number\|undefined` | Count of URLs that could not be resolved during the per-page offline rewrite. Aggregated by `flushJoin`. | ### Site object (`site`) @@ -313,7 +313,7 @@ Per-lane render environment setup. Handler: 2. Unpacks the shared payload via `unpackShared(_sharedSAB)`. 3. Awaits `initHighlighter()`. 4. Reconstructs the link-table `Map`s from the serialized pair arrays, and the `svgContents`, `vendoredVideos` and `vendoredImages` `Map`s from their serialized objects. -5. Builds the worker's own markdown-it via `createMarkdownIt({...})`, passing those maps in --- they are what let `svgInlinePlugin`, `videoLinkPlugin` and `remoteImagePlugin` resolve a reference without touching the filesystem. +5. Builds the worker's own markdown-it via `createMarkdownIt({...})`, passing those maps in --- they are what let `figurePlugin`, `videoLinkPlugin` and `remoteImagePlugin` resolve a reference without touching the filesystem. 6. Builds the offline base state (site-paths set, normalised baseurl) when `!skipOffline`. 7. When `checkTrees` is set, dynamically imports `check.mjs` and builds this lane's per-tree check environment (`root`, `tree`, `basePath`, and a `treeIndexFor` index per tree) into `_checkEnv`. The import is dynamic rather than static because `htmlparser2` costs ~23 ms and a build without `--check` must not pay it on every lane. 8. Stores the result in the worker's module-scope `_renderEnv`. @@ -433,7 +433,7 @@ addinProject.expected = ["symbolIndex", "prepDest"] addinProject.execute({ symbolIndex }) → { rel, bytes, files, untracked, roots, ms } | { failed, rel, error } | null ``` -Writes the help add-in's project file, the download [Help Add-In](../../tB/IDE/AddIns/Help) offers (see [Tools and Scripts](Tools#the-help-add-ins-project-file)). Reads `addin_project` from the config with `addinProjectOf`, and returns `null` when the config declares none or under `--dry-run`. Otherwise calls `writeAddinProject` from `addin-project.mjs` with the add-in's folder (`src`, resolved against the source root), `symbolIndex.json` and the trees to write: the online tree, and the offline tree unless the build has none or `offline_exclude` leaves the file out of it. `dispatch` has already put `dest` in both trees' indexes and in `sitePaths`, so the links to the file resolve in both trees, and `checkReport` waits for this task. A failure --- `git` missing, an unreadable file --- is caught and returned rather than thrown, so the trees are still written; `runBuild` prints the result and fails the build on `failed`. ~100 ms. +Writes the help add-in's project file, the download [Help Add-In](../../tB/IDE/AddIns/Help) offers (see [Build and Generation Tools](Tools-Build#the-help-add-ins-project-file)). Reads `addin_project` from the config with `addinProjectOf`, and returns `null` when the config declares none or under `--dry-run`. Otherwise calls `writeAddinProject` from `addin-project.mjs` with the add-in's folder (`src`, resolved against the source root), `symbolIndex.json` and the trees to write: the online tree, and the offline tree unless the build has none or `offline_exclude` leaves the file out of it. `dispatch` has already put `dest` in both trees' indexes and in `sitePaths`, so the links to the file resolve in both trees, and `checkReport` waits for this task. A failure --- `git` missing, an unreadable file --- is caught and returned rather than thrown, so the trees are still written; `runBuild` prints the result and fails the build on `failed`. ~100 ms. ### `writeAux` (main) @@ -509,7 +509,7 @@ checkReport.execute({ linkJoin, checkBook }) → void Formats every tree's result, decides the exit code, and optionally writes the machine-readable findings. - **Exit code** follows the same scheme as `check_links.mjs`: `1` when the check found a link or an integrity failure, whichever it was; the summary lines name which. Set through `failBuild`, which marks the build failed in `process.exitCode`, never by throwing. -- **`--check-findings `** writes the findings as JSON for [`check_links_diff.mjs`](Tools#check-links-diff) to diff against the standalone script's. Written *before* the exit code is decided, so a failing check still produces the file that says what it found. +- **`--check-findings `** writes the findings as JSON for [`check_links_diff.mjs`](Tools-Site-Checks#check-links-diff) to diff against the standalone script's. Written *before* the exit code is decided, so a failing check still produces the file that says what it found. - **`--check-audit-index`** additionally diffs the tree index the build derived from its own records against what actually landed on disk. This is the one failure mode the two-checker findings comparison structurally cannot see: a *missing* index entry turns a working link into a reported break, which is loud, but a *spurious* one makes the oracle answer "exists" for a path that 404s in production, and on a clean site nothing links to a path that does not exist, so nothing would ever notice. Cost is one `readdir` per tree. `scss` is in `expected` for a reason worth keeping: `--check-audit-index` reads the tree off disk, and the combined stylesheet is in the index from the moment `dispatch` builds it. Without that edge the audit can run first and report the file as "indexed but not on disk" --- which it was, for another few milliseconds. On the real site `scss` finishes long before the check; on a three-page fixture it does not, and the audit failed the build over nothing. `symbolIndex` is there for the same reason: `tB/symbols.json` is in the online tree's index too. So is `addinProject`, whose project file is in both trees' indexes. @@ -522,7 +522,7 @@ The same modules as above, with the full export list per file. ### `check.mjs` -The link and integrity check over rendered pages, which the standalone [`scripts/check_links.mjs`](Tools#check-links) also runs, over a tree it reads from disk. The build imports it dynamically --- on the workers by `renderEnvInit`, on main by `linkJoin` / `checkBook` / `checkReport` --- because `htmlparser2` costs ~23 ms to import and a build without `--check` must not pay it on sixteen lanes. +The link and integrity check over rendered pages, which the standalone [`scripts/check_links.mjs`](Tools-Site-Checks#check-links) also runs, over a tree it reads from disk. The build imports it dynamically --- on the workers by `renderEnvInit`, on main by `linkJoin` / `checkBook` / `checkReport` --- because `htmlparser2` costs ~23 ms to import and a build without `--check` must not pay it on sixteen lanes. | Symbol | Signature | Description | |---|---|---| @@ -547,7 +547,7 @@ The link and integrity check over rendered pages, which the standalone [`scripts ### `link-check.mjs` -The pure core shared by the build's fused check and the standalone [`scripts/check_links.mjs`](Tools#check-links). No filesystem traversal and no CLI --- it takes HTML and an oracle and returns findings, which is what lets one implementation serve both front ends. **The two front ends read the tree differently, and a checker that silently checks less reports a clean pass, so run [`check_links_diff.mjs`](Tools#check-links-diff) whenever this file, `check.mjs` or `check_links.mjs` changes.** +The pure core shared by the build's fused check and the standalone [`scripts/check_links.mjs`](Tools-Site-Checks#check-links). No filesystem traversal and no CLI --- it takes HTML and an oracle and returns findings, which is what lets one implementation serve both front ends. **The two front ends read the tree differently, and a checker that silently checks less reports a clean pass, so run [`check_links_diff.mjs`](Tools-Site-Checks#check-links-diff) whenever this file, `check.mjs` or `check_links.mjs` changes.** | Symbol | Signature | Description | |---|---|---| @@ -690,14 +690,14 @@ Both HTML escapers convert a value that is not a string with `String` first. | `renderPhase` | `(pages, site, staticFiles?) → Promise` | Renders each page's `rawContent` to `renderedContent` via the supplied site's markdown-it. Skips `layout: book-combined`. | | `applyPreRenderRewrites` | `(rawContent, md) → string` | The whole pre-render source rewrite chain, as the render stage applies it to each page before parsing: normalises line endings to LF, masks code with `maskCode` from `lib/markdown.mjs` using the site's markdown-it instance `md`, runs the four kramdown-parity rewrites, restores the code, then runs `rewriteAdmonitions` with the same instance. Throws a `TypeError` when `md` is omitted. Exported so `check_code_regions.mjs` and `check_examples.mjs` test the real chain rather than a copy of it. | | `applyPostRenderRewrites` | `(html) → string` | The rewrites over a page's whole rendered HTML, as the render stage applies them after parsing: `normaliseVoidTags` writes each void tag self-closed and in lower case (`
` becomes `
`), then `padEmptyCells` puts a no-break space in each empty table cell. Both go through `replaceOutsideCode`. Exported for `scripts/check_code_regions.mjs`. | -| `createMarkdownIt` | `({ highlighter, linkTables, baseurl, staticFiles, svgContents?, vendoredVideos?, vendoredImages?, counts? }) → MarkdownIt` | Builds the configured markdown-it instance: the eighteen plugins tabulated below, in their fixed order, plus eight renderer-rule overrides assigned directly. `svgContents` is a `Map` of pre-read SVG file contents; when present, `svgInlinePlugin` replaces a lone `.svg` image with an inline SVG wrapper. `vendoredVideos` and `vendoredImages` are the maps `videoLinkPlugin` and `remoteImagePlugin` resolve against, and `counts` is the registry `countPlugin` substitutes from. See [Extending](Extending#adding-a-markdown-it-plugin) for how to add a plugin. | +| `createMarkdownIt` | `({ highlighter, linkTables, baseurl, staticFiles, svgContents?, vendoredVideos?, vendoredImages?, counts? }) → MarkdownIt` | Builds the configured markdown-it instance: the twenty plugins tabulated below, in their fixed order, plus eight renderer-rule overrides assigned directly. `svgContents` is a `Map` of pre-read SVG file contents; when present, `figurePlugin` replaces a diagram, a lone `.svg` image, with a figure holding the inline SVG. `vendoredVideos` and `vendoredImages` are the maps `videoLinkPlugin` and `remoteImagePlugin` resolve against, and `counts` is the registry `countPlugin` substitutes from. See [Extending](Extending#adding-a-markdown-it-plugin) for how to add a plugin. | | `initHighlighter` | (re-export from `highlight.mjs`) | `() → Promise`. Initialises Shiki with the bundled twinBASIC grammar. | | `buildLinkTables` | `(pages) → { byPath, byUrl, byRedirect }` | Map lookups keyed by `srcRel`, `permalink`, and `redirect_from` entries. | | `serializeLinkTables` | `(lt) → { byPath, byUrl, byRedirect }` | Serializes the Maps to `[key, permalink]` pair arrays for structured-clone transfer to workers. | | `rewriteAdmonitions` | `(src, md) → string` | GFM admonition rewrite to the `markdown-alert markdown-alert-` class structure with the five SVG octicons. Leaves an admonition alone when its `[!TYPE]` line is inside a region that `blockRegions` from `lib/markdown.mjs` reports with the site's markdown-it instance `md`; a fence inside an admonition is part of its body. Throws a `TypeError` when `md` is omitted. | #### The plugin chain -`createMarkdownIt` applies nineteen plugins in a fixed order: three from npm, fifteen defined in `render.mjs` itself, and `countPlugin` from `counts.mjs`. Most of the in-tree ones exist to close a behavioural gap between markdown-it and kramdown, which rendered this content under Jekyll --- the site's ~870 pages were authored against kramdown's dialect, so matching it is a compatibility requirement rather than a preference. +`createMarkdownIt` applies twenty plugins in a fixed order: three from npm, sixteen defined in `render.mjs` itself, and `countPlugin` from `counts.mjs`. Most of the in-tree ones exist to close a behavioural gap between markdown-it and kramdown, which rendered this content under Jekyll --- the site's ~870 pages were authored against kramdown's dialect, so matching it is a compatibility requirement rather than a preference. The table below is the plugin chain only. The renderer rules `createMarkdownIt` replaces directly --- among them the one that wraps every table --- are listed under [Renderer overrides](#renderer-overrides). @@ -714,20 +714,21 @@ The table below is the plugin chain only. The renderer rules `createMarkdownIt` | 9 | `headingLevelNormalizePlugin` | in-tree | Repairs the legacy "h1 straight to h3" house style. On a page with `h1` and `h3` headings but no `h2` --- a raw HTML `

` counts as one --- it renumbers each markdown heading to one level deeper than its parent heading, which closes every skipped level in one pass: `h1`, `h3`, `h5` become `h1`, `h2`, `h3`. A page that already uses `h2` is left untouched. Inserted `before("header-id")`, so ids are slugged from the corrected levels. | | 10 | `tocPlugin` | in-tree | The `* TOC` + `{:toc}` marker becomes a nested `