From 23d4cb5df7c575485d8ee58034feba33a2b70768 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 13:31:59 +0200 Subject: [PATCH 01/17] DetoursLib 1.0.0.0: a Contributed section (Detours and DetoursMod, seven pages), its test project (19 cases, win32 and win64), sample template, snapshot and book chapter --- WIP.md | 2 +- builder/page-baseline.json | 2 +- docs/Reference/Contributed.md | 1 + .../Detours/CreateDetourAtDLLEntry.md | 62 +++++++++++ .../Detours/CreateDetourAtFuncAddress.md | 56 ++++++++++ .../DetoursLib/Detours/RemoveDetour.md | 56 ++++++++++ .../Contributed/DetoursLib/Detours/index.md | 74 +++++++++++++ .../DetoursMod/SlimDetoursInlineHook.md | 63 +++++++++++ .../DetoursLib/DetoursMod/index.md | 24 +++++ .../Reference/Contributed/DetoursLib/index.md | 94 ++++++++++++++++ docs/_book.yml | 7 ++ .../snapshots/contributed/DetoursLib/api.json | 61 +++++++++++ .../contributed-DetoursLib/Settings | 74 +++++++++++++ .../DetoursLib/Sources/DelegateTests.twin | 18 ++++ .../DetoursLib/Sources/DllEntryTests.twin | 101 ++++++++++++++++++ .../DetoursLib/Sources/FuncAddressTests.twin | 46 ++++++++ .../DetoursLib/Sources/Hooks.twin | 71 ++++++++++++ .../DetoursLib/Sources/InlineHookTests.twin | 66 ++++++++++++ .../DetoursLib/Sources/RemoveTests.twin | 85 +++++++++++++++ .../DetoursLib/Sources/RunAll.twin | 90 ++++++++++++++++ test/package-tests/DetoursLib/expect.json | 4 + 21 files changed, 1055 insertions(+), 2 deletions(-) create mode 100644 docs/Reference/Contributed/DetoursLib/Detours/CreateDetourAtDLLEntry.md create mode 100644 docs/Reference/Contributed/DetoursLib/Detours/CreateDetourAtFuncAddress.md create mode 100644 docs/Reference/Contributed/DetoursLib/Detours/RemoveDetour.md create mode 100644 docs/Reference/Contributed/DetoursLib/Detours/index.md create mode 100644 docs/Reference/Contributed/DetoursLib/DetoursMod/SlimDetoursInlineHook.md create mode 100644 docs/Reference/Contributed/DetoursLib/DetoursMod/index.md create mode 100644 docs/Reference/Contributed/DetoursLib/index.md create mode 100644 indexer/snapshots/contributed/DetoursLib/api.json create mode 100644 test/example-projects/contributed-DetoursLib/Settings create mode 100644 test/package-tests/DetoursLib/Sources/DelegateTests.twin create mode 100644 test/package-tests/DetoursLib/Sources/DllEntryTests.twin create mode 100644 test/package-tests/DetoursLib/Sources/FuncAddressTests.twin create mode 100644 test/package-tests/DetoursLib/Sources/Hooks.twin create mode 100644 test/package-tests/DetoursLib/Sources/InlineHookTests.twin create mode 100644 test/package-tests/DetoursLib/Sources/RemoveTests.twin create mode 100644 test/package-tests/DetoursLib/Sources/RunAll.twin create mode 100644 test/package-tests/DetoursLib/expect.json diff --git a/WIP.md b/WIP.md index 01f016b9..91fdba78 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. diff --git a/builder/page-baseline.json b/builder/page-baseline.json index 8f4838df..d75cc368 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 1475, + "pages": 1482, "staticFiles": 1063 } diff --git a/docs/Reference/Contributed.md b/docs/Reference/Contributed.md index fda89630..9df3f400 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 00000000..dc67e0ed --- /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 00000000..263bf45d --- /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 00000000..a538dac9 --- /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 00000000..3bb5d761 --- /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 00000000..bf933c60 --- /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 00000000..325bd5a9 --- /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 00000000..6eeee8a4 --- /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/_book.yml b/docs/_book.yml index fe007d44..a054b0ac 100644 --- a/docs/_book.yml +++ b/docs/_book.yml @@ -509,6 +509,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/ diff --git a/indexer/snapshots/contributed/DetoursLib/api.json b/indexer/snapshots/contributed/DetoursLib/api.json new file mode 100644 index 00000000..c88155b6 --- /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/test/example-projects/contributed-DetoursLib/Settings b/test/example-projects/contributed-DetoursLib/Settings new file mode 100644 index 00000000..735c250a --- /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/package-tests/DetoursLib/Sources/DelegateTests.twin b/test/package-tests/DetoursLib/Sources/DelegateTests.twin new file mode 100644 index 00000000..26ce527e --- /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 00000000..c1e53d74 --- /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 00000000..4cfa0489 --- /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 00000000..807e43c9 --- /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 00000000..b4841456 --- /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 00000000..fd586659 --- /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 00000000..9079f505 --- /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 00000000..37742664 --- /dev/null +++ b/test/package-tests/DetoursLib/expect.json @@ -0,0 +1,4 @@ +{ + "version": "1.0.0.0", + "cases": {} +} From 0ede27717532f4c3a08b72baaf608ffe45bbf707 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 14:00:19 +0200 Subject: [PATCH 02/17] Export Verbose: each DONE line shows \ for every backslash (BETA 1010 and 1007; #2444's cause); queued with the export lane's test and two controls, and a NOTE on Saving-and-Exporting --- BUGS-TO-REPORT.md | 30 +++++++++ bugs/export-verbose-done-escaped/repro.json | 8 +++ docs/IDE/Projects/Saving-and-Exporting.md | 3 + test/ide/export.test.mjs | 71 +++++++++++++++++++-- 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 bugs/export-verbose-done-escaped/repro.json diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 6e755a48..86e2aef3 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -5286,3 +5286,33 @@ 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 `&#92;` 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. + + + diff --git a/bugs/export-verbose-done-escaped/repro.json b/bugs/export-verbose-done-escaped/repro.json new file mode 100644 index 00000000..4653fd96 --- /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/docs/IDE/Projects/Saving-and-Exporting.md b/docs/IDE/Projects/Saving-and-Exporting.md index 63dce343..12ebf1e9 100644 --- a/docs/IDE/Projects/Saving-and-Exporting.md +++ b/docs/IDE/Projects/Saving-and-Exporting.md @@ -157,6 +157,9 @@ 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 `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/test/ide/export.test.mjs b/test/ide/export.test.mjs index f0f7dbb3..c08826ed 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"); From 52efccf37f619ea72b9dfaa458724603d4259220 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 14:25:02 +0200 Subject: [PATCH 03/17] IDE: the last three pictures on BETA 1010 (Designer_Zoom50 with the grid off, Preview_Window with its File menu open, Export_Console without Export Verbose); shoot_docs opens a native menu with postWindowMessage (tb-launch.ps1's post mode) and lays its window over the form's --- WIP.Screenshots.md | 33 ++-- builder/page-baseline.json | 2 +- docs/IDE/Forms/Designer.md | 2 +- docs/IDE/Forms/Images/Designer_Zoom50.png | Bin 0 -> 24274 bytes docs/IDE/Forms/Images/Preview_Window.png | Bin 8423 -> 8923 bytes docs/IDE/Forms/Previewing.md | 4 +- .../Projects/Images/Export_Console.light.png | Bin 0 -> 32385 bytes .../Projects/Images/Export_Console.light.svg | 1 + docs/IDE/Projects/Images/Export_Console.png | Bin 0 -> 30465 bytes docs/IDE/Projects/Images/Export_Console.svg | 1 + docs/IDE/Projects/Saving-and-Exporting.md | 2 +- scripts/lib/tb-ide.mjs | 50 +++++- scripts/lib/tb-launch.ps1 | 22 ++- scripts/shoot_docs.mjs | 167 ++++++++++++++++-- 14 files changed, 248 insertions(+), 36 deletions(-) create mode 100644 docs/IDE/Forms/Images/Designer_Zoom50.png create mode 100644 docs/IDE/Projects/Images/Export_Console.light.png create mode 100644 docs/IDE/Projects/Images/Export_Console.light.svg create mode 100644 docs/IDE/Projects/Images/Export_Console.png create mode 100644 docs/IDE/Projects/Images/Export_Console.svg diff --git a/WIP.Screenshots.md b/WIP.Screenshots.md index 2b05a2a8..3e38c4ff 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,13 @@ 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. 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/builder/page-baseline.json b/builder/page-baseline.json index d75cc368..b9be1a14 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", "pages": 1482, - "staticFiles": 1063 + "staticFiles": 1068 } diff --git a/docs/IDE/Forms/Designer.md b/docs/IDE/Forms/Designer.md index 997534fc..cc90d4a4 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 0000000000000000000000000000000000000000..7950a11cfc03d73daea6f0b5bc4bac5029ad9f61 GIT binary patch literal 24274 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 A(*OVf literal 0 HcmV?d00001 diff --git a/docs/IDE/Forms/Images/Preview_Window.png b/docs/IDE/Forms/Images/Preview_Window.png index 451b4c2a04722b8fd0c92648df0690f61cfa64df..5e1a08ed668c2ef530d4a834cfb7c21caebe7817 100644 GIT binary patch literal 8923 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 literal 8423 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 diff --git a/docs/IDE/Forms/Previewing.md b/docs/IDE/Forms/Previewing.md index 4bf29b22..6611f8ab 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/Projects/Images/Export_Console.light.png b/docs/IDE/Projects/Images/Export_Console.light.png new file mode 100644 index 0000000000000000000000000000000000000000..d97585335c694923d36d110233103cb0e2ebfe6b GIT binary patch literal 32385 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 z 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%0D$Ev1wdb0Y`A_uFg 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 literal 0 HcmV?d00001 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 00000000..3070c2b2 --- /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 0000000000000000000000000000000000000000..dcec1868224bcb3efa907e495ac59a3fcf8eae07 GIT binary patch literal 30465 zcmb@tRa9JEx2_Apo#3v)9YSz-2^21c1b26LcMk-2cXxLP?(XicyZ-N6|JqmQVz+kc zs#?u4=NN5B@9+EcSz!P<2_$$tcrY+9Bq>QzMKCbPYA`TxSlBP1U%GeFsKCHb!K6fm zlwH%$I$#_LB+?F3p)h=fF@62%TxDWOgvhzW;;CnEHvXuVWvu-C`*1cfxp6jmesQ+y z=E2k2vAo>m!qH~nURcyNFL#ox$*{=EFq`{LI+uDnG?QezgQ1x=QEXj+)z>hWU=$ou z{d2RAysVatjU4pZ{q*{(i<9MfBNc=y3`-h_DlGXDm-9bgVWnguZ@MNQod5Tl`&6}c zhA;~0e;R^X=QG?A_4-vZ{@?3@|Mwa@M&Q4#P~!^$sA&K5^fX~1$$$ERB{hSG`ril9 z{694o|8IvF6g*T^JalyAY)VqmTwR;H_wH-fLFMk$uyFqjQObptjm<%pGLMD*1T20g zF*Pi&MVql}vno~vF(mTeF~u+Bt0I|i`Epts&wPm2mVa|yqe>@^X@uLY_x_ZZV2hbB9M$!>9daL!Vt$o$n6>zQJEiOSr`OK9 zn$GiGj8FN0Z=y&YG)kACL?-Gp>=h)*&c_Sr$&M|MG#oD0L86V&_ILlA^Fa53@$W

nEVHAM^z!X^&xU84#4@OBxeQ5d0s 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 KdsiuJ;Qt@xvP7r= literal 0 HcmV?d00001 diff --git a/docs/IDE/Projects/Images/Export_Console.svg b/docs/IDE/Projects/Images/Export_Console.svg new file mode 100644 index 00000000..2140fabf --- /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 12ebf1e9..7ef4563c 100644 --- a/docs/IDE/Projects/Saving-and-Exporting.md +++ b/docs/IDE/Projects/Saving-and-Exporting.md @@ -160,7 +160,7 @@ Each export writes a first line, `[EXPORT] exporting current project to "…"`, > [!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/scripts/lib/tb-ide.mjs b/scripts/lib/tb-ide.mjs index c6a72a67..56184b0e 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 d117dcf0..736f2f93 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 75520838..27a6db51 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) { From 360cae19136d71c7644c2efc0c522fd7266cc91d Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 14:27:20 +0200 Subject: [PATCH 04/17] WIP.Screenshots: why the export shot removes its folder (a second export into it wrote no COMPLETED line; record only) --- WIP.Screenshots.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WIP.Screenshots.md b/WIP.Screenshots.md index 3e38c4ff..70662e7a 100644 --- a/WIP.Screenshots.md +++ b/WIP.Screenshots.md @@ -410,7 +410,9 @@ none, which `New Project.md` also shows). - **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. 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. From 3c34d16e4a69ac1a521535ecfb9a41b116065a1a Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 15:54:28 +0200 Subject: [PATCH 05/17] BUGS-TO-REPORT: nine Build, Clean, ActiveX and Package Publishing defects from the Building trials (BETA 1010 and 1007), with the building, building-hklm and building-llvm-box ide-test lanes as their reproducers --- BUGS-TO-REPORT.md | 247 +++++++++++++++ bugs/build-autoincrement-not-saved/repro.json | 8 + bugs/build-path-projectid-empty/repro.json | 6 + bugs/build-save-box-writes-nothing/repro.json | 8 + bugs/clean-hklm-delete-builds/repro.json | 6 + bugs/hklm-warning-twice/repro.json | 9 + bugs/llvm-build-box-closes-other/repro.json | 8 + bugs/publish-failure-ok-typeerror/repro.json | 6 + bugs/publisher-name-apostrophe/repro.json | 6 + bugs/publisher-password-tilde/repro.json | 6 + docs/IDE/Package Publishing.md | 20 -- test/ide/building-hklm.test.mjs | 204 ++++++++++++ test/ide/building-kit.mjs | 298 ++++++++++++++++++ test/ide/building-llvm-box.test.mjs | 110 +++++++ test/ide/building.test.mjs | 291 +++++++++++++++++ test/ide/lanes.mjs | 3 + test/ide/probes/building-ax/Settings | 30 ++ .../probes/building-ax/Sources/MyClass.twin | 8 + test/ide/probes/building-exe/Settings | 34 ++ .../probes/building-exe/Sources/MainMod.twin | 7 + test/ide/probes/building-pkg/Settings | 28 ++ .../Sources/HelloWorldModule.twin | 7 + 22 files changed, 1330 insertions(+), 20 deletions(-) create mode 100644 bugs/build-autoincrement-not-saved/repro.json create mode 100644 bugs/build-path-projectid-empty/repro.json create mode 100644 bugs/build-save-box-writes-nothing/repro.json create mode 100644 bugs/clean-hklm-delete-builds/repro.json create mode 100644 bugs/hklm-warning-twice/repro.json create mode 100644 bugs/llvm-build-box-closes-other/repro.json create mode 100644 bugs/publish-failure-ok-typeerror/repro.json create mode 100644 bugs/publisher-name-apostrophe/repro.json create mode 100644 bugs/publisher-password-tilde/repro.json delete mode 100644 docs/IDE/Package Publishing.md create mode 100644 test/ide/building-hklm.test.mjs create mode 100644 test/ide/building-kit.mjs create mode 100644 test/ide/building-llvm-box.test.mjs create mode 100644 test/ide/building.test.mjs create mode 100644 test/ide/probes/building-ax/Settings create mode 100644 test/ide/probes/building-ax/Sources/MyClass.twin create mode 100644 test/ide/probes/building-exe/Settings create mode 100644 test/ide/probes/building-exe/Sources/MainMod.twin create mode 100644 test/ide/probes/building-pkg/Settings create mode 100644 test/ide/probes/building-pkg/Sources/HelloWorldModule.twin diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 86e2aef3..a8008f09 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -5316,3 +5316,250 @@ Severity: cosmetic. The paths are hard to read, and a path copied from the conso +--- + +## 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. + + + diff --git a/bugs/build-autoincrement-not-saved/repro.json b/bugs/build-autoincrement-not-saved/repro.json new file mode 100644 index 00000000..8a5a3c20 --- /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 00000000..86eb04f3 --- /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 00000000..39d8e907 --- /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 00000000..f777e064 --- /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/hklm-warning-twice/repro.json b/bugs/hklm-warning-twice/repro.json new file mode 100644 index 00000000..50fedd1a --- /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/llvm-build-box-closes-other/repro.json b/bugs/llvm-build-box-closes-other/repro.json new file mode 100644 index 00000000..d17f4341 --- /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 00000000..db97af1d --- /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 00000000..6387a08c --- /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 00000000..abe5f9ac --- /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/docs/IDE/Package Publishing.md b/docs/IDE/Package Publishing.md deleted file mode 100644 index 30bac9aa..00000000 --- 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/test/ide/building-hklm.test.mjs b/test/ide/building-hklm.test.mjs new file mode 100644 index 00000000..8ef9ad7a --- /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 00000000..1e250a5d --- /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 00000000..ca3d8258 --- /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 00000000..ffcb629e --- /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/lanes.mjs b/test/ide/lanes.mjs index e203ff19..567b1558 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 00000000..0c4338ee --- /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 00000000..00f921bf --- /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 00000000..2608590b --- /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 00000000..3ef0917a --- /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 00000000..1a7af7ff --- /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 00000000..bd1284ba --- /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 From 0fe2038dbdee6e6fef03b2d6dc77162305d5f0a6 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 16:38:49 +0200 Subject: [PATCH 06/17] IDE: the Building chapter (seven pages under /tB/IDE/Building/, Package Publishing absorbed with its old URL redirected), callouts for the nine queued defects, 14 SHOT markers; contradictions fixed on Project Settings, Toolbar, LLVM, Fusion, tbIDE Project and Creating a TWINPACK package --- builder/page-baseline.json | 2 +- docs/Documentation/Book-Configuration.md | 2 +- docs/Features/Fusion.md | 2 + .../Packages/Creating a TWINPACK package.md | 12 +- .../ActiveX-Registration.md | 4 +- docs/IDE/Building/ActiveX-Registration.md | 101 +++++++++++ docs/IDE/Building/Build-Configuration.md | 87 ++++++++++ docs/IDE/Building/Build-Output.md | 94 ++++++++++ docs/IDE/Building/Build-and-Clean.md | 162 ++++++++++++++++++ docs/IDE/Building/LLVM-in-the-IDE.md | 57 ++++++ docs/IDE/Building/Package-Publishing.md | 119 +++++++++++++ docs/IDE/Building/index.md | 55 ++++++ docs/IDE/Menu/File.md | 6 +- docs/IDE/Menu/Project.md | 2 +- docs/IDE/Menu/Tools.md | 4 +- docs/IDE/Menu/View.md | 2 +- docs/IDE/Project Settings.md | 6 +- docs/IDE/Projects/Saving-and-Exporting.md | 4 +- docs/IDE/Toolbar.md | 12 +- docs/LLVM/Getting-Started.md | 2 +- docs/Reference/Built-In/tbIDE/Project.md | 2 +- docs/_book.yml | 6 +- 22 files changed, 711 insertions(+), 32 deletions(-) create mode 100644 docs/IDE/Building/ActiveX-Registration.md create mode 100644 docs/IDE/Building/Build-Configuration.md create mode 100644 docs/IDE/Building/Build-Output.md create mode 100644 docs/IDE/Building/Build-and-Clean.md create mode 100644 docs/IDE/Building/LLVM-in-the-IDE.md create mode 100644 docs/IDE/Building/Package-Publishing.md create mode 100644 docs/IDE/Building/index.md diff --git a/builder/page-baseline.json b/builder/page-baseline.json index b9be1a14..92bf638b 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 1482, + "pages": 1488, "staticFiles": 1068 } diff --git a/docs/Documentation/Book-Configuration.md b/docs/Documentation/Book-Configuration.md index 290bea06..7c799b40 100644 --- a/docs/Documentation/Book-Configuration.md +++ b/docs/Documentation/Book-Configuration.md @@ -87,7 +87,7 @@ These are warnings: the exit code does not change. A build with nothing to repor A link from inside the book to a left-out page opens that page on the website, since a site path goes nowhere in a PDF. The link check's pass over `book.html` lists each such link as `OUT OF BOOK`: _site-pdf/book.html: - OUT OF BOOK https://docs.twinbasic.com/tB/IDE/Project/PackagePublishing -- not in the book; opens the website + OUT OF BOOK https://docs.twinbasic.com/tB/IDE/Project/Splash -- not in the book; opens the website Look there first for a left-out page that belongs in the book after all. diff --git a/docs/Features/Fusion.md b/docs/Features/Fusion.md index cf62d7e2..fb064f9d 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/Packages/Creating a TWINPACK package.md b/docs/Features/Packages/Creating a TWINPACK package.md index 96d01db7..d35292d7 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 d338070c..a4815ddc 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 00000000..f8dd2609 --- /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 00000000..d829b7e6 --- /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 00000000..34f47be9 --- /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 00000000..dc4375ba --- /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 00000000..6661ed44 --- /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 00000000..6c4176c6 --- /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 00000000..016d7228 --- /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/Menu/File.md b/docs/IDE/Menu/File.md index 556f2e9d..41972ea8 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 dfdec5df..1962d9af 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 f19f577d..edf19f9c 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 534aee6c..f05d92e6 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/Project Settings.md b/docs/IDE/Project Settings.md index d99ab94f..02352308 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/Saving-and-Exporting.md b/docs/IDE/Projects/Saving-and-Exporting.md index 7ef4563c..19bbea73 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 diff --git a/docs/IDE/Toolbar.md b/docs/IDE/Toolbar.md index c6b2d759..ab6f4b70 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 5d5d3af3..cac44c16 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 b65dc02f..c511f3ec 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/_book.yml b/docs/_book.yml index a054b0ac..436d536c 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/ @@ -676,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 From 1fee6dd58a792b42197df58b28832ee01df3e2b2 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 16:38:49 +0200 Subject: [PATCH 07/17] tbbuild/tbrun: the default build path opens no Save dialog (BETA 1010 and 1007, five cases); the staged copy's build path is pinned to know where the built file is --- WIP.Harness.md | 17 +++++++---------- WIP.md | 2 +- docs/Documentation/Tools-Compiler.md | 11 ++++------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/WIP.Harness.md b/WIP.Harness.md index 6097a92d..3f7207c0 100644 --- a/WIP.Harness.md +++ b/WIP.Harness.md @@ -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 diff --git a/WIP.md b/WIP.md index 91fdba78..92ae4df0 100644 --- a/WIP.md +++ b/WIP.md @@ -135,7 +135,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. diff --git a/docs/Documentation/Tools-Compiler.md b/docs/Documentation/Tools-Compiler.md index e99a7109..279bea75 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 From 920aad19964179f3b1c7b934495e822d0b773a54 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 16:34:19 +0200 Subject: [PATCH 08/17] tbdocs: strip HTML comments from the rendered pages (stripHtmlCommentsPlugin), with tests --- WIP.Build.md | 14 +++++ builder/render.mjs | 91 +++++++++++++++++++++++++++ docs/Documentation/Pipeline-Stages.md | 7 ++- test/render.test.mjs | 58 ++++++++++++++++- 4 files changed, 166 insertions(+), 4 deletions(-) diff --git a/WIP.Build.md b/WIP.Build.md index 49217863..cfa27f50 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/builder/render.mjs b/builder/render.mjs
index a9c8ee19..fffa57ed 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("`) 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/test/render.test.mjs b/test/render.test.mjs
index 4cb97618..35f34c63 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"]); From 7800cf9725e7ecfd39e52fa15f8e6f1760967082 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 15:54:27 +0200 Subject: [PATCH 09/17] ide_js: read, name and patch the IDE's minified main.js (a name map that carries across builds, a fix spliced into a copy of the install, the Fix shape of an IDE entry); its tests in test.bat and CI, eslint-scope and prettier added (owner); the Fix shape of export-verbose-done-escaped --- .github/actions/run-gates/action.yml | 8 + BUGS-TO-REPORT.md | 38 +- WIP.Harness.md | 67 ++- WIP.md | 17 +- bugs/export-verbose-done-escaped/fix.js | 43 ++ docs/Documentation/Building.md | 1 + docs/Documentation/Images/Tools-Groups.dot | 20 +- docs/Documentation/Images/Tools-Groups.svg | 130 ++-- docs/Documentation/Tools-Defects.md | 49 +- docs/Documentation/Tools-Toolchain-Tests.md | 9 + docs/Documentation/Tools.md | 28 +- package-lock.json | 58 ++ package.json | 2 + scripts/ide-js/lib.mjs | 632 ++++++++++++++++++++ scripts/ide-js/names.json | 115 ++++ scripts/ide_js.mjs | 428 +++++++++++++ scripts/lib/cli-cases.mjs | 39 ++ test.bat | 8 + test/ide-js.test.mjs | 374 ++++++++++++ 19 files changed, 1984 insertions(+), 82 deletions(-) create mode 100644 bugs/export-verbose-done-escaped/fix.js create mode 100644 scripts/ide-js/lib.mjs create mode 100644 scripts/ide-js/names.json create mode 100644 scripts/ide_js.mjs create mode 100644 test/ide-js.test.mjs diff --git a/.github/actions/run-gates/action.yml b/.github/actions/run-gates/action.yml index fe205f39..a781539e 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 a8008f09..31bbea03 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. @@ -5314,7 +5325,32 @@ The cause looks to be the one in #2444. The compiler sends each `DONE` line in t 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. + + --- diff --git a/WIP.Harness.md b/WIP.Harness.md index 3f7207c0..833b03f5 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. @@ -1670,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.md b/WIP.md index 92ae4df0..a8ddd837 100644 --- a/WIP.md +++ b/WIP.md @@ -169,6 +169,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 +561,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 +574,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 +588,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 +651,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/bugs/export-verbose-done-escaped/fix.js b/bugs/export-verbose-done-escaped/fix.js new file mode 100644 index 00000000..9e210d5c --- /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/docs/Documentation/Building.md b/docs/Documentation/Building.md index 103e76c3..523da705 100644 --- a/docs/Documentation/Building.md +++ b/docs/Documentation/Building.md @@ -83,6 +83,7 @@ Do not drop `--check-audit-index` when typing the underlying build command: with && 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/Documentation/Images/Tools-Groups.dot b/docs/Documentation/Images/Tools-Groups.dot index 9d0e0834..a5c59041 100644 --- a/docs/Documentation/Images/Tools-Groups.dot +++ b/docs/Documentation/Images/Tools-Groups.dot @@ -12,7 +12,7 @@ // The clusters side by side in one row must have the same number of node rows. // dot spaces its ranks for the whole graph, so where a shorter cluster ends, // the gap its border needs opens inside its taller neighbour too. The -// invisible nodes (cp1, cp2, dp) pad the shorter ones. +// invisible node cp2 pads the shorter one. // // Every node and cluster label is a link, set in the block at the end so the // layout above stays readable: a node to its tool's entry, by the id its @@ -134,17 +134,17 @@ digraph tools_groups { t21 [label="check_cli"] t22 [label="check_pdf_shims_equiv"] t23 [label="check_impexp_parity"] t24 [label="check_axe_patch_equiv"] t25 [label="indexer.test"] t26 [label="check_package_tests"] - t27 [label="broker.test"] + t27 [label="broker.test"] t28 [label="ide-js.test"] t1 -> t5 -> t9 -> t13 -> t17 -> t21 -> t25 t2 -> t6 -> t10 -> t14 -> t18 -> t22 -> t26 t3 -> t7 -> t11 -> t15 -> t19 -> t23 -> t27 - t4 -> t8 -> t12 -> t16 -> t20 -> t24 + t4 -> t8 -> t12 -> t16 -> t20 -> t24 -> t28 { rank=same; t5 t6 t7 t8 } { rank=same; t9 t10 t11 t12 } { rank=same; t13 t14 t15 t16 } { rank=same; t17 t18 t19 t20 } { rank=same; t21 t22 t23 t24 } - { rank=same; t25 t26 t27 } + { rank=same; t25 t26 t27 t28 } } // ---- row 4 ---- @@ -175,8 +175,8 @@ digraph tools_groups { label="Defect Reproducers" d1 [label="bug_repro"] d2 [label="vb6run"] - dp [style=invis label=""] - d1 -> d2 -> dp + d3 [label="ide_js"] + d1 -> d2 -> d3 } // ---- rows ---- @@ -187,7 +187,7 @@ digraph tools_groups { edge [weight=1] w10 -> s1 b7 -> c1 s9 -> t1 s10 -> t2 cp2 -> t3 c6 -> t4 - t25 -> e1 t22 -> i1 t24 -> d1 + t25 -> e1 t22 -> i1 t28 -> d1 // ---- links ---- // Each link has a tooltip describing the tool or group, which a pointer shows on hover. @@ -222,7 +222,7 @@ digraph tools_groups { } subgraph cluster_defects { URL="Tools-Defects" - tooltip="Covers the tools that build and verify reproducer projects for twinBASIC defects and that run VB6 code to compare it with twinBASIC." + tooltip="Covers the tools that build and verify reproducer projects for twinBASIC defects, that run VB6 code to compare it with twinBASIC, and that patch the JavaScript of the IDE to test a fix." } w1 [URL="#buildbat" @@ -351,6 +351,8 @@ digraph tools_groups { tooltip="Verifies that 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."] t27 [URL="Tools-Toolchain-Tests#broker-test" tooltip="Tests tbbroker and its Node client: leases, held processes, waiting, ports, and desktops left with windows on them. Skipped unless a built broker is named."] + t28 [URL="Tools-Toolchain-Tests#ide-js-test" + tooltip="Runs unit tests for ide_js.mjs: the byte-exact round trip of a minified file, how a local's name carries to another build, and a fix patched in as the smallest edit."] e1 [URL="Tools-Samples#check-examples" tooltip="Compiles the code samples marked check_build and reports each diagnostic against the page line it came from. Samples marked check_run are also run."] @@ -382,4 +384,6 @@ digraph tools_groups { tooltip="Builds, runs and verifies the reproducer projects for the entries of BUGS-TO-REPORT.md, one folder for each entry."] d2 [URL="Tools-Defects#vb6run" tooltip="Builds and runs VB6 code, to compare what a documented sample prints in twinBASIC with what it prints in VB6."] + d3 [URL="Tools-Defects#ide-js" + tooltip="Reads, names and patches the minified JavaScript of the twinBASIC IDE, to test a fix for an IDE defect on a patched copy of the install and write its fix shape."] } diff --git a/docs/Documentation/Images/Tools-Groups.svg b/docs/Documentation/Images/Tools-Groups.svg index 8c9639e6..c32c15a1 100644 --- a/docs/Documentation/Images/Tools-Groups.svg +++ b/docs/Documentation/Images/Tools-Groups.svg @@ -1,8 +1,8 @@ - + 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/Tools-Defects.md b/docs/Documentation/Tools-Defects.md index 06aeacab..ca31c8ca 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 1427155e..0b86feb1 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 55658892..a9669f08 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/package-lock.json b/package-lock.json
index 45e2f49d..bf0d8751 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 3f52b564..6523ada3 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/ide-js/lib.mjs b/scripts/ide-js/lib.mjs
new file mode 100644
index 00000000..078462d3
--- /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 00000000..ffccce43
--- /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 00000000..027e7e91
--- /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 d8869fa1..23d32eee 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/test.bat b/test.bat
index 7e9e0369..a58a4b12 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/ide-js.test.mjs b/test/ide-js.test.mjs
new file mode 100644
index 00000000..47b89f5f
--- /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");
+  });
+});

