From f4448035c71219ddc087ccb3b5b02fc0a2ace9b9 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Tue, 20 Oct 2026 23:13:29 +0200 Subject: [PATCH 01/21] Help add-in: make every add-in's Ctrl and Alt shortcuts fire --- WIP.HelpAddin.md | 22 ++++++ add-in/Resources/SCRIPTS/addinkeys.js | 57 ++++++++++++++ add-in/Sources/HelpAddIn.twin | 6 ++ docs/IDE/AddIns/Help.md | 4 + .../Built-In/tbIDE/KeyboardShortcuts.md | 2 +- test/addin/keys.test.mjs | 78 ++++++++++++++++++- test/addin/lanes.mjs | 2 + 7 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 add-in/Resources/SCRIPTS/addinkeys.js diff --git a/WIP.HelpAddin.md b/WIP.HelpAddin.md index e00f4adfb..b89cffb1b 100644 --- a/WIP.HelpAddin.md +++ b/WIP.HelpAddin.md @@ -1233,6 +1233,28 @@ not: a restart saves a harness about a second against opening a new IDE. IDE. **Not done:** the DLLs themselves, built in CI --- the owner's plan is to build them there with everything else, once the compiler runs on Linux there. Why the pack is made the way it is: [WIP.Build.md](WIP.Build.md#the-help-add-ins-project-file). +11. **Every add-in's `{ctrl}` and `{alt}` shortcuts fire** (owner, 2026-10-20), tested by the + lane `keys-help` on BETA 1010: [keys.test.mjs](test/addin/keys.test.mjs) with the help + add-in loaded beside the keys probe. `BuildPane` puts `Resources/SCRIPTS/addinkeys.js` + into the page through a hidden element, as hover help does. It puts in capture-phase + `keydown` and `keyup` listeners on `window`, which run before the IDE's own: they record a + press made with Ctrl or Alt (not AltGr), and on its key-up, within 500 ms, run + `addinKeys` for it with the modifiers held at the key-down. They also delete the IDE's + `realKeyPresses` record of the key, which is from an earlier plain press, so the key-up + does not also run as the plain key (the bug's second half, and the IDE's own key-up + bindings, which it never runs for a modified key). The script goes in only while + `globalKeyDown`'s text still has the guard (see [Keyboard shortcuts](#keyboard-shortcuts)), + so a build that fixes the bug is left alone; a second run replaces the first one's + listeners. The IDE's key-down bindings still run beside an add-in's shortcut: Ctrl+- + is `tbEditor_DecreaseFontSize` as well. + + **Zoom, not done** (owner, 2026-10-20: the IDE's own Ctrl+/Ctrl- and Ctrl+Alt+/Ctrl+Alt- + font commands are enough for now). WebView2's own zoom is off in the IDE and no host call + reaches the main WebView2's zoom; CSS `zoom` on the page leaves Monaco at its old width + and puts context menus and hovers at the pointer's position times the zoom; the font + options scale text but not icons or controls. Emulated device scaling (what a WebView2 + `ZoomFactor` does) was right throughout, so whole-IDE zoom needs that host call + upstream. The probe and pictures are in `.claude/tooling-review-scratch/zoom/`. **Lookup:** diff --git a/add-in/Resources/SCRIPTS/addinkeys.js b/add-in/Resources/SCRIPTS/addinkeys.js new file mode 100644 index 000000000..299a3b94d --- /dev/null +++ b/add-in/Resources/SCRIPTS/addinkeys.js @@ -0,0 +1,57 @@ +// Makes add-in shortcuts with {ctrl} or {alt} fire, for every add-in. The +// IDE's key-down handler (globalKeyDown) records a press for its key-up +// dispatcher only when neither Ctrl nor Alt is held, so such a shortcut never +// fires, or fires as the plain key when that was pressed less than 500 ms +// before. This records those presses itself and runs their shortcuts on +// key-up as the IDE runs the others. It is put in only while the IDE has that +// guard, so a build that fixes it is left alone. Running it again replaces +// what an earlier run put in. +var old = window.tbDocsAddinKeys; +if (old) { + window.removeEventListener("keydown", old.down, true); + window.removeEventListener("keyup", old.up, true); + window.tbDocsAddinKeys = null; +} +var faulty = + typeof globalKeyDown === "function" && + typeof addinKeys === "object" && + typeof realKeyPresses === "object" && + /\(!\w+\.ctrlKey\|\|\w+\.key==="Control"\)&&\(!\w+\.altKey\|\|\w+\.key==="Alt"\)/.test(String(globalKeyDown)); +if (faulty) { + var presses = {}; + // The key's name as the IDE's key-up handler gives it. + var keyName = function (e) { + return e.code.startsWith("Key") ? e.code.substr(3) : e.key; + }; + var down = function (e) { + if (e.key === "Control" || e.key === "Alt" || !(e.ctrlKey || e.altKey)) return; + // AltGr types a character, as a plain key does. + if (e.getModifierState && e.getModifierState("AltGraph")) return; + presses[keyName(e)] = { at: performance.now(), ctrl: e.ctrlKey, shift: e.shiftKey, alt: e.altKey }; + }; + var up = function (e) { + var o = keyName(e); + var p = presses[o]; + if (!p) return; + delete presses[o]; + // The IDE's record of this key is from an earlier press without Ctrl or + // Alt; left, it would run this key-up as that key. + delete realKeyPresses[o]; + if (modalDialogs.length || isRenameWidgetActive) return; + if (performance.now() - p.at >= 500) return; + var shortcut = (p.ctrl ? "{ctrl}" : "") + (p.shift ? "{shift}" : "") + (p.alt ? "{alt}" : "") + o.toLowerCase(); + var handlers = addinKeys[shortcut]; + if (handlers) { + handlers.forEach(function (h) { + try { + h(); + } catch (err) { + console.error(err); + } + }); + } + }; + window.addEventListener("keydown", down, true); + window.addEventListener("keyup", up, true); + window.tbDocsAddinKeys = { down: down, up: up }; +} diff --git a/add-in/Sources/HelpAddIn.twin b/add-in/Sources/HelpAddIn.twin index 45e68da95..7e9c228cf 100644 --- a/add-in/Sources/HelpAddIn.twin +++ b/add-in/Sources/HelpAddIn.twin @@ -350,6 +350,12 @@ Private Class HelpAddIn Hovering.Attach mouseSink Hovering.Enabled = HoverOn End If + ' Makes every add-in's {ctrl} and {alt} shortcuts fire, which + ' the IDE's own key handling does not (addinkeys.js). + With .ChildDomElements.Add("helpKeys", "div") + .Properties.style.display = "none" + .Properties.innerHTML = PageScript(StrConv(LoadResDataInternal("addinkeys.js", "SCRIPTS"), VbStrConv.vbFromUTF8)) + End With End With End With Set ResultIds = New Collection diff --git a/docs/IDE/AddIns/Help.md b/docs/IDE/AddIns/Help.md index df2576d43..e6eb82026 100644 --- a/docs/IDE/AddIns/Help.md +++ b/docs/IDE/AddIns/Help.md @@ -109,6 +109,10 @@ The gear The window is open again the next time the IDE starts, if it was open when the IDE closed. With **Show real OS titlebar** turned on in the IDE's options, the window has an ordinary Windows title bar instead, in the IDE's dark or light colours, and **Attach** and the gear move next to the search box. +## Other add-ins' shortcuts + +While this add-in is loaded, the keyboard shortcuts that other add-ins register with Ctrl or Alt work. Without it, the IDE does not run an add-in's shortcut that includes Ctrl or Alt: see [KeyboardShortcuts](../../Packages/tbIDE/KeyboardShortcuts). The IDE's own command on the same keys still runs as well. + ## Getting the add-in The add-in is a twinBASIC project, which the IDE builds into two DLLs, one for each bitness. diff --git a/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md b/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md index 4b191a493..28e0cfee3 100644 --- a/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md +++ b/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md @@ -30,7 +30,7 @@ End Sub The shortcut fires when the key is released, wherever the focus is in the IDE's window --- in the code editor, in the DEBUG CONSOLE, or on nothing at all --- except while a dialog or the rename box is open. If the IDE has a command of its own on the same key, that command runs as well: F1, for one, also expands or collapses the signature help while it is showing. Shift+F12 has no command in the IDE's default keymap. > [!NOTE] BETA 1010 -> A shortcut that includes `{CTRL}` or `{ALT}` does not fire when the user presses it. The IDE matches an add-in's shortcut when the key is released, against a press of the same key less than half a second earlier, and it does not record a key pressed while Ctrl or Alt is held down. Such a shortcut fires only if the same key was also pressed on its own within that half second. Use a function key, alone or with `{SHIFT}`. +> A shortcut that includes `{CTRL}` or `{ALT}` does not fire when the user presses it. The IDE matches an add-in's shortcut when the key is released, against a press of the same key less than half a second earlier, and it does not record a key pressed while Ctrl or Alt is held down. Such a shortcut fires only if the same key was also pressed on its own within that half second. Use a function key, alone or with `{SHIFT}`. While the [Help Add-In](../../IDE/AddIns/Help) is loaded, shortcuts with `{CTRL}` and `{ALT}` fire as the others do, for every add-in. ## Methods diff --git a/test/addin/keys.test.mjs b/test/addin/keys.test.mjs index d205e5d71..e92bb8f26 100644 --- a/test/addin/keys.test.mjs +++ b/test/addin/keys.test.mjs @@ -8,9 +8,15 @@ // docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md and the filed report in // bugs/filed/ (twinbasic/twinbasic#2445), and then this file. // +// The lane named keys-help loads the help add-in beside the probe, and asserts +// that its addinkeys.js makes every add-in's {ctrl} and {alt} shortcuts fire +// once, on key-up, as the IDE runs the others. The cases that state the bug +// are the lane keys' alone. +// // Run it with addin-test.bat, which gives it a lane; on its own it is skipped. import assert from "node:assert/strict"; +import fs from "node:fs"; import path from "node:path"; import { before, test } from "node:test"; import { fileURLToPath } from "node:url"; @@ -23,6 +29,7 @@ import { scenario } from "./scenario.mjs"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const HOST = path.join(HERE, "host"); const PROBE = path.join(HERE, "probes", "keys"); +const ADDIN = path.join(HERE, "..", "..", "add-in"); // The key strings the probe says fired since a mark, in order. const firedSince = async (c, mark) => @@ -75,9 +82,21 @@ const paletteOpen = (c) => scenario("P1 and P2: add-in keyboard shortcuts", (lane) => { let c; + const withHelp = lane.name === "keys-help"; + // The tests that state the bug do not hold with the help add-in loaded. + const bug = { skip: withHelp && "the IDE's bug, which the help add-in repairs" }; before(async () => { await lane.addAddin(PROBE); - c = await lane.open(HOST); + if (withHelp) { + // The add-in as committed, without a help archive. F1 makes its pane, + // which is told a site nothing answers at, so no page comes from the network. + const src = path.join(lane.work, "help-src"); + fs.rmSync(src, { recursive: true, force: true }); + const skip = [path.join(ADDIN, "Build"), path.join(ADDIN, "Resources", "HELP")]; + fs.cpSync(ADDIN, src, { recursive: true, filter: (f) => !skip.includes(f) }); + await lane.addAddin(src); + } + c = await lane.open(HOST, withHelp ? { env: { TB_DOCS_HELP_SITE: "http://localhost:1" } } : undefined); }); test("the add-in loads and registers its shortcuts, lowercased", async () => { @@ -97,6 +116,12 @@ scenario("P1 and P2: add-in keyboard shortcuts", (lane) => { "{shift}d", "{shift}f1", ]); + if (withHelp) { + assert.ok( + await waitFor(c, async (c) => ((await readConsole(c)) ?? "").includes("[tbDocsHelp] loaded")), + "the help add-in never printed that it had loaded", + ); + } assert.ok(await blurAll(c), "the focus stayed on an element"); }); @@ -107,14 +132,14 @@ scenario("P1 and P2: add-in keyboard shortcuts", (lane) => { assert.deepEqual(await pressAlone(c, "F1", { shift: true }), ["{shift}f1"]); }); - test("P1: Ctrl and Alt keys do not fire", async () => { + test("P1: Ctrl and Alt keys do not fire", bug, async () => { assert.deepEqual(await pressAlone(c, "d", { ctrl: true, shift: true }), []); assert.deepEqual(await pressAlone(c, "d", { ctrl: true }), []); assert.deepEqual(await pressAlone(c, "f", { alt: true }), []); assert.equal(await menuOpen(c), false, "Alt+F opened a menu"); }); - test("P1: a Ctrl or Alt key fires when the same key was pressed alone just before", async () => { + test("P1: a Ctrl or Alt key fires when the same key was pressed alone just before", bug, async () => { assert.deepEqual( await pressThenQ(c, [ ["d"], @@ -127,6 +152,53 @@ scenario("P1 and P2: add-in keyboard shortcuts", (lane) => { ); }); + test("help add-in: its script is in, and Ctrl and Alt shortcuts fire once each", { + skip: !withHelp && "keys-help only", + }, async () => { + assert.equal(await c.evaluate("!!window.tbDocsAddinKeys"), true, "window.tbDocsAddinKeys is not set"); + assert.deepEqual(await pressAlone(c, "d", { ctrl: true, shift: true }), ["{ctrl}{shift}d"]); + assert.deepEqual(await pressAlone(c, "d", { ctrl: true }), ["{ctrl}d"]); + assert.deepEqual(await pressAlone(c, "f", { alt: true }), ["{alt}f"]); + assert.equal(await menuOpen(c), false, "Alt+F opened a menu"); + }); + + test("help add-in: the plain, Shift and function keys fire once each, as before", { + skip: !withHelp && "keys-help only", + }, async () => { + assert.deepEqual(await pressAlone(c, "d"), ["d"]); + assert.deepEqual(await pressAlone(c, "D"), ["{shift}d"]); + assert.deepEqual(await pressAlone(c, "F1"), ["f1"]); + assert.deepEqual(await pressAlone(c, "F1", { shift: true }), ["{shift}f1"]); + }); + + test("help add-in: d then Ctrl+D within 500 ms fires d once, then Ctrl+D once", { + skip: !withHelp && "keys-help only", + }, async () => { + assert.deepEqual(await pressThenQ(c, [["d"], ["d", { ctrl: true }]]), ["d", "{ctrl}d"]); + assert.deepEqual(await pressThenQ(c, [["f"], ["f", { alt: true }]]), ["{alt}f"]); + }); + + test("help add-in: a Ctrl+D held longer than 500 ms fires nothing", { + skip: !withHelp && "keys-help only", + }, async () => { + await sleep(600); + const mark = await consoleMark(c); + await c.evaluate( + `window.dispatchEvent(new KeyboardEvent("keydown", { key: "d", code: "KeyD", ctrlKey: true, bubbles: true }))`, + ); + await sleep(700); + await c.evaluate( + `window.dispatchEvent(new KeyboardEvent("keyup", { key: "d", code: "KeyD", ctrlKey: true, bubbles: true }))`, + ); + await pressKey(c, "q"); + assert.ok(await waitFired(c, mark, "q")); + await sleep(300); + assert.deepEqual( + (await firedSince(c, mark)).filter((k) => k !== "q"), + [], + ); + }); + test("P2: in the code editor F1 fires and types nothing, and a plain letter fires and types", async () => { await openFile(c, "/AddinHost/Sources/Main.twin", { line: 3, column: 9 }); await setCursor(c, 3, 9); diff --git a/test/addin/lanes.mjs b/test/addin/lanes.mjs index 658433698..d3297286e 100644 --- a/test/addin/lanes.mjs +++ b/test/addin/lanes.mjs @@ -31,6 +31,8 @@ export default [ // references (the hosts it opens are in probes/references-*); neither saves // anything. { file: "keys.test.mjs" }, + // The same file with the help add-in loaded, which makes {ctrl} and {alt} shortcuts fire. + { file: "keys.test.mjs", name: "keys-help", settings: ["tbDocsHelp"] }, { file: "panes.test.mjs" }, { file: "symbols.test.mjs" }, { file: "ideserver.test.mjs" }, From 92a749b2b6c80953490df03fa77631059c848507 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Tue, 20 Oct 2026 23:46:13 +0200 Subject: [PATCH 02/21] Help add-in 1.0.0: a version, CHANGELOG.md, and the help window follows the IDE's font size --- WIP.HelpAddin.md | 12 +++++ add-in/CHANGELOG.md | 67 ++++++++++++++++++++++++++++ add-in/Resources/SCRIPTS/fontsize.js | 26 +++++++++++ add-in/Resources/SHELL/shell.css | 4 +- add-in/Settings | 5 +++ add-in/Sources/HelpAddIn.twin | 24 +++++++++- docs/IDE/AddIns/Help.md | 2 + test/addin/help.test.mjs | 8 ++++ 8 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 add-in/CHANGELOG.md create mode 100644 add-in/Resources/SCRIPTS/fontsize.js diff --git a/WIP.HelpAddin.md b/WIP.HelpAddin.md index b89cffb1b..8b7dc465b 100644 --- a/WIP.HelpAddin.md +++ b/WIP.HelpAddin.md @@ -1198,6 +1198,12 @@ not: a restart saves a harness about a second against opening a new IDE. caption (no read-back of the DWM attribute), Alt+F4, the registry read, maximizing on a monitor other than the primary one (the lab machine has one), and the move itself, whose loop needs the real mouse, which the harness's desktop has none of. + **The window's text is in the IDE's font size** (owner, 2026-10-20): `fontsize.js`, + put into the page by `BuildPane`, sends `--ide-fontsize` to the hidden `helpFont` + element now and whenever a `MutationObserver` sees the root's style change, and the + add-in puts `:root{--ideFontSize:...}` before the theme's rule in the `theme:` message, + which `shell.css` sets the body in. The help lane checks the window's size against the + IDE page's. 8. **Settings**, tested by the `help` and `help-offline` lanes on BETA 1007 (owner's choice): a gear at the right end of the pane's bar, and on the detached window's title bar (in its bar under the Windows caption), opens a drop-down panel of @@ -1347,3 +1353,9 @@ The rules for testing add-ins bind every session and are in add-in in the real install's `addins\` or in `%APPDATA%\twinBASIC\addins\`, no real browser from a test, every `SaveSetting` application named in `lanes.mjs`, IDEs ended by pid, and one project per IDE. + +**A change a user of the add-in would notice gets an entry in `add-in/CHANGELOG.md` and a +new version** (owner, 2026-10-20), in the same commit: `project.versionMajor`, `Minor`, +`Build` and `Revision` in `add-in/Settings`, with `versionAutoIncrement` left `None`, so +that a lane's or a reader's build changes nothing. The changelog lists the add-in's own +behaviour, not the documentation or the symbol index it carries. diff --git a/add-in/CHANGELOG.md b/add-in/CHANGELOG.md new file mode 100644 index 000000000..c1983b62e --- /dev/null +++ b/add-in/CHANGELOG.md @@ -0,0 +1,67 @@ +# Changelog + +What changed in the twinBASIC Documentation Help add-in, newest first. The version is the +DLL's (its file properties show it). The documentation the add-in shows and its index of +symbols change with every build of the documentation and are not listed here. + +## 1.0.0 (2026-10-20) + +- Other add-ins' keyboard shortcuts that include Ctrl or Alt now work while this add-in is + loaded. The IDE on its own does not run them. +- The help window's text follows the IDE's font size (**Tools > IDE Options**, *IDE Font + Size*), as the IDE's own panels do, also when the size is changed while the window is open. + +## Before version numbers + +### 2026-10-18 + +- F1 on a data type in a declaration, such as `As String`, opens the data type; on + `String(` it opens the **String** function. + +### 2026-10-16 + +- The help pane, the first time it floats, fits a small IDE window: at most 480 by 640 + pixels, and never more than 85% of the IDE's width or height. + +### 2026-10-15 + +- Offline help: a page or file that the add-in's own copy of the documentation leaves out + (videos, downloads, the documentation's development pages) opens from the website. + +### 2026-10-09 + +- A line between the search results and the page, in the pane and in the help window. + +### 2026-10-07 + +- **Detach**: the help pane moves into a window of its own, which can go to another monitor. + **Attach** brings it back; closing the window hides it until the next F1. With the IDE's + **Show real OS titlebar** option, the window has an ordinary Windows title bar. +- A settings panel, opened with the gear: **Hover help** and **All packages**. +- The add-in offers only the pages of the packages the project references, unless **All + packages** is ticked. +- Hover help shows the links to a name's pages as the first line of the IDE's own mouse + hover, such as *Help: MsgBox function (VBA.Interaction)*. + +### 2026-10-06 + +- **Hover help**: links to the pages of the name under the mouse, in the IDE's hover. + Off by default. +- The search results list is as tall as its rows, up to about a third of the pane, and + clearing the search box hides it. + +### 2026-10-05 + +- Offline help: when the add-in is built with the documentation's archive, it shows the + pages from that copy, with no internet connection. + +### 2026-10-04 + +- First release. F1 opens the documentation page for the name under the cursor, in the + **TWINBASIC HELP** tool window, with a search box over the documentation's index. +- F1 asks the compiler where a name is declared, so it tells apart names spelled the same + (`c.Add` on a **Collection** opens **Collection.Add**). +- F1 on a name with no page, such as your own procedure, shows its declaration and its + `[Description]`. +- The pages follow the IDE's dark or light theme, and show without the website's + navigation. diff --git a/add-in/Resources/SCRIPTS/fontsize.js b/add-in/Resources/SCRIPTS/fontsize.js new file mode 100644 index 000000000..1fae05920 --- /dev/null +++ b/add-in/Resources/SCRIPTS/fontsize.js @@ -0,0 +1,26 @@ +// Tells the add-in the IDE's font size, now and whenever the IDE changes it, +// so that the detached window can be set in the same size. The IDE keeps it +// in --ide-fontsize on the page's root, and sets its text from that. Running +// this again replaces the observer an earlier run put in. +var sink = this.parentNode; +var old = window.tbDocsFontSize; +if (old) { + old.observer.disconnect(); + window.tbDocsFontSize = null; +} +var last = null; +var send = function () { + var size = getComputedStyle(document.documentElement).getPropertyValue("--ide-fontsize").trim(); + if (!size) size = getComputedStyle(document.body).fontSize; + if (size === last) return; + last = size; + try { + sink.tbDocsFontSize({ value: size }); + } catch (err) { + console.error(err); + } +}; +var observer = new MutationObserver(send); +observer.observe(document.documentElement, { attributes: true, attributeFilter: ["style"] }); +window.tbDocsFontSize = { observer: observer }; +send(); diff --git a/add-in/Resources/SHELL/shell.css b/add-in/Resources/SHELL/shell.css index ef183f022..ae4d94846 100644 --- a/add-in/Resources/SHELL/shell.css +++ b/add-in/Resources/SHELL/shell.css @@ -37,7 +37,7 @@ body { body { overflow: hidden; - font: 13px "Segoe UI", system-ui, sans-serif; + font: var(--ideFontSize, 13px) "Segoe UI", system-ui, sans-serif; } #helpResults { @@ -142,7 +142,7 @@ html[data-chrome="native"] #helpTitleBar { #helpTitleBar #helpSettingsPanel { color: var(--themeToolWindowBodyForeColor); - font: 13px "Segoe UI", system-ui, sans-serif; + font: var(--ideFontSize, 13px) "Segoe UI", system-ui, sans-serif; } /* The Maximize button shows Restore while the window is maximized. */ diff --git a/add-in/Settings b/add-in/Settings index 934e873a4..efd634fc2 100644 --- a/add-in/Settings +++ b/add-in/Settings @@ -65,6 +65,11 @@ } ], "project.settingsVersion": 1, + "project.versionAutoIncrement": "None", + "project.versionBuild": 0, + "project.versionMajor": 1, + "project.versionMinor": 0, + "project.versionRevision": 0, "project.warnings": { "errors": [], "ignored": [] diff --git a/add-in/Sources/HelpAddIn.twin b/add-in/Sources/HelpAddIn.twin index 7e9c228cf..b1f73115b 100644 --- a/add-in/Sources/HelpAddIn.twin +++ b/add-in/Sources/HelpAddIn.twin @@ -42,6 +42,7 @@ Private Class HelpAddIn Private LastSearch As String Private PageUrl As String ' the site path the frame was given Private HoverSink As HtmlElement ' where the compiler's answers come + Private IdeFontSize As String ' the IDE's font size, "" until fontsize.js tells it Private AskSeq As Long ' the number of the last question Private Asked As String ' its word, "" once answered Private Server As HelpServer ' Nothing unless the add-in has the archive @@ -266,7 +267,9 @@ Private Class HelpAddIn ' What the window is told of the theme: the group, for the plain palette it ' falls back on, a line break, and the rule with the theme's properties. Private Function ThemeMessage() As String - Return Host.Themes.ActiveThemeNameGroup & vbLf & ThemeRule(Host.Themes.ActiveThemeName) + Dim rule As String = ThemeRule(Host.Themes.ActiveThemeName) + If Len(IdeFontSize) > 0 Then rule = ":root{--ideFontSize:" & IdeFontSize & "}" & rule + Return Host.Themes.ActiveThemeNameGroup & vbLf & rule End Function ' The pane's elements. Showing a tool window sets its root's display to @@ -350,6 +353,12 @@ Private Class HelpAddIn Hovering.Attach mouseSink Hovering.Enabled = HoverOn End If + ' The IDE's font size, for the window (fontsize.js). + With .ChildDomElements.Add("helpFont", "div") + .Properties.style.display = "none" + .AddEventListener("tbDocsFontSize", AddressOf FontSizeChanged) + .Properties.innerHTML = PageScript(StrConv(LoadResDataInternal("fontsize.js", "SCRIPTS"), VbStrConv.vbFromUTF8)) + End With ' Makes every add-in's {ctrl} and {alt} shortcuts fire, which ' the IDE's own key handling does not (addinkeys.js). With .ChildDomElements.Add("helpKeys", "div") @@ -522,6 +531,19 @@ Private Class HelpAddIn Host.DebugConsole.PrintText TAG & "error " & Err.Number & ": " & Err.Description End Sub + ' The window is set in the IDE's font size, as it is when it opens and when + ' the size is changed while the window is open. + Private Sub FontSizeChanged(ByVal eventInfo As HtmlEventProperties) + On Error GoTo Failed + Dim size As String = eventInfo.value + If size = IdeFontSize Then Exit Sub + IdeFontSize = size + If Detached Then PostToWindow "theme:" & ThemeMessage() + Exit Sub + Failed: + Host.DebugConsole.PrintText TAG & "error " & Err.Number & ": " & Err.Description + End Sub + Private Sub HoverAnswered(ByVal eventInfo As HtmlEventProperties) On Error GoTo Failed If CLng(eventInfo.seq) <> AskSeq Or Len(Asked) = 0 Then Exit Sub diff --git a/docs/IDE/AddIns/Help.md b/docs/IDE/AddIns/Help.md index e6eb82026..79af59e41 100644 --- a/docs/IDE/AddIns/Help.md +++ b/docs/IDE/AddIns/Help.md @@ -123,6 +123,8 @@ The add-in is a twinBASIC project, which the IDE builds into two DLLs, one for e An add-in built this way serves the pages itself, from the copy of the documentation in its DLL, so the pane needs no internet connection. **Open in browser** still opens the page on the website, [docs.twinbasic.com](https://docs.twinbasic.com). +The add-in's version is in the DLL's file properties. `CHANGELOG.md`, in the project and in the source folder, lists what changed in each version. + ### From the source code The add-in's source code is in the `add-in` folder of the [documentation's repository](https://github.com/twinbasic/documentation). Build from it to change the add-in. The folder holds the project's files, not a project file, so pack it into one first with the [Import/Export Tool](../../../Features/Packages/Import-Export-Tool), from the repository's root folder: diff --git a/test/addin/help.test.mjs b/test/addin/help.test.mjs index 125ff261d..eacdbd4dc 100644 --- a/test/addin/help.test.mjs +++ b/test/addin/help.test.mjs @@ -1372,6 +1372,14 @@ scenario("the help add-in: F1 and the help pane", (lane) => { ); }); + test("the window is set in the IDE's font size", async () => { + const ide = await c.evaluate( + `getComputedStyle(document.documentElement).getPropertyValue("--ide-fontsize").trim()`, + ); + assert.ok(ide, "the IDE has no --ide-fontsize"); + assert.equal(await win.evaluate(`getComputedStyle(document.body).fontSize`), ide); + }); + test("the window's title bar is one row, with the title and five buttons, and Windows draws no caption", async () => { const bar = await win.evaluate(`(() => { const el = (id) => document.getElementById(id); From cf6325eb7ea47fc8d285a02491bdd310fe3766ca Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 21 Oct 2026 00:13:55 +0200 Subject: [PATCH 03/21] Every IDE the tools start runs with default settings, answered in the page (tb-defaults.mjs); try-help-addin keeps the user's --- WIP.Harness.md | 28 ++++++ WIP.md | 2 +- scripts/lib/tb-cdp.mjs | 22 ++++- scripts/lib/tb-defaults.mjs | 178 ++++++++++++++++++++++++++++++++++++ scripts/lib/tb-ide.mjs | 36 +++++++- scripts/lib/tb-lane.mjs | 18 +++- scripts/try_help_addin.mjs | 10 +- 7 files changed, 280 insertions(+), 14 deletions(-) create mode 100644 scripts/lib/tb-defaults.mjs diff --git a/WIP.Harness.md b/WIP.Harness.md index f14829655..3f12fdcc6 100644 --- a/WIP.Harness.md +++ b/WIP.Harness.md @@ -1182,6 +1182,34 @@ options and the remembered targets exactly as they are. It writes only its scrat a gate: it needs Windows and a real registry, and the CI runners are Ubuntu. Run it after changing `tb-registry.mjs`. +## Default settings, whatever the user saved + +`IDE_FLAGS` stop an IDE writing its settings, and nothing stops it reading them: the IDE reads its +options, panel layouts, keyboard groups and a few more keys through `hostAppObject.GetIDESetting(key)` +from the user's registry key, so a font size the owner set in their own IDE showed in every IDE the +tools started (a help-lane check failed on 14 px against 13 px). `scripts/lib/tb-defaults.mjs` makes +every IDE `launchIde` starts run with the defaults, in the page only and with no write to the +registry; `try-help-addin` passes `userSettings: true` and keeps the user's. + +The script has to run before `main.js` reads the keys, and `main.htm` commits 0.2 to 0.8 s after the +IDE's browser process writes `DevToolsActivePort`. So `installDefaultsHook` starts before the IDE does, +polls that file every 5 ms (`launchIde`'s own poll is at 100 ms and answers too late), connects to the +browser target, auto-attaches to every target with `waitForDebuggerOnStart`, adds the script to each page +with `Page.addScriptToEvaluateOnNewDocument` and resumes it. The page is `about:blank` when attached, and +never navigates twice. The script puts a setter on `window.hostAppObject`, which `main2.js` assigns, and +wraps the host object: `GetIDESetting` answers `""` for the blanked keys and `"{}"` for `GENERAL` and +`GENERAL2`, every other call goes through with `this` bound to the real object. The keys it left +unanswered are the licence's, `LICENCEAGREEMENTV3`, `ALLOWERRORREPORTINGV3`, `PUBLISHER` and `NEWYEAR_*`. +The recent lists are left alone: `test/ide/recent-projects-copies.test.mjs` needs the IDE's own, and +`shoot_docs.mjs`'s `PAGE_DEFAULTS` empties them where a picture shows them. + +A probe of 50 launches hooked 49. The one miss was a page whose document began before the script was +added. `attachIde` therefore checks `window.__tbDefaults`, the marker the script sets (it lists the keys +answered, never their values), and reloads the page once when it is missing: the hook's session still holds +the script, the reload takes about 40 ms, and the UI and the command-line project come back. A marker still +missing after that is a harness error, exit 2, since the run would have the user's settings. The hook's +connection lives as long as the process, so a `tbbuild --keep` IDE is hooked until `tbbuild` ends. + ## A private IDE for every lane **The compiler loads every DLL in `\addins\win32` or `\win64` as it starts.** A diff --git a/WIP.md b/WIP.md index ff49f7c8b..1e0347fba 100644 --- a/WIP.md +++ b/WIP.md @@ -131,7 +131,7 @@ node scripts/tbbuild.mjs C:/probe/Thing.twinproj # does it compile node scripts/tbrun.mjs # what does it print ``` -- **Every IDE the tools start is given `--no-save-ide-settings` and `--no-update-recent-files`, and BETA 1007 is the oldest build any tool runs** (owner: "we never run anything before that"). The two flags are `IDE_FLAGS` in `scripts/lib/tb-ide.mjs`, passed by `launchIde` on both its branches; any new code that starts `twinBASIC.exe` passes them too. They keep the IDE from writing the user's options, panel layouts, recent list, remembered build targets and licence answers to the registry; what they leave is `ProjectState`, the `.twinproj` association, the `Window` key and `RemoveRecentsList`, which `scripts/lib/tb-registry.mjs` puts back or a test blocks in its page. **Never pass `--reset-ide-settings`** (it writes empty options to the real registry even after `--no-save-ide-settings`); an unknown `--` flag is ignored without a word; a second positional argument opens a modal box nobody can answer. An older install is passed over by `findIde`, and an older one named by `--ide` or `TB_IDE` is refused with exit 2 before an IDE starts (`MIN_BUILD` and `unsupportedIde` in `scripts/lib/tb-install.mjs`): there is no fallback and no switch to allow it, and the control for a probe is BETA 1007 itself. The reasons and the registry's details are in [WIP.Harness.md](WIP.Harness.md#what-a-run-leaves-in-the-registry-and-putting-it-back). +- **Every IDE the tools start is given `--no-save-ide-settings` and `--no-update-recent-files`, and BETA 1007 is the oldest build any tool runs** (owner: "we never run anything before that"). The two flags are `IDE_FLAGS` in `scripts/lib/tb-ide.mjs`, passed by `launchIde` on both its branches; any new code that starts `twinBASIC.exe` passes them too. They keep the IDE from writing the user's options, panel layouts, recent list, remembered build targets and licence answers to the registry; what they leave is `ProjectState`, the `.twinproj` association, the `Window` key and `RemoveRecentsList`, which `scripts/lib/tb-registry.mjs` puts back or a test blocks in its page. **Never pass `--reset-ide-settings`** (it writes empty options to the real registry even after `--no-save-ide-settings`); an unknown `--` flag is ignored without a word; a second positional argument opens a modal box nobody can answer. An older install is passed over by `findIde`, and an older one named by `--ide` or `TB_IDE` is refused with exit 2 before an IDE starts (`MIN_BUILD` and `unsupportedIde` in `scripts/lib/tb-install.mjs`): there is no fallback and no switch to allow it, and the control for a probe is BETA 1007 itself. **The flags stop the IDE writing its settings, and a hook stops it reading them**: `launchIde` answers every IDE's `GetIDESetting` with "no saved value" for its options, panel layouts and keyboard groups, in the page only (`scripts/lib/tb-defaults.mjs`; the answer for `GENERAL` and `GENERAL2` must be `"{}"`, never `""`, which the IDE reads as a first run and opens its Options dialog), so every tool's IDE runs with the defaults; `attachIde` fails the run when the page did not run the hook, and `userSettings: true` is the opt-out, passed by `try-help-addin` only. The reasons and the registry's details are in [WIP.Harness.md](WIP.Harness.md#what-a-run-leaves-in-the-registry-and-putting-it-back). - **Pack and unpack projects with `impexp`, never with the compiler executable's `import` or `export` verbs.** The tooling does both in process, through `packTree` and `unpackProject` in `scripts/lib/tb-project.mjs`; by hand it is `node scripts/impexp.mjs`. The compiler's `import` refuses a project that embeds a package (exit 999, twinbasic/twinbasic#841), and both verbs exit 0 after the failures they report. The reasons, and the path and stdin rules for anything that does run the executable (`bug_repro`'s `cli` mode), are in [WIP.Harness.md](WIP.Harness.md#getting-at-the-twin-sources). - **`tbbuild` exit codes:** 0 clean, 1 the project has errors, 2 the harness failed or the project file cannot be read (empty, cut short, not a project: it is read before an IDE starts), 3 the compile never settled or met its limit, 4 the project crashes the compiler, 5 the build failed after a clean compile (an LLVM refusal in its log included, an exe linked or not, and the build's limit). `--json` returns one object, `--keep` leaves the IDE running. **`--build` and `--llvm` (which implies it) build the project after a clean compile**, because LLVM code generation runs in a build and a compile never asks for it. They first stage a copy with an explicit build path (`%TEMP%\tbbuild\\`), so the given `.twinproj` is untouched. A plain `--build` is the control for an `--llvm` run. `--llvm` refuses a Community or Personal licence. - **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. diff --git a/scripts/lib/tb-cdp.mjs b/scripts/lib/tb-cdp.mjs index e7891b851..036e53c46 100644 --- a/scripts/lib/tb-cdp.mjs +++ b/scripts/lib/tb-cdp.mjs @@ -32,10 +32,23 @@ export async function attach(port, match = "main.htm", { timeout = 30 * 1000, ty ).json(); const t = list.find((x) => x.type === type && x.url.includes(match)); if (!t) throw new Error(`no ${type} target matching ${JSON.stringify(match)} on port ${port}`); + return { url: t.url, ...(await connect(t.webSocketDebuggerUrl, { timeout, what: `port ${port}` })) }; +} - const ws = new WebSocket(t.webSocketDebuggerUrl); +/** + * Connect to a DevTools websocket: a page's, or the browser's own (the path on the + * second line of `DevToolsActivePort`), where `send` takes a `sessionId` to talk to a + * target the connection attached to. The time limits are those of `attach`. + * + * @param {string} wsUrl the `ws://` address + * @param {object} [o] + * @param {number} [o.timeout] milliseconds a call may take (default 30000) + * @param {string} [o.what] what the socket is for, in the message when it does not open + */ +export async function connect(wsUrl, { timeout = 30 * 1000, what = wsUrl } = {}) { + const ws = new WebSocket(wsUrl); await new Promise((res, rej) => { - const timer = setTimeout(() => rej(new Error(`the DevTools socket on port ${port} did not open`)), 10 * 1000); + const timer = setTimeout(() => rej(new Error(`the DevTools socket on ${what} did not open`)), 10 * 1000); ws.onopen = () => { clearTimeout(timer); res(); @@ -73,7 +86,7 @@ export async function attach(port, match = "main.htm", { timeout = 30 * 1000, ty pending.clear(); }; - const send = (method, params = {}, { timeout: ms = timeout } = {}) => + const send = (method, params = {}, { timeout: ms = timeout, sessionId } = {}) => new Promise((res, rej) => { // A WebSocket that is closing or closed discards what it is sent and does // not throw, so the call would wait out its whole timeout. @@ -103,7 +116,7 @@ export async function attach(port, match = "main.htm", { timeout = 30 * 1000, ty }, }); try { - ws.send(JSON.stringify({ id: i, method, params })); + ws.send(JSON.stringify({ id: i, method, params, ...(sessionId ? { sessionId } : {}) })); } catch { pending.delete(i); clearTimeout(timer); @@ -120,7 +133,6 @@ export async function attach(port, match = "main.htm", { timeout = 30 * 1000, ty }; return { - url: t.url, send, evaluate, on: (fn) => listeners.push(fn), diff --git a/scripts/lib/tb-defaults.mjs b/scripts/lib/tb-defaults.mjs new file mode 100644 index 000000000..108199c19 --- /dev/null +++ b/scripts/lib/tb-defaults.mjs @@ -0,0 +1,178 @@ +// The IDE with its default settings, whatever the user's own IDE saved. +// +// The IDE reads its per-user settings (options, panel layouts, keyboard groups, ...) +// through `hostAppObject.GetIDESetting(key)`. IDE_FLAGS (tb-ide.mjs) stop it writing them, +// and nothing stops it reading them: a font size the owner changed in their own IDE showed +// in every IDE the tools started. So each IDE a tool starts is answered "no saved value" for +// those keys, in the page only. The registry is never written. try-help-addin, which is the +// user's own IDE in all but its project, keeps its settings (`userSettings` of launchIde). +// WIP.Harness.md, "Default settings, whatever the user saved", says why it is built this way. +// +// A script has to run before main.js reads the settings, and main.htm is not loaded when +// the IDE has started: the IDE's browser process writes DevToolsActivePort, main.htm +// commits some 0.2 to 0.8 s after. So installDefaultsHook, started before the IDE is, waits +// for the file, connects to the *browser* target and auto-attaches to every target the +// browser creates, paused (waitForDebuggerOnStart); each page gets a script that runs at its +// document's start (Page.addScriptToEvaluateOnNewDocument), and is resumed. The script puts +// a setter on window.hostAppObject, which main2.js assigns, to wrap the host object. The +// connection stays open for the IDE's life: the script is the session's, and a page that +// reloads is hooked again only while it is. + +import { readFileSync, statSync } from "node:fs"; +import path from "node:path"; +import { connect } from "./tb-cdp.mjs"; + +// The keys answered with no saved value, and the answer each takes. GENERAL and GENERAL2 are +// parsed as the options' objects, and "" in their place is the first run's mark: the IDE then +// opens its Options dialog and goes no further. The others take "" as a fresh install's +// answer: JSON.parse fails inside a try and the variable keeps its default, parseJson gives +// undefined, and an empty targetArchitectureMemory reads as "unknown". Not here, and +// answered from the registry: the licence's keys, LICENCEAGREEMENTV3, ALLOWERRORREPORTINGV3, +// PUBLISHER and NEWYEAR_*, which are not settings of how the IDE looks. +export const BLANKED_KEYS = { + GENERAL: "{}", + GENERAL2: "{}", + LAYOUT: "", + LAYOUTv2: "", + CUSTOM_PANEL_LAYOUTS: "", + ACTIVE_PANEL_LAYOUT_NAME: "", + CUSTOM_KEYBOARD_SHORTCUT_GROUPS: "", + ACTIVE_KEYBOARD_SHORTCUT_GROUPS: "", + WEBPANEL_SETTINGS: "", + FORMS_windowsPalette: "", + targetArchitectureMemory: "", +}; + +// Runs in the page, in every document of the IDE's browser; only main.htm does anything. +// `window.__tbDefaults` is the marker that it ran, and its `keys` the keys it answered +// (never their values). Every function of the host object is bound to the real object: the +// proxy of a WebView2 host object refuses to be called with another `this`. +export function pageHook(answers) { + if (!/main\.htm$/i.test(location.pathname)) return; + const marker = { keys: [] }; + window.__tbDefaults = marker; + let real; + const wrap = (host) => + new Proxy(host, { + get(target, name) { + const member = target[name]; + if (typeof member !== "function") return member; + if (name !== "GetIDESetting") return (...a) => member.apply(target, a); + return (...a) => { + const key = String(a[0]); + if (!Object.hasOwn(answers, key)) return member.apply(target, a); + marker.keys.push(key); + return Promise.resolve(answers[key]); + }; + }, + }); + Object.defineProperty(window, "hostAppObject", { + configurable: true, + enumerable: true, + get: () => real, + set: (v) => { + real = v ? wrap(v) : v; + }, + }); +} + +export const hookSource = () => `(${pageHook})(${JSON.stringify(BLANKED_KEYS)})`; + +const wait = (ms) => new Promise((r) => setTimeout(r, ms)); + +/** + * Start hooking the IDE whose WebView2 profile is `profile`, from before it has started. + * The file is polled every 5 ms: launchIde's own poll is at 100 ms and answers too late. + * + * @param {object} o + * @param {string} o.profile the IDE's WEBVIEW2_USER_DATA_FOLDER + * @param {number} o.since when it was started; a DevToolsActivePort older than 2 s before is left over + * @returns {{ error: Error | null, closed: Promise, stop: () => void }} + * `error` is why no hook was installed (set once known); `stop` ends the polling and closes the + * connection, and is called when the IDE ends or fails to start. A hook that was installed leaves + * the page's `window.__tbDefaults`, which attachIde checks. + */ +export function installDefaultsHook({ profile, since }) { + const hook = { error: null, closed: null, stop: () => {} }; + let stopped = false; + let client = null; + hook.stop = () => { + stopped = true; + client?.close(); + }; + hook.closed = (async () => { + const file = path.join(profile, "EBWebView", "DevToolsActivePort"); + const until = Date.now() + 60 * 1000; + let port; + let wsPath; + while (!stopped) { + try { + if (statSync(file).mtimeMs >= since - 2000) { + const [a, b] = readFileSync(file, "utf8").split(/\r?\n/); + if (Number(a) > 0 && b) { + port = Number(a); + wsPath = b; + break; + } + } + } catch { + // not written yet + } + if (Date.now() > until) throw new Error("the IDE wrote no DevToolsActivePort in a minute"); + await wait(5); + } + if (stopped) return; + client = await connect(`ws://127.0.0.1:${port}${wsPath}`, { what: `the IDE's browser on port ${port}` }); + const source = hookSource(); + const prepare = async ({ sessionId, targetInfo }) => { + try { + if (targetInfo.type === "page") { + await client.send("Page.enable", {}, { sessionId }); + await client.send("Page.addScriptToEvaluateOnNewDocument", { source }, { sessionId }); + } + } finally { + // A target left paused never loads; one that is gone answers with an error. + await client.send("Runtime.runIfWaitingForDebugger", {}, { sessionId }).catch(() => {}); + } + }; + client.on((m) => { + if (m.method === "Target.attachedToTarget") prepare(m.params).catch(() => {}); + }); + await client.send("Target.setDiscoverTargets", { discover: true }); + await client.send("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }); + // A page that existed before the auto-attach is hooked late, when the document is still + // to commit; if it has, the reload attachIde does picks the script up. + const { targetInfos } = await client.send("Target.getTargets"); + for (const t of targetInfos.filter((x) => x.type === "page" && !x.attached)) { + const { sessionId } = await client.send("Target.attachToTarget", { targetId: t.targetId, flatten: true }); + await prepare({ sessionId, targetInfo: t }); + } + })().catch((e) => { + if (!stopped) hook.error = e; + }); + return hook; +} + +/** + * Whether the main page ran the hook, and if it did not, one reload, which the hook's session + * has the script waiting for: the page was hooked after its document had begun (about 1 launch + * in 50 on the dev box). Throws when it still did not, since the IDE would have the user's + * settings. Called by attachIde on a connection to the page. + * + * @param {{ evaluate: Function, send: Function }} c the connection + * @param {{ error: Error | null }} hook + */ +export async function checkDefaults(c, hook) { + const ran = () => c.evaluate("!!window.__tbDefaults").catch(() => false); + if (await ran()) return; + if (hook.error) throw new Error(`the IDE's settings were not made the defaults: ${hook.error.message}`); + await c.send("Page.reload", {}); + for (let i = 0; i < 400; i++) { + await wait(50); + if (await ran()) return; + } + throw new Error( + "the IDE's settings were not made the defaults: its page ran no hook, even after a reload " + + "(it would have the user's settings; userSettings: true is the opt-out)", + ); +} diff --git a/scripts/lib/tb-ide.mjs b/scripts/lib/tb-ide.mjs index ded95dd91..1e8319dac 100644 --- a/scripts/lib/tb-ide.mjs +++ b/scripts/lib/tb-ide.mjs @@ -17,6 +17,7 @@ import { secondsText, timeoutMs } from "./harness-settings.mjs"; import { connectBroker } from "./tb-broker.mjs"; import { attach } from "./tb-cdp.mjs"; import { click, pagePicture } from "./tb-click.mjs"; +import { checkDefaults, installDefaultsHook } from "./tb-defaults.mjs"; import { removeTree, removeTreeAsync } from "./tb-ide-copy.mjs"; import { consoleMark, keepClears, keptClears, linesSince, readConsole } from "./tb-ide-console.mjs"; import { unsupportedIde } from "./tb-install.mjs"; @@ -245,7 +246,12 @@ export async function leaseSlot({ tag = "", port = false } = {}) { * Only a test of the very thing a flag guards leaves * one out, and it puts back what it writes: the recent * list's test (test/ide/recent-projects-copies.test.mjs) - * @returns {Promise<{pid: number, launcher: import("node:child_process").ChildProcess | null, folders: string[], port: number | null, slot: number, desktop: string | null}>} + * @param {boolean} [o.userSettings] the IDE keeps the user's own settings (default false: + * the IDE is answered "no saved value" for its options, panel + * layouts and keyboard groups, in the page only, so it runs with + * the defaults; tb-defaults.mjs). Only a tool that is the user's own + * IDE in all but its project passes true: try-help-addin + * @returns {Promise<{pid: number, launcher: import("node:child_process").ChildProcess | null, folders: string[], port: number | null, slot: number, desktop: string | null, hook: object | null}>} * `pid` is the IDE's own. `folders` are its temp folder and WebView2 profile, * which shutdownIde removes. `port` is its DevTools port, for attachIde, or * null when the IDE never opened one within a minute. `desktop` is the private @@ -262,6 +268,7 @@ export async function launchIde({ env = {}, browserArgs = "", flags = IDE_FLAGS, + userSettings = false, }) { // Before anything starts: an IDE older than the flags would write the user's settings. const tooOld = unsupportedIde(exe); @@ -272,7 +279,7 @@ export async function launchIde({ const s = slot ?? own; try { const leased = Date.now(); - const started = await startIde({ exe, project, s, own, port, show, keep, env, browserArgs, flags }); + const started = await startIde({ exe, project, s, own, port, show, keep, env, browserArgs, flags, userSettings }); timing("launch", { project: project ? path.resolve(project) : null, leaseMs: leased - t0, @@ -286,7 +293,7 @@ export async function launchIde({ } // launchIde once the slot is settled; `own` is the slot when it is launchIde's own to hand on. -async function startIde({ exe, project, s, own, port, show, keep, env, browserArgs, flags }) { +async function startIde({ exe, project, s, own, port, show, keep, env, browserArgs, flags, userSettings }) { const exeWin = exe.split("/").join("\\"); const target = project ? path.resolve(project).split("/").join("\\") : ""; // Each IDE has a temp folder of its own, emptied at each launch in its slot @@ -326,6 +333,15 @@ async function startIde({ exe, project, s, own, port, show, keep, env, browserAr ...env, }; const found = (pid) => (port != null ? Promise.resolve(port) : devToolsPort(ideProfile, pid, started)); + // Started before the IDE is, since main.htm commits a fraction of a second after its port file + // is written; attachIde checks the hook ran, by the port. The hook's connection lives as long + // as this process, so a kept IDE (`keep`) is hooked only until this process ends. + const hook = userSettings ? null : installDefaultsHook({ profile: ideProfile, since: started }); + const hooked = async (pid, launcher) => { + const p = await found(pid); + if (hook && p) defaultsHooks.set(p, hook); + return { pid, launcher, folders, port: p, slot: s.slot, desktop: show ? null : s.desktop, hook }; + }; if (show) { const child = spawn(exeWin, [...flags, ...(target ? [target] : [])], { @@ -343,7 +359,7 @@ async function startIde({ exe, project, s, own, port, show, keep, env, browserAr child.unref(); // The slot names its folders; the IDE outlives nothing here, so it keeps the slot. await own?.handTo(child.pid); - return { pid: child.pid, launcher: null, folders, port: await found(child.pid), slot: s.slot, desktop: null }; + return hooked(child.pid, null); } // lib/tb-launch.ps1 does the Win32 calls Node cannot -- CreateDesktop, @@ -381,6 +397,7 @@ async function startIde({ exe, project, s, own, port, show, keep, env, browserAr env: fullEnv, }); } catch (e) { + hook?.stop(); // No IDE started, so nothing holds the folders and nothing else would remove them. for (const dir of folders) rmSync(dir, { recursive: true, force: true, maxRetries: 3 }); throw new Error( @@ -389,9 +406,13 @@ async function startIde({ exe, project, s, own, port, show, keep, env, browserAr } const { pid, launcher } = launched; await own?.handTo(keep ? pid : launcher.pid); - return { pid, launcher, folders, port: await found(pid), slot: s.slot, desktop: s.desktop }; + return hooked(pid, launcher); } +// The hook of each IDE launchIde started with the defaults, by its DevTools port: attachIde checks +// the IDEs it is told the port of, so no caller says whether to. +const defaultsHooks = new Map(); + // The DevTools port the IDE's WebView2 opened: the first line of DevToolsActivePort in its // profile, written once its browser process has bound the port. Only a file written since the // launch counts, in case a folder could not be emptied. Null when the IDE ends, or a minute @@ -741,6 +762,7 @@ export function keptIdeLines(pid) { export function shutdownIde(ide) { if (!ide) return; killTree(ide.pid); + ide.hook?.stop(); // The launcher holds the private desktop open and the only handle to the // IDE's job. It exits once the IDE does, but do not wait on that: killing it // closes the job, which ends anything the tree kill missed. @@ -772,6 +794,7 @@ export async function shutdownIdeAsync(ide) { await new Promise((resolve) => { execFile("taskkill", ["/PID", String(ide.pid), "/T", "/F"], { windowsHide: true }, () => resolve()); }); + ide.hook?.stop(); try { ide.launcher?.kill(); } catch { @@ -859,6 +882,7 @@ export async function attachIde(port, { tries = 60 } = {}) { } // still starting c.dialogs = []; c.pageBlocked = false; + const hook = defaultsHooks.get(port); c.on((m) => { if (m.method !== "Page.javascriptDialogOpening") return; const { type, message } = m.params; @@ -872,6 +896,8 @@ export async function attachIde(port, { tries = 60 } = {}) { } catch { c.pageBlocked = true; } + // A blocked page answers nothing, the hook's marker included. + if (hook && !c.pageBlocked) await checkDefaults(c, hook); c.wire = null; if (!c.pageBlocked) { const wire = watchCompiles(c); diff --git a/scripts/lib/tb-lane.mjs b/scripts/lib/tb-lane.mjs index 9eec699b6..e9b946cde 100644 --- a/scripts/lib/tb-lane.mjs +++ b/scripts/lib/tb-lane.mjs @@ -93,12 +93,24 @@ export class Lane { * @param {boolean} [o.show] IDEs on the user's desktop instead of a private one * @param {string} [o.browserArgs] more WebView2 browser arguments for every IDE * the lane opens, as launchIde takes them + * @param {boolean} [o.userSettings] every IDE keeps the user's own settings instead of the + * defaults, as launchIde takes it (try-help-addin only) * @param {{ width: number, height: number } | null} [o.page] the size each IDE's * page is given as the lane attaches to it (fixPageSize); null, the default, * leaves the window's own. addinLane gives a runner's lane PAGE */ - constructor({ name, slot = null, desktop = null, work, ide, show = false, browserArgs = "", page = null }) { - Object.assign(this, { name, slot, desktop, work, ide, show, browserArgs, page }); + constructor({ + name, + slot = null, + desktop = null, + work, + ide, + show = false, + browserArgs = "", + page = null, + userSettings = false, + }) { + Object.assign(this, { name, slot, desktop, work, ide, show, browserArgs, page, userSettings }); this.port = null; // the open IDE's DevTools port this.lease = null; // the slot, as launchIde takes it (leased()) this.exe = null; // the lane's copy of the install, made on first use @@ -292,6 +304,7 @@ export class Lane { env: { APPDATA: appdata, ...env }, browserArgs: this.browserArgs, flags, + userSettings: this.userSettings, }); Object.assign(this, { src, project, port: this.run.port }); this.c = await attachIde(this.port); @@ -327,6 +340,7 @@ export class Lane { show: this.show, env: { APPDATA: this.appdataDir(), ...env }, browserArgs: this.browserArgs, + userSettings: this.userSettings, }); Object.assign(this, { src: null, project: null, port: this.run.port }); this.c = await attachIde(this.port); diff --git a/scripts/try_help_addin.mjs b/scripts/try_help_addin.mjs index 04a8b68c5..a8a068ede 100644 --- a/scripts/try_help_addin.mjs +++ b/scripts/try_help_addin.mjs @@ -120,7 +120,15 @@ const tidy = startTidy({ prefixes: [work], window: false }); if (!tidy) die(2, "could not record the registry, so it could not be put back afterwards"); const settingsBefore = snapshotKeys([settingsKey(SETTINGS)]); -const lane = new Lane({ name: "try", slot: slot.slot, desktop: slot.desktop, work, ide, show: true }); +const lane = new Lane({ + name: "try", + slot: slot.slot, + desktop: slot.desktop, + work, + ide, + show: true, + userSettings: true, +}); let server = null; let stopping = false; From 22e6a20a0334adafca3a4a82d2f3e995e6d50d7a Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 21 Oct 2026 00:42:24 +0200 Subject: [PATCH 04/21] Help add-in 1.1.0: find in the shortcut editors (Ctrl+F, F3 keys, scrolls to each match), and a setting for each page-side fix --- WIP.HelpAddin.md | 23 +- add-in/CHANGELOG.md | 11 + add-in/Resources/SCRIPTS/addinkeys.js | 4 +- add-in/Resources/SCRIPTS/textfind.js | 228 ++++++++++++++++++ add-in/Resources/SHELL/shell.html | 2 + add-in/Resources/SHELL/shell.js | 10 +- add-in/Resources/STYLESHEETS/pane.css | 4 +- add-in/Settings | 2 +- add-in/Sources/HelpAddIn.twin | 85 ++++++- docs/IDE/AddIns/Help.md | 28 ++- .../IDE/AddIns/Images/Help_Settings.light.png | Bin 35146 -> 64312 bytes .../IDE/AddIns/Images/Help_Settings.light.svg | 2 +- docs/IDE/AddIns/Images/Help_Settings.png | Bin 33350 -> 60623 bytes docs/IDE/AddIns/Images/Help_Settings.svg | 2 +- scripts/shoot_docs.mjs | 7 +- test/addin/help.test.mjs | 191 +++++++++++++++ 16 files changed, 582 insertions(+), 17 deletions(-) create mode 100644 add-in/Resources/SCRIPTS/textfind.js diff --git a/WIP.HelpAddin.md b/WIP.HelpAddin.md index 8b7dc465b..1fe061ca2 100644 --- a/WIP.HelpAddin.md +++ b/WIP.HelpAddin.md @@ -1252,7 +1252,28 @@ not: a restart saves a harness about a second against opening a new IDE. `globalKeyDown`'s text still has the guard (see [Keyboard shortcuts](#keyboard-shortcuts)), so a build that fixes the bug is left alone; a second run replaces the first one's listeners. The IDE's key-down bindings still run beside an add-in's shortcut: Ctrl+- - is `tbEditor_DecreaseFontSize` as well. + is `tbEditor_DecreaseFontSize` as well. A settings-panel row, on by default and kept with + `SaveSetting` (`Fixes`, `AddinKeys`), turns it off: the script has a `$ON$` switch as + `hoverhelp.js` has and runs again at each change, taking the listeners out when off. The + window's panel has its own box, in step through `addinkeys:<1|0>` both ways. +12. **Ctrl+F in the configuration editors** (owner, 2026-10-21). Manage Keyboard Shortcuts + and Manage Panel Layouts (`showConfigEditor`) edit each entry in a plain + `textarea.genericMonospaceTextArea`, made again whenever another entry is picked, not + in Monaco, so there is no find widget to switch on; and `globalKeyDown` returns before + the keymap while a modal dialog is open. `Resources/SCRIPTS/textfind.js`, put in by + `BuildPane`, adds a capture-phase `keydown` listener on `window`: Ctrl+F in such a text + area puts a find box over it (positioned in the text area's parent), Enter and F3 select + the next match, Shift with either the one before, round past the end and in any case, + and Esc closes the box (the Visual Studio keys: F3 and Shift+F3 work with the box closed, + on the last query, and open the box when there is none; Ctrl+F3 and Ctrl+Shift+F3 take + the one-line selection, else the word at the caret (letters, digits, `_`), as the query + and search without the box). A match is scrolled to by measuring it in an off-screen + mirror of the text area (`matchRect`), a third down and clear of the box; + `window.tbDocsTextFind.visible(area)` tells the test whether the selection shows. Each handled key is stopped at `window`, so the dialog's own + Escape handler (which would close it unless the focus is in a text field) never sees it. + Focusing the text area does not reliably scroll it to the caret. A second + settings-panel row (`Fixes`, `TextFind`, on by default) turns it off, by the same + `$ON$` switch, and the window's box follows through `textfind:<1|0>`. **Zoom, not done** (owner, 2026-10-20: the IDE's own Ctrl+/Ctrl- and Ctrl+Alt+/Ctrl+Alt- font commands are enough for now). WebView2's own zoom is off in the IDE and no host call diff --git a/add-in/CHANGELOG.md b/add-in/CHANGELOG.md index c1983b62e..a296174aa 100644 --- a/add-in/CHANGELOG.md +++ b/add-in/CHANGELOG.md @@ -4,6 +4,17 @@ What changed in the twinBASIC Documentation Help add-in, newest first. The versi DLL's (its file properties show it). The documentation the add-in shows and its index of symbols change with every build of the documentation and are not listed here. +## 1.1.0 (2026-10-21) + +- Ctrl+F finds text in the editors of **Window > Keyboard Shortcuts > Manage Keyboard + Shortcuts** and of **Manage Panel Layouts**. Enter or F3 goes to the next match, + Shift+Enter or Shift+F3 to the one before (F3 also with the find box closed), Ctrl+F3 and + Ctrl+Shift+F3 search for the selection or the word at the caret without opening the box, + and Esc closes the find box and leaves the dialog open. The text scrolls to each match. +- The settings panel has two new boxes, both ticked by default: **Ctrl and Alt shortcuts of + add-ins** (the 1.0.0 fix) and **Find in the shortcut editors** (the Ctrl+F above). Unticking + one turns that feature off at once. + ## 1.0.0 (2026-10-20) - Other add-ins' keyboard shortcuts that include Ctrl or Alt now work while this add-in is diff --git a/add-in/Resources/SCRIPTS/addinkeys.js b/add-in/Resources/SCRIPTS/addinkeys.js index 299a3b94d..c22ce77f1 100644 --- a/add-in/Resources/SCRIPTS/addinkeys.js +++ b/add-in/Resources/SCRIPTS/addinkeys.js @@ -5,7 +5,8 @@ // before. This records those presses itself and runs their shortcuts on // key-up as the IDE runs the others. It is put in only while the IDE has that // guard, so a build that fixes it is left alone. Running it again replaces -// what an earlier run put in. +// what an earlier run put in, and puts in nothing when it is turned off. +var on = $ON$; var old = window.tbDocsAddinKeys; if (old) { window.removeEventListener("keydown", old.down, true); @@ -13,6 +14,7 @@ if (old) { window.tbDocsAddinKeys = null; } var faulty = + on && typeof globalKeyDown === "function" && typeof addinKeys === "object" && typeof realKeyPresses === "object" && diff --git a/add-in/Resources/SCRIPTS/textfind.js b/add-in/Resources/SCRIPTS/textfind.js new file mode 100644 index 000000000..6c1920c4a --- /dev/null +++ b/add-in/Resources/SCRIPTS/textfind.js @@ -0,0 +1,228 @@ +// Find in the text boxes of the IDE's configuration editors (Window > Keyboard +// Shortcuts > Manage Keyboard Shortcuts, and Manage Panel Layouts), which are +// plain text areas with no find of their own, inside a dialog where the IDE's +// key handling does nothing. The keys are those of Visual Studio and VS Code: +// Ctrl+F opens a find box over the text area; +// Enter, F3 select the next match of the query, Shift+Enter, Shift+F3 +// the one before, from the selection and round past the end; +// F3, Shift+F3 also work with the box closed, on the last query, and with +// no query yet F3 opens the box; +// Ctrl+F3, Ctrl+Shift+F3 +// take the selection (when on one line), or else the word at +// the caret, as the query, and find the next or previous +// match without opening the box; +// Esc closes the box only: the dialog stays open. +// A match is found in any case. Running this again replaces what an earlier run +// put in, and puts in nothing when it is turned off. +var on = $ON$; +var old = window.tbDocsTextFind; +if (old) { + window.removeEventListener("keydown", old.down, true); + if (old.bar) old.bar.remove(); + window.tbDocsTextFind = null; +} +if (on) { +var EDITOR = "textarea.genericMonospaceTextArea"; +var state = { bar: null, input: null, area: null, query: "" }; + +var barOpen = function () { + return state.bar !== null && state.bar.isConnected; +}; + +var close = function (focusArea) { + if (state.bar) state.bar.remove(); + var area = state.area; + state.bar = state.input = state.area = null; + if (focusArea && area && area.isConnected) area.focus(); +}; + +// Where the text from start to end is, in the text area's scrolling space (the +// origin is its padding box's top left, before any scrolling): measured in an +// off-screen copy of the text area's text, set the same way and as wide, with +// the match in a span. A match that wraps has the first line's top and left and +// the last line's bottom. +var matchRect = function (area, start, end) { + var cs = getComputedStyle(area); + var mirror = document.createElement("div"); + var props = [ + "fontFamily", "fontSize", "fontWeight", "fontStyle", "fontVariant", "letterSpacing", + "lineHeight", "textTransform", "textIndent", "wordSpacing", "tabSize", + "paddingTop", "paddingRight", "paddingBottom", "paddingLeft", + "whiteSpace", "overflowWrap", "wordBreak", "direction" + ]; + for (var i = 0; i < props.length; i++) mirror.style[props[i]] = cs[props[i]]; + mirror.style.cssText += + ";position:absolute;visibility:hidden;left:-99999px;top:0;box-sizing:border-box;border:0;margin:0;" + + "overflow:visible;width:" + area.clientWidth + "px"; + var span = document.createElement("span"); + span.textContent = area.value.substring(start, end) || "​"; + mirror.appendChild(document.createTextNode(area.value.substring(0, start))); + mirror.appendChild(span); + mirror.appendChild(document.createTextNode(area.value.substring(end) + "​")); + document.body.appendChild(mirror); + var origin = mirror.getBoundingClientRect(); + var rects = span.getClientRects(); + var first = rects[0]; + var last = rects[rects.length - 1]; + var right = -Infinity; + for (var j = 0; j < rects.length; j++) right = Math.max(right, rects[j].right); + var r = { + top: first.top - origin.top, + bottom: last.bottom - origin.top, + left: first.left - origin.left, + right: right - origin.left, + wraps: cs.whiteSpace.indexOf("pre-wrap") >= 0 || cs.whiteSpace === "normal" || cs.whiteSpace === "pre-line" + }; + mirror.remove(); + return r; +}; + +// Whether the selection is in view, and not under the find box, with the rect +// matchRect gives for it. +var visibleState = function (area) { + var r = matchRect(area, area.selectionStart, area.selectionEnd); + var box = area.getBoundingClientRect(); + var left = box.left + area.clientLeft + r.left - area.scrollLeft; + var right = box.left + area.clientLeft + r.right - area.scrollLeft; + var top = box.top + area.clientTop + r.top - area.scrollTop; + var bottom = box.top + area.clientTop + r.bottom - area.scrollTop; + var inView = + r.top >= area.scrollTop && + r.bottom <= area.scrollTop + area.clientHeight && + (r.wraps || (r.left >= area.scrollLeft && r.right <= area.scrollLeft + area.clientWidth)); + var covered = false; + if (barOpen()) { + var b = state.bar.getBoundingClientRect(); + covered = left < b.right && right > b.left && top < b.bottom && bottom > b.top; + } + return { visible: inView && !covered, covered: covered, rect: r, cover: barOpen() ? state.bar.offsetHeight + 8 : 0 }; +}; + +// Selects the match and scrolls so that it shows, about a third down the text +// area and clear of the find box. +var select = function (area, at, length) { + area.focus(); + area.setSelectionRange(at, at + length); + area.blur(); + area.focus(); + area.setSelectionRange(at, at + length); + var v = visibleState(area); + if (v.visible) return; + var r = v.rect; + area.scrollTop = Math.max(0, r.top - Math.max(v.cover, area.clientHeight / 3)); + if (!r.wraps) area.scrollLeft = Math.max(0, r.left - area.clientWidth / 3); +}; + +var find = function (area, backwards) { + if (barOpen()) state.query = state.input.value; + var query = state.query.toLowerCase(); + if (!area || !area.isConnected || !query) return; + var text = area.value.toLowerCase(); + var at; + if (backwards) { + at = area.selectionStart > 0 ? text.lastIndexOf(query, area.selectionStart - 1) : -1; + if (at < 0) at = text.lastIndexOf(query); + } else { + at = text.indexOf(query, area.selectionEnd); + if (at < 0) at = text.indexOf(query); + } + if (barOpen()) { + state.input.classList.toggle("tbDocsFindMissing", at < 0); + state.input.title = at < 0 ? "No match" : ""; + } + if (at >= 0) select(area, at, query.length); +}; + +var open = function (area) { + if (state.area !== area || !barOpen()) { + close(false); + var host = area.parentNode; + if (getComputedStyle(host).position === "static") host.style.position = "relative"; + var bar = document.createElement("div"); + bar.className = "tbDocsFindBar"; + bar.style.cssText = + "position:absolute;top:4px;right:20px;z-index:10;display:flex;gap:4px;padding:3px;" + + "background:var(--themeGeneralPanelBackColor);border:var(--themePropertySheetValueBorderColor);" + + "box-shadow:0 2px 6px rgba(0,0,0,0.35)"; + var input = document.createElement("input"); + input.type = "text"; + input.placeholder = "Find"; + input.setAttribute("aria-label", "Find"); + input.value = state.query; + input.style.cssText = + "width:180px;font:inherit;color:var(--themeToolWindowBodyForeColor);" + + "background:var(--themeGeneralPanelBackColor);border:var(--themePropertySheetValueBorderColor);outline:none"; + bar.appendChild(input); + host.appendChild(bar); + state.bar = bar; + state.input = input; + state.area = area; + window.tbDocsTextFind.bar = bar; + } + var picked = area.value.substring(area.selectionStart, area.selectionEnd); + if (picked && picked.indexOf("\n") < 0) state.input.value = picked; + state.input.focus(); + state.input.select(); +}; + +// The query for Ctrl+F3: the selection when it is on one line, else the word at +// the caret (letters, digits and _), which is then selected, so that a search +// backwards goes before it. +var takeQuery = function (area) { + var picked = area.value.substring(area.selectionStart, area.selectionEnd); + if (picked && picked.indexOf("\n") < 0) return picked; + var text = area.value; + var start = area.selectionStart; + var end = start; + var isWord = function (c) { + return /[A-Za-z0-9_]/.test(c); + }; + while (start > 0 && isWord(text.charAt(start - 1))) start--; + while (end < text.length && isWord(text.charAt(end))) end++; + if (start === end) return ""; + area.setSelectionRange(start, end); + return text.substring(start, end); +}; + +var down = function (e) { + var target = e.target; + var inArea = target instanceof HTMLTextAreaElement && target.matches(EDITOR); + var inBox = state.input !== null && target === state.input; + if (!inArea && !inBox) return; + var area = inArea ? target : state.area; + var handled = true; + if (e.ctrlKey && !e.altKey && e.code === "KeyF") { + open(area); + } else if (e.key === "F3" && e.ctrlKey && !e.altKey) { + var word = takeQuery(area); + if (word) { + state.query = word; + if (barOpen()) state.input.value = word; + find(area, e.shiftKey); + } + } else if (e.key === "F3" && !e.altKey) { + if (barOpen() || state.query) find(area, e.shiftKey); + else open(area); + } else if (inBox && e.key === "Enter") { + find(area, e.shiftKey); + } else if (barOpen() && e.key === "Escape") { + close(true); + } else { + handled = false; + } + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } +}; + +window.addEventListener("keydown", down, true); +window.tbDocsTextFind = { down: down, bar: null, visible: visibleState }; +var style = document.getElementById("tbDocsFindStyle"); +if (!style) { + style = document.createElement("style"); + style.id = "tbDocsFindStyle"; + style.textContent = ".tbDocsFindBar input.tbDocsFindMissing{border-color:#c03030 !important}"; + document.head.appendChild(style); +} +} diff --git a/add-in/Resources/SHELL/shell.html b/add-in/Resources/SHELL/shell.html index c0e9eda8c..4f704859b 100644 --- a/add-in/Resources/SHELL/shell.html +++ b/add-in/Resources/SHELL/shell.html @@ -18,6 +18,8 @@ diff --git a/add-in/Resources/SHELL/shell.js b/add-in/Resources/SHELL/shell.js index fb626c1c3..9523d7662 100644 --- a/add-in/Resources/SHELL/shell.js +++ b/add-in/Resources/SHELL/shell.js @@ -3,12 +3,12 @@ // way is "verb:payload", split at the first colon. // // Page to add-in: ready, search:, enter, pick:, hover:<1|0>, -// allpackages:<1|0>, attach, browser, frame: (the frame has loaded src), +// allpackages:<1|0>, addinkeys:<1|0>, textfind:<1|0>, attach, browser, frame: (the frame has loaded src), // and from the title bar drag (start moving the window), minimize, maxrestore, // close. // Add-in to page: theme: a line break and , search:, // results:, page:, summary:, hover:<1|0>, hoverdisabled:<1|0>, -// allpackages:<1|0>, browser:<1|0 enabled>, focus:, chrome: +// allpackages:<1|0>, addinkeys:<1|0>, textfind:<1|0>, browser:<1|0 enabled>, focus:, chrome: // (whether the page draws the title bar), state: (the window's). (function () { var host = window.chrome.webview; @@ -21,6 +21,8 @@ var browser = byId("helpBrowser"); var hover = byId("helpHover"); var allPackages = byId("helpAllPackages"); + var keysOn = byId("helpKeysOn"); + var findOn = byId("helpFindOn"); var settings = byId("helpSettings"); var settingsWrap = byId("helpSettingsWrap"); var settingsPanel = byId("helpSettingsPanel"); @@ -82,6 +84,8 @@ allPackages.addEventListener("change", function () { post("allpackages:" + (allPackages.checked ? "1" : "0")); }); + keysOn.addEventListener("change", function () { post("addinkeys:" + (keysOn.checked ? "1" : "0")); }); + findOn.addEventListener("change", function () { post("textfind:" + (findOn.checked ? "1" : "0")); }); // The settings panel opens under the gear, with the shade over the rest of // the window so that a click anywhere else closes it. A press over the @@ -172,6 +176,8 @@ case "hover": hover.checked = payload === "1"; break; case "hoverdisabled": hover.disabled = payload === "1"; break; case "allpackages": allPackages.checked = payload === "1"; break; + case "addinkeys": keysOn.checked = payload === "1"; break; + case "textfind": findOn.checked = payload === "1"; break; case "browser": browser.disabled = payload !== "1"; break; case "focus": search.focus(); break; case "chrome": setChrome(payload); break; diff --git a/add-in/Resources/STYLESHEETS/pane.css b/add-in/Resources/STYLESHEETS/pane.css index 6d6291180..caee3c02a 100644 --- a/add-in/Resources/STYLESHEETS/pane.css +++ b/add-in/Resources/STYLESHEETS/pane.css @@ -63,7 +63,9 @@ } #helpHoverLabel, -#helpAllPackagesLabel { +#helpAllPackagesLabel, +#helpKeysLabel, +#helpFindLabel { display: flex; align-items: center; gap: 4px; diff --git a/add-in/Settings b/add-in/Settings index efd634fc2..b46e86408 100644 --- a/add-in/Settings +++ b/add-in/Settings @@ -68,7 +68,7 @@ "project.versionAutoIncrement": "None", "project.versionBuild": 0, "project.versionMajor": 1, - "project.versionMinor": 0, + "project.versionMinor": 1, "project.versionRevision": 0, "project.warnings": { "errors": [], diff --git a/add-in/Sources/HelpAddIn.twin b/add-in/Sources/HelpAddIn.twin index b1f73115b..01474bb7d 100644 --- a/add-in/Sources/HelpAddIn.twin +++ b/add-in/Sources/HelpAddIn.twin @@ -49,6 +49,10 @@ Private Class HelpAddIn Private WithEvents Hovering As HoverHelp ' Nothing without an index Private HoverBox As HtmlElement ' the pane's Hover help checkbox Private AllBox As HtmlElement ' the pane's All packages checkbox + Private KeysBox As HtmlElement ' the pane's Ctrl or Alt shortcuts checkbox + Private FindBox As HtmlElement ' the pane's Find in the shortcut editors checkbox + Private KeysHost As HtmlElement ' where addinkeys.js is put in + Private FindHost As HtmlElement ' where textfind.js is put in Private Referenced As Collection ' the project's packages, keyed by name; Nothing if unknown Private DetachButton As HtmlElement Private SettingsButton As HtmlElement ' the gear at the bar's right end @@ -64,6 +68,8 @@ Private Class HelpAddIn Private SearchText As String ' what the search box holds, in either view Private HoverOn As Boolean ' what the Hover help box holds, in either view Private AllPackages As Boolean ' what the All packages box holds, in either view + Private KeysOn As Boolean ' what the Ctrl or Alt shortcuts box holds, in either view + Private FindOn As Boolean ' what the Find in the shortcut editors box holds, in either view Private SummaryHtml As String ' what ShowSummary shows Private ShowingSummary As Boolean ' the summary shows in place of the page @@ -83,6 +89,8 @@ Private Class HelpAddIn ' The window's settings: whether it is detached, whether it is shown, and ' where it is, in pixels. Private Const WINDOW_SECTION As String = "Window" + ' The settings of the two page-side fixes: addinkeys.js and textfind.js. + Private Const FIXES_SECTION As String = "Fixes" Public Sub New(ByVal Host As Host) Set Me.Host = Host @@ -278,6 +286,8 @@ Private Class HelpAddIn ' Kept apart from the box: a property read straight after it is set ' can still give the old value. HoverOn = GetSetting(SETTINGS_APP, "Hover", "On", "0") = "1" + KeysOn = GetSetting(SETTINGS_APP, FIXES_SECTION, "AddinKeys", "1") = "1" + FindOn = GetSetting(SETTINGS_APP, FIXES_SECTION, "TextFind", "1") = "1" Set Pane = Host.ToolWindows.Add("tbDocsHelp", PANE_ID) Pane.Title = "TWINBASIC HELP" Pane.ApplyCss StrConv(LoadResDataInternal("pane.css", "STYLESHEETS"), VbStrConv.vbFromUTF8) @@ -330,6 +340,20 @@ Private Class HelpAddIn AllBox.AddEventListener("change", AddressOf AllPackagesToggled) .ChildDomElements.Add("helpAllPackagesText", "span").Properties.innerText = "All packages" End With + With SettingsPanel.ChildDomElements.Add("helpKeysLabel", "label") + Set KeysBox = .ChildDomElements.Add("helpKeysOn", "input") + KeysBox.Properties.type = "checkbox" + KeysBox.Properties.checked = KeysOn + KeysBox.AddEventListener("change", AddressOf KeysToggled) + .ChildDomElements.Add("helpKeysText", "span").Properties.innerText = "Ctrl and Alt shortcuts of add-ins" + End With + With SettingsPanel.ChildDomElements.Add("helpFindLabel", "label") + Set FindBox = .ChildDomElements.Add("helpFindOn", "input") + FindBox.Properties.type = "checkbox" + FindBox.Properties.checked = FindOn + FindBox.AddEventListener("change", AddressOf FindToggled) + .ChildDomElements.Add("helpFindText", "span").Properties.innerText = "Find in the shortcut editors" + End With End With End With Set SettingsShade = .ChildDomElements.Add("helpSettingsShade", "div") @@ -361,10 +385,14 @@ Private Class HelpAddIn End With ' Makes every add-in's {ctrl} and {alt} shortcuts fire, which ' the IDE's own key handling does not (addinkeys.js). - With .ChildDomElements.Add("helpKeys", "div") - .Properties.style.display = "none" - .Properties.innerHTML = PageScript(StrConv(LoadResDataInternal("addinkeys.js", "SCRIPTS"), VbStrConv.vbFromUTF8)) - End With + Set KeysHost = .ChildDomElements.Add("helpKeys", "div") + KeysHost.Properties.style.display = "none" + ApplyFix KeysHost, "addinkeys.js", KeysOn + ' Ctrl+F in the text of the Manage Keyboard Shortcuts and + ' Manage Panel Layouts dialogs (textfind.js). + Set FindHost = .ChildDomElements.Add("helpTextFind", "div") + FindHost.Properties.style.display = "none" + ApplyFix FindHost, "textfind.js", FindOn End With End With Set ResultIds = New Collection @@ -398,6 +426,49 @@ Private Class HelpAddIn If Detached Then PostToWindow "hover:" & IIf(isOn, "1", "0") End Sub + ' Runs a page-side fix's script in the page: it puts the fix in when isOn, + ' and takes out what an earlier run or instance put in. + Private Sub ApplyFix(ByVal Target As HtmlElement, ByVal script As String, ByVal isOn As Boolean) + Dim js As String = StrConv(LoadResDataInternal(script, "SCRIPTS"), VbStrConv.vbFromUTF8) + Target.Properties.innerHTML = PageScript(Replace(js, "$ON$", IIf(isOn, "true", "false"))) + End Sub + + Private Sub KeysToggled(ByVal eventInfo As HtmlEventProperties) + On Error GoTo Failed + SetKeys KeysBox.Properties.checked + Exit Sub + Failed: + Host.DebugConsole.PrintText TAG & "error " & Err.Number & ": " & Err.Description + End Sub + + ' Turns the Ctrl and Alt shortcuts of add-ins on or off, from the pane's box + ' or the window's, and shows it in both. + Private Sub SetKeys(ByVal isOn As Boolean) + KeysOn = isOn + SaveSetting SETTINGS_APP, FIXES_SECTION, "AddinKeys", IIf(isOn, "1", "0") + ApplyFix KeysHost, "addinkeys.js", isOn + KeysBox.Properties.checked = isOn + If Detached Then PostToWindow "addinkeys:" & IIf(isOn, "1", "0") + End Sub + + Private Sub FindToggled(ByVal eventInfo As HtmlEventProperties) + On Error GoTo Failed + SetFind FindBox.Properties.checked + Exit Sub + Failed: + Host.DebugConsole.PrintText TAG & "error " & Err.Number & ": " & Err.Description + End Sub + + ' Turns Ctrl+F in the shortcut editors on or off, from the pane's box or + ' the window's, and shows it in both. + Private Sub SetFind(ByVal isOn As Boolean) + FindOn = isOn + SaveSetting SETTINGS_APP, FIXES_SECTION, "TextFind", IIf(isOn, "1", "0") + ApplyFix FindHost, "textfind.js", isOn + FindBox.Properties.checked = isOn + If Detached Then PostToWindow "textfind:" & IIf(isOn, "1", "0") + End Sub + ' Only the packages the project references are offered until the box is ' ticked, and it stays as it was left. Private Sub AllPackagesToggled(ByVal eventInfo As HtmlEventProperties) @@ -910,6 +981,10 @@ Private Class HelpAddIn SetHover payload = "1" Case "allpackages" SetAllPackages payload = "1" + Case "addinkeys" + SetKeys payload = "1" + Case "textfind" + SetFind payload = "1" Case "attach" Attach Case "close" @@ -945,6 +1020,8 @@ Private Class HelpAddIn HelpWin.Post "hoverdisabled:" & IIf(Hovering Is Nothing, "1", "0") HelpWin.Post "hover:" & IIf(HoverOn, "1", "0") HelpWin.Post "allpackages:" & IIf(AllPackages, "1", "0") + HelpWin.Post "addinkeys:" & IIf(KeysOn, "1", "0") + HelpWin.Post "textfind:" & IIf(FindOn, "1", "0") HelpWin.Post "search:" & SearchText PostResults If ShowingSummary Then diff --git a/docs/IDE/AddIns/Help.md b/docs/IDE/AddIns/Help.md index 79af59e41..cc0cc4b30 100644 --- a/docs/IDE/AddIns/Help.md +++ b/docs/IDE/AddIns/Help.md @@ -80,7 +80,7 @@ Without a **Description** attribute, the summary says *No description*. **Open i The gear opens the settings. A click anywhere else, or Esc, closes them. -![The top of the help pane with the settings open under the gear: the Hover help and All packages boxes, both unticked.](Images/Help_Settings.png){:width="480" height="150"} +![The top of the help pane with the settings open under the gear: four boxes. Hover help and All packages are unticked; Ctrl and Alt shortcuts of add-ins and Find in the shortcut editors are ticked.](Images/Help_Settings.png){:width="480" height="200"} **Hover help** : Adds the links described under [Hover help](#hover-help). Off by default. @@ -88,6 +88,12 @@ The gear opens the settings. A click anywhere else, or Esc, closes them. **All packages** : Offers the pages of every package this documentation covers. Off by default: F1, the hover links and the search then offer only the language itself and the packages the project references, so that a name from a package the project does not use never hides the one it does. With this off, F1 on a name that only an unreferenced package documents says *No help for 'Name' in the packages this project references*. +**Ctrl and Alt shortcuts of add-ins** +: Runs the keyboard shortcuts that other add-ins define with Ctrl or Alt, which the IDE on its own does not run: see [Other add-ins' shortcuts](#other-add-ins-shortcuts). On by default. + +**Find in the shortcut editors** +: Ctrl+F and F3 in the text of **Manage Keyboard Shortcuts** and **Manage Panel Layouts**: see [Finding text in the shortcut editors](#finding-text-in-the-shortcut-editors). On by default. + The settings, the last page shown and the help window's place are kept in the registry, under `HKEY_CURRENT_USER\Software\VB and VBA Program Settings\tbDocsHelp`. After the IDE restarts, the pane shows the page it showed last. ## The help window @@ -111,7 +117,25 @@ The window is open again the next time the IDE starts, if it was open when the I ## Other add-ins' shortcuts -While this add-in is loaded, the keyboard shortcuts that other add-ins register with Ctrl or Alt work. Without it, the IDE does not run an add-in's shortcut that includes Ctrl or Alt: see [KeyboardShortcuts](../../Packages/tbIDE/KeyboardShortcuts). The IDE's own command on the same keys still runs as well. +While this add-in is loaded, the keyboard shortcuts that other add-ins register with Ctrl or Alt work. Without it, the IDE does not run an add-in's shortcut that includes Ctrl or Alt: see [KeyboardShortcuts](../../Packages/tbIDE/KeyboardShortcuts). The IDE's own command on the same keys still runs as well. The setting **Ctrl and Alt shortcuts of add-ins** turns this off. + +## Finding text in the shortcut editors + +The text of **Window > Keyboard Shortcuts > Manage Keyboard Shortcuts**, and of **Manage Panel Layouts**, has no find of its own. While this add-in is loaded, these keys work there, as in Visual Studio: + +Ctrl+F +: Opens a find box over the text, holding the selected text if it is on one line. + +Enter, F3 +: Selects the next match. **Shift+Enter** and **Shift+F3** select the one before. F3 and Shift+F3 also work after the find box is closed, with the last text searched for. + +Ctrl+F3 +: Searches for the selected text, or for the word at the caret, without opening the find box. **Ctrl+Shift+F3** searches backwards. + +Esc +: Closes the find box. The dialog stays open. + +The search goes round past the end, and ignores case. The text scrolls to show each match. The setting **Find in the shortcut editors** turns this off. ## Getting the add-in diff --git a/docs/IDE/AddIns/Images/Help_Settings.light.png b/docs/IDE/AddIns/Images/Help_Settings.light.png index 816af5bacffde3834f4ea660cc6cd5711b3a541b..f95337e0ad2c8cf1046c88dc17563e76e78bb90a 100644 GIT binary patch literal 64312 zcmb@uWmH^S)Gdg+ySuvu*TUUHa0nXQLU4C?3GN;wxH|+15+GP`4^p_pJGr^{d)>df zd-NNFF$xaUsk7JK%jTSmNHrB%R3suK2nYyNc{wQ!2ngt22nfgo1SsIjX-O^?1Ox_z zyp)8NNA__JtT@gR`N#+iO$wT{1Sh8S@$l)5aED08)Pj&mM@PrwF&d6EXB`Bl6iB03 zk@MB~=J07`$>ftR=g`E{m9)?zGW6CsG=pD(qbCqF;GyKo*J z2@IG&Uks9dPw1#QVC_8WfB%Mx`oiU(U;X(lDx&p&?}Wsf{`YbL6iq(j-^(CaIdSa2 zmqU;e0_gs|2QCGq^56SoFeUtV=ZL&y^fS$*wXkkgmAi*))JNO0OJYPy6`m7zZWUBw+^;0dAE&*kNIP$&{CGTshP zUpP8>u9!a=78cZ0Y|Z$Xo22W%j~J1MwHSF`A#shefh)Wtl8&hRzkk8&DH(i}*(a>U z!7Zf>`8@lG=x_JIzQ{@~L}@zsA$0T zLL>UW069yvlO3rK$P5 zh+xgwQ)n5fknXi^_w$aqlk@lHvK3e9%+z~rza>Q?CK`|F*F?Xi?%?&izbKLVHv265R8gZTx2(MU z_z8X0LAc%jQQ3olDwWUC=@qO<99*rvwNpM~NfaPX~d2408T znV*wlxb>Eo=`7Q(iCWyMjbtqo`iZnL;-@3jgVGzTp0cWFugF>nY1L!RsY_Z)ub{)q{@t-(k-Iyxj0NfCDAZeb=S zBzXJf#&5~==w&2K`uW!=QCb!AYHnfcHj?Z45f+@po3$m({7AP4)BN|B_;pQ+>)S+J zY`l9l;If^;L2!bJ3RU{LR0yKKLQH(9H=AM5%Kl`Q>N#s?p6Hd|MSv!<^}?eA+i$#A zTr3=l=ig=V&4ZBx=yfXH9IH7`4NCNW6GN*}_QS zJFhT_3<}B$Dotfr>FNuZ_Abj*f3oK?saK-d6=q|B(>89;G=4c6cKE%RyJ&1RUB2EB zag~Y(lHV}Kcs|nc+@SwQzcPc zDaiB+gJx!C&P2m0dLcO|9`tyMbl<^vp*hppompTE+4Q?9tEw82_kx=?e-*1Ji$4uY ztY^%^h3Kf9W+2|UC0YL2KSnf?Xi9mep?Q}F3NqUKF*wmbI;iAKu<e&ZJP=p>7=ZGJ(fEnl0;QHg+8Wb5DWX%)0=W7aw(oxL$3v z$A=-<42=8aR5{aZ>o{YFN=Yu}$M5P!-srN{>6z7d%Huu^+Q&PYsEOKKp4}zYbHD4- zE<4RGota(O{2KOLPe?bG+#~Mi8{E$l*F?-?KePEsB?%$$rTcFb3UT2;MRi7r?DTtn zSX_L!S$h|iY*S^WqWLI}{K;#r+a+$g9`J_6;8_+~ zX+fr$y-RPqf(-4vQ@z6mjar5+E~kcWl9)=~4H&_vRj5UNf}TMi7erHHt=1yaMew=r zzrEKa(W?4Ur#_BWh`1_-yg;i^cpGFIK6uCfH`0PWP(;%!Wz@N!EN^U>_{5QkERQny zGC|I93+Wb0A-)t>T3eGJoLLp$1`ez0c3m2-b>1taHjPMt?37k-KTMxH*Vh>w9T6`I zy=8uH&5{k51Hnj&8wuUfa6?!*3&i8O^51rvl`M_N^R6QWUIN;^zC~0&&h)FLzJWLj zkt0yhQBl!C9N4s*gDTo+A0X<|d%#{)jjqI5pSiamPJZKK+U=fZiu%4R6?ub|RoC-X z2KK}%y#8j%Q51}kgi(yKgz+q1`>6DQV5Lec6;w!8O;mdN9q``tP}B-2vR{kv<~fy( zQ$3MUKGG8J?b{)RjL-Vc>t*&35q3{7rZyJ#MKSx!%b}l}P6|tEwqN@n&!2^|XKCnZ zvxU0Fc=&q2P8 zwK^uq|CKFO*|(WRZ*_Vwl-!4z9+Z>Fmb&3Lmep>`%tJ!Co&E3+_~881!lVh#FkFt3kk+j3e>uhw*2O&Hg0kftkgSmp_|-(F-E#p-{o{#BQ0 zEmD}EN-kDfT0B*BmqCcG`AWYsqKV!n@Igea+EB&iY7yKceSZQ!h0<{Qgo#hD2EJoN6?l z{09xYk-95dqtl2u*{7dXKxRogG-`|`T`^D4#24`mt^~rE&5V(ve)zxnFZ%Az!_u$0 zzP?X~WW$y&cKYaDK5^s0hE|8^?59)~A6KMCm#p;%+GmFRHoLyDWtkqQ#Nv=UK1|aO zA2=k-QW$iH^PL`|5et-AqjAFn!cJM8jE<7;O(OP zl9WtmG3*`*NL*Q98B`|!)nkA8oWpMS0vB@qR8b_QLpwOs>9eW*)yQndGs185z|{Z6 z_hIz(vBl8nUgXyIB_{8fQc6*FWOR{b&tA(z&dKF{A^0SQsm~n+$D7UgdxQOPt5QOy zF8SX|lj=@{Z*c-O+w{HH_0jxNdqe{Ku;mTCVD0U5=aoL<*A%KS8B4swXxL!m`>u=Y zJ(`t)df$k9dqJvxqsJcb1DJBM!P>dBMw`mG+XwK3!O(&}Nc7WHtqZ2nPrN!X3(n0j z9N<*P)p&MgoXyMQnUQ$%ozMcHQcGyAYoH$-9(B`HyKo^?xhTXdch_#I{Ts3FU@=bL z@!2ibQDHgt-t6|O7CslG%90{C{w~ohWF+bL&6n5?L%rBF)JLFz zIz4+%bh^;6ki27k;>b7OQz5Ma;upy?9Tg>8X+!}hw;XrACNeQ0{qRU=HyRGxFHPK0 zHG%-3i0K1O7&#XPOy!Becl(^51)d~y!Xn{>MD5P5*O3maRaxnm`e0IHC+_bu2~v4{ zlaK6PJFJ!PO#j?qvHMiy%?l6TsiY7wl6RXNK62|gQq$DbDxfX0;U?Y3DF_LiCPgNKpu$2%YnWS2_t=u+H^&6Iird#tRYfFUTZG?TvZx0~Pz z2P^-gmW{(vp6}JkHa}ww;pX4dlCnTv7#rE$v&aqk8 z=b5~YE|dC=tpB&Wc(FB@Sb2kNg+vN09Fw7rGmA{Y+IF;->Q>Oy`$ThbIk2 zg|(b)pN_P?uI>20<^Avr@ms$Jbl!CJo8NrS4l{9oL(pAQ?`U&YmcEo!NLyRmd))~h zp6otOHuVp3!czs3hm!YuB~?tGkSx>TEwy+#cGI2EE8p0tcn*O6vC!C`oQK!jz2V=M z`$Z^3^wWq{hr3t?Y>(fuMB&qaSirN_*_w!ukdQ^p7>CZ|>tXi-}eUcu?Q@d@H$p1nS=U<^RZS4%x#*Idy{s%k||DU^S1;G4=S(HQk zA3!D6S64}1`GuC8{{{ybiP4eq@o4J*o<^b^=KueP{C|4-`_xYlt3Q(eoQW52EhneZ zY1Af6DTm-O`x`KG$yY9C(*)Rm@ec|uaad#?Nl$wfU>Bc`%We1yd4on#)!HV>w3PRQ z1O*Kob_Y5>F}2bE@8$d-u-X6fa#D?V|JynN=@b}ugZlTtT<>`uPiJ4M+>y;)OV*|U0Ht0tEhDQKJpR?$*lqF zz-Q8mp!Q=0U@TGd(S*T4sZDu%dwW;cOD63~x1+fVjS>YhMdxS6jeS8$64h*e%=nDP ztdNipWUK^a$F)w=ILcN4`WUyM)y+ z+S&@)pV3%NGqJL2@wvZ9qE&QhBNGu38GXiQH_8vBzJ6NYe15#`!&1o={$Oh>WHuZ_ z^u}iATXMK2sdX{4&X;U{=l4%{=euL6+b@^-$fy!Sdye3vHq(JH?W(t|dW{TmrXDw7 zaKKhSr{+}e!~{##Ja_s>D58ZX$#66Yk%6e`FNQ*+unY8$cjw#4h^y?V4cJq&WvY4n zTUK;5G&_es%5H9MvfD>53*unnC|*{?MFx&gd{Gdgevq;#bwZ-Muj;or)aZU4USId` z4S@W;P&58~gJ4~1ip9T`KD@l_b$j}&w6ye?84j0n+k_d|azBAHl_eAtDiq-4<| zxsW@GWC}Q$x?9n7?Z%)~DYHN_9I4|6{}&*FKHi-0Ij*5>v(O)oY-NmlHQ1o>jA6`i zw{Jkhqe<0_#9Q~`gxB^w-w=lh^Z!Y1ViVgV@jA6er~?YGFV6*X+1c55!yrY)D&X|O zqmfw86iVCK9UJ>U4`rnzQ0W9pNXs4d1;giw`FGo|G#{WAY8BbJhR}&W_g3xuzubpc zSk1w;(A3#L(w}3KIAKY?dEho!NY;iD1did24mCCP=XH4?UdRPoz8?rfwLalc5O6qizc`(`-N(3&^Ku84HaXCzY7bm*T*MuC6BL)i- z)A?wQ5oTa`*m62wg5}WJR9w6_m04$lc#JOyslfQF+1Ji*~x75V1*sGPAN;Z03<-L#zkGYx>=s zoh8VTM_NAu{+ad0w$!`@rXq@_>(dz-GS-(M>5=c>RZ7j&8z^Yt;NV&fFZU)bWbOg* zY&2Rno~+L`1Qj^#w$b1D;0&f;*BUIwS6Uo7UjmZ*t2lV-%|_7pWlrnfA&_pz(W~Z4 zDI%gLmVs*k|3N^!z})97J-@%SGBh+af4e;#i;TxIDqkYjf3~|DT1Tm$SP9q*^kp+c z0Ame#=;G?|hf~bxdy`^=HW%xm`;bu1iItTs(rps}Mdud>zpG1SjEeFNaE7vwY&T)Z zL>`k3p>Sa>*HiB)co@>mH~EE2;&!t9@#X2hoWuL-z|`7$RGzn#K*Y-}9F3HtGg{C3 zWVy*>3~aDb;rOF;GJ#TZIwIn!4?0NVCP(N~As$FsqtRL&rNN9}>pQEcr^G~E+B4lIF{J^`smHaBP~-1G*Uqrbahfz-G&fH zw#1C`^#q}+w>2*A?w0bt0Jdv361TZE~gyhIhEp(#5gB_9W;y!b!+& zz01nx5;=k&)zG4Gnd#iZ`UCbXf0QaCV09O^N$KE0OB^Y0e5X2HZ5xo^6ZtwAVt{vO zc7sa|;RIo2{n_~&x&Kq21*a};KIOXtn!=)@#HB9pTRbF#4+%ex)nx-sXd`iP<}jh* zP{dEZWy8Y4ZaRZ72PMDM*2Y)Dd>0lGk(887bn2&1hIEEDg=7si%NW?qO=gG(iuqnE zIfe|;P4Mu+pms;C7V(%F zU{Quc58~Gcw8#YlIR*$fIiKL}qv0V?Q947>JwO|*?S$Rk-M$Z3jOv9jWf*S6;Czth zHYh^Hhbl+CNV`G<))`-f?5XR{yw5c?C1q!C53VBoNW3u!K3*^hlQO);XK|XT*A>B9 zg%u1Tc}fj|!0JL6YIU~OC5Md@AmV*PLG46V5fue(0bMWoOf|tiHwQ#Y$%H$**qE3Q zxuN?wQlY5NRvvL_5Zbs<=&BeV=I~5H!Y4a~rLlwm(A8S`8+!-|qP_Z{QhGQcI!(Rl=cr8brZsevFt1i-v& z79bE*BSi?c0aq4UrEf;<7o)ByRR;CVlMU91aDveA4b%SCNem!%hb%MOIz7Vitr%?zVrQHvVgalDN#(P@;J6V zH8y}5NAQvbn7O(zaH@Nlx(qj@d@L{2KBmPE8aLLtNEASF^-@H5kuk={415ii%H^^^ z!s_$~5TP_!J=kqtb#?U#PEqwW6V#9#6^49ge77Tt24{8{#9L`e=wLPn81?VF3{URg z89vGhl4#ZFr6y0Tnz#_Yk33@l;cwEg$jZs^2L}w(9?aFFQ5J*nx+*!UVB9wQLp$6K zp?(EFpoK!KQa>zlp=Npzs4?az?;IRNka4&MCqe24yl_6q0QG|zEi@BqMF1Kb5{wYE zG(s!^7FJLEUe3f?*=;p=7@u z$8ef(U80a0WFK%XuJ*&Q)d|v&*#|VDm}TO*fdz#xjoK108D{t$1-%vTu=i)=v5`lJ z6FU&g@i!2EjDaM=+Oc%QzC>`0ZwQd-HI+4Q%gdl5bIT{wDJRSzhy{h*;68s0!F%T1 zVRsm`f=R*;%4#X4Ew(FAssv*yTGU0rD$|1E|5Es&v zSW=z!3{}jV`ZBUg*d(ZaC`lgR2*3BWYvcklPlxv{TduO!Oto|`H}bs4&9Sc5PmBi8 zb6`8dHC!VjlT)@VcDK`JKTuh0BZdy?)QiZGW!`Fj8|6|H$bSy4AV5VGBMZ&yC)9)F z;fW9;!gnWY4(oY&Kt8;j5_`?>k|Bn94mU!r^df*|(kk!G?rV%GN${NlqEtC+*ah$K?DX{e8Go^jyRhd4j0=+U&hHxoG%O7Hz}3x9l$vy7 zz#+G^WONgw@S`BVPq7l;W;5y}z!efpi@U)jL{h|d%z)R>myB7{!;TOVu451r;DUm5ewwVDIp+bj5|AwDqMSLh)1_qoCziEUIEmAJfo( zKwW}#9RJC$`ws8isgODNZbLqdN9s_*Z0fiVl&};*e}N9l)LG;g&k5Q`6)_ZUJf^Yx zB8T8HDyBEO(f;xG7V|8Tm&ySv4hLI!UOy;cpQIl-S5mXk`r;-30yL$>@Io=9_L-Av zN{Y##A0}u96-QT8XAVx95YzN1a|lX^?7XIN&?x6AtS?;-%Q1?L1_GC*hY2GzG{?nEU!O#V1(+nB#GJg$ z;WNdrfL{CmumJ!2Fi1}Or3Ttu`$b+gU2(MrVoEz}zv*z90#Myj(m5#~xUXzC*Hdm9WZ3llXy>Isq_Dg%kPX zrj8&`{YkIMM#wY5Ji8b=YQ8tR8TMxy5@cl+jMh=*#ngnKn;4aM-h=j#R&tH<(-OADdLF($0%peZj;n)?m?|jl><@&aW1_sf3;h_bZ0~8HzX{NI!o#upOjyE{8BhjaMfUaXffz z#PLI63PXIk;hE*!hf5;Ni*V#1ySKSu>10bC6G233F!M^bi7f^;Xb_I@{yTvn8Ao_N zZerS5dBe|~ zA(`l(MEO{bZkmFrty}IvX-bJC?E;wuYP_bo!XQ&B#~#A1g#tE=D{)t1n>LDF5uV40 zLrG!kRva28Ftpc%A~DA_h+r&UmmG|kiM9fnT|Wb1ZK0AxT^C*SU{NpWJAOP}*E(Ju zG&hQN_oR6iQ7w~_3gZH*K@@C zp|JYJ%05Xf^9KmsZ{!2Cs{?Q`TS1#}cfnQ!=*?FM6l7*I!c3z?t+HY)lGhXRVt z%*?1=z1V()%DJy%9!|`Xz^QL44NgGkNe5@hQQ@f^-X|8KLG*_)e5F*+%%x@w@wVAA znnl)kW?#k3tlBxv8pD^SRcFNKA;1h8#Z>ds9eJ18vNT}cYa={Ez{7$$TT2x*RefY; zDPVvQ8r9Le#XRM&UWR|2<&;EBUlEsqD9p$hsTn=-SfOO@Nf9A!NtA}xO3f}IRrf_G zg`|?JF@R%TJpBAe&UvJYD9gb#t@RJn`yfu(^g2&cjN9@KetG>cPENTKAPo$dFLZoz z%m9V%y?fIDjdi*2#f*xD5jh}_*X3j*txIOT_w@8+f;5Q0-2fMiHHu#VjRU4CYoTbWLN? z?YH1uy6TswD>^607V}@5oJ1{>^9Z|tCImCC4zLSQO=a;pHao79Nuv#aJRONW%FkVrPtoRuXY^jv zOsjFU8wvgiw2ui17%4RXm~{z7L@U0B1`V1fUv6gdAy>-cH%8f;pv! zyjE$lndfgxz}F<8EBUm!v2zys`a_3sAxj{&4#xIbjZ8_Bfm^A>b5p z-WmkD4b@y>#OUjX>)-d2MSh#d5OT~BdN?`-^2K zfMRH9$YKqm8YPoGiVJXVge`oC_fV3r9(K29>$N71T*3}OCk<)}qtk4pgW(FD9K;Q( zL`)TSd^K~GHfhoHel}*4$Q)$~D1m13_aleKI;D`x zRDVBNN0h>5Nc5J&9F>R>Ko@5GUQrNEA)(>21zh1Ch?(-ba5vMxp?R7}SpXx zU^k2i^g#7y!zgUOO|$ST;RUtpVDfp&&u{c_NO?(7IdQdHtxHaSbXC9t%@#Amyu zQQ9xr*v=JKOBH_M;p7a|lg1nW5zUd(KsXClI|%E772odg*fJv6mQ*4a_DH}TKnCl! z+G)iWGF(W|G3`Q0JVA+@T%WCz&%++4bbLBX6R;(3I#~CAjWw!DPR2gYDKzft>RQT0 z2LW9KP1l|}4VqgD_U#K$Kg)Zjb#4*PZ7JX~qoT=g># zBNJ1lL@*qwpqmN(d<55n00re|k zDMN5_`JJ~awW}O{wF44KctCjLGlfBGzfi4fr>mZpqzy$>Vm_Y6+Tnf@^dS?GZihAr z!fgFc>NVeGcWip|iJSW1X?$eo(soEwb8G$fy6uYchuoN0HvXIXm zhohqmOgy<*oR=EUFN|SmZ*OmYvs%*4JVAH3cq*(KnOLG@8ZIbb(FkvIRIJ$a7zBc< zv3oGJaXw>LXJOKmtxdP%)e<_L8JE8B6 z&*!%0;TVI7Crv+V^!a#Dt>f`n-cdBxF_2qWehKT_1;BhC=H8XM&7}1en3o9vJT*r( zKa-HkY6W0y04BSLOVqWR%EkTC{-_%ewmB|r44p4k^RE7VAq)u^l=!LlrfQ=hqxv)S zB;A2iH`uff&Cod}y6OS}6?yVVq#@VsIgi)j_&(r4QgGOgN)%o>=89suq=uZ0`5 z0x&|y)z$)o{MmV>sa%nz$9v%945S^@dlyvwDp#jgk?j2(F&qD24fsJTtQ~Y~t%4{H z%ey+obT$O0F-USPtrU+nFybq&aokR56P!CKgC5F5owL6IvZIju@n|XbxwzLBjh{3; z+AlIz#HX$eTqyQz0ks;vH*RqSVTl(QadwAf-uF~`LVLqAGnkA4r`UoJEZ}2#nQK-r zt5}(UD>rv{cV}lzI7Vftv8?6G(abi|Yn*W?4(n-()Tlxwa&z8~m3x02dP5)-b9r%* zAPD*bwD%Zhf)=2ZvgR3^%mA8WJvLPLXkEO~_ejYf0;rj9LIVkW#lsn#W&q3{*PK?f z3sNN(L~j-;mp`?FNVND^y*0`Nfr~I}+8=@_XJc)#hn|N9@CgAVm^FLh)?o~Rj{r=3 zUR%CFqwoGsPp|*8nW-sdb`P3A#YIB#gll%-5ECtJ2|(uf{01YN!z=ZagG;7<9~}Ys zBhJSrYt+08bxUYi00)SzgB_j^Qrrd%K`1b{tqzW}!l}OgW2Jj?L2h z%e(r*AQ4p_>9>g12gQKH7vzge6^0|h21EcFEV!5g)ENbK;V8{gB?(G#qWWhq^}8sb zU2btOY_)PwTUJvRZ`_Qua10C&22yK1-=ecZuuF`&v>1v7CZt(bkOVs7E&Y@*fR)&+ zcRvG7x!OoUepO4uPI~zn< z67&F@>{OJM(O)1(_U`MvMX-iJY!Wl)AveyJFbo4l*u+h)XhPE_qT6r{yJ2xF;9Wh1b5v=O-_XEm-kWBno?512LjlCX>tJ9cP-=o|@>`sJ z>g>ZzyT+#vT~$?m-j0qURBxvLhXvgHA#$F$7>qBitcW2k0(h~4GXZpH8vzuxs_Wn{ z@`nW`T4s#S`wnCF)r}n_A}Y7n!Gw+P{yT4bDC1woJ5h0T!S_EQKNa^ATj2Q2?eYJJ z`2C1n2J}6wk>LN8uK(Xy_s{*n)pOV6l3Eh8TxM6~T|>* zzt59PzseXL$o%2(x9Ee3yaCp$DN#3ct5~7`^B*>Ua0>F!GKeCgdb+~Mk+i@~>MhtMBt!Snwq52U_;a=q{-DN^pWOVNS1zP`cH3Z8|t z^dxo|eM-H}{9`mJ2Z_LX-E(?+StaIfSip$2;qNcMPESmXTbCwTFnuWC-t_!yE2!Q1 zuUC*|YU0V(bO>8LkxCLQ!U+qLR3fa|xU6pfYzx)CdL4I;8r!(gUP{8u*lOaRosz!y zoa%J5%lGqNIuM)_lB!aubl_x#(RnGM~5@&?@4GsJ%+CQ@u5FRm~-$xEpApkQcO zz<;cR_H%QyrNp~;qf?R5#9rSh+qlOuAA7IP~E>{ z@*t!cT$1)A9t1Yco!;y;SpEG$AfRyH+9#!{{>W5AF&uEKFKrLYxwHm(z!|e1Q z!FM7o!q4o8O5;Suzo#O)R_ydUrPdVJZ)f1nAnFJvef3mk;eUKD@zN*tYb+_fEfxG9 z3z2Yl z$Z%IdY5M0mrSpS+HQA4Cz>1Rkl<_^)vyYF&(onJ<|EyA|lZ zd(&aR!n>(6Q%k~1MFZ?p=uWko-^EU3M~AnZ+$f+t&}!%Ke|cJ1SOEHP7dN*ZV99=# z&2~#%K6g$pRpomB#0{8pf1nrs1Tf*fz2e&Q+`Y7|80iDpfUUOZt$?U~;YamnD`C!2 z7jpqB*m=6%OqQOWo|Tm~l}%Mq5#VZPfT5L5c|Z^F4NVycn$kdH3lOO~`Qm|3SWYZc zw14fx*pE4I>F2f!?V8Hq%FTEoNP zdjf6s^oYKX>KES~%oG7a56dyT+$gVc&;FKz_pfw%P-b(`07q*1sP@!eMX%ay`)h zP3DQ^t!LE&^*Mj5>%NK&FxwFhG?9|mytWGyi;Ee_<@N^rmJ=C3OTdZlYzgQ~rfWq- zMQ0(wS66@>rm&|+EDjJ!By2J}ZoJ&C`}a6)1{D-go)QofBkPKp_QNDLN_Vz@I*WS* zaZ;l=MS>}`F#5)hu_8=JM14E}Z8`vTaB*@fBs1`k6SOLF4En1Ag>4-@@qTM?`i24kpll z17)vnn~TL7yn#0$TBN2f+MCS!YTM%Q%WG#e5wNvt7Vl&~RToxRz*@6C@pqt+aR#uW z{+b3czo%B9^Dz)|*%<|tQ|1IF1gU_*egTLGP?q(Qub$49C;}W3^TXsOjVD z+hVsAB*f&Q%n3NbH$BMbURsy6I`Xx>%A&0Gg3=G`k+{_-R|2T@UPndyWSr4xb9_AW zdw#e+zRzn?-7~XqS!G@wyxY7hTEI$}*fo`?tnDa_hI4P@HAB(Q#A>s*ocG_ir}mdt zON^g38M>F}$aH{hi<)Kes zkzrT8bWh|B*X8a^4c}$>`j0mZ9G^b@!YV<<2@l~iB&*AUgNHplu%^RwVP&Rea5AyC zpCp&`1s%#rOMg6B#+?P?mt(A6uCQl5J21@#?|lN)_n&z_KpomySj^{31P8;Tx3{$+ z!txyV1qKEJYIHNv0i!=53Bai)Gr7a@2i4l2AFf@~ArRSobj{4oX^Pvxr%{1)4u3Ty z8Gh1g`i<6*CD8#ABixX&9zaMzKL_FHwKz-~>jMfZOlleNg(5SxA{o>nq&q;K2B=`f zS3}bW8shXj+S}pa;nP`klc*6utPN&JvY85DtbQ$~{uWHcqHWl3Dpr)OZQ~N4D|JvB zaTzrhd&P4cWKth2S(>swvYJ{K`fl(1b|y9aeuh43t#%Io)U{3i%jZY2Vx)i9O0Dn= z0#euyo-<9c$)o)+F%ky%ZzBl)X|!&&r06o3NLTV32m*srBY>U{kM*tAy&}>fe)-be zg|hY0W%$f&PgtkIg)fnQb5x>QKhkAMV{pOBeI28Sfuu2QzSEZ&V!#!CPRtB&RYR`Yxbe30S3X~z)Xcon=&66h%EpZ=hrX@ zK)~zR$)+pKEiL}Hr=&Q|U|}tmFWMR+&~AYMK+HbZjX*HR zfu?pjFgH1ZT$q=GuhDF$JIKetz`)6=ejYoP&WMa8VPHu2}Y)lL6(+6I;J!om<=uUmVYi?WBBXhZ;?PDc(83Y&1kFe1iHJNkHL z|I9mNa@*xeH`mtH#Dy+u(auvROQo+osnllt8PFP`lkltUsV4YxKqF6yI}eYHtN>v* z^V@+A4wKese?Jt~($o|>oTUm^G&I#BzL}4o-$WFO=kwK}roO(uQ6-l}MK4Bf%9SKL zrb{yr(6VPUGBOZi4^T>bOhqKD?>!`_kWWTHjZID5*>JIHhrgq1I1XrVnBT@ED`5gt z)pwY}_~oc6&Y94z`6nbORRa2}9Ykd3f+Z=>Eib-qgF^9Hjz%K!Ei7Ms4MC0c9=3E?%m6ih&OnL_Edd=U}*p1`9o%QE;>L3WKVtUJ0F+ah@xLI_T zyx0&Y(_e45rd)v}P4Pym=-Z!YzDM>9-XkW_v9&g;NQ+#a<)#we0+LT!T4e>f=eyMD zth&bK>X#PPldDhFW@EO%fN;-)_t1J5$;;*de)gyG4~2#dUBZK%p7E`Z-owsGYiJYU zaAC0HI&Um1r6m;luL$r`V*D`0^dBzwYemTp6_@YFJvRtsPVX|@^rIi=5#F_u+GOC& zy{#PB&|0s~E=pYg(K@)u!|Mp|jOdf-bf#ynVL((}tWBJum^E>qfZsG8=p#eKjc`Lrtc*<-(1~hRMCkH$At!Vmc%dz1-T7s z8X`PA^!MR!LL#C{H_8i~ZRsiU;Z9W5rn-gQsWLj>&QEXp+fzjSw4FRX>8Yu??RQw( zjg|#prxbe1>YZmkTssOBf*i6~{#Si($u2GQFz9diNv@PuxI%e;7LX_cZj2jjGNiF1ycf z&>O28Rpt@EgngsbRa6{&d{fi?Q1D!}xI-xv2xR#9_-=K!^OZw*Vhj}UFg#@Y?`f}3 z{azleX9`9(22n4%-gow5Go%h${- z8^SDo4!*Ka_h&{&sH%&G6xVP{&5rY5X{vGx`!(@VdcyabBjmk)c)L$A%Y{_f+LrUN z3bF7g>9i!&M-CtHU(uS}2z`CjPB4BST6wqz!eY(3o0g=y)eWG`D6OtgZvbru?4L)E ztXl2IJNiaWDKsfr*;QxCGUsijgzR42;q~~tFYL|_j5JP#|9sqnNZ)@{=j-JZo5F>2 ziQZ}l(yWe#xMYy{i%h4Ar8I-U=D@KhJ2@Er=eToq&45m9XoUV9e#^pJXEa}l8XwdZTuIgIiWU9n(^vfI zgw9%HI^I)*AY4b!;;z@H!|imcVLiPdpQnTy!nam(msl1kz&|=6?~Bf zP$dD}L1s=)4wRJuhambJY@&qh9&w|Ev3_%JlN01{z0wuOM*f4Do9Uuv!xskEL%D=- zTO3z%u(0Xm)yP|3FJg8+k=>Q^gCFiasLavs=NrrTv&mk0Yo5p>N+R5zIR&q(XHpV+ zsWo=3yLzs7{&&v$?;>w%{n)N<0u^_Y( z+@@}&X8y}c4P8w&*+#O{xf({9AOQ&=u?tU>|C&l?*IO4D+Nty&a_#F=+kV%9`ejwN z$J*vjbetj@U+?{!A7r$H#ipwy&Qkl3R^Z6(wvOMRZu5*JrWsGC4>WIz1zw!$?iILq zg@}O!-j{8VZz75yI%u)3KyEHI#kq?nX5+mfK@5YgXMfA(m@%8_ZBEXGtQ!_1=>{ka zcOyw(bmCy6eZrUo8)o7WE8>C`iar&`yn^9Dl@eL+A5WVsha}6?UcP$2iYaP12W@Wv zY5)6>5m0xWwyoG?DI(uZH@co9!$X_K?rG5HnU5}MBu5AVm??TW?kNG{-ZLRFEY?(7t zp+@Of7+HGzX}55oN}<+#o99q%t7h^mW~A0Qs?2&{tF%f%$?XdGhZjo9%KRXRN`;7x z>FMcm20~nQ8yhA+CbfKV0Ghl@2HH}fr2?w9I_}Vr5Dzb}x8-UQ{AEFtaGrNy%5>K{ z4~awI-v@+g;vYa9qCR3_V`F|I_^Fu6G=vF584UBD>lB9}!w*dbU~>w1{=Q>V?)+F#V$4c9f^ zHhXb=LiF`J=Xbx)L$@CzQqt3nVZ6z#BwxCqO;ZYxD5T2zKy6!0|5XZm*Y`1`sQ=&~w)RbTDnYQQAg(R;*}r zh{wbyzI}Q2PEGS?ggy>J@KJDS#}zw}?ytEl;OOh0&eh=>)LEE8IHJ6>D11ys@DycG z`DO<#@Acbf(KSAU>3SjolVkcO=N+VB`WMM@4rtyWcyRJyxEBn2d;I~65G=?;;Q6p==dMiA+eRA~{UyOC~0y1QFS zxSN^zuXXRMyPoyTnin(Ua5(38zOnbG_QE4_Q`;_*a#n^^;Uc@0#SG=bIeUjMignh> zSZ=ZYgbUh#f^u_a{(Mm*lmvUc_Tqfr^b`*}w{RnKsv3$0Zwgc8>l#yA_jofu>>N_L zzBk`S2QJzEwD2bSC4RqWKPyMZ5@FxE|az_=}5+AncdbbHMjU9Mep|q&90O4zG!CW-!XG5Fz(Cb|Xf5PL-6H`nmZ}7Kjbs?P@@YpnSfuxV z$$W8rne7c-8u^MGlFd_n<%=Kg-3{u9Pvrvizx)Uoc&1$)l@Xa(-k_zA)hD;2@m`Hb zQMf|V$958tm)VYz=nS{3cro4H5$#Ol{9;Pqvdn+1G(Pne)WJXR{}I2)6TRlPxb2AM zsNjl)o=Y@oM5F!p^JWnbzPK9-5i{|{#{+7bn5p!XG6#1gGfOw-#UQ~3TmEPh-!H8j z8)TL4?3LQ`b$K|wCT0O$7Ipj@XQ%rN>;v3JtR;5}_|WNEcQRX~X#{7B{;n7{D0yzj zpI+oR4)JE$bdw2`+-nWrXRkeYZzQo#SFPNv#(<=rCWnq(GEQFB10ct?w%4OsWsYTbdfebl0$sWo)=+FTY*qv?osOaB~MgcMr z5S*ljhIRwT0bNbM-pfcH{HY>6@M9!*n?_vKxgP_@F$0_q0R#1+Fk;P(XiTr{rN(!C z;kML9QeNxPZ*8GuTHg|8OzD-=Q^|k~?vn)s=vO{H;M%gTuCA)85w$;(wsPjU14G{# zIEUTM^J;iznSZ+PUV0zOL~8BD!!FS)gZE=^Z%y)2a_|W^^UDuC2*~72+uIw?=F(K= z!1+pg&a!uh1llB{j4(PK|JC02S4K9OaP9B6ta`DfEk1D)R_F1QVwk>u;(=b~XKgfx zSKp+9N+nFGs6qBFx9*ZncH#LDEvwV<5I_FbzyN}`a~sl_*y{dWc!ssq#_elq zT55Xz^z9z=gb>A<-OJ-^`WO@6=SR=iW@ELUxRqKSd5JNc>K4BFAy8-YD?zqjb7wM^ z6NiX0n-p#_t^MCm4Ix8=%v&551zTonnlynYf zZiR~dIX!VVbE?CnvO1FpZ#!)M>A4h{sPqWr%L6p*(aF6|u|BbU!opPvW&A{y`f(<< zwjcKY0&D-`Gzu4G&T*%GI;kWEut|`HxlEK=dHmTCcbM}@q&PP}i_mu^g+7W|C7DG$ zmf{5XxlI~jM`T$f*+6UpjyykgU{UW)sAxOfP|=!1SzHTY@0%^+ISndpGBgWx!O#Jo z?Y6F1HZb(TpBKAkyqlY?`0N9;(`OpM2+=|jYe+-D8j%*qp$7?8W)GyKq*O51`eYT@ zTwndnY4v%gEkVlZz0E)yN>_HuZ$qSk(9fx#hn>beJ2eEY6t) zd41aj;~VsJx*}&zOy1%pF)ATnYi{|>OB!ujMO zdjT)aQ|X1$9Sz6pdug<@tzB=1Ds(+u83?BZ`aK3uiG<;Pmiysz#bs7~Q2W6>)JDVJ zutq_uR9Y%c%#E-eE{42`3=@w)ck}obHqIK$fpi2r%;(#`^0iFT19}3?{7-K4Sw?*L z@S)U$Vo10BdO`YDnxlK(?E4RGOmi-F<@mL5! zExbyLcDONZDJdx_KtMr=aTHtxhqP*py%TG45N{`t?U`=er7$zZ&I?ku-W&TlPthHRtsn zk?-K=8l0chvb_deZgny4nveoF7FW|xdRpZ%lR@npUw9-6!R z_20hYQDl8plKAQ+AEwC5-P5ckAB1xELhQZOuNLQy9tED)K`-w79F8ow^pQAMEK z;*Y2zC;w4sE(Q?HhSkTb3EfZ1DjfsY^mix7UrSvh=Y`HbWaM9G2t0KYRJbb?mhMav zI#>AT6q7srd-B%2^L-@^gxi#am}+WwXUpEv5u;lsd>Eeb!o_(kcSpMu=NbO+lRFKK zJz75Jbg36kPBb;ImQ5h_4qHr70S%yuz~mxt5Ap}ky-j3l6E*;{#?7+l1wa> z`sfcB0cAp46FZ7%%q5}Gm!67H}>rHqwcQG zMmEWBRi0tJ#lq07WVU;H8UCh+oODlYzH|G!uqbdWWn!AdwrCBxSznBV=USs zgy%|DS0mS+^x)q5L_^c1I}f%YmeyHeK>*aF$pxYR2&|d=zO#2q2C;pj=@2=NW8dcWaBq>cJNp5)>icl{xgG#yu z`O!M%2}~{XdNqlWPg`X}7S2eH>6F>ubZOov{H~^shmz=f>kCb#BcFD{9C_+_ zU6;-}il=bwdRKH(5+xZ~wtafx{&tVqP?j=PjvT;a{CDp!66yzc&tm`?xF3DE+)d^t zxm<)oc~=Vic*kJhbG!5rxnl1p$^LP1gn>et8ui0jg{eB~dZ&j`xYH^qz5pA?f85xx-HstN)7pCKSrTuKeMVq_~77oAk5h{K7X@<##<2Y6P zw9me!OJ+yxm4}Bz%Ewe`X*cUTv7M|JFYj_c-PK>f88a_sFVH)Ju* zJ04&3!$E4BES=)``#B z&l*2x{_xFHn^6_-DGyJ#V8z+?A3>vM!2OYIM5_fTA+94arL$YSFcARB)maU8IE0Gb!YkFmkpi z5ikjflveZw7)ns;{|J=N)a5CE*Bz;(m3mrv3RRgNS$`I5TXHLILEzs6pe!3?aj`51 zsvOc8QvOQ1Kz<~6jU#pMc(YIMy%FZ@_AfueY=>GW8_L7*8o_^ zh554!BUUgSKvM47AE*zi^r4lvN1jQ@yxy{<$I|O6n$~$rJyAjVfGc&5X`F&pMDb zL@&%>3z-Cc`CS9yiy=W=(aMtmF}w_!bEO=7f|z3K5VD@QL+9TdnhGy!SVf+;tg0LG zw=Fg%k)8UbDT|QVPiBuCgj~<$UZ@$shChuRjxYKkY+a=HV|F%<@=MngjKcBEXkd+py8-X!VKIdiYMPkV&& zwr!g&vHIUV*X3??UNg#zsPD=&NK~@GsDH7;8#m{mR`F&qqvT#^twA{4^bi5Sp!cFa z?}m5pAULS3taOMP*U}F^pyTlJ&hb1uY}RCa;$-S{Y|zErX2-MG5aC@>=TDhh@+JK7 zb5>lW8byet+p{+OWkkEWyuR#wp)zc)tT6s!`09k4sM+lFY?zGYEz>ObHzkGQf_kUe z)XUzip7#PWPf4bTuUNv-U;9z5Q|7Qw#Oxb&Y~+5dz4L%;%4k-*$dtVWxnBvrDN)U0 z0B3Wo>iJ37@v0DoIc9au<2`^T=*54r80#dhU#`?|&274Hn+Es8-Lp7X;*Yh;$;ima zXEplOxXCw-u}F|W6Nqbc8B!=4|1&!i_p>si`>s}*5j6LSdmJcq#B<#dCBJX_KmudS z45%~8EatV`Fm!8QB)pwF$%^a8 z^FtK`*50|u#6|B+q+>as-VM)t-=L&mag|lA|E{Oz>9KBb8bJKk?S9^ObmC@xsC`-4 zxiNfm8$Ukn=5FF8k~zUuF_DSRHX|3j9)=GCGEdUX(kPa{-ow|vE%IhysHiQ6*Ufi!u*%C4bvtQgG3aAw z$oLNvYP#qs63o8I&7=L6_?WLxs?X1`9t&KAejIFMNYDJD@wfs%{up@etUAR-^vq7B zEbC26YqFue?kM<$!8U&E1J}$(S7NAge3!~Oc#P@<&+o}xbFd6UROFSPS%!}Q-~FZF zt+^7AOZCaywJ^SrJ?+v>mES&f`9M-KTPHS{)K5J zZ&W<^h_I#?c6J~f_h5aPIhM00O|qe^>`j4g8TIYkpv?(3BKzlewO$Lm>?G&svt^Uw zPrW0AY8_g*7FcwbpZ`?_)+0>vs`cL%b#`)e92y#Wwti4rS_<7?3Ummm#2mUYTLbVv z=M7L>!oyjh0;NFm198yJ>S-KfJ793OF4t?+3}ah0}yz-vu2H z09vk~>wsi~|M$-19(;dyBj!Smtf_pnsj024 z4Tb_pS24D?-`(EcUS0+uReFi?UujKM@W{vrXyiB6*QKQvOF*Lwsh&3xoWBc=gnM5t z{=`8;3rgg*Xx78S`*`*D0+`|37D)oO)sD*wR(>v?o|hnMf}Z(j@;p)eoJ?W{CMISQ zzja(m39r70h)B8B@J&jAm6cWUCCigEiQC~|BA}$9`Mz;`{t!$EuQ+y4;sK%)b6Uga zjIWq(2dEDIJ#gto)8=GFA($Tk_wcvdJB9`u0t?Jt3N`Xa_vTyvWr-R8dsYN+^!j&F z1g?T2W6>J4OW@&8-*bJDD@`j0XpR_U!@>Jb1P(PZ@!_LKyJJ6)o@uy>qEAU)U%m#l z2Y}H(fBuA+jp5PJz__-6fpZx=Nl@58?hM2Lz%o66hwp$PX`&p|sH~(ELxHM~nAV>x ztOgJgzxAlLj!v%ABg?>e*>y|dCFDS9q89^76Ob+-&V%%XR67z1<92o9Ni|K>W#&jb3XXXgsmdmIC@w1e3@kigM8;=v|*^ZK=4Ck16> z%HYt@OR#d`f}H-lF{!_MEo2C9jr{-z-25*NK9-(}fs`V*!^u@?AsLO|-*lp_bZ-3t z`!yT*^Uci69y~x6#Zcr$c6x`1Vh%DLNNt&UA}(GBm|aT@TeWD+&P<&LUHYIrxE>zv zFSMPd#nduv4MSMSyGW|?mszRO@I5D|q-0zX5Dsob5C|Cw3BTi`6QTcBu-CvcIHCp32ZpR|Mn6J9v)tYYK{doDOZf_U+}|=eCl2IeTP}V zy%)6lIhq?)3xX@28Scjzb(ktDDr&pY$&XWL+Y+4qB&-bcNfw8b0=6pcHxooIono`V z_>S7=Ha6^^eYiuZZ#G`YU9lDsW+5FAr9>`*C-3txAu)05N@7vtRGo()9o;)vIHdo^ zS9OpSwK)jgn2RJ`5a9?=|E_;_3Hi~S`i%(L!p29xe*Y#*@$dcAdrqQ!rmU=dd~#9` zmOJc5pfk_KWJyz3`j7_HXLBgx~r; zTfcE~>gnyJD?H=fa(ry2J@sNnMNjXOvY#E;!g-p(i3dqafV=^cDp>3T@A2QzFM6$; zTq+<7y}>gsZa_^fC7l z8(AMbd4iYGy!~Cjsu)(Xpr9bw=s^A`A}st0S^D40h+gp4Fi)v2ELj1&sWsM6@bSnJ zq>8$z_$gvQGE#=ZLuR^#j1hRg2~8xVMpGk=jmCgs1Mjh{QC2F$&&%7w%vjH(rwNV= zPjJ@+V3A2UZ;ioY4WQRP6O~z6P;<|=Sdq81%-O>hV^`0?KSza3uK5_G9Z{z>2S}@d zyMtB-ek5c6O)@mo|2-Ku+DJM*lh?6=bd}%<8whnEB_wRNLWiqrJs`woXKH_cANUv( z>vdY&**Xt261Jq@^P8J&)p2!a%WwuO)C)pn0K!jW%QZN(i$8D#j}!k73rHMyTi(@% zz#Ir0%2=eEV5E9R)g<*t%5`XDq||2o#!PjS=BHqv`R+rl_3!WK{XqM?&@IDQkVO9{ z`fh>BhFRNPItM%qC7Woj2aToS8mYoJo}I6HWuA}dAEm&70XyCxzQ zGScAu@s9(QmwhDtL!l7Q2NAS#;8p6j)|ZrQfbY8mqf0E}3T~nY=n#nH2~t7BuOQp8 zhe)Zr&g-{|D(ISbz#OV;Y3Uqt;PSwCE=yWkde1+;dtess=E*W^vXUo1n&{f3)=mQZz{ zn|o~}m&oQ9JXq5_;uRAixbO$SXARE9#*09Smevp=IzE%Xx5Zq!j|8c|XyKEPyZ|X( zhAdj5iRI`w3JJKR>cK=zeY87kbSAn$TIm4sFhU`>J|!m7?SbN|ay^WKH@(3`foukg zbQ9(`6l(uKbaVsnmkmE%DF0A&n*Pqh0*~F45b0Z$1A1~HANjYf!3144+%ovrDF0?B zdLkiWrNQH^_4xCh(%JG~5pZmjnCB^epdJ8mZ5Lu-NX#gw;}llD^-josMl8qBa&}-q zb<71mS2dgikbr~Ev1U;11VMQ?Hg)*z1Ox;FNkHh)a2xv^b)(GBLjc@pp%I?<9MvUs z-QS*Nc7Y?a!1yo%azSK%FWjPp?8-XszxTW!RgO1!OMWs%h#wprgxe4{gesH{nfAz4 z*xl!Fms9b-L}sNXB@I*rXt?IhW;v)quf$p58VqEHBI&{D{4L+NAa+H=qhW`TBESp% z#nvnr?pRn@{3AH$Jl%igymu^0&S_!Y=CG%8YosT8*CEO+hlHBClKrM!1FTYi1t4o_ zh}y2y@A*I_n66_&66mw?_Tfyu*JPoQWbi47cv%nQNPSN~9!?DtrHTI8>SRL@S8{Gd zH5}6_-)Hh@dnTYf-?%s2ljtq!aULeQxTOA~PqL(}j9>T@rfttu71oqp&A!DwjqiN+ zGt}5UuNtVq4?JFP8x$2@R$9iUS<3bDg+__y2iQG>i~_#2O9=B=Y=!@b$%A=Bi!t8) z8ih4lZUG}8!aJTucooSpi1P9L4xmLR7-vj3Q4I+R$G|wdjuUzHY9C^|T)utobWzB7 z;;?)Wn~-qb;EK`fqhS5Pn?5Q!I$%AK?uzU0KAh@uM`=lXwqkEDwe|F<;CablKEj~h zANEb{ha20Z8rZsfdYJO~{^p%Oga-z^;hGr!R$b?`k9Mrof$V(V9sj-5cGzU`XK!9o z{7uI*B@1;pJX4S!-$9F8_P6QK{-ny#tniN_^Gj+A$FBnzh!)W5)6)B2|URE4Oq z*=zlPWJeMM8~gm+{r&XUdXFH+g$5b9Lc=lv7Vb5;2QW$QwcfP1l5|Z?O_1_qvU|>{ zT}1n>&!$0)mzURa6HJ<5{4hfhW=)5PEc+QJpdQe&F{XB>@mymi3O*_CUp*PuM8l&H za@}jhPHB2D?|wMi6`KzQ?0BMDcAu2zlN5nOCE}akaT~TZ6w9JOO@YZ5yT2&R(i-$} zMmM>7aD9?)BPrwiqjn-77=XIG->ANbwaTJD*?P9#AaHaTTe{%Y@`1)lAme0I`PsqB z@$vCu@SU5QYP8nsFY0lH-8dBEq;dM~AJZB;p&-1OrgBXI6^}Sm=#KYG;|mszyb-wA zKz&&pPo02dYiX%nXm{IrYkB!Elx>{D$jHch9^Eakb)CN5vb8OkX9#p&pGfplR#A~~ zT0^f(iM{DO(PiWv%?V5v?|#6>q?((96A13d&RX}yc)Vsk*FFMF& zE_Q^D_i^s#$KY?K4hn=1bj`hWUf+Yj?X!eiNfJW^OWV7F1Di1+YeMiDGc**@>$;#BUni(Wt=ng5>W&623 z-MDEIUZfNho#DMbJ=C|^*cvF%5B@^$Ws^f^pIWPtO#RB`=70f@$!6yd+pTZk*z+~> zG1q&%j1;sR-ioWdF+8nxJzBdj`dueK@Pl&5n0D-J=E2!vJeWVf>6V>j&MFQ*p`**N zvvaAiois5{m$VrhNUd|vIXZt8d|=TpM#iO!`GX~CWBYpOdhCbyn>TMfPPQaGTUE;` zUc)$yz~fI-)4TKJDerKoey=nuCm|vEX@1xD+TM8cG%P|BC_iakxCLF-hpfi-BnL8| z&fzT`fTiI_=jP9!a#Vr^h3cK})DXXJTzR2Ec10WFz*qUjbDRy>NH@5^xJ-Cm2!7zN z(E>f5!%$-fY7o} ztkZZgiMYqnyViKJnwXdvIIZ#4B(>e_`5(e{px}rpI;v$?qe`AvVLv&Z`}{YYBP<<- zpxED?^)7l5S`NEIYV|=XZ^v?zEb0$-zc9nPmpI=hk_kbmv_1AC8l`cl%dsm)Qe)%S z&3lEdADHYJsj#dNs}Ogu(`<8E)CIP-LOf?d6khYP-(H?(FJJq~r|ous5;XAgG~}*AjhDS9|M6l|4r1ZjBxJF!_^nT%o;%}JeGl|w zczCQ6(@TC%fs-brkB~B-QStCS@q95B-p5Dm&mi5qh|?}5;{tkl;gZ$Q`3z!KCMJeu zdC^W`Lh`e>SZ@k#45FW%pZ*SgdGsd;PYk4^;m@_t@o2>O%sOb>h=e1G*5bHp_?4!I zvs5?@eGSVV-h8|VCy~*J6R`Cj9-1pj$>JGQyFS!F;Lw*qFPzJ`rk|y?I=IZ#55~rTj?Be+`)z;Y0(JY%pp7+xXa^RQ%-=Hsf zojXa<%VkQkc&ap}t(h{i)nzM7?eOgW1Ybp8U*Ekq-7B^#DjLMKzfi7PPCTz?D#RNm zT`V8fkb;d5Ad%jsF=AY~(70aX{TJ;;OtQaNy$c-9(o<8Ra(g38@(x$jdk0apyoBw| z+Fm_Ggkb#Q#d|wl|6DOeq-^{*#Kl&5YNhc&Lh0;c`xU%G6xatXjyFi?3p(ZRB0x83 z1;-T<>X9E~K!u>boudw(+1kGMlRY&}o-L0?aVPFO3%IkOJvMl&gA*Q%$^rsov2gPtxm+HwkzmV2qOPb(ef;h!26 zdgbrh-Vt}F#x%_8{@qjvoqSh}xTj&UnMrovZ4xC#9}?$xO^GzS-Fn@T z5u_VM^eu5c;U;0Tl_tE1tlR872ffV?6;Ihe3A`x|2oU}U3Pg^qTPpGi9EV6I__Mb>9OufX`2VzytkZfef$F1xc# zckgy!kA*~M)7LjpW~pW4Dkll_4Ok>RH3=x#(xwnN9UEhxYNh-+U3J})CVXgrSGJ%a zRC?vW?-mchXv5_NljF(HxlNaf%%dKtVlTZ3CGExcIw8R*_P&-*EKYcCFGnp(`7G5- z-1C%$y?3Yy3O}N>7f7fl`3Bjf|9yz9#y(>q=l$E*P1^d!>Iva?_ItiRqG8|sSc?VXEBhfyDX zXgCSnCWKpM$?s3k>R~w)n+mEn#&a3C?ak56KFrmtIH^5*4Hp=((c1F*_Qp9!F5N!V zs(DLxy}_q<98Y!IaWjwN6tMHl~(U$0t|CTz? zyLEaP-+vJHg`US#4~?FXV88cr>gy|p7tR%PxjOl%v+=4Re(9fkBHunx20ontx~gYP zXfOD3x<63$4B>%8B!5VPPIVPfU;PR@q;R+`r|oL~&eVMpYuCsZ`Fr1K@BMPV!{tK2 zjZ&jMjG|2@*!PF|v4}Z;P#%q_U6^~}^gp5ez8zx1VgY^`8_#6s52S$}7XHNw{Vsd! z*4XdgTr4aJ!^k@>(>jgmeR^efxlysiULm?=zr3SAA0R`(jL<90z7Vw`EAC0)FS0Rq~Ksu6)l1=nl<6GU+Ko+7twFsTboyZ(bB{Zml#V-X|w2Zg+tEo zh?zx1?dtvaUN9X4d^F#%W;mSD0=}HQVQFQxKbgsNmyy$`oCkoKZffAqrJH8`DoaWbaMw3ScjFm5fwq9AR@c zyS*RQPW!T7B{ErFfzV{Bl|kN!;J(*t-!#p%M@`MFnj>eB64WLP%?#r|KGAFpJ@!8K;BEM zi$7E&rSgi}U*KC&QdCs?)dFa5>(>X)HZM&P83eQGQBluDL?-k}Atv3hwZi&$AF~S0 zWSK`z*_->$5-cp1TM7G@?%?Xhz&;iHKYr+sfbej)X zhi4!8*(AXU4Z87vcFBYAC_QAppF5M~)-B;C27w=RQkhOylz&6x3h$JsD&d>XVHTSv z=Q8uU<2s_a&F1L#^xMsO^-Q_Yl>#z$ZRABKP)KLfGuf!8HZ;kbn}4#{pZV!_u#~Hj zzcS-h1Gi3V-|0oeMD>fBp7Bno4o%TiPbfIkV;+U<7EYM&-t8Fx4DIaE2TD5&Ed|#o zLy^b-+}@mYWOZ!?CM~FpMU+mp<9-x`SIeV@eefLAj(JvSDC-t>DqG(BwO)uenC5eP~MLM`7&a;_mkH&2OC>Hr-2BEVQrWfzaT`r=SbE$MbH)_eB`#>C>;Ue1(>WK+#k$kz(;NQ>?i-UzK+vO&%ysd`R0>%*!a3q7EB;*nKjie+$ zDioP7?8ZJjID8?%v^c>}Huy{ihx1O1Jb*`#h~v37a1}(e13hUnpVjBm1YC4HpWqoP3w zY;N(a;4(TM-V=Fw0$~USPjdrXpQ$N*Ht8kjoij!j=dcH&j;A>_9_N~YkNacU;(us4 ze_7NJI+j(s))rx&R)8DRQny=DRRUX!iUY>d< z1teUGyS(>qN3;21{o6L;?R;otB;C~*P+<Nut2}^V8+Gj=8Kin5etJGi6#lO zNW45d2Zv^Xq0SfcHz)ja8jZsi3odq8vv%a<#KeSzUAY~hWc5YIj>1i=U{douqZeVF zia+0vW`1e2jaE~Z22T>luOgsXTn^-etGxz|=IcN(;ZP6YeL;Y`>&^O5h_KN9ZfAF~ z%{3gt!~?ym{p*zO+UFLW%>4XszB?)H$jFiQQ{}U|=vD&%HHwOz{yAxNkRj zIhkJ-K+n&(R95k)V%e{+59hQXpoD0w*88FWOEfU#C^N007meEmCIZHQA6J4m6Ij*}P z{Ow{qwtSH>dJzqK%cxq(iW&&?SA)$=0P_G{>4+v;<0WdWQ0>jnd1$<~4~j|{gc)%6 zwUd)m#N5^vKz|NkM0?@fa`_RTlytQCg(71gP#{7Jzfqhsfqt}|t-o!~K86qL+j}OP zmW2h~5qEJLAn5sjJ}oMTuE2Ay*K%h<-dFT&Oi;<1=67)?LA(}K@I4jT95W4wX=j9T z#-?Wo_vP>JZK6W%OWI0dm|hRKxVWxJmd9m1^)K^%>^DfQ409sFI}mI(JmYEU>4$zD zlV)Q!zeyWt?M)KY;6^(?=Z_!G?kidZ894;j2V7$~0}3ivgp%UA0Wo07F)=*^$dSX` zlf3=kgVwzh8AU|g0fYjmQW#z9X&nb-)Nv+93^?jNwJ8V0pWz$C%){ktd2Fn!?p6%*L{)2P$>F> zddkM&ugS^h5mhQFB2)cJcEkhtxn-N4pKLFe_SMz|)Wa+6y(WM4b>=Em9V~+?E68Ma z=C645pYI#$ua9zGVa`x<(tAa<{IME8KR+MB1qZc8-Gm@kPj~zk^ONLfj;wUeMX%fK zviM7IDxkZq{}iM+9vel$P%qh%JHzCHBkxW_}19;->b-$M^;**z8yN|f)~=_Xl<{M!W|M@+uH$rmVn-=$?}&!29X^Y zI?iruY5fNJwdwMwXrS{AJwDlUC5b)}3advtSSlQ9wgbP?E}%_AuQ1$c;bWJ#A$pZV z_I^_bllb6$j~n*N2@BN(8waNK&JP@|PXsrdZNVzHSX<7y$DkB!3P1 z1B-X>r2D-*>;I1Ys4Fk;D(r|`nQwUnP=>s`yxYMM>rc$s+KqAE{?=A|MWk)Bx(P}X zD(<2?dPji^^-!t3xa%l|wKMB7_w&OiU0vOz=}%7(=I7`21nggo-kvJ8n-<~OK8Fx8 zD09u)xeQNtCM%5Om(QUayDjQ+XiIf4GS;e{rRYQh^eQO7T6FRyDUrQ{O?uqc&|v4Zyi4q8_vUO@XA{dDch~*JC>;@;Q)C z`HhqPX$@Or_110gHH2t{U%H+s?FKHJUsg)~{o8O%jZgOWBLRWxFM#n^$R0H-y`68) z#OCLthA#dOpl(~%5Yqqy7g+k4HpPkx;miWXJ?(I2K)_jPZuIyA+=*Y|w2t@!(>jk= zZ2NPzAu|E-sWyayVRr$mnZU5*mqfH;G2#!87@`%P0!;?o2R=3R&9>xNdWP^}{-S)A zKPP{)0D?107L)k%sJzltBIE%I)zK}M!_lvHdNKTy<2^qdO-CcSysm9Jg?1^xvUWIO z7)Y2bxEg+xk;S7Bv+3KaA|QyQkIzBy^7`w3vQ^@=?#M?|wmI^0rj|9n?Q2%n&UDFj z#rJ8G2@4Ba`Sx&%wp|Nm<$c07x8-wy@z#gUcP2e$@K``d_&uB$s zjHi+dOl>Ag*8%*0NVI(u`IpAP2uUR_F+QIRtLQKO=B_aU@skBshf4f3 zSYOf?&AI5GS76@4beMo z(6`MQLSUse@Xrv%ZQo4ys+tqk=(NeF9ojncy6;-9VYmPgcSpzDZN(IY(tNNRul zeFSYh#GcUhTYMlkNUZVP({rPJ43Zs~JA&g84n*9=+91Qaaem4QC8K1#o(Wf{-Zm3>}sOt?H}v_M$M{_m+$PZZ*anYmBwd9-&*Z!O#26>`^?Sn zuU*xlow}coP#l>hRapLn-2*A1sTm(##Ff&%Hboc;3F#4W-S*c7Pes79r{?C2Nq*2R zXJmd136cI~Vq+85C2eG6q#hu{k@3NT>TS~x1uSNL1A{~fztu{zy!xCR7QQ_Qn}O&W ze0k5)8;bYFWrx7BN##!EPPWpu<8;Z$8+ZKF~Eowe^e0=ZX!&B5_;fJZCt zkH)eHfVqzeIbJ-+z+J|)AFOOl>>>7mtKu2zddR2G;%&@FN5B!o-od($_KJPF_I)u= zi?)=CGK{sV^5N}}CL$to#=2W2lqnq|!-et?OYj^VaJ68Oq-AD(m;9BA8@e`_=0sQg z4WEAV71`PrfOGfuNUee|&o>Jjo;`c!z)^}&19Ju5|LX}7Bh=n+c>->MmYsas&u`l5|7at6|l}TA#=oXbnCj8sSl`Xs*30G{;u;qpyHs@&J@%dFdo#n^~@Cx-aXQJ z=_K0T`D+*rcAqieE&Cmt<01XUB|`+;7cX?mbbpmt^zB>l6?>|ptKTl2F3x02NcTKn zvg)Vt+GCj*i(vsz?TqJH`YY(2!@|P}?y0cs{<78_^QtiH#%uVsBQ9#}8tC@PAiSR& zV<=oPp0y5QrSw0~+)aY**>opcL{2Uci?qjjZHl(V4ukq=a~7?P#U?Zk7$@m{Jko)M}Kj_ocq;)uhU!?jP0v4+}6E$bJ3j1@_)r^5xm8|9p#_jPs(r zVWp)!+zKS!b`S~TlWWnqBEpN4LINiszQ&FEB#K8Zxklax#kwPc4m36}dg;TFhGm#Z zkTuM3>WE}GBM?~rvnh4VZ`|ku=gW@cxAZ7?{UEQ35&ftOBx1ubPOH{!xB!oPTqRB7FVyPEL)?@1IPC{NqvRVz zq60|<)X>Snfv>bmQ&}`4ZQ~kEW0nL6EwJrf<{A-lR-xlm`Xr#N$mZpbUryNiuylud zn+#b8zST%Ohd@f21sh0AEee(0DSi3ty|7KLsY^5Gc$nEQ0hP#3(T3fs-ob;8TKa z;r!gs`3Vrv^I7)I!IC@`6_rYHFRlnuC4Z?0$hCCt<-+;N9IrAz_f}t_!^-K57H=a3 zu0@c21$ukYN-_6OUYz}SeoZ6ndDInaZU0u?KlV1;u4GZR0!ii!efT$ndeilt>B(_V zyF{r%JW8>-xu&{Ig%{TS7`C#|33C}$%}wQGM9F!Hy=QoYL&ikLXk%%~LfF;2ZTG;{ zpSwUIt`yW^YB0fJ>1=nwadB_{%N9B6DJ12xaBpC}a>zj!S~xpwty=$fzIA>!R5@(=g32THT1y|ouz z(a*ns|IC6v?OV01BAyAF6(KqK8iNi(zq~q^I&CR!?KbE&qG|QdHvqzy%p#9LW>|;y z;Aa|WXKsP7LcTkHcD(tUzebjzIB17Os&T`ko*cZGABkt%=^oeYObLO zm}7&HVIa8JVtkK3*^Khj07h8I6M`;~%m8=raX4z|chiAyElPbJ!Rin7!@|M_cw4`; zs^rD5=t%F73?v(~(q&gI`_ZfFr_*LI1YuK{8K-mgQ}B=sL7|U>hP$Pa`>pZ%<*)qP zqHo8RfBq7FeYG)BR#NAMV!de!opRvY=}N2Vf=NyogEM0MDdU#9ICt{(Ox@(D?Wdh- zmrwVf=jHubj>d}fKW+ei`2Awy=ThGO3BH2>UsL|r5r!< zAgoxbYT@HBK5X*VJMl!cRS-OL-hM|G2sqIA^{=@3U7j6i{(f2JwdkXOBHR) z#Znk4xBUd5KyesV;SdwggOU&5QY`q39U{;$Lpan%X;rO?C|)Ptl5JW4(ER0*p`jr@ zKU(r88ZjZ!pP-?v>=Ta&RA%^^xOS6}v{BqDkZ>AoW^0F4l*1`+!9Qav9Z zko!z=HxVJ$4f<;s>@o^-05su7H}Q+1K!N)^QaLGlEKfM(RnmtiGV-z*O}Ho~jN3@} zoXPp;A><5?)DF~VIw3SzQIyI`cV8P(0JY_Ieq`n1;&Nmt;<6LgI*&7hDaGm=*M=X| zV`gUh2li=jz=WoT(;ltcjlF!N5kb*81L-R_^ZJj+f}<=IP%SrW1?1~Gjr$V~c7g`* z;4oqNonek8r$iTCGt#0UZ^f&PEH}*vk5wrPNvL=@45ui)Z;QT}b3~XBnW%Cbe#8jM zQKk~N6%63VL&R_jas&Lt$lJ{AA4@;S`};LA5AVJHN_R#K^wJjRN9$KtS6gg)xu}Y~ zl1#GV>8LyuqEDVY(MFC`h`{B!tq&7Mvy|VOO057EXN=WBI|X_em>{tMmqt^4ez}7* zeFradw?0~yoF;tzirj~+*QgcA|#rtAmOS5h=6?1-1lRF?jjF@ z|1Rj$Gkf1E)FYwvaq3s5kKGR&01V9MRpc;rgcFqW0VIb|F@g>c)Z0rSL!lc{h+%2U z?Yl|1*_OiWX8SX3mqMW}@>&NE`zOmiPvv0hSdE-S73Yev%ZBw!mNo!bl~y%6 zT)bPJ^eN#9L{ADR$(EcAQ0fb@e1>tUfCG@f3Y6&H|2qxLAPGMHEz!*zaUFphYjmPd zQy69mH8QX#_JCL7L=NQdfG{yrd;S z^gfn2fpWv>44Dvld9ewA{jaohU zt#9}{cFmteaFmkkP*YQXwV-+(weJopJJ04(aw4MW410Wsmy8Bb(U~Ny);AdGGDRSA z6VYSd#D(-NWD`cH-ovnb%1%7%8#Ikrha33`+RYLildtb=$z?u7TI4X?B@TI94A^S(6{>LpH!nxpEMgDgeAZ>{`z6GJc8j@!i1oa+3cmQNguDCQBm&>u-8qC5;4C?*u zfB$1I%N%2%JT|FfL#2uvgzZ2iMSy@Qe0*{z^Dc}uSg0+or9Ae7zQzxe9HMXpmz4MN zpMF6N+>NAtKdaCYmJZVG8xb(EZ?I9V>rg;pA z^oZi&H=h-F@G*=t+&%{)7Jd5SUUedGURt=$AFfIaXZ0C>6iJ-{xSISE`HjM8JXi%` zC*^W}GhIl9sZgB>95&#OVSDzq1MrgCx;o&P8Md<^cK`Bz2|p}inkcMZ=F$<6>zo!u z^2$O9Yl3)mZ~$@jb%}{OpghZ2mA%TW+sKZajSv0=-a!yc$s4$X>*J6rI4OCfuEqQq7~FC&}DKjpheQG11J-m z43(>AivHeGO4w4cAv5piYPg>%eNw2c0#*X85*|f!zf69)k|7Tzc_y z#x4iKUkYN3E0lLzqyhp0402(rwKLzLG!9+|S_dL8eTVuQp5h@$72bNMwf-hVtg;*g zEG&$V(X#vqSdlT@Z%iDfcs^JTfu6>)-1xb z2v>&I)KUmwbq*&u#-WS(`ZK2*pYLqoB|axC=b*Gbieh`1H4K5ST?1i3V^!7F&6w!J zxeErAV}S#ysi|~ybeJ-}G2aa9M6kutQJu#j-$93zEAub0M@h)rVZaeZ<=jxZDo`}d_U0m4(Y7Twvypn zb};$@;)$YABSLXPuS#SVD4dDzD*4!wBgoDCsBi@gxf+no4&peTfkKwgaNPGe*fK2y zC(zdi^CJchg?7_n5*Q;>y;~gtfQVCdsfe@WG2KVwz&qT=x2{i!X#SNuU*EcsI|pM` z|3Ae2WmuG5*gpzOcXxMpNJw{ghje#?fOK~Vh&0kIARS6~Nh95Ybc3)L_y2j`ckj>p z!~W!Pj5EW`HP>3}Jb!f(gFuUn8y+qxorbd4h764eZN$Z0Seyp1|AX-)r+Z!*BB56K z1@56h1*KJU{KHFK((>x*J{BzIb(hv?^R?of_vI`GycHbW(jdkeS_FDHF_M#=a#;Im zW24~>H-27MuUyH4mg=Zc&v<*%0{rx3dUq6E zVLpH6t5-m%Az%Cja8}G?y5#@R11?5Tlw@(+UVvfUtVeQ6O8tMq)Bf{wmbSWXM}&s< zf}uM6;u5lSw)1H&;tf;SsNnY&|LEoQ==I~%RS?zhn#gQlQR(b=p7MJtfPDGP>r9H8 zPX)%cxo&`%F(;=9O99{*SzB8J#tlGUGU(J0%b*j|3@Y#yQ0uYge>A?6k{OiE<9+*< zL4Q_WM@45XoPnV{i^t*P&Uukm)}D~B7dUXqGOqjj`nt}}=VoRqG+WJnhQRQL44JtV zWxsF&MS-7Ran|lJllIiY!nWq?bjZnE-{RupG$!Nu#l@&m=$T+3Xg&&e&p;&+)bA0a}0>HgAIu%mZjGqpZf=Ls0mDV)nVAfPl&MQ*}ki5xVXz*MB zb^%`A^Uv%i_aMN>Yk><6P@Q1sPi4gm?iv9`JCLx1q9B-&20ZT@(qQczjDqbsY81gD z-Gz~~HHjGyj8rvj{dknnVLWa2-EpDf!)VEj-Q50ttltUR6y$2l$#R)mXkmZ7xd`=3 zMy+GAtw7&xn4J0bjsdY-*wT1_>ZF+K;oklj zR5ZKGhUkaxbb2*N&X7_57;J`m5Vf!TD<97r(C+voKm^c(A>sxu6sRFz(}N6gv2by{ z0F$5nCyV2Mz8rBb6iV#r$@@pTge<}{M%62g^R0PY+`~}3`T731xOo1MJDdWyXCx?s z_iJ6m0`z(#>5t!oJf7Q^Ub@AFR#;!-`k$#Rei0v>@~pljngoD`?)qp-0xm)D!_%vL zvLg^zDd*Wbv%dHPoYh{*s5Wcg8{TD7WMen5&x!9o;lUET^Y2SC>I__%nVFxS-V2~u z^suHN{026{*9j$`VF;BJhEsTcSt$~7IfbP+X{)PW+&H$UVy1>UAp*?VMmJ0o=G*Hs490NAV%_ne92AEj=lGd0cT2!Rm+%5pf{b!-}bYnvm*mS^9 zY7pO`;1>en8aLZ)`)fc=99RH=3+@7>MOZ?TyBov-tM*31T_618{pJ3LJm8;tC06=L zd&usz!TAgB9U$g>23Hpqb0cer#;h3#P65Gn@hvtk?h`>fs4J+{;opFW{UtYGgl$RJ zfb?krsS3d0vn6hr5OoZMaF_d&l-9CLz>nFNpz;Hphr`+LgB(C7PtfxNGPDW=QI1sV zY;E;8OG9iggKG27Y&p~|80sV@y)9^<{|>NN>70d*qUdiW>p+FtV-OY*;f8q2ZaO@H zN1Yxj>u&+U3Aq$pJK_)!q6^}{>x+KS43YHd>`AlA8dwl8Wdlls?5&-qiwip)Q-2tZ zVdhrwF$mWu=CVS*%(nau7GW`04qKTlfgl4sEHTK8%la4NFE(?Y3#0|tz{NsL5lQUS zaJe(AQ5%p0Fli^l?zimZ2RDBOs`Z;qL<&T~Vh3hXJvtKC%TQx;^X(_&uAbf!=X~R~ z#+=41P}zrqF3g;&9~uTGDVC+mzK{wBfoz72enq*HOd~UDbK2v z3UdOIiX|j^lEl1<6(};2jhVOat~NRGG^W4L>>3(PYqdKBecJh?0}(u=N6q$712pj3 z0aApq0btc#U0=K6kOu2KL&&NSWo>CoRr>^@Pv8l6As|Ep*rb8Q*4)afVgonIT-Btm zqg0O2l+s%F=I*TQpCrBX{g0>=tFLY=oMKHsxvtKEVk4Cse+CK8 zdOslpH6lD*30T$hfMsLo2*?OQ=_d5y!UFWaJ*&u|;O7Dpy@2+sApc8d<|Y>=H6+oWaY5m~S~ozU2pU%OUqFkWeqOMqK5!Tk9IfGWToNvLkXmqAGB2lNOwd%!d2 zN+pSofHW7|6}c5;sVjKL{zGL4{*ss=((IL_uF-24SX(pNp?ea`l)N`JR!jW$N#=9w zRln_FYhujk!UC`Zw@+;mQ^XpYUR9_yJnY8mP~@JpthzhRCLM;}p8iT_H449J@k?B9 zdzw%7j&lgOnYAChS=}_nc#_ln@YYWI(R`77Q@n66F_+b#eq?sm;v}SddQIZ%WhaLq zzU4?dsgM^RbRj1NG7`$lw~vV#`|hS_?JmhZ{~m6|;exav;1_SI+gDH5ni=i*AM-M8 zkfd_+6`ggP^>#$>59l`lc^rvG((N?|^Cwj#y0pd>YaggJzwV7p!3kHRvu7Ofz6b@x zBr^2P@04T3Da8rpeQ;iC6ym&i5zt6yHRttul0D@9M8FVMsF6N4Tg84=iXKs>4PEWD|LvQ3n}J#$G*r(|)(a z9UqTSPqnTIwPX@Vwab!Fl}{F%WIlV`*)B#=%vj>5C;sz8Yc=$iqM69U7E+=#>O@>@ zak6(gvww~Q%8Ub7s*922tjs`4?0j{TyP_;%_xig1P(~PYr!}KPg^;^GZhD8QQs=~F zPdm3(sFGZWtfMh*4_p@FF+rPC3L{W+xq&wJ=0NOSB8dR*@_kB@c==m<_1y__&tNQh!_!#mKGOXFj*eK9M-n>cHpPNZ~g^v_77Sh z(*aQFA^VWHk^n_|ojC(ChOLeXxozs4*M&>6b&pLr`~HGKk3X};5I`e@RtR_AUpq#V zD=&F}ywrg1bwD?CRMbB>*t-?$g*a0IEE!`2%ECa1?1T(ecOY?}suK4TM4^|Bx&UqC-qFVJ7id z^urWoW}@FEWfz_0j|Lw{m}Xt$Ctt8XeYJ*MH}SKYFyV~s+lS}hiNPyB_=8(vAo5X! zye#RUc{+w2;9dv^keO+NbpktsPJW(>_c6Z&uIG<|ec?>o<|rn_68 z_O8m^2NQE770^`B^|BG*;l=kMC>?e$-qs0G`HTfVh0Pi?nh@CO7NEfk&RG2HpozlH zwOAACalfDW&L23Qc<*W;A+ny)@bU%DoRnCClM?1E+T3F5DG;|egx;gd->v)$vyBgr z^Kw_fU#m}8xDJLPguk~px3!oS=t_%ZOVcViv_dcUSsj<_-v0ea7(@It_+Ay>d94NJ zZ}=OgZK$FbP@{bnrEIKv;tE9H3%|^uTm_ne(Kh!%qbrW9tLrtiuTC!M`W#el1pWu(}>$x#UVooU&v^n4@^Oq&M7PcB0NZBk$k8Uvl3jB52>d z*}@`emAcG$=>EjvEZU$VNee}VrCq$Cp;=5#OI-+^%b#X2pcsD1onP>%qwq2s5grO; zM?Nt?X)rl^@0i1J^bMkZ!D*TVu-Rr;hXQdN z;akKu+#3^o+TB&i8}lbSJ39bgXc=+z79y;FUd@1OmKc2V9&!)a%>@i?0f6|Ffj@Tv z6~z#`LzJIUOo*u4|6&20ZtJOpd)80Tf9b!N2Ze>8fZ+wy zB!iOo%gf6)wm5wU(mp<6p02IEtibsUPJ-T0ohHLfS1jp*%}wfJta-M^b@UH)xlN7> z@;(4%XLhiXiq(%N;4Sot_V@)!$nv?wB(+!fvZ=b=YT^r|DaC1mYGfT7Ee%a?5|7!e z>LQh1aBrq%tParHZ*BEg*5PL=@TmVFuWAh^C>ln=(-~w;lk|J&se-?LbCi*q`gM64 z17c7nF+1oVW}?z#f@9nBe6-#I`e0yU&O5iemgeTk&_^^P?y5ls2w0jueflKl2J-USCY_1M3Of9cg(mjJ(fIv&~#balij z9jQZXkQKuzF1?@lm$dzOOG3)=2Tfa~hiq{_>2n>oO15D7hlyh6lf8D2^O&fpAy5## zaCnecVt-O$eVYQ)HnqE~WT^YIOOScpziu+8)932UHFH_E_;UqB{q{(4<+uU&ZhXn~ zG{*sAp}p%-i2=|YRA^TgF2WsjKs};T^e1&-JrkQ7%tRDhNb=2Ia#FLoO%d*n>X_~pxe@+vQI2+(=lXcZXz6&O3tlNzaJWXYLibfP=w6rY7^@v&{yQ2%!gFHOL_bhrQ`VkR(bfPb- ziV>lPFvlg7Ol-4n+@Q_Z*;yK@T79n>>&wAWH$^O-F$6Q@v1rq(_(sj%{;}7KiCwmr z8@{(-Xeu8zDVj3N20-&mL&C8nF_c+RaU9-i-_6a;AcOrtBLrxl4_HJSux`I zAX%{RZA=fbbH&WknRHO~Stz$wbcw@A#-3#30rH{X2P46m@(v7c*u*%oLr!D6(1U0p zUZpVP^f}ug)|1%!^9S)BwKdKID>u^TdvkN7tP~|5KmSjZ0q7*jS2sEhj3FfhI+F5x zU3}lGgWw)8HFy&81Rky`mZs_py5O|c41F0aEG#gQTwPvnvrL7Vxs6tK0M(f8T#)qp z;D(KSlSOZe?|xUuRu`SYrdZ{`mxX!=HCkgs0@c-XDW@^Wbv-86Rtn_T1l%oTtbHvv z+k+}Nzt6Yft{xS7g}Hr?qwMB#?oJ2^yM_PxQjSga)po69xq3JH^92>_S;$9vv;>O$ zgMk93$t~VSb*gt;8y}x+tgOr|Etd+l8jLz$M0ny|nrs&k#koF~5B3hqvnGf|c6nyu z$`a#_d#EfoRUPXhc*`ltk+7H5R#&6A>(!b3OvqVIOOb?L{≫E$IG0{F~~`d@yl~ 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 diff --git a/docs/IDE/AddIns/Images/Help_Settings.light.svg b/docs/IDE/AddIns/Images/Help_Settings.light.svg index 3476fc998..a33f90952 100644 --- a/docs/IDE/AddIns/Images/Help_Settings.light.svg +++ b/docs/IDE/AddIns/Images/Help_Settings.light.svg @@ -1 +1 @@ -Mainî­±MiscellaneousPackagesReferencesResourcesSourcesInventory.twinTWINBASIC HELPî©¶Open in browserDetachSearch the documentation/Packages/Default Packages/VBA Package/Interaction ModuleMsgBoxDisplays a message in a modal dialog with a chosen set ofMsgBoxHover helpAll packages +Mainî­±MiscellaneousPackagesReferencesResourcesSourcesInventory.twinTWINBASIC HELPî©¶Open in browserDetachSearch the documentation/Packages/Default Packages/VBA Package/Interaction ModuleMsgBoxDisplays a message in a modal dialog with a chosen set ofbuttons, waits for the user to click a button, and returns aVbMsgBoxResult value identifying that button.MsgBoxHover helpAll packagesCtrl and Alt shortcuts of add-insFind in the shortcut editors diff --git a/docs/IDE/AddIns/Images/Help_Settings.png b/docs/IDE/AddIns/Images/Help_Settings.png index cdf34650bd6801157f5bb693e1c087e395a4a776..6163b4fd321254bec54e68f4b3d3bc52d70d922b 100644 GIT binary patch literal 60623 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 literal 33350 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 diff --git a/docs/IDE/AddIns/Images/Help_Settings.svg b/docs/IDE/AddIns/Images/Help_Settings.svg index 4ab0dc108..eea9a2afb 100644 --- a/docs/IDE/AddIns/Images/Help_Settings.svg +++ b/docs/IDE/AddIns/Images/Help_Settings.svg @@ -1 +1 @@ -Mainî­±MiscellaneousPackagesReferencesResourcesSourcesInventory.twinTWINBASIC HELPî©¶Open in browserDetachSearch the documentation/Packages/Default Packages/VBA Package/Interaction ModuleMsgBoxMsgBoxHover helpAll packages +Mainî­±MiscellaneousPackagesReferencesResourcesSourcesInventory.twinTWINBASIC HELPî©¶Open in browserDetachSearch the documentation/Packages/Default Packages/VBA Package/Interaction ModuleMsgBoxDisplays a message in a modal dialog with a chosen set ofbuttons, waits for the user to click a button, and returns aVbMsgBoxResult value identifying that button.MsgBoxHover helpAll packagesCtrl and Alt shortcuts of add-insFind in the shortcut editors diff --git a/scripts/shoot_docs.mjs b/scripts/shoot_docs.mjs index 2da41ac96..994b88aa9 100644 --- a/scripts/shoot_docs.mjs +++ b/scripts/shoot_docs.mjs @@ -12712,7 +12712,8 @@ function helpTakes(ctx) { await showPage(AT.msgBox, HELP_PAGE); await click(c, { toolWindow: PANE, css: "#helpSettings" }); if (!(await waitFor(c, settingsOpen))) throw new Error("the gear did not open the settings panel"); - // the picture shows the defaults, both unticked: the Hover picture ticks Hover help + // the picture shows the defaults: Hover help and All packages unticked, the two + // fixes ticked; the Hover picture ticks Hover help if (await inPane(`return root.querySelector("#helpHover").checked;`)) { await click(c, { toolWindow: PANE, css: "#helpHover" }); await sleep(300); @@ -12720,8 +12721,8 @@ function helpTakes(ctx) { await sleep(500); await paneAside(); const pane = await paneRect(); - // 150 CSS pixels ends between the page's heading and its first paragraph. - const png = await snap(c, "Settings", inside({ ...pane, height: 150 }), { frame: origin }); + // 200 CSS pixels holds the four rows and ends between the page's heading and its first paragraph. + const png = await snap(c, "Settings", inside({ ...pane, height: 200 }), { frame: origin }); await closeSettings(); return png; }); diff --git a/test/addin/help.test.mjs b/test/addin/help.test.mjs index eacdbd4dc..44357f9e2 100644 --- a/test/addin/help.test.mjs +++ b/test/addin/help.test.mjs @@ -939,6 +939,15 @@ scenario("the help add-in: F1 and the help pane", (lane) => { // The All packages box in the pane, and in the window's page. const allBox = () => inPane(c, `return root.querySelector("#helpAllPackages")?.checked ?? null;`); const windowAllBox = () => win.evaluate(`document.getElementById("helpAllPackages").checked`); + // The two page-side fixes: a box each in the settings panel, and what the + // fix's script leaves on the IDE's page while it is on. + const FIXES = [ + { box: "helpKeysOn", global: "tbDocsAddinKeys", label: "Ctrl and Alt shortcuts of add-ins" }, + { box: "helpFindOn", global: "tbDocsTextFind", label: "Find in the shortcut editors" }, + ]; + const paneFixBox = (id) => inPane(c, `return root.querySelector("#helpSettingsPanel #${id}")?.checked ?? null;`); + const windowFixBox = (id) => win.evaluate(`document.getElementById(${JSON.stringify(id)}).checked`); + const fixInstalled = (name) => c.evaluate(`!!window.${name}`); // Waits for the results to be `want`, a function of the rows, and fails with the rows. async function resultsBecome(want, what) { const rows = await waitFor(c, async (c) => { @@ -1085,6 +1094,158 @@ scenario("the help add-in: F1 and the help pane", (lane) => { ); }); + test("the settings panel has a box for each page-side fix, ticked, and the fixes are in the page", async () => { + for (const { box, global, label } of FIXES) { + assert.equal(await paneFixBox(box), true, `${label}: the pane's box is missing or unticked`); + assert.equal(await fixInstalled(global), true, `${label}: window.${global} is not set`); + const text = await inPane(c, `return root.querySelector("#${box}")?.parentNode.textContent.trim() ?? null;`); + assert.equal(text, label); + } + }); + + // Unticking a box takes the fix out of the page at once, ticking it puts it back. + test("unticking a fix's box removes it from the page, and ticking it puts it back", async () => { + for (const { box, global, label } of FIXES) { + await clickSettingBox(`#${box}`); + assert.ok(await waitFor(c, async () => (await fixInstalled(global)) === false), `${label}: still in the page`); + assert.equal(await paneFixBox(box), false); + await clickSettingBox(`#${box}`); + assert.ok(await waitFor(c, () => fixInstalled(global)), `${label}: not back in the page`); + assert.equal(await paneFixBox(box), true); + } + }); + + // The IDE's own command opens Manage Keyboard Shortcuts, whose first entry, + // the built-in group, is a read-only text area of JSON. + test("Ctrl+F in Manage Keyboard Shortcuts finds text, scrolls to it, and Esc leaves the dialog open", async () => { + const WORD = "ctrl"; + const AREA = ".modalDialogContainer textarea.genericMonospaceTextArea"; + const onArea = (expr) => + c.evaluate(`(() => { const a = document.querySelector(${JSON.stringify(AREA)}); return a ? ${expr} : null; })()`); + const modals = () => c.evaluate(`modalDialogs.length`); + const selection = () => + onArea( + `({ start: a.selectionStart, end: a.selectionEnd, text: a.value.substring(a.selectionStart, a.selectionEnd), focused: document.activeElement === a, top: a.scrollTop })`, + ); + const findBox = () => + c.evaluate(`(() => { + const i = document.querySelector(".tbDocsFindBar input"); + return i ? { focused: document.activeElement === i, missing: i.classList.contains("tbDocsFindMissing") } : null; + })()`); + // What the script measures for the selection: in view, and not under the find box. + const visibility = () => onArea(`window.tbDocsTextFind.visible(a)`); + // The selection is the match at `start` for `word`, has the focus, and shows. + const selectionIs = async (start, what, word = WORD) => { + const ok = await waitFor(c, async () => (await selection())?.start === start); + const s = await selection(); + assert.ok(ok, `${what}: the selection is ${JSON.stringify(s)}, expected a match at ${start}`); + assert.equal(s.text.toLowerCase(), word, what); + assert.equal(s.focused, true, `${what}: the text area has not got the focus`); + const v = await visibility(); + assert.ok(v.visible, `${what}: the match is not in view: ${JSON.stringify({ ...v, scrollTop: s.top })}`); + }; + const keyDown = (key, mods) => pressKey(c, key, mods); + assert.equal(await modals(), 0, "a dialog is open already"); + await c.evaluate(`executeIdeCommand("tbKeyboardShortcuts_ShowManageKeyboardShortcuts")`); + try { + assert.ok(await waitFor(c, async () => (await onArea("true")) === true), "the dialog has no text area"); + const { at, length, room } = await onArea(`(() => { + const t = a.value.toLowerCase(), at = []; + for (let i = t.indexOf(${JSON.stringify(WORD)}); i >= 0; i = t.indexOf(${JSON.stringify(WORD)}, i + 1)) at.push(i); + return { at, length: t.length, room: a.scrollHeight - a.clientHeight }; + })()`); + assert.ok(at.length >= 3, `"${WORD}" is in the text ${at.length} times`); + assert.ok(room > 0, "the text does not need scrolling"); + await onArea(`(a.focus(), a.setSelectionRange(0, 0), true)`); + + await pressKey(c, "f", { ctrl: true }); + assert.ok( + await waitFor(c, async () => (await findBox())?.focused === true), + `Ctrl+F opened no find box: ${JSON.stringify(await findBox())}`, + ); + assert.equal(await modals(), 1, "Ctrl+F closed the dialog"); + await typeText(c, WORD); + await pressKey(c, "Enter"); + await selectionIs(at[0], "Enter: the first match"); + await pressKey(c, "F3"); + await selectionIs(at[1], "F3: the next match"); + await pressKey(c, "F3", { shift: true }); + await selectionIs(at[0], "Shift+F3: the match before"); + // Round past the start to the last match, which is far down the text. + await pressKey(c, "F3", { shift: true }); + const last = at[at.length - 1]; + await selectionIs(last, "Shift+F3 from the first: the last match"); + const scrolledDown = (await selection()).top; + assert.ok(scrolledDown > 0, `the text area did not scroll to a match at ${last} of ${length}`); + // Backwards from the last match to the first scrolls up, a match at a time. + for (let n = at.length - 2; n >= 0; n--) { + await keyDown("F3", { shift: true }); + await selectionIs(at[n], `Shift+F3 to match ${n}`); + } + assert.ok((await selection()).top < scrolledDown, "going back to the first match did not scroll up"); + // Round past the start to the last, and past the end to the first. + await keyDown("F3", { shift: true }); + await selectionIs(last, "Shift+F3 round past the start"); + await keyDown("F3"); + await selectionIs(at[0], "F3 round past the end"); + + // A word that is not there marks the box, and leaves the selection. + const was = await selection(); + await pressKey(c, "f", { ctrl: true }); + assert.ok(await waitFor(c, async () => (await findBox())?.focused === true), "Ctrl+F did not return to the box"); + await typeText(c, "zzqq"); + await pressKey(c, "Enter"); + assert.ok( + await waitFor(c, async () => (await findBox())?.missing === true), + `no match was not shown: ${JSON.stringify(await findBox())}`, + ); + assert.equal((await selection()).start, was.start, "a search with no match moved the selection"); + + // The box searches for what it holds; Esc then closes it and only it. + await pressKey(c, "f", { ctrl: true }); + await typeText(c, WORD); + await pressKey(c, "Enter"); + await selectionIs(at[1], "Enter in the box after a search with no match"); + await pressKey(c, "Escape"); + assert.ok(await waitFor(c, async () => (await findBox()) === null), "Esc did not close the find box"); + assert.equal(await modals(), 1, "Esc closed the dialog"); + assert.equal((await selection()).focused, true, "the text area has not got the focus after Esc"); + + // With the box closed, F3 and Shift+F3 go on with the last query. + await keyDown("F3"); + await selectionIs(at[2], "F3 with the box closed"); + assert.equal(await findBox(), null, "F3 opened the box"); + await keyDown("F3", { shift: true }); + await selectionIs(at[1], "Shift+F3 with the box closed"); + + // Ctrl+F3 takes the word at the caret: its next occurrence, then, from there, the one before. + const { word, first, next, before } = await onArea(`(() => { + const t = a.value, counts = {}; + for (const m of t.matchAll(/[A-Za-z0-9_]+/g)) if (m[0].length >= 8) (counts[m[0]] ??= []).push(m.index); + const word = Object.keys(counts).find((w) => counts[w].length >= 3); + const lower = t.toLowerCase(), w = word.toLowerCase(); + const first = counts[word][0]; + const next = lower.indexOf(w, first + w.length); + return { word: w, first, next, before: lower.lastIndexOf(w, next - 1) }; + })()`); + await onArea(`(a.focus(), a.setSelectionRange(${first + 3}, ${first + 3}), true)`); + await keyDown("F3", { ctrl: true }); + await selectionIs(next, `Ctrl+F3 on ${word}`, word); + assert.equal(await findBox(), null, "Ctrl+F3 opened the box"); + await onArea(`(a.setSelectionRange(${next + 3}, ${next + 3}), true)`); + await keyDown("F3", { ctrl: true, shift: true }); + await selectionIs(before, `Ctrl+Shift+F3 on ${word}`, word); + } finally { + // Cancel, the dialog's own button; leaves the IDE as it was found. + await c.evaluate(`(() => { + const m = [...document.querySelectorAll(".modalDialogContainer")].pop(); + const b = m && [...m.querySelectorAll("button, .dialogButton, [class*=Button]")].find((e) => e.textContent.trim() === "Cancel"); + if (b) b.click(); else if (typeof hideModalDialog === "function" && modalDialogs.length) hideModalDialog(); + })()`); + assert.ok(await waitFor(c, async () => (await modals()) === 0), "the dialog did not close"); + } + }); + test("hover help is off until its box is ticked", async () => { assert.equal(await hoverBox(), false); const mark = await consoleMark(c); @@ -1672,6 +1833,36 @@ scenario("the help add-in: F1 and the help pane", (lane) => { assert.equal(await allBox(), false); }); + // Both ways, as for All packages: the window's box reaches the pane's and the + // page, and the pane's box, clicked though the pane is hidden, the window's. + test("the page-side fixes can be unticked in the window or the pane, and the other follows", async () => { + for (const { box, global, label } of FIXES) { + assert.equal(await windowFixBox(box), true, `${label}: the window's box is not ticked`); + await clickInWindow("#helpSettings"); + await settingsBecome(windowSettings, SETTINGS_OPEN, "the gear did not open the panel"); + await clickInWindow(`#${box}`); + assert.ok(await waitFor(c, async () => (await paneFixBox(box)) === false), `${label}: the pane's box is ticked`); + assert.ok(await waitFor(c, async () => (await fixInstalled(global)) === false), `${label}: still in the page`); + assert.equal(await windowFixBox(box), false); + assert.deepEqual(await windowSettings(), SETTINGS_OPEN, "unticking the box closed the panel"); + await clickInWindow(`#${box}`); + assert.ok(await waitFor(c, async () => (await paneFixBox(box)) === true), `${label}: the pane's box is unticked`); + assert.ok(await waitFor(c, () => fixInstalled(global)), `${label}: not back in the page`); + await clickInWindow("#helpSettings"); + await settingsBecome(windowSettings, SETTINGS_CLOSED, "the gear did not close the panel"); + + await inPane(c, `root.querySelector("#${box}").click(); return null;`); + assert.ok( + await waitFor(c, async () => (await windowFixBox(box)) === false), + `${label}: the window's box is ticked`, + ); + assert.equal(await fixInstalled(global), false); + await inPane(c, `root.querySelector("#${box}").click(); return null;`); + assert.ok(await waitFor(c, () => windowFixBox(box)), `${label}: the window's box is unticked`); + assert.ok(await waitFor(c, () => fixInstalled(global)), `${label}: not back in the page`); + } + }); + test("the window's results follow the All packages box", async () => { await win.evaluate(`document.getElementById("helpSearch").focus()`); await win.evaluate(`document.getElementById("helpSearch").value = ""`); From f2eada9a5ed94b36c8d7db108452f01e706ba43e Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Tue, 20 Oct 2026 22:06:18 +0200 Subject: [PATCH 05/21] WIP.Errors.md: the plan for listing the run-time errors each procedure raises A reader on Discord asked for what no VB or VBA reference gives: the errors a function, statement or operator can raise. The owner's decisions: data-driven error tables, measured in twinBASIC 32-bit and VB6 (64-bit too for operators and Math), scoped to Default, Core and Built-In, with the error prose already on pages moved into the tables. The file sets out what a page gets, the central error page, what the tooling (batch 0) must settle, what a session starting it must know and when it is done, and sixteen batches with their status. WIP.md's table points to it. --- WIP.Errors.md | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++ WIP.md | 1 + 2 files changed, 152 insertions(+) create mode 100644 WIP.Errors.md diff --git a/WIP.Errors.md b/WIP.Errors.md new file mode 100644 index 000000000..b752ae653 --- /dev/null +++ b/WIP.Errors.md @@ -0,0 +1,151 @@ +# twinBASIC Documentation --- Run-time errors per procedure + +The plan for adding to every documented procedure, statement and operator the run-time errors +it raises, and for the tools that measure them. Prompted by a reader on Discord: no VB or VBA +reference answers *which errors can this raise*, and a reader writing an error handler needs +exactly that. + +Like WIP.md, this file is not rendered through tbdocs. + +## Owner's decisions (2026-10-20) + +- **Data-driven.** Each procedure's errors are data: the cases that raise them. The page + section, the probe that measures them and the check that keeps them true are generated + from that data. Hand-written tables cannot be re-verified on a new BETA. +- **Measured, not copied.** Every row is observed in twinBASIC and in VB6. Microsoft's pages + are a source of cases to try, never of answers. +- **Runtimes.** twinBASIC 32-bit and VB6 for everything; twinBASIC 64-bit as well for the + operators and the `Math` module only. +- **Scope.** `docs/Reference/Default/` (VBA, VB, VBRUN), `docs/Reference/Core/` (statements and + operators) and `docs/Reference/Built-In/`. Not `Contributed/`. +- **Existing error prose moves into the tables.** A page that states an error in a sentence + ("an *LCID* that is not a valid locale identifier raises error 5") loses the sentence when + its table is written; what the sentence explains beyond the number goes into the row's + *When*. +- **A difference from VB6 is a bug** (the owner's rule for bugs generally): the table states + what twinBASIC does, a NOTE on the page states what VB6 does, and the difference is queued in + [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md). Where VB6 has no counterpart (LongLong, twinBASIC + additions), there is nothing to compare and nothing to file on that ground. + +## What a page gets + +An `## Errors` section after the page's main text and before `## Example`: + +| Error | When | +|---|---| +| 5, *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative | +| 94, *Invalid use of Null* | `Mid$` is given a *string* that is **Null** | + +- The number links to its row on the central page (below); the message is the runtime's own, + as `Error$(n)` returns it, in italics as the rest of the site writes messages. +- *When* is a condition on the arguments or the state, in the reader's terms --- not the + probe's expression. +- **What is listed**: what the procedure raises from its own arguments and state. + **What is not**: errors anything can raise (7 *Out of memory*, 28 *Out of stack space*), + error 13 from converting an argument before the call is made, and compile-time + diagnostics (TB codes). The central page says this once. +- A procedure that raises nothing gets no section, not an empty one --- unless its absence is + surprising enough to state (a function that returns a value where VB6 raises). +- The section is written between markers, so regenerating it replaces only it. + +## The central page + +`Reference/Run-time-Errors.md` (name to settle in batch 1): every trappable error number with +its message, measured with `Error$(n)` in both runtimes, the general exclusions above, and, +for each number, a generated list of the procedures whose tables name it. + +## Tooling (batch 0) + +To design and build first; the points below are what it must settle, not decisions yet. + +- **The data.** One file per area (per VBA module, per Core group), YAML like `_book.yml`, + which `builder/data.mjs` already reads with `js-yaml`. An entry names the page (by path or + by `symbols:`), and lists cases: the expression or statements that raise, the error number, + the *When* text, and optional per-runtime expectations (`vb6:`, `win64:`) where they differ. + Cases that need set-up (an open file, a form, an array) carry it. +- **The probe generator.** From a data file, one twinBASIC project and one VB6 project, each + case wrapped in `On Error Resume Next` / `Err.Clear` / print `Err.Number`, run with + `tbrun --exe` (and `--arch win64` where the decisions above say) and with + `scripts/vb6run.mjs`. The exe, not a `[RunAfterBuild]` run: the IDE context has its own + defects (see `module-init-reads-other-module-ide` in BUGS-TO-REPORT.md). +- **The checker.** Runs the probes and fails when a runtime disagrees with the data, or a + page's section disagrees with what the data generates. Run by a person when a new BETA is + installed, like `bug_repro.mjs verify` --- never by CI, which has no twinBASIC or VB6. +- **The section writer.** Writes each page's `## Errors` from the data, between markers, and + the central page's index. A gate in `test.bat` (no runtime needed) checks that every + section matches its data. +- **Operators.** A generator of its own: each operator over every pair of operand types + (Byte, Integer, Long, LongLong, Single, Double, Currency, Decimal, Date, String, Boolean, + Variant holding Empty or Null) at boundary values, giving a matrix of error numbers per + runtime. Each operator page gets a summary of it; the full matrix is a page of its own. + +## Starting batch 0 + +Read first: [WIP.Harness.md](WIP.Harness.md) (`tbbuild`, `tbrun`), [WIP.Build.md](WIP.Build.md) +(gates), [WIP.Authoring.md](WIP.Authoring.md) (page template), and `scripts/lib/vb6.mjs`'s +opening comment (driving VB6). Models to copy from: `scripts/check_package_tests.mjs` and +`scripts/check_code_regions.mjs` tie page text to tests with markers, and `scripts/bug_repro.mjs` +runs projects in both runtimes and compares the result with an expectation. + +What this repository has already learned that the probes must respect (BETA 1010): + +- **Run cases in the built exe** (`tbrun --exe`), and print with `TbRun.Out`, which `tbrun` + adds to the project: `Debug.Print` writes nothing in an exe. A `[RunAfterBuild]` run in the + IDE reads every module-level variable as 0 once one initializer reads another module's + variable (bug `module-init-reads-other-module-ide`), and may differ in other ways. +- **A `tbrun --exe` probe brings its own `Sub Main`**, without the console template's + `tbxMain.twin`; two `Main`s make the build fail with *'Main' is ambiguous*. A project has + one `[RunAfterBuild]` at most. +- **Never `MsgBox`, `InputBox` or an unhandled error** in a probe: each opens a modal box on a + private desktop. `tbrun --exe` closes them and reports them, and `vb6run` refuses a source + that calls the first two. Every case runs under `On Error Resume Next`. +- **Keep the generated code plain.** A module-level or class-field initializer cannot call a + procedure of the project, and a class field that reads another field fails the build (bug + `class-field-init-reads-field`). Assign in code instead. +- **VB6 cannot compile twinBASIC syntax** (`LongLong`, `Return` in a function, inline `Dim x =`, + generics). Cases for VB6 are written in the common subset; a twinBASIC-only case says so in + the data and is not run in VB6. +- **One case can stop a whole project**: a compile error anywhere fails all of it. The + generator should build a failing project again without the case named in the error, so one + bad case costs a rerun, not the batch. +- A new gate in `test.bat` must also be added to the gate list in + `docs/Documentation/Tools.md` (`check_gate_lists.mjs` fails otherwise), and a new tool gets + its section on the Tools pages under `docs/Documentation/`. + +Batch 0 is done when: + +1. The data format is written down in this file, with one complete example. +2. The generator, the runner (twinBASIC 32-bit, 64-bit on request, VB6) and the section + writer exist, with `--help`, and the gate is in `test.bat`. +3. A trial on three pages of different shapes --- a function with several errors + (`Mid` or `InStr`), a statement (`ReDim`), a procedure that raises nothing --- runs through + every step, and its sections are written. +4. **The owner has seen the trial's sections** before batch 2 starts: the format is settled + then, not before. The trial's pages may be committed or reverted, as the owner says. +5. This file's batch table says batch 0 is done, and what batch 1 needs to know. + +## Batches + +Each batch is one session. They are independent after batch 0, so any order works, and +each can be run when there is budget. Status is kept here. + +| # | Batch | Pages (about) | Status | +|---|---|---|---| +| 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | not started | +| 1 | The central Run-time errors page | 1 | not started | +| 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | not started | +| 3 | VBA `Conversion` | 28 | not started | +| 4 | VBA `Math` (also win64) | 17 | not started | +| 5 | VBA `DateTime` | 20 | not started | +| 6 | VBA `Information` and `Interaction` | 54 | not started | +| 7 | VBA `FileSystem` | 23 | not started | +| 8 | VBA `Collection` and `ErrObject` | 22 | not started | +| 9 | VBA `Financial` | 14 | not started | +| 10 | VBA `HiddenModule`, then `Compilation` and `TbExpressionService` (`Constants` has nothing to raise) | 68 | not started | +| 11 | Core file I/O statements (`Open`, `Get`, `Put`, `Input`, ...: errors 52--75) | 15 | not started | +| 12 | Core arrays and control flow (`ReDim`, `Erase`, `Resume`, `Return`, ...) | 15 | not started | +| 13 | Operators: the matrix generator and its pages (also win64) | 25 | not started | +| 14 | VB package (37 pages) and the VBRUN pages that document members rather than constants: properties and methods that raise 380, 383, 387 and the like; needs forms | 37 + part of 134 | not started | +| 15 | Built-In packages | 195 | not started | + +Page counts are the folders' `.md` files on 2026-10-20, index pages included; a batch's first step is to list the pages in it that document something that can raise. diff --git a/WIP.md b/WIP.md index 1e0347fba..0e801ec3c 100644 --- a/WIP.md +++ b/WIP.md @@ -26,6 +26,7 @@ one covering what you are about to change. | retake the documentation's IDE screenshots, or change the tool that takes them | [WIP.Screenshots.md](WIP.Screenshots.md) --- `shoot_docs.mjs`: the setups, cut-outs, the annotation overlay and the owner's decisions | | work on the IDE help add-in, or on testing IDE add-ins by machine | [WIP.HelpAddin.md](WIP.HelpAddin.md) --- the plan, the IDE facts it rests on, and the probes still open | | find the documentation work a new twinBASIC build leaves, or change the package indexer (`indexer/`) | [WIP.Indexer.md](WIP.Indexer.md) --- the store and the snapshots, the one shared scanner, `api.json` format 3, the owner's decisions and the open items | +| add or check the run-time errors a procedure, statement or operator raises | [WIP.Errors.md](WIP.Errors.md) --- the owner's decisions, the data-driven design, and the batches with their status | | look up what a `TBnnnn` diagnostic code is, or document a compiler warning | [upstream/twinbasic-diagnostic-ids.h](upstream/twinbasic-diagnostic-ids.h) --- the compiler's full list of diagnostic codes and their internal names, from Wayne Phillips (current as of BETA 1007); `docs/Features/Compiler-IDE/Compiler-Warnings.md` is the public page for the warnings | | run a use-case evaluation round | [eval/README.md](eval/README.md) --- start every evaluator with `eval/run_case.mjs`, **never as a subagent**: a subagent inherits this session's `CLAUDE.md`, and with it this file, which is the answer key the corpus withholds | From 9d1828d37841608d01904078f2124ee645609097 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Tue, 20 Oct 2026 22:34:25 +0200 Subject: [PATCH 06/21] Run-time errors, batch 0: the data, the probes, the section writer and its gate errors/*.yml lists, per page, the cases that raise each error and the cases that must raise nothing. scripts/errors.mjs run builds one probe per runtime (twinBASIC exe via tbrun --exe, win64 on request, VB6 via runRepro), drops and rebuilds around a case a compiler refuses, and compares every case with the data; write writes each page's Errors section between markers. check_errors.mjs, the new test.bat and CI gate, keeps sections and data in step. Trial: Mid (three rows, measured limits of start and length), ReDim (VB6 differs on an upper bound below the lower: NOTE, bug pending), Len (raises nothing, no section). All 37 cases agree in twinBASIC BETA 1010 and VB6. --- .github/actions/run-gates/action.yml | 6 + WIP.Errors.md | 130 +++++-- WIP.md | 3 +- docs/Documentation/Building.md | 1 + docs/Documentation/Tools-Defects.md | 19 ++ docs/Documentation/Tools-Toolchain-Tests.md | 11 + docs/Documentation/Tools.md | 22 +- docs/Reference/Core/ReDim.md | 21 +- docs/Reference/Default/VBA/Strings/Mid.md | 11 + errors/Core.yml | 27 ++ errors/VBA.Strings.yml | 44 +++ errors/messages.yml | 7 + scripts/check_errors.mjs | 223 ++++++++++++ scripts/errors.mjs | 351 +++++++++++++++++++ scripts/lib/cli-cases.mjs | 12 + scripts/lib/errors-data.mjs | 357 ++++++++++++++++++++ test.bat | 5 + 17 files changed, 1209 insertions(+), 41 deletions(-) create mode 100644 errors/Core.yml create mode 100644 errors/VBA.Strings.yml create mode 100644 errors/messages.yml create mode 100644 scripts/check_errors.mjs create mode 100644 scripts/errors.mjs create mode 100644 scripts/lib/errors-data.mjs diff --git a/.github/actions/run-gates/action.yml b/.github/actions/run-gates/action.yml index a781539e2..c5e009a8c 100644 --- a/.github/actions/run-gates/action.yml +++ b/.github/actions/run-gates/action.yml @@ -236,6 +236,12 @@ runs: - name: Verify the package test markers and projects (check_package_tests.mjs) shell: bash run: node scripts/check_package_tests.mjs + # Every page's Errors section is what scripts/errors.mjs write makes of the + # run-time error data under errors/, and the data is in order. Sources + # only: no twinBASIC, no VB6. + - name: Verify the Errors sections against their data (check_errors.mjs) + shell: bash + run: node scripts/check_errors.mjs # The symbol index (tB/symbols.json) is read by the IDE help add-in. A # build that indexes the reference cleanly says nothing about the rules # that did not fire on it, so these probes assert each against the case diff --git a/WIP.Errors.md b/WIP.Errors.md index b752ae653..0064da077 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -54,30 +54,110 @@ An `## Errors` section after the page's main text and before `## Example`: its message, measured with `Error$(n)` in both runtimes, the general exclusions above, and, for each number, a generated list of the procedures whose tables name it. -## Tooling (batch 0) - -To design and build first; the points below are what it must settle, not decisions yet. - -- **The data.** One file per area (per VBA module, per Core group), YAML like `_book.yml`, - which `builder/data.mjs` already reads with `js-yaml`. An entry names the page (by path or - by `symbols:`), and lists cases: the expression or statements that raise, the error number, - the *When* text, and optional per-runtime expectations (`vb6:`, `win64:`) where they differ. - Cases that need set-up (an open file, a form, an array) carry it. -- **The probe generator.** From a data file, one twinBASIC project and one VB6 project, each - case wrapped in `On Error Resume Next` / `Err.Clear` / print `Err.Number`, run with - `tbrun --exe` (and `--arch win64` where the decisions above say) and with - `scripts/vb6run.mjs`. The exe, not a `[RunAfterBuild]` run: the IDE context has its own - defects (see `module-init-reads-other-module-ide` in BUGS-TO-REPORT.md). -- **The checker.** Runs the probes and fails when a runtime disagrees with the data, or a - page's section disagrees with what the data generates. Run by a person when a new BETA is - installed, like `bug_repro.mjs verify` --- never by CI, which has no twinBASIC or VB6. -- **The section writer.** Writes each page's `## Errors` from the data, between markers, and - the central page's index. A gate in `test.bat` (no runtime needed) checks that every - section matches its data. -- **Operators.** A generator of its own: each operator over every pair of operand types - (Byte, Integer, Long, LongLong, Single, Double, Currency, Decimal, Date, String, Boolean, - Variant holding Empty or Null) at boundary values, giving a matrix of error numbers per - runtime. Each operator page gets a summary of it; the full matrix is a page of its own. +## Tooling (batch 0, built) + +Three files and a folder. Their reference is on the Tools pages: +[`errors.mjs`](docs/Documentation/Tools-Defects.md#errorsmjs) and +[`check_errors.mjs`](docs/Documentation/Tools-Toolchain-Tests.md#check_errorsmjs). + +- `errors/` --- the data: one YAML file per area (`VBA.Strings.yml`, `Core.yml`, ...) and + `messages.yml`, each error number's message. +- `scripts/errors.mjs run` --- builds one probe from the data per runtime, runs it, compares + each case with the data. twinBASIC 32-bit and VB6 by default, win64 for a file that says + `win64: true` or with `--runtime win64`. By hand only: it needs twinBASIC and VB6. A + case a compiler refuses is reported and dropped, and the probe rebuilt (VB6 stops at its + first error, so one rebuild per refused case there). `generate ` writes the probes + without running them; `--keep` keeps the ones `run` built. +- `scripts/errors.mjs write` --- writes each page's section from the data. +- `scripts/check_errors.mjs` --- the gate in `test.bat` and CI: data in order, every section + what `write` makes of it, no section without data. No runtime needed. +- `scripts/lib/errors-data.mjs` --- what the two share. + +How a probe runs a case: a `Private Sub C()` of its own, `Dim Result As Variant`, the +case's `decl` lines, `On Error Resume Next`, its `setup` lines (an error there is reported as +a setup failure, not as the case's result), then `Result = ` or its `run` lines, then +`case `. twinBASIC: a tree for `tbrun --exe`, module +`Probe` with `Sub Main` and a `[RunAfterBuild]` that only prints, so the IDE run passes and +the exe runs the cases. VB6: `Probe.vbp` and `Module1.bas` through `runRepro`, appending to +`out.txt` line by line, so a case that kills the exe still leaves the ones before it. + +### The data format + +```yaml +# errors/VBA.Strings.yml +beta: 1010 # the BETA the data was last measured on; titles the VB6 NOTE +win64: false # optional: run measures win64 too (operators, Math) +pages: + Reference/Default/VBA/Strings/Mid.md: # the page, as a path under docs/ + rows: # one table row each, in this order + - error: 5 # the number twinBASIC raises + when: "*start* is less than 1, or *length* is negative" # the When cell + cases: # every case must raise `error` in twinBASIC + - Mid("abc", 0) # a string is an expression: Result = Mid("abc", 0) + - Mid("abc", 1, -1) + - error: 94 + when: "**Mid$** or **MidB$** is given a *string* that is **Null**" + cases: [Mid$(Null, 1), MidB$(Null, 1)] + quiet: # cases that must raise nothing; never shown on the page + - Mid(Null, 1) + - Mid("abc", 4) + Reference/Core/ReDim.md: + rows: + - error: -2147467259 + when: an upper bound is less than its lower bound + vb6: 9 # VB6 raises this instead (0: nothing); writes the NOTE + bug: pending # a bugs/ reproducer, or pending until one is queued + cases: + - { decl: Dim X() As Long, run: ReDim X(0 To -1) } + - { decl: Dim X() As Long, run: "ReDim X(5 To 4, 1)" } + quiet: + - { decl: Dim X() As Long, setup: ReDim X(3), run: ReDim Preserve X(5) } + Reference/Default/VBA/Strings/Len.md: + quiet: # raises nothing: no section + - Len(Null) + - { expr: LenB(CLngLng(1)), only: tb } # twinBASIC syntax: not run in VB6 +``` + +A case is an expression, or a mapping with `expr` or `run` (statements; a multi-line string +for several) and optionally `decl` (`Dim` lines), `setup` (statements before it) and +`only: tb`. A row's `win64:` is the number win64 raises where it differs. A page with no +rows gets no section; one with `none: ""` and no rows gets a section holding that +sentence, for an absence of errors worth stating. Quote a case that holds `: `, starts with +a quote, or sits in a `{ }` mapping and holds a comma. `messages.yml` maps each number to its +message; `run` fails a case whose `Err.Description` is not that text. + +The section `write` makes, before the page's first Example heading: + +``` + +### Errors + +| Error | When | +|---|---| +| -2147467259, *Unspecified error* | an upper bound is less than its lower bound | + +> [!NOTE] BETA 1010 +> When an upper bound is less than its lower bound, VB6 raises error 9, *Subscript out of range*, instead. + + +``` + +### Decided while building, for the owner to confirm + +- **The heading takes the page's level for Example and See Also**: `###` on the VBA and Core + pages, which still use the old h1→h3 style; `##` on a page without those headings. +- **The message is `Err.Description`, not `Error$(n)`.** For VB's own numbers the two agree; + for an HRESULT they do not: `Error$(-2147467259)` is *Application-defined or + object-defined error*, while the error raised describes itself as *Unspecified error*, + which is what a handler sees. Batch 1 settles which the central page lists. +- **The number is not linked yet**: the central page does not exist. Batch 1 adds the link + in `renderSection` (`scripts/lib/errors-data.mjs`) and rewrites every section with `write`. +- **A row that differs from VB6 must name its reproducer**, or say `bug: pending`; the gate + refuses a `vb6:` row without it. +- **Errors from converting an argument are left out**, as the plan says: `Mid("abc", "x")` + raises 13 and `Mid("abc", Null)` 94 in both runtimes, in the conversion of *start* to + **Long**. `Mid("abc", 1, Null)` is in the table: *length* is a **Variant**, and the 94 is + Mid's own. ## Starting batch 0 @@ -131,7 +211,7 @@ each can be run when there is budget. Status is kept here. | # | Batch | Pages (about) | Status | |---|---|---|---| -| 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | not started | +| 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | built; trial sections (Mid, ReDim; Len raises nothing) await the owner | | 1 | The central Run-time errors page | 1 | not started | | 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | not started | | 3 | VBA `Conversion` | 28 | not started | diff --git a/WIP.md b/WIP.md index 0e801ec3c..97e810c6d 100644 --- a/WIP.md +++ b/WIP.md @@ -588,6 +588,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/errors.mjs run|generate|write` --- the run-time errors of the documented procedures, as data under `errors/` ([WIP.Errors.md](WIP.Errors.md), [Tools-Defects](docs/Documentation/Tools-Defects.md#errorsmjs)): `run` builds and runs a probe of the cases in twinBASIC (`tbrun --exe`; win64 on request) and VB6 and compares each with the data, `write` writes each page's Errors section. Its gate is `check_errors.mjs`; `run` is by hand, never CI. - `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). @@ -602,7 +603,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-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: +**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-nine gates cannot be affected by an edit under `docs/` at all. **Five can.** `check_errors.mjs` compares the reference pages' Errors sections with the data under `errors/`, so edit the data and run `node scripts/errors.mjs write`, never the section. `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 diff --git a/docs/Documentation/Building.md b/docs/Documentation/Building.md index 523da7058..7e1652725 100644 --- a/docs/Documentation/Building.md +++ b/docs/Documentation/Building.md @@ -89,6 +89,7 @@ Do not drop `--check-audit-index` when typing the underlying build command: with && node scripts/check_page_baseline.mjs \ && node scripts/check_book_coverage.mjs \ && node scripts/check_package_tests.mjs \ + && node scripts/check_errors.mjs \ && node scripts/check_symbol_index.mjs \ && node scripts/check_twin_parsers.mjs \ && node scripts/check_attribute_sweep.mjs \ diff --git a/docs/Documentation/Tools-Defects.md b/docs/Documentation/Tools-Defects.md index ca31c8ca1..c5dc767c6 100644 --- a/docs/Documentation/Tools-Defects.md +++ b/docs/Documentation/Tools-Defects.md @@ -149,6 +149,25 @@ Other options: `--timeout S` is the time limit, in seconds, for each run of the 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. +## errors.mjs +{: #errors } + + node scripts/errors.mjs run [...] [--only ] [--runtime tb|vb6|win64]... [--ide ] [--vb6 ] [--keep] + node scripts/errors.mjs generate [...] [--only ] + node scripts/errors.mjs write [...] + +Measures the run-time errors each documented procedure raises, and writes each reference page's Errors section from them. The errors are data, not prose: a YAML file per area under `errors/` lists, for each page, the cases that raise each error and the cases that must raise nothing, and `errors/messages.yml` gives each number's message. The format is in `WIP.Errors.md`. A file is named as it is in `errors/`; with none named, every file there is used. `--only ` keeps the pages whose path under `docs/` matches. + +| Command | Effect | +|---|---| +| `run` | Builds one probe of the cases for each runtime and runs it: 32-bit twinBASIC (`tb`) and VB6 by default, and 64-bit twinBASIC (`win64`) as well for a data file that says `win64: true`; `--runtime` names them instead, and may be repeated. Each case is a procedure of its own that runs under `On Error Resume Next` and reports `Err.Number` and `Err.Description`. The twinBASIC probe is a tree run by [`tbrun --exe`](Tools-Compiler#tbrun), so the cases run in the built exe and not in the IDE, and the VB6 probe is a `Probe.vbp` built and run by the code [`bug_repro.mjs vb6`](#bug-repro) uses, on a private desktop. A case a compiler refuses is reported with the compiler's message and left out, and the probe is built again without it, so one bad case costs a rebuild and not the run. Each case is printed with what each runtime raised, and a case is `FAIL` when a runtime raised another number than the data says, refused it, never reported, or raised a number whose message is not the one in `messages.yml`. `--keep` keeps the generated probes and prints where they are. | +| `generate` | Writes the probes to `/tb`, a tree for `tbrun --exe`, and `/vb6`, a `Probe.vbp` project, without running them. | +| `write` | Writes each page's section from the data, between `` and ``: before the page's first Example heading, else before See Also, else at the end, at the level the page gives those headings. A page whose data lists only cases that raise nothing gets no section, and loses one it had. Writing again changes nothing. [`check_errors.mjs`](Tools-Toolchain-Tests#check-errors) is its gate. | + +`run` needs twinBASIC and VB6, found as `tbrun` and [`vb6run.mjs`](#vb6run) find them, so it is outside every gate and outside CI, and is run by a person when a new BETA is installed. `generate` and `write` need neither. + +Exit codes: **0** `run`: every runtime agrees with the data; `generate`, `write`: done; **1** `run`: a runtime disagrees with the data; **2** a refused command line, data with a problem, a runtime that could not be run or a probe that ended early, or a crash. + ## ide_js.mjs {: #ide-js } diff --git a/docs/Documentation/Tools-Toolchain-Tests.md b/docs/Documentation/Tools-Toolchain-Tests.md index eb2be997a..0ad60e8db 100644 --- a/docs/Documentation/Tools-Toolchain-Tests.md +++ b/docs/Documentation/Tools-Toolchain-Tests.md @@ -395,6 +395,17 @@ Thirteen probes run first, on pages, sources and an `expect.json` built in memor Exit codes: **0** every probe passed and every test project is in order, **1** a probe failed, or a section or test project is not in order, **2** the gate could not run: a refused command line, or a crash. +## check_errors.mjs +{: #check-errors } + + node scripts/check_errors.mjs + +Verifies that every reference page's Errors section is what [`errors.mjs write`](Tools-Defects#errors) makes of the run-time error data under `errors/`, and that the data is in order: every file reads, with no key it does not know; every error number it names has its message in `errors/messages.yml`; a row that differs from VB6 names its reproducer under `bugs/`, or says `pending`; every page it names exists and is named by one file only; and no page under `docs/` has a section that no data file names. It reads only the sources, so it needs no twinBASIC, no VB6 and no built tree. Measuring the data is `errors.mjs run`, which is not a gate. + +Sixteen probes run first, on data and pages built in memory: how the data is read and refused, the section and its VB6 note, where a new section goes and that a marker inside a fence is not one, that writing twice changes nothing and removing a section gives the page back, and how the probe modules and their output are read. + +Exit codes: **0** every probe passed and every section matches its data, **1** a probe failed, or the data or a section is not in order, **2** the gate could not run: a refused command line, or a crash. + ## check_symbol_index.mjs {: #check-symbol-index } diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md index 887d4bde4..698f82d15 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-eight steps, each stopping the run if it fails: +The tests the toolchain has to pass. Twenty-nine 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. @@ -136,13 +136,14 @@ The tests the toolchain has to pass. Twenty-eight steps, each stopping the run i 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. +22. [`scripts/check_errors.mjs`](Tools-Toolchain-Tests#check-errors) --- verifies every page's Errors section is what the run-time error data under `errors/` writes, and that the data is in order. +23. [`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. +24. [`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. +25. [`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`. +26. [`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. +27. [`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. +28. [`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. +29. [`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: @@ -167,6 +168,7 @@ POSIX: && node scripts/check_page_baseline.mjs \ && node scripts/check_book_coverage.mjs \ && node scripts/check_package_tests.mjs \ + && node scripts/check_errors.mjs \ && node scripts/check_symbol_index.mjs \ && node scripts/check_twin_parsers.mjs \ && node scripts/check_attribute_sweep.mjs \ @@ -177,9 +179,9 @@ POSIX: Exit codes: **0** every step passed; otherwise the code of the step that stopped the run, as that step's entry gives it. -**All but four cannot be affected by an edit confined to `docs/`**, which is why they are separate from `check.bat`. Run this one when the change touches `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `indexer/` or `test/`, the site's scripts in `docs/assets/js/`, the help add-in's source in `add-in/`, a contributed package's section, a wrapper, or a workflow. Both CI workflows run every step unconditionally. +**All but five cannot be affected by an edit confined to `docs/`**, which is why they are separate from `check.bat`. Run this one when the change touches `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `indexer/` or `test/`, the site's scripts in `docs/assets/js/`, the help add-in's source in `add-in/`, a contributed package's section, a wrapper, or a workflow. Both CI workflows run every step unconditionally. -The four exceptions are [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions), [`check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists), which reads this page, [`check_lint.mjs`](Tools-Toolchain-Tests#check-lint), which lints the site's scripts in `docs/assets/js/`, and [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests), which reads the callouts of the contributed packages' sections. +The five exceptions are [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions), [`check_errors.mjs`](Tools-Toolchain-Tests#check-errors), which compares the reference pages' Errors sections with their data, [`check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists), which reads this page, [`check_lint.mjs`](Tools-Toolchain-Tests#check-lint), which lints the site's scripts in `docs/assets/js/`, and [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests), which reads the callouts of the contributed packages' sections. `check_code_regions.mjs` has two halves. Its corpus sweep tokenises every markdown file under `docs/`, so a page that provokes a rewrite into altering a code region fails it. Its fixed probes run against their own sources whatever the tree holds, and cover the *mirror* fault, where a rewrite silently stops firing; the sweep cannot see that one. After adding a page with an unusual code construct, run `test.bat` and read the built page too. diff --git a/docs/Reference/Core/ReDim.md b/docs/Reference/Core/ReDim.md index d5e8e9170..1991b6c90 100644 --- a/docs/Reference/Core/ReDim.md +++ b/docs/Reference/Core/ReDim.md @@ -29,7 +29,7 @@ The **ReDim** statement is used to size or resize a dynamic array that has alrea Use the **ReDim** statement repeatedly to change the number of elements and dimensions in an array. However, an array declared with one data type cannot later be changed to another data type by **ReDim**, unless the array is contained in a **Variant**. If the array is contained in a **Variant**, the type of the elements can be changed by using an **As** *type* clause, unless the **Preserve** keyword is used, in which case no changes of data type are permitted. -**ReDim** also works on a **Variant** that holds a value other than an array, or nothing at all: the value is discarded and the **Variant** then holds the new array. **ReDim Preserve** on a **Variant** that holds a value other than an array raises error 13 (*Type mismatch*). +**ReDim** also works on a **Variant** that holds a value other than an array, or nothing at all: the value is discarded and the **Variant** then holds the new array. With the **Preserve** keyword, only the last array dimension can be resized, and the number of dimensions cannot change. For example, when the array has only one dimension, that dimension can be resized because it is the last and only dimension. However, when the array has two or more dimensions, only the size of the last dimension can change while still preserving the contents of the array. @@ -42,13 +42,10 @@ ReDim X(10, 10, 10) ReDim Preserve X(10, 10, 15) ``` -Similarly, with **Preserve**, the size of the array can only change by changing the upper bound; changing the lower bound causes an error. +Similarly, with **Preserve**, the size of the array can only change by changing the upper bound. When an array becomes smaller than it was, data in the eliminated elements will be lost. -> [!NOTE] BETA 1010 -> An upper bound below the lower bound, as in `ReDim X(0 To -1)`, raises error -2147467259, *Unspecified error*. VB6 raises error 9, *Subscript out of range*. - When variables are initialized, a numeric variable is initialized to 0, a variable-length string is initialized to a zero-length string (""), and a fixed-length string is filled with zeros. **Variant** variables are initialized to **Empty**. Each element of a user-defined type variable is initialized as if it were a separate variable. A variable that refers to an object must be assigned an existing object by using the [**Set**](Set) statement before it can be used. Until it is assigned an object, the declared object variable has the special value **Nothing**, which indicates that it doesn't refer to any particular instance of an object. @@ -58,6 +55,20 @@ The **ReDim** statement acts as a declarative statement if the variable it decla > [!NOTE] > To resize an array contained in a **Variant**, the **Variant** variable must be explicitly declared before attempting to resize its array. + +### Errors + +| Error | When | +|---|---| +| -2147467259, *Unspecified error* | an upper bound is less than its lower bound | +| 9, *Subscript out of range* | **Preserve** changes a lower bound, or the bounds of any dimension but the last | +| 13, *Type mismatch* | **Preserve** is used on a **Variant** that does not hold an array | + +> [!NOTE] BETA 1010 +> When an upper bound is less than its lower bound, VB6 raises error 9, *Subscript out of range*, instead. + + + ### Example This example uses the **ReDim** statement to allocate and reallocate storage space for dynamic-array variables. It assumes the **Option Base** is **1**. diff --git a/docs/Reference/Default/VBA/Strings/Mid.md b/docs/Reference/Default/VBA/Strings/Mid.md index 5e2ad823e..5d2df6c00 100644 --- a/docs/Reference/Default/VBA/Strings/Mid.md +++ b/docs/Reference/Default/VBA/Strings/Mid.md @@ -33,6 +33,17 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi > [!TIP] > Use the [**Mid =**](../../Core/Mid-equals) statement to replace characters within a string. + +### Errors + +| Error | When | +|---|---| +| 5, *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative | +| 5, *Invalid procedure call or argument* | **Mid** or **Mid$** is given a *start* greater than 1,073,741,824 or a *length* greater than 1,073,741,823 | +| 94, *Invalid use of Null* | **Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | + + + ### Example This example uses the **Mid** function to return a specified number of characters from a string. diff --git a/errors/Core.yml b/errors/Core.yml new file mode 100644 index 000000000..a299bb99f --- /dev/null +++ b/errors/Core.yml @@ -0,0 +1,27 @@ +# Statements and operators of docs/Reference/Core/. Format: WIP.Errors.md. +beta: 1010 +pages: + Reference/Core/ReDim.md: + rows: + - error: -2147467259 + when: an upper bound is less than its lower bound + vb6: 9 + bug: pending + cases: + - { decl: Dim X() As Long, run: ReDim X(0 To -1) } + - { decl: Dim X() As Long, run: ReDim X(-1) } + - { decl: Dim X() As Long, run: "ReDim X(5 To 4, 1)" } + - error: 9 + when: "**Preserve** changes a lower bound, or the bounds of any dimension but the last" + cases: + - { decl: Dim X() As Long, setup: ReDim X(1 To 3), run: ReDim Preserve X(0 To 3) } + - { decl: Dim X() As Long, setup: "ReDim X(3, 3)", run: "ReDim Preserve X(4, 3)" } + - error: 13 + when: "**Preserve** is used on a **Variant** that does not hold an array" + cases: + - { decl: Dim V As Variant, setup: V = 1, run: ReDim Preserve V(3) } + quiet: + - { decl: Dim X() As Long, run: ReDim X(0 To 0) } + - { decl: Dim X() As Long, setup: ReDim X(3), run: ReDim Preserve X(5) } + - { decl: Dim V As Variant, setup: V = 1, run: ReDim V(3) } + - { decl: Dim X() As Long, setup: "ReDim X(3, 3)", run: "ReDim Preserve X(3, 5)" } diff --git a/errors/VBA.Strings.yml b/errors/VBA.Strings.yml new file mode 100644 index 000000000..30f8d5089 --- /dev/null +++ b/errors/VBA.Strings.yml @@ -0,0 +1,44 @@ +# The VBA Strings module. Format: WIP.Errors.md. +beta: 1010 +pages: + Reference/Default/VBA/Strings/Mid.md: + rows: + - error: 5 + when: "*start* is less than 1, or *length* is negative" + cases: + - Mid("abc", 0) + - Mid("abc", -1, 1) + - Mid("abc", 1, -1) + - Mid$("abc", 0) + - MidB("abc", 0) + - MidB$("abc", 1, -1) + - error: 5 + when: "**Mid** or **Mid$** is given a *start* greater than 1,073,741,824 or a *length* greater than 1,073,741,823" + cases: + - Mid("abc", 1073741825) + - Mid$("abc", 2147483647) + - Mid("abc", 1, 1073741824) + - Mid$("", 1, 2147483647) + - error: 94 + when: "**Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null**" + cases: + - Mid$(Null, 1) + - MidB$(Null, 1) + - Mid("abc", 1, Null) + - MidB$("abc", 1, Null) + quiet: + - Mid(Null, 1) + - Mid("abc", 4) + - Mid("abc", 1073741824) + - Mid("abc", 1, 1073741823) + - MidB(Null, 1) + - MidB("abc", 2147483647) + - MidB("abc", 1, 2147483647) + - Mid("", 1, 0) + Reference/Default/VBA/Strings/Len.md: + quiet: + - Len("") + - Len(Null) + - LenB(Null) + - Len(Empty) + - { decl: Dim S As String * 4, expr: Len(S) } diff --git a/errors/messages.yml b/errors/messages.yml new file mode 100644 index 000000000..2cc0ee679 --- /dev/null +++ b/errors/messages.yml @@ -0,0 +1,7 @@ +# The message of each run-time error number, as the runtime's Err.Description gives it +# when the error is raised. scripts/errors.mjs run checks every case against it. +5: Invalid procedure call or argument +9: Subscript out of range +13: Type mismatch +94: Invalid use of Null +-2147467259: Unspecified error diff --git a/scripts/check_errors.mjs b/scripts/check_errors.mjs new file mode 100644 index 000000000..434725c96 --- /dev/null +++ b/scripts/check_errors.mjs @@ -0,0 +1,223 @@ +#!/usr/bin/env node +// Gate: every page's Errors section is what the run-time error data under errors/ +// writes, and the data is in order. +// +// node scripts/check_errors.mjs +// +// The data (errors/*.yml, its format in WIP.Errors.md) lists the cases that raise each +// error a documented procedure raises; scripts/errors.mjs run measures them in +// twinBASIC and VB6, and scripts/errors.mjs write writes each page's section between +// and . This gate needs neither runtime. It checks +// that: +// * every data file and errors/messages.yml reads, with no unknown key, and every +// error number the data names has its message; +// * a row that differs from VB6 names its reproducer under bugs/, or says pending; +// * every page the data names exists, in one data file only, and its section is +// what write would make of it, or absent when the data gives it none; +// * no page under docs/ has a section the data does not name. +// It starts with probes of the library on inputs of its own. +// +// Exit codes: +// 0 all pass +// 1 a probe or the corpus failed +// 2 the gate could not run: a refused command line, or a crash + +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import yaml from "js-yaml"; +import { exitOnCrash, parseCli, printHelpAndExit, withUsageError } from "../lib/cli.mjs"; +import { markdownFiles } from "../lib/markdown-files.mjs"; +import { DOCS_DIR, REPO_ROOT } from "../lib/repo-paths.mjs"; +import { + caseAtLine, + dataFiles, + findSection, + loadDataFile, + loadMessages, + pageLevel, + pagePath, + probeCases, + probeModule, + readData, + readProbeOutput, + renderSection, + spliceSection, +} from "./lib/errors-data.mjs"; +import { createProbes } from "./lib/gate-probes.mjs"; + +exitOnCrash(); + +const USAGE = `usage: node scripts/check_errors.mjs [-h, --help] + +Checks that every page's Errors section is what scripts/errors.mjs write makes +of the data under errors/, and that the data is in order. Needs no twinBASIC or +VB6; measuring the data is scripts/errors.mjs run. + + -h, --help print this text and exit + +Exit codes: + 0 all pass + 1 a probe or the corpus failed + 2 the gate could not run: a refused command line, or a crash`; + +if ( + withUsageError(() => + parseCli(process.argv.slice(2), { options: { help: { type: "boolean", short: "h" } }, stopAt: ["help"] }), + ).values.help +) + printHelpAndExit(USAGE); + +const { check, report } = createProbes( + "check_errors", + "run node scripts/errors.mjs write for a stale section; the data's format is in WIP.Errors.md", +); + +// ------------------------------------------------------------------ probes + +const data = (doc) => { + const problems = []; + const read = readData(yaml.load(doc), "probe.yml", problems); + return { ...read, name: "probe.yml", problems }; +}; +const sample = data(` +beta: 1010 +pages: + A/B.md: + rows: + - error: 5 + when: "*x* is negative" + cases: [F(-1), { decl: Dim X() As Long, run: "ReDim X(1)\\nX(2) = 1" }] + - error: 9 + when: the index | is out + vb6: 5 + bug: pending + cases: [G(1)] + quiet: [F(1)] +`); +const messages = new Map([ + [5, "Invalid procedure call or argument"], + [9, "Subscript out of range"], +]); +check( + "a sample reads with only the problem it has", + sample.problems.length === 1 && /without \|/.test(sample.problems[0]), + sample.problems.join("\n"), +); +check( + "unknown keys, a missing when and vb6 without bug are refused", + data(` +beta: 1 +pages: + A.md: + rows: + - { error: 5, cases: [x], vb6: 9, colour: red } +`).problems.length === 3, +); +check("a page with nothing to probe is refused", data("beta: 1\npages:\n A.md: {}\n").problems.length === 1); + +const page = sample.pages[0]; +page.rows[1].when = "the index is out"; +const section = renderSection(page, { name: "probe.yml", beta: 1010, messages, level: "###" }); +check( + "the section has the table and the VB6 note", + section + .join("\n") + .includes( + "| 5, *Invalid procedure call or argument* | *x* is negative |\n| 9, *Subscript out of range* | the index is out |\n\n> [!NOTE] BETA 1010\n> When the index is out, VB6 raises error 5, *Invalid procedure call or argument*, instead.", + ), +); +check("a page with only quiet cases gets no section", renderSection({ rows: [], quiet: [1] }, { messages }) === null); + +const doc = "# T\n\ntext\n\n```tb\n### Example\n\n```\n\n### Example\n\ncode\n\n### See Also\n"; +const added = spliceSection(doc, section); +check( + "a new section goes before the Example heading, not one in a fence", + added.indexOf("### Errors") < added.lastIndexOf("### Example") && + added.indexOf("### Errors") > added.indexOf("```\n\n"), +); +check("writing it again changes nothing", spliceSection(added, section) === added); +check("removing it gives the page back", spliceSection(added, null) === doc); +check("a marker in a fence is not one", findSection(doc).section === null); +check("CRLF stays CRLF", spliceSection(doc.replace(/\n/g, "\r\n"), section) === added.replace(/\n/g, "\r\n")); +check("an unclosed section is reported", findSection("\n### Errors\n").problems.length === 1); +check( + "the level follows the page's Example heading", + pageLevel(doc) === "###" && pageLevel("# T\n## See Also\n") === "##", +); + +const cases = probeCases([sample], "tb"); +const vb6Cases = probeCases([sample], "vb6"); +check( + "cases carry what each runtime expects", + cases.map((c) => c.expect).join() === "5,5,9,0" && vb6Cases[2].expect === 5, +); +for (const dialect of ["tb", "vb6"]) { + const mod = probeModule(cases, dialect); + const lines = mod.text.split("\r\n"); + check( + `${dialect}: each case's span is its own Sub`, + mod.spans.every((s) => lines[s.from - 1] === `Private Sub C${s.id}()` && lines[s.to - 1] === "End Sub") && + caseAtLine(mod.spans, mod.spans[1].from + 3) === 2, + ); +} +const out = readProbeOutput(["case 1 5 Invalid procedure call or argument", "setup 2 9 x", "done"]); +check("probe output reads", out.done && out.seen.get(1).number === 5 && out.seen.get(2).setup); + +// ------------------------------------------------------------------ the corpus + +const { messages: known, problems: messageProblems } = loadMessages(); +const files = dataFiles().map((n) => loadDataFile(n)); +check("errors/ holds data", files.length > 0); +const problems = [...messageProblems, ...files.flatMap((f) => f.problems)]; +const owner = new Map(); +for (const f of files) { + for (const p of f.pages) { + if (owner.has(p.page)) problems.push(`${f.name}: ${p.page} is in ${owner.get(p.page)} too`); + owner.set(p.page, f.name); + for (const r of p.rows) { + for (const n of [r.error, r.vb6]) { + if (n !== undefined && n !== 0 && !known.has(n)) + problems.push(`${f.name}: ${p.page}: no message for ${n} in messages.yml`); + } + const slug = r.bug; + if ( + slug && + slug !== "pending" && + ![ + ["bugs", slug], + ["bugs", "filed", slug], + ].some((s) => existsSync(path.join(REPO_ROOT, ...s))) + ) { + problems.push(`${f.name}: ${p.page}: no reproducer bugs/${slug}`); + } + } + } +} +check("the data is in order", problems.length === 0, problems.join("\n")); + +const stale = []; +for (const f of files) { + for (const p of f.pages) { + const file = pagePath(p.page); + if (!existsSync(file)) { + stale.push(`docs/${p.page}: no such page (${f.name})`); + continue; + } + const text = readFileSync(file, "utf8"); + const marks = findSection(text).problems; + if (marks.length) { + stale.push(...marks.map((m) => `docs/${p.page}: ${m}`)); + continue; + } + const want = renderSection(p, { name: f.name, beta: f.beta, messages: known, level: pageLevel(text) }); + if (spliceSection(text, want) !== text) stale.push(`docs/${p.page}: the section is not what ${f.name} writes`); + } +} +for (const rel of await markdownFiles(DOCS_DIR)) { + if (owner.has(rel)) continue; + const { section: at, problems: marks } = findSection(readFileSync(path.join(DOCS_DIR, rel), "utf8")); + if (at || marks.length) stale.push(`docs/${rel}: an errors section that no data file names`); +} +check("every section is what the data writes", stale.length === 0, stale.join("\n")); + +process.exit(report()); diff --git a/scripts/errors.mjs b/scripts/errors.mjs new file mode 100644 index 000000000..167db53a6 --- /dev/null +++ b/scripts/errors.mjs @@ -0,0 +1,351 @@ +#!/usr/bin/env node +// The run-time errors each documented procedure raises: measuring them, and +// writing each page's Errors section from the data under errors/. +// +// node scripts/errors.mjs run [...] [--only ] [--runtime tb|vb6|win64]... +// node scripts/errors.mjs generate [...] [--only ] +// node scripts/errors.mjs write [...] +// +// Exit codes: +// 0 run: every runtime agrees with the data; generate, write: done +// 1 run: a runtime disagrees with the data +// 2 refused command line, bad data, or a runtime could not be run +// +// WHY THIS EXISTS +// +// No VB or VBA reference says which errors a procedure can raise, and a reader +// writing an error handler needs exactly that. Hand-written tables cannot be +// re-checked on a new BETA, so each page's errors are data -- the cases that +// raise them, errors/*.yml, whose format WIP.Errors.md describes -- and the +// section, the probe and the check are generated from it. `run` builds one +// probe from the data for each runtime (a twinBASIC exe through scripts/tbrun.mjs, +// a VB6 exe through lib/vb6.mjs) and compares what each case raised with what +// the data says. It needs twinBASIC and VB6, so it is run by a person when a +// BETA is installed, never by CI. `write` needs neither; check_errors.mjs is its +// gate. + +import { spawn } from "node:child_process"; +import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { die, exitOnCrash, parseCli, printHelpAndExit, regexOption, withUsageError } from "../lib/cli.mjs"; +import { REPO_ROOT } from "../lib/repo-paths.mjs"; +import { + caseAtLine, + dataFiles, + findSection, + loadDataFile, + loadMessages, + pageLevel, + pagePath, + probeCases, + probeModule, + readProbeOutput, + renderSection, + spliceSection, +} from "./lib/errors-data.mjs"; +import { encodeAnsi, findVb6, NO_VB6, parseMakeLog, runRepro } from "./lib/vb6.mjs"; + +exitOnCrash(); + +const USAGE = `usage: node scripts/errors.mjs run [...] [--only ] [--runtime R]... [--ide P] [--vb6 P] [--keep] + node scripts/errors.mjs generate [...] [--only ] + node scripts/errors.mjs write [...] [-h, --help] + +The run-time errors of the documented procedures, from the data under errors/ +(its format is in WIP.Errors.md). A file is named as it is in errors/; with none +named, every file there is used. + + run builds a probe of the cases for each runtime, runs it, and compares + what each case raised with the data. A case the compiler refuses is + reported and left out, and the probe built again without it. + generate writes the probes to /tb (a tree for scripts/tbrun.mjs --exe) + and /vb6 (Probe.vbp), without running them + write writes each page's Errors section from the data + + --only only the pages whose path under docs/ matches + --runtime R tb (32-bit twinBASIC), vb6 or win64 (64-bit twinBASIC); may be + repeated. By default tb and vb6, and win64 for a file that says + win64: true + --ide P the twinBASIC IDE, as for tbrun (default $TB_IDE, or the newest + on the Desktop) + --vb6 P VB6.EXE (default $VB6_EXE, or VB6's own install folder) + --keep keep the generated probes, and print where + -h, --help print this text and exit + +Exit codes: + 0 run: every runtime agrees with the data; generate, write: done + 1 run: a runtime disagrees with the data + 2 a refused command line, bad data, or a runtime that could not be run`; + +const RUNTIMES = ["tb", "vb6", "win64"]; +const COMMANDS = ["run", "generate", "write"]; +const TBRUN = path.join(REPO_ROOT, "scripts", "tbrun.mjs"); +const TB_TEMPLATE = path.join(REPO_ROOT, "test", "example-projects", "console", "Settings"); +const VB6_PROJECT = path.join(REPO_ROOT, "test", "repro-templates", "vb6", "Probe.vbp"); +const ROUNDS = 25; + +const { values, positionals } = withUsageError( + () => + parseCli(process.argv.slice(2), { + options: { + only: { type: "string" }, + runtime: { type: "string", multiple: true }, + ide: { type: "string" }, + vb6: { type: "string" }, + keep: { type: "boolean", default: false }, + help: { type: "boolean", short: "h" }, + }, + positionals: { min: 0 }, + stopAt: ["help"], + }), + { format: (err) => `${err.message}\n${USAGE}` }, +); +if (values.help) printHelpAndExit(USAGE); + +const usage = (msg) => die(2, `${msg}\n${USAGE}`); +const [command, ...rest] = positionals; +if (!COMMANDS.includes(command)) usage(command ? `unknown command: ${command}` : "give a command"); +const only = withUsageError(() => (values.only ? regexOption(values.only, { option: "--only" }) : null)); +for (const r of values.runtime) if (!RUNTIMES.includes(r)) usage(`--runtime: one of ${RUNTIMES.join(", ")}`); +let outDir = null; +if (command === "generate") { + if (!rest.length) usage("generate needs the folder to write to"); + outDir = path.resolve(rest.shift()); +} +if (command !== "run") { + for (const k of ["runtime", "ide", "vb6"]) if (values[k]?.length) usage(`--${k} is for run`); + if (values.keep) usage("--keep is for run"); +} +if (command === "write" && only) usage("--only is for run and generate"); + +// ------------------------------------------------------------------ the data + +const known = dataFiles(); +const names = rest.length ? rest.map((n) => path.basename(n)) : known; +for (const n of names) if (!known.includes(n)) die(2, `no such data file: errors/${n}`); +const files = names.map((n) => loadDataFile(n)); +const { messages, problems: messageProblems } = loadMessages(); +const problems = [...messageProblems, ...files.flatMap((f) => f.problems)]; +if (problems.length) + die(2, `errors: the data has problems; scripts/check_errors.mjs lists them all\n ${problems.join("\n ")}`); + +if (command === "write") { + let changed = 0; + for (const f of files) { + for (const p of f.pages) { + const file = pagePath(p.page); + let text; + try { + text = readFileSync(file, "utf8"); + } catch { + die(2, `errors/${f.name}: no such page: docs/${p.page}`); + } + const { problems: marks } = findSection(text); + if (marks.length) die(2, `docs/${p.page}: ${marks.join("; ")}`); + const section = renderSection(p, { name: f.name, beta: f.beta, messages, level: pageLevel(text) }); + const next = spliceSection(text, section); + if (next !== text) { + writeFileSync(file, next); + changed++; + console.log(`wrote docs/${p.page}`); + } + } + } + console.log(`errors: ${changed} page${changed === 1 ? "" : "s"} written`); + process.exit(0); +} + +// ------------------------------------------------------------------ the probes + +function writeTbTree(dir, cases) { + mkdirSync(path.join(dir, "Sources"), { recursive: true }); + cpSync(TB_TEMPLATE, path.join(dir, "Settings")); + const mod = probeModule(cases, "tb"); + writeFileSync(path.join(dir, "Sources", "Probe.twin"), mod.text); + return mod; +} + +function writeVb6Project(dir, cases) { + mkdirSync(dir, { recursive: true }); + cpSync(VB6_PROJECT, path.join(dir, "Probe.vbp")); + const mod = probeModule(cases, "vb6"); + writeFileSync(path.join(dir, "Module1.bas"), encodeAnsi(mod.text)); + return mod; +} + +if (command === "generate") { + const tb = probeCases(files, "tb", { only }); + const vb6 = probeCases(files, "vb6", { only }); + writeTbTree(path.join(outDir, "tb"), tb); + writeVb6Project(path.join(outDir, "vb6"), vb6); + console.log(`errors: ${tb.length} cases in ${path.join(outDir, "tb")}, ${vb6.length} in ${path.join(outDir, "vb6")}`); + process.exit(0); +} + +// run: node scripts/tbrun.mjs as a child, as bug_repro does; it runs on import. +function runNode(args) { + return new Promise((resolve) => { + const child = spawn(process.execPath, [TBRUN, ...args], { + cwd: REPO_ROOT, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (d) => { + stdout += d; + }); + child.stderr.on("data", (d) => { + stderr += d; + }); + child.on("close", (code) => resolve({ code, stdout, stderr })); + }); +} + +const work = mkdtempSync(path.join(tmpdir(), "errors-")); + +// What each case raised in one runtime: Map id -> { number, description } or +// { refused: message } or { missing: true }; plus a note for the harness. +async function measure(runtime, cases, run) { + const results = new Map(); + let left = cases; + for (let round = 1; round <= ROUNDS && left.length; round++) { + const dir = path.join(work, `${runtime}-${round}`); + const got = await run(dir, left); + if (got.harness) return { results, harness: got.harness }; + if (got.refused) { + for (const [id, message] of got.refused) results.set(id, { refused: message }); + left = left.filter((c) => !got.refused.has(c.id)); + continue; + } + const { seen, done } = readProbeOutput(got.lines); + for (const c of left) results.set(c.id, seen.get(c.id) ?? { missing: true }); + return { results, harness: done ? null : `the probe stopped before it ended${got.note ? `: ${got.note}` : ""}` }; + } + return { results, harness: left.length ? `still refused after ${ROUNDS} builds` : null }; +} + +async function runTb(dir, cases, arch) { + const mod = writeTbTree(dir, cases); + const args = [dir, "--exe", "--json", "--arch", arch]; + if (values.ide) args.push("--ide", values.ide); + const r = await runNode(args); + if (r.code === 1) { + const refused = new Map(); + for (const m of r.stdout.matchAll(/^\{ERROR\} \S*Probe\.twin \[(\d+),\d+\]: (.*)$/gm)) { + const id = caseAtLine(mod.spans, Number(m[1])); + if (id === null) return { harness: `a compile error outside every case: ${m[0]}` }; + refused.set(id, m[2]); + } + if (refused.size) return { refused }; + } + let json = null; + try { + json = JSON.parse(r.stdout.slice(r.stdout.indexOf("{"))); + } catch {} + if (!json?.exeRun) + return { harness: `tbrun exit ${r.code}: ${(r.stderr || r.stdout).trim().split("\n").slice(-3).join(" / ")}` }; + const e = json.exeRun; + const note = [e.fault, e.timedOut && "timed out", ...e.dialogs.map((d) => `a box: ${d.text}`)] + .filter(Boolean) + .join("; "); + return { lines: e.lines, note }; +} + +async function runVb6(dir, cases, vb6) { + const mod = writeVb6Project(dir, cases); + let r; + try { + r = await runRepro(vb6, dir, { timeoutMs: 120000 }); + } catch (e) { + return { harness: e.message.split("\n")[0] }; + } + if (!r.built) { + // VB6 reports the first error only. Its line counts from 0 and skips the + // Attribute line: the file's line less two. + const { line, message } = parseMakeLog(r.log, ["Module1"]); + const id = line === null ? null : caseAtLine(mod.spans, line + 2); + if (id === null) return { harness: `VB6 did not build the probe: ${message}` }; + return { refused: new Map([[id, message]]) }; + } + const note = [r.fault, r.timedOut && "timed out", ...r.dialogs.map((d) => `a box: ${d.text}`)] + .filter(Boolean) + .join("; "); + return { lines: r.lines, note }; +} + +const wanted = values.runtime.length ? values.runtime : null; +const plan = []; +const want = (rt) => (wanted ? wanted.includes(rt) : rt !== "win64"); +if (want("tb")) plan.push(["tb", (d, c) => runTb(d, c, "win32")]); +if (wanted ? wanted.includes("win64") : files.some((f) => f.win64)) { + plan.push(["win64", (d, c) => runTb(d, c, "win64")]); +} +if (want("vb6")) { + const vb6 = findVb6(values.vb6); + if (!vb6) die(2, values.vb6 ? `no such file: ${values.vb6}\n${NO_VB6}` : NO_VB6); + plan.push(["vb6", (d, c) => runVb6(d, c, vb6)]); +} + +const caseLists = Object.fromEntries( + plan.map(([rt]) => { + // win64 is measured for the files that ask for it, or for all when --runtime names it. + const from = rt === "win64" && !wanted?.includes("win64") ? files.filter((f) => f.win64) : files; + return [rt, probeCases(from, rt, { only })]; + }), +); +const measured = Object.fromEntries( + await Promise.all(plan.map(async ([rt, run]) => [rt, await measure(rt, caseLists[rt], run)])), +); +if (values.keep) console.log(`errors: the probes are in ${work}`); +else rmSync(work, { recursive: true, force: true }); + +// ------------------------------------------------------------------ the report + +let disagreements = 0; +let harness = 0; +for (const [rt] of plan) { + if (measured[rt].harness) { + harness++; + console.log(`${rt}: ${measured[rt].harness}`); + } +} +const shown = (r) => + !r ? "-" : r.refused ? "refused" : r.missing ? "none" : `${r.number}${r.setup ? " (setup)" : ""}`; +let lastPage = null; +const all = new Map(); +for (const [rt] of plan) for (const c of caseLists[rt]) all.set(c.id, c); +for (const c of [...all.values()].sort((a, b) => a.id - b.id)) { + if (c.page !== lastPage) { + console.log(`\ndocs/${c.page} (errors/${c.file})`); + lastPage = c.page; + } + const cells = []; + const wrong = []; + for (const [rt] of plan) { + const mine = caseLists[rt].find((x) => x.id === c.id); + if (!mine) continue; + const r = measured[rt].results.get(c.id); + cells.push(`${rt} ${shown(r)}`); + if (!r) continue; + if (r.refused) wrong.push(`${rt} refused it: ${r.refused}`); + else if (r.missing) wrong.push(`${rt} reported nothing`); + else if (r.setup) wrong.push(`${rt}: its setup raised ${r.number}, ${r.description}`); + else if (r.number !== mine.expect) wrong.push(`${rt} raised ${r.number}, the data says ${mine.expect}`); + else if (r.number !== 0 && r.description !== messages.get(r.number)) { + wrong.push( + `${rt}'s message is "${r.description}", errors/messages.yml has "${messages.get(r.number) ?? "nothing"}"`, + ); + } + } + console.log( + ` ${wrong.length ? "FAIL" : "ok "} ${String(c.row ? c.row.error : 0).padStart(11)} ${c.label} [${cells.join(", ")}]`, + ); + for (const w of wrong) console.log(` ${w}`); + if (wrong.length) disagreements++; +} +console.log( + `\nerrors: ${all.size} cases, ${disagreements ? `${disagreements} disagree with the data` : "all agree with the data"}`, +); +process.exit(harness ? 2 : disagreements ? 1 : 0); diff --git a/scripts/lib/cli-cases.mjs b/scripts/lib/cli-cases.mjs index e7dcd2ae9..4fb93d79b 100644 --- a/scripts/lib/cli-cases.mjs +++ b/scripts/lib/cli-cases.mjs @@ -123,6 +123,14 @@ const CASES = [ { tool: "scripts/vb6run.mjs", args: ["--docs", "--timeout"], exit: 2, stderr: /^--timeout needs a value\nusage: node scripts\/vb6run\.mjs / }, { tool: "scripts/vb6run.mjs", args: ["no-such-file.bas"], exit: 2, stderr: "cannot read no-such-file.bas: no such file\n" }, { tool: "scripts/vb6run.mjs", args: ["--docs", "--vb6", "no-such/VB6.EXE"], exit: 2, stderr: /^no such file: no-such\/VB6\.EXE\nno VB6 found: pass --vb6 / }, + { tool: "scripts/errors.mjs", args: [], exit: 2, stderr: /^give a command\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["measure"], exit: 2, stderr: /^unknown command: measure\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["run", "--help", "--bogus"], exit: 0, stdout: /^usage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["generate"], exit: 2, stderr: /^generate needs the folder to write to\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["run", "--runtime", "vb5"], exit: 2, stderr: /^--runtime: one of tb, vb6, win64\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["write", "--only", "Mid"], exit: 2, stderr: /^--only is for run and generate\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["write", "--keep"], exit: 2, stderr: /^--keep is for run\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["write", "no-such.yml"], exit: 2, stderr: "no such data file: errors/no-such.yml\n" }, { tool: "scripts/addin_test.mjs", args: ["--help"], exit: 0, stdout: /^usage: node scripts\/addin_test\.mjs / }, { tool: "scripts/addin_test.mjs", args: ["--ide"], exit: 2, stderr: "--ide needs a value\n" }, { tool: "scripts/addin_test.mjs", args: ["--ide", ""], exit: 2, stderr: "--ide needs a non-empty value\n" }, @@ -391,6 +399,7 @@ const HELP_TOOLS = { "scripts/check_attribute_sweep.mjs": null, "scripts/check_book_coverage.mjs": null, "scripts/check_ci_workflows.mjs": null, + "scripts/check_errors.mjs": null, "scripts/check_cli.mjs": null, "scripts/check_code_regions.mjs": null, "scripts/check_dot_fit.mjs": null, @@ -412,6 +421,7 @@ const HELP_TOOLS = { "scripts/compare_trees.mjs": null, "scripts/convert_em_dash_separators.mjs": null, "scripts/crawl_check.mjs": null, + "scripts/errors.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", @@ -484,6 +494,7 @@ const REFUSALS = { "scripts/check_links_diff.mjs": ["a"], "scripts/check_lint.mjs": [null, { prefix: "check_lint: " }], "scripts/check_package_tests.mjs": [null], + "scripts/check_errors.mjs": [null], "scripts/check_page_baseline.mjs": [null], "scripts/check_pdf_shims_equiv.mjs": [null], "scripts/check_publish_policy.mjs": ["src"], @@ -511,6 +522,7 @@ const REFUSALS = { "scripts/tbbuild.mjs": ["ide"], "scripts/tbrun.mjs": ["ide"], "scripts/vb6run.mjs": ["vb6"], + "scripts/errors.mjs": ["only"], }; // The cases whose folder must stay empty, as for a help request: a refusal // starts no IDE or browser and writes nothing, and a tool that read the flag as diff --git a/scripts/lib/errors-data.mjs b/scripts/lib/errors-data.mjs new file mode 100644 index 000000000..5c847bfcb --- /dev/null +++ b/scripts/lib/errors-data.mjs @@ -0,0 +1,357 @@ +// What scripts/errors.mjs and scripts/check_errors.mjs share: reading the run-time +// error data under errors/, the probe projects generated from it, and the `Errors` +// section each page gets from it. The format is written down in WIP.Errors.md. +// +// Every export here does its work when called, never on import. + +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; +import yaml from "js-yaml"; +import { DOCS_DIR, REPO_ROOT } from "../../lib/repo-paths.mjs"; + +export const ERRORS_DIR = path.join(REPO_ROOT, "errors"); +export const MESSAGES_FILE = "messages.yml"; + +const ROW_KEYS = new Set(["error", "when", "cases", "vb6", "win64", "bug"]); +const CASE_KEYS = new Set(["expr", "run", "decl", "setup", "only"]); +const PAGE_KEYS = new Set(["rows", "quiet", "none"]); +const FILE_KEYS = new Set(["beta", "win64", "pages"]); + +// ------------------------------------------------------------------ the data + +/** The data files under `dir`, as names relative to it, sorted; messages.yml is not one. */ +export function dataFiles(dir = ERRORS_DIR) { + if (!existsSync(dir)) return []; + return readdirSync(dir) + .filter((n) => n.endsWith(".yml") && n !== MESSAGES_FILE) + .sort(); +} + +/** errors/messages.yml: { number: message }, or a problem. */ +export function loadMessages(dir = ERRORS_DIR) { + const file = path.join(dir, MESSAGES_FILE); + if (!existsSync(file)) return { messages: new Map(), problems: [`${MESSAGES_FILE}: missing`] }; + const raw = yaml.load(readFileSync(file, "utf8")) ?? {}; + const messages = new Map(); + const problems = []; + for (const [k, v] of Object.entries(raw)) { + if (!/^-?\d+$/.test(k) || typeof v !== "string" || !v) + problems.push(`${MESSAGES_FILE}: ${k}: not a number and its message`); + else messages.set(Number(k), v); + } + return { messages, problems }; +} + +const isInt = (v) => Number.isInteger(v); + +// A case as the data writes it, a string (an expression) or a mapping, made one shape: +// { expr?, run?, decl: [], setup: [], only? } +function normaliseCase(c, where, problems) { + if (typeof c === "string") return { expr: c, decl: [], setup: [] }; + if (!c || typeof c !== "object" || Array.isArray(c)) { + problems.push(`${where}: a case is an expression or a mapping`); + return null; + } + for (const k of Object.keys(c)) if (!CASE_KEYS.has(k)) problems.push(`${where}: unknown key ${k}`); + if ((c.expr === undefined) === (c.run === undefined)) problems.push(`${where}: give one of expr and run`); + if (c.only !== undefined && c.only !== "tb") problems.push(`${where}: only: can only be tb`); + const lines = (v) => (v === undefined ? [] : String(v).replace(/\r/g, "").replace(/\n$/, "").split("\n")); + return { + expr: c.expr === undefined ? undefined : String(c.expr), + run: c.run === undefined ? undefined : lines(c.run), + decl: lines(c.decl), + setup: lines(c.setup), + only: c.only, + }; +} + +/** + * One data file, read and checked. `name` is relative to `dir`. + * @returns {{name, beta, win64, pages: Array<{page, rows, quiet, none}>, problems: string[]}} + * a row is { error, when, vb6?, win64?, bug?, cases }; `page` is relative to docs/ + */ +export function loadDataFile(name, dir = ERRORS_DIR) { + const problems = []; + let raw; + try { + raw = yaml.load(readFileSync(path.join(dir, name), "utf8")); + } catch (e) { + return { name, pages: [], problems: [`${name}: ${e.message.split("\n")[0]}`] }; + } + return { name, ...readData(raw, name, problems), problems }; +} + +/** The checking half of loadDataFile, over an already parsed document. */ +export function readData(raw, name, problems) { + if (!raw || typeof raw !== "object") { + problems.push(`${name}: not a mapping`); + return { beta: null, win64: false, pages: [] }; + } + for (const k of Object.keys(raw)) if (!FILE_KEYS.has(k)) problems.push(`${name}: unknown key ${k}`); + if (!isInt(raw.beta)) problems.push(`${name}: beta: the BETA the data was measured on, a number`); + const pages = []; + for (const [page, entry] of Object.entries(raw.pages ?? {})) { + const at = `${name}: ${page}`; + if (!page.endsWith(".md") || page.includes("\\")) + problems.push(`${at}: a page is a path under docs/, with / and .md`); + if (!entry || typeof entry !== "object") { + problems.push(`${at}: not a mapping`); + continue; + } + for (const k of Object.keys(entry)) if (!PAGE_KEYS.has(k)) problems.push(`${at}: unknown key ${k}`); + const rows = []; + (entry.rows ?? []).forEach((r, i) => { + const w = `${at}: row ${i + 1}`; + if (!r || typeof r !== "object") { + problems.push(`${w}: not a mapping`); + return; + } + for (const k of Object.keys(r)) if (!ROW_KEYS.has(k)) problems.push(`${w}: unknown key ${k}`); + if (!isInt(r.error) || r.error === 0) problems.push(`${w}: error: a number other than 0`); + if (typeof r.when !== "string" || !r.when.trim()) + problems.push(`${w}: when: the condition, in the reader's terms`); + else if (/\||\n/.test(r.when.trim())) problems.push(`${w}: when: one line, without |`); + if (r.vb6 !== undefined && !isInt(r.vb6)) problems.push(`${w}: vb6: a number`); + if (r.vb6 !== undefined && r.vb6 === r.error) problems.push(`${w}: vb6: the same as error; leave it out`); + if (r.vb6 !== undefined && typeof r.bug !== "string") { + problems.push(`${w}: bug: a row that differs from VB6 names its bugs/ slug, or pending`); + } + if (r.bug !== undefined && r.vb6 === undefined) problems.push(`${w}: bug: only on a row with vb6:`); + if (r.win64 !== undefined && !isInt(r.win64)) problems.push(`${w}: win64: a number`); + if (!Array.isArray(r.cases) || !r.cases.length) problems.push(`${w}: cases: at least one`); + const cases = (Array.isArray(r.cases) ? r.cases : []) + .map((c, j) => normaliseCase(c, `${w} case ${j + 1}`, problems)) + .filter(Boolean); + rows.push({ error: r.error, when: String(r.when ?? "").trim(), vb6: r.vb6, win64: r.win64, bug: r.bug, cases }); + }); + const quiet = (entry.quiet ?? []) + .map((c, j) => normaliseCase(c, `${at}: quiet ${j + 1}`, problems)) + .filter(Boolean); + if (entry.none !== undefined && (typeof entry.none !== "string" || rows.length)) { + problems.push(`${at}: none: a sentence, on a page without rows`); + } + if (!rows.length && !quiet.length) problems.push(`${at}: neither rows nor quiet cases`); + pages.push({ page, rows, quiet, none: entry.none }); + } + return { beta: raw.beta ?? null, win64: raw.win64 === true, pages }; +} + +// ------------------------------------------------------------------ the probes + +/** + * The cases of `files` to run in `runtime` ("tb", "vb6" or "win64"), each with + * what it expects. Ids are 1-based and stable for one list of files. + */ +export function probeCases(files, runtime, { only } = {}) { + const out = []; + let id = 0; + for (const f of files) { + for (const p of f.pages) { + const take = !only || only.test(p.page); + const push = (c, row, label) => { + id++; + if (!take || (runtime === "vb6" && c.only === "tb")) return; + let expect = row ? row.error : 0; + if (row && runtime === "vb6" && row.vb6 !== undefined) expect = row.vb6; + if (row && runtime === "win64" && row.win64 !== undefined) expect = row.win64; + out.push({ id, file: f.name, page: p.page, label, row, expect, ...c }); + }; + for (const row of p.rows) for (const c of row.cases) push(c, row, c.expr ?? c.run.join(" : ")); + for (const c of p.quiet) push(c, null, c.expr ?? c.run.join(" : ")); + } + } + return out; +} + +// A case's own procedure. `report` is the name of the procedure that writes a line. +function caseLines(c, report) { + const ind = (l) => ` ${l}`; + return [ + `Private Sub C${c.id}()`, + ind("Dim Result As Variant"), + ...c.decl.map(ind), + ind("On Error Resume Next"), + ...c.setup.map(ind), + ind(`If Err.Number <> 0 Then ${report} "setup ${c.id} " & Err.Number & " " & Err.Description: Exit Sub`), + ...(c.expr !== undefined ? [ind(`Result = ${c.expr}`)] : c.run.map(ind)), + ind(`${report} "case ${c.id} " & Err.Number & " " & Err.Description`), + "End Sub", + ]; +} + +/** + * The probe module for `cases`: the twinBASIC module (dialect "tb", a .twin file run as + * an exe by tbrun) or the VB6 one (dialect "vb6", Module1.bas of Probe.vbp). Each case + * is a Sub of its own, run under On Error Resume Next, that reports + * `case `; Main reports `done` last. + * @returns {{text: string, spans: Array<{id, from, to}>}} spans are 1-based file lines + */ +export function probeModule(cases, dialect) { + const head = + dialect === "tb" + ? [ + "' Generated by scripts/errors.mjs from errors/*.yml; do not edit.", + "Module Probe", + "Private Sub Report(ByVal S As String)", + " TbRun.Out S", + "End Sub", + "[RunAfterBuild]", + "Private Sub Built()", + " Debug.Cls", + ' TbRun.Out "ide"', + "End Sub", + ] + : [ + 'Attribute VB_Name = "Module1"', + "' Generated by scripts/errors.mjs from errors/*.yml; do not edit.", + "Option Explicit", + "Private Sub Report(ByVal S As String)", + " Dim F As Integer", + " F = FreeFile", + ' Open App.Path & "\\out.txt" For Append As #F', + " Print #F, S", + " Close #F", + "End Sub", + ]; + const lines = [...head, "Public Sub Main()", ...cases.map((c) => ` C${c.id}`), ' Report "done"', "End Sub"]; + const spans = []; + for (const c of cases) { + const from = lines.length + 1; + lines.push(...caseLines(c, "Report")); + spans.push({ id: c.id, from, to: lines.length }); + } + if (dialect === "tb") lines.push("End Module"); + return { text: `${lines.join("\r\n")}\r\n`, spans }; +} + +/** The id of the case whose procedure holds 1-based `line`, or null. */ +export const caseAtLine = (spans, line) => spans.find((s) => line >= s.from && line <= s.to)?.id ?? null; + +/** + * A probe's output lines, read: Map id -> { number, description, setup }, and + * whether `done` was reached. + */ +export function readProbeOutput(lines) { + const seen = new Map(); + let done = false; + for (const l of lines) { + if (l === "done") done = true; + const m = /^(case|setup) (\d+) (-?\d+) ?(.*)$/.exec(l); + if (m) seen.set(Number(m[2]), { number: Number(m[3]), description: m[4], setup: m[1] === "setup" }); + } + return { seen, done }; +} + +// ------------------------------------------------------------------ the section + +export const OPEN_MARKER = /^[ \t]*$/; +export const CLOSE_MARKER = /^[ \t]*$/; +const FENCE = /^[ \t]*(`{3,}|~{3,})/; +const ANCHOR = /^(#{2,6})[ \t]+(Examples?|See Also)[ \t]*$/; + +/** The page's lines with the fence state of each: [{text, fenced}]. */ +function scan(lines) { + let fence = null; + return lines.map((text) => { + const m = FENCE.exec(text); + const fenced = fence !== null || Boolean(m); + if (m) { + if (fence === null) fence = m[1]; + else if (m[1][0] === fence[0] && m[1].length >= fence.length && text.trim() === m[1]) fence = null; + } + return { text, fenced }; + }); +} + +/** + * The `errors` region of a page: { open, close } as 0-based line indexes (close + * inclusive), null when there is none, and problems with its markers. + */ +export function findSection(text) { + const lines = scan(text.split("\n")); + const problems = []; + let open = null; + let found = null; + lines.forEach(({ text: t, fenced }, i) => { + if (fenced) return; + const l = t.replace(/\r$/, ""); + if (OPEN_MARKER.test(l)) { + if (open !== null || found) problems.push(`line ${i + 1}: a second marker`); + else open = i; + } else if (CLOSE_MARKER.test(l)) { + if (open === null) problems.push(`line ${i + 1}: without its opening marker`); + else { + found = { open, close: i }; + open = null; + } + } + }); + if (open !== null) problems.push(`line ${open + 1}: the errors section is not closed with `); + return { section: found, problems }; +} + +/** The heading level the page uses for Example and See Also: "##" or "###"; "##" when it has neither. */ +export function pageLevel(text) { + for (const { text: t, fenced } of scan(text.split("\n"))) { + const m = !fenced && ANCHOR.exec(t.replace(/\r$/, "")); + if (m) return m[1]; + } + return "##"; +} + +const escapeCell = (s) => s.replace(/\|/g, "\\|"); + +/** + * The section's lines for page entry `p` of data file `name`, or null when the page + * gets none. `level` is the heading's "##"/"###". + */ +export function renderSection(p, { name, beta, messages, level }) { + if (!p.rows.length && p.none === undefined) return null; + const msg = (n) => messages.get(n) ?? "?"; + const out = [ + ``, + `${level} Errors`, + "", + ]; + if (p.none !== undefined) out.push(p.none.trim()); + else { + out.push("| Error | When |", "|---|---|"); + for (const r of p.rows) out.push(`| ${r.error}, *${msg(r.error)}* | ${escapeCell(r.when)} |`); + for (const r of p.rows.filter((x) => x.vb6 !== undefined)) { + const what = r.vb6 === 0 ? "raises no error" : `raises error ${r.vb6}, *${msg(r.vb6)}*, instead`; + out.push("", `> [!NOTE] BETA ${beta}`, `> When ${r.when}, VB6 ${what}.`); + } + } + out.push("", ""); + return out; +} + +/** + * The page text with its section set to `section` (lines, or null to remove it). + * A new section goes before the first Example heading, else before See Also, else + * at the end. + */ +export function spliceSection(text, section) { + const crlf = text.includes("\r\n"); + const lines = text.replace(/\r\n/g, "\n").split("\n"); + const { section: at } = findSection(lines.join("\n")); + if (at) { + let end = at.close + 1; + if (!section) while (end < lines.length && lines[end] === "") end++; + lines.splice(at.open, end - at.open, ...(section ?? [])); + } else if (section) { + const scanned = scan(lines); + const find = (re) => scanned.findIndex(({ text: t, fenced }) => !fenced && re.test(t)); + let i = find(/^#{2,6}[ \t]+Examples?[ \t]*$/); + if (i < 0) i = find(/^#{2,6}[ \t]+See Also[ \t]*$/); + if (i < 0) { + while (lines.length && lines[lines.length - 1] === "") lines.pop(); + lines.push("", ...section, ""); + } else lines.splice(i, 0, ...section, ""); + } + const out = lines.join("\n"); + return crlf ? out.replace(/\n/g, "\r\n") : out; +} + +/** The page's path on disk, for `page` relative to docs/. */ +export const pagePath = (page, docs = DOCS_DIR) => path.join(docs, ...page.split("/")); diff --git a/test.bat b/test.bat index a58a4b12c..05785aef3 100644 --- a/test.bat +++ b/test.bat @@ -177,6 +177,11 @@ node scripts/check_book_coverage.mjs @rem no tree, no install, well under a second. node scripts/check_package_tests.mjs @if errorlevel 1 goto :fail +@rem Every page's Errors section is what scripts\errors.mjs write makes of the +@rem run-time error data under errors\, and the data is in order. Its probes +@rem run on inputs built in memory first. Sources only: no twinBASIC, no VB6. +node scripts/check_errors.mjs +@if errorlevel 1 goto :fail @rem The symbol index (tB/symbols.json) is read by the IDE help add-in, @rem and a build that indexes the reference cleanly says nothing about the @rem rules that did not fire on it. These probes assert each one against From 31d479a5ca7d9d89aa7520a8fc5c0d191b306971 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Tue, 20 Oct 2026 23:11:09 +0200 Subject: [PATCH 07/21] Errors sections: every number in decimal and in VB hexadecimal -2147467259 (`&H80004005`), 5 (`&H5`): in the table and in the VB6 NOTE. WIP.Errors.md records the owner's answers on the trial's open points. --- WIP.Errors.md | 34 +++++++++++++---------- docs/Reference/Core/ReDim.md | 8 +++--- docs/Reference/Default/VBA/Strings/Mid.md | 6 ++-- scripts/check_errors.mjs | 2 +- scripts/lib/errors-data.mjs | 7 +++-- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/WIP.Errors.md b/WIP.Errors.md index 0064da077..3439d3896 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -134,26 +134,32 @@ The section `write` makes, before the page's first Example heading: | Error | When | |---|---| -| -2147467259, *Unspecified error* | an upper bound is less than its lower bound | +| -2147467259 (`&H80004005`), *Unspecified error* | an upper bound is less than its lower bound | > [!NOTE] BETA 1010 -> When an upper bound is less than its lower bound, VB6 raises error 9, *Subscript out of range*, instead. +> When an upper bound is less than its lower bound, VB6 raises error 9 (`&H9`), *Subscript out of range*, instead. ``` -### Decided while building, for the owner to confirm - -- **The heading takes the page's level for Example and See Also**: `###` on the VBA and Core - pages, which still use the old h1→h3 style; `##` on a page without those headings. -- **The message is `Err.Description`, not `Error$(n)`.** For VB's own numbers the two agree; - for an HRESULT they do not: `Error$(-2147467259)` is *Application-defined or - object-defined error*, while the error raised describes itself as *Unspecified error*, - which is what a handler sees. Batch 1 settles which the central page lists. -- **The number is not linked yet**: the central page does not exist. Batch 1 adds the link - in `renderSection` (`scripts/lib/errors-data.mjs`) and rewrites every section with `write`. -- **A row that differs from VB6 must name its reproducer**, or say `bug: pending`; the gate - refuses a `vb6:` row without it. +### Decided while building (owner's answers, 2026-10-20) + +- **The heading takes the page's level for Example** (owner: confirmed): `###` on the VBA and + Core pages, which the build promotes to `##` on a page with `#` and `###` but no `##`; + `##` on a page without an Example or See Also heading. +- **Every number is given in decimal and in VB hexadecimal** (owner), as a 32-bit **Long**: + -2147467259 (`&H80004005`), 5 (`&H5`), in the table and in the VB6 NOTE alike. + `errorNumber` in `scripts/lib/errors-data.mjs` writes it. +- **The message is `Err.Description`, not `Error$(n)`** (owner: undecided). For VB's own + numbers the two agree; for an HRESULT they do not: `Error$(-2147467259)` is + *Application-defined or object-defined error*, while the error raised describes itself as + *Unspecified error*, which is what a handler sees. Batch 1 settles which the central page + lists, and so what the sections show. +- **The number is not linked yet** (owner: agreed): the central page does not exist. Batch 1 + adds the link in `renderSection` (`scripts/lib/errors-data.mjs`) and rewrites every section + with `write`. +- **A row that differs from VB6 must name its reproducer**, or say `bug: pending` (owner: + agreed); the gate refuses a `vb6:` row without it. - **Errors from converting an argument are left out**, as the plan says: `Mid("abc", "x")` raises 13 and `Mid("abc", Null)` 94 in both runtimes, in the conversion of *start* to **Long**. `Mid("abc", 1, Null)` is in the table: *length* is a **Variant**, and the 94 is diff --git a/docs/Reference/Core/ReDim.md b/docs/Reference/Core/ReDim.md index 1991b6c90..4aa2e65c0 100644 --- a/docs/Reference/Core/ReDim.md +++ b/docs/Reference/Core/ReDim.md @@ -60,12 +60,12 @@ The **ReDim** statement acts as a declarative statement if the variable it decla | Error | When | |---|---| -| -2147467259, *Unspecified error* | an upper bound is less than its lower bound | -| 9, *Subscript out of range* | **Preserve** changes a lower bound, or the bounds of any dimension but the last | -| 13, *Type mismatch* | **Preserve** is used on a **Variant** that does not hold an array | +| -2147467259 (`&H80004005`), *Unspecified error* | an upper bound is less than its lower bound | +| 9 (`&H9`), *Subscript out of range* | **Preserve** changes a lower bound, or the bounds of any dimension but the last | +| 13 (`&HD`), *Type mismatch* | **Preserve** is used on a **Variant** that does not hold an array | > [!NOTE] BETA 1010 -> When an upper bound is less than its lower bound, VB6 raises error 9, *Subscript out of range*, instead. +> When an upper bound is less than its lower bound, VB6 raises error 9 (`&H9`), *Subscript out of range*, instead. diff --git a/docs/Reference/Default/VBA/Strings/Mid.md b/docs/Reference/Default/VBA/Strings/Mid.md index 5d2df6c00..51d4fafb4 100644 --- a/docs/Reference/Default/VBA/Strings/Mid.md +++ b/docs/Reference/Default/VBA/Strings/Mid.md @@ -38,9 +38,9 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi | Error | When | |---|---| -| 5, *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative | -| 5, *Invalid procedure call or argument* | **Mid** or **Mid$** is given a *start* greater than 1,073,741,824 or a *length* greater than 1,073,741,823 | -| 94, *Invalid use of Null* | **Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | +| 5 (`&H5`), *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative | +| 5 (`&H5`), *Invalid procedure call or argument* | **Mid** or **Mid$** is given a *start* greater than 1,073,741,824 or a *length* greater than 1,073,741,823 | +| 94 (`&H5E`), *Invalid use of Null* | **Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | diff --git a/scripts/check_errors.mjs b/scripts/check_errors.mjs index 434725c96..532fb7894 100644 --- a/scripts/check_errors.mjs +++ b/scripts/check_errors.mjs @@ -123,7 +123,7 @@ check( section .join("\n") .includes( - "| 5, *Invalid procedure call or argument* | *x* is negative |\n| 9, *Subscript out of range* | the index is out |\n\n> [!NOTE] BETA 1010\n> When the index is out, VB6 raises error 5, *Invalid procedure call or argument*, instead.", + "| 5 (`&H5`), *Invalid procedure call or argument* | *x* is negative |\n| 9 (`&H9`), *Subscript out of range* | the index is out |\n\n> [!NOTE] BETA 1010\n> When the index is out, VB6 raises error 5 (`&H5`), *Invalid procedure call or argument*, instead.", ), ); check("a page with only quiet cases gets no section", renderSection({ rows: [], quiet: [1] }, { messages }) === null); diff --git a/scripts/lib/errors-data.mjs b/scripts/lib/errors-data.mjs index 5c847bfcb..2a1fd73c2 100644 --- a/scripts/lib/errors-data.mjs +++ b/scripts/lib/errors-data.mjs @@ -299,6 +299,9 @@ export function pageLevel(text) { return "##"; } +/** An error number in decimal and in VB hexadecimal, as a 32-bit Long: -2147467259 (`&H80004005`). */ +export const errorNumber = (n) => `${n} (\`&H${(n >>> 0).toString(16).toUpperCase()}\`)`; + const escapeCell = (s) => s.replace(/\|/g, "\\|"); /** @@ -316,9 +319,9 @@ export function renderSection(p, { name, beta, messages, level }) { if (p.none !== undefined) out.push(p.none.trim()); else { out.push("| Error | When |", "|---|---|"); - for (const r of p.rows) out.push(`| ${r.error}, *${msg(r.error)}* | ${escapeCell(r.when)} |`); + for (const r of p.rows) out.push(`| ${errorNumber(r.error)}, *${msg(r.error)}* | ${escapeCell(r.when)} |`); for (const r of p.rows.filter((x) => x.vb6 !== undefined)) { - const what = r.vb6 === 0 ? "raises no error" : `raises error ${r.vb6}, *${msg(r.vb6)}*, instead`; + const what = r.vb6 === 0 ? "raises no error" : `raises error ${errorNumber(r.vb6)}, *${msg(r.vb6)}*, instead`; out.push("", `> [!NOTE] BETA ${beta}`, `> When ${r.when}, VB6 ${what}.`); } } From c6eb2a74b30e18fe8caf9d4a502c1ffe5fad3afb Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Tue, 20 Oct 2026 23:38:46 +0200 Subject: [PATCH 08/21] Run-time errors, batch 1: the central page, its generated index, and the sections linked to it docs/Reference/Run-time-Errors.md lists every number from 1 to 65535 with a message of its own (88, measured: twinBASIC 32-bit and VB6 agree on all of them) and the HRESULTs the data names, each with its anchor, its message and the pages whose Errors sections list it. errors.mjs write generates the index between markers; check_errors.mjs verifies it. Each number in a section now links to its anchor, and rows of a page that raise the same number share one table row, their conditions joined with
. errors/messages.yml takes { raised, error } where Err.Description and Error$(n) differ; the new errors.mjs messages command measures the file in both runtimes. --- WIP.Errors.md | 90 ++++++-- builder/page-baseline.json | 2 +- docs/Documentation/Tools-Defects.md | 10 +- docs/Documentation/Tools-Toolchain-Tests.md | 4 +- docs/Reference/Core/ReDim.md | 8 +- docs/Reference/Default/VBA/Strings/Mid.md | 5 +- docs/Reference/Run-time-Errors.md | 140 ++++++++++++ docs/Reference/index.md | 1 + docs/_book.yml | 5 + errors/messages.yml | 96 +++++++- scripts/check_errors.mjs | 156 ++++++++++++- scripts/errors.mjs | 134 +++++++++--- scripts/lib/cli-cases.mjs | 6 +- scripts/lib/errors-data.mjs | 229 ++++++++++++++++++-- 14 files changed, 802 insertions(+), 84 deletions(-) create mode 100644 docs/Reference/Run-time-Errors.md diff --git a/WIP.Errors.md b/WIP.Errors.md index 3439d3896..c46d26e93 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -48,11 +48,47 @@ An `## Errors` section after the page's main text and before `## Example`: surprising enough to state (a function that returns a value where VB6 raises). - The section is written between markers, so regenerating it replaces only it. -## The central page - -`Reference/Run-time-Errors.md` (name to settle in batch 1): every trappable error number with -its message, measured with `Error$(n)` in both runtimes, the general exclusions above, and, -for each number, a generated list of the procedures whose tables name it. +## The central page (batch 1, built) + +[`docs/Reference/Run-time-Errors.md`](docs/Reference/Run-time-Errors.md), permalink +`/Reference/Run-time-Errors`, under *Reference Section* in the nav and in the book's +*The Core Language* part after Compiler Constants. Hand-written: what the sections list and +leave out (7, 28, 13 and 94 from converting an argument, compile-time TB diagnostics), how a +number is written, the message rule, and a NOTE on `Err.Raise` of a number without a message. +Generated by `errors.mjs write`, between `` and ``: +one table row per number in `messages.yml`, with its anchor (`#error-5`, `#error--2147467259`), +its message and *Listed by*, links to the pages whose tables name it (twinBASIC's number, +not the `vb6:` one), sorted by title. VB's numbers come first in order, then the HRESULTs in +the order of their hexadecimal. Each section's numbers, in the table and in the VB6 NOTE, link +to their anchor. `check_errors.mjs` checks the index as it checks the sections. + +What was measured (BETA 1010, `errors.mjs messages`): + +- **Every number from 1 to 65535 with a message of its own: 88, and twinBASIC 32-bit and VB6 + agree on every one**, for `Error$(n)` and for `Err.Description` after `Err.Raise n`. They + are all in `messages.yml`, whether or not a table names them yet. +- **HRESULTs** are in `messages.yml` only when the data names one: -2147467259 so far. For + these the two messages differ, and the file says `{ raised, error }`. +- **Differences from VB6 that batch 8 (`ErrObject`) should take up**, none queued yet: + `Err.Raise n` of a number from 1 to 65535 without a message of its own leaves + `Err.Description` empty in twinBASIC below 747, and from 747 up gives the Windows system + message of that number (*Page fault was a transition fault.* for 747); VB6 gives + *Application-defined or object-defined error* (the central page says so in a NOTE). + VB6's `Err.Raise` of a common HRESULT raises its own number instead (`&H80070057` raises 5, + `&H80004001` 445, `&H80004002` 430, `&H80020003` 438, `&H80040154` 429, ...) and describes + the rest as *Automation error* with the HRESULT's text on a second line; twinBASIC keeps the + HRESULT and its text. `Err.Raise 65536` raises 5 in VB6, and `Error$(65536)` raises 6; + twinBASIC raises 65536 as *Automation error*, and `Error$(65536)` is generic. twinBASIC's + `Error$` of a few HRESULTs is VB's message (`Error$(&H80070057)` is *Invalid procedure call + or argument*), VB6's is generic. + +### The message: Err.Description, with Error$ beside it (recommended; owner to confirm) + +The sections show `Err.Description`, what a handler sees. For VB's numbers it is `Error$(n)` +too; the central page shows `Error$(n)` beside it where they differ, which so far is only the +HRESULTs (*Unspecified error*; **Error** gives *Application-defined or object-defined error*). +Showing `Error$(n)` in the sections instead would be a one-line change in `renderSection` +(`.raised` to `.error`). ## Tooling (batch 0, built) @@ -68,7 +104,12 @@ Three files and a folder. Their reference is on the Tools pages: case a compiler refuses is reported and dropped, and the probe rebuilt (VB6 stops at its first error, so one rebuild per refused case there). `generate
` writes the probes without running them; `--keep` keeps the ones `run` built. -- `scripts/errors.mjs write` --- writes each page's section from the data. +- `scripts/errors.mjs write` --- writes each page's section from the data, and the central + page's index (batch 1). +- `scripts/errors.mjs messages` --- measures `messages.yml` in twinBASIC 32-bit and VB6 + (batch 1): `Error$(n)` and, after `Err.Raise n`, `Err.Description`, for every number from 1 + to 65535 whose `Error$` is not generic and every other number the file lists. One probe per + runtime, a loop. By hand only, like `run`. - `scripts/check_errors.mjs` --- the gate in `test.bat` and CI: data in order, every section what `write` makes of it, no section without data. No runtime needed. - `scripts/lib/errors-data.mjs` --- what the two share. @@ -124,7 +165,11 @@ for several) and optionally `decl` (`Dim` lines), `setup` (statements before it) rows gets no section; one with `none: ""` and no rows gets a section holding that sentence, for an absence of errors worth stating. Quote a case that holds `: `, starts with a quote, or sits in a `{ }` mapping and holds a comma. `messages.yml` maps each number to its -message; `run` fails a case whose `Err.Description` is not that text. +message, or, where `Err.Description` of the raised error and `Error$(n)` differ, to +`{ raised: , error: }` (batch 1); `run` fails a case whose +`Err.Description` is not the `raised` text. A number a row names must be in the file; a new +HRESULT is added by hand and then measured with `errors.mjs messages`. Rows of a page that +raise the same number are shown as one table row, their *When* cells joined with `
`. The section `write` makes, before the page's first Example heading: @@ -134,10 +179,10 @@ The section `write` makes, before the page's first Example heading: | Error | When | |---|---| -| -2147467259 (`&H80004005`), *Unspecified error* | an upper bound is less than its lower bound | +| [-2147467259 (`&H80004005`)](../../Reference/Run-time-Errors#error--2147467259), *Unspecified error* | an upper bound is less than its lower bound | > [!NOTE] BETA 1010 -> When an upper bound is less than its lower bound, VB6 raises error 9 (`&H9`), *Subscript out of range*, instead. +> When an upper bound is less than its lower bound, VB6 raises error [9 (`&H9`)](../../Reference/Run-time-Errors#error-9), *Subscript out of range*, instead. ``` @@ -153,11 +198,13 @@ The section `write` makes, before the page's first Example heading: - **The message is `Err.Description`, not `Error$(n)`** (owner: undecided). For VB's own numbers the two agree; for an HRESULT they do not: `Error$(-2147467259)` is *Application-defined or object-defined error*, while the error raised describes itself as - *Unspecified error*, which is what a handler sees. Batch 1 settles which the central page - lists, and so what the sections show. -- **The number is not linked yet** (owner: agreed): the central page does not exist. Batch 1 - adds the link in `renderSection` (`scripts/lib/errors-data.mjs`) and rewrites every section - with `write`. + *Unspecified error*, which is what a handler sees. Batch 1 kept `Err.Description` and put + `Error$` beside it on the central page; see *The message* under The central page. +- **Each number links to its row on the central page** (owner: agreed; built in batch 1), in + the table and in the VB6 NOTE, relative to the page's permalink. +- **Rows that raise the same number make one table row** (owner, batch 1): their *When* + conditions joined with `
`, in the order of the data. The data keeps one row per + condition, each with its own cases; only the rendering merges them. - **A row that differs from VB6 must name its reproducer**, or say `bug: pending` (owner: agreed); the gate refuses a `vb6:` row without it. - **Errors from converting an argument are left out**, as the plan says: `Mid("abc", "x")` @@ -210,6 +257,19 @@ Batch 0 is done when: then, not before. The trial's pages may be committed or reverted, as the owner says. 5. This file's batch table says batch 0 is done, and what batch 1 needs to know. +## What batch 2 needs to know (from batch 1) + +- A row whose number is not in `messages.yml` fails the gate. VB's 88 numbers are all there; + an HRESULT is added by hand as `{ raised, error }` and checked with + `node scripts/errors.mjs messages`. +- A data page must have `title:` and `permalink:` in its front matter: the links to and from + the central page are built from them. +- Two rows with the same number are the way to give two conditions their own cases: the table + shows them as one row. +- *Listed by* on the central page is one table cell per number. Error 5 will be listed by + hundreds of pages; once batch 2 has added the Strings pages, look at that cell with the owner + and decide whether the list moves to a section per number. + ## Batches Each batch is one session. They are independent after batch 0, so any order works, and @@ -218,7 +278,7 @@ each can be run when there is budget. Status is kept here. | # | Batch | Pages (about) | Status | |---|---|---|---| | 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | built; trial sections (Mid, ReDim; Len raises nothing) await the owner | -| 1 | The central Run-time errors page | 1 | not started | +| 1 | The central Run-time errors page | 1 | done (branch `errors-batch1`, on top of `errors-batch0`); the page awaits the owner, and so does the `Err.Description`/`Error$` choice | | 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | not started | | 3 | VBA `Conversion` | 28 | not started | | 4 | VBA `Math` (also win64) | 17 | not started | diff --git a/builder/page-baseline.json b/builder/page-baseline.json index 0a44df948..80ca1509d 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 1502, + "pages": 1503, "staticFiles": 1124 } diff --git a/docs/Documentation/Tools-Defects.md b/docs/Documentation/Tools-Defects.md index c5dc767c6..b36420e8f 100644 --- a/docs/Documentation/Tools-Defects.md +++ b/docs/Documentation/Tools-Defects.md @@ -155,18 +155,20 @@ Exit codes: **0** the sample ran, or, with `--docs`, no fence differs and none r node scripts/errors.mjs run [...] [--only ] [--runtime tb|vb6|win64]... [--ide ] [--vb6 ] [--keep] node scripts/errors.mjs generate
[...] [--only ] node scripts/errors.mjs write [...] + node scripts/errors.mjs messages [--runtime tb|vb6]... [--ide ] [--vb6 ] [--keep] -Measures the run-time errors each documented procedure raises, and writes each reference page's Errors section from them. The errors are data, not prose: a YAML file per area under `errors/` lists, for each page, the cases that raise each error and the cases that must raise nothing, and `errors/messages.yml` gives each number's message. The format is in `WIP.Errors.md`. A file is named as it is in `errors/`; with none named, every file there is used. `--only ` keeps the pages whose path under `docs/` matches. +Measures the run-time errors each documented procedure raises, and writes each reference page's Errors section from them. The errors are data, not prose: a YAML file per area under `errors/` lists, for each page, the cases that raise each error and the cases that must raise nothing, and `errors/messages.yml` gives each number's message: the one `Err.Description` gives once the error is raised, and the one `Error$(number)` gives, where the two differ. The format is in `WIP.Errors.md`. A file is named as it is in `errors/`; with none named, every file there is used. `--only ` keeps the pages whose path under `docs/` matches. | Command | Effect | |---|---| | `run` | Builds one probe of the cases for each runtime and runs it: 32-bit twinBASIC (`tb`) and VB6 by default, and 64-bit twinBASIC (`win64`) as well for a data file that says `win64: true`; `--runtime` names them instead, and may be repeated. Each case is a procedure of its own that runs under `On Error Resume Next` and reports `Err.Number` and `Err.Description`. The twinBASIC probe is a tree run by [`tbrun --exe`](Tools-Compiler#tbrun), so the cases run in the built exe and not in the IDE, and the VB6 probe is a `Probe.vbp` built and run by the code [`bug_repro.mjs vb6`](#bug-repro) uses, on a private desktop. A case a compiler refuses is reported with the compiler's message and left out, and the probe is built again without it, so one bad case costs a rebuild and not the run. Each case is printed with what each runtime raised, and a case is `FAIL` when a runtime raised another number than the data says, refused it, never reported, or raised a number whose message is not the one in `messages.yml`. `--keep` keeps the generated probes and prints where they are. | | `generate` | Writes the probes to `/tb`, a tree for `tbrun --exe`, and `/vb6`, a `Probe.vbp` project, without running them. | -| `write` | Writes each page's section from the data, between `` and ``: before the page's first Example heading, else before See Also, else at the end, at the level the page gives those headings. A page whose data lists only cases that raise nothing gets no section, and loses one it had. Writing again changes nothing. [`check_errors.mjs`](Tools-Toolchain-Tests#check-errors) is its gate. | +| `write` | Writes each page's section from the data, between `` and ``: before the page's first Example heading, else before See Also, else at the end, at the level the page gives those headings. Rows of the data that raise the same number make one row of the table, their conditions one line each, in the order of the data. A page whose data lists only cases that raise nothing gets no section, and loses one it had. Each number in a section links to its row on the central page, [Run-time Errors](../../Reference/Run-time-Errors), and `write` also writes that page's index between `` and ``: a row for every number in `messages.yml`, with its anchor, its message and links to the pages whose sections list it. Writing again changes nothing. [`check_errors.mjs`](Tools-Toolchain-Tests#check-errors) is its gate. | +| `messages` | Builds one probe of the messages for each runtime, 32-bit twinBASIC and VB6 unless `--runtime` names one, runs it as `run` runs its probe, and compares it with `messages.yml`. The probe gives `Error$(n)` and, after `Err.Raise n`, `Err.Number` and `Err.Description`, for every number from 1 to 65535 whose `Error$` is not *Application-defined or object-defined error*, and for every other number the file lists. Every such number from 1 to 65535 must be in the file, and every number in it must give its messages: both in both runtimes for a number from 1 to 65535; for another number, both in twinBASIC and `Error$` in VB6, since VB6's `Err.Raise` turns many HRESULTs into its own numbers (`Err.Raise &H80070057` raises 5). Each disagreement is printed. | -`run` needs twinBASIC and VB6, found as `tbrun` and [`vb6run.mjs`](#vb6run) find them, so it is outside every gate and outside CI, and is run by a person when a new BETA is installed. `generate` and `write` need neither. +`run` and `messages` need twinBASIC and VB6, found as `tbrun` and [`vb6run.mjs`](#vb6run) find them, so they are outside every gate and outside CI, and are run by a person when a new BETA is installed. `generate` and `write` need neither. -Exit codes: **0** `run`: every runtime agrees with the data; `generate`, `write`: done; **1** `run`: a runtime disagrees with the data; **2** a refused command line, data with a problem, a runtime that could not be run or a probe that ended early, or a crash. +Exit codes: **0** `run`, `messages`: every runtime agrees with the data; `generate`, `write`: done; **1** `run`, `messages`: a runtime disagrees with the data; **2** a refused command line, data with a problem, a runtime that could not be run or a probe that ended early, or a crash. ## ide_js.mjs {: #ide-js } diff --git a/docs/Documentation/Tools-Toolchain-Tests.md b/docs/Documentation/Tools-Toolchain-Tests.md index 0ad60e8db..3149fdb5b 100644 --- a/docs/Documentation/Tools-Toolchain-Tests.md +++ b/docs/Documentation/Tools-Toolchain-Tests.md @@ -400,9 +400,9 @@ Exit codes: **0** every probe passed and every test project is in order, **1** a node scripts/check_errors.mjs -Verifies that every reference page's Errors section is what [`errors.mjs write`](Tools-Defects#errors) makes of the run-time error data under `errors/`, and that the data is in order: every file reads, with no key it does not know; every error number it names has its message in `errors/messages.yml`; a row that differs from VB6 names its reproducer under `bugs/`, or says `pending`; every page it names exists and is named by one file only; and no page under `docs/` has a section that no data file names. It reads only the sources, so it needs no twinBASIC, no VB6 and no built tree. Measuring the data is `errors.mjs run`, which is not a gate. +Verifies that every reference page's Errors section is what [`errors.mjs write`](Tools-Defects#errors) makes of the run-time error data under `errors/`, and that the data is in order: every file reads, with no key it does not know; every error number it names has its message in `errors/messages.yml`; a row that differs from VB6 names its reproducer under `bugs/`, or says `pending`; every page it names exists, has a title and a permalink, and is named by one file only; no page under `docs/` has a section that no data file names; and the index of the numbers on the central page, [Run-time Errors](../../Reference/Run-time-Errors), is what `write` makes of the data and `errors/messages.yml`. It reads only the sources, so it needs no twinBASIC, no VB6 and no built tree. Measuring the data is `errors.mjs run`, and measuring the messages `errors.mjs messages`; neither is a gate. -Sixteen probes run first, on data and pages built in memory: how the data is read and refused, the section and its VB6 note, where a new section goes and that a marker inside a fence is not one, that writing twice changes nothing and removing a section gives the page back, and how the probe modules and their output are read. +Twenty-eight probes run first, on data and pages built in memory: how the data and the messages are read and refused, the section with its numbers linked to the central page, its rows for one number merged into one, and its VB6 note, where a new section goes and that a marker inside a fence is not one, that writing twice changes nothing and removing a section gives the page back, the links between permalinks, the central page's index and its markers, how the probe modules and their output are read, and how the measured messages are compared with the list. Exit codes: **0** every probe passed and every section matches its data, **1** a probe failed, or the data or a section is not in order, **2** the gate could not run: a refused command line, or a crash. diff --git a/docs/Reference/Core/ReDim.md b/docs/Reference/Core/ReDim.md index 4aa2e65c0..3cc8823c3 100644 --- a/docs/Reference/Core/ReDim.md +++ b/docs/Reference/Core/ReDim.md @@ -60,12 +60,12 @@ The **ReDim** statement acts as a declarative statement if the variable it decla | Error | When | |---|---| -| -2147467259 (`&H80004005`), *Unspecified error* | an upper bound is less than its lower bound | -| 9 (`&H9`), *Subscript out of range* | **Preserve** changes a lower bound, or the bounds of any dimension but the last | -| 13 (`&HD`), *Type mismatch* | **Preserve** is used on a **Variant** that does not hold an array | +| [-2147467259 (`&H80004005`)](../../Reference/Run-time-Errors#error--2147467259), *Unspecified error* | an upper bound is less than its lower bound | +| [9 (`&H9`)](../../Reference/Run-time-Errors#error-9), *Subscript out of range* | **Preserve** changes a lower bound, or the bounds of any dimension but the last | +| [13 (`&HD`)](../../Reference/Run-time-Errors#error-13), *Type mismatch* | **Preserve** is used on a **Variant** that does not hold an array | > [!NOTE] BETA 1010 -> When an upper bound is less than its lower bound, VB6 raises error 9 (`&H9`), *Subscript out of range*, instead. +> When an upper bound is less than its lower bound, VB6 raises error [9 (`&H9`)](../../Reference/Run-time-Errors#error-9), *Subscript out of range*, instead. diff --git a/docs/Reference/Default/VBA/Strings/Mid.md b/docs/Reference/Default/VBA/Strings/Mid.md index 51d4fafb4..ced7c64b9 100644 --- a/docs/Reference/Default/VBA/Strings/Mid.md +++ b/docs/Reference/Default/VBA/Strings/Mid.md @@ -38,9 +38,8 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi | Error | When | |---|---| -| 5 (`&H5`), *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative | -| 5 (`&H5`), *Invalid procedure call or argument* | **Mid** or **Mid$** is given a *start* greater than 1,073,741,824 or a *length* greater than 1,073,741,823 | -| 94 (`&H5E`), *Invalid use of Null* | **Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative
**Mid** or **Mid$** is given a *start* greater than 1,073,741,824 or a *length* greater than 1,073,741,823 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | diff --git a/docs/Reference/Run-time-Errors.md b/docs/Reference/Run-time-Errors.md new file mode 100644 index 000000000..d4106f851 --- /dev/null +++ b/docs/Reference/Run-time-Errors.md @@ -0,0 +1,140 @@ +--- +title: Run-time Errors +parent: Reference Section +nav_order: 12 +permalink: /Reference/Run-time-Errors +index_also: trappable errors, error numbers, error messages +--- + +# Run-time Errors +{: .no_toc } + +Every run-time error number that has a message of its own, with that message, and the reference pages that list each number among the errors their procedure raises. + +A run-time error can be trapped: [**On Error**](../tB/Core/On-Error) sends it to a handler, which reads its number from [**Err.Number**](../tB/Modules/ErrObject/Number) and its message from [**Err.Description**](../tB/Modules/ErrObject/Description). A reference page that documents a procedure, a statement or an operator has an **Errors** section when it raises errors of its own: a table of each number it raises, and when. Each number there links to its row below. + +## What the Errors sections list + +A section lists the errors that a procedure raises from its own arguments and its own state, as twinBASIC raises them. Where VB6 raises another error, or none, a note under the table says so. Every row is measured in twinBASIC and in VB6, and not copied from another reference. + +A section does not list: + +- errors that any code can raise: 7, *Out of memory*, and 28, *Out of stack space*; +- an error in converting an argument to the type of its parameter before the call is made: 13, *Type mismatch*, for `Mid("abc", "x")`, and 94, *Invalid use of Null*, for `Mid("abc", Null)`, raised because the *start* parameter of **Mid** is a **Long**. A **Variant** parameter takes any value, so an error for a value of a **Variant** parameter is the procedure's own, and is listed; +- compile-time errors: the twinBASIC compiler reports these with diagnostic codes such as `TB5001`, before the program runs, and **On Error** cannot trap them. + +A procedure that raises no error of its own has no Errors section. + +## How a number is written + +Each number is given in decimal and in hexadecimal, as a 32-bit **Long**: 5 (`&H5`), -2147467259 (`&H80004005`). Numbers from 1 to 65535 are VB's own. A negative number is an HRESULT, a COM error code. Errors that a program raises for itself with [**Err.Raise**](../tB/Modules/ErrObject/Raise) usually add their own code to **vbObjectError**, -2147221504 (`&H80040000`). + +The message is the one that **Err.Description** gives for the error once it is raised. For the numbers from 1 to 65535, [**Error**](../tB/Modules/Conversion/Error)`(number)` gives the same text. For an HRESULT, **Error** gives *Application-defined or object-defined error*, as it does for any number without a message of its own, and the table shows both. + +> [!NOTE] BETA 1010 +> For a number from 1 to 65535 that has no message of its own, **Error** gives *Application-defined or object-defined error* in twinBASIC and VB6. After `Err.Raise` of such a number, **Err.Description** in VB6 is that same text; in twinBASIC it is empty for a number below 747, and for 747 and above it is the Windows system message of that number, such as *Page fault was a transition fault.* for 747. + +## Error numbers + +The messages are those of twinBASIC 32-bit and VB6, which agree for every number from 1 to 65535. *Listed by* names the pages whose Errors section lists the number. + + + +| Error | Message | Listed by | +|---|---|---| +| **3**{: #error-3 } (`&H3`) | *Return without GoSub* | --- | +| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [Mid](../tB/Modules/Strings/Mid) | +| **6**{: #error-6 } (`&H6`) | *Overflow* | --- | +| **7**{: #error-7 } (`&H7`) | *Out of memory* | --- | +| **9**{: #error-9 } (`&H9`) | *Subscript out of range* | [ReDim](../tB/Core/ReDim) | +| **10**{: #error-10 } (`&HA`) | *This array is fixed or temporarily locked* | --- | +| **11**{: #error-11 } (`&HB`) | *Division by zero* | --- | +| **13**{: #error-13 } (`&HD`) | *Type mismatch* | [ReDim](../tB/Core/ReDim) | +| **14**{: #error-14 } (`&HE`) | *Out of string space* | --- | +| **16**{: #error-16 } (`&H10`) | *Expression too complex* | --- | +| **17**{: #error-17 } (`&H11`) | *Can't perform requested operation* | --- | +| **18**{: #error-18 } (`&H12`) | *User interrupt occurred* | --- | +| **20**{: #error-20 } (`&H14`) | *Resume without error* | --- | +| **28**{: #error-28 } (`&H1C`) | *Out of stack space* | --- | +| **35**{: #error-35 } (`&H23`) | *Sub or Function not defined* | --- | +| **47**{: #error-47 } (`&H2F`) | *Too many DLL application clients* | --- | +| **48**{: #error-48 } (`&H30`) | *Error in loading DLL* | --- | +| **49**{: #error-49 } (`&H31`) | *Bad DLL calling convention* | --- | +| **51**{: #error-51 } (`&H33`) | *Internal error* | --- | +| **52**{: #error-52 } (`&H34`) | *Bad file name or number* | --- | +| **53**{: #error-53 } (`&H35`) | *File not found* | --- | +| **54**{: #error-54 } (`&H36`) | *Bad file mode* | --- | +| **55**{: #error-55 } (`&H37`) | *File already open* | --- | +| **57**{: #error-57 } (`&H39`) | *Device I/O error* | --- | +| **58**{: #error-58 } (`&H3A`) | *File already exists* | --- | +| **59**{: #error-59 } (`&H3B`) | *Bad record length* | --- | +| **61**{: #error-61 } (`&H3D`) | *Disk full* | --- | +| **62**{: #error-62 } (`&H3E`) | *Input past end of file* | --- | +| **63**{: #error-63 } (`&H3F`) | *Bad record number* | --- | +| **67**{: #error-67 } (`&H43`) | *Too many files* | --- | +| **68**{: #error-68 } (`&H44`) | *Device unavailable* | --- | +| **70**{: #error-70 } (`&H46`) | *Permission denied* | --- | +| **71**{: #error-71 } (`&H47`) | *Disk not ready* | --- | +| **74**{: #error-74 } (`&H4A`) | *Can't rename with different drive* | --- | +| **75**{: #error-75 } (`&H4B`) | *Path/File access error* | --- | +| **76**{: #error-76 } (`&H4C`) | *Path not found* | --- | +| **91**{: #error-91 } (`&H5B`) | *Object variable or With block variable not set* | --- | +| **92**{: #error-92 } (`&H5C`) | *For loop not initialized* | --- | +| **93**{: #error-93 } (`&H5D`) | *Invalid pattern string* | --- | +| **94**{: #error-94 } (`&H5E`) | *Invalid use of Null* | [Mid](../tB/Modules/Strings/Mid) | +| **96**{: #error-96 } (`&H60`) | *Unable to sink events of object because the object is already firing events to the maximum number of event receivers that it supports* | --- | +| **97**{: #error-97 } (`&H61`) | *Can not call friend function on object which is not an instance of defining class* | --- | +| **98**{: #error-98 } (`&H62`) | *A property or method call cannot include a reference to a private object, either as an argument or as a return value* | --- | +| **321**{: #error-321 } (`&H141`) | *Invalid file format* | --- | +| **322**{: #error-322 } (`&H142`) | *Can't create necessary temporary file* | --- | +| **325**{: #error-325 } (`&H145`) | *Invalid format in resource file* | --- | +| **380**{: #error-380 } (`&H17C`) | *Invalid property value* | --- | +| **381**{: #error-381 } (`&H17D`) | *Invalid property array index* | --- | +| **382**{: #error-382 } (`&H17E`) | *Set not supported at runtime* | --- | +| **383**{: #error-383 } (`&H17F`) | *Set not supported (read-only property)* | --- | +| **385**{: #error-385 } (`&H181`) | *Need property array index* | --- | +| **387**{: #error-387 } (`&H183`) | *Set not permitted* | --- | +| **393**{: #error-393 } (`&H189`) | *Get not supported at runtime* | --- | +| **394**{: #error-394 } (`&H18A`) | *Get not supported (write-only property)* | --- | +| **422**{: #error-422 } (`&H1A6`) | *Property not found* | --- | +| **423**{: #error-423 } (`&H1A7`) | *Property or method not found* | --- | +| **424**{: #error-424 } (`&H1A8`) | *Object required* | --- | +| **429**{: #error-429 } (`&H1AD`) | *ActiveX component can't create object* | --- | +| **430**{: #error-430 } (`&H1AE`) | *Class does not support Automation or does not support expected interface* | --- | +| **432**{: #error-432 } (`&H1B0`) | *File name or class name not found during Automation operation* | --- | +| **438**{: #error-438 } (`&H1B6`) | *Object doesn't support this property or method* | --- | +| **440**{: #error-440 } (`&H1B8`) | *Automation error* | --- | +| **442**{: #error-442 } (`&H1BA`) | *Connection to type library or object library for remote process has been lost. Press OK for dialog to remove reference.* | --- | +| **443**{: #error-443 } (`&H1BB`) | *Automation object does not have a default value* | --- | +| **445**{: #error-445 } (`&H1BD`) | *Object doesn't support this action* | --- | +| **446**{: #error-446 } (`&H1BE`) | *Object doesn't support named arguments* | --- | +| **447**{: #error-447 } (`&H1BF`) | *Object doesn't support current locale setting* | --- | +| **448**{: #error-448 } (`&H1C0`) | *Named argument not found* | --- | +| **449**{: #error-449 } (`&H1C1`) | *Argument not optional* | --- | +| **450**{: #error-450 } (`&H1C2`) | *Wrong number of arguments or invalid property assignment* | --- | +| **451**{: #error-451 } (`&H1C3`) | *Property let procedure not defined and property get procedure did not return an object* | --- | +| **452**{: #error-452 } (`&H1C4`) | *Invalid ordinal* | --- | +| **453**{: #error-453 } (`&H1C5`) | *Specified DLL function not found* | --- | +| **454**{: #error-454 } (`&H1C6`) | *Code resource not found* | --- | +| **455**{: #error-455 } (`&H1C7`) | *Code resource lock error* | --- | +| **457**{: #error-457 } (`&H1C9`) | *This key is already associated with an element of this collection* | --- | +| **458**{: #error-458 } (`&H1CA`) | *Variable uses an Automation type not supported in Visual Basic* | --- | +| **459**{: #error-459 } (`&H1CB`) | *Object or class does not support the set of events* | --- | +| **460**{: #error-460 } (`&H1CC`) | *Invalid clipboard format* | --- | +| **461**{: #error-461 } (`&H1CD`) | *Method or data member not found* | --- | +| **462**{: #error-462 } (`&H1CE`) | *The remote server machine does not exist or is unavailable* | --- | +| **463**{: #error-463 } (`&H1CF`) | *Class not registered on local machine* | --- | +| **481**{: #error-481 } (`&H1E1`) | *Invalid picture* | --- | +| **482**{: #error-482 } (`&H1E2`) | *Printer error* | --- | +| **735**{: #error-735 } (`&H2DF`) | *Can't save file to TEMP* | --- | +| **744**{: #error-744 } (`&H2E8`) | *Search text not found* | --- | +| **746**{: #error-746 } (`&H2EA`) | *Replacements too long* | --- | +| **-2147467259**{: #error--2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | [ReDim](../tB/Core/ReDim) | + + + +## See Also + +- [**On Error**](../tB/Core/On-Error) statement +- [**Err** object](../tB/Modules/ErrObject/) +- [**Error**](../tB/Modules/Conversion/Error) function diff --git a/docs/Reference/index.md b/docs/Reference/index.md index a5473cdb9..cbccd3b35 100644 --- a/docs/Reference/index.md +++ b/docs/Reference/index.md @@ -17,6 +17,7 @@ The reference is organized into three layers: the language constructs the compil - [**Enumerations**](Reference/Enumerations) -- index of all {{tbdocs:enumerations}} enumeration types across all {{tbdocs:packages}} packages, grouped by package and A--Z - [**Data Types**](Reference/Data-Types) -- storage size, range, and suffix for every intrinsic type (**Boolean** through **Variant**) - [**Compiler Constants**](Reference/Compiler-Constants) -- the `#If` symbols the compiler recognises +- [**Run-time Errors**](Reference/Run-time-Errors) -- every trappable error number with its message, and the pages that list it - [**Attributes**](../tB/Core/Attributes) -- `[Description(...)]`, `[COMCreatable(...)]`, and the rest of the attribute syntax - [**COM Interfaces**](Reference/COM-Interfaces/) -- the standard COM interfaces twinBASIC code implements or calls, such as **IEnumVARIANT** - [**twinBASIC Additions**](Reference/twinBASIC-Additions) -- curated list of language and runtime additions beyond standard VBA diff --git a/docs/_book.yml b/docs/_book.yml index aa4c7794c..379d104b5 100644 --- a/docs/_book.yml +++ b/docs/_book.yml @@ -357,6 +357,11 @@ parts: no_outline_entry: true no_heading_shift: true outline_closed: true + - title: Run-time Errors + page: /Reference/Run-time-Errors + no_outline_entry: true + no_heading_shift: true + outline_closed: true - title: Attributes page: /tB/Core/Attributes no_outline_entry: true diff --git a/errors/messages.yml b/errors/messages.yml index 2cc0ee679..27e7bf0da 100644 --- a/errors/messages.yml +++ b/errors/messages.yml @@ -1,7 +1,97 @@ -# The message of each run-time error number, as the runtime's Err.Description gives it -# when the error is raised. scripts/errors.mjs run checks every case against it. +# The message of each run-time error number: what Err.Description gives once the error +# is raised, and what Error$(number) gives. A number gives one message where the two +# agree, and raised: and error: where they differ. Every number from 1 to 65535 with a +# message of its own is here; another number is here when the data names it. +# scripts/errors.mjs messages measures the file, and run checks every case against it. +# Measured on BETA 1010, in twinBASIC 32-bit and VB6, which agree on every number below +# 65536. +3: Return without GoSub 5: Invalid procedure call or argument +6: Overflow +7: Out of memory 9: Subscript out of range +10: This array is fixed or temporarily locked +11: Division by zero 13: Type mismatch +14: Out of string space +16: Expression too complex +17: "Can't perform requested operation" +18: User interrupt occurred +20: Resume without error +28: Out of stack space +35: Sub or Function not defined +47: Too many DLL application clients +48: Error in loading DLL +49: Bad DLL calling convention +51: Internal error +52: Bad file name or number +53: File not found +54: Bad file mode +55: File already open +57: Device I/O error +58: File already exists +59: Bad record length +61: Disk full +62: Input past end of file +63: Bad record number +67: Too many files +68: Device unavailable +70: Permission denied +71: Disk not ready +74: "Can't rename with different drive" +75: Path/File access error +76: Path not found +91: Object variable or With block variable not set +92: For loop not initialized +93: Invalid pattern string 94: Invalid use of Null --2147467259: Unspecified error +96: Unable to sink events of object because the object is already firing events to the maximum number of event receivers that it supports +97: Can not call friend function on object which is not an instance of defining class +98: A property or method call cannot include a reference to a private object, either as an argument or as a return value +321: Invalid file format +322: "Can't create necessary temporary file" +325: Invalid format in resource file +380: Invalid property value +381: Invalid property array index +382: Set not supported at runtime +383: Set not supported (read-only property) +385: Need property array index +387: Set not permitted +393: Get not supported at runtime +394: Get not supported (write-only property) +422: Property not found +423: Property or method not found +424: Object required +429: "ActiveX component can't create object" +430: Class does not support Automation or does not support expected interface +432: File name or class name not found during Automation operation +438: "Object doesn't support this property or method" +440: Automation error +442: Connection to type library or object library for remote process has been lost. Press OK for dialog to remove reference. +443: Automation object does not have a default value +445: "Object doesn't support this action" +446: "Object doesn't support named arguments" +447: "Object doesn't support current locale setting" +448: Named argument not found +449: Argument not optional +450: Wrong number of arguments or invalid property assignment +451: Property let procedure not defined and property get procedure did not return an object +452: Invalid ordinal +453: Specified DLL function not found +454: Code resource not found +455: Code resource lock error +457: This key is already associated with an element of this collection +458: Variable uses an Automation type not supported in Visual Basic +459: Object or class does not support the set of events +460: Invalid clipboard format +461: Method or data member not found +462: The remote server machine does not exist or is unavailable +463: Class not registered on local machine +481: Invalid picture +482: Printer error +735: "Can't save file to TEMP" +744: Search text not found +746: Replacements too long +-2147467259: + raised: Unspecified error + error: Application-defined or object-defined error diff --git a/scripts/check_errors.mjs b/scripts/check_errors.mjs index 532fb7894..f7eebe519 100644 --- a/scripts/check_errors.mjs +++ b/scripts/check_errors.mjs @@ -14,7 +14,9 @@ // * a row that differs from VB6 names its reproducer under bugs/, or says pending; // * every page the data names exists, in one data file only, and its section is // what write would make of it, or absent when the data gives it none; -// * no page under docs/ has a section the data does not name. +// * no page under docs/ has a section the data does not name; +// * the index of the numbers on the central page, docs/Reference/Run-time-Errors.md, +// is what write makes of the data and errors/messages.yml. // It starts with probes of the library on inputs of its own. // // Exit codes: @@ -29,18 +31,27 @@ import { exitOnCrash, parseCli, printHelpAndExit, withUsageError } from "../lib/ import { markdownFiles } from "../lib/markdown-files.mjs"; import { DOCS_DIR, REPO_ROOT } from "../lib/repo-paths.mjs"; import { + CENTRAL_PAGE, caseAtLine, + compareMessages, dataFiles, findSection, loadDataFile, loadMessages, + messageProbeModule, pageLevel, + pageMeta, pagePath, probeCases, probeModule, readData, + readMessageOutput, + readMessages, readProbeOutput, + relativeLink, + renderIndex, renderSection, + spliceIndex, spliceSection, } from "./lib/errors-data.mjs"; import { createProbes } from "./lib/gate-probes.mjs"; @@ -94,10 +105,27 @@ pages: cases: [G(1)] quiet: [F(1)] `); -const messages = new Map([ - [5, "Invalid procedure call or argument"], - [9, "Subscript out of range"], -]); +const { messages, problems: sampleMessageProblems } = readMessages({ + 5: "Invalid procedure call or argument", + 9: "Subscript out of range", + "-2147467259": { raised: "Unspecified error", error: "Application-defined or object-defined error" }, +}); +check( + "messages read, in both forms", + sampleMessageProblems.length === 0 && + messages.get(-2147467259).raised === "Unspecified error" && + messages.get(5).error === messages.get(5).raised, +); +check( + "a bad message is refused", + readMessages({ + x: "a", + 1: "", + 2: { raised: "a" }, + 3: { raised: "a", error: "a" }, + 4: { raised: "a", error: "b", vb6: "c" }, + }).problems.length === 5, +); check( "a sample reads with only the problem it has", sample.problems.length === 1 && /without \|/.test(sample.problems[0]), @@ -117,15 +145,37 @@ check("a page with nothing to probe is refused", data("beta: 1\npages:\n A.md: const page = sample.pages[0]; page.rows[1].when = "the index is out"; -const section = renderSection(page, { name: "probe.yml", beta: 1010, messages, level: "###" }); +const section = renderSection(page, { + name: "probe.yml", + beta: 1010, + messages, + level: "###", + permalink: "/tB/Modules/Strings/Mid", +}); check( - "the section has the table and the VB6 note", + "the section has the table, its numbers linked to the central page, and the VB6 note", section .join("\n") .includes( - "| 5 (`&H5`), *Invalid procedure call or argument* | *x* is negative |\n| 9 (`&H9`), *Subscript out of range* | the index is out |\n\n> [!NOTE] BETA 1010\n> When the index is out, VB6 raises error 5 (`&H5`), *Invalid procedure call or argument*, instead.", + "| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *x* is negative |\n| [9 (`&H9`)](../../../Reference/Run-time-Errors#error-9), *Subscript out of range* | the index is out |\n\n> [!NOTE] BETA 1010\n> When the index is out, VB6 raises error [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*, instead.", ), ); +const merged = renderSection( + { + rows: [ + { error: 5, when: "*x* is negative", cases: [] }, + { error: 9, when: "the index is out", cases: [] }, + { error: 5, when: "*y* is 0", cases: [] }, + ], + quiet: [], + }, + { name: "probe.yml", beta: 1010, messages, level: "##", permalink: "/A" }, +).filter((l) => l.startsWith("| [")); +check( + "rows that raise the same number share one row, their conditions a line each", + merged.length === 2 && merged[0].endsWith("| *x* is negative
*y* is 0 |"), + merged.join("\n"), +); check("a page with only quiet cases gets no section", renderSection({ rows: [], quiet: [1] }, { messages }) === null); const doc = "# T\n\ntext\n\n```tb\n### Example\n\n```\n\n### Example\n\ncode\n\n### See Also\n"; @@ -160,6 +210,70 @@ for (const dialect of ["tb", "vb6"]) { caseAtLine(mod.spans, mod.spans[1].from + 3) === 2, ); } +check( + "links are relative to the permalinks", + relativeLink("/Reference/Run-time-Errors", "/tB/Core/ReDim") === "../tB/Core/ReDim" && + relativeLink("/tB/Modules/ErrObject/", "/Reference/Run-time-Errors") === "../../../Reference/Run-time-Errors" && + relativeLink("/Reference/Run-time-Errors", "/tB/Modules/ErrObject/") === "../tB/Modules/ErrObject/", +); +check( + "the title and the permalink come from the front matter", + pageMeta("---\ntitle: Mid\npermalink: /tB/Modules/Strings/Mid\n---\n# Mid\n").permalink === + "/tB/Modules/Strings/Mid" && pageMeta("# no front matter\n").title === null, +); + +const index = renderIndex([sample], messages, new Map([["A/B.md", { title: "B", permalink: "/tB/B" }]])); +check( + "the index has a row per number, in order, with its anchor and the pages that name it", + index.slice(4, 7).join("\n") === + "| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [B](../tB/B) |\n| **9**{: #error-9 } (`&H9`) | *Subscript out of range* | [B](../tB/B) |\n| **-2147467259**{: #error--2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | --- |", + index.join("\n"), +); +const central = "# T\n\n\nold\n\n\n## See Also\n"; +const spliced = spliceIndex(central, index); +check( + "the index goes between its markers, and writing it again changes nothing", + spliced.problem === null && + spliced.text.includes("| **5**") && + spliceIndex(spliced.text, index).text === spliced.text, +); +check("a page without the index markers is refused", spliceIndex("# T\n", index).problem !== null); + +const msgOut = readMessageOutput([ + "msg\t5\tInvalid procedure call or argument\t5\tInvalid procedure call or argument", + "done", +]); +const msg5 = new Map([ + [5, { raised: "Invalid procedure call or argument", error: "Invalid procedure call or argument" }], +]); +check( + "messages agree with a probe that says the same, and not with one that differs", + msgOut.done && + compareMessages(msg5, { tb: msgOut.seen }).length === 0 && + compareMessages(new Map(), { tb: msgOut.seen }).length === 1 && + compareMessages(msg5, { tb: new Map() }).length === 1, +); +check( + "VB6's Err.Raise of an HRESULT is not compared, its Error$ is", + compareMessages(new Map([[-2147467259, { raised: "Unspecified error", error: "G" }]]), { + vb6: new Map([[-2147467259, { error: "G", number: -2147467259, description: "Automation error" }]]), + }).length === 0 && + compareMessages(new Map([[-2147467259, { raised: "Unspecified error", error: "G" }]]), { + vb6: new Map([[-2147467259, { error: "H", number: -2147467259, description: "Automation error" }]]), + }).length === 1, +); + +for (const dialect of ["tb", "vb6"]) { + const lines = messageProbeModule([-2147467259], dialect).split("\r\n"); + check( + `${dialect}: the messages' probe runs every VB number, then the others, then ends`, + lines.includes(" For N = 1 To 65535") && + lines.includes(" One -2147467259, True") && + lines.filter((l) => l === "Public Sub Main()").length === 1 && + (dialect === "vb6" || lines.includes("End Module")), + ); +} + const out = readProbeOutput(["case 1 5 Invalid procedure call or argument", "setup 2 9 x", "done"]); check("probe output reads", out.done && out.seen.get(1).number === 5 && out.seen.get(2).setup); @@ -205,11 +319,19 @@ for (const f of files) { } const text = readFileSync(file, "utf8"); const marks = findSection(text).problems; + const meta = pageMeta(text); + if (!meta.title || !meta.permalink) marks.push("no title: or permalink: in the front matter, to link it by"); if (marks.length) { stale.push(...marks.map((m) => `docs/${p.page}: ${m}`)); continue; } - const want = renderSection(p, { name: f.name, beta: f.beta, messages: known, level: pageLevel(text) }); + const want = renderSection(p, { + name: f.name, + beta: f.beta, + messages: known, + level: pageLevel(text), + permalink: pageMeta(text).permalink, + }); if (spliceSection(text, want) !== text) stale.push(`docs/${p.page}: the section is not what ${f.name} writes`); } } @@ -220,4 +342,20 @@ for (const rel of await markdownFiles(DOCS_DIR)) { } check("every section is what the data writes", stale.length === 0, stale.join("\n")); +const metas = new Map(); +for (const f of files) { + for (const p of f.pages) { + if (existsSync(pagePath(p.page))) metas.set(p.page, pageMeta(readFileSync(pagePath(p.page), "utf8"))); + } +} +const centralFile = pagePath(CENTRAL_PAGE); +const centralText = existsSync(centralFile) ? readFileSync(centralFile, "utf8") : null; +const indexed = + centralText === null ? { problem: "no such page" } : spliceIndex(centralText, renderIndex(files, known, metas)); +check( + `the index on docs/${CENTRAL_PAGE} is what the data writes`, + !indexed.problem && indexed.text === centralText, + `docs/${CENTRAL_PAGE}: ${indexed.problem ?? "the index is stale"}`, +); + process.exit(report()); diff --git a/scripts/errors.mjs b/scripts/errors.mjs index 167db53a6..31fbc0fab 100644 --- a/scripts/errors.mjs +++ b/scripts/errors.mjs @@ -5,10 +5,11 @@ // node scripts/errors.mjs run [...] [--only ] [--runtime tb|vb6|win64]... // node scripts/errors.mjs generate
[...] [--only ] // node scripts/errors.mjs write [...] +// node scripts/errors.mjs messages [--runtime tb|vb6]... // // Exit codes: -// 0 run: every runtime agrees with the data; generate, write: done -// 1 run: a runtime disagrees with the data +// 0 run, messages: every runtime agrees with the data; generate, write: done +// 1 run, messages: a runtime disagrees with the data // 2 refused command line, bad data, or a runtime could not be run // // WHY THIS EXISTS @@ -20,9 +21,11 @@ // section, the probe and the check are generated from it. `run` builds one // probe from the data for each runtime (a twinBASIC exe through scripts/tbrun.mjs, // a VB6 exe through lib/vb6.mjs) and compares what each case raised with what -// the data says. It needs twinBASIC and VB6, so it is run by a person when a -// BETA is installed, never by CI. `write` needs neither; check_errors.mjs is its -// gate. +// the data says; `messages` does the same for errors/messages.yml, the message +// of every number. They need twinBASIC and VB6, so they are run by a person when +// a BETA is installed, never by CI. `write` -- each page's section, and the +// index of the numbers on the central page -- needs neither; check_errors.mjs is +// its gate. import { spawn } from "node:child_process"; import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; @@ -31,18 +34,26 @@ import path from "node:path"; import { die, exitOnCrash, parseCli, printHelpAndExit, regexOption, withUsageError } from "../lib/cli.mjs"; import { REPO_ROOT } from "../lib/repo-paths.mjs"; import { + CENTRAL_PAGE, caseAtLine, + compareMessages, dataFiles, findSection, loadDataFile, loadMessages, + messageProbeModule, pageLevel, + pageMeta, pagePath, probeCases, probeModule, + readMessageOutput, readProbeOutput, + renderIndex, renderSection, + spliceIndex, spliceSection, + VB_NUMBERS, } from "./lib/errors-data.mjs"; import { encodeAnsi, findVb6, NO_VB6, parseMakeLog, runRepro } from "./lib/vb6.mjs"; @@ -50,7 +61,8 @@ exitOnCrash(); const USAGE = `usage: node scripts/errors.mjs run [...] [--only ] [--runtime R]... [--ide P] [--vb6 P] [--keep] node scripts/errors.mjs generate [...] [--only ] - node scripts/errors.mjs write [...] [-h, --help] + node scripts/errors.mjs write [...] + node scripts/errors.mjs messages [--runtime R]... [--ide P] [--vb6 P] [--keep] [-h, --help] The run-time errors of the documented procedures, from the data under errors/ (its format is in WIP.Errors.md). A file is named as it is in errors/; with none @@ -61,25 +73,30 @@ named, every file there is used. reported and left out, and the probe built again without it. generate writes the probes to /tb (a tree for scripts/tbrun.mjs --exe) and /vb6 (Probe.vbp), without running them - write writes each page's Errors section from the data + write writes each page's Errors section from the data, and the index of + the numbers on the central page, docs/Reference/Run-time-Errors.md + messages builds a probe of the messages for each runtime, runs it, and + compares what Error$(n) and Err.Raise n give with errors/messages.yml: + every number from 1 to 65535 with a message of its own, and every + other number the file lists --only only the pages whose path under docs/ matches --runtime R tb (32-bit twinBASIC), vb6 or win64 (64-bit twinBASIC); may be repeated. By default tb and vb6, and win64 for a file that says - win64: true + win64: true; messages measures tb and vb6 only --ide P the twinBASIC IDE, as for tbrun (default $TB_IDE, or the newest on the Desktop) --vb6 P VB6.EXE (default $VB6_EXE, or VB6's own install folder) - --keep keep the generated probes, and print where + --keep run, messages: keep the generated probes, and print where -h, --help print this text and exit Exit codes: - 0 run: every runtime agrees with the data; generate, write: done - 1 run: a runtime disagrees with the data + 0 run, messages: every runtime agrees with the data; generate, write: done + 1 run, messages: a runtime disagrees with the data 2 a refused command line, bad data, or a runtime that could not be run`; const RUNTIMES = ["tb", "vb6", "win64"]; -const COMMANDS = ["run", "generate", "write"]; +const COMMANDS = ["run", "generate", "write", "messages"]; const TBRUN = path.join(REPO_ROOT, "scripts", "tbrun.mjs"); const TB_TEMPLATE = path.join(REPO_ROOT, "test", "example-projects", "console", "Settings"); const VB6_PROJECT = path.join(REPO_ROOT, "test", "repro-templates", "vb6", "Probe.vbp"); @@ -113,11 +130,15 @@ if (command === "generate") { if (!rest.length) usage("generate needs the folder to write to"); outDir = path.resolve(rest.shift()); } -if (command !== "run") { - for (const k of ["runtime", "ide", "vb6"]) if (values[k]?.length) usage(`--${k} is for run`); - if (values.keep) usage("--keep is for run"); +if (command !== "run" && command !== "messages") { + for (const k of ["runtime", "ide", "vb6"]) if (values[k]?.length) usage(`--${k} is for run and messages`); + if (values.keep) usage("--keep is for run and messages"); +} +if ((command === "write" || command === "messages") && only) usage("--only is for run and generate"); +if (command === "messages") { + if (rest.length) usage("messages takes no data file: it measures errors/messages.yml"); + if (values.runtime.includes("win64")) usage("--runtime: messages measures tb and vb6"); } -if (command === "write" && only) usage("--only is for run and generate"); // ------------------------------------------------------------------ the data @@ -142,8 +163,16 @@ if (command === "write") { die(2, `errors/${f.name}: no such page: docs/${p.page}`); } const { problems: marks } = findSection(text); + const meta = pageMeta(text); + if (!meta.title || !meta.permalink) marks.push("no title: or permalink: in the front matter, to link it by"); if (marks.length) die(2, `docs/${p.page}: ${marks.join("; ")}`); - const section = renderSection(p, { name: f.name, beta: f.beta, messages, level: pageLevel(text) }); + const section = renderSection(p, { + name: f.name, + beta: f.beta, + messages, + level: pageLevel(text), + permalink: meta.permalink, + }); const next = spliceSection(text, section); if (next !== text) { writeFileSync(file, next); @@ -152,24 +181,38 @@ if (command === "write") { } } } + // The index names the pages of every data file, whichever were named. + const every = known.map((n) => loadDataFile(n)); + const metas = new Map(); + for (const f of every) for (const p of f.pages) metas.set(p.page, pageMeta(readFileSync(pagePath(p.page), "utf8"))); + const centralFile = pagePath(CENTRAL_PAGE); + const central = readFileSync(centralFile, "utf8"); + const { text: index, problem } = spliceIndex(central, renderIndex(every, messages, metas)); + if (problem) die(2, `docs/${CENTRAL_PAGE}: ${problem}`); + if (index !== central) { + writeFileSync(centralFile, index); + changed++; + console.log(`wrote docs/${CENTRAL_PAGE}`); + } console.log(`errors: ${changed} page${changed === 1 ? "" : "s"} written`); process.exit(0); } // ------------------------------------------------------------------ the probes -function writeTbTree(dir, cases) { +// The module is the cases' probe, or `text` when it is given (the messages' probe). +function writeTbTree(dir, cases, text) { mkdirSync(path.join(dir, "Sources"), { recursive: true }); cpSync(TB_TEMPLATE, path.join(dir, "Settings")); - const mod = probeModule(cases, "tb"); + const mod = text === undefined ? probeModule(cases, "tb") : { text, spans: [] }; writeFileSync(path.join(dir, "Sources", "Probe.twin"), mod.text); return mod; } -function writeVb6Project(dir, cases) { +function writeVb6Project(dir, cases, text) { mkdirSync(dir, { recursive: true }); cpSync(VB6_PROJECT, path.join(dir, "Probe.vbp")); - const mod = probeModule(cases, "vb6"); + const mod = text === undefined ? probeModule(cases, "vb6") : { text, spans: [] }; writeFileSync(path.join(dir, "Module1.bas"), encodeAnsi(mod.text)); return mod; } @@ -226,9 +269,10 @@ async function measure(runtime, cases, run) { return { results, harness: left.length ? `still refused after ${ROUNDS} builds` : null }; } -async function runTb(dir, cases, arch) { - const mod = writeTbTree(dir, cases); +async function runTb(dir, cases, arch, text) { + const mod = writeTbTree(dir, cases, text); const args = [dir, "--exe", "--json", "--arch", arch]; + if (text !== undefined) args.push("--exe-run-timeout", "600"); if (values.ide) args.push("--ide", values.ide); const r = await runNode(args); if (r.code === 1) { @@ -253,11 +297,11 @@ async function runTb(dir, cases, arch) { return { lines: e.lines, note }; } -async function runVb6(dir, cases, vb6) { - const mod = writeVb6Project(dir, cases); +async function runVb6(dir, cases, vb6, text) { + const mod = writeVb6Project(dir, cases, text); let r; try { - r = await runRepro(vb6, dir, { timeoutMs: 120000 }); + r = await runRepro(vb6, dir, { timeoutMs: text === undefined ? 120000 : 600000 }); } catch (e) { return { harness: e.message.split("\n")[0] }; } @@ -275,6 +319,40 @@ async function runVb6(dir, cases, vb6) { return { lines: r.lines, note }; } +if (command === "messages") { + // The numbers past VB's own that the list gives are measured as well. + const others = [...messages.keys()].filter((n) => n < VB_NUMBERS.from || n > VB_NUMBERS.to).sort((a, b) => a - b); + const runtimes = values.runtime.length ? [...new Set(values.runtime)] : ["tb", "vb6"]; + const vb6 = runtimes.includes("vb6") ? findVb6(values.vb6) : null; + if (runtimes.includes("vb6") && !vb6) die(2, values.vb6 ? `no such file: ${values.vb6}\n${NO_VB6}` : NO_VB6); + const run = { + tb: (d) => runTb(d, [], "win32", messageProbeModule(others, "tb")), + vb6: (d) => runVb6(d, [], vb6, messageProbeModule(others, "vb6")), + }; + const seen = {}; + const broken = []; + await Promise.all( + runtimes.map(async (rt) => { + const got = await run[rt](path.join(work, rt)); + const read = got.lines ? readMessageOutput(got.lines) : null; + if (read?.done) seen[rt] = read.seen; + else if (got.harness) broken.push(`${rt}: ${got.harness}`); + else if (got.refused) broken.push(`${rt}: the probe did not build: ${[...got.refused.values()].join("; ")}`); + else broken.push(`${rt}: the probe stopped before it ended${got.note ? `: ${got.note}` : ""}`); + }), + ); + if (values.keep) console.log(`errors: the probes are in ${work}`); + else rmSync(work, { recursive: true, force: true }); + if (broken.length) die(2, broken.join("\n")); + const wrong = compareMessages(messages, seen); + for (const w of wrong) console.log(` ${w}`); + const against = runtimes.join(" and "); + console.log( + `errors: ${messages.size} numbers in messages.yml, ${wrong.length ? `${wrong.length} disagreements with ${against}` : `all agree with ${against}`}`, + ); + process.exit(wrong.length ? 1 : 0); +} + const wanted = values.runtime.length ? values.runtime : null; const plan = []; const want = (rt) => (wanted ? wanted.includes(rt) : rt !== "win64"); @@ -333,9 +411,9 @@ for (const c of [...all.values()].sort((a, b) => a.id - b.id)) { else if (r.missing) wrong.push(`${rt} reported nothing`); else if (r.setup) wrong.push(`${rt}: its setup raised ${r.number}, ${r.description}`); else if (r.number !== mine.expect) wrong.push(`${rt} raised ${r.number}, the data says ${mine.expect}`); - else if (r.number !== 0 && r.description !== messages.get(r.number)) { + else if (r.number !== 0 && r.description !== messages.get(r.number)?.raised) { wrong.push( - `${rt}'s message is "${r.description}", errors/messages.yml has "${messages.get(r.number) ?? "nothing"}"`, + `${rt}'s message is "${r.description}", errors/messages.yml has "${messages.get(r.number)?.raised ?? "nothing"}"`, ); } } diff --git a/scripts/lib/cli-cases.mjs b/scripts/lib/cli-cases.mjs index 4fb93d79b..f07833129 100644 --- a/scripts/lib/cli-cases.mjs +++ b/scripts/lib/cli-cases.mjs @@ -129,7 +129,11 @@ const CASES = [ { tool: "scripts/errors.mjs", args: ["generate"], exit: 2, stderr: /^generate needs the folder to write to\nusage: node scripts\/errors\.mjs / }, { tool: "scripts/errors.mjs", args: ["run", "--runtime", "vb5"], exit: 2, stderr: /^--runtime: one of tb, vb6, win64\nusage: node scripts\/errors\.mjs / }, { tool: "scripts/errors.mjs", args: ["write", "--only", "Mid"], exit: 2, stderr: /^--only is for run and generate\nusage: node scripts\/errors\.mjs / }, - { tool: "scripts/errors.mjs", args: ["write", "--keep"], exit: 2, stderr: /^--keep is for run\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["write", "--keep"], exit: 2, stderr: /^--keep is for run and messages\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["generate", "x", "--runtime", "tb"], exit: 2, stderr: /^--runtime is for run and messages\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["messages", "Core.yml"], exit: 2, stderr: /^messages takes no data file: it measures errors\/messages\.yml\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["messages", "--runtime", "win64"], exit: 2, stderr: /^--runtime: messages measures tb and vb6\nusage: node scripts\/errors\.mjs / }, + { tool: "scripts/errors.mjs", args: ["messages", "--only", "Mid"], exit: 2, stderr: /^--only is for run and generate\nusage: node scripts\/errors\.mjs / }, { tool: "scripts/errors.mjs", args: ["write", "no-such.yml"], exit: 2, stderr: "no such data file: errors/no-such.yml\n" }, { tool: "scripts/addin_test.mjs", args: ["--help"], exit: 0, stdout: /^usage: node scripts\/addin_test\.mjs / }, { tool: "scripts/addin_test.mjs", args: ["--ide"], exit: 2, stderr: "--ide needs a value\n" }, diff --git a/scripts/lib/errors-data.mjs b/scripts/lib/errors-data.mjs index 2a1fd73c2..e3c927004 100644 --- a/scripts/lib/errors-data.mjs +++ b/scripts/lib/errors-data.mjs @@ -7,6 +7,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import yaml from "js-yaml"; +import { parseFrontmatter } from "../../lib/frontmatter.mjs"; import { DOCS_DIR, REPO_ROOT } from "../../lib/repo-paths.mjs"; export const ERRORS_DIR = path.join(REPO_ROOT, "errors"); @@ -27,21 +28,37 @@ export function dataFiles(dir = ERRORS_DIR) { .sort(); } -/** errors/messages.yml: { number: message }, or a problem. */ -export function loadMessages(dir = ERRORS_DIR) { - const file = path.join(dir, MESSAGES_FILE); - if (!existsSync(file)) return { messages: new Map(), problems: [`${MESSAGES_FILE}: missing`] }; - const raw = yaml.load(readFileSync(file, "utf8")) ?? {}; +/** What Error$(n) gives for a number with no message of its own, in both runtimes. */ +export const GENERIC_MESSAGE = "Application-defined or object-defined error"; + +/** + * errors/messages.yml read: Map number -> { raised, error }, where `raised` is the + * Err.Description of the error once raised and `error` what Error$(number) gives. + * The file gives a number's one message, or `{ raised, error }` where they differ. + */ +export function readMessages(raw, name = MESSAGES_FILE) { const messages = new Map(); const problems = []; - for (const [k, v] of Object.entries(raw)) { - if (!/^-?\d+$/.test(k) || typeof v !== "string" || !v) - problems.push(`${MESSAGES_FILE}: ${k}: not a number and its message`); - else messages.set(Number(k), v); + const text = (v) => typeof v === "string" && v.length > 0; + for (const [k, v] of Object.entries(raw ?? {})) { + const pair = v && typeof v === "object" && !Array.isArray(v); + if (!/^-?\d+$/.test(k) || !(text(v) || (pair && text(v.raised) && text(v.error)))) + problems.push(`${name}: ${k}: not a number and its message, or its raised: and error: messages`); + else if (pair && Object.keys(v).some((x) => x !== "raised" && x !== "error")) + problems.push(`${name}: ${k}: only raised: and error:`); + else if (pair && v.raised === v.error) problems.push(`${name}: ${k}: raised: and error: agree; give one message`); + else messages.set(Number(k), pair ? { raised: v.raised, error: v.error } : { raised: v, error: v }); } return { messages, problems }; } +/** errors/messages.yml, read by readMessages, or a problem. */ +export function loadMessages(dir = ERRORS_DIR) { + const file = path.join(dir, MESSAGES_FILE); + if (!existsSync(file)) return { messages: new Map(), problems: [`${MESSAGES_FILE}: missing`] }; + return readMessages(yaml.load(readFileSync(file, "utf8"))); +} + const isInt = (v) => Number.isInteger(v); // A case as the data writes it, a string (an expression) or a mapping, made one shape: @@ -224,6 +241,92 @@ export function probeModule(cases, dialect) { return { text: `${lines.join("\r\n")}\r\n`, spans }; } +// ------------------------------------------------------------------ the messages + +/** The numbers whose message is VB's own: Error$ is defined for these. */ +export const VB_NUMBERS = { from: 1, to: 65535 }; + +/** + * The probe of the messages, in `dialect` as probeModule's: for every number from 1 + * to 65535 whose Error$ is not GENERIC_MESSAGE, and for each of `others`, a line + * `msg `, tab-separated, after + * `Err.Raise n`. An Error$ that raises is written `!`, and a line break in a + * description `\n`. + */ +export function messageProbeModule(others, dialect) { + const one = [ + "Private Sub One(ByVal N As Long, ByVal All As Boolean)", + " Dim E As String, D As String", + " On Error Resume Next", + " E = Error$(N)", + ' If Err.Number <> 0 Then E = "!" & Err.Number', + ` If Not All And E = "${GENERIC_MESSAGE}" Then Exit Sub`, + " Err.Clear", + " Err.Raise N", + ' D = Replace(Replace(Err.Description, vbCr, ""), vbLf, "\\n")', + ' Report "msg" & vbTab & N & vbTab & E & vbTab & Err.Number & vbTab & D', + " Err.Clear", + "End Sub", + ]; + const main = [ + "Public Sub Main()", + " Dim N As Long", + ` For N = ${VB_NUMBERS.from} To ${VB_NUMBERS.to}`, + " One N, False", + " Next", + ...others.map((n) => ` One ${n}, True`), + ' Report "done"', + "End Sub", + ]; + const { text } = probeModule([], dialect); + const lines = text.replace(/\r\n$/, "").split("\r\n"); + const at = lines.indexOf("Public Sub Main()"); + const end = lines.indexOf("End Sub", at); + lines.splice(at, end - at + 1, ...one, ...main); + return `${lines.join("\r\n")}\r\n`; +} + +/** A message probe's output: Map n -> { error, number, description }, and whether it ended. */ +export function readMessageOutput(lines) { + const seen = new Map(); + for (const l of lines) { + const p = l.split("\t"); + if (p[0] === "msg" && p.length === 5) + seen.set(Number(p[1]), { error: p[2], number: Number(p[3]), description: p[4] }); + } + return { seen, done: lines.includes("done") }; +} + +/** + * What is wrong with `messages` (readMessages's) against what the probes saw, one + * line each. Every number from 1 to 65535 with a message of its own in either + * runtime must be listed; each listed number's Error$ must be its `error` in both + * runtimes, and the description of the error raised its `raised`: in both runtimes + * for a number from 1 to 65535, in twinBASIC only for another (VB6's Err.Raise turns + * many HRESULTs into its own numbers). `seen` maps a runtime to readMessageOutput's map. + */ +export function compareMessages(messages, seen) { + const out = []; + const vbs = (n) => n >= VB_NUMBERS.from && n <= VB_NUMBERS.to; + for (const [rt, got] of Object.entries(seen)) { + for (const [n, g] of got) if (vbs(n) && !messages.has(n)) out.push(`${n}: ${rt} has "${g.error}", not listed`); + for (const [n, m] of messages) { + const g = got.get(n); + if (!g) { + if (vbs(n)) out.push(`${n}: ${rt}'s Error$ is "${GENERIC_MESSAGE}", the list has "${m.error}"`); + continue; + } + if (g.error !== m.error) out.push(`${n}: ${rt}'s Error$ is "${g.error}", the list has "${m.error}"`); + if (rt !== "vb6" || vbs(n)) { + if (g.number !== n) out.push(`${n}: ${rt}'s Err.Raise ${n} raised ${g.number}`); + else if (g.description !== m.raised) + out.push(`${n}: ${rt}'s description is "${g.description}", the list has "${m.raised}"`); + } + } + } + return out; +} + /** The id of the case whose procedure holds 1-based `line`, or null. */ export const caseAtLine = (spans, line) => spans.find((s) => line >= s.from && line <= s.to)?.id ?? null; @@ -304,13 +407,40 @@ export const errorNumber = (n) => `${n} (\`&H${(n >>> 0).toString(16).toUpperCas const escapeCell = (s) => s.replace(/\|/g, "\\|"); +/** The central page of the run-time errors, as a path under docs/, and its permalink. */ +export const CENTRAL_PAGE = "Reference/Run-time-Errors.md"; +export const CENTRAL_PERMALINK = "/Reference/Run-time-Errors"; + +/** The anchor of number `n` on the central page: error-5, error--2147467259. */ +export const errorAnchor = (n) => `error-${n}`; + +/** The link from the page at permalink `from` to the page at permalink `to`, as the site writes it: relative. */ +export function relativeLink(from, to) { + const base = from.endsWith("/") ? from : path.posix.dirname(from); + const rel = path.posix.relative(base, to.endsWith("/") ? to.slice(0, -1) : to) || "."; + return to.endsWith("/") && rel !== "." ? `${rel}/` : rel; +} + +/** A page's title and permalink, from its front matter; null for either it lacks. */ +export function pageMeta(text) { + let data = {}; + try { + data = parseFrontmatter(text)?.data ?? {}; + } catch {} + const s = (v) => (typeof v === "string" && v ? v : null); + return { title: s(data.title), permalink: s(data.permalink) }; +} + /** * The section's lines for page entry `p` of data file `name`, or null when the page - * gets none. `level` is the heading's "##"/"###". + * gets none. `level` is the heading's "##"/"###", `permalink` the page's own: each + * number links to its row on the central page. */ -export function renderSection(p, { name, beta, messages, level }) { +export function renderSection(p, { name, beta, messages, level, permalink }) { if (!p.rows.length && p.none === undefined) return null; - const msg = (n) => messages.get(n) ?? "?"; + const msg = (n) => messages.get(n)?.raised ?? "?"; + const central = relativeLink(permalink ?? "/", CENTRAL_PERMALINK); + const num = (n) => `[${errorNumber(n)}](${central}#${errorAnchor(n)})`; const out = [ ``, `${level} Errors`, @@ -319,9 +449,12 @@ export function renderSection(p, { name, beta, messages, level }) { if (p.none !== undefined) out.push(p.none.trim()); else { out.push("| Error | When |", "|---|---|"); - for (const r of p.rows) out.push(`| ${errorNumber(r.error)}, *${msg(r.error)}* | ${escapeCell(r.when)} |`); + // Rows that raise the same number share one row, their conditions a line each. + const byError = new Map(); + for (const r of p.rows) byError.set(r.error, [...(byError.get(r.error) ?? []), escapeCell(r.when)]); + for (const [n, whens] of byError) out.push(`| ${num(n)}, *${msg(n)}* | ${whens.join("
")} |`); for (const r of p.rows.filter((x) => x.vb6 !== undefined)) { - const what = r.vb6 === 0 ? "raises no error" : `raises error ${errorNumber(r.vb6)}, *${msg(r.vb6)}*, instead`; + const what = r.vb6 === 0 ? "raises no error" : `raises error ${num(r.vb6)}, *${msg(r.vb6)}*, instead`; out.push("", `> [!NOTE] BETA ${beta}`, `> When ${r.when}, VB6 ${what}.`); } } @@ -358,3 +491,71 @@ export function spliceSection(text, section) { /** The page's path on disk, for `page` relative to docs/. */ export const pagePath = (page, docs = DOCS_DIR) => path.join(docs, ...page.split("/")); + +// ------------------------------------------------------------------ the central page + +export const INDEX_OPEN = /^[ \t]*$/; +export const INDEX_CLOSE = /^[ \t]*$/; + +/** VB's own numbers in order, then the others in the order of their hexadecimal. */ +const byNumber = (a, b) => ((a > 0) - (b > 0) === 0 ? (a >>> 0) - (b >>> 0) : b - a); + +/** + * The central page's index of the numbers, as lines: one row for each number of + * `messages`, with its anchor, its message (and Error$'s where it differs) and links + * to the pages whose tables name it. `pages` maps a page under docs/ to its + * { title, permalink }. + */ +export function renderIndex(files, messages, pages) { + const naming = new Map(); + for (const f of files) { + for (const p of f.pages) { + for (const r of p.rows) { + if (!naming.has(r.error)) naming.set(r.error, new Set()); + naming.get(r.error).add(p.page); + } + } + } + const out = [ + "", + "", + "| Error | Message | Listed by |", + "|---|---|---|", + ]; + for (const n of [...messages.keys()].sort(byNumber)) { + const m = messages.get(n); + const hex = errorNumber(n).slice(String(n).length + 1); + const message = m.raised === m.error ? `*${m.raised}*` : `*${m.raised}*; **Error** gives *${m.error}*`; + const links = [...(naming.get(n) ?? [])] + .map((page) => pages.get(page) ?? { title: page, permalink: null }) + .sort((a, b) => a.title.localeCompare(b.title, "en")) + .map((p) => (p.permalink ? `[${p.title}](${relativeLink(CENTRAL_PERMALINK, p.permalink)})` : p.title)); + out.push(`| **${n}**{: #${errorAnchor(n)} } ${hex} | ${escapeCell(message)} | ${links.join(", ") || "---"} |`); + } + out.push("", ""); + return out; +} + +/** + * The central page's text with its index set to `index` (lines), or a problem: the + * page must hold the two markers, once each and in order, outside a fence. + * @returns {{text: string|null, problem: string|null}} + */ +export function spliceIndex(text, index) { + const crlf = text.includes("\r\n"); + const lines = text.replace(/\r\n/g, "\n").split("\n"); + const at = (re) => + scan(lines) + .map(({ text: t, fenced }, i) => (!fenced && re.test(t) ? i : -1)) + .filter((i) => i >= 0); + const open = at(INDEX_OPEN); + const close = at(INDEX_CLOSE); + if (open.length !== 1 || close.length !== 1 || close[0] < open[0]) + return { + text: null, + problem: "the page needs one and one , in order", + }; + lines.splice(open[0], close[0] - open[0] + 1, ...index); + const out = lines.join("\n"); + return { text: crlf ? out.replace(/\n/g, "\r\n") : out, problem: null }; +} From d96d7951aec9916e50eac424f483691d8481743e Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Tue, 20 Oct 2026 23:22:58 +0200 Subject: [PATCH 09/21] Queue the ReDim inverted-bounds error: -2147467259 where VB6 raises 9 --- BUGS-TO-REPORT.md | 94 +++++++++++++++++- .../redim-inverted-bounds-error.twinproj | Bin 0 -> 4465 bytes bugs/redim-inverted-bounds-error/repro.json | 15 +++ bugs/redim-inverted-bounds-error/src/Settings | 75 ++++++++++++++ .../src/Sources/RegressionTests.twin | 62 ++++++++++++ .../src/Sources/Startup.twin | 6 ++ .../vb6/Module1.bas | 30 ++++++ .../redim-inverted-bounds-error/vb6/Probe.vbp | 7 ++ errors/Core.yml | 2 +- 9 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 bugs/redim-inverted-bounds-error/redim-inverted-bounds-error.twinproj create mode 100644 bugs/redim-inverted-bounds-error/repro.json create mode 100644 bugs/redim-inverted-bounds-error/src/Settings create mode 100644 bugs/redim-inverted-bounds-error/src/Sources/RegressionTests.twin create mode 100644 bugs/redim-inverted-bounds-error/src/Sources/Startup.twin create mode 100644 bugs/redim-inverted-bounds-error/vb6/Module1.bas create mode 100644 bugs/redim-inverted-bounds-error/vb6/Probe.vbp diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 39d25c9c1..8e29322e8 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -1,4 +1,96 @@ -# twinBASIC bugs to report +# twi## `ReDim` with an upper bound less than its lower bound raises -2147467259, *Unspecified error*, where VB6 raises error 9 + +**Describe the bug** +`ReDim X(0 To -1)`, `ReDim X(-1)` and `ReDim X(5 To 4, 1)` on a dynamic array (`Dim X() As Long`), all of which give a dimension an upper bound below its lower bound, raise error -2147467259 (`&H80004005`), *Unspecified error*, where VB6 raises error 9, *Subscript out of range*. A handler that tests for 9 does not recognise it. Observed in the compiler's test mode, 32-bit and 64-bit, and in a probe run after a build. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `redim-inverted-bounds-error.twinproj` (attached as `redim-inverted-bounds-error.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass (`ReDim X(0 To 0)`, `ReDim X(-3 To -1)`, and a subscript above a fixed-size array's range, which raises 9) and the four cases on an inverted bound fail with `Actual: -2147467259`; `Err.Description` is *Unspecified error*. + +The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`redim-inverted-bounds-error-vb6.zip`) prints: 9, *Subscript out of range*, for each of the three statements. +```vb +' Regression tests: ReDim with an upper bound less than its lower bound raises error 9, +' "Subscript out of range". Expected numbers are VB6's (the VB6 project beside this one +' prints them). tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: bounds in order, an empty-looking but valid range, and a subscript out of range. + [TestCase] + Public Sub ReDim_OneElement_Succeeds() + Dim X() As Long + ReDim X(0 To 0) + Assert.Strict.AreEqual 0&, UBound(X) + End Sub + + [TestCase] + Public Sub ReDim_NegativeBoundsInOrder_Succeeds() + Dim X() As Long + ReDim X(-3 To -1) + Assert.Strict.AreEqual -3&, LBound(X) + End Sub + + [TestCase] + Public Sub FixedArray_SubscriptAboveRange_Raises9() + Dim A(1 To 3) As Long + On Error Resume Next + A(4) = 1 + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_ZeroToMinusOne_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(0 To -1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_MinusOne_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(-1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_FiveToFour_SecondDimension_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(5 To 4, 1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_ErrorDescription() + Dim X() As Long + On Error Resume Next + ReDim X(0 To -1) + Assert.Strict.AreEqual "Subscript out of range", Err.Description + End Sub + +End Module +``` + +**Expected behavior** +Error 9, *Subscript out of range*, as VB6 raises, and as twinBASIC itself raises for a subscript outside a fixed-size array. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. An error is raised wherever VB6 raises one, so no failure goes unreported, but a handler that tests for 9 does not recognise it, and *Unspecified error* does not say what is wrong. It is the same symptom as "An index out of range on a fixed-size array member of a UDT raises -2147467259, *Unspecified error*, where VB6 raises error 9": a bounds failure that reaches the program as *Unspecified error*, here from the allocation rather than from a subscript. + +What does not reproduce it: bounds in order (`0 To 0`, `-3 To -1`), and a subscript outside a fixed-size array that is not in a UDT. + + + +--- + +nBASIC bugs to report A queue, not a record. Each entry is a **product** bug --- something twinBASIC itself does wrong --- observed while working on this documentation. **Mark an entry once it has been diff --git a/bugs/redim-inverted-bounds-error/redim-inverted-bounds-error.twinproj b/bugs/redim-inverted-bounds-error/redim-inverted-bounds-error.twinproj new file mode 100644 index 0000000000000000000000000000000000000000..49756c5adbd43a74e63f512da4113e128fab1b34 GIT binary patch literal 4465 zcmcgw>u%dN6n497y!qb-48wj9MzA|JKs4x=cvE^^TU773cnNzg?${5Xh6>}mpJIK zga*RloN-vX{e26==Y^l)If!DWB*&LA-X*Wk}S7A)cZ^7`{^U5SnEuYRSLpA~*ecTYJMf-pM9LW-(9 zCp6ux6sX^?pko8=<6I0$CHE3$dL%;UM9z>)Q4+_PBTsRILePig3}%TGC}ih1E8Hal z3xpMU$l59uH&A($c*5r-mWU zF=uZ<2zgkL0K=g~AV#rL9CJe9c#w=Xt4Ml0p-9tG@Ce3$!u4m!cLfIgK`uVDMyzI$}2`!H^;4#2lR6*vR} z4s6cdOTamgTH9l1cwdF=$$lE$)?LEwN=?D5ub_R8qBN0!_e2t5w2Lp~tyAoZxrzQj zwS}40-mt(Z&{N$_FG?ogeC%l=pkEH2G9GgSPHGj_~>(3^MrPMDHHl| zH^!_F$@^r4eMSSgyqKy){RGZws0cN;&?BzU=WtIN^gW#AeX!{6mH853_E6!;uF?Xy zd-q9Qt*jE1imF0BEBpx0k#xCC;`;@(2aEH&c{})+Uqnt6^4}cijkIpR8Zm+@BE%wHhkvb#?u! zPeSZm$Sk6-SLj#WlhO~I(ZJ;aa*mv3vEVj^dM==@?r~}1DjdvvE0inBf}S}15}I*#DM|eBv0@X=_F8)ubj5s zs<(QU=2(`anax^D>(s4|X4M*Yquw(N!#3}bLxIrD+|^C#G7=W6D1cT9`UE0ZA>sHG zOrk&=tR6#Ptf?qkTPx2Y>bh>|W?k1{Fn@IOk($zVvzbAJJ)xiO(v_|7nq%GDc95M& z7Ns$nB81f#_^B&)%H5Zf$swTuI~SA5=u#Z+9X8F$M96@J*r>|TTinw$SM8BFsv9f! zh|9=h;UHx^Q<-anj$S3y9rrEcpwpPc&Zh}Yr*&n_b-K1pO~0Xc+D1>;dR@Dtnf1P- zSZS#wn*whC!d`&mnH$&pT|K?=$z-f(>c$}^zY*{qPzoo>d*tNc0sph z8GX1iTGQ;>n%RT9(=l5u&2BVwqi?pIez*HYTwupBnug4S=GYHi_yA&_`ZCFhc_QZF z)K}@DU3KO><>qHT&Kb33)}eFCf_`~=!5I%;2dizi?3&)w+E%-znP$h(+8r1?!T2fJaW!48i%$_F=fTFk5dL6)@OTBNSKUC$bj zy)5qF@~Y25)mNl=Z^X`BPF3>&`_pVzPD@*w{L}N+$}{!h49my!cQS>Lz$$&-%+OP| s@VcP#7`{xiTv>*XYzZxEH4*qVB|ZyqUdpro6uATqDgl;?*I6I+KfT4llmGw# literal 0 HcmV?d00001 diff --git a/bugs/redim-inverted-bounds-error/repro.json b/bugs/redim-inverted-bounds-error/repro.json new file mode 100644 index 000000000..5f4a12400 --- /dev/null +++ b/bugs/redim-inverted-bounds-error/repro.json @@ -0,0 +1,15 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.ReDim_OneElement_Succeeds$", + "^PASS RegressionTests.ReDim_NegativeBoundsInOrder_Succeeds$", + "^PASS RegressionTests.FixedArray_SubscriptAboveRange_Raises9$", + "^FAIL RegressionTests.ReDim_ZeroToMinusOne_Raises9: .*Actual: -2147467259", + "^FAIL RegressionTests.ReDim_MinusOne_Raises9: .*Actual: -2147467259", + "^FAIL RegressionTests.ReDim_FiveToFour_SecondDimension_Raises9: .*Actual: -2147467259", + "^FAIL RegressionTests.ReDim_ErrorDescription: .*Actual: \"Unspecified error\"" + ] + } +} diff --git a/bugs/redim-inverted-bounds-error/src/Settings b/bugs/redim-inverted-bounds-error/src/Settings new file mode 100644 index 000000000..526080587 --- /dev/null +++ b/bugs/redim-inverted-bounds-error/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "RedimInvertedBoundsError", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: ReDim with an upper bound below its lower bound raises -2147467259, *Unspecified error*, where VB6 raises error 9", + "project.exportPathIsV2": true, + "project.id": "{E8A737D9-E99E-4617-B39B-915A53D222A4}", + "project.name": "RedimInvertedBoundsError", + "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/redim-inverted-bounds-error/src/Sources/RegressionTests.twin b/bugs/redim-inverted-bounds-error/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..de377aac3 --- /dev/null +++ b/bugs/redim-inverted-bounds-error/src/Sources/RegressionTests.twin @@ -0,0 +1,62 @@ +' Regression tests: ReDim with an upper bound less than its lower bound raises error 9, +' "Subscript out of range". Expected numbers are VB6's (the VB6 project beside this one +' prints them). tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: bounds in order, an empty-looking but valid range, and a subscript out of range. + [TestCase] + Public Sub ReDim_OneElement_Succeeds() + Dim X() As Long + ReDim X(0 To 0) + Assert.Strict.AreEqual 0&, UBound(X) + End Sub + + [TestCase] + Public Sub ReDim_NegativeBoundsInOrder_Succeeds() + Dim X() As Long + ReDim X(-3 To -1) + Assert.Strict.AreEqual -3&, LBound(X) + End Sub + + [TestCase] + Public Sub FixedArray_SubscriptAboveRange_Raises9() + Dim A(1 To 3) As Long + On Error Resume Next + A(4) = 1 + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_ZeroToMinusOne_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(0 To -1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_MinusOne_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(-1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_FiveToFour_SecondDimension_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(5 To 4, 1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_ErrorDescription() + Dim X() As Long + On Error Resume Next + ReDim X(0 To -1) + Assert.Strict.AreEqual "Subscript out of range", Err.Description + End Sub + +End Module diff --git a/bugs/redim-inverted-bounds-error/src/Sources/Startup.twin b/bugs/redim-inverted-bounds-error/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/redim-inverted-bounds-error/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/redim-inverted-bounds-error/vb6/Module1.bas b/bugs/redim-inverted-bounds-error/vb6/Module1.bas new file mode 100644 index 000000000..9cff43dae --- /dev/null +++ b/bugs/redim-inverted-bounds-error/vb6/Module1.bas @@ -0,0 +1,30 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' What VB6 raises for ReDim with an upper bound less than its lower bound: error 9. +' Build and run it with: node scripts/bug_repro.mjs vb6 redim-inverted-bounds-error + +Private Sub Report(ByVal Label As String) + Print #9, Label & ": " & Err.Number & " " & Err.Description + Err.Clear +End Sub + +Sub Main() + Open App.Path & "\out.txt" For Output As #9 + On Error Resume Next + Dim X() As Long + Dim A(1 To 3) As Long + ReDim X(0 To 0) + Report "ReDim_OneElement" + ReDim X(-3 To -1) + Report "ReDim_NegativeBoundsInOrder" + A(4) = 1 + Report "FixedArray_SubscriptAboveRange" + ReDim X(0 To -1) + Report "ReDim_ZeroToMinusOne" + ReDim X(-1) + Report "ReDim_MinusOne" + ReDim X(5 To 4, 1) + Report "ReDim_FiveToFour_SecondDimension" + Close #9 +End Sub diff --git a/bugs/redim-inverted-bounds-error/vb6/Probe.vbp b/bugs/redim-inverted-bounds-error/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/redim-inverted-bounds-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/errors/Core.yml b/errors/Core.yml index a299bb99f..6f214e5e5 100644 --- a/errors/Core.yml +++ b/errors/Core.yml @@ -6,7 +6,7 @@ pages: - error: -2147467259 when: an upper bound is less than its lower bound vb6: 9 - bug: pending + bug: redim-inverted-bounds-error cases: - { decl: Dim X() As Long, run: ReDim X(0 To -1) } - { decl: Dim X() As Long, run: ReDim X(-1) } From 01c1de33cfb562d94712734a2f56f9294eaf35be Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Tue, 20 Oct 2026 23:47:54 +0200 Subject: [PATCH 10/21] WIP.Errors.md: batch 0 is done, its trial reviewed --- WIP.Errors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WIP.Errors.md b/WIP.Errors.md index c46d26e93..b23d077c1 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -277,7 +277,7 @@ each can be run when there is budget. Status is kept here. | # | Batch | Pages (about) | Status | |---|---|---|---| -| 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | built; trial sections (Mid, ReDim; Len raises nothing) await the owner | +| 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | done; the owner reviewed the trial sections (Mid, ReDim; Len raises nothing) on 2026-10-20 | | 1 | The central Run-time errors page | 1 | done (branch `errors-batch1`, on top of `errors-batch0`); the page awaits the owner, and so does the `Err.Description`/`Error$` choice | | 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | not started | | 3 | VBA `Conversion` | 28 | not started | From 5a4d5e5b14d71f8449fba34f7c549754a03d5e82 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Tue, 20 Oct 2026 23:59:05 +0200 Subject: [PATCH 11/21] WIP.Errors.md: batch 1 reviewed; the message is Err.Description --- WIP.Errors.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WIP.Errors.md b/WIP.Errors.md index b23d077c1..ee29c3e35 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -37,7 +37,7 @@ An `## Errors` section after the page's main text and before `## Example`: | 94, *Invalid use of Null* | `Mid$` is given a *string* that is **Null** | - The number links to its row on the central page (below); the message is the runtime's own, - as `Error$(n)` returns it, in italics as the rest of the site writes messages. + as `Err.Description` gives it when the error is raised (owner, 2026-10-20), in italics as the rest of the site writes messages. - *When* is a condition on the arguments or the state, in the reader's terms --- not the probe's expression. - **What is listed**: what the procedure raises from its own arguments and state. @@ -82,7 +82,7 @@ What was measured (BETA 1010, `errors.mjs messages`): `Error$` of a few HRESULTs is VB's message (`Error$(&H80070057)` is *Invalid procedure call or argument*), VB6's is generic. -### The message: Err.Description, with Error$ beside it (recommended; owner to confirm) +### The message: Err.Description, with Error$ beside it (owner: settled, 2026-10-20) The sections show `Err.Description`, what a handler sees. For VB's numbers it is `Error$(n)` too; the central page shows `Error$(n)` beside it where they differ, which so far is only the @@ -195,7 +195,7 @@ The section `write` makes, before the page's first Example heading: - **Every number is given in decimal and in VB hexadecimal** (owner), as a 32-bit **Long**: -2147467259 (`&H80004005`), 5 (`&H5`), in the table and in the VB6 NOTE alike. `errorNumber` in `scripts/lib/errors-data.mjs` writes it. -- **The message is `Err.Description`, not `Error$(n)`** (owner: undecided). For VB's own +- **The message is `Err.Description`, not `Error$(n)`** (owner: settled, "good enough"). For VB's own numbers the two agree; for an HRESULT they do not: `Error$(-2147467259)` is *Application-defined or object-defined error*, while the error raised describes itself as *Unspecified error*, which is what a handler sees. Batch 1 kept `Err.Description` and put @@ -278,7 +278,7 @@ each can be run when there is budget. Status is kept here. | # | Batch | Pages (about) | Status | |---|---|---|---| | 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | done; the owner reviewed the trial sections (Mid, ReDim; Len raises nothing) on 2026-10-20 | -| 1 | The central Run-time errors page | 1 | done (branch `errors-batch1`, on top of `errors-batch0`); the page awaits the owner, and so does the `Err.Description`/`Error$` choice | +| 1 | The central Run-time errors page | 1 | done; the owner reviewed the page and settled on `Err.Description` (2026-10-20) | | 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | not started | | 3 | VBA `Conversion` | 28 | not started | | 4 | VBA `Math` (also win64) | 17 | not started | From 5e76e4fd60ec069dc5f061fa004b10f9ceece43e Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 21 Oct 2026 00:01:53 +0200 Subject: [PATCH 12/21] Errors sections are now Common Errors, on a reader's request The heading write makes, the central page's references to the sections, the gate's probes, the tools' docs and WIP.Errors.md. --- .github/actions/run-gates/action.yml | 4 ++-- WIP.Errors.md | 4 ++-- WIP.md | 2 +- docs/Documentation/Tools-Defects.md | 2 +- docs/Documentation/Tools-Toolchain-Tests.md | 2 +- docs/Documentation/Tools.md | 4 ++-- docs/Reference/Core/ReDim.md | 2 +- docs/Reference/Default/VBA/Strings/Mid.md | 2 +- docs/Reference/Run-time-Errors.md | 6 +++--- scripts/check_errors.mjs | 10 +++++----- scripts/errors.mjs | 4 ++-- scripts/lib/errors-data.mjs | 4 ++-- test.bat | 2 +- 13 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/actions/run-gates/action.yml b/.github/actions/run-gates/action.yml index c5e009a8c..2cee750fb 100644 --- a/.github/actions/run-gates/action.yml +++ b/.github/actions/run-gates/action.yml @@ -236,10 +236,10 @@ runs: - name: Verify the package test markers and projects (check_package_tests.mjs) shell: bash run: node scripts/check_package_tests.mjs - # Every page's Errors section is what scripts/errors.mjs write makes of the + # Every page's Common Errors section is what scripts/errors.mjs write makes of the # run-time error data under errors/, and the data is in order. Sources # only: no twinBASIC, no VB6. - - name: Verify the Errors sections against their data (check_errors.mjs) + - name: Verify the Common Errors sections against their data (check_errors.mjs) shell: bash run: node scripts/check_errors.mjs # The symbol index (tB/symbols.json) is read by the IDE help add-in. A diff --git a/WIP.Errors.md b/WIP.Errors.md index ee29c3e35..3d5a8ff98 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -29,7 +29,7 @@ Like WIP.md, this file is not rendered through tbdocs. ## What a page gets -An `## Errors` section after the page's main text and before `## Example`: +A `## Common Errors` section (owner, 2026-10-21, on a reader's request) after the page's main text and before `## Example`: | Error | When | |---|---| @@ -175,7 +175,7 @@ The section `write` makes, before the page's first Example heading: ``` -### Errors +### Common Errors | Error | When | |---|---| diff --git a/WIP.md b/WIP.md index 97e810c6d..1f1ba134d 100644 --- a/WIP.md +++ b/WIP.md @@ -603,7 +603,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-four of its twenty-nine gates cannot be affected by an edit under `docs/` at all. **Five can.** `check_errors.mjs` compares the reference pages' Errors sections with the data under `errors/`, so edit the data and run `node scripts/errors.mjs write`, never the section. `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-nine gates cannot be affected by an edit under `docs/` at all. **Five can.** `check_errors.mjs` compares the reference pages' Common Errors sections with the data under `errors/`, so edit the data and run `node scripts/errors.mjs write`, never the section. `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 diff --git a/docs/Documentation/Tools-Defects.md b/docs/Documentation/Tools-Defects.md index b36420e8f..199209201 100644 --- a/docs/Documentation/Tools-Defects.md +++ b/docs/Documentation/Tools-Defects.md @@ -157,7 +157,7 @@ Exit codes: **0** the sample ran, or, with `--docs`, no fence differs and none r node scripts/errors.mjs write [...] node scripts/errors.mjs messages [--runtime tb|vb6]... [--ide ] [--vb6 ] [--keep] -Measures the run-time errors each documented procedure raises, and writes each reference page's Errors section from them. The errors are data, not prose: a YAML file per area under `errors/` lists, for each page, the cases that raise each error and the cases that must raise nothing, and `errors/messages.yml` gives each number's message: the one `Err.Description` gives once the error is raised, and the one `Error$(number)` gives, where the two differ. The format is in `WIP.Errors.md`. A file is named as it is in `errors/`; with none named, every file there is used. `--only ` keeps the pages whose path under `docs/` matches. +Measures the run-time errors each documented procedure raises, and writes each reference page's Common Errors section from them. The errors are data, not prose: a YAML file per area under `errors/` lists, for each page, the cases that raise each error and the cases that must raise nothing, and `errors/messages.yml` gives each number's message: the one `Err.Description` gives once the error is raised, and the one `Error$(number)` gives, where the two differ. The format is in `WIP.Errors.md`. A file is named as it is in `errors/`; with none named, every file there is used. `--only ` keeps the pages whose path under `docs/` matches. | Command | Effect | |---|---| diff --git a/docs/Documentation/Tools-Toolchain-Tests.md b/docs/Documentation/Tools-Toolchain-Tests.md index 3149fdb5b..d7b7ab907 100644 --- a/docs/Documentation/Tools-Toolchain-Tests.md +++ b/docs/Documentation/Tools-Toolchain-Tests.md @@ -400,7 +400,7 @@ Exit codes: **0** every probe passed and every test project is in order, **1** a node scripts/check_errors.mjs -Verifies that every reference page's Errors section is what [`errors.mjs write`](Tools-Defects#errors) makes of the run-time error data under `errors/`, and that the data is in order: every file reads, with no key it does not know; every error number it names has its message in `errors/messages.yml`; a row that differs from VB6 names its reproducer under `bugs/`, or says `pending`; every page it names exists, has a title and a permalink, and is named by one file only; no page under `docs/` has a section that no data file names; and the index of the numbers on the central page, [Run-time Errors](../../Reference/Run-time-Errors), is what `write` makes of the data and `errors/messages.yml`. It reads only the sources, so it needs no twinBASIC, no VB6 and no built tree. Measuring the data is `errors.mjs run`, and measuring the messages `errors.mjs messages`; neither is a gate. +Verifies that every reference page's Common Errors section is what [`errors.mjs write`](Tools-Defects#errors) makes of the run-time error data under `errors/`, and that the data is in order: every file reads, with no key it does not know; every error number it names has its message in `errors/messages.yml`; a row that differs from VB6 names its reproducer under `bugs/`, or says `pending`; every page it names exists, has a title and a permalink, and is named by one file only; no page under `docs/` has a section that no data file names; and the index of the numbers on the central page, [Run-time Errors](../../Reference/Run-time-Errors), is what `write` makes of the data and `errors/messages.yml`. It reads only the sources, so it needs no twinBASIC, no VB6 and no built tree. Measuring the data is `errors.mjs run`, and measuring the messages `errors.mjs messages`; neither is a gate. Twenty-eight probes run first, on data and pages built in memory: how the data and the messages are read and refused, the section with its numbers linked to the central page, its rows for one number merged into one, and its VB6 note, where a new section goes and that a marker inside a fence is not one, that writing twice changes nothing and removing a section gives the page back, the links between permalinks, the central page's index and its markers, how the probe modules and their output are read, and how the measured messages are compared with the list. diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md index 698f82d15..0b02cc696 100644 --- a/docs/Documentation/Tools.md +++ b/docs/Documentation/Tools.md @@ -136,7 +136,7 @@ The tests the toolchain has to pass. Twenty-nine steps, each stopping the run if 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_errors.mjs`](Tools-Toolchain-Tests#check-errors) --- verifies every page's Errors section is what the run-time error data under `errors/` writes, and that the data is in order. +22. [`scripts/check_errors.mjs`](Tools-Toolchain-Tests#check-errors) --- verifies every page's Common Errors section is what the run-time error data under `errors/` writes, and that the data is in order. 23. [`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. 24. [`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. 25. [`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`. @@ -181,7 +181,7 @@ Exit codes: **0** every step passed; otherwise the code of the step that stopped **All but five cannot be affected by an edit confined to `docs/`**, which is why they are separate from `check.bat`. Run this one when the change touches `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `indexer/` or `test/`, the site's scripts in `docs/assets/js/`, the help add-in's source in `add-in/`, a contributed package's section, a wrapper, or a workflow. Both CI workflows run every step unconditionally. -The five exceptions are [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions), [`check_errors.mjs`](Tools-Toolchain-Tests#check-errors), which compares the reference pages' Errors sections with their data, [`check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists), which reads this page, [`check_lint.mjs`](Tools-Toolchain-Tests#check-lint), which lints the site's scripts in `docs/assets/js/`, and [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests), which reads the callouts of the contributed packages' sections. +The five exceptions are [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions), [`check_errors.mjs`](Tools-Toolchain-Tests#check-errors), which compares the reference pages' Common Errors sections with their data, [`check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists), which reads this page, [`check_lint.mjs`](Tools-Toolchain-Tests#check-lint), which lints the site's scripts in `docs/assets/js/`, and [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests), which reads the callouts of the contributed packages' sections. `check_code_regions.mjs` has two halves. Its corpus sweep tokenises every markdown file under `docs/`, so a page that provokes a rewrite into altering a code region fails it. Its fixed probes run against their own sources whatever the tree holds, and cover the *mirror* fault, where a rewrite silently stops firing; the sweep cannot see that one. After adding a page with an unusual code construct, run `test.bat` and read the built page too. diff --git a/docs/Reference/Core/ReDim.md b/docs/Reference/Core/ReDim.md index 3cc8823c3..4b4bb0e17 100644 --- a/docs/Reference/Core/ReDim.md +++ b/docs/Reference/Core/ReDim.md @@ -56,7 +56,7 @@ The **ReDim** statement acts as a declarative statement if the variable it decla > To resize an array contained in a **Variant**, the **Variant** variable must be explicitly declared before attempting to resize its array. -### Errors +### Common Errors | Error | When | |---|---| diff --git a/docs/Reference/Default/VBA/Strings/Mid.md b/docs/Reference/Default/VBA/Strings/Mid.md index ced7c64b9..9b70b829f 100644 --- a/docs/Reference/Default/VBA/Strings/Mid.md +++ b/docs/Reference/Default/VBA/Strings/Mid.md @@ -34,7 +34,7 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi > Use the [**Mid =**](../../Core/Mid-equals) statement to replace characters within a string. -### Errors +### Common Errors | Error | When | |---|---| diff --git a/docs/Reference/Run-time-Errors.md b/docs/Reference/Run-time-Errors.md index d4106f851..0725689b0 100644 --- a/docs/Reference/Run-time-Errors.md +++ b/docs/Reference/Run-time-Errors.md @@ -13,7 +13,7 @@ Every run-time error number that has a message of its own, with that message, an A run-time error can be trapped: [**On Error**](../tB/Core/On-Error) sends it to a handler, which reads its number from [**Err.Number**](../tB/Modules/ErrObject/Number) and its message from [**Err.Description**](../tB/Modules/ErrObject/Description). A reference page that documents a procedure, a statement or an operator has an **Errors** section when it raises errors of its own: a table of each number it raises, and when. Each number there links to its row below. -## What the Errors sections list +## What the Common Errors sections list A section lists the errors that a procedure raises from its own arguments and its own state, as twinBASIC raises them. Where VB6 raises another error, or none, a note under the table says so. Every row is measured in twinBASIC and in VB6, and not copied from another reference. @@ -23,7 +23,7 @@ A section does not list: - an error in converting an argument to the type of its parameter before the call is made: 13, *Type mismatch*, for `Mid("abc", "x")`, and 94, *Invalid use of Null*, for `Mid("abc", Null)`, raised because the *start* parameter of **Mid** is a **Long**. A **Variant** parameter takes any value, so an error for a value of a **Variant** parameter is the procedure's own, and is listed; - compile-time errors: the twinBASIC compiler reports these with diagnostic codes such as `TB5001`, before the program runs, and **On Error** cannot trap them. -A procedure that raises no error of its own has no Errors section. +A procedure that raises no error of its own has no Common Errors section. ## How a number is written @@ -36,7 +36,7 @@ The message is the one that **Err.Description** gives for the error once it is r ## Error numbers -The messages are those of twinBASIC 32-bit and VB6, which agree for every number from 1 to 65535. *Listed by* names the pages whose Errors section lists the number. +The messages are those of twinBASIC 32-bit and VB6, which agree for every number from 1 to 65535. *Listed by* names the pages whose Common Errors section lists the number. diff --git a/scripts/check_errors.mjs b/scripts/check_errors.mjs index f7eebe519..569d7abb1 100644 --- a/scripts/check_errors.mjs +++ b/scripts/check_errors.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Gate: every page's Errors section is what the run-time error data under errors/ +// Gate: every page's Common Errors section is what the run-time error data under errors/ // writes, and the data is in order. // // node scripts/check_errors.mjs @@ -60,7 +60,7 @@ exitOnCrash(); const USAGE = `usage: node scripts/check_errors.mjs [-h, --help] -Checks that every page's Errors section is what scripts/errors.mjs write makes +Checks that every page's Common Errors section is what scripts/errors.mjs write makes of the data under errors/, and that the data is in order. Needs no twinBASIC or VB6; measuring the data is scripts/errors.mjs run. @@ -182,14 +182,14 @@ const doc = "# T\n\ntext\n\n```tb\n### Example\n\n```\n\n### E const added = spliceSection(doc, section); check( "a new section goes before the Example heading, not one in a fence", - added.indexOf("### Errors") < added.lastIndexOf("### Example") && - added.indexOf("### Errors") > added.indexOf("```\n\n"), + added.indexOf("### Common Errors") < added.lastIndexOf("### Example") && + added.indexOf("### Common Errors") > added.indexOf("```\n\n"), ); check("writing it again changes nothing", spliceSection(added, section) === added); check("removing it gives the page back", spliceSection(added, null) === doc); check("a marker in a fence is not one", findSection(doc).section === null); check("CRLF stays CRLF", spliceSection(doc.replace(/\n/g, "\r\n"), section) === added.replace(/\n/g, "\r\n")); -check("an unclosed section is reported", findSection("\n### Errors\n").problems.length === 1); +check("an unclosed section is reported", findSection("\n### Common Errors\n").problems.length === 1); check( "the level follows the page's Example heading", pageLevel(doc) === "###" && pageLevel("# T\n## See Also\n") === "##", diff --git a/scripts/errors.mjs b/scripts/errors.mjs index 31fbc0fab..208a0e03f 100644 --- a/scripts/errors.mjs +++ b/scripts/errors.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node // The run-time errors each documented procedure raises: measuring them, and -// writing each page's Errors section from the data under errors/. +// writing each page's Common Errors section from the data under errors/. // // node scripts/errors.mjs run [...] [--only ] [--runtime tb|vb6|win64]... // node scripts/errors.mjs generate [...] [--only ] @@ -73,7 +73,7 @@ named, every file there is used. reported and left out, and the probe built again without it. generate writes the probes to /tb (a tree for scripts/tbrun.mjs --exe) and /vb6 (Probe.vbp), without running them - write writes each page's Errors section from the data, and the index of + write writes each page's Common Errors section from the data, and the index of the numbers on the central page, docs/Reference/Run-time-Errors.md messages builds a probe of the messages for each runtime, runs it, and compares what Error$(n) and Err.Raise n give with errors/messages.yml: diff --git a/scripts/lib/errors-data.mjs b/scripts/lib/errors-data.mjs index e3c927004..be48a1321 100644 --- a/scripts/lib/errors-data.mjs +++ b/scripts/lib/errors-data.mjs @@ -1,5 +1,5 @@ // What scripts/errors.mjs and scripts/check_errors.mjs share: reading the run-time -// error data under errors/, the probe projects generated from it, and the `Errors` +// error data under errors/, the probe projects generated from it, and the `Common Errors` // section each page gets from it. The format is written down in WIP.Errors.md. // // Every export here does its work when called, never on import. @@ -443,7 +443,7 @@ export function renderSection(p, { name, beta, messages, level, permalink }) { const num = (n) => `[${errorNumber(n)}](${central}#${errorAnchor(n)})`; const out = [ ``, - `${level} Errors`, + `${level} Common Errors`, "", ]; if (p.none !== undefined) out.push(p.none.trim()); diff --git a/test.bat b/test.bat index 05785aef3..9859c4b62 100644 --- a/test.bat +++ b/test.bat @@ -177,7 +177,7 @@ node scripts/check_book_coverage.mjs @rem no tree, no install, well under a second. node scripts/check_package_tests.mjs @if errorlevel 1 goto :fail -@rem Every page's Errors section is what scripts\errors.mjs write makes of the +@rem Every page's Common Errors section is what scripts\errors.mjs write makes of the @rem run-time error data under errors\, and the data is in order. Its probes @rem run on inputs built in memory first. Sources only: no twinBASIC, no VB6. node scripts/check_errors.mjs From c476761d60ce32d3b1e80e6dea5c4b223d064893 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 21 Oct 2026 00:11:03 +0200 Subject: [PATCH 13/21] WIP.Errors.md: where things stand, and how batch 2 starts --- WIP.Errors.md | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/WIP.Errors.md b/WIP.Errors.md index 3d5a8ff98..33f3d97ea 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -33,9 +33,12 @@ A `## Common Errors` section (owner, 2026-10-21, on a reader's request) after th | Error | When | |---|---| -| 5, *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative | -| 94, *Invalid use of Null* | `Mid$` is given a *string* that is **Null** | +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Mid$** is given a *string* that is **Null** | +- The number is given in decimal and in VB hexadecimal, as a 32-bit **Long** (owner, + 2026-10-20). The heading takes the level of the page's Example heading: `###` on the + VBA and Core pages, which the build promotes to `##`. - The number links to its row on the central page (below); the message is the runtime's own, as `Err.Description` gives it when the error is raised (owner, 2026-10-20), in italics as the rest of the site writes messages. - *When* is a condition on the arguments or the state, in the reader's terms --- not the @@ -212,7 +215,7 @@ The section `write` makes, before the page's first Example heading: **Long**. `Mid("abc", 1, Null)` is in the table: *length* is a **Variant**, and the 94 is Mid's own. -## Starting batch 0 +## Before any batch Read first: [WIP.Harness.md](WIP.Harness.md) (`tbbuild`, `tbrun`), [WIP.Build.md](WIP.Build.md) (gates), [WIP.Authoring.md](WIP.Authoring.md) (page template), and `scripts/lib/vb6.mjs`'s @@ -220,7 +223,7 @@ opening comment (driving VB6). Models to copy from: `scripts/check_package_tests `scripts/check_code_regions.mjs` tie page text to tests with markers, and `scripts/bug_repro.mjs` runs projects in both runtimes and compares the result with an expectation. -What this repository has already learned that the probes must respect (BETA 1010): +What the probes must respect (BETA 1010; `errors.mjs` already does all of it): - **Run cases in the built exe** (`tbrun --exe`), and print with `TbRun.Out`, which `tbrun` adds to the project: `Debug.Print` writes nothing in an exe. A `[RunAfterBuild]` run in the @@ -238,24 +241,32 @@ What this repository has already learned that the probes must respect (BETA 1010 - **VB6 cannot compile twinBASIC syntax** (`LongLong`, `Return` in a function, inline `Dim x =`, generics). Cases for VB6 are written in the common subset; a twinBASIC-only case says so in the data and is not run in VB6. -- **One case can stop a whole project**: a compile error anywhere fails all of it. The - generator should build a failing project again without the case named in the error, so one - bad case costs a rerun, not the batch. +- **One case can stop a whole project**: a compile error anywhere fails all of it. `run` + builds a failing project again without the case named in the error. - A new gate in `test.bat` must also be added to the gate list in `docs/Documentation/Tools.md` (`check_gate_lists.mjs` fails otherwise), and a new tool gets its section on the Tools pages under `docs/Documentation/`. -Batch 0 is done when: - -1. The data format is written down in this file, with one complete example. -2. The generator, the runner (twinBASIC 32-bit, 64-bit on request, VB6) and the section - writer exist, with `--help`, and the gate is in `test.bat`. -3. A trial on three pages of different shapes --- a function with several errors - (`Mid` or `InStr`), a statement (`ReDim`), a procedure that raises nothing --- runs through - every step, and its sections are written. -4. **The owner has seen the trial's sections** before batch 2 starts: the format is settled - then, not before. The trial's pages may be committed or reverted, as the owner says. -5. This file's batch table says batch 0 is done, and what batch 1 needs to know. +## Where things stand (2026-10-21) + +Batches 0 and 1 are done and reviewed. Everything is on branch **`errors-batch0`** (worktree +`.claude/worktrees/infallible-wilson-117b19`), **not pushed and with no PR**: the tooling, +the trial sections (Mid, ReDim; Len raises nothing), the central page +`docs/Reference/Run-time-Errors.md` (merged from `errors-batch1`), the ReDim bug +`redim-inverted-bounds-error` in BUGS-TO-REPORT.md with its reproducer (merged from +`claude/cool-moser-5bdc31`), and the section's rename to *Common Errors*. Batch 2 starts +from that branch, or from staging once it is merged there. `test.bat` last passed in full on +batch 1's branch; after the merges and the rename, `check_errors`, `check_gate_lists`, +`check_cli` and `check_ci_workflows` pass. + +**Next: batch 2, the VBA `Strings` pilot.** List the module's pages +(`docs/Reference/Default/VBA/Strings/`), keep those that can raise, and for each: read the page +for error prose and for cases to try, write its entry in `errors/VBA.Strings.yml` (Mid and Len +are there already), `node scripts/errors.mjs run VBA.Strings.yml --only ` until twinBASIC +and VB6 agree with the data, `node scripts/errors.mjs write`, then remove the page's error +sentences that the table now carries. A VB6 difference is a row with `vb6:` and a reproducer +(`node scripts/bug_repro.mjs new `; `bug: pending` until it exists). Show the owner the +first few pages before doing the rest: batch 2 settles the style. ## What batch 2 needs to know (from batch 1) From 73b82bd9f19c892366a9481612d9c9f4c4e2f39c Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 21 Oct 2026 00:58:12 +0200 Subject: [PATCH 14/21] Errors batch 2 begins: Asc, Chr, InStr, InStrRev, Left, Right; error: 0 rows for what only VB6 raises --- WIP.Errors.md | 56 ++++-- docs/Reference/Default/VBA/Strings/Asc.md | 11 +- docs/Reference/Default/VBA/Strings/Chr.md | 10 + docs/Reference/Default/VBA/Strings/InStr.md | 19 +- .../Reference/Default/VBA/Strings/InStrRev.md | 12 +- docs/Reference/Default/VBA/Strings/Left.md | 10 + docs/Reference/Default/VBA/Strings/Right.md | 10 + docs/Reference/Run-time-Errors.md | 6 +- errors/VBA.Strings.yml | 182 +++++++++++++++++- scripts/check_errors.mjs | 16 ++ scripts/lib/errors-data.mjs | 21 +- 11 files changed, 321 insertions(+), 32 deletions(-) diff --git a/WIP.Errors.md b/WIP.Errors.md index 33f3d97ea..0416d8522 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -164,7 +164,10 @@ pages: A case is an expression, or a mapping with `expr` or `run` (statements; a multi-line string for several) and optionally `decl` (`Dim` lines), `setup` (statements before it) and -`only: tb`. A row's `win64:` is the number win64 raises where it differs. A page with no +`only: tb`. A row's `win64:` is the number win64 raises where it differs. A row of `error: 0` +with a `vb6:` other than 0 is a condition where twinBASIC raises nothing and VB6 does (batch 2, +`InStr` with `vbDatabaseCompare`): it gets its NOTE (*...; twinBASIC raises none.*) and no table +row, and a page whose rows are all such has a section of NOTEs alone. A page with no rows gets no section; one with `none: ""` and no rows gets a section holding that sentence, for an absence of errors worth stating. Quote a case that holds `: `, starts with a quote, or sits in a `{ }` mapping and holds a comma. `messages.yml` maps each number to its @@ -249,17 +252,13 @@ What the probes must respect (BETA 1010; `errors.mjs` already does all of it): ## Where things stand (2026-10-21) -Batches 0 and 1 are done and reviewed. Everything is on branch **`errors-batch0`** (worktree -`.claude/worktrees/infallible-wilson-117b19`), **not pushed and with no PR**: the tooling, -the trial sections (Mid, ReDim; Len raises nothing), the central page -`docs/Reference/Run-time-Errors.md` (merged from `errors-batch1`), the ReDim bug -`redim-inverted-bounds-error` in BUGS-TO-REPORT.md with its reproducer (merged from -`claude/cool-moser-5bdc31`), and the section's rename to *Common Errors*. Batch 2 starts -from that branch, or from staging once it is merged there. `test.bat` last passed in full on -batch 1's branch; after the merges and the rename, `check_errors`, `check_gate_lists`, -`check_cli` and `check_ci_workflows` pass. - -**Next: batch 2, the VBA `Strings` pilot.** List the module's pages +Batches 0 and 1 are done, reviewed and on staging. **Batch 2 is started, on staging**: Asc, +Chr, InStr, InStrRev, Left and Right have their data and sections (with Mid and Len, eight of +the module's pages), all 121 cases agreeing in twinBASIC 32-bit and VB6; `build`, `check` and +`test` pass. **Waiting for the owner's review of those six** before the rest; the questions +for it are under *Batch 2: open questions*. + +**Batch 2, the VBA `Strings` pilot.** List the module's pages (`docs/Reference/Default/VBA/Strings/`), keep those that can raise, and for each: read the page for error prose and for cases to try, write its entry in `errors/VBA.Strings.yml` (Mid and Len are there already), `node scripts/errors.mjs run VBA.Strings.yml --only ` until twinBASIC @@ -268,6 +267,37 @@ sentences that the table now carries. A VB6 difference is a row with `vb6:` and (`node scripts/bug_repro.mjs new `; `bug: pending` until it exists). Show the owner the first few pages before doing the rest: batch 2 settles the style. +## Batch 2: what the first six pages found, and open questions + +Measured on BETA 1010, twinBASIC 32-bit and VB6 agreeing unless said: + +- **`InStr` and `InStrRev` refuse `vbUseCompareOption` (-1)** with error 5; their pages listed + it as a setting, and the row is gone from both tables. A *compare* above 1 is read as an + LCID (3, 4, 1024, 1033 pass; 255, 65536, 99999 raise 5). `InStrB` ignores *compare*. +- **Two `InStr` differences from VB6, `bug: pending`:** VB6 raises 5 for *compare* 2 + (`vbDatabaseCompare`) where twinBASIC takes it as an LCID; and VB6 does not check the LCID + when *string1* or *string2* is zero-length, where twinBASIC raises 5. `InStrRev` checks it in + both. Not yet queued in BUGS-TO-REPORT.md. +- `Left`, `Right` (not the B forms) raise 5 above 1,073,741,823, as `Mid` does; `InStr` raises + 5 for a *start* above 1,073,741,824, `InStrRev` and `InStrB` do not. +- `Chr` raises 5 outside 0--255 on this machine (not a DBCS code page); `ChrW` outside + -32,768--65,535; **`ChrB` raises 6, *Overflow*, outside 0--255**. + +Open for the owner: + +1. **94 (and 6) from converting an argument.** The rule leaves them out, but which parameters + convert is invisible to the reader: `InStr`'s *start* is a **Variant** (its 94 is listed), + `InStrRev`'s is a **Long** (its 94 is not), and `Asc(Null)`, `Chr(Null)`, `InStrRev(Null, "a")` + and `InStr(1, "abc", "a", Null)` raise 94 by conversion and are not listed. The pages' own + sentences on those ("If *start* contains **Null**, an error occurs", InStrRev's 94 + paragraph) are kept for now. `ChrB`'s 6 is listed although it looks like a conversion to + **Byte**: the page says *charcode* is a **Long**. List conversion errors after all, or keep + them out and keep such sentences? +2. Whether the two `InStr` rows of error 5 on *compare* read well together, and the NOTE's + wording for a row of error 0. +3. Whether a behaviour that both runtimes share and that contradicts the page (the -1 setting) + is fixed silently, as here, or called out. + ## What batch 2 needs to know (from batch 1) - A row whose number is not in `messages.yml` fails the gate. VB's 88 numbers are all there; @@ -290,7 +320,7 @@ each can be run when there is budget. Status is kept here. |---|---|---|---| | 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | done; the owner reviewed the trial sections (Mid, ReDim; Len raises nothing) on 2026-10-20 | | 1 | The central Run-time errors page | 1 | done; the owner reviewed the page and settled on `Err.Description` (2026-10-20) | -| 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | not started | +| 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | started: 8 pages, waiting for the owner's review | | 3 | VBA `Conversion` | 28 | not started | | 4 | VBA `Math` (also win64) | 17 | not started | | 5 | VBA `DateTime` | 20 | not started | diff --git a/docs/Reference/Default/VBA/Strings/Asc.md b/docs/Reference/Default/VBA/Strings/Asc.md index ed9592c63..e1e2eb2f3 100644 --- a/docs/Reference/Default/VBA/Strings/Asc.md +++ b/docs/Reference/Default/VBA/Strings/Asc.md @@ -16,7 +16,7 @@ Syntax: - **AscW(** *string* **)** *string* -: *required* Any valid string expression. If *string* contains no characters, a run-time error occurs. +: *required* Any valid string expression. The range for returns from **Asc** is 0--255 on non-DBCS systems, but -32768--32767 on DBCS systems. @@ -25,6 +25,15 @@ The range for returns from **Asc** is 0--255 on non-DBCS systems, but -32768--32 The functions [**Chr**, **ChrB**, and **ChrW**](Chr) are the opposite of **Asc**, **AscB**, and **AscW**. The **Chr** functions convert an integer to a character string. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *string* is zero-length | + + + ### Example This example uses the **Asc** function to return a character code corresponding to the first letter in the string. diff --git a/docs/Reference/Default/VBA/Strings/Chr.md b/docs/Reference/Default/VBA/Strings/Chr.md index 26b17f13f..90b12f1bf 100644 --- a/docs/Reference/Default/VBA/Strings/Chr.md +++ b/docs/Reference/Default/VBA/Strings/Chr.md @@ -29,6 +29,16 @@ Numbers from 0--31 are the same as standard, nonprintable ASCII codes. For examp The functions [**Asc**, **AscB**, and **AscW**](Asc) are the opposite of **Chr**, **ChrB**, and **ChrW**. The **Asc** functions convert a string to an integer. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | **Chr** or **Chr$** is given a *charcode* less than 0 or greater than 255
**ChrW** or **ChrW$** is given a *charcode* less than -32,768 or greater than 65,535 | +| [6 (`&H6`)](../../../Reference/Run-time-Errors#error-6), *Overflow* | **ChrB** or **ChrB$** is given a *charcode* less than 0 or greater than 255 | + + + ### Example This example uses the **Chr** function to return the character associated with the specified character code. diff --git a/docs/Reference/Default/VBA/Strings/InStr.md b/docs/Reference/Default/VBA/Strings/InStr.md index 5654466a1..7c7f7fe60 100644 --- a/docs/Reference/Default/VBA/Strings/InStr.md +++ b/docs/Reference/Default/VBA/Strings/InStr.md @@ -15,7 +15,7 @@ Syntax: - **InStrB(** [ *start* **,** ] *string1*, *string2* [ **,** *compare* ] **)** *start* -: *optional* Numeric expression that sets the starting position for each search. If omitted, search begins at the first character position. *start* must be 1 or greater: 0 or a negative value raises error 5 (Invalid procedure call or argument), and **Null** raises error 94 (Invalid use of Null). The *start* argument is required if *compare* is specified. +: *optional* Numeric expression that sets the starting position for each search. If omitted, search begins at the first character position. *start* must be 1 or greater. The *start* argument is required if *compare* is specified. *string1* : *required* String expression being searched. @@ -30,7 +30,6 @@ The *compare* argument settings are: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the **Option Compare** statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | @@ -48,6 +47,22 @@ The *compare* argument settings are: The **InStrB** function is used with byte data contained in a string. Instead of returning the character position of the first occurrence of one string within another, **InStrB** returns the byte position. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is less than 1
**InStr** is given a *start* greater than 1,073,741,824
**InStr** is given a *compare* that is less than 0, or greater than 1 and not a valid locale identifier
**InStr** is given a *compare* that is not a valid locale identifier, and *string1* or *string2* is zero-length | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *start* is **Null** | + +> [!NOTE] BETA 1010 +> When **InStr** is given a *compare* that is not a valid locale identifier, and *string1* or *string2* is zero-length, VB6 raises no error. + +> [!NOTE] BETA 1010 +> When **InStr** is given a *compare* of 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*; twinBASIC raises none. + + + ### Example This example uses the **InStr** function to return the position of the first occurrence of one string within another. diff --git a/docs/Reference/Default/VBA/Strings/InStrRev.md b/docs/Reference/Default/VBA/Strings/InStrRev.md index 3aee7a6c2..7d6cd5e47 100644 --- a/docs/Reference/Default/VBA/Strings/InStrRev.md +++ b/docs/Reference/Default/VBA/Strings/InStrRev.md @@ -18,7 +18,7 @@ Syntax: **InStrRev(** *stringcheck*, *stringmatch* [ **,** *start* [ **,** *comp : *required* String expression being searched for. *start* -: *optional* Numeric expression that sets the starting position for each search. If omitted, -1 is used, which means that the search begins at the last character position. *start* must be -1, or 1 or greater: 0 or any other negative value raises error 5 (Invalid procedure call or argument). If *start* contains **Null**, an error occurs. +: *optional* Numeric expression that sets the starting position for each search. If omitted, -1 is used, which means that the search begins at the last character position. *start* must be -1, or 1 or greater. If *start* contains **Null**, an error occurs. *compare* : *optional* Numeric value indicating the kind of comparison to use when evaluating substrings. If omitted, a binary comparison is performed. See settings below. @@ -27,7 +27,6 @@ The *compare* argument can have the following values: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the [**Option Compare**](../../Core/Option) statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | @@ -48,6 +47,15 @@ If *stringcheck* or *stringmatch* is **Null**, error 94 (Invalid use of Null) oc **InStrRev** will not find an instance of *stringmatch* unless the position of the end character of *stringmatch* is less than or equal to *start*. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is 0, or less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | + + + ### Example This example uses **InStrRev** to find the last occurrence of a substring. diff --git a/docs/Reference/Default/VBA/Strings/Left.md b/docs/Reference/Default/VBA/Strings/Left.md index 0db760f65..279ecc0a3 100644 --- a/docs/Reference/Default/VBA/Strings/Left.md +++ b/docs/Reference/Default/VBA/Strings/Left.md @@ -27,6 +27,16 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi > [!NOTE] > Use the **LeftB** function with byte data contained in a string. Instead of specifying the number of characters to return, *length* specifies the number of bytes. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *length* is negative
**Left** or **Left$** is given a *length* greater than 1,073,741,823 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Left$** or **LeftB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | + + + ### Example This example uses the **Left** function to return a specified number of characters from the left side of a string. diff --git a/docs/Reference/Default/VBA/Strings/Right.md b/docs/Reference/Default/VBA/Strings/Right.md index d088f2302..ac33002e1 100644 --- a/docs/Reference/Default/VBA/Strings/Right.md +++ b/docs/Reference/Default/VBA/Strings/Right.md @@ -27,6 +27,16 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi > [!NOTE] > Use the **RightB** function with byte data contained in a string. Instead of specifying the number of characters to return, *length* specifies the number of bytes. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *length* is negative
**Right** or **Right$** is given a *length* greater than 1,073,741,823 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Right$** or **RightB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | + + + ### Example This example uses the **Right** function to return a specified number of characters from the right side of a string. diff --git a/docs/Reference/Run-time-Errors.md b/docs/Reference/Run-time-Errors.md index 0725689b0..1c72971f2 100644 --- a/docs/Reference/Run-time-Errors.md +++ b/docs/Reference/Run-time-Errors.md @@ -43,8 +43,8 @@ The messages are those of twinBASIC 32-bit and VB6, which agree for every number | Error | Message | Listed by | |---|---|---| | **3**{: #error-3 } (`&H3`) | *Return without GoSub* | --- | -| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [Mid](../tB/Modules/Strings/Mid) | -| **6**{: #error-6 } (`&H6`) | *Overflow* | --- | +| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Left](../tB/Modules/Strings/Left), [Mid](../tB/Modules/Strings/Mid), [Right](../tB/Modules/Strings/Right) | +| **6**{: #error-6 } (`&H6`) | *Overflow* | [Chr](../tB/Modules/Strings/Chr) | | **7**{: #error-7 } (`&H7`) | *Out of memory* | --- | | **9**{: #error-9 } (`&H9`) | *Subscript out of range* | [ReDim](../tB/Core/ReDim) | | **10**{: #error-10 } (`&HA`) | *This array is fixed or temporarily locked* | --- | @@ -81,7 +81,7 @@ The messages are those of twinBASIC 32-bit and VB6, which agree for every number | **91**{: #error-91 } (`&H5B`) | *Object variable or With block variable not set* | --- | | **92**{: #error-92 } (`&H5C`) | *For loop not initialized* | --- | | **93**{: #error-93 } (`&H5D`) | *Invalid pattern string* | --- | -| **94**{: #error-94 } (`&H5E`) | *Invalid use of Null* | [Mid](../tB/Modules/Strings/Mid) | +| **94**{: #error-94 } (`&H5E`) | *Invalid use of Null* | [InStr](../tB/Modules/Strings/InStr), [Left](../tB/Modules/Strings/Left), [Mid](../tB/Modules/Strings/Mid), [Right](../tB/Modules/Strings/Right) | | **96**{: #error-96 } (`&H60`) | *Unable to sink events of object because the object is already firing events to the maximum number of event receivers that it supports* | --- | | **97**{: #error-97 } (`&H61`) | *Can not call friend function on object which is not an instance of defining class* | --- | | **98**{: #error-98 } (`&H62`) | *A property or method call cannot include a reference to a private object, either as an argument or as a return value* | --- | diff --git a/errors/VBA.Strings.yml b/errors/VBA.Strings.yml index 30f8d5089..d96b5750c 100644 --- a/errors/VBA.Strings.yml +++ b/errors/VBA.Strings.yml @@ -1,6 +1,156 @@ # The VBA Strings module. Format: WIP.Errors.md. beta: 1010 pages: + Reference/Default/VBA/Strings/Asc.md: + rows: + - error: 5 + when: "*string* is zero-length" + cases: + - Asc("") + - AscB("") + - AscW("") + - { decl: Dim S As String, expr: Asc(S) } + quiet: + - Asc("A") + - Asc(ChrW(&H20AC)) + - AscW(ChrW(&H20AC)) + - AscB("A") + Reference/Default/VBA/Strings/Chr.md: + rows: + - error: 5 + when: "**Chr** or **Chr$** is given a *charcode* less than 0 or greater than 255" + cases: + - Chr(-1) + - Chr(256) + - Chr$(256) + - Chr(-32768) + - Chr(65535) + - error: 5 + when: "**ChrW** or **ChrW$** is given a *charcode* less than -32,768 or greater than 65,535" + cases: + - ChrW(-32769) + - ChrW(65536) + - ChrW$(65536) + - error: 6 + when: "**ChrB** or **ChrB$** is given a *charcode* less than 0 or greater than 255" + cases: + - ChrB(-1) + - ChrB(256) + - ChrB$(256) + quiet: + - Chr(0) + - Chr(255) + - ChrB(255) + - ChrW(-32768) + - ChrW(-1) + - ChrW(65535) + Reference/Default/VBA/Strings/InStr.md: + rows: + - error: 5 + when: "*start* is less than 1" + cases: + - InStr(0, "abc", "a") + - InStr(-1, "abc", "a") + - InStrB(0, "abc", "a") + - error: 5 + when: "**InStr** is given a *start* greater than 1,073,741,824" + cases: + - InStr(1073741825, "abc", "a") + - InStr(2147483647, "abc", "a") + - error: 5 + when: "**InStr** is given a *compare* that is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - InStr(1, "abc", "a", -1) + - InStr(1, "abc", "a", -2) + - InStr(1, "abc", "a", 255) + - InStr(1, "abc", "a", 65536) + - InStr(1, "abc", "a", 99999) + - error: 5 + when: "**InStr** is given a *compare* that is not a valid locale identifier, and *string1* or *string2* is zero-length" + vb6: 0 + bug: pending + cases: + - InStr(1, "", "a", 99999) + - InStr(1, "abc", "", 99999) + - error: 0 + when: "**InStr** is given a *compare* of 2 (**vbDatabaseCompare**)" + vb6: 5 + bug: pending + cases: + - InStr(1, "abc", "a", 2) + - error: 94 + when: "*start* is **Null**" + cases: + - InStr(Null, "abc", "a") + - InStrB(Null, "abc", "a") + quiet: + - InStr(1073741824, "abc", "a") + - InStrB(2147483647, "abc", "a") + - InStr(5, "abc", "") + - InStr(Null, "abc") + - InStr("abc", Null) + - InStr(1, Null, "a") + - InStr(1, "abc", "a", 0) + - InStr(1, "abc", "a", 1) + - InStr(1, "abc", "a", 1033) + - InStr(1, "abc", "a", 1041) + - InStrB(1, "abc", "a", 2) + - InStrB(1, "abc", "a", 99999) + Reference/Default/VBA/Strings/InStrRev.md: + rows: + - error: 5 + when: "*start* is 0, or less than -1" + cases: + - InStrRev("abc", "a", 0) + - InStrRev("abc", "a", -2) + - error: 5 + when: "*compare* is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - InStrRev("abc", "a", -1, -1) + - InStrRev("abc", "a", -1, -2) + - InStrRev("abc", "a", -1, 65536) + - InStrRev("abc", "a", -1, 99999) + - InStrRev("abc", "a", -1, 2147483647) + - InStrRev("", "a", -1, 99999) + quiet: + - InStrRev("abc", "a", 2147483647) + - InStrRev("abc", "", 5) + - InStrRev("abc", "a", -1, 2) + - InStrRev("abc", "a", -1, 1033) + Reference/Default/VBA/Strings/Left.md: + rows: + - error: 5 + when: "*length* is negative" + cases: + - Left("abc", -1) + - Left$("abc", -1) + - LeftB("abc", -1) + - LeftB$("abc", -2) + - error: 5 + when: "**Left** or **Left$** is given a *length* greater than 1,073,741,823" + cases: + - Left("abc", 1073741824) + - Left$("abc", 2147483647) + - error: 94 + when: "**Left$** or **LeftB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null**" + cases: + - Left$(Null, 1) + - LeftB$(Null, 1) + - Left("abc", Null) + - LeftB("abc", Null) + quiet: + - Left(Null, 1) + - LeftB(Null, 1) + - Left("abc", 0) + - Left("abc", 1073741823) + - LeftB("abc", 2147483647) + Reference/Default/VBA/Strings/Len.md: + quiet: + - Len("") + - Len(Null) + - LenB(Null) + - Len(Empty) + - { decl: Dim S As String * 4, expr: Len(S) } Reference/Default/VBA/Strings/Mid.md: rows: - error: 5 @@ -35,10 +185,30 @@ pages: - MidB("abc", 2147483647) - MidB("abc", 1, 2147483647) - Mid("", 1, 0) - Reference/Default/VBA/Strings/Len.md: + Reference/Default/VBA/Strings/Right.md: + rows: + - error: 5 + when: "*length* is negative" + cases: + - Right("abc", -1) + - Right$("abc", -1) + - RightB("abc", -1) + - RightB$("abc", -2) + - error: 5 + when: "**Right** or **Right$** is given a *length* greater than 1,073,741,823" + cases: + - Right("abc", 1073741824) + - Right$("abc", 2147483647) + - error: 94 + when: "**Right$** or **RightB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null**" + cases: + - Right$(Null, 1) + - RightB$(Null, 1) + - Right("abc", Null) + - RightB("abc", Null) quiet: - - Len("") - - Len(Null) - - LenB(Null) - - Len(Empty) - - { decl: Dim S As String * 4, expr: Len(S) } + - Right(Null, 1) + - RightB(Null, 1) + - Right("abc", 0) + - Right("abc", 1073741823) + - RightB("abc", 2147483647) diff --git a/scripts/check_errors.mjs b/scripts/check_errors.mjs index 569d7abb1..04a69a157 100644 --- a/scripts/check_errors.mjs +++ b/scripts/check_errors.mjs @@ -177,6 +177,22 @@ check( merged.join("\n"), ); check("a page with only quiet cases gets no section", renderSection({ rows: [], quiet: [1] }, { messages }) === null); +const vb6Only = renderSection( + { rows: [{ error: 0, when: "*x* is 2", vb6: 5, bug: "pending", cases: [] }], quiet: [] }, + { name: "probe.yml", beta: 1010, messages, level: "##", permalink: "/A" }, +).join("\n"); +check( + "a row of error 0 has its VB6 note and no table", + !vb6Only.includes("| Error |") && + vb6Only.includes( + "## Common Errors\n\n> [!NOTE] BETA 1010\n> When *x* is 2, VB6 raises error [5 (`&H5`)](Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*; twinBASIC raises none.\n\n", + ), + vb6Only, +); +check( + "error 0 is refused without a vb6: number", + data("beta: 1\npages:\n A.md:\n rows:\n - { error: 0, when: x, cases: [x] }\n").problems.length === 1, +); const doc = "# T\n\ntext\n\n```tb\n### Example\n\n```\n\n### Example\n\ncode\n\n### See Also\n"; const added = spliceSection(doc, section); diff --git a/scripts/lib/errors-data.mjs b/scripts/lib/errors-data.mjs index be48a1321..e1974219a 100644 --- a/scripts/lib/errors-data.mjs +++ b/scripts/lib/errors-data.mjs @@ -124,7 +124,10 @@ export function readData(raw, name, problems) { return; } for (const k of Object.keys(r)) if (!ROW_KEYS.has(k)) problems.push(`${w}: unknown key ${k}`); - if (!isInt(r.error) || r.error === 0) problems.push(`${w}: error: a number other than 0`); + // error: 0 is a condition where twinBASIC raises nothing and VB6 does: a NOTE, no table row. + if (!isInt(r.error) || (r.error === 0 && !r.vb6)) { + problems.push(`${w}: error: a number other than 0, or 0 with a vb6: other than 0`); + } if (typeof r.when !== "string" || !r.when.trim()) problems.push(`${w}: when: the condition, in the reader's terms`); else if (/\||\n/.test(r.when.trim())) problems.push(`${w}: when: one line, without |`); @@ -448,14 +451,22 @@ export function renderSection(p, { name, beta, messages, level, permalink }) { ]; if (p.none !== undefined) out.push(p.none.trim()); else { - out.push("| Error | When |", "|---|---|"); // Rows that raise the same number share one row, their conditions a line each. + // A row of error 0 raises nothing in twinBASIC: it has its NOTE and no table row. const byError = new Map(); - for (const r of p.rows) byError.set(r.error, [...(byError.get(r.error) ?? []), escapeCell(r.when)]); + for (const r of p.rows.filter((x) => x.error !== 0)) + byError.set(r.error, [...(byError.get(r.error) ?? []), escapeCell(r.when)]); + if (byError.size) out.push("| Error | When |", "|---|---|"); for (const [n, whens] of byError) out.push(`| ${num(n)}, *${msg(n)}* | ${whens.join("
")} |`); for (const r of p.rows.filter((x) => x.vb6 !== undefined)) { - const what = r.vb6 === 0 ? "raises no error" : `raises error ${num(r.vb6)}, *${msg(r.vb6)}*, instead`; - out.push("", `> [!NOTE] BETA ${beta}`, `> When ${r.when}, VB6 ${what}.`); + const what = + r.vb6 === 0 + ? "raises no error" + : r.error === 0 + ? `raises error ${num(r.vb6)}, *${msg(r.vb6)}*; twinBASIC raises none` + : `raises error ${num(r.vb6)}, *${msg(r.vb6)}*, instead`; + if (out.at(-1) !== "") out.push(""); + out.push(`> [!NOTE] BETA ${beta}`, `> When ${r.when}, VB6 ${what}.`); } } out.push("", ""); From a84ce0bd510fde5e0cd0959f0d18b5b7937567f8 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 21 Oct 2026 01:08:57 +0200 Subject: [PATCH 15/21] Errors batch 2: Null arguments' errors listed; refused VBA settings kept as comments --- WIP.Authoring.md | 7 +++ WIP.Errors.md | 62 ++++++++++--------- docs/Reference/Default/VBA/Strings/Asc.md | 1 + docs/Reference/Default/VBA/Strings/Chr.md | 1 + docs/Reference/Default/VBA/Strings/InStr.md | 8 ++- .../Reference/Default/VBA/Strings/InStrRev.md | 9 ++- docs/Reference/Default/VBA/Strings/Mid.md | 2 +- docs/Reference/Run-time-Errors.md | 6 +- errors/VBA.Strings.yml | 21 ++++++- 9 files changed, 77 insertions(+), 40 deletions(-) diff --git a/WIP.Authoring.md b/WIP.Authoring.md index 644d4b662..9457f0366 100644 --- a/WIP.Authoring.md +++ b/WIP.Authoring.md @@ -353,6 +353,13 @@ Add a `> [!NOTE]` callout or rewrite the affected section when source diverges. When in doubt about a tB-specific behavior, check `docs/Features/` and `docs/Reference/index.md` before assuming VBA semantics carry over. +**VBA-Docs content that twinBASIC and VB6 both contradict is commented out, not deleted** +(owner, 2026-10-21): the documentation is meant to become switchable between VB6, VBA and +twinBASIC, and the VBA text should not be lost. Put it in an HTML comment that starts +`` comment on both. A *compare* above 1 is read as an LCID (3, 4, 1024, 1033 pass; 255, 65536, 99999 raise 5). `InStrB` ignores *compare*. - **Two `InStr` differences from VB6, `bug: pending`:** VB6 raises 5 for *compare* 2 (`vbDatabaseCompare`) where twinBASIC takes it as an LCID; and VB6 does not check the LCID @@ -282,21 +287,18 @@ Measured on BETA 1010, twinBASIC 32-bit and VB6 agreeing unless said: 5 for a *start* above 1,073,741,824, `InStrRev` and `InStrB` do not. - `Chr` raises 5 outside 0--255 on this machine (not a DBCS code page); `ChrW` outside -32,768--65,535; **`ChrB` raises 6, *Overflow*, outside 0--255**. +- Every **Null** argument of the six raises 94, and every number beyond a **Long** raises 6, in + both runtimes. -Open for the owner: - -1. **94 (and 6) from converting an argument.** The rule leaves them out, but which parameters - convert is invisible to the reader: `InStr`'s *start* is a **Variant** (its 94 is listed), - `InStrRev`'s is a **Long** (its 94 is not), and `Asc(Null)`, `Chr(Null)`, `InStrRev(Null, "a")` - and `InStr(1, "abc", "a", Null)` raise 94 by conversion and are not listed. The pages' own - sentences on those ("If *start* contains **Null**, an error occurs", InStrRev's 94 - paragraph) are kept for now. `ChrB`'s 6 is listed although it looks like a conversion to - **Byte**: the page says *charcode* is a **Long**. List conversion errors after all, or keep - them out and keep such sentences? -2. Whether the two `InStr` rows of error 5 on *compare* read well together, and the NOTE's - wording for a row of error 0. -3. Whether a behaviour that both runtimes share and that contradicts the page (the -1 setting) - is fixed silently, as here, or called out. +The owner's answers (2026-10-21): + +1. **An error from a Null argument is listed**, whatever raises it; 13 for a value of the + wrong kind and 6 for a number outside the documented type are not (*What a page gets*). + The pages' "if ... is **Null**, an error occurs" sentences go. +2. The two `InStr` rows on *compare*, and the NOTE of an `error: 0` row, read well; the NOTE + was checked again against a run and the built page. +3. Content both runtimes contradict is the page's fault: commented out, not deleted, so the + VBA text is kept for docs that will switch between VB6, VBA and twinBASIC. ## What batch 2 needs to know (from batch 1) @@ -320,7 +322,7 @@ each can be run when there is budget. Status is kept here. |---|---|---|---| | 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | done; the owner reviewed the trial sections (Mid, ReDim; Len raises nothing) on 2026-10-20 | | 1 | The central Run-time errors page | 1 | done; the owner reviewed the page and settled on `Err.Description` (2026-10-20) | -| 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | started: 8 pages, waiting for the owner's review | +| 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | under way: 8 pages; style settled by the owner (2026-10-21) | | 3 | VBA `Conversion` | 28 | not started | | 4 | VBA `Math` (also win64) | 17 | not started | | 5 | VBA `DateTime` | 20 | not started | diff --git a/docs/Reference/Default/VBA/Strings/Asc.md b/docs/Reference/Default/VBA/Strings/Asc.md index e1e2eb2f3..2de5c1b2d 100644 --- a/docs/Reference/Default/VBA/Strings/Asc.md +++ b/docs/Reference/Default/VBA/Strings/Asc.md @@ -31,6 +31,7 @@ The functions [**Chr**, **ChrB**, and **ChrW**](Chr) are the opposite of **Asc** | Error | When | |---|---| | [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *string* is zero-length | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *string* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/Chr.md b/docs/Reference/Default/VBA/Strings/Chr.md index 90b12f1bf..7366e8bf4 100644 --- a/docs/Reference/Default/VBA/Strings/Chr.md +++ b/docs/Reference/Default/VBA/Strings/Chr.md @@ -36,6 +36,7 @@ The functions [**Asc**, **AscB**, and **AscW**](Asc) are the opposite of **Chr** |---|---| | [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | **Chr** or **Chr$** is given a *charcode* less than 0 or greater than 255
**ChrW** or **ChrW$** is given a *charcode* less than -32,768 or greater than 65,535 | | [6 (`&H6`)](../../../Reference/Run-time-Errors#error-6), *Overflow* | **ChrB** or **ChrB$** is given a *charcode* less than 0 or greater than 255 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *charcode* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/InStr.md b/docs/Reference/Default/VBA/Strings/InStr.md index 7c7f7fe60..d6efe854c 100644 --- a/docs/Reference/Default/VBA/Strings/InStr.md +++ b/docs/Reference/Default/VBA/Strings/InStr.md @@ -24,7 +24,7 @@ Syntax: : *required* String expression sought. *compare* -: *optional* Specifies the type of string comparison. If *compare* is **Null**, an error occurs. If *compare* is omitted, the [**Option Compare**](../../Core/Option) setting determines the type of comparison. Specify a valid LCID (LocaleID) to use locale-specific rules in the comparison. +: *optional* Specifies the type of string comparison. If *compare* is omitted, the [**Option Compare**](../../Core/Option) setting determines the type of comparison. Specify a valid LCID (LocaleID) to use locale-specific rules in the comparison. The *compare* argument settings are: @@ -33,6 +33,10 @@ The *compare* argument settings are: | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + **Return values:** the first row that applies gives the result. | If | **InStr** returns | @@ -53,7 +57,7 @@ The **InStrB** function is used with byte data contained in a string. Instead of | Error | When | |---|---| | [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is less than 1
**InStr** is given a *start* greater than 1,073,741,824
**InStr** is given a *compare* that is less than 0, or greater than 1 and not a valid locale identifier
**InStr** is given a *compare* that is not a valid locale identifier, and *string1* or *string2* is zero-length | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *start* is **Null** | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *start* or *compare* is **Null** | > [!NOTE] BETA 1010 > When **InStr** is given a *compare* that is not a valid locale identifier, and *string1* or *string2* is zero-length, VB6 raises no error. diff --git a/docs/Reference/Default/VBA/Strings/InStrRev.md b/docs/Reference/Default/VBA/Strings/InStrRev.md index 7d6cd5e47..e2381083f 100644 --- a/docs/Reference/Default/VBA/Strings/InStrRev.md +++ b/docs/Reference/Default/VBA/Strings/InStrRev.md @@ -18,7 +18,7 @@ Syntax: **InStrRev(** *stringcheck*, *stringmatch* [ **,** *start* [ **,** *comp : *required* String expression being searched for. *start* -: *optional* Numeric expression that sets the starting position for each search. If omitted, -1 is used, which means that the search begins at the last character position. *start* must be -1, or 1 or greater. If *start* contains **Null**, an error occurs. +: *optional* Numeric expression that sets the starting position for each search. If omitted, -1 is used, which means that the search begins at the last character position. *start* must be -1, or 1 or greater. *compare* : *optional* Numeric value indicating the kind of comparison to use when evaluating substrings. If omitted, a binary comparison is performed. See settings below. @@ -30,6 +30,10 @@ The *compare* argument can have the following values: | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + **Return values:** the first row that applies gives the result. | If | **InStrRev** returns | @@ -40,7 +44,7 @@ The *compare* argument can have the following values: | *stringmatch* is not found | 0 | | *stringmatch* is found within *stringcheck* | Position at which match is found | -If *stringcheck* or *stringmatch* is **Null**, error 94 (Invalid use of Null) occurs. Unlike **InStr**, **InStrRev** does not return **Null**. +Unlike **InStr**, **InStrRev** does not return **Null**: a **Null** argument is an error. > [!NOTE] > The syntax for the **InStrRev** function is not the same as the syntax for the [**InStr**](InStr) function --- note the swapped order of the search arguments. @@ -53,6 +57,7 @@ If *stringcheck* or *stringmatch* is **Null**, error 94 (Invalid use of Null) oc | Error | When | |---|---| | [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is 0, or less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | any argument is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/Mid.md b/docs/Reference/Default/VBA/Strings/Mid.md index 9b70b829f..c7909c282 100644 --- a/docs/Reference/Default/VBA/Strings/Mid.md +++ b/docs/Reference/Default/VBA/Strings/Mid.md @@ -39,7 +39,7 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi | Error | When | |---|---| | [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative
**Mid** or **Mid$** is given a *start* greater than 1,073,741,824 or a *length* greater than 1,073,741,823 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *start* or *length* that is **Null** | diff --git a/docs/Reference/Run-time-Errors.md b/docs/Reference/Run-time-Errors.md index 1c72971f2..19fe34c4a 100644 --- a/docs/Reference/Run-time-Errors.md +++ b/docs/Reference/Run-time-Errors.md @@ -11,7 +11,7 @@ index_also: trappable errors, error numbers, error messages Every run-time error number that has a message of its own, with that message, and the reference pages that list each number among the errors their procedure raises. -A run-time error can be trapped: [**On Error**](../tB/Core/On-Error) sends it to a handler, which reads its number from [**Err.Number**](../tB/Modules/ErrObject/Number) and its message from [**Err.Description**](../tB/Modules/ErrObject/Description). A reference page that documents a procedure, a statement or an operator has an **Errors** section when it raises errors of its own: a table of each number it raises, and when. Each number there links to its row below. +A run-time error can be trapped: [**On Error**](../tB/Core/On-Error) sends it to a handler, which reads its number from [**Err.Number**](../tB/Modules/ErrObject/Number) and its message from [**Err.Description**](../tB/Modules/ErrObject/Description). A reference page that documents a procedure, a statement or an operator has a **Common Errors** section when it raises errors of its own: a table of each number it raises, and when. Each number there links to its row below. ## What the Common Errors sections list @@ -20,7 +20,7 @@ A section lists the errors that a procedure raises from its own arguments and it A section does not list: - errors that any code can raise: 7, *Out of memory*, and 28, *Out of stack space*; -- an error in converting an argument to the type of its parameter before the call is made: 13, *Type mismatch*, for `Mid("abc", "x")`, and 94, *Invalid use of Null*, for `Mid("abc", Null)`, raised because the *start* parameter of **Mid** is a **Long**. A **Variant** parameter takes any value, so an error for a value of a **Variant** parameter is the procedure's own, and is listed; +- errors in converting an argument to the type its page gives for the parameter: 13, *Type mismatch*, for an argument of the wrong kind, such as `Mid("abc", "x")`, and 6, *Overflow*, for a number outside the range of that type, such as a *start* of 2147483648 for **Mid**, whose *start* is a **Long**. A **Null** argument is listed, whether the procedure raises the error itself or in converting the argument: `Mid("abc", Null)` raises 94, *Invalid use of Null*; - compile-time errors: the twinBASIC compiler reports these with diagnostic codes such as `TB5001`, before the program runs, and **On Error** cannot trap them. A procedure that raises no error of its own has no Common Errors section. @@ -81,7 +81,7 @@ The messages are those of twinBASIC 32-bit and VB6, which agree for every number | **91**{: #error-91 } (`&H5B`) | *Object variable or With block variable not set* | --- | | **92**{: #error-92 } (`&H5C`) | *For loop not initialized* | --- | | **93**{: #error-93 } (`&H5D`) | *Invalid pattern string* | --- | -| **94**{: #error-94 } (`&H5E`) | *Invalid use of Null* | [InStr](../tB/Modules/Strings/InStr), [Left](../tB/Modules/Strings/Left), [Mid](../tB/Modules/Strings/Mid), [Right](../tB/Modules/Strings/Right) | +| **94**{: #error-94 } (`&H5E`) | *Invalid use of Null* | [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Left](../tB/Modules/Strings/Left), [Mid](../tB/Modules/Strings/Mid), [Right](../tB/Modules/Strings/Right) | | **96**{: #error-96 } (`&H60`) | *Unable to sink events of object because the object is already firing events to the maximum number of event receivers that it supports* | --- | | **97**{: #error-97 } (`&H61`) | *Can not call friend function on object which is not an instance of defining class* | --- | | **98**{: #error-98 } (`&H62`) | *A property or method call cannot include a reference to a private object, either as an argument or as a return value* | --- | diff --git a/errors/VBA.Strings.yml b/errors/VBA.Strings.yml index d96b5750c..2e858dd6d 100644 --- a/errors/VBA.Strings.yml +++ b/errors/VBA.Strings.yml @@ -10,6 +10,9 @@ pages: - AscB("") - AscW("") - { decl: Dim S As String, expr: Asc(S) } + - error: 94 + when: "*string* is **Null**" + cases: [Asc(Null), AscB(Null), AscW(Null)] quiet: - Asc("A") - Asc(ChrW(&H20AC)) @@ -37,6 +40,9 @@ pages: - ChrB(-1) - ChrB(256) - ChrB$(256) + - error: 94 + when: "*charcode* is **Null**" + cases: [Chr(Null), Chr$(Null), ChrB(Null), ChrW(Null)] quiet: - Chr(0) - Chr(255) @@ -79,10 +85,12 @@ pages: cases: - InStr(1, "abc", "a", 2) - error: 94 - when: "*start* is **Null**" + when: "*start* or *compare* is **Null**" cases: - InStr(Null, "abc", "a") - InStrB(Null, "abc", "a") + - InStr(1, "abc", "a", Null) + - InStrB(1, "abc", "a", Null) quiet: - InStr(1073741824, "abc", "a") - InStrB(2147483647, "abc", "a") @@ -112,6 +120,13 @@ pages: - InStrRev("abc", "a", -1, 99999) - InStrRev("abc", "a", -1, 2147483647) - InStrRev("", "a", -1, 99999) + - error: 94 + when: "any argument is **Null**" + cases: + - InStrRev(Null, "a") + - InStrRev("abc", Null) + - InStrRev("abc", "a", Null) + - InStrRev("abc", "a", -1, Null) quiet: - InStrRev("abc", "a", 2147483647) - InStrRev("abc", "", 5) @@ -170,10 +185,12 @@ pages: - Mid("abc", 1, 1073741824) - Mid$("", 1, 2147483647) - error: 94 - when: "**Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null**" + when: "**Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *start* or *length* that is **Null**" cases: - Mid$(Null, 1) - MidB$(Null, 1) + - Mid("abc", Null) + - MidB$("abc", Null) - Mid("abc", 1, Null) - MidB$("abc", 1, Null) quiet: From 2c2e1fc9fe29dc4b639a0381af11463e0bff906b Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 21 Oct 2026 01:18:48 +0200 Subject: [PATCH 16/21] Queue two InStr compare bugs: vbDatabaseCompare accepted (also StrComp, Filter), and an invalid LCID refused on a zero-length string; ReDim entry moved below the header --- BUGS-TO-REPORT.md | 440 ++++++++++++++---- .../database-compare-no-error.twinproj | Bin 0 -> 5155 bytes bugs/database-compare-no-error/repro.json | 17 + bugs/database-compare-no-error/src/Settings | 75 +++ .../src/Sources/RegressionTests.twin | 79 ++++ .../src/Sources/Startup.twin | 6 + .../database-compare-no-error/vb6/Module1.bas | 30 ++ bugs/database-compare-no-error/vb6/Probe.vbp | 7 + .../instr-invalid-lcid-empty-string.twinproj | Bin 0 -> 6306 bytes .../repro.json | 20 + .../src/Settings | 75 +++ .../src/Sources/RegressionTests.twin | 115 +++++ .../src/Sources/Startup.twin | 6 + .../vb6/Module1.bas | 33 ++ .../vb6/Probe.vbp | 7 + errors/VBA.Strings.yml | 4 +- 16 files changed, 819 insertions(+), 95 deletions(-) create mode 100644 bugs/database-compare-no-error/database-compare-no-error.twinproj create mode 100644 bugs/database-compare-no-error/repro.json create mode 100644 bugs/database-compare-no-error/src/Settings create mode 100644 bugs/database-compare-no-error/src/Sources/RegressionTests.twin create mode 100644 bugs/database-compare-no-error/src/Sources/Startup.twin create mode 100644 bugs/database-compare-no-error/vb6/Module1.bas create mode 100644 bugs/database-compare-no-error/vb6/Probe.vbp create mode 100644 bugs/instr-invalid-lcid-empty-string/instr-invalid-lcid-empty-string.twinproj create mode 100644 bugs/instr-invalid-lcid-empty-string/repro.json create mode 100644 bugs/instr-invalid-lcid-empty-string/src/Settings create mode 100644 bugs/instr-invalid-lcid-empty-string/src/Sources/RegressionTests.twin create mode 100644 bugs/instr-invalid-lcid-empty-string/src/Sources/Startup.twin create mode 100644 bugs/instr-invalid-lcid-empty-string/vb6/Module1.bas create mode 100644 bugs/instr-invalid-lcid-empty-string/vb6/Probe.vbp diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 8e29322e8..494389228 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -1,96 +1,4 @@ -# twi## `ReDim` with an upper bound less than its lower bound raises -2147467259, *Unspecified error*, where VB6 raises error 9 - -**Describe the bug** -`ReDim X(0 To -1)`, `ReDim X(-1)` and `ReDim X(5 To 4, 1)` on a dynamic array (`Dim X() As Long`), all of which give a dimension an upper bound below its lower bound, raise error -2147467259 (`&H80004005`), *Unspecified error*, where VB6 raises error 9, *Subscript out of range*. A handler that tests for 9 does not recognise it. Observed in the compiler's test mode, 32-bit and 64-bit, and in a probe run after a build. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `redim-inverted-bounds-error.twinproj` (attached as `redim-inverted-bounds-error.zip`). -2. Run its test cases, the module below. On BETA 1010 the three controls pass (`ReDim X(0 To 0)`, `ReDim X(-3 To -1)`, and a subscript above a fixed-size array's range, which raises 9) and the four cases on an inverted bound fail with `Actual: -2147467259`; `Err.Description` is *Unspecified error*. - -The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`redim-inverted-bounds-error-vb6.zip`) prints: 9, *Subscript out of range*, for each of the three statements. -```vb -' Regression tests: ReDim with an upper bound less than its lower bound raises error 9, -' "Subscript out of range". Expected numbers are VB6's (the VB6 project beside this one -' prints them). tbrun --tests runs each case and prints PASS or FAIL. -[TestFixture] -Module RegressionTests - - ' Controls: bounds in order, an empty-looking but valid range, and a subscript out of range. - [TestCase] - Public Sub ReDim_OneElement_Succeeds() - Dim X() As Long - ReDim X(0 To 0) - Assert.Strict.AreEqual 0&, UBound(X) - End Sub - - [TestCase] - Public Sub ReDim_NegativeBoundsInOrder_Succeeds() - Dim X() As Long - ReDim X(-3 To -1) - Assert.Strict.AreEqual -3&, LBound(X) - End Sub - - [TestCase] - Public Sub FixedArray_SubscriptAboveRange_Raises9() - Dim A(1 To 3) As Long - On Error Resume Next - A(4) = 1 - Assert.Strict.AreEqual 9&, Err.Number - End Sub - - [TestCase] - Public Sub ReDim_ZeroToMinusOne_Raises9() - Dim X() As Long - On Error Resume Next - ReDim X(0 To -1) - Assert.Strict.AreEqual 9&, Err.Number - End Sub - - [TestCase] - Public Sub ReDim_MinusOne_Raises9() - Dim X() As Long - On Error Resume Next - ReDim X(-1) - Assert.Strict.AreEqual 9&, Err.Number - End Sub - - [TestCase] - Public Sub ReDim_FiveToFour_SecondDimension_Raises9() - Dim X() As Long - On Error Resume Next - ReDim X(5 To 4, 1) - Assert.Strict.AreEqual 9&, Err.Number - End Sub - - [TestCase] - Public Sub ReDim_ErrorDescription() - Dim X() As Long - On Error Resume Next - ReDim X(0 To -1) - Assert.Strict.AreEqual "Subscript out of range", Err.Description - End Sub - -End Module -``` - -**Expected behavior** -Error 9, *Subscript out of range*, as VB6 raises, and as twinBASIC itself raises for a subscript outside a fixed-size array. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 1010 - -**Additional context** -Severity: low. An error is raised wherever VB6 raises one, so no failure goes unreported, but a handler that tests for 9 does not recognise it, and *Unspecified error* does not say what is wrong. It is the same symptom as "An index out of range on a fixed-size array member of a UDT raises -2147467259, *Unspecified error*, where VB6 raises error 9": a bounds failure that reaches the program as *Unspecified error*, here from the allocation rather than from a subscript. - -What does not reproduce it: bounds in order (`0 To 0`, `-3 To -1`), and a subscript outside a fixed-size array that is not in a UDT. - - - ---- - -nBASIC bugs to report +# twinBASIC bugs to report A queue, not a record. Each entry is a **product** bug --- something twinBASIC itself does wrong --- observed while working on this documentation. **Mark an entry once it has been @@ -6499,3 +6407,349 @@ Severity: medium. A `[RunAfterBuild]` procedure that signs or post-processes the Not tried: a run with F5 (only `[RunAfterBuild]` was observed), a variable of the module's own read with a qualified name (`Other.Limit2`), 64-bit, LLVM. + +--- + +## `ReDim` with an upper bound less than its lower bound raises -2147467259, *Unspecified error*, where VB6 raises error 9 + +**Describe the bug** +`ReDim X(0 To -1)`, `ReDim X(-1)` and `ReDim X(5 To 4, 1)` on a dynamic array (`Dim X() As Long`), all of which give a dimension an upper bound below its lower bound, raise error -2147467259 (`&H80004005`), *Unspecified error*, where VB6 raises error 9, *Subscript out of range*. A handler that tests for 9 does not recognise it. Observed in the compiler's test mode, 32-bit and 64-bit, and in a probe run after a build. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `redim-inverted-bounds-error.twinproj` (attached as `redim-inverted-bounds-error.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass (`ReDim X(0 To 0)`, `ReDim X(-3 To -1)`, and a subscript above a fixed-size array's range, which raises 9) and the four cases on an inverted bound fail with `Actual: -2147467259`; `Err.Description` is *Unspecified error*. + +The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`redim-inverted-bounds-error-vb6.zip`) prints: 9, *Subscript out of range*, for each of the three statements. +```vb +' Regression tests: ReDim with an upper bound less than its lower bound raises error 9, +' "Subscript out of range". Expected numbers are VB6's (the VB6 project beside this one +' prints them). tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: bounds in order, an empty-looking but valid range, and a subscript out of range. + [TestCase] + Public Sub ReDim_OneElement_Succeeds() + Dim X() As Long + ReDim X(0 To 0) + Assert.Strict.AreEqual 0&, UBound(X) + End Sub + + [TestCase] + Public Sub ReDim_NegativeBoundsInOrder_Succeeds() + Dim X() As Long + ReDim X(-3 To -1) + Assert.Strict.AreEqual -3&, LBound(X) + End Sub + + [TestCase] + Public Sub FixedArray_SubscriptAboveRange_Raises9() + Dim A(1 To 3) As Long + On Error Resume Next + A(4) = 1 + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_ZeroToMinusOne_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(0 To -1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_MinusOne_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(-1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_FiveToFour_SecondDimension_Raises9() + Dim X() As Long + On Error Resume Next + ReDim X(5 To 4, 1) + Assert.Strict.AreEqual 9&, Err.Number + End Sub + + [TestCase] + Public Sub ReDim_ErrorDescription() + Dim X() As Long + On Error Resume Next + ReDim X(0 To -1) + Assert.Strict.AreEqual "Subscript out of range", Err.Description + End Sub + +End Module +``` + +**Expected behavior** +Error 9, *Subscript out of range*, as VB6 raises, and as twinBASIC itself raises for a subscript outside a fixed-size array. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. An error is raised wherever VB6 raises one, so no failure goes unreported, but a handler that tests for 9 does not recognise it, and *Unspecified error* does not say what is wrong. It is the same symptom as "An index out of range on a fixed-size array member of a UDT raises -2147467259, *Unspecified error*, where VB6 raises error 9": a bounds failure that reaches the program as *Unspecified error*, here from the allocation rather than from a subscript. + +What does not reproduce it: bounds in order (`0 To 0`, `-3 To -1`), and a subscript outside a fixed-size array that is not in a UDT. + + + +--- + +## A *compare* of 2 (`vbDatabaseCompare`) raises no error in `InStr`, `StrComp` and `Filter`, where VB6 raises error 5 + +**Describe the bug** +`InStr(1, "abc", "a", vbDatabaseCompare)`, `StrComp("a", "A", vbDatabaseCompare)` and `Filter(F, "a", True, vbDatabaseCompare)` raise no error, where VB6 raises error 5, *Invalid procedure call or argument*. `vbDatabaseCompare` (2) is a setting for Microsoft Access only. twinBASIC runs a textual comparison for it: `InStr(1, "abc", "A", vbDatabaseCompare)` returns 1, as `InStr(1, "abc", "A", 1033)` does, so it seems to take 2 as a locale identifier, as it takes any other value above 1. Observed in the compiler's test mode, 32-bit and 64-bit. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `database-compare-no-error.twinproj` (attached as `database-compare-no-error.zip`). +2. Run its test cases, the module below. On BETA 1010 the five controls pass (`InStr` with 0 and with 1, `InStr` with 3, and `InStrB` and `InStrRev` with 2, none of which VB6 refuses) and the four cases on a compare of 2 in `InStr`, `StrComp` and `Filter` fail with `Actual: 0`; `Err.Description` is empty. + +The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`database-compare-no-error-vb6.zip`) prints: 5, *Invalid procedure call or argument*, for `InStr`, `StrComp` and `Filter` with a compare of 2, and no error for the controls, nor for `Replace` and `Split` with 2. +```vb +' Regression tests: InStr, StrComp and Filter with a compare of 2 (vbDatabaseCompare) raise error 5, +' "Invalid procedure call or argument", outside Microsoft Access. Expected results are +' VB6's (the VB6 project beside this one prints them). tbrun --tests runs each case and +' prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: the two compare settings InStr accepts, and the neighbours VB6 lets + ' through with a compare of 2 or 3. + [TestCase] + Public Sub InStr_BinaryCompare_Finds() + Assert.Strict.AreEqual CVar(1&), InStr(1, "abc", "a", vbBinaryCompare) + End Sub + + [TestCase] + Public Sub InStr_TextCompare_Finds() + Assert.Strict.AreEqual CVar(1&), InStr(1, "abc", "A", vbTextCompare) + End Sub + + [TestCase] + Public Sub InStr_CompareThree_NoError() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", 3) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + [TestCase] + Public Sub InStrB_DatabaseCompare_NoError() + On Error Resume Next + Dim R As Variant + R = InStrB(1, "abc", "a", vbDatabaseCompare) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + [TestCase] + Public Sub InStrRev_DatabaseCompare_NoError() + On Error Resume Next + Dim R As Variant + R = InStrRev("abc", "a", -1, vbDatabaseCompare) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + ' The defect, in InStr, StrComp and Filter. + [TestCase] + Public Sub StrComp_DatabaseCompare_Raises5() + On Error Resume Next + Dim R As Variant + R = StrComp("a", "A", vbDatabaseCompare) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Filter_DatabaseCompare_Raises5() + Dim F(0 To 1) As String + F(0) = "abc": F(1) = "x" + On Error Resume Next + Dim R As Variant + R = Filter(F, "a", True, vbDatabaseCompare) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStr_DatabaseCompare_Raises5() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", vbDatabaseCompare) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStr_DatabaseCompare_ErrorDescription() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", vbDatabaseCompare) + Assert.Strict.AreEqual "Invalid procedure call or argument", Err.Description + End Sub + +End Module +``` + +**Expected behavior** +Error 5, *Invalid procedure call or argument*, from `InStr`, `StrComp` and `Filter` given a compare of 2, as VB6 raises. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. Code brought over from Access VBA that passes `vbDatabaseCompare` gets a textual comparison with no error, where VB6 stops it with error 5. + +What does not reproduce it: a compare of 0 or 1; a compare of 3, which VB6 also accepts; and `InStrB`, `InStrRev`, `Replace` and `Split` with a compare of 2, which VB6 accepts too and twinBASIC accepts likewise. + + + +--- + +## `InStr` raises error 5 for an invalid locale ID when *string1* or *string2* is zero-length, where VB6 raises nothing + +**Describe the bug** +`InStr(1, "", "a", 99999)`, `InStr(1, "abc", "", 99999)` and `InStr(1, "", "", 99999)` raise error 5, *Invalid procedure call or argument*, because 99999 is not a valid locale identifier. VB6 raises no error and returns 0, 1 and 0: its `InStr` returns before it checks *compare* when either string is zero-length. The same holds for a *compare* of -2. twinBASIC's own `InStrB` and `StrComp` already return before the check, as VB6 does. Observed in the compiler's test mode, 32-bit and 64-bit. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `instr-invalid-lcid-empty-string.twinproj` (attached as `instr-invalid-lcid-empty-string.zip`). +2. Run its test cases, the module below. On BETA 1010 the seven controls pass (`InStr` with no zero-length string and with *start* past the end, `InStrRev` and `Replace`, all of which raise 5 in VB6 too, and `InStrB` and `StrComp`, which return as VB6 does) and the five `InStr` cases on a zero-length string fail with `Actual: 5`. + +The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`instr-invalid-lcid-empty-string-vb6.zip`) prints: no error from `InStr` with a zero-length *string1* or *string2*, whatever the *compare*, and the results 0, 1 and 0. +```vb +' Regression tests: InStr given a compare that is not a valid locale ID raises no error +' when string1 or string2 is zero-length, and returns its usual result. Expected results +' are VB6's (the VB6 project beside this one prints them). tbrun --tests runs each case and +' prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: VB6 raises 5 for the invalid locale ID whenever neither string is empty. + [TestCase] + Public Sub InStr_BadLcid_Raises5() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "a", 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStr_BadLcid_StartPastEnd_Raises5() + On Error Resume Next + Dim R As Variant + R = InStr(5, "abc", "a", 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub InStrRev_BadLcid_EmptyString1_Raises5() + On Error Resume Next + Dim R As Variant + R = InStrRev("", "a", -1, 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Replace_BadLcid_EmptyExpression_Raises5() + On Error Resume Next + Dim R As Variant + R = Replace("", "a", "x", 1, -1, 99999) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + ' Controls: InStrB and StrComp return before the check, as VB6's InStr does. + [TestCase] + Public Sub InStrB_BadLcid_EmptyString1_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStrB(1, "", "a", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStrB_BadLcid_EmptyString2_ReturnsStart() + On Error Resume Next + Dim R As Variant + R = InStrB(1, "abc", "", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(1&), R + End Sub + + [TestCase] + Public Sub StrComp_BadLcid_EmptyString1_ReturnsMinusOne() + On Error Resume Next + Dim R As Variant + R = StrComp("", "a", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(-1), R + End Sub + + ' The defect: a zero-length string1 or string2 returns before the compare is checked. + [TestCase] + Public Sub InStr_BadLcid_EmptyString1_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStr(1, "", "a", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStr_BadLcid_EmptyString2_ReturnsStart() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(1&), R + End Sub + + [TestCase] + Public Sub InStr_BadLcid_BothEmpty_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStr(1, "", "", 99999) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStr_MinusTwo_EmptyString1_ReturnsZero() + On Error Resume Next + Dim R As Variant + R = InStr(1, "", "a", -2) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(0&), R + End Sub + + [TestCase] + Public Sub InStr_MinusTwo_EmptyString2_ReturnsStart() + On Error Resume Next + Dim R As Variant + R = InStr(1, "abc", "", -2) + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.AreEqual CVar(1&), R + End Sub + +End Module +``` + +**Expected behavior** +No error, and the result `InStr` gives for a zero-length string with a valid *compare* (0 for a zero-length *string1*, *start* for a zero-length *string2*), as VB6 returns, and as twinBASIC's `InStrB` and `StrComp` return. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. Only a *compare* that VB6 would refuse for any other strings is affected, but a program that ran under VB6 stops with error 5 where a string is empty. + +What does not reproduce it: an invalid *compare* with neither string zero-length, or with *start* past the end of *string1*, which VB6 refuses too; `InStrRev` and `Replace`, which check *compare* first in VB6 as well; and `InStrB` and `StrComp`, which already behave as VB6 does. + + diff --git a/bugs/database-compare-no-error/database-compare-no-error.twinproj b/bugs/database-compare-no-error/database-compare-no-error.twinproj new file mode 100644 index 0000000000000000000000000000000000000000..40db1ac831bb43c66ccb74765c54564dfa3559c6 GIT binary patch literal 5155 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 Date: Wed, 21 Oct 2026 01:20:52 +0200 Subject: [PATCH 17/21] Errors batch 2: the rest of the Strings module, 22 more pages --- WIP.Errors.md | 40 +- docs/Reference/Default/VBA/Strings/Filter.md | 22 +- docs/Reference/Default/VBA/Strings/Format.md | 10 + .../Default/VBA/Strings/FormatCurrency.md | 10 + .../Default/VBA/Strings/FormatDateTime.md | 10 + .../Default/VBA/Strings/FormatNumber.md | 10 + .../Default/VBA/Strings/FormatPercent.md | 10 + docs/Reference/Default/VBA/Strings/Join.md | 14 + docs/Reference/Default/VBA/Strings/LCase.md | 9 + docs/Reference/Default/VBA/Strings/LTrim.md | 9 + .../Default/VBA/Strings/MonthName.md | 10 + docs/Reference/Default/VBA/Strings/RTrim.md | 9 + docs/Reference/Default/VBA/Strings/Replace.md | 16 +- docs/Reference/Default/VBA/Strings/Space.md | 10 + docs/Reference/Default/VBA/Strings/Split.md | 15 +- docs/Reference/Default/VBA/Strings/StrComp.md | 20 +- docs/Reference/Default/VBA/Strings/StrConv.md | 19 +- .../Default/VBA/Strings/StrReverse.md | 10 +- docs/Reference/Default/VBA/Strings/String.md | 17 +- docs/Reference/Default/VBA/Strings/Trim.md | 9 + docs/Reference/Default/VBA/Strings/UCase.md | 9 + .../Default/VBA/Strings/WeekdayName.md | 10 + docs/Reference/Default/VBA/Strings/index.md | 2 +- docs/Reference/Run-time-Errors.md | 6 +- errors/VBA.Strings.yml | 415 ++++++++++++++++++ 25 files changed, 699 insertions(+), 22 deletions(-) diff --git a/WIP.Errors.md b/WIP.Errors.md index 270260722..a52b1bce8 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -256,11 +256,11 @@ What the probes must respect (BETA 1010; `errors.mjs` already does all of it): ## Where things stand (2026-10-21) -Batches 0 and 1 are done, reviewed and on staging. **Batch 2 is under way, on staging**: Asc, -Chr, InStr, InStrRev, Left and Right have their data and sections (with Mid and Len, eight of -the module's pages), all 136 cases agreeing in twinBASIC 32-bit and VB6. The owner reviewed -the first six on 2026-10-21 and settled the style (*Batch 2: what the first six pages found*); -the rest of the module follows it. +Batches 0 and 1 are done, reviewed and on staging. **Batch 2 is done, on staging**: every page +of the Strings module has its data (339 cases, all agreeing in twinBASIC 32-bit and VB6), and +every one but Len and the index has a section. The owner reviewed the first six on 2026-10-21 +and settled the style (*Batch 2: what the first six pages found*). Left open: four rows still +`bug: pending` and error 5's *Listed by* cell (*Batch 2: the rest of the module*). **Batch 2, the VBA `Strings` pilot.** List the module's pages (`docs/Reference/Default/VBA/Strings/`), keep those that can raise, and for each: read the page @@ -300,6 +300,34 @@ The owner's answers (2026-10-21): 3. Content both runtimes contradict is the page's fault: commented out, not deleted, so the VBA text is kept for docs that will switch between VB6, VBA and twinBASIC. +## Batch 2: the rest of the module + +Found on BETA 1010, both runtimes agreeing unless said: + +- **`vbUseCompareOption` (-1) raises 5 in Filter, Replace, Split and StrComp too**; their rows + are `` comments, and the module's index no longer offers it. +- **`vbDatabaseCompare` (2)**: Filter and StrComp accept it as InStr does, where VB6 raises 5 + (`database-compare-no-error`, with InStr's); Replace and Split accept it in both. +- **`StrComp` checks no LCID when a string is Null or zero-length** (`StrComp("a", "", 99999)` + returns), in both runtimes; Replace and Split check it on a zero-length string, in both. +- `FormatCurrency` raises 5 for *numDigitsAfterDecimal* above 255; FormatNumber and + FormatPercent take 2147483647. `String` and `Space` raise 5 above 1,073,741,823 and 14 + (*Out of string space*, not listed) at it. +- `Filter(Null, "a")` and `Join(Null)` raise 13, listed under the Null rule. +- The String page's two "**Null** is returned" sentences are `` comments: + `String(Null, "a")` raises 94 in both, and `String(3, Null)` in twinBASIC only. + +**Four rows are `bug: pending`, not yet queued in BUGS-TO-REPORT.md**: + +- `String(3, Null)`: twinBASIC raises 94, VB6 returns **Null** (`String$` raises 94 in both). +- `StrConv("a", 4, 1041)` (vbWide, vbKatakana... with an East Asian *LCID* on a system whose + locale is not East Asian): twinBASIC raises 5, VB6 converts. The page said so already. +- `Join` and `Filter` of a `Long` array: twinBASIC accepts it, VB6 raises 5 (Join) and 13 + (Filter). **Ask the owner whether this is a defect or a twinBASIC extension** before queuing. + +**Error 5's *Listed by* cell** on the central page has 22 links after this batch. Decide with +the owner, as the note below says, whether the list moves to a section per number. + ## What batch 2 needs to know (from batch 1) - A row whose number is not in `messages.yml` fails the gate. VB's 88 numbers are all there; @@ -322,7 +350,7 @@ each can be run when there is budget. Status is kept here. |---|---|---|---| | 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | done; the owner reviewed the trial sections (Mid, ReDim; Len raises nothing) on 2026-10-20 | | 1 | The central Run-time errors page | 1 | done; the owner reviewed the page and settled on `Err.Description` (2026-10-20) | -| 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | under way: 8 pages; style settled by the owner (2026-10-21) | +| 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | done: 28 sections; four rows `bug: pending` (see *Batch 2: the rest of the module*) | | 3 | VBA `Conversion` | 28 | not started | | 4 | VBA `Math` (also win64) | 17 | not started | | 5 | VBA `DateTime` | 20 | not started | diff --git a/docs/Reference/Default/VBA/Strings/Filter.md b/docs/Reference/Default/VBA/Strings/Filter.md index e9d0003e5..98d7444a7 100644 --- a/docs/Reference/Default/VBA/Strings/Filter.md +++ b/docs/Reference/Default/VBA/Strings/Filter.md @@ -27,12 +27,32 @@ The *compare* argument can have the following values: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the [**Option Compare**](../../Core/Option) statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + The array returned by the **Filter** function contains only enough elements to contain the number of matched items. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [13 (`&HD`)](../../../Reference/Run-time-Errors#error-13), *Type mismatch* | *sourcearray* is **Null**, or has more than one dimension | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *match*, *include* or *compare* is **Null**, or an element of *sourcearray* is **Null** | + +> [!NOTE] BETA 1010 +> When *compare* is 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*; twinBASIC raises none. + +> [!NOTE] BETA 1010 +> When *sourcearray* is a **Long** array, VB6 raises error [13 (`&HD`)](../../../Reference/Run-time-Errors#error-13), *Type mismatch*; twinBASIC raises none. + + + ### See Also - [Join](Join), [Split](Split) functions diff --git a/docs/Reference/Default/VBA/Strings/Format.md b/docs/Reference/Default/VBA/Strings/Format.md index c3316281f..ca859a230 100644 --- a/docs/Reference/Default/VBA/Strings/Format.md +++ b/docs/Reference/Default/VBA/Strings/Format.md @@ -97,6 +97,16 @@ If the calendar is Gregorian, the meaning of *format* expression symbols is unch | *s* | 0-59 (Second of minute, with no leading zero) | | *ss* | 00-59 (Second of minute, with a leading zero) | + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *firstDayOfWeek* is less than 0 or greater than 7, or *firstWeekOfYear* is less than 0 or greater than 3 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *format*, *firstDayOfWeek* or *firstWeekOfYear* is **Null** | + + + ### Example This example shows various uses of the **Format** function to format values using both named formats and user-defined formats. For the date separator (`/`), time separator (`:`), and AM/PM literal, the actual formatted output displayed by the system depends on the locale settings on which the code is running. When times and dates are displayed in the development environment, the short time format and short date format of the code locale are used. When displayed by running code, the short time format and short date format of the system locale are used, which may differ from the code locale. For this example, English/U.S. is assumed. `MyTime` and `MyDate` are displayed in the development environment using the current system short time setting and short date setting. diff --git a/docs/Reference/Default/VBA/Strings/FormatCurrency.md b/docs/Reference/Default/VBA/Strings/FormatCurrency.md index f462c42f3..57a41f523 100644 --- a/docs/Reference/Default/VBA/Strings/FormatCurrency.md +++ b/docs/Reference/Default/VBA/Strings/FormatCurrency.md @@ -36,6 +36,16 @@ The *includeLeadingDigit*, *useParensForNegativeNumbers*, and *groupDigits* argu When one or more optional arguments are omitted, the values for omitted arguments are provided by the computer's regional settings. The position of the currency symbol relative to the currency value is determined by the system's regional settings. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1 or greater than 255
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | an argument other than *expression* is **Null** | + + + ### See Also - [Format](Format), [FormatNumber](FormatNumber), [FormatPercent](FormatPercent) functions diff --git a/docs/Reference/Default/VBA/Strings/FormatDateTime.md b/docs/Reference/Default/VBA/Strings/FormatDateTime.md index eed19a4d4..033da52a5 100644 --- a/docs/Reference/Default/VBA/Strings/FormatDateTime.md +++ b/docs/Reference/Default/VBA/Strings/FormatDateTime.md @@ -27,6 +27,16 @@ The *namedFormat* argument has the following settings: | **vbLongTime** | 3 | Display a time by using the time format specified in the system regional settings. | | **vbShortTime** | 4 | Display a time by using the 24-hour format (`hh:mm`). | + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *namedFormat* is less than 0 or greater than 4 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *namedFormat* is **Null** | + + + ### Example This example uses **FormatDateTime** to display a date value in several formats. diff --git a/docs/Reference/Default/VBA/Strings/FormatNumber.md b/docs/Reference/Default/VBA/Strings/FormatNumber.md index 47bd012b9..92844e91e 100644 --- a/docs/Reference/Default/VBA/Strings/FormatNumber.md +++ b/docs/Reference/Default/VBA/Strings/FormatNumber.md @@ -36,6 +36,16 @@ The *includeLeadingDigit*, *useParensForNegativeNumbers*, and *groupDigits* argu When one or more optional arguments are omitted, the values for omitted arguments are provided by the computer's regional settings. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | an argument other than *expression* is **Null** | + + + ### See Also - [Format](Format), [FormatCurrency](FormatCurrency), [FormatPercent](FormatPercent) functions diff --git a/docs/Reference/Default/VBA/Strings/FormatPercent.md b/docs/Reference/Default/VBA/Strings/FormatPercent.md index eec9109c3..c92444a0a 100644 --- a/docs/Reference/Default/VBA/Strings/FormatPercent.md +++ b/docs/Reference/Default/VBA/Strings/FormatPercent.md @@ -36,6 +36,16 @@ The *includeLeadingDigit*, *useParensForNegativeNumbers*, and *groupDigits* argu When one or more optional arguments are omitted, the values for omitted arguments are provided by the computer's regional settings. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | an argument other than *expression* is **Null** | + + + ### See Also - [Format](Format), [FormatCurrency](FormatCurrency), [FormatNumber](FormatNumber) functions diff --git a/docs/Reference/Default/VBA/Strings/Join.md b/docs/Reference/Default/VBA/Strings/Join.md index ef273f311..2b469a588 100644 --- a/docs/Reference/Default/VBA/Strings/Join.md +++ b/docs/Reference/Default/VBA/Strings/Join.md @@ -17,6 +17,20 @@ Syntax: **Join(** *sourcearray* [ **,** *delimiter* ] **)** *delimiter* : *optional* String character used to separate the substrings in the returned string. If omitted, the space character (`" "`) is used. If *delimiter* is a zero-length string (`""`), all items in the list are concatenated with no delimiters. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *sourcearray* has more than one dimension | +| [13 (`&HD`)](../../../Reference/Run-time-Errors#error-13), *Type mismatch* | *sourcearray* is **Null**, or an element of it is an array | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *delimiter* is **Null**, or an element of *sourcearray* is **Null** | + +> [!NOTE] BETA 1010 +> When *sourcearray* is a **Long** array, VB6 raises error [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*; twinBASIC raises none. + + + ### Example This example uses **Join** to concatenate an array of strings with a delimiter. diff --git a/docs/Reference/Default/VBA/Strings/LCase.md b/docs/Reference/Default/VBA/Strings/LCase.md index e2c09eb58..26d1c6d42 100644 --- a/docs/Reference/Default/VBA/Strings/LCase.md +++ b/docs/Reference/Default/VBA/Strings/LCase.md @@ -18,6 +18,15 @@ The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Vari Only uppercase letters are converted to lowercase; all lowercase letters and nonletter characters remain unchanged. + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **LCase$** is given a *string* that is **Null** | + + + ### Example This example uses the **LCase** function to return a lowercase version of a string. diff --git a/docs/Reference/Default/VBA/Strings/LTrim.md b/docs/Reference/Default/VBA/Strings/LTrim.md index 7ec27124f..8ef525b60 100644 --- a/docs/Reference/Default/VBA/Strings/LTrim.md +++ b/docs/Reference/Default/VBA/Strings/LTrim.md @@ -16,6 +16,15 @@ Syntax: **LTrim$(** *string* **)**, **LTrim(** *string* **)** The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Variant** (**String**). + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **LTrim$** is given a *string* that is **Null** | + + + ### Example This example uses the **LTrim** function to strip leading spaces from a string variable. diff --git a/docs/Reference/Default/VBA/Strings/MonthName.md b/docs/Reference/Default/VBA/Strings/MonthName.md index f4e4b0919..9183d47b9 100644 --- a/docs/Reference/Default/VBA/Strings/MonthName.md +++ b/docs/Reference/Default/VBA/Strings/MonthName.md @@ -17,6 +17,16 @@ Syntax: **MonthName(** *month* [ **,** *abbreviate* ] **)** *abbreviate* : *optional* **Boolean** value that indicates if the month name is to be abbreviated. If omitted, the default is **False**, which means that the month name is not abbreviated. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *month* is less than 1 or greater than 12 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *month* or *abbreviate* is **Null** | + + + ### Example This example uses **MonthName** to return the full and abbreviated name of a month. diff --git a/docs/Reference/Default/VBA/Strings/RTrim.md b/docs/Reference/Default/VBA/Strings/RTrim.md index a61ef03ef..4afcaa86b 100644 --- a/docs/Reference/Default/VBA/Strings/RTrim.md +++ b/docs/Reference/Default/VBA/Strings/RTrim.md @@ -16,6 +16,15 @@ Syntax: **RTrim$(** *string* **)**, **RTrim(** *string* **)** The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Variant** (**String**). + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **RTrim$** is given a *string* that is **Null** | + + + ### Example This example uses the **RTrim** function to strip trailing spaces from a string variable. diff --git a/docs/Reference/Default/VBA/Strings/Replace.md b/docs/Reference/Default/VBA/Strings/Replace.md index 51d995989..07c530229 100644 --- a/docs/Reference/Default/VBA/Strings/Replace.md +++ b/docs/Reference/Default/VBA/Strings/Replace.md @@ -33,16 +33,18 @@ The *compare* argument can have the following values: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the [**Option Compare**](../../Core/Option) statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + **Return values:** | If | **Replace** returns | |------------------------------------------|----------------------------------------------------------------------------------------------------| | *expression* is zero-length | Zero-length string (`""`) | -| *expression* is **Null** | An error. | | *find* is zero-length | Copy of *expression*. | | *replace* is zero-length | Copy of *expression* with all occurrences of *find* removed. | | *start* > **Len**(*expression*) | Zero-length string. String replacement begins at the position indicated by *start*. | @@ -50,6 +52,16 @@ The *compare* argument can have the following values: The return value of the **Replace** function is a string, with substitutions made, that begins at the position specified by *start* and concludes at the end of the *expression* string. It's not a copy of the original string from start to finish. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is less than 1, or *count* is less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | any argument is **Null** | + + + ### See Also - [InStr](InStr), [StrComp](StrComp) functions diff --git a/docs/Reference/Default/VBA/Strings/Space.md b/docs/Reference/Default/VBA/Strings/Space.md index 02a165a05..19d49cb40 100644 --- a/docs/Reference/Default/VBA/Strings/Space.md +++ b/docs/Reference/Default/VBA/Strings/Space.md @@ -18,6 +18,16 @@ The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Vari The **Space** function is useful for formatting output and clearing data in fixed-length strings. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *number* is negative, or greater than 1,073,741,823 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *number* is **Null** | + + + ### Example This example uses the **Space** function to return a string consisting of a specified number of spaces. diff --git a/docs/Reference/Default/VBA/Strings/Split.md b/docs/Reference/Default/VBA/Strings/Split.md index ae809c8ac..1c737bc97 100644 --- a/docs/Reference/Default/VBA/Strings/Split.md +++ b/docs/Reference/Default/VBA/Strings/Split.md @@ -27,10 +27,23 @@ The *compare* argument can have the following values: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the [**Option Compare**](../../Core/Option) statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *limit* is less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | any argument is **Null** | + + + ### Example This example shows how to use the **Split** function. diff --git a/docs/Reference/Default/VBA/Strings/StrComp.md b/docs/Reference/Default/VBA/Strings/StrComp.md index 90af8536a..56b8ad58a 100644 --- a/docs/Reference/Default/VBA/Strings/StrComp.md +++ b/docs/Reference/Default/VBA/Strings/StrComp.md @@ -18,16 +18,19 @@ Syntax: **StrComp(** *string1*, *string2* [ **,** *compare* ] **)** : *required* Any valid string expression. *compare* -: *optional* Specifies the type of string comparison. If the *compare* argument is **Null**, an error occurs. If *compare* is omitted, the [**Option Compare**](../../Core/Option) setting determines the type of comparison. +: *optional* Specifies the type of string comparison. If *compare* is omitted, the [**Option Compare**](../../Core/Option) setting determines the type of comparison. The *compare* argument settings are: | Constant | Value | Description | |------------------------|-------|------------------------------------------------------------------------------------------| -| **vbUseCompareOption** | -1 | Performs a comparison by using the setting of the **Option Compare** statement. | | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | + + **Return values:** | If | **StrComp** returns | @@ -37,6 +40,19 @@ The *compare* argument settings are: | *string1* is greater than *string2* | 1 | | *string1* or *string2* is **Null** | **Null** | + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *compare* is **Null** | + +> [!NOTE] BETA 1010 +> When *compare* is 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*; twinBASIC raises none. + + + ### Example This example uses the **StrComp** function to return the results of a string comparison. If the third argument is 1, a textual comparison is performed; if the third argument is 0 or omitted, a binary comparison is performed. diff --git a/docs/Reference/Default/VBA/Strings/StrConv.md b/docs/Reference/Default/VBA/Strings/StrConv.md index 7bfc724b8..dcc7494eb 100644 --- a/docs/Reference/Default/VBA/Strings/StrConv.md +++ b/docs/Reference/Default/VBA/Strings/StrConv.md @@ -18,7 +18,7 @@ Syntax: **StrConv(** *string*, *conversion* [ **,** *LCID* ] **)** : *required* **Integer**. The sum of values specifying the type of conversion to perform. *LCID* -: *optional* The LocaleID, if different than the system LocaleID. (The system LocaleID is the default.) An *LCID* that is not a valid locale identifier raises error 5. +: *optional* The LocaleID, if different than the system LocaleID. (The system LocaleID is the default.) The *conversion* argument settings are: @@ -37,7 +37,7 @@ The *conversion* argument settings are: | **vbFromUTF8** | 512 | Converts UTF-8 text held in a **Byte** array to a string. An invalid byte sequence becomes the replacement character U+FFFD. | > [!NOTE] -> These constants are specified by twinBASIC. As a result, they may be used anywhere in code in place of the actual values. Most can be combined, for example, **vbUpperCase + vbWide**, except when they are mutually exclusive, for example, **vbUnicode + vbFromUnicode**. The constants **vbWide**, **vbNarrow**, **vbKatakana**, and **vbHiragana** cause run-time errors when used in locales where they don't apply. +> These constants are specified by twinBASIC. As a result, they may be used anywhere in code in place of the actual values. Most can be combined, for example, **vbUpperCase + vbWide**, except when they are mutually exclusive, for example, **vbUnicode + vbFromUnicode**. The following are valid word separators for proper casing: **Null** (`Chr$(0)`), horizontal tab (`Chr$(9)`), linefeed (`Chr$(10)`), vertical tab (`Chr$(11)`), form feed (`Chr$(12)`), carriage return (`Chr$(13)`), space (SBCS) (`Chr$(32)`). The actual value for a space varies by country/region for DBCS. No other character, such as a hyphen or an apostrophe, starts a word: `"mc-donald"` becomes `"Mc-donald"`. Every word is capitalised, whatever the number of separators before it: `"a b"` (two spaces) becomes `"A B"`, and `"a" & vbCrLf & "b"` becomes `"A" & vbCrLf & "B"`. @@ -45,10 +45,23 @@ If *string* is **Null**, **StrConv** returns **Null**. The case conversions map each character by the Windows rules for the character itself, and do not apply the language-specific rules of *LCID*. With an *LCID* of 1055 (Turkish), `"i"` becomes `"I"`, not the dotted capital I. The case conversions also work on characters outside the system code page, such as Greek and Cyrillic letters on a system with a Western European code page. -**vbUnicode** and **vbFromUnicode** use the default code page of the system, whatever *LCID* is given. **vbWide**, **vbNarrow**, **vbKatakana** and **vbHiragana** depend on the system locale as well: on a system whose locale is not East Asian they raise error 5 even when *LCID* names a Japanese locale. Visual Basic 6 applies the code page and locale of *LCID* to all six. +**vbUnicode** and **vbFromUnicode** use the default code page of the system, whatever *LCID* is given. **vbWide**, **vbNarrow**, **vbKatakana** and **vbHiragana** depend on the system locale in the same way. Visual Basic 6 applies the code page and locale of *LCID* to all six. When converting from a **Byte** array in ANSI format to a string, use the **StrConv** function. When converting from such an array in Unicode format, use an assignment statement. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *conversion* is a value the table does not list, or combines **vbUnicode**, **vbFromUnicode**, **vbUTF8** or **vbFromUTF8** with another value
*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** on a system whose locale is not East Asian
*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** and *LCID* names an East Asian locale, on a system whose locale is not East Asian
*LCID* is not a valid locale identifier | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *conversion* or *LCID* is **Null** | + +> [!NOTE] BETA 1010 +> When *conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** and *LCID* names an East Asian locale, on a system whose locale is not East Asian, VB6 raises no error. + + + ### Example This example uses the **StrConv** function to convert a Unicode string to an ANSI string. diff --git a/docs/Reference/Default/VBA/Strings/StrReverse.md b/docs/Reference/Default/VBA/Strings/StrReverse.md index 15a2be0df..7968b9576 100644 --- a/docs/Reference/Default/VBA/Strings/StrReverse.md +++ b/docs/Reference/Default/VBA/Strings/StrReverse.md @@ -12,7 +12,15 @@ Returns a string in which the character order of a specified string is reversed. Syntax: **StrReverse(** *expression* **)** *expression* -: *required* The string whose characters are to be reversed. If *expression* is a zero-length string (`""`), a zero-length string is returned. If *expression* is **Null**, an error occurs. +: *required* The string whose characters are to be reversed. If *expression* is a zero-length string (`""`), a zero-length string is returned. + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *expression* is **Null** | + + ### Example diff --git a/docs/Reference/Default/VBA/Strings/String.md b/docs/Reference/Default/VBA/Strings/String.md index a2b9c164a..aee8819df 100644 --- a/docs/Reference/Default/VBA/Strings/String.md +++ b/docs/Reference/Default/VBA/Strings/String.md @@ -12,15 +12,28 @@ Returns a **String** containing a repeating character string of the length speci Syntax: **String$(** *number*, *character* **)**, **String(** *number*, *character* **)** *number* -: *required* **Long**. Length of the returned string. If *number* contains **Null**, **Null** is returned. +: *required* **Long**. Length of the returned string. *character* -: *required* **Variant**. Character code specifying the character or string expression whose first character is used to build the return string. If *character* contains **Null**, **Null** is returned. +: *required* **Variant**. Character code specifying the character or string expression whose first character is used to build the return string. The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Variant** (**String**). When *character* is a number greater than 255, **String** converts the number to a valid character code by using this formula: *character* **Mod** 256. + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *number* is negative, or greater than 1,073,741,823
*character* is a zero-length string | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *number* is **Null**, or **String$** is given a *character* that is **Null**
**String** is given a *character* that is **Null** | + +> [!NOTE] BETA 1010 +> When **String** is given a *character* that is **Null**, VB6 raises no error. + + + ### Example This example uses the **String** function to return repeating character strings of the length specified. diff --git a/docs/Reference/Default/VBA/Strings/Trim.md b/docs/Reference/Default/VBA/Strings/Trim.md index a45d39cf5..00d1e11ef 100644 --- a/docs/Reference/Default/VBA/Strings/Trim.md +++ b/docs/Reference/Default/VBA/Strings/Trim.md @@ -16,6 +16,15 @@ Syntax: **Trim$(** *string* **)**, **Trim(** *string* **)** The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Variant** (**String**). + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Trim$** is given a *string* that is **Null** | + + + ### Example This example uses the **Trim** function to strip both leading and trailing spaces from a string variable. diff --git a/docs/Reference/Default/VBA/Strings/UCase.md b/docs/Reference/Default/VBA/Strings/UCase.md index a8ed5334b..55d558ef4 100644 --- a/docs/Reference/Default/VBA/Strings/UCase.md +++ b/docs/Reference/Default/VBA/Strings/UCase.md @@ -18,6 +18,15 @@ The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Vari Only lowercase letters are converted to uppercase; all uppercase letters and nonletter characters remain unchanged. + +### Common Errors + +| Error | When | +|---|---| +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **UCase$** is given a *string* that is **Null** | + + + ### Example This example uses the **UCase** function to return an uppercase version of a string. diff --git a/docs/Reference/Default/VBA/Strings/WeekdayName.md b/docs/Reference/Default/VBA/Strings/WeekdayName.md index c185066ca..a6c0baad2 100644 --- a/docs/Reference/Default/VBA/Strings/WeekdayName.md +++ b/docs/Reference/Default/VBA/Strings/WeekdayName.md @@ -33,6 +33,16 @@ The *firstdayofweek* argument can have the following values: | **vbFriday** | 6 | Friday | | **vbSaturday** | 7 | Saturday | + +### Common Errors + +| Error | When | +|---|---| +| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *weekday* is less than 1 or greater than 7, or *firstdayofweek* is less than 0 or greater than 7 | +| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | any argument is **Null** | + + + ### Example This example uses **WeekdayName** to return the full and abbreviated name of a weekday. diff --git a/docs/Reference/Default/VBA/Strings/index.md b/docs/Reference/Default/VBA/Strings/index.md index bba5b594d..e8e6c956f 100644 --- a/docs/Reference/Default/VBA/Strings/index.md +++ b/docs/Reference/Default/VBA/Strings/index.md @@ -21,7 +21,7 @@ Debug.Print Chr(65) ' "A" ## Searching and comparing -[**StrComp**](StrComp) compares two strings and returns -1, 0, or 1 to report which is greater (or equal). [**InStr**](InStr) and [**InStrRev**](InStrRev) return the position of one string inside another, scanning forward from a chosen start position or backward from one. All three accept an optional *compare* argument controlling whether the comparison is case-sensitive (**vbBinaryCompare**), case-insensitive (**vbTextCompare**), or governed by the surrounding [**Option Compare**](../../Core/Option) setting (**vbUseCompareOption**). Note that **InStrRev** swaps the order of the haystack and needle arguments relative to **InStr**. +[**StrComp**](StrComp) compares two strings and returns -1, 0, or 1 to report which is greater (or equal). [**InStr**](InStr) and [**InStrRev**](InStrRev) return the position of one string inside another, scanning forward from a chosen start position or backward from one. All three accept an optional *compare* argument controlling whether the comparison is case-sensitive (**vbBinaryCompare**), or case-insensitive (**vbTextCompare**); when it is omitted, **StrComp** and **InStr** follow the surrounding [**Option Compare**](../../Core/Option) setting. Note that **InStrRev** swaps the order of the haystack and needle arguments relative to **InStr**. ```tb check_build Debug.Print InStr("Hello, world", "o") ' 5 (first match, forward) diff --git a/docs/Reference/Run-time-Errors.md b/docs/Reference/Run-time-Errors.md index 19fe34c4a..4568523bc 100644 --- a/docs/Reference/Run-time-Errors.md +++ b/docs/Reference/Run-time-Errors.md @@ -43,13 +43,13 @@ The messages are those of twinBASIC 32-bit and VB6, which agree for every number | Error | Message | Listed by | |---|---|---| | **3**{: #error-3 } (`&H3`) | *Return without GoSub* | --- | -| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Left](../tB/Modules/Strings/Left), [Mid](../tB/Modules/Strings/Mid), [Right](../tB/Modules/Strings/Right) | +| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [Filter](../tB/Modules/Strings/Filter), [Format](../tB/Modules/Strings/Format), [FormatCurrency](../tB/Modules/Strings/FormatCurrency), [FormatDateTime](../tB/Modules/Strings/FormatDateTime), [FormatNumber](../tB/Modules/Strings/FormatNumber), [FormatPercent](../tB/Modules/Strings/FormatPercent), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Join](../tB/Modules/Strings/Join), [Left](../tB/Modules/Strings/Left), [Mid](../tB/Modules/Strings/Mid), [MonthName](../tB/Modules/Strings/MonthName), [Replace](../tB/Modules/Strings/Replace), [Right](../tB/Modules/Strings/Right), [Space](../tB/Modules/Strings/Space), [Split](../tB/Modules/Strings/Split), [StrComp](../tB/Modules/Strings/StrComp), [StrConv](../tB/Modules/Strings/StrConv), [String](../tB/Modules/Strings/String), [WeekdayName](../tB/Modules/Strings/WeekdayName) | | **6**{: #error-6 } (`&H6`) | *Overflow* | [Chr](../tB/Modules/Strings/Chr) | | **7**{: #error-7 } (`&H7`) | *Out of memory* | --- | | **9**{: #error-9 } (`&H9`) | *Subscript out of range* | [ReDim](../tB/Core/ReDim) | | **10**{: #error-10 } (`&HA`) | *This array is fixed or temporarily locked* | --- | | **11**{: #error-11 } (`&HB`) | *Division by zero* | --- | -| **13**{: #error-13 } (`&HD`) | *Type mismatch* | [ReDim](../tB/Core/ReDim) | +| **13**{: #error-13 } (`&HD`) | *Type mismatch* | [Filter](../tB/Modules/Strings/Filter), [Join](../tB/Modules/Strings/Join), [ReDim](../tB/Core/ReDim) | | **14**{: #error-14 } (`&HE`) | *Out of string space* | --- | | **16**{: #error-16 } (`&H10`) | *Expression too complex* | --- | | **17**{: #error-17 } (`&H11`) | *Can't perform requested operation* | --- | @@ -81,7 +81,7 @@ The messages are those of twinBASIC 32-bit and VB6, which agree for every number | **91**{: #error-91 } (`&H5B`) | *Object variable or With block variable not set* | --- | | **92**{: #error-92 } (`&H5C`) | *For loop not initialized* | --- | | **93**{: #error-93 } (`&H5D`) | *Invalid pattern string* | --- | -| **94**{: #error-94 } (`&H5E`) | *Invalid use of Null* | [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Left](../tB/Modules/Strings/Left), [Mid](../tB/Modules/Strings/Mid), [Right](../tB/Modules/Strings/Right) | +| **94**{: #error-94 } (`&H5E`) | *Invalid use of Null* | [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [Filter](../tB/Modules/Strings/Filter), [Format](../tB/Modules/Strings/Format), [FormatCurrency](../tB/Modules/Strings/FormatCurrency), [FormatDateTime](../tB/Modules/Strings/FormatDateTime), [FormatNumber](../tB/Modules/Strings/FormatNumber), [FormatPercent](../tB/Modules/Strings/FormatPercent), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Join](../tB/Modules/Strings/Join), [LCase](../tB/Modules/Strings/LCase), [Left](../tB/Modules/Strings/Left), [LTrim](../tB/Modules/Strings/LTrim), [Mid](../tB/Modules/Strings/Mid), [MonthName](../tB/Modules/Strings/MonthName), [Replace](../tB/Modules/Strings/Replace), [Right](../tB/Modules/Strings/Right), [RTrim](../tB/Modules/Strings/RTrim), [Space](../tB/Modules/Strings/Space), [Split](../tB/Modules/Strings/Split), [StrComp](../tB/Modules/Strings/StrComp), [StrConv](../tB/Modules/Strings/StrConv), [String](../tB/Modules/Strings/String), [StrReverse](../tB/Modules/Strings/StrReverse), [Trim](../tB/Modules/Strings/Trim), [UCase](../tB/Modules/Strings/UCase), [WeekdayName](../tB/Modules/Strings/WeekdayName) | | **96**{: #error-96 } (`&H60`) | *Unable to sink events of object because the object is already firing events to the maximum number of event receivers that it supports* | --- | | **97**{: #error-97 } (`&H61`) | *Can not call friend function on object which is not an instance of defining class* | --- | | **98**{: #error-98 } (`&H62`) | *A property or method call cannot include a reference to a private object, either as an argument or as a return value* | --- | diff --git a/errors/VBA.Strings.yml b/errors/VBA.Strings.yml index 2881f156f..df7f2b084 100644 --- a/errors/VBA.Strings.yml +++ b/errors/VBA.Strings.yml @@ -229,3 +229,418 @@ pages: - Right("abc", 0) - Right("abc", 1073741823) - RightB("abc", 2147483647) + Reference/Default/VBA/Strings/Filter.md: + rows: + - error: 5 + when: "*compare* is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - Filter(Array("ab", "c"), "a", True, -1) + - Filter(Array("ab", "c"), "a", True, -2) + - Filter(Array("ab", "c"), "a", True, 99999) + - error: 0 + when: "*compare* is 2 (**vbDatabaseCompare**)" + vb6: 5 + bug: database-compare-no-error + cases: + - Filter(Array("ab", "c"), "a", True, 2) + - error: 0 + when: "*sourcearray* is a **Long** array" + vb6: 13 + bug: pending + cases: + - decl: Dim A(1) As Long + expr: Filter(A, "a") + - error: 13 + when: "*sourcearray* is **Null**, or has more than one dimension" + cases: + - Filter(Null, "a") + - decl: Dim A(1, 1) As String + expr: Filter(A, "a") + - error: 94 + when: "*match*, *include* or *compare* is **Null**, or an element of *sourcearray* is **Null**" + cases: + - Filter(Array("ab", "c"), Null) + - Filter(Array("ab", "c"), "a", Null) + - Filter(Array("ab", "c"), "a", True, Null) + - Filter(Array("ab", Null), "a") + quiet: + - Filter(Array("ab", 1), "a") + - Filter(Array(), "a") + - Filter(Array("ab"), "a", 2) + - Filter(Array("ab", "c"), "a", True, 1033) + - decl: Dim A() As String + expr: Filter(A, "a") + Reference/Default/VBA/Strings/Format.md: + rows: + - error: 5 + when: "*firstDayOfWeek* is less than 0 or greater than 7, or *firstWeekOfYear* is less than 0 or greater than 3" + cases: + - Format(Now, "ww", -1) + - Format(Now, "ww", 8) + - Format(Now, "ww", 1, -1) + - Format(Now, "ww", 1, 4) + - Format$(1, "0", 8) + - error: 94 + when: "*format*, *firstDayOfWeek* or *firstWeekOfYear* is **Null**" + cases: + - Format(1, Null) + - Format(Now, "ww", Null) + - Format(Now, "ww", 1, Null) + quiet: + - Format(Null) + - Format$(Null) + - Format("abc", "0.00") + - Format(1, "") + - Format(Now, "ww", 7, 3) + - Format(Now, "ww", 0, 0) + Reference/Default/VBA/Strings/FormatCurrency.md: + rows: + - error: 5 + when: "*numDigitsAfterDecimal* is less than -1 or greater than 255" + cases: + - FormatCurrency(1, -2) + - FormatCurrency(1, 256) + - FormatCurrency(0, 1000) + - error: 5 + when: "*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0" + cases: + - FormatCurrency(1, 2, -3) + - FormatCurrency(1, 2, 1) + - FormatCurrency(1, 2, -1, 5) + - FormatCurrency(1, 2, -1, -1, 5) + - error: 94 + when: "an argument other than *expression* is **Null**" + cases: + - FormatCurrency(1, Null) + - FormatCurrency(1, 2, Null) + - FormatCurrency(1, 2, -1, Null) + - FormatCurrency(1, 2, -1, -1, Null) + quiet: + - FormatCurrency(Null) + - FormatCurrency(1, 255) + - FormatCurrency(1, 2, -2, -2, -2) + - FormatCurrency(1, 2, 0, 0, 0) + Reference/Default/VBA/Strings/FormatDateTime.md: + rows: + - error: 5 + when: "*namedFormat* is less than 0 or greater than 4" + cases: + - FormatDateTime(Now, -1) + - FormatDateTime(Now, 5) + - error: 94 + when: "*namedFormat* is **Null**" + cases: + - FormatDateTime(Now, Null) + quiet: + - FormatDateTime(Null) + - FormatDateTime(Now, 0) + - FormatDateTime(Now, 4) + Reference/Default/VBA/Strings/FormatNumber.md: + rows: + - error: 5 + when: "*numDigitsAfterDecimal* is less than -1" + cases: + - FormatNumber(1, -2) + - error: 5 + when: "*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0" + cases: + - FormatNumber(1, 2, -3) + - FormatNumber(1, 2, 1) + - FormatNumber(1, 2, -1, 5) + - FormatNumber(1, 2, -1, -1, 5) + - error: 94 + when: "an argument other than *expression* is **Null**" + cases: + - FormatNumber(1, Null) + - FormatNumber(1, 2, Null) + - FormatNumber(1, 2, -1, Null) + - FormatNumber(1, 2, -1, -1, Null) + quiet: + - FormatNumber(Null) + - FormatNumber(1, 1000) + - FormatNumber(1, 2, -2, -2, -2) + - FormatNumber(1, 2, 0, 0, 0) + Reference/Default/VBA/Strings/FormatPercent.md: + rows: + - error: 5 + when: "*numDigitsAfterDecimal* is less than -1" + cases: + - FormatPercent(1, -2) + - error: 5 + when: "*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0" + cases: + - FormatPercent(1, 2, -3) + - FormatPercent(1, 2, 1) + - FormatPercent(1, 2, -1, 5) + - FormatPercent(1, 2, -1, -1, 5) + - error: 94 + when: "an argument other than *expression* is **Null**" + cases: + - FormatPercent(1, Null) + - FormatPercent(1, 2, Null) + - FormatPercent(1, 2, -1, Null) + - FormatPercent(1, 2, -1, -1, Null) + quiet: + - FormatPercent(Null) + - FormatPercent(1, 100) + - FormatPercent(1, 2, -2, -2, -2) + Reference/Default/VBA/Strings/Join.md: + rows: + - error: 0 + when: "*sourcearray* is a **Long** array" + vb6: 5 + bug: pending + cases: + - decl: Dim A(1) As Long + expr: Join(A) + - error: 5 + when: "*sourcearray* has more than one dimension" + cases: + - decl: Dim A(1, 1) As String + expr: Join(A) + - error: 13 + when: "*sourcearray* is **Null**, or an element of it is an array" + cases: + - Join(Null) + - Join(Array(Array(1))) + - error: 94 + when: "*delimiter* is **Null**, or an element of *sourcearray* is **Null**" + cases: + - Join(Array("a", "b"), Null) + - Join(Array("a", Null)) + quiet: + - Join(Array(1, 2)) + - Join(Array()) + - decl: Dim A() As String + expr: Join(A) + Reference/Default/VBA/Strings/LCase.md: + rows: + - error: 94 + when: "**LCase$** is given a *string* that is **Null**" + cases: [LCase$(Null)] + quiet: [LCase(Null), LCase(1)] + Reference/Default/VBA/Strings/LTrim.md: + rows: + - error: 94 + when: "**LTrim$** is given a *string* that is **Null**" + cases: [LTrim$(Null)] + quiet: [LTrim(Null)] + Reference/Default/VBA/Strings/MonthName.md: + rows: + - error: 5 + when: "*month* is less than 1 or greater than 12" + cases: [MonthName(0), MonthName(13), MonthName(-1)] + - error: 94 + when: "*month* or *abbreviate* is **Null**" + cases: + - MonthName(Null) + - MonthName(1, Null) + quiet: + - MonthName(12, True) + - MonthName(1, 2) + Reference/Default/VBA/Strings/Replace.md: + rows: + - error: 5 + when: "*start* is less than 1, or *count* is less than -1" + cases: + - Replace("abc", "a", "b", 0) + - Replace("abc", "a", "b", -1) + - Replace("abc", "a", "b", 1, -2) + - error: 5 + when: "*compare* is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - Replace("abc", "a", "b", 1, -1, -1) + - Replace("abc", "a", "b", 1, -1, -2) + - Replace("abc", "a", "b", 1, -1, 99999) + - Replace("", "a", "b", 1, -1, 99999) + - error: 94 + when: "any argument is **Null**" + cases: + - Replace(Null, "a", "b") + - Replace("abc", Null, "b") + - Replace("abc", "a", Null) + - Replace("abc", "a", "b", Null) + - Replace("abc", "a", "b", 1, Null) + - Replace("abc", "a", "b", 1, -1, Null) + quiet: + - Replace("abc", "a", "b", 5) + - Replace("abc", "a", "b", 1073741825) + - Replace("abc", "a", "b", 1, 0) + - Replace("abc", "a", "b", 1, -1, 2) + - Replace("abc", "a", "b", 1, -1, 1033) + Reference/Default/VBA/Strings/RTrim.md: + rows: + - error: 94 + when: "**RTrim$** is given a *string* that is **Null**" + cases: [RTrim$(Null)] + quiet: [RTrim(Null)] + Reference/Default/VBA/Strings/Space.md: + rows: + - error: 5 + when: "*number* is negative, or greater than 1,073,741,823" + cases: [Space(-1), Space$(-1), Space(1073741824), Space(2147483647)] + - error: 94 + when: "*number* is **Null**" + cases: [Space(Null), Space$(Null)] + quiet: [Space(0)] + Reference/Default/VBA/Strings/Split.md: + rows: + - error: 5 + when: "*limit* is less than -1" + cases: + - Split("a b", " ", -2) + - error: 5 + when: "*compare* is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - Split("a b", " ", -1, -1) + - Split("a b", " ", -1, -2) + - Split("a b", " ", -1, 99999) + - Split("", " ", -1, 99999) + - error: 94 + when: "any argument is **Null**" + cases: + - Split(Null) + - Split("a b", Null) + - Split("a b", " ", Null) + - Split("a b", " ", -1, Null) + quiet: + - Split("a b", " ", 0) + - Split("a b", " ", -1, 2) + - Split("a b", " ", -1, 1033) + Reference/Default/VBA/Strings/StrComp.md: + rows: + - error: 5 + when: "*compare* is less than 0, or greater than 1 and not a valid locale identifier" + cases: + - StrComp("a", "b", -1) + - StrComp("a", "b", -2) + - StrComp("a", "b", 99999) + - error: 0 + when: "*compare* is 2 (**vbDatabaseCompare**)" + vb6: 5 + bug: database-compare-no-error + cases: + - StrComp("a", "b", 2) + - error: 94 + when: "*compare* is **Null**" + cases: + - StrComp("a", "b", Null) + quiet: + - StrComp(Null, "b") + - StrComp(Null, "b", 99999) + - StrComp("", "", 99999) + - StrComp("a", "", 99999) + - StrComp("a", "b", 1033) + Reference/Default/VBA/Strings/StrConv.md: + rows: + - error: 5 + when: "*conversion* is a value the table does not list, or combines **vbUnicode**, **vbFromUnicode**, **vbUTF8** or **vbFromUTF8** with another value" + cases: + - StrConv("a", 64 + 128) + - StrConv("a", 1 + 64) + - StrConv("a", 128 + 2) + - StrConv("a", 1024) + - StrConv("a", -1) + - { expr: 'StrConv("a", 256 + 512)', only: tb } + - { expr: 'StrConv("a", 64 + 256)', only: tb } + - error: 5 + when: "*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** on a system whose locale is not East Asian" + cases: + - StrConv("a", 4) + - StrConv("a", 8) + - StrConv("a", 16) + - StrConv("a", 32) + - error: 5 + when: "*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** and *LCID* names an East Asian locale, on a system whose locale is not East Asian" + vb6: 0 + bug: pending + cases: + - StrConv("a", 4, 1041) + - StrConv("a", 16, 1041) + - error: 5 + when: "*LCID* is not a valid locale identifier" + cases: + - StrConv("a", 1, 99999) + - StrConv("a", 1, -1) + - error: 94 + when: "*conversion* or *LCID* is **Null**" + cases: + - StrConv("a", Null) + - StrConv("a", 1, Null) + quiet: + - StrConv(Null, 1) + - StrConv("a", 0) + - StrConv("a", 1 + 2) + - StrConv("a", 3) + - StrConv("a", 64) + - StrConv("a", 128) + - StrConv("a", 1, 1033) + - { expr: 'StrConv("a", 256)', only: tb } + - { expr: 'StrConv("a", 512)', only: tb } + Reference/Default/VBA/Strings/StrReverse.md: + rows: + - error: 94 + when: "*expression* is **Null**" + cases: [StrReverse(Null)] + quiet: [StrReverse(""), StrReverse(1)] + Reference/Default/VBA/Strings/String.md: + rows: + - error: 5 + when: "*number* is negative, or greater than 1,073,741,823" + cases: + - String(-1, "a") + - String$(-1, "a") + - String(1073741824, "a") + - String(2147483647, "a") + - error: 5 + when: "*character* is a zero-length string" + cases: + - String(3, "") + - error: 94 + when: "*number* is **Null**, or **String$** is given a *character* that is **Null**" + cases: + - String(Null, "a") + - String$(Null, "a") + - String$(3, Null) + - error: 94 + when: "**String** is given a *character* that is **Null**" + vb6: 0 + bug: pending + cases: + - String(3, Null) + quiet: + - String(0, "a") + - String(3, "abc") + - String(3, 65) + - String(3, 256) + - String(3, -1) + Reference/Default/VBA/Strings/Trim.md: + rows: + - error: 94 + when: "**Trim$** is given a *string* that is **Null**" + cases: [Trim$(Null)] + quiet: [Trim(Null)] + Reference/Default/VBA/Strings/UCase.md: + rows: + - error: 94 + when: "**UCase$** is given a *string* that is **Null**" + cases: [UCase$(Null)] + quiet: [UCase(Null)] + Reference/Default/VBA/Strings/WeekdayName.md: + rows: + - error: 5 + when: "*weekday* is less than 1 or greater than 7, or *firstdayofweek* is less than 0 or greater than 7" + cases: + - WeekdayName(0) + - WeekdayName(8) + - WeekdayName(1, False, -1) + - WeekdayName(1, False, 8) + - error: 94 + when: "any argument is **Null**" + cases: + - WeekdayName(Null) + - WeekdayName(1, Null) + - WeekdayName(1, False, Null) + quiet: + - WeekdayName(7, False, 0) + - WeekdayName(1, 2) From 7f5095480b11414afc70f64ea26bff0a0c557d32 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 21 Oct 2026 01:27:22 +0200 Subject: [PATCH 18/21] Errors: pages per number in a section of their own; Join and Filter of typed arrays an extension --- WIP.Errors.md | 24 +++++++++------- docs/Reference/Default/VBA/Strings/Filter.md | 6 ++-- docs/Reference/Default/VBA/Strings/Join.md | 6 ++-- docs/Reference/Run-time-Errors.md | 30 ++++++++++++++------ errors/VBA.Strings.yml | 26 ++++++++--------- scripts/check_errors.mjs | 13 +++++++-- scripts/lib/errors-data.mjs | 18 ++++++++++-- 7 files changed, 80 insertions(+), 43 deletions(-) diff --git a/WIP.Errors.md b/WIP.Errors.md index a52b1bce8..a663dfc7d 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -260,7 +260,7 @@ Batches 0 and 1 are done, reviewed and on staging. **Batch 2 is done, on staging of the Strings module has its data (339 cases, all agreeing in twinBASIC 32-bit and VB6), and every one but Len and the index has a section. The owner reviewed the first six on 2026-10-21 and settled the style (*Batch 2: what the first six pages found*). Left open: four rows still -`bug: pending` and error 5's *Listed by* cell (*Batch 2: the rest of the module*). +`bug: pending` until the two last bugs are queued (*Batch 2: the rest of the module*). **Batch 2, the VBA `Strings` pilot.** List the module's pages (`docs/Reference/Default/VBA/Strings/`), keep those that can raise, and for each: read the page @@ -317,16 +317,18 @@ Found on BETA 1010, both runtimes agreeing unless said: - The String page's two "**Null** is returned" sentences are `` comments: `String(Null, "a")` raises 94 in both, and `String(3, Null)` in twinBASIC only. -**Four rows are `bug: pending`, not yet queued in BUGS-TO-REPORT.md**: - -- `String(3, Null)`: twinBASIC raises 94, VB6 returns **Null** (`String$` raises 94 in both). -- `StrConv("a", 4, 1041)` (vbWide, vbKatakana... with an East Asian *LCID* on a system whose - locale is not East Asian): twinBASIC raises 5, VB6 converts. The page said so already. -- `Join` and `Filter` of a `Long` array: twinBASIC accepts it, VB6 raises 5 (Join) and 13 - (Filter). **Ask the owner whether this is a defect or a twinBASIC extension** before queuing. +The owner's answers (2026-10-21): -**Error 5's *Listed by* cell** on the central page has 22 links after this batch. Decide with -the owner, as the note below says, whether the list moves to a section per number. +- `String(3, Null)` (twinBASIC raises 94, VB6 returns **Null**) and `StrConv("a", 4, 1041)` + (vbWide, vbKatakana... with an East Asian *LCID* on a system whose locale is not East + Asian: twinBASIC raises 5, VB6 converts) **are bugs**, queued by a separate session. +- `Join` and `Filter` taking a `Long` or `Double` array (VB6 raises 5 and 13 for a `Long` one) + **is a twinBASIC extension**, not a bug: a NOTE in each page's prose, twinBASIC-only quiet + cases in the data, no `vb6:` row. +- **The pages that list a number moved out of the table** into a section after it, *Pages that + list each error*: a line per number that some page lists, its pages comma-separated in + alphabetical order, anchored `#listed-`. The table's *Listed by* cell is a link to that + line (*22 pages*), or `---`. ## What batch 2 needs to know (from batch 1) @@ -350,7 +352,7 @@ each can be run when there is budget. Status is kept here. |---|---|---|---| | 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | done; the owner reviewed the trial sections (Mid, ReDim; Len raises nothing) on 2026-10-20 | | 1 | The central Run-time errors page | 1 | done; the owner reviewed the page and settled on `Err.Description` (2026-10-20) | -| 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | done: 28 sections; four rows `bug: pending` (see *Batch 2: the rest of the module*) | +| 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | done: 28 sections, reviewed by the owner (2026-10-21) | | 3 | VBA `Conversion` | 28 | not started | | 4 | VBA `Math` (also win64) | 17 | not started | | 5 | VBA `DateTime` | 20 | not started | diff --git a/docs/Reference/Default/VBA/Strings/Filter.md b/docs/Reference/Default/VBA/Strings/Filter.md index 98d7444a7..726fe27ae 100644 --- a/docs/Reference/Default/VBA/Strings/Filter.md +++ b/docs/Reference/Default/VBA/Strings/Filter.md @@ -14,6 +14,9 @@ Syntax: **Filter(** *sourcearray*, *match* [ **,** *include* [ **,** *compare* ] *sourcearray* : *required* One-dimensional array of strings to be searched. +> [!NOTE] +> twinBASIC also takes a *sourcearray* whose elements are of a type other than **String** or **Variant**, such as **Long** or **Double**. For a **Long** array VB6 raises error 13, *Type mismatch*. + *match* : *required* String to search for. @@ -48,9 +51,6 @@ The array returned by the **Filter** function contains only enough elements to c > [!NOTE] BETA 1010 > When *compare* is 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*; twinBASIC raises none. -> [!NOTE] BETA 1010 -> When *sourcearray* is a **Long** array, VB6 raises error [13 (`&HD`)](../../../Reference/Run-time-Errors#error-13), *Type mismatch*; twinBASIC raises none. - ### See Also diff --git a/docs/Reference/Default/VBA/Strings/Join.md b/docs/Reference/Default/VBA/Strings/Join.md index 2b469a588..8fed1bb4d 100644 --- a/docs/Reference/Default/VBA/Strings/Join.md +++ b/docs/Reference/Default/VBA/Strings/Join.md @@ -17,6 +17,9 @@ Syntax: **Join(** *sourcearray* [ **,** *delimiter* ] **)** *delimiter* : *optional* String character used to separate the substrings in the returned string. If omitted, the space character (`" "`) is used. If *delimiter* is a zero-length string (`""`), all items in the list are concatenated with no delimiters. +> [!NOTE] +> twinBASIC also takes a *sourcearray* whose elements are of a type other than **String** or **Variant**, such as **Long** or **Double**. For a **Long** array VB6 raises error 5, *Invalid procedure call or argument*. + ### Common Errors @@ -26,9 +29,6 @@ Syntax: **Join(** *sourcearray* [ **,** *delimiter* ] **)** | [13 (`&HD`)](../../../Reference/Run-time-Errors#error-13), *Type mismatch* | *sourcearray* is **Null**, or an element of it is an array | | [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *delimiter* is **Null**, or an element of *sourcearray* is **Null** | -> [!NOTE] BETA 1010 -> When *sourcearray* is a **Long** array, VB6 raises error [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*; twinBASIC raises none. - ### Example diff --git a/docs/Reference/Run-time-Errors.md b/docs/Reference/Run-time-Errors.md index 4568523bc..76a6b2ce6 100644 --- a/docs/Reference/Run-time-Errors.md +++ b/docs/Reference/Run-time-Errors.md @@ -36,20 +36,20 @@ The message is the one that **Err.Description** gives for the error once it is r ## Error numbers -The messages are those of twinBASIC 32-bit and VB6, which agree for every number from 1 to 65535. *Listed by* names the pages whose Common Errors section lists the number. +The messages are those of twinBASIC 32-bit and VB6, which agree for every number from 1 to 65535. *Listed by* links to the number's line under [Pages that list each error](#pages-that-list-each-error), which names the pages whose Common Errors section lists it, in alphabetical order. - + | Error | Message | Listed by | |---|---|---| | **3**{: #error-3 } (`&H3`) | *Return without GoSub* | --- | -| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [Filter](../tB/Modules/Strings/Filter), [Format](../tB/Modules/Strings/Format), [FormatCurrency](../tB/Modules/Strings/FormatCurrency), [FormatDateTime](../tB/Modules/Strings/FormatDateTime), [FormatNumber](../tB/Modules/Strings/FormatNumber), [FormatPercent](../tB/Modules/Strings/FormatPercent), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Join](../tB/Modules/Strings/Join), [Left](../tB/Modules/Strings/Left), [Mid](../tB/Modules/Strings/Mid), [MonthName](../tB/Modules/Strings/MonthName), [Replace](../tB/Modules/Strings/Replace), [Right](../tB/Modules/Strings/Right), [Space](../tB/Modules/Strings/Space), [Split](../tB/Modules/Strings/Split), [StrComp](../tB/Modules/Strings/StrComp), [StrConv](../tB/Modules/Strings/StrConv), [String](../tB/Modules/Strings/String), [WeekdayName](../tB/Modules/Strings/WeekdayName) | -| **6**{: #error-6 } (`&H6`) | *Overflow* | [Chr](../tB/Modules/Strings/Chr) | +| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [22 pages](#listed-5) | +| **6**{: #error-6 } (`&H6`) | *Overflow* | [1 page](#listed-6) | | **7**{: #error-7 } (`&H7`) | *Out of memory* | --- | -| **9**{: #error-9 } (`&H9`) | *Subscript out of range* | [ReDim](../tB/Core/ReDim) | +| **9**{: #error-9 } (`&H9`) | *Subscript out of range* | [1 page](#listed-9) | | **10**{: #error-10 } (`&HA`) | *This array is fixed or temporarily locked* | --- | | **11**{: #error-11 } (`&HB`) | *Division by zero* | --- | -| **13**{: #error-13 } (`&HD`) | *Type mismatch* | [Filter](../tB/Modules/Strings/Filter), [Join](../tB/Modules/Strings/Join), [ReDim](../tB/Core/ReDim) | +| **13**{: #error-13 } (`&HD`) | *Type mismatch* | [3 pages](#listed-13) | | **14**{: #error-14 } (`&HE`) | *Out of string space* | --- | | **16**{: #error-16 } (`&H10`) | *Expression too complex* | --- | | **17**{: #error-17 } (`&H11`) | *Can't perform requested operation* | --- | @@ -81,7 +81,7 @@ The messages are those of twinBASIC 32-bit and VB6, which agree for every number | **91**{: #error-91 } (`&H5B`) | *Object variable or With block variable not set* | --- | | **92**{: #error-92 } (`&H5C`) | *For loop not initialized* | --- | | **93**{: #error-93 } (`&H5D`) | *Invalid pattern string* | --- | -| **94**{: #error-94 } (`&H5E`) | *Invalid use of Null* | [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [Filter](../tB/Modules/Strings/Filter), [Format](../tB/Modules/Strings/Format), [FormatCurrency](../tB/Modules/Strings/FormatCurrency), [FormatDateTime](../tB/Modules/Strings/FormatDateTime), [FormatNumber](../tB/Modules/Strings/FormatNumber), [FormatPercent](../tB/Modules/Strings/FormatPercent), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Join](../tB/Modules/Strings/Join), [LCase](../tB/Modules/Strings/LCase), [Left](../tB/Modules/Strings/Left), [LTrim](../tB/Modules/Strings/LTrim), [Mid](../tB/Modules/Strings/Mid), [MonthName](../tB/Modules/Strings/MonthName), [Replace](../tB/Modules/Strings/Replace), [Right](../tB/Modules/Strings/Right), [RTrim](../tB/Modules/Strings/RTrim), [Space](../tB/Modules/Strings/Space), [Split](../tB/Modules/Strings/Split), [StrComp](../tB/Modules/Strings/StrComp), [StrConv](../tB/Modules/Strings/StrConv), [String](../tB/Modules/Strings/String), [StrReverse](../tB/Modules/Strings/StrReverse), [Trim](../tB/Modules/Strings/Trim), [UCase](../tB/Modules/Strings/UCase), [WeekdayName](../tB/Modules/Strings/WeekdayName) | +| **94**{: #error-94 } (`&H5E`) | *Invalid use of Null* | [28 pages](#listed-94) | | **96**{: #error-96 } (`&H60`) | *Unable to sink events of object because the object is already firing events to the maximum number of event receivers that it supports* | --- | | **97**{: #error-97 } (`&H61`) | *Can not call friend function on object which is not an instance of defining class* | --- | | **98**{: #error-98 } (`&H62`) | *A property or method call cannot include a reference to a private object, either as an argument or as a return value* | --- | @@ -129,7 +129,21 @@ The messages are those of twinBASIC 32-bit and VB6, which agree for every number | **735**{: #error-735 } (`&H2DF`) | *Can't save file to TEMP* | --- | | **744**{: #error-744 } (`&H2E8`) | *Search text not found* | --- | | **746**{: #error-746 } (`&H2EA`) | *Replacements too long* | --- | -| **-2147467259**{: #error--2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | [ReDim](../tB/Core/ReDim) | +| **-2147467259**{: #error--2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | [1 page](#listed--2147467259) | + +## Pages that list each error + +**5**{: #listed-5 } (`&H5`): [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [Filter](../tB/Modules/Strings/Filter), [Format](../tB/Modules/Strings/Format), [FormatCurrency](../tB/Modules/Strings/FormatCurrency), [FormatDateTime](../tB/Modules/Strings/FormatDateTime), [FormatNumber](../tB/Modules/Strings/FormatNumber), [FormatPercent](../tB/Modules/Strings/FormatPercent), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Join](../tB/Modules/Strings/Join), [Left](../tB/Modules/Strings/Left), [Mid](../tB/Modules/Strings/Mid), [MonthName](../tB/Modules/Strings/MonthName), [Replace](../tB/Modules/Strings/Replace), [Right](../tB/Modules/Strings/Right), [Space](../tB/Modules/Strings/Space), [Split](../tB/Modules/Strings/Split), [StrComp](../tB/Modules/Strings/StrComp), [StrConv](../tB/Modules/Strings/StrConv), [String](../tB/Modules/Strings/String), [WeekdayName](../tB/Modules/Strings/WeekdayName) + +**6**{: #listed-6 } (`&H6`): [Chr](../tB/Modules/Strings/Chr) + +**9**{: #listed-9 } (`&H9`): [ReDim](../tB/Core/ReDim) + +**13**{: #listed-13 } (`&HD`): [Filter](../tB/Modules/Strings/Filter), [Join](../tB/Modules/Strings/Join), [ReDim](../tB/Core/ReDim) + +**94**{: #listed-94 } (`&H5E`): [Asc](../tB/Modules/Strings/Asc), [Chr](../tB/Modules/Strings/Chr), [Filter](../tB/Modules/Strings/Filter), [Format](../tB/Modules/Strings/Format), [FormatCurrency](../tB/Modules/Strings/FormatCurrency), [FormatDateTime](../tB/Modules/Strings/FormatDateTime), [FormatNumber](../tB/Modules/Strings/FormatNumber), [FormatPercent](../tB/Modules/Strings/FormatPercent), [InStr](../tB/Modules/Strings/InStr), [InStrRev](../tB/Modules/Strings/InStrRev), [Join](../tB/Modules/Strings/Join), [LCase](../tB/Modules/Strings/LCase), [Left](../tB/Modules/Strings/Left), [LTrim](../tB/Modules/Strings/LTrim), [Mid](../tB/Modules/Strings/Mid), [MonthName](../tB/Modules/Strings/MonthName), [Replace](../tB/Modules/Strings/Replace), [Right](../tB/Modules/Strings/Right), [RTrim](../tB/Modules/Strings/RTrim), [Space](../tB/Modules/Strings/Space), [Split](../tB/Modules/Strings/Split), [StrComp](../tB/Modules/Strings/StrComp), [StrConv](../tB/Modules/Strings/StrConv), [String](../tB/Modules/Strings/String), [StrReverse](../tB/Modules/Strings/StrReverse), [Trim](../tB/Modules/Strings/Trim), [UCase](../tB/Modules/Strings/UCase), [WeekdayName](../tB/Modules/Strings/WeekdayName) + +**-2147467259**{: #listed--2147467259 } (`&H80004005`): [ReDim](../tB/Core/ReDim) diff --git a/errors/VBA.Strings.yml b/errors/VBA.Strings.yml index df7f2b084..61f865199 100644 --- a/errors/VBA.Strings.yml +++ b/errors/VBA.Strings.yml @@ -243,13 +243,6 @@ pages: bug: database-compare-no-error cases: - Filter(Array("ab", "c"), "a", True, 2) - - error: 0 - when: "*sourcearray* is a **Long** array" - vb6: 13 - bug: pending - cases: - - decl: Dim A(1) As Long - expr: Filter(A, "a") - error: 13 when: "*sourcearray* is **Null**, or has more than one dimension" cases: @@ -266,6 +259,12 @@ pages: quiet: - Filter(Array("ab", 1), "a") - Filter(Array(), "a") + - decl: Dim A(1) As Long + expr: Filter(A, "a") + only: tb + - decl: Dim A(1) As Double + expr: Filter(A, "a") + only: tb - Filter(Array("ab"), "a", 2) - Filter(Array("ab", "c"), "a", True, 1033) - decl: Dim A() As String @@ -386,13 +385,6 @@ pages: - FormatPercent(1, 2, -2, -2, -2) Reference/Default/VBA/Strings/Join.md: rows: - - error: 0 - when: "*sourcearray* is a **Long** array" - vb6: 5 - bug: pending - cases: - - decl: Dim A(1) As Long - expr: Join(A) - error: 5 when: "*sourcearray* has more than one dimension" cases: @@ -411,6 +403,12 @@ pages: quiet: - Join(Array(1, 2)) - Join(Array()) + - decl: Dim A(1) As Long + expr: Join(A) + only: tb + - decl: Dim A(1) As Double + expr: Join(A) + only: tb - decl: Dim A() As String expr: Join(A) Reference/Default/VBA/Strings/LCase.md: diff --git a/scripts/check_errors.mjs b/scripts/check_errors.mjs index 04a69a157..dcd61a2f4 100644 --- a/scripts/check_errors.mjs +++ b/scripts/check_errors.mjs @@ -240,9 +240,18 @@ check( const index = renderIndex([sample], messages, new Map([["A/B.md", { title: "B", permalink: "/tB/B" }]])); check( - "the index has a row per number, in order, with its anchor and the pages that name it", + "the index has a row per number, in order, with its anchor and a link to its pages", index.slice(4, 7).join("\n") === - "| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [B](../tB/B) |\n| **9**{: #error-9 } (`&H9`) | *Subscript out of range* | [B](../tB/B) |\n| **-2147467259**{: #error--2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | --- |", + "| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [1 page](#listed-5) |\n| **9**{: #error-9 } (`&H9`) | *Subscript out of range* | [1 page](#listed-9) |\n| **-2147467259**{: #error--2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | --- |", + index.join("\n"), +); +check( + "the section after the table lists each named number's pages, only those numbers", + index + .join("\n") + .endsWith( + "## Pages that list each error\n\n**5**{: #listed-5 } (`&H5`): [B](../tB/B)\n\n**9**{: #listed-9 } (`&H9`): [B](../tB/B)\n\n", + ), index.join("\n"), ); const central = "# T\n\n\nold\n\n\n## See Also\n"; diff --git a/scripts/lib/errors-data.mjs b/scripts/lib/errors-data.mjs index e1974219a..0633cf4b8 100644 --- a/scripts/lib/errors-data.mjs +++ b/scripts/lib/errors-data.mjs @@ -417,6 +417,9 @@ export const CENTRAL_PERMALINK = "/Reference/Run-time-Errors"; /** The anchor of number `n` on the central page: error-5, error--2147467259. */ export const errorAnchor = (n) => `error-${n}`; +/** The anchor of number `n`'s list of pages on the central page: listed-5, listed--2147467259. */ +export const listedAnchor = (n) => `listed-${n}`; + /** The link from the page at permalink `from` to the page at permalink `to`, as the site writes it: relative. */ export function relativeLink(from, to) { const base = from.endsWith("/") ? from : path.posix.dirname(from); @@ -528,11 +531,13 @@ export function renderIndex(files, messages, pages) { } } const out = [ - "", + "", "", "| Error | Message | Listed by |", "|---|---|---|", ]; + // The table links each number to its line in the section after it, which names the pages. + const listed = []; for (const n of [...messages.keys()].sort(byNumber)) { const m = messages.get(n); const hex = errorNumber(n).slice(String(n).length + 1); @@ -541,7 +546,16 @@ export function renderIndex(files, messages, pages) { .map((page) => pages.get(page) ?? { title: page, permalink: null }) .sort((a, b) => a.title.localeCompare(b.title, "en")) .map((p) => (p.permalink ? `[${p.title}](${relativeLink(CENTRAL_PERMALINK, p.permalink)})` : p.title)); - out.push(`| **${n}**{: #${errorAnchor(n)} } ${hex} | ${escapeCell(message)} | ${links.join(", ") || "---"} |`); + const count = links.length + ? `[${links.length} ${links.length === 1 ? "page" : "pages"}](#${listedAnchor(n)})` + : "---"; + out.push(`| **${n}**{: #${errorAnchor(n)} } ${hex} | ${escapeCell(message)} | ${count} |`); + if (links.length) listed.push(`**${n}**{: #${listedAnchor(n)} } ${hex}: ${links.join(", ")}`); + } + if (listed.length) { + out.push("", "## Pages that list each error", ""); + for (const line of listed) out.push(line, ""); + out.pop(); } out.push("", ""); return out; From d8f997042ccdd776c3d5bfaa5223d9a467144fff Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 21 Oct 2026 01:28:13 +0200 Subject: [PATCH 19/21] Queue two Strings bugs: String(n, Null) raises 94 where VB6 returns Null; StrConv's East Asian conversions refuse an East Asian LCID on a Western system --- BUGS-TO-REPORT.md | 183 ++++++++++++++++++ bugs/strconv-east-asian-lcid/repro.json | 17 ++ bugs/strconv-east-asian-lcid/src/Settings | 75 +++++++ .../src/Sources/RegressionTests.twin | 63 ++++++ .../src/Sources/Startup.twin | 6 + .../strconv-east-asian-lcid.twinproj | Bin 0 -> 4737 bytes bugs/strconv-east-asian-lcid/vb6/Module1.bas | 38 ++++ bugs/strconv-east-asian-lcid/vb6/Probe.vbp | 7 + bugs/string-null-character/repro.json | 15 ++ bugs/string-null-character/src/Settings | 75 +++++++ .../src/Sources/RegressionTests.twin | 60 ++++++ .../src/Sources/Startup.twin | 6 + .../string-null-character.twinproj | Bin 0 -> 4359 bytes bugs/string-null-character/vb6/Module1.bas | 23 +++ bugs/string-null-character/vb6/Probe.vbp | 7 + errors/VBA.Strings.yml | 4 +- 16 files changed, 577 insertions(+), 2 deletions(-) create mode 100644 bugs/strconv-east-asian-lcid/repro.json create mode 100644 bugs/strconv-east-asian-lcid/src/Settings create mode 100644 bugs/strconv-east-asian-lcid/src/Sources/RegressionTests.twin create mode 100644 bugs/strconv-east-asian-lcid/src/Sources/Startup.twin create mode 100644 bugs/strconv-east-asian-lcid/strconv-east-asian-lcid.twinproj create mode 100644 bugs/strconv-east-asian-lcid/vb6/Module1.bas create mode 100644 bugs/strconv-east-asian-lcid/vb6/Probe.vbp create mode 100644 bugs/string-null-character/repro.json create mode 100644 bugs/string-null-character/src/Settings create mode 100644 bugs/string-null-character/src/Sources/RegressionTests.twin create mode 100644 bugs/string-null-character/src/Sources/Startup.twin create mode 100644 bugs/string-null-character/string-null-character.twinproj create mode 100644 bugs/string-null-character/vb6/Module1.bas create mode 100644 bugs/string-null-character/vb6/Probe.vbp diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 494389228..608163ecf 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -6753,3 +6753,186 @@ Severity: low. Only a *compare* that VB6 would refuse for any other strings is a What does not reproduce it: an invalid *compare* with neither string zero-length, or with *start* past the end of *string1*, which VB6 refuses too; `InStrRev` and `Replace`, which check *compare* first in VB6 as well; and `InStrB` and `StrComp`, which already behave as VB6 does. + +--- + +## `String` with a *character* of `Null` raises error 94, where VB6 returns `Null` + +**Describe the bug** +`String(3, Null)`, the **Variant** form of `String`, raises error 94, *Invalid use of Null*. VB6 raises no error and returns **Null** (`VarType` 1), as it does for `String(0, Null)`. `String$(3, Null)` raises 94 in both, and so does a *number* of **Null** in either form, so only the **Variant** form with a **Null** *character* differs. Observed in the compiler's test mode, 32-bit and 64-bit. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `string-null-character.twinproj` (attached as `string-null-character.zip`). +2. Run its test cases, the module below. On BETA 1010 the four controls pass (`String(3, "a")`, a **Null** *number* in `String` and `String$`, and `String$(3, Null)`, which raise 94 in VB6 too) and the three cases on `String` with a **Null** *character* fail: `Actual: 94`, or `runtime error 94: Invalid use of Null`. + +The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`string-null-character-vb6.zip`) prints: no error and `VarType` 1 (**Null**) for `String(3, Null)` and `String(0, Null)`, and 94 for the controls. +```vb +' Regression tests: the Variant form String, given a character of Null, returns Null +' with no error; String$ raises error 94. Expected results are VB6's (the VB6 project +' beside this one prints them). tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: a letter, a Null number in either form, and String$ with a Null character. + [TestCase] + Public Sub String_Letter_Repeats() + Assert.Strict.AreEqual CVar("aaa"), String(3, "a") + End Sub + + [TestCase] + Public Sub String_NullNumber_Raises94() + On Error Resume Next + Dim R As Variant + R = String(Null, "a") + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + [TestCase] + Public Sub StringDollar_NullNumber_Raises94() + On Error Resume Next + Dim R As Variant + R = String$(Null, "a") + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + [TestCase] + Public Sub StringDollar_NullCharacter_Raises94() + On Error Resume Next + Dim R As Variant + R = String$(3, Null) + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + ' The defect: String with a Null character returns Null. + [TestCase] + Public Sub String_NullCharacter_NoError() + On Error Resume Next + Dim R As Variant + R = String(3, Null) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + [TestCase] + Public Sub String_NullCharacter_ReturnsNull() + Dim R As Variant + R = String(3, Null) + Assert.Strict.AreEqual vbNull, VarType(R) + End Sub + + [TestCase] + Public Sub String_ZeroNullCharacter_ReturnsNull() + Dim R As Variant + R = String(0, Null) + Assert.Strict.AreEqual vbNull, VarType(R) + End Sub + +End Module +``` + +**Expected behavior** +`String(number, Null)` returns **Null** with no error, as VB6 does, and as the **Variant** forms of other string functions propagate **Null**; `String$` keeps raising 94. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. Code that relies on **Null** propagating through `String`, as VB6 code reading database fields can, stops with error 94. + +What does not reproduce it: `String$` with a **Null** *character*, and a **Null** *number* in either form, all of which raise 94 in VB6 as well. + + + +--- + +## `StrConv` raises error 5 for `vbWide`, `vbNarrow`, `vbKatakana` and `vbHiragana` with an East Asian *LCID* on a system whose locale is not East Asian, where VB6 converts + +**Describe the bug** +On a system whose locale is 1033 (English, United States), `StrConv("a", vbWide, 1041)`, `StrConv(ChrW(&HFF41&), vbNarrow, 1041)`, `StrConv(ChrW(&H3042), vbKatakana, 1041)` and `StrConv(ChrW(&H30A2), vbHiragana, 1041)` raise error 5, *Invalid procedure call or argument*, although the *LCID* argument, 1041, names Japanese. VB6 converts with the rules of the locale *LCID* names, and returns U+FF41, `a`, U+30A2 and U+3042. The same holds for 2052 (Chinese, PRC) and 1042 (Korean). twinBASIC seems to check the system's locale rather than *LCID*'s. Observed in the compiler's test mode, 32-bit and 64-bit. + +**To Reproduce** +Steps to reproduce the behavior: +1. On a system whose locale is not East Asian, open `strconv-east-asian-lcid.twinproj` (attached as `strconv-east-asian-lcid.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass (`vbUpperCase` with 1041, and `vbWide` with no *LCID* and with 1033, which VB6 refuses too) and the six cases on an East Asian *LCID* fail with `runtime error 5: Invalid procedure call or argument`. + +The module is meant to be adapted as a regression test. Each case asserts VB6's result, which the attached VB6 project (`strconv-east-asian-lcid-vb6.zip`) prints, with the system's locale: no error and the converted characters for 1041, 2052 and 1042, and 5 for no *LCID* and for 1033. +```vb +' Regression tests: StrConv's vbWide, vbNarrow, vbKatakana and vbHiragana convert when +' the LCID argument names an East Asian locale, whatever the system's locale. Expected +' results are VB6's on a system whose locale is 1033, English (United States) (the VB6 +' project beside this one prints them). tbrun --tests runs each case and prints PASS +' or FAIL. The controls assume such a system. +[TestFixture] +Module RegressionTests + + ' Controls: another conversion with an East Asian LCID, and vbWide with no East + ' Asian locale at all, which VB6 refuses too. + [TestCase] + Public Sub UpperCase_Japanese_Converts() + Assert.Strict.AreEqual CVar("A"), StrConv("a", vbUpperCase, 1041) + End Sub + + [TestCase] + Public Sub Wide_NoLcid_Raises5() + On Error Resume Next + Dim R As Variant + R = StrConv("a", vbWide) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Wide_English_Raises5() + On Error Resume Next + Dim R As Variant + R = StrConv("a", vbWide, 1033) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + ' The defect. + [TestCase] + Public Sub Wide_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 1041) + End Sub + + [TestCase] + Public Sub Narrow_Japanese_Converts() + Assert.Strict.AreEqual CVar("a"), StrConv(ChrW(&HFF41&), vbNarrow, 1041) + End Sub + + [TestCase] + Public Sub Katakana_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&H30A2)), StrConv(ChrW(&H3042), vbKatakana, 1041) + End Sub + + [TestCase] + Public Sub Hiragana_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&H3042)), StrConv(ChrW(&H30A2), vbHiragana, 1041) + End Sub + + [TestCase] + Public Sub Wide_Chinese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 2052) + End Sub + + [TestCase] + Public Sub Wide_Korean_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 1042) + End Sub + +End Module +``` + +**Expected behavior** +The conversion, made with the rules of the locale *LCID* names, as VB6 makes it. Error 5 only when neither *LCID* nor, without one, the system's locale is East Asian. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045), system locale 1033 + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. A program that converts Japanese, Chinese or Korean text with an explicit *LCID*, so that it runs on any system, stops with error 5 on a Western one. + +What does not reproduce it: another conversion (`vbUpperCase`) with an East Asian *LCID*; and `vbWide` with no *LCID* or with 1033, which VB6 refuses as well. Not checked on a system whose locale is East Asian. + + diff --git a/bugs/strconv-east-asian-lcid/repro.json b/bugs/strconv-east-asian-lcid/repro.json new file mode 100644 index 000000000..dd8170dbb --- /dev/null +++ b/bugs/strconv-east-asian-lcid/repro.json @@ -0,0 +1,17 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.UpperCase_Japanese_Converts$", + "^PASS RegressionTests.Wide_NoLcid_Raises5$", + "^PASS RegressionTests.Wide_English_Raises5$", + "^FAIL RegressionTests.Wide_Japanese_Converts: [(]runtime error 5: ", + "^FAIL RegressionTests.Narrow_Japanese_Converts: [(]runtime error 5: ", + "^FAIL RegressionTests.Katakana_Japanese_Converts: [(]runtime error 5: ", + "^FAIL RegressionTests.Hiragana_Japanese_Converts: [(]runtime error 5: ", + "^FAIL RegressionTests.Wide_Chinese_Converts: [(]runtime error 5: ", + "^FAIL RegressionTests.Wide_Korean_Converts: [(]runtime error 5: " + ] + } +} diff --git a/bugs/strconv-east-asian-lcid/src/Settings b/bugs/strconv-east-asian-lcid/src/Settings new file mode 100644 index 000000000..93cb6022f --- /dev/null +++ b/bugs/strconv-east-asian-lcid/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "StrconvEastAsianLcid", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `StrConv` raises error 5 for `vbWide`, `vbNarrow`, `vbKatakana` and `vbHiragana` with an East Asian *LCID* on a system whose locale is not East Asian, where VB6 converts", + "project.exportPathIsV2": true, + "project.id": "{36B180F9-66F1-4AD3-94A0-8E2BDAEF20FB}", + "project.name": "StrconvEastAsianLcid", + "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/strconv-east-asian-lcid/src/Sources/RegressionTests.twin b/bugs/strconv-east-asian-lcid/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..6785d5493 --- /dev/null +++ b/bugs/strconv-east-asian-lcid/src/Sources/RegressionTests.twin @@ -0,0 +1,63 @@ +' Regression tests: StrConv's vbWide, vbNarrow, vbKatakana and vbHiragana convert when +' the LCID argument names an East Asian locale, whatever the system's locale. Expected +' results are VB6's on a system whose locale is 1033, English (United States) (the VB6 +' project beside this one prints them). tbrun --tests runs each case and prints PASS +' or FAIL. The controls assume such a system. +[TestFixture] +Module RegressionTests + + ' Controls: another conversion with an East Asian LCID, and vbWide with no East + ' Asian locale at all, which VB6 refuses too. + [TestCase] + Public Sub UpperCase_Japanese_Converts() + Assert.Strict.AreEqual CVar("A"), StrConv("a", vbUpperCase, 1041) + End Sub + + [TestCase] + Public Sub Wide_NoLcid_Raises5() + On Error Resume Next + Dim R As Variant + R = StrConv("a", vbWide) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Wide_English_Raises5() + On Error Resume Next + Dim R As Variant + R = StrConv("a", vbWide, 1033) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + ' The defect. + [TestCase] + Public Sub Wide_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 1041) + End Sub + + [TestCase] + Public Sub Narrow_Japanese_Converts() + Assert.Strict.AreEqual CVar("a"), StrConv(ChrW(&HFF41&), vbNarrow, 1041) + End Sub + + [TestCase] + Public Sub Katakana_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&H30A2)), StrConv(ChrW(&H3042), vbKatakana, 1041) + End Sub + + [TestCase] + Public Sub Hiragana_Japanese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&H3042)), StrConv(ChrW(&H30A2), vbHiragana, 1041) + End Sub + + [TestCase] + Public Sub Wide_Chinese_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 2052) + End Sub + + [TestCase] + Public Sub Wide_Korean_Converts() + Assert.Strict.AreEqual CVar(ChrW(&HFF41&)), StrConv("a", vbWide, 1042) + End Sub + +End Module diff --git a/bugs/strconv-east-asian-lcid/src/Sources/Startup.twin b/bugs/strconv-east-asian-lcid/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/strconv-east-asian-lcid/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/strconv-east-asian-lcid/strconv-east-asian-lcid.twinproj b/bugs/strconv-east-asian-lcid/strconv-east-asian-lcid.twinproj new file mode 100644 index 0000000000000000000000000000000000000000..c7409b31986625cb24a5f856982083e7accf83ab GIT binary patch literal 4737 zcmcgw+fw6567}?6@h)#WZ~IW;=xAU9Ib>OYO~gFNvOME$ZUGEz1ny|ca$(nuEP1L* z4Ftir?9=|-e#M^D1>}L4-e$Vj54Kd7%u^>*nMeNnzyI=|uWEm*)oLe3g-QG&;M#av z@^~+l6AV9o{)o$0wXg9#oG%h(#N_mLA@<~0aV3R*Tv)69fo@*aR`I@NhljMqq!oP2XxG z=J21-Hd%195FvOcyEs!C8Q4*Ro%Ob{Dv23q`5xFwv?%s5saeZzw>Mc3Pa~;kZ2gB= z!VFFbZ+e5R6G#{#y9<^42q$bTG|mhh*qy`z<1&UjtekH&nHj4zW|oyv#c-{e;Ngsg z9Bzq`V)>DGaza)U#RlGRugOl~2^1NXM8K{!C}BFq&JucawSGZ?8ps=yD)DZ$zMo9e zNK^tu(|WbOT4(sIF$5_yw+qJ!@TnpO6moVg4QwxQP0`rQK<07H*ThK{kJ4y0EQ}k* zqlls-ffPi6{i{?XiY7?_m~dpEeHHa7kM>1q5>6rbnSO`T^@BhIUco1+wiG?iD z>l^uM+Tm%9p(fH!hNkH$5&WF;i1}w+tv9^J#%7t3>kZzZ6nwC|iTJv$!c+kE;kD%F zS8!2MzduOmL4SYDCC;-|#d8SOknIZ0XJI-JH|7y-U(VSvFf+hIwW!P-vu}TilstX> zq+4&0qDnOn()n1Z+|?(ZrpWOB!*Pi7cKfp&YAt&zCgKXu-N&;ji`t75$)Bn7^*7%S z25#%k#?zF_>)ea7DKCw`A;oz$#g}kaEANZ2mBsqy=n6b-+jFY4Jq)`J$b9SyFMJ}) zF7g?00#GHq4>$pzaF&i-z{}U=C z-!rJFM&(UvwN8TKtJ+`jjpnLMx_GKL`j{+wTCt|A)s>#H&t+T|<4O&D>WcK!i7-aS zQ(cuczyB9z?&>QIblWRAO%*rjhfNuyK}w?=yR6X{SDZ9NV-w4YzTf1F#i=xrpasuK z!llLIR7R5{Zf3Ol^<6H+=>C2*>e1Y2^!n~7R}=@3J@@Zl-+3yWL4AbO-S3;P?*=jw z!Hp3y_3Qhm=1FAcV4y3wny}!X!P2UU(4mqGIv%Y%7C6czgXl!Y)#5S}379OOmdoel#5bfTzWG)41zA!Nd!!>ouG&hqxEum; zL-Lwv9O^R%XEG`k)fmYM9rUif-D`F1LD$;e9<(gi>$k10>)BQ(aC&_&7&!Kzw@fHW z&z#GrT7QYtB%>306BK* zdQUS;gbcRqUdQR%R^Rt}mfIc#R(HGIvRbX~R=4kZ?vB$b36a`Q<_kO(s(3GguOb78 zE*TQ-bfW#?X@RU{?ZQ9YKN{`@$Lz@S-+JE#?-;%A^t_Xy&nzfch~+t0lw%o5bIS?` zY<-ZZx!$O7meX7r$Vn^*NZe{i7KPoclHMD+g0=zl)tyB&AOYuSCP z)9vh7uG@30P7hasx3kj^20MdRzx!p_&tIe}e17g^UUKjy654Soq3bK}5QvA)tcxyx zmX{&qJ;mv9I@cSAd3NEsq*qUp5k$nps%x%dk_y;O1*(xX#H@ysuayQgT_z4L@>@AY zRTESS%YpbRdAzGwyLea6>RtnX1{gUPO}roDZ-y|L2&kN_bLW2*-C{eHv-*7|mi`N_ C8aX5Y literal 0 HcmV?d00001 diff --git a/bugs/strconv-east-asian-lcid/vb6/Module1.bas b/bugs/strconv-east-asian-lcid/vb6/Module1.bas new file mode 100644 index 000000000..95bde77bf --- /dev/null +++ b/bugs/strconv-east-asian-lcid/vb6/Module1.bas @@ -0,0 +1,38 @@ +Attribute VB_Name = "Module1" +Option Explicit + +Private Declare Function GetSystemDefaultLCID Lib "kernel32" () As Long + +' What VB6 does with StrConv's East Asian conversions given an East Asian LCID, on a +' system whose locale is not East Asian. +' Build and run it with: node scripts/bug_repro.mjs vb6 strconv-east-asian-lcid + +Private Function Codes(ByVal S As Variant) As String + Dim I As Long + If VarType(S) <> vbString Then Codes = "-": Exit Function + For I = 1 To Len(S) + Codes = Codes & Hex$(AscW(Mid$(S, I, 1))) & " " + Next +End Function + +Private Sub Report(ByVal Label As String, ByVal Result As Variant) + Print #9, Label & ": " & Err.Number & " " & Err.Description & " (" & Codes(Result) & ")" + Err.Clear +End Sub + +Sub Main() + Dim R As Variant + Open App.Path & "\out.txt" For Output As #9 + On Error Resume Next + Print #9, "System locale: " & Hex$(GetSystemDefaultLCID()) + R = Empty: R = StrConv("a", vbUpperCase, 1041): Report "UpperCase_1041", R + R = Empty: R = StrConv("a", vbWide): Report "Wide_NoLcid", R + R = Empty: R = StrConv("a", vbWide, 1033): Report "Wide_1033", R + R = Empty: R = StrConv("a", vbWide, 1041): Report "Wide_1041", R + R = Empty: R = StrConv(ChrW(&HFF41), vbNarrow, 1041): Report "Narrow_1041", R + R = Empty: R = StrConv(ChrW(&H3042), vbKatakana, 1041): Report "Katakana_1041", R + R = Empty: R = StrConv(ChrW(&H30A2), vbHiragana, 1041): Report "Hiragana_1041", R + R = Empty: R = StrConv("a", vbWide, 2052): Report "Wide_2052", R + R = Empty: R = StrConv("a", vbWide, 1042): Report "Wide_1042", R + Close #9 +End Sub diff --git a/bugs/strconv-east-asian-lcid/vb6/Probe.vbp b/bugs/strconv-east-asian-lcid/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/strconv-east-asian-lcid/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/string-null-character/repro.json b/bugs/string-null-character/repro.json new file mode 100644 index 000000000..9064e3703 --- /dev/null +++ b/bugs/string-null-character/repro.json @@ -0,0 +1,15 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.String_Letter_Repeats$", + "^PASS RegressionTests.String_NullNumber_Raises94$", + "^PASS RegressionTests.StringDollar_NullNumber_Raises94$", + "^PASS RegressionTests.StringDollar_NullCharacter_Raises94$", + "^FAIL RegressionTests.String_NullCharacter_NoError: .*Actual: 94 ", + "^FAIL RegressionTests.String_NullCharacter_ReturnsNull: [(]runtime error 94: ", + "^FAIL RegressionTests.String_ZeroNullCharacter_ReturnsNull: [(]runtime error 94: " + ] + } +} diff --git a/bugs/string-null-character/src/Settings b/bugs/string-null-character/src/Settings new file mode 100644 index 000000000..99b8c6187 --- /dev/null +++ b/bugs/string-null-character/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "StringNullCharacter", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `String` with a *character* of `Null` raises error 94, where VB6 returns `Null`", + "project.exportPathIsV2": true, + "project.id": "{5B37188A-FBCA-47E0-8FA0-6F64E51CE76F}", + "project.name": "StringNullCharacter", + "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/string-null-character/src/Sources/RegressionTests.twin b/bugs/string-null-character/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..54474e3ad --- /dev/null +++ b/bugs/string-null-character/src/Sources/RegressionTests.twin @@ -0,0 +1,60 @@ +' Regression tests: the Variant form String, given a character of Null, returns Null +' with no error; String$ raises error 94. Expected results are VB6's (the VB6 project +' beside this one prints them). tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' Controls: a letter, a Null number in either form, and String$ with a Null character. + [TestCase] + Public Sub String_Letter_Repeats() + Assert.Strict.AreEqual CVar("aaa"), String(3, "a") + End Sub + + [TestCase] + Public Sub String_NullNumber_Raises94() + On Error Resume Next + Dim R As Variant + R = String(Null, "a") + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + [TestCase] + Public Sub StringDollar_NullNumber_Raises94() + On Error Resume Next + Dim R As Variant + R = String$(Null, "a") + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + [TestCase] + Public Sub StringDollar_NullCharacter_Raises94() + On Error Resume Next + Dim R As Variant + R = String$(3, Null) + Assert.Strict.AreEqual 94&, Err.Number + End Sub + + ' The defect: String with a Null character returns Null. + [TestCase] + Public Sub String_NullCharacter_NoError() + On Error Resume Next + Dim R As Variant + R = String(3, Null) + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + [TestCase] + Public Sub String_NullCharacter_ReturnsNull() + Dim R As Variant + R = String(3, Null) + Assert.Strict.AreEqual vbNull, VarType(R) + End Sub + + [TestCase] + Public Sub String_ZeroNullCharacter_ReturnsNull() + Dim R As Variant + R = String(0, Null) + Assert.Strict.AreEqual vbNull, VarType(R) + End Sub + +End Module diff --git a/bugs/string-null-character/src/Sources/Startup.twin b/bugs/string-null-character/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/string-null-character/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/string-null-character/string-null-character.twinproj b/bugs/string-null-character/string-null-character.twinproj new file mode 100644 index 0000000000000000000000000000000000000000..4d8919c87268241fec5c80c386364460dfaa372d GIT binary patch literal 4359 zcmc&&+iv4F5cO`acAKZRZ+##Pqj46NA=`>CDf%GG(!ywNu+ya2I7n!jiMf>}m!xt* zkk9GQ^(#6fO0m63vFYvx^@AdEhMY5p!!KrU7x_Aj?HcX>p^HZc=VkND;<*t&7;=apvCpA)uqXiJzx~ zGJBN$El?KM+cV{K_ zJozFa(gh{EQ4+JCIF6r8i1`VEI0@f{M+2RY(amMAo^=17jQet|0~&G+R>bn)LvzX- zCZg37`564>Qv>k{_%S`|g0NP=n)xF4Ej)*c3Kh>Emed{y)B2I>WQlbtm+w#>W{0wr zejl2`7GVe(AG#B{T)97YKyq8i7yT$a=NGY{3aWgShfs33lEp1j?6Gr-^|V+4wX}d% z>3n{`Rlrc;k)~%wcO3IzvQO0%l}op})xh!HXw*g7XjHo0*Ohsf&e+{)>DCthlqO7?FQ&ju)2y3T!!+O}eK^@0PMN0FOn||Xz)u#L;2(@KfP336v11j@ zlF&kIAe;ju4dkQfY>h@oJe)*Vax@y=$fLcZrZpN#HHiXNuc~0Yw4`Z0wnVX6qyE?u zc{3YFK|i5bZ^eo$I)!P{%TsqNg4xua3_8e@b0f2OIA8ALAcKQb?vW&H3XK!b}zwHjl zzU};KzjjXucHLcj*msBleJwPPGhmUAdBD{TNez&)2YsU4SmI36yfmRn%qFlgln}}Y zjn^-V1>#5cqff^Z?4^H^<1yz)q0)h zVPE&^OMHInU|uos906UoFrW+h6F7+b>M7Goe5lh9#66zVLacj&Jsn+eFY&VtOFa^C zuXRO5h~*qsV}dFs-^WP~M_c*g6u)Odc&vx~0 Date: Wed, 21 Oct 2026 01:40:49 +0200 Subject: [PATCH 20/21] Page shortcuts: tb: links to the page with shortcut: ; Errors' anchors are the bare numbers --- WIP.Authoring.md | 10 + WIP.Errors.md | 10 +- builder/cpu-worker.mjs | 4 +- builder/render.mjs | 43 ++++- docs/Documentation/Authoring.md | 8 + docs/Documentation/Builder.md | 1 + docs/Reference/Core/ReDim.md | 8 +- docs/Reference/Default/VBA/Strings/Asc.md | 4 +- docs/Reference/Default/VBA/Strings/Chr.md | 6 +- docs/Reference/Default/VBA/Strings/Filter.md | 8 +- docs/Reference/Default/VBA/Strings/Format.md | 4 +- .../Default/VBA/Strings/FormatCurrency.md | 4 +- .../Default/VBA/Strings/FormatDateTime.md | 4 +- .../Default/VBA/Strings/FormatNumber.md | 4 +- .../Default/VBA/Strings/FormatPercent.md | 4 +- docs/Reference/Default/VBA/Strings/InStr.md | 6 +- .../Reference/Default/VBA/Strings/InStrRev.md | 4 +- docs/Reference/Default/VBA/Strings/Join.md | 6 +- docs/Reference/Default/VBA/Strings/LCase.md | 2 +- docs/Reference/Default/VBA/Strings/LTrim.md | 2 +- docs/Reference/Default/VBA/Strings/Left.md | 4 +- docs/Reference/Default/VBA/Strings/Mid.md | 4 +- .../Default/VBA/Strings/MonthName.md | 4 +- docs/Reference/Default/VBA/Strings/RTrim.md | 2 +- docs/Reference/Default/VBA/Strings/Replace.md | 4 +- docs/Reference/Default/VBA/Strings/Right.md | 4 +- docs/Reference/Default/VBA/Strings/Space.md | 4 +- docs/Reference/Default/VBA/Strings/Split.md | 4 +- docs/Reference/Default/VBA/Strings/StrComp.md | 6 +- docs/Reference/Default/VBA/Strings/StrConv.md | 4 +- .../Default/VBA/Strings/StrReverse.md | 3 +- docs/Reference/Default/VBA/Strings/String.md | 4 +- docs/Reference/Default/VBA/Strings/Trim.md | 2 +- docs/Reference/Default/VBA/Strings/UCase.md | 2 +- .../Default/VBA/Strings/WeekdayName.md | 4 +- docs/Reference/Run-time-Errors.md | 177 +++++++++--------- scripts/check_errors.mjs | 15 +- scripts/errors.mjs | 1 - scripts/lib/errors-data.mjs | 17 +- test/render.test.mjs | 51 ++++- 40 files changed, 284 insertions(+), 174 deletions(-) diff --git a/WIP.Authoring.md b/WIP.Authoring.md index 9457f0366..26636360e 100644 --- a/WIP.Authoring.md +++ b/WIP.Authoring.md @@ -138,6 +138,16 @@ and a keyword such as `ElseIf` belongs on a page only when the page explains it. Relative links resolve against the **rendered URL** (the page's `permalink:`), not the file path. Pages that share a URL folder can use bare names (`[Y](Y)`); crossing folders needs `../` to climb out. +**A page many pages link to can have a shortcut** (owner, 2026-10-21): `shortcut: ` in +its front matter, and every page links to it as `[x](tb:)` or `[x](tb:#anchor)`, +from any folder, with no `../`. The build (`relativeLinksPlugin` in `builder/render.mjs`) +writes the page's permalink; the fragment is kept as written, and the link check checks it. A +name is a word (letters, digits, `_`, `-`, starting with a letter); an unknown name, a name two +pages claim, and `tb:` as an image's source fail the build. Inside code a `tb:` link is left as +written. The first is `shortcut: Errors` on `Reference/Run-time-Errors.md`, whose anchors are the +error numbers themselves: `[5](tb:Errors#5)`. Any page that wants one may have one; a plain +markdown preview (GitHub, an editor) shows such a link as dead. + The URL prefixes are *not* uniform across packages — VBA pages live one segment shallower than VBRUN pages, so cross-package links are asymmetric: - Core statement → `/tB/Core/` diff --git a/WIP.Errors.md b/WIP.Errors.md index a663dfc7d..d28e00f44 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -33,8 +33,8 @@ A `## Common Errors` section (owner, 2026-10-21, on a reader's request) after th | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Mid$** is given a *string* that is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **Mid$** is given a *string* that is **Null** | - The number is given in decimal and in VB hexadecimal, as a 32-bit **Long** (owner, 2026-10-20). The heading takes the level of the page's Example heading: `###` on the @@ -64,7 +64,7 @@ A `## Common Errors` section (owner, 2026-10-21, on a reader's request) after th leave out (7, 28, 13 and 94 from converting an argument, compile-time TB diagnostics), how a number is written, the message rule, and a NOTE on `Err.Raise` of a number without a message. Generated by `errors.mjs write`, between `` and ``: -one table row per number in `messages.yml`, with its anchor (`#error-5`, `#error--2147467259`), +one table row per number in `messages.yml`, with its anchor, the number itself (`#5`, `#-2147467259`), its message and *Listed by*, links to the pages whose tables name it (twinBASIC's number, not the `vb6:` one), sorted by title. VB's numbers come first in order, then the HRESULTs in the order of their hexadecimal. Each section's numbers, in the table and in the VB6 NOTE, link @@ -190,10 +190,10 @@ The section `write` makes, before the page's first Example heading: | Error | When | |---|---| -| [-2147467259 (`&H80004005`)](../../Reference/Run-time-Errors#error--2147467259), *Unspecified error* | an upper bound is less than its lower bound | +| [-2147467259 (`&H80004005`)](tb:Errors#-2147467259), *Unspecified error* | an upper bound is less than its lower bound | > [!NOTE] BETA 1010 -> When an upper bound is less than its lower bound, VB6 raises error [9 (`&H9`)](../../Reference/Run-time-Errors#error-9), *Subscript out of range*, instead. +> When an upper bound is less than its lower bound, VB6 raises error [9 (`&H9`)](tb:Errors#9), *Subscript out of range*, instead. ``` diff --git a/builder/cpu-worker.mjs b/builder/cpu-worker.mjs index 47c623ea2..767755d75 100644 --- a/builder/cpu-worker.mjs +++ b/builder/cpu-worker.mjs @@ -631,7 +631,7 @@ async function pullLoop() { } } -function reconstructLinkTables({ byPath, byUrl, byRedirect }) { +function reconstructLinkTables({ byPath, byUrl, byRedirect, byShortcut }) { const make = (pairs) => new Map(pairs.map(([k, pl]) => [k, { permalink: pl }])); - return { byPath: make(byPath), byUrl: make(byUrl), byRedirect: make(byRedirect) }; + return { byPath: make(byPath), byUrl: make(byUrl), byRedirect: make(byRedirect), byShortcut: make(byShortcut) }; } diff --git a/builder/render.mjs b/builder/render.mjs index fffa57ed2..91755c0ce 100644 --- a/builder/render.mjs +++ b/builder/render.mjs @@ -1590,14 +1590,29 @@ function renderTocItems(items, indent, out) { // ---------- §5.3 relative-links plugin -------------------------------------- +// A page's `shortcut:` front-matter key names it for `tb:` links, which +// resolve to its permalink wherever it moves. A name is a word, and is unique. +export const SHORTCUT_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/; + export function buildLinkTables(pages) { const byPath = new Map(); const byUrl = new Map(); const byRedirect = new Map(); + const byShortcut = new Map(); for (const p of pages) { putOnce(byPath, p.srcRel, p); + const shortcut = p.frontmatter.shortcut; + if (shortcut !== undefined) { + if (typeof shortcut !== "string" || !SHORTCUT_NAME.test(shortcut)) { + throw new Error(`${p.srcRel}: shortcut: must be a name of letters, digits, _ and -, starting with a letter`); + } + const other = byShortcut.get(shortcut); + if (other) throw new Error(`${p.srcRel}: shortcut ${shortcut} is already ${other.srcRel}'s`); + byShortcut.set(shortcut, p); + } + const url = p.permalink.replace(/^\//, ""); if (url !== "") { putOnce(byUrl, url, p); @@ -1611,15 +1626,20 @@ export function buildLinkTables(pages) { } } - return { byPath, byUrl, byRedirect }; + return { byPath, byUrl, byRedirect, byShortcut }; } // Serialize linkTables for cross-thread transfer. resolveLink() only reads // .permalink from each page, so shipping [key, permalink] pairs is sufficient. // Workers reconstruct minimal { permalink } stubs via reconstructLinkTables. export function serializeLinkTables(lt) { - const pairs = (m) => [...m.entries()].map(([k, p]) => [k, p.permalink]); - return { byPath: pairs(lt.byPath), byUrl: pairs(lt.byUrl), byRedirect: pairs(lt.byRedirect) }; + const pairs = (m) => [...(m ?? new Map()).entries()].map(([k, p]) => [k, p.permalink]); + return { + byPath: pairs(lt.byPath), + byUrl: pairs(lt.byUrl), + byRedirect: pairs(lt.byRedirect), + byShortcut: pairs(lt.byShortcut), + }; } function putOnce(map, key, value) { @@ -1651,6 +1671,23 @@ function relativeLinksPlugin(md, ctx) { const idx = token.attrIndex(attrName); if (idx < 0) return; const value = token.attrs[idx][1]; + // `tb:[#fragment]`: the page whose front matter says `shortcut: `. + // Before the scheme test, which would pass it through as a URL. + if (/^tb:/i.test(value)) { + if (!ctx.linkTables) return; // a tool rendering without the site's pages + const [name, frag] = splitFragment(value.slice(3)); + const target = ctx.linkTables.byShortcut?.get(name); + if (attrName !== "href") throw new Error(`${fromPage.srcRel}: ${value} is a page shortcut, not an image`); + if (!target) { + throw new Error( + `${fromPage.srcRel}: no page has the shortcut in ${value}; ` + + "a page names its shortcut with `shortcut: ` in its front matter", + ); + } + const url = `${ctx.baseurl}${target.permalink}`; + token.attrs[idx][1] = frag ? `${url}#${frag}` : url; + return; + } if (/^([a-z][a-z0-9+.\-]*:|#|\/\/)/i.test(value)) return; const [pathPart, fragPart] = splitFragment(value); diff --git a/docs/Documentation/Authoring.md b/docs/Documentation/Authoring.md index 440a5d1ab..6171ba4e1 100644 --- a/docs/Documentation/Authoring.md +++ b/docs/Documentation/Authoring.md @@ -944,6 +944,14 @@ Relative links resolve against a page's **rendered URL** (its `permalink`), not When the right number of `../` steps is not obvious, copy a working link from a neighbouring page that points where you want to go and change the final segment. [`build.bat`](Building#checking-link-integrity) catches any link that resolves to nothing. +### Page shortcuts + +A page that many pages link to can be given a short name, its **shortcut**, with `shortcut: ` in its frontmatter. Any page then links to it as `[text](tb:)`, or to an anchor in it as `[text](tb:#anchor)`, from any folder and with no `../`. The build writes the page's real URL in its place, so the link keeps working when the page's file moves. + +The [Run-time Errors](../../Reference/Run-time-Errors) page has the shortcut `Errors`, and each error number is an anchor of its own, so `[5](tb:Errors#5)` links to error 5. + +A shortcut is a single word of letters, digits, `_` and `-`, starting with a letter, and no two pages may share one. The build fails on a `tb:` link whose name no page has, and the link check reports an anchor the page does not have. A `tb:` link inside code is left as written. A plain markdown preview, such as GitHub's, shows a `tb:` link as broken; the built site does not. + ## Crossing between VBA and a package: the depth asymmetry **VBA pages sit one URL segment shallower than every package page.** The VBA diff --git a/docs/Documentation/Builder.md b/docs/Documentation/Builder.md index 04148dc95..d8dc612db 100644 --- a/docs/Documentation/Builder.md +++ b/docs/Documentation/Builder.md @@ -633,6 +633,7 @@ The build aborts or flips the exit code in these cases: - **Nav integrity.** An orphan or ambiguous `parent:` throws inside `nav.execute()`, which aborts the build through `Scheduler._abort()`. - **Unpublishable file type.** Every non-page under `docs/` is copied into the output verbatim, so `publish-policy.mjs` holds an allowlist of publishable types and throws on anything else. It runs in `discover` over the static-file inventory, naming the source path before a byte is written, and again in `dispatch` over each tree's derived inventory, the only sweep that sees redirect stubs, vendored theme assets and generated auxiliaries. Neither depends on `--check`. This failure aborts instead of setting an exit code: a broken link leaves a tree worth inspecting, a tree carrying a private key does not. `SOURCE_EXTENSIONS` and `BUILD_EXTENSIONS` are separate sets so the build can emit `sitemap.xml` and `search-data.json` without allowing a stray `docs/secrets.json`; [`scripts/check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy) asserts they stay separate. - **Redirect collision.** A `redirect_from:` entry becomes a stub page at that URL, so `deriveRedirectStubs()` refuses an entry pointing at a URL some page already publishes, and two pages declaring the same entry. Both name the source file on each side. +- **Page shortcut.** `buildLinkTables()` throws when two pages claim the same `shortcut:` or one is not a name, and `relativeLinksPlugin` throws, naming the page, on a `tb:` link whose name no page has, and on `tb:` as an image's source. Markdown-it would otherwise write the `tb:` URL into the page as it stands. See [Page shortcuts](Authoring#page-shortcuts). - **Destination collision.** `assertNoDestinationCollisions()` runs before the write phase and throws if a static file's destination path equals a page's. The static-file copy and the page write run in parallel, so the survivor would otherwise depend on I/O ordering. - **Missing PDF input.** Phase 8 aborts when the book cannot be assembled: no page (or more than one) has `layout: book-combined`, a font `print.css` names is absent from the source tree (the message names `scripts/build_fonts.py`), or an image `book.html` references is not under the source tree. The last is the one that fires in practice; `--tolerate-missing-images` downgrades only that one to a warning. - **A page the book manifest does not mention.** Not a failure. `bookCoverage()` compares every page with `_book.yml`'s entries and its `left_out:` list, and the summary prints a `book:` warning for a page in neither, a page in both, an entry that matches no page, and a landing or foreword URL that names no page. It runs only when the book is built, and [`check_book_coverage.mjs`](Tools-Toolchain-Tests#check-book-coverage) holds each finding to a probe. See [Book Configuration](Book-Configuration#pages-left-out-of-the-book). diff --git a/docs/Reference/Core/ReDim.md b/docs/Reference/Core/ReDim.md index 4b4bb0e17..9bc714c02 100644 --- a/docs/Reference/Core/ReDim.md +++ b/docs/Reference/Core/ReDim.md @@ -60,12 +60,12 @@ The **ReDim** statement acts as a declarative statement if the variable it decla | Error | When | |---|---| -| [-2147467259 (`&H80004005`)](../../Reference/Run-time-Errors#error--2147467259), *Unspecified error* | an upper bound is less than its lower bound | -| [9 (`&H9`)](../../Reference/Run-time-Errors#error-9), *Subscript out of range* | **Preserve** changes a lower bound, or the bounds of any dimension but the last | -| [13 (`&HD`)](../../Reference/Run-time-Errors#error-13), *Type mismatch* | **Preserve** is used on a **Variant** that does not hold an array | +| [-2147467259 (`&H80004005`)](tb:Errors#-2147467259), *Unspecified error* | an upper bound is less than its lower bound | +| [9 (`&H9`)](tb:Errors#9), *Subscript out of range* | **Preserve** changes a lower bound, or the bounds of any dimension but the last | +| [13 (`&HD`)](tb:Errors#13), *Type mismatch* | **Preserve** is used on a **Variant** that does not hold an array | > [!NOTE] BETA 1010 -> When an upper bound is less than its lower bound, VB6 raises error [9 (`&H9`)](../../Reference/Run-time-Errors#error-9), *Subscript out of range*, instead. +> When an upper bound is less than its lower bound, VB6 raises error [9 (`&H9`)](tb:Errors#9), *Subscript out of range*, instead. diff --git a/docs/Reference/Default/VBA/Strings/Asc.md b/docs/Reference/Default/VBA/Strings/Asc.md index 2de5c1b2d..cb6cdb747 100644 --- a/docs/Reference/Default/VBA/Strings/Asc.md +++ b/docs/Reference/Default/VBA/Strings/Asc.md @@ -30,8 +30,8 @@ The functions [**Chr**, **ChrB**, and **ChrW**](Chr) are the opposite of **Asc** | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *string* is zero-length | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *string* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *string* is zero-length | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *string* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/Chr.md b/docs/Reference/Default/VBA/Strings/Chr.md index 7366e8bf4..38b8a918c 100644 --- a/docs/Reference/Default/VBA/Strings/Chr.md +++ b/docs/Reference/Default/VBA/Strings/Chr.md @@ -34,9 +34,9 @@ The functions [**Asc**, **AscB**, and **AscW**](Asc) are the opposite of **Chr** | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | **Chr** or **Chr$** is given a *charcode* less than 0 or greater than 255
**ChrW** or **ChrW$** is given a *charcode* less than -32,768 or greater than 65,535 | -| [6 (`&H6`)](../../../Reference/Run-time-Errors#error-6), *Overflow* | **ChrB** or **ChrB$** is given a *charcode* less than 0 or greater than 255 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *charcode* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | **Chr** or **Chr$** is given a *charcode* less than 0 or greater than 255
**ChrW** or **ChrW$** is given a *charcode* less than -32,768 or greater than 65,535 | +| [6 (`&H6`)](tb:Errors#6), *Overflow* | **ChrB** or **ChrB$** is given a *charcode* less than 0 or greater than 255 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *charcode* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/Filter.md b/docs/Reference/Default/VBA/Strings/Filter.md index 726fe27ae..d37994c8b 100644 --- a/docs/Reference/Default/VBA/Strings/Filter.md +++ b/docs/Reference/Default/VBA/Strings/Filter.md @@ -44,12 +44,12 @@ The array returned by the **Filter** function contains only enough elements to c | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *compare* is less than 0, or greater than 1 and not a valid locale identifier | -| [13 (`&HD`)](../../../Reference/Run-time-Errors#error-13), *Type mismatch* | *sourcearray* is **Null**, or has more than one dimension | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *match*, *include* or *compare* is **Null**, or an element of *sourcearray* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [13 (`&HD`)](tb:Errors#13), *Type mismatch* | *sourcearray* is **Null**, or has more than one dimension | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *match*, *include* or *compare* is **Null**, or an element of *sourcearray* is **Null** | > [!NOTE] BETA 1010 -> When *compare* is 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*; twinBASIC raises none. +> When *compare* is 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument*; twinBASIC raises none. diff --git a/docs/Reference/Default/VBA/Strings/Format.md b/docs/Reference/Default/VBA/Strings/Format.md index ca859a230..e1ab23d2a 100644 --- a/docs/Reference/Default/VBA/Strings/Format.md +++ b/docs/Reference/Default/VBA/Strings/Format.md @@ -102,8 +102,8 @@ If the calendar is Gregorian, the meaning of *format* expression symbols is unch | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *firstDayOfWeek* is less than 0 or greater than 7, or *firstWeekOfYear* is less than 0 or greater than 3 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *format*, *firstDayOfWeek* or *firstWeekOfYear* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *firstDayOfWeek* is less than 0 or greater than 7, or *firstWeekOfYear* is less than 0 or greater than 3 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *format*, *firstDayOfWeek* or *firstWeekOfYear* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/FormatCurrency.md b/docs/Reference/Default/VBA/Strings/FormatCurrency.md index 57a41f523..213e15c00 100644 --- a/docs/Reference/Default/VBA/Strings/FormatCurrency.md +++ b/docs/Reference/Default/VBA/Strings/FormatCurrency.md @@ -41,8 +41,8 @@ When one or more optional arguments are omitted, the values for omitted argument | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1 or greater than 255
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | an argument other than *expression* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1 or greater than 255
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | an argument other than *expression* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/FormatDateTime.md b/docs/Reference/Default/VBA/Strings/FormatDateTime.md index 033da52a5..bfbb46875 100644 --- a/docs/Reference/Default/VBA/Strings/FormatDateTime.md +++ b/docs/Reference/Default/VBA/Strings/FormatDateTime.md @@ -32,8 +32,8 @@ The *namedFormat* argument has the following settings: | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *namedFormat* is less than 0 or greater than 4 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *namedFormat* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *namedFormat* is less than 0 or greater than 4 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *namedFormat* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/FormatNumber.md b/docs/Reference/Default/VBA/Strings/FormatNumber.md index 92844e91e..c6fa1d98c 100644 --- a/docs/Reference/Default/VBA/Strings/FormatNumber.md +++ b/docs/Reference/Default/VBA/Strings/FormatNumber.md @@ -41,8 +41,8 @@ When one or more optional arguments are omitted, the values for omitted argument | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | an argument other than *expression* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | an argument other than *expression* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/FormatPercent.md b/docs/Reference/Default/VBA/Strings/FormatPercent.md index c92444a0a..c5a2776e0 100644 --- a/docs/Reference/Default/VBA/Strings/FormatPercent.md +++ b/docs/Reference/Default/VBA/Strings/FormatPercent.md @@ -41,8 +41,8 @@ When one or more optional arguments are omitted, the values for omitted argument | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | an argument other than *expression* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *numDigitsAfterDecimal* is less than -1
*includeLeadingDigit*, *useParensForNegativeNumbers* or *groupDigits* is not -2, -1 or 0 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | an argument other than *expression* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/InStr.md b/docs/Reference/Default/VBA/Strings/InStr.md index d6efe854c..7b230541f 100644 --- a/docs/Reference/Default/VBA/Strings/InStr.md +++ b/docs/Reference/Default/VBA/Strings/InStr.md @@ -56,14 +56,14 @@ The **InStrB** function is used with byte data contained in a string. Instead of | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is less than 1
**InStr** is given a *start* greater than 1,073,741,824
**InStr** is given a *compare* that is less than 0, or greater than 1 and not a valid locale identifier
**InStr** is given a *compare* that is not a valid locale identifier, and *string1* or *string2* is zero-length | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *start* or *compare* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *start* is less than 1
**InStr** is given a *start* greater than 1,073,741,824
**InStr** is given a *compare* that is less than 0, or greater than 1 and not a valid locale identifier
**InStr** is given a *compare* that is not a valid locale identifier, and *string1* or *string2* is zero-length | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *start* or *compare* is **Null** | > [!NOTE] BETA 1010 > When **InStr** is given a *compare* that is not a valid locale identifier, and *string1* or *string2* is zero-length, VB6 raises no error. > [!NOTE] BETA 1010 -> When **InStr** is given a *compare* of 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*; twinBASIC raises none. +> When **InStr** is given a *compare* of 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument*; twinBASIC raises none. diff --git a/docs/Reference/Default/VBA/Strings/InStrRev.md b/docs/Reference/Default/VBA/Strings/InStrRev.md index e2381083f..7c568ef2f 100644 --- a/docs/Reference/Default/VBA/Strings/InStrRev.md +++ b/docs/Reference/Default/VBA/Strings/InStrRev.md @@ -56,8 +56,8 @@ Unlike **InStr**, **InStrRev** does not return **Null**: a **Null** argument is | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is 0, or less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | any argument is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *start* is 0, or less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | any argument is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/Join.md b/docs/Reference/Default/VBA/Strings/Join.md index 8fed1bb4d..7e5791e73 100644 --- a/docs/Reference/Default/VBA/Strings/Join.md +++ b/docs/Reference/Default/VBA/Strings/Join.md @@ -25,9 +25,9 @@ Syntax: **Join(** *sourcearray* [ **,** *delimiter* ] **)** | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *sourcearray* has more than one dimension | -| [13 (`&HD`)](../../../Reference/Run-time-Errors#error-13), *Type mismatch* | *sourcearray* is **Null**, or an element of it is an array | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *delimiter* is **Null**, or an element of *sourcearray* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *sourcearray* has more than one dimension | +| [13 (`&HD`)](tb:Errors#13), *Type mismatch* | *sourcearray* is **Null**, or an element of it is an array | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *delimiter* is **Null**, or an element of *sourcearray* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/LCase.md b/docs/Reference/Default/VBA/Strings/LCase.md index 26d1c6d42..3bb378926 100644 --- a/docs/Reference/Default/VBA/Strings/LCase.md +++ b/docs/Reference/Default/VBA/Strings/LCase.md @@ -23,7 +23,7 @@ Only uppercase letters are converted to lowercase; all lowercase letters and non | Error | When | |---|---| -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **LCase$** is given a *string* that is **Null** | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **LCase$** is given a *string* that is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/LTrim.md b/docs/Reference/Default/VBA/Strings/LTrim.md index 8ef525b60..875f61573 100644 --- a/docs/Reference/Default/VBA/Strings/LTrim.md +++ b/docs/Reference/Default/VBA/Strings/LTrim.md @@ -21,7 +21,7 @@ The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Vari | Error | When | |---|---| -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **LTrim$** is given a *string* that is **Null** | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **LTrim$** is given a *string* that is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/Left.md b/docs/Reference/Default/VBA/Strings/Left.md index 279ecc0a3..4ef70fc27 100644 --- a/docs/Reference/Default/VBA/Strings/Left.md +++ b/docs/Reference/Default/VBA/Strings/Left.md @@ -32,8 +32,8 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *length* is negative
**Left** or **Left$** is given a *length* greater than 1,073,741,823 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Left$** or **LeftB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *length* is negative
**Left** or **Left$** is given a *length* greater than 1,073,741,823 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **Left$** or **LeftB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/Mid.md b/docs/Reference/Default/VBA/Strings/Mid.md index c7909c282..8b4ea13d0 100644 --- a/docs/Reference/Default/VBA/Strings/Mid.md +++ b/docs/Reference/Default/VBA/Strings/Mid.md @@ -38,8 +38,8 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative
**Mid** or **Mid$** is given a *start* greater than 1,073,741,824 or a *length* greater than 1,073,741,823 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *start* or *length* that is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *start* is less than 1, or *length* is negative
**Mid** or **Mid$** is given a *start* greater than 1,073,741,824 or a *length* greater than 1,073,741,823 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **Mid$** or **MidB$** is given a *string* that is **Null**, or any form is given a *start* or *length* that is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/MonthName.md b/docs/Reference/Default/VBA/Strings/MonthName.md index 9183d47b9..fff7091c6 100644 --- a/docs/Reference/Default/VBA/Strings/MonthName.md +++ b/docs/Reference/Default/VBA/Strings/MonthName.md @@ -22,8 +22,8 @@ Syntax: **MonthName(** *month* [ **,** *abbreviate* ] **)** | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *month* is less than 1 or greater than 12 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *month* or *abbreviate* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *month* is less than 1 or greater than 12 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *month* or *abbreviate* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/RTrim.md b/docs/Reference/Default/VBA/Strings/RTrim.md index 4afcaa86b..8fa757d5a 100644 --- a/docs/Reference/Default/VBA/Strings/RTrim.md +++ b/docs/Reference/Default/VBA/Strings/RTrim.md @@ -21,7 +21,7 @@ The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Vari | Error | When | |---|---| -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **RTrim$** is given a *string* that is **Null** | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **RTrim$** is given a *string* that is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/Replace.md b/docs/Reference/Default/VBA/Strings/Replace.md index 07c530229..9bcca4c66 100644 --- a/docs/Reference/Default/VBA/Strings/Replace.md +++ b/docs/Reference/Default/VBA/Strings/Replace.md @@ -57,8 +57,8 @@ The return value of the **Replace** function is a string, with substitutions mad | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *start* is less than 1, or *count* is less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | any argument is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *start* is less than 1, or *count* is less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | any argument is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/Right.md b/docs/Reference/Default/VBA/Strings/Right.md index ac33002e1..e64c6f0fe 100644 --- a/docs/Reference/Default/VBA/Strings/Right.md +++ b/docs/Reference/Default/VBA/Strings/Right.md @@ -32,8 +32,8 @@ To determine the number of characters in *string*, use the [**Len**](Len) functi | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *length* is negative
**Right** or **Right$** is given a *length* greater than 1,073,741,823 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Right$** or **RightB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *length* is negative
**Right** or **Right$** is given a *length* greater than 1,073,741,823 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **Right$** or **RightB$** is given a *string* that is **Null**, or any form is given a *length* that is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/Space.md b/docs/Reference/Default/VBA/Strings/Space.md index 19d49cb40..8d392ad65 100644 --- a/docs/Reference/Default/VBA/Strings/Space.md +++ b/docs/Reference/Default/VBA/Strings/Space.md @@ -23,8 +23,8 @@ The **Space** function is useful for formatting output and clearing data in fixe | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *number* is negative, or greater than 1,073,741,823 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *number* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *number* is negative, or greater than 1,073,741,823 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *number* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/Split.md b/docs/Reference/Default/VBA/Strings/Split.md index 1c737bc97..51a2225dc 100644 --- a/docs/Reference/Default/VBA/Strings/Split.md +++ b/docs/Reference/Default/VBA/Strings/Split.md @@ -39,8 +39,8 @@ The *compare* argument can have the following values: | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *limit* is less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | any argument is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *limit* is less than -1
*compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | any argument is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/StrComp.md b/docs/Reference/Default/VBA/Strings/StrComp.md index 56b8ad58a..be61eda11 100644 --- a/docs/Reference/Default/VBA/Strings/StrComp.md +++ b/docs/Reference/Default/VBA/Strings/StrComp.md @@ -45,11 +45,11 @@ The *compare* argument settings are: | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *compare* is less than 0, or greater than 1 and not a valid locale identifier | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *compare* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *compare* is less than 0, or greater than 1 and not a valid locale identifier | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *compare* is **Null** | > [!NOTE] BETA 1010 -> When *compare* is 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*; twinBASIC raises none. +> When *compare* is 2 (**vbDatabaseCompare**), VB6 raises error [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument*; twinBASIC raises none. diff --git a/docs/Reference/Default/VBA/Strings/StrConv.md b/docs/Reference/Default/VBA/Strings/StrConv.md index dcc7494eb..93a25f2db 100644 --- a/docs/Reference/Default/VBA/Strings/StrConv.md +++ b/docs/Reference/Default/VBA/Strings/StrConv.md @@ -54,8 +54,8 @@ When converting from a **Byte** array in ANSI format to a string, use the **StrC | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *conversion* is a value the table does not list, or combines **vbUnicode**, **vbFromUnicode**, **vbUTF8** or **vbFromUTF8** with another value
*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** on a system whose locale is not East Asian
*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** and *LCID* names an East Asian locale, on a system whose locale is not East Asian
*LCID* is not a valid locale identifier | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *conversion* or *LCID* is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *conversion* is a value the table does not list, or combines **vbUnicode**, **vbFromUnicode**, **vbUTF8** or **vbFromUTF8** with another value
*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** on a system whose locale is not East Asian
*conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** and *LCID* names an East Asian locale, on a system whose locale is not East Asian
*LCID* is not a valid locale identifier | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *conversion* or *LCID* is **Null** | > [!NOTE] BETA 1010 > When *conversion* includes **vbWide**, **vbNarrow**, **vbKatakana** or **vbHiragana** and *LCID* names an East Asian locale, on a system whose locale is not East Asian, VB6 raises no error. diff --git a/docs/Reference/Default/VBA/Strings/StrReverse.md b/docs/Reference/Default/VBA/Strings/StrReverse.md index 7968b9576..21dfaaee1 100644 --- a/docs/Reference/Default/VBA/Strings/StrReverse.md +++ b/docs/Reference/Default/VBA/Strings/StrReverse.md @@ -13,12 +13,13 @@ Syntax: **StrReverse(** *expression* **)** *expression* : *required* The string whose characters are to be reversed. If *expression* is a zero-length string (`""`), a zero-length string is returned. + ### Common Errors | Error | When | |---|---| -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *expression* is **Null** | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *expression* is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/String.md b/docs/Reference/Default/VBA/Strings/String.md index aee8819df..448dda40d 100644 --- a/docs/Reference/Default/VBA/Strings/String.md +++ b/docs/Reference/Default/VBA/Strings/String.md @@ -26,8 +26,8 @@ When *character* is a number greater than 255, **String** converts the number to | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *number* is negative, or greater than 1,073,741,823
*character* is a zero-length string | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | *number* is **Null**, or **String$** is given a *character* that is **Null**
**String** is given a *character* that is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *number* is negative, or greater than 1,073,741,823
*character* is a zero-length string | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | *number* is **Null**, or **String$** is given a *character* that is **Null**
**String** is given a *character* that is **Null** | > [!NOTE] BETA 1010 > When **String** is given a *character* that is **Null**, VB6 raises no error. diff --git a/docs/Reference/Default/VBA/Strings/Trim.md b/docs/Reference/Default/VBA/Strings/Trim.md index 00d1e11ef..1909d8451 100644 --- a/docs/Reference/Default/VBA/Strings/Trim.md +++ b/docs/Reference/Default/VBA/Strings/Trim.md @@ -21,7 +21,7 @@ The `$`-suffixed form returns a **String**; the unsuffixed form returns a **Vari | Error | When | |---|---| -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **Trim$** is given a *string* that is **Null** | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **Trim$** is given a *string* that is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/UCase.md b/docs/Reference/Default/VBA/Strings/UCase.md index 55d558ef4..12acf0850 100644 --- a/docs/Reference/Default/VBA/Strings/UCase.md +++ b/docs/Reference/Default/VBA/Strings/UCase.md @@ -23,7 +23,7 @@ Only lowercase letters are converted to uppercase; all uppercase letters and non | Error | When | |---|---| -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | **UCase$** is given a *string* that is **Null** | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **UCase$** is given a *string* that is **Null** | diff --git a/docs/Reference/Default/VBA/Strings/WeekdayName.md b/docs/Reference/Default/VBA/Strings/WeekdayName.md index a6c0baad2..37888516c 100644 --- a/docs/Reference/Default/VBA/Strings/WeekdayName.md +++ b/docs/Reference/Default/VBA/Strings/WeekdayName.md @@ -38,8 +38,8 @@ The *firstdayofweek* argument can have the following values: | Error | When | |---|---| -| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *weekday* is less than 1 or greater than 7, or *firstdayofweek* is less than 0 or greater than 7 | -| [94 (`&H5E`)](../../../Reference/Run-time-Errors#error-94), *Invalid use of Null* | any argument is **Null** | +| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *weekday* is less than 1 or greater than 7, or *firstdayofweek* is less than 0 or greater than 7 | +| [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | any argument is **Null** | diff --git a/docs/Reference/Run-time-Errors.md b/docs/Reference/Run-time-Errors.md index 76a6b2ce6..bb65cf91c 100644 --- a/docs/Reference/Run-time-Errors.md +++ b/docs/Reference/Run-time-Errors.md @@ -3,6 +3,7 @@ title: Run-time Errors parent: Reference Section nav_order: 12 permalink: /Reference/Run-time-Errors +shortcut: Errors index_also: trappable errors, error numbers, error messages --- @@ -42,94 +43,94 @@ The messages are those of twinBASIC 32-bit and VB6, which agree for every number | Error | Message | Listed by | |---|---|---| -| **3**{: #error-3 } (`&H3`) | *Return without GoSub* | --- | -| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [22 pages](#listed-5) | -| **6**{: #error-6 } (`&H6`) | *Overflow* | [1 page](#listed-6) | -| **7**{: #error-7 } (`&H7`) | *Out of memory* | --- | -| **9**{: #error-9 } (`&H9`) | *Subscript out of range* | [1 page](#listed-9) | -| **10**{: #error-10 } (`&HA`) | *This array is fixed or temporarily locked* | --- | -| **11**{: #error-11 } (`&HB`) | *Division by zero* | --- | -| **13**{: #error-13 } (`&HD`) | *Type mismatch* | [3 pages](#listed-13) | -| **14**{: #error-14 } (`&HE`) | *Out of string space* | --- | -| **16**{: #error-16 } (`&H10`) | *Expression too complex* | --- | -| **17**{: #error-17 } (`&H11`) | *Can't perform requested operation* | --- | -| **18**{: #error-18 } (`&H12`) | *User interrupt occurred* | --- | -| **20**{: #error-20 } (`&H14`) | *Resume without error* | --- | -| **28**{: #error-28 } (`&H1C`) | *Out of stack space* | --- | -| **35**{: #error-35 } (`&H23`) | *Sub or Function not defined* | --- | -| **47**{: #error-47 } (`&H2F`) | *Too many DLL application clients* | --- | -| **48**{: #error-48 } (`&H30`) | *Error in loading DLL* | --- | -| **49**{: #error-49 } (`&H31`) | *Bad DLL calling convention* | --- | -| **51**{: #error-51 } (`&H33`) | *Internal error* | --- | -| **52**{: #error-52 } (`&H34`) | *Bad file name or number* | --- | -| **53**{: #error-53 } (`&H35`) | *File not found* | --- | -| **54**{: #error-54 } (`&H36`) | *Bad file mode* | --- | -| **55**{: #error-55 } (`&H37`) | *File already open* | --- | -| **57**{: #error-57 } (`&H39`) | *Device I/O error* | --- | -| **58**{: #error-58 } (`&H3A`) | *File already exists* | --- | -| **59**{: #error-59 } (`&H3B`) | *Bad record length* | --- | -| **61**{: #error-61 } (`&H3D`) | *Disk full* | --- | -| **62**{: #error-62 } (`&H3E`) | *Input past end of file* | --- | -| **63**{: #error-63 } (`&H3F`) | *Bad record number* | --- | -| **67**{: #error-67 } (`&H43`) | *Too many files* | --- | -| **68**{: #error-68 } (`&H44`) | *Device unavailable* | --- | -| **70**{: #error-70 } (`&H46`) | *Permission denied* | --- | -| **71**{: #error-71 } (`&H47`) | *Disk not ready* | --- | -| **74**{: #error-74 } (`&H4A`) | *Can't rename with different drive* | --- | -| **75**{: #error-75 } (`&H4B`) | *Path/File access error* | --- | -| **76**{: #error-76 } (`&H4C`) | *Path not found* | --- | -| **91**{: #error-91 } (`&H5B`) | *Object variable or With block variable not set* | --- | -| **92**{: #error-92 } (`&H5C`) | *For loop not initialized* | --- | -| **93**{: #error-93 } (`&H5D`) | *Invalid pattern string* | --- | -| **94**{: #error-94 } (`&H5E`) | *Invalid use of Null* | [28 pages](#listed-94) | -| **96**{: #error-96 } (`&H60`) | *Unable to sink events of object because the object is already firing events to the maximum number of event receivers that it supports* | --- | -| **97**{: #error-97 } (`&H61`) | *Can not call friend function on object which is not an instance of defining class* | --- | -| **98**{: #error-98 } (`&H62`) | *A property or method call cannot include a reference to a private object, either as an argument or as a return value* | --- | -| **321**{: #error-321 } (`&H141`) | *Invalid file format* | --- | -| **322**{: #error-322 } (`&H142`) | *Can't create necessary temporary file* | --- | -| **325**{: #error-325 } (`&H145`) | *Invalid format in resource file* | --- | -| **380**{: #error-380 } (`&H17C`) | *Invalid property value* | --- | -| **381**{: #error-381 } (`&H17D`) | *Invalid property array index* | --- | -| **382**{: #error-382 } (`&H17E`) | *Set not supported at runtime* | --- | -| **383**{: #error-383 } (`&H17F`) | *Set not supported (read-only property)* | --- | -| **385**{: #error-385 } (`&H181`) | *Need property array index* | --- | -| **387**{: #error-387 } (`&H183`) | *Set not permitted* | --- | -| **393**{: #error-393 } (`&H189`) | *Get not supported at runtime* | --- | -| **394**{: #error-394 } (`&H18A`) | *Get not supported (write-only property)* | --- | -| **422**{: #error-422 } (`&H1A6`) | *Property not found* | --- | -| **423**{: #error-423 } (`&H1A7`) | *Property or method not found* | --- | -| **424**{: #error-424 } (`&H1A8`) | *Object required* | --- | -| **429**{: #error-429 } (`&H1AD`) | *ActiveX component can't create object* | --- | -| **430**{: #error-430 } (`&H1AE`) | *Class does not support Automation or does not support expected interface* | --- | -| **432**{: #error-432 } (`&H1B0`) | *File name or class name not found during Automation operation* | --- | -| **438**{: #error-438 } (`&H1B6`) | *Object doesn't support this property or method* | --- | -| **440**{: #error-440 } (`&H1B8`) | *Automation error* | --- | -| **442**{: #error-442 } (`&H1BA`) | *Connection to type library or object library for remote process has been lost. Press OK for dialog to remove reference.* | --- | -| **443**{: #error-443 } (`&H1BB`) | *Automation object does not have a default value* | --- | -| **445**{: #error-445 } (`&H1BD`) | *Object doesn't support this action* | --- | -| **446**{: #error-446 } (`&H1BE`) | *Object doesn't support named arguments* | --- | -| **447**{: #error-447 } (`&H1BF`) | *Object doesn't support current locale setting* | --- | -| **448**{: #error-448 } (`&H1C0`) | *Named argument not found* | --- | -| **449**{: #error-449 } (`&H1C1`) | *Argument not optional* | --- | -| **450**{: #error-450 } (`&H1C2`) | *Wrong number of arguments or invalid property assignment* | --- | -| **451**{: #error-451 } (`&H1C3`) | *Property let procedure not defined and property get procedure did not return an object* | --- | -| **452**{: #error-452 } (`&H1C4`) | *Invalid ordinal* | --- | -| **453**{: #error-453 } (`&H1C5`) | *Specified DLL function not found* | --- | -| **454**{: #error-454 } (`&H1C6`) | *Code resource not found* | --- | -| **455**{: #error-455 } (`&H1C7`) | *Code resource lock error* | --- | -| **457**{: #error-457 } (`&H1C9`) | *This key is already associated with an element of this collection* | --- | -| **458**{: #error-458 } (`&H1CA`) | *Variable uses an Automation type not supported in Visual Basic* | --- | -| **459**{: #error-459 } (`&H1CB`) | *Object or class does not support the set of events* | --- | -| **460**{: #error-460 } (`&H1CC`) | *Invalid clipboard format* | --- | -| **461**{: #error-461 } (`&H1CD`) | *Method or data member not found* | --- | -| **462**{: #error-462 } (`&H1CE`) | *The remote server machine does not exist or is unavailable* | --- | -| **463**{: #error-463 } (`&H1CF`) | *Class not registered on local machine* | --- | -| **481**{: #error-481 } (`&H1E1`) | *Invalid picture* | --- | -| **482**{: #error-482 } (`&H1E2`) | *Printer error* | --- | -| **735**{: #error-735 } (`&H2DF`) | *Can't save file to TEMP* | --- | -| **744**{: #error-744 } (`&H2E8`) | *Search text not found* | --- | -| **746**{: #error-746 } (`&H2EA`) | *Replacements too long* | --- | -| **-2147467259**{: #error--2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | [1 page](#listed--2147467259) | +| **3**{: #3 } (`&H3`) | *Return without GoSub* | --- | +| **5**{: #5 } (`&H5`) | *Invalid procedure call or argument* | [22 pages](#listed-5) | +| **6**{: #6 } (`&H6`) | *Overflow* | [1 page](#listed-6) | +| **7**{: #7 } (`&H7`) | *Out of memory* | --- | +| **9**{: #9 } (`&H9`) | *Subscript out of range* | [1 page](#listed-9) | +| **10**{: #10 } (`&HA`) | *This array is fixed or temporarily locked* | --- | +| **11**{: #11 } (`&HB`) | *Division by zero* | --- | +| **13**{: #13 } (`&HD`) | *Type mismatch* | [3 pages](#listed-13) | +| **14**{: #14 } (`&HE`) | *Out of string space* | --- | +| **16**{: #16 } (`&H10`) | *Expression too complex* | --- | +| **17**{: #17 } (`&H11`) | *Can't perform requested operation* | --- | +| **18**{: #18 } (`&H12`) | *User interrupt occurred* | --- | +| **20**{: #20 } (`&H14`) | *Resume without error* | --- | +| **28**{: #28 } (`&H1C`) | *Out of stack space* | --- | +| **35**{: #35 } (`&H23`) | *Sub or Function not defined* | --- | +| **47**{: #47 } (`&H2F`) | *Too many DLL application clients* | --- | +| **48**{: #48 } (`&H30`) | *Error in loading DLL* | --- | +| **49**{: #49 } (`&H31`) | *Bad DLL calling convention* | --- | +| **51**{: #51 } (`&H33`) | *Internal error* | --- | +| **52**{: #52 } (`&H34`) | *Bad file name or number* | --- | +| **53**{: #53 } (`&H35`) | *File not found* | --- | +| **54**{: #54 } (`&H36`) | *Bad file mode* | --- | +| **55**{: #55 } (`&H37`) | *File already open* | --- | +| **57**{: #57 } (`&H39`) | *Device I/O error* | --- | +| **58**{: #58 } (`&H3A`) | *File already exists* | --- | +| **59**{: #59 } (`&H3B`) | *Bad record length* | --- | +| **61**{: #61 } (`&H3D`) | *Disk full* | --- | +| **62**{: #62 } (`&H3E`) | *Input past end of file* | --- | +| **63**{: #63 } (`&H3F`) | *Bad record number* | --- | +| **67**{: #67 } (`&H43`) | *Too many files* | --- | +| **68**{: #68 } (`&H44`) | *Device unavailable* | --- | +| **70**{: #70 } (`&H46`) | *Permission denied* | --- | +| **71**{: #71 } (`&H47`) | *Disk not ready* | --- | +| **74**{: #74 } (`&H4A`) | *Can't rename with different drive* | --- | +| **75**{: #75 } (`&H4B`) | *Path/File access error* | --- | +| **76**{: #76 } (`&H4C`) | *Path not found* | --- | +| **91**{: #91 } (`&H5B`) | *Object variable or With block variable not set* | --- | +| **92**{: #92 } (`&H5C`) | *For loop not initialized* | --- | +| **93**{: #93 } (`&H5D`) | *Invalid pattern string* | --- | +| **94**{: #94 } (`&H5E`) | *Invalid use of Null* | [28 pages](#listed-94) | +| **96**{: #96 } (`&H60`) | *Unable to sink events of object because the object is already firing events to the maximum number of event receivers that it supports* | --- | +| **97**{: #97 } (`&H61`) | *Can not call friend function on object which is not an instance of defining class* | --- | +| **98**{: #98 } (`&H62`) | *A property or method call cannot include a reference to a private object, either as an argument or as a return value* | --- | +| **321**{: #321 } (`&H141`) | *Invalid file format* | --- | +| **322**{: #322 } (`&H142`) | *Can't create necessary temporary file* | --- | +| **325**{: #325 } (`&H145`) | *Invalid format in resource file* | --- | +| **380**{: #380 } (`&H17C`) | *Invalid property value* | --- | +| **381**{: #381 } (`&H17D`) | *Invalid property array index* | --- | +| **382**{: #382 } (`&H17E`) | *Set not supported at runtime* | --- | +| **383**{: #383 } (`&H17F`) | *Set not supported (read-only property)* | --- | +| **385**{: #385 } (`&H181`) | *Need property array index* | --- | +| **387**{: #387 } (`&H183`) | *Set not permitted* | --- | +| **393**{: #393 } (`&H189`) | *Get not supported at runtime* | --- | +| **394**{: #394 } (`&H18A`) | *Get not supported (write-only property)* | --- | +| **422**{: #422 } (`&H1A6`) | *Property not found* | --- | +| **423**{: #423 } (`&H1A7`) | *Property or method not found* | --- | +| **424**{: #424 } (`&H1A8`) | *Object required* | --- | +| **429**{: #429 } (`&H1AD`) | *ActiveX component can't create object* | --- | +| **430**{: #430 } (`&H1AE`) | *Class does not support Automation or does not support expected interface* | --- | +| **432**{: #432 } (`&H1B0`) | *File name or class name not found during Automation operation* | --- | +| **438**{: #438 } (`&H1B6`) | *Object doesn't support this property or method* | --- | +| **440**{: #440 } (`&H1B8`) | *Automation error* | --- | +| **442**{: #442 } (`&H1BA`) | *Connection to type library or object library for remote process has been lost. Press OK for dialog to remove reference.* | --- | +| **443**{: #443 } (`&H1BB`) | *Automation object does not have a default value* | --- | +| **445**{: #445 } (`&H1BD`) | *Object doesn't support this action* | --- | +| **446**{: #446 } (`&H1BE`) | *Object doesn't support named arguments* | --- | +| **447**{: #447 } (`&H1BF`) | *Object doesn't support current locale setting* | --- | +| **448**{: #448 } (`&H1C0`) | *Named argument not found* | --- | +| **449**{: #449 } (`&H1C1`) | *Argument not optional* | --- | +| **450**{: #450 } (`&H1C2`) | *Wrong number of arguments or invalid property assignment* | --- | +| **451**{: #451 } (`&H1C3`) | *Property let procedure not defined and property get procedure did not return an object* | --- | +| **452**{: #452 } (`&H1C4`) | *Invalid ordinal* | --- | +| **453**{: #453 } (`&H1C5`) | *Specified DLL function not found* | --- | +| **454**{: #454 } (`&H1C6`) | *Code resource not found* | --- | +| **455**{: #455 } (`&H1C7`) | *Code resource lock error* | --- | +| **457**{: #457 } (`&H1C9`) | *This key is already associated with an element of this collection* | --- | +| **458**{: #458 } (`&H1CA`) | *Variable uses an Automation type not supported in Visual Basic* | --- | +| **459**{: #459 } (`&H1CB`) | *Object or class does not support the set of events* | --- | +| **460**{: #460 } (`&H1CC`) | *Invalid clipboard format* | --- | +| **461**{: #461 } (`&H1CD`) | *Method or data member not found* | --- | +| **462**{: #462 } (`&H1CE`) | *The remote server machine does not exist or is unavailable* | --- | +| **463**{: #463 } (`&H1CF`) | *Class not registered on local machine* | --- | +| **481**{: #481 } (`&H1E1`) | *Invalid picture* | --- | +| **482**{: #482 } (`&H1E2`) | *Printer error* | --- | +| **735**{: #735 } (`&H2DF`) | *Can't save file to TEMP* | --- | +| **744**{: #744 } (`&H2E8`) | *Search text not found* | --- | +| **746**{: #746 } (`&H2EA`) | *Replacements too long* | --- | +| **-2147467259**{: #-2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | [1 page](#listed--2147467259) | ## Pages that list each error diff --git a/scripts/check_errors.mjs b/scripts/check_errors.mjs index dcd61a2f4..68febe136 100644 --- a/scripts/check_errors.mjs +++ b/scripts/check_errors.mjs @@ -32,6 +32,7 @@ import { markdownFiles } from "../lib/markdown-files.mjs"; import { DOCS_DIR, REPO_ROOT } from "../lib/repo-paths.mjs"; import { CENTRAL_PAGE, + CENTRAL_SHORTCUT, caseAtLine, compareMessages, dataFiles, @@ -153,11 +154,11 @@ const section = renderSection(page, { permalink: "/tB/Modules/Strings/Mid", }); check( - "the section has the table, its numbers linked to the central page, and the VB6 note", + "the section has the table, its numbers linked to the central page by its shortcut, and the VB6 note", section .join("\n") .includes( - "| [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument* | *x* is negative |\n| [9 (`&H9`)](../../../Reference/Run-time-Errors#error-9), *Subscript out of range* | the index is out |\n\n> [!NOTE] BETA 1010\n> When the index is out, VB6 raises error [5 (`&H5`)](../../../Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*, instead.", + "| [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument* | *x* is negative |\n| [9 (`&H9`)](tb:Errors#9), *Subscript out of range* | the index is out |\n\n> [!NOTE] BETA 1010\n> When the index is out, VB6 raises error [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument*, instead.", ), ); const merged = renderSection( @@ -185,7 +186,7 @@ check( "a row of error 0 has its VB6 note and no table", !vb6Only.includes("| Error |") && vb6Only.includes( - "## Common Errors\n\n> [!NOTE] BETA 1010\n> When *x* is 2, VB6 raises error [5 (`&H5`)](Reference/Run-time-Errors#error-5), *Invalid procedure call or argument*; twinBASIC raises none.\n\n", + "## Common Errors\n\n> [!NOTE] BETA 1010\n> When *x* is 2, VB6 raises error [5 (`&H5`)](tb:Errors#5), *Invalid procedure call or argument*; twinBASIC raises none.\n\n", ), vb6Only, ); @@ -242,7 +243,7 @@ const index = renderIndex([sample], messages, new Map([["A/B.md", { title: "B", check( "the index has a row per number, in order, with its anchor and a link to its pages", index.slice(4, 7).join("\n") === - "| **5**{: #error-5 } (`&H5`) | *Invalid procedure call or argument* | [1 page](#listed-5) |\n| **9**{: #error-9 } (`&H9`) | *Subscript out of range* | [1 page](#listed-9) |\n| **-2147467259**{: #error--2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | --- |", + "| **5**{: #5 } (`&H5`) | *Invalid procedure call or argument* | [1 page](#listed-5) |\n| **9**{: #9 } (`&H9`) | *Subscript out of range* | [1 page](#listed-9) |\n| **-2147467259**{: #-2147467259 } (`&H80004005`) | *Unspecified error*; **Error** gives *Application-defined or object-defined error* | --- |", index.join("\n"), ); check( @@ -355,7 +356,6 @@ for (const f of files) { beta: f.beta, messages: known, level: pageLevel(text), - permalink: pageMeta(text).permalink, }); if (spliceSection(text, want) !== text) stale.push(`docs/${p.page}: the section is not what ${f.name} writes`); } @@ -382,5 +382,10 @@ check( !indexed.problem && indexed.text === centralText, `docs/${CENTRAL_PAGE}: ${indexed.problem ?? "the index is stale"}`, ); +check( + `docs/${CENTRAL_PAGE} has the shortcut the sections link to it by`, + centralText !== null && pageMeta(centralText).shortcut === CENTRAL_SHORTCUT, + `its front matter needs shortcut: ${CENTRAL_SHORTCUT}`, +); process.exit(report()); diff --git a/scripts/errors.mjs b/scripts/errors.mjs index 208a0e03f..e8ebe86e5 100644 --- a/scripts/errors.mjs +++ b/scripts/errors.mjs @@ -171,7 +171,6 @@ if (command === "write") { beta: f.beta, messages, level: pageLevel(text), - permalink: meta.permalink, }); const next = spliceSection(text, section); if (next !== text) { diff --git a/scripts/lib/errors-data.mjs b/scripts/lib/errors-data.mjs index 0633cf4b8..736f948e3 100644 --- a/scripts/lib/errors-data.mjs +++ b/scripts/lib/errors-data.mjs @@ -413,9 +413,11 @@ const escapeCell = (s) => s.replace(/\|/g, "\\|"); /** The central page of the run-time errors, as a path under docs/, and its permalink. */ export const CENTRAL_PAGE = "Reference/Run-time-Errors.md"; export const CENTRAL_PERMALINK = "/Reference/Run-time-Errors"; +/** The central page's shortcut (`shortcut:` in its front matter): sections link to it as tb:Errors#5. */ +export const CENTRAL_SHORTCUT = "Errors"; -/** The anchor of number `n` on the central page: error-5, error--2147467259. */ -export const errorAnchor = (n) => `error-${n}`; +/** The anchor of number `n` on the central page, the number itself: 5, -2147467259 (tb:Errors#5). */ +export const errorAnchor = (n) => `${n}`; /** The anchor of number `n`'s list of pages on the central page: listed-5, listed--2147467259. */ export const listedAnchor = (n) => `listed-${n}`; @@ -434,19 +436,18 @@ export function pageMeta(text) { data = parseFrontmatter(text)?.data ?? {}; } catch {} const s = (v) => (typeof v === "string" && v ? v : null); - return { title: s(data.title), permalink: s(data.permalink) }; + return { title: s(data.title), permalink: s(data.permalink), shortcut: s(data.shortcut) }; } /** * The section's lines for page entry `p` of data file `name`, or null when the page - * gets none. `level` is the heading's "##"/"###", `permalink` the page's own: each - * number links to its row on the central page. + * gets none. `level` is the heading's "##"/"###". Each number links to its row on + * the central page through the page's shortcut, tb:Errors#5. */ -export function renderSection(p, { name, beta, messages, level, permalink }) { +export function renderSection(p, { name, beta, messages, level }) { if (!p.rows.length && p.none === undefined) return null; const msg = (n) => messages.get(n)?.raised ?? "?"; - const central = relativeLink(permalink ?? "/", CENTRAL_PERMALINK); - const num = (n) => `[${errorNumber(n)}](${central}#${errorAnchor(n)})`; + const num = (n) => `[${errorNumber(n)}](tb:${CENTRAL_SHORTCUT}#${errorAnchor(n)})`; const out = [ ``, `${level} Common Errors`, diff --git a/test/render.test.mjs b/test/render.test.mjs index 35f34c639..28cf40186 100644 --- a/test/render.test.mjs +++ b/test/render.test.mjs @@ -6,7 +6,8 @@ // builder/theme-pictures.mjs); and for a diagram's SVG as it is inlined, its // boxes linking or not (builder/inline-diagram.mjs), on what Graphviz itself // writes; and for a callout's label, `> [!WARNING] BETA 1005` -// (lib/callout-labels.mjs). +// (lib/callout-labels.mjs); and for a page shortcut, `tb:Errors#error-5` +// (relativeLinksPlugin, buildLinkTables). // // The build compares whole pages, so a plugin that goes wrong only on input // the corpus does not hold passes it: kramdownEllipsisPlugin shortened every @@ -25,7 +26,13 @@ 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, initHighlighter } from "../builder/render.mjs"; +import { + applyPreRenderRewrites, + buildLinkTables, + createMarkdownIt, + initHighlighter, + serializeLinkTables, +} from "../builder/render.mjs"; import { lightOnly, pairSizes, @@ -209,6 +216,46 @@ describe("rewriteHtml with a website behind the offline tree", () => { }); }); +// A page shortcut: `tb:Errors#error-5` links to the page whose front matter says +// `shortcut: Errors`, its fragment kept; an unknown or doubled name fails the build. +describe("page shortcuts (tb:)", () => { + const page = (srcRel, permalink, frontmatter = {}) => ({ srcRel, permalink, frontmatter }); + const tables = buildLinkTables([ + page("Reference/Run-time-Errors.md", "/Reference/Run-time-Errors", { shortcut: "Errors" }), + page("Reference/Other.md", "/Reference/Other"), + ]); + const site = createMarkdownIt({ highlighter: null, linkTables: tables, baseurl: "/b", staticFiles: new Set() }); + const render = (src) => site.render(src, { page: { srcRel: "Reference/Default/VBA/Strings/Mid.md" } }).trim(); + + test("a shortcut resolves to the page's permalink, with or without a fragment", () => { + assert.equal( + render("[5](tb:Errors#error-5) and [all](tb:Errors)"), + '

5 and all

', + ); + }); + + test("a shortcut in a code span is left as written, unknown or not", () => { + assert.match(render("`[5](tb:Nobody)`"), /]*>\[5\]\(tb:Nobody\)<\/code>/); + }); + + test("an unknown shortcut, and a shortcut as an image, fail the render, naming the page", () => { + assert.throws(() => render("[x](tb:Nobody#a)"), /Mid\.md: no page has the shortcut in tb:Nobody#a/); + assert.throws(() => render("![x](tb:Errors)"), /Mid\.md: tb:Errors is a page shortcut, not an image/); + }); + + test("a shortcut that two pages claim, or that is not a name, fails the link tables", () => { + assert.throws( + () => buildLinkTables([page("A.md", "/A", { shortcut: "X" }), page("B.md", "/B", { shortcut: "X" })]), + /B\.md: shortcut X is already A\.md's/, + ); + assert.throws(() => buildLinkTables([page("A.md", "/A", { shortcut: "a b" })]), /A\.md: shortcut: must be a name/); + }); + + test("the shortcuts cross to the workers with the other tables", () => { + assert.deepEqual(serializeLinkTables(tables).byShortcut, [["Errors", "/Reference/Run-time-Errors"]]); + }); +}); + // A picture in two themes: the page names X.png, and the build writes the light picture and // the dark one when X.light.png is among the static files, the book the light one alone. describe("themePairPlugin and lightOnly", () => { From 0d934c0c072d3b520e13b3625d29fbc74dd3f342 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 21 Oct 2026 01:43:08 +0200 Subject: [PATCH 21/21] WIP.Errors.md: batch 2 done, how a batch runs, ready for batch 3 --- WIP.Errors.md | 434 ++++++++++++++++++++++++-------------------------- 1 file changed, 206 insertions(+), 228 deletions(-) diff --git a/WIP.Errors.md b/WIP.Errors.md index d28e00f44..cfec70d2e 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -7,29 +7,55 @@ exactly that. Like WIP.md, this file is not rendered through tbdocs. -## Owner's decisions (2026-10-20) +## Where things stand (2026-10-21) + +**Batches 0, 1 and 2 are done, reviewed by the owner, and committed on `staging`** (not +pushed; last commit `d3f3e514`). The tooling, the central page and every page of the VBA +`Strings` module are in place: 339 cases in `errors/VBA.Strings.yml`, all agreeing in +twinBASIC 32-bit and VB6 on BETA 1010, and a Common Errors section on 28 of the module's 30 +pages (Len raises nothing; the index documents nothing). No row is `bug: pending`. Every +difference from VB6 found so far is queued in BUGS-TO-REPORT.md with a reproducer under +`bugs/`, for the owner to file: `redim-inverted-bounds-error`, `database-compare-no-error`, +`instr-invalid-lcid-empty-string`, `string-null-character`, `strconv-east-asian-lcid`. + +**Next: batch 3, VBA `Conversion`** (or any other batch: they are independent). Follow *How a +batch runs* below; the style is settled, so a batch no longer needs the owner's review of its +first pages, only of the questions it raises. -- **Data-driven.** Each procedure's errors are data: the cases that raise them. The page - section, the probe that measures them and the check that keeps them true are generated - from that data. Hand-written tables cannot be re-verified on a new BETA. +## Owner's decisions + +- **Data-driven** (2026-10-20). Each procedure's errors are data: the cases that raise them. + The page section, the probe that measures them and the check that keeps them true are + generated from that data. Hand-written tables cannot be re-verified on a new BETA. - **Measured, not copied.** Every row is observed in twinBASIC and in VB6. Microsoft's pages - are a source of cases to try, never of answers. + are a source of cases to try, never of answers. A statement about VB6 names only what was + run in VB6 (a `Long` array, not "any typed array"). - **Runtimes.** twinBASIC 32-bit and VB6 for everything; twinBASIC 64-bit as well for the operators and the `Math` module only. - **Scope.** `docs/Reference/Default/` (VBA, VB, VBRUN), `docs/Reference/Core/` (statements and operators) and `docs/Reference/Built-In/`. Not `Contributed/`. - **Existing error prose moves into the tables.** A page that states an error in a sentence - ("an *LCID* that is not a valid locale identifier raises error 5") loses the sentence when - its table is written; what the sentence explains beyond the number goes into the row's - *When*. + ("an *LCID* that is not a valid locale identifier raises error 5", "if *compare* is + **Null**, an error occurs") loses the sentence when its table is written; what the sentence + explains beyond the number goes into the row's *When*. - **A difference from VB6 is a bug** (the owner's rule for bugs generally): the table states what twinBASIC does, a NOTE on the page states what VB6 does, and the difference is queued in [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md). Where VB6 has no counterpart (LongLong, twinBASIC - additions), there is nothing to compare and nothing to file on that ground. + additions), there is nothing to compare and nothing to file on that ground. **Ask the owner + when twinBASIC accepts what VB6 refuses**: it may be an extension (2026-10-21: `Join` and + `Filter` of a `Long` or `Double` array are; a NOTE in the page's prose, twinBASIC-only quiet + cases, no `vb6:` row). +- **Content that twinBASIC and VB6 both contradict is the page's fault** (2026-10-21): it is + commented out, not deleted, as ``, outside any table, so + the VBA text is kept for docs that will one day switch between VB6, VBA and twinBASIC + (WIP.Authoring.md, *twinBASIC deviations from VBA to flag*). The first were the + `vbUseCompareOption` rows of six *compare* tables and two "**Null** is returned" sentences of + String. ## What a page gets -A `## Common Errors` section (owner, 2026-10-21, on a reader's request) after the page's main text and before `## Example`: +A `## Common Errors` section (owner, 2026-10-21, on a reader's request) after the page's main +text and before `## Example`: | Error | When | |---|---| @@ -37,46 +63,61 @@ A `## Common Errors` section (owner, 2026-10-21, on a reader's request) after th | [94 (`&H5E`)](tb:Errors#94), *Invalid use of Null* | **Mid$** is given a *string* that is **Null** | - The number is given in decimal and in VB hexadecimal, as a 32-bit **Long** (owner, - 2026-10-20). The heading takes the level of the page's Example heading: `###` on the - VBA and Core pages, which the build promotes to `##`. -- The number links to its row on the central page (below); the message is the runtime's own, - as `Err.Description` gives it when the error is raised (owner, 2026-10-20), in italics as the rest of the site writes messages. + 2026-10-20): -2147467259 (`&H80004005`), 5 (`&H5`), in the table and in the VB6 NOTE alike. +- The number links to its row on the central page as `tb:Errors#` (owner, 2026-10-21): the + central page has `shortcut: Errors` in its front matter and its anchors are the bare numbers. + How shortcuts work: WIP.Authoring.md, *Cross-section linking*. +- The message is `Err.Description` of the raised error (owner, 2026-10-20), in italics. - *When* is a condition on the arguments or the state, in the reader's terms --- not the - probe's expression. + probe's expression. Name the form when only some forms raise (**Mid$**, **LCase$**). - **What is listed**: what the procedure raises from its own arguments and state, and **any error a Null argument raises**, in the procedure or in converting the argument (owner, - 2026-10-21: which parameters convert is invisible to the reader). - **What is not**: errors anything can raise (7 *Out of memory*, 28 *Out of stack space*), - errors in converting an argument to the type its page gives for the parameter (13 for a - value of the wrong kind, 6 for a number outside that type's range, 2147483648 for a - **Long**), and compile-time diagnostics (TB codes). The central page says this once. A 6 - for a value inside the documented type's range is listed (`ChrB(256)`: the page says - **Long**). -- A procedure that raises nothing gets no section, not an empty one --- unless its absence is - surprising enough to state (a function that returns a value where VB6 raises). + 2026-10-21: which parameters convert is invisible to the reader; `Filter(Null, "a")`'s 13 is + listed). + **What is not**: errors anything can raise (7 *Out of memory*, 14 *Out of string space*, 28 + *Out of stack space*), errors in converting an argument to the type its page gives for the + parameter (13 for a value of the wrong kind, 6 for a number outside that type's range, such + as 2147483648 for a **Long**), and compile-time diagnostics (TB codes). The central page says + this once. A 6 for a value inside the documented type's range is listed (`ChrB(256)`: the + page says **Long**). +- The heading takes the level of the page's Example heading: `###` on the VBA and Core pages, + which the build promotes to `##`; `##` on a page without an Example or See Also heading. +- Rows of a page that raise the same number are one table row, their *When* cells joined with + `
` in the order of the data. +- A row where twinBASIC raises nothing and VB6 raises (`error: 0`, below) is a NOTE only. +- A procedure that raises nothing gets no section --- unless its absence is surprising enough + to state (`none:`). - The section is written between markers, so regenerating it replaces only it. -## The central page (batch 1, built) +## The central page [`docs/Reference/Run-time-Errors.md`](docs/Reference/Run-time-Errors.md), permalink -`/Reference/Run-time-Errors`, under *Reference Section* in the nav and in the book's -*The Core Language* part after Compiler Constants. Hand-written: what the sections list and -leave out (7, 28, 13 and 94 from converting an argument, compile-time TB diagnostics), how a -number is written, the message rule, and a NOTE on `Err.Raise` of a number without a message. -Generated by `errors.mjs write`, between `` and ``: -one table row per number in `messages.yml`, with its anchor, the number itself (`#5`, `#-2147467259`), -its message and *Listed by*, links to the pages whose tables name it (twinBASIC's number, -not the `vb6:` one), sorted by title. VB's numbers come first in order, then the HRESULTs in -the order of their hexadecimal. Each section's numbers, in the table and in the VB6 NOTE, link -to their anchor. `check_errors.mjs` checks the index as it checks the sections. - -What was measured (BETA 1010, `errors.mjs messages`): +`/Reference/Run-time-Errors`, `shortcut: Errors`, under *Reference Section* in the nav and in +the book's *The Core Language* part after Compiler Constants. Hand-written: what the sections +list and leave out, how a number is written, the message rule, and a NOTE on `Err.Raise` of a +number without a message. Generated by `errors.mjs write`, between `` +and ``: + +- a table, one row per number in `messages.yml`: the number, anchored by itself (`#5`, + `#-2147467259`), its hexadecimal, its message (and `Error$`'s where it differs), and + *Listed by*, a link (*22 pages*) to the number's line below, or `---`; +- a section *Pages that list each error*: a line per number some table names (twinBASIC's + number, not the `vb6:` one), anchored `#listed-`, its pages comma-separated and sorted by + title (owner, 2026-10-21). + +VB's numbers come first in order, then the HRESULTs in the order of their hexadecimal. +`check_errors.mjs` checks the index as it checks the sections, and that the page keeps its +shortcut. + +What `errors.mjs messages` found (BETA 1010): - **Every number from 1 to 65535 with a message of its own: 88, and twinBASIC 32-bit and VB6 agree on every one**, for `Error$(n)` and for `Err.Description` after `Err.Raise n`. They are all in `messages.yml`, whether or not a table names them yet. - **HRESULTs** are in `messages.yml` only when the data names one: -2147467259 so far. For - these the two messages differ, and the file says `{ raised, error }`. + these the two messages differ, and the file says `{ raised, error }`. The sections show + `Err.Description` (owner: settled); showing `Error$(n)` instead would be a one-line change in + `renderSection` (`.raised` to `.error`). - **Differences from VB6 that batch 8 (`ErrObject`) should take up**, none queued yet: `Err.Raise n` of a number from 1 to 65535 without a message of its own leaves `Err.Description` empty in twinBASIC below 747, and from 747 up gives the Windows system @@ -90,37 +131,28 @@ What was measured (BETA 1010, `errors.mjs messages`): `Error$` of a few HRESULTs is VB's message (`Error$(&H80070057)` is *Invalid procedure call or argument*), VB6's is generic. -### The message: Err.Description, with Error$ beside it (owner: settled, 2026-10-20) - -The sections show `Err.Description`, what a handler sees. For VB's numbers it is `Error$(n)` -too; the central page shows `Error$(n)` beside it where they differ, which so far is only the -HRESULTs (*Unspecified error*; **Error** gives *Application-defined or object-defined error*). -Showing `Error$(n)` in the sections instead would be a one-line change in `renderSection` -(`.raised` to `.error`). - -## Tooling (batch 0, built) +## Tooling -Three files and a folder. Their reference is on the Tools pages: +Their reference is on the Tools pages: [`errors.mjs`](docs/Documentation/Tools-Defects.md#errorsmjs) and [`check_errors.mjs`](docs/Documentation/Tools-Toolchain-Tests.md#check_errorsmjs). - `errors/` --- the data: one YAML file per area (`VBA.Strings.yml`, `Core.yml`, ...) and `messages.yml`, each error number's message. -- `scripts/errors.mjs run` --- builds one probe from the data per runtime, runs it, compares - each case with the data. twinBASIC 32-bit and VB6 by default, win64 for a file that says - `win64: true` or with `--runtime win64`. By hand only: it needs twinBASIC and VB6. A - case a compiler refuses is reported and dropped, and the probe rebuilt (VB6 stops at its - first error, so one rebuild per refused case there). `generate ` writes the probes - without running them; `--keep` keeps the ones `run` built. -- `scripts/errors.mjs write` --- writes each page's section from the data, and the central - page's index (batch 1). -- `scripts/errors.mjs messages` --- measures `messages.yml` in twinBASIC 32-bit and VB6 - (batch 1): `Error$(n)` and, after `Err.Raise n`, `Err.Description`, for every number from 1 - to 65535 whose `Error$` is not generic and every other number the file lists. One probe per - runtime, a loop. By hand only, like `run`. +- `node scripts/errors.mjs run [] [--only ]` --- builds one probe from the data + per runtime, runs it, and compares each case with the data. twinBASIC 32-bit and VB6 by + default, win64 for a file that says `win64: true` or with `--runtime win64`. By hand only: + it needs twinBASIC and VB6. A run of the whole Strings file takes a few minutes. A case a + compiler refuses is reported and dropped, and the probe rebuilt. `generate ` writes the + probes without running them; `--keep` keeps the ones `run` built. +- `node scripts/errors.mjs write` --- writes each page's section and the central page's index. +- `node scripts/errors.mjs messages` --- measures `messages.yml` in twinBASIC 32-bit and VB6. - `scripts/check_errors.mjs` --- the gate in `test.bat` and CI: data in order, every section - what `write` makes of it, no section without data. No runtime needed. -- `scripts/lib/errors-data.mjs` --- what the two share. + what `write` makes of it, no section without data, the index current, the central page's + shortcut. No runtime needed. A data problem makes `run` exit 2 with only "the data has + problems"; `check_errors.mjs` names it. +- `scripts/lib/errors-data.mjs` --- what the two share (`renderSection`, `renderIndex`, + `errorAnchor`, `CENTRAL_SHORTCUT`, ...). How a probe runs a case: a `Private Sub C()` of its own, `Dim Result As Variant`, the case's `decl` lines, `On Error Resume Next`, its `setup` lines (an error there is reported as @@ -146,89 +178,91 @@ pages: - Mid("abc", 1, -1) - error: 94 when: "**Mid$** or **MidB$** is given a *string* that is **Null**" - cases: [Mid$(Null, 1), MidB$(Null, 1)] + cases: + - Mid$(Null, 1) + - MidB$(Null, 1) quiet: # cases that must raise nothing; never shown on the page - Mid(Null, 1) - Mid("abc", 4) + Reference/Default/VBA/Strings/InStr.md: + rows: + - error: 0 # twinBASIC raises nothing, VB6 raises: a NOTE, no table row + when: "**InStr** is given a *compare* of 2 (**vbDatabaseCompare**)" + vb6: 5 # what VB6 raises instead (0: nothing); writes the NOTE + bug: database-compare-no-error # a bugs/ reproducer, or pending until queued + cases: + - InStr(1, "abc", "a", 2) Reference/Core/ReDim.md: rows: - error: -2147467259 when: an upper bound is less than its lower bound - vb6: 9 # VB6 raises this instead (0: nothing); writes the NOTE - bug: pending # a bugs/ reproducer, or pending until one is queued + vb6: 9 + bug: redim-inverted-bounds-error cases: - { decl: Dim X() As Long, run: ReDim X(0 To -1) } - { decl: Dim X() As Long, run: "ReDim X(5 To 4, 1)" } quiet: - { decl: Dim X() As Long, setup: ReDim X(3), run: ReDim Preserve X(5) } - Reference/Default/VBA/Strings/Len.md: - quiet: # raises nothing: no section - - Len(Null) - - { expr: LenB(CLngLng(1)), only: tb } # twinBASIC syntax: not run in VB6 + Reference/Default/VBA/Strings/Join.md: + quiet: + - decl: Dim A(1) As Long # block form for a case whose text holds commas + expr: Join(A) + only: tb # not run in VB6 (twinBASIC syntax, or an extension) + - { expr: 'StrConv("a", 256)', only: tb } # single quotes around double quotes ``` A case is an expression, or a mapping with `expr` or `run` (statements; a multi-line string for several) and optionally `decl` (`Dim` lines), `setup` (statements before it) and `only: tb`. A row's `win64:` is the number win64 raises where it differs. A row of `error: 0` -with a `vb6:` other than 0 is a condition where twinBASIC raises nothing and VB6 does (batch 2, -`InStr` with `vbDatabaseCompare`): it gets its NOTE (*...; twinBASIC raises none.*) and no table -row, and a page whose rows are all such has a section of NOTEs alone. A page with no -rows gets no section; one with `none: ""` and no rows gets a section holding that -sentence, for an absence of errors worth stating. Quote a case that holds `: `, starts with -a quote, or sits in a `{ }` mapping and holds a comma. `messages.yml` maps each number to its -message, or, where `Err.Description` of the raised error and `Error$(n)` differ, to -`{ raised: , error: }` (batch 1); `run` fails a case whose -`Err.Description` is not the `raised` text. A number a row names must be in the file; a new -HRESULT is added by hand and then measured with `errors.mjs messages`. Rows of a page that -raise the same number are shown as one table row, their *When* cells joined with `
`. - -The section `write` makes, before the page's first Example heading: +needs a `vb6:` other than 0 and a `bug:`; its NOTE reads *When ..., VB6 raises error N, ...; +twinBASIC raises none.* A page with no rows gets no section; one with `none: ""` and +no rows gets a section holding that sentence. `messages.yml` maps each number to its message, +or to `{ raised: , error: }` where they differ; `run` fails a +case whose `Err.Description` is not the `raised` text. A number a row names must be in the +file; a new HRESULT is added by hand and then measured with `errors.mjs messages`. A data page +must have `title:` and `permalink:` in its front matter. -``` - -### Common Errors +**YAML pitfalls**, each met in batch 2: -| Error | When | -|---|---| -| [-2147467259 (`&H80004005`)](tb:Errors#-2147467259), *Unspecified error* | an upper bound is less than its lower bound | +- **A flow list breaks on a comma inside a case**: `cases: [Mid(Null, 1)]` is two items. Use a + block list (`- Mid(Null, 1)`) for any case with a comma; a flow list only for cases with none. +- Quote a case that holds `: `, starts with a quote, or sits in a `{ }` mapping and holds a + comma; in a `{ }` mapping, a case holding double quotes goes in single quotes. -> [!NOTE] BETA 1010 -> When an upper bound is less than its lower bound, VB6 raises error [9 (`&H9`)](tb:Errors#9), *Subscript out of range*, instead. - - -``` - -### Decided while building (owner's answers, 2026-10-20) - -- **The heading takes the page's level for Example** (owner: confirmed): `###` on the VBA and - Core pages, which the build promotes to `##` on a page with `#` and `###` but no `##`; - `##` on a page without an Example or See Also heading. -- **Every number is given in decimal and in VB hexadecimal** (owner), as a 32-bit **Long**: - -2147467259 (`&H80004005`), 5 (`&H5`), in the table and in the VB6 NOTE alike. - `errorNumber` in `scripts/lib/errors-data.mjs` writes it. -- **The message is `Err.Description`, not `Error$(n)`** (owner: settled, "good enough"). For VB's own - numbers the two agree; for an HRESULT they do not: `Error$(-2147467259)` is - *Application-defined or object-defined error*, while the error raised describes itself as - *Unspecified error*, which is what a handler sees. Batch 1 kept `Err.Description` and put - `Error$` beside it on the central page; see *The message* under The central page. -- **Each number links to its row on the central page** (owner: agreed; built in batch 1), in - the table and in the VB6 NOTE, relative to the page's permalink. -- **Rows that raise the same number make one table row** (owner, batch 1): their *When* - conditions joined with `
`, in the order of the data. The data keeps one row per - condition, each with its own cases; only the rendering merges them. -- **A row that differs from VB6 must name its reproducer**, or say `bug: pending` (owner: - agreed); the gate refuses a `vb6:` row without it. -- **Errors from converting an argument are left out**: `Mid("abc", "x")` raises 13 in both - runtimes. Superseded for **Null** in batch 2 (owner, 2026-10-21): `Mid("abc", Null)`'s 94, - from converting *start* to **Long**, is now listed, as every Null argument's error is. - -## Before any batch +## How a batch runs Read first: [WIP.Harness.md](WIP.Harness.md) (`tbbuild`, `tbrun`), [WIP.Build.md](WIP.Build.md) -(gates), [WIP.Authoring.md](WIP.Authoring.md) (page template), and `scripts/lib/vb6.mjs`'s -opening comment (driving VB6). Models to copy from: `scripts/check_package_tests.mjs` and -`scripts/check_code_regions.mjs` tie page text to tests with markers, and `scripts/bug_repro.mjs` -runs projects in both runtimes and compares the result with an expectation. +(gates), [WIP.Authoring.md](WIP.Authoring.md) (page template, *Cross-section linking*, +*twinBASIC deviations from VBA to flag*), and `scripts/lib/vb6.mjs`'s opening comment +(driving VB6). + +1. **List the batch's pages**, and for each read its syntax, parameters, settings tables and + every sentence that mentions an error, **Null** or a limit. +2. **Explore first.** Write a temporary `errors/Explore.yml` that names the real pages and puts + every candidate case under `quiet:`, then `node scripts/errors.mjs run Explore.yml`: each + case's line shows what both runtimes raised, `[tb 5, vb6 5]`. Try, for every parameter: + **Null**; 0, -1, -2; the upper limit and one past it (strings stop at 1,073,741,823; + 2147483647); a value just past a **Long** (2147483648#, which raises 6 and is not listed); + an empty string; a 2-D array, an unallocated one and a typed one for an array parameter; + **every value a settings table on the page lists**, and values between and beyond them + (a *compare* above 1 is an LCID: 1033 is valid, 99999 is not). Probe a boundary from both + sides before writing it. **Delete `Explore.yml` before committing.** +3. **Write the data** in the area's file, one row per condition, quiet cases for each + boundary's passing side, then `node scripts/errors.mjs run --only "\.md"` until + it says *all agree with the data*. +4. **`node scripts/errors.mjs write`**, then edit the pages: remove each error sentence the + table now carries; comment out, as ``, what both runtimes contradict; move a + VB6 claim that the table's NOTE now makes. Check index pages (a module's `index.md`) for the + same claims. +5. **A difference from VB6**: a row with `vb6:` (or `error: 0`), `bug: pending`, and either + queue it yourself (`node scripts/bug_repro.mjs new `, the entry in + BUGS-TO-REPORT.md, then the slug in the data) or list it for the owner. **Where twinBASIC + accepts what VB6 refuses, ask the owner** whether it is an extension before queuing. +6. **Gates**: `build.bat` (0 broken), `node scripts/check_errors.mjs`, `test.bat`; + `check.bat` once per batch. Commit on staging (the owner's choice for this work), one-line + message, no Co-Authored-By trailer. **When another session shares the checkout, `git add` + by path**, never `-A`, so its files are not swept into the commit. +7. Update *Where things stand* and the batch's row below, and list the questions for the owner. What the probes must respect (BETA 1010; `errors.mjs` already does all of it): @@ -237,111 +271,54 @@ What the probes must respect (BETA 1010; `errors.mjs` already does all of it): IDE reads every module-level variable as 0 once one initializer reads another module's variable (bug `module-init-reads-other-module-ide`), and may differ in other ways. - **A `tbrun --exe` probe brings its own `Sub Main`**, without the console template's - `tbxMain.twin`; two `Main`s make the build fail with *'Main' is ambiguous*. A project has - one `[RunAfterBuild]` at most. + `tbxMain.twin`; two `Main`s make the build fail with *'Main' is ambiguous*. - **Never `MsgBox`, `InputBox` or an unhandled error** in a probe: each opens a modal box on a - private desktop. `tbrun --exe` closes them and reports them, and `vb6run` refuses a source - that calls the first two. Every case runs under `On Error Resume Next`. + private desktop. Every case runs under `On Error Resume Next`. - **Keep the generated code plain.** A module-level or class-field initializer cannot call a procedure of the project, and a class field that reads another field fails the build (bug - `class-field-init-reads-field`). Assign in code instead. + `class-field-init-reads-field`). - **VB6 cannot compile twinBASIC syntax** (`LongLong`, `Return` in a function, inline `Dim x =`, - generics). Cases for VB6 are written in the common subset; a twinBASIC-only case says so in - the data and is not run in VB6. + generics), and has no counterpart for twinBASIC additions such as StrConv's `vbUTF8` (256). + A twinBASIC-only case says `only: tb`. - **One case can stop a whole project**: a compile error anywhere fails all of it. `run` builds a failing project again without the case named in the error. -- A new gate in `test.bat` must also be added to the gate list in - `docs/Documentation/Tools.md` (`check_gate_lists.mjs` fails otherwise), and a new tool gets - its section on the Tools pages under `docs/Documentation/`. - -## Where things stand (2026-10-21) - -Batches 0 and 1 are done, reviewed and on staging. **Batch 2 is done, on staging**: every page -of the Strings module has its data (339 cases, all agreeing in twinBASIC 32-bit and VB6), and -every one but Len and the index has a section. The owner reviewed the first six on 2026-10-21 -and settled the style (*Batch 2: what the first six pages found*). Left open: four rows still -`bug: pending` until the two last bugs are queued (*Batch 2: the rest of the module*). - -**Batch 2, the VBA `Strings` pilot.** List the module's pages -(`docs/Reference/Default/VBA/Strings/`), keep those that can raise, and for each: read the page -for error prose and for cases to try, write its entry in `errors/VBA.Strings.yml` (Mid and Len -are there already), `node scripts/errors.mjs run VBA.Strings.yml --only ` until twinBASIC -and VB6 agree with the data, `node scripts/errors.mjs write`, then remove the page's error -sentences that the table now carries. A VB6 difference is a row with `vb6:` and a reproducer -(`node scripts/bug_repro.mjs new `; `bug: pending` until it exists). Check every -setting a page lists (*compare* and the like) in both runtimes, and comment out what both -refuse (WIP.Authoring.md, *twinBASIC deviations from VBA to flag*). - -## Batch 2: what the first six pages found - -Measured on BETA 1010, twinBASIC 32-bit and VB6 agreeing unless said: - -- **`InStr` and `InStrRev` refuse `vbUseCompareOption` (-1)** with error 5; their pages listed - it as a setting, and the row is now a `` comment on both. A *compare* above 1 is read as an - LCID (3, 4, 1024, 1033 pass; 255, 65536, 99999 raise 5). `InStrB` ignores *compare*. -- **Two `InStr` differences from VB6, `bug: pending`:** VB6 raises 5 for *compare* 2 - (`vbDatabaseCompare`) where twinBASIC takes it as an LCID; and VB6 does not check the LCID - when *string1* or *string2* is zero-length, where twinBASIC raises 5. `InStrRev` checks it in - both. Not yet queued in BUGS-TO-REPORT.md. -- `Left`, `Right` (not the B forms) raise 5 above 1,073,741,823, as `Mid` does; `InStr` raises - 5 for a *start* above 1,073,741,824, `InStrRev` and `InStrB` do not. -- `Chr` raises 5 outside 0--255 on this machine (not a DBCS code page); `ChrW` outside - -32,768--65,535; **`ChrB` raises 6, *Overflow*, outside 0--255**. -- Every **Null** argument of the six raises 94, and every number beyond a **Long** raises 6, in - both runtimes. - -The owner's answers (2026-10-21): - -1. **An error from a Null argument is listed**, whatever raises it; 13 for a value of the - wrong kind and 6 for a number outside the documented type are not (*What a page gets*). - The pages' "if ... is **Null**, an error occurs" sentences go. -2. The two `InStr` rows on *compare*, and the NOTE of an `error: 0` row, read well; the NOTE - was checked again against a run and the built page. -3. Content both runtimes contradict is the page's fault: commented out, not deleted, so the - VBA text is kept for docs that will switch between VB6, VBA and twinBASIC. - -## Batch 2: the rest of the module - -Found on BETA 1010, both runtimes agreeing unless said: - -- **`vbUseCompareOption` (-1) raises 5 in Filter, Replace, Split and StrComp too**; their rows - are `` comments, and the module's index no longer offers it. -- **`vbDatabaseCompare` (2)**: Filter and StrComp accept it as InStr does, where VB6 raises 5 - (`database-compare-no-error`, with InStr's); Replace and Split accept it in both. -- **`StrComp` checks no LCID when a string is Null or zero-length** (`StrComp("a", "", 99999)` - returns), in both runtimes; Replace and Split check it on a zero-length string, in both. -- `FormatCurrency` raises 5 for *numDigitsAfterDecimal* above 255; FormatNumber and - FormatPercent take 2147483647. `String` and `Space` raise 5 above 1,073,741,823 and 14 - (*Out of string space*, not listed) at it. -- `Filter(Null, "a")` and `Join(Null)` raise 13, listed under the Null rule. -- The String page's two "**Null** is returned" sentences are `` comments: - `String(Null, "a")` raises 94 in both, and `String(3, Null)` in twinBASIC only. - -The owner's answers (2026-10-21): - -- `String(3, Null)` (twinBASIC raises 94, VB6 returns **Null**) and `StrConv("a", 4, 1041)` - (vbWide, vbKatakana... with an East Asian *LCID* on a system whose locale is not East - Asian: twinBASIC raises 5, VB6 converts) **are bugs**, queued by a separate session. -- `Join` and `Filter` taking a `Long` or `Double` array (VB6 raises 5 and 13 for a `Long` one) - **is a twinBASIC extension**, not a bug: a NOTE in each page's prose, twinBASIC-only quiet - cases in the data, no `vb6:` row. -- **The pages that list a number moved out of the table** into a section after it, *Pages that - list each error*: a line per number that some page lists, its pages comma-separated in - alphabetical order, anchored `#listed-`. The table's *Listed by* cell is a link to that - line (*22 pages*), or `---`. - -## What batch 2 needs to know (from batch 1) - -- A row whose number is not in `messages.yml` fails the gate. VB's 88 numbers are all there; - an HRESULT is added by hand as `{ raised, error }` and checked with - `node scripts/errors.mjs messages`. -- A data page must have `title:` and `permalink:` in its front matter: the links to and from - the central page are built from them. -- Two rows with the same number are the way to give two conditions their own cases: the table - shows them as one row. -- *Listed by* on the central page is one table cell per number. Error 5 will be listed by - hundreds of pages; once batch 2 has added the Strings pages, look at that cell with the owner - and decide whether the list moves to a section per number. +- **A case that allocates near the string limit raises 14** (`String(1073741823, "a")` in + 32-bit): use it to show that the 5 above the limit is a check of its own, never as a row. + +## Batch 2 findings (VBA `Strings`, BETA 1010) + +Both runtimes agree unless said. The pages and data carry each of these; they are here so a +later batch knows what to look for. + +- **`vbUseCompareOption` (-1) raises 5** in InStr, InStrRev, Filter, Replace, Split and StrComp, + though VBA-Docs lists it: their rows are `` comments, and the module's index no + longer offers it. Omitting *compare* still follows **Option Compare** where the page says so. +- **A *compare* above 1 is an LCID**: 3, 4, 1024, 1033 pass; 255, 65536, 99999 raise 5. + `InStrB` ignores *compare*. +- **`vbDatabaseCompare` (2)**: InStr, Filter and StrComp take it as an LCID where VB6 raises 5 + (`database-compare-no-error`); Replace and Split take it in both. +- **The LCID check and empty strings**: twinBASIC's InStr checks it when a string is + zero-length, VB6's does not (`instr-invalid-lcid-empty-string`); InStrRev, Replace and Split + check it in both; StrComp checks it in neither when a string is **Null** or zero-length. +- **Limits**: Mid, Left, Right (not the B forms), String and Space raise 5 above 1,073,741,823 + characters; InStr raises 5 for a *start* above 1,073,741,824, InStrRev and InStrB do not; + FormatCurrency raises 5 for *numDigitsAfterDecimal* above 255, FormatNumber and + FormatPercent take 2147483647. Chr raises 5 outside 0--255 (on a code page that is not + double-byte), ChrW outside -32,768--65,535, ChrB 6 outside 0--255. +- **Settings ranges** that raise 5: MonthName's *month* outside 1--12; WeekdayName's *weekday* + outside 1--7 and *firstdayofweek* outside 0--7; Format's *firstDayOfWeek* outside 0--7 and + *firstWeekOfYear* outside 0--3; FormatDateTime's *namedFormat* outside 0--4; the Format... + functions' tristates other than -2, -1, 0 and *numDigitsAfterDecimal* below -1; Split's + *limit* and Replace's *count* below -1; StrConv's unlisted values and any sum that combines + vbUnicode, vbFromUnicode, vbUTF8 or vbFromUTF8 with another value. +- **Null**: every $ form raises 94 for a **Null** string where the Variant form returns + **Null**; `String(3, Null)` raises 94 in twinBASIC only (`string-null-character`); the + Format... functions return for a **Null** *expression*. +- **StrConv's East Asian conversions** (vbWide, vbNarrow, vbKatakana, vbHiragana) raise 5 on a + system whose locale is not East Asian; given an East Asian *LCID* VB6 converts and + twinBASIC still raises (`strconv-east-asian-lcid`). +- **Extensions** (owner): `Join` and `Filter` take a `Long` or `Double` array; VB6 raises 5 and + 13 for a `Long` one. ## Batches @@ -350,21 +327,22 @@ each can be run when there is budget. Status is kept here. | # | Batch | Pages (about) | Status | |---|---|---|---| -| 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | done; the owner reviewed the trial sections (Mid, ReDim; Len raises nothing) on 2026-10-20 | -| 1 | The central Run-time errors page | 1 | done; the owner reviewed the page and settled on `Err.Description` (2026-10-20) | -| 2 | **Pilot: VBA `Strings`** --- settles the section's style with the owner before anything scales | 30 | done: 28 sections, reviewed by the owner (2026-10-21) | +| 0 | Tooling: data format, probe generator (twinBASIC and VB6), checker, section writer, the gate | --- | done; reviewed 2026-10-20 | +| 1 | The central Run-time errors page | 1 | done; reviewed 2026-10-20; list of pages per number and `tb:Errors` shortcut 2026-10-21 | +| 2 | **Pilot: VBA `Strings`** --- settled the section's style with the owner | 30 | done: 28 sections, 339 cases; reviewed 2026-10-21 | | 3 | VBA `Conversion` | 28 | not started | | 4 | VBA `Math` (also win64) | 17 | not started | | 5 | VBA `DateTime` | 20 | not started | | 6 | VBA `Information` and `Interaction` | 54 | not started | | 7 | VBA `FileSystem` | 23 | not started | -| 8 | VBA `Collection` and `ErrObject` | 22 | not started | +| 8 | VBA `Collection` and `ErrObject` (and the `Err.Raise` differences under *The central page*) | 22 | not started | | 9 | VBA `Financial` | 14 | not started | | 10 | VBA `HiddenModule`, then `Compilation` and `TbExpressionService` (`Constants` has nothing to raise) | 68 | not started | | 11 | Core file I/O statements (`Open`, `Get`, `Put`, `Input`, ...: errors 52--75) | 15 | not started | -| 12 | Core arrays and control flow (`ReDim`, `Erase`, `Resume`, `Return`, ...) | 15 | not started | +| 12 | Core arrays and control flow (`ReDim`, `Erase`, `Resume`, `Return`, ...); ReDim has data already, in `errors/Core.yml` | 15 | not started | | 13 | Operators: the matrix generator and its pages (also win64) | 25 | not started | | 14 | VB package (37 pages) and the VBRUN pages that document members rather than constants: properties and methods that raise 380, 383, 387 and the like; needs forms | 37 + part of 134 | not started | | 15 | Built-In packages | 195 | not started | -Page counts are the folders' `.md` files on 2026-10-20, index pages included; a batch's first step is to list the pages in it that document something that can raise. +Page counts are the folders' `.md` files on 2026-10-20, index pages included; a batch's first +step is to list the pages in it that document something that can raise.