diff --git a/.github/actions/run-gates/action.yml b/.github/actions/run-gates/action.yml index fe205f392..a781539e2 100644 --- a/.github/actions/run-gates/action.yml +++ b/.github/actions/run-gates/action.yml @@ -162,6 +162,14 @@ runs: - name: Unit-test the package indexer (test/indexer.test.mjs) shell: bash run: node --test test/indexer.test.mjs + # Unit tests for scripts/ide_js.mjs, which reads, names and patches the + # twinBASIC IDE's minified main.js: the byte-exact round trip, how a name + # carries to another build, and a fix patched in as the smallest token edit, + # on a few lines written in the test. No install, no browser, no built tree, + # about 1 s. + - name: Unit-test the IDE's main.js tool (test/ide-js.test.mjs) + shell: bash + run: node --test test/ide-js.test.mjs # A regex that backtracks exponentially does not fail a build, it stops # one: the corpus passes until a page happens to contain the trigger, and # then a render worker sits inside String.replace forever. Nothing else diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 6e755a48e..8124b90dc 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -79,6 +79,17 @@ asserts the correct result, covering what the old issue described (these pass) a 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 IDE defect whose cause is in the IDE's own JavaScript carries a Fix shape** (owner, +2026-10-18): a **Fix shape** section after **Additional context**, inside the issue body. It +opens with the line `ide_js.mjs shape` prints, which names the function, the file and the +build and says which local names are ours and what the build calls them; then the diff; +then one paragraph on why the change fixes the defect; then one line on what the entry's test +showed on a patched copy of the install. The diff is generated, never written by hand: +`node scripts/ide_js.mjs shape bugs//fix.js`, where `fix.js` is the fix, whole +functions in our names, beside the reproducer ([WIP.md](WIP.md#driving-the-twinbasic-compiler), +*Fixing the IDE itself*). The minified edits and their offsets stay out of the entry. The +entry's HTML comment names the fix file. + **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. @@ -5286,3 +5297,989 @@ The reference is not one of the compiler's installed packages, and its own name Severity: low. A type library is labelled as a package. + +--- + +## With Export Verbose, each DONE line of Export Project shows \ for every backslash + +**Describe the bug** +With *Export Verbose* on (Project Settings, Export), Export Project prints a `DONE:` line in the DEBUG CONSOLE for each file it writes. Every backslash in those paths shows as the text `\`. The files are written correctly. It was observed in an IDE started with an `APPDATA` of its own, reading the console's lines and the rows it draws, and the folder written. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open any project, and set *Export Verbose* to YES in Project Settings, Export. +2. Export Project into an empty folder. +3. The DEBUG CONSOLE shows `[EXPORT] DONE: \Settings`, `[EXPORT] DONE: \Sources\Startup.twin`, and so on for each file. + +**Expected behavior** +`[EXPORT] DONE: \Settings`, `[EXPORT] DONE: \Sources\Startup.twin`. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Also on BETA 1007, with the same lines. The other `[EXPORT]` lines are right: the first line, `exporting current project to "\\?\C:\...\"...`, the `COMPLETED (... folders, ... files)` line, and, when the export folder already holds files, each `DELETED: \\?\C:\...` line. The files written and their contents are right. + +The cause looks to be the one in #2444. The compiler sends each `DONE` line in three parts: `[EXPORT] DONE: `, then the path, then the line break. `debugOutputPartial` in `ide/main.js` adds the path to a line that is still open, and passes it through `TEXTtoHTML` twice, so `\` is stored as `\` and drawn as `\`. A `DELETED` line comes in one part and is escaped once. + +Severity: cosmetic. The paths are hard to read, and a path copied from the console is wrong. + +**Fix shape** + +`debugOutputPartial` in `ide/main.js`, BETA 1010, in a readable form. The local names are ours, not twinBASIC's (`text`, `colorClass`, `isHTML`, `timeStamp`, `endsLine`, `addedHTML`, `lineHTML`, `stampHTML`); the build calls them `e`, `t`, `o`, `n`, `r`, `i`, `n`, `i`. + +```diff +@@ -6,11 +6,11 @@ + if (debugConsolePartialLineItem) { + let lineHTML = debugConsoleContent.onAsyncGetItemHTML(debugConsolePartialLineItem); + if (debugConsolePartialLineItem_LastColorClass === colorClass) { +- text = TEXTtoHTML(text); ++ text = isHTML ? text : TEXTtoHTML(text); + } else { + text = "" + (isHTML ? text : TEXTtoHTML(text)); + } +- var addedHTML = isHTML ? text : TEXTtoHTML(text); ++ var addedHTML = text; + debugConsoleContent.updateItem( + debugConsolePartialLineItem, + lineHTML + addedHTML + (endsLine ? "" : ""), +``` + +Both branches of the `if` already escape `text`, so the second `TEXTtoHTML` escapes it again (this bug and #2444); after a colour change it also escapes the `` markup that the `else` branch added. The first changed line is a related case: HTML text that continues a line in the same colour is escaped, though it is HTML. Drop that line if `isHTML` never continues a line. + +Tested on a copy of BETA 1010 with this change (two edits in `main.js`, every other byte the same): the DONE lines read `DONE: \Settings` and `DONE: \Sources\Startup.twin`, the other `[EXPORT]` lines are unchanged, and #2444's reproducer no longer reproduces. + + + +--- + +## Auto-Increment raises the version in memory only, and the increments are lost + +**Describe the bug** +With *(VERSION) Auto-Increment* set in Project Settings, each successful build prints `[BUILD] Auto-incremented next build number to: ...` and raises the version in the Settings the IDE holds. The project is not marked changed and its file is not written, so Close Project asks nothing, and the increments are lost: the next session builds, and publishes, version numbers already used. It was observed in an IDE started with an `APPDATA` of its own, reading the console, the Settings the IDE holds, its changed state, and the project file. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a saved Standard EXE project at version 1.0.7.3, with *(VERSION) Auto-Increment* set to `Revision`. +2. Press Build. The console prints `[BUILD] Auto-incremented next build number to: 1.0.7.4`. +3. Press Build again: `... to: 1.0.7.5`. The Settings the IDE holds say 1.0.7.5, and the project is not marked changed. +4. File > Close Project. Nothing is asked. The project file still says 1.0.7.3. + +**Expected behavior** +The project marked changed after an increment, so that Close Project asks to save it, or the new number written to the project file. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Also on BETA 1007. The same for a package, and for the `Build` part. The version the Package Publishing pane shows stays at the old number too, and no `[COMPILER] Project settings updated` line is printed. + +Severity: high. The increments are lost without a word, and two builds, or two published packages, can carry the same version. + + + +--- + +## ${ProjectID} in the build path becomes an empty string + +**Describe the bug** +In Project Settings, *Build Output Path* accepts `${ProjectID}`, but a build replaces it with an empty string, with no message. Two projects whose paths differ only by it write the same file. It was observed in an IDE started with an `APPDATA` of its own, reading the console and the folder written. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a saved Standard EXE project named `BuildProbeExe`, which has a Project ID. +2. Set *Build Output Path* to `${SourcePath}\Build\${ProjectID}_${ProjectName}.${FileExtension}`. +3. Press Build. The console prints `[LINKER] SUCCESS created output file '\Build\_BuildProbeExe.exe'`. + +**Expected behavior** +The project's ID in the file name, or the build path refused as for an unknown variable (`[COMPILER] build path contains unrecognized variable.`). + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Also on BETA 1007. In the same path `${SourcePath}`, `${ProjectName}` and `${FileExtension}` are expanded, and so are `${VersionMajor}` to `${VersionRevision}` and `${Bitness}`. + +Severity: medium. The file name is wrong without a word, and projects sharing the pattern overwrite each other's output. + + + +--- + +## Save in the build's "You must first save the project" box writes no project file + +**Describe the bug** +Build, or Clean, on a project that was never saved shows *You must first save the project. Save now?*. **Save** shows the Save As dialog and takes the path chosen, but writes no `.twinproj`, and builds nothing. Nothing says so. The project stays marked changed. It was observed in an IDE started with an `APPDATA` of its own, answering the Save As dialog in the page and reading the folder chosen, the console and the IDE's state. + +**To Reproduce** +Steps to reproduce the behavior: +1. Start the IDE with no project, choose *Standard EXE*, **Open**, then **Continue**. +2. Press Build. The box reads *You must first save the project. Save now?* (**Save** / **Cancel**). Choose **Save**, and pick `MyNewProject.twinproj` in an empty folder. +3. No file is written, and the console prints nothing. +4. File > Close Project asks *You've made changes to this project. Save now?* + +**Expected behavior** +The project saved to the file chosen. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Also on BETA 1007. Ctrl+S then writes the file, to the path chosen in step 2 (`[PROJECT] twinBASIC project saving to disk [DONE]`). A second Build, without Ctrl+S, builds into a `Build` folder beside the path chosen, and still writes no project file. That **Save** does not go on to build is a separate matter. + +Severity: low. Nothing is lost while the IDE runs, but the box reads as if it saved. + + + +--- + +## Delete It And Continue in Clean's box builds a new DLL instead of deleting the old one + +**Describe the bug** +When the previous DLL of an ActiveX project cannot be deregistered, Clean shows the box Build shows, *Build failed: unable to unregister the previous DLL. Continue anyway?*, and its **Delete It And Continue** builds a new DLL rather than deleting the old one. It was observed with *Register DLLs to HKLM*, in an IDE started with an `APPDATA` of its own and not elevated, reading the console and the build folder. + +**To Reproduce** +Steps to reproduce the behavior: +1. In an IDE that is not elevated, open an ActiveX DLL project with *Register DLLs to HKLM* set to YES (answer **Continue Anyway** to the warning). +2. Press Build. The DLL is written, and `[REGISTER] type-library registration FAILED. DllRegisterServer() returned -2147220992` is printed. +3. Press Clean. The console prints `[UNREGISTER] type-library deregistration FAILED. DllUnregisterServer() returned -2147220991` and `[BUILD] failed`, and the box above shows. Choose **Delete It And Continue**. +4. The console prints `[BUILD] Starting...` and `[LINKER] SUCCESS created output file '...\BuildProbeAx_win32.dll'`. The DLL is written again, and no `[BUILD-CLEAN]` line is printed. + +**Expected behavior** +At Clean, the DLL deleted, as the button says. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Also on BETA 1007. At Build, the same box's **Delete It And Continue** builds the DLL again, which is what it is there for. Clean of an ActiveX DLL registered in HKCU deregisters and deletes it. + +Severity: low. The DLL is replaced, not removed; nothing is lost. + + + +--- + +## The HKLM warning shows twice as a project opens, and for a Standard EXE too + +**Describe the bug** +In an IDE that is not elevated, a project with *Register DLLs to HKLM* set to YES shows the warning *This project registers itself to the HKEY_LOCAL_MACHINE registry hive which requires ADMIN permissions ...* twice as it opens, and its load waits for each **Continue Anyway**. A Standard EXE with the setting shows it too, though it registers nothing. It was observed in an IDE started with an `APPDATA` of its own, counting the boxes as they showed. + +**To Reproduce** +Steps to reproduce the behavior: +1. In an IDE that is not elevated, open an ActiveX DLL project with *Register DLLs to HKLM* set to YES. +2. The warning shows; choose **Continue Anyway**. The same warning shows again; choose **Continue Anyway** again. +3. Open a Standard EXE project with the same setting set to YES. The warning shows, twice. + +**Expected behavior** +The warning once per open, and only for an ActiveX DLL or ActiveX Control. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Also on BETA 1007, twice. An ActiveX DLL registered in HKCU opens with no box. + +Severity: cosmetic. An extra box, and a warning that does not apply. + + + +--- + +## A box opened during an LLVM build is closed in place of the Compiling... box, which stays and blocks the IDE + +**Describe the bug** +An LLVM build shows a modal *Compiling...* box. At the build's end the IDE closes the box on top, whichever it is. When another box has opened over *Compiling...*, that box is closed instead, and a *Compiling...* box stays at 0.0% with no button: the IDE takes no input until its page is reloaded. It was observed in an IDE started with an `APPDATA` of its own, with the other box opened from the page at the build's first progress event, since a person cannot open one while *Compiling...* covers the IDE. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a Standard EXE project with `+llvm` in Compiler Options (BUILD), with a licence that allows LLVM. +2. Press Build. The *Compiling...* box shows. +3. While it shows, open another box (in the IDE page: `showModalNotification("A second box")`). +4. The build ends (`[LINKER] SUCCESS created output file ...`). The other box is gone, and *Compiling... 0.0%* stays, with no button. + +**Expected behavior** +The *Compiling...* box closed at the build's end, and the other box left open. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Also on BETA 1007. `event_setBuildProgress` in `ide/main.js` calls `hideModalDialog()` for the end of the build (-1), which closes the top-most modal box, not the progress box; a second progress event of 0 while the other box is on top also stacks a second *Compiling...* box. It is not known whether the compiler itself opens a box during an LLVM build (as the box of a failed deregistration). An LLVM build with no other box closes its *Compiling...* box. + +Severity: low. Only a box opened during the build reaches it, and the IDE must be reloaded. + + + +--- + +## A remembered Package Publisher password containing ~ is read back wrong + +**Describe the bug** +In the Package Publishing sign-in box, *Remember password on this device* stores the password in the IDE's settings with `ObfuscateString`, and the IDE reads it back at its next start with `UnobfuscateString`. Each `~` in the password comes back as a NUL character, so the pre-filled password is wrong and the sign-in fails. It was observed in an IDE started with an `APPDATA` of its own, with the IDE's own two functions saving into the page rather than the registry, and nothing sent to the server. + +**To Reproduce** +Steps to reproduce the behavior: +1. In the sign-in box (Package Publishing, SIGN IN), log in with a password containing `~`, such as `pa~ss~word`, with *Remember password on this device* ticked. +2. Restart the IDE, and open SIGN IN again. +3. The password box holds `pa?ss?word`, each `?` a replacement character (U+FFFD), not `pa~ss~word`. + +**Expected behavior** +The password read back as it was typed. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Also on BETA 1007. `ObfuscateString(e, t, o = 126)` in `ide/main.js` shifts each character code up to and including 126 by `t` (111) modulo 126, and `UnobfuscateString` shifts back by `126 - 111`. For `~` (126) the stored code is 111 and the result is 0. A password without `~` comes back as it was typed. + +Severity: low. Signing in fails until the password is typed again; nothing is lost. + + + +--- + +## A saved publisher name containing ' is cut short in the sign-in box + +**Describe the bug** +The Package Publishing sign-in box fills its LOGIN user name from the saved publisher name by writing it into the box's HTML as `value=''`, with no escaping. A name containing `'` ends the attribute early: the box shows the name up to the apostrophe, and the rest becomes a stray attribute of the input element. The saved password is written the same way. It was observed in an IDE started with an `APPDATA` of its own, with the name set in the page, and nothing sent to the server. + +**To Reproduce** +Steps to reproduce the behavior: +1. Have the IDE save the publisher name `O'Brien` (log in or register with it). +2. Open Package Publishing, SIGN IN. +3. The LOGIN user name box holds `O`, and its input element has an attribute named `brien'`. + +**Expected behavior** +The box holding `O'Brien`. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Also on BETA 1007. `showSignInForm` in `ide/main.js` builds the form from a template string with `value='${packagePublisherName}'`, which becomes `innerHTML`. A name with no apostrophe is shown whole. + +Severity: low. The name has to be typed again. + + + +--- + +## OK in the box of a failed publish raises a page TypeError + +**Describe the bug** +When the package server answers a publish with an HTTPS status it has no message for, the IDE shows *[PUBLISH] FAILED. Server returned HTTPS code *. Its **OK** closes the box, and the page then raises `Uncaught TypeError: t is not a function`. It was observed in an IDE started with an `APPDATA` of its own, with the server call replaced in the page by one that fails with 500, so that nothing reached the server. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a package project, sign in, and PUBLISH THIS PACKAGE, at a time when the server answers with an error status (500, say). +2. The box reads *[PUBLISH] FAILED. Server returned HTTPS code 500*. Choose **OK**. +3. The box closes, and the page raises the error above. + +**Expected behavior** +The box closed with no error. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Also on BETA 1007. `packagePublisherPublishNow` in `ide/main.js` calls `showModalNotification("[PUBLISH] FAILED. Server returned HTTPS code " + e, true)`, passing `true` where `showModalNotification` takes a function to call after OK. The other `[PUBLISH] FAILED` boxes pass none, and their OK raises no error (checked with 551, *Package already uploaded with this exact version number.*). + +Severity: cosmetic. An error in the page after the box has closed. + + + + +--- + +## `Dim x As Any` compiles, though `As Any` is only for the parameter of a `Declare` + +**Describe the bug** +`As Any` stands for "an argument of any type" and means something only as a parameter of a `Declare` statement. twinBASIC refuses it in most other places with TB5079, *Unrecognized datatype symbol 'Any'*: as a Sub's or Function's parameter, as a Function's return type, as a field of a `Type`, in `Dim a(1) As Any` and in `ReDim b(1) As Any`. It accepts it, with no diagnostic, on a single variable: `Dim x As Any` and `Static s As Any` in a procedure, `Private m As Any` at module level and `Public Item As Any` as a field of a class. The variable cannot be used: `x = 5` is TB5028, and reading it fails when the program runs or is built. It was observed in the IDE's diagnostics and in runs. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `dim-as-any.twinproj` (attached as `dim-as-any.zip`). Its module `AsAnyTests`, below, has one declaration with `As Any` on each line that ends in a `' CASE : ` comment: the diagnostic code the line must get, `error` for any error, or `none`. +2. Compile it. These get TB5079: a `Type` field, a Function's return type, a Sub's `ByRef` and `ByVal` parameters, `Dim a(1) As Any` and `ReDim b(1) As Any`. The `Declare` with `ByRef Destination As Any` and the control `Dim c As Long` get none, as they should. These four also get none, and are the defect: + ``` + Public Item As Any ' a field of a class + Private ModuleLevel As Any ' at module level + Dim x As Any ' in a procedure + Static s As Any ' in a procedure + ``` +3. To see what such a variable is, run `Dim x As Any: Debug.Print TypeName(x)` in a project of its own. In a built exe it raises error 458, *Variable uses an Automation type not supported in Visual Basic*, and an IDE run of it ends with no output. `x = 5` (or `x = "text"`) is TB5028, *Unable to convert type 'Integer' to type 'Any'*. `Dim v As Variant: v = x` compiles clean and the build then fails with `compilation (codegen) error detected in 'Startup.Mainrootmain'`. A bare `Dim x As Any` with no other use compiles, builds and runs, and `VarPtr(x)` is not 0. +```vb +' Regression tests for the report, as compile cases: each line that ends in a +' comment ' CASE : must give that diagnostic on that line (a +' code, "error" for any error, or "none"). tbrun --tests checks them. +' As Any is meant only for the parameter of a Declare statement, where it lets +' the caller pass an argument of any type. Anywhere else it names no type, so +' every line below that is not a Declare parameter or the Long control is a +' compile error. + +Class HoldsAny + Public Item As Any ' CASE ClassField_AsAny: error +End Class + +Module AsAnyTests + + Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As LongPtr) ' CASE Declare_ParameterAsAny: none + + Private Type HasAny + Value As Any ' CASE UdtField_AsAny: error + End Type + + Private ModuleLevel As Any ' CASE ModuleVariable_AsAny: error + + Private Function ReturnsAny() As Any ' CASE FunctionReturn_AsAny: error + End Function + + Private Sub TakesAny(ByRef p As Any) ' CASE SubParameter_AsAny: error + End Sub + + Private Sub TakesAnyByVal(ByVal p As Any) ' CASE SubParameterByVal_AsAny: error + End Sub + + Private Sub Declarations() + Dim x As Any ' CASE LocalDim_AsAny: error + Static s As Any ' CASE LocalStatic_AsAny: error + Dim a(1) As Any ' CASE LocalArrayDim_AsAny: error + ReDim b(1) As Any ' CASE ReDim_AsAny: error + Dim c As Long ' CASE Control_DimAsLong: none + End Sub + +End Module +``` + +**Expected behavior** +A compile error on each of the four, TB5079 as on the neighbouring declarations: `As Any` names no type, and a variable of it can be neither assigned nor read. The `Declare` parameter keeps compiling. VB6 refuses all of them. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +BETA 1007 gives the same results: the four fail, the eight others pass. The run-time results in step 3 were checked on BETA 1010 only. + +VB6 does not accept `As Any` outside a `Declare` parameter. Its command-line build stops at the declaration with a compile error: `Private ModuleLevel As Any` and a `Type` field give *Expected: New or type name*, a Function's return type and a Sub's parameter give *Expected: type name*, and `Dim x As Any`, `Static s As Any` and `Dim a(1) As Any` in a procedure give *Syntax error*. The `Declare` with `ByRef ... As Any` builds. The reproducer's `vb6/` project is that control; the other declarations were each tried in it alone. + +Severity: low. Few programs write it, and a variable that cannot be used is found at the first use, but a declaration that compiles tells the author the type exists, and the failure comes late: a codegen error at build time, or error 458 in a run. + + + +--- + +## A `Date` outside January 1, 100 to December 31, 9999 is accepted with no error + +**Describe the bug** +A `Date` holds a count of days from -657434 (January 1, 100) to 2958465 (December 31, 9999), plus the time of day. VB6 raises error 6, *Overflow*, for every way of making one outside that range. twinBASIC raises nothing: the `Date` holds the out-of-range number, and `CStr`, `Format` and `Year` of it then raise error 5, *Invalid procedure call or argument*. It happens with an assignment from a `Long` or a `Double`, with `CDate`, and with arithmetic on the two ends of the range. It was observed in IDE runs and in built exes, in a 32-bit and a 64-bit build. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `date-out-of-range-no-error.twinproj` (attached as `date-out-of-range-no-error.zip`). Its module `DateRangeTests`, below, has one `[TestCase]` for each way of making a `Date` out of range, asserting the error 6; the last three are controls. +2. Run the test cases. The first six fail, with `Err.Number` 0; the three controls pass. +3. To see what the `Date` is, run `Dim d As Date: d = 2958466: Debug.Print CDbl(d)` in a project of its own. It prints 2958466, and `Debug.Print CStr(d)` raises error 5. +```vb +[TestFixture] +Module DateRangeTests + + [TestCase] + Public Sub AssignLongPastLargest_Raises6() + Dim d As Date + Dim n As Long = 2958466 + On Error Resume Next + d = n + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + [TestCase] + Public Sub CDatePastLargest_Raises6() + Dim d As Date + Dim n As Long = 2958466 + On Error Resume Next + d = CDate(n) + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + [TestCase] + Public Sub CDateBeforeSmallest_Raises6() + Dim d As Date + Dim n As Long = -657435 + On Error Resume Next + d = CDate(n) + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + [TestCase] + Public Sub AddOneDayToLargest_Raises6() + Dim d As Date + Dim last As Date = #12/31/9999# + On Error Resume Next + d = last + 1 + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + [TestCase] + Public Sub SubtractOneDayFromSmallest_Raises6() + Dim d As Date + Dim first As Date = #1/1/100# + On Error Resume Next + d = first - 1 + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + [TestCase] + Public Sub AssignHugeDouble_Raises6() + Dim d As Date + Dim x As Double = 1E+300 + On Error Resume Next + d = x + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + [TestCase] + Public Sub AssignLargest_Accepted() + Dim d As Date + Dim n As Long = 2958465 + On Error Resume Next + d = n + Assert.Strict.AreEqual 0, Err.Number + Assert.Strict.AreEqual 2958465#, CDbl(d) + End Sub + + [TestCase] + Public Sub AssignSmallest_Accepted() + Dim d As Date + Dim n As Long = -657434 + On Error Resume Next + d = n + Assert.Strict.AreEqual 0, Err.Number + Assert.Strict.AreEqual -657434#, CDbl(d) + End Sub + + [TestCase] + Public Sub OutOfRangeInteger_Raises6() + Dim i As Integer + Dim n As Long = 40000 + On Error Resume Next + i = n + Assert.Strict.AreEqual 6, Err.Number + End Sub + +End Module +``` + +**Expected behavior** +Error 6, *Overflow*, at each of the six, as in VB6, and the `Date` keeps its old value. The two ends of the range stay accepted. A `Date` then always holds a value that `CStr`, `Format` and `Year` can read. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +BETA 1007 gives the same results, and so do IDE runs and built exes, 32-bit and 64-bit. `DateAdd("d", 1, #12/31/9999#)` and `DateSerial(10000, 1, 1)` raise error 5, and `CDate("12/31/10000")` raises error 13, so those do not accept the value. VB6 6.0 raises error 6 for each of the six cases, and builds the controls (the reproducer's `vb6/` project prints them, attached as `date-out-of-range-no-error-vb6.zip`). + +Severity: medium. An out-of-range date is stored silently, and the failure comes later, where the value is formatted or split into parts, or never, where the number is only compared or saved. + + + +--- + +## `CLngLng(2^63)` returns the smallest LongLong, with no error + +**Describe the bug** +2^63 (9223372036854775808) is one past the largest `LongLong`, and converting it must raise error 6, *Overflow*, as `CLngLng(1E+19)` does and as a `Double` variable holding 2^63 does. When the `Double` is a constant expression, twinBASIC converts it to -9223372036854775808, the smallest `LongLong`, and raises nothing: `CLngLng(2 ^ 63)`, `CLngLng(9223372036854775808#)` and `CLngLng(CSng(2 ^ 63))`. An unsuffixed whole-number literal beyond the `Long` range is a `Double`, so `q = 9223372036854775807` (the largest `LongLong`, written without the `^` suffix) is the same constant and gives the same wrong result. It was observed in IDE runs and in built exes, in a 32-bit and a 64-bit build. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `clnglng-2-to-63-no-overflow.twinproj` (attached as `clnglng-2-to-63-no-overflow.zip`). Its module `LongLongOverflowTests`, below, has one `[TestCase]` for each constant that gives the wrong result, asserting the error 6, and four controls. +2. Run the test cases. The first four fail, with `Err.Number` 0; the four controls pass: a `Double` variable holding 2^63, the constant `1E+19`, the constant `2 ^ 63 - 1024` (the largest `Double` below 2^63, which fits) and `-(2 ^ 63)` (the smallest `LongLong`, which fits). +```vb +[TestFixture] +Module LongLongOverflowTests + + [TestCase] + Public Sub CLngLng_ConstantTwoToThe63_Raises6() + Dim q As LongLong + On Error Resume Next + q = CLngLng(2 ^ 63) + Assert.Strict.AreEqual 6, Err.Number + End Sub + + [TestCase] + Public Sub CLngLng_DoubleLiteral2To63_Raises6() + Dim q As LongLong + On Error Resume Next + q = CLngLng(9223372036854775808#) + Assert.Strict.AreEqual 6, Err.Number + End Sub + + [TestCase] + Public Sub Assign_UnsuffixedLargestLongLongLiteral_Raises6() + Dim q As LongLong + On Error Resume Next + q = 9223372036854775807 + Assert.Strict.AreEqual 6, Err.Number + End Sub + + [TestCase] + Public Sub CLngLng_ConstantSingleTwoToThe63_Raises6() + Dim q As LongLong + On Error Resume Next + q = CLngLng(CSng(2 ^ 63)) + Assert.Strict.AreEqual 6, Err.Number + End Sub + + [TestCase] + Public Sub CLngLng_VariableTwoToThe63_Raises6() + Dim q As LongLong + Dim x As Double = 2 ^ 63 + On Error Resume Next + q = CLngLng(x) + Assert.Strict.AreEqual 6, Err.Number + End Sub + + [TestCase] + Public Sub CLngLng_ConstantOneE19_Raises6() + Dim q As LongLong + On Error Resume Next + q = CLngLng(1E+19) + Assert.Strict.AreEqual 6, Err.Number + End Sub + + [TestCase] + Public Sub CLngLng_ConstantBelowTwoToThe63_Accepted() + Dim q As LongLong + On Error Resume Next + q = CLngLng(2 ^ 63 - 1024) + Assert.Strict.AreEqual 0, Err.Number + Assert.Strict.AreEqual 9223372036854774784^, q + End Sub + + [TestCase] + Public Sub CLngLng_ConstantMinus2To63_Accepted() + Dim q As LongLong + On Error Resume Next + q = CLngLng(-(2 ^ 63)) + Assert.Strict.AreEqual 0, Err.Number + Assert.Strict.AreEqual -9223372036854775807^ - 1, q + End Sub + +End Module +``` + +**Expected behavior** +Error 6, *Overflow*, at each of the four, as for `CLngLng(1E+19)` and for the same value in a variable. VB6 has no `LongLong`; the nearest case, the first `Double` beyond the `Long` range, `CLng(2 ^ 31)`, `CLng(2147483648#)` and `n = 2147483648#`, raises error 6 there as a constant and as a variable (the reproducer's `vb6/` project prints them, attached as `clnglng-2-to-63-no-overflow-vb6.zip`). + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +BETA 1007 gives the same results, and so do IDE runs and built exes, 32-bit and 64-bit. The narrower conversions of constants are right: `CLng(2 ^ 31)`, `CLng(CSng(2 ^ 31))`, `CInt(32768#)`, `CByte(256#)` and `CLngPtr(2 ^ 63)` in a 32-bit build raise error 6. A related defect seen on the way has its own entry: *In a 32-bit build, a constant `Double` expression raises error 6 after a constant that overflows a `Long` or `LongLong`*. + +Severity: medium. A silently wrong value, but only for one constant, and constants this size are rare. The unsuffixed `9223372036854775807` is the one a program is likely to write. + + + +--- + +## `Set` to a class or interface the object does not support raises -2147467262, where VB6 raises 13 + +**Describe the bug** +Assigning an object with `Set` to a variable of a class or interface that the object does not support fails at run time. VB6 raises error 13, *Type mismatch*. twinBASIC raises -2147467262, the `HRESULT` `E_NOINTERFACE` (*No such interface supported*), so a handler that tests for 13 does not recognise it. It happens for a twinBASIC class, an interface, a COM object (`Scripting.Dictionary` to `Collection`) and a `Variant` that holds the object. It was observed in IDE runs and in built exes, in a 32-bit and a 64-bit build. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `set-unsupported-class-error-number.twinproj` (attached as `set-unsupported-class-error-number.zip`). Its module `SetToUnsupportedTypeTests`, below, has one `[TestCase]` for each kind of source, asserting the error 13, and a control. +2. Run the test cases. The first four fail, with `Err.Number` -2147467262; the control, a `Set` to the object's own class, passes. +```vb +Class ClsA + Public X As Long +End Class + +Class ClsB + Public Y As Long +End Class + +Interface IFoo + Sub Hello() +End Interface + +[TestFixture] +Module SetToUnsupportedTypeTests + + [TestCase] + Public Sub SetToOtherClass_Raises13() + Dim o As Object = New ClsA + Dim b As ClsB + On Error Resume Next + Set b = o + Assert.Strict.AreEqual 13, Err.Number ' VB6: 13 + End Sub + + [TestCase] + Public Sub SetToInterfaceTheClassLacks_Raises13() + Dim o As Object = New ClsA + Dim f As IFoo + On Error Resume Next + Set f = o + Assert.Strict.AreEqual 13, Err.Number ' VB6: 13 + End Sub + + [TestCase] + Public Sub SetDictionaryToCollection_Raises13() + Dim o As Object = CreateObject("Scripting.Dictionary") + Dim c As Collection + On Error Resume Next + Set c = o + Assert.Strict.AreEqual 13, Err.Number ' VB6: 13 + End Sub + + [TestCase] + Public Sub SetFromVariantHoldingOtherClass_Raises13() + Dim v As Variant + Dim b As ClsB + Set v = New ClsA + On Error Resume Next + Set b = v + Assert.Strict.AreEqual 13, Err.Number ' VB6: 13 + End Sub + + [TestCase] + Public Sub SetToOwnClass_RaisesNothing() + Dim o As Object = New ClsB + Dim b As ClsB + On Error Resume Next + Set b = o + Assert.Strict.AreEqual 0, Err.Number + End Sub + +End Module +``` + +**Expected behavior** +Error 13, *Type mismatch*, as in VB6, so that code ported from VB6 which handles 13 keeps working. The reproducer's `vb6/` project builds the same four assignments and prints error 13 for each (attached as `set-unsupported-class-error-number-vb6.zip`). + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +BETA 1007 gives the same results, and so do IDE runs and built exes, 32-bit and 64-bit. `TypeOf o Is ClsB` is False for these objects and can be tested first. Severity: low. An error is raised, and only the number differs. + + + +--- + +## `Len` and `LenB` accept an array and return the size of its descriptor + +**Describe the bug** +`Len` and `LenB` take a string or a variable of a simple or user-defined type. VB6 refuses an array as the argument, when the program is compiled (*Variable required - can't assign to this expression*). twinBASIC accepts it, and returns a number that is not the size of the array: in a 32-bit build 40 for a fixed-size one-dimensional array of any element type and length (`Dim a(9) As Byte`, `Dim a(3) As Long` and `Dim a(0) As Byte` all give 40), 48 for two dimensions, and 4 for a dynamic array (the size of the reference); in a 64-bit build 48, 56 and 8. A program that writes `LenB(Buffer)` for a `Byte` buffer, expecting its length in bytes, gets a number that never changes with the array. It was observed in IDE runs and in built exes. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `lenb-of-array-accepted.twinproj` (attached as `lenb-of-array-accepted.zip`). Its module `LenArrayTests`, below, has one declaration with an array argument on each line that ends in a `' CASE : ` comment: the diagnostic code the line must get, `error` for any error, or `none`. +2. Run the cases, with `tbrun --tests` or by compiling the project. The five lines with an array argument get no diagnostic, and are the defect; the two controls, an element and a `Long` variable, get none, as they should. +3. To see the numbers, run `Dim a(9) As Byte: Debug.Print LenB(a)` in a project of its own. It prints 40. +```vb +' CASE : comments, as above. +Module LenArrayTests + + Private Sub Cases() + Dim fixedBytes(9) As Byte + Dim fixedLongs(3) As Long + Dim dynamicBytes() As Byte + Dim n As Long + ReDim dynamicBytes(4) + + n = LenB(fixedBytes) ' CASE LenB_FixedByteArray: error + n = LenB(fixedLongs) ' CASE LenB_FixedLongArray: error + n = LenB(dynamicBytes) ' CASE LenB_DynamicByteArray: error + n = Len(fixedBytes) ' CASE Len_FixedByteArray: error + n = Len(dynamicBytes) ' CASE Len_DynamicByteArray: error + + n = LenB(fixedBytes(0)) ' CASE LenB_ByteElement: none + n = LenB(n) ' CASE LenB_LongVariable: none + End Sub + +End Module +``` + +**Expected behavior** +A compile error on each of the five, as in VB6: the array is not a valid argument. The two controls keep compiling. VB6 gives the compile error *Variable required - can't assign to this expression* for `LenB` of a fixed `Byte` array, a fixed `Long` array and a dynamic `Byte` array, and for `Len` of a fixed and of a dynamic `Byte` array (each was tried in the reproducer's `vb6/` project alone, attached as `lenb-of-array-accepted-vb6.zip`; the control, `LenB(a(0))`, builds and gives 1). + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +BETA 1007 gives the same results. Severity: low. Few programs write it, but the number looks plausible for a small array, and the author finds out only when the buffer is larger. + + + +--- + +## A 64-bit build prints infinity and NaN as `inf` and `nan`, and `Format` adds the picture's decimals + +**Describe the bug** +A `Double` or `Single` that holds infinity or NaN prints in VB6 as `1.#INF`, `-1.#INF`, `1.#QNAN` and `-1.#IND`, and `Format(x, "0.00")` gives `1.#INF` or `1.#R`. A 32-bit twinBASIC build prints exactly those. A 64-bit build prints the C runtime's text: `inf`, `-inf`, `nan` and `-nan(ind)`, and `Format(x, "0.00")` gives `inf.00` and `nan.00`. `CStr`, `Str`, concatenation and `Debug.Print` all give the same text. Arithmetic raises an error rather than make these values, so they come from memory, a COM component or a file; the reproducer copies their bytes into a variable. It was observed in IDE runs and in built exes. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `infinity-text-64-bit.twinproj` (attached as `infinity-text-64-bit.zip`). Its module `InfinityTextTests`, below, has one `[TestCase]` for each text, asserting what VB6 gives, and a control. +2. Run the test cases as a 64-bit build (`tbrun --tests --arch win64`). The first eight fail, with the 64-bit text as `Actual`; the control passes. As a 32-bit build all nine pass. +```vb +[TestFixture] +Module InfinityTextTests + + Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As LongPtr) + + Private Function DoubleFromBits(ByVal Bits As LongLong) As Double + Dim result As Double + CopyMemory result, Bits, 8 + Return result + End Function + + Private Function SingleFromBits(ByVal Bits As Long) As Single + Dim result As Single + CopyMemory result, Bits, 4 + Return result + End Function + + [TestCase] + Public Sub CStrOfInfinity_Is1HashINF() + Assert.Strict.AreEqual "1.#INF", CStr(DoubleFromBits(&H7FF0000000000000^)) + End Sub + + [TestCase] + Public Sub CStrOfNegativeInfinity_IsMinus1HashINF() + Assert.Strict.AreEqual "-1.#INF", CStr(DoubleFromBits(&HFFF0000000000000^)) + End Sub + + [TestCase] + Public Sub CStrOfNaN_Is1HashQNAN() + Assert.Strict.AreEqual "1.#QNAN", CStr(DoubleFromBits(&H7FF8000000000000^)) + End Sub + + [TestCase] + Public Sub CStrOfIndeterminate_IsMinus1HashIND() + Assert.Strict.AreEqual "-1.#IND", CStr(DoubleFromBits(&HFFF8000000000000^)) + End Sub + + [TestCase] + Public Sub ConcatenatedInfinity_Is1HashINF() + Assert.Strict.AreEqual "x=1.#INF", "x=" & DoubleFromBits(&H7FF0000000000000^) + End Sub + + [TestCase] + Public Sub FormatOfInfinity_Is1HashINF() + Assert.Strict.AreEqual "1.#INF", Format(DoubleFromBits(&H7FF0000000000000^), "0.00") + End Sub + + [TestCase] + Public Sub FormatOfNaNWithPicture_Is1HashR() + Assert.Strict.AreEqual "1.#R", Format(DoubleFromBits(&H7FF8000000000000^), "0.00") + End Sub + + [TestCase] + Public Sub CStrOfSingleInfinity_Is1HashINF() + Assert.Strict.AreEqual "1.#INF", CStr(SingleFromBits(&H7F800000)) + End Sub + + [TestCase] + Public Sub CStrOfOrdinaryDouble_IsUnchanged() + Assert.Strict.AreEqual "1.5", CStr(1.5) + End Sub + +End Module +``` + +**Expected behavior** +The same text in a 64-bit build as in a 32-bit one and in VB6: `1.#INF`, `-1.#INF`, `1.#QNAN`, `-1.#IND`, and `1.#INF` or `1.#R` from `Format` with a picture. The reproducer's `vb6/` project prints them (attached as `infinity-text-64-bit-vb6.zip`). + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +BETA 1007 gives the same results. The reproducer's `repro.json` names the 64-bit build (`"arch": "win64"`). Severity: low. A program meets these values rarely, but one that tests or parses the text, or shows `Format(x, "0.00")` of one, behaves differently in the two builds. + + + +--- + +## In a 32-bit build, a constant `Double` expression raises error 6 after a constant that overflows a `Long` or `LongLong` + +**Describe the bug** +In a 32-bit build, a procedure that converts a constant `Double` that does not fit a `Long` or a `LongLong` (`l = 2 ^ 31`, `l = 3000000000#`, `CLng(2 ^ 31)`, `CLngLng(1E+19)`) then raises error 6, *Overflow*, at the next constant `Double` expression that has an operator after the `^`, although that expression is in range: `2 ^ 10 * 2`, `1.5 * 2`, `1E+10 + 1`, `2 ^ 63 - 1024`. The statement completes and stores the right value (2048 for `d = 2 ^ 10 * 2`); the error is reported late, as if a floating-point exception left pending by the first conversion were taken by the next one. It was found as `q = 2 ^ 63 - 1024` with `q As LongLong`, the largest `Double` below 2^63, which raises error 6. It was observed in IDE runs and in built exes, in a 32-bit build only. + +The error depends on the statement it follows, not on the size or the type: +- The expression needs an operator after the `^`. `2 ^ 11`, `2048#`, `9E+18`, `1500 * 2` (Integer arithmetic) and `x * 2` with a `Double` variable do not raise it. +- The destination does not matter: `Long`, `Integer`, `Byte`, `Currency`, `Single`, `Double`, `Variant`, `LongLong`. +- The first statement must convert a constant that does not fit a `Long` or a `LongLong`. A `Byte` or `Integer` constant that does not fit (`b = 256`, `i = 40000`), the same overflow through a `Double` variable (`l = x`) and `Err.Raise 6` do not trigger it. `CLngLng(2 ^ 63)`, which raises no error of its own (see the `CLngLng(2^63)` entry), triggers it. +- `Err.Clear` between the two statements changes nothing. A second constant expression after the one that raised is accepted. +- A function whose body is `F = 2 ^ 63 - 1024` raises the error when it is called from a procedure that has such a conversion, before or after the call. The function then raises it for its other callers too, those with no conversion among them. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `const-double-expression-after-overflow-32bit.twinproj` (attached as `const-double-expression-after-overflow-32bit.zip`). Its module `RegressionTests`, below, has one `[TestCase]` for each of five statements that raise the error, asserting that none is raised, and six controls. +2. Run the test cases in a 32-bit build. The first five fail with `Err.Number` 6; the six controls pass: the same expressions with no overflow before them, a single power, a variable product, a `Byte` overflow and an overflow through a variable. In a 64-bit build all eleven pass. +```vb +[TestFixture] +Module RegressionTests + + ' Called by two cases: the first has the Long overflow, the second has none. + Private Function TwoToThe63MinusK() As LongLong + TwoToThe63MinusK = 2 ^ 63 - 1024 + End Function + + [TestCase] + Public Sub Double_ConstantProductAfterLongOverflow_Accepted() + Dim d As Double, l As Long, n As Long + On Error Resume Next + l = 2 ^ 31 + Err.Clear + d = 2 ^ 10 * 2 + n = Err.Number + Assert.Strict.AreEqual 0, n + Assert.Strict.AreEqual 2048#, d + End Sub + + [TestCase] + Public Sub LongLong_TwoToThe63MinusKAfterLongOverflow_Accepted() + Dim q As LongLong, l As Long, n As Long + On Error Resume Next + l = 2 ^ 31 + Err.Clear + q = 2 ^ 63 - 1024 + n = Err.Number + Assert.Strict.AreEqual 0, n + Assert.Strict.AreEqual 9223372036854774784^, q + End Sub + + [TestCase] + Public Sub LongLong_TwoToThe63MinusKAfterCLngLng1E19_Accepted() + Dim q As LongLong, n As Long + On Error Resume Next + q = CLngLng(1E+19) + Err.Clear + q = 2 ^ 63 - 1024 + n = Err.Number + Assert.Strict.AreEqual 0, n + Assert.Strict.AreEqual 9223372036854774784^, q + End Sub + + [TestCase] + Public Sub Function_ReturnsTwoToThe63MinusK_CallerHasLongOverflow_Accepted() + Dim q As LongLong, l As Long, n As Long + On Error Resume Next + q = TwoToThe63MinusK() + n = Err.Number + l = 2 ^ 31 + Assert.Strict.AreEqual 0, n + End Sub + + [TestCase] + Public Sub Function_ReturnsTwoToThe63MinusK_CallerHasNoOverflow_Accepted() + Dim q As LongLong, n As Long + On Error Resume Next + q = TwoToThe63MinusK() + n = Err.Number + Assert.Strict.AreEqual 0, n + End Sub + + [TestCase] + Public Sub Double_ConstantProduct_Alone_Accepted() + Dim d As Double, n As Long + On Error Resume Next + d = 2 ^ 10 * 2 + n = Err.Number + Assert.Strict.AreEqual 0, n + Assert.Strict.AreEqual 2048#, d + End Sub + + [TestCase] + Public Sub LongLong_TwoToThe63MinusK_Alone_Accepted() + Dim q As LongLong, n As Long + On Error Resume Next + q = 2 ^ 63 - 1024 + n = Err.Number + Assert.Strict.AreEqual 0, n + Assert.Strict.AreEqual 9223372036854774784^, q + End Sub + + [TestCase] + Public Sub Double_SinglePowerAfterLongOverflow_Accepted() + Dim d As Double, l As Long, n As Long + On Error Resume Next + l = 2 ^ 31 + Err.Clear + d = 2 ^ 11 + n = Err.Number + Assert.Strict.AreEqual 0, n + End Sub + + [TestCase] + Public Sub Double_VariableProductAfterLongOverflow_Accepted() + Dim d As Double, x As Double, l As Long, n As Long + x = 1024 + On Error Resume Next + l = 2 ^ 31 + Err.Clear + d = x * 2 + n = Err.Number + Assert.Strict.AreEqual 0, n + End Sub + + [TestCase] + Public Sub Double_ConstantProductAfterByteOverflow_Accepted() + Dim d As Double, b As Byte, n As Long + On Error Resume Next + b = 256 + Err.Clear + d = 2 ^ 10 * 2 + n = Err.Number + Assert.Strict.AreEqual 0, n + End Sub + + [TestCase] + Public Sub Double_ConstantProductAfterRuntimeLongOverflow_Accepted() + Dim d As Double, x As Double, l As Long, n As Long + x = 2 ^ 31 + On Error Resume Next + l = x + Err.Clear + d = 2 ^ 10 * 2 + n = Err.Number + Assert.Strict.AreEqual 0, n + End Sub + +End Module +``` + +**Expected behavior** +No error at any of the five: an expression whose value fits is accepted whatever statement came before it, as in a 64-bit build. VB6 accepts `d = 2 ^ 10 * 2` and `n = 2 ^ 30 + 2 ^ 29` directly after `l = 2 ^ 31` and `l = 2147483648#`, which raise error 6 themselves (the reproducer's `vb6/` project prints them, attached as `const-double-expression-after-overflow-32bit-vb6.zip`; VB6 has no `LongLong`). + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +The reproducer's `repro.json` names the 32-bit build (`"arch": "win32"`). A 64-bit build, IDE run or exe, gives no error for any case. + +This looks like the same cause as #2497 (fixed in BETA 1004), where a constant out of the `Long` range made the first conversion of a `Single` or `Double` variable to an integer raise error 6 in an IDE run. That case is fixed; this one remains in 32-bit builds, in IDE runs and built exes alike. + +Severity: low. The error is raised and is easy to see; the statement that raises it holds a correct value. A program meets it only when a constant that overflows is followed in the same procedure by a constant `Double` expression, which is mostly code that tests overflow handling. + + diff --git a/WIP.Build.md b/WIP.Build.md index 492178631..cfa27f50e 100644 --- a/WIP.Build.md +++ b/WIP.Build.md @@ -476,6 +476,20 @@ an inline ``, welds the code to the word next to it --- `a x would come out as `ax b`. Trimming is conditional on which element bounds the segment, so `
` boundaries trim as they always did.
 
+### HTML comments never reach the output
+
+`stripHtmlCommentsPlugin` (a core rule in `builder/render.mjs`, owner's request) drops every
+`` the markdown holds from all three trees: the SHOT markers, the ``
+region markers and notes. It edits `html_block` and `html_inline` tokens only, which is why a
+comment shown in a fence or a code span is untouched: those are `fence` and `code_inline` tokens,
+escaped by their own renderers. It is registered after `tightLooseListPlugin` and must stay
+there, because that rule sees a comment block as a block of its own when it decides a list item's
+tightness. **Nothing downstream reads a comment from rendered HTML**: `check_package_tests.mjs` and
+`package-test-cases.mjs` read the markdown source, and what the build needs from a comment is
+written by the templates, not by markdown, so it stays: the `Begin/End Jekyll SEO tag` pair
+(`offline-rewrite.mjs` finds the block by it) and the two icon licences in `template.mjs`. A rule
+that needs a marker in a page's output uses an element or an attribute, never a comment.
+
 ### The book refuses a stale source tree
 
 Testing only that `docs\_site-pdf\book.html` **exists** is not enough: edit a
diff --git a/WIP.Harness.md b/WIP.Harness.md
index 6097a92d7..833b03f5d 100644
--- a/WIP.Harness.md
+++ b/WIP.Harness.md
@@ -12,7 +12,7 @@ Read it before changing `scripts/tbbuild.mjs`, `scripts/tbrun.mjs`,
 `scripts/lib/tb-launch.ps1`, `scripts/lib/tb-registry.mjs`,
 `scripts/lib/tb-ide-copy.mjs`, `scripts/lib/tb-project.mjs`,
 `scripts/lib/tb-addin.mjs`, `scripts/lib/tb-click.mjs`, `scripts/lib/tb-operate.mjs`,
-`scripts/lib/tb-lane.mjs`,
+`scripts/lib/tb-lane.mjs`, `scripts/ide_js.mjs`, `scripts/ide-js/lib.mjs`,
 anything under `test/addin/`, or `scripts/census_attributes.mjs`, and before
 concluding anything about twinBASIC syntax from a sweep of exported sources.
 
@@ -586,13 +586,11 @@ The probe is a module with a `[RunAfterBuild]` Sub, which the IDE runs once the
 linked. Reader-facing documentation is the [`tbrun.mjs` entry in
 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
-you build it. On the private desktop that dialog is invisible and unreachable, so the build
-simply never happens --- and **the WebView2 renderer stays responsive throughout**, so
-`Runtime.evaluate` answers normally and every health check says the IDE is fine. `tbrun`
-pins the path in its staged copy, which is why it insists on a source tree it can edit
-rather than a packed project it cannot.
+**The script owns the tree** so that it can pin `project.buildPath` in its staged copy and
+know where the built file is, which is why it insists on a source tree it can edit rather than
+a packed project it cannot. The default `${SourcePath}\Build\...` path opens no dialog: a
+project opened from a `.twinproj` with it builds, and the compiler makes `Build\` itself
+(BETA 1010 and 1007, packed by `impexp` or from the template, in a new folder, built twice).
 
 Rules it follows:
 
@@ -935,11 +933,10 @@ built it, not in the binary:
 The first three are decided when compiling; the last three can only come from the running
 process, and they agree.
 
-**The build path's folder has to be explicit; its file name need not be.** `tbrun` builds
+**The build path's file name need not be explicit.** `tbrun` builds
 into its own `out` folder under the IDE's own name,
 `${ProjectName}_${Architecture}.${FileExtension}`, giving `ArchProbe_win32.exe` and
-`ArchProbe_win64.exe` with no Save dialog --- so the trap above comes from the default
-`${SourcePath}\Build\...`, not from the variables. The name is looked for after the build
+`ArchProbe_win64.exe`. The name is looked for after the build
 rather than assumed, because `${FileExtension}` follows the build type.
 
 **A switch of target is not saved.** The IDE saves the target it last built a project for
@@ -1673,3 +1670,68 @@ target) and reload (47 s: two builds and two restarts).
 - An `--only` that matches nothing is refused with exit code 2. `--timeout 8` ends lanes
   mid-build and leaves the registry as found and nothing running.
 - Exporting the samples leaves the install's `projects` folder as it was, mtimes included.
+
+## Reading and patching the IDE's main.js
+
+`scripts/ide_js.mjs` and its library `scripts/ide-js/lib.mjs` (the rules are in
+[WIP.md](WIP.md#driving-the-twinbasic-compiler), *Fixing the IDE itself*; the usage is on
+[Tools-Defects](docs/Documentation/Tools-Defects.md#ide_jsmjs)). It came out of a study on
+2026-10-18, whose prototype it replaces.
+
+**What the file is.** BETA 1010's `ide/main.js` is one line of 1,263,324 bytes: 314,309
+tokens, no comment, no source map. Its top level is 979 function declarations, 46 classes
+and 793 `var`/`const` declarations, so 1,295 units (functions and class methods). It has
+9,346 locals, 9,245 of them one letter long. The minifier removed whitespace and renamed
+locals and parameters, and nothing else: every top-level name, property and global
+survives, which is why a unit's name is the anchor of the map.
+
+**Why edits are splices, never a print.** Any printer (prettier, recast, astring) rewrites
+the whole file --- quotes, parentheses, spacing --- so a patched copy would differ from the
+install in every byte, and a lane result could not be put down to the fix. acorn's tokens with
+their ranges, the comments and the whitespace between them make up every byte of the file
+(`survey`'s `coverage`, exact on 1007 and 1010), and an edit replaces a range of the original
+text, so a patched `main.js` differs from the install only inside its edits. The view is
+prettier's, for reading and for writing a fix, and nothing goes back from it character by
+character: a fix comes back as the token diff of the unit.
+
+**Why a local is keyed by where it is declared.** The minifier gives the same local another
+letter in another build, so a minified name keys nothing. A scope path and a place in the
+scope's declarations do not change while the code does not, and a unit's shape (its syntax
+tree with every local replaced by its key, hashed) says whether it did. From 1007 to 1010,
+1,294 of the 1,295 units have the same shape; the one that changed is `designer_PropertyPage`.
+The committed map's 13 names, named on 1007, check on 1010 as 9 exact, 1 carried and 3 for
+review, all three in `designer_PropertyPage`.
+
+**Why a `review` name is not applied.** A local used more or less often in a changed unit is
+probably the same variable, but may not be, and a wrong name in a view misleads more than a
+minified one does. `seal` after a look applies it again.
+
+**Why the patch checks shapes twice.** First, the fix in our names and the fix turned back into
+the build's names must have the same shape: a new variable the fix names like one of the
+build's minified names would otherwise take over that name's references. Second, each patched
+unit must have the shape of the fix, so the patched file runs what the fix shows, whatever the
+token diff and the spaces it glues in did.
+
+**Why tokens compare strings by value.** prettier writes `"x"` for `'x'` and `0.5` for `.5`, so
+a fix copied from the view would otherwise rewrite every literal it touches. Parentheses are
+not normalised: prettier adds them around mixed `&&` and `||`, and such a fix comes back with
+edits that add them. They change no shape, so the patch is right, just not the smallest.
+
+**A fix is a diff against one build.** `patch` turns every difference between a fix's unit
+and the build's into an edit, so a fix written on one BETA and patched into a later one whose
+unit changed takes the later build's change back out. Before patching a build other than the
+one a fix was written on, run `shape` on it: its diff is exactly what the patch changes.
+
+**The IDE does not check `main.js`.** A copy of BETA 1010 with the two edits of
+`export-verbose-done-escaped`'s fix ran the export lane: the defect's test reported that it
+no longer reproduced, its two controls and the lane's six other tests passed, and
+`bug_repro.mjs verify debug-print-escaped-twice` said NO LONGER REPRODUCES (it reproduces on an
+unpatched, byte-identical copy). That was the study's prototype; the tool's patch of the
+same fix is byte for byte the same.
+
+**The packages.** eslint-scope does the scope analysis, which decides which identifiers are
+one variable: hoisting, block and class scopes and `catch` all have to be right for a rename
+to be, and it is ESLint's own. prettier prints the view and the fix shape only, and is loaded
+on first use. Both are in `package.json` (owner, 2026-10-18), prettier at an exact version:
+another version may print a unit otherwise, and a fix shape's diff and line numbers are what
+it prints.
diff --git a/WIP.HelpAddin.md b/WIP.HelpAddin.md
index 05b768bd5..e00f4adfb 100644
--- a/WIP.HelpAddin.md
+++ b/WIP.HelpAddin.md
@@ -799,10 +799,12 @@ has the complete list):
   `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
   decision per keyword. Deferred.
-- **Data types** --- `Long`, `String`, `LongPtr` --- have their page at
-  `/Reference/Data-Types`, outside `/tB/`, so the index cannot carry them without breaking
-  its own rule. Giving that page a `/tB/` permalink, with the old one in `redirect_from:`,
-  would. Deferred.
+- **Data types are done** (owner, 2026-10-18), as the one exception to the `/tB/` rule:
+  each type the summary page's `symbols:` names is a language entry of kind `type` at its
+  `##` section of `/Reference/Data-Types` (`#long`), which links to the type's own page,
+  `/tB/Core/Long`. With `package` null, `Find` gives a bare `String` the type before the
+  VBA function, and a hover that names `VBA.Strings` (F1 on `String(5, "x")`) still picks
+  the function; the help lane has a case for each.
 - **The offline tree has no copy of the index.** The add-in embeds its own, and the help
   archive (increment 5) is the offline tree as built, less `HELP_ARCHIVE_SKIP` in
   `builder/help-archive-step.mjs` (the Documentation Development section, Videos,
diff --git a/WIP.Screenshots.md b/WIP.Screenshots.md
index 2b05a2a89..70662e7a9 100644
--- a/WIP.Screenshots.md
+++ b/WIP.Screenshots.md
@@ -340,8 +340,13 @@ whose project does not refer to the VB package.
 - **The zoom box is set by a `change` event on `#zoomSelect`**, never by the arrow keys, which
   also move the selected controls. Designer_Zoom shows the box and its list only, four rows of
   it: the Light theme's list has a smaller font, and the whole list makes the light picture 100
-  pixels shorter than the dark one. The form at 50 % is not in it: its scaled dots differ in
-  every capture, even with the ground left out.
+  pixels shorter than the dark one. The form at 50 % is a picture of its own, Designer_Zoom50,
+  taken `once` (dark only, owner): the grid is turned off for it first with the command
+  `tbFormDesigner_TurnGridIndicatorsOff` (what the toolbar's grid button toggles; settled by the
+  form's style changing), because the scaled dots differ in every capture even with the ground
+  left out. The change is the form's (its tab shows it changed, so the clip is formStateShot's,
+  without the tab row), and `formsReset` discards it: the shots after it keep their dots. Its
+  SVG is 4 % off the PNG (the scaled text), so the page shows the PNG.
 - **The Toolbox is scrolled with the mouse wheel**: a capture never holds more than the real
   window's height, so a taller page does not show more of it. Show Control Names is set from the
   panel's `...` menu and put back afterwards (saved nowhere). The search of Toolbox_Search is
@@ -350,8 +355,14 @@ whose project does not refer to the VB package.
 - **A report's PAGE WIDTH, LEFT MARGIN and RIGHT MARGIN bars show only under the pointer**
   (opacity 0, 1 on hover): Report_Controls rests the pointer on PAGE WIDTH.
 - **Preview_Window** is a running program's window, taken once with `captureWindow` from the
-  IDE's desktop. The File menu is not open in it: a native menu is a window
-  of its own (`#32768`), which the tool cannot yet capture and lay over the form's window.
+  IDE's desktop, with its File menu open. A key pressed on a private desktop reaches no window,
+  so the menu is opened as Alt+F opens it: `postWindowMessage` (`tb-ide.mjs`, the `post` mode
+  of `tb-launch.ps1`) posts `WM_SYSCOMMAND` `SC_KEYMENU` `'f'` to the form's window. The menu is
+  then a top-level window of class `#32768` of the same process (`openMenuWindow`); both windows
+  are captured (`windowPicture`, whose user-name check covers the menu's window texts too) and
+  `withMenu` lays the menu's picture over the form's where it is on the desktop, the ground
+  beyond the form transparent. The menu is drawn with its mnemonics underlined, as the keyboard
+  opens it. `WM_CANCELMODE` posted to the form closes it before the run is stopped.
 - PROPERTIES shows no description line: in BETA 1005 its text is set in a footer of the
   designer's own properties box, which is 0 pixels wide, so nothing is drawn.
 
@@ -364,7 +375,7 @@ project file the lane opens is `project.twinproj`, the name the title shows. `pr
 the same project with Microsoft Scripting Runtime's symbol overridden as `SR` and a type library
 that is not on the machine (`MISSING_LIBRARY`) put in the staged `Settings`; the IDE opens
 Project Settings by itself a second after loading it, and the setup's prepare closes it.
-`projects-start` is the IDE with no project. Its 15 pictures are under `docs/IDE/Projects/Images/`;
+`projects-start` is the IDE with no project. Its 16 pictures are under `docs/IDE/Projects/Images/`;
 `Menu_File_Project` is the File menu with the project open (the Menu pages keep `Menu_File`, with
 none, which `New Project.md` also shows).
 
@@ -396,11 +407,15 @@ none, which `New Project.md` also shows).
   which `PAGE_DEFAULTS` blocks anyway).
 - Settings_Close types into the description box with real keys, after `templateBuildPath`, and
   closes the question with Yes, close, which keeps nothing.
-- **Not taken: the export console** (marker `projects-export-console` left). It was reached (Export
-  Path set in the staged copy to a folder under `PACKAGE_ROOT`, so that the first line holds no
-  user name; the console floated before Clear, which the docked console is too narrow to show; the
-  list scrolled with the wheel to its top and its end, as two parts of a composite), but with
-  Export Verbose every `DONE` line shows `\` for each backslash, a defect not yet decided on.
+- **Export_Console** is File > Export Project with Export Verbose off (owner: the verbose `DONE`
+  lines show `\` for each backslash). `stageProjects` sets `project.exportPath` in the
+  staged `Settings` of both projects setups to `PACKAGE_ROOT\Export` (`PJ_EXPORT`), so that the
+  first line names no user; the shot removes that folder before the export and after it, because
+  a second export without Export Verbose into the folder the first one filled wrote no `COMPLETED`
+  line within 60 s (BETA 1010; not narrowed, not queued: owner, record only). The
+  console is floated (docked, it is too narrow for the first line) and emptied with its Clear
+  button before the export, and emptied again afterwards; the shot is the setup's last. The
+  times are set in the page (`fixConsoleTimes`). Both lines fit with no scrolling.
 
 ## Special IDE states
 
diff --git a/WIP.md b/WIP.md
index 01f016b91..fe879f4d9 100644
--- a/WIP.md
+++ b/WIP.md
@@ -38,7 +38,7 @@ sibling. History goes nowhere: git keeps it.
 
 Reference documentation is **complete** for all thirteen packages: VBA, VBRUN, VB, WebView2, Assert, CustomControls, CEF, WinEventLogLib, WinNamedPipesLib, WinServicesLib, tbIDE, WinNativeCommonCtls and AppGlobalClassObject. It is adapted from primary sources: Microsoft VBA-Docs (CC-BY-4.0) for the runtime library, `.twin` source for the twinBASIC-specific packages. The CEF and WebView2 packages also carry a tutorial set.
 
-Twenty-six **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: ArrayListLib, FileProps, Fmt, Generics, OLEGuids, WinReg, TwinTimerLib, tbFileSysTools, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, TBMANLIB_Xml, tbProjPacker, tbRegExp, tsJson, tsPwGen, VBA_JSON, xCalendar, AutoCompleteLib and VBComDlg complete; BigNumberLib its main class only. TBMANLIB_ConsoleCore's console claims are checked by its test cases, each on a console screen buffer of its own with its input written by `WriteConsoleInput`; TBMANLIB_ConsoleComponents' follow the source, because the samples harness cannot capture a console. Generics (seven tuple classes) and xCalendar (one control) are each one page (owner), so `analyze` in audit mode lists their classes and members as undocumented. What xCalendar's page says it does at run time comes from `test/package-tests/xCalendar/probe/`, which runs the control on a form in a built exe. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names; Generics' `exclude_from_docs` names its test code, `HelloWorldModule`, which the page does not mention. VBComDlg's `exclude_from_docs` names `CommonDialogBase`, and AutoCompleteLib's `AutoCompleteHandle`, each a module whose public procedures only its class calls. tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, and tbProjPacker's its helper modules, `modImpExp` and `modWin32`: the indexer counts a Public Enum as public even inside a Private Module, because a built-in package's are reached through its library symbol written with an asterisk (`*WinNativeCommonCtls`, as `ImlDrawConstants` says). By its bare name no such enum is reachable, in a built-in or a contributed package (TB5079 on BETA 1010). OLEGuids documents each interface's methods on the interface's page. What TBMANLIB_Dialog's pages say its dialogs do comes from `test/package-tests/TBMANLIB_Dialog/probe/`, which operates them in a built exe on a private desktop; run it again when the package's version changes. TBMANLIB_Dialog 1.1.0.0 has a page of its own for each class except cDialog, which keeps a page for each member, so `analyze` in audit mode lists the members of those one-page classes as undocumented; its `exclude_from_docs` names its test module, `TestModule`, and the seven COM interface declarations that cDialogEx calls inside (`IOleWindow`, `IModalWindow`, `IShellItem`, `IShellItemArray`, `IFileDialog`, `IFileOpenDialog`, `IFileSaveDialog`), which its landing page describes in a paragraph. cDialogEx 1.1.0.0 returns the name of the chosen file, not its path, ignores `InitialDir` and ignores a `Filter` that has no closing pipe, each documented as a WARNING. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. tbRegExp 1.9.0.0 raises `vbObjectError + 3016` from a search that uses up its step budget and returns nothing (1.8.0.0's `SearchAborted` is gone); the budget is 10 steps for each character squared, at least 1,000,000 and at most 100,000,000, for the search of one match, and `RegExp.SearchStepLimit` replaces it with a fixed number, documented on its own page and in a section of the landing page. On the dev box (BETA 1010, win32) `\d+x` costs 2.0 × n² steps (2.002 to 2.009 for 500 to 2,000 digits), so it finishes on 7,000 digits (16 s) and raises on 10,000 (16 s); `(a+)+b` raises in 0.2 s on 30 or 300 `a` but takes 16 s on 3,000; `a*` matches 1,000,000 characters whole in about half a second. Its pattern syntax has a page of its own, `PatternSyntax.md`. Each one that is not Experimental has a chapter in the book's Packages part; the Experimental ones are left out of the book. **ArrayListLib, BigNumberLib, GdiPlus, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_ListBoxHScroll, TBMANLIB_Xml, WinReg and xCalendar are listed under Experimental Packages** (`docs/Reference/Experimental.md`, owner): ArrayListLib and WinReg do not compile on BETA 1010 (each documented from its source and a copy of it that builds, its samples compile-only; in a copy of ArrayListLib 1.6.151.1 with the arguments of its three `VSwap` calls passed `ByVal` and `LSetByVal` taking a `LongPtr`, a For Each over a list with no items fails, ToString of such a list never returns, Reverse keeps the middle pair of an even run in place and a second GetEnumerator over a list ignores its arguments; in a copy of WinReg's source with its four failing calls passed `ByVal`, ReadString, GetRegistryValueType and the first ConvertValueToString give wrong results), xCalendar has a grid that keeps the old month's dates when Value is set to another month, and text colour properties that have no effect, BigNumberLib 1.0.2.0 compiles, but loses the minus sign after `IsNegative = True` and in a negative operand of the greatest common divisor and the least common multiple, makes a zero from `AssignFromLong` negative, and does not return from the modular inverse of zero (all four confirmed by a second agent), TBMANLIB 1.0.0.29 does not compile in a 64-bit build (TB5001 in cSubClass, where the window procedure is passed as a Long) and has many members that fail or give wrong results, TBMANLIB_ConsoleComponents cannot take a cConsole of the published TBMANLIB_ConsoleCore (it embeds a copy of it, the same package at 1.0.0.1, whose types the compiler treats as different; checked in a project made by the IDE's own import, in either order), TBMANLIB_ConsoleCore reads keys wrongly, TBMANLIB_Diagnostic cannot create its Dictionary and misplaces List items, TBMANLIB_Dialog 1.1.0.0 has classes that fail at their main task (cDialogFindReplace reports nothing, cDialogPageSetup ignores paper and orientation, cDialogEx returns names without their folder, cDialogPassword shows the password in its confirmation boxes; every defect confirmed by a second agent, owner), TBMANLIB_ListBoxHScroll 1.0.0.1 moves the thumb 20 px per arrow and one page per track click while the list box scrolls its contents by its own step, so thumb and text disagree (confirmed by a second agent, owner), TBMANLIB_Aes runs other cipher modes than the ones named (its default CBC is ECB) and fails the NIST test vectors, and TBMANLIB_Xml's parser misreads self-closing elements and entities and does not return for some malformed text (and its XPath engine reads the step `..` and `and` with `or` wrongly). TBMANLIB_Xml's `exclude_from_docs` names its test module, `mXmlTests`, which its landing page describes in a paragraph, and the five types of its XPath engine (`XPathTokenType`, `XPathAxisType`, `XPathOperator`, `XPathStep`, `XPathPredicate`), which no public member takes or returns and which the mXPath page mentions in a paragraph; tsPwGen's names its five test modules and its six internal modules, and tsJson's its test and internal modules likewise, which their landing pages describe in a paragraph each. TBMANLIB_Diagnostic's `List` page has a NOTE labelled BETA 1010 under its `SetItem` sample: in a 32-bit IDE run `Err.Description` of the error 5 that `SetItem 0` raises on a list of one item (`Dim x As New List(Of String)`) is "Automation error", while the exe, a 64-bit IDE run and VB6 give "Invalid procedure call or argument" (`bugs/list-error5-automation-text`, with its case `ListTests.SetItem_AtZeroOnOneItemList_ErrorHasTheRuntimeText`). The samples harness runs in the IDE and has no per-sample way to state output that differs from an exe's, so that sample prints `Err.Number` only and the text is in the NOTE; when the entry is retired the NOTE goes and the sample prints `Err.Description` again, with the output `5: Invalid procedure call or argument`. See *Where things live* for how a package moves between the two lists.
+Twenty-seven **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: ArrayListLib, DetoursLib, FileProps, Fmt, Generics, OLEGuids, WinReg, TwinTimerLib, tbFileSysTools, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, TBMANLIB_Xml, tbProjPacker, tbRegExp, tsJson, tsPwGen, VBA_JSON, xCalendar, AutoCompleteLib and VBComDlg complete; BigNumberLib its main class only. TBMANLIB_ConsoleCore's console claims are checked by its test cases, each on a console screen buffer of its own with its input written by `WriteConsoleInput`; TBMANLIB_ConsoleComponents' follow the source, because the samples harness cannot capture a console. Generics (seven tuple classes) and xCalendar (one control) are each one page (owner), so `analyze` in audit mode lists their classes and members as undocumented. What xCalendar's page says it does at run time comes from `test/package-tests/xCalendar/probe/`, which runs the control on a form in a built exe. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names; Generics' `exclude_from_docs` names its test code, `HelloWorldModule`, which the page does not mention. VBComDlg's `exclude_from_docs` names `CommonDialogBase`, and AutoCompleteLib's `AutoCompleteHandle`, each a module whose public procedures only its class calls. DetoursLib's names its demonstration module, `DetoursTest` (message boxes), which its landing page describes in a paragraph. Its test cases run in the IDE's own process, so each detours `MulDiv` of kernel32 and answers for itself only for one marked argument, passing every other call (another thread's included) to the original. `CreateDetourAtFuncAddress` does not check a *newAddress* of 0, and SlimDetours then reads address 0 inside a transaction that has the process's other threads suspended; no case tries it, since it would leave the IDE frozen, and the page states the missing check from the source alone. tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, and tbProjPacker's its helper modules, `modImpExp` and `modWin32`: the indexer counts a Public Enum as public even inside a Private Module, because a built-in package's are reached through its library symbol written with an asterisk (`*WinNativeCommonCtls`, as `ImlDrawConstants` says). By its bare name no such enum is reachable, in a built-in or a contributed package (TB5079 on BETA 1010). OLEGuids documents each interface's methods on the interface's page. What TBMANLIB_Dialog's pages say its dialogs do comes from `test/package-tests/TBMANLIB_Dialog/probe/`, which operates them in a built exe on a private desktop; run it again when the package's version changes. TBMANLIB_Dialog 1.1.0.0 has a page of its own for each class except cDialog, which keeps a page for each member, so `analyze` in audit mode lists the members of those one-page classes as undocumented; its `exclude_from_docs` names its test module, `TestModule`, and the seven COM interface declarations that cDialogEx calls inside (`IOleWindow`, `IModalWindow`, `IShellItem`, `IShellItemArray`, `IFileDialog`, `IFileOpenDialog`, `IFileSaveDialog`), which its landing page describes in a paragraph. cDialogEx 1.1.0.0 returns the name of the chosen file, not its path, ignores `InitialDir` and ignores a `Filter` that has no closing pipe, each documented as a WARNING. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. tbRegExp 1.9.0.0 raises `vbObjectError + 3016` from a search that uses up its step budget and returns nothing (1.8.0.0's `SearchAborted` is gone); the budget is 10 steps for each character squared, at least 1,000,000 and at most 100,000,000, for the search of one match, and `RegExp.SearchStepLimit` replaces it with a fixed number, documented on its own page and in a section of the landing page. On the dev box (BETA 1010, win32) `\d+x` costs 2.0 × n² steps (2.002 to 2.009 for 500 to 2,000 digits), so it finishes on 7,000 digits (16 s) and raises on 10,000 (16 s); `(a+)+b` raises in 0.2 s on 30 or 300 `a` but takes 16 s on 3,000; `a*` matches 1,000,000 characters whole in about half a second. Its pattern syntax has a page of its own, `PatternSyntax.md`. Each one that is not Experimental has a chapter in the book's Packages part; the Experimental ones are left out of the book. **ArrayListLib, BigNumberLib, GdiPlus, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_ListBoxHScroll, TBMANLIB_Xml, WinReg and xCalendar are listed under Experimental Packages** (`docs/Reference/Experimental.md`, owner): ArrayListLib and WinReg do not compile on BETA 1010 (each documented from its source and a copy of it that builds, its samples compile-only; in a copy of ArrayListLib 1.6.151.1 with the arguments of its three `VSwap` calls passed `ByVal` and `LSetByVal` taking a `LongPtr`, a For Each over a list with no items fails, ToString of such a list never returns, Reverse keeps the middle pair of an even run in place and a second GetEnumerator over a list ignores its arguments; in a copy of WinReg's source with its four failing calls passed `ByVal`, ReadString, GetRegistryValueType and the first ConvertValueToString give wrong results), xCalendar has a grid that keeps the old month's dates when Value is set to another month, and text colour properties that have no effect, BigNumberLib 1.0.2.0 compiles, but loses the minus sign after `IsNegative = True` and in a negative operand of the greatest common divisor and the least common multiple, makes a zero from `AssignFromLong` negative, and does not return from the modular inverse of zero (all four confirmed by a second agent), TBMANLIB 1.0.0.29 does not compile in a 64-bit build (TB5001 in cSubClass, where the window procedure is passed as a Long) and has many members that fail or give wrong results, TBMANLIB_ConsoleComponents cannot take a cConsole of the published TBMANLIB_ConsoleCore (it embeds a copy of it, the same package at 1.0.0.1, whose types the compiler treats as different; checked in a project made by the IDE's own import, in either order), TBMANLIB_ConsoleCore reads keys wrongly, TBMANLIB_Diagnostic cannot create its Dictionary and misplaces List items, TBMANLIB_Dialog 1.1.0.0 has classes that fail at their main task (cDialogFindReplace reports nothing, cDialogPageSetup ignores paper and orientation, cDialogEx returns names without their folder, cDialogPassword shows the password in its confirmation boxes; every defect confirmed by a second agent, owner), TBMANLIB_ListBoxHScroll 1.0.0.1 moves the thumb 20 px per arrow and one page per track click while the list box scrolls its contents by its own step, so thumb and text disagree (confirmed by a second agent, owner), TBMANLIB_Aes runs other cipher modes than the ones named (its default CBC is ECB) and fails the NIST test vectors, and TBMANLIB_Xml's parser misreads self-closing elements and entities and does not return for some malformed text (and its XPath engine reads the step `..` and `and` with `or` wrongly). TBMANLIB_Xml's `exclude_from_docs` names its test module, `mXmlTests`, which its landing page describes in a paragraph, and the five types of its XPath engine (`XPathTokenType`, `XPathAxisType`, `XPathOperator`, `XPathStep`, `XPathPredicate`), which no public member takes or returns and which the mXPath page mentions in a paragraph; tsPwGen's names its five test modules and its six internal modules, and tsJson's its test and internal modules likewise, which their landing pages describe in a paragraph each. TBMANLIB_Diagnostic's `List` page has a NOTE labelled BETA 1010 under its `SetItem` sample: in a 32-bit IDE run `Err.Description` of the error 5 that `SetItem 0` raises on a list of one item (`Dim x As New List(Of String)`) is "Automation error", while the exe, a 64-bit IDE run and VB6 give "Invalid procedure call or argument" (`bugs/list-error5-automation-text`, with its case `ListTests.SetItem_AtZeroOnOneItemList_ErrorHasTheRuntimeText`). The samples harness runs in the IDE and has no per-sample way to state output that differs from an exe's, so that sample prints `Err.Number` only and the text is in the NOTE; when the entry is retired the NOTE goes and the sample prints `Err.Description` again, with the output `5: Invalid procedure call or argument`. See *Where things live* for how a package moves between the two lists.
 
 The rest of this file is the maintenance guide for updating existing pages or adding new ones: package surface notes, page conventions, and the integrity check.
 
@@ -85,6 +85,7 @@ The rest of this file is the maintenance guide for updating existing pages or ad
 - `docs/Reference/Built-In/tbIDE/` — IDE Extensibility package (this is the **addin SDK**). The package is type-only — it ships **public interfaces + CoClasses** that an addin DLL binds to; every implementation behind them lives in the twinBASIC IDE itself. The user-facing surface is one entry-point factory (`tbCreateCompilerAddin`) plus 23 CoClasses grouped by role: the addin contract (`AddIn`), the root API (`Host`), the loaded `Project`, the editors collection (`Editor` / `CodeEditor` / `Editors`), the virtual file system (`FileSystem` / `FileSystemItem` / `Folder` / `File`), the in-IDE UI surface (`Toolbar` / `Toolbars` / `Button` / `ToolWindow` / `ToolWindows`), the HTML DOM inside a tool window (`HtmlElement` / `HtmlElements` / `HtmlElementProperty` / `HtmlElementProperties` / `HtmlEventProperty` / `HtmlEventProperties`), the `DebugConsole`, `KeyboardShortcuts`, `Themes`, and the single concrete user-instantiable helper class `AddinTimer`. Flat layout — one page per CoClass / Class plus the index landing.
 - `docs/Reference/Contributed/` — the contributed packages, one folder each, landing page `Contributed.md` beside it. **A package's landing page opens, under its H1, with `**Author:**`** (owner): the publisher TWINSERV lists (`publisher` in `indexer/.packages/manifests/contributed.json`), or the author the package's own sources name when that is someone else (VBA_JSON: Tim Hall, published by WaynePhillipsEA). Next to the author, on the same line, `**Version:**`: the version the section documents, which is its `indexed_from` and changes with it (owner). Then `**On GitHub:**` with a link, when a repository holds the package, or the library the package says it is made from; search GitHub for the package's name and the author's account, and check that the repository holds the package's code (its history too: `TBMANLIB_*` were never in woeoio's `tbman`). A landing page with one or two sections has no `* TOC` list. A class with member pages is a folder, `/index.md` at `/tB/Packages/Contributed///` with its members beside it, so a member links to its class as `(.)`; a class documented on one page is `.md`. Each package that is not Experimental is a chapter of the book's Packages part in `_book.yml`. A section's `indexed_from` is the four-part TWINSERV version, and its snapshot is under `indexer/snapshots/contributed/`. **Every NOTE and WARNING of a section is backed by test cases** in the package's test project, `test/package-tests//` (owner; [its README](test/package-tests/README.md)), and names them in a `` marker on the line above it, which `check_package_tests.mjs` in `test.bat` enforces. **A NOTE or WARNING is only for a real difference or limit** (owner): a statement that describes how the package works stays prose, tied to its cases by a region, the marker on a line of its own above it and `` after it (owner). A difference from the object the package replaces that gives a different result silently (tbRegExp's `\1(a)`, `{,m}`, `$~` against VBScript) is a NOTE, not a WARNING (owner). A case checks what is correct, not what the package does (owner): a defect is a failing case, recorded in the project's `expect.json`, so the project tells the package's author what to fix. A section whose project is not written yet is in that gate's `PENDING` set. **A package that does not compile on the newest BETA gets no test project** (owner: ArrayListLib, WinReg): it stays in `PENDING` until a version of it compiles. One that compiles for one architecture only is tested on that one (owner: TBMANLIB, win32; `arches` in its `expect.json`, test/package-tests/README.md). `package-tests.bat` runs the cases; a case it reports *fixed* means the callout goes. **The landing page ends, after See Also, with `## Test cases`**: one or two sentences linking to `-tests.twinproj` (owner), which the build publishes beside the section without the package's code in it (owner; one exception, owner: TBMANLIB_ConsoleComponents' project carries verbatim copies of seven of its component files, in `Sources/ComponentSources/`, because the package's own components cannot be given a console object, and its drawing can be checked only on the copies): it references the package as a linked package, which the reader puts in `%APPDATA%\twinBASIC\packages` ([WIP.PackageTests.md](WIP.PackageTests.md)). Its samples compile in the `contributed-` sample template, whose `Settings` names the package's version: **change that version with `indexed_from`** ([WIP.ExamplesBuild.md](WIP.ExamplesBuild.md#template-projects)). **A contributed package is documented and its samples compiled against the newest twinBASIC BETA installed, never an older one** (owner): the version that matters is the package's, and a statement about the compiler (such as "does not compile on BETA 1010") names that newest build. **A contributed package that does not compile on that build, or has defects that a program would meet, is listed under Experimental Packages** (owner); a minor defect that the section documents does not make it Experimental (owner: VBComDlg, whose Replace All raises no event, stays Contributed; owner: nor does a fixed limit that the section states at the top of its landing page; owner: tsJson 1.0.15.0 stays Contributed, its defects confirmed by a second agent and each stated in a WARNING with a way round it). **A package moves from Contributed to Experimental for its defects only after a separate agent has reproduced each defect independently** and ruled out misuse, a wrong expectation, a twinBASIC defect and the harness (owner): `docs/Reference/Experimental.md`, at `/tB/Packages/Experimental/`, after Contributed Packages. Its landing page's `parent:` is `Experimental Packages`, its line is in `Experimental.md` and not in `Contributed.md`, and it has no chapter in the PDF book (owner: the book is for packages ready for use in a program): its entry is in `left_out:` of `docs/_book.yml`, in the list of Experimental packages, and the Experimental Packages page is left out with it. Its folder and URLs stay under `Contributed/`, so a package that is fixed moves back by changing only those three and the book (its entry leaves `left_out:` and its chapter is added after VBComDlg's), and its pages keep their addresses. A link from a book page to a left-out one opens the website.
 - `docs/Downloads/index.md` — the Downloads page, `/Downloads`, a top-level page at `nav_order: 4` and in `_book.yml`'s `left_out:`. **Every file the documentation publishes for readers has a card or a row there**, and the page that documents it keeps its own link (owner). Cards are `
` inside `
` (styles at the end of `custom.scss`), with the theme's `.btn` for each file. The release job publishes every same-site link on the built page that has the `download` attribute, beside the zip and the PDF, so a new download needs no change to the workflow. `{{tbdocs:size:}}` gives a published file's size and `{{tbdocs:packageTestRows}}` the test projects' rows, both expanded before render; the help add-in's size is prose ("about 51 MB" in October 2026; it is also on the Help Add-In page), because its project embeds the offline tree, this page included, so it cannot be known before render. The PDF's release URL spells it `twinBASIC.Book.pdf`: GitHub turns the space into a dot, and the spaced name 404s. The footer's **Read Offline** link (`read_offline_link` in `_config.yml`) goes to the page's `#read-offline`. +- `docs/Reference/Data-Types.md` and `docs/Reference/Data-Types/.md` — the intrinsic data types (owner, 2026-10-18): one page per type at `/tB/Core/` (`parent: Data Types`, `grand_parent: Reference Section`, since the Features page *New Data Types* has the same title), and the summary page, `/Reference/Data-Types`, with one `## ` section per type that links to it. The summary's `symbols:` names the types, and the symbol index points each at its section (kind `type`, the one URL outside `/tB/`), so keep those headings' text. `Long.md` is the model page. - `docs/Reference/Statements.md` — alphabetical index of language statements. - `docs/Reference/Procedures and Functions.md` — alphabetical index of procedures/functions. - `docs/LLVM/` — the LLVM section: compiling with the LLVM back end. A top-level section between Features and Reference Section in the nav, at `nav_order: 6` (Features is 5). `index.md` is the landing page and `Getting-Started.md` the only page so far, with its screenshots under `Images/`. Everything it describes arrived in **BETA 984**. Its language claims are checked on BETA 1010 with `tbrun --llvm` and `--exe`: the comma, space and comma-space spellings of `[CompilerOptions]` all turn LLVM on (`+optimize` alone does not, and an attribute replaces the project's options, so `[CompilerOptions("+optimize")]` is non-LLVM under project-wide LLVM), `[CompilerOptions("")]` turns it off under project-wide LLVM, the long CPU-flag sample runs, an error is lost between procedures when either the callee or the caller is LLVM-compiled, `Err.Raise` is unreadable in an LLVM handler (`-353703423`), a `Double` `*`, `+`, `-` or `^` overflow raises no error (`^` gives 0, the others infinity; `FloatingPointErrorChecks(True)` changes nothing), the conversions round half away from zero, and `Debug.Assert` is skipped and its condition not evaluated, and an exe built with LLVM ends with an access violation and no box at the first `GoSub` it runs, 32-bit and 64-bit, while an IDE run is right (the page's WARNING and the GoSub...Return page's, `bugs/gosub-llvm-exe-crash`), and a 64-bit LLVM call of a `Declare` that passes `ByVal 0&` for a ByRef eighth parameter of a 32-bit type can end with an access violation in the API, in an exe and in an IDE run, depending on what the stack held (the first to seventh parameters and a `ByRef LongPtr` eighth are right; the page's WARNING and the Declare page's, `bugs/llvm64-byval-to-byref-declare-crash`), and a call that passes `ByVal` for a ByRef parameter of a twinBASIC Sub or Function is a codegen error in either architecture, after which the calling procedure does not run (a 64-bit exe faults on entering it; the page's WARNING and the Call page's, `bugs/llvm-byval-to-byref-sub-codegen`), and a `For i = 1 To 7 Step x` whose `x` is a `ByVal Long` or `Single` parameter, in a procedure with a `ByRef ... As Boolean` or `As Integer` parameter after `x`, runs with a wrong step (or a wrong start or end, when those are parameters too) in an exe and in an IDE run, silently; the value depends on where the caller's argument for the `ByRef` parameter is stored, 20 in a 64-bit build and 25 in a 32-bit one for a local (a second parameter of another type, `ByVal`, or placed first, an `Integer`, `Double` or `Currency` step, a `Do` loop, no `Step` and a literal `Step` are right; the page's WARNING and the For...Next page's, `bugs/llvm-for-step-constant`). Also on 1010: with LLVM on under Compiler Options (DEBUG) for the whole project, an IDE run of a `[CompilerOptions("")]` procedure that calls an LLVM-compiled one ends in an access violation at the call (a built exe is fine; the page's WARNING). Its samples are marked `check_build`, which compiles them; `examples.bat --llvm` also builds them with LLVM. @@ -135,7 +136,7 @@ node scripts/tbrun.mjs # 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: **every run takes a slot from `tbbroker`** ([WIP.Broker.md](WIP.Broker.md)) --- a private desktop, and the number its user-data folder, temp folder (`%TEMP%\tbbuild-tmp-`), work folders and project ids are named by --- **and its IDE's DevTools port is one Windows picks** (`--remote-debugging-port=0`, read back from `DevToolsActivePort`), so runs at once never meet on a port or a desktop. Never choose a port for an IDE, and never make a desktop: `leaseSlot`, `launchIde` and `launchOnDesktop` (`scripts/lib/tb-ide.mjs`) take them from the broker. `shutdownIde` removes the temp folder and the user-data folder (`%TEMP%\tbbuild-wv2-`, 28 MB) once the IDE has ended, and `launchIde` empties both again at the next launch in the slot, for what a killed run left; a kept IDE's stay until the next run. - **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, 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 its limit, 7 with `--tests`, a case failed. **Every phase's limit is in `test/harness.json`**, with an option and a `TB_` variable over it: [test/harness.md](test/harness.md). +- **`tbrun` takes an exported tree, not a `.twinproj`**, because it pins `project.buildPath` in its own staged copy, to know where the built file is. 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, 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 its limit, 7 with `--tests`, a case failed. **Every phase's limit is in `test/harness.json`**, with an option and a `TB_` variable over it: [test/harness.md](test/harness.md). - **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 1010 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 : `. 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 ""`) 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 1010) --- 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. @@ -169,6 +170,17 @@ named for the runner's process, so runs of either runner at once never share any rule above applies to it unchanged: the private `APPDATA`, `TB_ADDIN_TEST=1`, and ending an IDE by its pid. Its scenario files import `scenario` from `test/addin/scenario.mjs`. +**Fixing the IDE itself** is `scripts/ide_js.mjs`, which reads, names and patches the IDE's +minified `ide/main.js`, so that an IDE entry of `BUGS-TO-REPORT.md` can carry a fix tested on a +patched copy of the install (modifying the IDE has Wayne Phillips' blessing; owner). How it +works, and what it rests on: [WIP.Harness.md](WIP.Harness.md#reading-and-patching-the-ides-mainjs). + +- **Patch only a copy of an install, never one on the Desktop.** `patch --copy-to ` makes the copy under `%TEMP%` (`makeIdeCopy`) and writes the patched `main.js` into it; pass the copy's `twinBASIC.exe` to `ide-test.bat --ide`, and delete the copy when done. `patch --out` refuses a file inside an install that is not under `%TEMP%`. Never write a patched `main.js` anywhere else, by this tool or by hand. +- **Wayne Phillips' own sources are never used** (owner): not fetched, not looked for (GitHub, the package server, caches), not kept on this machine. The tool works only from what an install ships. Every name in `scripts/ide-js/names.json` is ours, given from what the code does, and a fix shape says so. +- **No copy of `main.js`, whole or viewed, goes into the repository**: `patch --out` and `view --out` refuse a file inside it, and `test/ide-js.test.mjs` works on a few lines written in the test. A view goes under `%TEMP%`. +- **A fix is `bugs//fix.js`**, whole units in the map's names, beside the entry's reproducer. Name in the map every local the fix shows, so the fix shape reads as code. +- **Run `shape` before patching any build but the fix's own.** Every difference between the fix's unit and the build's becomes an edit, so a unit the later build changed would have that change taken out; `shape`'s diff is exactly what the patch changes. + **Trying the help add-in by hand** is `try-help-addin.bat` (`scripts/try_help_addin.mjs`): one IDE set up as a lane is (`Lane` from `scripts/lib/tb-lane.mjs`), on the user's desktop, opened on `test/addin/helphost` with `docs/_site` served to the pane. @@ -550,7 +562,7 @@ 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`), 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 IDE-folder unit tests (`node --test test/ide-folders.test.mjs`), the broker tests (`node --test test/broker.test.mjs`, skipped unless `TBBROKER_EXE` names a built broker), the add-in project-file tests (`node --test test/addin-project.test.mjs`), the package-indexer tests (`node --test test/indexer.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 package-test markers and projects (`scripts/check_package_tests.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 IDE-folder unit tests (`node --test test/ide-folders.test.mjs`), the broker tests (`node --test test/broker.test.mjs`, skipped unless `TBBROKER_EXE` names a built broker), the add-in project-file tests (`node --test test/addin-project.test.mjs`), the package-indexer tests (`node --test test/indexer.test.mjs`), the IDE `main.js` tool's tests (`node --test test/ide-js.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 package-test markers and projects (`scripts/check_package_tests.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; ~190 s over the 2,660 samples marked (1,076 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 ` 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). @@ -563,6 +575,7 @@ Why the report separates the wedged task from the merely blocked ones, and why - **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. **The archive leaves out `HELP_ARCHIVE_SKIP`** (`builder/help-archive-step.mjs`): the Documentation Development section (`BuildInfo.html` and `assets/images/gantt.svg` with it, so the chart never reaches the zip), Videos and their poster frames, Challenges, and **every download, which stays online** (owner): a file readers download goes in a `downloads` folder beside its page, and the step fails the build when the Downloads page offers a file the archive would hold. The add-in's server redirects any file the archive lacks to the website, because pages link to all of these; a new entry needs nothing else. 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//` 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 `-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/-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//`; `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): 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`. Each `tbbuild` and `tbrun` it starts takes a slot of its own from `tbbroker`; `--jobs N` runs N at once. Its command-line cases are in `scripts/lib/cli-cases.mjs`, and need no IDE. - `node scripts/vb6run.mjs ` and `node scripts/vb6run.mjs --docs [--only ]` --- 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). +- `node scripts/ide_js.mjs ` --- reads, names and patches the IDE's minified `ide/main.js` ([Tools-Defects](docs/Documentation/Tools-Defects.md#ide_jsmjs)): `survey`, `units`, `list`, `name`, `seal`, `check`, `view`, `patch` and `shape`. It reads files and starts nothing; the rules for it are under [Driving the twinBASIC compiler](#driving-the-twinbasic-compiler) (*Fixing the IDE itself*). It has no wrapper and is outside every gate and CI, since it needs an install; its tests are `test/ide-js.test.mjs`, in `test.bat`, and its command-line cases are in `scripts/lib/cli-cases.mjs`. **After a new BETA, run `check` on it**, and `seal` the map once each `review` and `lost` name has been looked at. 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 indexer/twin-index.mjs derive` 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 (or `--release`), so **run it when the reference is re-indexed against a newer build** and commit it with the pages and the snapshots. See [WIP.HelpAddin.md, Stage 3](WIP.HelpAddin.md#stage-3-the-symbol-index-generated-by-the-docs-build). @@ -576,7 +589,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/`, `indexer/`, `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-two of its twenty-six gates cannot be affected by an edit under `docs/` at all. **Four can.** `check_package_tests.mjs` reads the callouts of the contributed packages' sections. `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 `/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/`, `indexer/`, `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-four of its twenty-eight gates cannot be affected by an edit under `docs/` at all. **Four can.** `check_package_tests.mjs` reads the callouts of the contributed packages' sections. `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 `/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 @@ -639,6 +652,7 @@ wrapper: | `test.bat` | `test/broker.test.mjs` | `tbbroker` and its client (`scripts/lib/tb-broker.mjs`): one broker per pipe, leases, held processes, waiting, ports, a desktop left with windows on it, a restarted broker finding a desktop in use. Skipped unless `TBBROKER_EXE` names a built broker. 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` | | `test.bat` | `test/indexer.test.mjs` | the package indexer (`indexer/lib/`) keeps its promise that a snapshot changes only when the API does: VB6 files named by `VB_Name`, else by their file; a package's files in code-unit order with the `Packages` folder left out; `api.json` types sorted by path with `in` for nested ones, members by name, Enum values and Type fields in declaration order, a type character stripped from an unbracketed name only, the same bytes from the same files in any order; `diffApi` and `auditCoverage` on small hand-made inputs. Reads `indexer/sample.twinpack`; no network, git or install. Run by `node --test` | +| `test.bat` | `test/ide-js.test.mjs` | `scripts/ide_js.mjs` and `scripts/ide-js/lib.mjs`, on a few lines of minified JavaScript written in the test: the tokens, comments and whitespace are every byte; a local's key is the same under other names, and so is a unit's shape; a name is exact, carried, for review or lost as its unit and binding changed; the map refuses a name that is not an identifier, is given twice or hides another; a fix written from the view becomes the smallest token edit, every other byte kept, and a fix of a missing unit or with a new variable named like a minified one is refused; the tool writes a patched `main.js` and a copy of an install only under `%TEMP%`, never into the repository. Run by `node --test` | | `test.bat` | `check_pdf_shims_equiv` | the book's pdf-lib shims write what stock pdf-lib writes: one document written by the gate and one built with `PDFDocument.create` are each saved both ways in child processes, and each pair of files is compared object by object with streams inflated and their cross-reference entries checked; every shim must run, and the members of pdf-lib the shims patch must be those its `PATCHES` lists, each run in one document or the other unless marked there as not reached; each of them must also be in the target table its shim passes to `checkTargets`, which `book/lib/shim-targets.mjs` records for the side to ask | | `test.bat` | `check_impexp_parity` | `impexp.mjs` and `impexp.py` pass the same built-in tests, and one sequence of commands through each gives the exit code each command is there for, the same output and the same files. Without Python it reports itself skipped and passes, but fails when `CI=true`. With `IMPEXP_EXE` naming the built exe of the twinBASIC edition (`scripts/impexp/`, Windows only), it compares that too | | `test.bat` | `check_publish_policy`, `check_gate_lists`, `check_page_baseline`, `check_book_coverage`, `check_axe_patch_equiv` | the gates on the gates | diff --git a/add-in/Resources/SYMBOLS/symbols.json b/add-in/Resources/SYMBOLS/symbols.json index 31d6d79ee..d8e114075 100644 --- a/add-in/Resources/SYMBOLS/symbols.json +++ b/add-in/Resources/SYMBOLS/symbols.json @@ -4,6 +4,20 @@ "packages": {"VBA":"VBA","VBRUN":"VBRUN","VB":"VB","AppGlobalClassProject":"AppGlobalClassObject","cefPackage":"CEF","CustomControls":"CustomControls","CustomControlsPackage":"CustomControls","Assert":"Assert","WebView2Package":"WebView2","WinEventLogLib":"WinEventLogLib","WinNamedPipesLib":"WinNamedPipesLib","WinNativeCommonCtls":"WinNativeCommonCtls","WinServicesLib":"WinServicesLib","tbIDE":"tbIDE"}, "interfaces": {"VBA._Collection":"VBA.Collection","VBA._ErrObject":"VBA.ErrObject","VBA.ITbExpressionService":"VBA.TbExpressionService","VBRUN._PropertyBag":"VBRUN.PropertyBag","VB._App":"VB.App","VB._Clipboard":"VB.Clipboard","VB._Screen":"VB.Screen","VB.VBGlobal":"VB.Global","AppGlobalClassProject._App":"AppGlobalClassProject.App","CustomControls._CustomControlTimer":"CustomControls.CustomControlTimer","CustomControls._CustomControlContext":"CustomControls.CustomControlContext","CustomControls._CustomFormContext":"CustomControls.CustomFormContext","CustomControls._CustomControlsCollection":"CustomControls.CustomControlsCollection","tbIDE.IAddInV1":"tbIDE.AddIn","tbIDE.IButtonV1":"tbIDE.Button","tbIDE.ICodeEditorV1":"tbIDE.CodeEditor","tbIDE.IDebugConsoleV1":"tbIDE.DebugConsole","tbIDE.IEditorV1":"tbIDE.Editor","tbIDE.IEditorsV1":"tbIDE.Editors","tbIDE.IFileV2":"tbIDE.File","tbIDE.IFileSystemV1":"tbIDE.FileSystem","tbIDE.IFileSystemItemV1":"tbIDE.FileSystemItem","tbIDE.IFolderV1":"tbIDE.Folder","tbIDE.IHostV1":"tbIDE.Host","tbIDE.IHtmlElementV1":"tbIDE.HtmlElement","tbIDE.IHtmlElementPropertiesV1":"tbIDE.HtmlElementProperties","tbIDE.IHtmlElementPropertyV1":"tbIDE.HtmlElementProperty","tbIDE.IHtmlElementsV1":"tbIDE.HtmlElements","tbIDE.IHtmlEventPropertiesV1":"tbIDE.HtmlEventProperties","tbIDE.IHtmlEventPropertyV1":"tbIDE.HtmlEventProperty","tbIDE.IKeyboardShortcutsV1":"tbIDE.KeyboardShortcuts","tbIDE.IProjectV1":"tbIDE.Project","tbIDE.IThemesV1":"tbIDE.Themes","tbIDE.IToolWindowV1":"tbIDE.ToolWindow","tbIDE.IToolWindowsV1":"tbIDE.ToolWindows","tbIDE.IToolbarV1":"tbIDE.Toolbar","tbIDE.IToolbarsV1":"tbIDE.Toolbars"}, "symbols": [ + {"name":"Boolean","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#boolean"}, + {"name":"Byte","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#byte"}, + {"name":"Currency","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#currency"}, + {"name":"Date","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#date"}, + {"name":"Decimal","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#decimal"}, + {"name":"Double","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#double"}, + {"name":"Integer","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#integer"}, + {"name":"Long","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#long"}, + {"name":"LongLong","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#longlong"}, + {"name":"LongPtr","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#longptr"}, + {"name":"Object","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#object"}, + {"name":"Single","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#single"}, + {"name":"String","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#string"}, + {"name":"Variant","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#variant"}, {"name":"AddressOf","package":null,"container":null,"kind":"operator","url":"/tB/Core/AddressOf"}, {"name":"Alias","package":null,"container":null,"kind":"statement","url":"/tB/Core/Alias"}, {"name":"And","package":null,"container":null,"kind":"operator","url":"/tB/Core/And"}, diff --git a/bugs/build-autoincrement-not-saved/repro.json b/bugs/build-autoincrement-not-saved/repro.json new file mode 100644 index 000000000..8a5a3c20b --- /dev/null +++ b/bugs/build-autoincrement-not-saved/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:building", + "tests": [ + "Auto-Increment raises the version in the IDE only: the project is not marked changed, its file keeps the old number, and Close Project asks nothing" + ], + "steps": "1. Open a saved Standard EXE project whose Project Settings has (VERSION) Auto-Increment set to Revision, at version 1.0.7.3.\n2. Press Build. The console prints [BUILD] Auto-incremented next build number to: 1.0.7.4.\n3. Press Build again: ... to: 1.0.7.5. The Settings the IDE holds say 1.0.7.5; the project is not marked changed.\n4. File > Close Project: nothing is asked. The project file still says 1.0.7.3, and the next session builds 1.0.7.3 again." +} diff --git a/bugs/build-path-projectid-empty/repro.json b/bugs/build-path-projectid-empty/repro.json new file mode 100644 index 000000000..86eb04f30 --- /dev/null +++ b/bugs/build-path-projectid-empty/repro.json @@ -0,0 +1,6 @@ +{ + "mode": "lane", + "lane": "ide:building", + "tests": ["${ProjectID} in Build Output Path becomes an empty string"], + "steps": "1. Open a saved Standard EXE project named BuildProbeExe, which has a Project ID.\n2. In Project Settings, set Build Output Path to ${SourcePath}\\Build\\${ProjectID}_${ProjectName}.${FileExtension}.\n3. Press Build. The exe is written as Build\\_BuildProbeExe.exe: ${ProjectID} became an empty string.\nControl: ${SourcePath}, ${ProjectName} and ${FileExtension} in the same path are expanded." +} diff --git a/bugs/build-save-box-writes-nothing/repro.json b/bugs/build-save-box-writes-nothing/repro.json new file mode 100644 index 000000000..39d8e9078 --- /dev/null +++ b/bugs/build-save-box-writes-nothing/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:building", + "tests": [ + "Save in the build's 'You must first save the project' box takes the path but writes no project file and builds nothing, and Close Project then asks to save" + ], + "steps": "1. Start the IDE with no project, choose Standard EXE, Open, then Continue.\n2. Press Build. The box reads You must first save the project. Save now? Choose Save, and pick a file name in the Save As dialog.\n3. No .twinproj is written, nothing is built, and the console prints nothing. The project stays marked changed.\n4. File > Close Project asks You've made changes to this project. Save now?\nControl: Ctrl+S then writes the project file." +} diff --git a/bugs/clean-hklm-delete-builds/repro.json b/bugs/clean-hklm-delete-builds/repro.json new file mode 100644 index 000000000..f777e064e --- /dev/null +++ b/bugs/clean-hklm-delete-builds/repro.json @@ -0,0 +1,6 @@ +{ + "mode": "lane", + "lane": "ide:building-hklm", + "tests": ["Clean shows the same box, and its Delete It And Continue builds a new DLL instead of deleting the old one"], + "steps": "1. In an IDE that is not elevated, open an ActiveX DLL project with Register DLLs to HKLM set to YES (answer Continue Anyway to the HKLM warning).\n2. Press Build: the DLL is written, and its registration fails.\n3. Press Clean. The box reads Build failed: unable to unregister the previous DLL. Continue anyway? Choose Delete It And Continue.\n4. The console prints [BUILD] Starting... and [LINKER] SUCCESS created output file ...: a new DLL is built, and the old one is not deleted.\nControl: at Build, Delete It And Continue in the same box builds the DLL again; Clean of an ActiveX DLL registered in HKCU deletes it and its registration." +} diff --git a/bugs/clnglng-2-to-63-no-overflow/clnglng-2-to-63-no-overflow.twinproj b/bugs/clnglng-2-to-63-no-overflow/clnglng-2-to-63-no-overflow.twinproj new file mode 100644 index 000000000..868fdb08a Binary files /dev/null and b/bugs/clnglng-2-to-63-no-overflow/clnglng-2-to-63-no-overflow.twinproj differ diff --git a/bugs/clnglng-2-to-63-no-overflow/repro.json b/bugs/clnglng-2-to-63-no-overflow/repro.json new file mode 100644 index 000000000..d09f50ea4 --- /dev/null +++ b/bugs/clnglng-2-to-63-no-overflow/repro.json @@ -0,0 +1,16 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^FAIL LongLongOverflowTests.CLngLng_ConstantTwoToThe63_Raises6: ", + "^FAIL LongLongOverflowTests.CLngLng_DoubleLiteral2To63_Raises6: ", + "^FAIL LongLongOverflowTests.Assign_UnsuffixedLargestLongLongLiteral_Raises6: ", + "^FAIL LongLongOverflowTests.CLngLng_ConstantSingleTwoToThe63_Raises6: ", + "^PASS LongLongOverflowTests.CLngLng_VariableTwoToThe63_Raises6$", + "^PASS LongLongOverflowTests.CLngLng_ConstantOneE19_Raises6$", + "^PASS LongLongOverflowTests.CLngLng_ConstantBelowTwoToThe63_Accepted$", + "^PASS LongLongOverflowTests.CLngLng_ConstantMinus2To63_Accepted$" + ] + } +} diff --git a/bugs/clnglng-2-to-63-no-overflow/src/Settings b/bugs/clnglng-2-to-63-no-overflow/src/Settings new file mode 100644 index 000000000..b9ea6f361 --- /dev/null +++ b/bugs/clnglng-2-to-63-no-overflow/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "Clnglng2To63NoOverflow", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: x", + "project.exportPathIsV2": true, + "project.id": "{49CD240F-D98A-4846-845F-C1F4CF2FBC5E}", + "project.name": "Clnglng2To63NoOverflow", + "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/clnglng-2-to-63-no-overflow/src/Sources/RegressionTests.twin b/bugs/clnglng-2-to-63-no-overflow/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..0a7aeb235 --- /dev/null +++ b/bugs/clnglng-2-to-63-no-overflow/src/Sources/RegressionTests.twin @@ -0,0 +1,81 @@ +' Regression tests: 2^63 (9223372036854775808) is one past the largest LongLong, and +' converting it must raise error 6, Overflow, as every other out-of-range value does +' (CLngLng(1E+19), a Double variable holding 2^63). When the Double is a constant +' expression, BETA 1010 and BETA 1007 convert it to -9223372036854775808, the smallest +' LongLong, with no error. An unsuffixed whole-number literal beyond Long is a Double, so +' 9223372036854775807 (the largest LongLong, written without ^) is the same constant 2^63 +' and gives the same wrong result. VB6 has no LongLong; CLng(2147483648#) raises 6 there. +[TestFixture] +Module LongLongOverflowTests + + [TestCase] + Public Sub CLngLng_ConstantTwoToThe63_Raises6() + Dim q As LongLong + On Error Resume Next + q = CLngLng(2 ^ 63) + Assert.Strict.AreEqual 6, Err.Number + End Sub + + [TestCase] + Public Sub CLngLng_DoubleLiteral2To63_Raises6() + Dim q As LongLong + On Error Resume Next + q = CLngLng(9223372036854775808#) + Assert.Strict.AreEqual 6, Err.Number + End Sub + + [TestCase] + Public Sub Assign_UnsuffixedLargestLongLongLiteral_Raises6() + Dim q As LongLong + On Error Resume Next + q = 9223372036854775807 + Assert.Strict.AreEqual 6, Err.Number + End Sub + + [TestCase] + Public Sub CLngLng_ConstantSingleTwoToThe63_Raises6() + Dim q As LongLong + On Error Resume Next + q = CLngLng(CSng(2 ^ 63)) + Assert.Strict.AreEqual 6, Err.Number + End Sub + + ' Controls: the same values reach the same conversion without being constants, or + ' are not 2^63, and the conversion is right. + + [TestCase] + Public Sub CLngLng_VariableTwoToThe63_Raises6() + Dim q As LongLong + Dim x As Double = 2 ^ 63 + On Error Resume Next + q = CLngLng(x) + Assert.Strict.AreEqual 6, Err.Number + End Sub + + [TestCase] + Public Sub CLngLng_ConstantOneE19_Raises6() + Dim q As LongLong + On Error Resume Next + q = CLngLng(1E+19) + Assert.Strict.AreEqual 6, Err.Number + End Sub + + [TestCase] + Public Sub CLngLng_ConstantBelowTwoToThe63_Accepted() + Dim q As LongLong + On Error Resume Next + q = CLngLng(2 ^ 63 - 1024) + Assert.Strict.AreEqual 0, Err.Number + Assert.Strict.AreEqual 9223372036854774784^, q + End Sub + + [TestCase] + Public Sub CLngLng_ConstantMinus2To63_Accepted() + Dim q As LongLong + On Error Resume Next + q = CLngLng(-(2 ^ 63)) + Assert.Strict.AreEqual 0, Err.Number + Assert.Strict.AreEqual -9223372036854775807^ - 1, q + End Sub + +End Module diff --git a/bugs/clnglng-2-to-63-no-overflow/src/Sources/Startup.twin b/bugs/clnglng-2-to-63-no-overflow/src/Sources/Startup.twin new file mode 100644 index 000000000..20233b3a9 --- /dev/null +++ b/bugs/clnglng-2-to-63-no-overflow/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/clnglng-2-to-63-no-overflow/vb6/Module1.bas b/bugs/clnglng-2-to-63-no-overflow/vb6/Module1.bas new file mode 100644 index 000000000..3f595de6d --- /dev/null +++ b/bugs/clnglng-2-to-63-no-overflow/vb6/Module1.bas @@ -0,0 +1,40 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' VB6 has no LongLong, so the nearest case is the conversion of the first Double beyond +' the Long range, 2^31, to a Long, written as a constant expression and as a variable. Both +' raise error 6, Overflow, which is what the twinBASIC reproducer asserts for 2^63 and +' LongLong. The controls are the last Double inside the range. + +Private Sub Report(ByVal Label As String, ByVal Value As Double) + Print #9, Label & " -> error " & Err.Number & " " & Err.Description & ", n = " & Value + Err.Clear +End Sub + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Dim n As Long, x As Double + On Error Resume Next + + n = 0: n = CLng(2 ^ 31) + Report "CLng(2 ^ 31)", n + n = 0: n = CLng(2147483648#) + Report "CLng(2147483648#)", n + n = 0: n = 2147483648# + Report "n = 2147483648#", n + x = 2 ^ 31 + n = 0: n = CLng(x) + Report "CLng(x), x = 2 ^ 31", n + n = 0: n = CLng(2147483647.4) + Report "control: CLng(2147483647.4)", n + n = 0: n = CLng(-(2 ^ 31)) + Report "control: CLng(-(2 ^ 31))", n + + 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/clnglng-2-to-63-no-overflow/vb6/Probe.vbp b/bugs/clnglng-2-to-63-no-overflow/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/clnglng-2-to-63-no-overflow/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/const-double-expression-after-overflow-32bit/const-double-expression-after-overflow-32bit.twinproj b/bugs/const-double-expression-after-overflow-32bit/const-double-expression-after-overflow-32bit.twinproj new file mode 100644 index 000000000..ab06e7608 Binary files /dev/null and b/bugs/const-double-expression-after-overflow-32bit/const-double-expression-after-overflow-32bit.twinproj differ diff --git a/bugs/const-double-expression-after-overflow-32bit/repro.json b/bugs/const-double-expression-after-overflow-32bit/repro.json new file mode 100644 index 000000000..607e99175 --- /dev/null +++ b/bugs/const-double-expression-after-overflow-32bit/repro.json @@ -0,0 +1,20 @@ +{ + "mode": "test", + "arch": "win32", + "expect": { + "exit": 7, + "output": [ + "^FAIL RegressionTests.Double_ConstantProductAfterLongOverflow_Accepted: ", + "^FAIL RegressionTests.LongLong_TwoToThe63MinusKAfterLongOverflow_Accepted: ", + "^FAIL RegressionTests.LongLong_TwoToThe63MinusKAfterCLngLng1E19_Accepted: ", + "^FAIL RegressionTests.Function_ReturnsTwoToThe63MinusK_CallerHasLongOverflow_Accepted: ", + "^FAIL RegressionTests.Function_ReturnsTwoToThe63MinusK_CallerHasNoOverflow_Accepted: ", + "^PASS RegressionTests.Double_ConstantProduct_Alone_Accepted$", + "^PASS RegressionTests.LongLong_TwoToThe63MinusK_Alone_Accepted$", + "^PASS RegressionTests.Double_SinglePowerAfterLongOverflow_Accepted$", + "^PASS RegressionTests.Double_VariableProductAfterLongOverflow_Accepted$", + "^PASS RegressionTests.Double_ConstantProductAfterByteOverflow_Accepted$", + "^PASS RegressionTests.Double_ConstantProductAfterRuntimeLongOverflow_Accepted$" + ] + } +} diff --git a/bugs/const-double-expression-after-overflow-32bit/src/Settings b/bugs/const-double-expression-after-overflow-32bit/src/Settings new file mode 100644 index 000000000..be7eb5cab --- /dev/null +++ b/bugs/const-double-expression-after-overflow-32bit/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ConstDoubleExpressionAfterOverflow32bit", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: In a 32-bit build, a constant Double expression raises error 6 after a constant Long or LongLong overflow", + "project.exportPathIsV2": true, + "project.id": "{0CB5E7C0-0CFB-4E41-B65B-0F7DD40F1D87}", + "project.name": "ConstDoubleExpressionAfterOverflow32bit", + "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/const-double-expression-after-overflow-32bit/src/Sources/RegressionTests.twin b/bugs/const-double-expression-after-overflow-32bit/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..8c0960294 --- /dev/null +++ b/bugs/const-double-expression-after-overflow-32bit/src/Sources/RegressionTests.twin @@ -0,0 +1,135 @@ +' Regression tests: in a 32-bit build, a constant Double expression with an operator after the ^ (2 ^ 10 * 2, +' 2 ^ 63 - 1024) raises error 6, Overflow, when the same procedure also converts an out-of-range constant +' Double to a Long or LongLong (l = 2 ^ 31, CLngLng(1E+19)). The constant is not out of range; it is +' correct alone, in a 64-bit build, and in a procedure without the other statement. Each case below is +' correct in a 64-bit build. +[TestFixture] +Module RegressionTests + + ' Called by two cases: the first has the Long overflow, the second has none. + Private Function TwoToThe63MinusK() As LongLong + TwoToThe63MinusK = 2 ^ 63 - 1024 + End Function + + [TestCase] + Public Sub Double_ConstantProductAfterLongOverflow_Accepted() + Dim d As Double, l As Long, n As Long + On Error Resume Next + l = 2 ^ 31 + Err.Clear + d = 2 ^ 10 * 2 + n = Err.Number + Assert.Strict.AreEqual 0, n + Assert.Strict.AreEqual 2048#, d + End Sub + + [TestCase] + Public Sub LongLong_TwoToThe63MinusKAfterLongOverflow_Accepted() + Dim q As LongLong, l As Long, n As Long + On Error Resume Next + l = 2 ^ 31 + Err.Clear + q = 2 ^ 63 - 1024 + n = Err.Number + Assert.Strict.AreEqual 0, n + Assert.Strict.AreEqual 9223372036854774784^, q + End Sub + + [TestCase] + Public Sub LongLong_TwoToThe63MinusKAfterCLngLng1E19_Accepted() + Dim q As LongLong, n As Long + On Error Resume Next + q = CLngLng(1E+19) + Err.Clear + q = 2 ^ 63 - 1024 + n = Err.Number + Assert.Strict.AreEqual 0, n + Assert.Strict.AreEqual 9223372036854774784^, q + End Sub + + [TestCase] + Public Sub Function_ReturnsTwoToThe63MinusK_CallerHasLongOverflow_Accepted() + Dim q As LongLong, l As Long, n As Long + On Error Resume Next + q = TwoToThe63MinusK() + n = Err.Number + l = 2 ^ 31 + Assert.Strict.AreEqual 0, n + End Sub + + [TestCase] + Public Sub Function_ReturnsTwoToThe63MinusK_CallerHasNoOverflow_Accepted() + Dim q As LongLong, n As Long + On Error Resume Next + q = TwoToThe63MinusK() + n = Err.Number + Assert.Strict.AreEqual 0, n + End Sub + + [TestCase] + Public Sub Double_ConstantProduct_Alone_Accepted() + Dim d As Double, n As Long + On Error Resume Next + d = 2 ^ 10 * 2 + n = Err.Number + Assert.Strict.AreEqual 0, n + Assert.Strict.AreEqual 2048#, d + End Sub + + [TestCase] + Public Sub LongLong_TwoToThe63MinusK_Alone_Accepted() + Dim q As LongLong, n As Long + On Error Resume Next + q = 2 ^ 63 - 1024 + n = Err.Number + Assert.Strict.AreEqual 0, n + Assert.Strict.AreEqual 9223372036854774784^, q + End Sub + + [TestCase] + Public Sub Double_SinglePowerAfterLongOverflow_Accepted() + Dim d As Double, l As Long, n As Long + On Error Resume Next + l = 2 ^ 31 + Err.Clear + d = 2 ^ 11 + n = Err.Number + Assert.Strict.AreEqual 0, n + End Sub + + [TestCase] + Public Sub Double_VariableProductAfterLongOverflow_Accepted() + Dim d As Double, x As Double, l As Long, n As Long + x = 1024 + On Error Resume Next + l = 2 ^ 31 + Err.Clear + d = x * 2 + n = Err.Number + Assert.Strict.AreEqual 0, n + End Sub + + [TestCase] + Public Sub Double_ConstantProductAfterByteOverflow_Accepted() + Dim d As Double, b As Byte, n As Long + On Error Resume Next + b = 256 + Err.Clear + d = 2 ^ 10 * 2 + n = Err.Number + Assert.Strict.AreEqual 0, n + End Sub + + [TestCase] + Public Sub Double_ConstantProductAfterRuntimeLongOverflow_Accepted() + Dim d As Double, x As Double, l As Long, n As Long + x = 2 ^ 31 + On Error Resume Next + l = x + Err.Clear + d = 2 ^ 10 * 2 + n = Err.Number + Assert.Strict.AreEqual 0, n + End Sub + +End Module diff --git a/bugs/const-double-expression-after-overflow-32bit/src/Sources/Startup.twin b/bugs/const-double-expression-after-overflow-32bit/src/Sources/Startup.twin new file mode 100644 index 000000000..20233b3a9 --- /dev/null +++ b/bugs/const-double-expression-after-overflow-32bit/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/const-double-expression-after-overflow-32bit/vb6/Module1.bas b/bugs/const-double-expression-after-overflow-32bit/vb6/Module1.bas new file mode 100644 index 000000000..c4aa4f588 --- /dev/null +++ b/bugs/const-double-expression-after-overflow-32bit/vb6/Module1.bas @@ -0,0 +1,41 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' VB6 has no LongLong. The twinBASIC reproducer asserts that a constant Double expression with an +' operator after the ^ (2 ^ 10 * 2) is accepted after a constant Double that does not fit a Long +' has raised error 6. VB6 has the same two statements for a Double and a Long: the first raises error 6 +' and the second is accepted with its own value and no error. + +Private Sub Report(ByVal Label As String, ByVal Value As Double) + Print #9, Label & " -> error " & Err.Number & " " & Err.Description & ", value = " & Value + Err.Clear +End Sub + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Dim d As Double, l As Long, n As Long + On Error Resume Next + + l = 0: l = 2 ^ 31 + Report "l = 2 ^ 31 (the overflow)", l + d = 0: d = 2 ^ 10 * 2 + Report "d = 2 ^ 10 * 2, directly after", d + d = 0: d = 2 ^ 10 * 2 + Report "d = 2 ^ 10 * 2, again", d + + l = 0: l = 2147483648# + Report "l = 2147483648# (the overflow)", l + n = 0: n = 2 ^ 30 + 2 ^ 29 + Report "n = 2 ^ 30 + 2 ^ 29, directly after", n + + d = 0: d = 2 ^ 10 * 2 + Report "control: d = 2 ^ 10 * 2 with no overflow before", d + + 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/const-double-expression-after-overflow-32bit/vb6/Probe.vbp b/bugs/const-double-expression-after-overflow-32bit/vb6/Probe.vbp new file mode 100644 index 000000000..f17bd0bfb --- /dev/null +++ b/bugs/const-double-expression-after-overflow-32bit/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/date-out-of-range-no-error/date-out-of-range-no-error.twinproj b/bugs/date-out-of-range-no-error/date-out-of-range-no-error.twinproj new file mode 100644 index 000000000..71307de3e Binary files /dev/null and b/bugs/date-out-of-range-no-error/date-out-of-range-no-error.twinproj differ diff --git a/bugs/date-out-of-range-no-error/repro.json b/bugs/date-out-of-range-no-error/repro.json new file mode 100644 index 000000000..a345156a7 --- /dev/null +++ b/bugs/date-out-of-range-no-error/repro.json @@ -0,0 +1,17 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^FAIL DateRangeTests.AssignLongPastLargest_Raises6: ", + "^FAIL DateRangeTests.CDatePastLargest_Raises6: ", + "^FAIL DateRangeTests.CDateBeforeSmallest_Raises6: ", + "^FAIL DateRangeTests.AddOneDayToLargest_Raises6: ", + "^FAIL DateRangeTests.SubtractOneDayFromSmallest_Raises6: ", + "^FAIL DateRangeTests.AssignHugeDouble_Raises6: ", + "^PASS DateRangeTests.AssignLargest_Accepted$", + "^PASS DateRangeTests.AssignSmallest_Accepted$", + "^PASS DateRangeTests.OutOfRangeInteger_Raises6$" + ] + } +} diff --git a/bugs/date-out-of-range-no-error/src/Settings b/bugs/date-out-of-range-no-error/src/Settings new file mode 100644 index 000000000..c20812468 --- /dev/null +++ b/bugs/date-out-of-range-no-error/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "DateOutOfRangeNoError", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: x", + "project.exportPathIsV2": true, + "project.id": "{BE360AD7-A162-49DA-8420-28CEBF403730}", + "project.name": "DateOutOfRangeNoError", + "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/date-out-of-range-no-error/src/Sources/RegressionTests.twin b/bugs/date-out-of-range-no-error/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..feed0ef12 --- /dev/null +++ b/bugs/date-out-of-range-no-error/src/Sources/RegressionTests.twin @@ -0,0 +1,96 @@ +' Regression tests: a Date holds days from -657434 (January 1, 100) to 2958465 (December 31, +' 9999), plus the time of day. A value outside that range cannot be a Date, and VB6 raises +' error 6, Overflow, for every way of making one (the VB6 project beside this one prints +' them). In BETA 1010 and BETA 1007 none of them raises an error: the Date holds the +' out-of-range number, and what it then prints or formats is meaningless. Each [TestCase] +' asserts the error; the two controls, the largest and the smallest Date, must stay accepted. +[TestFixture] +Module DateRangeTests + + [TestCase] + Public Sub AssignLongPastLargest_Raises6() + Dim d As Date + Dim n As Long = 2958466 + On Error Resume Next + d = n + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + [TestCase] + Public Sub CDatePastLargest_Raises6() + Dim d As Date + Dim n As Long = 2958466 + On Error Resume Next + d = CDate(n) + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + [TestCase] + Public Sub CDateBeforeSmallest_Raises6() + Dim d As Date + Dim n As Long = -657435 + On Error Resume Next + d = CDate(n) + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + [TestCase] + Public Sub AddOneDayToLargest_Raises6() + Dim d As Date + Dim last As Date = #12/31/9999# + On Error Resume Next + d = last + 1 + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + [TestCase] + Public Sub SubtractOneDayFromSmallest_Raises6() + Dim d As Date + Dim first As Date = #1/1/100# + On Error Resume Next + d = first - 1 + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + [TestCase] + Public Sub AssignHugeDouble_Raises6() + Dim d As Date + Dim x As Double = 1E+300 + On Error Resume Next + d = x + Assert.Strict.AreEqual 6, Err.Number ' VB6: 6 + End Sub + + ' Controls: the ends of the range are accepted, and error 6 is what an out-of-range + ' value raises in an Integer, so the cases above are not testing the harness. + + [TestCase] + Public Sub AssignLargest_Accepted() + Dim d As Date + Dim n As Long = 2958465 + On Error Resume Next + d = n + Assert.Strict.AreEqual 0, Err.Number + Assert.Strict.AreEqual 2958465#, CDbl(d) + End Sub + + [TestCase] + Public Sub AssignSmallest_Accepted() + Dim d As Date + Dim n As Long = -657434 + On Error Resume Next + d = n + Assert.Strict.AreEqual 0, Err.Number + Assert.Strict.AreEqual -657434#, CDbl(d) + End Sub + + [TestCase] + Public Sub OutOfRangeInteger_Raises6() + Dim i As Integer + Dim n As Long = 40000 + On Error Resume Next + i = n + Assert.Strict.AreEqual 6, Err.Number + End Sub + +End Module diff --git a/bugs/date-out-of-range-no-error/src/Sources/Startup.twin b/bugs/date-out-of-range-no-error/src/Sources/Startup.twin new file mode 100644 index 000000000..20233b3a9 --- /dev/null +++ b/bugs/date-out-of-range-no-error/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/date-out-of-range-no-error/vb6/Module1.bas b/bugs/date-out-of-range-no-error/vb6/Module1.bas new file mode 100644 index 000000000..f7df4ba8f --- /dev/null +++ b/bugs/date-out-of-range-no-error/vb6/Module1.bas @@ -0,0 +1,50 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' What VB6 does where the twinBASIC reproducer's cases assert error 6: every way of +' making a Date outside January 1, 100 to December 31, 9999 raises error 6, Overflow, and +' the Date keeps its old value (0 here). The two controls are the ends of the range. + +Private Sub Report(ByVal Label As String, ByVal Value As Double) + Print #9, Label & " -> error " & Err.Number & " " & Err.Description & ", d = " & Value + Err.Clear +End Sub + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Dim d As Date, n As Long, x As Double, last As Date, first As Date + On Error Resume Next + + n = 2958466 + d = 0: d = n + Report "d = 2958466 (Long)", CDbl(d) + d = 0: d = CDate(n) + Report "CDate(2958466)", CDbl(d) + n = -657435 + d = 0: d = CDate(n) + Report "CDate(-657435)", CDbl(d) + last = #12/31/9999# + d = 0: d = last + 1 + Report "#12/31/9999# + 1", CDbl(d) + first = #1/1/100# + d = 0: d = first - 1 + Report "#1/1/100# - 1", CDbl(d) + x = 1E+300 + d = 0: d = x + Report "d = 1E+300 (Double)", CDbl(d) + + n = 2958465 + d = 0: d = n + Report "control: d = 2958465", CDbl(d) + n = -657434 + d = 0: d = n + Report "control: d = -657434", CDbl(d) + + 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/date-out-of-range-no-error/vb6/Probe.vbp b/bugs/date-out-of-range-no-error/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/date-out-of-range-no-error/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/dim-as-any/dim-as-any.twinproj b/bugs/dim-as-any/dim-as-any.twinproj new file mode 100644 index 000000000..195993b17 Binary files /dev/null and b/bugs/dim-as-any/dim-as-any.twinproj differ diff --git a/bugs/dim-as-any/repro.json b/bugs/dim-as-any/repro.json new file mode 100644 index 000000000..d2242b6a1 --- /dev/null +++ b/bugs/dim-as-any/repro.json @@ -0,0 +1,20 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS Declare_ParameterAsAny$", + "^PASS UdtField_AsAny$", + "^PASS FunctionReturn_AsAny$", + "^PASS SubParameter_AsAny$", + "^PASS SubParameterByVal_AsAny$", + "^PASS LocalArrayDim_AsAny$", + "^PASS ReDim_AsAny$", + "^PASS Control_DimAsLong$", + "^FAIL ClassField_AsAny: ", + "^FAIL ModuleVariable_AsAny: ", + "^FAIL LocalDim_AsAny: ", + "^FAIL LocalStatic_AsAny: " + ] + } +} diff --git a/bugs/dim-as-any/src/Settings b/bugs/dim-as-any/src/Settings new file mode 100644 index 000000000..d96f78709 --- /dev/null +++ b/bugs/dim-as-any/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "DimAsAny", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Dim ... As Any compiles, though As Any is only for a Declare parameter", + "project.exportPathIsV2": true, + "project.id": "{09EA3335-7C7E-4F86-855D-60D3E4F8A87E}", + "project.name": "DimAsAny", + "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-any/src/Sources/RegressionTests.twin b/bugs/dim-as-any/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..d7c512d08 --- /dev/null +++ b/bugs/dim-as-any/src/Sources/RegressionTests.twin @@ -0,0 +1,40 @@ +' Regression tests for the report, as compile cases: each line that ends in a +' comment ' CASE : must give that diagnostic on that line (a +' code, "error" for any error, or "none"). tbrun --tests checks them. +' As Any is meant only for the parameter of a Declare statement, where it lets +' the caller pass an argument of any type. Anywhere else it names no type, so +' every line below that is not a Declare parameter or the Long control is a +' compile error. + +Class HoldsAny + Public Item As Any ' CASE ClassField_AsAny: error +End Class + +Module AsAnyTests + + Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As LongPtr) ' CASE Declare_ParameterAsAny: none + + Private Type HasAny + Value As Any ' CASE UdtField_AsAny: error + End Type + + Private ModuleLevel As Any ' CASE ModuleVariable_AsAny: error + + Private Function ReturnsAny() As Any ' CASE FunctionReturn_AsAny: error + End Function + + Private Sub TakesAny(ByRef p As Any) ' CASE SubParameter_AsAny: error + End Sub + + Private Sub TakesAnyByVal(ByVal p As Any) ' CASE SubParameterByVal_AsAny: error + End Sub + + Private Sub Declarations() + Dim x As Any ' CASE LocalDim_AsAny: error + Static s As Any ' CASE LocalStatic_AsAny: error + Dim a(1) As Any ' CASE LocalArrayDim_AsAny: error + ReDim b(1) As Any ' CASE ReDim_AsAny: error + Dim c As Long ' CASE Control_DimAsLong: none + End Sub + +End Module diff --git a/bugs/dim-as-any/src/Sources/Startup.twin b/bugs/dim-as-any/src/Sources/Startup.twin new file mode 100644 index 000000000..20233b3a9 --- /dev/null +++ b/bugs/dim-as-any/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/dim-as-any/vb6/Module1.bas b/bugs/dim-as-any/vb6/Module1.bas new file mode 100644 index 000000000..82c17e21a --- /dev/null +++ b/bugs/dim-as-any/vb6/Module1.bas @@ -0,0 +1,29 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' VB6 refuses As Any anywhere but the parameter of a Declare statement. The Declare +' below is the control, and the project builds. The other declarations of the +' twinBASIC reproducer were each tried in this module alone (the build stops at the +' first it refuses); the command-line build's compile error for each was: +' +' Private ModuleLevel As Any Expected: New or type name +' Private Type HasAny: Value As Any Expected: New or type name +' Private Function ReturnsAny() As Any Expected: type name +' Private Sub TakesAny(ByRef p As Any) Expected: type name +' Dim x As Any (in a procedure) Syntax error +' Static s As Any (in a procedure) Syntax error +' Dim a(1) As Any (in a procedure) Syntax error + +Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long) + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Print #9, "probe 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/dim-as-any/vb6/Probe.vbp b/bugs/dim-as-any/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/dim-as-any/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/export-verbose-done-escaped/fix.js b/bugs/export-verbose-done-escaped/fix.js new file mode 100644 index 000000000..9e210d5c3 --- /dev/null +++ b/bugs/export-verbose-done-escaped/fix.js @@ -0,0 +1,43 @@ +// Fix for export-verbose-done-escaped and twinbasic#2444: text that continues an +// open DEBUG CONSOLE line is escaped once, not twice. Written on BETA 1010; the local +// names are ours (scripts/ide-js/names.json), not twinBASIC's. Applied with +// `node scripts/ide_js.mjs patch bugs/export-verbose-done-escaped/fix.js --copy-to `. +function debugOutputPartial(text, colorClass, isHTML, timeStamp) { + var endsLine = text.endsWith("\r\n"); + if (endsLine) { + text = text.substr(0, text.length - 2); + } + if (debugConsolePartialLineItem) { + let lineHTML = debugConsoleContent.onAsyncGetItemHTML(debugConsolePartialLineItem); + if (debugConsolePartialLineItem_LastColorClass === colorClass) { + text = isHTML ? text : TEXTtoHTML(text); + } else { + text = "" + (isHTML ? text : TEXTtoHTML(text)); + } + var addedHTML = text; + debugConsoleContent.updateItem( + debugConsolePartialLineItem, + lineHTML + addedHTML + (endsLine ? "" : ""), + ); + } else { + let stampHTML = ""; + if (timeStamp) { + stampHTML += timeStamp; + } else { + stampHTML += GetJsTimeStamp(); + } + debugConsolePartialLineItem = debugConsoleContent.addItem( + "" + + stampHTML + + "" + + (isHTML ? text : TEXTtoHTML(text)) + + (endsLine ? "" : ""), + ); + } + if (endsLine) { + debugConsolePartialLineItem = undefined; + } + debugConsolePartialLineItem_LastColorClass = colorClass; +} diff --git a/bugs/export-verbose-done-escaped/repro.json b/bugs/export-verbose-done-escaped/repro.json new file mode 100644 index 000000000..4653fd961 --- /dev/null +++ b/bugs/export-verbose-done-escaped/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:export", + "tests": [ + "with Export Verbose, every DONE line shows \ for each backslash of the path, and the files are right" + ], + "steps": "1. A project with Export Verbose on (Project Settings, Export).\n2. Export Project into an empty folder: the DEBUG CONSOLE shows each DONE line as [EXPORT] DONE: \Sources\Startup.twin, \ for each backslash; the files are written right.\nControl: the DELETED lines of a second verbose export, and the first and COMPLETED lines of an export without Export Verbose, show the backslashes." +} diff --git a/bugs/filed/llvm-double-overflow-no-error/REPORT.md b/bugs/filed/llvm-double-overflow-no-error/REPORT.md index 71edf8d3a..b5aa9b4f6 100644 --- a/bugs/filed/llvm-double-overflow-no-error/REPORT.md +++ b/bugs/filed/llvm-double-overflow-no-error/REPORT.md @@ -52,4 +52,4 @@ A `Single` addition, subtraction, multiplication or division that overflows rais Severity: a silent wrong result. An overflow that raises 6 in a debug run and no error in the LLVM-compiled build changes what a program computes, and nothing marks the difference. - + diff --git a/bugs/filed/single-overflow-no-error/REPORT.md b/bugs/filed/single-overflow-no-error/REPORT.md index 2c3b2fbbb..1f1def5de 100644 --- a/bugs/filed/single-overflow-no-error/REPORT.md +++ b/bugs/filed/single-overflow-no-error/REPORT.md @@ -50,4 +50,4 @@ A `Single` constant expression that overflows is refused by VB6 at compile time Severity: a silent wrong result. A `Single` overflow that ends the program or reaches a handler in VB6 gives `Inf` and carries on in twinBASIC. - + diff --git a/bugs/hklm-warning-twice/repro.json b/bugs/hklm-warning-twice/repro.json new file mode 100644 index 000000000..50fedd1a8 --- /dev/null +++ b/bugs/hklm-warning-twice/repro.json @@ -0,0 +1,9 @@ +{ + "mode": "lane", + "lane": "ide:building-hklm", + "tests": [ + "an ActiveX DLL with Register DLLs to HKLM shows the HKLM warning twice as it opens", + "a Standard EXE with Register DLLs to HKLM shows the HKLM warning too, twice as it opens" + ], + "steps": "1. In an IDE that is not elevated, open an ActiveX DLL project with Register DLLs to HKLM set to YES.\n2. The box WARNING: This project registers itself to the HKEY_LOCAL_MACHINE registry hive ... shows; choose Continue Anyway. The same box shows again; choose Continue Anyway again.\n3. Open a Standard EXE project with the same setting set to YES: the same box shows, twice, though a Standard EXE registers nothing.\nControl: an ActiveX DLL registered in HKCU opens with no box." +} diff --git a/bugs/infinity-text-64-bit/infinity-text-64-bit.twinproj b/bugs/infinity-text-64-bit/infinity-text-64-bit.twinproj new file mode 100644 index 000000000..be7ea6451 Binary files /dev/null and b/bugs/infinity-text-64-bit/infinity-text-64-bit.twinproj differ diff --git a/bugs/infinity-text-64-bit/repro.json b/bugs/infinity-text-64-bit/repro.json new file mode 100644 index 000000000..f4e23dd97 --- /dev/null +++ b/bugs/infinity-text-64-bit/repro.json @@ -0,0 +1,18 @@ +{ + "mode": "test", + "arch": "win64", + "expect": { + "exit": 7, + "output": [ + "^FAIL InfinityTextTests.CStrOfInfinity_Is1HashINF: ", + "^FAIL InfinityTextTests.CStrOfNegativeInfinity_IsMinus1HashINF: ", + "^FAIL InfinityTextTests.CStrOfNaN_Is1HashQNAN: ", + "^FAIL InfinityTextTests.CStrOfIndeterminate_IsMinus1HashIND: ", + "^FAIL InfinityTextTests.ConcatenatedInfinity_Is1HashINF: ", + "^FAIL InfinityTextTests.FormatOfInfinity_Is1HashINF: ", + "^FAIL InfinityTextTests.FormatOfNaNWithPicture_Is1HashR: ", + "^FAIL InfinityTextTests.CStrOfSingleInfinity_Is1HashINF: ", + "^PASS InfinityTextTests.CStrOfOrdinaryDouble_IsUnchanged$" + ] + } +} diff --git a/bugs/infinity-text-64-bit/src/Settings b/bugs/infinity-text-64-bit/src/Settings new file mode 100644 index 000000000..b5177fedc --- /dev/null +++ b/bugs/infinity-text-64-bit/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "InfinityText64Bit", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: x", + "project.exportPathIsV2": true, + "project.id": "{B377680C-A8FD-4693-8B36-B58FEAF1092C}", + "project.name": "InfinityText64Bit", + "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/infinity-text-64-bit/src/Sources/RegressionTests.twin b/bugs/infinity-text-64-bit/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..224c45b1d --- /dev/null +++ b/bugs/infinity-text-64-bit/src/Sources/RegressionTests.twin @@ -0,0 +1,73 @@ +' Regression tests: the text of a Double or Single that holds infinity or NaN. VB6, and a +' 32-bit twinBASIC build, give 1.#INF, -1.#INF, 1.#QNAN and -1.#IND (Format with a number +' picture gives 1.#R for NaN). A 64-bit build gives inf, -inf, nan and -nan(ind), and +' Format(x, "0.00") joins the picture's decimals on: inf.00. The values are made by +' copying their bytes into a variable, since arithmetic raises an error instead of +' producing one. The cases assert VB6's text, so they fail in a 64-bit build (repro.json +' names it) and pass in a 32-bit one (the VB6 project beside this one prints it). + +[TestFixture] +Module InfinityTextTests + + Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As LongPtr) + + Private Function DoubleFromBits(ByVal Bits As LongLong) As Double + Dim result As Double + CopyMemory result, Bits, 8 + Return result + End Function + + Private Function SingleFromBits(ByVal Bits As Long) As Single + Dim result As Single + CopyMemory result, Bits, 4 + Return result + End Function + + [TestCase] + Public Sub CStrOfInfinity_Is1HashINF() + Assert.Strict.AreEqual "1.#INF", CStr(DoubleFromBits(&H7FF0000000000000^)) + End Sub + + [TestCase] + Public Sub CStrOfNegativeInfinity_IsMinus1HashINF() + Assert.Strict.AreEqual "-1.#INF", CStr(DoubleFromBits(&HFFF0000000000000^)) + End Sub + + [TestCase] + Public Sub CStrOfNaN_Is1HashQNAN() + Assert.Strict.AreEqual "1.#QNAN", CStr(DoubleFromBits(&H7FF8000000000000^)) + End Sub + + [TestCase] + Public Sub CStrOfIndeterminate_IsMinus1HashIND() + Assert.Strict.AreEqual "-1.#IND", CStr(DoubleFromBits(&HFFF8000000000000^)) + End Sub + + [TestCase] + Public Sub ConcatenatedInfinity_Is1HashINF() + Assert.Strict.AreEqual "x=1.#INF", "x=" & DoubleFromBits(&H7FF0000000000000^) + End Sub + + [TestCase] + Public Sub FormatOfInfinity_Is1HashINF() + Assert.Strict.AreEqual "1.#INF", Format(DoubleFromBits(&H7FF0000000000000^), "0.00") + End Sub + + [TestCase] + Public Sub FormatOfNaNWithPicture_Is1HashR() + Assert.Strict.AreEqual "1.#R", Format(DoubleFromBits(&H7FF8000000000000^), "0.00") + End Sub + + [TestCase] + Public Sub CStrOfSingleInfinity_Is1HashINF() + Assert.Strict.AreEqual "1.#INF", CStr(SingleFromBits(&H7F800000)) + End Sub + + ' Control: an ordinary Double prints the same in both builds. + + [TestCase] + Public Sub CStrOfOrdinaryDouble_IsUnchanged() + Assert.Strict.AreEqual "1.5", CStr(1.5) + End Sub + +End Module diff --git a/bugs/infinity-text-64-bit/src/Sources/Startup.twin b/bugs/infinity-text-64-bit/src/Sources/Startup.twin new file mode 100644 index 000000000..20233b3a9 --- /dev/null +++ b/bugs/infinity-text-64-bit/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/infinity-text-64-bit/vb6/Module1.bas b/bugs/infinity-text-64-bit/vb6/Module1.bas new file mode 100644 index 000000000..48640b0d1 --- /dev/null +++ b/bugs/infinity-text-64-bit/vb6/Module1.bas @@ -0,0 +1,48 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The text VB6 gives for infinity and NaN, which the twinBASIC reproducer's cases assert +' (a 32-bit twinBASIC build gives the same; a 64-bit one gives inf, -inf, nan, -nan(ind), +' inf.00 and nan.00). The values are made by copying their bytes into the variable. + +Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long) + +Private Sub P(ByVal s As String) + Print #9, s +End Sub + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Dim x As Double, w(1) As Long, s As Single, n As Long + On Error Resume Next + + w(0) = 0 + w(1) = &H7FF00000 + CopyMemory x, w(0), 8 + P "CStr(+Infinity) = " & CStr(x) + P "Concatenated = x=" & x + P "Format(x, 0.00) = " & Format(x, "0.00") + w(1) = &HFFF00000 + CopyMemory x, w(0), 8 + P "CStr(-Infinity) = " & CStr(x) + w(1) = &H7FF80000 + CopyMemory x, w(0), 8 + P "CStr(NaN) = " & CStr(x) + P "Format(NaN, 0.00) = " & Format(x, "0.00") + w(1) = &HFFF80000 + CopyMemory x, w(0), 8 + P "CStr(indeterminate) = " & CStr(x) + n = &H7F800000 + CopyMemory s, n, 4 + P "CStr(Single +Infinity) = " & CStr(s) + P "CStr(1.5) = " & CStr(1.5) + P "error " & Err.Number & " " & Err.Description + + 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/infinity-text-64-bit/vb6/Probe.vbp b/bugs/infinity-text-64-bit/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/infinity-text-64-bit/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/lenb-of-array-accepted/lenb-of-array-accepted.twinproj b/bugs/lenb-of-array-accepted/lenb-of-array-accepted.twinproj new file mode 100644 index 000000000..6f987456c Binary files /dev/null and b/bugs/lenb-of-array-accepted/lenb-of-array-accepted.twinproj differ diff --git a/bugs/lenb-of-array-accepted/repro.json b/bugs/lenb-of-array-accepted/repro.json new file mode 100644 index 000000000..d220b0eaf --- /dev/null +++ b/bugs/lenb-of-array-accepted/repro.json @@ -0,0 +1,15 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS LenB_ByteElement$", + "^PASS LenB_LongVariable$", + "^FAIL LenB_FixedByteArray: ", + "^FAIL LenB_FixedLongArray: ", + "^FAIL LenB_DynamicByteArray: ", + "^FAIL Len_FixedByteArray: ", + "^FAIL Len_DynamicByteArray: " + ] + } +} diff --git a/bugs/lenb-of-array-accepted/src/Settings b/bugs/lenb-of-array-accepted/src/Settings new file mode 100644 index 000000000..7b45b33ad --- /dev/null +++ b/bugs/lenb-of-array-accepted/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "LenbOfArrayAccepted", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: x", + "project.exportPathIsV2": true, + "project.id": "{C80953D6-F5A8-4AF5-8F31-F80AE009D91E}", + "project.name": "LenbOfArrayAccepted", + "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/lenb-of-array-accepted/src/Sources/RegressionTests.twin b/bugs/lenb-of-array-accepted/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..a4f59ad60 --- /dev/null +++ b/bugs/lenb-of-array-accepted/src/Sources/RegressionTests.twin @@ -0,0 +1,33 @@ +' Regression tests as compile cases: each line that ends in a comment +' ' CASE : +' must give that diagnostic on that line (a code, "error" for any error, or "none"). +' tbrun --tests checks them. +' +' Len and LenB take a string or a variable of a simple or user-defined type. VB6 refuses an +' array as the argument of either, when the program is compiled ("Variable required - can't +' assign to this expression"; the VB6 project beside this one lists what each line gave). +' BETA 1010 and BETA 1007 accept it and return a number that is not the array's size in +' bytes: for a fixed-size array of any element type and length 40 in a 32-bit build (48 +' in a 64-bit one) with one dimension and 48 (56) with two, for a dynamic array 4 (8). +' The lines whose argument is an element or a simple variable are the controls. + +Module LenArrayTests + + Private Sub Cases() + Dim fixedBytes(9) As Byte + Dim fixedLongs(3) As Long + Dim dynamicBytes() As Byte + Dim n As Long + ReDim dynamicBytes(4) + + n = LenB(fixedBytes) ' CASE LenB_FixedByteArray: error + n = LenB(fixedLongs) ' CASE LenB_FixedLongArray: error + n = LenB(dynamicBytes) ' CASE LenB_DynamicByteArray: error + n = Len(fixedBytes) ' CASE Len_FixedByteArray: error + n = Len(dynamicBytes) ' CASE Len_DynamicByteArray: error + + n = LenB(fixedBytes(0)) ' CASE LenB_ByteElement: none + n = LenB(n) ' CASE LenB_LongVariable: none + End Sub + +End Module diff --git a/bugs/lenb-of-array-accepted/src/Sources/Startup.twin b/bugs/lenb-of-array-accepted/src/Sources/Startup.twin new file mode 100644 index 000000000..20233b3a9 --- /dev/null +++ b/bugs/lenb-of-array-accepted/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/lenb-of-array-accepted/vb6/Module1.bas b/bugs/lenb-of-array-accepted/vb6/Module1.bas new file mode 100644 index 000000000..d0b9bdd08 --- /dev/null +++ b/bugs/lenb-of-array-accepted/vb6/Module1.bas @@ -0,0 +1,28 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' VB6 refuses an array as the argument of Len or LenB. The line below, with an element as +' the argument, is the control, and the project builds. Each of these was tried in this +' module alone (the build stops at the first line it refuses), and each gave the compile +' error Variable required - can't assign to this expression: +' +' Dim a(9) As Byte: n = LenB(a) +' Dim a(9) As Long: n = LenB(a) +' Dim a() As Byte: n = LenB(a) +' Dim a(9) As Byte: n = Len(a) +' Dim a() As Byte: n = Len(a) + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Dim a(9) As Byte + Dim n As Long + n = LenB(a(0)) + Print #9, "LenB(a(0)) = " & n + 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/lenb-of-array-accepted/vb6/Probe.vbp b/bugs/lenb-of-array-accepted/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/lenb-of-array-accepted/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/llvm-build-box-closes-other/repro.json b/bugs/llvm-build-box-closes-other/repro.json new file mode 100644 index 000000000..d17f43415 --- /dev/null +++ b/bugs/llvm-build-box-closes-other/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:building-llvm-box", + "tests": [ + "a box opened during an LLVM build is closed at its end in place of the Compiling... box, which stays with no button and blocks the IDE" + ], + "steps": "1. Open a Standard EXE project with +llvm in Compiler Options (BUILD), with a licence that allows LLVM.\n2. Press Build. The Compiling... box shows.\n3. While it shows, another box opens (the test opens one with the IDE's own showModalNotification, at the build's first progress event).\n4. At the build's end the other box is closed, and a Compiling... box stays at 0.0% with no button: the IDE takes no input until the page is reloaded.\nControl: an LLVM build with no other box closes its Compiling... box at its end." +} diff --git a/bugs/publish-failure-ok-typeerror/repro.json b/bugs/publish-failure-ok-typeerror/repro.json new file mode 100644 index 000000000..db97af1de --- /dev/null +++ b/bugs/publish-failure-ok-typeerror/repro.json @@ -0,0 +1,6 @@ +{ + "mode": "lane", + "lane": "ide:building", + "tests": ["OK in the box of a publish that fails with an HTTPS code raises a page TypeError"], + "steps": "1. Open a package project.\n2. Publish it, with the server answering an HTTPS status other than 200, 551, 552 and 553 (the test calls the IDE's own packagePublisherPublishNow with its server call replaced in the page by one that fails with 500; nothing reaches the server).\n3. The box reads [PUBLISH] FAILED. Server returned HTTPS code 500. Choose OK.\n4. The box closes, and the page raises Uncaught TypeError: t is not a function.\nControl: OK in the box of a publish refused with 551 (version already uploaded) raises no error." +} diff --git a/bugs/publisher-name-apostrophe/repro.json b/bugs/publisher-name-apostrophe/repro.json new file mode 100644 index 000000000..6387a08c1 --- /dev/null +++ b/bugs/publisher-name-apostrophe/repro.json @@ -0,0 +1,6 @@ +{ + "mode": "lane", + "lane": "ide:building", + "tests": ["a saved publisher name containing ' is cut at the apostrophe in the sign-in box"], + "steps": "1. Open a package project, and View > PACKAGE PUBLISHING.\n2. Have the IDE hold the publisher name O'Brien (the test sets it in the page, as a saved name is loaded).\n3. SIGN IN: the LOGIN user name box holds O, and the box's input element has a stray attribute brien'.\nControl: a name with no apostrophe is shown whole." +} diff --git a/bugs/publisher-password-tilde/repro.json b/bugs/publisher-password-tilde/repro.json new file mode 100644 index 000000000..abe5f9ac0 --- /dev/null +++ b/bugs/publisher-password-tilde/repro.json @@ -0,0 +1,6 @@ +{ + "mode": "lane", + "lane": "ide:building", + "tests": ["a remembered publisher password containing ~ is read back with NUL in place of each ~"], + "steps": "1. Open a package project, and View > PACKAGE PUBLISHING.\n2. Have the IDE remember the publisher password pa~ss~word (the test sets it in the page and calls the IDE's own savePublisherInfo, and its record stands in for the registry).\n3. The IDE reads it back as at its next start (its own loadPublisherInfo): each ~ is now a NUL character.\n4. SIGN IN: the LOGIN password box holds pa?ss?word, each ? a replacement character (U+FFFD).\nControl: a remembered password with no ~ is read back as it was." +} diff --git a/bugs/set-unsupported-class-error-number/repro.json b/bugs/set-unsupported-class-error-number/repro.json new file mode 100644 index 000000000..f9ee6c3b8 --- /dev/null +++ b/bugs/set-unsupported-class-error-number/repro.json @@ -0,0 +1,13 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^FAIL SetToUnsupportedTypeTests.SetToOtherClass_Raises13: ", + "^FAIL SetToUnsupportedTypeTests.SetToInterfaceTheClassLacks_Raises13: ", + "^FAIL SetToUnsupportedTypeTests.SetDictionaryToCollection_Raises13: ", + "^FAIL SetToUnsupportedTypeTests.SetFromVariantHoldingOtherClass_Raises13: ", + "^PASS SetToUnsupportedTypeTests.SetToOwnClass_RaisesNothing$" + ] + } +} diff --git a/bugs/set-unsupported-class-error-number/set-unsupported-class-error-number.twinproj b/bugs/set-unsupported-class-error-number/set-unsupported-class-error-number.twinproj new file mode 100644 index 000000000..2a37eaaea Binary files /dev/null and b/bugs/set-unsupported-class-error-number/set-unsupported-class-error-number.twinproj differ diff --git a/bugs/set-unsupported-class-error-number/src/Settings b/bugs/set-unsupported-class-error-number/src/Settings new file mode 100644 index 000000000..9dd095587 --- /dev/null +++ b/bugs/set-unsupported-class-error-number/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "SetUnsupportedClassErrorNumber", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: x", + "project.exportPathIsV2": true, + "project.id": "{349F9358-4263-401A-9727-E10BFA24965E}", + "project.name": "SetUnsupportedClassErrorNumber", + "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/set-unsupported-class-error-number/src/Sources/RegressionTests.twin b/bugs/set-unsupported-class-error-number/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..405f07c35 --- /dev/null +++ b/bugs/set-unsupported-class-error-number/src/Sources/RegressionTests.twin @@ -0,0 +1,68 @@ +' Regression tests: Set to a variable of a class or interface that the object on the right +' does not support. VB6 raises error 13, Type mismatch (the VB6 project beside this one +' prints it). BETA 1010 and BETA 1007 raise -2147467262, the HRESULT E_NOINTERFACE ("No such +' interface supported"), so a handler that tests for 13 does not recognise it. The control +' is a Set to the object's own class, which raises nothing. + +Class ClsA + Public X As Long +End Class + +Class ClsB + Public Y As Long +End Class + +Interface IFoo + Sub Hello() +End Interface + +[TestFixture] +Module SetToUnsupportedTypeTests + + [TestCase] + Public Sub SetToOtherClass_Raises13() + Dim o As Object = New ClsA + Dim b As ClsB + On Error Resume Next + Set b = o + Assert.Strict.AreEqual 13, Err.Number ' VB6: 13 + End Sub + + [TestCase] + Public Sub SetToInterfaceTheClassLacks_Raises13() + Dim o As Object = New ClsA + Dim f As IFoo + On Error Resume Next + Set f = o + Assert.Strict.AreEqual 13, Err.Number ' VB6: 13 + End Sub + + [TestCase] + Public Sub SetDictionaryToCollection_Raises13() + Dim o As Object = CreateObject("Scripting.Dictionary") + Dim c As Collection + On Error Resume Next + Set c = o + Assert.Strict.AreEqual 13, Err.Number ' VB6: 13 + End Sub + + [TestCase] + Public Sub SetFromVariantHoldingOtherClass_Raises13() + Dim v As Variant + Dim b As ClsB + Set v = New ClsA + On Error Resume Next + Set b = v + Assert.Strict.AreEqual 13, Err.Number ' VB6: 13 + End Sub + + [TestCase] + Public Sub SetToOwnClass_RaisesNothing() + Dim o As Object = New ClsB + Dim b As ClsB + On Error Resume Next + Set b = o + Assert.Strict.AreEqual 0, Err.Number + End Sub + +End Module diff --git a/bugs/set-unsupported-class-error-number/src/Sources/Startup.twin b/bugs/set-unsupported-class-error-number/src/Sources/Startup.twin new file mode 100644 index 000000000..20233b3a9 --- /dev/null +++ b/bugs/set-unsupported-class-error-number/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/set-unsupported-class-error-number/vb6/ClsA.cls b/bugs/set-unsupported-class-error-number/vb6/ClsA.cls new file mode 100644 index 000000000..1f4667a3e --- /dev/null +++ b/bugs/set-unsupported-class-error-number/vb6/ClsA.cls @@ -0,0 +1,16 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "ClsA" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = True +Option Explicit + +Public X As Long diff --git a/bugs/set-unsupported-class-error-number/vb6/ClsB.cls b/bugs/set-unsupported-class-error-number/vb6/ClsB.cls new file mode 100644 index 000000000..f7ecafac4 --- /dev/null +++ b/bugs/set-unsupported-class-error-number/vb6/ClsB.cls @@ -0,0 +1,16 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "ClsB" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = True +Option Explicit + +Public Y As Long diff --git a/bugs/set-unsupported-class-error-number/vb6/IFoo.cls b/bugs/set-unsupported-class-error-number/vb6/IFoo.cls new file mode 100644 index 000000000..6e39eb094 --- /dev/null +++ b/bugs/set-unsupported-class-error-number/vb6/IFoo.cls @@ -0,0 +1,17 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "IFoo" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = False +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = True +Option Explicit + +Public Sub Hello() +End Sub diff --git a/bugs/set-unsupported-class-error-number/vb6/Module1.bas b/bugs/set-unsupported-class-error-number/vb6/Module1.bas new file mode 100644 index 000000000..c63b513ec --- /dev/null +++ b/bugs/set-unsupported-class-error-number/vb6/Module1.bas @@ -0,0 +1,40 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' What VB6 does where the twinBASIC reproducer's cases assert error 13: a Set to a variable +' of a class that the object does not support raises error 13, Type mismatch. The control +' is a Set to the object's own class. + +Private Sub Report(ByVal Label As String) + Print #9, Label & " -> error " & Err.Number & " " & Err.Description + Err.Clear +End Sub + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Dim o As Object, b As ClsB, f As IFoo, c As Collection, v As Variant + On Error Resume Next + + Set o = New ClsA + Set b = o + Report "Set b = o (ClsA to ClsB)" + Set f = o + Report "Set f = o (ClsA to IFoo)" + Set o = CreateObject("Scripting.Dictionary") + Set c = o + Report "Set c = o (Dictionary to Collection)" + Set v = New ClsA + Set b = v + Report "Set b = v (Variant holding ClsA to ClsB)" + Set o = New ClsB + Set b = o + Report "control: Set b = o (ClsB to ClsB)" + + 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/set-unsupported-class-error-number/vb6/Probe.vbp b/bugs/set-unsupported-class-error-number/vb6/Probe.vbp new file mode 100644 index 000000000..a5b26905e --- /dev/null +++ b/bugs/set-unsupported-class-error-number/vb6/Probe.vbp @@ -0,0 +1,10 @@ +Type=Exe +Module=Module1; Module1.bas +Class=ClsA; ClsA.cls +Class=ClsB; ClsB.cls +Class=IFoo; IFoo.cls +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/builder/page-baseline.json b/builder/page-baseline.json index 8f4838df1..a243fc34c 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 1475, - "staticFiles": 1063 + "pages": 1502, + "staticFiles": 1068 } diff --git a/builder/render.mjs b/builder/render.mjs index a9c8ee19d..fffa57ed2 100644 --- a/builder/render.mjs +++ b/builder/render.mjs @@ -480,6 +480,7 @@ export function createMarkdownIt(ctx) { md.use(figurePlugin, ctx); md.use(videoLinkPlugin, ctx); md.use(remoteImagePlugin, ctx); + md.use(stripHtmlCommentsPlugin); // Substitutes {{tbdocs:}} in prose. Registered last so it runs after // `replacements` has done the dash and quote transforms -- see counts.mjs // for why the layer matters and what it deliberately cannot reach. @@ -491,6 +492,96 @@ export function createMarkdownIt(ctx) { return md; } +// Author comments (``: SHOT markers, test-region markers, notes) +// are not for the reader, so no page carries one. Only the parser's html_block +// and html_inline tokens are touched: a comment shown in a fence or in a code +// span is text there, escaped by its own renderer, and never reaches this rule. +// It runs after tightLooseListPlugin, which reads a comment block as a block +// of its own, so a list's looseness is decided with the comment still there. +// +// A raw HTML block holding a script, style or textarea element is left alone, +// since a `", start + 4); + if (close < 0) break; + out += html.slice(pos, start); + let end = close + 3; + const lineStart = out.lastIndexOf("\n") + 1; + const newline = html.indexOf("\n", end); + const restOfLine = html.slice(end, newline < 0 ? html.length : newline); + const atLineStart = out.slice(lineStart).trim() === ""; + if (restOfLine.trim() === "") { + if (atLineStart) { + out = out.slice(0, lineStart); + end = newline < 0 ? html.length : newline + 1; + } else { + out = out.replace(/[ \t]+$/, ""); + end = newline < 0 ? html.length : newline; + } + } else if (atLineStart) { + end += restOfLine.length - restOfLine.trimStart().length; + } + pos = end; + } + return out + html.slice(pos); +} + +const isComment = (c) => c.type === "html_inline" && c.content.startsWith(" - + tools_groups @@ -56,16 +56,16 @@ cluster_ide - -IDE Automation + +IDE Automation cluster_defects - - -Defect Reproducers + + +Defect Reproducers @@ -619,133 +619,150 @@ - + +t28 + + +ide-js.test + + + + + e1 - + check_examples - + e2 - + gen_attribute_probes - + e3 - + census_attributes - + e4 - + sweep_attributes - + e5 - + package_tests - + i1 - - -addin_test + + +addin_test - + i2 - - -ide_test + + +ide_test - + i3 - - -try_help_addin + + +try_help_addin - + i4 - - -shoot_docs + + +shoot_docs - + i5 - - -imagestrip + + +imagestrip - + i8 - - -probe_theme_semicolon + + +probe_theme_semicolon - + i7 - - -svgshot/replay + + +svgshot/replay - + d1 - - -bug_repro + + +bug_repro - + d2 - - -vb6run + + +vb6run + + + + + +d3 + + +ide_js - @@ -802,8 +819,9 @@ - + + @@ -813,6 +831,6 @@ - + diff --git a/docs/Documentation/Permanent-Links.md b/docs/Documentation/Permanent-Links.md index 18cd98c30..9fd64a182 100644 --- a/docs/Documentation/Permanent-Links.md +++ b/docs/Documentation/Permanent-Links.md @@ -50,6 +50,8 @@ The build generates `/tB/symbols.json`, which lists every name the reference doc - **`interfaces`** maps an interface to the class whose members it declares. The compiler names a member by the interface it is declared on (hovering over `Collection.Add` says `VBA._Collection`), so a reader holding the compiler's answer looks the interface up here first. - **`symbols`** has one entry per name and URL. `name` is as the package declares it and is matched without regard to case. `package` and `container` are null for the language itself. `url` is a path below the site's root: a page's permalink exactly as written, with the heading's id after `#` for a member documented under a heading. `kind` is one of `statement`, `keyword`, `operator`, `directive`, `attribute`, `package`, `module`, `class`, `control`, `interface`, `enum`, `type`, `object`, `function`, `sub`, `property`, `method`, `event`, `constant`, `variable`, `field`, `enumvalue`, `delegate` and `member` --- the last for a name the pages document and the packages do not declare. +The intrinsic data types are the language's own entries of kind `type`, and their URLs are the one exception to the `/tB/` prefix: each points at the type's section of the [Data Types](../../Reference/Data-Types) page, as `/Reference/Data-Types#long` does, and the section links to the type's own page, `/tB/Core/Long`. Those anchors are part of this contract like the others in the index. + A name can have several entries: `Add` is a method of a dozen classes, and `Print` is a statement, a method of six classes and a member of `Debug`. The index does not choose between them; its reader does. The `$` form of a function has the entry of its base --- `Left$` goes to `Left` --- as the section on modules below describes. ## /tB/Core/\ @@ -74,6 +76,14 @@ A name can have several entries: `Add` is a method of a dozen classes, and `Prin - [Unload](../../tB/Core/Unload), [Unlock](../../tB/Core/Unlock) - [While-Wend](../../tB/Core/While-Wend), [Width](../../tB/Core/Width), [With](../../tB/Core/With), [Write](../../tB/Core/Write) +## /tB/Core/\ + +Each intrinsic data type has a page of its own beside the statements. The page of **Date** is `/tB/Core/Date-Type`, because `/tB/Core/Date` is a redirect to the **Date** property of the DateTime module: + +- [Boolean](../../tB/Core/Boolean), [Byte](../../tB/Core/Byte), [Integer](../../tB/Core/Integer), [Long](../../tB/Core/Long), [LongLong](../../tB/Core/LongLong), [LongPtr](../../tB/Core/LongPtr) +- [Single](../../tB/Core/Single), [Double](../../tB/Core/Double), [Currency](../../tB/Core/Currency), [Decimal](../../tB/Core/Decimal) +- [Date](../../tB/Core/Date-Type), [String](../../tB/Core/String), [Variant](../../tB/Core/Variant), [Object](../../tB/Core/Object) + ## /tB/Modules/\/\ Within each VBA module, each procedure, property, or statement has its own page, e.g. [**LenB**: /tB/Modules/Strings/Len](../../tB/Modules/Strings/Len). The `$`-suffixed and `B`/`W` variants share the base symbol's page, so `LenB`, `Len$`, etc. all use the [`Len`](../../tB/Modules/Strings/Len) page. diff --git a/docs/Documentation/Pipeline-Stages.md b/docs/Documentation/Pipeline-Stages.md index 41d972d97..0f3849b0d 100644 --- a/docs/Documentation/Pipeline-Stages.md +++ b/docs/Documentation/Pipeline-Stages.md @@ -740,14 +740,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 pre-render source rewrite chain the render stage applies 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. | | `applyPostRenderRewrites` | `(html) → string` | The rewrites over a page's rendered HTML 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 twenty plugins tabulated below, in their fixed order, plus eight renderer-rule overrides. `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. | +| `createMarkdownIt` | `({ highlighter, linkTables, baseurl, staticFiles, svgContents?, vendoredVideos?, vendoredImages?, counts? }) → MarkdownIt` | Builds the configured markdown-it instance: the twenty-one plugins tabulated below, in their fixed order, plus eight renderer-rule overrides. `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 twenty plugins in a fixed order: three from npm, sixteen defined in `render.mjs`, and `countPlugin` from `counts.mjs`. Most of the in-tree ones 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 requirement. +`createMarkdownIt` applies twenty-one plugins in a fixed order: three from npm, seventeen defined in `render.mjs`, and `countPlugin` from `counts.mjs`. Most of the in-tree ones 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 requirement. The table lists the plugin chain only. The renderer rules `createMarkdownIt` replaces directly, among them the one that wraps every table, are under [Renderer overrides](#renderer-overrides). @@ -772,7 +772,8 @@ The table lists the plugin chain only. The renderer rules `createMarkdownIt` rep | 17 | `figurePlugin` | in-tree | Makes a figure of an image that is its paragraph's only content: a diagram, a picture in two themes (the pair `themePairPlugin` wrote), or any other image. Its core rule `figures` hides the `

` around the image (the wrapper is a `

`) and wraps the image or images in `figure_open` and `figure_close` tokens. The `figure_open` renderer writes a `
` holding a `
` bar of icon buttons (download and copy by format, and zoom) and a `
`, and sets `page.hasFigure`. A diagram, whose `src` names a site `.svg` with no `.png` beside it and whose content is in `ctx.svgContents`, is written into the container as the SVG itself, labelled with the alt text; with no alt text it logs a warning and gets no `role`. Any other image stays an `` in the container. An image with text beside it, or inside a link, gets no figure. The wrapper carries `data-tall` when the figure is at least as tall as its bar stood on end. | | 18 | `videoLinkPlugin` | in-tree | A link marked `{: .video }` becomes a locally vendored poster frame linking out to the video page. The thumbnail `src` is **root-absolute**, because the PDF book flattens every page into one document and a page-relative `src` would resolve against the book root. | | 19 | `remoteImagePlugin` | in-tree | A `github.com/user-attachments/…` image becomes the copy [`vendor-assets.mjs`](#vendor-assetsmjs) downloaded, in both markdown `![…]()` and raw `` syntax. Also root-absolute, for the same reason. | -| 20 | `countPlugin` | `counts.mjs` | Substitutes each `{{tbdocs:}}` in prose, and in image alt text, with the count the build derived under that name. Its core rule is pushed last, so it runs after `replacements` and sees the text the reader will. Code spans and fences are untouched. Does nothing when `ctx.counts` is absent. | +| 20 | `stripHtmlCommentsPlugin` | in-tree | Removes the author's HTML comments (``) from the output: a comment block is dropped, and a comment inside a paragraph or a raw HTML block is cut out with the blank it leaves. It works on `html_block` and `html_inline` tokens only, so a comment shown in a fence or a code span stays, and it leaves a raw HTML block that holds a `script`, `style` or `textarea` element as written. It runs after `tightLooseListPlugin`, which reads a comment block as a block of its own. The comments the templates write (the SEO block and the two icon licences) are not markdown and stay. | +| 21 | `countPlugin` | `counts.mjs` | Substitutes each `{{tbdocs:}}` in prose, and in image alt text, with the count the build derived under that name. Its core rule is pushed last, so it runs after `replacements` and sees the text the reader will. Code spans and fences are untouched. Does nothing when `ctx.counts` is absent. | **Registration order is not execution order**, in two ways. Both matter when inserting a plugin. diff --git a/docs/Documentation/Tools-Compiler.md b/docs/Documentation/Tools-Compiler.md index e99a7109b..279bea75b 100644 --- a/docs/Documentation/Tools-Compiler.md +++ b/docs/Documentation/Tools-Compiler.md @@ -45,7 +45,7 @@ twinBASIC has no command-line build. None of the compiler executable's verbs --- **A build with LLVM in it holds a lease on twinBASIC's LLVM cache server** (`scripts/lib/tb-llvm-cache.mjs`), from before the IDE starts until it has ended. The server is one process that every compiler on the machine shares; without a lease the first compiler to need it starts it inside its own IDE's job, and it ends with that IDE, under every other run that is using it. With a lease it is started outside every IDE and ends about 15 seconds after the last lease is released. `--json` gives the lease as `llvmCache`. -A packed project's default build path opens a *Save* dialog that the private desktop hides. So either flag first exports the project into `%TEMP%\tbbuild\\src`, packs a copy with an explicit build path under `%TEMP%\tbbuild\\out` and a project id of its own, and opens that copy. Once the run is over the copy and its sources are removed, and so is the whole folder when nothing was built. After a build that succeeded, `%TEMP%\tbbuild\\out` stays, because it holds the file `built:` names; the next run in that slot replaces it. +So that the built file lands in a folder the run owns and the given project is left untouched, either flag first exports the project into `%TEMP%\tbbuild\\src`, packs a copy with an explicit build path under `%TEMP%\tbbuild\\out` and a project id of its own, and opens that copy. Once the run is over the copy and its sources are removed, and so is the whole folder when nothing was built. After a build that succeeded, `%TEMP%\tbbuild\\out` stays, because it holds the file `built:` names; the next run in that slot replaces it. **Each run has a slot of its own** from [`tbbroker`](#tbbroker): a private desktop, and the number its folders and project id are named by, which no other run on the machine holds at the same time. The IDE's temp folder (`%TEMP%\tbbuild-tmp-`, its `TEMP` and `TMP`) and WebView2 user-data folder (`%TEMP%\tbbuild-wv2-`, about 28 MB) are emptied when the IDE starts and removed once it has ended. The first run after you log on starts the broker. @@ -212,12 +212,9 @@ one whose image is on the reap list and that has no window open. A new one with reported and left alone, since it may be a copy the user opened. Two concurrent runs driving Excel cannot tell their servers apart, so the first to finish harvests both: pass `--no-reap` there and sweep once at the end. -> [!IMPORTANT] -> A project whose `project.buildPath` is still the default `${SourcePath}\Build\...` template -> opens a native *Save* dialog on build. On a private desktop that dialog is invisible, so the -> build never happens, while every health check says the IDE is fine. `tbrun` sets the path to -> a folder of its own in its staged copy. The file keeps the IDE's own name, -> *project name*`_`*target*`.`*extension* --- for example `ArchProbe_win64.exe`. +`tbrun` sets `project.buildPath` to a folder of its own in its staged copy, so that it knows +where the built file is. The file keeps the IDE's own name, +*project name*`_`*target*`.`*extension* --- for example `ArchProbe_win64.exe`. Like `tbbuild`, it leaves the IDE's registry entries as it found them: it deletes every entry under its own temp folder once the IDE has exited, and again at the start of a run, which diff --git a/docs/Documentation/Tools-Defects.md b/docs/Documentation/Tools-Defects.md index 06aeacab9..ca31c8ca1 100644 --- a/docs/Documentation/Tools-Defects.md +++ b/docs/Documentation/Tools-Defects.md @@ -9,7 +9,7 @@ permalink: /Documentation/Development/Tools-Defects # Defect Reproducers {: .no_toc } -Builds and verifies the reproducer projects for twinBASIC defects, and runs VB6 code to compare what twinBASIC does with what VB6 does. The command-line conventions every tool shares are under [Command-line conventions](Tools#command-line-conventions). +Builds and verifies the reproducer projects for twinBASIC defects, runs VB6 code to compare what twinBASIC does with what VB6 does, and reads and patches the IDE's own JavaScript to test a fix for an IDE defect. The command-line conventions every tool shares are under [Command-line conventions](Tools#command-line-conventions). * TOC goes here {:toc} @@ -148,3 +148,50 @@ Every process the tool starts has a time limit and is ended by its pid when it r Other options: `--timeout S` is the time limit, in seconds, for each run of the built exe (default 30), and `--json` prints one JSON object in place of the text. Exit codes: **0** the sample ran, or, with `--docs`, no fence differs and none raised an error; **1** a VB6 compile error, a run-time error, a sample during which the exe ended (a VB6 exe that dies of an access violation can exit with code 0, so the exe ending before the time limit in the middle of a sample is what shows it; the fault the Application event log records is named when there is one, and so are a box the exe opened and what the VB runtime logged) or a sample that did not return, or, with `--docs`, at least one fence that differs or raised an error; **2** the harness could not run --- a refused command line, a file that is missing, a sample that is refused, no VB6, VB6 failing to build, or a crash. + +## ide_js.mjs +{: #ide-js } + + node scripts/ide_js.mjs survey [--ide ] [--json] + node scripts/ide_js.mjs units [--ide ] [--json] + node scripts/ide_js.mjs list [--ide ] + node scripts/ide_js.mjs name =... [--ide ] [--map ] + node scripts/ide_js.mjs seal | check [--ide ] [--map ] [--json] + node scripts/ide_js.mjs view ... | --all --out [--ide ] [--map ] + node scripts/ide_js.mjs patch --out | --copy-to [--ide ] [--map ] + node scripts/ide_js.mjs shape [--ide ] [--map ] + +Reads, names and patches the twinBASIC IDE's own JavaScript, `ide/main.js` in the install, so that an IDE defect in `BUGS-TO-REPORT.md` can carry a *fix shape*: the function that is wrong, in a readable form, changed so that the defect goes, and tested on a patched copy of the install. The library is `scripts/ide-js/lib.mjs`. + +`main.js` is one line of about 1.3 MB. Its minifier removed the whitespace and renamed the local variables and parameters, mostly to one letter; it left every top-level function and class, every property and every global with its own name. So a top-level function (`debugOutputPartial`) or a method of a top-level class (`Class.method`, `Class.get name`, `Class.static name`) is a *unit* whose name is the same in every build, and the tool works unit by unit. The file is parsed with acorn, keeping its tokens and their places, and nothing is ever printed from the syntax tree back into `main.js`: an edit replaces a range of the original text, so every byte outside the edits is the install's byte. `survey` shows that the tokens, the comments and the whitespace between them make up every byte of the file. + +**The name map**, `scripts/ide-js/names.json`, gives names to the locals and parameters of units. The names are the documentation's own, given from what the code does, and never twinBASIC's: the tool works only from what an install ships. A local is keyed by its unit and by where it is declared --- the path of its scope inside the unit and its place in that scope's declarations (`0#1` is the unit's second parameter or variable, `0/2#0` the first binding of its third nested scope) --- never by its minified name, which changes from build to build. `name` adds names and records, beside each, what the local is in the build it was named on: its minified name, its kind and how often it is used, with the unit's *shape*, a hash of its syntax tree with every local replaced by its key. `check` says how each name applies to another build: + +| Verdict | Meaning | +|---|---| +| `exact` | The unit's shape is unchanged: the code is the same, and the name is certainly this local's. | +| `carried` | The unit changed, but the key still finds a local of the same kind, used as often. The name is applied. | +| `review` | The key finds a local of the same kind that is used more or less often: probably the same variable. The name is not applied until `seal` records it again. | +| `lost` | The unit is gone, or the key finds no local, or one of another kind. The name is not applied; `list` shows what is there now. | + +`name` refuses a name that is not an identifier, one given to two locals of a unit, and one that would hide another name the unit uses: a global it refers to, or a local that keeps its minified name. After a new BETA, `check` it, look at each `review` and `lost` name with `list` and `view`, name again what needs it, and `seal` the map against the new build. + +| Command | Effect | +|---|---| +| `survey` | What the minifier left: the size, what is at the top level, the number of units, globals and locals, the lengths of the local names, whether there is a source map, and whether the token list rebuilds the file byte for byte. | +| `units ` | The units that are the same, changed, added and removed between `--ide`'s build and another, by shape. | +| `list ` | The unit's locals, each with its key, minified name, kind, the number of times it is used and a few characters around a use, to name. | +| `name =...` | Gives names to locals of the unit, keeping its other names, and seals the unit against `--ide`'s build. | +| `seal` | Seals every unit of the map against `--ide`'s build again, after its names have been checked there. | +| `check` | Each name of the map with its verdict for `--ide`'s build, and a count of each. | +| `view ...` | The readable view: each unit with the map's names put in (only the `exact` and `carried` ones), pretty-printed by prettier, under a comment that names the build, where the unit is in `main.js`, its shape, the names applied and any not applied. `--all --out ` writes the whole file so, about 50,000 lines. | +| `patch ` | Splices a fix into `main.js`. `` holds whole units in the map's names, as the view prints them: top-level functions, and a class holding each method that is fixed. For each unit, the map's names are turned back into the build's minified names, the unit's tokens are compared with the fix's (a string by its value, so the view's quotes are no difference), and each run of tokens that differs becomes one edit of the minified text. The patched file is parsed again, and each patched unit must have the fix's shape, so the patched `main.js` runs the code the fix shows. Each edit is printed before and after, in the minified text. Every difference becomes an edit, so a fix patched into a later build whose unit changed takes that change back out: run `shape` on that build first. | +| `shape ` | The fix shape, as markdown for the defect's entry: for each unit of the fix, which names are the map's and what the build calls them, and a diff of the unit as the build has it against the fix, both in the map's names and printed alike. | + +**Only a copy of an install is ever patched.** `patch --copy-to ` copies the install into a folder of its own under ``, which must be under the system temp folder, without its `projects\` and `addins\` folders, writes the patched `main.js` into the copy and prints the copy's `twinBASIC.exe`. Give that to the IDE tests, `ide-test.bat --ide \twinBASIC.exe --only `, to see the defect's test report that it no longer reproduces while the controls pass; delete the folder afterwards. `patch --out ` writes the patched `main.js` alone, and refuses a file inside the repository or inside an install that is not under the temp folder. `view --out` refuses the repository too: no copy of `main.js`, whole or viewed, belongs in it. A fix is kept as `bugs//fix.js` beside the defect's reproducer. + +Options: `--ide ` is the install, as its `twinBASIC.exe`, its folder or a `main.js` (default: `$TB_IDE`, else the newest `twinBASIC_IDE_BETA_` on the Desktop); `--map ` the name map (default `scripts/ide-js/names.json`); `--json` prints one JSON object for `survey`, `units` and `check`. A command refuses an option that does not apply to it. + +It reads files and starts nothing, so it needs no IDE running and no private desktop. It is outside every gate and outside CI, since it needs an install; its tests are [`ide-js.test.mjs`](Tools-Toolchain-Tests#ide-js-test). + +Exit codes: **0** done, and for `check`, no name is lost; for `survey`, the token list rebuilds the file; **1** a finding: `check` found a lost name, `survey` a byte the token list does not account for, or, for `patch` and `shape`, the fix does not apply to the build (a unit it names is missing, a name in it is one of the build's minified names, or the patched unit is not the fix); **2** a refused command line, an install or a file that cannot be read, a `main.js` or a fix that does not parse, a name the map may not take, a refused `--out` or `--copy-to`, or a crash. diff --git a/docs/Documentation/Tools-Toolchain-Tests.md b/docs/Documentation/Tools-Toolchain-Tests.md index 1427155e4..0b86feb1e 100644 --- a/docs/Documentation/Tools-Toolchain-Tests.md +++ b/docs/Documentation/Tools-Toolchain-Tests.md @@ -337,6 +337,15 @@ Unit tests for the [package indexer](Tools-Build#twin-index) in `indexer/lib/`, Exit codes: **0** every test passed, **1** a test failed. +## ide-js.test.mjs +{: #ide-js-test } + + node --test test/ide-js.test.mjs + +Unit tests for [`ide_js.mjs`](Tools-Defects#ide-js) and its library, `scripts/ide-js/lib.mjs`, which read, name and patch the twinBASIC IDE's minified `ide/main.js`. The "builds" are a few lines of minified JavaScript written in the test: one program under two sets of local names, and three changes a later build can make to one of its functions. No copy of an IDE's `main.js` is in the repository. The round trip is tested on a text with comments, a template literal and a regular expression: the tokens, the comments and the whitespace between them are every byte of it. The keys are tested for what they promise: a local's key says where it is declared, so it is the same under other names, and the same code under other names has the same shape. The name map is tested on each verdict: a name is *exact* in an unchanged function, *carried* when the function changed but the variable is used as often, *review* when it is used more or less often, and *lost* when its key finds a binding of another kind, none, or no function at all. The names the map refuses are tested too: one that is not an identifier, a reserved word, one given to two variables, and one that would hide another name the function uses. A fix is tested as it is written from the view, in our names and with the printer's quotes and semicolons: it becomes the smallest token edit of the minified text, every other byte kept, and a function the fix leaves as it was gives no edit. A fix of a function the build does not have, and a new variable named like one of the build's minified names, are refused. The line diff of a fix shape is tested for its context and its hunk headers. The command line is tested by running the tool on a small install in the temp folder: `check`'s exit codes, `patch` writing a `main.js` under the temp folder and a copy of the install there, and refusing the repository and a folder outside the temp folder. No twinBASIC install, no built tree, about 1 s. + +Exit codes: **0** every test passed, **1** a test failed. + ## check_page_baseline.mjs {: #check-page-baseline } diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md index 556588927..a9669f08b 100644 --- a/docs/Documentation/Tools.md +++ b/docs/Documentation/Tools.md @@ -113,7 +113,7 @@ A local pass of [`check_a11y.mjs`](Tools-Site-Checks#check-a11y) does not predic test.bat -The tests the toolchain has to pass. Twenty-seven steps, each stopping the run if it fails: +The tests the toolchain has to pass. Twenty-eight steps, each stopping the run if it fails: 1. [`scripts/check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy) --- verifies the publish allowlist still refuses the types it should. Needs neither a browser nor a built tree. 2. [`scripts/check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists) --- verifies the two gate lists on this page still match the wrappers that run them. @@ -130,18 +130,19 @@ The tests the toolchain has to pass. Twenty-seven steps, each stopping the run i 13. [`test/broker.test.mjs`](Tools-Toolchain-Tests#broker-test) --- tests [`tbbroker`](Tools-Compiler#tbbroker), which hands out private desktops, slots and ports, and its Node client. Skipped unless `TBBROKER_EXE` names a built broker. 14. [`test/addin-project.test.mjs`](Tools-Toolchain-Tests#addin-project-test) --- unit tests for how the build packs the help add-in's project file: exactly the files git tracks in `add-in/`, never `Resources/HELP/`, the same bytes from any checkout. 15. [`test/indexer.test.mjs`](Tools-Toolchain-Tests#indexer-test) --- unit tests for the package indexer: the VB6 preprocessing, the files of a package, the `api.json` built from them (sorted by code unit, nested types by their path, values and fields in declaration order, the same bytes from the same files) and the differ that compares two of them. -16. [`scripts/check_regex_safety.mjs`](Tools-Toolchain-Tests#check-regex-safety) --- refuses a regex that can backtrack exponentially, written as a literal or built from constants. -17. [`scripts/check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions) --- verifies no pre-render rewrite alters the contents of a code fence or code span, that the rewrites over rendered HTML leave a raw `
` or `` alone, and that `lib/markdown.mjs` and `lib/frontmatter.mjs` pass their probes.
-18. [`scripts/check_page_baseline.mjs`](Tools-Toolchain-Tests#check-page-baseline) --- verifies the page-count drift guard still refuses a fall.
-19. [`scripts/check_book_coverage.mjs`](Tools-Toolchain-Tests#check-book-coverage) --- verifies the build still warns about a page `docs/_book.yml` does not mention.
-20. [`scripts/check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests) --- verifies every NOTE and WARNING of a contributed package's section names the test cases behind it, and that each package test project is in order.
-21. [`scripts/check_symbol_index.mjs`](Tools-Toolchain-Tests#check-symbol-index) --- verifies the symbol index still places each kind of symbol, and its drift guard still refuses a lost URL.
-22. [`scripts/check_twin_parsers.mjs`](Tools-Toolchain-Tests#check-twin-parsers) --- verifies the scanners of twinBASIC source and of the attribute reference still read the shapes each once misread.
-23. [`scripts/check_attribute_sweep.mjs`](Tools-Toolchain-Tests#check-attribute-sweep) --- verifies the logic of the attribute sweep: its site skeletons, how it reads a probe's diagnostics, how it batches probes, and how it compares the answers with `Attributes.md`.
-24. [`scripts/check_cli.mjs`](Tools-Toolchain-Tests#check-cli) --- verifies `lib/cli.mjs`, the command-line parser, and each tool's recorded command-line errors.
-25. [`scripts/check_pdf_shims_equiv.mjs`](Tools-Toolchain-Tests#check-pdf-shims-equiv) --- verifies the book's pdf-lib shims write what stock pdf-lib writes, patch the members of pdf-lib it lists, have each of them in their shim's table of targets, and run.
-26. [`scripts/check_impexp_parity.mjs`](Tools-Toolchain-Tests#check-impexp-parity) --- verifies the editions of the impexp tool pass the same built-in tests, and exit, print and write the same for one sequence of commands. Without Python it reports itself skipped and passes, except in CI. It compares the twinBASIC edition too, when `IMPEXP_EXE` names its built exe.
-27. [`scripts/check_axe_patch_equiv.mjs`](Tools-Toolchain-Tests#check-axe-patch-equiv) --- verifies the vendored axe source patch still produces identical colour values.
+16. [`test/ide-js.test.mjs`](Tools-Toolchain-Tests#ide-js-test) --- unit tests for [`ide_js.mjs`](Tools-Defects#ide-js), which reads, names and patches the IDE's minified `main.js`: the byte-exact round trip, how a name carries to another build, and a fix patched in as the smallest edit, on a few lines written in the test.
+17. [`scripts/check_regex_safety.mjs`](Tools-Toolchain-Tests#check-regex-safety) --- refuses a regex that can backtrack exponentially, written as a literal or built from constants.
+18. [`scripts/check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions) --- verifies no pre-render rewrite alters the contents of a code fence or code span, that the rewrites over rendered HTML leave a raw `
` or `` alone, and that `lib/markdown.mjs` and `lib/frontmatter.mjs` pass their probes.
+19. [`scripts/check_page_baseline.mjs`](Tools-Toolchain-Tests#check-page-baseline) --- verifies the page-count drift guard still refuses a fall.
+20. [`scripts/check_book_coverage.mjs`](Tools-Toolchain-Tests#check-book-coverage) --- verifies the build still warns about a page `docs/_book.yml` does not mention.
+21. [`scripts/check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests) --- verifies every NOTE and WARNING of a contributed package's section names the test cases behind it, and that each package test project is in order.
+22. [`scripts/check_symbol_index.mjs`](Tools-Toolchain-Tests#check-symbol-index) --- verifies the symbol index still places each kind of symbol, and its drift guard still refuses a lost URL.
+23. [`scripts/check_twin_parsers.mjs`](Tools-Toolchain-Tests#check-twin-parsers) --- verifies the scanners of twinBASIC source and of the attribute reference still read the shapes each once misread.
+24. [`scripts/check_attribute_sweep.mjs`](Tools-Toolchain-Tests#check-attribute-sweep) --- verifies the logic of the attribute sweep: its site skeletons, how it reads a probe's diagnostics, how it batches probes, and how it compares the answers with `Attributes.md`.
+25. [`scripts/check_cli.mjs`](Tools-Toolchain-Tests#check-cli) --- verifies `lib/cli.mjs`, the command-line parser, and each tool's recorded command-line errors.
+26. [`scripts/check_pdf_shims_equiv.mjs`](Tools-Toolchain-Tests#check-pdf-shims-equiv) --- verifies the book's pdf-lib shims write what stock pdf-lib writes, patch the members of pdf-lib it lists, have each of them in their shim's table of targets, and run.
+27. [`scripts/check_impexp_parity.mjs`](Tools-Toolchain-Tests#check-impexp-parity) --- verifies the editions of the impexp tool pass the same built-in tests, and exit, print and write the same for one sequence of commands. Without Python it reports itself skipped and passes, except in CI. It compares the twinBASIC edition too, when `IMPEXP_EXE` names its built exe.
+28. [`scripts/check_axe_patch_equiv.mjs`](Tools-Toolchain-Tests#check-axe-patch-equiv) --- verifies the vendored axe source patch still produces identical colour values.
 
 POSIX:
 
@@ -160,6 +161,7 @@ POSIX:
       && node --test test/broker.test.mjs \
       && node --test test/addin-project.test.mjs \
       && node --test test/indexer.test.mjs \
+      && node --test test/ide-js.test.mjs \
       && node scripts/check_regex_safety.mjs \
       && node scripts/check_code_regions.mjs \
       && node scripts/check_page_baseline.mjs \
diff --git a/docs/Features/Fusion.md b/docs/Features/Fusion.md
index cf62d7e2d..fb064f9dd 100644
--- a/docs/Features/Fusion.md
+++ b/docs/Features/Fusion.md
@@ -70,6 +70,8 @@ When this occurs, you will see a note in the DEBUG CONSOLE:
 
 This additional EXE acts as the out-of-process container for those controls and is managed automatically by the twinBASIC IDE
 
+Ctrl+Shift+B in the IDE builds the host EXE on request, without building the project. The DEBUG CONSOLE then shows that the Fusion server file was built, or that building it failed. See [The Fusion host](../tB/IDE/Building/Build-Output#the-fusion-host).
+
 ## ActiveX Hosting EXE Output Path
 
 A project-level setting allows you to control where the Fusion host EXE is generated:
diff --git a/docs/Features/Language/Data-Types.md b/docs/Features/Language/Data-Types.md
index 07fe226a2..8a2acbbfd 100644
--- a/docs/Features/Language/Data-Types.md
+++ b/docs/Features/Language/Data-Types.md
@@ -7,23 +7,23 @@ permalink: /Features/Language/Data-Types
 
 # New Data Types
 
-twinBASIC introduces several new data types to enhance your programming capabilities.
+twinBASIC introduces several new data types to enhance your programming capabilities. Each has a reference page of its own, linked below.
 
 ## LongPtr
 
-Meant primarily to handle pointers, `LongPtr` is a 4-byte (32 bits) signed integer in 32bit mode, and a signed 8-byte integer (64 bits) in 64bit mode.
+Meant primarily to handle pointers, `LongPtr` is a 4-byte (32 bits) signed integer in 32bit mode, and a signed 8-byte integer (64 bits) in 64bit mode. See [**LongPtr**](../../tB/Core/LongPtr).
 
 ## LongLong
 
-A signed 8-byte (64 bits) integer, ranging from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Note that this type is available in both 32bit and 64bit mode (VBA restricts it to 64bit mode).
+A signed 8-byte (64 bits) integer, ranging from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Note that this type is available in both 32bit and 64bit mode (VBA restricts it to 64bit mode). See [**LongLong**](../../tB/Core/LongLong).
 
 ## Decimal
 
-In twinBASIC, `Decimal` is implemented as a full, regular data type, in addition to use within a `Variant`. This is a 16-byte (128 bits) type which holds a 12-byte (96 bits) integer with variable decimal point scaling and sign bit information. Values range from -79,228,162,514,264,337,593,543,950,335 to 79,228,162,514,264,337,593,543,950,335.
+In twinBASIC, `Decimal` is implemented as a full, regular data type, in addition to use within a `Variant`. This is a 16-byte (128 bits) type which holds a 12-byte (96 bits) integer with variable decimal point scaling and sign bit information. Values range from -79,228,162,514,264,337,593,543,950,335 to 79,228,162,514,264,337,593,543,950,335. See [**Decimal**](../../tB/Core/Decimal).
 
 ## Type Support
 
 All of the datatype management features also exist for these types:
-- `DefDec`/`DefLngLng`/`DefLongPtr` - Default type declarations
-- `CDec`/`CLngLng`/`CLongPtr` - Type conversion functions
-- `vbDecimal`/`vbLongLong`/`vbLongPtr` - Type check constants
+- `DefDec`/`DefLngLng`/`DefLngPtr` - Default type declarations
+- `CDec`/`CLngLng`/`CLngPtr` - Type conversion functions
+- `vbDecimal`/`vbLongLong` - Type check constants. `VarType` of a `LongPtr` gives `vbLong` in a 32bit build and `vbLongLong` in a 64bit build.
diff --git a/docs/Features/Language/Literals.md b/docs/Features/Language/Literals.md
index 6eebd2e2a..56f0ba483 100644
--- a/docs/Features/Language/Literals.md
+++ b/docs/Features/Language/Literals.md
@@ -19,7 +19,7 @@ The `&H`, `&O`, and `&B` literals can all be grouped using an underscore, for ex
 
 ## Date Literals
 
-Dates and times are written between number signs: `#2026-03-17#`, `#1:45 PM#`, `#3/17/2026 13:45:30#`. The forms the compiler accepts, and how it reads the month, day and year, are described under [Data Types → Date literals](../../Reference/Data-Types#date-literals).
+Dates and times are written between number signs: `#2026-03-17#`, `#1:45 PM#`, `#3/17/2026 13:45:30#`. The forms the compiler accepts, and how it reads the month, day and year, are described under [Date → Date literals](../../tB/Core/Date-Type#date-literals).
 
 ## Example
 
diff --git a/docs/Features/Packages/Creating a TWINPACK package.md b/docs/Features/Packages/Creating a TWINPACK package.md
index 96d01db77..d35292d70 100644
--- a/docs/Features/Packages/Creating a TWINPACK package.md	
+++ b/docs/Features/Packages/Creating a TWINPACK package.md	
@@ -19,7 +19,7 @@ Once you've created the project, you should find the extra 'PACKAGE PUBLISHING'
 
 ![The PACKAGE PUBLISHING panel, floating over the editor, with EDIT links for Namespace, Description, Licence, Visibility and Version and a SIGN IN link for Publisher, above the PUBLISH THIS PACKAGE button](Images/9eeffbcf-d73e-4a92-bce5-811ed60aba98.png){:width="348" height="378"}
 
-You should now edit the Namespace, Description, Licence, Visibility and Version properties appropriately by using the package manager 'EDIT' links, which will take you to the individual settings in the `Settings` file.   Once you've edited them, remember to close (and save) the `Settings` file in order for your changes to be reflected in the package manager panel.
+You should now edit the Namespace, Description, Licence, Visibility and Version properties appropriately by using the package manager 'EDIT' links, which open Project Settings at the individual setting.   Once you've edited a value, press **Apply Changes** in order for your change to be reflected in the package manager panel. [Package Publishing](../../tB/IDE/Building/Package-Publishing) describes the panel.
 
 - **Namespace:** this is the symbol that will be used to group your components in projects that reference your package.  For example, a package that provides a series of different dialog classes might use the namespace `Dialogs`.
 - **Description:** this is the descriptive text that will appear in the `Settings`->`References` list.  If you plan to share this package, think carefully about the description so that others can discover your package through TWINSERV.
@@ -51,15 +51,9 @@ If you're publishing your package onto TWINSERV, you don't need to create the TW
 
 ***Publishing packages onto TWINSERV requires you to first create a publisher account.  If you haven't done so, you'll be prompted to do so at this stage.***
 
-You will then be prompted to confirm the package details:
+You will then be prompted to confirm the package details, in a box that lists the publisher, package namespace, description, version and licence, and asks whether to proceed. After pressing **Continue**, the package will be uploaded to TWINSERV.   The `DEBUG CONSOLE` shows that the upload has started, and a box with **OK** shows the result. [Package Publishing](../../tB/IDE/Building/Package-Publishing#publishing) shows the box and lists the results.
 
-![A confirmation dialog listing the publisher, package namespace, description, version and licence, asking whether to proceed](Images/packPublishPackage1.png){:width="478" height="257"}
-
-After pressing `YES`, the package will be uploaded to TWINSERV.   Check the `DEBUG CONSOLE` for completion notices:
-
-![The DEBUG CONSOLE showing the publish attempt followed by a successful result](Images/packPublishComplete1.png){:width="626" height="64"}
-
- If the package got uploaded successfully, it should be available via TWINSERV within a few moments.   If you've created a `PUBLIC` package, others will be able to see and download it at this point.
+If the package got uploaded successfully, it should be available via TWINSERV within a few moments.   If you've created a `PUBLIC` package, others will be able to see and download it at this point.
 
 See [Importing a package from TWINSERV](Importing-TWINSERV) for referencing and using the uploaded packages.
 
diff --git a/docs/Features/Project-Configuration/ActiveX-Registration.md b/docs/Features/Project-Configuration/ActiveX-Registration.md
index d338070c4..a4815ddcc 100644
--- a/docs/Features/Project-Configuration/ActiveX-Registration.md
+++ b/docs/Features/Project-Configuration/ActiveX-Registration.md
@@ -18,4 +18,6 @@ Register ActiveX builds to `HKEY_LOCAL_MACHINE` or `HKEY_CURRENT_USER` option. W
 
 ## Build-Time Registration
 
-Registration at build time is optional. tB provides the Project: Register DLL after build option so you can disable automatic registration, if for example you wanted to move the file first.
+Registration at build time is optional. The *Register DLL after build* setting in [Project Settings](../../tB/IDE/Project-Settings#register-dll-after-build) lets you disable automatic registration, if for example you wanted to move the file first.
+
+[ActiveX registration](../../tB/IDE/Building/ActiveX-Registration) describes what the IDE shows when a build registers a DLL, the warning for a project that registers to `HKEY_LOCAL_MACHINE`, and what **Clean** removes.
diff --git a/docs/IDE/Building/ActiveX-Registration.md b/docs/IDE/Building/ActiveX-Registration.md
new file mode 100644
index 000000000..f8dd26099
--- /dev/null
+++ b/docs/IDE/Building/ActiveX-Registration.md
@@ -0,0 +1,101 @@
+---
+title: ActiveX registration
+parent: Building
+grand_parent: IDE
+nav_order: 4
+permalink: /tB/IDE/Building/ActiveX-Registration
+index_also:
+  - register a dll
+index:
+  - deregister a dll
+  - run as administrator
+  - delete it and continue
+  - hklm
+---
+
+# ActiveX registration
+
+A build of an ActiveX DLL or an ActiveX control registers the new file, so that other programs can create its classes. **Clean** takes the registration away again. This page describes what is registered, where, what the console says, and the questions the IDE asks when registration goes wrong.
+
+The settings and the two registry hives are described in [Project Settings](../Project-Settings#register-dlls-to-hklm) and in [ActiveX Registration Options](../../../Features/Project-Configuration/ActiveX-Registration). A Standard DLL, an EXE and a package are not registered.
+
+## Registering after a build
+
+Two settings decide it:
+
+- [*Register DLL after build*](../Project-Settings#register-dll-after-build) is **Yes** by default. With **No**, the build writes the file and registers nothing.
+- [*Register DLLs to HKLM*](../Project-Settings#register-dlls-to-hklm) is **No** by default, and the templates keep it so. The registration then goes to `HKEY_CURRENT_USER`, which needs no special rights.
+
+The compiler does the registration, as the Windows user that runs the IDE, and the console reports it:
+
+```
+[LINKER] SUCCESS created output file ''
+[LINKER]    Open Folder
+[REGISTER] type-library registration completed. DllRegisterServer() returned OK
+```
+
+A 32-bit build writes its keys under `Software\Classes\WOW6432Node` in `HKEY_CURRENT_USER`: `CLSID`, `Interface` and the program ID. Its type library is under `TypeLib`, in the `win32` branch. A 64-bit build writes the same keys without `WOW6432Node`, and its type library is in the `win64` branch.
+
+A project made from the **ActiveX DLL** template leaves [*Use Project ID for type library ID*](../Project-Settings#use-project-id-for-type-library-id) at **No**, so each build gives the type library a new GUID. The **ActiveX Control** template sets it to **Yes**.
+
+## Clean
+
+**Clean** runs the registration in reverse, then deletes the file:
+
+```
+[BUILD] Starting...
+[UNREGISTER] previous type-library deregistration completed. DllUnregisterServer() returned OK
+[BUILD-CLEAN] build cleanup completed.
+```
+
+Every key that the build wrote is gone after it. There is no separate command to register or deregister a file by hand: registration is the build's, and deregistration is **Clean**'s. A **Build** that replaces a registered file deregisters the old one first; [a failure there](#when-the-old-dll-cannot-be-deregistered) is described below.
+
+To keep a DLL from being registered, set *Register DLL after build* to **No**.
+
+## The warning at the machine-wide registry
+
+A project with *Register DLLs to HKLM* at **Yes** registers its DLL under `HKEY_LOCAL_MACHINE`, and Windows lets only an administrator write there. When you open such a project in an IDE that does not run as an administrator, a box opens before the project loads:
+
+**WARNING: This project registers itself to the HKEY_LOCAL_MACHINE registry hive which requires ADMIN permissions. The twinBASIC IDE is not currently running elevated, and so the registration of the DLL will fail during the build process. For the build to be successful, please restart the twinBASIC IDE using the 'Run As Administrator' Windows option.**
+
+Its one button is **Continue Anyway**. The box does not appear when *Register DLL after build* is **No**. An IDE that runs as an administrator shows **ADMIN** in its title area and does not open it.
+
+
+
+> [!NOTE] BETA 1010
+> The warning opens twice each time you open the project, and also for a Standard EXE project that has *Register DLLs to HKLM* at **Yes**, although an EXE registers nothing. Choose **Continue Anyway** each time. To stop the box, set *Register DLLs to HKLM* or *Register DLL after build* to **No**.
+
+If you build anyway, the DLL is written and its registration fails:
+
+```
+[LINKER] SUCCESS created output file ''
+[REGISTER] type-library registration FAILED.   DllRegisterServer() returned -2147220992
+[BUILD] failed
+```
+
+## When the old DLL cannot be deregistered
+
+A build first deregisters the DLL of the previous build. When that fails, the console prints `[UNREGISTER] type-library deregistration FAILED.   DllUnregisterServer() returned `. What the IDE does next depends on the code.
+
+For a DLL registered under `HKEY_LOCAL_MACHINE`, in an IDE that is not elevated, the failure code is `-2147220991`. The build stops with `[BUILD] failed`, and a box opens:
+
+**Build failed: unable to unregister the previous DLL.   Continue anyway?**
+
+
+
+### Delete It And Continue
+{: #delete-it-and-continue }
+
+**Delete It And Continue** builds again and ignores the old build. The console prints `[BUILD] Starting...` again. **Cancel** stops there.
+
+> [!NOTE] BETA 1010
+> The box that opens at **Clean** has the same buttons, but **Delete It And Continue** there builds a new DLL instead of deleting the old one: the console prints `[LINKER] SUCCESS` and a failed `[REGISTER]` line. Choose **Cancel** at **Clean**: the box does not clean.
+
+If the previous file is not a DLL any more, for example because something else replaced it, the deregistration fails with `-2147467259`, the IDE asks nothing and the build goes on. The new DLL is registered normally.
+
+## See Also
+
+- [Project Settings](../Project-Settings#register-dlls-to-hklm) -- *Register DLLs to HKLM* and *Register DLL after build*
+- [ActiveX Registration Options](../../../Features/Project-Configuration/ActiveX-Registration) -- the two settings in the language documentation
+- [Build and Clean](Build-and-Clean) -- the commands and their log
+- [What a build writes](Build-Output#what-each-project-type-produces) -- the file of each project type
diff --git a/docs/IDE/Building/Build-Configuration.md b/docs/IDE/Building/Build-Configuration.md
new file mode 100644
index 000000000..d829b7e60
--- /dev/null
+++ b/docs/IDE/Building/Build-Configuration.md
@@ -0,0 +1,87 @@
+---
+title: Build configuration
+parent: Building
+grand_parent: IDE
+nav_order: 2
+permalink: /tB/IDE/Building/Build-Configuration
+index:
+  - win32
+  - win64
+  - safe mode
+  - 32-bit
+  - 64-bit
+  - target architecture
+---
+
+# Build configuration
+
+The box to the left of the toolbar's build buttons chooses the architecture that the project is compiled for, and starts the compiler in safe mode. It is the only build setting that is a choice of the IDE and not a setting of the project.
+
+![The IDE toolbar with a project open but not running.](../Images/Toolbar_2.png){:width="1228" height="35"}
+
+The box is a drop-down list with three entries, and its tooltip is **Choose a build configuration**.
+
+**win32**
+: Compiles a 32-bit program.
+
+**win64**
+: Compiles a 64-bit program. See [Project Types](../../../Features/Project-Configuration/Project-Types#calling-a-standard-dll-from-vba-or-excel) for a DLL called from VBA or Excel.
+
+**safeMode**
+: Opens the project without the compiler. See [Safe mode](#safe-mode).
+
+
+
+There are no **Debug** and **Release** entries, and none for AnyCPU or ARM. The split between the program that runs in the IDE and the program that a build writes is made in [Project Settings](../Project-Settings), by two sets of options: [*Compiler Options (BUILD)*](../Project-Settings#compiler-options-build) for **Build** and [*Compiler Options (DEBUG)*](../Project-Settings#compiler-options-debug) for **Start**.
+
+## Changing the architecture
+
+Ctrl+F1 chooses **win64** and Ctrl+F2 chooses **win32**. They do nothing when the box already shows the entry. [Manage Keyboard Shortcuts](../Customising/Keyboard-Shortcuts) can change them.
+
+A change restarts the compiler as a program of the other architecture. The IDE does not ask first. A program that is running or paused is ended at once. While the new compiler loads the project, a box that says **Please wait...** covers the IDE, for about ten seconds in a small project. Then:
+
+- the **CALL STACK**, **VARIABLES**, **WATCHES** and **HISTORY** panels and the list of running programs are empty;
+- the **DIAGNOSTICS** rows are cleared;
+- every open form designer shows the [RESYNC](../Forms/Previewing#resync) treatment, as it does after any restart of the compiler.
+
+With no project open, nothing restarts: the box only sets what the next project will use.
+
+## What the choice changes
+
+- **The file.** A **win32** build writes a 32-bit file and a **win64** build a 64-bit one. `${Architecture}` in the [*Build Output Path*](../Project-Settings#build-output-path) is `win32` or `win64`, and `${Bitness}` is `32` or `64`. The default path puts `${Architecture}` in the file name, so the two builds sit side by side.
+- **The code.** `#If Win64` is true in **win64** and false in **win32**, and `LongPtr` is 8 bytes in **win64**. The editor treats a branch of an `#If Win64` block as active or greys it out to match. [Compiler Constants](../../../Reference/Compiler-Constants#appearance) shows the same code in both modes.
+- **The program that runs in the IDE.** **Start** (F5) and **Run Or Preview** (F6) run the project in the compiler process, so a **win64** run is a 64-bit program. See [Running](../Debugging/Running#what-start-runs).
+
+Two other settings of the project decide which compiler program runs it: [*Large Address Aware (LAA)*](../Project-Settings#large-address-aware-laa) and [*Data Execution Prevention Aware (DEP)*](../Project-Settings#data-execution-prevention-aware-dep). When the project's settings do not match the compiler that is running, the IDE starts a compiler that does, and loads the project in it. The built file follows the same two settings.
+
+## Where the choice is kept
+
+The IDE keeps the choice for each project, under the path of its `.twinproj` file, in its own settings (the `targetArchitectureMemory` value; see [where the IDE keeps its settings](../Customising/#where-the-ide-keeps-its-settings)). It is not in the project: the `Settings` file does not hold it, and a copy of the project on another machine starts with the box as that machine's IDE shows it.
+
+When you open a project from its file, from **Open Recent** or from the command line, the IDE sets the box to the remembered choice before the project loads. A project with no remembered choice leaves the box as it is. A project made from a template, a sample or a folder reads no choice, and a project imported from a VB6 project starts as **win32**. A project that was never saved has no path, so its choice lasts for the session.
+
+## Safe mode
+
+**safeMode** opens the project's files without starting the compiler's services. It gives access to the project's files when the compiler's services get in the way, for example when the compiler keeps crashing. After a project loads in safe mode, a box explains it, in these words: **SafeMode is designed to give you access to the file system of your project, without starting all of the associated compiler services. Use this mode to investigate / fix / recover your code before saving and restarting in normal mode.** Choose **OK**.
+
+
+
+In safe mode:
+
+- **Build** and **Clean** look available and do nothing. No line is written and no box opens.
+- The DIAGNOSTICS rows are not cleared, and the form designer's **RESYNC** button answers **Can't resync the form designer in Safe Mode**.
+- The choice is not remembered, and no remembered choice is read when a project opens.
+
+The IDE switches to safe mode on its own when it has restarted the compiler more than three times in 60 seconds. It says **Compiler crash loop detected.  Restarting in SAFE mode.**
+
+## Restart the compiler
+
+The toolbar button **Restart the compiler** and **Tools → Restart the compiler** end the compiler and start a new one, for the architecture the box shows. With Alt held, the button also restarts the process that caches LLVM code. [Running](../Debugging/Running#restarting-the-compiler) describes what a restart does to a running program.
+
+## See Also
+
+- [Toolbar](../Reference/Toolbar#build-configuration) -- the box among the other buttons
+- [Build and Clean](Build-and-Clean) -- the commands that use the choice
+- [What a build writes](Build-Output) -- the name of the file for each choice
+- [64-bit](../../../Features/64bit) -- writing code for both architectures
+- [Project Settings](../Project-Settings) -- *Compiler Options*, *Large Address Aware* and *Data Execution Prevention Aware*
diff --git a/docs/IDE/Building/Build-Output.md b/docs/IDE/Building/Build-Output.md
new file mode 100644
index 000000000..34f47be92
--- /dev/null
+++ b/docs/IDE/Building/Build-Output.md
@@ -0,0 +1,94 @@
+---
+title: What a build writes
+parent: Building
+grand_parent: IDE
+nav_order: 3
+permalink: /tB/IDE/Building/Build-Output
+index:
+  - build output path
+  - open build output folder
+  - auto-increment
+  - build type
+  - fusion host
+---
+
+# What a build writes
+
+A build writes one file, and the project's *Build Type* decides what kind. This page describes where the file goes and what it is called, how to open its folder, how the version numbers change, and the other file a build can write.
+
+## The build output path
+
+The [*Build Output Path*](../Project-Settings#build-output-path) setting is the full path of the file. It can use variables, and [Project Settings](../Project-Settings#build-output-path) lists them. A new project sets it to
+
+```
+${SourcePath}\Build\${ProjectName}_${Architecture}.${FileExtension}
+```
+
+a file in a `Build` folder beside the `.twinproj`, named for the project and for the architecture of the [build configuration](Build-Configuration). `${ProjectName}` is the *Project Name* setting, not the name of the `.twinproj` file. `${FileExtension}` follows the project's [type](#what-each-project-type-produces).
+
+A build makes the folders of the path when they are missing, nested ones too. The IDE does not check the text of the setting, so a mistake shows only in the build:
+
+- A path that ends in a backslash fails with `[LINKER] FAILED to create output file '' (error code 3)`. So does a path with forward slashes, when its `Build` folder does not exist yet; the folder is not made.
+- An unknown variable such as `${Nope}` prints `[COMPILER] build path contains unrecognized variable.` and builds nothing.
+- `${IdePath}`, which the samples that build an add-in use, is the folder the IDE is installed in.
+
+> [!WARNING] BETA 1010
+> `${ProjectID}` in the path is replaced with nothing. A path of `${SourcePath}\Build\${ProjectID}_x.exe` builds a file called `_x.exe`, with no message, and two projects that use the same pattern overwrite each other's file. Do not use `${ProjectID}`; name the file with `${ProjectName}`.
+
+The other variables, `${VersionMajor}`, `${VersionMinor}`, `${VersionBuild}`, `${VersionRevision}` and `${Bitness}`, give the numbers and the bitness as the settings and the box hold them.
+
+## Open Build Output Folder
+
+**Project → Open Build Output Folder...** opens the folder of the build output path in Windows Explorer. It opens the folder whether or not a file is in it. If the folder does not exist, Windows Explorer decides what to show.
+
+The IDE reads the path once, when the project loads or the compiler restarts, and keeps that copy. So the command does not follow the project after:
+
+- the first save of a project that had no file. It says **Project is not yet saved.  Please save the project first.**, even after you have saved and built;
+- a change of the *Build Output Path* in Project Settings. It opens the old folder.
+
+Close the project and open it again, or choose **Tools → Restart the compiler**, and the command opens the right folder. The same message shows when the path has an unknown variable.
+
+**Project → Open Project Folder...** opens the folder of the `.twinproj` file, and gives the same message for a project with no file. The **Open Folder** link in the console, which follows a successful build, opens the folder of the file just built.
+
+## Version numbers
+
+The *Major*, *Minor*, *Build* and *Revision* settings of the [version resource](../Project-Settings#version-resource) go into the built file. The *Auto-Increment* setting makes the build raise one of them by 1 after each successful build, and the console says so:
+
+```
+[BUILD] Auto-incremented next build number to: 1.0.8.3
+```
+
+The type library of an ActiveX DLL or control has its own pair of settings, [*Type Library Version*](../Project-Settings#type-library-version), and its own *Auto-Increment*. A build prints `[BUILD] Auto-incremented type-library version number to: ` for it.
+
+> [!WARNING] BETA 1010
+> The number that *Auto-Increment* raises is changed in the IDE's memory only. The project is not marked as changed, the `Settings` file is not written, and the **Version** row of the **PACKAGE PUBLISHING** pane stays as it was. **Close Project** asks nothing, and the increments are lost: the next session builds again with the old numbers, and a package is published with a number that was used already. Set the numbers by hand in Project Settings before you build a version that you release or publish, and save the project.
+
+## What each project type produces
+
+The *Build Type* setting is a list of five values. The file is the same kind for every project of a type, and the registration and the log depend on it too.
+
+| Build Type | File | After the build |
+|---|---|---|
+| **Standard EXE** | `.exe` | The links **Launch EXE** and **Open Folder**. |
+| **ActiveX DLL** | `.dll` | Registered, unless *Register DLL after build* is **No**. See [ActiveX registration](ActiveX-Registration). |
+| **ActiveX Control** | `.ocx` | The same. |
+| **Standard DLL** | `.dll` | Not registered. |
+| **Package TWINPACK** | `.twinpack` | No links. The file is a package; see [Creating a TWINPACK package](../../../Features/Packages/Creating-TWINPACK). |
+
+A console program, a Windows service and a kernel-mode driver are Standard EXE projects with other settings. A console program sets [*Is Console Application*](../Project-Settings#is-console-application). A driver sets [*Native Subsystem*](../Project-Settings#native-subsystem) and [*Override Entry Point*](../Project-Settings#override-entry-point). A service is a Standard EXE that uses the [WinServicesLib](../../Packages/WinServicesLib/) package. The New Project dialog has a template for the console program only. [Project Types](../../../Features/Project-Configuration/Project-Types) describes all of them, and the Standard DLL as well.
+
+When you change the *Build Type* and apply the change in Project Settings, the IDE saves the project and restarts the compiler, and the console prints `[COMPILER] Project settings changes require compiler-restart`. The **PACKAGE PUBLISHING** pane switches on for a package and dims for any other type. The next **Build** writes the new kind of file, and **Clean** removes only the file of the current path, so the old file stays.
+
+## The Fusion host
+
+A project that references an ActiveX control may need the host program of [Fusion](../../../Features/Fusion#automatic-fusion-host-exe-generation). The compiler builds it on its own when a project loads and a referenced control needs it. The host is a second file. Its path is the [*ActiveX Fusion Host EXE Output Path*](../Project-Settings#activex-fusion-host-exe-output-path) setting. When that is empty, the path is the build output path with `win32host` or `win64host` in place of `${Architecture}`.
+
+Ctrl+Shift+B builds the host on request, without building the project. The console prints `[BUILD] successfully built Fusion server file ''`, or `[BUILD] failed to build Fusion server file ''`. In a project without a Fusion reference, the key does nothing: no line and no file. The key has no menu item and no button. The command's id is `tbBuild_Fusion`.
+
+## See Also
+
+- [Project Settings](../Project-Settings) -- *Build Type*, *Build Output Path* and the version settings
+- [Build and Clean](Build-and-Clean) -- the commands
+- [Project Types](../../../Features/Project-Configuration/Project-Types) -- Standard DLLs, console programs, services and drivers
+- [Fusion](../../../Features/Fusion) -- the host program for ActiveX controls
+- [Project menu](../Reference/Menu/Project) -- **Open Build Output Folder...**
diff --git a/docs/IDE/Building/Build-and-Clean.md b/docs/IDE/Building/Build-and-Clean.md
new file mode 100644
index 000000000..dc4375ba3
--- /dev/null
+++ b/docs/IDE/Building/Build-and-Clean.md
@@ -0,0 +1,162 @@
+---
+title: Build and Clean
+parent: Building
+grand_parent: IDE
+nav_order: 1
+permalink: /tB/IDE/Building/Build-and-Clean
+index:
+  - build
+  - clean
+  - build log
+  - launch exe
+  - locked by
+---
+
+# Build and Clean
+
+**Build** compiles the project into a file. **Clean** deletes the file the last build made, and takes its registration out of the registry. This page describes both commands, what the **DEBUG CONSOLE** shows while they run, and what to do when they do not do what you expect.
+
+**File → Build** and the toolbar button **Build** are one command. So are **File → Clean** and the toolbar button **Clean (deregister & delete build)**. Both need a project that is open, and are greyed out until one is. Neither has a key.
+
+![The File menu with a project open.](../Projects/Images/Menu_File_Project.png){:width="190" height="337"}
+
+The IDE only sends the request. The compiler compiles, links, writes the file, registers a DLL and prints the log. The IDE shows the log in the **DEBUG CONSOLE**, shows the [progress box](LLVM-in-the-IDE#the-progress-box) when the compiler reports progress, and asks the two questions described below.
+
+## Build
+
+A build does these steps:
+
+1. **The licence check.** The compiler tells the IDE what licence it found when it starts. A **Build** pressed before that answer waits for it up to 5 seconds. If the answer does not come, the console prints **[BUILD] failed due to licence error.  Please try again.** and nothing is built. Press **Build** again.
+2. **The question for a project with no file.** See [A project with no file](#a-project-with-no-file).
+3. **The build.** The compiler compiles the project and writes the file that [*Build Output Path*](../Project-Settings#build-output-path) names. [What a build writes](Build-Output) describes the file, and [Build configuration](Build-Configuration) says what the box on the toolbar chooses.
+4. **The log.** Every step writes its lines to the console. [What the console shows](#what-the-console-shows) lists them.
+
+When the build is finished, the IDE does nothing more. It does not run the new file, open its folder or select it anywhere. The console's links are the way to do those.
+
+### Unsaved changes
+{: #unsaved-changes }
+
+A build does not save the project and does not ask you to. The compiler builds the project as the IDE holds it, so the file contains the edit you typed a second ago, saved or not. The `.twinproj` file is not touched. Press Ctrl+S to save as well.
+
+### A project with no file
+{: #a-project-with-no-file }
+
+A project made from a template, a sample or a folder, or imported from a VB6 project, has no `.twinproj` file until it is saved. **Build** and **Clean** both open a box titled **twinBASIC** for such a project, with the question **You must first save the project.  Save now?** and the buttons **Save** and **Cancel**.
+
+
+
+**Cancel** closes the box. Nothing is written, and the console prints nothing. **Save** opens the Windows **Save As** dialog, with the name of the project's folder proposed, such as `NewProject`.
+
+The build does not go on after the dialog. Press **Build** again. The folder you chose is now `${SourcePath}`, so the default build path puts the file in a `Build` folder in it.
+
+> [!NOTE] BETA 1010
+> **Save** in this box makes the file you choose the project's file, but writes no `.twinproj` there, and nothing is built. The project stays changed, and **File → Close Project** still asks **You've made changes to this project.  Save now?** Press Ctrl+S after the dialog to write the file.
+
+A project that you opened from a `.twinproj` file has its file, and builds with no question. [Saving and exporting](../Projects/Saving-and-Exporting#a-project-with-no-file) describes the project with no file.
+
+## What the console shows
+{: #what-the-console-shows }
+
+The **DEBUG CONSOLE** is the only log of a build. The IDE does not clear it when a build starts, so the lines follow whatever is there. It does not bring the console to the front either: if the panel is closed or hidden in your [panel layout](../Customising/Panel-Layouts), open it from the **View** menu first. With **Show Timestamps** on, each line starts with its time; see [Debug Console](../Debugging/Debug-Console#what-the-console-shows).
+
+A build that succeeds writes these lines:
+
+| Project | Lines |
+|---|---|
+| Standard EXE | `[BUILD] Starting...`, then `[LINKER] SUCCESS created output file ''`, then a line with the links **Launch EXE** and **Open Folder**. |
+| ActiveX DLL | The same, with only **Open Folder**, and then `[REGISTER] type-library registration completed. DllRegisterServer() returned OK`. |
+| ActiveX Control | As an ActiveX DLL. If the type library's *Auto-Increment* is on, `[BUILD] Auto-incremented type-library version number to: ` comes first. |
+| Standard DLL | As an ActiveX DLL, with no `[REGISTER]` line. |
+| Package TWINPACK | `[BUILD] Creating TWINPACK file ''`, then `[BUILD] successful.` There are no links. |
+
+
+
+The log has no summary line. A clean build ends with the line for the file, or with the links beneath it. A build with errors ends differently, as [When a build fails](#when-a-build-fails) shows.
+
+### The links
+
+The console turns some words in a build's lines into links.
+
+**Launch EXE**
+: Starts the built executable. The compiler starts it, and the console prints `[SHELL] process  started.` and, when it ends, `[SHELL] process  exited with code `.
+
+**Open Folder**
+: Opens the folder of the built file in Windows Explorer. A build of a DLL or a control has this link only.
+
+**Terminate**
+: Is red, and follows **LOCKED BY:** after a build that could not replace a file in use. It ends that program. See [The old file is in use](#the-old-file-is-in-use).
+
+## When a build fails
+{: #when-a-build-fails }
+
+### The compiler is not ready
+
+When the box on the toolbar is changed, a box that says **Please wait...** covers the IDE until the new compiler has the project. A press of **Build** in that time reaches nothing. Wait until the box closes.
+
+In [safe mode](Build-Configuration#safe-mode), **Build** and **Clean** look available and do nothing: the console prints no line and no box opens.
+
+### A program is running or paused
+
+While a program is running in the IDE, or paused at a breakpoint, the **Build** button stays available. A press prints `ERROR: main thread is busy.` in the console and builds nothing. The program is not affected. End the program first, then build.
+
+### An error in the code
+
+An error in a procedure that nothing calls does not stop the build, although the DIAGNOSTICS panel lists it: the file is written. An error in a procedure that **Sub Main** reaches stops the build:
+
+```
+[BUILD] Starting...
+[LINKER] compilation (codegen) error detected in 'MainMod.Mainrootmain' at line #1
+[LINKER] FAILED due to compilation errors ''
+[BUILD] failed
+```
+
+
+
+No box opens. The [DIAGNOSTICS](../Code/Diagnostics) panel is unchanged: it already listed the error. The failed build deletes the file the previous build made.
+
+Other failures write their own `[LINKER] FAILED ...` or `[BUILD] failed ...` line that names the file or the setting at fault. A *Build Output Path* that ends in a backslash fails with `[LINKER] FAILED to create output file '' (error code 3)`, and so does one with forward slashes when its `Build` folder does not exist yet. A path with a variable that is not one of the listed ones, such as `${Nope}`, prints `[COMPILER] build path contains unrecognized variable.` and builds nothing. [What a build writes](Build-Output#the-build-output-path) lists the variables.
+
+### The old file is in use
+{: #the-old-file-is-in-use }
+
+A build cannot replace a file that a running program holds open. The console says so, and names the program:
+
+```
+[LINKER] FAILED to create output file '' (error code 32)
+LOCKED BY:
+   Program_win32.exe (pid 4120) Terminate
+[BUILD] failed
+```
+
+The old file is kept. The red link **Terminate** ends the program, with no question, and the next **Build** succeeds. The same program stops **Clean** from deleting the file. The line is `[BUILD-CLEAN] failed to delete the previous build file.  Check the file is not in use or currently executing.`
+
+
+
+When the file is a DLL that is registered, a failed deregistration can open another question. See [ActiveX registration](ActiveX-Registration#delete-it-and-continue).
+
+## Clean
+
+**Clean** deletes the file the build path names, and takes the DLL's registration out of the registry when the file is an ActiveX DLL or control. It deletes that one file. The `Build` folder and any other file in it stay.
+
+Its log is short:
+
+```
+[BUILD] Starting...
+[UNREGISTER] previous type-library deregistration completed. DllUnregisterServer() returned OK
+[BUILD-CLEAN] build cleanup completed.
+```
+
+The `[UNREGISTER]` line comes only for an ActiveX build. **Clean** on a project that was never built prints the first and last lines, and so does a second **Clean**. After you change the *Build Type* setting, **Clean** removes the file of the current path only: the file of the old type stays.
+
+
+
+**Clean** also asks the **twinBASIC** question for a project with no file. See [A project with no file](#a-project-with-no-file).
+
+## See Also
+
+- [Build configuration](Build-Configuration) -- 32-bit, 64-bit and safe mode
+- [What a build writes](Build-Output) -- the file, its path and the version numbers
+- [ActiveX registration](ActiveX-Registration) -- what Build and Clean do to the registry
+- [Debug Console](../Debugging/Debug-Console) -- the panel that shows the log
+- [Diagnostics](../Code/Diagnostics) -- where the compile errors are listed
+- [File menu](../Reference/Menu/File#build) -- the menu entries
diff --git a/docs/IDE/Building/LLVM-in-the-IDE.md b/docs/IDE/Building/LLVM-in-the-IDE.md
new file mode 100644
index 000000000..6661ed446
--- /dev/null
+++ b/docs/IDE/Building/LLVM-in-the-IDE.md
@@ -0,0 +1,57 @@
+---
+title: LLVM in the IDE
+parent: Building
+grand_parent: IDE
+nav_order: 5
+permalink: /tB/IDE/Building/LLVM-in-the-IDE
+index:
+  - llvm
+  - compiling progress
+  - flush the llvm cache
+---
+
+# LLVM in the IDE
+
+This page describes what the IDE shows when a project is compiled with LLVM: where the options are, the licence banners, the progress box and the cache. What LLVM is, which options to choose and what the LLVM compiler does not support are in [Getting Started with LLVM](../../../LLVM/Getting-Started).
+
+## Where the options are
+
+The options are in two sections of [Project Settings](../Project-Settings): [*Compiler Options (BUILD)*](../Project-Settings#compiler-options-build) and [*Compiler Options (DEBUG)*](../Project-Settings#compiler-options-debug). Each has a tick box, **Enable LLVM Compilation**, two for optimization and one for each CPU feature.
+
+- **Build** compiles with the *BUILD* options. The file that it writes is the one compiled with LLVM.
+- **Start** (F5) runs the project with the *DEBUG* options. The IDE cannot debug code that LLVM compiled, so the description of this section advises against turning LLVM on there.
+
+The IDE has no **Build with LLVM** button. Both sections are empty in a new project.
+
+The IDE Options has three more, which belong to the compiler and its cache and are not part of any project: the number of threads, the threshold for reporting a complex procedure, and whether the cache process stays alive after the IDE closes. A change to any of them restarts the compiler. See [Compiler and LLVM](../Customising/IDE-Options#compiler-and-llvm).
+
+## The licence banners
+
+LLVM compilation depends on the licence. When the settings dialog is drawn, each of the two sections shows a banner next to the licence badge, if the licence limits it:
+
+- With a **Personal** licence, the banner says that the optimizations apply only to the built-in packages (VB, VBA, VBRUN) and not to your own code, and advises an upgrade to a Professional licence.
+- With a **Community** licence, the banner says that the optimizations are not applied, and advises an upgrade to a paid licence.
+- With a **Professional** or **Ultimate** licence there is no banner.
+
+The banner follows the licence badge at the moment the dialog opens. [Getting Started with LLVM](../../../LLVM/Getting-Started#availability) says what each edition gets.
+
+## The progress box
+{: #the-progress-box }
+
+When the compiler reports progress, the IDE opens a box titled **Compiling...** with a bar,, and the compiler can also fill an item of the status bar with the percentage, such as **LLVM: n%**. The box is modal: the IDE cannot be used until it closes, and it closes by itself when the build ends. Which builds report progress is the compiler's choice. A small project compiled with LLVM takes a fraction of a second, so the box is gone almost at once.
+
+
+
+> [!NOTE] BETA 1010
+> If another box opens while **Compiling...** is on screen, the end of the build closes that box instead of the progress box, so the other box is lost. The **Compiling...** box stays, with no button, and the IDE cannot be used. Restart the IDE.
+
+## The cache
+
+The compiler keeps a cache of LLVM code, in a process of its own, so that a later build is faster. **Tools → Flush the LLVM compiler cache** ends that process and restarts the compiler from the project in memory, as the console says with `restarting from MEMORY`. The next build compiles everything again and takes longer. **Restart the compiler**, with Alt held, restarts the cache process too.
+
+## See Also
+
+- [Getting Started with LLVM](../../../LLVM/Getting-Started) -- turning LLVM on, the options and what the LLVM compiler does not support
+- [Project Settings](../Project-Settings#compiler-options-build) -- the *Compiler Options* sections
+- [IDE Options](../Customising/IDE-Options#compiler-and-llvm) -- the three LLVM options of the IDE
+- [Build and Clean](Build-and-Clean) -- the commands that compile
diff --git a/docs/IDE/Building/Package-Publishing.md b/docs/IDE/Building/Package-Publishing.md
new file mode 100644
index 000000000..6c4176c65
--- /dev/null
+++ b/docs/IDE/Building/Package-Publishing.md
@@ -0,0 +1,119 @@
+---
+title: Package Publishing
+parent: Building
+grand_parent: IDE
+nav_order: 6
+permalink: /tB/IDE/Building/Package-Publishing
+redirect_from:
+  - /tB/IDE/Project/PackagePublishing
+index:
+  - publish a package
+  - twinserv
+  - publisher account
+  - sign in
+---
+
+# Package Publishing
+
+The **PACKAGE PUBLISHING** pane shows the details of a project that builds a package, takes you to the settings that hold them, and publishes the package to TWINSERV, the package database. This page describes the pane, the publisher account and what the publish button does.
+
+A package that you build for your own use does not need the pane: **Build** writes a `.twinpack` file, as [Build output](Build-Output#what-each-project-type-produces) describes. [Creating a TWINPACK package](../../../Features/Packages/Creating-TWINPACK) is the tutorial, from the sample project to the finished package, and describes the contents of a package.
+
+## The pane
+
+**View → PACKAGE PUBLISHING** shows the pane. It is a panel like the others, so you can dock it, float it or close it. See [Panel Layouts](../Customising/Panel-Layouts).
+
+![The Package Publishing pane for a package project.](../Images/PackagePublishing_1.png){:width="300" height="330"}
+
+The pane has six rows, each with a label and a value. Five of the rows come from the project's settings, and the link at the right of each opens the setting.
+
+| Row | Shows | The setting |
+|---|---|---|
+| **Publisher** | Your publisher name, once you have signed in. The link is **SIGN IN**, or **SIGN OUT** after you have. | None. See [The publisher account](#the-publisher-account). |
+| **Namespace** | The project's name. | [*Project Name*](../Project-Settings#project-name) |
+| **Description** | The project's description. | [*Project Description*](../Project-Settings#project-description) |
+| **Licence** | The package's licence. | [*Licence Type*](../Project-Settings#licence-type) |
+| **Visibility** | **PRIVATE** or **PUBLIC**. A **PUBLIC** package can be downloaded by everyone. | [*Package Visibility*](../Project-Settings#package-visibility) |
+| **Version** | The four numbers, such as `1.0.0.0`. | The four numbers of the [*VERSION Resource*](../Project-Settings#version-resource) |
+
+Under the rows are a grey line, **(changes not effective until published)**, which is always there, and the button **PUBLISH THIS PACKAGE**.
+
+### Only for a package project
+
+The pane fills only for a project whose *Build Type* is **Package TWINPACK**. For any other project, and when no project is open, the pane stays where it is, dimmed and with empty values, and does not react to the mouse.
+
+![The Package Publishing pane for any other project, dimmed.](../Images/PackagePublishing.png){:width="300" height="268"}
+
+Changing the *Build Type* of a project in [Project Settings](../Project-Settings#build-type) switches the pane on or off, once the compiler has restarted.
+
+### Editing a value
+
+An **EDIT** link opens [Project Settings](../Project-Settings), with the filter box already holding the id of the setting: `project.name`, `project.description`, `project.licence`, `project.packageVisibility` or `project.version`. The filter for **Version** is a text filter, so it lists every setting whose id contains `project.version`: the four numbers, *Auto-Increment* and the version texts.
+
+Change the value and press **Apply Changes**. The pane shows the new values once the compiler has them, and the console prints `[COMPILER] Project settings updated`. A change that needs the compiler to restart prints `[COMPILER] Project settings changes require compiler-restart`, and the IDE saves the project and restarts.
+
+## The publisher account
+
+TWINSERV accepts a package only from a publisher that has an account. The account belongs to the package server, not to the IDE: the IDE has no setting for the server, no other sign-in provider, and no way to reset a password or change the e-mail address.
+
+**SIGN IN** opens a box titled **twinBASIC Publisher Login / Signup**. The terms and conditions are beside two forms, which two tabs choose, **LOGIN** and **REGISTER**. The box has one button, **Cancel**. It opens on **LOGIN**, with the password box focused, when the IDE knows a publisher name, and on **REGISTER** when it does not.
+
+
+
+
+
+**To register**, fill in **USERNAME** (at least 4 characters, visible to everyone), **EMAIL** (used for account recovery only), **PASSWORD** (at least 8 characters), tick **I agree to the terms & conditions**, and press **SIGN UP**. The IDE checks these on your machine first, in this order, and shows the first fault as **ERROR:** under the form, with the field focused: **Username must be at least 4 characters long**, **invalid email address**, **Password must be at least 8 characters long**, **You must agree to the terms and conditions**. The server can still refuse the name (**Username already taken.**). When the account is made, a box says **Publisher account created successfully.  You will now be logged in.**
+
+**To log in**, fill in **USERNAME** and **PASSWORD** and press **LOG IN**. A wrong name or password shows **Publisher login FAILED.  Username not found.** or **Publisher login FAILED.  Incorrect password.** When the server cannot be reached, the form says that the HTTPS request failed. After a login, the link in the pane reads **SIGN OUT** and the **Publisher** row shows your name. **SIGN OUT** forgets the login, and keeps the saved name.
+
+The login is valid for the run of the IDE and no longer, and it can end sooner, when TWINSERV ends the session. The next publish then opens the sign-in box again. Each start of the IDE needs a new login.
+
+### What the IDE saves
+
+The IDE saves the publisher name. It saves the password only when **Remember password on this device** is ticked. The next time the box opens, it fills in the form.
+
+The password is stored in the IDE's settings in the registry with a simple obfuscation, which is not encryption. Anyone who can read your registry can recover it. Leave the box unticked on a machine that others use.
+
+> [!NOTE] BETA 1010
+> A remembered password that contains `~` comes back wrong: the form is filled in with a different password, and the login fails. Do not tick **Remember password on this device** for such a password, and type it each time.
+
+> [!NOTE] BETA 1010
+> A saved publisher name that contains a single quote, such as `O'Brien`, is cut at the quote in the pre-filled form (**USERNAME** shows `O`). A remembered password with a quote is cut in the same way. Type the whole name and password into the boxes.
+
+## Publishing
+
+**PUBLISH THIS PACKAGE** uploads the package to TWINSERV. It builds the package in memory for the purpose: no `.twinpack` file is written, and no build is needed first.
+
+1. **The login.** If you are not logged in, the sign-in box opens and the publish stops. After you log in, press **PUBLISH THIS PACKAGE** again; the publish does not resume.
+2. **The project.** The compiler builds the package, and a project whose [*Project ID*](../Project-Settings#project-id) is all zeros is refused with **ERROR: Please first edit the Settings, giving the project a proper name and description**.
+3. **The confirmation.** A box lists what is about to be published and asks **Do you want to proceed?**
+4. **The upload.** **Continue** starts it.
+
+The confirmation reads **IMPORTANT: Please check the following information before publishing to the online database:** and then lists **PUBLISHER**, **PACKAGE NAMESPACE**, **PACKAGE DESCRIPTION**, **PACKAGE VERSION** and **PACKAGE LICENCE**. For a package with the visibility **PUBLIC**, the box starts with **WARNING: publishing a PUBLIC package will allow everyone to download the package and see the sourcecode within it.** Its buttons are **Continue**, which is the default, and **Cancel**.
+
+
+
+**Continue** prints `[PUBLISH] attempting to publish project '' to the online database...` in the console. The result is not in the console: a box with **OK** shows it.
+
+
+
+| Result | The box says |
+|---|---|
+| Uploaded | **[PUBLISH] successful.  Your package should appear in the package manager database shortly.** |
+| The version is on TWINSERV already | **[PUBLISH] FAILED.  Package already uploaded with this exact version number.** Raise the version and publish again. |
+| The ID is another publisher's | **[PUBLISH] FAILED.  Package already registered to another publisher (change your Project ID setting).** |
+| The login has ended | The sign-in box opens again. |
+| Anything else | **[PUBLISH] FAILED.  Server returned HTTPS code** and the number. |
+
+> [!NOTE] BETA 1010
+> After you press **OK** in a box that says **[PUBLISH] FAILED.  Server returned HTTPS code**, the IDE raises a script error (`t is not a function`) that only the IDE's developer tools show. Nothing else changes, and the failure is as the box said: check the number, then publish again.
+
+The IDE has no command that removes or overwrites a published package. A version that has been uploaded stays on TWINSERV, and the next upload needs a higher version.
+
+## See Also
+
+- [Creating a TWINPACK package](../../../Features/Packages/Creating-TWINPACK) -- making a package, its files, and the two ways to finish it
+- [Importing a package from TWINSERV](../../../Features/Packages/Importing-TWINSERV) -- using a published package in another project
+- [Project Settings](../Project-Settings) -- *Build Type*, *Package Visibility* and the other settings of a package
+- [Build output](Build-Output) -- the `.twinpack` file that **Build** writes
+- [View menu](../Reference/Menu/View) -- the entry that shows the pane
diff --git a/docs/IDE/Building/index.md b/docs/IDE/Building/index.md
new file mode 100644
index 000000000..016d72286
--- /dev/null
+++ b/docs/IDE/Building/index.md
@@ -0,0 +1,55 @@
+---
+title: Building
+parent: IDE
+nav_order: 7
+permalink: /tB/IDE/Building/
+has_toc: false
+index_also:
+  - build
+index:
+  - make an exe
+  - compile to a file
+---
+
+# Building
+
+A build turns the project into a file: an executable, a DLL, an ActiveX control or a package. The IDE does not compile anything itself. It asks the compiler to build, and shows the compiler's log in the **DEBUG CONSOLE**.
+
+- [**Build and Clean**](Build-and-Clean) --- the two commands, the question for a project with no file, what the console prints, and what to do when a build fails or cannot replace the old file.
+- [**Build configuration**](Build-Configuration) --- the box on the toolbar: 32-bit, 64-bit and safe mode, and what a change does.
+- [**What a build writes**](Build-Output) --- the file, its path and name, the version numbers, the Fusion host, the **Open Build Output Folder** command, and what each project type produces.
+- [**ActiveX registration**](ActiveX-Registration) --- how an ActiveX DLL or control is registered after a build, the warning for the machine-wide registry, and how Clean takes it away.
+- [**LLVM in the IDE**](LLVM-in-the-IDE) --- the progress box, the licence banners, and the cache.
+- [**Package Publishing**](Package-Publishing) --- the pane that sets a package's details, and the button that publishes it.
+
+The commands are listed by menu in the [File menu](../Reference/Menu/File#build) and [Project menu](../Reference/Menu/Project) reference pages, and as buttons in the [Toolbar](../Reference/Toolbar).
+
+## What the IDE offers
+
+The IDE has two build commands, **Build** and **Clean**. They are in the **File** menu and on the toolbar, and a project must be open. Neither has a key.
+
+| Command | Where | Key |
+|---|---|---|
+| **Build** | **File → Build**; the toolbar button **Build** | none |
+| **Clean** | **File → Clean**; the toolbar button **Clean (deregister & delete build)** | none |
+| Build configuration | The box to the left of the toolbar's build buttons | Ctrl+F1 chooses **win64**, Ctrl+F2 chooses **win32** |
+| **Restart the compiler** | The toolbar button; **Tools → Restart the compiler** | none |
+| Build the Fusion host | No menu item and no button | Ctrl+Shift+B |
+
+[Manage Keyboard Shortcuts](../Customising/Keyboard-Shortcuts) can give **Build** and **Clean** keys of their own.
+
+The IDE has no **Rebuild**, no **Build All** and no choice between an incremental and a full build. It has no **Debug** and **Release** configurations: the only configuration is the architecture, and the pair of *Compiler Options* in [Project Settings](../Project-Settings#compiler-options-build) is the nearest thing to the split. It has no setting for signing the built file, and no menu command or key that runs the built file: the console's **Launch EXE** link does. A signing tool is run from a procedure with the [**RunAfterBuild**](../../Core/Attributes#runafterbuild) attribute, and [**App.LastBuildPath**](../../Packages/AppGlobalClassObject/_App/LastBuildPath) says where the file is.
+
+The build log is the **DEBUG CONSOLE**. The IDE writes no log file, and the console ends a build with a line for the file, not with a summary such as "Build succeeded".
+
+## What a build does not do
+
+- **It does not save.** The compiler builds the project as the IDE holds it, with every edit you have made, saved or not. The `.twinproj` file is not written, and the open editors are not saved. [Build and Clean](Build-and-Clean#unsaved-changes) has the details.
+- **It does not run the program.** **Start** (F5) runs the project inside the compiler, without a built file. See [Running](../Debugging/Running#what-start-runs). To run the built file, use the **Launch EXE** link that the console shows after the build, or open the file's folder and start it as any other program. A file that you start from Windows Explorer has no **PROCESS** box in the status bar, which lists only programs that your program starts. See [Running](../Debugging/Running#programs-that-your-program-starts).
+
+## See Also
+
+- [Project Settings](../Project-Settings) -- *Build Type*, *Build Output Path*, the version numbers and the other settings of a build
+- [Project Types](../../../Features/Project-Configuration/Project-Types) -- Standard DLLs, console programs, services and drivers
+- [Running and debugging](../Debugging/) -- running a project in the IDE
+- [Toolbar](../Reference/Toolbar) -- the buttons of the toolbar
diff --git a/docs/IDE/Forms/Designer.md b/docs/IDE/Forms/Designer.md
index 997534fc8..cc90d4a47 100644
--- a/docs/IDE/Forms/Designer.md
+++ b/docs/IDE/Forms/Designer.md
@@ -174,7 +174,7 @@ With a designer focused, the **View** menu shows **Zoom In** and **Zoom Out** in
 
 ![The zoom box of the toolbar showing 50 %, with its list open below it and 50 % highlighted between 25 % and 75 %; the list scrolls on to further values.](Images/Designer_Zoom.png){:width="240" height="119"}
 
-
+![frmMain in the designer at 50 %, drawn at half size with its grid hidden; the readout under it still gives 380 x 270.](Images/Designer_Zoom50.png){:width="196" height="172"}
 
 
 > [!NOTE] BETA 1010
diff --git a/docs/IDE/Forms/Images/Designer_Zoom50.png b/docs/IDE/Forms/Images/Designer_Zoom50.png
new file mode 100644
index 000000000..7950a11cf
Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_Zoom50.png differ
diff --git a/docs/IDE/Forms/Images/Preview_Window.png b/docs/IDE/Forms/Images/Preview_Window.png
index 451b4c2a0..5e1a08ed6 100644
Binary files a/docs/IDE/Forms/Images/Preview_Window.png and b/docs/IDE/Forms/Images/Preview_Window.png differ
diff --git a/docs/IDE/Forms/Previewing.md b/docs/IDE/Forms/Previewing.md
index 4bf29b22a..6611f8ab9 100644
--- a/docs/IDE/Forms/Previewing.md
+++ b/docs/IDE/Forms/Previewing.md
@@ -23,9 +23,7 @@ Click **PREVIEW** in the toolbar, titled *Launch the form in isolation to test i
 - Preview works for a form and for a report. For a user control or a property page, the button is greyed out and F6 does nothing.
 - F6 in the form's code also previews the form.
 
-![The previewed form as a window of its own, titled frmMain, with its File menu in the menu bar and the controls laid out as in the designer.](Images/Preview_Window.png){:width="381" height="320"}
-
-
+![The previewed form as a window of its own, titled frmMain, with its File menu open over the top left corner and the other controls laid out as in the designer.](Images/Preview_Window.png){:width="381" height="320"}
 
 
 A preview is a debug session. `Form_Load` runs, a breakpoint in it stops the run, a click on a button runs its `Click` handler, and closing the window runs `Form_Unload` and `Form_Terminate` and writes `[DEBUGGER] done.` to the **DEBUG CONSOLE**. [Run Or Preview](../Debugging/Running#run-or-preview) describes what F6 does in each place, and [Breakpoints and stepping](../Debugging/Breakpoints-and-Stepping) what you can do while it is stopped.
diff --git a/docs/IDE/Menu/File.md b/docs/IDE/Menu/File.md
index 556f2e9de..41972ea87 100644
--- a/docs/IDE/Menu/File.md
+++ b/docs/IDE/Menu/File.md
@@ -50,7 +50,9 @@ The commands from **Close Project** to **Clean** work on the open project, and a
 {: #build }
 
 **Build**
-: Compiles the project into the file that the [*Build Output Path*](../../Project-Settings#build-output-path) setting names. The project templates set it to `${SourcePath}\Build\${ProjectName}_${Architecture}.${FileExtension}`, a file in a `Build` folder beside the `.twinproj`. `${Architecture}` is `win32` or `win64`, as chosen in the [build configuration](../Toolbar#build-configuration) box on the toolbar. It has no key.
+: Compiles the project into the file that the [*Build Output Path*](../../Project-Settings#build-output-path) setting names, for the architecture that the [build configuration](../Toolbar#build-configuration) box on the toolbar shows. It saves nothing, and runs nothing. For a project with no file, it first asks for one. It has no key. See [Build and Clean](../../Building/Build-and-Clean) and [What a build writes](../../Building/Build-Output).
 
 **Clean**
-: Deregisters the previously built DLL, if there is one, and deletes the previous build file. It has no key.
+: Deregisters the previously built DLL, if there is one, and deletes the previous build file. It has no key. See [Clean](../../Building/Build-and-Clean#clean) and [ActiveX registration](../../Building/ActiveX-Registration).
+
+The [Building](../../Building/) chapter describes both, with the log that the **DEBUG CONSOLE** shows.
diff --git a/docs/IDE/Menu/Project.md b/docs/IDE/Menu/Project.md
index dfdec5dfd..1962d9afb 100644
--- a/docs/IDE/Menu/Project.md
+++ b/docs/IDE/Menu/Project.md
@@ -36,6 +36,6 @@ Every command needs an open project, and is greyed out until one is.
 : Opens the folder of the project's `.twinproj` file in Windows Explorer. For a project that has no file yet, it shows the notification **Project is not yet saved.  Please save the project first.** It has no key.
 
 **Open Build Output Folder...**
-: Opens the folder of the file that the [Build Output Path](../../Project-Settings#build-output-path) names. It does not create the folder, and it gives the same notification when the project has no file or no build output path. It has no key.
+: Opens the folder of the file that the [Build Output Path](../../Project-Settings#build-output-path) names. It does not create the folder. It gives the same notification when the project has no file, and goes on giving it after the first save, and opens the old folder after a change of the path, until the project is opened again or the compiler restarts. It has no key. See [Open Build Output Folder](../../Building/Build-Output#open-build-output-folder).
 
 See [Open a folder](../../Projects/Saving-and-Exporting#open-a-folder).
diff --git a/docs/IDE/Menu/Tools.md b/docs/IDE/Menu/Tools.md
index f19f577d2..edf19f9cb 100644
--- a/docs/IDE/Menu/Tools.md
+++ b/docs/IDE/Menu/Tools.md
@@ -13,5 +13,5 @@ redirect_from:
 ![The Tools menu open with no project loaded: IDE Options beside a sliders icon, then, below a separator, the greyed-out Restart the compiler and Flush the LLVM compiler cache.](Images/Menu_Tools.png){:width="217" height="113"}
 
 - **IDE Options...** --- opens the **IDE Options** dialog, which holds the IDE's 31 options. The command has no default key. [IDE Options](../../Customising/IDE-Options) describes the dialog, with its picture, and every option.
-- **Restart the compiler** --- ends the compiler and starts a new one, which compiles the project again and loads the add-ins afresh.
-- **Flush the LLVM compiler cache** --- clears the cache of code compiled with [LLVM](../../../../LLVM/Getting-Started), so that the next build compiles everything.
+- **Restart the compiler** --- ends the compiler and starts a new one, which compiles the project again and loads the add-ins afresh. See [Restart the compiler](../../Building/Build-Configuration#restart-the-compiler).
+- **Flush the LLVM compiler cache** --- clears the cache of code compiled with [LLVM](../../../../LLVM/Getting-Started), so that the next build compiles everything. See [The cache](../../Building/LLVM-in-the-IDE#the-cache).
diff --git a/docs/IDE/Menu/View.md b/docs/IDE/Menu/View.md
index 534aee6cb..f05d92e6c 100644
--- a/docs/IDE/Menu/View.md
+++ b/docs/IDE/Menu/View.md
@@ -64,7 +64,7 @@ The last seventeen items show a panel. A panel that is in no layout in use comes
 : The search panel. See [Finding](../../Code/Finding).
 
 **PACKAGE PUBLISHING**
-: The details of a project built as a package. See [Package Publishing](../../Project/PackagePublishing).
+: The details of a project built as a package, and the button that publishes it. See [Package Publishing](../../Building/Package-Publishing).
 
 **TOOLBAR**
 : The row of buttons under the menu. See [Toolbar](../Toolbar).
diff --git a/docs/IDE/Package Publishing.md b/docs/IDE/Package Publishing.md
deleted file mode 100644
index 30bac9aa2..000000000
--- a/docs/IDE/Package Publishing.md	
+++ /dev/null
@@ -1,20 +0,0 @@
----
-title: Package Publishing
-parent: IDE
-# nav_order: 
-permalink: /tB/IDE/Project/PackagePublishing
----
-
-# Package Publishing
-
-The Package Publishing pane manages the metadata for the current project when it is published as a twinBASIC package, including the package name, version, and description.
-
-When a project isn't open this will be empty.
-
-![The Package Publishing pane with every value blank. Six labelled rows read Publisher, Namespace, Description, Licence, Visibility and Version, each with an Edit link on the right except Publisher, which offers Sign In. A note says changes are not effective until published, above a greyed out Publish This Package button.](Images/PackagePublishing.png){:width="300" height="268"}
-
-Once you open a project whose build type is Package TWINPACK, the pane shows its properties and you can edit them. For any other build type the values stay blank.
-
-![The Package Publishing pane with a package project open and its values filled in: Namespace SampleProject, Description SampleProject description, Licence MIT, Visibility PRIVATE and Version 1.0.0.0. Publisher still offers Sign In, and the Publish This Package button is now enabled.](Images/PackagePublishing_1.png){:width="300" height="330"}
-
-Click the "EDIT" and this will open the [Project Settings](../Project-Settings)
diff --git a/docs/IDE/Project Settings.md b/docs/IDE/Project Settings.md
index d99ab94f4..023523081 100644
--- a/docs/IDE/Project Settings.md	
+++ b/docs/IDE/Project Settings.md	
@@ -72,7 +72,7 @@ When set to **Yes**, the type library that the build produces takes the [Project
 
 ## Build Output Path
 
-The full path of the file the compiler creates. Its description in the dialog lists these variables: `${SourcePath}`, the folder that holds the `.twinproj` file, and `${ProjectName}`, `${ProjectID}`, `${FileExtension}`, `${Bitness}`, `${Architecture}`, `${VersionMajor}`, `${VersionMinor}`, `${VersionBuild}` and `${VersionRevision}`. `${Architecture}` is `win32` or `win64`, whichever the toolbar's [build configuration](Reference/Toolbar#build-configuration) box is set to, and `${Bitness}` is the matching `32` or `64`, so `${ProjectName}_${Bitness}.${FileExtension}` names `MathGreetLib_32.dll` or `MathGreetLib_64.dll`. The IDE add-in samples use one more that the description leaves out, `${IdePath}`, the folder the IDE is installed in: their `${IdePath}\addins\${Architecture}\${ProjectName}.${FileExtension}` builds straight into the IDE's own `addins` folder, which works until the IDE has loaded the add-in --- see [Rebuilding an addin the IDE has loaded](../Packages/tbIDE/#rebuilding-an-addin-the-ide-has-loaded).
+The full path of the file the compiler creates. Its description in the dialog lists these variables: `${SourcePath}`, the folder that holds the `.twinproj` file, and `${ProjectName}`, `${ProjectID}` (see the [warning about it](Building/Build-Output#the-build-output-path)), `${FileExtension}`, `${Bitness}`, `${Architecture}`, `${VersionMajor}`, `${VersionMinor}`, `${VersionBuild}` and `${VersionRevision}`. `${Architecture}` is `win32` or `win64`, whichever the toolbar's [build configuration](Reference/Toolbar#build-configuration) box is set to, and `${Bitness}` is the matching `32` or `64`, so `${ProjectName}_${Bitness}.${FileExtension}` names `MathGreetLib_32.dll` or `MathGreetLib_64.dll`. The IDE add-in samples use one more that the description leaves out, `${IdePath}`, the folder the IDE is installed in: their `${IdePath}\addins\${Architecture}\${ProjectName}.${FileExtension}` builds straight into the IDE's own `addins` folder, which works until the IDE has loaded the add-in --- see [Rebuilding an addin the IDE has loaded](../Packages/tbIDE/#rebuilding-an-addin-the-ide-has-loaded).
 
 Every project template sets it to `${SourcePath}\Build\${ProjectName}_${Architecture}.${FileExtension}`: a `Build` folder beside the `.twinproj` file, and a different file name for each architecture. A Standard DLL project named `MathGreetLib` builds `Build\MathGreetLib_win32.dll` or `Build\MathGreetLib_win64.dll`, and a `Declare` that calls it has to name that file --- see [Calling a Standard DLL from VBA or Excel](../../Features/Project-Configuration/Project-Types#calling-a-standard-dll-from-vba-or-excel). In the `Settings` file it is `project.buildPath`.
 
@@ -148,11 +148,11 @@ The part of the type library's version that goes up by 1 after each successful b
 For an **ActiveX DLL** or **ActiveX Control** build. When set to **Yes**, the DLL's `DllRegisterServer` and `DllUnregisterServer` register it under `HKEY_LOCAL_MACHINE` instead of `HKEY_CURRENT_USER`, as VB6 DLLs do. It is **No** by default. In the `Settings` file it is `project.dllRegisterLocalMachine`.
 
 > [!IMPORTANT]
-> Registering under `HKEY_LOCAL_MACHINE` needs administrator rights, so with this setting at **Yes**, the IDE usually has to be started with **Run as administrator**.
+> Registering under `HKEY_LOCAL_MACHINE` needs administrator rights, so with this setting at **Yes**, the IDE usually has to be started with **Run as administrator**. An IDE that is not elevated shows a [warning box](Building/ActiveX-Registration#the-warning-at-the-machine-wide-registry) when it opens such a project.
 
 ## Register DLL after build
 
-For an **ActiveX DLL** or **ActiveX Control** build. When set to **Yes**, the IDE registers the DLL as soon as the build finishes. It is **Yes** by default. **No** leaves the registration to the developer. In the `Settings` file it is `project.dllRegisterAfterBuild`.
+For an **ActiveX DLL** or **ActiveX Control** build. When set to **Yes**, the compiler registers the DLL as soon as the build finishes. It is **Yes** by default. **No** leaves the registration to the developer. In the `Settings` file it is `project.dllRegisterAfterBuild`.
 
 ## COM Initialization
 
diff --git a/docs/IDE/Projects/Images/Export_Console.light.png b/docs/IDE/Projects/Images/Export_Console.light.png
new file mode 100644
index 000000000..d97585335
Binary files /dev/null and b/docs/IDE/Projects/Images/Export_Console.light.png differ
diff --git a/docs/IDE/Projects/Images/Export_Console.light.svg b/docs/IDE/Projects/Images/Export_Console.light.svg
new file mode 100644
index 000000000..3070c2b22
--- /dev/null
+++ b/docs/IDE/Projects/Images/Export_Console.light.svg
@@ -0,0 +1 @@
+DEBUG CONSOLE>16:41:58.204[EXPORT] exporting current project to "\\?\C:\tbshots\Export\"...16:41:58.204[EXPORT] COMPLETED (68 folders, 479 files)
diff --git a/docs/IDE/Projects/Images/Export_Console.png b/docs/IDE/Projects/Images/Export_Console.png
new file mode 100644
index 000000000..dcec18682
Binary files /dev/null and b/docs/IDE/Projects/Images/Export_Console.png differ
diff --git a/docs/IDE/Projects/Images/Export_Console.svg b/docs/IDE/Projects/Images/Export_Console.svg
new file mode 100644
index 000000000..2140fabf6
--- /dev/null
+++ b/docs/IDE/Projects/Images/Export_Console.svg
@@ -0,0 +1 @@
+DEBUG CONSOLE>16:41:58.204[EXPORT] exporting current project to "\\?\C:\tbshots\Export\"...16:41:58.204[EXPORT] COMPLETED (68 folders, 479 files)
diff --git a/docs/IDE/Projects/Saving-and-Exporting.md b/docs/IDE/Projects/Saving-and-Exporting.md
index 63dce343d..19bbea730 100644
--- a/docs/IDE/Projects/Saving-and-Exporting.md
+++ b/docs/IDE/Projects/Saving-and-Exporting.md
@@ -63,7 +63,7 @@ Some commands save the project besides **Save Project**: **Save Project As** sav
 
 A project made from a template or a sample, from **Import from folder...**, or from a VB6 project has no file until it is saved. The title bar says `Unsaved.twinproj`. **Save Project** asks for a file name first, with the **Save As** dialog, and then saves. An import that saves the project asks for a file name in the same way.
 
-**Open Project Folder...** and **Open Build Output Folder...** of the **Project** menu show the notification **Project is not yet saved.  Please save the project first.** for such a project.
+**Open Project Folder...** and **Open Build Output Folder...** of the **Project** menu show the notification **Project is not yet saved.  Please save the project first.** for such a project. **Build** and **Clean** ask you to save first; see [A project with no file](../Building/Build-and-Clean#a-project-with-no-file).
 
 ## What the IDE remembers about a project
 
@@ -100,7 +100,7 @@ A project whose file is gone shows the box **Failed to load the project (error c
 
 ## Open a folder
 
-**Project → Open Project Folder...** opens the folder of the `.twinproj` file in Windows Explorer. **Project → Open Build Output Folder...** opens the folder of the file that the [Build Output Path](../Project-Settings#build-output-path) names. The command does not create the folder. With no build output path set, it gives the same notification as for a project with no file.
+**Project → Open Project Folder...** opens the folder of the `.twinproj` file in Windows Explorer. **Project → Open Build Output Folder...** opens the folder of the file that the [Build Output Path](../Project-Settings#build-output-path) names. The command does not create the folder. With no build output path set, it gives the same notification as for a project with no file. [Open Build Output Folder](../Building/Build-Output#open-build-output-folder) says when it still gives it after the project is saved.
 
 ## Export Project
 
@@ -157,7 +157,10 @@ The tB executable's `import` cannot pack a folder while any folder remains under
 
 Each export writes a first line, `[EXPORT] exporting current project to "…"`, and a last line: `[EXPORT] COMPLETED (72 folders, 478 files)`, or the failure lines above. With [*Export Verbose*](../Project-Settings#export-verbose) set, it also writes a line for each file and folder it deletes, `[EXPORT]  DELETED: …`, and for each file it writes, `[EXPORT]  DONE: …`. If the compiler answers the IDE's request with an error code, a box reads **ERROR: exportProject failed.** and the code.
 
-
+> [!NOTE] BETA 1010
+> Each `DONE` line shows `\` for every backslash of its path: `[EXPORT]  DONE: \Sources\Startup.twin` for the file `Sources\Startup.twin` of the export folder. The files are written correctly, and the other `[EXPORT]` lines show their backslashes.
+
+![The DEBUG CONSOLE, floating, after Export Project with Export Verbose off: the line that starts the export and names the export folder, and the COMPLETED line with the numbers of folders and files.](Images/Export_Console.png){:width="600" height="132"}
 
 The `export` command of the tB executable and of the import/export script deletes nothing. See [What both programs do](../../../Features/Packages/Import-Export-Tool#what-both-programs-do).
 
diff --git a/docs/IDE/Toolbar.md b/docs/IDE/Toolbar.md
index c6b2d7598..ab6f4b704 100644
--- a/docs/IDE/Toolbar.md
+++ b/docs/IDE/Toolbar.md
@@ -29,10 +29,10 @@ redirect_from:
 - Step Over (Shift+F8 / F10) -- [Step commands](../Debugging/Breakpoints-and-Stepping#step-commands)
 - Step Into (F8 / F11) -- [Step commands](../Debugging/Breakpoints-and-Stepping#step-commands)
 - Step Out (Ctrl+Shift+F8 / Shift+F11) -- [Step commands](../Debugging/Breakpoints-and-Stepping#step-commands)
-- [Choose a build configuration](#build-configuration)
-- Restart the compiler
-- Clean (Deregister & delete build)
-- Build
+- [Choose a build configuration](#build-configuration) -- [Build configuration](../Building/Build-Configuration)
+- Restart the compiler -- [Restart the compiler](../Building/Build-Configuration#restart-the-compiler)
+- Clean (deregister & delete build) -- [Clean](../Building/Build-and-Clean#clean)
+- Build -- [Build](../Building/Build-and-Clean#build)
 - Comment Selection (Ctrl+K)
 - Uncomment Selection (Ctrl+Shift+K)
 - Indent Block (Ctrl+])
@@ -64,8 +64,8 @@ The build configuration box chooses whether the project is compiled as 32-bit or
 
 The IDE remembers the choice for each project, and sets the box back to it when the project is opened again. It keeps the choice in its own settings, under the path of the `.twinproj` file, not in the project. A project with no remembered choice uses whatever the box shows, and a newly started IDE shows **win32**.
 
-The box also sets the `Win64` compiler constant --- 1 in **win64**, 0 in **win32** --- and so decides which branch of an `#If Win64` block the editor treats as active and which it greys out. [Compiler Constants](../../../Reference/Compiler-Constants#appearance) shows the same code in both modes.
+Every change restarts the compiler as a program of the chosen architecture, with no question, and a program that is running or paused is ended. The box also sets the `Win64` compiler constant --- 1 in **win64**, 0 in **win32** --- and so decides which branch of an `#If Win64` block the editor treats as active and which it greys out. [Compiler Constants](../../../Reference/Compiler-Constants#appearance) shows the same code in both modes.
 
 The default [Build Output Path](../Project-Settings#build-output-path) puts `win32` or `win64` in the file name, so the two builds do not overwrite each other.
 
-The box has a third entry, **safeMode**. It opens the project's files without starting the compiler services, and the IDE describes it as a way to investigate, fix or recover code before saving it and restarting in normal mode. The IDE also switches to **safeMode** by itself when the compiler keeps crashing, and says *Compiler crash loop detected. Restarting in SAFE mode.*
+The box has a third entry, **safeMode**. It opens the project's files without starting the compiler services, and the IDE switches to it by itself when the compiler keeps crashing. [Build configuration](../Building/Build-Configuration) describes the three entries, what a change does and what safe mode does.
diff --git a/docs/LLVM/Getting-Started.md b/docs/LLVM/Getting-Started.md
index 5d5d3af3d..cac44c16a 100644
--- a/docs/LLVM/Getting-Started.md
+++ b/docs/LLVM/Getting-Started.md
@@ -39,7 +39,7 @@ The remaining options, from **LLVM: Target CPUs with AES** to **LLVM: Target CPU
 > [!IMPORTANT]
 > If you tick a CPU feature that the CPU running your program does not have, the program crashes or does not run at all.
 
-When LLVM is on, a progress dialog appears when you build or run your program showing the completion percentage of the build process. For large applications, and for applications with very large individual procedures, compiling with LLVM can take much longer than the standard compiler. A very large application can take several minutes or more to compile, depending on your hardware.
+When LLVM is on and the compiler reports its progress, the IDE shows a **Compiling...** box with the completion percentage; see [LLVM in the IDE](../tB/IDE/Building/LLVM-in-the-IDE#the-progress-box). For large applications, and for applications with very large individual procedures, compiling with LLVM can take much longer than the standard compiler. A very large application can take several minutes or more to compile, depending on your hardware.
 
 twinBASIC keeps a cache of compiled code, so later builds are usually much faster: code that has not changed is not compiled again. To clear the cache and compile everything on the next build, choose **Flush the LLVM compiler cache** on the [**Tools** menu](../tB/IDE/Reference/Menu/Tools).
 
diff --git a/docs/Reference/Built-In/tbIDE/Project.md b/docs/Reference/Built-In/tbIDE/Project.md
index b65dc02fd..c511f3ecb 100644
--- a/docs/Reference/Built-In/tbIDE/Project.md
+++ b/docs/Reference/Built-In/tbIDE/Project.md
@@ -86,7 +86,7 @@ The four components of the project's current version (`Major.Minor.Build.Revisio
 ### Build
 {: .no_toc }
 
-Builds the project. Equivalent to the IDE's File → Make/Build… command. Writes the output to [**BuildOutputPath**](#buildoutputpath); errors raised during compilation appear in the DEBUG CONSOLE the same way they would for a user-initiated build.
+Builds the project. Equivalent to the IDE's **File → Build** command; see [Build and Clean](../../../tB/IDE/Building/Build-and-Clean). Writes the output to [**BuildOutputPath**](#buildoutputpath); compile errors are listed in the DIAGNOSTICS panel, and the DEBUG CONSOLE gets the log lines of the build, the same as for a user-initiated build.
 
 Syntax: *project*.**Build**
 
diff --git a/docs/Reference/Contributed.md b/docs/Reference/Contributed.md
index fda89630d..9df3f400c 100644
--- a/docs/Reference/Contributed.md
+++ b/docs/Reference/Contributed.md
@@ -13,6 +13,7 @@ Community-authored packages published through TWINSERV, the twinBASIC package re
 Each package is versioned apart from twinBASIC, and its section says which version it documents. Packages that are not yet ready for use in a program are listed under [Experimental Packages](../Experimental/).
 
 - [**AutoCompleteLib**](AutoCompleteLib/) -- Windows shell auto-complete for text-input controls
+- [**DetoursLib**](DetoursLib/) -- detours of functions in the program's own process, made with SlimDetours
 - [**FileProps**](FileProps/) -- the Windows shell properties of a file, read and written
 - [**Fmt**](Fmt/) -- **StringFormat**, a string built from a format string and values, in the style of C#'s **String.Format**
 - [**Generics**](Generics/) -- tuples, read-only objects that hold one to seven values of declared types
diff --git a/docs/Reference/Contributed/DetoursLib/Detours/CreateDetourAtDLLEntry.md b/docs/Reference/Contributed/DetoursLib/Detours/CreateDetourAtDLLEntry.md
new file mode 100644
index 000000000..dc67e0ed0
--- /dev/null
+++ b/docs/Reference/Contributed/DetoursLib/Detours/CreateDetourAtDLLEntry.md
@@ -0,0 +1,62 @@
+---
+title: CreateDetourAtDLLEntry
+parent: Detours
+permalink: /tB/Packages/Contributed/DetoursLib/Detours/CreateDetourAtDLLEntry
+has_toc: false
+---
+
+# CreateDetourAtDLLEntry
+{: .no_toc }
+
+Detours a function of a loaded DLL, found by the names of the DLL and the function, and returns the address that calls the original function.
+
+Syntax: **CreateDetourAtDLLEntry** ( *libName*, *funcName*, *newAddress* ) **As LongPtr**
+
+*libName*
+: *required* A **String**, the name of a DLL that the process has already loaded, such as `"user32"` or `"user32.dll"`. A name without an extension means a `.dll` file.
+
+*funcName*
+: *required* A **String**, the name under which the DLL exports the function, such as `"SetFocus"` or `"MessageBoxW"`. The name is case-sensitive.
+
+*newAddress*
+: *required* A **LongPtr**, the address of the detour, usually **AddressOf** and the name of a procedure in a module.
+
+
+Returns the address of the trampoline, which calls the original function. It is not the function's own address: a call to the function goes to the detour, and a call through the returned address runs the original. Keep the address for as long as the detour is in place, convert it to a [**Delegate**](../../../../Core/Delegate) type to call the original, and pass it to [**RemoveDetour**](RemoveDetour) to remove the detour.
+
+
+The function finds the DLL with the **GetModuleHandle** API and the function with **GetProcAddress**, then places the detour with [**SlimDetoursInlineHook**](../DetoursMod/SlimDetoursInlineHook), as [**CreateDetourAtFuncAddress**](CreateDetourAtFuncAddress) does. It does not load the DLL.
+
+
+> [!NOTE] 1.0.0.0
+> The DLL must already be loaded in the process, or the function raises error 5, `CreateDetourAtDLLEntry libName: DLL library not found`. To detour a function of a DLL that may not be loaded yet, load the DLL first with the **LoadLibrary** API.
+
+[Errors](.#errors) lists the errors the function raises.
+
+## Example
+
+This module detours **SetFocus** of `user32` so that the program can see which window each call gives the focus to.
+
+```tb check_build
+Private Delegate Function SetFocusFn (ByVal hWnd As LongPtr) As LongPtr
+Private RealSetFocus As SetFocusFn
+
+Private Function MySetFocus(ByVal hWnd As LongPtr) As LongPtr
+    Debug.Print "SetFocus &H"; Hex(hWnd)
+    Return RealSetFocus(hWnd)
+End Function
+
+Public Sub WatchFocus()
+    RealSetFocus = CType(Of SetFocusFn)(CreateDetourAtDLLEntry("user32", "SetFocus", AddressOf MySetFocus))
+End Sub
+
+Public Sub StopWatchingFocus()
+    RemoveDetour RealSetFocus, AddressOf MySetFocus
+End Sub
+```
+
+## See Also
+
+- [CreateDetourAtFuncAddress](CreateDetourAtFuncAddress) function -- detours the function at an address
+- [RemoveDetour](RemoveDetour) procedure
+- [Writing a detour](../#writing-a-detour)
diff --git a/docs/Reference/Contributed/DetoursLib/Detours/CreateDetourAtFuncAddress.md b/docs/Reference/Contributed/DetoursLib/Detours/CreateDetourAtFuncAddress.md
new file mode 100644
index 000000000..263bf45d8
--- /dev/null
+++ b/docs/Reference/Contributed/DetoursLib/Detours/CreateDetourAtFuncAddress.md
@@ -0,0 +1,56 @@
+---
+title: CreateDetourAtFuncAddress
+parent: Detours
+permalink: /tB/Packages/Contributed/DetoursLib/Detours/CreateDetourAtFuncAddress
+has_toc: false
+---
+
+# CreateDetourAtFuncAddress
+{: .no_toc }
+
+Detours the function at an address, and returns the address that calls the original function.
+
+Syntax: **CreateDetourAtFuncAddress** ( *procAddress*, *newAddress* ) **As LongPtr**
+
+*procAddress*
+: *required* A **LongPtr**, the address of the function's code in the process, such as an address that the **GetProcAddress** API returns.
+
+*newAddress*
+: *required* A **LongPtr**, the address of the detour, usually **AddressOf** and the name of a procedure in a module. It must not be 0: unlike [**CreateDetourAtDLLEntry**](CreateDetourAtDLLEntry), this function does not check it, and passes it to SlimDetours as it is.
+
+
+Returns the address of the trampoline, which calls the original function. It is not *procAddress*: after the call, a call to *procAddress* goes to the detour, and a call through the returned address runs the original. Keep the address for as long as the detour is in place, and pass it to [**RemoveDetour**](RemoveDetour) to remove the detour.
+
+
+The function places the detour with [**SlimDetoursInlineHook**](../DetoursMod/SlimDetoursInlineHook). It suits a function that **CreateDetourAtDLLEntry** cannot find by name: one that a DLL exports only by ordinal, a function of a DLL that the program loads with the **LoadLibrary** API and finds with **GetProcAddress** itself, or a method of a COM object, whose address is in the object's table of methods. [Errors](.#errors) lists the errors the function raises.
+
+## Example
+
+This module detours **Sleep** of `kernel32`, found by its address, so that every wait in the process is a tenth as long.
+
+```tb check_build
+Private DeclareWide PtrSafe Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleW" (ByVal lpModuleName As String) As LongPtr
+Private Declare PtrSafe Function GetProcAddress Lib "kernel32" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr
+
+Private Delegate Sub SleepFn (ByVal dwMilliseconds As Long)
+Private RealSleep As SleepFn
+
+Private Sub MySleep(ByVal dwMilliseconds As Long)
+    RealSleep(dwMilliseconds \ 10)
+End Sub
+
+Public Sub StartDetour()
+    Dim target As LongPtr = GetProcAddress(GetModuleHandle("kernel32"), "Sleep")
+    RealSleep = CType(Of SleepFn)(CreateDetourAtFuncAddress(target, AddressOf MySleep))
+End Sub
+
+Public Sub StopDetour()
+    RemoveDetour RealSleep, AddressOf MySleep
+End Sub
+```
+
+## See Also
+
+- [CreateDetourAtDLLEntry](CreateDetourAtDLLEntry) function -- detours a function found by its name
+- [RemoveDetour](RemoveDetour) procedure
+- [Writing a detour](../#writing-a-detour)
diff --git a/docs/Reference/Contributed/DetoursLib/Detours/RemoveDetour.md b/docs/Reference/Contributed/DetoursLib/Detours/RemoveDetour.md
new file mode 100644
index 000000000..a538dac9d
--- /dev/null
+++ b/docs/Reference/Contributed/DetoursLib/Detours/RemoveDetour.md
@@ -0,0 +1,56 @@
+---
+title: RemoveDetour
+parent: Detours
+permalink: /tB/Packages/Contributed/DetoursLib/Detours/RemoveDetour
+has_toc: false
+---
+
+# RemoveDetour
+{: .no_toc }
+
+Removes a detour and puts the original function back.
+
+Syntax: **RemoveDetour** *patchedAddress*, *newAddress*
+
+*patchedAddress*
+: *required* A **LongPtr**, the address that [**CreateDetourAtDLLEntry**](CreateDetourAtDLLEntry) or [**CreateDetourAtFuncAddress**](CreateDetourAtFuncAddress) returned when the detour was placed: the trampoline, not the function's own address. A variable of a **Delegate** type that holds it can be passed as it is.
+
+*newAddress*
+: *required* A **LongPtr**, the address of the detour, the same one that was passed when the detour was placed.
+
+
+After the call, a call to the function runs the original code again and no longer reaches the detour. The trampoline is freed, so *patchedAddress* must not be called again, and a variable that holds it should be cleared. The function can then be detoured again.
+
+
+
+A *newAddress* other than the detour in place raises error &HD0000173, `RemoveDetour SlimDetoursInlineHook failed: D0000173`, and leaves the detour in place. An argument that is 0 raises error 5. [Errors](.#errors) lists the descriptions.
+
+
+*patchedAddress* is passed **ByVal**, so the procedure cannot give back the function's own address, which SlimDetours stores in its argument when it removes the detour. A program that needs that address after the detour is removed keeps its own copy, or calls [**SlimDetoursInlineHook**](../DetoursMod/SlimDetoursInlineHook) directly.
+
+## Example
+
+```tb check_build
+Private Declare PtrSafe Function MulDiv Lib "kernel32" (ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long
+Private Delegate Function MulDivFn (ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long
+Private RealMulDiv As LongPtr
+
+Private Function MyMulDiv(ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long
+    Dim original As MulDivFn = CType(Of MulDivFn)(RealMulDiv)
+    Return original(nNumber, nNumerator, nDenominator) + 1
+End Function
+
+Public Sub Demo()
+    RealMulDiv = CreateDetourAtDLLEntry("kernel32", "MulDiv", AddressOf MyMulDiv)
+    Debug.Print MulDiv(6, 4, 3)     ' 9, from MyMulDiv
+    RemoveDetour RealMulDiv, AddressOf MyMulDiv
+    RealMulDiv = 0
+    Debug.Print MulDiv(6, 4, 3)     ' 8, from the original
+End Sub
+```
+
+## See Also
+
+- [CreateDetourAtDLLEntry](CreateDetourAtDLLEntry) function
+- [CreateDetourAtFuncAddress](CreateDetourAtFuncAddress) function
+- [Ending the program](../#ending-the-program) -- why every detour is removed before the program ends
diff --git a/docs/Reference/Contributed/DetoursLib/Detours/index.md b/docs/Reference/Contributed/DetoursLib/Detours/index.md
new file mode 100644
index 000000000..3bb5d761f
--- /dev/null
+++ b/docs/Reference/Contributed/DetoursLib/Detours/index.md
@@ -0,0 +1,74 @@
+---
+title: Detours
+parent: DetoursLib Package
+permalink: /tB/Packages/Contributed/DetoursLib/Detours/
+has_toc: false
+---
+
+# Detours module
+{: .no_toc }
+
+Places a detour on a function of the program's process, and removes it.
+
+The module has three procedures and no state of its own. A detour is placed with [**CreateDetourAtDLLEntry**](CreateDetourAtDLLEntry), which finds the function by the names of its DLL and of the function, or with [**CreateDetourAtFuncAddress**](CreateDetourAtFuncAddress), which takes the function's address. Both return the address that calls the original function, its *trampoline*. [**RemoveDetour**](RemoveDetour) takes that address and the detour's, and puts the function back.
+
+
+The trampoline holds a copy of the instructions that the detour replaced, followed by a jump to the rest of the function, so a call through it runs the original function. The program keeps it for as long as the detour is in place: the detour calls the original through it, and **RemoveDetour** needs it. After **RemoveDetour** the trampoline is freed, and the address must not be called again. A function whose detour was removed can be detoured again, with the same detour or another.
+
+```tb check_build
+Private Declare PtrSafe Function LoadLibrary Lib "kernel32" Alias "LoadLibraryW" (ByVal lpLibFileName As LongPtr) As LongPtr
+Private Declare PtrSafe Function GetProcAddress Lib "kernel32" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr
+
+Private Delegate Function GetFileVersionInfoSizeFn (ByVal lptstrFilename As LongPtr, ByRef lpdwHandle As Long) As Long
+
+Private RealSize As LongPtr
+Private Calls As Long
+
+Private Function MySize(ByVal lptstrFilename As LongPtr, ByRef lpdwHandle As Long) As Long
+    Calls = Calls + 1
+    Dim original As GetFileVersionInfoSizeFn = CType(Of GetFileVersionInfoSizeFn)(RealSize)
+    Return original(lptstrFilename, lpdwHandle)
+End Function
+
+Public Sub CountVersionQueries()
+    ' Find the function by its address, in a DLL loaded here.
+    Dim hVersion As LongPtr = LoadLibrary(StrPtr("version.dll"))
+    Dim target As LongPtr = GetProcAddress(hVersion, "GetFileVersionInfoSizeW")
+    RealSize = CreateDetourAtFuncAddress(target, AddressOf MySize)
+
+    ' ... code that reads version information ...
+
+    RemoveDetour RealSize, AddressOf MySize
+    RealSize = 0
+    Debug.Print "GetFileVersionInfoSizeW was called"; Calls; "times"
+End Sub
+```
+
+
+## Errors
+
+
+Each procedure checks its arguments first, and raises error 5 with one of these descriptions:
+
+| Procedure | Description | When |
+|---|---|---|
+| **CreateDetourAtDLLEntry** | `CreateDetourAtDLLEntry newAddress: bad function address` | *newAddress* is 0 |
+| **CreateDetourAtDLLEntry** | `CreateDetourAtDLLEntry libName: DLL library not found` | no DLL of that name is loaded in the process |
+| **CreateDetourAtDLLEntry** | `CreateDetourAtDLLEntry funcName: function name not found in DLL library` | the DLL exports no function of that name |
+| **CreateDetourAtFuncAddress** | `CreateDetourAtFuncAddress origAddress: bad function address` | *procAddress* is 0 |
+| **RemoveDetour** | `RemoveDetour patchedAddress: bad pointer` | *patchedAddress* is 0 |
+| **RemoveDetour** | `RemoveDetour newAddress: bad pointer` | *newAddress* is 0 |
+
+When SlimDetours refuses the change, the procedure raises the **HRESULT** that [**SlimDetoursInlineHook**](../DetoursMod/SlimDetoursInlineHook) returned, a negative number, as the error number. The description is the procedure's name, `SlimDetoursInlineHook failed: ` and the number in hexadecimal: **RemoveDetour** given a detour other than the one in place raises &HD0000173 with the description `RemoveDetour SlimDetoursInlineHook failed: D0000173`. A refused call leaves the function as it was.
+
+
+## Procedures
+
+- [CreateDetourAtDLLEntry](CreateDetourAtDLLEntry) -- detours a function of a loaded DLL, found by its name
+- [CreateDetourAtFuncAddress](CreateDetourAtFuncAddress) -- detours the function at an address
+- [RemoveDetour](RemoveDetour) -- removes a detour and puts the function back
+
+## See Also
+
+- [DetoursMod](../DetoursMod/) module -- the SlimDetours function these procedures call
+- [DetoursLib Package](../) -- writing a detour, and the package's example
diff --git a/docs/Reference/Contributed/DetoursLib/DetoursMod/SlimDetoursInlineHook.md b/docs/Reference/Contributed/DetoursLib/DetoursMod/SlimDetoursInlineHook.md
new file mode 100644
index 000000000..bf933c607
--- /dev/null
+++ b/docs/Reference/Contributed/DetoursLib/DetoursMod/SlimDetoursInlineHook.md
@@ -0,0 +1,63 @@
+---
+title: SlimDetoursInlineHook
+parent: DetoursMod
+permalink: /tB/Packages/Contributed/DetoursLib/DetoursMod/SlimDetoursInlineHook
+has_toc: false
+---
+
+# SlimDetoursInlineHook
+{: .no_toc }
+
+Places or removes one detour, and returns an **HRESULT**.
+
+Syntax: **SlimDetoursInlineHook** ( *bEnable*, *ppPointer*, *pDetour* ) **As Long**
+
+*bEnable*
+: *required* A **Long**: 1 to place the detour, 0 to remove it.
+
+*ppPointer*
+: *required* A **LongPtr** variable, passed **ByRef**. To place a detour, it holds the address of the function's code; to remove one, the trampoline that placing it stored there. The function changes it when it succeeds.
+
+*pDetour*
+: *required* A **LongPtr**, the address of the detour.
+
+
+Returns an **HRESULT** that SlimDetours makes from a Windows **NTSTATUS** code. Success is not 0: it is &H10000000, an **HRESULT** with only its facility bit for **NTSTATUS** codes set. A failure is a negative number, such as &HD0000173 for a removal whose *pDetour* is not the detour in place. So test the result with `>= 0` for success and `< 0` for failure, as the [**Detours**](../Detours/) procedures do.
+
+When it places a detour, the function stores the trampoline, the address that calls the original function, in *ppPointer*. When it removes one, it frees the trampoline and stores the address of the function's code in *ppPointer* again. When it fails, it leaves *ppPointer* and the function as they were.
+
+
+SlimDetours changes the code with the process's other threads suspended, and moves a thread that was in the middle of the replaced instructions, so the change is safe while other threads run.
+
+## Example
+
+```tb check_build
+Private Declare PtrSafe Function MulDiv Lib "kernel32" (ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long
+Private DeclareWide PtrSafe Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleW" (ByVal lpModuleName As String) As LongPtr
+Private Declare PtrSafe Function GetProcAddress Lib "kernel32" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr
+Private Delegate Function MulDivFn (ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long
+
+Private pMulDiv As LongPtr
+
+Private Function MyMulDiv(ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long
+    Dim original As MulDivFn = CType(Of MulDivFn)(pMulDiv)
+    Return original(nNumber, nNumerator, nDenominator) + 1
+End Function
+
+Public Sub Demo()
+    pMulDiv = GetProcAddress(GetModuleHandle("kernel32"), "MulDiv")
+    Dim hr As Long = SlimDetoursInlineHook(1, pMulDiv, AddressOf MyMulDiv)
+    If hr < 0 Then
+        Debug.Print "Not placed: &H"; Hex(hr)
+        Exit Sub
+    End If
+    Debug.Print MulDiv(6, 4, 3)     ' 9, from MyMulDiv
+    hr = SlimDetoursInlineHook(0, pMulDiv, AddressOf MyMulDiv)
+    Debug.Print MulDiv(6, 4, 3)     ' 8, from the original
+End Sub
+```
+
+## See Also
+
+- [CreateDetourAtFuncAddress](../Detours/CreateDetourAtFuncAddress) function -- the same, with its result turned into an error
+- [RemoveDetour](../Detours/RemoveDetour) procedure
diff --git a/docs/Reference/Contributed/DetoursLib/DetoursMod/index.md b/docs/Reference/Contributed/DetoursLib/DetoursMod/index.md
new file mode 100644
index 000000000..325bd5a9e
--- /dev/null
+++ b/docs/Reference/Contributed/DetoursLib/DetoursMod/index.md
@@ -0,0 +1,24 @@
+---
+title: DetoursMod
+parent: DetoursLib Package
+permalink: /tB/Packages/Contributed/DetoursLib/DetoursMod/
+has_toc: false
+---
+
+# DetoursMod module
+{: .no_toc }
+
+Declares the SlimDetours function that the package calls.
+
+The module holds one **Declare** statement, [**SlimDetoursInlineHook**](SlimDetoursInlineHook), for the function of the SlimDetours library that places or removes one detour. The library is linked into the program with [**Import Library**](../../../../../Features/Advanced/Static-Linking): `KNSoft.SlimDetours_X64.lib` in a 64-bit build and `KNSoft.SlimDetours_X86.lib` in a 32-bit one, which are files of the package. The statement is **Public**, so a program can call the function directly.
+
+The procedures of the [**Detours**](../Detours/) module call it, check its result and raise an error when it fails. A program that calls it directly checks the result itself.
+
+## Functions
+
+- [SlimDetoursInlineHook](SlimDetoursInlineHook) -- places or removes one detour, and returns an **HRESULT**
+
+## See Also
+
+- [Detours](../Detours/) module
+- [SlimDetours on GitHub](https://github.com/KNSoft/KNSoft.SlimDetours) -- the library's source and documentation
diff --git a/docs/Reference/Contributed/DetoursLib/index.md b/docs/Reference/Contributed/DetoursLib/index.md
new file mode 100644
index 000000000..6eeee8a46
--- /dev/null
+++ b/docs/Reference/Contributed/DetoursLib/index.md
@@ -0,0 +1,94 @@
+---
+title: DetoursLib Package
+parent: Contributed Packages
+has_toc: false
+exclude_from_docs:
+  - DetoursTest
+indexed_from: 1.0.0.0
+permalink: /tB/Packages/Contributed/DetoursLib/
+---
+
+# DetoursLib Package
+{: .no_toc }
+
+**Author:** WaynePhillipsEA. **Version:** 1.0.0.0. **On GitHub:** [KNSoft/KNSoft.SlimDetours](https://github.com/KNSoft/KNSoft.SlimDetours), the C library that the package wraps.
+
+The **DetoursLib** contributed package detours a function of the program's own process: it replaces the first instructions of the function's code with a jump to a twinBASIC procedure, the *detour*, so that every call of the function goes to the detour instead. The detour can call the original function through an address the package returns, and can change the arguments, the result, or both. A detour of a Windows API function receives the calls the program makes through its own **Declare** statements, and the calls that other code in the process makes, a DLL's included. It does not affect any other process.
+
+The package wraps **SlimDetours**, a C library based on Microsoft Detours, which is linked into the program with [**Import Library**](../../../../Features/Advanced/Static-Linking), so the program needs no DLL beside it. It works in 32-bit and 64-bit programs. Its source stops the compile with an error on a twinBASIC build older than BETA 1006. This section documents version 1.0.0.0, published under the MIT licence.
+
+The package has two public modules:
+
+- [**Detours**](Detours/) places and removes a detour: [**CreateDetourAtDLLEntry**](Detours/CreateDetourAtDLLEntry) detours a function of a loaded DLL, named by the DLL and the function, [**CreateDetourAtFuncAddress**](Detours/CreateDetourAtFuncAddress) detours the function at an address, and [**RemoveDetour**](Detours/RemoveDetour) puts the function back.
+- [**DetoursMod**](DetoursMod/) declares [**SlimDetoursInlineHook**](DetoursMod/SlimDetoursInlineHook), the SlimDetours function that the three procedures call.
+
+A third public module, **DetoursTest**, is the package's own demonstration. Its one public procedure, **TestDetours**, detours the **SetFocus** API of `user32`, calls it, removes the detour and calls it again, and shows each result in a message box. It is not part of the package's interface and is not documented further.
+
+
+This module detours **MulDiv** of `kernel32` so that a zero divisor gives 0 instead of -1, and passes every other call to the original function:
+
+```tb check_build
+Private Declare PtrSafe Function MulDiv Lib "kernel32" (ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long
+
+' A delegate type with the signature of the function that is detoured.
+Private Delegate Function MulDivFn (ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long
+
+' The address that calls the original MulDiv, while the detour is in place.
+Private RealMulDiv As MulDivFn
+
+' The detour: the same parameters and result as MulDiv.
+Private Function MyMulDiv(ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long
+    If nDenominator = 0 Then Return 0
+    Return RealMulDiv(nNumber, nNumerator, nDenominator)
+End Function
+
+Public Sub Demo()
+    RealMulDiv = CType(Of MulDivFn)(CreateDetourAtDLLEntry("kernel32", "MulDiv", AddressOf MyMulDiv))
+    Debug.Print MulDiv(10, 3, 0)     ' 0, from MyMulDiv
+    Debug.Print MulDiv(10, 3, 2)     ' 15, from the original, called by MyMulDiv
+    RemoveDetour RealMulDiv, AddressOf MyMulDiv
+    Debug.Print MulDiv(10, 3, 0)     ' -1, from the original
+End Sub
+```
+
+
+## Writing a detour
+
+A detour is a procedure in a module, passed with [**AddressOf**](../../../Core/AddressOf). It must take the same parameters, in the same order and of the same sizes, and return the same type as the function it replaces, because it receives the calls that were made to that function.
+
+To call the original function, declare a [**Delegate**](../../../Core/Delegate) type with the same signature, and convert the address that **CreateDetourAtDLLEntry** or **CreateDetourAtFuncAddress** returns to it with [**CType**](../../../Modules/Conversion/CType). Calling the original from the detour is optional: a detour that does not call it replaces the function completely.
+
+
+A delegate passes a **String** argument as a pointer to UTF-16 text, as a **DeclareWide** statement does. That is right for the `W` functions of the Windows API, but an ANSI function, one whose name ends in `A`, reads the text as single bytes. To call the original of an ANSI function, declare its string parameters **As LongPtr** in the delegate, and pass the address of ANSI text.
+
+
+A detour that calls the original and then does more work can change the thread's last error, which the caller of the function may read with **GetLastError**. Such a detour reads the last error with **GetLastError** after the original returns, and sets it again with **SetLastError** before it returns itself.
+
+## Errors
+
+
+The three procedures of [**Detours**](Detours/) raise run-time error 5, *Invalid procedure call or argument*, for an argument that is 0 or names nothing, with a description that names the procedure and the argument. The one argument they do not check is the detour's address passed to **CreateDetourAtFuncAddress**. When SlimDetours itself refuses to place or remove a detour, they raise its result as the error number, an **HRESULT** with its top bit set, and the description is the procedure's name, `SlimDetoursInlineHook failed: ` and the number in hexadecimal. Either way the function is left as it was. [**Detours**](Detours/#errors) lists the errors.
+
+
+## Ending the program
+
+Remove every detour with **RemoveDetour** before the program ends, or before the code of the detour is unloaded. A detour still in place jumps to the detour's code for as long as the process runs. A program run in the IDE runs in the IDE's own process, so while a detour is in place it also receives the calls that the IDE makes to that function.
+
+## Modules
+
+- [Detours](Detours/) -- places a detour on a function and removes it
+  - [CreateDetourAtDLLEntry](Detours/CreateDetourAtDLLEntry) -- detours a function of a loaded DLL, found by its name
+  - [CreateDetourAtFuncAddress](Detours/CreateDetourAtFuncAddress) -- detours the function at an address
+  - [RemoveDetour](Detours/RemoveDetour) -- removes a detour and puts the function back
+- [DetoursMod](DetoursMod/) -- the declaration of the SlimDetours function that the package calls
+  - [SlimDetoursInlineHook](DetoursMod/SlimDetoursInlineHook) -- places or removes one detour, and returns an **HRESULT**
+
+## See Also
+
+- [Delegate Types](../../../../Features/Language/Delegates) -- calling a function through its address
+- [Static Linking](../../../../Features/Advanced/Static-Linking) -- how the package links SlimDetours into the program
+- [Declare](../../../Core/Declare) statement
+
+## Test cases
+
+The notes and warnings of this section are checked by the test cases in [DetoursLib-tests.twinproj](DetoursLib-tests.twinproj), a twinBASIC project. It uses DetoursLib as a [linked package](../../../../Features/Packages/Linked#manual-management): put the package's `.twinpack` in `%APPDATA%\twinBASIC\packages`, then run the project, and each case prints whether it passes.
diff --git a/docs/Reference/Data-Types.md b/docs/Reference/Data-Types.md
index be2d88a91..c32650f9b 100644
--- a/docs/Reference/Data-Types.md
+++ b/docs/Reference/Data-Types.md
@@ -4,11 +4,12 @@ parent: Reference Section
 nav_order: 10
 has_toc: false
 permalink: /Reference/Data-Types
+symbols: [Boolean, Byte, Integer, Long, LongLong, LongPtr, Single, Double, Currency, Decimal, Date, String, Variant, Object]
 ---
 
 # Data Types
 
-twinBASIC supports fourteen intrinsic data types. They fall into four broad categories: numeric (integer and floating-point), text, date/time, and reference/generic. This page is the canonical lookup for storage size, value range, and the type-declaration suffix where one exists.
+twinBASIC supports fourteen intrinsic data types. They fall into four broad categories: numeric (integer and floating-point), text, date/time, and reference/generic. This page is the canonical lookup for storage size, value range, and the type-declaration suffix where one exists, with a short section for each type. Each type has a page of its own with its literals, conversions and arithmetic.
 
 For the twinBASIC-specific additions to this set --- **LongLong**, **LongPtr**, and **Decimal** as a standalone type --- see [Features → New Data Types](../Features/Language/Data-Types).
 
@@ -19,151 +20,127 @@ For the twinBASIC-specific additions to this set --- **LongLong**, **LongPtr**,
 
 | Type | Suffix | Storage | Range |
 |------|--------|---------|-------|
-| **Boolean** | (none) | 2 bytes | `True` or `False` |
-| **Byte** | (none) | 1 byte | 0 to 255 |
-| **Integer** | `%` | 2 bytes | -32,768 to 32,767 |
-| **Long** | `&` | 4 bytes | -2,147,483,648 to 2,147,483,647 |
-| **LongLong** | `^` | 8 bytes | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
-| **LongPtr** | (none) | 4 bytes (32-bit) / 8 bytes (64-bit) | Same as **Long** or **LongLong** depending on target |
-| **Single** | `!` | 4 bytes | ±1.401298E-45 to ±3.402823E38 |
-| **Double** | `#` | 8 bytes | ±4.94065645841246E-324 to ±1.79769313486232E308 |
-| **Currency** | `@` | 8 bytes | -922,337,203,685,477.5808 to 922,337,203,685,477.5807 |
-| **Decimal** | (none) | 16 bytes | ±79,228,162,514,264,337,593,543,950,335 (up to 28 decimal places) |
-| **Date** | (none) | 8 bytes | January 1, 100 to December 31, 9999 |
-| **String** | `$` | variable | Up to ~2 billion characters |
-| **Variant** | (none) | 16 bytes (+ heap data) | Any of the above |
-| **Object** | (none) | 4 bytes (32-bit) / 8 bytes (64-bit) | A COM interface reference |
+| [**Boolean**](#boolean) | (none) | 2 bytes | `True` or `False` |
+| [**Byte**](#byte) | (none) | 1 byte | 0 to 255 |
+| [**Integer**](#integer) | `%` | 2 bytes | -32,768 to 32,767 |
+| [**Long**](#long) | `&` | 4 bytes | -2,147,483,648 to 2,147,483,647 |
+| [**LongLong**](#longlong) | `^` | 8 bytes | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
+| [**LongPtr**](#longptr) | (none) | 4 bytes (32-bit) / 8 bytes (64-bit) | Same as **Long** or **LongLong** depending on target |
+| [**Single**](#single) | `!` | 4 bytes | ±1.401298E-45 to ±3.402823E38 |
+| [**Double**](#double) | `#` | 8 bytes | ±4.94065645841247E-324 to ±1.79769313486232E308 |
+| [**Currency**](#currency) | `@` | 8 bytes | -922,337,203,685,477.5808 to 922,337,203,685,477.5807 |
+| [**Decimal**](#decimal) | (none) | 16 bytes | ±79,228,162,514,264,337,593,543,950,335 (up to 28 decimal places) |
+| [**Date**](#date) | (none) | 8 bytes | January 1, 100 to December 31, 9999 |
+| [**String**](#string) | `$` | variable | Up to ~2 billion characters |
+| [**Variant**](#variant) | (none) | 16 bytes (32-bit) / 24 bytes (64-bit) (+ heap data) | Any of the above |
+| [**Object**](#object) | (none) | 4 bytes (32-bit) / 8 bytes (64-bit) | A COM interface reference |
 
 The suffix column lists the character that can optionally follow a literal or identifier to force its type in source code --- for example, `42&` is a **Long** literal, `3.14#` is a **Double**, and `Total!` declares a **Single** variable in a type-implicit context.
 
----
+Integer overflow raises a run-time error (error 6) by default. Overflow does not wrap silently, with one exception: integer division of the most negative value by -1, described under [the **\\** operator](../tB/Core/IntegerDivide). Which type an arithmetic result has, and so where it overflows, is set out in [Result types and promotion](Operators#result-types-and-promotion).
 
-## Integer types
+**Any** is not a data type. A parameter of a [**Declare**](../tB/Core/Declare) statement declared **As Any** takes an argument of any type, which the compiler does not check:
 
-**Boolean** stores `True` (-1) or `False` (0). The runtime treats any non-zero value as `True` when a **Boolean** is expected; only -1 is the canonical `True`. Assigning any non-zero integer to a **Boolean** normalises it to -1.
+```tb check_build slot=module
+Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
+    (ByRef Destination As Any, ByRef Source As Any, ByVal Length As LongPtr)
+```
 
-**Byte** is the only unsigned integer type. It holds values 0--255, which makes it the natural element type for byte arrays used in binary I/O and buffer operations.
+> [!NOTE] BETA 1010
+> A local variable, a **Static** variable, or a module or class field declared **As Any** compiles with no diagnostic; VB6 refuses each of them. The variable cannot be used: assigning to it is refused (TB5028), reading it into a **Variant** makes the build fail, and **TypeName** of it raises error 458 in a built program. Declare such a variable with a real type. A **Function**'s return type, a **Sub**'s parameter, a UDT field and an array **As Any** are refused (TB5079).
 
-**Integer** holds small signed integers. In most code, **Long** is a better choice: it is no slower on 32-bit hardware and never overflows on values above 32,767. **Integer** is useful when interfacing with structures or APIs that declare 16-bit fields.
+---
 
-**Long** is the most common integer type. It covers the full range of Win32 `DWORD` and `int` values and is the default type for index variables and counters.
+## Boolean
 
-**LongLong** is an 8-byte signed integer available in both 32-bit and 64-bit builds. In VBA it is restricted to 64-bit targets; twinBASIC lifts that restriction and allows **LongLong** in 32-bit projects. Use it when a value can exceed 2,147,483,647 --- file sizes, tick counts, GUIDs, and 64-bit Win32 handles. The suffix `^` marks a **LongLong** literal: `9_000_000_000^`.
+**Boolean** stores `True` (-1) or `False` (0) in 2 bytes, and has no type-declaration character. [**Boolean**](../tB/Core/Boolean) describes its conversions and its use in logic and arithmetic.
 
-**LongPtr** changes width with the compilation target: 4 bytes in a 32-bit build, 8 bytes in a 64-bit build. It is the correct type for Win32 handles, window handles (**HWND**), and pointers in `Declare` statements that must work in both modes. It has no literal suffix --- declare the variable with `Dim x As LongPtr` and assign it a numeric expression.
+---
 
-Integer overflow raises a run-time error (error 6) by default. Overflow does not wrap silently, with one exception: integer division of the most negative value by -1, described under [the **\\** operator](../tB/Core/IntegerDivide). Which type an arithmetic result has, and so where it overflows, is set out in [Result types and promotion](Operators#result-types-and-promotion).
+## Byte
+
+**Byte** is the only unsigned integer type: 1 byte, from 0 to 255, with no type-declaration character. It is the natural element type for byte arrays used in binary I/O and buffer operations. [**Byte**](../tB/Core/Byte) describes its conversions, overflow and arrays.
 
 ---
 
-## Floating-point types
+## Integer
 
-**Single** and **Double** follow the IEEE 754 standard for single-precision and double-precision floating-point respectively. Both can represent `NaN` and `Infinity` as bit patterns, though the VBA runtime raises an error on most operations that would produce them.
+**Integer** is a signed 16-bit integer: 2 bytes, from -32,768 to 32,767. Its type-declaration character is `%`. In most code **Long** is a better choice; **Integer** is useful when interfacing with structures or APIs that declare 16-bit fields. [**Integer**](../tB/Core/Integer) describes its literals, conversions and overflow.
 
-**Double** is the default type of untyped numeric literals that contain a decimal point (`3.14` is a **Double**). It is accurate to approximately 15--16 significant decimal digits. Choose it for general-purpose floating-point arithmetic.
+---
 
-**Single** is accurate to approximately 6--7 significant decimal digits. It is smaller and may be faster in tight loops, but the reduced precision makes it unsuitable for financial or scientific calculations where rounding error matters.
+## Long
 
-Assigning a value too large for a **Single** to a **Single** variable raises error 6, *Overflow*: `s = 1E+300` does, and so does `s = x ^ 2` when the **Double** result is too large.
+**Long** is a signed 32-bit integer: 4 bytes, from -2,147,483,648 to 2,147,483,647, in 32-bit and 64-bit builds alike. Its type-declaration character is `&`. It is the most common integer type, the usual choice for counters, indexes and 32-bit Win32 values. [**Long**](../tB/Core/Long) describes its literals, conversions and overflow.
 
-> [!NOTE]
-> VB6 raises no error for these assignments: the **Single** holds infinity (`1.#INF`).
+---
 
-**Currency** is a fixed-point type, stored internally as a 64-bit signed integer scaled by 10,000. It avoids the binary rounding errors of IEEE 754 types and has exactly four decimal places; a value with more places is rounded to four, and a value exactly halfway is rounded to the even digit, so `CCur("0.12345")` is 0.1234. It suits monetary values whose amounts and intermediate results never need more than four decimal places --- [Decimal or Currency for money](#decimal-or-currency-for-money) compares the two types.
+## LongLong
 
----
+**LongLong** is an 8-byte signed integer available in both 32-bit and 64-bit builds. In VBA it is restricted to 64-bit targets; twinBASIC lifts that restriction and allows **LongLong** in 32-bit projects. Use it when a value can exceed 2,147,483,647 --- file sizes, tick counts, GUIDs, and 64-bit Win32 handles. The suffix `^` marks a **LongLong** literal: `9000000000^`. [**LongLong**](../tB/Core/LongLong) describes its literals, conversions and overflow.
 
-## Decimal
+---
 
-**Decimal** is a 16-byte type using a 12-byte (96-bit) integer with a variable decimal-point scale and a sign bit. It provides up to 29 significant digits and up to 28 decimal places, making it the highest-precision numeric type available.
+## LongPtr
 
-**Decimal** stores decimal fractions exactly: ten additions of `CDec("0.1")` equal 1, where the same sum of **Double** values does not, and `CDec("1000.01") * CDec("0.0325")` is exactly 32.500325. Division is rounded to fit the type --- `CDec(1) / 3` keeps 28 decimal places --- so `CDec(1) / 3 * 3` is 0.9999999999999999999999999999, not 1.
+**LongPtr** changes width with the compilation target: 4 bytes in a 32-bit build, 8 bytes in a 64-bit build. It is the correct type for Win32 handles, window handles (**HWND**), and pointers in `Declare` statements that must work in both modes. It has no literal suffix --- declare the variable with `Dim x As LongPtr` and assign it a numeric expression. [**LongPtr**](../tB/Core/LongPtr) describes how it is declared, converted and used in calculations.
 
-**Decimal** ranks above every other numeric type in arithmetic: a **Decimal** combined with any other type, including **Double**, **Currency** and **Date**, gives a **Decimal**. The exceptions are `^`, which always gives a **Double**, and `\` and `Mod`, which give a **Long** or **LongLong** when either operand is a **Variant**. See [Result types and promotion](Operators#result-types-and-promotion).
+---
 
-> [!NOTE]
-> In twinBASIC, **Decimal** is available both as a **Variant** subtype (as in VBA) and as a standalone declared type --- `Dim x As Decimal` compiles and runs. The conversion function [**CDec**](../tB/Modules/Conversion/CDec) returns a **Decimal** value.
+## Single
 
-### Decimal or Currency for money
+**Single** is a 32-bit IEEE 754 floating-point number: 4 bytes, accurate to approximately 6--7 significant decimal digits. Its type-declaration character is `!`. It is smaller than a **Double**, but the reduced precision makes it unsuitable for financial or scientific calculations where rounding error matters. [**Single**](../tB/Core/Single) describes its literals, conversions and overflow.
 
-Both types store decimal fractions exactly, and both are suitable for money. They differ in how many decimal places they keep, how large a value they hold, and what mixing them with a **Double** gives:
+---
 
-| Compared | **Currency** | **Decimal** |
-|:--|:--|:--|
-| Decimal places | 4 | up to 28 |
-| Largest value | 922,337,203,685,477.5807 | 79,228,162,514,264,337,593,543,950,335 |
-| A rate of 0.03125 | stored as 0.0312 | stored as 0.03125 |
-| 1000.01 × 0.0325 | 32.5003 | 32.500325 |
-| 10 / 3 | a **Double**, 3.33333333333333 | a **Decimal**, 3.3333333333333333333333333333 |
-| With a **Double** | **Currency** under `+` and `-`, **Double** under `*` and `/` | **Decimal** under `+`, `-`, `*` and `/` |
+## Double
 
-**Currency** is enough when every amount and every intermediate result has at most four decimal places. **Decimal** keeps more: interest rates with five or more places, per-unit prices, and the results of division. Code that holds money in a **Variant** filled by [**CDec**](../tB/Modules/Conversion/CDec) gets the same result types from a declared **Decimal**, except under `\` and `Mod`, where the **Variant** gives a **Long**.
+**Double** is a 64-bit IEEE 754 floating-point number: 8 bytes, accurate to approximately 15--16 significant decimal digits. Its type-declaration character is `#`. It is the default type of untyped numeric literals that contain a decimal point (`3.14` is a **Double**), and the choice for general-purpose floating-point arithmetic. [**Double**](../tB/Core/Double) describes its literals, conversions and overflow.
 
 ---
 
-## Date
+## Currency
 
-**Date** is stored as an IEEE 754 double: the integer part counts days from the epoch (December 30, 1899), and the fractional part represents the time of day (0.0 at midnight, 0.5 at noon). The representable range is January 1, 100 to December 31, 9999.
+**Currency** is a fixed-point type, stored internally as a 64-bit signed integer scaled by 10,000. It avoids the binary rounding errors of IEEE 754 types and has exactly four decimal places; a value with more places is rounded to four, and a value exactly halfway is rounded to the even digit, so `CCur("0.12345")` is 0.1234. Its type-declaration character is `@`. [**Currency**](../tB/Core/Currency) describes its literals, conversions and arithmetic, and [Decimal or Currency for money](../tB/Core/Decimal#decimal-or-currency-for-money) compares the two types.
 
-The [**Date**](../tB/Modules/DateTime/Date) and [**Time**](../tB/Modules/DateTime/Time) properties return the current date and time. [**Now**](../tB/Modules/DateTime/Now) returns both combined. Because **Date** is ultimately a **Double**, arithmetic on **Date** values works: adding 1 advances by one day, subtracting two dates gives the number of days between them. A **Date** plus or minus a number is still a **Date**, and the difference of two **Date** values is a **Double** --- see [Result types and promotion](Operators#result-types-and-promotion).
+---
 
-### Date literals
+## Decimal
 
-A date literal is a date, a time of day, or both, written between number signs (`#`). Its type is **Date**, and its value is fixed when the project is compiled: the executable holds the number, not the text, so the regional settings of the machine that runs the program do not affect it.
+**Decimal** is a 16-byte type using a 96-bit integer with a variable decimal-point scale and a sign. It provides up to 29 significant digits and up to 28 decimal places, making it the highest-precision numeric type available, and it stores decimal fractions exactly. It has no type-declaration character. In twinBASIC it is a standalone declared type as well as a **Variant** subtype. [**Decimal**](../tB/Core/Decimal) describes its conversions and arithmetic.
 
-```tb check_build
-Dim Deadline As Date = #2026-03-17#          ' March 17, 2026
-Dim Reminder As Date = #1:45 PM#             ' 1:45 PM on December 30, 1899, which is day 0
-Dim Meeting As Date = #3/17/2026 13:45:30#   ' March 17, 2026, 1:45:30 PM
-```
+---
 
-The date can be written month first with `/` or `-` between the parts (`#3/17/2026#`, `#3-17-2026#`), year first (`#2026-03-17#`, `#2026/3/17#`), or with an English month name (`#Mar 17, 2026#`, `#17 March 2026#`). The time can be 24-hour (`#13:45#`, `#13:45:30#`) or 12-hour with `AM` or `PM` in either case (`#1:45 PM#`, `#1:45 pm#`). A literal with both puts a space between the date and the time. A time on its own has the date part December 30, 1899.
+## Date
 
-The compiler reads the parts in the same order whatever the regional format of the machine that compiles the project. These results were checked under English (United States) and under English (United Kingdom), whose short dates put the month and the day in opposite orders:
+**Date** is stored as an IEEE 754 double: the integer part counts days from the epoch (December 30, 1899), and the fractional part represents the time of day (0.0 at midnight, 0.5 at noon). The representable range is January 1, 100 to December 31, 9999. A **Date** literal is written between number signs: `#2026-03-17#`. [**Date**](../tB/Core/Date-Type) describes its literals, conversions and arithmetic.
 
-- `#1/2/2026#` is January 2, 2026, under both: the first number is always the month.
-- When the first number cannot be a month, the first two numbers are swapped: `#13/1/2026#` is January 13, 2026. So a day-first literal compiles without a diagnostic, and is read correctly only when its day is 13 or more.
-- A two-digit year was placed between 1950 and 2049, the range the machine's Windows settings specify for two-digit years: `#1/2/49#` is 2049, and `#1/2/50#` is 1950.
-- A literal with no year, such as `#1/2#`, takes the year in which the project is compiled.
-- A literal that is not a real date or time is a compile error, *TB5085 bad date*: `#2/29/2026#`, `#2/30/2026#`, `#24:00#` and `#13:60#` are all refused. So are fractional seconds (`#13:45:30.5#`), a `T` between the date and the time (`#2026-01-02T13:45:30#`), and, under English (United States), dots between the parts of a date (`#1.2.2026#`).
+### Date literals
 
-> [!NOTE]
-> A date literal and [**CDate**](../tB/Modules/Conversion/CDate) can read the same text differently. The literal is read month first, when the project is compiled; **CDate** reads a string in the order of the regional format of the machine the program runs on. Under English (United Kingdom), `#1/2/2026#` is January 2 and `CDate("1/2/2026")` is February 1. Written year first, `#2026-01-02#` and `CDate("2026-01-02")` are January 2 under both formats.
+A date literal is a date, a time of day, or both, between number signs (`#`). The forms the compiler accepts, and how it reads the month, day and year, are described under [Date literals](../tB/Core/Date-Type#date-literals) on the **Date** page.
 
 ---
 
 ## String
 
-**String** holds a sequence of Unicode characters, stored internally as a COM `BSTR` (a length-prefixed wide-character string). The length is measured in characters, not bytes; each character is 2 bytes wide (UTF-16 LE). A **String** can hold up to approximately 2 billion characters, limited in practice by available memory.
-
-A **String** variable initialises to `vbNullString` (a null `BSTR` pointer), which is distinct from a zero-length string (`""`). Most string operations treat both as empty, but the distinction matters when passing strings to APIs that distinguish a null pointer from an empty buffer. See [**StrPtr**](../tB/Modules/Information/StrPtr) for the address of the underlying buffer.
-
-Fixed-length strings --- `Dim s As String * 20` --- occupy exactly the specified number of characters, padded with spaces on the right or truncated on assignment. They are useful for fixed-width binary file records.
+**String** holds a sequence of Unicode characters, stored as a COM `BSTR` (UTF-16, 2 bytes for each character) with a length measured in characters. A variable-length **String** holds up to about 2 billion characters. Its type-declaration character is `$`. A fixed-length **String** --- `Dim s As String * 20` --- has exactly the declared length, padded with spaces or cut off on assignment. [**String**](../tB/Core/String) describes both kinds, the null string, and joining and converting strings. The [**String**](../tB/Modules/Strings/String) function is a different page.
 
 ---
 
 ## Variant
 
-**Variant** is a tagged union that can hold any of the types in the table above, plus `Null`, `Empty`, and arrays. Its 16-byte header stores a type tag ([**VbVarType**](../tB/Modules/Constants/VbVarType)) followed by type-specific data. When the value is a **String**, **Object**, or array, the 8-byte data slot holds a pointer to heap-allocated storage.
-
-`Empty` is the default state of an uninitialised **Variant** --- it is distinct from `0`, `""`, `False`, and `Null`. Test for it with [**IsEmpty**](../tB/Modules/Information/IsEmpty). `Null` propagates through arithmetic and comparison; use [**IsNull**](../tB/Modules/Information/IsNull) to detect it.
-
-**Variant** is the required type for parameters and return values in late-bound COM calls, and for any function whose return type varies at runtime. It carries a small overhead on each operation compared to a typed variable because the runtime must check the tag. Prefer typed variables when the type is known at design time.
+**Variant** holds a value of any type and carries its type with it, together with the special values `Empty`, `Null` and error values. It takes 16 bytes in a 32-bit build and 24 in a 64-bit build. A variable declared with no type is a **Variant**. [**Variant**](../tB/Core/Variant) describes its subtypes, `Empty` and `Null`, and arithmetic. Prefer typed variables when the type is known when the program is written.
 
 ---
 
 ## Object
 {: index="late binding" }
 
-**Object** holds a COM interface reference --- a pointer to a vtable. In a 32-bit build it occupies 4 bytes; in a 64-bit build, 8 bytes. The runtime calls `AddRef` on assignment and `Release` when the variable goes out of scope or is set to `Nothing`.
-
-`Nothing` is the zero-valued **Object** reference. Test for it with `If obj Is Nothing Then`.
-
-An **Object** variable can hold any COM-compatible object; the runtime resolves member calls through `IDispatch` (late binding). Declaring the variable with a specific class or interface type --- `Dim fs As FileSystemObject` --- enables early binding, which is faster and produces compile-time type checking.
+**Object** holds a reference to a COM object, in 4 bytes in a 32-bit build and 8 bytes in a 64-bit build. `Nothing` is the reference to no object. An **Object** variable can hold any COM-compatible object; the runtime resolves member calls through `IDispatch` (late binding). Declaring the variable with a specific class or interface type --- `Dim fs As FileSystemObject` --- enables early binding, which produces compile-time checking of member names. [**Object**](../tB/Core/Object) describes assignment with `Set`, `Is` and late binding.
 
 ---
 
-### See Also
+## See Also
 
 - [New Data Types](../Features/Language/Data-Types) -- **LongLong**, **LongPtr**, and **Decimal** in depth
 - [Enumerations](Enumerations) -- index of all enumeration types across all packages
diff --git a/docs/Reference/Data-Types/Boolean.md b/docs/Reference/Data-Types/Boolean.md
new file mode 100644
index 000000000..d276700fe
--- /dev/null
+++ b/docs/Reference/Data-Types/Boolean.md
@@ -0,0 +1,108 @@
+---
+title: Boolean
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 1
+permalink: /tB/Core/Boolean
+vba_attribution: true
+---
+
+# Boolean
+{: .no_toc }
+
+A value that is either **True** or **False**.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As Boolean** [ **=** *expression* ]
+- [**DefBool**](Deftype) *letterrange*
+
+A **Boolean** variable is declared **As Boolean**. [**DefBool**](Deftype) makes **Boolean** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. **Boolean** has no type-declaration character. A **Boolean** that is not given a value holds **False**.
+
+A comparison such as `5 > 3` gives a **Boolean**, and a **Boolean** is what `If`, `While` and `Do` test. The **Variant** subtype for a **Boolean** is **vbBoolean** (11), and [**TypeName**](../Modules/Information/TypeName) gives `Boolean`. A **Boolean** prints as `True` or `False`, and [**Write #**](Write) writes it as `#TRUE#` or `#FALSE#`.
+
+## Storage and range
+
+A **Boolean** is stored in 2 bytes, as a signed 16-bit number: **False** is 0 and **True** is -1, with every bit set. It has the same size in a 32-bit and a 64-bit build.
+
+```tb check_run
+Dim Flag As Boolean
+Debug.Print Flag                    ' False
+Debug.Print LenB(Flag)              ' 2
+Debug.Print TypeName(Flag)          ' Boolean
+Debug.Print VarType(Flag) = vbBoolean ' True
+Debug.Print CInt(True)              ' -1
+```
+
+## Literals
+
+The keywords **True** and **False** are the only **Boolean** literals. Their type is **Boolean**.
+
+## Conversions
+
+[**CBool**](../Modules/Conversion/CBool) converts an expression to a **Boolean**, and so does assigning one to a **Boolean** variable. A number that is 0 becomes **False**, and every other number, a fraction such as 0.1 included, becomes **True**; it is stored as -1. **Empty** becomes **False**. A string is read first as the word `True` or `False` in any combination of upper and lower case, then as a number, so `CBool("TRUE")` and `CBool("1")` are **True** and `CBool("0")` is **False**. Any other string raises error 13, *Type mismatch*, and so does a string with spaces around the word. **Null** raises error 94, *Invalid use of Null*.
+
+A **Boolean** converted to a number gives 0 for **False** and -1 for **True**. A **Byte** receives 255 from **True**, since -1 does not fit and the conversion keeps the bit pattern.
+
+Because **True** is -1, a number is not equal to **True** unless it is -1 itself: `5 = True` is **False**, although `CBool(5) = True` is **True**. Compare with [**CBool**](../Modules/Conversion/CBool) or with 0, not with **True**.
+
+```tb check_run
+Dim Flag As Boolean
+Flag = 5
+Debug.Print Flag                    ' True
+Debug.Print CInt(Flag)              ' -1
+Debug.Print CBool("false")          ' False
+Debug.Print CBool(0.1)              ' True
+Debug.Print 5 = True                ' False
+Debug.Print CBool(5) = True         ' True
+Debug.Print CByte(True)             ' 255
+```
+
+## Arithmetic and logic
+
+In arithmetic a **Boolean** counts as an **Integer**, so `True + 0` is the **Integer** -1 and `-True` is the **Integer** 1. [Result types and promotion](../../Reference/Operators#result-types-and-promotion) has the rules for a **Boolean** beside other types.
+
+`And`, `Or`, `Xor`, `Eqv`, `Imp` and `Not` work on the bits of their operands. With two **Boolean** operands the result is a **Boolean**. With a number as an operand they work bit by bit on numbers: `Not 1` is -2, which is not 0 and therefore counts as **True** in an `If`, whereas `Not True` is **False**. A number used as a condition is **True** when it is not 0, so convert it with [**CBool**](../Modules/Conversion/CBool) before applying `Not` or `And`. See [Operators](../../Reference/Operators#bitwise).
+
+```tb check_run
+Debug.Print TypeName(True And True) ' Boolean
+Debug.Print TypeName(True + 0)      ' Integer
+Debug.Print True + 0                ' -1
+Debug.Print Not True                ' False
+Debug.Print Not 1                   ' -2
+Debug.Print Not CBool(1)            ' False
+Debug.Print True And 5              ' 5
+```
+
+## Example
+
+This example reads a list of settings, accepting a word or a number for each, and reports the ones that are not valid.
+
+```tb check_run
+Dim Entry As Variant
+Dim Flag As Boolean
+On Error Resume Next
+For Each Entry In Array("12", "0", "true", "yes")
+    Err.Clear
+    Flag = CBool(Entry)
+    If Err.Number = 0 Then
+        Debug.Print Entry & " -> " & Flag
+    Else
+        Debug.Print Entry & " -> error " & Err.Number
+    End If
+Next
+' Output:
+' 12 -> True
+' 0 -> False
+' true -> True
+' yes -> error 13
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#boolean) -- the summary of all fourteen intrinsic types
+- [Integer](Integer) type -- what a **Boolean** counts as in arithmetic
+- [CBool](../Modules/Conversion/CBool) function
+- [Deftype](Deftype) statement
+- [Operators](../../Reference/Operators#bitwise) -- `And`, `Or`, `Not` and the other bitwise operators
+- [Result types and promotion](../../Reference/Operators#result-types-and-promotion) -- the type of an arithmetic result
diff --git a/docs/Reference/Data-Types/Byte.md b/docs/Reference/Data-Types/Byte.md
new file mode 100644
index 000000000..5b66ff0bc
--- /dev/null
+++ b/docs/Reference/Data-Types/Byte.md
@@ -0,0 +1,122 @@
+---
+title: Byte
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 2
+permalink: /tB/Core/Byte
+vba_attribution: true
+---
+
+# Byte
+{: .no_toc }
+
+An unsigned 8-bit integer, from 0 to 255.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As Byte** [ **=** *expression* ]
+- [**DefByte**](Deftype) *letterrange*
+
+A **Byte** variable is declared **As Byte**. [**DefByte**](Deftype) makes **Byte** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. **Byte** has no type-declaration character. A **Byte** that is not given a value holds 0.
+
+**Byte** is the only unsigned integer type, and the natural type for binary data: buffers, file contents and the bytes of a string are held in arrays of **Byte**. The **Variant** subtype for a **Byte** is **vbByte** (17), and [**TypeName**](../Modules/Information/TypeName) gives `Byte`.
+
+## Storage and range
+
+A **Byte** is stored in 1 byte and holds the whole numbers from 0 to 255. It has the same size in a 32-bit and a 64-bit build.
+
+```tb check_run
+Dim Value As Byte
+Debug.Print Value                   ' 0
+Debug.Print LenB(Value)             ' 1
+Debug.Print TypeName(Value)         ' Byte
+Debug.Print VarType(Value) = vbByte ' True
+```
+
+## Literals
+
+There is no **Byte** literal. A whole number such as `255` is an **Integer** (see [**Integer**](Integer#literals)), and the same holds for `&HFF`. A literal is converted when it is assigned to a **Byte** variable: `Value = 255` is accepted, and `Value = 256` and `Value = -1` raise error 6, *Overflow*. Use [**CByte**](../Modules/Conversion/CByte) to produce a **Byte** value in an expression.
+
+## Conversions
+
+[**CByte**](../Modules/Conversion/CByte) converts an expression to a **Byte**, and so does assigning one to a **Byte** variable. A number with a fraction is rounded to the nearest whole number, and a number exactly halfway between two is rounded to the even one: 2.5 becomes 2, 3.5 becomes 4 and 254.5 becomes 254. A string is read as a number, so `CByte("&HFF")` is 255 and `CByte(" 7 ")` is 7. **False** becomes 0 and **True** becomes 255.
+
+A value outside 0 to 255 after rounding raises error 6, *Overflow*: `CByte(255.5)` does, because it rounds to 256, and so does `CByte(-1)`. A string that is not a number raises error 13, *Type mismatch*.
+
+```tb check_run
+Dim Value As Byte
+Value = 2.5
+Debug.Print Value                   ' 2
+Value = 3.5
+Debug.Print Value                   ' 4
+Debug.Print CByte("&HFF")           ' 255
+Debug.Print CByte(True)             ' 255
+On Error Resume Next
+Value = CByte(255.5)
+Debug.Print Err.Number              ' 6
+```
+
+## Arithmetic and overflow
+
+An operation on two **Byte** values gives a **Byte**, so a result above 255 or below 0 raises error 6, *Overflow*, whatever the variable that receives it: with `Value` holding 100, `Total = Value * Value` raises the error even when `Total` is a **Long**. A **Byte** combined with an **Integer** gives an **Integer**, and the literal `1` is an **Integer**, so `Value + 1` is an **Integer**: it does not overflow, but assigning 256 to a **Byte** does. `\` and `Mod` on two **Byte** values give a **Byte**, and unary `-` gives an **Integer**. Division with `/` gives a **Double** unless the other operand is a **Single** or a **Decimal**. To compute with a wider type, convert one operand first: `CLng(Value) * Value`. [Result types and promotion](../../Reference/Operators#result-types-and-promotion) has the full rules.
+
+```tb check_run
+Dim Value As Byte = 100
+Dim Total As Long
+Debug.Print TypeName(Value + Value) ' Byte
+Debug.Print TypeName(Value + 1)     ' Integer
+Debug.Print TypeName(-Value)        ' Integer
+Total = CLng(Value) * Value
+Debug.Print Total                   ' 10000
+On Error Resume Next
+Total = Value * Value
+Debug.Print Err.Number              ' 6
+```
+
+[**IntegerOverflowChecks(False)**](Attributes#integeroverflowchecks) turns the overflow check off for a class, a module or a procedure, and a **Byte** result then wraps around: 255 plus 1 is 0.
+
+A **Variant** holding a **Byte** does not overflow: a result that does not fit becomes an **Integer**, a **Long** or a **Double**, whichever is the first to hold it.
+
+## Byte arrays
+
+An array of **Byte** holds binary data. Assigning a **String** to a dynamic **Byte** array copies the string's characters as they are stored, two bytes for each, low byte first, so `"ab"` gives the four bytes 97, 0, 98, 0. Assigning the array to a **String** variable reverses it. [**StrConv**](../Modules/Strings/StrConv) with `vbFromUnicode` converts the characters to the system's ANSI code page, one byte for each character, and `vbUnicode` converts back. The **Variant** subtype of a **Byte** array is `vbArray + vbByte`.
+
+```tb check_run
+Dim Data() As Byte
+Dim Text As String
+Data = "ab"
+Debug.Print UBound(Data) + 1        ' 4
+Debug.Print Data(0) & " " & Data(1) & " " & Data(2) ' 97 0 98
+Text = Data
+Debug.Print Text                    ' ab
+Data = StrConv("ab", vbFromUnicode)
+Debug.Print UBound(Data) + 1        ' 2
+Debug.Print StrConv(Data, vbUnicode) ' ab
+```
+
+## Example
+
+This example converts a string to ANSI bytes, shows each byte in hexadecimal and adds them up. The sum, 500, does not fit in a **Byte**, so it is held in a **Long**; masking it with `&HFF` gives a one-byte checksum.
+
+```tb check_run
+Dim Data() As Byte
+Dim Total As Long
+Dim Digits As String
+Dim i As Long
+Data = StrConv("Hello", vbFromUnicode)
+For i = LBound(Data) To UBound(Data)
+    Total = Total + Data(i)
+    Digits = Digits & Right$("0" & Hex$(Data(i)), 2) & " "
+Next
+Debug.Print Trim$(Digits)           ' 48 65 6C 6C 6F
+Debug.Print Total                   ' 500
+Debug.Print Total And &HFF          ' 244
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#byte) -- the summary of all fourteen intrinsic types
+- [Integer](Integer), [Long](Long) -- the signed integer types
+- [CByte](../Modules/Conversion/CByte) function
+- [Deftype](Deftype) statement
+- [Result types and promotion](../../Reference/Operators#result-types-and-promotion) -- the type of an arithmetic result
diff --git a/docs/Reference/Data-Types/Currency.md b/docs/Reference/Data-Types/Currency.md
new file mode 100644
index 000000000..18bca70e3
--- /dev/null
+++ b/docs/Reference/Data-Types/Currency.md
@@ -0,0 +1,102 @@
+---
+title: Currency
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 9
+permalink: /tB/Core/Currency
+vba_attribution: true
+---
+
+# Currency
+{: .no_toc }
+
+A fixed-point number with four decimal places, from -922,337,203,685,477.5808 to 922,337,203,685,477.5807.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As Currency** [ **=** *expression* ]
+- **Dim** *varname*`@`
+- [**DefCur**](Deftype) *letterrange*
+
+A **Currency** variable is declared **As Currency**, or by the type-declaration character `@` after its name. [**DefCur**](Deftype) makes **Currency** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. A **Currency** that is not given a value holds 0.
+
+**Currency** suits money and other calculations in which the digits must come out exactly. It stores a decimal number with exactly four digits after the point, so it has none of the binary rounding errors of [**Double**](Double). The **Variant** subtype for a **Currency** is **vbCurrency** (6), and [**TypeName**](../Modules/Information/TypeName) gives `Currency`.
+
+## Storage and range
+
+A **Currency** is stored in 8 bytes as a 64-bit integer that is scaled by 10,000, so it has 15 digits before the decimal point and 4 after it. It has the same size in a 32-bit and a 64-bit build.
+
+A value with more than four decimal places is rounded to four, and a value exactly halfway between two is rounded to the even digit: the string `"0.12345"` becomes 0.1234 and `"0.12355"` becomes 0.1236. A **Double** is first converted to the nearest value it can represent, which is not always exactly halfway, so rounding a **Double** can go either way.
+
+```tb check_run
+Dim Price As Currency
+Dim Tax@
+Debug.Print Price                   ' 0
+Debug.Print LenB(Price)             ' 8
+Debug.Print TypeName(Tax)           ' Currency
+Debug.Print VarType(Price) = vbCurrency ' True
+Debug.Print CCur("0.12345")         ' 0.1234
+Debug.Print CCur("0.12355")         ' 0.1236
+Debug.Print 0.1@ * 3 = 0.3@         ' True
+Debug.Print 0.1 * 3 = 0.3           ' False
+```
+
+## Literals
+
+The suffix `@` makes a number a **Currency**: `5@` and `1.5@`. Without it a number with a decimal point is a **Double**. A literal with more than four decimals is rounded: `0.12345@` is 0.1234. The largest literal is `922337203685477.5807@`.
+
+## Conversions
+
+[**CCur**](../Modules/Conversion/CCur) converts an expression to a **Currency**, and so does assigning one to a **Currency** variable. A string is read as a number, and `True` is -1. A value outside the range raises error 6, *Overflow*: `CCur(1E+15)` does. A string that is not a number raises error 13, *Type mismatch*.
+
+A **Currency** converted to a whole number is rounded to the nearest, with halves going to the even number: `CLng(2.5@)` is 2 and `CLng(3.5@)` is 4.
+
+```tb check_run
+Debug.Print CCur(" 12.5 ")          ' 12.5
+Debug.Print CCur(True)              ' -1
+Debug.Print CLng(2.5@)              ' 2
+Debug.Print CLng(3.5@)              ' 4
+On Error Resume Next
+Debug.Print CCur(1E+15)
+Debug.Print Err.Number              ' 6
+```
+
+## Arithmetic and overflow
+
+An operation on two **Currency** values gives a **Currency**, and so does a **Currency** combined with a **Byte**, **Integer**, **Long**, **LongLong**, **Boolean** or **String** under `+`, `-` and `*`, and with a **Single** or a **Double** under `+` and `-`. A **Currency** multiplied by a **Single** or a **Double** gives a **Double**, and so does any division with `/`: `10@ / 3@` is the **Double** 3.33333333333333. `\` and `Mod` give a **Long** and `^` a **Double**. Combined with a **Decimal** it gives a **Decimal**. [Result types and promotion](../../Reference/Operators#result-types-and-promotion) has the full rules.
+
+A product keeps four places and drops the rest: `0.0001@ * 0.0001@` is 0. A result outside the range raises error 6, *Overflow*, and a division by zero raises error 11. A **Variant** holding a **Currency** raises error 6 on overflow too, where a **Variant** holding a smaller type widens.
+
+For money with more than four decimal places, or results of division that must stay exact, use [**Decimal**](Decimal); [Decimal or Currency for money](Decimal#decimal-or-currency-for-money) compares the two.
+
+```tb check_run
+Dim Price As Currency = 19.99
+Debug.Print TypeName(Price * 3)     ' Currency
+Debug.Print TypeName(Price + 0.5)   ' Currency
+Debug.Print TypeName(Price * 0.5)   ' Double
+Debug.Print TypeName(Price / 3)     ' Double
+Debug.Print Price * 3               ' 59.97
+Debug.Print 0.0001@ * 0.0001@       ' 0
+```
+
+## Example
+
+This example adds a 7.25% tax to an order of three items. The tax is rounded to whole cents with [**Round**](../Modules/Math/Round) before it is added, so the total never carries a fraction of a cent.
+
+```tb check_run
+Dim Subtotal As Currency
+Dim Tax As Currency
+Subtotal = 19.99@ * 3
+Tax = Round(Subtotal * 0.0725, 2)
+Debug.Print Subtotal                ' 59.97
+Debug.Print Tax                     ' 4.35
+Debug.Print Subtotal + Tax          ' 64.32
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#currency) -- the summary of all fourteen intrinsic types
+- [Decimal](Decimal), [Double](Double), [Single](Single) -- the other non-integer types
+- [CCur](../Modules/Conversion/CCur) function
+- [Deftype](Deftype) statement
+- [Result types and promotion](../../Reference/Operators#result-types-and-promotion) -- the type of an arithmetic result
diff --git a/docs/Reference/Data-Types/Date.md b/docs/Reference/Data-Types/Date.md
new file mode 100644
index 000000000..ce15199ed
--- /dev/null
+++ b/docs/Reference/Data-Types/Date.md
@@ -0,0 +1,128 @@
+---
+title: Date
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 11
+permalink: /tB/Core/Date-Type
+vba_attribution: true
+---
+
+# Date
+{: .no_toc }
+
+A date and a time of day, from January 1, 100 to December 31, 9999.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As Date** [ **=** *expression* ]
+- [**DefDate**](Deftype) *letterrange*
+
+A **Date** variable is declared **As Date**. [**DefDate**](Deftype) makes **Date** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. **Date** has no type-declaration character. A **Date** that is not given a value holds 0, which is midnight at the start of December 30, 1899.
+
+The [**Date**](../Modules/DateTime/Date) and [**Time**](../Modules/DateTime/Time) functions return the current date and time, and [**Now**](../Modules/DateTime/Now) returns both in a **Date**. The **Variant** subtype for a **Date** is **vbDate** (7), and [**TypeName**](../Modules/Information/TypeName) gives `Date`.
+
+## Storage and range
+
+A **Date** is stored in 8 bytes as a **Double**. The integer part counts days from December 30, 1899, which is day 0: day 1 is December 31, 1899, and the days before it are negative. The fraction is the time of day, 0.5 being noon. January 1, 100 is day -657,434, and December 31, 9999 is day 2,958,465 plus the time of day. A **Date** has the same size in a 32-bit and a 64-bit build.
+
+A number or a date outside that range cannot be a **Date**, and an assignment, a conversion or an addition that would make one raises error 6, *Overflow*.
+
+> [!WARNING] BETA 1010
+> A **Date** outside the range is accepted with no error: `d = 2958466`, `CDate(-657435)`, `#12/31/9999# + 1` and an assignment of `1E+300` each store the number, and **CStr**, **Format** and **Year** of the value then raise error 5, *Invalid procedure call or argument*. VB6 raises error 6, *Overflow*, at the assignment. Compare a day number with -657,434 and 2,958,465 before assigning it to a **Date**.
+
+A **Date** prints, and is converted to a string, in the short date and time formats of the regional settings of the computer that runs the program. To get the same text on every computer, give [**Format**](../Modules/Strings/Format) a format string such as `"yyyy-mm-dd hh:nn:ss"`.
+
+```tb check_run
+Dim Moment As Date
+Debug.Print CDbl(Moment)            ' 0
+Debug.Print LenB(Moment)            ' 8
+Debug.Print TypeName(Moment)        ' Date
+Debug.Print VarType(Moment) = vbDate ' True
+Debug.Print Format(Moment, "yyyy-mm-dd hh:nn:ss") ' 1899-12-30 00:00:00
+Debug.Print CDbl(#1/1/100#)         ' -657434
+Debug.Print CDbl(#12/31/9999#)      ' 2958465
+```
+
+## Date literals
+
+A date literal is a date, a time of day, or both, written between number signs (`#`). Its type is **Date**, and its value is fixed when the project is compiled: the executable holds the number, not the text, so the regional settings of the machine that runs the program do not affect it.
+
+```tb check_build
+Dim Deadline As Date = #2026-03-17#          ' March 17, 2026
+Dim Reminder As Date = #1:45 PM#             ' 1:45 PM on December 30, 1899, which is day 0
+Dim Meeting As Date = #3/17/2026 13:45:30#   ' March 17, 2026, 1:45:30 PM
+```
+
+The date can be written month first with `/` or `-` between the parts (`#3/17/2026#`, `#3-17-2026#`), year first (`#2026-03-17#`, `#2026/3/17#`), or with an English month name (`#Mar 17, 2026#`, `#17 March 2026#`). The time can be 24-hour (`#13:45#`, `#13:45:30#`) or 12-hour with `AM` or `PM` in either case (`#1:45 PM#`, `#1:45 pm#`). A literal with both puts a space between the date and the time. A time on its own has the date part December 30, 1899.
+
+The compiler reads the parts in the same order whatever the regional format of the machine that compiles the project. These results were checked under English (United States) and under English (United Kingdom), whose short dates put the month and the day in opposite orders:
+
+- `#1/2/2026#` is January 2, 2026, under both: the first number is always the month.
+- When the first number cannot be a month, the first two numbers are swapped: `#13/1/2026#` is January 13, 2026. So a day-first literal compiles without a diagnostic, and is read correctly only when its day is 13 or more.
+- A two-digit year was placed between 1950 and 2049, the range the machine's Windows settings specify for two-digit years: `#1/2/49#` is 2049, and `#1/2/50#` is 1950.
+- A literal with no year, such as `#1/2#`, takes the year in which the project is compiled.
+- A literal that is not a real date or time is a compile error, *TB5085 bad date*: `#2/29/2026#`, `#2/30/2026#`, `#24:00#` and `#13:60#` are all refused. So are fractional seconds (`#13:45:30.5#`), a `T` between the date and the time (`#2026-01-02T13:45:30#`), and, under English (United States), dots between the parts of a date (`#1.2.2026#`).
+
+> [!NOTE]
+> A date literal and [**CDate**](../Modules/Conversion/CDate) can read the same text differently. The literal is read month first, when the project is compiled; **CDate** reads a string in the order of the regional format of the machine the program runs on. Under English (United Kingdom), `#1/2/2026#` is January 2 and `CDate("1/2/2026")` is February 1. Written year first, `#2026-01-02#` and `CDate("2026-01-02")` are January 2 under both formats.
+
+## Conversions
+
+[**CDate**](../Modules/Conversion/CDate) converts an expression to a **Date**, and so does assigning one to a **Date** variable. A number is read as a count of days from the epoch, with the fraction as the time of day, and negative numbers are before December 30, 1899: `CDate(1.5)` is noon on December 31, 1899. A string is read as a date, a time or both, in the formats of the regional settings; a string in year-month-day order is read the same everywhere. A time on its own has the date part December 30, 1899. **Empty** becomes day 0 and `True` becomes day -1. A string that is not a date, and an empty string, raise error 13, *Type mismatch*; **Null** raises error 94, *Invalid use of Null*.
+
+A **Date** converted to a number gives its day count: [**CDbl**](../Modules/Conversion/CDbl) keeps the time of day as a fraction, and [**CLng**](../Modules/Conversion/CLng) rounds to the nearest day, with halves going to the even day.
+
+```tb check_run
+Const Stamp As String = "yyyy-mm-dd hh:nn:ss"
+Debug.Print Format(CDate(0), Stamp)                     ' 1899-12-30 00:00:00
+Debug.Print Format(CDate(1.5), Stamp)                   ' 1899-12-31 12:00:00
+Debug.Print Format(CDate(-1.5), Stamp)                  ' 1899-12-29 12:00:00
+Debug.Print Format(CDate("2026-03-17 13:45:30"), Stamp) ' 2026-03-17 13:45:30
+Debug.Print Format(CDate("13:45"), Stamp)               ' 1899-12-30 13:45:00
+Debug.Print CDbl(#3/17/2026#)                           ' 46098
+Debug.Print CLng(#3/17/2026 6:00 PM#)                   ' 46099
+On Error Resume Next
+Debug.Print CDate("not a date")
+Debug.Print Err.Number                                  ' 13
+```
+
+## Arithmetic
+
+A number added to a **Date** is a number of days, and a fraction is part of a day: adding 1 advances the date by a day and adding 0.5 by twelve hours. The result is a **Date**. Subtracting a number gives a **Date**, and subtracting one **Date** from another gives a **Double**, the number of days between them, with the time of day as a fraction. `*` and `/` treat a **Date** as a **Double** and give a **Double**. Comparison operators compare the moments in time. [Result types and promotion](../../Reference/Operators#result-types-and-promotion) has the full rules.
+
+Adding a number of days cannot add a month or a year, whose lengths vary; [**DateAdd**](../Modules/DateTime/DateAdd) adds days, months, years and the other intervals.
+
+```tb check_run
+Const Stamp As String = "yyyy-mm-dd hh:nn"
+Dim Start As Date = #3/17/2026 9:30 AM#
+Dim Finish As Date = #3/20/2026 6:00 PM#
+Debug.Print Format(Start + 1.5, Stamp)  ' 2026-03-18 21:30
+Debug.Print TypeName(Start + 1)         ' Date
+Debug.Print TypeName(Finish - Start)    ' Double
+Debug.Print Round(Finish - Start, 4)    ' 3.3542
+Debug.Print Start < Finish              ' True
+Debug.Print Format(DateAdd("m", 1, Start), Stamp) ' 2026-04-17 09:30
+```
+
+## Example
+
+This example finds how many whole days and hours there are between two moments.
+
+```tb check_run
+Dim Start As Date = #3/17/2026 9:30 AM#
+Dim Finish As Date = #3/20/2026 6:00 PM#
+Dim Elapsed As Double = Finish - Start
+Debug.Print Int(Elapsed) & " days"                          ' 3 days
+Debug.Print Round((Elapsed - Int(Elapsed)) * 24, 1) & " hours" ' 8.5 hours
+Debug.Print DateDiff("d", Start, Finish)                    ' 3
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#date) -- the summary of all fourteen intrinsic types
+- [CDate](../Modules/Conversion/CDate) function
+- [Date](../Modules/DateTime/Date), [Time](../Modules/DateTime/Time), [Now](../Modules/DateTime/Now) -- the current date and time
+- [DateAdd](../Modules/DateTime/DateAdd), [DateSerial](../Modules/DateTime/DateSerial) -- functions that build and change dates
+- [Literals](../../Features/Language/Literals) -- how the compiler reads literals
+- [Deftype](Deftype) statement
+- [Result types and promotion](../../Reference/Operators#result-types-and-promotion) -- the type of an arithmetic result
diff --git a/docs/Reference/Data-Types/Decimal.md b/docs/Reference/Data-Types/Decimal.md
new file mode 100644
index 000000000..51e2f8d50
--- /dev/null
+++ b/docs/Reference/Data-Types/Decimal.md
@@ -0,0 +1,126 @@
+---
+title: Decimal
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 10
+permalink: /tB/Core/Decimal
+vba_attribution: true
+---
+
+# Decimal
+{: .no_toc }
+
+A decimal number of up to 29 digits, up to 28 of them after the decimal point.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As Decimal** [ **=** *expression* ]
+- [**DefDec**](Deftype) *letterrange*
+
+A **Decimal** variable is declared **As Decimal**. [**DefDec**](Deftype) makes **Decimal** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. **Decimal** has no type-declaration character. A **Decimal** that is not given a value holds 0.
+
+**Decimal** stores decimal fractions exactly, with more digits than any other numeric type. It suits money and measurements whose digits must come out as written, and results of division that must keep their precision. The **Variant** subtype for a **Decimal** is **vbDecimal** (14), and [**TypeName**](../Modules/Information/TypeName) gives `Decimal`.
+
+> [!NOTE]
+> **Decimal** is a full declared type in twinBASIC: `Dim Amount As Decimal` compiles and runs. In VBA a **Decimal** can exist only as the subtype of a **Variant**, made with [**CDec**](../Modules/Conversion/CDec), and VB6 has no **Decimal** at all: `Dim x As Decimal` is a compile error there.
+
+## Storage and range
+
+A **Decimal** is stored in 16 bytes, in both 32-bit and 64-bit builds: a 96-bit unsigned integer, a sign, and a scale from 0 to 28 that says how many digits of the integer are after the decimal point. With a scale of 0 the largest value is 79,228,162,514,264,337,593,543,950,335. With a scale of 28 the largest value is 7.9228162514264337593543950335, and the smallest value that is not 0 is 0.0000000000000000000000000001. The range is the same for negative values.
+
+```tb check_run
+Dim Amount As Decimal
+Debug.Print Amount                  ' 0
+Debug.Print LenB(Amount)            ' 16
+Debug.Print TypeName(Amount)        ' Decimal
+Debug.Print VarType(Amount) = vbDecimal ' True
+Amount = CDec("79228162514264337593543950335")
+Debug.Print Amount                  ' 79228162514264337593543950335
+Debug.Print CDec("0.0000000000000000000000000001") ' 0.0000000000000000000000000001
+```
+
+## Literals
+
+There is no **Decimal** literal. A number with a decimal point is a **Double**, and so is a whole number beyond the **Long** range, so a **Double** with 15 significant digits is all a literal can give. Convert a string with [**CDec**](../Modules/Conversion/CDec) to keep every digit: `CDec("0.1")`. A **Double** or **Long** assigned to a **Decimal** variable is converted: `Dim Amount As Decimal = 1.5`.
+
+`CDec(0.1)` is 0.1 because a **Double** converts with 15 significant digits, so `CDec(1 / 3)` is 0.333333333333333, not 28 threes.
+
+## Conversions
+
+[**CDec**](../Modules/Conversion/CDec) converts an expression to a **Decimal**, and so does assigning one to a **Decimal** variable. A string is read as a number, and `True` is -1. A value with more than 28 decimal places is rounded to 28, and a value exactly halfway is rounded to the even digit: `CDec("0.00000000000000000000000000015")` is 0.0000000000000000000000000002, and `CDec("0.00000000000000000000000000005")` is 0. A value outside the range raises error 6, *Overflow*: `CDec(1E+29)` does. A string that is not a number raises error 13, *Type mismatch*.
+
+A **Decimal** converted to a whole number is rounded to the nearest, with halves going to the even number: assigning `CDec(2.5)` to a **Long** gives 2 and `CDec(3.5)` gives 4.
+
+```tb check_run
+Dim Amount As Decimal
+Dim Whole As Long
+Debug.Print CDec("0.1")             ' 0.1
+Debug.Print CDec("0.00000000000000000000000000015") ' 0.0000000000000000000000000002
+Debug.Print CDec(True)              ' -1
+Whole = CDec(2.5)
+Debug.Print Whole                   ' 2
+On Error Resume Next
+Amount = CDec(1E+29)
+Debug.Print Err.Number              ' 6
+```
+
+## Arithmetic and overflow
+
+An operation with a **Decimal** gives a **Decimal**, whatever the type of the other operand: a **Byte**, **Integer**, **Long**, **LongLong**, **Single**, **Double**, **Currency** or **Date** beside a **Decimal** gives a **Decimal**. The exceptions are `^`, which always gives a **Double**, and `\` and `Mod`, which give a **Decimal** for two declared **Decimal** values but a **Long** when either operand is a **Variant**. [Result types and promotion](../../Reference/Operators#result-types-and-promotion) has the full rules.
+
+Addition, subtraction and multiplication are exact while the result fits. Division is rounded to fit the type: `CDec(1) / 3` keeps 28 decimal places, so `CDec(1) / 3 * 3` is 0.9999999999999999999999999999, not 1. A result outside the range raises error 6, *Overflow*, and a division by zero raises error 11.
+
+```tb check_run
+Dim Total As Decimal
+Dim i As Long
+For i = 1 To 10
+    Total = Total + CDec("0.1")
+Next
+Debug.Print Total                   ' 1
+Debug.Print Total = 1               ' True
+Debug.Print CDec("1000.01") * CDec("0.0325") ' 32.500325
+Debug.Print CDec(1) / 3             ' 0.3333333333333333333333333333
+Debug.Print CDec(1) / 3 * 3         ' 0.9999999999999999999999999999
+Debug.Print TypeName(Total + 1#)    ' Decimal
+Debug.Print TypeName(Total ^ 2)     ' Double
+```
+
+A **Variant** holding a **Decimal** raises error 6 on overflow, as the declared type does.
+
+### Decimal or Currency for money
+
+Both types store decimal fractions exactly, and both are suitable for money. They differ in how many decimal places they keep, how large a value they hold, and what mixing them with a **Double** gives:
+
+| Compared | **Currency** | **Decimal** |
+|:--|:--|:--|
+| Decimal places | 4 | up to 28 |
+| Largest value | 922,337,203,685,477.5807 | 79,228,162,514,264,337,593,543,950,335 |
+| A rate of 0.03125 | stored as 0.0312 | stored as 0.03125 |
+| 1000.01 × 0.0325 | 32.5003 | 32.500325 |
+| 10 / 3 | a **Double**, 3.33333333333333 | a **Decimal**, 3.3333333333333333333333333333 |
+| With a **Double** | **Currency** under `+` and `-`, **Double** under `*` and `/` | **Decimal** under `+`, `-`, `*` and `/` |
+
+**Currency** is enough when every amount and every intermediate result has at most four decimal places. **Decimal** keeps more: interest rates with five or more places, per-unit prices, and the results of division. Code that holds money in a **Variant** filled by [**CDec**](../Modules/Conversion/CDec) gets the same result types from a declared **Decimal**, except under `\` and `Mod`, where the **Variant** gives a **Long**.
+
+## Example
+
+This example adds a tax at 7.125% to an invoice. The rate has five decimal places and is stored exactly in a **Decimal**; a **Currency** would drop its fifth decimal and store 0.0712. The tax is kept to its full four places, and rounding to cents is the last step.
+
+```tb check_run
+Dim Net As Decimal = CDec("1234.56")
+Dim Rate As Decimal = CDec("0.07125")
+Dim Tax As Decimal
+Tax = Net * Rate
+Debug.Print Tax                             ' 87.9624
+Debug.Print Net + Tax                       ' 1322.5224
+Debug.Print Round(Net + Tax, 2)             ' 1322.52
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#decimal) -- the summary of all fourteen intrinsic types
+- [Currency](Currency), [Double](Double), [Single](Single) -- the other non-integer types
+- [CDec](../Modules/Conversion/CDec) function
+- [Deftype](Deftype) statement
+- [New Data Types](../../Features/Language/Data-Types) -- the new types in twinBASIC
+- [Result types and promotion](../../Reference/Operators#result-types-and-promotion) -- the type of an arithmetic result
diff --git a/docs/Reference/Data-Types/Double.md b/docs/Reference/Data-Types/Double.md
new file mode 100644
index 000000000..8bf87e293
--- /dev/null
+++ b/docs/Reference/Data-Types/Double.md
@@ -0,0 +1,123 @@
+---
+title: Double
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 8
+permalink: /tB/Core/Double
+vba_attribution: true
+---
+
+# Double
+{: .no_toc }
+
+A 64-bit IEEE 754 double-precision floating-point number.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As Double** [ **=** *expression* ]
+- **Dim** *varname*`#`
+- [**DefDbl**](Deftype) *letterrange*
+
+A **Double** variable is declared **As Double**, or by the type-declaration character `#` after its name. [**DefDbl**](Deftype) makes **Double** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. A **Double** that is not given a value holds 0.
+
+**Double** is the usual type for general floating-point arithmetic. It has about 15 significant decimal digits. The **Variant** subtype for a **Double** is **vbDouble** (5), and [**TypeName**](../Modules/Information/TypeName) gives `Double`.
+
+## Storage and range
+
+A **Double** is stored in 8 bytes: a sign bit, an 11-bit exponent and a 52-bit fraction. It holds values from -1.79769313486231E308 to -4.94065645841247E-324 and from 4.94065645841247E-324 to 1.79769313486232E308, and 0. It has the same size in a 32-bit and a 64-bit build. A value too small for the type becomes 0 with no error.
+
+A **Double** holds whole numbers exactly up to 2^53, which is 9,007,199,254,740,992. Most decimal fractions are not stored exactly, so a calculation can differ from the decimal answer in its last digits, and two results that look equal can compare as different. **Debug.Print** and conversion to a **String** with [**CStr**](../Modules/Conversion/CStr) show 15 significant digits.
+
+```tb check_run
+Dim Value As Double
+Dim Rate#
+Debug.Print Value                   ' 0
+Debug.Print LenB(Value)             ' 8
+Debug.Print TypeName(Rate)          ' Double
+Debug.Print VarType(Value) = vbDouble ' True
+Debug.Print 1 / 3                   ' 0.333333333333333
+Debug.Print 0.1 + 0.2 = 0.3         ' False
+Debug.Print (0.1 + 0.2) - 0.3       ' 5.55111512312578E-17
+```
+
+## Literals
+
+A number with a decimal point or an exponent is a **Double**, and so is a whole number above 2,147,483,647. The suffix `#` makes any number a **Double**: `1#`. An exponent is written with `E` or `D`; `1E5` and `1D5` are both 100000. A decimal point can have no digits before it or after it: `.5` and `5.`.
+
+```tb check_run
+Debug.Print TypeName(3.14)          ' Double
+Debug.Print TypeName(.5)            ' Double
+Debug.Print TypeName(5.)            ' Double
+Debug.Print TypeName(1E5)           ' Double
+Debug.Print TypeName(1D5)           ' Double
+Debug.Print TypeName(1#)            ' Double
+Debug.Print TypeName(2147483648)    ' Double
+Debug.Print 1.79769313486231E+308   ' 1.79769313486231E+308
+Debug.Print 4.94065645841247E-324   ' 4.94065645841247E-324
+```
+
+## Conversions
+
+[**CDbl**](../Modules/Conversion/CDbl) converts an expression to a **Double**, and so does assigning one to a **Double** variable. A string is read as a number using the regional settings of the computer that runs the program, so `CDbl("1,5")` depends on whether the comma is the decimal separator there. `True` is -1. A string that is not a number raises error 13, *Type mismatch*, and a string too large for the type, such as `"1E309"`, raises error 6, *Overflow*.
+
+```tb check_run
+Debug.Print CDbl("1.5")             ' 1.5
+Debug.Print CDbl(True)              ' -1
+Debug.Print CDbl(0.1!)              ' 0.100000001490116
+On Error Resume Next
+Debug.Print CDbl("1E309")
+Debug.Print Err.Number              ' 6
+```
+
+## Arithmetic and overflow
+
+An operation on two **Double** values gives a **Double**. A **Double** combined with a **Byte**, **Integer**, **Long**, **LongLong**, **Single** or **Boolean** gives a **Double**; combined with a **Currency** it gives a **Currency** under `+` and `-` and a **Double** under `*` and `/`; combined with a **Decimal**, a **Decimal**. `^` always gives a **Double**. [Result types and promotion](../../Reference/Operators#result-types-and-promotion) has the full rules.
+
+A result too large for the type raises error 6, *Overflow*: `1E+308 * 10` does, and so does `1E+308 + 1E+308`. Dividing by zero raises error 11, *Division by zero*; `0 / 0` raises error 6, and `Sqr(-1)` raises error 5, *Invalid procedure call or argument*. A result too small for the type is 0, with no error. A **Variant** holding a **Double** raises error 6 on overflow too, where a **Variant** holding a smaller type widens.
+
+A **Double** can hold the IEEE values infinity and NaN, which can arrive from memory or from a COM component, but arithmetic never produces one: it raises an error instead. As in VB6, they print as `1.#INF`, `-1.#INF`, `1.#QNAN` and, for the indeterminate value, `-1.#IND`. A comparison with NaN does not follow the IEEE rules: `NaN = NaN` is True, as in VB6, and so is `NaN = 0`, so a comparison cannot tell a NaN from a number.
+
+> [!NOTE] BETA 1010
+> A 64-bit build prints infinity and NaN as `inf`, `-inf`, `nan` and `-nan(ind)`, and **Format** with a number picture such as `"0.00"` gives `inf.00` and `nan.00`. A 32-bit build and VB6 print `1.#INF` and `1.#QNAN`, and **Format** gives `1.#INF` and `1.#R`. Do not test or parse the text of these values.
+
+```tb check_run
+Dim Value As Double = 1E+308
+On Error Resume Next
+Value = Value * 10
+Debug.Print Err.Number              ' 6
+Err.Clear
+Value = 1
+Value = Value / 0
+Debug.Print Err.Number              ' 11
+Err.Clear
+Value = 1E-300
+Value = Value * 1E-300
+Debug.Print Err.Number              ' 0
+Debug.Print Value                   ' 0
+```
+
+> [!WARNING] BETA 1010
+> A **Double** addition, subtraction, multiplication or exponentiation (`^`) that overflows raises no error in code compiled with LLVM: the result is infinity, and 0 for `^`. Code compiled without LLVM raises error 6, as VB6 does. See [Language support](../../LLVM/Getting-Started#language-support).
+
+## Example
+
+This example adds 0.1 to a **Double** a million times. The total is close to 100,000 but not equal to it, so the comparison allows a small difference.
+
+```tb check_run
+Dim Total As Double
+Dim i As Long
+For i = 1 To 1000000
+    Total = Total + 0.1
+Next
+Debug.Print Total                           ' 100000.000001333
+Debug.Print Total = 100000                  ' False
+Debug.Print Abs(Total - 100000) < 0.001     ' True
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#double) -- the summary of all fourteen intrinsic types
+- [Single](Single), [Currency](Currency), [Decimal](Decimal) -- the other non-integer types
+- [CDbl](../Modules/Conversion/CDbl) function
+- [Deftype](Deftype) statement
+- [Result types and promotion](../../Reference/Operators#result-types-and-promotion) -- the type of an arithmetic result
diff --git a/docs/Reference/Data-Types/Integer.md b/docs/Reference/Data-Types/Integer.md
new file mode 100644
index 000000000..d2cb5db93
--- /dev/null
+++ b/docs/Reference/Data-Types/Integer.md
@@ -0,0 +1,123 @@
+---
+title: Integer
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 3
+permalink: /tB/Core/Integer
+vba_attribution: true
+---
+
+# Integer
+{: .no_toc }
+
+A signed 16-bit integer, from -32,768 to 32,767.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As Integer** [ **=** *expression* ]
+- **Dim** *varname*`%`
+- [**DefInt**](Deftype) *letterrange*
+
+An **Integer** variable is declared **As Integer**, or by the type-declaration character `%` after its name. [**DefInt**](Deftype) makes **Integer** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. An **Integer** that is not given a value holds 0.
+
+**Integer** suits 16-bit fields in structures and API calls, and small enumerated values. For counters and indexes use [**Long**](Long): it is no slower, and it does not overflow at 32,767. The **Variant** subtype for an **Integer** is **vbInteger** (2), and [**TypeName**](../Modules/Information/TypeName) gives `Integer`.
+
+## Storage and range
+
+An **Integer** is stored in 2 bytes as a two's-complement number. It holds the whole numbers from -32,768 to 32,767. It has the same size in a 32-bit and a 64-bit build.
+
+```tb check_run
+Dim Count As Integer
+Dim Total%
+Debug.Print Count                   ' 0
+Debug.Print LenB(Count)             ' 2
+Debug.Print TypeName(Total)         ' Integer
+Debug.Print VarType(Count) = vbInteger ' True
+```
+
+## Literals
+
+A whole-number literal with no type-declaration character is an **Integer** from -32,768 to 32,767, a **Long** beyond that up to 2,147,483,647, and a **Double** beyond that. The minus sign is not part of the literal, so `-32768` is a **Long**: the literal 32768 is a **Long**, then negated. The suffix `%` makes a whole-number literal an **Integer**: `5%`.
+
+A hexadecimal (`&H`), octal (`&O`) or binary (`&B`) literal is an **Integer** when its value fits in 16 bits: `&H7FFF` is 32,767, `&H8000` is -32,768 and `&HFFFF` is -1. With one more digit it is a **Long**. The suffix `&` makes a 16-bit pattern a positive **Long**: `&H8000&` is 32,768. The digits can be grouped with underscores, as in `&B1111_0000`; a decimal literal cannot be.
+
+```tb check_run
+Debug.Print TypeName(32767)         ' Integer
+Debug.Print TypeName(32768)         ' Long
+Debug.Print TypeName(5%)            ' Integer
+Debug.Print &H7FFF                  ' 32767
+Debug.Print &H8000                  ' -32768
+Debug.Print &HFFFF                  ' -1
+Debug.Print TypeName(&H10000)       ' Long
+Debug.Print &H8000&                 ' 32768
+```
+
+> [!NOTE]
+> Binary literals (`&B`) and the underscores that group digits are twinBASIC additions; VBA and VB6 have `&H` and `&O` only. See [Literals](../../Features/Language/Literals).
+
+## Conversions
+
+[**CInt**](../Modules/Conversion/CInt) converts an expression to an **Integer**, and so does assigning one to an **Integer** variable. A number with a fraction is rounded to the nearest whole number, and a number exactly halfway between two is rounded to the even one: 2.5 becomes 2, 3.5 becomes 4 and -2.5 becomes -2. A string is read as a number, so `CInt("&HFFFF")` is -1, and `True` is -1.
+
+A value outside the range after rounding raises error 6, *Overflow*: `CInt(32767.5)` does, because it rounds to 32,768, and so does `CInt("65535")`. A string that is not a number raises error 13, *Type mismatch*.
+
+```tb check_run
+Dim Count As Integer
+Count = 2.5
+Debug.Print Count                   ' 2
+Count = 3.5
+Debug.Print Count                   ' 4
+Debug.Print CInt(-2.5)              ' -2
+Debug.Print CInt("&HFFFF")          ' -1
+On Error Resume Next
+Count = 40000
+Debug.Print Err.Number              ' 6
+```
+
+## Arithmetic and overflow
+
+An operation on two **Integer** values gives an **Integer**, and so does an **Integer** combined with a **Byte** or a **Boolean**. Combined with a **Long** it gives a **Long**, and with a **LongLong** a **LongLong**; with a **Single**, a **Single**; with a **Double**, a **Double**. `^` always gives a **Double**, and so does `/` unless the other operand is a **Single** or a **Decimal**. `\` and `Mod` on two **Integer** values give an **Integer**. [Result types and promotion](../../Reference/Operators#result-types-and-promotion) has the full rules.
+
+A result outside the range raises error 6, *Overflow*: adding 1 to 32,767 does, and so does negating -32,768. The type of the result is set by the operands, not by the variable that receives it, so `Total = Count * 200` overflows when `Count` is an **Integer** holding 300, even with `Total` a **Long**, because the literal `200` is an **Integer** too. Writing `200&` makes it a **Long** product.
+
+```tb check_run
+Dim Count As Integer = 300
+Dim Total As Long
+Debug.Print TypeName(Count + Count) ' Integer
+Debug.Print TypeName(Count / Count) ' Double
+Debug.Print Count * 200&            ' 60000
+On Error Resume Next
+Total = Count * 200
+Debug.Print Err.Number              ' 6
+```
+
+[**IntegerOverflowChecks(False)**](Attributes#integeroverflowchecks) turns the overflow check off for a class, a module or a procedure, and an **Integer** result outside the range then wraps around: 32,767 plus 1 is -32,768. Dividing -32,768 by -1 with [`\`](IntegerDivide) raises error 6 even then.
+
+> [!NOTE]
+> A VB6 program compiled to native code raises no error when it negates an **Integer** holding -32,768, or divides it by -1 with `\`. twinBASIC raises error 6 for both.
+
+A **Variant** holding an **Integer** does not overflow: a result that does not fit becomes a **Long**, so a **Variant** holding 32,767 plus 1 holds the **Long** 32,768.
+
+## Example
+
+A Win32 API reports a signed coordinate in a 16-bit field of a **Long**, read as an unsigned value between 0 and 65,535. This example converts such a value to the **Integer** it represents, and back.
+
+```tb check_run
+Dim Raw As Long = 65534
+Dim Coordinate As Integer
+If Raw > 32767 Then
+    Coordinate = Raw - 65536
+Else
+    Coordinate = Raw
+End If
+Debug.Print Coordinate                      ' -2
+Debug.Print Coordinate And &HFFFF&          ' 65534
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#integer) -- the summary of all fourteen intrinsic types
+- [Byte](Byte), [Long](Long), [LongLong](LongLong) -- the other integer types
+- [CInt](../Modules/Conversion/CInt) function
+- [Deftype](Deftype) statement
+- [Result types and promotion](../../Reference/Operators#result-types-and-promotion) -- the type of an arithmetic result
diff --git a/docs/Reference/Data-Types/Long.md b/docs/Reference/Data-Types/Long.md
new file mode 100644
index 000000000..1d0ef290e
--- /dev/null
+++ b/docs/Reference/Data-Types/Long.md
@@ -0,0 +1,138 @@
+---
+title: Long
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 4
+permalink: /tB/Core/Long
+vba_attribution: true
+---
+
+# Long
+{: .no_toc }
+
+A signed 32-bit integer, from -2,147,483,648 to 2,147,483,647.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As Long** [ **=** *expression* ]
+- **Dim** *varname*`&`
+- [**DefLng**](Deftype) *letterrange*
+
+A **Long** variable is declared **As Long**, or by the type-declaration character `&` after its name. [**DefLng**](Deftype) makes **Long** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. A **Long** that is not given a value holds 0.
+
+**Long** is the usual type for counters, indexes and 32-bit Win32 values. The **Variant** subtype for a **Long** is **vbLong** (3), and [**TypeName**](../Modules/Information/TypeName) gives `Long`.
+
+## Storage and range
+
+A **Long** is stored in 4 bytes as a two's-complement number. It holds the whole numbers from -2,147,483,648 to 2,147,483,647. It has the same size in a 32-bit and a 64-bit build; [**LongPtr**](LongPtr) is the type that changes size with the build.
+
+```tb check_run
+Dim Count As Long
+Dim Total&
+Debug.Print Count                   ' 0
+Debug.Print LenB(Count)             ' 4
+Debug.Print TypeName(Total)         ' Long
+Debug.Print VarType(Count) = vbLong ' True
+```
+
+## Literals
+
+A whole-number literal with no type-declaration character is an **Integer** from -32,768 to 32,767, a **Long** beyond that up to 2,147,483,647, and a **Double** beyond that. The minus sign is not part of the literal, so `-32768` is a **Long**: the literal 32768 is a **Long**, then negated. The suffix `&` makes any whole-number literal in the range a **Long**: `5&`.
+
+A hexadecimal (`&H`), octal (`&O`) or binary (`&B`) literal is an **Integer** when its value fits in 16 bits and a **Long** when it fits in 32: `&HFFFF` is the **Integer** -1, and `&H80000000` is the **Long** -2,147,483,648. The suffix `&` makes a 16-bit pattern a positive **Long**: `&HFFFF&` is 65,535. A pattern wider than 32 bits is a [**LongLong**](LongLong). The digits of a `&H`, `&O` or `&B` literal can be grouped with underscores, as in `&HFFFF_0000`; a decimal literal cannot.
+
+```tb check_run
+Debug.Print TypeName(32767)         ' Integer
+Debug.Print TypeName(32768)         ' Long
+Debug.Print TypeName(2147483648)    ' Double
+Debug.Print TypeName(5&)            ' Long
+Debug.Print &HFFFF                  ' -1
+Debug.Print &HFFFF&                 ' 65535
+Debug.Print &H80000000              ' -2147483648
+Debug.Print &B1010_1010&            ' 170
+```
+
+> [!NOTE]
+> Binary literals (`&B`) and the underscores that group digits are twinBASIC additions; VBA and VB6 have `&H` and `&O` only. See [Literals](../../Features/Language/Literals).
+
+## Conversions
+
+[**CLng**](../Modules/Conversion/CLng) converts an expression to a **Long**, and so does assigning one to a **Long** variable. A number with a fraction is rounded to the nearest whole number, and a number exactly halfway between two is rounded to the even one: 2.5 becomes 2, 3.5 becomes 4, and -2.5 becomes -2. A string is read as a number, so `CLng(" 42 ")` is 42, and `True` is -1.
+
+A value outside the range after rounding raises error 6, *Overflow*: `CLng(2147483647.5)` does, because it rounds to 2,147,483,648. A string that is not a number raises error 13, *Type mismatch*.
+
+```tb check_run
+Dim n As Long
+n = 2.5
+Debug.Print n                       ' 2
+n = 3.5
+Debug.Print n                       ' 4
+Debug.Print CLng(-2.5)              ' -2
+Debug.Print CLng(" 42 ")            ' 42
+Debug.Print CLng(True)              ' -1
+```
+
+## Arithmetic and overflow
+
+An operation on two **Long** values gives a **Long**, and so does a **Long** combined with a **Byte**, **Integer** or **Boolean**. Combined with a **LongLong** it gives a **LongLong**; with a **Single** or **Double**, a **Double**; with a **Currency**, a **Currency**; with a **Decimal**, a **Decimal**. `^` always gives a **Double**, and so does `/` unless the other operand is a **Decimal**. `\` and `Mod` on two **Long** values give a **Long**. [Result types and promotion](../../Reference/Operators#result-types-and-promotion) has the full rules.
+
+A result outside the range raises error 6, *Overflow*: adding 1 to 2,147,483,647 does, and so does negating -2,147,483,648. The type of the result is set by the operands, not by the variable that receives it, so `n = i * 200` overflows when `i` is an **Integer** holding 300, even with `n` a **Long**: the product of two **Integer** values is an **Integer**. Writing `200&` makes it a **Long** product.
+
+```tb check_run
+Dim n As Long = 1000
+Dim i As Integer = 300
+Debug.Print TypeName(n + i)         ' Long
+Debug.Print TypeName(n * 1.5)       ' Double
+Debug.Print n * i                   ' 300000
+On Error Resume Next
+n = i * 200
+Debug.Print Err.Number              ' 6
+```
+
+[**IntegerOverflowChecks(False)**](Attributes#integeroverflowchecks) turns the overflow check off for a class, a module or a procedure, and a **Long** result outside the range then wraps around instead: 2,147,483,647 plus 1 is -2,147,483,648. Dividing -2,147,483,648 by -1 with [`\`](IntegerDivide) raises error 6 even then.
+
+```tb check_build slot=module
+[IntegerOverflowChecks(False)]
+Public Function NextHash(ByVal Hash As Long, ByVal Code As Long) As Long
+    Return Hash * 31 + Code     ' NextHash(100000000, 7) is -1194967289
+End Function
+```
+
+> [!NOTE]
+> A VB6 program compiled to native code raises no error when it negates a **Long** holding -2,147,483,648: the result is -2,147,483,648. twinBASIC raises error 6.
+
+A **Variant** holding a **Long** does not overflow: a result that does not fit becomes a **Double**, so a **Variant** holding 2,147,483,647 plus 1 holds the **Double** 2,147,483,648.
+
+## Long and 64-bit code
+
+A **Long** is 4 bytes in a 64-bit build too, so it cannot hold an address or a handle there. Declare a pointer or a handle --- in a [**Declare**](Declare) statement, a **Type** or a variable --- as [**LongPtr**](LongPtr), which is a **Long** in a 32-bit build and a **LongLong** in a 64-bit one. Keep **Long** for values that are 32 bits wide in both, such as a Win32 `DWORD`, `INT` or `BOOL`.
+
+## Example
+
+This example adds the whole numbers from 1 to 100,000. The sum, 5,000,050,000, is too large for a **Long**: a **Long** total raises error 6 when it passes 2,147,483,647, at the 65,536th addition. A **LongLong** total holds it.
+
+```tb check_run
+Dim i As Long
+Dim Wide As LongLong
+For i = 1 To 100000
+    Wide = Wide + i
+Next
+Debug.Print Wide                            ' 5000050000
+
+Dim Total As Long
+On Error GoTo Overflowed
+For i = 1 To 100000
+    Total = Total + i
+Next
+Exit Sub
+Overflowed:
+Debug.Print "Error " & Err.Number & " at " & i  ' Error 6 at 65536
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#long) -- the summary of all fourteen intrinsic types
+- [Integer](Integer), [LongLong](LongLong), [LongPtr](LongPtr) -- the other integer types
+- [CLng](../Modules/Conversion/CLng) function
+- [Deftype](Deftype) statement
+- [Result types and promotion](../../Reference/Operators#result-types-and-promotion) -- the type of an arithmetic result
diff --git a/docs/Reference/Data-Types/LongLong.md b/docs/Reference/Data-Types/LongLong.md
new file mode 100644
index 000000000..e088d1389
--- /dev/null
+++ b/docs/Reference/Data-Types/LongLong.md
@@ -0,0 +1,133 @@
+---
+title: LongLong
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 5
+permalink: /tB/Core/LongLong
+vba_attribution: true
+---
+
+# LongLong
+{: .no_toc }
+
+A signed 64-bit integer, from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As LongLong** [ **=** *expression* ]
+- **Dim** *varname*`^`
+- [**DefLngLng**](Deftype) *letterrange*
+
+A **LongLong** variable is declared **As LongLong**, or by the type-declaration character `^` after its name. [**DefLngLng**](Deftype) makes **LongLong** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. A **LongLong** that is not given a value holds 0.
+
+Use **LongLong** for whole numbers that can pass 2,147,483,647: file sizes, tick counts and 64-bit Win32 values. The **Variant** subtype for a **LongLong** is **vbLongLong** (20), and [**TypeName**](../Modules/Information/TypeName) gives `LongLong`.
+
+> [!NOTE]
+> **LongLong** is available in 32-bit builds as well as 64-bit ones. VBA allows it only in 64-bit builds, and VB6 has no **LongLong**.
+
+## Storage and range
+
+A **LongLong** is stored in 8 bytes as a two's-complement number. It has the same size in a 32-bit and a 64-bit build; [**LongPtr**](LongPtr) is the type that changes size with the build.
+
+```tb check_run
+Dim Big As LongLong
+Dim Ticks^
+Debug.Print Big                     ' 0
+Debug.Print LenB(Big)               ' 8
+Debug.Print TypeName(Ticks)         ' LongLong
+Debug.Print VarType(Big) = vbLongLong ' True
+```
+
+## Literals
+
+A whole-number literal beyond the **Long** range is a **Double**, so `3000000000` is not a **LongLong**. The suffix `^` makes a whole-number literal a **LongLong**: `3000000000^`. It reaches 9,223,372,036,854,775,807^; the literal 9,223,372,036,854,775,808^ is refused with *TB5174, Invalid number*, so write the smallest value as `-9223372036854775807^ - 1` or `&H8000000000000000`.
+
+A hexadecimal (`&H`), octal (`&O`) or binary (`&B`) literal wider than 32 bits is a **LongLong**: `&H100000000` is 4,294,967,296 and `&HFFFFFFFFFFFFFFFF` is -1. The suffix `^` makes a 32-bit pattern a positive **LongLong**: `&HFFFFFFFF^` is 4,294,967,295, where `&HFFFFFFFF` is the **Long** -1. The digits can be grouped with underscores, as in `&HFFFF_FFFF_0000_0000`; a decimal literal cannot be.
+
+```tb check_run
+Debug.Print TypeName(3000000000)    ' Double
+Debug.Print TypeName(3000000000^)   ' LongLong
+Debug.Print TypeName(5^)            ' LongLong
+Debug.Print TypeName(&H100000000)   ' LongLong
+Debug.Print &HFFFFFFFF              ' -1
+Debug.Print &HFFFFFFFF^             ' 4294967295
+Debug.Print &H8000000000000000      ' -9223372036854775808
+```
+
+> [!NOTE]
+> Binary literals (`&B`) and the underscores that group digits are twinBASIC additions; VBA and VB6 have `&H` and `&O` only. See [Literals](../../Features/Language/Literals).
+
+## Conversions
+
+[**CLngLng**](../Modules/Conversion/CLngLng) converts an expression to a **LongLong**, and so does assigning one to a **LongLong** variable. A number with a fraction is rounded to the nearest whole number, and a number exactly halfway between two is rounded to the even one: 2.5 becomes 2 and 3.5 becomes 4. A string is read as a number, so `CLngLng("9223372036854775807")` is the largest value, and `True` is -1.
+
+A value outside the range raises error 6, *Overflow*: `CLngLng(1E+19)` does. A string that is not a number raises error 13, *Type mismatch*.
+
+Assigning a **LongLong** variable to a **Long** or **Integer** variable, as `Small = Big`, is refused at compile time with *TB5001*; write `CLng(Big)` or `CInt(Big)`, which raises error 6 when the value does not fit.
+
+```tb check_run
+Dim Big As LongLong
+Big = 4.5
+Debug.Print Big                     ' 4
+Debug.Print CLngLng(3.5)            ' 4
+Debug.Print CLngLng("9223372036854775807") ' 9223372036854775807
+Debug.Print CLngLng(True)           ' -1
+On Error Resume Next
+Big = CLngLng(1E+19)
+Debug.Print Err.Number              ' 6
+```
+
+> [!WARNING] BETA 1010
+> The constant 2^63, one past the largest **LongLong**, converts to -9223372036854775808 with no error: `CLngLng(2 ^ 63)`, `CLngLng(9223372036854775808#)` and `CLngLng(CSng(2 ^ 63))` all do, and so does `Big = 9223372036854775807`, because a whole-number literal without the `^` suffix is a **Double** that rounds to 2^63. A **Double** variable holding 2^63 raises error 6, and so does `1E+19`. VB6 has no **LongLong**; it raises error 6 for the first **Double** beyond the **Long** range. Write the largest value as `9223372036854775807^`.
+
+> [!NOTE] BETA 1010
+> In a 32-bit build, a constant expression with an operator after the `^`, such as `Big = 2 ^ 63 - 1024`, raises error 6 when its procedure has already converted a constant **Double** that does not fit a **Long** or a **LongLong**: `Small = 2 ^ 31`, `Big = CLngLng(1E+19)` and `Big = CLngLng(2 ^ 63)` all do. The value fits, and the statement stores it; the error is raised once, at the first such expression. A 64-bit build raises no error. A single literal, a single power such as `2 ^ 62` and arithmetic on a variable are not affected. A function whose body is the expression raises the error in the same way when a procedure that calls it holds such a conversion. Write the value as one literal, as in `Big = 9223372036854774784^`; `CLngLng` around the expression does not avoid the error.
+
+## Arithmetic and overflow
+
+An operation on two **LongLong** values gives a **LongLong**, and so does a **LongLong** combined with a **Byte**, **Integer**, **Long** or **Boolean**. Combined with a **Single** or **Double** it gives a **Double**; with a **Currency**, a **Currency**; with a **Decimal**, a **Decimal**. `^` always gives a **Double**, and so does `/` unless the other operand is a **Decimal**. `\` and `Mod` on two **LongLong** values give a **LongLong**. [Result types and promotion](../../Reference/Operators#result-types-and-promotion) has the full rules.
+
+A result outside the range raises error 6, *Overflow*: adding 1 to 9,223,372,036,854,775,807 does, and so does negating the smallest value. [**IntegerOverflowChecks(False)**](Attributes#integeroverflowchecks) turns the overflow check off for a class, a module or a procedure, and a **LongLong** result then wraps around: the largest value plus 1 is -9,223,372,036,854,775,808.
+
+A **Variant** holding a **LongLong** does not overflow: a result that does not fit becomes a **Double**, so a **Variant** holding 9,223,372,036,854,775,807 plus 1 holds the **Double** 9.22337203685478E+18. A **Double** holds integers exactly only up to 2^53, so the result is not exact.
+
+```tb check_run
+Dim Big As LongLong = 3000000000^
+Debug.Print TypeName(Big + 1)       ' LongLong
+Debug.Print TypeName(Big * 1.5)     ' Double
+Debug.Print Big \ 7                 ' 428571428
+Debug.Print Big Mod 7               ' 4
+Debug.Print 1^ << 40                ' 1099511627776
+Dim Wide As Variant = 9223372036854775807^
+Wide = Wide + 1
+Debug.Print TypeName(Wide)          ' Double
+On Error Resume Next
+Big = 9223372036854775807^
+Big = Big + 1
+Debug.Print Err.Number              ' 6
+```
+
+## LongLong and 64-bit code
+
+In a 64-bit build [**LongPtr**](LongPtr) is a **LongLong**, so a pointer or handle that a 64-bit build holds in 8 bytes is declared **LongPtr**, not **LongLong**. Use **LongLong** for values that are 64 bits wide in both builds, such as a Win32 `ULONGLONG`, `LARGE_INTEGER` or `FILETIME` value.
+
+## Example
+
+The Win32 file functions return a file's size as two 32-bit halves. The low half is a **Long**, so a size above 2 GB makes it negative; masking it with `&HFFFFFFFF^` reads it as unsigned, and the sum is a **LongLong**.
+
+```tb check_run
+Dim High As Long = 2
+Dim Low As Long = -1
+Dim Size As LongLong
+Size = High * 4294967296^ + (Low And &HFFFFFFFF^)
+Debug.Print Size                    ' 12884901887
+Debug.Print Size \ 1073741824       ' 11
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#longlong) -- the summary of all fourteen intrinsic types
+- [Long](Long), [LongPtr](LongPtr), [Integer](Integer) -- the other integer types
+- [CLngLng](../Modules/Conversion/CLngLng) function
+- [Deftype](Deftype) statement
+- [Result types and promotion](../../Reference/Operators#result-types-and-promotion) -- the type of an arithmetic result
diff --git a/docs/Reference/Data-Types/LongPtr.md b/docs/Reference/Data-Types/LongPtr.md
new file mode 100644
index 000000000..a345fde10
--- /dev/null
+++ b/docs/Reference/Data-Types/LongPtr.md
@@ -0,0 +1,86 @@
+---
+title: LongPtr
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 6
+permalink: /tB/Core/LongPtr
+vba_attribution: true
+---
+
+# LongPtr
+{: .no_toc }
+
+A signed integer as wide as a pointer: a **Long** in a 32-bit build and a **LongLong** in a 64-bit build.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As LongPtr** [ **=** *expression* ]
+- [**DefLngPtr**](Deftype) *letterrange*
+
+A **LongPtr** variable is declared **As LongPtr**. [**DefLngPtr**](Deftype) makes **LongPtr** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. **LongPtr** has no type-declaration character. A **LongPtr** that is not given a value holds 0.
+
+Use **LongPtr** for pointers and handles: window handles, memory addresses, and the parameters and results of API functions that take them. The same source then builds for 32-bit and 64-bit targets.
+
+## Storage and range
+
+A **LongPtr** is 4 bytes in a 32-bit build, with the range of a **Long**, and 8 bytes in a 64-bit build, with the range of a **LongLong**. It is not a type of its own at run time: [**TypeName**](../Modules/Information/TypeName) gives `Long` in a 32-bit build and `LongLong` in a 64-bit one, and [**VarType**](../Modules/Information/VarType) gives **vbLong** (3) or **vbLongLong** (20). There is no **vbLongPtr** constant. A **Variant** that holds a **LongPtr** holds a **Long** or a **LongLong**.
+
+The constant `vbPtrSize` is the size of a pointer in the build, 4 or 8, and [**VarPtr**](../Modules/Information/VarPtr), [**StrPtr**](../Modules/Information/StrPtr) and [**ObjPtr**](../Modules/Information/ObjPtr) return a **LongPtr**.
+
+```tb check_run
+Dim Ptr As LongPtr
+Dim Value As Long = 42
+Debug.Print Ptr                     ' 0
+Debug.Print LenB(Ptr) = vbPtrSize   ' True
+Debug.Print TypeName(Ptr) = IIf(vbPtrSize = 8, "LongLong", "Long") ' True
+Ptr = VarPtr(Value)
+Debug.Print Ptr <> 0                ' True
+```
+
+## Literals
+
+There is no **LongPtr** literal and no type-declaration character. Assign a number or the result of a pointer function, or convert with [**CLngPtr**](../Modules/Conversion/CLngPtr): `Dim Ptr As LongPtr = 5`.
+
+## Conversions
+
+[**CLngPtr**](../Modules/Conversion/CLngPtr) converts an expression to a **LongPtr**, and so does assigning one to a **LongPtr** variable. A value outside the range raises error 6, *Overflow*, and the range depends on the build: `CLngPtr(2147483648#)` raises it in a 32-bit build and returns 2,147,483,648 in a 64-bit one.
+
+A **LongPtr** variable assigned to a **Long** variable, as `Size = Ptr`, compiles in a 32-bit build and is refused in a 64-bit build with *TB5001*, because a **LongLong** does not narrow implicitly to a **Long**. Write `CLng(Ptr)` where a 32-bit value is wanted, which raises error 6 in a 64-bit build when the address does not fit.
+
+## Arithmetic
+
+Arithmetic on a **LongPtr** follows the type it stands for in the build: **Long** in a 32-bit build and **LongLong** in a 64-bit one. Adding a **Long** to a **LongPtr** gives a **LongPtr**, so `Ptr + 4` can step a pointer by 4 bytes, and a result that does not fit raises error 6. See [Result types and promotion](../../Reference/Operators#result-types-and-promotion).
+
+## LongPtr in Declare statements
+
+A [**Declare**](Declare) statement that is to work in both builds is written with the `PtrSafe` keyword, and declares every pointer and handle **As LongPtr**, as the example below does.
+
+> [!NOTE]
+> VB6 has no **LongPtr**: a VB6 **Declare** passes handles and pointers as **Long**.
+
+## Example
+
+This example declares a Win32 function that returns the handle of a window, and the one that copies memory. The same code builds for 32-bit and 64-bit: the handle and the address are **LongPtr**, and the length is the pointer-sized `SIZE_T`.
+
+```tb check_build slot=module
+Private Declare PtrSafe Function GetForegroundWindow Lib "user32" () As LongPtr
+Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
+    (ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As LongPtr)
+
+Public Function CopyLong(ByVal Source As Long) As Long
+    CopyMemory VarPtr(CopyLong), VarPtr(Source), LenB(Source)
+End Function
+
+Public Function IsAnyWindowActive() As Boolean
+    IsAnyWindowActive = GetForegroundWindow() <> 0
+End Function
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#longptr) -- the summary of all fourteen intrinsic types
+- [Long](Long), [LongLong](LongLong) -- the types a **LongPtr** stands for
+- [CLngPtr](../Modules/Conversion/CLngPtr) function
+- [Declare](Declare) statement
+- [Deftype](Deftype) statement
+- [New Data Types](../../Features/Language/Data-Types) -- the new types in twinBASIC
diff --git a/docs/Reference/Data-Types/Object.md b/docs/Reference/Data-Types/Object.md
new file mode 100644
index 000000000..56934977d
--- /dev/null
+++ b/docs/Reference/Data-Types/Object.md
@@ -0,0 +1,100 @@
+---
+title: Object
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 14
+permalink: /tB/Core/Object
+vba_attribution: true
+---
+
+# Object
+{: .no_toc }
+
+A reference to any object, whose members are looked up when the program runs.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As Object**
+- [**Set**](Set) *varname* **=** *objectexpression*
+- [**DefObj**](Deftype) *letterrange*
+
+An **Object** variable is declared **As Object**. [**DefObj**](Deftype) makes **Object** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. **Object** has no type-declaration character. An **Object** variable that is not given a value holds **Nothing**.
+
+An **Object** variable can refer to an object of any class, from a twinBASIC class, a control or a COM component such as `Scripting.Dictionary`. Declaring a variable with a specific class or interface type instead, `Dim Items As Collection`, lets the compiler check the member names when it compiles the program. [**VarType**](../Modules/Information/VarType) of an object variable is **vbObject** (9), and [**TypeName**](../Modules/Information/TypeName) gives the name of the object's class, or `Nothing`.
+
+## Storage and range
+
+An **Object** variable holds a reference to an object, not the object. It takes 4 bytes in a 32-bit build and 8 bytes in a 64-bit build. Two variables can refer to the same object; [**ObjPtr**](../Modules/Information/ObjPtr) gives the address of the object a variable refers to, 0 for **Nothing**.
+
+```tb check_run
+Dim Item As Object
+Debug.Print TypeName(Item)          ' Nothing
+Debug.Print VarType(Item) = vbObject ' True
+Debug.Print Item Is Nothing         ' True
+Debug.Print ObjPtr(Item)            ' 0
+Set Item = New Collection
+Debug.Print TypeName(Item)          ' Collection
+Debug.Print Item Is Nothing         ' False
+Debug.Print IsObject(Item)          ' True
+```
+
+## Literals
+
+**Nothing** is the only **Object** literal: it is a reference to no object. An object is made with [**New**](New), or by a function such as `CreateObject`.
+
+## Assigning
+
+An object reference is assigned with [**Set**](Set): `Set Item = New Collection`. Without `Set` the assignment goes to the object's default member instead, and fails with error 91, *Object variable or With block variable not set*, when the variable is **Nothing**. `Set Item = Nothing` releases the reference.
+
+[**Is**](Is) compares two references and tells whether they refer to the same object: `A Is B`, and `A Is Nothing` for a variable with no object.
+
+A reference can be assigned to a variable of a more specific type, `Set Items = Item`, which checks that the object supports that type.
+
+> [!NOTE] BETA 1010
+> Assigning an object to a variable of a class or interface it does not support raises the error -2147467262, the **HRESULT** `E_NOINTERFACE`, *No such interface supported*. VB6 raises error 13, *Type mismatch*. Test the object first with `If TypeOf Item Is Collection Then`, or have the handler accept both numbers.
+
+## Late binding
+{: index_also="late binding" }
+
+The members of an **Object** variable are not known when the program is compiled. Each call is looked up by name through **IDispatch** when the statement runs, which is *late binding* ([**IDispatch**](../../Reference/COM-Interfaces/IDispatch#late-binding-in-twinbasic) describes it). A member that the object does not have raises error 438, *Object doesn't support this property or method*, at that moment, and a member called on **Nothing** raises error 91. A variable declared with a specific class is bound when the program is compiled, and a member the class does not have is a compile error.
+
+```tb check_run
+Dim Table As Object
+Dim Count As Long
+Set Table = CreateObject("Scripting.Dictionary")
+Table.Add "first", 1
+Debug.Print Table.Count             ' 1
+Debug.Print Table("first")          ' 1
+On Error Resume Next
+Table.NoSuchMember
+Debug.Print Err.Number              ' 438
+Err.Clear
+Set Table = Nothing
+Count = Table.Count
+Debug.Print Err.Number              ' 91
+```
+
+## Example
+
+This example keeps a dictionary in an **Object** variable and tells whether two variables refer to the same dictionary.
+
+```tb check_run
+Dim A As Object
+Dim B As Object
+Set A = CreateObject("Scripting.Dictionary")
+Set B = A
+B.Add "key", "value"
+Debug.Print A.Count                 ' 1
+Debug.Print A Is B                  ' True
+Set B = CreateObject("Scripting.Dictionary")
+Debug.Print A Is B                  ' False
+Debug.Print B.Count                 ' 0
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#object) -- the summary of all fourteen intrinsic types
+- [Variant](Variant) type -- a **Variant** can hold an object reference too
+- [Set](Set), [New](New), [Is](Is) -- statements and operators for object references
+- [IDispatch](../../Reference/COM-Interfaces/IDispatch) -- how late-bound calls reach an object
+- [Deftype](Deftype) statement
diff --git a/docs/Reference/Data-Types/Single.md b/docs/Reference/Data-Types/Single.md
new file mode 100644
index 000000000..d203d5f03
--- /dev/null
+++ b/docs/Reference/Data-Types/Single.md
@@ -0,0 +1,120 @@
+---
+title: Single
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 7
+permalink: /tB/Core/Single
+vba_attribution: true
+---
+
+# Single
+{: .no_toc }
+
+A 32-bit IEEE 754 single-precision floating-point number.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As Single** [ **=** *expression* ]
+- **Dim** *varname*`!`
+- [**DefSng**](Deftype) *letterrange*
+
+A **Single** variable is declared **As Single**, or by the type-declaration character `!` after its name. [**DefSng**](Deftype) makes **Single** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. A **Single** that is not given a value holds 0.
+
+A **Single** has about 7 significant decimal digits. It takes half the space of a [**Double**](Double), which makes it suited to large arrays of approximate values and to API structures with 4-byte floating-point fields. It is not suited to money or to results that depend on small differences. The **Variant** subtype for a **Single** is **vbSingle** (4), and [**TypeName**](../Modules/Information/TypeName) gives `Single`.
+
+## Storage and range
+
+A **Single** is stored in 4 bytes: a sign bit, an 8-bit exponent and a 23-bit fraction. It holds values from -3.402823E38 to -1.401298E-45 and from 1.401298E-45 to 3.402823E38, and 0. It has the same size in a 32-bit and a 64-bit build. A value too small for the type becomes 0 with no error.
+
+Most decimal fractions are not stored exactly, and a **Single** keeps only about 7 digits of them. `0.1` is stored as the nearest **Single**, which is about 0.100000001490116 when read as a **Double**, and 16,777,216 plus 1 is 16,777,216 again, because the type has no room for the 1.
+
+```tb check_run
+Dim Ratio As Single
+Dim Level!
+Debug.Print Ratio                   ' 0
+Debug.Print LenB(Ratio)             ' 4
+Debug.Print TypeName(Level)         ' Single
+Debug.Print VarType(Ratio) = vbSingle ' True
+Ratio = 0.1
+Debug.Print CDbl(Ratio)             ' 0.100000001490116
+Debug.Print CSng(1 / 3)             ' 0.3333333
+Dim Big As Single = 16777216
+Debug.Print Big + 1 = Big           ' True
+```
+
+## Literals
+
+A number with a decimal point or an exponent is a **Double**, so `1.5` and `1E3` are **Double** values. The suffix `!` makes a literal a **Single**: `1.5!`, `1E3!` and `1!`. A **Single** literal can be written with an exponent: `3.402823E+38!`.
+
+```tb check_run
+Debug.Print TypeName(1.5)           ' Double
+Debug.Print TypeName(1.5!)          ' Single
+Debug.Print TypeName(1!)            ' Single
+Debug.Print TypeName(1E3!)          ' Single
+Debug.Print 3.402823E+38!           ' 3.402823E+38
+Debug.Print 1.401298E-45!           ' 1.401298E-45
+```
+
+## Conversions
+
+[**CSng**](../Modules/Conversion/CSng) converts an expression to a **Single**, and so does assigning one to a **Single** variable. A string is read as a number, so `CSng("1.5")` is 1.5, and `True` is -1. A string that is not a number raises error 13, *Type mismatch*.
+
+A value too large for the type raises error 6, *Overflow*: `CSng(1E+39)` does, and so does assigning a **Double** that is too large, as `Ratio = 1E+300`.
+> [!NOTE]
+> VB6 raises no error when a **Double** that is too large is assigned to a **Single**: the **Single** then holds infinity (`1.#INF`). twinBASIC raises error 6.
+
+```tb check_run
+Dim Ratio As Single
+Debug.Print CSng("1.5")             ' 1.5
+Debug.Print CSng(True)              ' -1
+On Error Resume Next
+Ratio = 1E+39
+Debug.Print Err.Number              ' 6
+Err.Clear
+Ratio = CSng("abc")
+Debug.Print Err.Number              ' 13
+```
+
+## Arithmetic and overflow
+
+An operation on two **Single** values gives a **Single**, and so does a **Single** combined with a **Byte**, **Integer** or **Boolean**, with `/` as well as with `+`, `-` and `*`. Combined with a **Long** or **LongLong** it gives a **Double**, and so it does with a **Double**; with a **Currency**, a **Currency** under `+` and `-` and a **Double** under `*` and `/`; with a **Decimal**, a **Decimal**. `^` always gives a **Double**, and `\` and `Mod` give a **Long** or a wider integer type. [Result types and promotion](../../Reference/Operators#result-types-and-promotion) has the full rules.
+
+```tb check_run
+Dim Ratio As Single = 0.5
+Debug.Print TypeName(Ratio + Ratio) ' Single
+Debug.Print TypeName(Ratio * 2)     ' Single
+Debug.Print TypeName(Ratio / 2)     ' Single
+Debug.Print TypeName(Ratio + 1&)    ' Double
+Debug.Print TypeName(Ratio ^ 2)     ' Double
+Debug.Print TypeName(Ratio \ 1)     ' Long
+Debug.Print 0.1! + 0.2! = 0.3!      ' True
+Debug.Print 0.1 + 0.2 = 0.3         ' False
+```
+
+> [!WARNING] BETA 1010
+> An addition, subtraction, multiplication or division whose result is a declared **Single** raises no error when the result overflows: the **Single** holds infinity (`1.#INF`) instead. VB6 raises error 6, *Overflow*. Declare the variable **Double** where an overflow must be caught. Code compiled with LLVM has a similar defect for **Double**; see [Language support](../../LLVM/Getting-Started#language-support).
+
+A **Variant** holding a **Single** does not overflow: a result that does not fit becomes a **Double**.
+
+## Example
+
+This example adds 0.1 to a **Single** ten times. The total prints as 1 but is not equal to 1, so a comparison of floating-point values should allow a small difference.
+
+```tb check_run
+Dim Total As Single
+Dim i As Long
+For i = 1 To 10
+    Total = Total + 0.1!
+Next
+Debug.Print Total                           ' 1
+Debug.Print Total = 1                       ' False
+Debug.Print Abs(Total - 1) < 0.000001       ' True
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#single) -- the summary of all fourteen intrinsic types
+- [Double](Double), [Currency](Currency), [Decimal](Decimal) -- the other non-integer types
+- [CSng](../Modules/Conversion/CSng) function
+- [Deftype](Deftype) statement
+- [Result types and promotion](../../Reference/Operators#result-types-and-promotion) -- the type of an arithmetic result
diff --git a/docs/Reference/Data-Types/String.md b/docs/Reference/Data-Types/String.md
new file mode 100644
index 000000000..fa9085ce0
--- /dev/null
+++ b/docs/Reference/Data-Types/String.md
@@ -0,0 +1,140 @@
+---
+title: String
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 12
+permalink: /tB/Core/String
+vba_attribution: true
+---
+
+# String
+{: .no_toc }
+
+A sequence of Unicode characters, of variable or of fixed length.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* **As String** [ **=** *expression* ]
+- [**Dim**](Dim) *varname* **As String \*** *length*
+- **Dim** *varname*`$`
+- [**DefStr**](Deftype) *letterrange*
+
+A variable-length **String** variable is declared **As String**, or by the type-declaration character `$` after its name. A fixed-length one is declared **As String \*** *length*, where *length* is the number of characters. [**DefStr**](Deftype) makes **String** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*. A variable-length **String** that is not given a value holds the null string, which is empty; a fixed-length one holds spaces.
+
+The **Variant** subtype for a **String** is **vbString** (8), and [**TypeName**](../Modules/Information/TypeName) gives `String`. This page describes the type. The [**String**](../Modules/Strings/String) function builds a string of one repeated character.
+
+## Storage and range
+
+A variable-length **String** is a COM `BSTR`: a pointer to the characters, which are stored as UTF-16, 2 bytes each. [**Len**](../Modules/Strings/Len) counts characters, and **LenB** counts bytes, so `Len` of `"abc"` is 3 and `LenB` is 6. A character outside the Basic Multilingual Plane takes two UTF-16 units, and `Len` counts both. A variable-length **String** holds up to about 2 billion characters, limited in practice by memory.
+
+A **String** variable that was never assigned holds the null string, a null pointer: `Len` is 0 and [**StrPtr**](../Modules/Information/StrPtr) is 0. `vbNullString` is the null string, and `""` is an empty string with a pointer to an empty buffer. The two compare equal and most code treats them alike; the difference matters for an API that distinguishes a null pointer from an empty string.
+
+```tb check_run
+Dim Text As String
+Dim Title$
+Debug.Print Len(Text)               ' 0
+Debug.Print StrPtr(Text)            ' 0
+Debug.Print TypeName(Title)         ' String
+Debug.Print VarType(Text) = vbString ' True
+Text = "abc"
+Debug.Print Len(Text)               ' 3
+Debug.Print LenB(Text)              ' 6
+Text = ""
+Debug.Print StrPtr(Text) <> 0       ' True
+Debug.Print Text = vbNullString     ' True
+Dim Pair As String = ChrW(&HD83D) & ChrW(&HDE00)
+Debug.Print Len(Pair)               ' 2
+```
+
+## Fixed-length strings
+
+A fixed-length **String** always has the length it is declared with. A value shorter than that is padded on the right with spaces when it is assigned, and a longer one is cut off. `LenB` is twice the length, and a fixed-length member of a **Type** takes that many bytes. Join a fixed-length string to another with `&`, and the result is a variable-length **String**.
+
+```tb check_run
+Dim Code As String * 3
+Debug.Print "[" & Code & "]"        ' [   ]
+Code = "a"
+Debug.Print "[" & Code & "]"        ' [a  ]
+Code = "abcdef"
+Debug.Print "[" & Code & "]"        ' [abc]
+Debug.Print Len(Code)               ' 3
+Debug.Print LenB(Code)              ' 6
+```
+
+> [!NOTE]
+> A fixed-length **String** can have more characters in twinBASIC than in VB6, whose limit is 65,526: `String * 1000000` compiles and runs. VB6 refuses a longer one with the error *Invalid length for fixed-length string*.
+
+## Literals
+
+A string literal is text between double quotation marks, and its type is **String**. To put a double quotation mark inside it, write two in a row; `Chr$(34)` gives the same character. There is no type-declaration character for a literal.
+
+```tb check_run
+Debug.Print "say ""hi"""            ' say "hi"
+Debug.Print "say " & Chr$(34) & "hi" & Chr$(34) ' say "hi"
+Debug.Print Len("a""b")             ' 3
+```
+
+## Conversions
+
+[**CStr**](../Modules/Conversion/CStr) converts an expression to a **String**, and so does assigning one to a **String** variable: a number gives its digits, and `True` gives `True`. **Empty** gives an empty string. **Null** raises error 94, *Invalid use of Null*, for the assignment and for **CStr**.
+
+A string assigned to a number is read as a number: spaces at both ends are ignored, and `&H` introduces a hexadecimal number. An empty string and a string that is not a number raise error 13, *Type mismatch*.
+
+```tb check_run
+Dim Text As String
+Dim Number As Long
+Text = 5
+Debug.Print Text & "|" & TypeName(Text) ' 5|String
+Debug.Print CStr(True)              ' True
+Number = " 12 "
+Debug.Print Number                  ' 12
+Number = "&H10"
+Debug.Print Number                  ' 16
+On Error Resume Next
+Number = "abc"
+Debug.Print Err.Number              ' 13
+Err.Clear
+Text = Null
+Debug.Print Err.Number              ' 94
+```
+
+## Joining and comparing
+
+`&` joins two values into a **String**, whatever their types; a **Null** operand counts as an empty string. `+` joins two **String** operands too, but a **String** beside a number is converted to a number and added: `"34" + 6` is the **Double** 40, where `"34" & 6` is `346`. A comparison of two strings is case-sensitive by default: `"a" = "A"` is **False**. See [Result types and promotion](../../Reference/Operators#result-types-and-promotion) for `+`, `-` and `*` on strings.
+
+A function that has a `$` form, such as [**Left**](../Modules/Strings/Left) and `Left$`, returns a **String** in the `$` form, and a **Variant** in the other: `Left(Null, 1)` is **Null**.
+
+```tb check_run
+Debug.Print "34" + "6"              ' 346
+Debug.Print "34" + 6                ' 40
+Debug.Print TypeName("34" + 6)      ' Double
+Debug.Print "34" & 6                ' 346
+Debug.Print TypeName("a" & Null)    ' String
+Debug.Print "a" = "A"               ' False
+Debug.Print TypeName(Left$("abc", 1)) ' String
+Debug.Print TypeName(Left(Null, 1)) ' Null
+```
+
+## Example
+
+This example reads a fixed-width record of a 10-character surname followed by a 4-digit quantity.
+
+```tb check_run
+Dim Record As String = "Smith     0042"
+Dim Surname As String * 10
+Dim Quantity As String * 4
+Surname = Record
+Quantity = Mid$(Record, 11)
+Debug.Print "[" & RTrim$(Surname) & "]" ' [Smith]
+Debug.Print CLng(Quantity) + 1          ' 43
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#string) -- the summary of all fourteen intrinsic types
+- [String](../Modules/Strings/String) function -- builds a string of one repeated character
+- [Byte](Byte) type -- byte arrays hold the bytes of a string
+- [CStr](../Modules/Conversion/CStr) function
+- [Len](../Modules/Strings/Len), [StrPtr](../Modules/Information/StrPtr) -- the length and the address of a string
+- [Deftype](Deftype) statement
+- [Result types and promotion](../../Reference/Operators#result-types-and-promotion) -- the type of an arithmetic result
diff --git a/docs/Reference/Data-Types/Variant.md b/docs/Reference/Data-Types/Variant.md
new file mode 100644
index 000000000..d396b9ac4
--- /dev/null
+++ b/docs/Reference/Data-Types/Variant.md
@@ -0,0 +1,173 @@
+---
+title: Variant
+parent: Data Types
+grand_parent: Reference Section
+nav_order: 13
+permalink: /tB/Core/Variant
+vba_attribution: true
+---
+
+# Variant
+{: .no_toc }
+
+A value of any type, which carries its own type with it.
+
+Syntax:
+
+- [**Dim**](Dim) *varname* [ **As Variant** ] [ **=** *expression* ]
+- [**DefVar**](Deftype) *letterrange*
+
+A variable declared with no type is a **Variant**, and so is one declared **As Variant**. [**DefVar**](Deftype) makes **Variant** the default type of the variables, parameters and functions whose names start with the letters in *letterrange*; with no **Def** statement it already is. **Variant** has no type-declaration character. A **Variant** that is not given a value holds **Empty**.
+
+A **Variant** can hold a number of any numeric type, a **Date**, a **String**, a **Boolean**, an object reference or an array, and the special values **Empty**, **Null** and an error value. A fixed-length **String** assigned to a **Variant** is held as a variable-length one. [**VarType**](../Modules/Information/VarType) and [**TypeName**](../Modules/Information/TypeName) report which type a **Variant** holds at the moment; a **Variant** can hold a different type after each assignment. The **Variant** subtype for a **Variant** itself, **vbVariant** (12), appears only in the type of an array of **Variant**.
+
+> [!NOTE]
+> In `Dim a, b As Long` only `b` is a **Long**. `a` has no `As` clause of its own, so it is a **Variant**. Write `Dim a As Long, b As Long`.
+
+## Storage and range
+
+A **Variant** takes 16 bytes in a 32-bit build and 24 bytes in a 64-bit build. [**LenB**](../Modules/Strings/Len) of a **Variant** variable is not its size: it gives the length in bytes of the value it holds, as a **String**, so `LenB(CVar(5))` is 2.
+
+A **Variant** operates on its contents the way the contents' own type does, with a few differences set out under [Arithmetic](#arithmetic). A **Variant** takes more space and more time for each operation than a variable of a specific type, since the type is checked at run time: use a typed variable where the type is known.
+
+The type tags, as [**VarType**](../Modules/Information/VarType) returns them:
+
+| Constant | Value | The **Variant** holds |
+|:--|--:|:--|
+| **vbEmpty** | 0 | nothing assigned (**Empty**) |
+| **vbNull** | 1 | **Null**, no valid data |
+| **vbInteger** | 2 | an **Integer** |
+| **vbLong** | 3 | a **Long** |
+| **vbSingle** | 4 | a **Single** |
+| **vbDouble** | 5 | a **Double** |
+| **vbCurrency** | 6 | a **Currency** |
+| **vbDate** | 7 | a **Date** |
+| **vbString** | 8 | a **String** |
+| **vbObject** | 9 | an object reference, or **Nothing** |
+| **vbError** | 10 | an error value |
+| **vbBoolean** | 11 | a **Boolean** |
+| **vbVariant** | 12 | only in an array of **Variant** |
+| **vbDataObject** | 13 | a data-access object |
+| **vbDecimal** | 14 | a **Decimal** |
+| **vbByte** | 17 | a **Byte** |
+| **vbLongLong** | 20 | a **LongLong** |
+| **vbArray** | 8192 | added to another value: an array of that type |
+
+A **LongPtr** is held as a **Long** or a **LongLong** according to the build. [**VbVarType**](../Modules/Constants/VbVarType) lists the constants.
+
+```tb check_run
+Dim Item As Variant
+For Each Item In Array(1, 40000, 3000000000#, 1.5!, 1.5, 1.5@, #1/1/2000#, "s", True, CByte(1), CDec(1), 3000000000^)
+    Debug.Print TypeName(Item) & " " & VarType(Item)
+Next
+' Output:
+' Integer 2
+' Long 3
+' Double 5
+' Single 4
+' Double 5
+' Currency 6
+' Date 7
+' String 8
+' Boolean 11
+' Byte 17
+' Decimal 14
+' LongLong 20
+```
+
+## Literals
+
+A **Variant** has no literal of its own. A literal assigned to a **Variant** keeps its type, so `v = 1` holds an **Integer** and `v = 40000` a **Long**. The keywords **Empty** and **Null** are the **Variant** values for no value and for no valid data. [**CVar**](../Modules/Conversion/CVar) converts an expression to a **Variant**; the conversion functions such as [**CLng**](Long#conversions) and [**CDec**](Decimal#conversions) set the subtype.
+
+## Empty, Null and error values
+
+**Empty** is what a **Variant** holds before it is assigned. It counts as 0 in a numeric expression and as an empty string in a string expression, and `Empty = 0` and `Empty = ""` are both **True**. Test for it with [**IsEmpty**](../Modules/Information/IsEmpty).
+
+**Null** means that the **Variant** holds no valid data. A numeric expression with a **Null** operand is **Null**, a comparison with one is **Null**, and `&` treats it as an empty string. Test for it with [**IsNull**](../Modules/Information/IsNull); `v = Null` is never **True**. Assigning **Null** to a variable of another type raises error 94, *Invalid use of Null*.
+
+An error value is made with [**CVErr**](../Modules/Conversion/CVErr) and tested with [**IsError**](../Modules/Information/IsError). It does not raise an error when it is assigned or passed on, which lets a function return the failure as its result. Arithmetic with one raises error 13, *Type mismatch*.
+
+```tb check_run
+Dim v As Variant
+Debug.Print IsEmpty(v)              ' True
+Debug.Print v + 1                   ' 1
+Debug.Print "[" & v & "]"           ' []
+Debug.Print v = 0                   ' True
+v = Null
+Debug.Print IsNull(v)               ' True
+Debug.Print IsNull(v + 1)           ' True
+Debug.Print "x" & v                 ' x
+Debug.Print TypeName(v = v)         ' Null
+v = CVErr(5)
+Debug.Print IsError(v)              ' True
+Debug.Print TypeName(v)             ' Error
+Debug.Print CLng(v)                 ' 5
+```
+
+## Arithmetic
+
+A **Variant** holding a number is used as a number: `+`, `-` and `*` give the result type that the types it holds would. When the operand is a string that can be read as a number the result is a number, so a **Variant** holding 5 plus a **Variant** holding the text `"3"` is the **Double** 8, whereas `&` always joins. Two **Variant** values that hold text are joined by `+`.
+
+A result that does not fit the type is widened rather than raising an error, up to a **Double**: a **Variant** holding the **Byte** 255 times itself is the **Long** 65,025. A **Double**, a **Currency** or a **Decimal** result that does not fit raises error 6. [Result types and promotion](../../Reference/Operators#variant-operands) has the rules for each operator.
+
+```tb check_run
+Dim a As Variant = 5
+Dim b As Variant = "3"
+Debug.Print a + b                   ' 8
+Debug.Print TypeName(a + b)         ' Double
+Debug.Print a & b                   ' 53
+Debug.Print CVar("5") + CVar("3")   ' 53
+Dim n As Variant = CByte(255)
+Debug.Print TypeName(n * n)         ' Long
+Debug.Print n * n                   ' 65025
+```
+
+## Arrays and objects
+
+An array held in a **Variant** reports `vbArray` added to the type of its elements: `Array(1, 2)` holds an array of **Variant** (8,204), and an array of **Integer** assigned to a **Variant** is 8,194. `UBound` and `IsArray` work on the **Variant** directly. An object is stored with `Set`: `Set v = obj` makes `VarType` 9 and `TypeName` the name of the object's class, and `Set v = Nothing` leaves a **Variant** that still reports `vbObject`. Without `Set`, `v = obj` reads the object's default member instead.
+
+```tb check_run
+Dim v As Variant
+Dim Numbers(1 To 2) As Integer
+Dim Items As New Collection
+v = Array(1, 2)
+Debug.Print VarType(v)              ' 8204
+Debug.Print TypeName(v)             ' Variant()
+Debug.Print UBound(v)               ' 1
+v = Numbers
+Debug.Print VarType(v)              ' 8194
+Set v = Items
+Debug.Print TypeName(v)             ' Collection
+Debug.Print IsObject(v)             ' True
+```
+
+## Example
+
+This example adds up a list of values from a worksheet-like source, which can hold numbers, text that holds numbers, empty cells and missing data. A **Variant** takes each of them, and a test for **Null** keeps it from spoiling the total.
+
+```tb check_run
+Dim Total As Double
+Dim Item As Variant
+For Each Item In Array(10, "20", Null, Empty, 2.5)
+    If IsNull(Item) Then
+        Debug.Print "skipped a Null"
+    Else
+        Total = Total + Item
+    End If
+Next
+Debug.Print Total
+' Output:
+' skipped a Null
+' 32.5
+```
+
+## See Also
+
+- [Data Types](../../Reference/Data-Types#variant) -- the summary of all fourteen intrinsic types
+- [VarType](../Modules/Information/VarType), [TypeName](../Modules/Information/TypeName) -- the type a **Variant** holds
+- [VbVarType](../Modules/Constants/VbVarType) -- the subtype constants
+- [IsEmpty](../Modules/Information/IsEmpty), [IsNull](../Modules/Information/IsNull), [IsError](../Modules/Information/IsError), [IsArray](../Modules/Information/IsArray), [IsObject](../Modules/Information/IsObject) -- tests for what a **Variant** holds
+- [CVar](../Modules/Conversion/CVar), [CVErr](../Modules/Conversion/CVErr) functions
+- [Object](Object) type
+- [Deftype](Deftype) statement
+- [Variant operands](../../Reference/Operators#variant-operands) -- how operators treat a **Variant**
diff --git a/docs/Reference/Default/VBA/Strings/Len.md b/docs/Reference/Default/VBA/Strings/Len.md
index 4dd5913b6..89cc00293 100644
--- a/docs/Reference/Default/VBA/Strings/Len.md
+++ b/docs/Reference/Default/VBA/Strings/Len.md
@@ -28,6 +28,9 @@ One (and only one) of the two possible arguments must be specified. With user-de
 > [!NOTE]
 > **Len** may not be able to determine the actual number of storage bytes required when used with variable-length strings in user-defined data types.
 
+> [!WARNING] BETA 1010
+> **Len** and **LenB** accept an array as *varname* and return a number that is not its size in bytes: 40 for a fixed-size one-dimensional array of any element type or length in a 32-bit build (48 in a 64-bit build), 48 (56) for two dimensions, and 4 (8) for a dynamic array. VB6 refuses an array here when the program is compiled. Multiply the element count by the size of one element instead: `(UBound(Buffer) - LBound(Buffer) + 1) * LenB(Buffer(LBound(Buffer)))`.
+
 ### Example
 
 This example uses **Len** to return the number of characters in a string or the number of bytes required to store a variable. The `Type...End Type` block defining `CustomerRecord` must be preceded by the keyword **Private** if it appears in a class module. In a standard module, a **Type** statement can be **Public**.
diff --git a/docs/Reference/Glossary.md b/docs/Reference/Glossary.md
index 5ea8b6dcd..f37538912 100644
--- a/docs/Reference/Glossary.md
+++ b/docs/Reference/Glossary.md
@@ -215,7 +215,7 @@ Dates are stored as part of a real number. Values to the left of the decimal rep
 
 ## date literal
 
-Any sequence of characters with a valid format that is surrounded by number signs (`#`): a date, a time of day, or both, such as `#2026-03-17#`, `#1:45 PM#` or `#3/17/2026 13:45:30#`. The compiler converts a date literal to its **Date** value when it builds the project. See [Date literals](../Reference/Data-Types#date-literals) for the forms it accepts and how it reads their parts.
+Any sequence of characters with a valid format that is surrounded by number signs (`#`): a date, a time of day, or both, such as `#2026-03-17#`, `#1:45 PM#` or `#3/17/2026 13:45:30#`. The compiler converts a date literal to its **Date** value when it builds the project. See [Date literals](../tB/Core/Date-Type#date-literals) for the forms it accepts and how it reads their parts.
 
 ## date separators
 
diff --git a/docs/_book.yml b/docs/_book.yml
index fe007d441..5e21fb1f1 100644
--- a/docs/_book.yml
+++ b/docs/_book.yml
@@ -297,6 +297,11 @@ parts:
         page: /tB/IDE/Debugging/
         landing_is_target: true
         outline_closed: true
+      - title: Building
+        landing_page: /tB/IDE/Building/
+        page: /tB/IDE/Building/
+        landing_is_target: true
+        outline_closed: true
       - title: Customising the IDE
         landing_page: /tB/IDE/Customising/
         page: /tB/IDE/Customising/
@@ -343,7 +348,7 @@ parts:
         nav_page: Reference Section/Operators
         outline_closed: true
       - title: Data Types
-        page: /Reference/Data-Types
+        nav_page: Reference Section/Data Types
         no_outline_entry: true
         no_heading_shift: true
         outline_closed: true
@@ -509,6 +514,13 @@ parts:
         landing_is_target: true
         outline_closed: true
 
+      - title: DetoursLib Package
+        subtitle: Detours of functions in the program's own process
+        landing_page: /tB/Packages/Contributed/DetoursLib/
+        page: /tB/Packages/Contributed/DetoursLib/
+        landing_is_target: true
+        outline_closed: true
+
       - title: FileProps Package
         subtitle: The Windows shell properties of a file
         landing_page: /tB/Packages/Contributed/FileProps/
@@ -669,7 +681,6 @@ left_out:
       - /tB/IDE/Reference
       - /tB/IDE/Project/Webpage
       - /tB/IDE/Project/Splash
-      - /tB/IDE/Project/PackagePublishing
       - /tB/IDE/AddIns/GlobalSearch
       - /tB/IDE/Reference/Menu/File
       - /tB/IDE/Reference/Menu/Edit
diff --git a/indexer/snapshots/contributed/DetoursLib/api.json b/indexer/snapshots/contributed/DetoursLib/api.json
new file mode 100644
index 000000000..c88155b61
--- /dev/null
+++ b/indexer/snapshots/contributed/DetoursLib/api.json
@@ -0,0 +1,61 @@
+{
+  "format": 3,
+  "package": "DetoursLib",
+  "project": "DetoursLib",
+  "version": "1.0.0.0",
+  "types": [
+    {
+      "name": "Detours",
+      "kind": "module",
+      "vis": "public",
+      "file": "Detours.twin",
+      "signature": "Module Detours",
+      "members": [
+        {"name":"CFALSE","kind":"const","vis":"private","signature":"Private Const CFALSE As Long = 0"},
+        {"name":"CTRUE","kind":"const","vis":"private","signature":"Private Const CTRUE As Long = 1"},
+        {"name":"CreateDetourAtDLLEntry","kind":"function","vis":"public","signature":"Public Function CreateDetourAtDLLEntry(ByVal libName As String, ByVal funcName As String, ByVal newAddress As LongPtr) As LongPtr","returnType":"LongPtr"},
+        {"name":"CreateDetourAtFuncAddress","kind":"function","vis":"public","signature":"Public Function CreateDetourAtFuncAddress(ByVal procAddress As LongPtr, ByVal newAddress As LongPtr) As LongPtr","returnType":"LongPtr"},
+        {"name":"GetModuleHandle","kind":"function","vis":"private","declare":true,"signature":"Private DeclareWide PtrSafe Function GetModuleHandle Lib \"kernel32\" Alias \"GetModuleHandleW\" (Optional ByVal lpModuleName As String) As LongPtr","returnType":"LongPtr"},
+        {"name":"GetProcAddress","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function GetProcAddress Lib \"kernel32\" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr","returnType":"LongPtr"},
+        {"name":"RemoveDetour","kind":"sub","vis":"public","signature":"Public Sub RemoveDetour(ByVal patchedAddress As LongPtr, ByVal newAddress As LongPtr)"}
+      ]
+    },
+    {
+      "name": "DetoursMod",
+      "kind": "module",
+      "vis": "public",
+      "file": "Detours.twin",
+      "signature": "Module DetoursMod",
+      "members": [
+        {"name":"SlimDetoursInlineHook","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function SlimDetoursInlineHook Lib DETOURS Alias \"SlimDetoursInlineHook\" (ByVal bEnable As Long, ByRef ppPointer As LongPtr, ByVal pDetour As LongPtr) As Long","returnType":"Long"}
+      ]
+    },
+    {
+      "name": "DetoursMod",
+      "kind": "module",
+      "vis": "public",
+      "file": "Detours.twin",
+      "signature": "Module DetoursMod",
+      "members": [
+        {"name":"SlimDetoursInlineHook","kind":"function","vis":"public","declare":true,"signature":"Public Declare PtrSafe Function SlimDetoursInlineHook Lib DETOURS Alias \"SlimDetoursInlineHook@12\" (ByVal bEnable As Long, ByRef ppPointer As LongPtr, ByVal pDetour As LongPtr) As Long","returnType":"Long"}
+      ]
+    },
+    {
+      "name": "DetoursTest",
+      "kind": "module",
+      "vis": "public",
+      "file": "DetoursTest.twin",
+      "signature": "Module DetoursTest",
+      "members": [
+        {"name":"PatchedSetFocus","kind":"function","vis":"private","signature":"Private Function PatchedSetFocus(ByVal hwnd As LongPtr) As LongPtr","returnType":"LongPtr"},
+        {"name":"SetFocus","kind":"function","vis":"private","declare":true,"signature":"Private Declare PtrSafe Function SetFocus Lib \"user32\" (ByVal hwnd As LongPtr) As LongPtr","returnType":"LongPtr"},
+        {"name":"TestDetours","kind":"sub","vis":"public","signature":"Public Sub TestDetours()"},
+        {"name":"TogglePatchesOnOff","kind":"sub","vis":"private","signature":"Private Sub TogglePatchesOnOff()"},
+        {"name":"_SetFocus","kind":"delegate","vis":"private","signature":"Private Delegate Function _SetFocus (ByVal hwnd As LongPtr) As LongPtr","returnType":"LongPtr"},
+        {"name":"g_Real_SetFocus","kind":"field","vis":"private","signature":"Private g_Real_SetFocus As _SetFocus"}
+      ]
+    }
+  ],
+  "aliases": [],
+  "problems": []
+}
diff --git a/package-lock.json b/package-lock.json
index 45e2f49d3..bf0d8751a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
         "acorn": "^8.0",
         "acorn-walk": "^8.0",
         "axe-core": "4.13.0",
+        "eslint-scope": "8.4.0",
         "fast-glob": "^3.3",
         "html-entities": "^2.6.0",
         "htmlparser2": "^12.0.0",
@@ -23,6 +24,7 @@
         "markdown-it-footnote": "^4.0",
         "pdf-lib": "1.17.1",
         "picocolors": "^1.1.1",
+        "prettier": "3.9.10",
         "puppeteer": "25.0.4",
         "recheck": "4.5.0",
         "sass": "^1.0",
@@ -1299,6 +1301,46 @@
         "node": ">=6"
       }
     },
+    "node_modules/eslint-scope": {
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+      "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
     "node_modules/events-universal": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
@@ -1937,6 +1979,22 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/prettier": {
+      "version": "3.9.10",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz",
+      "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
     "node_modules/progress": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
diff --git a/package.json b/package.json
index 3f52b5642..6523ada35 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
     "acorn": "^8.0",
     "acorn-walk": "^8.0",
     "axe-core": "4.13.0",
+    "eslint-scope": "8.4.0",
     "fast-glob": "^3.3",
     "html-entities": "^2.6.0",
     "htmlparser2": "^12.0.0",
@@ -19,6 +20,7 @@
     "markdown-it-footnote": "^4.0",
     "pdf-lib": "1.17.1",
     "picocolors": "^1.1.1",
+    "prettier": "3.9.10",
     "puppeteer": "25.0.4",
     "recheck": "4.5.0",
     "sass": "^1.0",
diff --git a/scripts/check_symbol_index.mjs b/scripts/check_symbol_index.mjs
index b61792ee6..838c95539 100644
--- a/scripts/check_symbol_index.mjs
+++ b/scripts/check_symbol_index.mjs
@@ -1838,6 +1838,10 @@ const pages = [
   page("Reference/Attributes.md", "/tB/Core/Attributes", "Attributes", "Reference Section",
     [h(1, "Attributes", "attributes"), h(2, "AppObject (optional Bool)", "appobject")]),
   page("Reference/Default/VBA/Guide.md", "/Reference/Guide", "Guide", "VBA Package", [h(1, "Guide")]),
+  page("Reference/Data-Types.md", "/Reference/Data-Types", "Data Types", "Reference Section",
+    [h(1, "Data Types", "data-types"), h(2, "Quick reference", "quick-reference"), h(2, "Long", "long"),
+      h(2, "Overflow", "overflow"), h(2, "String", "string"), h(3, "Fixed-length strings", "fixed-length-strings")],
+    { symbols: ["Long", "String"] }),
 ];
 
 const r = deriveSymbolIndex({ pages, api });
@@ -1935,7 +1939,37 @@ check(
   r.interfaces["tbIDE.ICodeEditorV1"] === "tbIDE.CodeEditor" && r.interfaces["VBA._Collection"] === "VBA.Collection",
   show(r.interfaces),
 );
-check("only /tB/ pages are read", !r.symbols.some((s) => !s.url.startsWith("/tB/")) && !find("Guide").length);
+check(
+  "only /tB/ pages and the data types page are read",
+  !r.symbols.some((s) => !s.url.startsWith("/tB/") && !s.url.startsWith("/Reference/Data-Types#")) &&
+    !find("Guide").length,
+);
+check(
+  "a data type is the language's, at the ## heading its symbols: entry names",
+  show(find("Long")) ===
+    show([{ name: "Long", package: null, container: null, kind: "type", url: "/Reference/Data-Types#long" }]) &&
+    urlOf("String").join() === "/Reference/Data-Types#string",
+  show(r.symbols.filter((s) => s.kind === "type")),
+);
+check(
+  "a ## heading the data types page's symbols: does not name is not a type",
+  !find("Overflow").length && !find("Quick").length,
+  show(r.symbols.filter((s) => s.kind === "type")),
+);
+{
+  const broken = pages.map((p) => (p.src === "Reference/Data-Types.md" ? { ...p, symbols: ["Long", "Currency"] } : p));
+  let error = null;
+  try {
+    deriveSymbolIndex({ pages: broken, api });
+  } catch (e) {
+    error = e;
+  }
+  check(
+    "a type the data types page names without its ## heading fails",
+    /Currency/.test(String(error?.message)),
+    String(error?.message),
+  );
+}
 check(
   "a declared member no page documents is a gap, not an entry",
   r.gaps.some((g) => g.container === "Collection" && g.name === "Count") && !find("Count").length,
diff --git a/scripts/ide-js/lib.mjs b/scripts/ide-js/lib.mjs
new file mode 100644
index 000000000..078462d37
--- /dev/null
+++ b/scripts/ide-js/lib.mjs
@@ -0,0 +1,632 @@
+// Reading, naming and patching the twinBASIC IDE's minified ide/main.js, for
+// scripts/ide_js.mjs and its tests (test/ide-js.test.mjs).
+//
+// The minifier only removes whitespace and renames locals and parameters: every
+// top-level function and class, every property and every global keeps its name.
+// So a top-level function or a method of a top-level class is a *unit* whose name
+// is the same in every build, and the map of our names for its locals is keyed
+// by the unit and by where each binding is declared inside it, never by its
+// minified name, which changes from build to build.
+//
+// Nothing is ever printed from the syntax tree into main.js. The tree (acorn,
+// with ranges and the token list) only says where things are, and every edit is
+// a splice of the original text at a range, so a byte that no edit covers is
+// the input's byte. `coverage` shows that the tokens, the comments and the
+// whitespace between them account for every byte of the file. The readable view
+// (prettier) is only for reading and for writing a fix; a fix comes back into
+// main.js as the smallest token edit of the unit (patchUnits).
+//
+// The names are ours, never twinBASIC's: Wayne Phillips' own sources are never
+// used (owner), so a name is what we understood a variable to be.
+
+import { createHash } from "node:crypto";
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import * as acorn from "acorn";
+import { analyze } from "eslint-scope";
+import { buildNumber } from "../lib/tb-install.mjs";
+
+/** A fix that does not apply to the build: a unit it names is missing, or the patch is not the fix. */
+export class FixError extends Error {
+  constructor(message) {
+    super(message);
+    this.name = "FixError";
+  }
+}
+
+/** A name the map may not take: not an identifier, given twice in a unit, or one that would hide another name. */
+export class NameError extends Error {
+  constructor(message) {
+    super(message);
+    this.name = "NameError";
+  }
+}
+
+/** The main.js of an install, from its twinBASIC.exe, its folder or the file itself. */
+export function mainJsOf(p) {
+  if (p.toLowerCase().endsWith(".js")) return p;
+  const dir = p.toLowerCase().endsWith(".exe") ? path.dirname(p) : p;
+  return path.join(dir, "ide", "main.js");
+}
+
+/** "BETA " for the install a main.js is in, or null when its build cannot be read. */
+export function buildOf(mainJs) {
+  const n = buildNumber(path.join(path.dirname(path.dirname(path.resolve(mainJs))), "twinBASIC.exe"));
+  return n === null ? null : `BETA ${n}`;
+}
+
+export function parse(text) {
+  const tokens = [];
+  const comments = [];
+  const ast = acorn.parse(text, {
+    ecmaVersion: "latest",
+    sourceType: "script",
+    ranges: true,
+    onToken: tokens,
+    onComment: comments,
+    allowHashBang: true,
+  });
+  return { text, ast, tokens: tokens.filter((t) => t.type !== acorn.tokTypes.eof), comments };
+}
+
+export function load(file) {
+  return parse(readFileSync(file, "utf8"));
+}
+
+/**
+ * Whether the token list accounts for the whole file: every byte is in a token,
+ * in a comment or in whitespace between them, nothing overlaps, and putting the
+ * pieces back together gives the text.
+ */
+export function coverage({ text, tokens, comments }) {
+  const pieces = [...tokens, ...comments].sort((a, b) => a.start - b.start);
+  let at = 0;
+  let gaps = 0;
+  let overlaps = 0;
+  let rebuilt = "";
+  for (const p of pieces) {
+    if (p.start < at) overlaps++;
+    else {
+      const between = text.slice(at, p.start);
+      if (!/^\s*$/.test(between)) gaps++;
+      rebuilt += between + text.slice(p.start, p.end);
+      at = p.end;
+    }
+  }
+  const tail = text.slice(at);
+  if (!/^\s*$/.test(tail)) gaps++;
+  rebuilt += tail;
+  return { pieces: pieces.length, gaps, overlaps, exact: rebuilt === text && gaps === 0 && overlaps === 0 };
+}
+
+/**
+ * The units a map is keyed on, by name: each top-level function declaration,
+ * and each method of a top-level class ("Class.method", "Class.get name",
+ * "Class.static m"). The value is the function node: for a method, its
+ * FunctionExpression, `(params) { body }`.
+ */
+export function namedUnits(ast) {
+  const units = new Map();
+  for (const node of ast.body) {
+    if (node.type === "FunctionDeclaration" && node.id) units.set(node.id.name, node);
+    if (node.type === "ClassDeclaration" && node.id) {
+      for (const m of node.body.body) {
+        if (m.type !== "MethodDefinition" || m.key.type !== "Identifier") continue;
+        units.set(unitNameOf(node, m), m.value);
+      }
+    }
+  }
+  return units;
+}
+
+const unitNameOf = (cls, m) =>
+  `${cls.id.name}.${m.static ? "static " : ""}${m.kind === "get" || m.kind === "set" ? `${m.kind} ` : ""}${m.key.name}`;
+
+/** eslint-scope's ScopeManager for a parse, made once. */
+export function scopes(parsed) {
+  parsed.scopeManager ??= analyze(parsed.ast, { ecmaVersion: 2022, sourceType: "script" });
+  return parsed.scopeManager;
+}
+
+/**
+ * The bindings declared inside a unit, each under a key that says where it is
+ * declared and not what it is called: the scope's path from the unit's own
+ * function scope (the index of each child scope in source order) and the
+ * binding's place in that scope's order of declaration. "0#1" is the unit's
+ * second parameter or variable, "0/2#0" the first binding of its third nested
+ * scope. Returns undefined when the parse has no unit of that name.
+ *
+ * Each binding has its minified `name`, `def` (eslint-scope's definition type:
+ * Parameter, Variable, FunctionName, ClassName, CatchClause...), `kind` (var,
+ * let, const, or paramN for the Nth parameter), `refs` (its reference count) and
+ * `ranges`, every place the name is written.
+ */
+export function bindings(parsed, unitName) {
+  const node = namedUnits(parsed.ast).get(unitName);
+  if (!node) return undefined;
+  const root = scopes(parsed).acquire(node);
+  const out = [];
+  const visit = (scope, at) => {
+    let ordinal = 0;
+    if (scope.type !== "function-expression-name") {
+      for (const v of scope.variables) {
+        if (v.defs.length === 0) continue; // the implicit `arguments`
+        const def = v.defs[0];
+        const starts = new Set();
+        const ranges = [];
+        for (const id of [...v.identifiers, ...v.references.map((r) => r.identifier)]) {
+          if (starts.has(id.start)) continue;
+          starts.add(id.start);
+          ranges.push([id.start, id.end]);
+        }
+        out.push({
+          key: `${at}#${ordinal++}`,
+          name: v.name,
+          def: def.type,
+          kind:
+            def.type === "Parameter"
+              ? `param${def.node.params.findIndex((p) => p.start <= def.name.start && def.name.end <= p.end)}`
+              : (def.parent?.kind ?? ""),
+          refs: v.references.length,
+          ranges,
+        });
+      }
+    }
+    for (const [i, c] of scope.childScopes.entries()) visit(c, `${at}/${i}`);
+  };
+  visit(root, "0");
+  return { node, bindings: out, free: new Set(root.through.map((r) => r.identifier.name)) };
+}
+
+/**
+ * The unit's syntax tree with every binding replaced by its key, hashed. Equal
+ * hashes in two builds mean the unit is the same code under other local names.
+ * The tree and not the text, so that a printer's parentheses, quotes and
+ * whitespace do not count as a change, nor does `{t}` against `{t: e}`: a
+ * property's key is hashed as its name, never as the binding a shorthand's
+ * key shares its place with.
+ */
+export function shapeHash(node, list) {
+  const keyAt = new Map();
+  for (const b of list) for (const r of b.ranges) keyAt.set(r[0], b.key);
+  const h = createHash("sha256");
+  const SKIP = new Set(["start", "end", "range", "loc", "raw", "shorthand"]);
+  const emit = (n, asName = false) => {
+    if (Array.isArray(n)) {
+      h.update("[");
+      for (const x of n) emit(x);
+      h.update("]");
+    } else if (n && typeof n === "object") {
+      if (n.type === "Identifier" && keyAt.has(n.start) && !asName) {
+        h.update(`\u{241F}${keyAt.get(n.start)};`);
+        return;
+      }
+      h.update("{");
+      for (const k of Object.keys(n)) {
+        if (SKIP.has(k)) continue;
+        h.update(`${k}:`);
+        emit(n[k], n.type === "Property" && k === "key" && !n.computed);
+      }
+      h.update("}");
+    } else h.update(`${typeof n}:${String(n)};`);
+  };
+  emit(node);
+  return h.digest("hex").slice(0, 16);
+}
+
+/** A unit's bindings and its shape hash, or undefined when the parse has no such unit. */
+export function unitInfo(parsed, unitName) {
+  const b = bindings(parsed, unitName);
+  return b && { ...b, shape: shapeHash(b.node, b.bindings) };
+}
+
+/**
+ * The text of [start, end) with each edit [from, to, replacement] applied.
+ * Edits may not overlap. A shorthand property's value (`{e}` or `{e = 1}`,
+ * whose starts `shorthand` maps to their keys) keeps its key: renaming it
+ * writes `e: new`.
+ */
+export function splice(text, start, end, edits, shorthand = new Map()) {
+  const sorted = [...edits].sort((a, b) => a[0] - b[0]);
+  let out = "";
+  let at = start;
+  for (const [from, to, rep] of sorted) {
+    if (from < at || to > end) throw new Error(`edit ${from}-${to} is outside ${start}-${end} or overlaps another`);
+    const key = shorthand.get(from);
+    out += text.slice(at, from) + (key !== undefined && key !== rep ? `${key}: ${rep}` : rep);
+    at = to;
+  }
+  return out + text.slice(at, end);
+}
+
+/**
+ * The start of each shorthand property's value identifier inside `node`, to
+ * its key's name. Every node is visited, a destructuring pattern's properties
+ * included, which acorn-walk's walkers pass over.
+ */
+export function shorthandStarts(node) {
+  const map = new Map();
+  forEachNode(node, (n) => {
+    if (n.type !== "Property" || !n.shorthand) return;
+    const id = n.value.type === "AssignmentPattern" ? n.value.left : n.value;
+    map.set(id.start, n.key.name);
+  });
+  return map;
+}
+
+function forEachNode(n, fn) {
+  if (Array.isArray(n)) for (const x of n) forEachNode(x, fn);
+  else if (n && typeof n.type === "string") {
+    fn(n);
+    for (const [k, v] of Object.entries(n)) if (k !== "range" && v && typeof v === "object") forEachNode(v, fn);
+  }
+}
+
+// ---- the name map ----
+
+/** An empty map, as names.json holds it. */
+export const emptyMap = () => ({
+  format: 1,
+  about:
+    "Our names for minified locals of the twinBASIC IDE's ide/main.js, keyed by unit and scope path (scripts/ide_js.mjs). These are our names, not the twinBASIC source's.",
+  units: {},
+});
+
+/**
+ * How each name of a unit's map entry applies to a build:
+ *   exact    the unit's shape is unchanged, so the code is the same and the name
+ *            is certainly this binding's
+ *   carried  the unit changed, but its key still finds a binding of the same
+ *            kind with the same reference count
+ *   review   the key finds a binding of the same kind whose reference count
+ *            changed: probably the same variable, used more or less often
+ *   lost     the unit is gone, or the key finds no binding or one of another
+ *            kind; the name is not applied
+ * Exact and carried names are applied; review and lost ones are not.
+ */
+export function checkUnit(parsed, unitName, entry) {
+  const u = unitInfo(parsed, unitName);
+  const verdicts = [];
+  for (const [key, n] of Object.entries(entry.names)) {
+    const x = u?.bindings.find((y) => y.key === key);
+    let verdict;
+    let why = "";
+    if (!u) {
+      verdict = "lost";
+      why = "the unit is gone";
+    } else if (!x || x.def !== n.def || x.kind !== n.kind) {
+      verdict = "lost";
+      why = x ? `the key is now ${x.def} ${x.kind} ${x.name}` : "no binding has that key";
+    } else if (u.shape === entry.shape) verdict = "exact";
+    else if (x.refs === n.refs) verdict = "carried";
+    else {
+      verdict = "review";
+      why = `${n.refs} references, now ${x.refs}`;
+    }
+    verdicts.push({ key, name: n.name, was: n.was, now: x?.name, verdict, why });
+  }
+  return { info: u, verdicts };
+}
+
+/** The names of a unit's entry that apply to this build, key to our name (exact and carried). */
+export function applicableNames(parsed, unitName, entry) {
+  const names = {};
+  const skipped = [];
+  if (!entry) return { names, skipped, info: unitInfo(parsed, unitName) };
+  const { info, verdicts } = checkUnit(parsed, unitName, entry);
+  for (const v of verdicts) {
+    if (v.verdict === "exact" || v.verdict === "carried") names[v.key] = v.name;
+    else skipped.push(v);
+  }
+  return { names, skipped, info };
+}
+
+const isIdentifierName = (s) => {
+  if (!/^[A-Za-z_$][\w$]*$/.test(s) || s === "arguments" || s === "eval") return false;
+  try {
+    acorn.parse(`"use strict"; var ${s};`, { ecmaVersion: "latest" });
+    return true;
+  } catch {
+    return false;
+  }
+};
+
+/**
+ * The map entry for a unit, sealed against a build: each name in `names` (key
+ * to our name) with what its binding is in that build. Refuses a name that is
+ * not an identifier, one given to two bindings of the unit, and one that would
+ * hide another name the unit uses: a global or outer name it refers to, or a
+ * binding of the unit that keeps its minified name.
+ */
+export function sealUnit(parsed, unitName, names, build) {
+  const u = unitInfo(parsed, unitName);
+  if (!u) throw new NameError(`the build has no unit named ${unitName}`);
+  const seen = new Map();
+  const entry = { namedOn: build, shape: u.shape, names: {} };
+  for (const [key, name] of Object.entries(names)) {
+    const x = u.bindings.find((y) => y.key === key);
+    if (!x) throw new NameError(`${unitName} has no binding ${key}`);
+    if (!isIdentifierName(name)) throw new NameError(`${unitName} ${key}: not a name a variable can have: ${name}`);
+    if (seen.has(name)) throw new NameError(`${unitName}: ${name} is given to both ${seen.get(name)} and ${key}`);
+    seen.set(name, key);
+    if (u.free.has(name)) throw new NameError(`${unitName} ${key}: ${name} would hide the outer ${name} the unit uses`);
+    entry.names[key] = { name, was: x.name, def: x.def, kind: x.kind, refs: x.refs };
+  }
+  for (const x of u.bindings) {
+    if (!(x.key in names) && seen.has(x.name))
+      throw new NameError(`${unitName} ${seen.get(x.name)}: ${x.name} would hide ${x.key}, which keeps that name`);
+  }
+  return entry;
+}
+
+/** The unit's own text with the names `names` (key to our name) put in. */
+export function renamedUnitText(parsed, info, names) {
+  const edits = [];
+  for (const x of info.bindings) {
+    const want = names[x.key];
+    if (want) for (const r of x.ranges) edits.push([r[0], r[1], want]);
+  }
+  return splice(parsed.text, info.node.start, info.node.end, edits, shorthandStarts(info.node));
+}
+
+// ---- the readable view ----
+
+let prettier;
+/** Pretty-printed JavaScript, as prettier prints it; loaded on first use. */
+export async function pretty(code) {
+  prettier ??= await import("prettier");
+  return prettier.format(code, { parser: "babel", printWidth: 100 });
+}
+
+/** A unit's text as code that parses on its own: a method is put back in a class of its own name. */
+function standalone(unitName, text) {
+  const dot = unitName.indexOf(".");
+  return dot < 0 ? text : `class ${unitName.slice(0, dot)} {\n${unitName.slice(dot + 1)}${text}\n}`;
+}
+
+/**
+ * The readable view of a unit: its text in our names, pretty-printed, under a
+ * comment naming the build, where the unit is in main.js, its shape, our names
+ * and any name of the map this build does not take. Returns undefined when the
+ * build has no such unit.
+ */
+export async function unitView(parsed, unitName, entry, build) {
+  const { names, skipped, info } = applicableNames(parsed, unitName, entry);
+  if (!info) return undefined;
+  const ours = Object.values(names);
+  return (
+    `// ${unitName}: ${build ?? "unknown build"}, main.js @${info.node.start}+${info.node.end - info.node.start}, shape ${info.shape}\n` +
+    (ours.length ? `// our names, not twinBASIC's: ${ours.join(", ")}\n` : "") +
+    (skipped.length
+      ? `// not applied, the map does not fit this build: ${skipped.map((v) => `${v.key} ${v.name} (${v.verdict})`).join(", ")}\n`
+      : "") +
+    (await pretty(standalone(unitName, renamedUnitText(parsed, info, names))))
+  );
+}
+
+/** The whole file with every unit of the map in our names, pretty-printed, for reading only. */
+export async function fileView(parsed, map, build) {
+  const edits = [];
+  for (const [unit, entry] of Object.entries(map.units)) {
+    const { names, info } = applicableNames(parsed, unit, entry);
+    if (!info) continue;
+    const sh = shorthandStarts(info.node);
+    for (const x of info.bindings) {
+      const want = names[x.key];
+      if (!want) continue;
+      for (const r of x.ranges) edits.push([r[0], r[1], sh.has(r[0]) ? `${sh.get(r[0])}: ${want}` : want]);
+    }
+  }
+  const units = Object.keys(map.units).length;
+  return `// ${build ?? "unknown build"} main.js, ${units} unit(s) in our names, not twinBASIC's\n${await pretty(splice(parsed.text, 0, parsed.text.length, edits))}`;
+}
+
+// ---- patching ----
+
+// A token's text, and what it is compared by: a string by its value and a number
+// by its value, so that the view's quotes (prettier writes "x" for 'x') and
+// number forms (0.5 for .5) are not differences.
+function tokenList(parsed, start, end) {
+  const out = [];
+  for (const t of parsed.tokens) {
+    if (t.start < start || t.end > end) continue;
+    const v = parsed.text.slice(t.start, t.end);
+    const label = t.type.label;
+    const cmp = label === "string" ? `s:${JSON.stringify(t.value)}` : label === "num" ? `n:${t.value}` : v;
+    out.push({ s: t.start, e: t.end, v, cmp });
+  }
+  return out;
+}
+
+// Tokens a printer may add or leave out without changing the program: a ";"
+// just before "}", and a trailing "," before ")", "]" or "}" (not an array hole).
+function significant(list) {
+  return list.filter((t, i) => {
+    const next = list[i + 1]?.v;
+    const prev = list[i - 1]?.v;
+    if (t.v === ";" && next === "}") return false;
+    return !(t.v === "," && (next === ")" || next === "]" || next === "}") && prev !== "," && prev !== "[");
+  });
+}
+
+/** The runs where two token lists differ, as index ranges [a0, a1) of `a` and [b0, b1) of `b`. */
+export function tokenHunks(a, b) {
+  let lo = 0;
+  while (lo < a.length && lo < b.length && a[lo].cmp === b[lo].cmp) lo++;
+  let ha = a.length;
+  let hb = b.length;
+  while (ha > lo && hb > lo && a[ha - 1].cmp === b[hb - 1].cmp) {
+    ha--;
+    hb--;
+  }
+  const n = ha - lo;
+  const m = hb - lo;
+  const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
+  for (let i = n - 1; i >= 0; i--)
+    for (let j = m - 1; j >= 0; j--)
+      dp[i][j] = a[lo + i].cmp === b[lo + j].cmp ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
+  const hunks = [];
+  let i = 0;
+  let j = 0;
+  let open = null;
+  const close = () => {
+    if (open) hunks.push({ a0: lo + open.i, b0: lo + open.j, a1: lo + i, b1: lo + j });
+    open = null;
+  };
+  while (i < n || j < m) {
+    if (i < n && j < m && a[lo + i].cmp === b[lo + j].cmp) {
+      close();
+      i++;
+      j++;
+    } else {
+      open ??= { i, j };
+      if (j < m && (i === n || dp[i][j + 1] >= dp[i + 1][j])) j++;
+      else i++;
+    }
+  }
+  close();
+  // Two edits a token or two apart, with the same tokens between them, read
+  // better as one: "o?e:TEXTtoHTML(e)" -> "e" rather than three pieces.
+  const merged = [];
+  for (const h of hunks) {
+    const last = merged.at(-1);
+    if (last && h.a0 - last.a1 <= 2 && h.b0 - last.b1 === h.a0 - last.a1) Object.assign(last, { a1: h.a1, b1: h.b1 });
+    else merged.push({ ...h });
+  }
+  return merged;
+}
+
+// The space two pieces of code need between them: between two word characters,
+// and where "+ +", "- -", "/ /" or "/ *" would otherwise run together.
+const WORD = /[\w$]/;
+const glue = (x, y) =>
+  !x || !y ? "" : (WORD.test(x.at(-1)) && WORD.test(y[0])) || /^([+-])\1$|^\/[/*]$/.test(x.at(-1) + y[0]) ? " " : "";
+
+/**
+ * Splices a fix into the build's main.js. `fix` is a parse of a file of whole
+ * units in our names: top-level functions, and classes holding the methods
+ * that are fixed. For each unit, our names are turned back into this build's
+ * minified names (only the names the map applies to this build; any other name
+ * is kept as written), the unit's tokens are compared with the fix's, and each
+ * run of tokens that differs becomes one edit of the minified text. The
+ * patched file is parsed again, and each patched unit must have the shape of
+ * the fix, so the patched main.js runs the code the fix shows.
+ *
+ * Returns { text, edits: [{ unit, from, to, before, after }], skipped: [unit, verdicts] }.
+ * Throws a FixError when the fix does not apply.
+ */
+export function patchUnits(parsed, fix, map) {
+  const units = [...namedUnits(fix.ast)];
+  if (!units.length) throw new FixError("the fix holds no top-level function and no class method");
+  // Our names back to the build's, over the whole fix file, so that a method
+  // still parses in its class.
+  const renames = [];
+  const plan = [];
+  const skipped = [];
+  for (const [unit] of units) {
+    const { names, skipped: notApplied, info } = applicableNames(parsed, unit, map.units[unit]);
+    if (!info) throw new FixError(`the build has no unit named ${unit}`);
+    if (notApplied.length) skipped.push([unit, notApplied]);
+    const back = new Map();
+    for (const [key, ours] of Object.entries(names)) back.set(ours, info.bindings.find((x) => x.key === key).name);
+    const fb = unitInfo(fix, unit);
+    for (const x of fb.bindings) {
+      if (back.has(x.name)) for (const r of x.ranges) renames.push([r[0], r[1], back.get(x.name)]);
+    }
+    plan.push({ unit, info, oursShape: fb.shape });
+  }
+  const shorthand = new Map();
+  for (const [, node] of units) for (const [k, v] of shorthandStarts(node)) shorthand.set(k, v);
+  const minified = parse(splice(fix.text, 0, fix.text.length, renames, shorthand));
+
+  const edits = [];
+  for (const p of plan) {
+    const want = unitInfo(minified, p.unit);
+    // A name the fix gives a new variable can be one of the minified names: if
+    // turning ours back made a reference find another variable, the shapes differ.
+    if (want.shape !== p.oursShape)
+      throw new FixError(`${p.unit}: a name in the fix is one of the build's minified names; rename it`);
+    p.wantShape = want.shape;
+    const orig = significant(tokenList(parsed, p.info.node.start, p.info.node.end));
+    const next = significant(tokenList(minified, want.node.start, want.node.end));
+    for (const h of tokenHunks(orig, next)) {
+      const from = h.a0 < h.a1 ? orig[h.a0].s : orig[h.a0 - 1].e;
+      const to = h.a0 < h.a1 ? orig[h.a1 - 1].e : from;
+      let after = next.slice(h.b0, h.b1).reduce((s, t) => s + glue(s, t.v) + t.v, "");
+      if (after) after = glue(parsed.text.slice(from - 1, from), after) + after + glue(after, parsed.text[to]);
+      edits.push({ unit: p.unit, from, to, before: parsed.text.slice(from, to), after });
+    }
+  }
+
+  const text = splice(
+    parsed.text,
+    0,
+    parsed.text.length,
+    edits.map((e) => [e.from, e.to, e.after]),
+  );
+  let patched;
+  try {
+    patched = parse(text);
+  } catch (err) {
+    throw new FixError(`the patched main.js does not parse: ${err.message}`);
+  }
+  for (const p of plan) {
+    const got = unitInfo(patched, p.unit);
+    if (got?.shape !== p.wantShape)
+      throw new FixError(`${p.unit}: the patched unit is not the fix (shape ${got?.shape}, the fix's ${p.wantShape})`);
+  }
+  return { text, edits, skipped };
+}
+
+// ---- the fix shape ----
+
+/** A unified diff of two texts' lines, `context` lines around each change; "" when they are equal. */
+export function lineDiff(a, b, { context = 3 } = {}) {
+  const x = a.replace(/\n$/, "").split("\n");
+  const y = b.replace(/\n$/, "").split("\n");
+  const toks = (lines) => lines.map((v) => ({ cmp: v }));
+  const hunks = tokenHunks(toks(x), toks(y)).map((h) => ({ ...h }));
+  if (!hunks.length) return "";
+  // Hunks whose context would touch are printed as one.
+  const groups = [];
+  for (const h of hunks) {
+    const g = groups.at(-1);
+    if (g && h.a0 - g.at(-1).a1 <= 2 * context) g.push(h);
+    else groups.push([h]);
+  }
+  const out = [];
+  for (const g of groups) {
+    const a0 = Math.max(0, g[0].a0 - context);
+    const b0 = Math.max(0, g[0].b0 - context);
+    const a1 = Math.min(x.length, g.at(-1).a1 + context);
+    const b1 = Math.min(y.length, g.at(-1).b1 + context);
+    out.push(`@@ -${a0 + 1},${a1 - a0} +${b0 + 1},${b1 - b0} @@`);
+    let i = a0;
+    for (const h of g) {
+      for (; i < h.a0; i++) out.push(` ${x[i]}`);
+      for (let k = h.a0; k < h.a1; k++) out.push(`-${x[k]}`);
+      for (let k = h.b0; k < h.b1; k++) out.push(`+${y[k]}`);
+      i = h.a1;
+    }
+    for (; i < a1; i++) out.push(` ${x[i]}`);
+  }
+  return `${out.join("\n")}\n`;
+}
+
+/**
+ * The fix shape of each unit of a fix: the unit as the build has it and as
+ * the fix has it, both in our names and printed alike, and the diff between.
+ * Returns [{ unit, ours: [[minified, ours]], before, after, diff }].
+ */
+export async function fixShapes(parsed, fix, map) {
+  const out = [];
+  for (const [unit, node] of namedUnits(fix.ast)) {
+    const { names, info } = applicableNames(parsed, unit, map.units[unit]);
+    if (!info) throw new FixError(`the build has no unit named ${unit}`);
+    const before = await pretty(standalone(unit, renamedUnitText(parsed, info, names)));
+    const after = await pretty(standalone(unit, fix.text.slice(node.start, node.end)));
+    const ours = Object.entries(names).map(([key, name]) => [info.bindings.find((x) => x.key === key).name, name]);
+    out.push({ unit, ours, before, after, diff: lineDiff(before, after) });
+  }
+  return out;
+}
diff --git a/scripts/ide-js/names.json b/scripts/ide-js/names.json
new file mode 100644
index 000000000..ffccce437
--- /dev/null
+++ b/scripts/ide-js/names.json
@@ -0,0 +1,115 @@
+{
+  "format": 1,
+  "about": "Our names for minified locals of the IDE's main.js, keyed by unit and scope path (scripts/ide_js.mjs). These are our names, not the twinBASIC source's.",
+  "units": {
+    "debugOutputPartial": {
+      "namedOn": "BETA 1007",
+      "shape": "ea9f1277c4c9df1b",
+      "names": {
+        "0#0": {
+          "name": "text",
+          "was": "e",
+          "def": "Parameter",
+          "kind": "param0",
+          "refs": 13
+        },
+        "0#1": {
+          "name": "colorClass",
+          "was": "t",
+          "def": "Parameter",
+          "kind": "param1",
+          "refs": 4
+        },
+        "0#2": {
+          "name": "isHTML",
+          "was": "o",
+          "def": "Parameter",
+          "kind": "param2",
+          "refs": 3
+        },
+        "0#3": {
+          "name": "timeStamp",
+          "was": "n",
+          "def": "Parameter",
+          "kind": "param3",
+          "refs": 2
+        },
+        "0#4": {
+          "name": "endsLine",
+          "was": "r",
+          "def": "Variable",
+          "kind": "var",
+          "refs": 5
+        },
+        "0#5": {
+          "name": "addedHTML",
+          "was": "i",
+          "def": "Variable",
+          "kind": "var",
+          "refs": 2
+        },
+        "0/1#0": {
+          "name": "lineHTML",
+          "was": "n",
+          "def": "Variable",
+          "kind": "let",
+          "refs": 2
+        },
+        "0/2#0": {
+          "name": "stampHTML",
+          "was": "i",
+          "def": "Variable",
+          "kind": "let",
+          "refs": 4
+        }
+      }
+    },
+    "TEXTtoHTML": {
+      "namedOn": "BETA 1007",
+      "shape": "ec9b42ee6730cbed",
+      "names": {
+        "0#0": {
+          "name": "text",
+          "was": "e",
+          "def": "Parameter",
+          "kind": "param0",
+          "refs": 9
+        }
+      }
+    },
+    "designer_PropertyPage": {
+      "namedOn": "BETA 1007",
+      "shape": "8adba2241a344d25",
+      "names": {
+        "0/0#0": {
+          "name": "container",
+          "was": "e",
+          "def": "Parameter",
+          "kind": "param0",
+          "refs": 1
+        },
+        "0/0#4": {
+          "name": "placeholder",
+          "was": "r",
+          "def": "Variable",
+          "kind": "var",
+          "refs": 4
+        },
+        "0/1#0": {
+          "name": "element",
+          "was": "e",
+          "def": "Parameter",
+          "kind": "param0",
+          "refs": 1
+        },
+        "0/2#1": {
+          "name": "propertyText",
+          "was": "t",
+          "def": "Parameter",
+          "kind": "param1",
+          "refs": 4
+        }
+      }
+    }
+  }
+}
diff --git a/scripts/ide_js.mjs b/scripts/ide_js.mjs
new file mode 100644
index 000000000..027e7e911
--- /dev/null
+++ b/scripts/ide_js.mjs
@@ -0,0 +1,428 @@
+#!/usr/bin/env node
+// Reads, names and patches the twinBASIC IDE's minified ide/main.js, so that an
+// IDE defect report in BUGS-TO-REPORT.md can carry a "Fix shape": the fixed
+// function in a readable form, close to what twinBASIC's developer works with,
+// tested on a patched copy of the install. The library is scripts/ide-js/lib.mjs,
+// which says how it works; the name map is scripts/ide-js/names.json; each fix
+// is bugs//fix.js beside its entry's reproducer.
+//
+// Two rules (WIP.Harness.md): only a copy of an install is ever patched, never
+// one on the Desktop (`patch` writes into a copy under the temp folder, or to a
+// file outside every install); and Wayne Phillips' own sources are never used,
+// so every name in the map is ours.
+//
+// Exit codes: 0 done, 1 a finding (a name lost, a fix that does not apply),
+// 2 the tool could not do its job -- see USAGE.
+
+import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import { die, exitOnCrash, parseCli, printHelpAndExit, withUsageError } from "../lib/cli.mjs";
+import { REPO_ROOT } from "../lib/repo-paths.mjs";
+import {
+  buildOf,
+  checkUnit,
+  coverage,
+  emptyMap,
+  FixError,
+  fileView,
+  fixShapes,
+  load,
+  mainJsOf,
+  NameError,
+  namedUnits,
+  parse,
+  patchUnits,
+  scopes,
+  sealUnit,
+  unitInfo,
+  unitView,
+} from "./ide-js/lib.mjs";
+import { makeIdeCopy } from "./lib/tb-ide-copy.mjs";
+import { findIde } from "./lib/tb-install.mjs";
+
+exitOnCrash();
+
+const DEFAULT_MAP = path.join(REPO_ROOT, "scripts", "ide-js", "names.json");
+
+const USAGE = `usage: node scripts/ide_js.mjs  [arguments] [--ide ] [--map ] [--out ] [--copy-to ] [--all] [--json] [-h, --help]
+
+Reads, names and patches the twinBASIC IDE's minified ide/main.js. A unit is a
+top-level function ("debugOutputPartial") or a method of a top-level class
+("Class.method"), whose name the minifier keeps; the map gives our names to the
+locals and parameters inside units, keyed by where each is declared, so that it
+applies to a new build.
+
+Commands:
+  survey                    what the minifier left in main.js, and whether the
+                            token list rebuilds it byte for byte
+  units      the units that are the same, changed, added and
+                            removed between --ide's build and another
+  list                the unit's bindings, each with its key, to name
+  name  =...
+                            give names to bindings of a unit, in the map, sealed
+                            against --ide's build
+  seal                      seal every unit of the map against --ide's build again,
+                            after its names have been checked there
+  check                     how each name of the map applies to --ide's build:
+                            exact, carried, review or lost
+  view ...            the readable view of units, in our names
+  view --all --out    the whole file, every unit of the map in our names
+                            (about 50,000 lines; never into the repository)
+  patch             splice a fix (whole units in our names) into main.js:
+                            --copy-to  copies the install into , under
+                            the temp folder, with the patched main.js, and prints
+                            its twinBASIC.exe; --out  writes main.js only
+  shape             the fix shape: each unit of the fix as the build has
+                            it and as fixed, a diff in our names, as markdown
+
+Options:
+  --ide    the install: its twinBASIC.exe, its folder or a main.js
+                    (default: $TB_IDE, else the newest twinBASIC_IDE_BETA_ on
+                    the Desktop)
+  --map       the name map (default: scripts/ide-js/names.json)
+  --out       view, patch: write to this file, not stdout; refused inside
+                    the repository, and inside an install not under the temp folder
+  --copy-to    patch: the folder for the patched copy, under the temp folder
+  --all             view: the whole file
+  --json            survey, units, check: one JSON object
+  -h, --help        print this text and exit
+
+Exit codes:
+  0  done; check: no name is lost; survey: the token list rebuilds the file
+  1  a finding: check, a name is lost; survey, a byte the token list does not
+     account for; patch, shape, a fix that does not apply to the build (a unit
+     it names is missing, a name in it is one of the build's minified names, or
+     the patched unit is not the fix)
+  2  a refused command line, an install or a file that cannot be read, a main.js
+     or fix that does not parse, a name the map may not take, a refused --out or
+     --copy-to, or a crash`;
+
+const usageError = { format: (err) => `${err.message}\n${USAGE}` };
+
+// What each command takes, as [positionals min, max, options]; anything else is refused.
+const COMMANDS = {
+  survey: [0, 0, ["ide", "json"]],
+  units: [1, 1, ["ide", "json"]],
+  list: [1, 1, ["ide"]],
+  name: [2, Infinity, ["ide", "map"]],
+  seal: [0, 0, ["ide", "map"]],
+  check: [0, 0, ["ide", "map", "json"]],
+  view: [0, Infinity, ["ide", "map", "out", "all"]],
+  patch: [1, 1, ["ide", "map", "out", "copyTo"]],
+  shape: [1, 1, ["ide", "map"]],
+};
+const COMMAND_ARGS = {
+  units: "another install",
+  list: "a unit",
+  name: "a unit and at least one =",
+  view: "a unit, or --all",
+  patch: "a fix",
+  shape: "a fix",
+};
+const FLAG = (key) => `--${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
+
+const { values, positionals } = withUsageError(
+  () =>
+    parseCli(process.argv.slice(2), {
+      options: {
+        ide: { type: "string" },
+        map: { type: "string" },
+        out: { type: "string" },
+        "copy-to": { type: "string" },
+        all: { type: "boolean", default: false },
+        json: { type: "boolean", default: false },
+        help: { type: "boolean", short: "h", default: false },
+      },
+      positionals: { min: 0, max: Infinity },
+      stopAt: ["help"],
+    }),
+  usageError,
+);
+if (values.help) printHelpAndExit(USAGE);
+
+// A command line refused after the parse: the message, then the usage. All of
+// these are checked before an install is read.
+const refuse = (message) => die(2, `${message}\n${USAGE}`);
+const insideTemp = (p) => insideDir(tmpdir(), p);
+
+const [command, ...args] = positionals;
+if (!command) die(2, USAGE);
+if (!(command in COMMANDS)) refuse(`unknown command: ${command}`);
+const [min, max, applies] = COMMANDS[command];
+for (const key of ["ide", "map", "out", "copyTo", "all", "json"]) {
+  const given = values[key] !== undefined && values[key] !== false;
+  if (given && !applies.includes(key)) refuse(`${FLAG(key)} does not apply to ${command}`);
+}
+// view takes units, or --all and none
+const fewest = command === "view" && !values.all ? 1 : min;
+const most = command === "view" && values.all ? 0 : max;
+if (args.length < fewest) refuse(`${command} needs ${COMMAND_ARGS[command]}`);
+if (args.length > most) refuse(`unexpected argument: ${args[most]}`);
+if (command === "view" && values.all && !values.out) refuse("view --all needs --out ");
+if (command === "patch" && !values.out === !values.copyTo)
+  refuse("patch needs one of --out  and --copy-to ");
+if (command === "name") {
+  for (const pair of args.slice(1)) if (!/^[^=]+=.+$/.test(pair)) refuse(`expected =, got: ${pair}`);
+}
+// Never into an install that is not a copy under the temp folder, and never
+// into the repository, which holds no copy of main.js.
+if (values.out) {
+  const out = path.resolve(values.out);
+  if (insideDir(REPO_ROOT, out)) refuse(`refusing --out ${out}: it is inside the repository`);
+  for (let dir = path.dirname(out); !insideTemp(out) && path.dirname(dir) !== dir; dir = path.dirname(dir)) {
+    if (existsSync(path.join(dir, "twinBASIC.exe")) || existsSync(path.join(dir, "ide", "main.js")))
+      refuse(`refusing --out ${out}: it is inside the install ${dir}`);
+  }
+}
+if (values.copyTo && !insideTemp(values.copyTo))
+  refuse(`refusing --copy-to ${path.resolve(values.copyTo)}: it is not under ${tmpdir()}`);
+
+/** The install's main.js, parsed, with its file and build. */
+function loadInstall(given) {
+  const ide = findIde(given);
+  if (!ide) die(2, "no twinBASIC install: pass --ide or set TB_IDE");
+  const file = mainJsOf(ide);
+  if (values.out && path.resolve(values.out).toLowerCase() === path.resolve(file).toLowerCase())
+    die(2, `refusing --out ${path.resolve(values.out)}: it is the main.js read`);
+  let parsed;
+  try {
+    parsed = load(file);
+  } catch (err) {
+    die(2, err.code === "ENOENT" ? `no main.js at ${file}` : `${file} does not parse: ${err.message}`);
+  }
+  return { file, parsed, build: buildOf(file) };
+}
+
+function loadFix(file) {
+  try {
+    return parse(readFileSync(file, "utf8"));
+  } catch (err) {
+    return die(2, err.code === "ENOENT" ? `no fix at ${file}` : `${file} does not parse: ${err.message}`);
+  }
+}
+
+const mapFile = values.map ?? DEFAULT_MAP;
+function readMap({ orEmpty = false } = {}) {
+  try {
+    const map = JSON.parse(readFileSync(mapFile, "utf8"));
+    if (map.format !== 1 || typeof map.units !== "object") die(2, `${mapFile} is not a name map of format 1`);
+    return map;
+  } catch (err) {
+    if (err.code === "ENOENT" && orEmpty) return emptyMap();
+    return die(2, err.code === "ENOENT" ? `no name map at ${mapFile}` : `${mapFile} cannot be read: ${err.message}`);
+  }
+}
+const writeMap = (map) => writeFileSync(mapFile, `${JSON.stringify(map, null, 2)}\n`);
+
+const excerpt = (text, at) => text.slice(Math.max(0, at - 25), at + 35).replace(/\s+/g, " ");
+const print = (obj, lines) => console.log(values.json ? JSON.stringify(obj, null, 2) : lines.join("\n"));
+
+if (command === "survey") {
+  const t0 = performance.now();
+  const { file, parsed, build } = loadInstall(values.ide);
+  const parseMs = Math.round(performance.now() - t0);
+  const { text, ast } = parsed;
+  const top = {};
+  for (const n of ast.body) {
+    const k = n.type === "VariableDeclaration" ? `${n.type}(${n.kind})` : n.type;
+    top[k] = (top[k] ?? 0) + 1;
+  }
+  const sm = scopes(parsed);
+  let locals = 0;
+  const lengths = {};
+  for (const s of sm.scopes) {
+    if (s === sm.globalScope) continue;
+    for (const v of s.variables) {
+      if (!v.defs.length) continue;
+      locals++;
+      lengths[v.name.length] = (lengths[v.name.length] ?? 0) + 1;
+    }
+  }
+  const count = (re) => (text.match(re) ?? []).length;
+  const report = {
+    file,
+    build,
+    bytes: Buffer.byteLength(text),
+    lines: text.split("\n").length,
+    parseMs,
+    tokens: parsed.tokens.length,
+    comments: parsed.comments.length,
+    coverage: coverage(parsed),
+    topLevel: top,
+    units: namedUnits(ast).size,
+    globalNames: sm.globalScope.variables.length,
+    implicitGlobals: sm.globalScope.through.length,
+    localBindings: locals,
+    localNameLengths: lengths,
+    sourceMappingURL: count(/sourceMappingURL/g),
+  };
+  print(
+    report,
+    Object.entries(report).map(([k, v]) => `${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`),
+  );
+  process.exit(report.coverage.exact ? 0 : 1);
+}
+
+if (command === "units") {
+  const a = loadInstall(values.ide);
+  const b = loadInstall(args[0]);
+  const ua = namedUnits(a.parsed.ast);
+  const ub = namedUnits(b.parsed.ast);
+  const changed = [];
+  const tally = { same: 0, changed: 0, removed: 0, added: 0 };
+  for (const name of ua.keys()) {
+    if (!ub.has(name)) {
+      tally.removed++;
+      continue;
+    }
+    if (unitInfo(a.parsed, name).shape === unitInfo(b.parsed, name).shape) tally.same++;
+    else {
+      tally.changed++;
+      changed.push(name);
+    }
+  }
+  for (const name of ub.keys()) if (!ua.has(name)) tally.added++;
+  print({ from: a.build, to: b.build, ...tally, changed }, [
+    `${a.build ?? a.file} -> ${b.build ?? b.file}: ${JSON.stringify(tally)}`,
+    `changed: ${changed.join(", ")}`,
+  ]);
+  process.exit(0);
+}
+
+if (command === "list") {
+  const { parsed } = loadInstall(values.ide);
+  const u = unitInfo(parsed, args[0]);
+  if (!u) die(2, `no unit named ${args[0]}`);
+  console.log(`${args[0]}: ${u.node.end - u.node.start} bytes at ${u.node.start}, shape ${u.shape}`);
+  for (const x of u.bindings) {
+    const use = x.ranges[1] ?? x.ranges[0];
+    console.log(
+      `  ${x.key.padEnd(8)} ${x.name.padEnd(4)} ${x.def.padEnd(10)} ${x.kind.padEnd(7)} refs ${String(x.refs).padStart(2)}  ${excerpt(parsed.text, use[0])}`,
+    );
+  }
+  process.exit(0);
+}
+
+if (command === "name" || command === "seal") {
+  const { parsed, build } = loadInstall(values.ide);
+  const map = readMap({ orEmpty: command === "name" });
+  const work = {};
+  if (command === "name") {
+    const [unit, ...pairs] = args;
+    const names = Object.fromEntries(Object.entries(map.units[unit]?.names ?? {}).map(([key, n]) => [key, n.name]));
+    for (const pair of pairs) {
+      const at = pair.indexOf("=");
+      names[pair.slice(0, at)] = pair.slice(at + 1);
+    }
+    work[unit] = names;
+  } else {
+    for (const [unit, entry] of Object.entries(map.units))
+      work[unit] = Object.fromEntries(Object.entries(entry.names).map(([key, n]) => [key, n.name]));
+  }
+  try {
+    for (const [unit, names] of Object.entries(work)) map.units[unit] = sealUnit(parsed, unit, names, build);
+  } catch (err) {
+    if (err instanceof NameError) die(2, err.message);
+    throw err;
+  }
+  writeMap(map);
+  console.log(`sealed ${Object.keys(work).join(", ") || "nothing"} against ${build ?? "an unknown build"}`);
+  process.exit(0);
+}
+
+if (command === "check") {
+  const { parsed, build } = loadInstall(values.ide);
+  const map = readMap();
+  const tally = { exact: 0, carried: 0, review: 0, lost: 0 };
+  const rows = [];
+  for (const [unit, entry] of Object.entries(map.units)) {
+    for (const v of checkUnit(parsed, unit, entry).verdicts) {
+      tally[v.verdict]++;
+      rows.push({ unit, ...v });
+    }
+  }
+  print({ build, ...tally, names: rows }, [
+    ...rows.map(
+      (r) =>
+        `${r.verdict.padEnd(8)} ${r.unit} ${r.key} ${r.was} -> ${r.now ?? "?"} = ${r.name}${r.why ? `  (${r.why})` : ""}`,
+    ),
+    `${build ?? "unknown build"}: ${JSON.stringify(tally)}`,
+  ]);
+  process.exit(tally.lost ? 1 : 0);
+}
+
+if (command === "view") {
+  const { parsed, build } = loadInstall(values.ide);
+  const map = readMap({ orEmpty: values.map === undefined });
+  let out = "";
+  if (values.all) out = await fileView(parsed, map, build);
+  for (const unit of args) {
+    const v = await unitView(parsed, unit, map.units[unit], build);
+    if (v === undefined) die(2, `no unit named ${unit}`);
+    out += v;
+  }
+  if (values.out) writeOut(values.out, out);
+  else process.stdout.write(out);
+  process.exit(0);
+}
+
+// patch and shape
+const { file, parsed, build } = loadInstall(values.ide);
+const fix = loadFix(args[0]);
+const map = readMap();
+try {
+  if (command === "shape") {
+    for (const s of await fixShapes(parsed, fix, map)) {
+      const code = (list) => list.map((n) => `\`${n}\``).join(", ");
+      const ours = s.ours.length
+        ? ` The local names are ours, not twinBASIC's (${code(s.ours.map(([, o]) => o))}); the build calls them ${code(s.ours.map(([m]) => m))}.`
+        : "";
+      console.log(
+        `\`${s.unit}\` in \`ide/main.js\`, ${build ?? "an unknown build"}, in a readable form.${ours}\n\n` +
+          `\`\`\`diff\n${s.diff || " (no change)\n"}\`\`\`\n`,
+      );
+    }
+    process.exit(0);
+  }
+  const r = patchUnits(parsed, fix, map);
+  for (const [unit, verdicts] of r.skipped) {
+    const names = verdicts.map((v) => v.name).join(", ");
+    console.log(`${unit}: names the map does not apply to this build, kept as written: ${names}`);
+  }
+  for (const e of r.edits) {
+    const pre = parsed.text.slice(Math.max(0, e.from - 16), e.from);
+    const post = parsed.text.slice(e.to, e.to + 8);
+    console.log(`${e.unit} @${e.from}:\n  ${pre}${e.before}${post}\n  ${pre}${e.after}${post}`);
+  }
+  const net = Buffer.byteLength(r.text) - Buffer.byteLength(parsed.text);
+  console.log(`${r.edits.length} edit(s), ${net} byte(s) net; each patched unit is the fix`);
+  if (values.out) {
+    writeOut(values.out, r.text);
+    console.log(`patched main.js: ${path.resolve(values.out)}`);
+  } else {
+    const exe = path.join(path.dirname(path.dirname(path.resolve(file))), "twinBASIC.exe");
+    let copy;
+    try {
+      copy = makeIdeCopy({ ide: exe, dest: values.copyTo });
+    } catch (err) {
+      die(2, err.message);
+    }
+    writeFileSync(mainJsOf(copy), r.text);
+    console.log(`patched copy: ${copy}`);
+  }
+  process.exit(0);
+} catch (err) {
+  if (err instanceof FixError) die(1, err.message);
+  throw err;
+}
+
+function insideDir(root, p) {
+  const rel = path.relative(path.resolve(root), path.resolve(p));
+  return !!rel && !rel.startsWith("..") && !path.isAbsolute(rel);
+}
+
+function writeOut(given, text) {
+  mkdirSync(path.dirname(path.resolve(given)), { recursive: true });
+  writeFileSync(given, text);
+}
diff --git a/scripts/lib/cli-cases.mjs b/scripts/lib/cli-cases.mjs
index d8869fa16..23d32eee2 100644
--- a/scripts/lib/cli-cases.mjs
+++ b/scripts/lib/cli-cases.mjs
@@ -413,6 +413,7 @@ const HELP_TOOLS = {
   "scripts/convert_em_dash_separators.mjs": null,
   "scripts/crawl_check.mjs": null,
   "scripts/gen_attribute_probes.mjs": "Generate a twinBASIC probe project for Reference/Attributes.md applicability.\n",
+  "scripts/ide_js.mjs": null,
   "scripts/impexp.mjs": "Usage:\n",
   "scripts/package_tests.mjs": null,
   "scripts/pick_a11y_sample.mjs": null,
@@ -495,6 +496,7 @@ const REFUSALS = {
   "scripts/convert_em_dash_separators.mjs": [null, { args: ["--check", "--bogus"] }],
   "scripts/crawl_check.mjs": ["timeout"],
   "scripts/gen_attribute_probes.mjs": [null],
+  "scripts/ide_js.mjs": ["ide"],
   "scripts/impexp.mjs": [null, { prefix: "ERROR: " }],
   "scripts/package_tests.mjs": ["ide"],
   "scripts/pick_a11y_sample.mjs": ["sweep"],
@@ -755,6 +757,43 @@ bad(
   bad(tool, ["file", "no-such-bug", "12"], "no such reproducer: bugs/no-such-bug\n");
 }
 
+// ide_js follows the message with its usage. It checks its command line, and where
+// --out and --copy-to point, before it reads an install, so none of these reads one;
+// the last is the install TB_IDE names, which does not exist.
+{
+  const tool = "scripts/ide_js.mjs";
+  bad(tool, [], new RegExp(`^usage: node ${literal(tool)} `));
+  bad(tool, ["frobnicate"], thenUsage("unknown command: frobnicate", tool));
+  bad(tool, ["list"], thenUsage("list needs a unit", tool));
+  bad(tool, ["units"], thenUsage("units needs another install", tool));
+  bad(tool, ["name", "show"], thenUsage("name needs a unit and at least one =", tool));
+  bad(tool, ["name", "show", "0#0"], thenUsage("expected =, got: 0#0", tool));
+  bad(tool, ["view"], thenUsage("view needs a unit, or --all", tool));
+  bad(tool, ["view", "--all"], thenUsage("view --all needs --out ", tool));
+  bad(tool, ["view", "show", "--all", "--out", "x.js"], thenUsage("unexpected argument: show", tool));
+  bad(tool, ["survey", "extra"], thenUsage("unexpected argument: extra", tool));
+  bad(tool, ["survey", "--map", "x.json"], thenUsage("--map does not apply to survey", tool));
+  bad(tool, ["check", "--out", "x.js"], thenUsage("--out does not apply to check", tool));
+  bad(tool, ["shape", "fix.js", "--copy-to", "x"], thenUsage("--copy-to does not apply to shape", tool));
+  bad(tool, ["patch", "fix.js"], thenUsage("patch needs one of --out  and --copy-to ", tool));
+  bad(
+    tool,
+    ["patch", "fix.js", "--out", "a.js", "--copy-to", "b"],
+    thenUsage("patch needs one of --out  and --copy-to ", tool),
+  );
+  bad(
+    tool,
+    ["patch", "fix.js", "--out", path.join(REPO_ROOT, "main.js")],
+    new RegExp(`^refusing --out .+: it is inside the repository\\nusage: node ${literal(tool)} `),
+  );
+  bad(
+    tool,
+    ["patch", "fix.js", "--copy-to", path.join(REPO_ROOT, "copy")],
+    new RegExp(`^refusing --copy-to .+: it is not under .+\\nusage: node ${literal(tool)} `),
+  );
+  bad(tool, ["survey"], /^no main\.js at .+main\.js\n$/);
+}
+
 // vb6run reads its command line before it reads its file or looks for VB6, and
 // follows the message with its usage.
 {
diff --git a/scripts/lib/tb-ide.mjs b/scripts/lib/tb-ide.mjs
index c6a72a67a..56184b0e0 100644
--- a/scripts/lib/tb-ide.mjs
+++ b/scripts/lib/tb-ide.mjs
@@ -505,17 +505,26 @@ export async function launchOnDesktop({ exe, desktop, tag, job = true, ...rest }
   return run;
 }
 
-async function startOnDesktop({ exe, arg = "", args, desktop, job, env, cwd, stdout, stderr, dialogs }) {
-  const script = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "tb-launch.ps1"), "utf8");
-  // -EncodedCommand is UTF-16 in base64, 2.7 characters to each of the script's,
-  // and a command line stops at 32,767: the script outgrew that at about 12,000
-  // characters (ENAMETOOLONG). So the script travels in an environment variable,
-  // which stops at 32,767 characters of its own, and the command is a bootstrap
-  // that takes it out of the environment, so that the program it launches does
-  // not inherit it, and runs it.
+// tb-launch.ps1 as PowerShell is given it. -EncodedCommand is UTF-16 in base64, 2.7 characters
+// to each of the script's, and a command line stops at 32,767: the script outgrew that at about
+// 12,000 characters (ENAMETOOLONG). So the script travels in an environment variable, which
+// stops at 32,767 characters of its own, and the command is a bootstrap that takes it out of
+// the environment, so that the program it launches does not inherit it, and runs it. Each line
+// that is only a comment is emptied, which keeps an error's line number: the comments are a
+// quarter of the file, and no line of its C# starts with `#`.
+function launchScript() {
+  const script = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "tb-launch.ps1"), "utf8").replace(
+    /^[ \t]*#.*$/gm,
+    "",
+  );
   if (script.length > 30000) {
     throw new Error(`tb-launch.ps1 is ${script.length} characters, and an environment variable stops at 32,767`);
   }
+  return script;
+}
+
+async function startOnDesktop({ exe, arg = "", args, desktop, job, env, cwd, stdout, stderr, dialogs }) {
+  const script = launchScript();
   const ps = spawn(
     "powershell",
     ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(BOOTSTRAP, "utf16le").toString("base64")],
@@ -577,7 +586,7 @@ async function startOnDesktop({ exe, arg = "", args, desktop, job, env, cwd, std
 // Runs tb-launch.ps1 in its window mode (TBBUILD_WINDOWS) on a private desktop and returns
 // the one line of JSON it prints; throws with its error.
 function onDesktopWindows(desktop, vars) {
-  const script = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "tb-launch.ps1"), "utf8");
+  const script = launchScript();
   return new Promise((resolve, reject) => {
     const ps = spawn(
       "powershell",
@@ -661,6 +670,29 @@ export function captureWindow(desktop, hwnd, file, { flags = 2 } = {}) {
   });
 }
 
+/**
+ * Posts a message to a window on a private desktop (PostMessage). A key pressed on a desktop
+ * that is not on screen reaches no window, so this is how a window's own menu is opened there:
+ * WM_SYSCOMMAND (0x112) with SC_KEYMENU (0xF100) and a letter as lParam, as Alt and the letter
+ * do. The menu is then a top-level window of class #32768 on the desktop.
+ *
+ * @param {string} desktop
+ * @param {number} hwnd   from desktopWindows
+ * @param {number} msg
+ * @param {number} [wParam]
+ * @param {number} [lParam]
+ * @returns {Promise<{posted: true}>}
+ */
+export function postWindowMessage(desktop, hwnd, msg, wParam = 0, lParam = 0) {
+  return onDesktopWindows(desktop, {
+    TBBUILD_WINDOWS: "post",
+    TBBUILD_HWND: String(hwnd),
+    TBBUILD_MSG: String(msg),
+    TBBUILD_WPARAM: String(wParam),
+    TBBUILD_LPARAM: String(lParam),
+  });
+}
+
 /**
  * What a tool prints under --keep: the kept IDE's pid, and the commands that end
  * it with every process it started (/T takes the compiler and the rest of its
diff --git a/scripts/lib/tb-launch.ps1 b/scripts/lib/tb-launch.ps1
index d117dcf09..736f2f93f 100644
--- a/scripts/lib/tb-launch.ps1
+++ b/scripts/lib/tb-launch.ps1
@@ -57,6 +57,14 @@
 #                            window's colour key and alpha, and the texts of the
 #                            window and its child windows, for the caller to
 #                            search for the user name
+#   TBBUILD_WINDOWS=post     PostMessage of TBBUILD_MSG, with TBBUILD_WPARAM and
+#                            TBBUILD_LPARAM, to the window TBBUILD_HWND: a key
+#                            pressed on a desktop that is not on screen reaches
+#                            no window, so WM_SYSCOMMAND SC_KEYMENU with a letter
+#                            opens a window's menu, as Alt and the letter do
+#
+# tb-ide.mjs passes the script with each line that is only a comment emptied, so
+# no line of the C# below may start with `#`.
 #
 # The calls are made on a thread of their own that has been put on the desktop,
 # since a thread sees and draws only the windows of its own desktop, and that is
@@ -129,6 +137,7 @@ public static class TbWindows {
   [DllImport("user32.dll")] static extern uint GetDpiForWindow(IntPtr h);
   [DllImport("user32.dll")] static extern IntPtr SetThreadDpiAwarenessContext(IntPtr ctx);
   [DllImport("user32.dll", SetLastError = true)] static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint flags);
+  [DllImport("user32.dll", SetLastError = true)] static extern bool PostMessage(IntPtr h, uint m, IntPtr w, IntPtr l);
   [DllImport("user32.dll")] static extern bool GetLayeredWindowAttributes(IntPtr h, out uint key, out byte alpha, out uint flags);
   [DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr h, int a, out RECT r, int n);
   [DllImport("user32.dll")] static extern bool GetClientRect(IntPtr h, out RECT r);
@@ -246,6 +255,15 @@ public static class TbWindows {
     });
     return result;
   }
+
+  public static string Post(string desktop, long hwnd, uint msg, long w, long l) {
+    OnDesktop(desktop, (desk) => {
+      IntPtr h = new IntPtr(hwnd);
+      if (!IsWindow(h)) throw new Exception("there is no window " + hwnd);
+      if (!PostMessage(h, msg, new IntPtr(w), new IntPtr(l))) throw Failed("PostMessage");
+    });
+    return "{\"posted\":true}";
+  }
 }
 '@
   $desktop = $env:TBBUILD_DESKTOP
@@ -254,8 +272,10 @@ public static class TbWindows {
   } elseif ($env:TBBUILD_WINDOWS -eq "capture") {
     $flags = if ($env:TBBUILD_FLAGS) { [uint32]$env:TBBUILD_FLAGS } else { [uint32]2 }
     Write-Output ([TbWindows]::Capture($desktop, [long]$env:TBBUILD_HWND, $flags, $env:TBBUILD_FILE))
+  } elseif ($env:TBBUILD_WINDOWS -eq "post") {
+    Write-Output ([TbWindows]::Post($desktop, [long]$env:TBBUILD_HWND, [uint32]$env:TBBUILD_MSG, [long]$env:TBBUILD_WPARAM, [long]$env:TBBUILD_LPARAM))
   } else {
-    throw "TBBUILD_WINDOWS is neither list nor capture: $($env:TBBUILD_WINDOWS)"
+    throw "TBBUILD_WINDOWS is not list, capture or post: $($env:TBBUILD_WINDOWS)"
   }
   exit 0
 }
diff --git a/scripts/shoot_docs.mjs b/scripts/shoot_docs.mjs
index 75520838d..27a6db517 100644
--- a/scripts/shoot_docs.mjs
+++ b/scripts/shoot_docs.mjs
@@ -175,6 +175,7 @@ import {
   killTree,
   launchOnDesktop,
   leaseSlot,
+  postWindowMessage,
   shutdownIde,
   sleep,
   waitForCompile,
@@ -1377,15 +1378,17 @@ function stageProjects(run, { refs = false } = {}) {
   unpackProject(path.join(path.dirname(ide), "projects", "_Standard EXE", "projectName.twinproj"), template);
   mkdirSync(path.join(src, "Resources", "ICON"), { recursive: true });
   cpSync(path.join(template, "Resources", "ICON", "twinBASIC.ico"), path.join(src, "Resources", "ICON", "AppIcon.ico"));
+  const file = path.join(src, "Settings");
+  const settings = JSON.parse(readFileSync(file, "utf8"));
+  // Export Project's folder, a folder of the tool's own (PACKAGE_ROOT), which the export makes
+  settings["project.exportPath"] = PJ_EXPORT();
   if (refs) {
-    const file = path.join(src, "Settings");
-    const settings = JSON.parse(readFileSync(file, "utf8"));
     const list = settings["project.references"];
     const scripting = list.find((r) => r.name === "Microsoft Scripting Runtime");
     scripting.overrideSymbol = "SR";
     list.push(MISSING_LIBRARY);
-    writeFileSync(file, `${JSON.stringify(settings, null, "\t")}\n`);
   }
+  writeFileSync(file, `${JSON.stringify(settings, null, "\t")}\n`);
   return src;
 }
 
@@ -4952,6 +4955,55 @@ const formsChapterShots = [
       }
     },
   },
+  {
+    // frmMain at 50 %, picked in the zoom box as Designer_Zoom picks it: the half-size form, its
+    // readouts still in form pixels. The form's grid is turned off first (its command, as the toolbar's grid
+    // button runs it; a change of the form that formsReset discards): its dots, scaled, come out
+    // differently in every capture. Taken once, in the dark theme only, as the owner decided.
+    out: FORMS_IMAGE("Designer_Zoom50"),
+    setup: "forms-chapter",
+    once: true,
+    async take({ c }) {
+      await formsDesigner(c);
+      try {
+        const look = () =>
+          c.evaluate(
+            `(() => { const d = ${DESIGNER_GLOBALS}.designer; return d.style.cssText + "|" + getComputedStyle(d).backgroundImage; })()`,
+          );
+        const before = await look();
+        await command(c, "tbFormDesigner_TurnGridIndicatorsOff");
+        if (!(await waitFor(c, async () => (await look()) !== before, { timeout: 5000, interval: 50 }))) {
+          throw new Error("turning the grid off changed nothing on the form");
+        }
+        const full = (await formArea(c)).width;
+        await c.evaluate(`(() => {
+  zoomSelect.selectedIndex = [...zoomSelect.options].findIndex((o) => o.text === "50%");
+  zoomSelect.dispatchEvent(new Event("change", { bubbles: true }));
+})()`);
+        const half = await waitFor(
+          c,
+          async () => {
+            const w = (await formArea(c)).width;
+            return w < full * 0.6 ? w : null;
+          },
+          { timeout: 5000, interval: 50 },
+        );
+        if (!half) throw new Error("50 % in the zoom box did not shrink the form");
+        await designerShown(c);
+        await parkMouse(c);
+        // the form, its menu bar and its readouts, clipped as formStateShot clips them: the box
+        // is at the toolbar's far end, and the tab above shows the form changed
+        const area = await formArea(c);
+        const clip = snapOut({ x: area.x, y: area.y, width: area.width + 6, height: area.height + 8 });
+        return await capture(c, "Designer_Zoom50", clip, {
+          away: () => parkMouse(c),
+          unsteady: await groundUnsteady(c, clip),
+        });
+      } finally {
+        await formsReset(c);
+      }
+    },
+  },
   {
     // a right click on Command1, with Command2(0) selected too, and the Align submenu open
     out: FORMS_IMAGE("Controls_ContextMenu"),
@@ -5379,24 +5431,38 @@ const formsChapterShots = [
   },
   {
     // frmMain previewed with the toolbar's PREVIEW: its window on the IDE's desktop, as it draws
-    // itself, with the File menu of the menu editor in its menu bar; taken once, as a running
-    // program's window is
+    // itself, with the File menu of the menu editor open, as Alt+F opens it (WM_SYSCOMMAND
+    // SC_KEYMENU 'f' posted to the window), the menu's own window laid over the form's; taken
+    // once, as a running program's window is
     out: FORMS_IMAGE("Preview_Window"),
     setup: "forms-chapter",
     once: true,
     async take(ctx) {
       const { c } = ctx;
       await formsDesigner(c);
+      let w = null;
       try {
         const preview = await rectOf(c, "#menuBarPreview");
         if (!preview) throw new Error("the toolbar has no PREVIEW button");
         await pressAt(c, preview.x + preview.width / 2, preview.y + preview.height / 2);
         await parkMouse(c);
-        const w = await formWindow(ctx.desktop, { title: "frmMain", timeout: 60000 });
-        return besideEachOther([
-          await windowPicture(ctx.desktop, w.hwnd, "preview", path.join(ctx.windows, "preview.png")),
-        ]);
+        w = await formWindow(ctx.desktop, { title: "frmMain", timeout: 60000 });
+        await postWindowMessage(ctx.desktop, w.hwnd, WM_SYSCOMMAND, SC_KEYMENU, "f".charCodeAt(0));
+        const menu = await waitFor(null, () => openMenuWindow(ctx.desktop, w.pid), { timeout: 5000, interval: 100 });
+        if (!menu) throw new Error("the File menu of the previewed form did not open");
+        const form = await windowPicture(ctx.desktop, w.hwnd, "preview", path.join(ctx.windows, "preview.png"));
+        const drop = await windowPicture(ctx.desktop, menu.hwnd, "its File menu", path.join(ctx.windows, "menu.png"));
+        // the form's rectangle again, in case it moved since it was listed
+        const now = (await desktopWindows(ctx.desktop)).find((x) => x.hwnd === w.hwnd) ?? w;
+        return besideEachOther([withMenu(now, form, menu, drop)]);
       } finally {
+        if (w && (await openMenuWindow(ctx.desktop, w.pid))) {
+          await postWindowMessage(ctx.desktop, w.hwnd, WM_CANCELMODE);
+          await waitFor(null, async () => !(await openMenuWindow(ctx.desktop, w.pid)), {
+            timeout: 5000,
+            interval: 100,
+          });
+        }
         await stopRun(c);
         await clearConsole(c);
         await formsReset(c);
@@ -6265,8 +6331,57 @@ const projectsShots = [
       }
     },
   },
+  {
+    // File > Export Project into the Export Path the staged Settings give (PJ_EXPORT, a folder of
+    // the tool's own, so that the first line names no user), Export Verbose off: the DEBUG
+    // CONSOLE floated (docked, it is too narrow for the first line) and emptied with its Clear
+    // button first, then the two lines the export writes. The console is emptied again
+    // afterwards, as the load left it for no other picture.
+    out: PJ_IMAGE("Export_Console"),
+    setup: "projects",
+    async take({ c }) {
+      await projectsReset(c);
+      // each export into an empty folder, as the first one is
+      rmSync(PJ_EXPORT(), { recursive: true, force: true });
+      try {
+        const box = await floatPanel(c, "DEBUG CONSOLE", { width: 600, height: 132 });
+        await clearConsole(c);
+        await openMenu(c, "File");
+        const it = (await menuItems(c)).find((i) => i.text.startsWith("Export Project"));
+        if (!it) throw new Error("the File menu has no Export Project");
+        await mouseMove(c, it.x + 20, it.y + it.height / 2);
+        await clickAt(c, it.x + 20, it.y + it.height / 2);
+        const text = await waitFor(
+          c,
+          async () => {
+            const t = await consoleText(c);
+            return /\[EXPORT\] (COMPLETED|export failed)/.test(t) && t;
+          },
+          { timeout: 60000, interval: 200 },
+        );
+        if (!text?.includes("COMPLETED")) {
+          const open = (await modals(c)).map((m) => m.title);
+          throw new Error(
+            `Export Project did not complete: ${JSON.stringify(text || (await consoleText(c)))}, dialogs ${JSON.stringify(open)}`,
+          );
+        }
+        await parkMouse(c);
+        await fixConsoleTimes(c);
+        await frames(c);
+        return await capture(c, "Export_Console", snapOut(box), { away: () => parkMouse(c) });
+      } finally {
+        await clearConsole(c);
+        await unfloatPanel(c, "DEBUG CONSOLE");
+        await projectsReset(c);
+        rmSync(PJ_EXPORT(), { recursive: true, force: true });
+      }
+    },
+  },
 ];
 
+// The Export Path the projects setups' staged Settings give, for Export_Console.
+const PJ_EXPORT = () => path.join(PACKAGE_ROOT, "Export");
+
 // The Recent tab's entries for Failed_To_Load: paths of projects that do not exist (the first
 // is double-clicked), in place of the user's own list, in the page only.
 const RECENT_GONE = [
@@ -9718,9 +9833,41 @@ async function windowPicture(desktop, hwnd, name, file) {
       }
     }
   }
-  return { picture: crop(picture, info.frame), dpi: info.dpi };
+  return { picture: crop(picture, info.frame), dpi: info.dpi, frame: info.frame };
+}
+
+// A window with the menu its menu bar opens: the window's picture, and the menu's window (a
+// top-level window of class #32768, as the system draws a menu) laid over it where it is on the
+// desktop, `win` and `menu` each as desktopWindows lists it. The ground beyond the window, where
+// the menu reaches past it, is transparent.
+function withMenu(win, winPicture, menu, menuPicture) {
+  const at = (w, p) => [w.rect[0] + p.frame[0], w.rect[1] + p.frame[1]];
+  const [wx, wy] = at(win, winPicture);
+  const [mx, my] = at(menu, menuPicture);
+  const x0 = Math.min(wx, mx);
+  const y0 = Math.min(wy, my);
+  const width = Math.max(wx + winPicture.picture.width, mx + menuPicture.picture.width) - x0;
+  const height = Math.max(wy + winPicture.picture.height, my + menuPicture.picture.height) - y0;
+  const rgba = Buffer.alloc(width * height * 4);
+  for (const [p, x, y] of [
+    [winPicture.picture, wx - x0, wy - y0],
+    [menuPicture.picture, mx - x0, my - y0],
+  ]) {
+    for (let r = 0; r < p.height; r++)
+      rgba.set(p.rgba.subarray(r * p.width * 4, (r + 1) * p.width * 4), ((y + r) * width + x) * 4);
+  }
+  return { picture: { width, height, rgba }, dpi: winPicture.dpi };
 }
 
+// The open menu of the program `pid` on `desktop` (class #32768), or null.
+const openMenuWindow = async (desktop, pid) =>
+  (await desktopWindows(desktop)).find((w) => w.depth === 0 && w.visible && w.class === "#32768" && w.pid === pid) ??
+  null;
+
+const WM_SYSCOMMAND = 0x112;
+const SC_KEYMENU = 0xf100;
+const WM_CANCELMODE = 0x1f;
+
 // The pictures of windows side by side, their tops level, WINDOW_GAP apart on a transparent
 // ground, as a PNG to be shown at its size at 96 DPI.
 function besideEachOther(windows) {
diff --git a/test.bat b/test.bat
index 7e9e03699..a58a4b12c 100644
--- a/test.bat
+++ b/test.bat
@@ -122,6 +122,14 @@ node --test test/addin-project.test.mjs
 @rem install. No tree, no browser, well under a second.
 node --test test/indexer.test.mjs
 @if errorlevel 1 goto :fail
+@rem Unit tests for scripts/ide_js.mjs, which reads, names and patches the
+@rem twinBASIC IDE's minified main.js: the byte-exact round trip, the keys
+@rem a local is named by and how a name carries to another build, and a fix
+@rem patched in as the smallest token edit. The "builds" are a few lines
+@rem written in the test; no copy of an IDE's main.js is in the repository.
+@rem No tree, no install, about 1 s.
+node --test test/ide-js.test.mjs
+@if errorlevel 1 goto :fail
 @rem A regex that backtracks exponentially is a hang waiting for the
 @rem right input, and nothing that reads the site can see it: the corpus
 @rem passes until some page happens to contain the trigger, and then the
diff --git a/test/addin/help.test.mjs b/test/addin/help.test.mjs
index c37e46022..125ff261d 100644
--- a/test/addin/help.test.mjs
+++ b/test/addin/help.test.mjs
@@ -512,6 +512,16 @@ scenario("the help add-in: F1 and the help pane", (lane) => {
     // ErrObject.Number from every other Number.
     [14, 36, "/tB/Packages/VB/App/#path", "App.Path"],
     [14, 64, "/tB/Modules/ErrObject/Number", "Err.Number"],
+    // A data type is the language's, at its section of the Data Types page;
+    // the function or property of the same name is picked by the hover's
+    // package, which names VBA for String( and Date, and nothing for a type.
+    // No two of the page's cases are next to each other: a change of
+    // fragment alone does not load the page again.
+    [8, 29, "/Reference/Data-Types#long", "a data type: Long"],
+    [42, 27, "/tB/Modules/Strings/String", 'String(5, "x"): the function, not the type'],
+    [42, 18, "/Reference/Data-Types#string", "As String: the type, not the function"],
+    [43, 25, "/tB/Modules/DateTime/Date", "= Date: the property, not the type"],
+    [43, 18, "/Reference/Data-Types#date", "As Date: the type, not the property"],
   ];
   for (const [line, column, url, what] of CASES) {
     test(`F1 on ${what} (${line}:${column})`, async () => {
diff --git a/test/addin/helphost/Sources/Cases.twin b/test/addin/helphost/Sources/Cases.twin
index e00d5d7b4..2482d8f93 100644
--- a/test/addin/helphost/Sources/Cases.twin
+++ b/test/addin/helphost/Sources/Cases.twin
@@ -37,4 +37,9 @@ Module Cases
     End Sub
 
     ' CefBrowser is in a package this project does not reference.
+
+    Public Sub Types()
+        Dim s As String = String(5, "x")
+        Dim d As Date = Date
+    End Sub
 End Module
diff --git a/test/example-batches.test.mjs b/test/example-batches.test.mjs
index f6ac46af0..c3a9bb9f5 100644
--- a/test/example-batches.test.mjs
+++ b/test/example-batches.test.mjs
@@ -147,6 +147,7 @@ test("test/harness.json is read as its contract says", () => {
   // The overrides: option, then variable, then file; seconds in, ms out.
   const harness = copy();
   harness.timeouts.compile = 30;
+  harness.timeouts.projectOpen = 15;
   assert.equal(timeoutMs("compile", undefined, { env: {}, harness }), 30000);
   assert.equal(timeoutMs("compile", undefined, { env: { TB_COMPILE_TIMEOUT: "2.5" }, harness }), 2500);
   assert.equal(timeoutMs("compile", 0.25, { env: { TB_COMPILE_TIMEOUT: "2.5" }, harness }), 250);
diff --git a/test/example-projects/contributed-DetoursLib/Settings b/test/example-projects/contributed-DetoursLib/Settings
new file mode 100644
index 000000000..735c250aa
--- /dev/null
+++ b/test/example-projects/contributed-DetoursLib/Settings
@@ -0,0 +1,74 @@
+{
+	"configuration.inherits": "Defaults",
+	"project.appTitle": "Documentation samples",
+	"project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}",
+	"project.buildType": "Standard EXE",
+	"project.description": "Template for scripts/check_examples.mjs. project.id, project.name and project.buildPath are rewritten in the staged copy, one per batch.",
+	"project.exportPathIsV2": true,
+	"project.id": "{7B247400-0000-4000-9000-7B2474000000}",
+	"project.name": "DocSamples",
+	"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
+		},
+		{
+			"autoCtlExt": true,
+			"id": "{51073BF8-B242-47E4-9800-DF586ED07F52}",
+			"licence": "MIT",
+			"name": "[IMPORTED] DetoursLib - Lightweight x86/x64 inline function hooking for twinBASIC, allowing interception and replacement of Windows API calls and native functions using KNSoft SlimDetours.",
+			"path32": "/Packages/DetoursLib",
+			"path64": "/Packages/DetoursLib",
+			"symbolId": "DetoursLib",
+			"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/test/harness.json b/test/harness.json
index ac877f304..a8c29a794 100644
--- a/test/harness.json
+++ b/test/harness.json
@@ -119,15 +119,15 @@
     }
   },
   "timeouts": {
-    "projectOpen": 15,
+    "projectOpen": 10,
     "handshake": 10,
-    "load": 120,
-    "setup": 30,
-    "compile": 30,
-    "licence": 60,
-    "build": 15,
-    "compilerRun": 60,
+    "load": 10,
+    "setup": 10,
+    "compile": 10,
+    "licence": 10,
+    "build": 10,
+    "compilerRun": 10,
     "compilerRunQuiet": 8,
-    "exeRun": 60
+    "exeRun": 10
   }
 }
diff --git a/test/harness.md b/test/harness.md
index 07396993c..6cf08c88a 100644
--- a/test/harness.md
+++ b/test/harness.md
@@ -273,11 +273,17 @@ All in seconds, decimals allowed. Each runs from a start event to an end event:
 Small fixed waits are internal and outside the file (tblaunch's 2 s crash grace and 5 s for the
 compiler's exit, the documentation harness's polls).
 
-The values rest on BETA 1010: the compiler's own compile takes at most 0.30 s under the IDE and
-1.27 s under tblaunch, cold start included; the documentation harness's whole compile wait,
-project load included, at most 13.8 s; IDE start to project open at most 4.04 s; a build at most
-1.0 s, 4.97 s with LLVM; a compiler run at most 0.83 s, 25.3 s with LLVM (one LLVM run printed for
-15 s and finished).
+Every limit but `compilerRunQuiet` is 10 s (owner): a compiler that is not done in seconds is
+wedged and does not recover, and a sample's run takes seconds too. The values rest on BETA 1010:
+a clean batch's whole trip through tblaunch (start, compile, build, the IDE run and the exe run;
+plain, exe and LLVM exe, win32 and win64, four lanes at once) takes at most 6.4 s, about 2.5 s at
+the median; the compiler's own compile at most 0.30 s under the IDE and 1.27 s under tblaunch,
+cold start included; IDE start to project open at most 4.04 s; a build at most 1.0 s, 4.97 s with
+LLVM; a compiler run at most 0.83 s without LLVM. What 10 s cuts off is known and accepted: the
+documentation harness's compile wait for the WinReg batches (12.5--13.8 s, project load
+included) and one TBMANLIB_Aes LLVM run that printed for 15 s, both Experimental packages.
+`package_tests.mjs` passes its own limit for a test case's run (`--timeout`, 60 s by default),
+because a case's run time is the package's own: tbRegExp's step-limit cases take about 16 s.
 
 **Overrides**, in seconds: **an option beats a variable, which beats the file**, which beats a
 built-in default (tblaunch only). A tool has the options of the phases it runs.
@@ -314,3 +320,9 @@ an LLVM refusal is always a `build` failure (owner); the build's lines end at `[
 tbRegExp at `build`; the timeouts and their overrides as above (owner's approval of the values;
 `handshake` added at tblaunch's request), and 60 samples at `compiler-run` that end silently,
 fault or hang under LLVM (owner).
+
+On 2026-10-18 the limits' values changed within version 1, the keys unchanged: `projectOpen`
+15 → 10, `load` 120 → 10, `setup` 30 → 10, `compile` 30 → 10, `licence` 60 → 10, `build` 15 → 10,
+`compilerRun` 60 → 10, `exeRun` 60 → 10 (owner, at tblaunch's request with its timings; tblaunch's
+built-in defaults move to the same values in its contract version 5). A reader of version 1 takes
+whatever values the file holds, so no version was raised.
diff --git a/test/ide-js.test.mjs b/test/ide-js.test.mjs
new file mode 100644
index 000000000..47b89f5fc
--- /dev/null
+++ b/test/ide-js.test.mjs
@@ -0,0 +1,374 @@
+// Unit tests for scripts/ide-js/lib.mjs and scripts/ide_js.mjs, the tool that reads, names and
+// patches the twinBASIC IDE's minified ide/main.js: the byte-exact round trip, the keys a binding
+// is named by, how a name carries to another build (exact, carried, review, lost), the names the
+// map refuses, splicing, and a fix patched in as the smallest token edit. The tool's own refusals
+// to write into the repository or outside the temp folder are run as a child process.
+//
+// The "builds" are a few lines written here, minified by hand: one program under two sets of
+// local names, and the changes a new build makes to a function. No copy of an IDE's main.js is
+// in the repository, and none is needed. Runs with a bare `node --test test/ide-js.test.mjs`: no
+// tree, no IDE.
+
+import assert from "node:assert/strict";
+import { execFileSync } from "node:child_process";
+import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import { after, before, describe, test } from "node:test";
+import { REPO_ROOT } from "../lib/repo-paths.mjs";
+import {
+  bindings,
+  checkUnit,
+  coverage,
+  FixError,
+  fixShapes,
+  lineDiff,
+  NameError,
+  namedUnits,
+  parse,
+  patchUnits,
+  renamedUnitText,
+  sealUnit,
+  splice,
+  unitInfo,
+} from "../scripts/ide-js/lib.mjs";
+
+// Build A, and build B: the same program, every local renamed as a minifier would.
+const A =
+  "var g=2;/*kept*/function show(e,t){var n=e.length;if(t){let r=e+n;return r}return wrap(e)}" +
+  "function wrap(e){return'<'+e+'>'+`${e}`}" +
+  "class Box{size(e){return e*g}static make(e,{t}){return new Box(e,t)}}\n";
+const B =
+  "var g=2;/*kept*/function show(o,e){var t=o.length;if(e){let n=o+t;return n}return wrap(o)}" +
+  "function wrap(t){return'<'+t+'>'+`${t}`}" +
+  "class Box{size(t){return t*g}static make(n,{t:e}){return new Box(n,e)}}\n";
+// What a later build can do to show: use a variable once more, drop a parameter, drop the function.
+const MORE_REFS = B.replace("let n=o+t;", "let n=o+t+t;");
+const ONE_PARAM = B.replace("function show(o,e){var t=o.length;if(e)", "function show(o){var t=o.length;if(t)");
+const NO_SHOW = B.replace(/function show.*?return wrap\(o\)\}/, "");
+
+const NAMES = {
+  show: { "0#0": "text", "0#1": "flag", "0#2": "len", "0/0#0": "joined" },
+  wrap: { "0#0": "s" },
+  "Box.size": { "0#0": "count" },
+  "Box.static make": { "0#1": "tag" },
+};
+
+/** The map sealed against build A, as `name` writes it. */
+function mapOnA() {
+  const a = parse(A);
+  const units = {};
+  for (const [unit, names] of Object.entries(NAMES)) units[unit] = sealUnit(a, unit, names, "BETA 1");
+  return { format: 1, about: "test", units };
+}
+
+describe("the round trip", () => {
+  test("the tokens, comments and whitespace between them are every byte of the file", () => {
+    const text = `${A}  /* a comment */ var x = /a+/g; // and a line comment\n`;
+    const c = coverage(parse(text));
+    assert.equal(c.gaps, 0);
+    assert.equal(c.overlaps, 0);
+    assert.equal(c.exact, true);
+  });
+
+  test("a splice with no edits is the input", () => {
+    assert.equal(splice(A, 0, A.length, []), A);
+  });
+});
+
+describe("units and bindings", () => {
+  test("a unit is a top-level function or a method of a top-level class", () => {
+    assert.deepEqual([...namedUnits(parse(A).ast).keys()], ["show", "wrap", "Box.size", "Box.static make"]);
+  });
+
+  test("a binding's key says where it is declared, not what it is called", () => {
+    const key = (text) => bindings(parse(text), "show").bindings.map((x) => [x.key, x.def, x.kind, x.refs]);
+    assert.deepEqual(key(A), [
+      ["0#0", "Parameter", "param0", 3],
+      ["0#1", "Parameter", "param1", 1],
+      ["0#2", "Variable", "var", 2],
+      ["0/0#0", "Variable", "let", 2],
+    ]);
+    assert.deepEqual(key(B), key(A));
+    assert.deepEqual(
+      bindings(parse(A), "show").bindings.map((x) => x.name),
+      ["e", "t", "n", "r"],
+    );
+  });
+
+  test("the same code under other local names has the same shape, and changed code another", () => {
+    assert.equal(unitInfo(parse(A), "show").shape, unitInfo(parse(B), "show").shape);
+    assert.notEqual(unitInfo(parse(A), "show").shape, unitInfo(parse(MORE_REFS), "show").shape);
+  });
+
+  test("a destructured parameter is numbered by the parameter it is in", () => {
+    const x = bindings(parse(A), "Box.static make").bindings.find((b) => b.name === "t");
+    assert.equal(x.kind, "param1");
+  });
+});
+
+describe("the name map across builds", () => {
+  const verdicts = (text, unit = "show") =>
+    Object.fromEntries(checkUnit(parse(text), unit, mapOnA().units[unit]).verdicts.map((v) => [v.name, v.verdict]));
+
+  test("sealing records what each named binding is in the build", () => {
+    const entry = mapOnA().units.show;
+    assert.equal(entry.namedOn, "BETA 1");
+    assert.deepEqual(entry.names["0#2"], { name: "len", was: "n", def: "Variable", kind: "var", refs: 2 });
+  });
+
+  test("an unchanged unit takes every name exactly, whatever the minifier called them", () => {
+    assert.deepEqual(verdicts(B), { text: "exact", flag: "exact", len: "exact", joined: "exact" });
+  });
+
+  test("in a changed unit, a binding used as often is carried and one used more is for review", () => {
+    assert.deepEqual(verdicts(MORE_REFS), { text: "carried", flag: "carried", len: "review", joined: "carried" });
+  });
+
+  test("a key that finds a binding of another kind, or none, loses its name", () => {
+    const v = checkUnit(parse(ONE_PARAM), "show", mapOnA().units.show).verdicts;
+    assert.deepEqual(
+      v.map((x) => [x.name, x.verdict]),
+      [
+        ["text", "carried"],
+        ["flag", "lost"],
+        ["len", "lost"],
+        ["joined", "carried"],
+      ],
+    );
+    assert.match(v[1].why, /now Variable var t/);
+    assert.match(v[2].why, /no binding has that key/);
+  });
+
+  test("a unit the build no longer has loses every name", () => {
+    assert.deepEqual(verdicts(NO_SHOW), { text: "lost", flag: "lost", len: "lost", joined: "lost" });
+  });
+
+  test("the map refuses a name that is not an identifier, is given twice, or hides another", () => {
+    const a = parse(A);
+    const refused = (unit, names, re) =>
+      assert.throws(
+        () => sealUnit(a, unit, names, "BETA 1"),
+        (e) => e instanceof NameError && re.test(e.message),
+      );
+    refused("show", { "0#0": "two words" }, /not a name a variable can have/);
+    refused("show", { "0#0": "class" }, /not a name a variable can have/);
+    refused("show", { "0#0": "text", "0#1": "text" }, /given to both 0#0 and 0#1/);
+    refused("show", { "0#0": "wrap" }, /would hide the outer wrap/);
+    refused("show", { "0#0": "t" }, /would hide 0#1, which keeps that name/);
+    refused("show", { "9#9": "x" }, /no binding 9#9/);
+    refused("nothing", {}, /no unit named nothing/);
+  });
+
+  test("our names are put in at every place the binding is written, a shorthand keeping its key", () => {
+    const b = parse(B);
+    const map = mapOnA();
+    const names = (unit) => Object.fromEntries(Object.entries(map.units[unit].names).map(([k, n]) => [k, n.name]));
+    assert.equal(
+      renamedUnitText(b, unitInfo(b, "show"), names("show")),
+      "function show(text,flag){var len=text.length;if(flag){let joined=text+len;return joined}return wrap(text)}",
+    );
+    const a = parse(A);
+    assert.equal(
+      renamedUnitText(a, unitInfo(a, "Box.static make"), names("Box.static make")),
+      "(e,{t: tag}){return new Box(e,tag)}",
+    );
+  });
+});
+
+describe("splicing", () => {
+  test("each edit replaces its range and nothing else", () => {
+    assert.equal(
+      splice("abcdef", 0, 6, [
+        [4, 5, "E"],
+        [1, 2, "BB"],
+      ]),
+      "aBBcdEf",
+    );
+  });
+
+  test("overlapping edits and edits outside the range are refused", () => {
+    assert.throws(() =>
+      splice("abcdef", 0, 6, [
+        [1, 3, "x"],
+        [2, 4, "y"],
+      ]),
+    );
+    assert.throws(() => splice("abcdef", 2, 4, [[1, 2, "x"]]));
+  });
+});
+
+describe("patching", () => {
+  // A fix as it is written from the view: our names, prettier's quotes and semicolons.
+  const FIX = `function show(text, flag) {
+  var len = text.length;
+  if (flag) {
+    let joined = text + len + 1;
+    return joined;
+  }
+  return wrap(text);
+}
+function wrap(s) {
+  return "<" + s + ">" + \`\${s}\`;
+}
+class Box {
+  size(count) {
+    return count * g * 3;
+  }
+}
+`;
+
+  test("a fix becomes the smallest token edit of the build's text, every other byte kept", () => {
+    const r = patchUnits(parse(B), parse(FIX), mapOnA());
+    assert.deepEqual(
+      r.edits.map((e) => [e.unit, e.before, e.after]),
+      [
+        ["show", "", "+1"],
+        ["Box.size", "", "*3"],
+      ],
+    );
+    assert.equal(r.text, B.replace("let n=o+t;", "let n=o+t+1;").replace("return t*g}", "return t*g*3}"));
+    assert.deepEqual(r.skipped, []);
+  });
+
+  test("a unit the fix leaves as it was gives no edit, though printed with other quotes", () => {
+    const r = patchUnits(parse(B), parse('function wrap(s) { return "<" + s + ">" + `${s}`; }'), mapOnA());
+    assert.equal(r.edits.length, 0);
+    assert.equal(r.text, B);
+  });
+
+  test("a fix of a unit the build does not have is refused", () => {
+    assert.throws(
+      () => patchUnits(parse(NO_SHOW), parse(FIX), mapOnA()),
+      (e) => e instanceof FixError && /no unit named show/.test(e.message),
+    );
+  });
+
+  test("a new variable named like a minified one is refused, since it would change what a name finds", () => {
+    const fix = "function show(text, flag) { var len = text.length; var e = 1; return wrap(text + e); }";
+    assert.throws(
+      () => patchUnits(parse(B), parse(fix), mapOnA()),
+      (e) => e instanceof FixError && /one of the build's minified names/.test(e.message),
+    );
+  });
+
+  test("a name the map does not apply to the build is kept as written, and said", () => {
+    const r = patchUnits(parse(MORE_REFS), parse("function show(text, flag) { return 1; }"), mapOnA());
+    assert.deepEqual(
+      r.skipped.map(([unit, v]) => [unit, v.map((x) => x.name)]),
+      [["show", ["len"]]],
+    );
+  });
+});
+
+describe("the fix shape", () => {
+  test("a line diff has its context and a header for each run of changes", () => {
+    assert.equal(lineDiff("a\nb\nc\n", "a\nB\nc\n"), "@@ -1,3 +1,3 @@\n a\n-b\n+B\n c\n");
+    assert.equal(lineDiff("a\n", "a\n"), "");
+    const many = Array.from({ length: 20 }, (_, i) => `l${i}`).join("\n");
+    const d = lineDiff(many, many.replace("l2\n", "x\n").replace("l17\n", "y\n"));
+    assert.equal(d.match(/^@@/gm).length, 2);
+  });
+
+  test("the unit before and after are printed alike, in our names, and differ only in the fix", async () => {
+    const [s] = await fixShapes(
+      parse(B),
+      parse(
+        "function show(text, flag) { var len = text.length; if (flag) { let joined = text + len + 1; return joined; } return wrap(text); }",
+      ),
+      mapOnA(),
+    );
+    assert.equal(s.unit, "show");
+    assert.deepEqual(s.ours, [
+      ["o", "text"],
+      ["e", "flag"],
+      ["t", "len"],
+      ["n", "joined"],
+    ]);
+    assert.deepEqual(
+      s.diff.split("\n").filter((l) => /^[-+]/.test(l)),
+      ["-    let joined = text + len;", "+    let joined = text + len + 1;"],
+    );
+  });
+});
+
+describe("the command line", () => {
+  const TOOL = path.join(REPO_ROOT, "scripts", "ide_js.mjs");
+  let root;
+  let install;
+  let map;
+  let fix;
+
+  // Child processes run with this process's own Node flags.
+  const run = (...args) => {
+    try {
+      const stdout = execFileSync(process.execPath, [...process.execArgv, TOOL, ...args], {
+        encoding: "utf8",
+        stdio: ["ignore", "pipe", "pipe"],
+      });
+      return { code: 0, stdout, stderr: "" };
+    } catch (e) {
+      return { code: e.status, stdout: e.stdout, stderr: e.stderr };
+    }
+  };
+
+  before(() => {
+    root = mkdtempSync(path.join(tmpdir(), "ide-js-test-"));
+    install = path.join(root, "twinBASIC_IDE_BETA_2");
+    mkdirSync(path.join(install, "ide"), { recursive: true });
+    writeFileSync(path.join(install, "ide", "main.js"), B);
+    writeFileSync(path.join(install, "ide", "build.js"), "var tbVersionInfo = 'twinBASIC IDE BETA 2';");
+    writeFileSync(path.join(install, "twinBASIC.exe"), "");
+    map = path.join(root, "names.json");
+    writeFileSync(map, JSON.stringify(mapOnA()));
+    fix = path.join(root, "fix.js");
+    writeFileSync(fix, "function show(text, flag) { var len = text.length; return wrap(text + len); }");
+  });
+
+  after(() => {
+    rmSync(root, { recursive: true, force: true });
+  });
+
+  test("check says how the map applies, and exits 1 only when a name is lost", () => {
+    const r = run("check", "--ide", install, "--map", map);
+    assert.equal(r.code, 0);
+    assert.match(r.stdout, /^exact {4}show 0#0 e -> o = text$/m);
+    assert.match(r.stdout, /BETA 2: \{"exact":7,"carried":0,"review":0,"lost":0\}/);
+    const other = path.join(root, "other.js");
+    writeFileSync(other, ONE_PARAM);
+    assert.equal(run("check", "--ide", other, "--map", map).code, 1);
+  });
+
+  test("patch writes a main.js under the temp folder, and nothing into the repository", () => {
+    const out = path.join(root, "out", "main.js");
+    const r = run("patch", fix, "--ide", install, "--map", map, "--out", out);
+    assert.equal(r.code, 0, r.stderr);
+    assert.equal(readFileSync(out, "utf8"), B.replace("if(e){let n=o+t;return n}return wrap(o)", "return wrap(o+t)"));
+    const inRepo = run("patch", fix, "--ide", install, "--map", map, "--out", path.join(REPO_ROOT, "main.js"));
+    assert.equal(inRepo.code, 2);
+    assert.match(inRepo.stderr, /^refusing --out .*: it is inside the repository\nusage: /);
+    assert.equal(
+      run("patch", fix, "--ide", install, "--map", map, "--out", path.join(install, "ide", "main.js")).code,
+      2,
+    );
+  });
+
+  test("patch copies an install only into a folder under the temp folder", () => {
+    const r = run("patch", fix, "--ide", install, "--map", map, "--copy-to", path.join(REPO_ROOT, "copy"));
+    assert.equal(r.code, 2);
+    assert.match(r.stderr, /^refusing --copy-to .*: it is not under /);
+    const ok = run("patch", fix, "--ide", install, "--map", map, "--copy-to", path.join(root, "copy"));
+    assert.equal(ok.code, 0, ok.stderr);
+    const copied = readFileSync(path.join(root, "copy", "twinBASIC_IDE_BETA_2", "ide", "main.js"), "utf8");
+    assert.notEqual(copied, B);
+    assert.equal(readFileSync(path.join(install, "ide", "main.js"), "utf8"), B);
+  });
+
+  test("a fix that does not apply is a finding, exit 1", () => {
+    const missing = path.join(root, "missing.js");
+    writeFileSync(missing, "function nowhere() {}");
+    const r = run("patch", missing, "--ide", install, "--map", map, "--out", path.join(root, "x.js"));
+    assert.equal(r.code, 1);
+    assert.equal(r.stderr, "the build has no unit named nowhere\n");
+  });
+});
diff --git a/test/ide/building-hklm.test.mjs b/test/ide/building-hklm.test.mjs
new file mode 100644
index 000000000..8ef9ad7ae
--- /dev/null
+++ b/test/ide/building-hklm.test.mjs
@@ -0,0 +1,204 @@
+// Register DLLs to HKLM in an IDE that is not elevated, with real clicks: the
+// defects queued in BUGS-TO-REPORT.md from the trials for the Building pages
+// (their fact sheet's B13 and B12), "The HKLM warning shows twice as a project
+// opens, and for a Standard EXE too" and "Delete It And Continue in Clean's
+// box builds a new DLL instead of deleting the old one". Three IDEs, one after
+// the other: test/ide/probes/building-ax as it is (registering in HKCU), then
+// with Register DLLs to HKLM, then test/ide/probes/building-exe with it.
+//
+// The HKCU build writes the class's registration into HKCU; the Clean after it
+// removes it, and the test checks that every key is gone (building-kit.mjs's
+// axKeys: the fixture's fixed GUIDs, its ProgID and its type library, which is
+// the project's id). The HKLM builds fail to register, since the IDE is not
+// elevated, and write nothing; the last test checks that no key of the
+// fixture's exists in HKCU or HKLM. Nothing here writes the registry itself.
+//
+// Each test states what BETA 1010 does. If one fails after an IDE update, the
+// IDE has changed: update the matching entry in BUGS-TO-REPORT.md or
+// bugs/filed/, and then this file.
+//
+// Run it with ide-test.bat, which gives it a lane; on its own it is skipped.
+
+import assert from "node:assert/strict";
+import { existsSync, statSync } from "node:fs";
+import path from "node:path";
+import { before, test } from "node:test";
+import { scenario } from "../addin/scenario.mjs";
+import {
+  answer,
+  axKeys,
+  boxes,
+  guard,
+  ideSettings,
+  J,
+  linesAfter,
+  pageErrors,
+  presentKeys,
+  press,
+  sleep,
+  TEMPLATE_BUILD_PATH,
+  variant,
+} from "./building-kit.mjs";
+
+const AX = "BuildProbeAx";
+const HKLM_BOX =
+  "[twinBASIC] WARNING: This project registers itself to the HKEY_LOCAL_MACHINE registry hive which requires ADMIN permissions. The twinBASIC IDE is not currently running elevated, and so the registration of the DLL will fail during the build process. For the build to be successful, please restart the twinBASIC IDE using the 'Run As Administrator' Windows option. {Continue Anyway}";
+const UNREGISTER_BOX =
+  "[twinBASIC] Build failed: unable to unregister the previous DLL. Continue anyway? {Delete It And Continue/Cancel}";
+
+scenario("Building: Register DLLs to HKLM", (lane) => {
+  let c, keys;
+  const dll = (folder) => path.join(folder, "Build", `${AX}_win32.dll`);
+
+  // Open a project, answering each HKLM warning with Continue Anyway as it
+  // shows: the project's load waits for it. The boxes shown, each counted once:
+  // a box still on screen just after its click is marked in the page as seen.
+  async function openAnswering(src, folder) {
+    const answered = [];
+    let done = false;
+    const opening = lane.open(src, { folder, buildPath: TEMPLATE_BUILD_PATH }).finally(() => {
+      done = true;
+    });
+    const answering = (async () => {
+      for (let quietAfter = 0; !done || quietAfter < 10; ) {
+        const cc = lane.c;
+        if (cc) {
+          try {
+            const { fresh, showing } = await cc.evaluate(`(() => {
+              const ms = [...document.querySelectorAll(".modalDialogContainer")].filter((m) => m.getBoundingClientRect().width && m.innerText.includes("HKEY_LOCAL_MACHINE"));
+              const fresh = ms.filter((m) => !m.__seen);
+              for (const m of fresh) m.__seen = true;
+              return { fresh: fresh.length, showing: ms.length };
+            })()`);
+            if (fresh) {
+              const b = (await boxes(cc)).find((x) => x.includes("HKEY_LOCAL_MACHINE"));
+              for (let i = 0; i < fresh; i++) answered.push(b);
+            }
+            if (showing) {
+              await answer(cc, "Continue Anyway");
+              quietAfter = 0;
+              continue;
+            }
+          } catch {}
+        }
+        if (done) quietAfter++;
+        await sleep(300);
+      }
+    })();
+    c = await opening;
+    await answering;
+    await guard(c);
+    return answered;
+  }
+
+  before(async () => {
+    const folder = path.join(lane.work, "ax");
+    c = await lane.open(variant("building-ax", path.join(lane.work, "src-ax")), {
+      folder,
+      buildPath: TEMPLATE_BUILD_PATH,
+    });
+    await guard(c);
+    keys = axKeys((await ideSettings(c, AX))["project.id"]);
+  });
+
+  test("control: an ActiveX DLL registered in HKCU opens with no box, and Clean after its build deletes it and its registration", async () => {
+    const folder = path.join(lane.work, "ax");
+    assert.deepEqual(await boxes(c), []);
+    assert.deepEqual(presentKeys(keys), [], "the fixture's keys exist before its build");
+    const b = await press(c, "build");
+    let cleaned = false;
+    try {
+      assert.ok(
+        b.lines.some((l) =>
+          /^\[REGISTER\] type-library registration completed\.\s+DllRegisterServer\(\) returned OK$/.test(l),
+        ),
+        J(b.lines),
+      );
+      assert.ok(existsSync(dll(folder)));
+      assert.ok(presentKeys(keys).length > 0, "the build registered nothing");
+      const r = await press(c, "clean");
+      cleaned = true;
+      assert.ok(
+        r.lines.some((l) =>
+          /^\[UNREGISTER\] previous type-library deregistration completed\.\s+DllUnregisterServer\(\) returned OK$/.test(
+            l,
+          ),
+        ),
+        J(r.lines),
+      );
+      assert.ok(r.lines.includes("[BUILD-CLEAN] build cleanup completed."), J(r.lines));
+      assert.deepEqual(r.boxes, []);
+    } finally {
+      if (!cleaned) await press(c, "clean");
+    }
+    assert.equal(existsSync(dll(folder)), false);
+    assert.deepEqual(presentKeys(keys), [], "Clean left the registration");
+  });
+
+  test("an ActiveX DLL with Register DLLs to HKLM shows the HKLM warning twice as it opens", async () => {
+    await lane.closeProject();
+    const src = variant("building-ax", path.join(lane.work, "src-ax-hklm"), {
+      "project.dllRegisterLocalMachine": true,
+    });
+    const answered = await openAnswering(src, path.join(lane.work, "ax-hklm"));
+    assert.deepEqual(answered, [HKLM_BOX, HKLM_BOX]);
+    assert.deepEqual(await boxes(c), []);
+  });
+
+  test("control: at Build, Delete It And Continue in the box of a failed deregistration builds the DLL again", async () => {
+    const folder = path.join(lane.work, "ax-hklm");
+    const first = await press(c, "build");
+    assert.ok(
+      first.lines.some((l) =>
+        /^\[REGISTER\] type-library registration FAILED\.\s+DllRegisterServer\(\) returned -2147220992$/.test(l),
+      ),
+      J(first.lines),
+    );
+    assert.equal(first.lines.at(-1), "[BUILD] failed");
+    assert.ok(existsSync(dll(folder)));
+    const second = await press(c, "build");
+    assert.deepEqual(second.boxes, [UNREGISTER_BOX]);
+    assert.ok(
+      second.lines.some((l) =>
+        /^\[UNREGISTER\] type-library deregistration FAILED\.\s+DllUnregisterServer\(\) returned -2147220991$/.test(l),
+      ),
+      J(second.lines),
+    );
+    const before = statSync(dll(folder)).mtimeMs;
+    await answer(c, "Delete It And Continue");
+    const lines = await linesAfter(c, second);
+    assert.ok(lines.includes(`[LINKER] SUCCESS created output file '${dll(folder)}'`), J(lines));
+    assert.ok(statSync(dll(folder)).mtimeMs > before, "the DLL was not written again");
+    assert.deepEqual(presentKeys(keys), []);
+  });
+
+  test("Clean shows the same box, and its Delete It And Continue builds a new DLL instead of deleting the old one", async () => {
+    const folder = path.join(lane.work, "ax-hklm");
+    const r = await press(c, "clean");
+    assert.deepEqual(r.boxes, [UNREGISTER_BOX]);
+    const before = statSync(dll(folder)).mtimeMs;
+    await answer(c, "Delete It And Continue");
+    const lines = await linesAfter(c, r);
+    assert.ok(lines.includes("[BUILD] Starting..."), J(lines));
+    assert.ok(lines.includes(`[LINKER] SUCCESS created output file '${dll(folder)}'`), J(lines));
+    assert.ok(!lines.some((l) => l.startsWith("[BUILD-CLEAN]")), J(lines));
+    assert.ok(existsSync(dll(folder)), "the DLL was deleted");
+    assert.ok(statSync(dll(folder)).mtimeMs > before, "the DLL was not written again");
+    assert.deepEqual(presentKeys(keys), []);
+    assert.deepEqual(await pageErrors(c), []);
+  });
+
+  test("a Standard EXE with Register DLLs to HKLM shows the HKLM warning too, twice as it opens", async () => {
+    await lane.closeProject();
+    const src = variant("building-exe", path.join(lane.work, "src-exe-hklm"), {
+      "project.dllRegisterLocalMachine": true,
+    });
+    const answered = await openAnswering(src, path.join(lane.work, "exe-hklm"));
+    assert.deepEqual(answered, [HKLM_BOX, HKLM_BOX]);
+    assert.deepEqual(await boxes(c), []);
+  });
+
+  test("no registration of the fixture's is left in HKCU or HKLM", () => {
+    assert.deepEqual(presentKeys(keys), []);
+  });
+});
diff --git a/test/ide/building-kit.mjs b/test/ide/building-kit.mjs
new file mode 100644
index 000000000..1e250a5d3
--- /dev/null
+++ b/test/ide/building-kit.mjs
@@ -0,0 +1,298 @@
+// What the Building lanes share: building.test.mjs, building-hklm.test.mjs and
+// building-llvm-box.test.mjs. Not a lane itself (lanes.mjs does not name it).
+//
+// Each lane opens a project from test/ide/probes/building-* (or a variant of one,
+// written into the lane's work folder) in a lane's copy of the install, and
+// presses Build, Clean and the boxes' buttons with the mouse. Every build, Clean
+// and save writes into the lane's work folder.
+//
+// guard() replaces, as soon as a project is open:
+//  - the page's hostAppObject, by a proxy that does nothing for SaveIDESetting,
+//    ChangeWindowSetting, AddRecentsList and RemoveRecentsList (which write the
+//    user's registry), records the PUBLISHER value a test saves, answers
+//    GetIDESetting("PUBLISHER") from window.__publisherSetting when a test sets
+//    one, answers every native dialog from window.__answers[name] ("" is Cancel),
+//    and records every Shell call, running only the IDE's own taskkill of its
+//    compiler;
+//  - root.Shell, which the console's Launch EXE and Open Folder links and Open
+//    Build Output Folder call, by a recorder, again for each new root: the IDE
+//    makes root anew at every compiler connection, a restart's included, and a
+//    root left unstubbed would open the real Explorer;
+//  - the page's XMLHttpRequest and fetch, so that no request reaches the
+//    package server (www.everythingaccess.com): one is recorded and sent nowhere.
+//    CDP blocks the server's URLs as well, and records any request the page
+//    makes to it; serverRequests() returns both, and a test fails on any.
+// It also records the page's uncaught errors.
+
+import { spawnSync } from "node:child_process";
+import { cpSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { sleep } from "../../scripts/lib/tb-ide.mjs";
+import { consoleMark, linesSince } from "../../scripts/lib/tb-ide-console.mjs";
+import { answerMessageBox, click, clickAt, messageBoxes, waitFor } from "../../scripts/lib/tb-operate.mjs";
+import { diskText } from "./projects-kit.mjs";
+
+export const J = JSON.stringify;
+const HERE = path.dirname(fileURLToPath(import.meta.url));
+export const PROBES = path.join(HERE, "probes");
+export { diskText, messageBoxes, sleep };
+
+const SERVER = /everythingaccess\.com/i;
+
+/**
+ * The templates' Build Output Path, which the lane's open would otherwise
+ * replace with its own (\out\.exe): the build writes into the
+ * project's folder, which is in the lane's work folder.
+ */
+export const TEMPLATE_BUILD_PATH = "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}";
+
+const GUARD_JS = `(() => {
+  if (!window.__stubRoot) {
+    window.__shell = [];
+    window.__stubRoot = () => {
+      if (typeof root !== "undefined" && root && root.Shell && !root.Shell.__stub) {
+        root.Shell = function (cmdline, muted) { window.__shell.push("root.Shell: " + String(cmdline)); };
+        root.Shell.__stub = true;
+      }
+    };
+    setInterval(window.__stubRoot, 20);
+  }
+  window.__stubRoot();
+  if (window.__guarded) return true;
+  window.__guarded = true;
+  window.__answers = {};
+  window.__hostCalls = [];
+  window.__errors = [];
+  window.__net = [];
+  window.__publisherSaved = null;
+  window.__publisherSetting = undefined;
+  window.addEventListener("error", (e) => window.__errors.push(String(e.message)));
+  window.addEventListener("unhandledrejection", (e) => window.__errors.push("rejection: " + String(e.reason && e.reason.message || e.reason)));
+  const blocked = new Set(["ChangeWindowSetting", "AddRecentsList", "RemoveRecentsList"]);
+  const dialogs = new Set(["ChooseFolder", "OpenFiles", "OpenIconCursor", "OpenImage", "OpenPackage", "OpenProject", "OpenProjectVBP", "OpenTypeLibrary", "SaveProjectAs"]);
+  hostAppObject = new Proxy(hostAppObject, {
+    get(target, key) {
+      if (blocked.has(key)) return async () => undefined;
+      if (key === "SaveIDESetting") return async (name, value) => { if (name === "PUBLISHER") window.__publisherSaved = String(value); };
+      if (key === "GetIDESetting") return (name, ...rest) => name === "PUBLISHER" && window.__publisherSetting !== undefined ? Promise.resolve(window.__publisherSetting) : target.GetIDESetting(name, ...rest);
+      if (dialogs.has(key)) return (...args) => {
+        const a = window.__answers[key] ?? "";
+        window.__hostCalls.push([key, args.map(String), a]);
+        return Promise.resolve(a);
+      };
+      if (key === "Shell") return (cmdline, ...rest) => {
+        window.__hostCalls.push(["Shell", [String(cmdline)], ""]);
+        if (/^taskkill \\/PID \\d+ \\/F$/.test(String(cmdline))) return target.Shell(cmdline, ...rest);
+        window.__shell.push(String(cmdline));
+        return Promise.resolve(0);
+      };
+      return Reflect.get(target, key);
+    },
+  });
+  const server = ${SERVER};
+  const open = XMLHttpRequest.prototype.open, send = XMLHttpRequest.prototype.send;
+  XMLHttpRequest.prototype.open = function (m, u, ...r) { this.__u = String(u); return open.call(this, m, server.test(String(u)) ? "http://127.0.0.1:9/blocked" : u, ...r); };
+  XMLHttpRequest.prototype.send = function (...a) { if (server.test(this.__u || "")) window.__net.push(this.__u.replace(/\\?.*/, "?...")); return send.apply(this, a); };
+  const f = window.fetch;
+  window.fetch = function (u, ...r) { if (server.test(String(u))) { window.__net.push("fetch " + String(u).replace(/\\?.*/, "?...")); return Promise.reject(new Error("blocked")); } return f.call(this, u, ...r); };
+  return true;
+})()`;
+
+/** Install the guard on a connection just opened; again after anything that reloads the page. */
+export async function guard(c) {
+  await c.evaluate(GUARD_JS);
+  if (!c.__buildingGuard) {
+    c.__buildingGuard = true;
+    c.__serverRequests = [];
+    await c.send("Network.enable");
+    await c.send("Network.setBlockedURLs", { urls: ["*everythingaccess*"] });
+    c.on((m) => {
+      if (m.method === "Network.requestWillBeSent" && SERVER.test(m.params.request.url))
+        c.__serverRequests.push(m.params.request.url.replace(/\?.*/, "?..."));
+    });
+  }
+}
+
+/** Every request to the package server seen so far, by CDP or by the page's guard (none, or the test fails). */
+export const serverRequests = async (c) => [
+  ...(c.__serverRequests ?? []),
+  ...(await c.evaluate("window.__net.slice()")),
+];
+export const pageErrors = (c) => c.evaluate("window.__errors.splice(0)");
+export const hostCalls = (c) => c.evaluate("window.__hostCalls.splice(0)");
+export const shells = (c) => c.evaluate("window.__shell.slice()");
+export const setAnswer = (c, name, value) => c.evaluate(`window.__answers[${J(name)}] = ${J(value)}`);
+
+/** The message boxes showing, as "[title] text {buttons}". */
+export const boxes = async (c) =>
+  (await messageBoxes(c)).map(
+    (b) => `[${b.title}] ${String(b.text).replace(/\s+/g, " ").trim()} {${b.buttons.join("/")}}`,
+  );
+
+/** Click a button of the box on top, by its caption, with the mouse. */
+export async function answer(c, caption) {
+  await answerMessageBox(c, caption);
+  await sleep(500);
+}
+
+/** Wait for a box whose text matches `re`; its "[title] text {buttons}", or null. */
+export async function waitBox(c, re, { timeout = 10 * 1000 } = {}) {
+  return waitFor(c, async () => (await boxes(c)).find((b) => re.test(b)) ?? null, { timeout, interval: 200 });
+}
+
+// The centre of the smallest visible element under `scope` whose text, its
+// white space made single, is `text` (or starts with it); null when none is.
+const centreJs = (scope, text, starts) => `(() => {
+  const norm = (e) => e.textContent.replace(/\\s+/g, " ").trim();
+  const area = (e) => e.getBoundingClientRect().width * e.getBoundingClientRect().height;
+  const e = [...document.querySelectorAll(${J(scope)})]
+    .filter((e) => (${starts} ? norm(e).startsWith(${J(text)}) : norm(e) === ${J(text)}) && area(e) > 0)
+    .sort((a, b) => area(a) - area(b))[0];
+  if (!e) return null;
+  const r = e.getBoundingClientRect();
+  return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
+})()`;
+
+/** Click, with the mouse, the smallest visible element whose whole text is `text`. */
+export async function clickText(c, text, { scope = "*", startsWith = false } = {}) {
+  const p = await c.evaluate(centreJs(scope, text, startsWith));
+  if (!p) throw new Error(`nothing on screen reads ${J(text)}`);
+  await clickAt(c, p.x, p.y);
+  await sleep(600);
+}
+
+/** A root menu (File, View, ...) opened with the mouse, then one of its items. */
+export async function menu(c, root, item) {
+  await clickText(c, root, { scope: "[id^=rootMenu]" });
+  await clickText(c, item, { scope: "#contextMenu > *", startsWith: true });
+}
+
+const END_RE =
+  /\[LINKER\] SUCCESS|\[BUILD\] successful|\[LINKER\] FAILED|\[BUILD\] failed|\[BUILD-CLEAN\]|NATIVE EXCEPTION|\[BUILD ERROR\]/;
+
+/**
+ * Press Build or Clean on the toolbar with the mouse, and return the console's
+ * lines since the press once a closing line has been followed by `quiet` ms of
+ * nothing new (or `timeout` has passed), with the boxes showing then.
+ */
+export async function press(c, which, { timeout = 60 * 1000, quiet = 2000 } = {}) {
+  const mark = await consoleMark(c);
+  await waitFor(c, () => c.evaluate("licenceIsSet === true && !(lockUICount > 0)"), { timeout: 60 * 1000 });
+  await click(c, which === "clean" ? "trashIcon" : "buildIcon");
+  const t0 = Date.now();
+  let lines = [],
+    last = -1,
+    changed = Date.now(),
+    ended = false;
+  while (Date.now() - t0 < timeout) {
+    lines = (await linesSince(c, mark)).map((l) => l.trim()).filter(Boolean);
+    if (lines.length !== last) {
+      last = lines.length;
+      changed = Date.now();
+    }
+    ended ||= lines.some((l) => END_RE.test(l));
+    if ((ended || (await boxes(c)).length) && Date.now() - changed > quiet) break;
+    await sleep(150);
+  }
+  return { lines, boxes: await boxes(c), mark };
+}
+
+/**
+ * After a box that a press left was answered: the console's lines since the
+ * press (`r`, press's result) past those it returned, once a closing line has
+ * come among them and nothing new for `quiet` ms.
+ */
+export async function linesAfter(c, r, { timeout = 30 * 1000, quiet = 1500 } = {}) {
+  const t0 = Date.now();
+  let lines = [],
+    last = -1,
+    changed = Date.now();
+  while (Date.now() - t0 < timeout) {
+    lines = (await linesSince(c, r.mark))
+      .map((l) => l.trim())
+      .filter(Boolean)
+      .slice(r.lines.length);
+    if (lines.length !== last) {
+      last = lines.length;
+      changed = Date.now();
+    }
+    if (lines.some((l) => END_RE.test(l)) && Date.now() - changed > quiet) break;
+    await sleep(150);
+  }
+  return lines;
+}
+
+/** Console lines since `mark`, trimmed, once a line matches `re` (or after `timeout`). */
+export async function linesUntil(c, mark, re, { timeout = 30 * 1000, quiet = 1500 } = {}) {
+  await waitFor(c, async () => (await linesSince(c, mark)).some((l) => re.test(l)), { timeout });
+  await sleep(quiet);
+  return (await linesSince(c, mark)).map((l) => l.trim()).filter(Boolean);
+}
+
+/**
+ * A copy of a fixture under `dir`, its Settings changed (undefined deletes a
+ * key): the tree a lane opens.
+ */
+export function variant(fixture, dir, settings = {}) {
+  cpSync(path.join(PROBES, fixture), dir, { recursive: true });
+  const f = path.join(dir, "Settings");
+  const s = JSON.parse(readFileSync(f, "utf8"));
+  for (const [k, v] of Object.entries(settings)) {
+    if (v === undefined) delete s[k];
+    else s[k] = v;
+  }
+  writeFileSync(f, JSON.stringify(s, null, "\t"), "utf8");
+  return dir;
+}
+
+/** The project's Settings as the IDE holds them now, parsed. */
+export const ideSettings = (c, name) =>
+  c.evaluate(
+    `new Promise((res) => { const n = fs.tree.resolvePath("twinbasic:/${name}/Settings"); if (!n) return res(null); fs.readFile(n, (enc, data) => { try { res(JSON.parse(new TextDecoder().decode(new Uint8Array(data)))); } catch (e) { res(null); } }); })`,
+    { awaitPromise: true },
+  );
+
+/** The four parts of a project version from a Settings object. */
+export const version = (s) =>
+  s && ["Major", "Minor", "Build", "Revision"].map((p) => s[`project.version${p}`] ?? 0).join(".");
+
+/** Whether the IDE has the project marked changed, and its unsaved transactions. */
+export const projectState = (c) =>
+  c.evaluate(
+    `({ dirty: typeof projectIsDirty === "undefined" ? null : projectIsDirty, tx: typeof g_SessionFilesystemTransactions === "undefined" ? null : g_SessionFilesystemTransactions.length, path: typeof projectFilePath === "undefined" ? null : projectFilePath ?? null })`,
+  );
+
+export function mkdir(dir) {
+  mkdirSync(dir, { recursive: true });
+  return dir;
+}
+
+// ------------------------------------------------------------------ the registry (read only)
+
+/** The fixed GUIDs of test/ide/probes/building-ax's class. */
+export const AX_IDS = ["A001", "A002", "A003"].map((x) => `{B01D0000-0000-4000-8000-00000000${x}}`);
+
+/**
+ * The keys an ActiveX build of building-ax can write, in HKCU and HKLM, both
+ * views. Its type library id is the project's id, which the lane gives it
+ * (laneProjectId(2, slot)).
+ */
+export function axKeys(projectId) {
+  const rel = [
+    ...AX_IDS.flatMap((g) => [
+      `CLSID\\${g}`,
+      `Interface\\${g}`,
+      `WOW6432Node\\CLSID\\${g}`,
+      `WOW6432Node\\Interface\\${g}`,
+    ]),
+    `TypeLib\\${projectId}`,
+    `WOW6432Node\\TypeLib\\${projectId}`,
+    "BuildProbeAx.MyClass",
+  ];
+  return ["HKCU\\Software\\Classes", "HKLM\\Software\\Classes"].flatMap((root) => rel.map((r) => `${root}\\${r}`));
+}
+
+/** The keys of `keys` that exist (reg query, read only). */
+export const presentKeys = (keys) =>
+  keys.filter((k) => spawnSync("reg", ["query", k], { encoding: "utf8", windowsHide: true }).status === 0);
diff --git a/test/ide/building-llvm-box.test.mjs b/test/ide/building-llvm-box.test.mjs
new file mode 100644
index 000000000..ca3d82581
--- /dev/null
+++ b/test/ide/building-llvm-box.test.mjs
@@ -0,0 +1,110 @@
+// The Compiling... box of an LLVM build, with a second box open while it shows:
+// the defect queued in BUGS-TO-REPORT.md from the trials for the Building pages
+// (their fact sheet's B7), "A box opened during an LLVM build is closed in place
+// of the Compiling... box, which stays and blocks the IDE". A lane of its own,
+// since the defect leaves the IDE blocked: test/ide/probes/building-exe with
+// +llvm in Compiler Options (BUILD). LLVM needs a licence that allows it; the
+// lane's IDE reads the user's.
+//
+// A person cannot open a box during the build, since the Compiling... box covers
+// the IDE, so the test opens one from the page, with the IDE's own
+// showModalNotification, at the build's first progress event.
+//
+// Each test states what BETA 1010 does. If one fails after an IDE update, the
+// IDE has changed: update the matching entry in BUGS-TO-REPORT.md or
+// bugs/filed/, and then this file.
+//
+// Run it with ide-test.bat, which gives it a lane; on its own it is skipped.
+
+import assert from "node:assert/strict";
+import path from "node:path";
+import { before, test } from "node:test";
+import { holdLlvmCache } from "../../scripts/lib/tb-llvm-cache.mjs";
+import { scenario } from "../addin/scenario.mjs";
+import { boxes, guard, J, pageErrors, press, sleep, variant } from "./building-kit.mjs";
+
+// Each build progress event, as [percent, the titles of the boxes showing just
+// before the IDE handles it]; when window.__secondBox is set, the IDE's own
+// showModalNotification opens a box at the first event that is not the end (-1).
+const HOOK_JS = `(() => {
+  if (window.__progressHooked) return;
+  window.__progressHooked = true;
+  window.__progress = [];
+  window.__secondBox = false;
+  const proto = Object.getPrototypeOf(debugSocket);
+  const real = proto.event_setBuildProgress;
+  proto.event_setBuildProgress = function (e) {
+    const p = e.body.percent10 / 10;
+    window.__progress.push([p, [...document.querySelectorAll(".modalDialogContainer")].filter((m) => m.getBoundingClientRect().width).map((m) => (m.querySelector(".modalTitleBar")?.innerText || "").trim())]);
+    const r = real.call(this, e);
+    if (p !== -1 && window.__secondBox) { window.__secondBox = false; showModalNotification("A second box"); }
+    return r;
+  };
+})()`;
+
+scenario("Building: a box during an LLVM build", (lane) => {
+  let c, lease;
+
+  // A lease on the LLVM cache server first, so that the server is never one
+  // this IDE's compiler starts in its own job, which would end with it and take
+  // the cache from every other run on the machine (tb-llvm-cache.mjs).
+  before(async () => {
+    lease = await holdLlvmCache(lane.ide);
+    const src = variant("building-exe", path.join(lane.work, "src-llvm"), {
+      "compiler.buildOptions": "+llvm",
+      "project.versionAutoIncrement": undefined,
+    });
+    c = await lane.open(src, { folder: path.join(lane.work, "llvm") });
+    await guard(c);
+    await c.evaluate(HOOK_JS);
+  });
+
+  const state = () =>
+    c.evaluate(
+      `({ progress: window.__progress.splice(0), lock: lockUICount, modals: [...document.querySelectorAll(".modalDialogContainer")].filter((m) => m.getBoundingClientRect().width).map((m) => (m.querySelector(".modalTitleBar")?.innerText || "").trim()) })`,
+    );
+
+  test("a box opened during an LLVM build is closed at its end in place of the Compiling... box, which stays with no button and blocks the IDE", {
+    timeout: 120 * 1000,
+  }, async () => {
+    await c.evaluate("window.__secondBox = true");
+    const r = await press(c, "build");
+    assert.ok(
+      r.lines.some((l) => l.startsWith("[LINKER] SUCCESS created output file")),
+      J(r.lines),
+    );
+    await sleep(3000);
+    const s = await state();
+    assert.equal(s.progress.at(-1)[0], -1, J(s.progress));
+    assert.deepEqual(s.modals, ["Compiling..."], J(s));
+    assert.deepEqual(await boxes(c), ["[Compiling...] Compiling... 0.0% {}"]);
+    assert.equal(s.lock, 1);
+    // What is left closed from the page, so that the lane closes cleanly.
+    for (let i = 0; i < 3 && (await c.evaluate("modalDialogs.length")) > 0; i++) await c.evaluate("hideModalDialog()");
+    await sleep(500);
+    assert.deepEqual(await c.evaluate("[modalDialogs.length, lockUICount]"), [0, 0]);
+    assert.deepEqual(await pageErrors(c), []);
+  });
+
+  test("control: an LLVM build shows the Compiling... box and closes it at its end", {
+    timeout: 120 * 1000,
+  }, async () => {
+    const r = await press(c, "build");
+    assert.ok(
+      r.lines.some((l) => l.startsWith("[LINKER] SUCCESS created output file")),
+      J(r.lines),
+    );
+    await sleep(1500);
+    const s = await state();
+    assert.ok(
+      s.progress.some(([p, m]) => p === -1 && m.includes("Compiling...")),
+      J(s.progress),
+    );
+    assert.equal(s.progress.at(-1)[0], -1);
+    assert.deepEqual(s.modals, []);
+    assert.equal(s.lock, 0);
+  });
+
+  // Released once the lane's IDE has ended.
+  return () => lease?.release();
+});
diff --git a/test/ide/building.test.mjs b/test/ide/building.test.mjs
new file mode 100644
index 000000000..ffcb629e4
--- /dev/null
+++ b/test/ide/building.test.mjs
@@ -0,0 +1,291 @@
+// Build, the build path, Auto-Increment, the first save from the build's box and
+// the Package Publishing sign-in, with real clicks: the defects queued in
+// BUGS-TO-REPORT.md from the trials for the Building pages (their fact sheet's
+// B15, B14, B11, B1, B4 and B2). Three IDEs, one after the other:
+//  - test/ide/probes/building-exe, a Standard EXE with (VERSION) Auto-Increment
+//    Revision at 1.0.7.3 and a Build Output Path holding ${ProjectID}:
+//    "${ProjectID} in the build path becomes an empty string" and
+//    "Auto-Increment raises the version in memory only, and the increments are
+//    lost";
+//  - test/ide/probes/building-pkg, a package, with its Package Publishing pane:
+//    "A remembered publisher password containing ~ is read back wrong",
+//    "A saved publisher name containing ' is cut short in the sign-in box" and
+//    "OK in the publish failure box raises a page TypeError". Nothing reaches the
+//    package server: the guard (building-kit.mjs) blocks it and fails the test on
+//    any request, a publish's server call is a stub in the page, and the
+//    publisher's name and password are test values set in the page, the IDE's
+//    own put back in the page at the end, never read out of it;
+//  - no project, then New Project (Standard EXE): "Save in the build's 'You must
+//    first save the project' box writes no project file".
+//
+// Each test states what BETA 1010 does. If one fails after an IDE update, the
+// IDE has changed: update the matching entry in BUGS-TO-REPORT.md or
+// bugs/filed/, and then this file.
+//
+// Run it with ide-test.bat, which gives it a lane; on its own it is skipped.
+
+import assert from "node:assert/strict";
+import { existsSync, readdirSync } from "node:fs";
+import path from "node:path";
+import { before, test } from "node:test";
+import { consoleMark, linesSince } from "../../scripts/lib/tb-ide-console.mjs";
+import { pressKey, waitFor } from "../../scripts/lib/tb-operate.mjs";
+import { scenario } from "../addin/scenario.mjs";
+import {
+  answer,
+  boxes,
+  clickText,
+  diskText,
+  guard,
+  hostCalls,
+  ideSettings,
+  J,
+  linesUntil,
+  menu,
+  mkdir,
+  pageErrors,
+  press,
+  projectState,
+  serverRequests,
+  setAnswer,
+  sleep,
+  variant,
+  version,
+  waitBox,
+} from "./building-kit.mjs";
+
+const EXE = "BuildProbeExe";
+const ID_PATH = "${SourcePath}\\Build\\${ProjectID}_${ProjectName}.${FileExtension}";
+
+scenario("Building", (lane) => {
+  let c;
+  const exeDir = path.join(lane.work, "exe");
+  const built = {};
+
+  before(async () => {
+    const src = variant("building-exe", path.join(lane.work, "src-exe"));
+    c = await lane.open(src, { folder: exeDir, buildPath: ID_PATH });
+    await guard(c);
+  });
+
+  // ------------------------------------------------------------- the build path and Auto-Increment
+
+  test("${ProjectID} in Build Output Path becomes an empty string", async () => {
+    const id = (await ideSettings(c, EXE))["project.id"];
+    assert.match(id, /^\{7B247200-/, "the project has no id of the lane's");
+    const r = await press(c, "build");
+    Object.assign(built, r);
+    const out = path.join(exeDir, "Build", `_${EXE}.exe`);
+    assert.ok(r.lines.includes(`[LINKER] SUCCESS created output file '${out}'`), J(r.lines));
+    assert.deepEqual(readdirSync(path.join(exeDir, "Build")), [`_${EXE}.exe`]);
+  });
+
+  test("control: ${SourcePath}, ${ProjectName} and ${FileExtension} in the same Build Output Path are expanded", () => {
+    assert.ok(
+      built.lines.some(
+        (l) => l.startsWith(`[LINKER] SUCCESS created output file '${exeDir}\\Build\\`) && l.endsWith(`${EXE}.exe'`),
+      ),
+      J(built.lines),
+    );
+  });
+
+  test("Auto-Increment raises the version in the IDE only: the project is not marked changed, its file keeps the old number, and Close Project asks nothing", async () => {
+    assert.ok(built.lines.includes("[BUILD] Auto-incremented next build number to: 1.0.7.4"), J(built.lines));
+    const r = await press(c, "build");
+    assert.ok(r.lines.includes("[BUILD] Auto-incremented next build number to: 1.0.7.5"), J(r.lines));
+    assert.equal(version(await ideSettings(c, EXE)), "1.0.7.5");
+    assert.deepEqual(await projectState(c), { dirty: false, tx: 0, path: lane.project });
+    assert.equal(version(JSON.parse(diskText(lane.project, "Settings"))), "1.0.7.3");
+    await menu(c, "File", "Close Project");
+    await sleep(3000);
+    assert.deepEqual(
+      (await boxes(c)).filter((b) => !b.startsWith("[New / Open")),
+      [],
+      "Close Project asked something",
+    );
+    assert.equal(version(JSON.parse(diskText(lane.project, "Settings"))), "1.0.7.3");
+    assert.deepEqual(await pageErrors(c), []);
+  });
+
+  // ------------------------------------------------------------- Package Publishing
+
+  test("a remembered publisher password containing ~ is read back with NUL in place of each ~", async () => {
+    await lane.closeProject();
+    const src = variant("building-pkg", path.join(lane.work, "src-pkg"));
+    c = await lane.open(src, { folder: path.join(lane.work, "pkg") });
+    await guard(c);
+    // The IDE's own publisher settings, kept in the page and put back by the last
+    // Package Publishing test; never read out of it.
+    await c.evaluate(
+      `window.__pubKeep = { n: packagePublisherName, p: packagePublisherPassword, r: packagePublisherRememberPassword, s: packagePublisherSessionID }; packagePublisherSessionID = ""; 0`,
+    );
+    await menu(c, "View", "PACKAGE PUBLISHING");
+    const got = await remembered("pa~ss~word");
+    assert.deepEqual(got.saved, true);
+    assert.deepEqual(
+      got.codes,
+      [..."pa\0ss\0word"].map((ch) => ch.charCodeAt(0)),
+    );
+    const form = await signInForm();
+    assert.equal(form.user, "TestPublisher");
+    // The NUL, written into the form's HTML, is read back as U+FFFD.
+    assert.equal(form.password, "pa\u{FFFD}ss\u{FFFD}word");
+    assert.deepEqual(await serverRequests(c), []);
+  });
+
+  test("control: a remembered publisher password without ~ is read back as it was", async () => {
+    const got = await remembered("Test-Pass_123!");
+    assert.deepEqual(
+      got.codes,
+      [..."Test-Pass_123!"].map((ch) => ch.charCodeAt(0)),
+    );
+    const form = await signInForm();
+    assert.equal(form.password, "Test-Pass_123!");
+    assert.deepEqual(await serverRequests(c), []);
+  });
+
+  test("a saved publisher name containing ' is cut at the apostrophe in the sign-in box", async () => {
+    await c.evaluate(
+      `packagePublisherName = "O'Brien"; packagePublisherPassword = ""; packagePublisherRememberPassword = false; 0`,
+    );
+    const form = await signInForm();
+    assert.equal(form.user, "O");
+    assert.ok(form.attributes.includes("brien'"), J(form.attributes));
+    assert.deepEqual(await serverRequests(c), []);
+  });
+
+  test("control: a saved publisher name with no apostrophe is shown whole in the sign-in box", async () => {
+    await c.evaluate(`packagePublisherName = "TestPublisher"; 0`);
+    const form = await signInForm();
+    assert.equal(form.user, "TestPublisher");
+    assert.deepEqual(await serverRequests(c), []);
+  });
+
+  test("OK in the box of a publish that fails with an HTTPS code raises a page TypeError", async () => {
+    const box = await failedPublish(500);
+    assert.equal(box, "[twinBASIC] [PUBLISH] FAILED. Server returned HTTPS code 500 {OK}");
+    await answer(c, "OK");
+    assert.deepEqual(await boxes(c), []);
+    assert.deepEqual(await pageErrors(c), ["Uncaught TypeError: t is not a function"]);
+  });
+
+  test("control: OK in the box of a publish refused as a version already uploaded raises no error", async () => {
+    const box = await failedPublish(551);
+    assert.equal(box, "[twinBASIC] [PUBLISH] FAILED. Package already uploaded with this exact version number. {OK}");
+    await answer(c, "OK");
+    assert.deepEqual(await boxes(c), []);
+    assert.deepEqual(await pageErrors(c), []);
+    // The IDE's own publisher settings and server call, back as they were.
+    await c.evaluate(
+      `serverHttpsGetUtf8 = window.__realServer; window.__publisherSetting = undefined; packagePublisherName = __pubKeep.n; packagePublisherPassword = __pubKeep.p; packagePublisherRememberPassword = __pubKeep.r; packagePublisherSessionID = __pubKeep.s; delete window.__pubKeep; 0`,
+    );
+    assert.deepEqual(await serverRequests(c), []);
+  });
+
+  // The IDE saves a password with the page's own savePublisherInfo (into the
+  // guard's record, never the registry), and reads it back with its own
+  // loadPublisherInfo, as at its next start.
+  async function remembered(password) {
+    return c.evaluate(
+      `(async () => {
+        packagePublisherName = "TestPublisher"; packagePublisherPassword = ${J(password)}; packagePublisherRememberPassword = true;
+        window.__publisherSaved = null;
+        await new Promise((r) => savePublisherInfo(r));
+        window.__publisherSetting = window.__publisherSaved;
+        packagePublisherName = ""; packagePublisherPassword = ""; packagePublisherRememberPassword = false;
+        loadPublisherInfo();
+        await new Promise((r) => setTimeout(r, 500));
+        return { saved: window.__publisherSaved !== null, codes: [...packagePublisherPassword].map((ch) => ch.charCodeAt(0)) };
+      })()`,
+      { awaitPromise: true },
+    );
+  }
+
+  // SIGN IN in the pane, the sign-in box's LOGIN fields, then Cancel.
+  async function signInForm() {
+    await clickText(c, "SIGN IN");
+    assert.ok(await waitBox(c, /Publisher Login \/ Signup/), "no sign-in box");
+    const form = await c.evaluate(`(() => {
+      const u = document.getElementById("PublisherUserIdLogin"), p = document.getElementById("PublisherPasswordLogin");
+      return { user: u.value, password: p.value, attributes: [...u.attributes].map((a) => a.name) };
+    })()`);
+    await answer(c, "Cancel");
+    assert.deepEqual(await boxes(c), []);
+    return form;
+  }
+
+  // The IDE's packagePublisherPublishNow, as Continue in the publish confirmation
+  // calls it, with the server call replaced in the page by one that fails with
+  // `status`; the box it shows.
+  async function failedPublish(status) {
+    await pageErrors(c);
+    await c.evaluate(`(() => {
+      window.__realServer = window.__realServer || serverHttpsGetUtf8;
+      serverHttpsGetUtf8 = function (url, ok, fail) { setTimeout(() => fail(${status}), 50); };
+      packagePublisherPublishNow(new Uint8Array(4), "BuildProbePkg");
+    })()`);
+    const box = await waitBox(c, /PUBLISH/);
+    assert.deepEqual(await serverRequests(c), []);
+    return box;
+  }
+
+  // ------------------------------------------------------------- a project never saved
+
+  test("Save in the build's 'You must first save the project' box takes the path but writes no project file and builds nothing, and Close Project then asks to save", async () => {
+    await lane.closeProject();
+    lane.includeProjects();
+    c = await lane.openNoProject();
+    await waitFor(
+      c,
+      () =>
+        c.evaluate(
+          `(() => { try { return typeof hostAppObject !== "undefined" && [...document.querySelectorAll(".modalTitleBar")].some((b) => b.innerText.includes("New / Open")); } catch { return false; } })()`,
+        ),
+      { timeout: 90 * 1000, interval: 250 },
+    );
+    await sleep(1500);
+    await guard(c);
+    await clickText(c, "Standard EXE", { scope: ".frontPageListViewItem" });
+    await answer(c, "Open");
+    assert.ok(await waitBox(c, /New Project Options/, { timeout: 60 * 1000 }), "no New Project Options");
+    await answer(c, "Continue");
+    await waitFor(c, () => c.evaluate(`typeof licenceIsSet !== "undefined" && licenceIsSet === true`), {
+      timeout: 60 * 1000,
+    });
+    await sleep(2000);
+    await guard(c);
+    const target = path.join(mkdir(path.join(lane.work, "new")), "MyNewProject.twinproj");
+    await setAnswer(c, "SaveProjectAs", target);
+    const mark = await consoleMark(c);
+    const r = await press(c, "build", { timeout: 5000 });
+    assert.deepEqual(r.boxes, ["[twinBASIC] You must first save the project. Save now? {Save/Cancel}"]);
+    await answer(c, "Save");
+    await sleep(4000);
+    assert.deepEqual(await hostCalls(c), [["SaveProjectAs", ["NewProject"], target]]);
+    assert.deepEqual(await projectState(c), { dirty: true, tx: 1, path: target });
+    assert.equal(existsSync(target), false);
+    assert.deepEqual(
+      (await linesSince(c, mark)).filter((l) => l.trim()),
+      [],
+      "the build or the save printed something",
+    );
+    assert.deepEqual(readdirSync(path.dirname(target)), []);
+    await menu(c, "File", "Close Project");
+    assert.equal(
+      await waitBox(c, /changes/),
+      "[twinBASIC] You've made changes to this project. Save now? {Save/Discard Changes/Cancel}",
+    );
+    await answer(c, "Cancel");
+    assert.equal(existsSync(target), false);
+  });
+
+  test("control: Ctrl+S then writes the project file", async () => {
+    const target = path.join(lane.work, "new", "MyNewProject.twinproj");
+    const mark = await consoleMark(c);
+    await pressKey(c, "s", { ctrl: true });
+    const lines = await linesUntil(c, mark, /\[PROJECT\]/);
+    assert.ok(lines.includes("[PROJECT] twinBASIC project saving to disk [DONE]"), J(lines));
+    assert.ok(existsSync(target));
+    assert.deepEqual(await pageErrors(c), []);
+  });
+});
diff --git a/test/ide/export.test.mjs b/test/ide/export.test.mjs
index f0f7dbb31..c08826ed0 100644
--- a/test/ide/export.test.mjs
+++ b/test/ide/export.test.mjs
@@ -11,7 +11,7 @@
 // Run it with ide-test.bat, which gives it a lane; on its own it is skipped.
 
 import assert from "node:assert/strict";
-import { chmodSync, existsSync, mkdirSync, readdirSync, symlinkSync, writeFileSync } from "node:fs";
+import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
 import path from "node:path";
 import { before, test } from "node:test";
 import { fileURLToPath } from "node:url";
@@ -38,10 +38,12 @@ scenario("Export Project", (lane) => {
   const readOnly = [];
 
   // Export into a folder and wait for the compiler's last [EXPORT] line; the
-  // [EXPORT] lines, without the prefix and trimmed.
-  async function exportTo(folder) {
+  // [EXPORT] lines, without the prefix and trimmed. The second argument of
+  // exportProjectTo() is the project's Export Verbose setting, which the menu
+  // command passes; every test but the control below exports with it on.
+  async function exportTo(folder, verbose = true) {
     const mark = await consoleMark(c);
-    await c.evaluate(`exportProjectTo(${JSON.stringify(folder)}, true), null`);
+    await c.evaluate(`exportProjectTo(${JSON.stringify(folder)}, ${verbose}), null`);
     const lines = await waitFor(
       c,
       async () => {
@@ -73,6 +75,67 @@ scenario("Export Project", (lane) => {
     assert.ok(packages.includes("VBA"), `no Packages\\VBA among ${packages.join(", ")}`);
   });
 
+  // bugs/export-verbose-done-escaped: the compiler sends each DONE line in
+  // three parts, "[EXPORT]  DONE: ", the path and the line break, and the
+  // console's debugOutputPartial() passes the text that continues an open line
+  // through TEXTtoHTML() twice (twinbasic/twinbasic#2444's cause), so each
+  // backslash is stored as &#92; and drawn as \. The DELETED lines,
+  // sent whole, are right; so are the files.
+  test("with Export Verbose, every DONE line shows \ for each backslash of the path, and the files are right", async () => {
+    const out = path.join(root, "verbose");
+    mkdirSync(out);
+    const lines = await exportTo(out);
+    const done = lines.filter((l) => l.startsWith("DONE: "));
+    assert.equal(lines[0], `exporting current project to "\\\\?\\${out}\\"...`);
+    assert.match(lines.at(-1), /^COMPLETED \(\d+ folders, \d+ files\)$/);
+    assert.ok(done.includes("DONE: \Settings"), `no DONE: \Settings among ${JSON.stringify(done.slice(0, 3))}`);
+    assert.ok(done.includes("DONE: \Sources\Startup.twin"));
+    assert.deepEqual(
+      done.filter((l) => l.includes("\\") || !l.includes("\")),
+      [],
+      "a DONE line shows a backslash",
+    );
+    // As the pane draws them: the rows it has drawn, which are the last ones.
+    const drawn = await c.evaluate(
+      `[...debugConsoleOutput.querySelectorAll("span.debugConsoleOutputText")].map((e) => e.textContent).filter((t) => t.includes("DONE: "))`,
+    );
+    assert.ok(drawn.length > 0, "the pane draws no DONE row");
+    assert.deepEqual(
+      drawn.filter((t) => t.includes("\\") || !t.includes("\")),
+      [],
+      "a drawn DONE row shows a backslash",
+    );
+    // Each path, with its backslashes put back, is a file the export wrote.
+    const missing = done.map((l) => l.slice(6).replaceAll("\", "\\")).filter((r) => !existsSync(path.join(out, r)));
+    assert.deepEqual(missing, []);
+    const startup = path.join("Sources", "Startup.twin");
+    assert.equal(readFileSync(path.join(out, startup), "utf8"), readFileSync(path.join(PROJECT, startup), "utf8"));
+  });
+
+  test("control: with Export Verbose, a second export's DELETED lines show the paths' backslashes", async () => {
+    const out = path.join(root, "verbose");
+    const lines = await exportTo(out);
+    const deleted = lines.filter((l) => l.startsWith("DELETED: "));
+    assert.ok(
+      deleted.includes(`DELETED: \\\\?\\${path.join(out, "Sources", "Startup.twin")}`),
+      `no DELETED line for Sources\\Startup.twin among ${JSON.stringify(deleted.slice(0, 3))}`,
+    );
+    assert.deepEqual(
+      deleted.filter((l) => l.includes("\")),
+      [],
+    );
+  });
+
+  test("control: without Export Verbose, the first line and the COMPLETED line show the path's backslashes", async () => {
+    const out = path.join(root, "quiet");
+    mkdirSync(out);
+    const lines = await exportTo(out, false);
+    assert.equal(lines.length, 2, JSON.stringify(lines));
+    assert.equal(lines[0], `exporting current project to "\\\\?\\${out}\\"...`);
+    assert.match(lines[1], /^COMPLETED \(\d+ folders, \d+ files\)$/);
+    assert.ok(existsSync(path.join(out, "Sources", "Startup.twin")));
+  });
+
   test("a junction in the export folder is not followed, and its target is kept", async () => {
     const out = path.join(root, "junction");
     const outside = path.join(root, "outside");
diff --git a/test/ide/lanes.mjs b/test/ide/lanes.mjs
index e203ff191..567b1558b 100644
--- a/test/ide/lanes.mjs
+++ b/test/ide/lanes.mjs
@@ -43,4 +43,7 @@ export default [
   { file: "projects.test.mjs" },
   { file: "projects-loss.test.mjs" },
   { file: "projects-import-crash.test.mjs" },
+  { file: "building.test.mjs" },
+  { file: "building-hklm.test.mjs" },
+  { file: "building-llvm-box.test.mjs" },
 ];
diff --git a/test/ide/probes/building-ax/Settings b/test/ide/probes/building-ax/Settings
new file mode 100644
index 000000000..0c4338ee6
--- /dev/null
+++ b/test/ide/probes/building-ax/Settings
@@ -0,0 +1,30 @@
+{
+	"configuration.inherits": "Defaults",
+	"project.appTitle": "BuildProbeAx",
+	"project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}",
+	"project.buildType": "ActiveX DLL",
+	"project.description": "BuildProbe ActiveX DLL",
+	"project.dllRegisterLocalMachine": false,
+	"project.exportPathIsV2": true,
+	"project.id": "{EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA00}",
+	"project.name": "BuildProbeAx",
+	"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",
+			"versionMajor": 2,
+			"versionMinor": 0
+		}
+	],
+	"project.settingsVersion": 1,
+	"project.useProjectIdForTypeLibraryId": true,
+	"project.warnings": {
+		"errors": [],
+		"ignored": []
+	},
+	"runtime.useUnicodeStandardLibrary": true
+}
diff --git a/test/ide/probes/building-ax/Sources/MyClass.twin b/test/ide/probes/building-ax/Sources/MyClass.twin
new file mode 100644
index 000000000..00f921bf2
--- /dev/null
+++ b/test/ide/probes/building-ax/Sources/MyClass.twin
@@ -0,0 +1,8 @@
+[ClassId("B01D0000-0000-4000-8000-00000000A001")]
+[InterfaceId("B01D0000-0000-4000-8000-00000000A002")]
+[EventInterfaceId("B01D0000-0000-4000-8000-00000000A003")]
+Class MyClass
+    Public Function Hello() As String
+        Return "hello"
+    End Function
+End Class
diff --git a/test/ide/probes/building-exe/Settings b/test/ide/probes/building-exe/Settings
new file mode 100644
index 000000000..2608590bd
--- /dev/null
+++ b/test/ide/probes/building-exe/Settings
@@ -0,0 +1,34 @@
+{
+	"configuration.inherits": "Defaults",
+	"project.appTitle": "BuildProbeExe",
+	"project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}",
+	"project.buildType": "Standard EXE",
+	"project.description": "BuildProbeExe",
+	"project.exportPathIsV2": true,
+	"project.id": "{EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA00}",
+	"project.name": "BuildProbeExe",
+	"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",
+			"versionMajor": 2,
+			"versionMinor": 0
+		}
+	],
+	"project.settingsVersion": 1,
+	"project.startupObject": "Sub Main",
+	"project.versionAutoIncrement": "Revision",
+	"project.versionBuild": 7,
+	"project.versionMajor": 1,
+	"project.versionMinor": 0,
+	"project.versionRevision": 3,
+	"project.warnings": {
+		"errors": [],
+		"ignored": []
+	},
+	"runtime.useUnicodeStandardLibrary": true
+}
diff --git a/test/ide/probes/building-exe/Sources/MainMod.twin b/test/ide/probes/building-exe/Sources/MainMod.twin
new file mode 100644
index 000000000..3ef0917a2
--- /dev/null
+++ b/test/ide/probes/building-exe/Sources/MainMod.twin
@@ -0,0 +1,7 @@
+Module MainMod
+    Public Const MARKER As String = "V1"
+
+    Public Sub Main()
+        Debug.Print MARKER
+    End Sub
+End Module
diff --git a/test/ide/probes/building-pkg/Settings b/test/ide/probes/building-pkg/Settings
new file mode 100644
index 000000000..1a7af7ff4
--- /dev/null
+++ b/test/ide/probes/building-pkg/Settings
@@ -0,0 +1,28 @@
+{
+	"configuration.inherits": "Defaults",
+	"project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}",
+	"project.buildType": "Package TWINPACK",
+	"project.description": "BuildProbe package",
+	"project.id": "{EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA00}",
+	"project.licence": "MIT",
+	"project.name": "BuildProbePkg",
+	"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",
+			"versionMajor": 2,
+			"versionMinor": 0
+		}
+	],
+	"project.versionMajor": 1,
+	"project.versionMinor": 0,
+	"project.warnings": {
+		"errors": [],
+		"ignored": []
+	},
+	"runtime.useUnicodeStandardLibrary": true
+}
diff --git a/test/ide/probes/building-pkg/Sources/HelloWorldModule.twin b/test/ide/probes/building-pkg/Sources/HelloWorldModule.twin
new file mode 100644
index 000000000..bd1284ba3
--- /dev/null
+++ b/test/ide/probes/building-pkg/Sources/HelloWorldModule.twin
@@ -0,0 +1,7 @@
+Module HelloWorldModule
+
+    Public Function Hello() As String
+        Return "Hello world!"
+    End Function
+
+End Module
diff --git a/test/package-tests/DetoursLib/Sources/DelegateTests.twin b/test/package-tests/DetoursLib/Sources/DelegateTests.twin
new file mode 100644
index 000000000..26ce527e1
--- /dev/null
+++ b/test/package-tests/DetoursLib/Sources/DelegateTests.twin
@@ -0,0 +1,18 @@
+' DetoursLib: how a delegate passes a String, which decides how a detour of an ANSI function calls the original.
+[TestFixture]
+Module DelegateTests
+
+    Private Delegate Function StrLenFn (ByVal s As String) As Long
+
+    [TestCase]
+    Public Sub Delegate_StringArgument_IsPassedAsUtf16()
+        Expect.Begin
+        Dim k As LongPtr = Hooks.GetModuleHandle("kernel32")
+        Dim lenW As StrLenFn = CType(Of StrLenFn)(Hooks.GetProcAddress(k, "lstrlenW"))
+        Dim lenA As StrLenFn = CType(Of StrLenFn)(Hooks.GetProcAddress(k, "lstrlenA"))
+        Expect.AreEqual 4, lenW("abcd"), "lstrlenW(""abcd"") called through a delegate with a ByVal String parameter"
+        Expect.AreEqual 1, lenA("abcd"), "lstrlenA(""abcd"") called through such a delegate: it reads UTF-16 text, so it stops after ""a"""
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/DetoursLib/Sources/DllEntryTests.twin b/test/package-tests/DetoursLib/Sources/DllEntryTests.twin
new file mode 100644
index 000000000..c1e53d746
--- /dev/null
+++ b/test/package-tests/DetoursLib/Sources/DllEntryTests.twin
@@ -0,0 +1,101 @@
+' DetoursLib: CreateDetourAtDLLEntry, which finds a function of a loaded DLL by name and detours it.
+[TestFixture]
+Module DllEntryTests
+
+    [TestCase]
+    Public Sub CreateDetourAtDLLEntry_CallsGoToTheDetour()
+        Expect.Begin
+        Hooks.Reset
+        On Error Resume Next
+        Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("kernel32", "MulDiv", AddressOf Hooks.DetourA)
+        Expect.AreEqual 0, Err.Number, "error from CreateDetourAtDLLEntry(""kernel32"", ""MulDiv"", AddressOf DetourA)"
+        Err.Clear
+        Expect.AreEqual -1, Hooks.MulDiv(Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1) through its Declare, detoured to DetourA"
+        Expect.AreEqual 1, Hooks.CallsA, "calls DetourA received"
+        Hooks.Unhook AddressOf Hooks.DetourA
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CreateDetourAtDLLEntry_ReturnsAnAddressThatCallsTheOriginal()
+        Expect.Begin
+        Hooks.Reset
+        On Error Resume Next
+        Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("kernel32", "MulDiv", AddressOf Hooks.DetourA)
+        Expect.AreEqual 0, Err.Number, "error from CreateDetourAtDLLEntry"
+        Err.Clear
+        Expect.IsTrue Hooks.RealMulDiv <> 0, "the address CreateDetourAtDLLEntry returns is not 0"
+        Expect.IsTrue Hooks.RealMulDiv <> Hooks.MulDivAddress(), "the address returned is not the address of MulDiv's code"
+        Expect.AreEqual Hooks.Marked, Hooks.CallAt(Hooks.RealMulDiv, Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1) called through the address returned"
+        Expect.AreEqual 0, Hooks.CallsA, "calls DetourA received from the call through the address returned"
+        Hooks.Unhook AddressOf Hooks.DetourA
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CreateDetourAtDLLEntry_DetourCanPassCallsToTheOriginal()
+        Expect.Begin
+        Hooks.Reset
+        On Error Resume Next
+        Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("kernel32", "MulDiv", AddressOf Hooks.DetourA)
+        Expect.AreEqual 0, Err.Number, "error from CreateDetourAtDLLEntry"
+        Err.Clear
+        Expect.AreEqual 8, Hooks.MulDiv(6, 4, 3), "MulDiv(6, 4, 3), which DetourA passes to the original"
+        Hooks.Unhook AddressOf Hooks.DetourA
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CreateDetourAtDLLEntry_NameWithExtension_IsAccepted()
+        Expect.Begin
+        Hooks.Reset
+        On Error Resume Next
+        Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("kernel32.dll", "MulDiv", AddressOf Hooks.DetourA)
+        Expect.AreEqual 0, Err.Number, "error from CreateDetourAtDLLEntry(""kernel32.dll"", ""MulDiv"", ...)"
+        Err.Clear
+        Expect.AreEqual -1, Hooks.MulDiv(Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1), detoured to DetourA"
+        Hooks.Unhook AddressOf Hooks.DetourA
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CreateDetourAtDLLEntry_DllNotLoaded_RaisesError5()
+        Expect.Begin
+        Hooks.Reset
+        On Error Resume Next
+        Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("tbdocs-no-such-library", "MulDiv", AddressOf Hooks.DetourA)
+        Expect.AreEqual 5, Err.Number, "error from CreateDetourAtDLLEntry for a DLL the process has not loaded"
+        Expect.AreEqual "CreateDetourAtDLLEntry libName: DLL library not found", Err.Description, "its description"
+        Err.Clear
+        Expect.AreEqual 0, Hooks.RealMulDiv, "the value returned"
+        Hooks.Unhook AddressOf Hooks.DetourA
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CreateDetourAtDLLEntry_NoSuchFunction_RaisesError5()
+        Expect.Begin
+        Hooks.Reset
+        On Error Resume Next
+        Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("kernel32", "TbDocsNoSuchFunction", AddressOf Hooks.DetourA)
+        Expect.AreEqual 5, Err.Number, "error from CreateDetourAtDLLEntry for a name kernel32 does not export"
+        Expect.AreEqual "CreateDetourAtDLLEntry funcName: function name not found in DLL library", Err.Description, "its description"
+        Err.Clear
+        Hooks.Unhook AddressOf Hooks.DetourA
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CreateDetourAtDLLEntry_NoDetour_RaisesError5()
+        Expect.Begin
+        Hooks.Reset
+        On Error Resume Next
+        Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("kernel32", "MulDiv", 0)
+        Expect.AreEqual 5, Err.Number, "error from CreateDetourAtDLLEntry with newAddress 0"
+        Expect.AreEqual "CreateDetourAtDLLEntry newAddress: bad function address", Err.Description, "its description"
+        Err.Clear
+        Expect.AreEqual 8, Hooks.MulDiv(6, 4, 3), "MulDiv(6, 4, 3) afterwards"
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/DetoursLib/Sources/FuncAddressTests.twin b/test/package-tests/DetoursLib/Sources/FuncAddressTests.twin
new file mode 100644
index 000000000..4cfa04891
--- /dev/null
+++ b/test/package-tests/DetoursLib/Sources/FuncAddressTests.twin
@@ -0,0 +1,46 @@
+' DetoursLib: CreateDetourAtFuncAddress, which detours the function at an address.
+[TestFixture]
+Module FuncAddressTests
+
+    [TestCase]
+    Public Sub CreateDetourAtFuncAddress_CallsGoToTheDetour()
+        Expect.Begin
+        Hooks.Reset
+        On Error Resume Next
+        Hooks.RealMulDiv = DetoursLib.CreateDetourAtFuncAddress(Hooks.MulDivAddress(), AddressOf Hooks.DetourA)
+        Expect.AreEqual 0, Err.Number, "error from CreateDetourAtFuncAddress(
, AddressOf DetourA)" + Err.Clear + Expect.AreEqual -1, Hooks.MulDiv(Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1), detoured to DetourA" + Expect.AreEqual 8, Hooks.MulDiv(6, 4, 3), "MulDiv(6, 4, 3), which DetourA passes to the original" + Hooks.Unhook AddressOf Hooks.DetourA + Expect.Done + End Sub + + [TestCase] + Public Sub CreateDetourAtFuncAddress_ReturnsAnAddressThatCallsTheOriginal() + Expect.Begin + Hooks.Reset + On Error Resume Next + Hooks.RealMulDiv = DetoursLib.CreateDetourAtFuncAddress(Hooks.MulDivAddress(), AddressOf Hooks.DetourA) + Expect.AreEqual 0, Err.Number, "error from CreateDetourAtFuncAddress" + Err.Clear + Expect.IsTrue Hooks.RealMulDiv <> Hooks.MulDivAddress(), "the address returned is not the address passed" + Expect.AreEqual Hooks.Marked, Hooks.CallAt(Hooks.RealMulDiv, Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1) called through the address returned" + Hooks.Unhook AddressOf Hooks.DetourA + Expect.Done + End Sub + + [TestCase] + Public Sub CreateDetourAtFuncAddress_ZeroAddress_RaisesError5() + Expect.Begin + Hooks.Reset + On Error Resume Next + Hooks.RealMulDiv = DetoursLib.CreateDetourAtFuncAddress(0, AddressOf Hooks.DetourA) + Expect.AreEqual 5, Err.Number, "error from CreateDetourAtFuncAddress with procAddress 0" + Expect.AreEqual "CreateDetourAtFuncAddress origAddress: bad function address", Err.Description, "its description" + Err.Clear + Hooks.Unhook AddressOf Hooks.DetourA + Expect.Done + End Sub + +End Module diff --git a/test/package-tests/DetoursLib/Sources/Hooks.twin b/test/package-tests/DetoursLib/Sources/Hooks.twin new file mode 100644 index 000000000..807e43c98 --- /dev/null +++ b/test/package-tests/DetoursLib/Sources/Hooks.twin @@ -0,0 +1,71 @@ +' DetoursLib: what the cases share. The function they detour is MulDiv of kernel32, which the +' process can call from any thread, so each detour gives its own answer only for one marked +' argument, 4242, and passes every other call to the original through the trampoline. +Module Hooks + + Public Const Marked As Long = 4242 + + Public Declare PtrSafe Function MulDiv Lib "kernel32" (ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long + Public DeclareWide PtrSafe Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleW" (ByVal lpModuleName As String) As LongPtr + Public Declare PtrSafe Function GetProcAddress Lib "kernel32" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr + Public Declare PtrSafe Function GetLastError Lib "kernel32" () As Long + Public Declare PtrSafe Sub SetLastError Lib "kernel32" (ByVal dwErrCode As Long) + + Public Delegate Function MulDivFn (ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long + + ' The trampoline of the detour in place, or 0. + Public RealMulDiv As LongPtr + ' How many times each detour ran. + Public CallsA As Long + Public CallsB As Long + + ' The address of MulDiv's code, as GetProcAddress gives it. + Public Function MulDivAddress() As LongPtr + Return GetProcAddress(GetModuleHandle("kernel32"), "MulDiv") + End Function + + ' Calls the function at a trampoline or a code address as MulDiv. With no address (a call + ' from another thread before the case has stored the trampoline) it computes the answer. + Public Function CallAt(ByVal address As LongPtr, ByVal a As Long, ByVal b As Long, ByVal c As Long) As Long + If address = 0 Then + If c = 0 Then Return -1 + Return CLng(CDbl(a) * b / c) + End If + Dim fn As MulDivFn = CType(Of MulDivFn)(address) + Return fn(a, b, c) + End Function + + ' Detour A: gives -1 for the marked argument, else calls the original. + Public Function DetourA(ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long + If nNumber = Marked Then + CallsA = CallsA + 1 + Return -1 + End If + Return CallAt(RealMulDiv, nNumber, nNumerator, nDenominator) + End Function + + ' Detour B: gives -2 for the marked argument, else calls the original. + Public Function DetourB(ByVal nNumber As Long, ByVal nNumerator As Long, ByVal nDenominator As Long) As Long + If nNumber = Marked Then + CallsB = CallsB + 1 + Return -2 + End If + Return CallAt(RealMulDiv, nNumber, nNumerator, nDenominator) + End Function + + ' Removes the detour a case left in place after a failed check, so the next case starts clean. + Public Sub Unhook(ByVal detour As LongPtr) + If RealMulDiv <> 0 Then + On Error Resume Next + DetoursLib.RemoveDetour RealMulDiv, detour + RealMulDiv = 0 + End If + End Sub + + Public Sub Reset() + RealMulDiv = 0 + CallsA = 0 + CallsB = 0 + End Sub + +End Module diff --git a/test/package-tests/DetoursLib/Sources/InlineHookTests.twin b/test/package-tests/DetoursLib/Sources/InlineHookTests.twin new file mode 100644 index 000000000..b4841456e --- /dev/null +++ b/test/package-tests/DetoursLib/Sources/InlineHookTests.twin @@ -0,0 +1,66 @@ +' DetoursLib: SlimDetoursInlineHook of the module DetoursMod, the SlimDetours function the others call. +[TestFixture] +Module InlineHookTests + + [TestCase] + Public Sub SlimDetoursInlineHook_Enable_SucceedsAndStoresTheTrampoline() + Expect.Begin + Hooks.Reset + Dim p As LongPtr = Hooks.MulDivAddress() + Dim hr As Long = DetoursMod.SlimDetoursInlineHook(1, p, AddressOf Hooks.DetourA) + Expect.AreEqual &H10000000, hr, "SlimDetoursInlineHook(1, p, AddressOf DetourA) with p the address of MulDiv" + If hr >= 0 Then + Hooks.RealMulDiv = p + Expect.IsTrue p <> Hooks.MulDivAddress(), "p afterwards is not the address of MulDiv" + Expect.AreEqual -1, Hooks.MulDiv(Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1), detoured to DetourA" + Expect.AreEqual Hooks.Marked, Hooks.CallAt(p, Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1) called through p" + hr = DetoursMod.SlimDetoursInlineHook(0, p, AddressOf Hooks.DetourA) + If hr >= 0 Then Hooks.RealMulDiv = 0 + End If + Expect.Done + End Sub + + [TestCase] + Public Sub SlimDetoursInlineHook_Disable_StoresTheFunctionAgain() + Expect.Begin + Hooks.Reset + Dim p As LongPtr = Hooks.MulDivAddress() + Dim hr As Long = DetoursMod.SlimDetoursInlineHook(1, p, AddressOf Hooks.DetourA) + If hr < 0 Then + Expect.Fail "SlimDetoursInlineHook(1, p, AddressOf DetourA) failed: " & Hex(hr) + Else + Hooks.RealMulDiv = p + Dim trampoline As LongPtr = p + hr = DetoursMod.SlimDetoursInlineHook(0, p, AddressOf Hooks.DetourA) + Expect.AreEqual &H10000000, hr, "SlimDetoursInlineHook(0, p, AddressOf DetourA)" + If hr >= 0 Then + Hooks.RealMulDiv = 0 + Expect.IsTrue p <> trampoline, "p after the call is no longer the trampoline" + Expect.AreEqual 8, Hooks.CallAt(p, 6, 4, 3), "MulDiv(6, 4, 3) called through p" + Expect.AreEqual Hooks.Marked, Hooks.MulDiv(Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1) after the detour is removed" + End If + End If + Expect.Done + End Sub + + [TestCase] + Public Sub SlimDetoursInlineHook_FailureIsANegativeHResult() + Expect.Begin + Hooks.Reset + Dim p As LongPtr = Hooks.MulDivAddress() + Dim hr As Long = DetoursMod.SlimDetoursInlineHook(1, p, AddressOf Hooks.DetourA) + If hr < 0 Then + Expect.Fail "SlimDetoursInlineHook(1, p, AddressOf DetourA) failed: " & Hex(hr) + Else + Hooks.RealMulDiv = p + Dim q As LongPtr = p + Dim hr2 As Long = DetoursMod.SlimDetoursInlineHook(0, q, AddressOf Hooks.DetourB) + Expect.AreEqual CLng(&HD0000173), hr2, "SlimDetoursInlineHook(0, ...) naming DetourB while DetourA is in place" + Expect.AreEqual p, q, "the variable after the failed call" + hr = DetoursMod.SlimDetoursInlineHook(0, p, AddressOf Hooks.DetourA) + If hr >= 0 Then Hooks.RealMulDiv = 0 + End If + Expect.Done + End Sub + +End Module diff --git a/test/package-tests/DetoursLib/Sources/RemoveTests.twin b/test/package-tests/DetoursLib/Sources/RemoveTests.twin new file mode 100644 index 000000000..fd586659b --- /dev/null +++ b/test/package-tests/DetoursLib/Sources/RemoveTests.twin @@ -0,0 +1,85 @@ +' DetoursLib: RemoveDetour, which takes a detour away and puts the function back. +[TestFixture] +Module RemoveTests + + [TestCase] + Public Sub RemoveDetour_RestoresTheFunction() + Expect.Begin + Hooks.Reset + On Error Resume Next + Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("kernel32", "MulDiv", AddressOf Hooks.DetourA) + Expect.AreEqual 0, Err.Number, "error from CreateDetourAtDLLEntry" + Err.Clear + DetoursLib.RemoveDetour Hooks.RealMulDiv, AddressOf Hooks.DetourA + Expect.AreEqual 0, Err.Number, "error from RemoveDetour(
, AddressOf DetourA)" + If Err.Number = 0 Then Hooks.RealMulDiv = 0 + Err.Clear + Expect.AreEqual Hooks.Marked, Hooks.MulDiv(Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1) after RemoveDetour" + Expect.AreEqual 0, Hooks.CallsA, "calls DetourA received" + Hooks.Unhook AddressOf Hooks.DetourA + Expect.Done + End Sub + + [TestCase] + Public Sub RemoveDetour_ThenCreateAgain_DetoursAgain() + Expect.Begin + Hooks.Reset + On Error Resume Next + Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("kernel32", "MulDiv", AddressOf Hooks.DetourA) + DetoursLib.RemoveDetour Hooks.RealMulDiv, AddressOf Hooks.DetourA + Hooks.RealMulDiv = 0 + Expect.AreEqual 0, Err.Number, "error from the first CreateDetourAtDLLEntry and RemoveDetour" + Err.Clear + Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("kernel32", "MulDiv", AddressOf Hooks.DetourB) + Expect.AreEqual 0, Err.Number, "error from CreateDetourAtDLLEntry with DetourB after DetourA was removed" + Err.Clear + Expect.AreEqual -2, Hooks.MulDiv(Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1), detoured to DetourB" + Hooks.Unhook AddressOf Hooks.DetourB + Expect.Done + End Sub + + [TestCase] + Public Sub RemoveDetour_ZeroPatchedAddress_RaisesError5() + Expect.Begin + On Error Resume Next + DetoursLib.RemoveDetour 0, AddressOf Hooks.DetourA + Expect.AreEqual 5, Err.Number, "error from RemoveDetour with patchedAddress 0" + Expect.AreEqual "RemoveDetour patchedAddress: bad pointer", Err.Description, "its description" + Expect.Done + End Sub + + [TestCase] + Public Sub RemoveDetour_ZeroNewAddress_RaisesError5() + Expect.Begin + Hooks.Reset + On Error Resume Next + Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("kernel32", "MulDiv", AddressOf Hooks.DetourA) + Err.Clear + DetoursLib.RemoveDetour Hooks.RealMulDiv, 0 + Expect.AreEqual 5, Err.Number, "error from RemoveDetour with newAddress 0" + Expect.AreEqual "RemoveDetour newAddress: bad pointer", Err.Description, "its description" + Err.Clear + Expect.AreEqual -1, Hooks.MulDiv(Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1) after the refused RemoveDetour: still detoured" + Hooks.Unhook AddressOf Hooks.DetourA + Expect.Done + End Sub + + [TestCase] + Public Sub RemoveDetour_AnotherDetour_RaisesAndLeavesTheDetourInPlace() + Expect.Begin + Hooks.Reset + On Error Resume Next + Hooks.RealMulDiv = DetoursLib.CreateDetourAtDLLEntry("kernel32", "MulDiv", AddressOf Hooks.DetourA) + Err.Clear + DetoursLib.RemoveDetour Hooks.RealMulDiv, AddressOf Hooks.DetourB + Expect.IsTrue Err.Number <> 0, "RemoveDetour naming DetourB, while DetourA is the detour in place, raises an error" + Expect.AreEqual "RemoveDetour SlimDetoursInlineHook failed: D0000173", Err.Description, "its description" + Expect.AreEqual CLng(&HD0000173), Err.Number, "its number" + Err.Clear + Expect.AreEqual -1, Hooks.MulDiv(Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1) after the failed RemoveDetour: still detoured to DetourA" + Hooks.Unhook AddressOf Hooks.DetourA + Expect.AreEqual Hooks.Marked, Hooks.MulDiv(Hooks.Marked, 1, 1), "MulDiv(4242, 1, 1) after RemoveDetour naming DetourA" + Expect.Done + End Sub + +End Module diff --git a/test/package-tests/DetoursLib/Sources/RunAll.twin b/test/package-tests/DetoursLib/Sources/RunAll.twin new file mode 100644 index 000000000..9079f5053 --- /dev/null +++ b/test/package-tests/DetoursLib/Sources/RunAll.twin @@ -0,0 +1,90 @@ +' Generated by scripts/package_tests.mjs from this project's [TestCase] Subs and expect.json: +' do not edit. Run Sub Main: each case prints PASS, FAIL or SKIP in the DEBUG CONSOLE. +' The cases are written for the package DetoursLib, version 1.0.0.0. Each one checks what is +' correct, so a case that fails names a defect of the package. +Module RunAll + + Public Sub Main() + Expect.StartRun "DetoursLib 1.0.0.0: 19 test cases" + On Error Resume Next + Expect.StartCase + Err.Clear + DelegateTests.Delegate_StringArgument_IsPassedAsUtf16 + Expect.EndCase "DelegateTests.Delegate_StringArgument_IsPassedAsUtf16", Err.Number, Err.Description + Expect.StartCase + Err.Clear + DllEntryTests.CreateDetourAtDLLEntry_CallsGoToTheDetour + Expect.EndCase "DllEntryTests.CreateDetourAtDLLEntry_CallsGoToTheDetour", Err.Number, Err.Description + Expect.StartCase + Err.Clear + DllEntryTests.CreateDetourAtDLLEntry_ReturnsAnAddressThatCallsTheOriginal + Expect.EndCase "DllEntryTests.CreateDetourAtDLLEntry_ReturnsAnAddressThatCallsTheOriginal", Err.Number, Err.Description + Expect.StartCase + Err.Clear + DllEntryTests.CreateDetourAtDLLEntry_DetourCanPassCallsToTheOriginal + Expect.EndCase "DllEntryTests.CreateDetourAtDLLEntry_DetourCanPassCallsToTheOriginal", Err.Number, Err.Description + Expect.StartCase + Err.Clear + DllEntryTests.CreateDetourAtDLLEntry_NameWithExtension_IsAccepted + Expect.EndCase "DllEntryTests.CreateDetourAtDLLEntry_NameWithExtension_IsAccepted", Err.Number, Err.Description + Expect.StartCase + Err.Clear + DllEntryTests.CreateDetourAtDLLEntry_DllNotLoaded_RaisesError5 + Expect.EndCase "DllEntryTests.CreateDetourAtDLLEntry_DllNotLoaded_RaisesError5", Err.Number, Err.Description + Expect.StartCase + Err.Clear + DllEntryTests.CreateDetourAtDLLEntry_NoSuchFunction_RaisesError5 + Expect.EndCase "DllEntryTests.CreateDetourAtDLLEntry_NoSuchFunction_RaisesError5", Err.Number, Err.Description + Expect.StartCase + Err.Clear + DllEntryTests.CreateDetourAtDLLEntry_NoDetour_RaisesError5 + Expect.EndCase "DllEntryTests.CreateDetourAtDLLEntry_NoDetour_RaisesError5", Err.Number, Err.Description + Expect.StartCase + Err.Clear + FuncAddressTests.CreateDetourAtFuncAddress_CallsGoToTheDetour + Expect.EndCase "FuncAddressTests.CreateDetourAtFuncAddress_CallsGoToTheDetour", Err.Number, Err.Description + Expect.StartCase + Err.Clear + FuncAddressTests.CreateDetourAtFuncAddress_ReturnsAnAddressThatCallsTheOriginal + Expect.EndCase "FuncAddressTests.CreateDetourAtFuncAddress_ReturnsAnAddressThatCallsTheOriginal", Err.Number, Err.Description + Expect.StartCase + Err.Clear + FuncAddressTests.CreateDetourAtFuncAddress_ZeroAddress_RaisesError5 + Expect.EndCase "FuncAddressTests.CreateDetourAtFuncAddress_ZeroAddress_RaisesError5", Err.Number, Err.Description + Expect.StartCase + Err.Clear + InlineHookTests.SlimDetoursInlineHook_Enable_SucceedsAndStoresTheTrampoline + Expect.EndCase "InlineHookTests.SlimDetoursInlineHook_Enable_SucceedsAndStoresTheTrampoline", Err.Number, Err.Description + Expect.StartCase + Err.Clear + InlineHookTests.SlimDetoursInlineHook_Disable_StoresTheFunctionAgain + Expect.EndCase "InlineHookTests.SlimDetoursInlineHook_Disable_StoresTheFunctionAgain", Err.Number, Err.Description + Expect.StartCase + Err.Clear + InlineHookTests.SlimDetoursInlineHook_FailureIsANegativeHResult + Expect.EndCase "InlineHookTests.SlimDetoursInlineHook_FailureIsANegativeHResult", Err.Number, Err.Description + Expect.StartCase + Err.Clear + RemoveTests.RemoveDetour_RestoresTheFunction + Expect.EndCase "RemoveTests.RemoveDetour_RestoresTheFunction", Err.Number, Err.Description + Expect.StartCase + Err.Clear + RemoveTests.RemoveDetour_ThenCreateAgain_DetoursAgain + Expect.EndCase "RemoveTests.RemoveDetour_ThenCreateAgain_DetoursAgain", Err.Number, Err.Description + Expect.StartCase + Err.Clear + RemoveTests.RemoveDetour_ZeroPatchedAddress_RaisesError5 + Expect.EndCase "RemoveTests.RemoveDetour_ZeroPatchedAddress_RaisesError5", Err.Number, Err.Description + Expect.StartCase + Err.Clear + RemoveTests.RemoveDetour_ZeroNewAddress_RaisesError5 + Expect.EndCase "RemoveTests.RemoveDetour_ZeroNewAddress_RaisesError5", Err.Number, Err.Description + Expect.StartCase + Err.Clear + RemoveTests.RemoveDetour_AnotherDetour_RaisesAndLeavesTheDetourInPlace + Expect.EndCase "RemoveTests.RemoveDetour_AnotherDetour_RaisesAndLeavesTheDetourInPlace", Err.Number, Err.Description + On Error GoTo 0 + Expect.EndRun + End Sub + +End Module diff --git a/test/package-tests/DetoursLib/expect.json b/test/package-tests/DetoursLib/expect.json new file mode 100644 index 000000000..377426648 --- /dev/null +++ b/test/package-tests/DetoursLib/expect.json @@ -0,0 +1,4 @@ +{ + "version": "1.0.0.0", + "cases": {} +} diff --git a/test/render.test.mjs b/test/render.test.mjs index 4cb976188..35f34c639 100644 --- a/test/render.test.mjs +++ b/test/render.test.mjs @@ -25,7 +25,7 @@ import { stripXmlPrologue } from "../builder/dot.mjs"; import { inlineDiagram } from "../builder/inline-diagram.mjs"; import { isWebsiteOnlyLink, rewriteHtml, websiteOf } from "../builder/offline-rewrite.mjs"; import { assignPackageVersions, calloutLabelsOf, parseCalloutLabel } from "../lib/callout-labels.mjs"; -import { applyPreRenderRewrites, createMarkdownIt } from "../builder/render.mjs"; +import { applyPreRenderRewrites, createMarkdownIt, initHighlighter } from "../builder/render.mjs"; import { lightOnly, pairSizes, @@ -85,6 +85,62 @@ describe("kramdownEllipsisPlugin", () => { // The offline tree's URL rewrite (builder/offline-rewrite.mjs): a link to a // file the offline tree holds becomes page-relative; one to a file only the // website holds becomes the website's absolute URL; one to neither is a miss. +describe("stripHtmlCommentsPlugin", async () => { + const withFences = createMarkdownIt({ + highlighter: await initHighlighter(), + linkTables: { byPath: new Map() }, + baseurl: "", + staticFiles: new Set(), + }); + // The page as the build renders it: the pre-render rewrites first, which join a + // comment line to the paragraph above it. + const render = (src) => withFences.render(applyPreRenderRewrites(src, withFences), { page: { srcRel: "A.md" } }); + + const cases = [ + ["a comment block between paragraphs", "before\n\n\n\nafter\n", "

before

\n

after

\n"], + ["a multi-line comment block", "before\n\n\n\nafter\n", "

before

\n

after

\n"], + ["a comment line under a paragraph", "text\n\n\nnext\n", "

text

\n

next

\n"], + ["a comment inside a paragraph", "a b\n", "

a b

\n"], + ["a comment at the end of a paragraph", "a \n", "

a

\n"], + ["a comment in a heading", "## Head \n", '

Head

\n'], + [ + "a comment inside a raw HTML block", + "
\n\n

x

\n
\n", + "
\n

x

\n
\n", + ], + ["a comment in a list item", "- one\n \n- two\n", "
    \n
  • one
  • \n
  • two
  • \n
\n"], + [ + "a comment before a callout", + "\n> [!NOTE] BETA 1010\n> body\n", + (html) => !html.includes("\n> body\n", + (html) => !html.includes("\n\n", + "\n", + ], + ["an unterminated comment is not one", "\n```\n\n`` and done\n"); + assert.equal(html.match(/<!-- keep -->/g).length, 2); + assert.ok(!html.includes("go"), html); + }); +}); + describe("rewriteHtml with a website behind the offline tree", () => { const offline = new Set(["/a/Page.html", "/a/b/Other.html", "/a/b/inner.txt"]); const online = new Set([...offline, "/a/downloads/File.zip", "/a/Space Name.zip"]);