From bfdbf6948d25381b7a3f5af8a1fd03c37b2a495e Mon Sep 17 00:00:00 2001
From: Kuba Sunderland-Ober 
Date: Sun, 18 Oct 2026 16:46:52 +0200
Subject: [PATCH 10/17] harness.json: every limit but compilerRunQuiet is 10 s
 (owner, with tblaunch's timings), still version 1; harness.md says what 10 s
 cuts off and that package tests keep their own 60 s

---
 test/example-batches.test.mjs |  1 +
 test/harness.json             | 16 ++++++++--------
 test/harness.md               | 22 +++++++++++++++++-----
 3 files changed, 26 insertions(+), 13 deletions(-)

diff --git a/test/example-batches.test.mjs b/test/example-batches.test.mjs
index f6ac46af..c3a9bb9f 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/harness.json b/test/harness.json
index ac877f30..a8c29a79 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 07396993..6cf08c88 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.

From 521d12625a683240235cd9f4977fae65bcd1f998 Mon Sep 17 00:00:00 2001
From: Kuba Sunderland-Ober 
Date: Sun, 18 Oct 2026 15:51:08 +0200
Subject: [PATCH 11/17] Data types: a section per type on the summary page,
 indexed as kind type for the help add-in (F1 on As String opens the type,
 String( the function); Long's own page as the model

---
 WIP.HelpAddin.md                       |  10 +-
 WIP.md                                 |   1 +
 add-in/Resources/SYMBOLS/symbols.json  |  14 +++
 builder/symbol-baseline.json           |  14 +++
 builder/symbols.mjs                    |  21 +++-
 docs/Documentation/Permanent-Links.md  |   8 ++
 docs/Reference/Data-Types.md           |  86 ++++++++++-----
 docs/Reference/Data-Types/Long.md      | 138 +++++++++++++++++++++++++
 docs/_book.yml                         |   2 +-
 scripts/check_symbol_index.mjs         |  36 ++++++-
 test/addin/help.test.mjs               |  10 ++
 test/addin/helphost/Sources/Cases.twin |   5 +
 12 files changed, 311 insertions(+), 34 deletions(-)
 create mode 100644 docs/Reference/Data-Types/Long.md

diff --git a/WIP.HelpAddin.md b/WIP.HelpAddin.md
index 05b768bd..e00f4adf 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.md b/WIP.md
index a8ddd837..fe879f4d 100644
--- a/WIP.md
+++ b/WIP.md
@@ -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. diff --git a/add-in/Resources/SYMBOLS/symbols.json b/add-in/Resources/SYMBOLS/symbols.json index 31d6d79e..d8e11407 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/builder/symbol-baseline.json b/builder/symbol-baseline.json index 6c8ae89b..398827a3 100644 --- a/builder/symbol-baseline.json +++ b/builder/symbol-baseline.json @@ -1,6 +1,20 @@ { "src": "docs", "urls": [ + "/Reference/Data-Types#boolean", + "/Reference/Data-Types#byte", + "/Reference/Data-Types#currency", + "/Reference/Data-Types#date", + "/Reference/Data-Types#decimal", + "/Reference/Data-Types#double", + "/Reference/Data-Types#integer", + "/Reference/Data-Types#long", + "/Reference/Data-Types#longlong", + "/Reference/Data-Types#longptr", + "/Reference/Data-Types#object", + "/Reference/Data-Types#single", + "/Reference/Data-Types#string", + "/Reference/Data-Types#variant", "/tB/Core/AddressOf", "/tB/Core/Alias", "/tB/Core/And", diff --git a/builder/symbols.mjs b/builder/symbols.mjs index 0f7efa2d..b2359ac0 100644 --- a/builder/symbols.mjs +++ b/builder/symbols.mjs @@ -24,6 +24,10 @@ // operators". The first word of each form is the statement or operator, // the rest keywords of it: Loop, Next, Wend, Then. Attributes.md, which is // /tB/Core/Attributes, has one attribute to each `##`. +// - Reference/Data-Types.md, the one page outside /tB/ that is read: each +// intrinsic type its `symbols:` names is a `type`, at the `##` heading of +// that name, a short section that links to the type's own page (owner, +// 2026-10-18). A name with no such heading fails the build. // - A package folder: a page is a type's when its title or first heading // names a type the package declares ("Strings Module" is Strings, "Len, // LenB" is Len and LenB). A page under a type's page in the nav, named @@ -67,6 +71,7 @@ export const PACKAGE_FOLDERS = [ const CORE_FOLDER = "Reference/Core/"; const ATTRIBUTES_PAGE = "Reference/Attributes.md"; +export const DATA_TYPES_PAGE = "Reference/Data-Types.md"; // Section headings under which every heading names a member, and the kind // each gives one the packages do not declare. @@ -242,6 +247,12 @@ export function deriveSymbolIndex({ pages, api }) { const tbPages = pages.filter((p) => String(p.url).startsWith("/tB/")); // ---- the language + const dataTypes = pages.find((p) => p.src === DATA_TYPES_PAGE); + for (const name of dataTypes?.symbols ?? []) { + const h = dataTypes.headings.find((x) => x.level === 2 && x.id && x.text.toLowerCase() === name.toLowerCase()); + if (!h) throw new Error(`${DATA_TYPES_PAGE} names ${name} in symbols: and has no ## ${name} heading`); + add(entry(name, null, null, "type", `${dataTypes.url}#${h.id}`)); + } for (const page of tbPages) { if (page.src === ATTRIBUTES_PAGE) { for (const h of page.headings) { @@ -593,12 +604,16 @@ export const SYMBOL_INDEX_FORMAT = 1; export const SYMBOL_INDEX_REL = "tB/symbols.json"; /** - * The build's pages as deriveSymbolIndex reads them: those under /tB/, with the - * headings of their rendered HTML. + * The build's pages as deriveSymbolIndex reads them: those under /tB/, and the + * data types page, with the headings of their rendered HTML. */ export function symbolPages(pages) { return pages - .filter((p) => String(p.permalink ?? "").startsWith("/tB/") && typeof p.renderedContent === "string") + .filter( + (p) => + (String(p.permalink ?? "").startsWith("/tB/") || p.srcRel === DATA_TYPES_PAGE) && + typeof p.renderedContent === "string", + ) .map((p) => { const fm = p.frontmatter ?? {}; return { diff --git a/docs/Documentation/Permanent-Links.md b/docs/Documentation/Permanent-Links.md index 18cd98c3..be99a7e7 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,12 @@ 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: + +- [Long](../../tB/Core/Long) + ## /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/Reference/Data-Types.md b/docs/Reference/Data-Types.md index be2d88a9..85d57c4c 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. 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,56 +20,91 @@ 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.94065645841246E-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 (+ 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). + +**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: + +```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) +``` + --- -## Integer types +## Boolean **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. +--- + +## Byte + **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. +--- + +## Integer + **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. +--- -**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^`. +## Long -**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. +**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. -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). +--- + +## 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^`. --- -## Floating-point types +## LongPtr -**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. +**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. -**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 -**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. +**Single** follows the IEEE 754 standard for single-precision floating-point. It is accurate to approximately 6--7 significant decimal digits. It is smaller than a **Double** and may be faster in tight loops, but the reduced precision makes it unsuitable for financial or scientific calculations where rounding error matters. 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. > [!NOTE] > VB6 raises no error for these assignments: the **Single** holds infinity (`1.#INF`). +--- + +## Double + +**Double** follows the IEEE 754 standard for double-precision floating-point. Both **Single** and **Double** can represent `NaN` and `Infinity` as bit patterns, though the VBA runtime raises an error on most operations that would produce them. + +**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. + +--- + +## Currency + **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. --- @@ -163,7 +199,7 @@ An **Object** variable can hold any COM-compatible object; the runtime resolves --- -### 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/Long.md b/docs/Reference/Data-Types/Long.md new file mode 100644 index 00000000..bef1f904 --- /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**](../../Reference/Data-Types#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**](../../Reference/Data-Types#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**. Division with `/` and `^` always give a **Double**, and `\` 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**](../../Reference/Data-Types#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](../../Reference/Data-Types#integer), [LongLong](../../Reference/Data-Types#longlong), [LongPtr](../../Reference/Data-Types#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/_book.yml b/docs/_book.yml index 436d536c..5e21fb1f 100644 --- a/docs/_book.yml +++ b/docs/_book.yml @@ -348,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 diff --git a/scripts/check_symbol_index.mjs b/scripts/check_symbol_index.mjs index b61792ee..838c9553 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/test/addin/help.test.mjs b/test/addin/help.test.mjs index c37e4602..125ff261 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 e00d5d7b..2482d8f9 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 From 04f6cf3d70cf83e1701692ea1cf74e6893f16bc0 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 16:08:22 +0200 Subject: [PATCH 12/17] BUGS-TO-REPORT: Dim x As Any compiles (locals, Static, module and class fields; BETA 1010 and 1007; VB6 refuses), with a test reproducer and its VB6 side --- BUGS-TO-REPORT.md | 77 ++++++++++++++++++ bugs/dim-as-any/dim-as-any.twinproj | Bin 0 -> 4133 bytes bugs/dim-as-any/repro.json | 20 +++++ bugs/dim-as-any/src/Settings | 75 +++++++++++++++++ .../src/Sources/RegressionTests.twin | 40 +++++++++ bugs/dim-as-any/src/Sources/Startup.twin | 6 ++ bugs/dim-as-any/vb6/Module1.bas | 29 +++++++ bugs/dim-as-any/vb6/Probe.vbp | 7 ++ 8 files changed, 254 insertions(+) create mode 100644 bugs/dim-as-any/dim-as-any.twinproj create mode 100644 bugs/dim-as-any/repro.json create mode 100644 bugs/dim-as-any/src/Settings create mode 100644 bugs/dim-as-any/src/Sources/RegressionTests.twin create mode 100644 bugs/dim-as-any/src/Sources/Startup.twin create mode 100644 bugs/dim-as-any/vb6/Module1.bas create mode 100644 bugs/dim-as-any/vb6/Probe.vbp diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 31bbea03..f20fe009 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -5599,3 +5599,80 @@ 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. + + 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 0000000000000000000000000000000000000000..195993b172cab450eabaf6f4cac61c147284b5f6 GIT binary patch literal 4133 zcmcgvTTdG~6i&NW*yU~aZ6B-%sUR^?Zjc03l_trgMN0|_6jmY?a%N0$nb;%S14M|Q zvwv=X#hzo&gd_#Fedy+4GWNND=lC4YAAkSy&x^v(g+igrC%*Q@6kZ>GKf>$F!te0h zn

h79CF$w$De3D$eu+W1;W^E?ySa;MwOoWN}Odlj#BvsskQ0cSyr4I$l)qlOwOd z-+*bEs&JA1($SIa=yU3KNPbcH%}S3I)0#_h%(T%ZbHjxdMg{oW1W#U(0n6ElK|XRJ z6)~5LD2Bmq5voZjCkc-k38`j!moOS$l9&qyeAEyoB2Bm;bZzw&vQC&VguEgh|0p1@ z-_r?uvrAsH+k}OH-5WwCsWxQHuQU0G)3K1+@Q_I1R9fX5nZk%|ks?z{s-ks{iYc+n zEds63BHB(cE>4?Sh#=!vI3Tfs%nsd}8XBwZ-XA=q0!U-b9E2adCYt>J_LPs%M%E2PWy;Ou2 zl>sxU66os2=CZz=$?+`d%YDq5mi@%69n&jjRXfa+%)kSxz%&E&3H`wGoa*)zfQ4*h z`kvQsdA`nwOS&x|y|L*K#JHi~liOvq_9Yx_pgdv%WsaaLz`23^FP4B6bHpbk(Hh;T ze3l+7=_^I2Pg@-@lpZ}IZ7I)K4AD{BC-P$LxPEaC8hwqFU1&2{{TLpWYnCc}b)~qr ziUZ?C;a7Mmg7Rzyrn-_}%EDN^85-t^4&xwK;=^ z>yEqQsq9V)2J|Kt9w9ve#hZC+rdtv;iRK0$ESPmtr0JGAZMEg^`xEe zX2{u-4Dxv$McUepxaqi>UYi zGZ{}FhgP%R@GIr6*K9Q#UcKI~dd)Vx1%9K^4SJ1UrQ7;ux$w1S>YKy8VC2j2{pw<` z)@`xF<0GbKHe7W!s!v?gf@5J*_ds{Ih67i3aCq~}fUSoa+YMC$CqBsAZsxxGZ1cDF zR*IKg*dc$GtvP%9<`8;+X_EXS+ M4RCGs-d%qF1v9yR?*IS* literal 0 HcmV?d00001 diff --git a/bugs/dim-as-any/repro.json b/bugs/dim-as-any/repro.json new file mode 100644 index 00000000..d2242b6a --- /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 00000000..d96f7870 --- /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 00000000..d7c512d0 --- /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 00000000..20233b3a --- /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 00000000..82c17e21 --- /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 00000000..4d210b0e --- /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 From cf73c0b16c3af053d96c44fafdd34b5e1b8f2654 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 16:33:53 +0200 Subject: [PATCH 13/17] Data types: a page for each of the other thirteen intrinsic types (Date at /tB/Core/Date-Type), the summary sections trimmed to link to them, the As Any NOTE; DefLngPtr and CLngPtr named right on New Data Types --- .../llvm-double-overflow-no-error/REPORT.md | 2 +- bugs/filed/single-overflow-no-error/REPORT.md | 2 +- docs/Documentation/Permanent-Links.md | 6 +- docs/Features/Language/Data-Types.md | 14 +- docs/Features/Language/Literals.md | 2 +- docs/Reference/Data-Types.md | 99 ++-------- docs/Reference/Data-Types/Boolean.md | 108 +++++++++++ docs/Reference/Data-Types/Byte.md | 122 ++++++++++++ docs/Reference/Data-Types/Currency.md | 102 +++++++++++ docs/Reference/Data-Types/Date.md | 123 +++++++++++++ docs/Reference/Data-Types/Decimal.md | 126 +++++++++++++ docs/Reference/Data-Types/Double.md | 120 ++++++++++++ docs/Reference/Data-Types/Integer.md | 123 +++++++++++++ docs/Reference/Data-Types/Long.md | 10 +- docs/Reference/Data-Types/LongLong.md | 127 +++++++++++++ docs/Reference/Data-Types/LongPtr.md | 86 +++++++++ docs/Reference/Data-Types/Object.md | 100 ++++++++++ docs/Reference/Data-Types/Single.md | 120 ++++++++++++ docs/Reference/Data-Types/String.md | 140 ++++++++++++++ docs/Reference/Data-Types/Variant.md | 173 ++++++++++++++++++ docs/Reference/Glossary.md | 2 +- 21 files changed, 1610 insertions(+), 97 deletions(-) create mode 100644 docs/Reference/Data-Types/Boolean.md create mode 100644 docs/Reference/Data-Types/Byte.md create mode 100644 docs/Reference/Data-Types/Currency.md create mode 100644 docs/Reference/Data-Types/Date.md create mode 100644 docs/Reference/Data-Types/Decimal.md create mode 100644 docs/Reference/Data-Types/Double.md create mode 100644 docs/Reference/Data-Types/Integer.md create mode 100644 docs/Reference/Data-Types/LongLong.md create mode 100644 docs/Reference/Data-Types/LongPtr.md create mode 100644 docs/Reference/Data-Types/Object.md create mode 100644 docs/Reference/Data-Types/Single.md create mode 100644 docs/Reference/Data-Types/String.md create mode 100644 docs/Reference/Data-Types/Variant.md diff --git a/bugs/filed/llvm-double-overflow-no-error/REPORT.md b/bugs/filed/llvm-double-overflow-no-error/REPORT.md index 71edf8d3..b5aa9b4f 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 2c3b2fbb..1f1def5d 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/docs/Documentation/Permanent-Links.md b/docs/Documentation/Permanent-Links.md index be99a7e7..9fd64a18 100644 --- a/docs/Documentation/Permanent-Links.md +++ b/docs/Documentation/Permanent-Links.md @@ -78,9 +78,11 @@ A name can have several entries: `Add` is a method of a dozen classes, and `Prin ## /tB/Core/\ -Each intrinsic data type has a page of its own beside the statements: +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: -- [Long](../../tB/Core/Long) +- [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/\/\ diff --git a/docs/Features/Language/Data-Types.md b/docs/Features/Language/Data-Types.md index 07fe226a..8a2acbbf 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 6eebd2e2..56f0ba48 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/Reference/Data-Types.md b/docs/Reference/Data-Types.md index 85d57c4c..c32650f9 100644 --- a/docs/Reference/Data-Types.md +++ b/docs/Reference/Data-Types.md @@ -9,7 +9,7 @@ symbols: [Boolean, Byte, Integer, Long, LongLong, LongPtr, Single, Double, Curre # 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, with a short section for each type. +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). @@ -27,12 +27,12 @@ For the twinBASIC-specific additions to this set --- **LongLong**, **LongPtr**, | [**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.94065645841246E-324 to ±1.79769313486232E308 | +| [**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 (+ heap data) | Any of the above | +| [**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. @@ -46,23 +46,26 @@ Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _ (ByRef Destination As Any, ByRef Source As Any, ByVal Length As LongPtr) ``` +> [!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). + --- ## Boolean -**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. +**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. --- ## Byte -**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. +**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. --- ## Integer -**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. +**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. --- @@ -74,128 +77,66 @@ Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _ ## 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** 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. --- ## LongPtr -**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** 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. --- ## Single -**Single** follows the IEEE 754 standard for single-precision floating-point. It is accurate to approximately 6--7 significant decimal digits. It is smaller than a **Double** and may be faster in tight loops, but the reduced precision makes it unsuitable for financial or scientific calculations where rounding error matters. - -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. - -> [!NOTE] -> VB6 raises no error for these assignments: the **Single** holds infinity (`1.#INF`). +**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. --- ## Double -**Double** follows the IEEE 754 standard for double-precision floating-point. Both **Single** and **Double** can represent `NaN` and `Infinity` as bit patterns, though the VBA runtime raises an error on most operations that would produce them. - -**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. +**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. --- ## Currency -**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. +**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. --- ## 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. - -**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. - -**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. - -### 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**](../tB/Modules/Conversion/CDec) gets the same result types from a declared **Decimal**, except under `\` and `Mod`, where the **Variant** gives a **Long**. +**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. --- ## Date -**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. - -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** 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. ### 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**](../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. --- diff --git a/docs/Reference/Data-Types/Boolean.md b/docs/Reference/Data-Types/Boolean.md new file mode 100644 index 00000000..d276700f --- /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 00000000..5b66ff0b --- /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 00000000..18bca70e --- /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 00000000..f7e70f69 --- /dev/null +++ b/docs/Reference/Data-Types/Date.md @@ -0,0 +1,123 @@ +--- +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 **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 00000000..51e2f8d5 --- /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 00000000..b4b75379 --- /dev/null +++ b/docs/Reference/Data-Types/Double.md @@ -0,0 +1,120 @@ +--- +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. + +```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 00000000..d2cb5db9 --- /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 index bef1f904..1d0ef290 100644 --- a/docs/Reference/Data-Types/Long.md +++ b/docs/Reference/Data-Types/Long.md @@ -24,7 +24,7 @@ A **Long** variable is declared **As Long**, or by the type-declaration characte ## 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**](../../Reference/Data-Types#longptr) is the type that changes size with the build. +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 @@ -39,7 +39,7 @@ Debug.Print VarType(Count) = vbLong ' True 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**](../../Reference/Data-Types#longlong). The digits of a `&H`, `&O` or `&B` literal can be grouped with underscores, as in `&HFFFF_0000`; a decimal literal cannot. +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 @@ -74,7 +74,7 @@ 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**. Division with `/` and `^` always give a **Double**, and `\` and `Mod` on two **Long** values give a **Long**. [Result types and promotion](../../Reference/Operators#result-types-and-promotion) has the full rules. +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. @@ -105,7 +105,7 @@ A **Variant** holding a **Long** does not overflow: a result that does not fit b ## 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**](../../Reference/Data-Types#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`. +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 @@ -132,7 +132,7 @@ 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](../../Reference/Data-Types#integer), [LongLong](../../Reference/Data-Types#longlong), [LongPtr](../../Reference/Data-Types#longptr) -- the other integer 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 00000000..ac1198ec --- /dev/null +++ b/docs/Reference/Data-Types/LongLong.md @@ -0,0 +1,127 @@ +--- +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 +``` + +## 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 00000000..a345fde1 --- /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 00000000..576a4bcf --- /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 in twinBASIC, the **HRESULT** `E_NOINTERFACE`. VB6 raises error 13, *Type mismatch*. + +## 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 00000000..d203d5f0 --- /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 00000000..fa9085ce --- /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 00000000..d396b9ac --- /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/Glossary.md b/docs/Reference/Glossary.md index 5ea8b6dc..f3753891 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 From fed28860a3894f869b44c4c706f77b8e46c45f30 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 17:00:54 +0200 Subject: [PATCH 14/17] BUGS-TO-REPORT: five data type defects (Date out of range, CLngLng of 2^63, Set's error number, Len/LenB of an array, 64-bit infinity text; BETA 1010 and 1007), with test reproducers, VB6 sides and their callouts --- BUGS-TO-REPORT.md | 440 ++++++++++++++++++ .../clnglng-2-to-63-no-overflow.twinproj | Bin 0 -> 5272 bytes bugs/clnglng-2-to-63-no-overflow/repro.json | 16 + bugs/clnglng-2-to-63-no-overflow/src/Settings | 75 +++ .../src/Sources/RegressionTests.twin | 81 ++++ .../src/Sources/Startup.twin | 6 + .../vb6/Module1.bas | 40 ++ .../clnglng-2-to-63-no-overflow/vb6/Probe.vbp | 7 + .../date-out-of-range-no-error.twinproj | Bin 0 -> 5590 bytes bugs/date-out-of-range-no-error/repro.json | 17 + bugs/date-out-of-range-no-error/src/Settings | 75 +++ .../src/Sources/RegressionTests.twin | 96 ++++ .../src/Sources/Startup.twin | 6 + .../vb6/Module1.bas | 50 ++ bugs/date-out-of-range-no-error/vb6/Probe.vbp | 7 + .../infinity-text-64-bit.twinproj | Bin 0 -> 5220 bytes bugs/infinity-text-64-bit/repro.json | 18 + bugs/infinity-text-64-bit/src/Settings | 75 +++ .../src/Sources/RegressionTests.twin | 73 +++ .../src/Sources/Startup.twin | 6 + bugs/infinity-text-64-bit/vb6/Module1.bas | 48 ++ bugs/infinity-text-64-bit/vb6/Probe.vbp | 7 + .../lenb-of-array-accepted.twinproj | Bin 0 -> 4053 bytes bugs/lenb-of-array-accepted/repro.json | 15 + bugs/lenb-of-array-accepted/src/Settings | 75 +++ .../src/Sources/RegressionTests.twin | 33 ++ .../src/Sources/Startup.twin | 6 + bugs/lenb-of-array-accepted/vb6/Module1.bas | 28 ++ bugs/lenb-of-array-accepted/vb6/Probe.vbp | 7 + .../repro.json | 13 + ...et-unsupported-class-error-number.twinproj | Bin 0 -> 4517 bytes .../src/Settings | 75 +++ .../src/Sources/RegressionTests.twin | 68 +++ .../src/Sources/Startup.twin | 6 + .../vb6/ClsA.cls | 16 + .../vb6/ClsB.cls | 16 + .../vb6/IFoo.cls | 17 + .../vb6/Module1.bas | 40 ++ .../vb6/Probe.vbp | 10 + docs/Reference/Data-Types/Date.md | 5 + docs/Reference/Data-Types/Double.md | 5 +- docs/Reference/Data-Types/LongLong.md | 3 + docs/Reference/Data-Types/Object.md | 2 +- docs/Reference/Default/VBA/Strings/Len.md | 3 + 44 files changed, 1584 insertions(+), 2 deletions(-) create mode 100644 bugs/clnglng-2-to-63-no-overflow/clnglng-2-to-63-no-overflow.twinproj create mode 100644 bugs/clnglng-2-to-63-no-overflow/repro.json create mode 100644 bugs/clnglng-2-to-63-no-overflow/src/Settings create mode 100644 bugs/clnglng-2-to-63-no-overflow/src/Sources/RegressionTests.twin create mode 100644 bugs/clnglng-2-to-63-no-overflow/src/Sources/Startup.twin create mode 100644 bugs/clnglng-2-to-63-no-overflow/vb6/Module1.bas create mode 100644 bugs/clnglng-2-to-63-no-overflow/vb6/Probe.vbp create mode 100644 bugs/date-out-of-range-no-error/date-out-of-range-no-error.twinproj create mode 100644 bugs/date-out-of-range-no-error/repro.json create mode 100644 bugs/date-out-of-range-no-error/src/Settings create mode 100644 bugs/date-out-of-range-no-error/src/Sources/RegressionTests.twin create mode 100644 bugs/date-out-of-range-no-error/src/Sources/Startup.twin create mode 100644 bugs/date-out-of-range-no-error/vb6/Module1.bas create mode 100644 bugs/date-out-of-range-no-error/vb6/Probe.vbp create mode 100644 bugs/infinity-text-64-bit/infinity-text-64-bit.twinproj create mode 100644 bugs/infinity-text-64-bit/repro.json create mode 100644 bugs/infinity-text-64-bit/src/Settings create mode 100644 bugs/infinity-text-64-bit/src/Sources/RegressionTests.twin create mode 100644 bugs/infinity-text-64-bit/src/Sources/Startup.twin create mode 100644 bugs/infinity-text-64-bit/vb6/Module1.bas create mode 100644 bugs/infinity-text-64-bit/vb6/Probe.vbp create mode 100644 bugs/lenb-of-array-accepted/lenb-of-array-accepted.twinproj create mode 100644 bugs/lenb-of-array-accepted/repro.json create mode 100644 bugs/lenb-of-array-accepted/src/Settings create mode 100644 bugs/lenb-of-array-accepted/src/Sources/RegressionTests.twin create mode 100644 bugs/lenb-of-array-accepted/src/Sources/Startup.twin create mode 100644 bugs/lenb-of-array-accepted/vb6/Module1.bas create mode 100644 bugs/lenb-of-array-accepted/vb6/Probe.vbp create mode 100644 bugs/set-unsupported-class-error-number/repro.json create mode 100644 bugs/set-unsupported-class-error-number/set-unsupported-class-error-number.twinproj create mode 100644 bugs/set-unsupported-class-error-number/src/Settings create mode 100644 bugs/set-unsupported-class-error-number/src/Sources/RegressionTests.twin create mode 100644 bugs/set-unsupported-class-error-number/src/Sources/Startup.twin create mode 100644 bugs/set-unsupported-class-error-number/vb6/ClsA.cls create mode 100644 bugs/set-unsupported-class-error-number/vb6/ClsB.cls create mode 100644 bugs/set-unsupported-class-error-number/vb6/IFoo.cls create mode 100644 bugs/set-unsupported-class-error-number/vb6/Module1.bas create mode 100644 bugs/set-unsupported-class-error-number/vb6/Probe.vbp diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index f20fe009..0d61b7c1 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -5676,3 +5676,443 @@ VB6 does not accept `As Any` outside a `Declare` parameter. Its command-line bui 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. Seen on the way and not covered by the cases: in a 32-bit build `q = 2 ^ 63 - 1024` (assigned without `CLngLng`) raises error 6 although the value fits, where a 64-bit build stores 9223372036854774784; `CLngLng(2 ^ 63 - 1024)` is right in both. + +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. + + 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 0000000000000000000000000000000000000000..868fdb08a098e2d6db93f07efd44ed4c02dc4e62 GIT binary patch literal 5272 zcmcgwZExea5%%r1y(X={wf%G-2zP-^(8!Qw#V>of53(#Dg60jeFDVdj7P8IQyvn4P zq}C3C{G9%V{-^$m&QPLXHeNJatk2k0Bynaq^UUz!P~ZRS5C8e5@RvfN5GH&K|Gb{4 zRnNu6M=I|VF^7TG?{{$fs_$D1a;%ZB&h&lOc7mBY>PSJ^mWtW4~TMZn%v@!W0pl zl3A=Y(GyCNSc3bSoC-e1|3@U|qvHM>G87!3)Qpb_(`1^0jEtG0gi0wSsT~muHK-^; zLHL0PFe!;hwIl8w8S^nEALAsYWF)9Us|VpJ%)`eAmFRDk<{_w(wnzsF%1IXE%|s+4 zoE3vOER&lF<;K%o4g!eLIi+K+(Ij2Wa+HopE9wQL;#ORY!alim8w^IG1#z|@b7baF z(>O^04z#;MWX|-2aFIn`CIKfYSLyvdThI}igTK__>2v^*CrncrCuBe$1UP^u5OEvi z5m5q7KcBx&4z_1wE}7PV0Qvy9k~@<^lvVMRE`cy;p=peJ%sx_;#e*_)iEaQ!X`;*I zx>X|+NEpOpBm9A2Rt~(1U-ui;n&0^AVU}w}YG_8%a&i9_Yp}x>I+gTYasNz=Qb?9H zVQaw@YH`1~PvB=%!dRg^K3_tj*^qS8fte8Id>`hC?9D|_^d_`ceSd{jrfLU=nG!0t z*_3=Bfm%+t8oJ;lG6e}sErnpu=>oEcO8EOgeza2^c;t>iMXd${rJ%seT`k#Amjg+o z4=LmhDjO`8&rL=%=?FjtufHI!tXNO8axm1iv$&oY6Xb8VQ37&o%-{dKTm`2Yi`Z(& zAZjyuJgU%{{|Wf zV}XQ`ZyG<649iuuyqq@{UjzRa1Puu?d)fu3QKk9S*Usj~y8>dDENDY&QRv9lk{Jw# zbOv46jyRq;&;xk>|IVBHoa%Ac_zWbUu#aDF_;*L-Lk8uSZxI%pGsaVuZ4o=e#d$fG zd>JNNTB-US;=c_T#3<_fVA$}IF+#m?kh76jSthR^p=#`^gf2>NtKRJZYISq z6gq}@HtR8+P?Xqy2-%b$q%0X-#(IJZuOGACAD^H4{T7b(`>!7_GvnvbxIMjp{TRsM z1e%B;c3n^9*N+{R&}ab-A@cLn7W;UxLHmFhAH{M+qJKo2Vk4@ClFcwk6uhDk&nSg6 z%3HG7l!VS24om3aiMsYcSj&`dK)^<5;?Zx0ZO?Z*PP^F%9KYe$oQ8kgal%T+4?A9` z6&^>QILjeG)O`tX!Z;f(X3#w{z0D6v@1aZO5Cn|gLX!3{qkDU$+(FlMJ=d?g4&3CI z(B9!I*Y#_egP@ZPjh2g%!lU){;xr;bs>RgQhd~NPXMj!>u_=Xb`~4fnM`Et}{qBRh zxwxtM{l3y8I3Rdsoeb7>YW_AIO{Z0FtB!h@4n%Td@XkzHYjE(wVmOO`5fW^Ax!d+N zVvZ*%0#k_3HQ z2NuvA=TBc-oHyaWq8}M%2kcZUpG!elX~G(*Hl3Ou!t!Z1TTTlO>`rie?0Ox)5p}}w zS4lxIn~`xM25<@*Cb5D~1p{O{3(Ss?hgp9zlZH5FPJnjKmnEaJi8^elS&To@#4n<+~bYEOR;$@Id0^jZ`iD4K1vz# zH}mb4CXAcC`!;`u^Qw>(>2vvp-;1@!HIujSy&~hvl1#Z~Q(8_H{h70&7*V^iW?v5< OzycCrasMgnY5ogGd!#e~ literal 0 HcmV?d00001 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 00000000..d09f50ea --- /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 00000000..b9ea6f36 --- /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 00000000..0a7aeb23 --- /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 00000000..20233b3a --- /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 00000000..3f595de6 --- /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 00000000..4d210b0e --- /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/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 0000000000000000000000000000000000000000..71307de3e52fade015b6b8e00bdf183da6ad8184 GIT binary patch literal 5590 zcmc&&Uvt|w5?6A)>&dO(u5W$7D4DT66%|UN^~X2&K#^2WSKIQ~zN9l&CIgX>gq8wT z0F15i_~Z1;^%LCV?SiBwrEYDfO~MC>1Qz?}x4YN{zW(`(U!Ih{DwRq;)ohsR;pCKx zDLYOADH93ftLJa<`qa=4=8HsX7M(8_?2wNom7J*+VX5?2{CHa0hTkKuLKeqVuq54} zL3Tj%clbS_;g58>$>Q^Dx(t7(OeLudH|L*b^GJ5~>)F5H^pny*%<8FRO7TRTGo@8c zUvgmbAW{bV3^4{f^VxC%Zli*?i0-Y1XUTTRWr+&t8=*;#3hmV?^^gBgq8W)`&~q(l~|h zR4X3Ap=qb{wlHpq>CvkFFB@-B6@UWv&uXWGWpPOV=<2BGebmw33OH87NYODKqO5?dBSn3D6F;AYcd>GpFzh>dhGy zU`Gu8tdW3*Gjf4W^{8U6ku_C{NgxFdnGXLnulh0}VIs6lVpTOGF_jQ++E|0cYEEO| zs*TI5WS+vS(tsTeL$-iWuWfG`QGI@;Q;E`!k|>RtSzuDeMA!D#_7;I>T`l}RNyjk{ z$yqukF2v$g942CVLX|$u6nS%M+}YaQ%hux>pU+7I$`&6OYo8Ds($?PnTv_IhSYzNw3Iq`mve^Tq+C>jdp)8%>ce&HTN zq7;b$;rp!Kc+srCz&-WZV?Z!$z9IG1nvWO)kRw{sP-jT=p_063Buuz`2aOl?7f^s6 z1x3ch`b1b{Tj0ObDf1K97}+Cp;mX8_|Fc29pdG6CaZo- zK<|*%$GoCb3AH>gDp}df>o?XnQvsoVu9wJ6*`2&c$jUz90?X_`Xg0;_sS;DwLa2j^ z&~fq>WPluO9k7|f(vMYcMy0^g`GCzjb5W75Zm#fqm#0T&?6?dsZc$ zKaO{!yaqhHa%EuL=Ib9oBX1s8D&HTie>NJq3h$4`>XeYj-*2Ta$2`qxRu*Ghv);cr zHax58-v@+0zI(}7uQ?lEv*vAk3;X{kr7z+4OjD`T#jlSCK1}AlUQSb_&stM=M7b!E z<99{jap#O_Oi8tV3i-^3nd|MXas?XI37@8tYB;jgxR^1?wW=JDiq9rAjo}@R6y-|Z zzSrntan5zj48>iCxJ~kL%H!yS>KW2Mzs}B1_A)9dVYP99Sx4@oZY;6e(lO| z#xMnmDVP;_`05I5Bc9EcFPe0BlgG!dGLcQ6{g;vz~e)o66K zRIi4wTFz*sbdgr>i5_c{&VwtUa+Sx&PbSlw2$Zq@7E{jTr0PP@^$g@!9HnJ*v& zWqxk}0*jbH=Y@g=fEV8I=wxshoRSmQ`_BC~cunv^py!?qJYvDg1`7Bb z@Z@72bG;fZ2*sTy>AIb2%9lA!|;7{ zus0kN?C|p%U2|J8RbGEzW}6nIg>l_#K5GSrS=>SL; 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 00000000..4d210b0e --- /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/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 0000000000000000000000000000000000000000..be7ea645103a191013eefc2ea5a43cf1811ea27e GIT binary patch literal 5220 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 D#7kr- literal 0 HcmV?d00001 diff --git a/bugs/lenb-of-array-accepted/repro.json b/bugs/lenb-of-array-accepted/repro.json new file mode 100644 index 00000000..d220b0ea --- /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 00000000..7b45b33a --- /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 00000000..a4f59ad6 --- /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 00000000..20233b3a --- /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 00000000..d0b9bdd0 --- /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 00000000..4d210b0e --- /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/set-unsupported-class-error-number/repro.json b/bugs/set-unsupported-class-error-number/repro.json new file mode 100644 index 00000000..f9ee6c3b --- /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 0000000000000000000000000000000000000000..2a37eaaeac0b960cb4a7f39e4d5b1e7e3042ff95 GIT binary patch literal 4517 zcmc&&TW{Mo6n497y!mZ=8wSD%kOYVfSyp@r&<9zTTVsh6*lxNZE(%$uZFaKgkyI`S z@^kjKU$$Sdb12D|CP7jRZS|mtoj%5A2r(famv)oUbdn%}uj7I}1l`N@C z7HMV_fX_c5V~6x;Mn)9U9TI?sC{BEG0si@;kP<$DRe%@~E(nVh6%#+8M9q94odLn< zJq;A$kzNTlovA{8hm1LuB;tysK$pl_vr2@|B2D?>LZGR7XplsgsHs}X?g>Pl+Ch9{Air5%vz#`7*^S?+0~j=wf41fuXhSo-mZyn;05yL*Z7B@rq( z$INQ8D-{d+wva#O;~269yc_Vf;==Ii6hsy@TeG>oM4mM_P?zRC0bqpo;$2VYO1XmS zkrba6n-h*coFrNIf^_NS8Y&sh2x$PU)O*}MiHNRp$a)#iDKK6^qT&fY7?u%vK~mpw z%aM`_Rq89j0#$MZ_5OgBNCA@9E(skd-J6r1y2&?+SvWrR z)eIT8Z<4_W*SEuA6VHaj?VHnNUg-LBdi!qs#u34cDd-$Zln_r;iazk8cxV=9ww!XqT?QSfUV!+>^3t~M#@6;6t1VMm@>K&(VeH=tl+ z6me6r8|_Brpl;Y!wPM(2*)bY5t7dp*v)OhmyHP#xJ}@={2eq9a0U(DK$RqD+0rLV= z_t_)pgbEsgGjMtiLEFLzZ*3J*Lrv4POuJ$l@R7dU>548uLpz9QLo}o0B(HdpMM8%tLCGR5)7asoMAP zC_1lL_wvZ=`G|)dje8<8lXgJ`CdIM;o(qs^rDkW_EYjO1I&-VfHlb-Bn9aJ?GL4q& zG!47b_KZfgQZ~xv#zCWHJ9f>gFQH-5!6-Bv)vRqU7En3;svsjR6$VV_F2FVX5J)~y9d?MsQ z>r1Uvx6!l_W;&l1jPfSx$Bzc%!E$KS?V3|ITSmQ6uNk)8w2XQazC5Q^YkBQjyWDDg zvREWTM&|mbf?NM8$do({-;Yjq%ht&bueTVQ)R^A&rW@ppQmd!g+55JeJ*#R313^Ti9Cm&c1f=+ kia1hiPD`<*uOk-lF@^C(U$Du)P_97&Ho!vuHkon$11OZlQvd(} literal 0 HcmV?d00001 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 00000000..9dd09558 --- /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 00000000..405f07c3 --- /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 00000000..20233b3a --- /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 00000000..1f4667a3 --- /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 00000000..f7ecafac --- /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 00000000..6e39eb09 --- /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 00000000..c63b513e --- /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 00000000..a5b26905 --- /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/docs/Reference/Data-Types/Date.md b/docs/Reference/Data-Types/Date.md index f7e70f69..ce15199e 100644 --- a/docs/Reference/Data-Types/Date.md +++ b/docs/Reference/Data-Types/Date.md @@ -25,6 +25,11 @@ The [**Date**](../Modules/DateTime/Date) and [**Time**](../Modules/DateTime/Time 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 diff --git a/docs/Reference/Data-Types/Double.md b/docs/Reference/Data-Types/Double.md index b4b75379..8bf87e29 100644 --- a/docs/Reference/Data-Types/Double.md +++ b/docs/Reference/Data-Types/Double.md @@ -75,7 +75,10 @@ An operation on two **Double** values gives a **Double**. A **Double** combined 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. +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 diff --git a/docs/Reference/Data-Types/LongLong.md b/docs/Reference/Data-Types/LongLong.md index ac1198ec..f20c66ef 100644 --- a/docs/Reference/Data-Types/LongLong.md +++ b/docs/Reference/Data-Types/LongLong.md @@ -77,6 +77,9 @@ 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^`. + ## 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. diff --git a/docs/Reference/Data-Types/Object.md b/docs/Reference/Data-Types/Object.md index 576a4bcf..56934977 100644 --- a/docs/Reference/Data-Types/Object.md +++ b/docs/Reference/Data-Types/Object.md @@ -51,7 +51,7 @@ An object reference is assigned with [**Set**](Set): `Set Item = New Collection` 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 in twinBASIC, the **HRESULT** `E_NOINTERFACE`. VB6 raises error 13, *Type mismatch*. +> 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" } diff --git a/docs/Reference/Default/VBA/Strings/Len.md b/docs/Reference/Default/VBA/Strings/Len.md index 4dd5913b..89cc0029 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**. From 791b1072be9d27ca4781fbe0efe140381cc6846e Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 17:03:27 +0200 Subject: [PATCH 15/17] page-baseline: 1502 pages with the data type pages --- builder/page-baseline.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/page-baseline.json b/builder/page-baseline.json index 92bf638b..a243fc34 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 1488, + "pages": 1502, "staticFiles": 1068 } From 816c1a068cc861e8350349361c22de837b4b55b5 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 17:37:36 +0200 Subject: [PATCH 16/17] BUGS-TO-REPORT: in a 32-bit build a constant Double expression raises error 6 after a constant that overflows a Long or LongLong (BETA 1010), with a test reproducer, its VB6 side and a NOTE on LongLong --- BUGS-TO-REPORT.md | 167 +++++++++++++++++- ...e-expression-after-overflow-32bit.twinproj | Bin 0 -> 6854 bytes .../repro.json | 20 +++ .../src/Settings | 75 ++++++++ .../src/Sources/RegressionTests.twin | 135 ++++++++++++++ .../src/Sources/Startup.twin | 6 + .../vb6/Module1.bas | 41 +++++ .../vb6/Probe.vbp | 7 + docs/Reference/Data-Types/LongLong.md | 3 + 9 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 bugs/const-double-expression-after-overflow-32bit/const-double-expression-after-overflow-32bit.twinproj create mode 100644 bugs/const-double-expression-after-overflow-32bit/repro.json create mode 100644 bugs/const-double-expression-after-overflow-32bit/src/Settings create mode 100644 bugs/const-double-expression-after-overflow-32bit/src/Sources/RegressionTests.twin create mode 100644 bugs/const-double-expression-after-overflow-32bit/src/Sources/Startup.twin create mode 100644 bugs/const-double-expression-after-overflow-32bit/vb6/Module1.bas create mode 100644 bugs/const-double-expression-after-overflow-32bit/vb6/Probe.vbp diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 0d61b7c1..28293c91 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -5886,7 +5886,7 @@ Error 6, *Overflow*, at each of the four, as for `CLngLng(1E+19)` and for the sa - 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. Seen on the way and not covered by the cases: in a 32-bit build `q = 2 ^ 63 - 1024` (assigned without `CLngLng`) raises error 6 although the value fits, where a 64-bit build stores 9223372036854774784; `CLngLng(2 ^ 63 - 1024)` is right in both. +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. @@ -6116,3 +6116,168 @@ The same text in a 64-bit build as in a 32-bit one and in VB6: `1.#INF`, `-1.#IN 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. + +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/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 0000000000000000000000000000000000000000..ab06e76082da97343f7ba2bc2ee1e3a7bc503463 GIT binary patch literal 6854 zcmdT}ZExc?687$0?=HC-xDV~O`#`u0Y;tEAvaR@K5B(s^a+_=3kfhlH@ivfU+F?&7 zy(G0x5aj3F|LO1Oujq_kEX(UOPByzm4Xh%O!VAem3gFfVP^o zO{B*ZRfI|j;~Hg>Mxjh*my}03lsBPdDuqvnp`^qMlt6DFTv|nDLv4tWAw52(b2rgc z;;9zk{+{4t0w(Zp^u864uOnyb)iQDRpp}oes_wt5?d|OfIR)S6GiHi#p@|6L-v~Sg zJO3eo4O3{v7gADRgI|KvatvO>%7Mp+adS2agbei|Mgks^0Jxe?DAyI@dj2KxJw?fY z0wI^|MA2C^P$_PnV}yI`N&`S|iktgl7zT79vJnA_o5f86zn8@Ef`AUm;F{gdXdNu|l9r=qVIvt(J6<5*BYWf)0tm@b$`jYpEb1Mf*MJs0*C)DmLs-4tZ!5JP|qg;_U&pqAzeNOw(VN2UT@aSdZV>tHJetm zW&KztZ=V+GBvV!0YWq$CFCqP4-Xd=KjF|Ci1%LmTFsBLS;+R6#}^^t3Cm=>*t*WqBFQ_d2nm{u;bhn9zxbT+g(8##yd(j*(W>gUEysg z)B9+CJ{ZJRXX4JaruXLS9M=X2A`W}RnRtL_vYkdY`QIX1bZg8+e_x!AL$29`u5}v7 z!Sz7SVG^q^EV27pca=f(iP1y?#+S|FCRP-l6+VaOiT0!pr|%aWy3m>2O%s{)>T#4^ z-}XJm)8ozp!QVH91(Xx2HRMjUSl9UT&(L#I+}tR^U3J99ArvLxQd(gg?%7PM(koKx z(vcSi@D4SJjS}3;e}XG~#hXq~nGPsQL`x;k6u(s{$}5PfB}bmVL=7));`$7J-}d_* ztfch&FK!@%p|t}jYusMExUr>w$uu;;-Tk)m;-<&2G^HuW%GB)&{aAcL{Qw~!dU8nI zAKbjykSbrYDb5-Nk10$L?!b7<-+f?hCtsgQDmx3+AY2Z)x{65Z1$mF4S_FkNL}FK+ z)xaq&Lz=^$4bjyb)9LKEO~*7$r`Ito*Qy$w#!kmDd(CdwGJDl-tGNUU2Ox3mqar3E zlpw-TJz#p3W0H=jq#V$L-gBVK26Ag-qm+7VnsBSGgU9fTA5Qv)O{QrzVvk`=;76lO zoH~4_t{?8X#16HXL`;pKgWf5?RY$N&&Z~a^HRD4uQ~mzQwR(N{x?%PEN)JUqYZV;~ z7RNNKRb!yKRIjZZqpl|d5$r|*h*hTAAeP9&eeWkBK_^!5(CxID@u=Cv*4wBr4^3~! z?6hiK)95-j1hd|Ajdr76HLBJ2PP=Q_R=8& 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 00000000..f17bd0bf --- /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/docs/Reference/Data-Types/LongLong.md b/docs/Reference/Data-Types/LongLong.md index f20c66ef..e088d138 100644 --- a/docs/Reference/Data-Types/LongLong.md +++ b/docs/Reference/Data-Types/LongLong.md @@ -80,6 +80,9 @@ 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. From 2a93989c810e0fbf2f37e3b29773bb764878b151 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 18 Oct 2026 17:39:38 +0200 Subject: [PATCH 17/17] BUGS-TO-REPORT: the 32-bit constant-expression entry cites #2497, likely the same cause --- BUGS-TO-REPORT.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 28293c91..8124b90d 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -6278,6 +6278,8 @@ No error at any of the five: an expression whose value fits is accepted whatever **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.