From 482fe192a2ba28232d8526b80fb2c66232dab5fa Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 4 Oct 2026 18:10:55 +0200 Subject: [PATCH 01/34] bug_repro vb6: report an exe the event log says faulted; a VB6 exe that dies of an access violation exits 0 --- docs/Documentation/Tools.md | 2 +- scripts/bug_repro.mjs | 11 +++++- scripts/lib/vb6.mjs | 72 +++++++++++++++++++++++++++++++++---- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md index 97460cfd4..bd358df75 100644 --- a/docs/Documentation/Tools.md +++ b/docs/Documentation/Tools.md @@ -1102,7 +1102,7 @@ the bug unless `expect.exit` names it. Reproducers run one at a time. `--jobs N` once, each in the IDE on its own port, from `--port` up. `verify` tidies the IDE's registry entries once for all of them, as [`check_examples.mjs`](#check-examples) does. -Exit codes: **0** done --- a project that compiled, built or ran as it should, or, for `verify`, every reproducer that can be run on its own still reproduces; **1** a finding: the project has errors, or its build failed after a clean compile, or, for `vb6`, VB6 refused the project, or, for `verify`, at least one reproducer no longer reproduces; **2** a refused command line, a `repro.json` that is not valid, no IDE, a project that could not be packed, a harness that failed, or a crash; for `vb6`, no VB6, a reproducer with no `vb6/` folder, a project that has no `Probe.vbp` or calls `MsgBox` or `InputBox`, or VB6 failing to build it; for `verify`, a lane's harness failed; for `file`, an entry that is missing, ambiguous or marked unreadably, or a `bugs/filed/` already there, with nothing changed; **3** `new` found `bugs/` or `bugs/filed/` already there; **4** the compile never settled; **5** the project crashes the compiler; **6** `run`: the probe printed nothing; for `vb6`, the exe wrote no `out.txt`, or an empty one; **7** `run`: the probe ended before it returned; **8** `run --exe`, and `vb6`: the exe exited with a code other than 0, or was still running after `--timeout`; **9** `run` and `vb6`: a picture that `images` names was not written, or could not be read. +Exit codes: **0** done --- a project that compiled, built or ran as it should, or, for `verify`, every reproducer that can be run on its own still reproduces; **1** a finding: the project has errors, or its build failed after a clean compile, or, for `vb6`, VB6 refused the project, or, for `verify`, at least one reproducer no longer reproduces; **2** a refused command line, a `repro.json` that is not valid, no IDE, a project that could not be packed, a harness that failed, or a crash; for `vb6`, no VB6, a reproducer with no `vb6/` folder, a project that has no `Probe.vbp` or calls `MsgBox` or `InputBox`, or VB6 failing to build it; for `verify`, a lane's harness failed; for `file`, an entry that is missing, ambiguous or marked unreadably, or a `bugs/filed/` already there, with nothing changed; **3** `new` found `bugs/` or `bugs/filed/` already there; **4** the compile never settled; **5** the project crashes the compiler; **6** `run`: the probe printed nothing; for `vb6`, the exe wrote no `out.txt`, or an empty one; **7** `run`: the probe ended before it returned; **8** `run --exe`, and `vb6`: the exe exited with a code other than 0, or was still running after `--timeout`, or, with `vb6`, the Application event log records that it faulted (a VB6 exe that dies of an access violation exits with code 0); **9** `run` and `vb6`: a picture that `images` names was not written, or could not be read. ### vb6run.mjs {: #vb6run } diff --git a/scripts/bug_repro.mjs b/scripts/bug_repro.mjs index cbaa18190..cc3fd5240 100644 --- a/scripts/bug_repro.mjs +++ b/scripts/bug_repro.mjs @@ -263,7 +263,7 @@ Exit codes: 6 run, vb6: the probe printed nothing (vb6: no out.txt, or an empty one) 7 run: the probe ended before it returned 8 run --exe, vb6: the exe exited with a code other than 0, or was still running - after --timeout + after --timeout; vb6: or the event log records that the exe faulted 9 run, vb6: a picture that repro.json's "images" names was not written, or could not be read`; @@ -1114,6 +1114,15 @@ async function buildAndRunVb6(slug, exe, names, imagesDir) { return 6; } if (r.status !== 0) { + // A VB6 exe that faults exits with code 0; the event log's record is what says it faulted. + if (r.fault) { + const { code, module, offset } = r.fault; + console.error( + `vb6: ${REPRO_PROJECT}.exe ended with exception 0x${code} in ${module} at offset 0x${offset} ` + + `(the Application event log's record; its exit code was ${r.status})`, + ); + return 8; + } console.error(`vb6: ${REPRO_PROJECT}.exe exited with code ${r.status}`); return 8; } diff --git a/scripts/lib/vb6.mjs b/scripts/lib/vb6.mjs index 466caec40..6daef13f3 100644 --- a/scripts/lib/vb6.mjs +++ b/scripts/lib/vb6.mjs @@ -66,6 +66,14 @@ // sample, as the twinBASIC dispatcher does. A twinBASIC `Declare PtrSafe` is not VB6: // declaresForVb6 drops PtrSafe and reads LongPtr as Long. // +// 11. A VB6 EXE THAT DIES OF AN ACCESS VIOLATION EXITS WITH CODE 0. Windows Error +// Reporting writes an "Application Error" record (event 1000) to the Application log, +// naming the exe's path and the exception code, but GetExitCodeProcess reads 0, so the +// exit code says the run succeeded. runRepro looks for that record (recordedFault) and +// reports the fault beside the exit code. A reproducer is built in a folder of its own, +// so the path names one run. (A Probe.exe that called Advise with a null sink on a VB6 +// class's connection point, 2026-10-04: c0000005 in MSVBVM60.DLL, exit code 0.) +// // The probes for the rewrite and the translation are in vb6Probes() at the end. import { execFileSync, spawn } from "node:child_process"; @@ -807,20 +815,59 @@ export function unattended(vbp, on = true) { return `${kept.join("\r\n")}\r\n`; } +const XML_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }; + +/** + * The fault that Windows Error Reporting recorded in the Application log for `exe`, a + * full path, at or after `since` (a Date.now() value), or null when there is none or the + * log cannot be read. See 11 at the top: a VB6 exe that faults exits with code 0, and + * this record is what says it faulted. + * + * @returns {{code: string, module: string, offset: string} | null} + * the exception code, the faulting module and the offset in it, in hex as the record has them + */ +export function recordedFault(exe, since) { + const ms = Math.max(0, Date.now() - since) + 5000; + const query = `*[System[Provider[@Name='Application Error'] and (EventID=1000) and TimeCreated[timediff(@SystemTime) <= ${ms}]]]`; + let xml; + try { + xml = execFileSync("wevtutil", ["qe", "Application", `/q:${query}`, "/f:xml"], { + encoding: "utf8", + windowsHide: true, + }); + } catch { + return null; + } + const want = exe.toLowerCase(); + for (const event of xml.split("")) { + // The record's data are unnamed, in a fixed order: the app's name, version and time stamp, + // the module's name, version and time stamp, the exception code, the offset, the process + // id, its start time, then the app's path. + const data = [...event.matchAll(/([^<]*)<\/Data>/g)].map((m) => + m[1].replace(/&(amp|lt|gt|quot|apos);/g, (_, e) => XML_ENTITIES[e]), + ); + if (data[10]?.toLowerCase() === want) return { code: data[6], module: data[3], offset: data[7] }; + } + return null; +} + /** * Run a reproducer's built exe on a private desktop, inside a kill-on-close job, * for at most `timeoutMs`, and read the out.txt it wrote. `env` adds variables to the exe's * environment, which is the caller's otherwise. * - * @returns {Promise<{lines: string[], status: number|null, timedOut: boolean}>} + * @returns {Promise<{lines: string[], status: number|null, timedOut: boolean, fault: {code: string, module: string, offset: string} | null}>} + * `fault` is what recordedFault found for the exe, whatever its exit code */ async function runReproExe(work, timeoutMs, env = {}) { const outPath = path.join(work, REPRO_OUT); rmSync(outPath, { force: true }); + const exe = path.join(work, `${REPRO_PROJECT}.exe`); + const since = Date.now(); let run; try { run = await launchOnDesktop({ - exe: path.join(work, `${REPRO_PROJECT}.exe`), + exe, desktop: `bugrepro-vb6-${process.pid}`, env: { ...process.env, ...env }, }); @@ -841,15 +888,17 @@ async function runReproExe(work, timeoutMs, env = {}) { } const status = await run.exited; const lines = existsSync(outPath) ? splitLines(decodeAnsi(readFileSync(outPath))) : []; - return { lines, status: timedOut ? null : status, timedOut }; + const fault = timedOut ? null : recordedFault(exe, since); + return { lines, status: timedOut ? null : status, timedOut, fault }; } /** * Build and run the VB6 project in `dir` (a reproducer's vb6/ folder), in a copy * of its sources under the OS temp folder, which is removed unless `keep`. * - * @returns {Promise<{built: boolean, log: string, lines: string[], status: number|null, timedOut: boolean, work: string, kept: boolean}>} - * `lines` is what out.txt held; a VB6 that did not finish building throws + * @returns {Promise<{built: boolean, log: string, lines: string[], status: number|null, timedOut: boolean, fault: {code: string, module: string, offset: string} | null, work: string, kept: boolean}>} + * `lines` is what out.txt held, `fault` the exe's fault from the event log (see 11 at the + * top); a VB6 that did not finish building throws */ export async function runRepro(vb6, dir, { timeoutMs = 30000, keep = false, env = {} } = {}) { const work = mkdtempSync(path.join(tmpdir(), "bugrepro-vb6-")); @@ -862,10 +911,19 @@ export async function runRepro(vb6, dir, { timeoutMs = 30000, keep = false, env if (made.timedOut || made.error) { throw new Error(`VB6 did not finish building${made.error ? `: ${made.error}` : " in time"}\n${made.log}`); } - const result = { built: made.built, log: made.log, lines: [], status: null, timedOut: false, work, kept: keep }; + const result = { + built: made.built, + log: made.log, + lines: [], + status: null, + timedOut: false, + fault: null, + work, + kept: keep, + }; if (!made.built) return result; const ran = await runReproExe(work, Math.min(timeoutMs, 2147483647), env); - return { ...result, lines: ran.lines, status: ran.status, timedOut: ran.timedOut }; + return { ...result, lines: ran.lines, status: ran.status, timedOut: ran.timedOut, fault: ran.fault }; } finally { if (!keep) rmSync(work, { recursive: true, force: true }); } From d9e1385982ba004214d7856193a50bda4dd1e68d Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 4 Oct 2026 18:11:01 +0200 Subject: [PATCH 02/34] BUGS: VB6 sides for four COM entries; Unadvise entry narrowed (VB6 also E_NOINTERFACE); CallByName retry retired (VB6 does the same), its WARNING now a NOTE --- BUGS-TO-REPORT.md | 84 +++++++-------- bugs/advise-nothing-crashes/vb6/Module1.bas | 89 ++++++++++++++++ bugs/advise-nothing-crashes/vb6/Probe.vbp | 8 ++ bugs/advise-nothing-crashes/vb6/Source.cls | 16 +++ bugs/advise-unadvise-hresults/vb6/Module1.bas | 97 ++++++++++++++++++ .../advise-unadvise-hresults/vb6/NotASink.cls | 16 +++ bugs/advise-unadvise-hresults/vb6/Probe.vbp | 9 ++ bugs/advise-unadvise-hresults/vb6/Source.cls | 16 +++ ...callbyname-membernotfound-retried.twinproj | Bin 4643 -> 0 bytes .../repro.json | 10 -- .../src/Settings | 61 ----------- .../src/Sources/Startup.twin | 52 ---------- bugs/gettypeinfo-badindex/vb6/Module1.bas | 73 +++++++++++++ bugs/gettypeinfo-badindex/vb6/Probe.vbp | 8 ++ bugs/gettypeinfo-badindex/vb6/Widget.cls | 17 +++ .../vb6/Module1.bas | 33 ++++++ .../stdole-idispatch-late-bound/vb6/Probe.vbp | 9 ++ .../vb6/Widget.cls | 20 ++++ .../COM-Interfaces/IConnectionPoint.md | 6 +- docs/Reference/COM-Interfaces/IDispatch.md | 5 +- 20 files changed, 456 insertions(+), 173 deletions(-) create mode 100644 bugs/advise-nothing-crashes/vb6/Module1.bas create mode 100644 bugs/advise-nothing-crashes/vb6/Probe.vbp create mode 100644 bugs/advise-nothing-crashes/vb6/Source.cls create mode 100644 bugs/advise-unadvise-hresults/vb6/Module1.bas create mode 100644 bugs/advise-unadvise-hresults/vb6/NotASink.cls create mode 100644 bugs/advise-unadvise-hresults/vb6/Probe.vbp create mode 100644 bugs/advise-unadvise-hresults/vb6/Source.cls delete mode 100644 bugs/callbyname-membernotfound-retried/callbyname-membernotfound-retried.twinproj delete mode 100644 bugs/callbyname-membernotfound-retried/repro.json delete mode 100644 bugs/callbyname-membernotfound-retried/src/Settings delete mode 100644 bugs/callbyname-membernotfound-retried/src/Sources/Startup.twin create mode 100644 bugs/gettypeinfo-badindex/vb6/Module1.bas create mode 100644 bugs/gettypeinfo-badindex/vb6/Probe.vbp create mode 100644 bugs/gettypeinfo-badindex/vb6/Widget.cls create mode 100644 bugs/stdole-idispatch-late-bound/vb6/Module1.bas create mode 100644 bugs/stdole-idispatch-late-bound/vb6/Probe.vbp create mode 100644 bugs/stdole-idispatch-late-bound/vb6/Widget.cls diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index ef05bce1c..9a1e7e374 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -888,7 +888,7 @@ Steps to reproduce the behavior: w.Boom 1 (early bound): error 5, ran 1 time(s), Property Get ran 0 ``` 3. See `Hello` run twice, and `Property Get Prop` run after the `Property Let` that failed. The error from the assignment is `&H80020009` (`DISP_E_EXCEPTION`) where the `Property Let` raised 5. -4. To see the second call, implement `IDispatch` in a `NotDispatchable` class (see `bugs/callbyname-membernotfound-retried/`, which has one, and whose `Invoke` can return another code) whose `Invoke` prints `wFlags`, `pDispParams.cArgs` and whether `pVarResult` is null, and returns a failure code. With `Fail3` returning `DISP_E_TYPEMISMATCH` and `Fail5` raising error 5: +4. To see the second call, implement `IDispatch` in a `NotDispatchable` class whose `Invoke` prints `wFlags`, `pDispParams.cArgs` and whether `pVarResult` is null, and returns a failure code. With `Fail3` returning `DISP_E_TYPEMISMATCH` and `Fail5` raising error 5: ``` o.Fail3 1 Invoke flags=1 cArgs=1 result=null, then Invoke flags=3 cArgs=0 result=set o.Fail3 = 5 Invoke flags=4 cArgs=1 result=null, then Invoke flags=3 cArgs=0 result=set @@ -914,41 +914,6 @@ The second call resembles VB's rule for `o.Member(args)` on a property that retu --- -## `CallByName` calls `Invoke` a second time, with no result, when the first call returns DISP_E_MEMBERNOTFOUND - -**Describe the bug** -`CallByName` on an object whose `IDispatch.Invoke` returns `DISP_E_MEMBERNOTFOUND` calls `Invoke` twice: first with a result variant (`pVarResult` supplied), then again with the same identifier and flags and a null `pVarResult`. A late-bound statement for the same member, `o.Anything`, calls it once. Seen with a class that implements `IDispatch` itself and counts the calls. - -**To Reproduce** -Steps to reproduce the behavior: -1. Open `callbyname-membernotfound-retried.twinproj` (attached as `callbyname-membernotfound-retried.zip`). Its one source file, `Startup.twin`, declares a copy of `IDispatch` (the `stdole` one cannot be called) and a `NotDispatchable` class `Recorder` that implements it. Its `GetIDsOfNames` returns 1 for any name, and its `Invoke` prints its `wFlags`, says whether `pVarResult` is null, counts the call, and returns `DISP_E_MEMBERNOTFOUND` with `Err.ReturnHResult`. -2. Run it. `Main` calls `CallByName o, "Anything", vbMethod` and then `o.Anything`, with `On Error Resume Next`, through `Dim o As Object = New Recorder`: - ``` - Invoke 1: wFlags 1, result supplied - Invoke 2: wFlags 1, result null - CallByName: error 438, Invoke called 2 time(s) - Invoke 1: wFlags 1, result null - o.Anything: error 438, Invoke called 1 time(s) - ``` - -**Expected behavior** -One `Invoke` per `CallByName`, as for the statement form. A method that did its work and then reported `DISP_E_MEMBERNOTFOUND` (a scripting host whose members are resolved inside `Invoke` is an example) runs again, and a call with arguments and side effects is repeated. - -**Desktop:** - - OS: Windows 10 Pro 22H2 (build 19045) - - twinBASIC compiler version: BETA 995 - -**Additional context** -Severity: low. It only shows with an `IDispatch` that returns `DISP_E_MEMBERNOTFOUND` after doing work. - -What does not reproduce it: `CallByName` with `vbGet` or `vbLet` against `DISP_E_TYPEMISMATCH`, `DISP_E_BADPARAMCOUNT`, `E_FAIL`, an error raised in `Invoke`, or success (one `Invoke` each); `Invoke` returning `DISP_E_MEMBERNOTFOUND` to a late-bound statement or to `x = o.Member` (one `Invoke` each). `vbGet`, `vbMethod` and `vbLet` all repeat after `DISP_E_MEMBERNOTFOUND`. - -A separate behaviour, in its own entry, repeats a failed late-bound statement that passes arguments as a property read: see the entry "A late-bound call that passes arguments and fails is issued a second time, without them". The two are not the same: that one is for calls with arguments and needs no `DISP_E_MEMBERNOTFOUND`, and this one is `CallByName` with or without arguments. - - - ---- - ## A late-bound call to a member that does not exist raises &H80020006, and `CallByName` raises &H80004005, where VB6 raises 438 **Describe the bug** @@ -1020,6 +985,12 @@ Steps to reproduce the behavior: **Expected behavior** The call goes through the interface: `GetTypeInfoCount` returns the count (1 for a twinBASIC class), and a wrong number or type of argument is a compile error, as for any other interface method. If `stdole.IDispatch` is meant to be an alias of `Object`, `d.GetTypeInfoCount` should still not compile, or the type should not list four methods that cannot be called. A project's own declaration of the interface, with the same `[InterfaceId]`, calls them correctly. +VB6 refuses the declaration itself. The same program in VB6 (attached as `stdole-idispatch-late-bound-vb6.zip`), with a reference to OLE Automation, does not build: +``` +Compile Error in File '...\Module1.bas', Line 16 : Function or interface marked as restricted, or the function uses an Automation type not supported in Visual Basic +``` +`Dim d As IDispatch` is refused the same way. With `Dim d As Object` in its place, VB6 runs `d.Hello` and raises 438 for the other three statements, so twinBASIC's `stdole.IDispatch` behaves as VB6's `Object` does, apart from the error number (see the entry about `&H80020006` and 438). + **Desktop:** - OS: Windows 10 Pro 22H2 (build 19045) - twinBASIC compiler version: BETA 995 @@ -1029,7 +1000,7 @@ Severity: low, since a project can declare its own copy of the interface, but th What was tried: `GetIDsOfNames` and `Invoke` with zero arguments, and `GetTypeInfo` with a null pointer, each end the run the same way; calls with arguments of the wrong type compile (`d.GetTypeInfoCount "a"`, `d.GetTypeInfo "a", "b", "c"`, `d.Invoke "a", "b", "c", "d", "e", "f", "g", "h"` all compile). An unhandled `Err.Raise 5` in the same harness ends the run the same way, so the silent end is how an unhandled error shows there, not a separate fault. Running the built exe with `--exe` was not possible on this machine (the harness could not start it). - + --- @@ -1053,6 +1024,16 @@ Steps to reproduce the behavior: **Expected behavior** `DISP_E_BADINDEX` (`&H8002000B`), which the `IDispatch::GetTypeInfo` documentation gives for an index that is not valid, for an index the object does not have. `E_UNEXPECTED` (*Catastrophic failure*) is the code for a call made at a time when the object cannot take it, and it reads as a fault in the object. +VB6 does not return `E_UNEXPECTED`. The same calls on a VB6 class, made through `DispCallFunc` (the VB6 project is attached as `gettypeinfo-badindex-vb6.zip`), return `TYPE_E_ELEMENTNOTFOUND` (`&H8002802B`) for every index but 0: +``` +GetTypeInfoCount = 1 (HRESULT 0) +GetTypeInfo(0): HRESULT 0, pointer returned True +GetTypeInfo(1): HRESULT 8002802B, pointer returned False +GetTypeInfo(2): HRESULT 8002802B, pointer returned False +GetTypeInfo(-1): HRESULT 8002802B, pointer returned False +``` +Either `DISP_E_BADINDEX` or VB6's `TYPE_E_ELEMENTNOTFOUND` says that the index names nothing; `E_UNEXPECTED` does not. + **Desktop:** - OS: Windows 10 Pro 22H2 (build 19045) - twinBASIC compiler version: BETA 995 @@ -1060,7 +1041,7 @@ Steps to reproduce the behavior: **Additional context** Severity: minor. A caller that checks for `DISP_E_BADINDEX` to learn that an object has no more type information gets a different code; `GetTypeInfo(0)` and `GetTypeInfoCount` are right. - + --- @@ -1182,16 +1163,18 @@ Steps to reproduce the behavior: **Additional context** Severity: low in practice, since a program rarely advises a null sink, but a COM client written in any language can send one, and the connection point crashes the process that hosts the class instead of refusing the call. -What was tried: an object that is not a sink gives an ordinary error (`E_NOINTERFACE`, `&H80004002`, see the entry about `Advise` and `Unadvise` error codes), so only a null pointer crashes. The address of the crash is the same in the standalone probe and in this project. +What was tried: an object that is not a sink gives an ordinary error (`E_NOINTERFACE`, `&H80004002`), so only a null pointer crashes. The address of the crash is the same in the standalone probe and in this project. + +VB6 crashes too. The same call on the connection point of a VB6 class with an event, made through `DispCallFunc` (the VB6 project is attached as `advise-nothing-crashes-vb6.zip`), ends the exe with an access violation (`0xC0000005` in `MSVBVM60.DLL`, in the Application event log) after it writes `before`. A fix therefore costs no compatibility: no program depends on the crash. - + --- -## `Advise` with a sink that lacks the outgoing interface fails with `E_NOINTERFACE`, and `Unadvise` with a cookie that names no connection succeeds +## `Unadvise` with a cookie that names no connection succeeds, where VB6 returns CONNECT_E_NOCONNECTION **Describe the bug** -On the connection point of a twinBASIC class that has an `Event`, two calls return a different code from the one the COM contract names. `Advise` with a sink that does not answer `QueryInterface` for the outgoing interface fails with `E_NOINTERFACE` (`&H80004002`), where the contract returns `CONNECT_E_CANNOTCONNECT` (`&H80040201`). `Unadvise` with a cookie that names no connection (0 and 99 were tried) succeeds silently and does nothing, where the contract returns an error. +On the connection point of a twinBASIC class that has an `Event`, `Unadvise` with a cookie that names no connection (0 and 99 were tried) succeeds silently and does nothing. The COM contract returns an error, and VB6 returns `CONNECT_E_NOCONNECTION` (`&H80040200`). **To Reproduce** Steps to reproduce the behavior: @@ -1210,17 +1193,26 @@ Steps to reproduce the behavior: ``` **Expected behavior** -`Advise` fails with `CONNECT_E_CANNOTCONNECT`. The Windows SDK page for [IConnectionPoint::Advise](https://learn.microsoft.com/en-us/windows/win32/api/ocidl/nf-ocidl-iconnectionpoint-advise) lists it as "The sink does not support the interface required by this connection point", and says to implementers: "The connection point must query the *pUnkSink* pointer for the correct outgoing interface. If this query fails, this method must return CONNECT_E_CANNOTCONNECT." `Unadvise` reports the bad cookie: the page for [IConnectionPoint::Unadvise](https://learn.microsoft.com/en-us/windows/win32/api/ocidl/nf-ocidl-iconnectionpoint-unadvise) lists `E_POINTER` for "The value in *dwCookie* does not represent a valid connection". It is not `S_OK`. +`Unadvise` reports the bad cookie, as VB6 does. The page for [IConnectionPoint::Unadvise](https://learn.microsoft.com/en-us/windows/win32/api/ocidl/nf-ocidl-iconnectionpoint-unadvise) lists `E_POINTER` for "The value in *dwCookie* does not represent a valid connection", and `CONNECT_E_NOCONNECTION` is the code the `CONNECT_E_` range has for it. It is not `S_OK`. + +The same calls on the connection point of a VB6 class with an event, made through `DispCallFunc` (the VB6 project is attached as `advise-unadvise-hresults-vb6.zip`): +``` +Advise, a sink without the outgoing interface: error=80004002 + cookie=-1 +Unadvise 99: error=80040200 +before Unadvise 0 +``` +`Unadvise 0` comes last because it ends the VB6 exe with an access violation (`0xC0000005` in `MSVBVM60.DLL`). twinBASIC's 0 for that cookie is no worse than VB6's crash; the cookie 99 is the clean comparison. **Desktop:** - OS: Windows 10 Pro 22H2 (build 19045) - twinBASIC compiler version: BETA 995 **Additional context** -Severity: low. A client that tests for `CONNECT_E_CANNOTCONNECT` to tell a refused sink from other failures never sees it, and a client that releases a connection twice, or with a wrong cookie, is told it worked. +Severity: low. A client that releases a connection twice, or with a wrong cookie, is told it worked. -What was tried: a sink that is an ordinary twinBASIC class gives the same `E_NOINTERFACE` whether it has no members or implements `IDispatch` (the outgoing interface's identifier is generated for each build, so a twinBASIC class cannot answer for it). Nothing is connected afterwards, and no event reaches the sink. Calling `Unadvise` a second time with the cookie of the failed `Advise`, which is 0, also returns without an error. - +`Advise` with a sink that does not answer `QueryInterface` for the outgoing interface fails with `E_NOINTERFACE` (`&H80004002`), where the Windows SDK page for [IConnectionPoint::Advise](https://learn.microsoft.com/en-us/windows/win32/api/ocidl/nf-ocidl-iconnectionpoint-advise) says the method "must return CONNECT_E_CANNOTCONNECT" (`&H80040201`). VB6 returns `E_NOINTERFACE` too, so that is not reported here; the reproducer prints it as a control. A sink that is an ordinary twinBASIC class gives the same `E_NOINTERFACE` whether it has no members or implements `IDispatch` (the outgoing interface's identifier is generated for each build, so a twinBASIC class cannot answer for it). Nothing is connected afterwards, and no event reaches the sink. Calling `Unadvise` a second time with the cookie of the failed `Advise`, which is 0, also returns without an error. + --- diff --git a/bugs/advise-nothing-crashes/vb6/Module1.bas b/bugs/advise-nothing-crashes/vb6/Module1.bas new file mode 100644 index 000000000..a9f005544 --- /dev/null +++ b/bugs/advise-nothing-crashes/vb6/Module1.bas @@ -0,0 +1,89 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' VB6 cannot call IConnectionPoint directly, so the vtable is called with DispCallFunc. +' IUnknown: 0 QueryInterface, 1 AddRef, 2 Release. +' IConnectionPointContainer: 3 EnumConnectionPoints, 4 FindConnectionPoint. +' IEnumConnectionPoints: 3 Next. IConnectionPoint: 3 GetConnectionInterface, +' 4 GetConnectionPointContainer, 5 Advise, 6 Unadvise. + +Private Declare Function DispCallFunc Lib "oleaut32.dll" (ByVal pvInstance As Long, ByVal oVft As Long, ByVal cc As Long, ByVal vtReturn As Integer, ByVal cActuals As Long, prgvt As Integer, prgpvarg As Long, pvargResult As Variant) As Long +Private Declare Function IIDFromString Lib "ole32.dll" (ByVal lpsz As Long, lpiid As Any) As Long +Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long) + +Private Const CC_STDCALL As Long = 4 +Private Const VT_I4 As Integer = 3 + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Cases + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub + +' Calls slot Slot of the COM object pObj with up to four 32-bit arguments; returns its HRESULT. +Function CallSlot(ByVal pObj As Long, ByVal Slot As Long, ByVal nArgs As Long, ByVal a0 As Long, ByVal a1 As Long, ByVal a2 As Long, ByVal a3 As Long) As Long + Dim pVft As Long + Dim vals(0 To 3) As Variant + Dim vts(0 To 3) As Integer + Dim ptrs(0 To 3) As Long + Dim vRet As Variant + Dim hr As Long + Dim i As Long + vals(0) = a0: vals(1) = a1: vals(2) = a2: vals(3) = a3 + For i = 0 To 3 + vts(i) = VT_I4 + ptrs(i) = VarPtr(vals(i)) + Next + CopyMemory pVft, ByVal pObj, 4 + vRet = CLng(0) + hr = DispCallFunc(pObj, Slot * 4, CC_STDCALL, VT_I4, nArgs, vts(0), ptrs(0), vRet) + If hr <> 0 Then + CallSlot = hr + Else + CallSlot = CLng(vRet) + End If +End Function + +Sub Cases() + Dim src As New Source + Dim iidContainer(0 To 15) As Byte + Dim pContainer As Long, pEnum As Long, pPoint As Long, fetched As Long + Dim cookie As Long + Dim hr As Long + + IIDFromString StrPtr("{B196B284-BAB4-101A-B69C-00AA00341D07}"), iidContainer(0) + hr = CallSlot(ObjPtr(src), 0, 2, VarPtr(iidContainer(0)), VarPtr(pContainer), 0, 0) + Print #9, "QueryInterface IConnectionPointContainer: error=" & Hex$(hr) + If hr <> 0 Then Exit Sub + + hr = CallSlot(pContainer, 3, 1, VarPtr(pEnum), 0, 0, 0) + Print #9, "EnumConnectionPoints: error=" & Hex$(hr) + If hr <> 0 Then Exit Sub + + hr = CallSlot(pEnum, 3, 3, 1, VarPtr(pPoint), VarPtr(fetched), 0) + Print #9, "Next: error=" & Hex$(hr) & " fetched=" & fetched + If hr <> 0 Or pPoint = 0 Then Exit Sub + + ' Advise with a null sink. The file is closed around the call so the line survives a crash. + Print #9, "before" + Close #9 + cookie = -1 + hr = CallSlot(pPoint, 5, 2, 0, VarPtr(cookie), 0, 0) + Open App.Path & "\out.txt" For Append As #9 + Print #9, "after Advise(Nothing): error=" & Hex$(hr) & " cookie=" & cookie + + If hr = 0 And cookie <> 0 Then + hr = CallSlot(pPoint, 6, 1, cookie, 0, 0, 0) + Print #9, "Unadvise " & cookie & ": error=" & Hex$(hr) + End If + + CallSlot pPoint, 2, 0, 0, 0, 0, 0 + CallSlot pEnum, 2, 0, 0, 0, 0, 0 + CallSlot pContainer, 2, 0, 0, 0, 0, 0 +End Sub diff --git a/bugs/advise-nothing-crashes/vb6/Probe.vbp b/bugs/advise-nothing-crashes/vb6/Probe.vbp new file mode 100644 index 000000000..1e85b094a --- /dev/null +++ b/bugs/advise-nothing-crashes/vb6/Probe.vbp @@ -0,0 +1,8 @@ +Type=Exe +Module=Module1; Module1.bas +Class=Source; Source.cls +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/advise-nothing-crashes/vb6/Source.cls b/bugs/advise-nothing-crashes/vb6/Source.cls new file mode 100644 index 000000000..2ed814eab --- /dev/null +++ b/bugs/advise-nothing-crashes/vb6/Source.cls @@ -0,0 +1,16 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Source" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = True +Option Explicit + +Public Event Ping() diff --git a/bugs/advise-unadvise-hresults/vb6/Module1.bas b/bugs/advise-unadvise-hresults/vb6/Module1.bas new file mode 100644 index 000000000..2f3b37f1d --- /dev/null +++ b/bugs/advise-unadvise-hresults/vb6/Module1.bas @@ -0,0 +1,97 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' VB6 cannot call IConnectionPoint directly, so the vtable is called with DispCallFunc. +' IUnknown: 0 QueryInterface, 1 AddRef, 2 Release. +' IConnectionPointContainer: 3 EnumConnectionPoints, 4 FindConnectionPoint. +' IEnumConnectionPoints: 3 Next. IConnectionPoint: 3 GetConnectionInterface, +' 4 GetConnectionPointContainer, 5 Advise, 6 Unadvise. + +Private Declare Function DispCallFunc Lib "oleaut32.dll" (ByVal pvInstance As Long, ByVal oVft As Long, ByVal cc As Long, ByVal vtReturn As Integer, ByVal cActuals As Long, prgvt As Integer, prgpvarg As Long, pvargResult As Variant) As Long +Private Declare Function IIDFromString Lib "ole32.dll" (ByVal lpsz As Long, lpiid As Any) As Long + +Private Const CC_STDCALL As Long = 4 +Private Const VT_I4 As Integer = 3 + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Cases + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub + +' Calls slot Slot of the COM object pObj with up to four 32-bit arguments; returns its HRESULT. +Function CallSlot(ByVal pObj As Long, ByVal Slot As Long, ByVal nArgs As Long, ByVal a0 As Long, ByVal a1 As Long, ByVal a2 As Long, ByVal a3 As Long) As Long + Dim vals(0 To 3) As Variant + Dim vts(0 To 3) As Integer + Dim ptrs(0 To 3) As Long + Dim vRet As Variant + Dim hr As Long + Dim i As Long + vals(0) = a0: vals(1) = a1: vals(2) = a2: vals(3) = a3 + For i = 0 To 3 + vts(i) = VT_I4 + ptrs(i) = VarPtr(vals(i)) + Next + vRet = CLng(0) + hr = DispCallFunc(pObj, Slot * 4, CC_STDCALL, VT_I4, nArgs, vts(0), ptrs(0), vRet) + If hr <> 0 Then + CallSlot = hr + Else + CallSlot = CLng(vRet) + End If +End Function + +Sub Report(ByVal What As String, ByVal hr As Long) + Note What & ": error=" & Hex$(hr) +End Sub + +Sub Note(ByVal Text As String) + Print #9, Text + ' Close and reopen so every line is on disk even if the next call crashes the exe. + Close #9 + Open App.Path & "\out.txt" For Append As #9 +End Sub + +Sub Cases() + Dim src As New Source + Dim sink As New NotASink + Dim iidContainer(0 To 15) As Byte + Dim pContainer As Long, pEnum As Long, pPoint As Long, fetched As Long + Dim cookie As Long + Dim hr As Long + + IIDFromString StrPtr("{B196B284-BAB4-101A-B69C-00AA00341D07}"), iidContainer(0) + hr = CallSlot(ObjPtr(src), 0, 2, VarPtr(iidContainer(0)), VarPtr(pContainer), 0, 0) + Report "QueryInterface IConnectionPointContainer", hr + If hr <> 0 Then Exit Sub + + hr = CallSlot(pContainer, 3, 1, VarPtr(pEnum), 0, 0, 0) + Report "EnumConnectionPoints", hr + If hr <> 0 Then Exit Sub + + hr = CallSlot(pEnum, 3, 3, 1, VarPtr(pPoint), VarPtr(fetched), 0) + Report "Next (fetched=" & fetched & ")", hr + If hr <> 0 Or pPoint = 0 Then Exit Sub + + cookie = -1 + hr = CallSlot(pPoint, 5, 2, ObjPtr(sink), VarPtr(cookie), 0, 0) + Report "Advise, a sink without the outgoing interface", hr + Print #9, " cookie=" & cookie + + hr = CallSlot(pPoint, 6, 1, 99, 0, 0, 0) + Report "Unadvise 99", hr + ' Unadvise 0 ends the VB6 exe on the spot, so it is last and flagged first. + Note "before Unadvise 0" + hr = CallSlot(pPoint, 6, 1, 0, 0, 0, 0) + Report "Unadvise 0", hr + + CallSlot pPoint, 2, 0, 0, 0, 0, 0 + CallSlot pEnum, 2, 0, 0, 0, 0, 0 + CallSlot pContainer, 2, 0, 0, 0, 0, 0 +End Sub diff --git a/bugs/advise-unadvise-hresults/vb6/NotASink.cls b/bugs/advise-unadvise-hresults/vb6/NotASink.cls new file mode 100644 index 000000000..7be037462 --- /dev/null +++ b/bugs/advise-unadvise-hresults/vb6/NotASink.cls @@ -0,0 +1,16 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "NotASink" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = True +Option Explicit + +Public Value As Long diff --git a/bugs/advise-unadvise-hresults/vb6/Probe.vbp b/bugs/advise-unadvise-hresults/vb6/Probe.vbp new file mode 100644 index 000000000..1fee81151 --- /dev/null +++ b/bugs/advise-unadvise-hresults/vb6/Probe.vbp @@ -0,0 +1,9 @@ +Type=Exe +Module=Module1; Module1.bas +Class=Source; Source.cls +Class=NotASink; NotASink.cls +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/advise-unadvise-hresults/vb6/Source.cls b/bugs/advise-unadvise-hresults/vb6/Source.cls new file mode 100644 index 000000000..2ed814eab --- /dev/null +++ b/bugs/advise-unadvise-hresults/vb6/Source.cls @@ -0,0 +1,16 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Source" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = True +Option Explicit + +Public Event Ping() diff --git a/bugs/callbyname-membernotfound-retried/callbyname-membernotfound-retried.twinproj b/bugs/callbyname-membernotfound-retried/callbyname-membernotfound-retried.twinproj deleted file mode 100644 index eb1f1f4d5b0e14412815499c7d68435f7cc487d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4643 zcmds5TTkOg6b{=f+49)8N*!5f3o40B5?Dam2gh-CtrCa=%T^+*m19qmS)3VbW(=f= zpVQw~slTG<_>$NxY!`K_O64Iqb1rk{o9|qlAAW!O=StX)X2WcmW(^*)4?BOtCDUx}8k=v|s(r~WJx$5Nnz$XN zB2W8MTSRl>Tx-e$MU)OiNbBxve!;~RU#k-MJBdf+CDjO?%g4e7y!A$VKA>X~`I^BJ zu@pHJe6o430qCB2Az`OzovUtHj~mJ@`^T|`+9$K{vef@X09iDJske9zqV12NvW<5f5GCJG(k zeNE`%KoLgL_oO#VqzHN8NYR-mfqHSM^Sz5g=h~-H@dk>**pd@R%7Psr?(kqqn6>I! z^+@QPHQp$s#11{B$bkAn22`eZ52+T?8RZ&+&y|s4Xpdy-$D@$>1oL=thJ`F+DH5(^ z{O%L!M;_X}e%C=73vE;9^OMgM0z1qW^ zH_h$GALYMKs*eJ&3~vcRht0?`@yQF49UHF|IR}X-{mgmZ_E;z=F)3)jRz;8P=bewHQqGh!T&tfb*;_6W;cM5g5mmtA+f zQK(>$j$6dwaTtP{q2eeCp}!khBfyOwOn?UDKod27A|3an|IT^Wb9!xOaCCgyJ$`-E zxpVQD?@1-YjPHpchRS9|EY-@&RfqbaCn*8;oOolJw8R3#V5T6(1xzC}Y&HyHh0IgL z80fGkE^$x+78)e05gn?aXL_0mUMRPDmqQOxv!BV$GAY@o-ctB|4AE|*ieH7!k2VcNRMlm0hX^=2Y)>CeR zm}oYm8*0;7Zh`i8NWDV*xxu39~q#Td| z;dym+6_~QRYGgq|ZG=~b=0{%{k|4nZXH)QD!dc;49_;aWRR%+;IYH@mIjFnzOV)O9#2nC!OhGtqR`Y}{v}=ChFq-30E`Vdm?S zO5|0~doLt-Y@|WutBfbBCPaFRk`f@@9kadH=$N&RZMAEyX4k3h?>4t<+uQqan=PyL zys?)9!jvs$5inV1Z-B5@Mh1k7`3O6nsOO$$k>#kJ+sD1Wd*}>E-?D$PUOI0HzTma3 z6W1m+a@K~4WdkRu586At%7~=XsSi_DllMR!BBP*GY zfq%OMbnoxrPbgnqeG68LE}*iGa+c-{ToO86iqoGT(x#6j%0~wbbD?$RNses^6fm2N zq**5g2lrYvpxq~p=sf+(CXj+KzCl6C7T~9V<+p;j^S9L`Nev0VGC 0 Then + CallSlot = hr + Else + CallSlot = CLng(result) + End If +End Function + +Private Sub TryIndex(ByVal Ptr As Long, ByVal Index As Long) + Dim ti As Long + Dim hr As Long + ti = 0 + hr = CallSlot(Ptr, 4, 3, Index, 0, VarPtr(ti)) + Print #9, "GetTypeInfo(" & Index & "): HRESULT " & Hex(hr) & ", pointer returned " & (ti <> 0) + If ti <> 0 Then hr = CallSlot(ti, 2, 0, 0, 0, 0) +End Sub + +Private Sub Cases() + Dim w As Widget + Dim o As Object + Dim p As Long + Dim count As Long + Dim hr As Long + Set w = New Widget + Set o = w + p = ObjPtr(o) + hr = CallSlot(p, 3, 1, VarPtr(count), 0, 0) + Print #9, "GetTypeInfoCount = " & count & " (HRESULT " & Hex(hr) & ")" + TryIndex p, 0 + TryIndex p, 1 + TryIndex p, 2 + TryIndex p, -1 +End Sub diff --git a/bugs/gettypeinfo-badindex/vb6/Probe.vbp b/bugs/gettypeinfo-badindex/vb6/Probe.vbp new file mode 100644 index 000000000..cd40da3dd --- /dev/null +++ b/bugs/gettypeinfo-badindex/vb6/Probe.vbp @@ -0,0 +1,8 @@ +Type=Exe +Module=Module1; Module1.bas +Class=Widget; Widget.cls +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/gettypeinfo-badindex/vb6/Widget.cls b/bugs/gettypeinfo-badindex/vb6/Widget.cls new file mode 100644 index 000000000..03afe78f1 --- /dev/null +++ b/bugs/gettypeinfo-badindex/vb6/Widget.cls @@ -0,0 +1,17 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Widget" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = True +Option Explicit + +Public Sub Hello() +End Sub diff --git a/bugs/stdole-idispatch-late-bound/vb6/Module1.bas b/bugs/stdole-idispatch-late-bound/vb6/Module1.bas new file mode 100644 index 000000000..c22a1bc7d --- /dev/null +++ b/bugs/stdole-idispatch-late-bound/vb6/Module1.bas @@ -0,0 +1,33 @@ +Attribute VB_Name = "Module1" +Option Explicit + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Cases + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub + +Sub Cases() + Dim w As New Widget + Dim d As stdole.IDispatch + Dim count As Long + Set d = w + On Error Resume Next + d.Hello + Print #9, "d.Hello: error " & Err.Number & ", Hello ran " & w.Hits & " time(s)" + Err.Clear + d.GetTypeInfoCount count + Print #9, "d.GetTypeInfoCount: error " & Err.Number & " (" & Hex(Err.Number) & ") " & Err.Description + Err.Clear + d.GetTypeInfoCount "a", "b", "c" + Print #9, "d.GetTypeInfoCount ""a"", ""b"", ""c"": error " & Err.Number + Err.Clear + d.NoSuchMethod + Print #9, "d.NoSuchMethod: error " & Err.Number +End Sub diff --git a/bugs/stdole-idispatch-late-bound/vb6/Probe.vbp b/bugs/stdole-idispatch-late-bound/vb6/Probe.vbp new file mode 100644 index 000000000..3b3f36ec0 --- /dev/null +++ b/bugs/stdole-idispatch-late-bound/vb6/Probe.vbp @@ -0,0 +1,9 @@ +Type=Exe +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#C:\Windows\SysWOW64\stdole2.tlb#OLE Automation +Module=Module1; Module1.bas +Class=Widget; Widget.cls +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/stdole-idispatch-late-bound/vb6/Widget.cls b/bugs/stdole-idispatch-late-bound/vb6/Widget.cls new file mode 100644 index 000000000..8d956a145 --- /dev/null +++ b/bugs/stdole-idispatch-late-bound/vb6/Widget.cls @@ -0,0 +1,20 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Widget" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = True +Option Explicit + +Public Hits As Long + +Public Sub Hello() + Hits = Hits + 1 +End Sub diff --git a/docs/Reference/COM-Interfaces/IConnectionPoint.md b/docs/Reference/COM-Interfaces/IConnectionPoint.md index 0d0ba17bf..cfbfbcaf3 100644 --- a/docs/Reference/COM-Interfaces/IConnectionPoint.md +++ b/docs/Reference/COM-Interfaces/IConnectionPoint.md @@ -125,12 +125,12 @@ Returns a **Long**, the cookie that identifies the connection. The cookie is not In twinBASIC: - Cookies start at 1 for each source object and increase by one for every connection made. A cookie is not used again after its connection ends. -- A sink that does not answer for the outgoing interface fails with `E_NOINTERFACE` (`&H80004002`), where the COM contract names `CONNECT_E_CANNOTCONNECT`. An object of an ordinary twinBASIC class is such a sink, even when the class implements **IDispatch**, and so is a class declared **NotDispatchable** that implements it. A twinBASIC class cannot answer for the identifier, because the identifier of a class's event interface changes with every build and so cannot be declared. A sink that works is the object twinBASIC creates for a **WithEvents** variable, which **EnumConnections** returns (see the example). +- A sink that does not answer for the outgoing interface fails with `E_NOINTERFACE` (`&H80004002`), where the COM contract names `CONNECT_E_CANNOTCONNECT`. VB6 returns `E_NOINTERFACE` too. An object of an ordinary twinBASIC class is such a sink, even when the class implements **IDispatch**, and so is a class declared **NotDispatchable** that implements it. A twinBASIC class cannot answer for the identifier, because the identifier of a class's event interface changes with every build and so cannot be declared. A sink that works is the object twinBASIC creates for a **WithEvents** variable, which **EnumConnections** returns (see the example). - A sink that is already connected to the point is not connected a second time. **Advise** raises no error, returns 0, and adds nothing. - Passing **Nothing** is a defect in BETA 995, described in the warning below. > [!WARNING] -> BETA 995 has a defect: **Advise** with **Nothing** as the sink ends the program with an access violation, and any unsaved data is lost. The COM contract returns `E_POINTER`. Test the sink for **Nothing** before calling **Advise**. +> BETA 995 has a defect: **Advise** with **Nothing** as the sink ends the program with an access violation, and any unsaved data is lost. The COM contract returns `E_POINTER`. VB6 ends with an access violation as well. Test the sink for **Nothing** before calling **Advise**. ### Unadvise {: .no_toc } @@ -145,7 +145,7 @@ Syntax: *object*.**Unadvise** *dwCookie* The connection point releases the reference it held to the sink, and the source stops calling it. An event that the source raises afterwards does not reach that sink. > [!WARNING] -> BETA 995 has a defect: **Unadvise** with a cookie that names no connection, 0 included, succeeds and does nothing, where the COM contract returns an error (`E_POINTER`). A caller that releases a connection twice, or with a wrong cookie, is told that it worked. +> BETA 995 has a defect: **Unadvise** with a cookie that names no connection, 0 included, succeeds and does nothing, where the COM contract returns an error (`E_POINTER`). VB6 returns `CONNECT_E_NOCONNECTION` (`&H80040200`) for a cookie it never issued. A caller that releases a connection twice, or with a wrong cookie, is told that it worked. A **WithEvents** variable whose connection was ended with **Unadvise** can still be set to **Nothing** without an error. diff --git a/docs/Reference/COM-Interfaces/IDispatch.md b/docs/Reference/COM-Interfaces/IDispatch.md index 8867d7f9a..d9d4c5b10 100644 --- a/docs/Reference/COM-Interfaces/IDispatch.md +++ b/docs/Reference/COM-Interfaces/IDispatch.md @@ -231,7 +231,10 @@ The four [**VbCallType**](../../tB/Modules/Constants/VbCallType) values are the > In VBA an unknown member raises error 438. In twinBASIC it raises `&H80020006`, so an error handler that tests for 438 does not catch it. This holds for objects of any origin: a twinBASIC class, a **Collection**, a **Dictionary** and a **FileSystemObject** all behave alike. > [!WARNING] -> BETA 995 has a defect: a call that fails inside **Invoke** can be made twice, so its side effects happen twice and nothing says so. VB6 makes the call once. A failed property assignment is followed by a second **Invoke** with *wFlags* 3, and **CallByName** repeats a call that returned `DISP_E_MEMBERNOTFOUND` with a null *pVarResult*. A call with more arguments than a **Sub** takes runs the **Sub**, then fails with error 13, and a late-bound statement does this twice. Code that implements **Invoke** must not change state before it can still fail. +> BETA 995 has a defect: a call that fails inside **Invoke** can be made twice, so its side effects happen twice and nothing says so. VB6 makes the call once. A failed property assignment is followed by a second **Invoke** with *wFlags* 3. A call with more arguments than a **Sub** takes runs the **Sub**, then fails with error 13, and a late-bound statement does this twice. Code that implements **Invoke** must not change state before it can still fail. + +> [!NOTE] +> **CallByName** repeats a call that returned `DISP_E_MEMBERNOTFOUND`: a second **Invoke** with the same identifier and *wFlags* and a null *pVarResult*, for **VbMethod**, **VbGet** and **VbLet** alike. VB6 does the same. A late-bound statement makes the call once. ## Classes written in twinBASIC From a62fc59008b3cc17ff874f5b071ab932267f0ea6 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 4 Oct 2026 18:28:30 +0200 Subject: [PATCH 03/34] vb6run: a sample during which the exe ended is reported as such, with the event log's fault when there is one, not as one that did not return --- docs/Documentation/Tools.md | 4 +-- scripts/lib/vb6.mjs | 58 +++++++++++++++++++++++++++++-------- scripts/vb6run.mjs | 22 ++++++++++---- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md index bd358df75..49c9d3151 100644 --- a/docs/Documentation/Tools.md +++ b/docs/Documentation/Tools.md @@ -1119,11 +1119,11 @@ Builds and runs Visual Basic 6 code, so that what a documented sample prints in | Mode | Effect | |---|---| | `` | A `.bas` module, or a text file of bare statements; `-` reads statements from standard input. A file that defines `Sub Main` is a whole module: it is built as written, its `Sub Main` is renamed so that the generated `Main` can start it, and it keeps its `Attribute VB_Name` line or is given one. Any other file is the body of a generated procedure. What the sample printed goes to standard output. A compile error is reported to standard error as VB6 reports it, with the line given as a line of the file, and a run-time error as `[vb6] error : `. | -| `--docs` | Reads the documentation's `check_run` fences with the reader `check_examples.mjs` uses and builds each as a module of its own in a VB6 project; the fences that need no other fence share one project. The run fences of a `projname=` group are built in a project of their own, since class and module names collide between groups, together with the other fences of the group, each of which is a file. A `slot=file` fence is translated into VB6 components: every `Class ` block becomes a class module and every `Module ` block a standard module, `Public`, `Private` or `Friend` before the keyword being accepted, and anything outside those blocks (`Declare`, `Type`, `Enum`, `Const`, procedures) becomes one more standard module. Nothing else is translated. A construct VB6 has no form for, such as an `Interface`, a `CoClass`, a generic or an attribute line, stays where it is and VB6 refuses it, so each run fence of a group whose files do not build is `not VB6`, with the first error at its line in the page. Each of the run fences ends as `same` (VB6 prints what the page says twinBASIC prints), `differs` (the lines that differ, the page against VB6, with the page path and line), `not VB6` (VB6 refuses to compile it, with its first error; most twinBASIC syntax ends here, and it is informational), `error` (a run-time error, or it did not return), or `refused` (the sample cannot be run, for the reasons `check_run` gives). A fence that says `project=form` is built with a blank `Form1.frm`, which VB6 refuses to build with Unattended Execution, so such a project is built without it and its exe runs on a private desktop; each sample's forms are unloaded when it ends. On a `Declare` statement, `PtrSafe` is dropped and `LongPtr` is read as `Long`, because VB6 knows neither. A compile error stops VB6 at the first module that fails, so that module is dropped and the project is built again until it builds. A summary line gives the count of each. `--only ` keeps the pages whose path under `docs/` matches. | +| `--docs` | Reads the documentation's `check_run` fences with the reader `check_examples.mjs` uses and builds each as a module of its own in a VB6 project; the fences that need no other fence share one project. The run fences of a `projname=` group are built in a project of their own, since class and module names collide between groups, together with the other fences of the group, each of which is a file. A `slot=file` fence is translated into VB6 components: every `Class ` block becomes a class module and every `Module ` block a standard module, `Public`, `Private` or `Friend` before the keyword being accepted, and anything outside those blocks (`Declare`, `Type`, `Enum`, `Const`, procedures) becomes one more standard module. Nothing else is translated. A construct VB6 has no form for, such as an `Interface`, a `CoClass`, a generic or an attribute line, stays where it is and VB6 refuses it, so each run fence of a group whose files do not build is `not VB6`, with the first error at its line in the page. Each of the run fences ends as `same` (VB6 prints what the page says twinBASIC prints), `differs` (the lines that differ, the page against VB6, with the page path and line), `not VB6` (VB6 refuses to compile it, with its first error; most twinBASIC syntax ends here, and it is informational), `error` (a run-time error, the exe ended during it, or it did not return), or `refused` (the sample cannot be run, for the reasons `check_run` gives). A fence that says `project=form` is built with a blank `Form1.frm`, which VB6 refuses to build with Unattended Execution, so such a project is built without it and its exe runs on a private desktop; each sample's forms are unloaded when it ends. On a `Declare` statement, `PtrSafe` is dropped and `LongPtr` is read as `Long`, because VB6 knows neither. A compile error stops VB6 at the first module that fails, so that module is dropped and the project is built again until it builds. A summary line gives the count of each. `--only ` keeps the pages whose path under `docs/` matches. | Other options: `--timeout S` is the time limit, in seconds, for each run of the built exe (default 30), and `--json` prints one JSON object in place of the text. -Exit codes: **0** the sample ran, or, with `--docs`, no fence differs and none raised an error; **1** a VB6 compile error, a run-time error 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. +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) 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. ### addin_test.mjs {: #addin-test } diff --git a/scripts/lib/vb6.mjs b/scripts/lib/vb6.mjs index 6daef13f3..b7cc09659 100644 --- a/scripts/lib/vb6.mjs +++ b/scripts/lib/vb6.mjs @@ -69,9 +69,16 @@ // 11. A VB6 EXE THAT DIES OF AN ACCESS VIOLATION EXITS WITH CODE 0. Windows Error // Reporting writes an "Application Error" record (event 1000) to the Application log, // naming the exe's path and the exception code, but GetExitCodeProcess reads 0, so the -// exit code says the run succeeded. runRepro looks for that record (recordedFault) and -// reports the fault beside the exit code. A reproducer is built in a folder of its own, -// so the path names one run. (A Probe.exe that called Advise with a null sink on a VB6 +// exit code says the run succeeded. runRepro and runExe look for that record +// (recordedFault) and report the fault beside the exit code. Every build is in a +// folder of its own, so the path names one run. THE RECORD IS WRITTEN ONLY FOR AN EXE +// STARTED THROUGH tb-launch.ps1 (launchOnDesktop: runRepro, and runExe for a form +// project). The same exe spawned by Node directly faults with the same exit code 0 and +// leaves no record (2026-10-04, both for the Advise crash in MSVBVM60.DLL and for +// RtlMoveMemory from address 0 in ntdll.dll); Node's error mode, which a child +// inherits, is the likely cause. So runBatch also says which sample the exe ended in +// before the time limit: one that began, never ended, and was not ended by the time +// limit died there, record or not. (A Probe.exe that called Advise with a null sink on a VB6 // class's connection point, 2026-10-04: c0000005 in MSVBVM60.DLL, exit code 0.) // // The probes for the rewrite and the translation are in vb6Probes() at the end. @@ -715,7 +722,8 @@ export async function make(vb6, dir, { timeoutMs = 120000, project = PROJECT } = * as its command line; a reproducer's is `project: "Probe"`, which writes * `outName` and takes `args` (none by default). * - * @returns {Promise<{lines: string[], status: number|null, timedOut: boolean, error?: string}>} + * @returns {Promise<{lines: string[], status: number|null, timedOut: boolean, fault: {code: string, module: string, offset: string} | null, error?: string}>} + * `fault` is what recordedFault found for the exe (see 11 at the top) */ export async function runExe( dir, @@ -723,11 +731,14 @@ export async function runExe( ) { const outPath = path.join(dir, outName); rmSync(outPath, { force: true }); + const exe = path.join(dir, `${project}.exe`); + const since = Date.now(); const r = desktop - ? await runOnDesktop(path.join(dir, `${project}.exe`), args.join(" "), { timeoutMs }) - : await runLimited(path.join(dir, `${project}.exe`), args, { cwd: dir, timeoutMs }); + ? await runOnDesktop(exe, args.join(" "), { timeoutMs }) + : await runLimited(exe, args, { cwd: dir, timeoutMs }); const lines = existsSync(outPath) ? splitLines(decodeAnsi(readFileSync(outPath))) : []; - return { lines, status: r.status, timedOut: r.timedOut, error: r.error }; + const fault = r.timedOut || r.error ? null : recordedFault(exe, since); + return { lines, status: r.status, timedOut: r.timedOut, fault, error: r.error }; } // ------------------------------------------------------- a reproducer's project @@ -819,7 +830,7 @@ const XML_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }; /** * The fault that Windows Error Reporting recorded in the Application log for `exe`, a - * full path, at or after `since` (a Date.now() value), or null when there is none or the + * full path, at or after `since` (a Date.now() value taken before the exe started), or null when there is none or the * log cannot be read. See 11 at the top: a VB6 exe that faults exits with code 0, and * this record is what says it faulted. * @@ -840,6 +851,10 @@ export function recordedFault(exe, since) { } const want = exe.toLowerCase(); for (const event of xml.split("")) { + // The query's window reaches back before `since`, and runBatch runs one exe again after a + // sample that did not return: a record from before this run is another run's. + const at = / `exception 0x${code} in ${module} at offset 0x${offset}`; + +/** What a report says of a sample in runBatch's `died`. */ +export const diedText = ({ status, fault }) => + fault + ? `ended the exe with ${faultText(fault)}` + : `ended the exe, which exited with code ${status} and no fault in the event log`; + /** * Run a reproducer's built exe on a private desktop, inside a kill-on-close job, * for at most `timeoutMs`, and read the out.txt it wrote. `env` adds variables to the exe's @@ -1011,12 +1035,16 @@ export async function buildBatch(vb6, dir, modules, { timeoutMs, support = [] } * @param {number} count samples in the built project * @param {{timeoutMs?: number, desktop?: boolean}} o `desktop` runs the exe on a private * desktop, for a project with a form - * @returns {Promise<{items: ReturnType["items"], hung: number[]}>} - * `hung` lists the samples that began and never ended + * @returns {Promise<{items: ReturnType["items"], hung: number[], died: Map}>} + * `hung` lists the samples that began and never ended. `died` holds those of them + * during which the exe ended by itself, before the time limit: its exit code, and the + * fault the event log recorded, when there is one (see 11 at the top). The rest of + * `hung` were still running at the time limit. */ export async function runBatch(dir, count, { timeoutMs, desktop = false } = {}) { const items = Array.from({ length: count }, () => ({ began: false, ended: false, output: [], error: null })); const hung = []; + const died = new Map(); let start = 0; while (start < count) { const r = await runExe(dir, { start, timeoutMs, desktop }); @@ -1028,15 +1056,21 @@ export async function runBatch(dir, count, { timeoutMs, desktop = false } = {}) if (parsed.done) break; // The run stopped before it finished: the last sample to begin is the one it was in. const last = parsed.items.map((it) => it.began && !it.ended).lastIndexOf(true); + // An exe that ended before the time limit, in the middle of a sample, died in it. + const end = r.timedOut ? null : { status: r.status, fault: r.fault }; if (last < start) { // Nothing began: the exe never got going (it was killed, or it died), and rerunning cannot help. - if (!parsed.items.some((it) => it.began)) hung.push(start); + if (!parsed.items.some((it) => it.began)) { + hung.push(start); + if (end) died.set(start, end); + } break; } hung.push(last); + if (end) died.set(last, end); start = last + 1; } - return { items, hung }; + return { items, hung, died }; } // -------------------------------------------------------------------- probes diff --git a/scripts/vb6run.mjs b/scripts/vb6run.mjs index 7a5d8d584..eb0ba09b5 100644 --- a/scripts/vb6run.mjs +++ b/scripts/vb6run.mjs @@ -42,6 +42,7 @@ import { buildBatch, declarationsModule, declaresMain, + diedText, findVb6, makeWorkDir, moduleFor, @@ -83,8 +84,8 @@ generated Sub Main opens, because Debug.Print writes nothing in a compiled exe. same, differs (the lines that differ, page against VB6), not VB6 (VB6 refuses to compile it, or the files of its group; most twinBASIC syntax ends here, and it is informational), error (a - run-time error, or it did not return), refused (the sample cannot - be run) + run-time error, the exe ended during it, or it did not return), + refused (the sample cannot be run) --only with --docs, the pages whose path under docs/ matches this regular expression --vb6 VB6.EXE (default: $VB6_EXE, else VB98\\VB6.EXE under Program Files @@ -97,7 +98,10 @@ generated Sub Main opens, because Debug.Print writes nothing in a compiled exe. Exit codes: 0 the sample ran; with --docs, no fence differs and none raised an error - 1 a VB6 compile error, a run-time error, or a sample that did not return; with + 1 a VB6 compile error, a run-time error, a sample during which the exe ended + (a VB6 exe that dies of a fault can exit with code 0; the fault the + Application event log records is named when there is one), or a sample that + did not return; with --docs, at least one fence 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 (give --vb6 or set VB6_EXE), VB6 failing to @@ -188,7 +192,12 @@ async function sample() { const item = run.items[0]; result.output = item.output; let code = 0; - if (run.hung.length || !item.began) { + const died = run.died.get(0); + if (died) { + result.state = "crashed"; + result.error = { message: `the sample ${diedText(died)}` }; + code = 1; + } else if (run.hung.length || !item.began) { result.state = item.began ? "did not return" : "not run"; result.error = { message: `${result.state} (time limit ${timeoutMs / 1000} s)` }; code = 1; @@ -276,7 +285,10 @@ async function docs() { ran.forEach((r, i) => { const item = run.items[i]; const f = r.fence; - if (run.hung.includes(i)) { + if (run.died.has(i)) { + r.state = "error"; + r.detail.push(diedText(run.died.get(i))); + } else if (run.hung.includes(i)) { r.state = "error"; r.detail.push(`did not return within ${timeoutMs / 1000} s`); } else if (!item.began) { From 5b40be3b057914d560a7a7279b2b8d3fd8e8d52e Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 4 Oct 2026 18:39:47 +0200 Subject: [PATCH 04/34] builder: regenerate package-api.json on twinBASIC BETA 995 --- builder/package-api.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builder/package-api.json b/builder/package-api.json index 269d95bc0..05e9538ee 100644 --- a/builder/package-api.json +++ b/builder/package-api.json @@ -1,5 +1,5 @@ { - "build": 987, + "build": 995, "packages": { "AppGlobalClassProject": { "exports": ["AppGlobalClassObject"], @@ -628,7 +628,7 @@ {"name":"Strings","kind":"module","members":{"Asc":"function","AscW":"function","AscB":"function","Chr$":"function","Chr":"function","ChrW$":"function","ChrW":"function","ChrB$":"function","ChrB":"function","Mid$":"function","Mid":"function","MidB$":"function","MidB":"function","Left$":"function","Left":"function","LeftB$":"function","LeftB":"function","Right$":"function","Right":"function","RightB$":"function","RightB":"function","Space$":"function","Space":"function","String$":"function","String":"function","LTrim$":"function","LTrim":"function","RTrim$":"function","RTrim":"function","Trim$":"function","Trim":"function","MonthName":"function","WeekdayName":"function","StrReverse":"function","LCase$":"function","LCase":"function","UCase$":"function","UCase":"function","Format$":"function","Format":"function","FormatCurrency":"function","FormatNumber":"function","FormatPercent":"function","FormatDateTime":"function","StrComp":"function","StrConv":"function","Join":"function","Replace":"function","InStr$":"function","InStr":"function","InStrB":"function","InStrRev":"function","Filter":"function","Split":"function","Len$":"function","Len":"function","LenB$":"function","LenB":"function"}}, {"name":"TextEncodingConstants","kind":"module","members":{"default_system_ansi":"const","utf_7":"const","utf_7_bom":"const","utf_8":"const","utf_8_bom":"const","utf_16":"const","utf_16_bom":"const","us_ascii":"const","koi8_u":"const","koi8_r":"const","big5":"const","iso_8859_1_latin1":"const","iso_8859_2_latin2":"const","iso_8859_3_latin3":"const","iso_8859_4_latin4":"const","iso_8859_5_cyrillic":"const","iso_8859_6_arabic":"const","iso_8859_7_greek":"const","iso_8859_8_hebrew":"const","iso_8859_9_latin5_turkish":"const","iso_8859_10_latin6_nordic":"const","iso_8859_11_thai":"const","iso_8859_13_latin8_baltic":"const","iso_8859_14_latin8_celtic":"const","iso_8859_15_latin9_euro":"const","iso_8859_16_latin10_balkan":"const","windows_1250_central_europe":"const","windows_1251_cyrillic":"const","windows_1252_western":"const","windows_1253_greek":"const","windows_1254_turkish":"const","windows_1255_hebrew":"const","windows_1256_arabic":"const","windows_1257_baltic":"const","windows_1258_vietnamese":"const","ibm_850_western_europe":"const","ibm_852_central_and_eastern_europe":"const","ibm_855_cyrillic":"const","ibm_856_hebrew":"const","ibm_857_turkish":"const","ibm_858_western_europe":"const","ibm_860_portuguese":"const","ibm_861_icelandic":"const","ibm_862_hebrew":"const","ibm_863_canadian":"const","ibm_865_danish":"const","ibm_866_cyrillic":"const","ibm_869_greek":"const","ibm_932_japanese":"const","ibm_949_korean":"const"},"hiddenMembers":["default_system_ansi","utf_7","utf_7_bom","utf_8","utf_8_bom","utf_16","utf_16_bom","us_ascii","koi8_u","koi8_r","big5","iso_8859_1_latin1","iso_8859_2_latin2","iso_8859_3_latin3","iso_8859_4_latin4","iso_8859_5_cyrillic","iso_8859_6_arabic","iso_8859_7_greek","iso_8859_8_hebrew","iso_8859_9_latin5_turkish","iso_8859_10_latin6_nordic","iso_8859_11_thai","iso_8859_13_latin8_baltic","iso_8859_14_latin8_celtic","iso_8859_15_latin9_euro","iso_8859_16_latin10_balkan","windows_1250_central_europe","windows_1251_cyrillic","windows_1252_western","windows_1253_greek","windows_1254_turkish","windows_1255_hebrew","windows_1256_arabic","windows_1257_baltic","windows_1258_vietnamese","ibm_850_western_europe","ibm_852_central_and_eastern_europe","ibm_855_cyrillic","ibm_856_hebrew","ibm_857_turkish","ibm_858_western_europe","ibm_860_portuguese","ibm_861_icelandic","ibm_862_hebrew","ibm_863_canadian","ibm_865_danish","ibm_866_cyrillic","ibm_869_greek","ibm_932_japanese","ibm_949_korean"]}, {"name":"TestGeneric","kind":"class","members":{"Something":"sub"}}, - {"name":"_HiddenModule","kind":"module","hidden":true,"members":{"PictureToByteArray":"function","CreateGUID":"function","GetMem1":"sub","GetMem2":"sub","GetMem4":"sub","GetMem8":"sub","GetMemPtr":"sub","PutMem1":"sub","PutMem2":"sub","PutMem4":"sub","PutMem8":"sub","PutMemPtr":"sub","vbaObjAddref":"sub","vbaObjRelease":"sub","vbaObjSetAddref":"function","vbaObjSet":"function","vbaRefVarAry":"function","vbaAryMove":"sub","CreateStdPictureFromHandle":"function","RuntimeGetBitmapBits":"sub","RuntimeSetBitmapBits":"sub","RuntimeConvertBitmapToIcon":"sub","RuntimeConvertBitmap32ToIcon":"sub","RuntimeCreateScaledPicture":"function","RuntimeCreatePremultipliedRGBABitmap32bpp":"function","RuntimePrinterGetFonts":"sub","RuntimePatchGetClipBoxForHdc":"sub","RuntimeCreateExpressionService":"function","RuntimeCreateResampledBitmap":"function","Eval":"function","vbaCopyBytes":"function","vbaCopyBytesZero":"function","AllocMem":"function","FreeMem":"sub","SetThreadGlobalErrorTrap":"sub","SuppressRaiseEvents":"sub","DontInitNextCreatedCOMInstance":"sub","ExclusivitySectionEnter":"sub","ExclusivitySectionLeave":"sub","ClipboardGetTextInternal":"function","ClipboardSetTextInternal":"sub","LoadPictureInternal":"function","LoadResDataInternal":"function","CreateClassPropertyBagUCRead2":"function","CreateClassPropertyBagUCWrite2":"function","CreateClassPropertyBagUCRead":"function","CreateClassPropertyBagUCWrite":"function","GetPropertyBagUCWriterContents":"function","HasExclusiveDefaultDispatchValue":"function","ShowPropertyPages":"sub","SynchronizeWindowSizeWithParent":"sub","CreateCoreWebView2Environment":"sub","BringWindowToFront":"sub","CrashDAP":"sub","CrashLSP":"sub","CrashFS":"sub","CrashRoot":"sub","ObjPtr":"function","StrPtr":"function","ConvertIconToBitmap":"function","Input$":"function","Input":"function","InputB$":"function","InputB":"function","Width":"sub","_FileWidth":"sub","_InStrBreak":"function","CreateCollectionObject":"function","Array":"function","Int3Breakpoint":"sub","Emit":"sub","EmitAny":"sub","GetDeclaredTypeProgId":"function","GetDeclaredTypeIid":"function","GetDeclaredTypeEventIid":"function","GetDeclaredTypeClsid":"function","GetDeclaredMinEnumValue":"function","GetDeclaredMaxEnumValue":"function","StackOffset":"function","StackArgsSize":"function","UnprotectedAccess":"function","CheckBadFPUInstruction":"sub","VarPtr":"function","InternalDebugHoverEval":"function","GetErrNumber":"function","LetErrNumber":"sub","GetErrSource":"function","LetErrSource":"sub","GetErrDescription":"function","LetErrDescription":"sub","GetErrHelpFile":"function","LetErrHelpFile":"sub","GetErrHelpContext":"function","LetErrHelpContext":"sub","ErrRaise":"sub","ErrClear":"sub","GetErrLastDllError":"function","LetErrLastDllError":"sub","GetErrLastHresult":"function","GetErrReturnHResult":"function","LetErrReturnHResult":"sub","EnsureContainerIsLoaded":"sub","ResetFirstMethodAccessFlag":"sub","MemZero":"sub","GetClasInstanceAxMiscFlags":"sub","GetClassInstanceSerializer":"function","GetClassInstanceRefCount":"function","RuntimeGetChildControlsList":"function","RuntimeCreateGetMessageHook":"function","vbaCastObj":"function","InterlockedExchangePointer":"function","InterlockedCompareExchangePointer":"function","InterlockedCompareExchange32":"function","InterlockedCompareExchange64":"function","InterlockedIncrement32":"function","InterlockedDecrement32":"function","GetShortcutTextByEnum":"function","RuntimeSetWithEvents":"function","RuntimeReadFileDataBase64":"function","RuntimeRaisePropertyNotifySinkChangedEvent":"sub","RuntimeGetPropertyPageClsidByName":"sub","RuntimeMapDispIdToPropertyPageClsid":"function","RuntimeGetTypeInfoPropertyList":"function","RuntimeCreateRecurseProtectedObjectWrapper":"function","GetInheritedOwner":"function","SetClassOverrideDispatch":"sub","RuntimeSharedMemCreateHost":"function","RuntimeSharedMemCreateClient":"function","RuntimeSharedMemSendMessage":"sub","RuntimeSharedMemReceiveMessage":"function","RuntimeFusionRegisterInterface":"sub","RuntimeFusionRegisterClass":"sub","RuntimeFusionCreateObject":"function","RuntimeFusionCreatePortalHost":"function","RuntimeFusionCreatePortalClient":"function","RuntimeFusionSetChannelNotificationCallback":"sub","RuntimeFusionCreateLinkedChannel":"sub","RuntimeFusionJoinLinkedChannel":"sub","RuntimeFusionProcessMessage":"sub","RuntimeFusionGetThreadID":"function","RuntimeSetActiveXInitializationOverride":"sub","RuntimeShellAutoExit":"function","RuntimeGetFusionActiveXOverrides":"sub","RuntimeShowAxAboutBox":"sub","RuntimeAxGetMiscStatus":"function","RuntimeAxGetSnapshotBits":"function","RuntimePaintBits":"sub","RuntimeFusionGetExePath":"function","RuntimeFusionStartProcess":"sub","InterlockedIncrement64":"function","InterlockedDecrement64":"function","RuntimeOverrideFusionIpcNoWait":"sub","RuntimeFusionCancelLinkedChannel":"sub","RuntimeBase64Encode":"function","RuntimeFusionCreateExplicitLinkedChannelCallback":"sub","RuntimeCreateSharedMemData":"function","RuntimeOpenSharedMemData":"function","RuntimeAccessSharedMemData":"function","RuntimeUnaccessSharedMemData":"sub","RuntimeCloseSharedMemData":"sub","RuntimeFusionInteruptPendingIPC_UIThread":"function","RuntimeFusionSetPortalTimeout":"sub","RuntimeFusionClosePortal":"sub","RuntimeBase64Decode":"function","RuntimeGetProjectConditionalCompilationArgValue":"function"},"hiddenMembers":["SuppressRaiseEvents","DontInitNextCreatedCOMInstance","ExclusivitySectionEnter","ExclusivitySectionLeave","ClipboardGetTextInternal","ClipboardSetTextInternal","LoadPictureInternal","LoadResDataInternal","CreateClassPropertyBagUCRead2","CreateClassPropertyBagUCWrite2","CreateClassPropertyBagUCRead","CreateClassPropertyBagUCWrite","GetPropertyBagUCWriterContents","HasExclusiveDefaultDispatchValue","ShowPropertyPages","SynchronizeWindowSizeWithParent","CreateCoreWebView2Environment","BringWindowToFront","CrashDAP","CrashLSP","CrashFS","CrashRoot","_FileWidth","_InStrBreak","CreateCollectionObject","Int3Breakpoint","CheckBadFPUInstruction","InternalDebugHoverEval","GetErrNumber","LetErrNumber","GetErrSource","LetErrSource","GetErrDescription","LetErrDescription","GetErrHelpFile","LetErrHelpFile","GetErrHelpContext","LetErrHelpContext","ErrRaise","ErrClear","GetErrLastDllError","LetErrLastDllError","GetErrLastHresult","GetErrReturnHResult","LetErrReturnHResult","EnsureContainerIsLoaded","ResetFirstMethodAccessFlag","MemZero","GetClasInstanceAxMiscFlags","GetClassInstanceSerializer","GetClassInstanceRefCount","SetClassOverrideDispatch","RuntimeSharedMemCreateHost","RuntimeSharedMemCreateClient","RuntimeSharedMemSendMessage","RuntimeSharedMemReceiveMessage","RuntimeFusionRegisterInterface","RuntimeFusionRegisterClass","RuntimeFusionCreateObject","RuntimeFusionCreatePortalHost","RuntimeFusionCreatePortalClient","RuntimeFusionSetChannelNotificationCallback","RuntimeFusionCreateLinkedChannel","RuntimeFusionJoinLinkedChannel","RuntimeFusionProcessMessage","RuntimeFusionGetThreadID","RuntimeShellAutoExit","RuntimeGetFusionActiveXOverrides","RuntimeShowAxAboutBox","RuntimeAxGetMiscStatus","RuntimeAxGetSnapshotBits","RuntimePaintBits","RuntimeFusionGetExePath","RuntimeFusionStartProcess"]}, + {"name":"_HiddenModule","kind":"module","hidden":true,"members":{"PictureToByteArray":"function","CreateGUID":"function","GetMem1":"sub","GetMem2":"sub","GetMem4":"sub","GetMem8":"sub","GetMemPtr":"sub","PutMem1":"sub","PutMem2":"sub","PutMem4":"sub","PutMem8":"sub","PutMemPtr":"sub","vbaObjAddref":"sub","vbaObjRelease":"sub","vbaObjSetAddref":"function","vbaObjSet":"function","vbaRefVarAry":"function","vbaAryMove":"sub","CreateStdPictureFromHandle":"function","RuntimeGetBitmapBits":"sub","RuntimeSetBitmapBits":"sub","RuntimeConvertBitmapToIcon":"sub","RuntimeConvertBitmap32ToIcon":"sub","RuntimeCreateScaledPicture":"function","RuntimeCreatePremultipliedRGBABitmap32bpp":"function","RuntimePrinterGetFonts":"sub","RuntimePatchGetClipBoxForHdc":"sub","RuntimeCreateExpressionService":"function","RuntimeCreateResampledBitmap":"function","Eval":"function","vbaCopyBytes":"function","vbaCopyBytesZero":"function","AllocMem":"function","FreeMem":"sub","SetThreadGlobalErrorTrap":"sub","SuppressRaiseEvents":"sub","DontInitNextCreatedCOMInstance":"sub","ExclusivitySectionEnter":"sub","ExclusivitySectionLeave":"sub","ClipboardGetTextInternal":"function","ClipboardSetTextInternal":"sub","LoadPictureInternal":"function","LoadResDataInternal":"function","CreateClassPropertyBagUCRead2":"function","CreateClassPropertyBagUCWrite2":"function","CreateClassPropertyBagUCRead":"function","CreateClassPropertyBagUCWrite":"function","GetPropertyBagUCWriterContents":"function","HasExclusiveDefaultDispatchValue":"function","ShowPropertyPages":"sub","SynchronizeWindowSizeWithParent":"sub","CreateCoreWebView2Environment":"sub","BringWindowToFront":"sub","CrashDAP":"sub","CrashLSP":"sub","CrashFS":"sub","CrashRoot":"sub","ObjPtr":"function","StrPtr":"function","ConvertIconToBitmap":"function","Input$":"function","Input":"function","InputB$":"function","InputB":"function","Width":"sub","_FileWidth":"sub","_InStrBreak":"function","CreateCollectionObject":"function","Array":"function","Int3Breakpoint":"sub","Emit":"sub","EmitAny":"sub","GetDeclaredTypeProgId":"function","GetDeclaredTypeIid":"function","GetDeclaredTypeEventIid":"function","GetDeclaredTypeClsid":"function","GetDeclaredMinEnumValue":"function","GetDeclaredMaxEnumValue":"function","StackOffset":"function","StackArgsSize":"function","UnprotectedAccess":"function","CheckBadFPUInstruction":"sub","VarPtr":"function","InternalDebugHoverEval":"function","GetErrNumber":"function","LetErrNumber":"sub","GetErrSource":"function","LetErrSource":"sub","GetErrDescription":"function","LetErrDescription":"sub","GetErrHelpFile":"function","LetErrHelpFile":"sub","GetErrHelpContext":"function","LetErrHelpContext":"sub","ErrRaise":"sub","ErrClear":"sub","GetErrLastDllError":"function","LetErrLastDllError":"sub","GetErrLastHresult":"function","GetErrReturnHResult":"function","LetErrReturnHResult":"sub","EnsureContainerIsLoaded":"sub","ResetFirstMethodAccessFlag":"sub","MemZero":"sub","GetClasInstanceAxMiscFlags":"sub","GetClassInstanceSerializer":"function","GetClassInstanceRefCount":"function","RuntimeGetChildControlsList":"function","RuntimeCreateGetMessageHook":"function","vbaCastObj":"function","InterlockedExchangePointer":"function","InterlockedCompareExchangePointer":"function","InterlockedCompareExchange32":"function","InterlockedCompareExchange64":"function","InterlockedIncrement32":"function","InterlockedDecrement32":"function","GetShortcutTextByEnum":"function","RuntimeSetWithEvents":"function","RuntimeReadFileDataBase64":"function","RuntimeRaisePropertyNotifySinkChangedEvent":"sub","RuntimeGetPropertyPageClsidByName":"sub","RuntimeMapDispIdToPropertyPageClsid":"function","RuntimeGetTypeInfoPropertyList":"function","RuntimeCreateRecurseProtectedObjectWrapper":"function","GetInheritedOwner":"function","SetClassOverrideDispatch":"sub","RuntimeSharedMemCreateHost":"function","RuntimeSharedMemCreateClient":"function","RuntimeSharedMemSendMessage":"sub","RuntimeSharedMemReceiveMessage":"function","RuntimeFusionRegisterInterface":"sub","RuntimeFusionRegisterClass":"sub","RuntimeFusionCreateObject":"function","RuntimeFusionCreatePortalHost":"function","RuntimeFusionCreatePortalClient":"function","RuntimeFusionSetChannelNotificationCallback":"sub","RuntimeFusionCreateLinkedChannel":"sub","RuntimeFusionJoinLinkedChannel":"sub","RuntimeFusionProcessMessage":"sub","RuntimeFusionGetThreadID":"function","RuntimeSetActiveXInitializationOverride":"sub","RuntimeShellAutoExit":"function","RuntimeGetFusionActiveXOverrides":"sub","RuntimeShowAxAboutBox":"sub","RuntimeAxGetMiscStatus":"function","RuntimeAxGetSnapshotBits":"function","RuntimePaintBits":"sub","RuntimeFusionGetExePath":"function","RuntimeFusionStartProcess":"sub","InterlockedIncrement64":"function","InterlockedDecrement64":"function","RuntimeOverrideFusionIpcNoWait":"sub","RuntimeFusionCancelLinkedChannel":"sub","RuntimeBase64Encode":"function","RuntimeFusionCreateExplicitLinkedChannelCallback":"sub","RuntimeCreateSharedMemData":"function","RuntimeOpenSharedMemData":"function","RuntimeAccessSharedMemData":"function","RuntimeUnaccessSharedMemData":"sub","RuntimeCloseSharedMemData":"sub","RuntimeFusionInteruptPendingIPC_UIThread":"function","RuntimeFusionSetPortalTimeout":"sub","RuntimeFusionClosePortal":"sub","RuntimeBase64Decode":"function","SetClassOverrideEvents":"sub","RuntimeGetProjectConditionalCompilationArgValue":"function"},"hiddenMembers":["SuppressRaiseEvents","DontInitNextCreatedCOMInstance","ExclusivitySectionEnter","ExclusivitySectionLeave","ClipboardGetTextInternal","ClipboardSetTextInternal","LoadPictureInternal","LoadResDataInternal","CreateClassPropertyBagUCRead2","CreateClassPropertyBagUCWrite2","CreateClassPropertyBagUCRead","CreateClassPropertyBagUCWrite","GetPropertyBagUCWriterContents","HasExclusiveDefaultDispatchValue","ShowPropertyPages","SynchronizeWindowSizeWithParent","CreateCoreWebView2Environment","BringWindowToFront","CrashDAP","CrashLSP","CrashFS","CrashRoot","_FileWidth","_InStrBreak","CreateCollectionObject","Int3Breakpoint","CheckBadFPUInstruction","InternalDebugHoverEval","GetErrNumber","LetErrNumber","GetErrSource","LetErrSource","GetErrDescription","LetErrDescription","GetErrHelpFile","LetErrHelpFile","GetErrHelpContext","LetErrHelpContext","ErrRaise","ErrClear","GetErrLastDllError","LetErrLastDllError","GetErrLastHresult","GetErrReturnHResult","LetErrReturnHResult","EnsureContainerIsLoaded","ResetFirstMethodAccessFlag","MemZero","GetClasInstanceAxMiscFlags","GetClassInstanceSerializer","GetClassInstanceRefCount","SetClassOverrideDispatch","RuntimeSharedMemCreateHost","RuntimeSharedMemCreateClient","RuntimeSharedMemSendMessage","RuntimeSharedMemReceiveMessage","RuntimeFusionRegisterInterface","RuntimeFusionRegisterClass","RuntimeFusionCreateObject","RuntimeFusionCreatePortalHost","RuntimeFusionCreatePortalClient","RuntimeFusionSetChannelNotificationCallback","RuntimeFusionCreateLinkedChannel","RuntimeFusionJoinLinkedChannel","RuntimeFusionProcessMessage","RuntimeFusionGetThreadID","RuntimeShellAutoExit","RuntimeGetFusionActiveXOverrides","RuntimeShowAxAboutBox","RuntimeAxGetMiscStatus","RuntimeAxGetSnapshotBits","RuntimePaintBits","RuntimeFusionGetExePath","RuntimeFusionStartProcess","SetClassOverrideEvents"]}, {"name":"IGetMessageHook","kind":"interface","members":{"RegisterMessage":"sub","Start":"sub","Stop":"sub"}}, {"name":"EnumDescendantsModeFlags","kind":"enum","in":"IGetMessageHook","values":["ExactWindow","AllDescendants","DirectChildren"]}, {"name":"GetMessageHookHelper","kind":"module","members":{"GetMessageHandler":"delegate"}}, From 7a4a14f341e69ca69234f70ce7764db0ff36fcf5 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 4 Oct 2026 18:51:31 +0200 Subject: [PATCH 05/34] WIP.HelpAddin: drop history, shorten --- WIP.HelpAddin.md | 875 ++++++++++++++++++++--------------------------- 1 file changed, 369 insertions(+), 506 deletions(-) diff --git a/WIP.HelpAddin.md b/WIP.HelpAddin.md index ab4a8a6d4..deec0a0d9 100644 --- a/WIP.HelpAddin.md +++ b/WIP.HelpAddin.md @@ -4,22 +4,19 @@ See [WIP.md](WIP.md) for the maintenance guide. This file covers the planned twi add-in that shows the documentation for the symbol under the cursor, and the harness that tests IDE add-ins by machine, which the add-in is developed against. -**Status: Stages 1 and 2 are done, and Stage 3's index is built** --- `addin-test.bat` -operates Samples 10 and 15 end to end and leaves the registry as it found it; all fourteen -of Stage 2's questions are answered, thirteen of them by nine probe lanes that fail when a -later IDE build behaves differently; and every build publishes `tB/symbols.json`, 5,536 -names at 4,086 URLs, under a drift guard that fails the build when one of its URLs goes. -Stage 3's embedded mode, a `theme` parameter the site would read, waits until the add-in -works (decided 2026-09-25). Stage 4, the add-in itself, is next. This file -replaces the June draft, `add-in/PLAN.md` -in commit `d159acf8` ("Roughly plan the help add-in"). That commit is on no branch --- only -the detached HEAD of an old worktree keeps it --- so everything in it worth keeping is here, -corrected, and nothing depends on it surviving. [What changed from the June -draft](#what-changed-from-the-june-draft) lists the corrections. - -Facts about the IDE are from **BETA 983**, and were re-checked on BETA 995 where the text says so. An offset like `main.js@611152` is a byte offset -into the one-line `ide/main.js`; offsets move with every build, so in any other build search -for the quoted text instead. Each fact is one of three kinds: +**Status.** Stages 1 to 3 stand; Stage 4, the add-in itself, is next (increment 1). + +- `addin-test.bat` operates Samples 10 and 15 end to end and leaves the registry as it found it. +- All fourteen of Stage 2's questions are answered. Thirteen are held by nine probe lanes that + fail when a later IDE build behaves differently. +- Every build publishes `tB/symbols.json`, 5,536 names at 4,086 URLs, under a drift guard that + fails the build when one of its URLs goes. +- Stage 3's embedded mode, a `theme` parameter the site would read, waits until the add-in works. + +Facts about the IDE are from **BETA 983**, and were re-checked on BETA 995 where the text says +so. An offset like `main.js@611152` is a byte offset into the one-line `ide/main.js`. Offsets +move with every build, so in any other build search for the quoted text instead. Each fact is +one of three kinds: - stated plainly: read in the IDE's code or registry; - marked *(reported)*: from a static reading or a harvested finding, not re-checked; @@ -38,8 +35,8 @@ It also makes a documentation promise real. [Permanent Links](docs/Documentation/Permanent-Links.md) says that "the IDE help system" relies on the `/tB/` URLs, but the IDE has no documentation help today: its scripts contain neither `docs.twinbasic.com` nor `/tB/` *(reported)*. This add-in is the first real consumer of that -contract, and it is what [WIP.Authoring.md](WIP.Authoring.md#description-attributes-are-not-connected-to-this-documentation) -waits on before `[Description]` text and these pages can be connected. +contract. [WIP.Authoring.md](WIP.Authoring.md#description-attributes-are-not-connected-to-this-documentation) +waits on it before `[Description]` text and these pages can be connected. ## What the IDE gives an add-in @@ -59,11 +56,10 @@ the compiler what a symbol is.** Each of those gaps shapes a stage below. folders cannot be read off the binary. There are two. One is the install's: the add-in samples build into `${IdePath}\addins\${Architecture}\`, and the shipped install has `addins\win32\` and `addins\win64\` beside `bin\`, each holding - `tbGlobalSearchAddIn1.dll` --- so every IDE that `tbbuild`, `tbrun` and `examples.bat` - start today loads the Global Search add-in. Measured through the compiler's own list - (`loadedAddins` in `tb-ide-addins.mjs`): the real install's IDE reports `GlobalSearchAddIn - AddIn`, and a copy of it with empty `addins` folders reports none. The other is the - user's, in the next item. + `tbGlobalSearchAddIn1.dll`. So every IDE that `tbbuild`, `tbrun` and `examples.bat` start + loads the Global Search add-in, unless it is a private copy with empty `addins` folders + (`loadedAddins` in `tb-ide-addins.mjs` asks the compiler: the real install reports + `GlobalSearchAddIn AddIn`, the copy reports none). The other is the user's, in the next item. - **The compiler also loads the add-ins in `%APPDATA%\twinBASIC\addins\` (P6).** The page makes that folder at startup. It gives the host's `CreateCommonFolders` (`main.js@961019`) the text `%APPDATA%\twinBASIC`, which the host expands in the IDE's @@ -80,19 +76,17 @@ the compiler what a symbol is.** Each of those gaps shapes a stage below. in its own `APPDATA`. So a DLL in the user's folder loads into every IDE the user starts, from any install, and into every IDE `tbbuild`, `tbrun` and `examples.bat` start. An IDE started with `APPDATA` naming another folder loads none of the user's add-ins, which is - how the add-in lanes keep them out (Stage 1, item 3). None of it needed a DLL in the - user's own folder. The FAQ's answer to "Does twinBASIC support addins?" named `addins\` - without the `win32` and `win64` folders under it. It names them now, and says that a DLL - placed in `addins\` itself is not loaded. -- **An add-in runs inside the compiler's process.** Measured (P10): the process id an - add-in read with `GetCurrentProcessId` was that of `twinBASIC_win32_noDEP.exe`, which - `twinBASIC.exe` starts as a direct child, beside the page server - `twinBASIC_win32.exe --ide=`. So an add-in sees the environment the IDE was started - with. **A compiler restart is a new process**: the toolbar's restart button - (`#restartIcon`, bound to `tbCompiler_Restart`, which calls `root.forceTerminate()`) ended - the compiler, and the next one, with a new process id, loaded the add-in again and ran its - `OnProjectLoaded` a second time. The DEBUG CONSOLE was not cleared; the IDE added - `restarting from MEMORY []`. + how the add-in lanes keep them out (Stage 1, item 3). The FAQ's answer to "Does twinBASIC + support addins?" names the `win32` and `win64` folders and says that a DLL placed in + `addins\` itself is not loaded. +- **An add-in runs inside the compiler's process (P10).** The process id an add-in read with + `GetCurrentProcessId` was that of `twinBASIC_win32_noDEP.exe`, which `twinBASIC.exe` starts + as a direct child, beside the page server `twinBASIC_win32.exe --ide=`. So an add-in + sees the environment the IDE was started with. **A compiler restart is a new process**: the + toolbar's restart button (`#restartIcon`, bound to `tbCompiler_Restart`, which calls + `root.forceTerminate()`) ended the compiler, and the next one, with a new process id, + loaded the add-in again and ran its `OnProjectLoaded` a second time. The DEBUG CONSOLE was + not cleared; the IDE added `restarting from MEMORY []`. - **Load failures have their own messages** in the compiler's strings: `Failed to load addin. LoadLibrary() failed.`, `Entry point not found. Addin may have been compiled for a newer version of the twinBASIC IDE.`, `Entry point 'tbCreateCompilerAddin' call @@ -107,26 +101,26 @@ the compiler what a symbol is.** Each of those gaps shapes a stage below. `shiftKeyDown` follows the keymap's `tbMisc_ShiftKeyStateDown` and `...Up` actions, so a test that presses Shift must not do it while a project is opening. - **The linker exports `tbCreateCompilerAddin` as `tbCreateCompilerAddin_v3`, and the - loader takes three names (P14).** Read in the compiler's code and measured on BETA 983 and 995 by - [test/addin/entry.test.mjs](test/addin/entry.test.mjs). The loader --- in BETA 983 the - code at `0x1EAB6275` in `twinBASIC_win32.dll`, and in another build the code that pushes - the address of the string `tbCreateCompilerAddin_v2`, found with `dumpbin /disasm`; the - same in `twinBASIC_win64.dll` --- asks `GetProcAddress` for + loader takes three names (P14).** Read in the compiler's code and measured on BETA 983 and + 995 by [test/addin/entry.test.mjs](test/addin/entry.test.mjs). The loader --- in BETA 983 + the code at `0x1EAB6275` in `twinBASIC_win32.dll`, and in another build the code that + pushes the address of the string `tbCreateCompilerAddin_v2`, found with `dumpbin /disasm`; + the same in `twinBASIC_win64.dll` --- asks `GetProcAddress` for `tbCreateCompilerAddin`, then `tbCreateCompilerAddin_v2`, then `tbCreateCompilerAddin_v3`, and calls the first it finds **the same way whichever it is**: one argument, the `Host`, `stdcall` on win32. It asks what that returns for `IAddInV1` --- - `{F1BAB9A7-09A3-436C-8B57-A57A76C5DF98}`, the package's own --- and reads its `Name`. The - linker, for each `[DllExport]` function, swaps the export name for - `tbCreateCompilerAddin_v3` when the function's name is `tbCreateCompilerAddin`, and exports - nothing under the name written. So the three names are not three signatures: the suffix - is a version stamp, and an IDE whose loader does not know the stamp refuses the DLL. - Measured with the probe's one export patched in four copies: the plain name, `_v2` and - `_v3` all loaded, and `_v4` did not, with `[EntryV4.dll] Failed to load addin. Entry point - not found. Addin may have been compiled for a newer version of the twinBASIC IDE.` in the - DEBUG CONSOLE and an `Unknown Addin` in the compiler's list. Both of P7's builds, win32 and - win64, exported `_v3` alone. Every install on this machine, BETA 947 to 995, has the same - three names in its loader, and each one's shipped Global Search add-in exports `_v3` alone, - so both stamps are older than BETA 947. + `{F1BAB9A7-09A3-436C-8B57-A57A76C5DF98}`, the package's own --- and reads its `Name`. For + each `[DllExport]` function, the linker swaps the export name for `tbCreateCompilerAddin_v3` + when the function's name is `tbCreateCompilerAddin`, and exports nothing under the name + written. So the three names are not three signatures: the suffix is a version stamp, and an + IDE whose loader does not know the stamp refuses the DLL. With the probe's one export + patched in four copies, the plain name, `_v2` and `_v3` all loaded, and `_v4` did not, with + `[EntryV4.dll] Failed to load addin. Entry point not found. Addin may have been compiled + for a newer version of the twinBASIC IDE.` in the DEBUG CONSOLE and an `Unknown Addin` in + the compiler's list. Both of P7's builds, win32 and win64, exported `_v3` alone. Every + install on this machine, BETA 947 to 995, has the same three names in its loader, and each + one's shipped Global Search add-in exports `_v3` alone, so both stamps are older than BETA + 947. - **Add-ins cannot be switched off.** The Add-Ins menu lists the loaded add-ins with ticks, and every item calls `notSupportedMenuOption()` *(reported)*. - **A compiler restart loads every add-in again, from the file in its folder then (P9).** @@ -136,27 +130,26 @@ the compiler what a symbol is.** Each of those gaps shapes a stage below. first takes away what every add-in added: `removeAddinAlterations` removes their toolbar buttons and shortcuts, and hides each tool window's body behind the text `(currently unavailable)`, leaving the window where it was. It ends the old compiler with `taskkill - /F` (`forceTerminate`, `main.js@1050553`), so **no add-in's `Class_Terminate` runs**: - measured with the probe writing a line to a file from it, which it did for an object it - dropped as it loaded and never for itself. The new compiler then loads the add-ins - as it starts, and each runs `OnProjectLoaded` again, in a new process, so **an add-in - keeps no state across a restart.** A window it adds again under the same id is the same - window, emptied and shown again (below, under Tool windows). So the rebuild loop works - without ending the IDE: rename the loaded DLL aside, which P8 allows, put the new build in - its place, and restart the compiler. Measured: with build A renamed aside, a restart - loaded nothing, left no button or shortcut, and left both of A's windows showing the text; - the renamed file could be deleted, since the compiler that held it had ended. With build - B then copied in under A's name, the next restart loaded B, whose first line came 1.0 s - after the click (4.4 s with another lane building at the same time); B had one button, - one shortcut that fired B alone, and A's two windows, filled with B's content. A restart's - compile settled after 6.6 s, against 7.6 s for a new IDE to open the project and settle - its compile, so a harness saves little by the loop; a person keeps the IDE, its open files - and its layout. + /F` (`forceTerminate`, `main.js@1050553`), so **no add-in's `Class_Terminate` runs**: the + probe wrote a line to a file from it, which it did for an object it dropped as it loaded + and never for itself. The new compiler then loads the add-ins as it starts, and each runs + `OnProjectLoaded` again, in a new process, so **an add-in keeps no state across a + restart.** A window it adds again under the same id is the same window, emptied and shown + again (below, under Tool windows). So the rebuild loop works without ending the IDE: + rename the loaded DLL aside, which P8 allows, put the new build in its place, and restart + the compiler. With build A renamed aside, a restart loaded nothing, left no button or + shortcut, and left both of A's windows showing the text; the renamed file could be + deleted, since the compiler that held it had ended. With build B then copied in under A's + name, the next restart loaded B, whose first line came 1.0 s after the click (4.4 s with + another lane building at the same time); B had one button, one shortcut that fired B + alone, and A's two windows, filled with B's content. A restart's compile settled after + 6.6 s, against 7.6 s for a new IDE to open the project and settle its compile, so a + harness saves little by the loop; a person keeps the IDE, its open files and its layout. - **The build target picks the compiler, and the compiler picks the folder (P7).** The IDE remembers the target of each project in the shared registry, as one JSON object in `IDESettings\targetArchitectureMemory` keyed by project path, and opens a project in the - target remembered for it --- or, with none, in the first on its list, win32. Measured with - a differently named DLL in each folder: a project with no memory got + target remembered for it --- or, with none, in the first on its list, win32. With a + differently named DLL in each folder, a project with no memory got `twinBASIC_win32_noDEP.exe`, which loaded `addins\win32` alone; a project remembered as win64 got `twinBASIC_win64_noDEP.exe` with `twinBASIC_nativedbg_win64.exe`, which tried `addins\win64` alone. **Switching the target of an open project (Ctrl+F1 / Ctrl+F2) @@ -195,8 +188,8 @@ whose lane is [test/addin/keys.test.mjs](test/addin/keys.test.mjs). `{CTRL}{SHIFT}d`, does not work. The bug is in [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md), and the published [KeyboardShortcuts](docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md) page has a NOTE, the prefix-order rule, which keys fire, and an example on Shift+F12. -- **F1 fires, and is shared with the IDE (P2).** The default keymap binds it to - `tbHelp_ToggleExpandSignatureHelp` on key-down ([Window.md](docs/IDE/Menu/Window.md) lists +- **F1 fires, and is shared with the IDE (P2).** The default keymap binds it + to `tbHelp_ToggleExpandSignatureHelp` on key-down ([Window.md](docs/IDE/Menu/Window.md) lists the keymap), a command that acts only while signature help is showing. The add-in's `f1` fired with the focus in the code editor, in the DEBUG CONSOLE's entry box and on nothing; it typed nothing, and Monaco's command palette, which Monaco binds to F1, did not open --- @@ -220,26 +213,25 @@ Read at `main.js@1002292` (`toolWindowElementAddChild`) and `@1005960` [test/addin/panes.test.mjs](test/addin/panes.test.mjs). - **A tool window is part of the main document**, inside an open shadow root, not an iframe. - Measured on Samples 10 and 15: `toolWindowsById` is keyed by the *second* argument the - add-in gave `ToolWindows.Add` (`"GlobalSearchAddInData"`, `"WaynesWindowData"`), its - `bodyElement` is in the shadow root, and the root's host is `#toolWindow` - (`#toolWindow900`). A window an add-in created and has not shown is there already, and - every element in it has no size. A window's content can be taller than the window: Sample - 10's eleventh button had a size and a place, but its place was under the window's bottom - edge, where a click lands on the resize handle. + On Samples 10 and 15, `toolWindowsById` is keyed by the *second* argument the add-in gave + `ToolWindows.Add` (`"GlobalSearchAddInData"`, `"WaynesWindowData"`), its `bodyElement` is + in the shadow root, and the root's host is `#toolWindow` (`#toolWindow900`). A window an + add-in created and has not shown is there already, and every element in it has no size. A + window's content can be taller than the window: Sample 10's eleventh button had a size and + a place, but its place was under the window's bottom edge, where a click lands on the + resize handle. - **A window's id is its identity, and a window given none shares the id `""`.** `createToolWindow` (`main.js@992211`) files a window under the second argument of `ToolWindows.Add`, and `createToolWindowById` returns the window the page already has under that id, with its body emptied, rather than a new one; the add-in's new `ToolWindow` - is then bound to it, since the page answers with its number. Measured on BETA 983 and 995: P9's - window given no id was under `""`, and the last test of - [panes.test.mjs](test/addin/panes.test.mjs) opened two windows given no id and got one, - titled by the second, holding the second's element and what was then added through the - first's object, while the first's own element was gone. So **every tool window needs an - id of its own**; the published ToolWindows page said to leave it out for a window that is - not kept, and has an IMPORTANT now. The same rule is why a restart does no harm to a window - with an id: the add-in's `Add` after the restart gets its old window back, emptied and - shown again (P9). None of the shipped samples leaves the id out. + is then bound to it, since the page answers with its number. P9's window given no id was + under `""`, and the last test of [panes.test.mjs](test/addin/panes.test.mjs) opened two + windows given no id and got one, titled by the second, holding the second's element and + what was then added through the first's object, while the first's own element was gone. + So **every tool window needs an id of its own**; the published ToolWindows page has an + IMPORTANT saying so. The same rule is why a restart does no harm to a window with an id: + the add-in's `Add` after the restart gets its old window back, emptied and shown again + (P9). None of the shipped samples leaves the id out. - **An add-in's toolbar button is `#addinButton-`**, with the id the add-in gave `AddButton`, inside `#rootMenu2`, and its caption as its `title`. - **`HtmlElements.Add(id, tagName)` accepts any tag.** The four IDE widget tags (`chartjs`, @@ -255,15 +247,15 @@ Read at `main.js@1002292` (`toolWindowElementAddChild`) and `@1005960` the published ToolWindow page says so. - **Property sets go straight to the DOM**, as `r[a]=e.value`: `innerHTML`, `src` and `srcdoc` pass through unchanged. **A property whose name starts with `on` is dropped - silently, and the call still reports success** --- measured (P4): the probe's `.onclick = - "..."` raised no error, and the element had neither an `onclick` property nor attribute. - The test is on the last name of the path. A step in a property path that is an array is - called as a function, so DOM methods can be reached too. + silently, and the call still reports success** (P4): the probe's `.onclick = "..."` raised + no error, and the element had neither an `onclick` property nor attribute. The test is on + the last name of the path. A step in a property path that is an array is called as a + function, so DOM methods can be reached too. - **An inline handler inside `innerHTML` runs as the IDE page's own script (P4).** It is an - attribute, not a property, so it is not dropped. Measured: an `` set through `innerHTML` ran its handler at once, with nothing clicked, and - an inline `onclick` ran when clicked; both saw `typeof openEditors === "object"`, a global - of the IDE's page. That is the one way an add-in can call the page's internals; see [Open + attribute, not a property, so it is not dropped. An `` set + through `innerHTML` ran its handler at once, with nothing clicked, and an inline `onclick` + ran when clicked; both saw `typeof openEditors === "object"`, a global of the IDE's page. + That is the one way an add-in can call the page's internals; see [Open decisions](#open-decisions) for where it may be used. **Sample 15 already relies on it:** each search result it gives its list view's `addItem` is HTML with an inline `onclick='raiseEvent("onClickMatch", event, true, path, line, column)'`, and a click on @@ -281,7 +273,7 @@ Read at `main.js@1002292` (`toolWindowElementAddChild`) and `@1005960` `rootEventHandler` and calls `rootEventHandler[name](event)`. Only three kinds of node have one: a `listview` or `virtuallistview` container (the list view object), the shadow root of an `AddMonacoWidget` widget (the widget's own element), and one of the IDE's - dialogs. **So `raiseEvent` from plain tool-window HTML throws (P12, measured):** + dialogs. **So `raiseEvent` from plain tool-window HTML throws (P12):** `TypeError: Cannot read properties of null (reading 'rootEventHandler')`, at the shadow root, whose `parentNode` is `null`, and the add-in's listener is not called. **The stored function can be called directly:** `onclick='this.parentNode.p12Event(event)'` reached the @@ -300,9 +292,9 @@ Read at `main.js@1002292` (`toolWindowElementAddChild`) and `@1005960` `Content-Security-Policy`, `http-equiv` or `frame-ancestors` in any `ide\*.htm` --- and neither does the compiler's HTTP header template *(reported)*. The host DLL, `bin/twinBASIC_ide_win32.dll`, names no WebView2 navigation event at all, so nothing - intercepts a frame's navigation. So the most expensive tier of the June draft --- whole - pages inside the IDE --- is the cheapest, and the site's own navigation and search come - with it. Measured with the probe lane, on pages it serves itself on `localhost`: + intercepts a frame's navigation. So whole pages inside the IDE are the cheapest option, + and the site's own navigation and search come with it. With the probe lane serving pages + itself on `localhost`: - the frame loaded the page whose URL the add-in set as `src` (the server saw `sec-fetch-dest: iframe`), followed a link in the page, and moved again when the add-in set `src` a second time; the add-in's `"load"` listener heard every load; @@ -312,7 +304,7 @@ Read at `main.js@1002292` (`toolWindowElementAddChild`) and `@1005960` add-in's `f1`, and did once the focus was back in the IDE's own document; - **the page's colour scheme is WebView2's, never the IDE's.** The IDE sets none on its WebView2 --- no `ColorScheme` in `main.js`, no `PreferredColorScheme` in the host DLL - --- so a page's `prefers-color-scheme` is Windows' app mode by default. On this + --- so a page's `prefers-color-scheme` is Windows' app mode by default. On the lab machine all three were dark, so the lane cannot tell them apart; a lab IDE whose WebView2 was told to prefer light (`--blink-settings=preferredColorScheme=1` added to its `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`) showed the framed page light, and its own @@ -321,16 +313,16 @@ Read at `main.js@1002292` (`toolWindowElementAddChild`) and `@1005960` pane matches the IDE only when Windows' app mode happens to agree** --- see Stage 3's embedded mode. - **The live site works in the frame too** (lab, 2026-09-24). The KeyboardShortcuts page on - docs.twinbasic.com loaded in 0.9 s as a cross-site frame, with a process and a DevTools - target of its own (type `iframe` in `/json/list`; the page's `Page.getFrameTree` does not - list it). It applied its own dark theme, had its search box and `lunr`, scrolled under - the wheel, and a link to Host navigated the frame. The site, served by GitHub Pages, sends - neither `X-Frame-Options` nor a CSP (`curl -I`, 2026-09-24), and no page of the built - site has a `target="_blank"` link, so its links stay in the frame. The host DLL does name - `NewWindowRequested`, and what it does with a new window was not tried, since it might - start a browser. A link to a site that refuses to be framed, such as GitHub, would show - the browser's error page in the pane; not tried either. + **The live site works in the frame too** (checked in the lab, not a lane). The + KeyboardShortcuts page on docs.twinbasic.com loaded in 0.9 s as a cross-site frame, with + a process and a DevTools target of its own (type `iframe` in `/json/list`; the page's + `Page.getFrameTree` does not list it). It applied its own dark theme, had its search box + and `lunr`, scrolled under the wheel, and a link to Host navigated the frame. The site, + served by GitHub Pages, sends neither `X-Frame-Options` nor a CSP (`curl -I`), and no page + of the built site has a `target="_blank"` link, so its links stay in the frame. The host + DLL does name `NewWindowRequested`, and what it does with a new window was not tried, + since it might start a browser. A link to a site that refuses to be framed, such as + GitHub, would show the browser's error page in the pane; not tried either. 3. **The IDE's WEBPAGE panel**, a second native WebView2 whose default URL is `https://www.google.com`, controlled through `hostAppObject.SetAdditionalWebview2Url` and the `tbWebpage_ShowPanel` command *(reported)*. Page internals only. @@ -368,7 +360,7 @@ have: writing into the install, which every new build replaces. Still deferred. ### What is under the cursor - **The add-in API gives the editor's text, selection and cursor, and nothing about - symbols.** Hence the June draft's own word extraction ([Stage + symbols.** Hence the add-in's own word extraction ([Stage 4](#stage-4-the-add-in-in-increments)) and, for context, its own parser of the project's source. - **The compiler knows, and names the package, the container and the kind (P5).** Measured @@ -418,7 +410,7 @@ have: writing into the install, which every new build replaces. Still deferred. ### Dialogs - `Host.ShowMessageBox` and `Host.ShowNotification` are drawn in the page, not as native - dialogs, and a harness reads them and clicks them (measured on Sample 10). A message box + dialogs, and a harness reads them and clicks them (checked on Sample 10). A message box is a `.modalDialogContainer` holding a `.modalTitleBar` (the title as a text node, then a close button), a `.simpleMsgBox` with the message and a `.msgBoxButton` per button; the add-in's call returns once one is clicked. A notification's text is the `.msgBoxText` of @@ -426,14 +418,14 @@ have: writing into the install, which every new build replaces. Still deferred. - **The IDE calls `alert()` at 37 sites**, 33 in `main.js` and 4 in `main2.js`, and never `confirm()` or `prompt()`. An `alert()` blocks the renderer, so the harness records and dismisses every one (`Page.javascriptDialogOpening`, then `Page.handleJavaScriptDialog`), - which `attachIde` now does. The ones an add-in test could reach: the rename provider + which `attachIde` does. The ones an add-in test could reach: the rename provider (`alert("need to massage workspace edits here...")`), Find with an invalid regular expression, and an unknown message on any of the compiler's sockets. A notification's "Copy to clipboard" link also calls `alert()`, but plain `ShowNotification` messages hide that link. **An alert that opened before the harness attached cannot be dismissed over - CDP** (measured): the page then answers nothing, and the harness reports it as the likely - cause. The IDE's own candidates are its "IDE startup failure" alert and "Bad command line - syntax.", which `launchIde`'s single argument never provokes. + CDP**: the page then answers nothing, and the harness reports it as the likely cause. The + IDE's own candidates are its "IDE startup failure" alert and "Bad command line syntax.", + which `launchIde`'s single argument never provokes. ### IDE state outside the install @@ -441,27 +433,20 @@ have: writing into the install, which every new build replaces. Still deferred. `IDESettings` (13 values --- `GENERAL`, `GENERAL2`, `LAYOUTv2`, `WEBPANEL_SETTINGS`, `targetArchitectureMemory`, the licence values, ...), `ProjectState` (one value per project ever opened), `RecentlyOpened` (21 values) and `Window` (6). Every IDE a user runs shares - them, and so does every IDE a harness starts. -- **The existing harness already leaves entries there.** On 2026-09-23, **318 of the 424 - `ProjectState` values** were temp projects from `examples.bat` lanes, `%TEMP%\tbprobe` and - scratchpad probes, and **all 21 `RecentlyOpened` entries** were temp projects: the user's - own recent projects had been pushed out of the IDE's recent list entirely. Both were - cleared by hand the same day --- 339 `ProjectState` values by then, because a harness run - in another session had added 21 more in the meantime. [Stage - 1](#stage-1-testing-add-ins-by-machine) fixes this for every harness tool, not only for - add-in tests. + them, and so does every IDE a harness starts. A harness that opens temp projects fills + `ProjectState` with them and pushes the user's own recent projects out of + `RecentlyOpened`, so every harness tool puts the registry back ([Stage + 1](#stage-1-testing-add-ins-by-machine)). - **The `.twinproj` association** is `HKCU\Software\Classes\.twinproj` → - `twinBASIC.ProjectFile`, and it currently points at the newest install's (BETA 995) `twinBASIC.exe`. A - harvested finding said every launch re-registers it. Key timestamps say otherwise: - `DefaultIcon` and `shell\open\command` were last written when BETA 983 was installed, and - a day of launches of that build did not touch them. **The IDE rewrites them when its path - differs**, measured in item 2: read while an IDE copy in `%TEMP%` was running, the keys - pointed into the copy, while the real install's IDE, run the same way, left them alone. - So a copy points the user's association at a folder that is about to be deleted, and - [tb-registry.mjs](scripts/lib/tb-registry.mjs) puts it back (three writes). + `twinBASIC.ProjectFile`, and it points at the newest install's `twinBASIC.exe`. **The IDE + rewrites `DefaultIcon` and `shell\open\command` when its path differs** (an IDE copy in + `%TEMP%` pointed them into the copy; the real install's IDE left them alone), and does not + rewrite them on every launch. So a copy points the user's association at a folder that is + about to be deleted, and [tb-registry.mjs](scripts/lib/tb-registry.mjs) puts it back + (three writes). - **`%APPDATA%\twinBASIC`** holds the user's downloaded packages and empty `addins\win32`, `addins\win64`, `locale` and `themes` folders, and it is shared by every install. A - compile session wrote nothing to it. + compile session writes nothing to it. - **`SaveSetting` from an add-in writes to the same tree**, under `VB and VBA Program Settings\`, so it is shared with any installed copy of the same add-in. A test that changes an add-in-wide option changes it for the user too, which @@ -473,303 +458,196 @@ have: writing into the install, which every new build replaces. Still deferred. ## The plan -### Stage 0: this file +### Stage 0: where the add-in lives -Done: the June draft is folded in here, and WIP.md points here. **The add-in's source goes -in `add-in/` at the repository root**, where the June draft put it, as an exported source +**The add-in's source goes in `add-in/` at the repository root**, as an exported source tree (`Settings` plus `Sources/*.twin`, the shape `tbrun` takes), so that it diffs as text -and stays outside `docs/`, where the publish allowlist would refuse it. +and stays outside `docs/`, where the publish allowlist would refuse it. `add-in/` holds the +add-in's tree and nothing else: `stageProject` copies the whole folder it is given and packs +the copy, so a probe kept inside it would be packed into the add-in's project. ### Stage 1: testing add-ins by machine -Everything after this stage is developed against it. - -1. **One library instead of logic inside two scripts.** The code that starts the IDE, - attaches, waits for the project, reads the console, watches for a compiler crash and - kills the process tree is written inline in [scripts/tbbuild.mjs](scripts/tbbuild.mjs) - and [scripts/tbrun.mjs](scripts/tbrun.mjs), and finding the IDE is written three times - (`tbbuild`, `tbrun`, [scripts/lib/tb-install.mjs](scripts/lib/tb-install.mjs)). Move it - into `scripts/lib/`, keep both scripts' behaviour and exit codes exactly, and check the - move with a full `examples.bat` run (1,116 samples, ~110 s) and a `tbrun` probe. - - **Done:** [scripts/lib/tb-ide.mjs](scripts/lib/tb-ide.mjs). 14 fixture cases were run - before and after --- clean, compile errors, warnings, the BUGS-TO-REPORT crash, argument - errors, and `tbrun` output and errors --- with every exit code and output line the same - apart from two bug fixes, and `examples.bat` passed 1,116 of 1,116 both times. The two - bugs, a relative project path that never loaded and `tbrun --ide` building with a - different install, are in [WIP.Harness.md](WIP.Harness.md). Two more gaps showed up while - reading the code, and both matter here, because add-in tests will hit dialogs: **`tbbuild` - cannot record an `alert()`** --- it listens for `Page.javascriptDialogOpening` but never - sends `Page.enable`, and CDP delivers no Page event without it --- and **no - `Runtime.evaluate` has a timeout**, so a dialog blocking the renderer would hang the wait - loop rather than let it time out. Neither is tested yet. Item 5 fixes both and proves the - fix with a probe that opens a dialog. -2. **A private IDE for every lane.** The install is 91 MB (`bin` 37, `projects` 30, `ide` - 15, `packages` 7). Hardlink it into the lane's work folder, with a real, empty - `addins\win32` and `addins\win64`, so that a test add-in never loads into the user's IDE - and two lanes never share one. Copy, rather than link, anything the IDE writes to during a - session (**P11**), and copy everything when `%TEMP%` and the install are on different - volumes. - - **Done, as a copy rather than hardlinks:** - [scripts/lib/tb-ide-copy.mjs](scripts/lib/tb-ide-copy.mjs), described in [WIP.Harness.md, - A private IDE for every lane](WIP.Harness.md#a-private-ide-for-every-lane). P11 came back - negative --- a session writes nothing into its install --- and without `projects\` the - copy is 57 MB and takes 380 ms, so linking would have saved nothing worth the risk. The - copy's compiler loaded no add-in where the real install's loaded Global Search, the 14 - fixture cases gave identical output from it, and the real install stayed byte-identical. - **The work also found and closed a process leak** that every harness tool had: a compiler - the IDE restarts after a crash can start while `taskkill /T` walks the tree, and survives - it, holding the install's files open. The IDE now runs inside a kill-on-close job ([The IDE - runs inside a job](WIP.Harness.md#the-ide-runs-inside-a-job)), which also means a run that - dies takes its IDEs with it. -3. **Leave the registry as it was found**, for every harness tool: - - save `HKCU\Software\Classes\.twinproj` and `twinBASIC.ProjectFile` before a run, and - restore them once the last lane has ended; - - delete the `ProjectState` values the run created, and restore `RecentlyOpened`. Match - paths with either separator: some tools store them with forward slashes - (`C:/Users/.../Temp/tbprobe/...`), and a backslash-only pattern missed 15 of them in the - hand cleanup; - - save and restore the add-in's own `SaveSetting` key; - - refuse to start while `%APPDATA%\twinBASIC\addins\*` holds a DLL, until P6 says - whether that folder matters --- it does, and the lanes now keep out of it instead - (below). - - The complete answer is a separate Windows account for test runs, which only the user can - create. Start with the above. - - **Done for `tbbuild`, `tbrun` and `check_examples`:** - [scripts/lib/tb-registry.mjs](scripts/lib/tb-registry.mjs), described in [WIP.Harness.md, - What a run leaves in the registry](WIP.Harness.md#what-a-run-leaves-in-the-registry-and-putting-it-back). - The 14 fixture cases, run one at a time, and a full `examples.bat` run leave the - registry identical, value for value, with every output line unchanged. The work also - found an IDE bug, now in [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md): a recent list shorter - than 21 entries gets its empty slots filled with copies of the last entry. Item 4 added a - fourth thing to put back, the build target the IDE remembers for each project path, - because a lane that inherits `win64` builds and loads the wrong bitness. - - **The last two bullets are done in the runner (item 7).** It records the `SaveSetting` - keys a lane names and puts them back. It refused to start while - `%APPDATA%\twinBASIC\addins` held a DLL until P6 said the folder matters, and then that - refusal became an `APPDATA` of each lane's own: every IDE a lane starts, the add-in - builds' included, gets `\appdata`, and the lane checks afterwards that the IDE's - add-ins folder is under it (`checkAddinsRoot` in `tb-ide-addins.mjs`). So the user's add-ins - never load into a test IDE, and the user need not move them out to run the tests; - verified with a stand-in `%APPDATA%` holding the Global Search add-in, where the P6 lane's - IDE loaded its own probe alone. **Item 7 also corrected the recent list.** The - sweep was exact only on an empty list, which is what the list was when it was verified: a - run that began with one entry ended with seventeen copies of it, because of the bug above, - and a full list loses its oldest entry for every project a run opens. The tidy now - records the whole list and puts it back as found ([WIP.Harness.md, What a run leaves in - the registry](WIP.Harness.md#what-a-run-leaves-in-the-registry-and-putting-it-back)). -4. **Build, then load.** The add-in is a Standard DLL. In a staged copy of its tree, pin - `project.buildPath` to an explicit file in the lane IDE's `addins\\`, the way - `tbrun` pins its exe path: the default `${SourcePath}\Build\...` template has already - cost a run with an invisible Save dialog, and whether the samples' `${IdePath}` template - behaves any better is untested. Build, end that IDE, then start the same lane IDE on a - test project; its compiler loads the add-in as it starts. One project per IDE, as always. - - **Done, building into the work folder instead:** `buildAddin` in - [scripts/lib/tb-addin.mjs](scripts/lib/tb-addin.mjs) builds with the lane's copy into - `\out\`, and `addAddin` in `tb-ide-copy.mjs` then puts the DLL in the copy's - `addins\win32`. Built straight into `addins`, a rebuild would meet the previous build - loaded by the very IDE doing the building, and a loaded add-in cannot be overwritten - (P8). [WIP.Harness.md, Building an add-in and loading it](WIP.Harness.md#building-an-add-in-and-loading-it) - has the rest: how the build log is read, how a build is made for win32 or win64 (win32 - only until P7 was answered), and what was measured. - Samples 10 and 15 both built and loaded, and the tree staging that `tbrun` did is now - [scripts/lib/tb-project.mjs](scripts/lib/tb-project.mjs), shared by both. +Built. Everything after this stage is developed against it. [WIP.Harness.md](WIP.Harness.md) +has the harness side of each item. + +1. **One library for starting and driving the IDE:** + [scripts/lib/tb-ide.mjs](scripts/lib/tb-ide.mjs), shared by `tbbuild`, `tbrun` and + `check_examples`. Every CDP call has a time limit, and the connection records and + dismisses `alert()` dialogs. An alert already open before the harness attached is the one + case it cannot handle, and it says so. `launchIde` refuses a DevTools port another IDE + holds, since the harness would otherwise operate that IDE. +2. **A private IDE for every lane:** [scripts/lib/tb-ide-copy.mjs](scripts/lib/tb-ide-copy.mjs), + described in [WIP.Harness.md, A private IDE for every + lane](WIP.Harness.md#a-private-ide-for-every-lane). It is a copy, not hardlinks: a session + writes nothing into its install (P11), and without `projects\` the copy is 57 MB and takes + 380 ms. The copy's `addins\win32` and `addins\win64` are real and empty, so a test add-in + never loads into the user's IDE and two lanes never share one. The IDE runs inside a + kill-on-close job ([The IDE runs inside a + job](WIP.Harness.md#the-ide-runs-inside-a-job)), so a run that dies takes its IDEs with it + and no restarted compiler outlives `taskkill /T`. +3. **The registry is left as it was found**, for every harness tool: + [scripts/lib/tb-registry.mjs](scripts/lib/tb-registry.mjs), described in + [WIP.Harness.md, What a run leaves in the + registry](WIP.Harness.md#what-a-run-leaves-in-the-registry-and-putting-it-back). + - It saves `HKCU\Software\Classes\.twinproj` and `twinBASIC.ProjectFile` before a run and + restores them once the last lane has ended, and never puts back an association that + points into the temp folder. + - It deletes the `ProjectState` values the run created and restores the whole + `RecentlyOpened` list as found. Match paths with either separator: some tools store them + with forward slashes (`C:/Users/.../Temp/tbprobe/...`). A full list loses its oldest + entry for every project a run opens. + - It puts back the build target the IDE remembers for each project path, because a lane + that inherits `win64` builds and loads the wrong bitness. + - The add-in runner (item 7) records the `SaveSetting` keys a lane names and puts them back. + - Every IDE a lane starts, the add-in builds' included, gets an `APPDATA` of the lane's + own, `\appdata`, and the lane checks afterwards that the IDE's add-ins folder is + under it (`checkAddinsRoot` in `tb-ide-addins.mjs`). So the user's add-ins never load + into a test IDE, and the user need not move them out to run the tests. + - A recent list shorter than 21 entries gets its empty slots filled with copies of the + last entry (an IDE bug, in [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md)). + + The complete answer would be a separate Windows account for test runs, which only the + user can create. +4. **Build, then load.** `buildAddin` in [scripts/lib/tb-addin.mjs](scripts/lib/tb-addin.mjs) + builds the add-in with the lane's copy into `\out\`, for win32 or win64, and + `addAddin` in `tb-ide-copy.mjs` then puts the DLL in the copy's `addins\`. Built + straight into `addins`, a rebuild would meet the previous build loaded by the very IDE + doing the building, and a loaded add-in cannot be overwritten (P8). + [WIP.Harness.md, Building an add-in and loading + it](WIP.Harness.md#building-an-add-in-and-loading-it) has how the build log is read. The + tree staging is [scripts/lib/tb-project.mjs](scripts/lib/tb-project.mjs), shared with + `tbrun`. One project per IDE, as always: build, end that IDE, then start the same lane IDE + on a test project, and its compiler loads the add-in as it starts. 5. **Operating the IDE and reading it**, as library calls over CDP: + [scripts/lib/tb-operate.mjs](scripts/lib/tb-operate.mjs), with `readCrash` in + `tb-ide.mjs`, described in [WIP.Harness.md, Operating the IDE and reading + it](WIP.Harness.md#operating-the-ide-and-reading-it). - open a file: `fs.tree.resolvePath("twinbasic://Sources/")`, then - `openEditors.openFile(node,false,false,false,line,col)`, line and column counted from 1 - (measured); + `openEditors.openFile(node,false,false,false,line,col)`, line and column counted from 1; - move the cursor or select: `window.editor` is the one Monaco code editor, given the model of whichever file's tab is selected (`setPosition`, `setSelection`, - `getModel().getValue()`; measured). Not `monaco.editor.getEditors()`, which also + `getModel().getValue()`). Not `monaco.editor.getEditors()`, which also returns editors that add-ins created *(reported)*; - press keys: `Input.dispatchKeyEvent` key-down, then key-up, with real `key` and `code` values, less than 500 ms apart; - click: real `Input.dispatchMouseEvent` presses at the element's centre. The IDE's own - controls ignore `element.click()` --- `tbrun` learned that on `#buildIcon`; - - ask which add-ins loaded: `loadedAddins(c)` in `tb-ide-addins.mjs` (done for item 2), which - lists a DLL that failed to load as `Unknown Addin`; - - build the open project: `buildProject(c)` in `tb-ide.mjs` (done for item 4); + controls ignore `element.click()`. `click` waits up to five seconds for its target, + since a list view draws a row a moment after the row is in its data; + - ask which add-ins loaded: `loadedAddins(c)` in `tb-ide-addins.mjs`, which lists a DLL + that failed to load as `Unknown Addin`; + - build the open project: `buildProject(c)` in `tb-ide.mjs`; - read a tool window through `toolWindowsById[].bodyElement`; read the DEBUG CONSOLE's backing array, notifications and message boxes; dismiss any `alert()`; - notice a compiler restart or crash, as `tbbuild`'s console check already does. - - **Done:** [scripts/lib/tb-operate.mjs](scripts/lib/tb-operate.mjs), with `readCrash` in - `tb-ide.mjs`, described in [WIP.Harness.md, Operating the IDE and reading - it](WIP.Harness.md#operating-the-ide-and-reading-it). Both acceptance scenarios were - carried out with it by hand on a lab IDE: **Sample 15** --- the toolbar button, the - search typed key by key, both files' results, and a click on one match that opened - `Haystack.twin` at line 4, column 13 --- and **Sample 10** --- its tool window, the - three-button message box answered `button2`, the follow-up answered `ok`, a notification - and a DEBUG CONSOLE line. The two gaps item 1 found are closed: every CDP call has a time - limit, and the connection records and dismisses dialogs, proved with an `alert()`; an - alert already open before the harness attached is the one case it cannot handle, and it - says so. Two dangers were closed on the way: `launchIde` refuses a DevTools port another - IDE holds, since the harness would otherwise operate that IDE, and the registry tidy no - longer puts back an association that pointed into the temp folder. The runner (item 7) - turns the two scenarios into tests. -6. **No real side effects.** The add-in's URL opener checks an environment variable (name to - be chosen) and, when it is set, prints `open ` to the DEBUG CONSOLE instead of + notice a compiler restart or crash, as `tbbuild`'s console check does. +6. **No real side effects: `TB_ADDIN_TEST`.** An add-in's URL opener treats the variable as + set when it is not empty, and then prints `open ` to the DEBUG CONSOLE instead of starting a browser. On a private desktop a real browser would start where nobody can see - it and outlive the run. **P10** checks that the variable reaches the compiler process. - - **Done:** the variable is **`TB_ADDIN_TEST`**, and P10 answered yes (Stage 2 has the - measurement). An add-in treats it as set when it is not empty. `launchIde` in - [tb-ide.mjs](scripts/lib/tb-ide.mjs) sets it to `1` for **every** IDE the harness starts, - `tbbuild`'s, `tbrun`'s and `examples.bat`'s included, because each of them loads whatever - add-ins the user has installed, on a desktop nobody watches; a caller's `env` can set it - otherwise, or leave it out with the value `undefined`. `openedUrls(c, { since })` in - [tb-operate.mjs](scripts/lib/tb-operate.mjs) reads the `open ` lines back, and - `consoleMark(c)` in `tb-ide-console.mjs` takes the mark that `since` names, so a scenario asks what - was opened after the key it pressed. A line counts only when what follows `open ` has no - white space in it, as a URL has none, so an ordinary line that starts with the word is not - read as one. `PrintText` stores its text escaped (`` as `<b>`), so a URL comes - back exactly as printed, `&` included. - - **The probe is a lane.** It was thirty lines in scratch at first (`Host_OnProjectLoaded` - printing `Environ$("TB_ADDIN_TEST")`, the same through `GetEnvironmentVariableW`, and its - own process id), and nothing guarded the answer although the rule above depends on it. - It is now [test/addin/probes/env](test/addin/probes/env) with - [env.test.mjs](test/addin/env.test.mjs): the variable reads `1` both ways in the - compiler's process, again in the process a restart starts, and unset in an IDE started - without it. The add-in checks the variable itself from Stage 4 on (increment 1 below). - `add-in/` holds the add-in's tree and nothing else: `stageProject` copies the whole - folder it is given and packs the copy, so a probe kept inside it would be packed into - the add-in's project. -7. **A runner.** Scenarios in JavaScript under `node:test`, one IDE per test project, lanes - by `--port` as today. The add-in's pure twinBASIC logic --- word extraction, lookup --- is - tested without loading any add-in: a test project holds those modules and a - `[RunAfterBuild]` runner that prints one line per case, `tbrun` builds and runs it, and - the JavaScript side checks the lines. The wrapper is `addin-test.bat`, outside every gate - and CI for the reason `examples.bat` is: it needs Windows and a twinBASIC install. - - **Done:** [scripts/addin_test.mjs](scripts/addin_test.mjs), with the lanes in + it and outlive the run. `launchIde` in [tb-ide.mjs](scripts/lib/tb-ide.mjs) sets it to `1` + for **every** IDE the harness starts, `tbbuild`'s, `tbrun`'s and `examples.bat`'s + included, because each of them loads whatever add-ins the user has installed, on a desktop + nobody watches; a caller's `env` can set it otherwise, or leave it out with the value + `undefined`. `openedUrls(c, { since })` in [tb-operate.mjs](scripts/lib/tb-operate.mjs) + reads the `open ` lines back, and `consoleMark(c)` in `tb-ide-console.mjs` takes the + mark that `since` names, so a scenario asks what was opened after the key it pressed. A + line counts only when what follows `open ` has no white space in it, as a URL has none. + `PrintText` stores its text escaped (`` as `<b>`), so a URL comes back exactly as + printed, `&` included. The rule rests on P10, which is a lane: + [test/addin/probes/env](test/addin/probes/env) with [env.test.mjs](test/addin/env.test.mjs). +7. **A runner.** [scripts/addin_test.mjs](scripts/addin_test.mjs), with the lanes in [test/addin/](test/addin/) and what a scenario gets in [scripts/lib/tb-lane.mjs](scripts/lib/tb-lane.mjs), described in [WIP.Harness.md, The - add-in test runner](WIP.Harness.md#the-add-in-test-runner). A lane is one scenario file, - run in a process of its own with its own port and copy of the install; the runner owns - the registry, the add-ins' saved settings included, and checks it afterwards. Ctrl+C and - a lane timeout both end the lanes and still put the registry back. The pure-logic tests - wait for Stage 4, which writes that logic. They belong in a lane too, built and run in - the lane's own copy rather than by `tbrun`: a `tbrun` started under the runner leaves its + add-in test runner](WIP.Harness.md#the-add-in-test-runner). A lane is one scenario file + under `node:test`, run in a process of its own with its own port and copy of the install; + the runner owns the registry, the add-ins' saved settings included, and checks it + afterwards. Ctrl+C and a lane timeout both end the lanes and still put the registry back. + The wrapper is `addin-test.bat`, outside every gate and CI for the reason `examples.bat` + is: it needs Windows and a twinBASIC install. `removeTree` retries a delete that an ending + IDE still blocks, since on Node 24 `rmSync`'s own `maxRetries` does not. + + **Not done:** the pure-logic tests of Stage 4's code (word extraction, lookup). A test + project holds those modules and a `[RunAfterBuild]` runner that prints one line per case, + and the JavaScript side checks the lines. They belong in a lane, built and run in the + lane's own copy rather than by `tbrun`: a `tbrun` started under the runner leaves its registry entries to the runner, which sweeps only the lanes' folders. - Two library changes came out of it: `click` waits up to five seconds for its target, - since a list view draws a row a moment after the row is in its data, and `removeTree` - retries a delete that an ending IDE still blocks, since on Node 24 `rmSync`'s own - `maxRetries` does not. - -**Done when** the harness operates two shipped samples end to end, and the user's registry -is unchanged afterwards: +Stage 1's acceptance, met: both scenarios pass under `addin-test.bat`, and the whole +registry, `IDESettings` included through hashes, is identical around the run. -- **Sample 10:** toolbar button, then its tool window, then a message box. +- **Sample 10:** toolbar button, then its tool window, then a message box (the three-button + box answered `button2`, the follow-up answered `ok`, then a notification and a DEBUG + CONSOLE line). - **Sample 15:** type a search, see the results, click one, and the right file opens at the - right line. - -**Met on 2026-09-24, BETA 983:** both scenarios pass under `addin-test.bat`, and a -comparison of the whole registry around the run, `IDESettings` included through hashes, -was identical. + right line (`Haystack.twin`, line 4, column 13). ### Stage 2: probes that decide the design Most probes are a small add-in plus a scenario. P5, P11 and P13 need only CDP and the file -system. P14, planned as a question for upstream, was answered from the compiler's own code -and then measured. Record every answer in this file with the build number it was measured -on. +system. Record every answer in this file with the build number it was measured on. -**Stage 2 is done** (2026-09-25, BETA 983): every question is answered, and the eight lanes -pass together with the two sample lanes, ten in all, in 2 minutes 21 seconds at two at a -time. +All fourteen questions are answered, and the ten lanes (eight probe lanes and the two sample +lanes) pass together in about 2 minutes 21 seconds at two at a time. **A probe whose answer something else rests on becomes a lane**: its add-in in `test/addin/probes//`, its scenario beside the others, listed in `lanes.mjs`, with each test asserting what the build did. A later build that behaves differently then fails the -run, and the failure says what to update. [keys.test.mjs](test/addin/keys.test.mjs) (P1, -P2) is the first --- the KeyboardShortcuts page's NOTE and two entries in BUGS-TO-REPORT.md -rest on it --- and [panes.test.mjs](test/addin/panes.test.mjs) (P3, P4, P12) the second, -under the NOTEs on the HtmlElement, HtmlElementProperties, HtmlElements and ToolWindow -pages. [symbols.test.mjs](test/addin/symbols.test.mjs) (P5) holds up Stage 4's context -and an entry in BUGS-TO-REPORT.md; it needs no add-in, only the project in -`probes/symbols`. [ideserver.test.mjs](test/addin/ideserver.test.mjs) (P13) holds up the -offline route, and [appdata.test.mjs](test/addin/appdata.test.mjs) (P6) the lanes' own -`APPDATA` and the FAQ's answer on where add-ins go. [arch.test.mjs](test/addin/arch.test.mjs) -(P7) holds up the Add Ins page's account of which folder loads when, and the win64 builds -`buildAddin` now makes; [reload.test.mjs](test/addin/reload.test.mjs) (P8, P9) the account -of a compiler restart on the tbIDE package page and the ToolWindows page; -[entry.test.mjs](test/addin/entry.test.mjs) (P14) the entry point's NOTE on the tbIDE -package page; and [env.test.mjs](test/addin/env.test.mjs) (P10) the rule that an add-in under -test opens nothing, which reads `TB_ADDIN_TEST`. P9 turned up the shared id `""` of windows given none, and the test of it -went in the panes lane, with the rest of what a tool window does. A probe that settles a -question once stays in scratch; so did the two P3 checks that need the -network or a changed WebView2, the live site in the frame and the colour scheme with +run, and the failure says what to update. The lanes and what rests on each: + +- [keys.test.mjs](test/addin/keys.test.mjs) (P1, P2): the KeyboardShortcuts page's NOTE and + two entries in BUGS-TO-REPORT.md. +- [panes.test.mjs](test/addin/panes.test.mjs) (P3, P4, P12, and the shared id `""` found + with P9): the NOTEs on the HtmlElement, HtmlElementProperties, HtmlElements and ToolWindow + pages. +- [symbols.test.mjs](test/addin/symbols.test.mjs) (P5): Stage 4's context and an entry in + BUGS-TO-REPORT.md. It needs no add-in, only the project in `probes/symbols`. +- [ideserver.test.mjs](test/addin/ideserver.test.mjs) (P13): the offline route. +- [appdata.test.mjs](test/addin/appdata.test.mjs) (P6): the lanes' own `APPDATA` and the + FAQ's answer on where add-ins go. +- [arch.test.mjs](test/addin/arch.test.mjs) (P7): the Add Ins page's account of which folder + loads when, and the win64 builds `buildAddin` makes. +- [reload.test.mjs](test/addin/reload.test.mjs) (P8, P9): the account of a compiler restart + on the tbIDE package page and the ToolWindows page. +- [entry.test.mjs](test/addin/entry.test.mjs) (P14): the entry point's NOTE on the tbIDE + package page. +- [env.test.mjs](test/addin/env.test.mjs) (P10): the rule that an add-in under test opens + nothing, which reads `TB_ADDIN_TEST`. + +A probe that settles a question once stays in scratch. So do the two P3 checks that need the +network or a changed WebView2: the live site in the frame, and the colour scheme with WebView2 preferring light. +The table gives each answer in brief; the sections above have the detail. + | # | Question | What it decides | |---|---|---| -| P1 | Do `{ctrl}` and `{alt}` add-in shortcuts ever fire? Register `{ctrl}{shift}d`, `{alt}f`, `{shift}d`, `d` and `f1`, and press each. **Answered, BETA 983 and 995: no.** `d`, `{shift}d`, `f1` and `{shift}f1` fire; `{ctrl}{shift}d`, `{ctrl}d` and `{alt}f` fire only when the same key was pressed on its own less than 500 ms before. Queued in BUGS-TO-REPORT.md; the KeyboardShortcuts page has a NOTE. | the bug report; which key the add-in uses; the NOTE on the KeyboardShortcuts page | +| P1 | Do `{ctrl}` and `{alt}` add-in shortcuts ever fire? **Answered, BETA 983 and 995: no.** `d`, `{shift}d`, `f1` and `{shift}f1` fire; `{ctrl}{shift}d`, `{ctrl}d` and `{alt}f` fire only when the same key was pressed on its own less than 500 ms before. Queued in BUGS-TO-REPORT.md; the KeyboardShortcuts page has a NOTE. | the bug report; which key the add-in uses; the NOTE on the KeyboardShortcuts page | | P2 | Does the add-in's `f1` fire with focus in the code editor, and what happens with signature help showing? **Answered, BETA 983 and 995: yes.** It fires with the focus in the code editor, in the DEBUG CONSOLE and on nothing, and types nothing. With signature help showing, the IDE expands or collapses it as well, and logs `command failed: "tbHelp_ToggleExpandSignatureHelp"`. | F1 or another key --- F1 | -| P3 | Does an `iframe` of a documentation page load and navigate inside a tool window? Size, scrolling, theme. **Answered, BETA 983 and 995: yes.** It loads, follows its own links, moves when the add-in sets `src`, scrolls, and fills the window under a wrapper's flex layout; the live site works too. Keys in the frame never reach the add-in, and the page's colour scheme is Windows', not the IDE's. | how pages are shown --- in the pane | -| P4 | Does `innerHTML` render, and do inline handlers in it run page script? **Answered, BETA 983 and 995: yes, and yes.** It renders, and its inline handlers run as the IDE page's own script --- an ``'s `onerror` with nothing clicked --- with its globals in reach. A property whose name starts with `on` is dropped, and the add-in hears no error. | how summaries are drawn; whether the page-internals route exists --- it does | -| P5 | What does hover return for `MsgBox`, `Collection.Add`, `ToolWindows.Add` and a symbol declared in the project? What does definition return for a package symbol? **Answered, BETA 983 and 995:** hover gives the declaration, which says the kind, then a heading naming where it is declared: `in VBA.Interaction`, `in VBA._Collection`, `in tbIDE.IToolWindowsV1`, `in SymbolsProbe.Symbols` --- a class's members by its default interface, not by the class's name. Definition gives the declaration in the package's own source, `.../Packages/VBA/Sources/Interaction.twin`, which the IDE opens. Nothing for `Debug.Print` or a statement. Only page script can ask. | compiler-assisted context is possible; which route is [Stage 4](#stage-4-the-add-in-in-increments), increment 3 | -| P6 | Does the compiler also load add-ins from `%APPDATA%\twinBASIC\addins\`? This needs a DLL placed there for a moment, and the user's own IDE would load it too --- **ask before running it.** **Answered, BETA 983 and 995: yes**, from `addins\win32` there and not from `addins` itself. The page expands `%APPDATA%` in the IDE's environment and sends the folder, and the compiler loads from what it is sent. Measured with `APPDATA` pointed at a folder of the lane's own, so no DLL went in the user's. | harness isolation --- every lane IDE gets an `APPDATA` of its own | -| P7 | Which bitness does the compiler start in, and does switching the build target restart it in the other one and load the other `addins` folder? **Answered, BETA 983 and 995: yes, and yes.** The target a project opens in picks the compiler --- win32 when the IDE remembers none, `twinBASIC_win64_noDEP.exe` for a project remembered as win64 --- and each compiler loads the folders of its own bitness alone, the install's and `%APPDATA%`'s. Switching the target of an open project restarts the compiler in the other bitness, and the new one loads the other folders: a 64-bit build of the probe in each win64 folder loaded, and ran 64-bit, once the project was switched to win64, and the 32-bit ones again after a switch back. | building and testing both bitnesses --- `buildAddin` builds either, and a shipped add-in needs both | +| P3 | Does an `iframe` of a documentation page load and navigate inside a tool window? **Answered, BETA 983 and 995: yes.** It loads, follows its own links, moves when the add-in sets `src`, scrolls, and fills the window under a wrapper's flex layout; the live site works too. Keys in the frame never reach the add-in, and the page's colour scheme is Windows', not the IDE's. | how pages are shown --- in the pane | +| P4 | Does `innerHTML` render, and do inline handlers in it run page script? **Answered, BETA 983 and 995: yes, and yes.** Inline handlers run as the IDE page's own script, with its globals in reach. A property whose name starts with `on` is dropped, and the add-in hears no error. | how summaries are drawn; whether the page-internals route exists --- it does | +| P5 | What does hover return for `MsgBox`, `Collection.Add`, `ToolWindows.Add` and a symbol declared in the project? What does definition return for a package symbol? **Answered, BETA 983 and 995:** hover gives the declaration, then a heading naming where it is declared: `in VBA.Interaction`, `in VBA._Collection`, `in tbIDE.IToolWindowsV1`, `in SymbolsProbe.Symbols` --- a class's members by its default interface, not by the class's name. Definition gives the declaration in the package's own source, which the IDE opens. Nothing for `Debug.Print` or a statement. Only page script can ask. | compiler-assisted context is possible; which route is [Stage 4](#stage-4-the-add-in-in-increments), increment 3 | +| P6 | Does the compiler also load add-ins from `%APPDATA%\twinBASIC\addins\`? Needs a DLL placed there, which the user's own IDE would load too --- **ask before running it against the real `%APPDATA%`.** **Answered, BETA 983 and 995: yes**, from `addins\win32` there and not from `addins` itself. The compiler loads from the folder the page sends it. The lane points `APPDATA` at a folder of its own, so no DLL goes in the user's. | harness isolation --- every lane IDE gets an `APPDATA` of its own | +| P7 | Which bitness does the compiler start in, and does switching the build target restart it in the other one and load the other `addins` folder? **Answered, BETA 983 and 995: yes, and yes.** The target a project opens in picks the compiler, and each compiler loads the folders of its own bitness alone, the install's and `%APPDATA%`'s. | building and testing both bitnesses --- `buildAddin` builds either, and a shipped add-in needs both | | P8 | Is a loaded add-in DLL locked against being overwritten? **Answered, BETA 983 and 995: yes.** While its IDE runs, overwriting fails (`EBUSY`) and deleting fails (`EPERM`), though renaming works; the hold outlasts the compiler's exit by a few tens of milliseconds. The P9 lane checks all three again. | the rebuild loop --- the DLL is built outside `addins`, and copied in once the IDE has ended, or renamed aside first (P9) | -| P9 | Does a compiler restart reload add-ins from disk? **Answered, BETA 983 and 995: yes.** A restart --- the restart button, a switch of build target, or the IDE's own after a crash --- removes every add-in's buttons and shortcuts, leaves its windows showing `(currently unavailable)`, kills the old compiler, so that no `Class_Terminate` runs, and starts a compiler that loads whatever file is in the folders then. With the loaded DLL renamed aside, a restart loaded nothing; with a new build put in its place, the next restart loaded it, about a second after the click, and it got its old windows back by their ids. | a rebuild loop without restarting the IDE --- it works: rename aside, copy in, restart | -| P10 | Does an environment variable set by the harness reach the add-in (`Environ$`)? **Answered, BETA 983 and 995: yes**, through the launcher, the IDE and the compiler the IDE starts. With `TB_ADDIN_TEST=1` in `launchIde`'s environment, `Environ$` and `GetEnvironmentVariableW` both returned `1` in the add-in, whose process id was the compiler's, and a compiler started by the restart button, a new process, returned it too; left out, `Environ$` was empty and the Win32 call said unset. Held by [test/addin/env.test.mjs](test/addin/env.test.mjs). `WEBVIEW2_USER_DATA_FOLDER`, which `launchIde` always sets, arrived with the lane's port in it. | the side-effect switch | +| P9 | Does a compiler restart reload add-ins from disk? **Answered, BETA 983 and 995: yes.** The restart removes every add-in's buttons and shortcuts, leaves its windows showing `(currently unavailable)`, kills the old compiler so that no `Class_Terminate` runs, and starts a compiler that loads whatever file is in the folders then. A window comes back by its id. | a rebuild loop without restarting the IDE --- it works: rename aside, copy in, restart | +| P10 | Does an environment variable set by the harness reach the add-in (`Environ$`)? **Answered, BETA 983 and 995: yes**, through the launcher, the IDE and the compiler the IDE starts, and again in a compiler a restart starts. With `TB_ADDIN_TEST=1` in `launchIde`'s environment, `Environ$` and `GetEnvironmentVariableW` both returned `1`; left out, `Environ$` was empty and the Win32 call said unset. `WEBVIEW2_USER_DATA_FOLDER`, which `launchIde` always sets, arrived with the lane's port in it. | the side-effect switch | | P11 | Does the IDE write into its own install folder during a session? **Answered, BETA 983 and 995: no.** On 983 a compile, a compiler crash and a `tbrun` build-and-run left all 233 files byte-identical, mtimes included. On 995 the install (235 files) was byte-identical after exports, a `tbbuild` and two `tbrun` runs; `%APPDATA%` was not re-checked. | hardlinks or copies --- copies, for safety, at 380 ms | | P12 | Does `raiseEvent` from plain tool-window HTML throw? **Answered, BETA 983 and 995: yes** --- `TypeError: Cannot read properties of null (reading 'rootEventHandler')`, and the listener is not called. An inline handler that calls the listener `AddEventListener` stored on its parent, `this.parentNode.(event)`, reaches the add-in. | how the pane's events are written | -| P13 | Does the compiler's HTTP server serve any file placed under `ide\`? **Answered, BETA 983 and 995: yes**, and it is the page server, `twinBASIC_win32.exe --ide=`, not the compiler. Any file, byte for byte, below the page's own path, including one written after the IDE started; a frame with a relative `src` shows it on the IDE page's own origin. A query string makes a 404, and `.html` has no `Content-Type`. | an offline route --- it exists ([Offline](#ways-to-show-a-page)) | -| P14 | What do `tbCreateCompilerAddin_v2` and `_v3` expect? **Answered, BETA 983 and 995: what `tbCreateCompilerAddin` does.** The loader looks for the plain name, then `_v2`, then `_v3`, and calls whichever it finds with the `Host` alone and asks the result for `IAddInV1`. The names are version stamps: the linker exports a function named `tbCreateCompilerAddin` as `tbCreateCompilerAddin_v3` alone, and an IDE that knows none of a DLL's names refuses it as `compiled for a newer version of the twinBASIC IDE`, as a patched `_v4` was. | nothing in the design --- the add-in declares `tbCreateCompilerAddin` as the package says; the tbIDE page has a NOTE | +| P13 | Does the compiler's HTTP server serve any file placed under `ide\`? **Answered, BETA 983 and 995: yes**, and it is the page server, `twinBASIC_win32.exe --ide=`, not the compiler. A frame with a relative `src` shows the file on the IDE page's own origin. A query string makes a 404, and `.html` has no `Content-Type`. | an offline route --- it exists ([Offline](#ways-to-show-a-page)) | +| P14 | What do `tbCreateCompilerAddin_v2` and `_v3` expect? **Answered, BETA 983 and 995: what `tbCreateCompilerAddin` does.** The names are version stamps: the linker exports a function named `tbCreateCompilerAddin` as `tbCreateCompilerAddin_v3` alone, and an IDE that knows none of a DLL's names refuses it as `compiled for a newer version of the twinBASIC IDE`, as a patched `_v4` was. | nothing in the design --- the add-in declares `tbCreateCompilerAddin` as the package says; the tbIDE page has a NOTE | ### Stage 3: the symbol index, generated by the docs build -The June draft hand-wrote about 80 entries, and some of its URLs were wrong: `Collection` is -at `/tB/Modules/Collection`, not under VBRUN. The index is generated instead. - -- **From the documentation:** every page's `permalink:` exactly as written. Folder-style - pages keep their trailing slash (`/tB/Packages/VB/CheckBox/`), single-file pages do not - (`/tB/Packages/tbIDE/ToolWindows`), so a URL is copied, never assembled. **A member is - either a page or a heading.** `Collection.Add` has its own page, - `/tB/Modules/Collection/Add`; `ToolWindows.Add` is a heading on its class page, and its URL - uses the id tbdocs gives that heading (`### Add` gives `#add`), read from the build and - never computed a second time. Canonical URLs only, never a `redirect_from` alias. The - documentation defines two kinds of alias the index must know: the `$`, `B` and `W` - variants share the base page (`LenB` → `/tB/Modules/Strings/Len`), and attributes are - `/tB/Core/Attributes#`. -- **From the packages:** the public API from an `export` of the shipped packages --- the - export and its cache already exist in - [scripts/census_attributes.mjs](scripts/census_attributes.mjs). That supplies each - symbol's package, container and kind, and lists public symbols that have no page, which - measures documentation coverage as a side effect. **It must also supply each class's - default interface**, because that is what the compiler names a class's members by (P5): - hover says `in VBA._Collection` for `Collection.Add` and `in tbIDE.IToolWindowsV1` for - `ToolWindows.Add`, and a lookup that takes the compiler's word has to map `_Collection` - to `Collection`. -- **Output:** one JSON file published with the site, emitted the way - `assets/js/search-data.json` is, at a stable URL so that an installed add-in can fetch a - newer index. A copy is also built into the add-in, for when the site cannot be reached. -- **Gates:** every URL is under `/tB/` and resolves; an entry that disappears fails the - build, the way `builder/page-baseline.json` treats a fall in the page count --- which is - what catches a reworded heading breaking a member's anchor; retiring an entry follows the - rules in [Permanent Links](docs/Documentation/Permanent-Links.md). -- **An embedded mode for pages: P3 says the theme needs one.** The pages work in the pane - unmodified, but they follow Windows' app mode, not the IDE's theme, and the add-in cannot - reach into the frame to change that: the live site is cross-site, so neither its document - nor its storage is the IDE page's. The site can take the theme from its URL instead: a - `theme=dark|light` query parameter, read by the no-flash snippet in `renderHead` - ([builder/template.mjs](builder/template.mjs)) and set as `data-theme` without being - stored, so a reader's own choice on the site is left as it is. Hiding the header and - navigation is a separate, optional question, untested. **Deferred until the add-in works, - at least in part** (decided 2026-09-25): it changes what the published pages do, and it - needs a test that the parameter keeps working. It serves the live site only: the IDE's - own server answers any URL with a query string with a 404 (P13), so the offline route - would need the theme some other way. +The index is the `symbolIndex` task of the build, in [builder/symbols.mjs](builder/symbols.mjs), +and it publishes [`/tB/symbols.json`](docs/Documentation/Permanent-Links.md#the-symbol-index) --- +under `/tB/` because the file is part of the same contract as the URLs in it, and its URL is +fixed from here on. Permanent Links documents the format for any reader; +[Building](docs/Documentation/Building.md#the-symbol-index) and +[WIP.Build.md](WIP.Build.md#the-symbol-index-and-the-drift-guard-on-its-urls) the build side. +It has 5,536 entries at 4,086 distinct URLs; 782 KB, 52 KB gzipped, under 100 ms of the build. +The file is emitted the way `assets/js/search-data.json` is, so an installed add-in can fetch +a newer index; a copy is also to be built into the add-in, for when the site cannot be reached. -The June data model stands: +The data model: ```json { "name": "Add", "package": "tbIDE", "container": "ToolWindows", "kind": "method", @@ -783,7 +661,83 @@ The June data model stands: `control`, `property`, `method` or `event`. - `url` --- the path below the documentation root. -Names that belong to more than one page, to test lookup against: +**The entries come from the pages, and the packages annotate them.** An index built from the +packages would carry page-less names and put the pages' own layout in the wrong place: of the +2,368 public members of VB's documented classes, 173 have no page or heading, and of +WinNativeCommonCtls' 738, 294 --- inherited members that pages name in a sentence +(`DTPicker` "inherits ... Anchors, Dock, Font ...") or not at all. + +- **From the documentation:** every page's `permalink:` exactly as written. Folder-style + pages keep their trailing slash (`/tB/Packages/VB/CheckBox/`), single-file pages do not + (`/tB/Packages/tbIDE/ToolWindows`), so a URL is copied, never assembled. Canonical URLs + only, never a `redirect_from` alias. **A member is either a page or a heading.** + `Collection.Add` has its own page, `/tB/Modules/Collection/Add`; `ToolWindows.Add` is a + heading on its class page, and its URL uses the id tbdocs gives that heading (`### Add` + gives `#add`), read from the build and never computed a second time. Two kinds of alias: + the `$`, `B` and `W` variants share the base page (`LenB` → `/tB/Modules/Strings/Len`), and + attributes are `/tB/Core/Attributes#`. +- **The reading rules**, in [builder/symbols.mjs](builder/symbols.mjs)'s header: a page's + title and its first heading's comma list name a type or member; a page under a type's page + in the nav is one of its members; a heading on a type's page whose every part is an + identifier and one a member is that member's, as is any heading under Properties, Methods + or Events; an inherited member documented on its declaring type's page has that URL from + every type that inherits it (`CodeEditor.Close` is `Editor#close`); a page filed under one + module and declared in another belongs to the declarer (VBA's `Array` is under Information + and declared in `_HiddenModule`); a `$` form shares its base's URL; and a Core page's title + gives its statement and keywords (`Do...Loop`: `Do`, and `Loop` as a keyword). **Every + package page gives at least one entry**; a build names any that stops. +- **`symbols:`**, an optional frontmatter key, names what a page documents when its title + cannot: the (Default) module is `_HiddenModule`, and the comparison operators page + documents `=`, `<>` and four more. A page's first heading `# \ and \= operators` must be + written `\\=`, since `\=` is a markdown escape. +- **From the packages:** [builder/package-api.json](builder/package-api.json), which + [scripts/build_package_api.mjs](scripts/build_package_api.mjs) writes from the packages of + an install, through the export of [scripts/lib/tb-packages.mjs](scripts/lib/tb-packages.mjs) + (shared with `census_attributes.mjs`) and a declaration scanner, + [scripts/lib/twin-api.mjs](scripts/lib/twin-api.mjs). It is committed, like + `inter-metrics.json`, because CI has no install. 1,379 types in 16 exports, 255 KB. It + supplies each symbol's package, container and kind, lists the public symbols that have no + page (documentation coverage as a side effect), and supplies each class's default + interface, which is what the compiler names a class's members by (P5). Four facts about + the packages shaped it: + - **A package is known by its project name, not its folder's.** TwinBasicAssertions is + `Assert`, all three CEF builds are `cefPackage` (their declared APIs are identical, and + the generator checks that they stay so), and AppGlobalClassObject is + `AppGlobalClassProject`. That is what code writes and hover says, so the index's + `package` is the project name, and its `packages` map gives each project's name on the + pages. + - **Visibility cannot be read off one declaration.** CEF's `CefLogSeverity` is a `Public + Enum` inside a `Private Module`, documented and used; CustomControls' `Borders` is a + `Private Class` a control's `Borders` property returns; `CheckBox` declares almost + nothing itself, its members being on `Private Class CheckBoxBaseCtl` and four classes + above it; and VB's `Clipboard` is a public CoClass whose members are on `Private + Interface _Clipboard`. So every declared type is kept and marked, a non-public one + keeping its members only when a public type exposes them, by inheritance or as the + interface a public CoClass is built on. + - **`[Hidden]` is not "absent".** `Module [_HiddenModule]` carries it, and its members are + globals. + - **The default-interface map comes from CoClasses:** `interfaces` maps `VBA._Collection` + to `VBA.Collection` and `tbIDE.IToolWindowsV1` to `tbIDE.ToolWindows`, 37 in all, which + is P5's hover answer turned into a lookup. A lookup that takes the compiler's word has + to map `_Collection` to `Collection`. +- **Regenerate** `package-api.json` with `node scripts/build_package_api.mjs` when the + reference is re-indexed against a newer build, and commit it with the pages. + +**The gates.** `builder/symbol-baseline.json` lists every published URL, and a build that +loses one fails --- the case it exists for is a reworded member heading, which moves an +anchor an installed add-in still holds. A new URL rewrites the baseline. Retiring an entry +follows the rules in [Permanent Links](docs/Documentation/Permanent-Links.md). +`scripts/check_symbol_index.mjs`, a `test.bat` gate, asserts each derivation rule, each +scanner trap and the guard's refusals on fixtures. Every URL resolves by construction, and +`tB/symbols.json` is in the online tree's index, so `--check-audit-index` sees it written. +The build's summary gives the count of public symbols no page documents, 602 today, and +`--symbol-gaps ` lists them: 173 VB and 294 WinNativeCommonCtls members inherited and +named only in prose, 48 WinNativeCommonCtls types --- enumerations and structures declared in +its controls' base classes and its public `...Consts` modules --- 39 members of VBA's +`_HiddenModule` and `Interaction`, and a tail of a few each. + +**Names that belong to more than one page**, to test lookup against (the generated index +has the complete list): - `FileSystem` --- the VBA module and the tbIDE class. - `Line` --- the VB control, the CustomControls style class, and the `Line Input` statement. @@ -798,98 +752,28 @@ Names that belong to more than one page, to test lookup against: - `Item` --- `Collection.Item`, and `Item` on six tbIDE classes (`Editors`, `Folder`, `HtmlElements`, `HtmlElementProperties`, `HtmlEventProperties`, `Toolbars`). -The generated index produces the complete list. - -#### What was built (2026-09-25, BETA 983) - -**Everything above except the embedded mode.** The index is the `symbolIndex` task of the -build, in [builder/symbols.mjs](builder/symbols.mjs), and it publishes -[`/tB/symbols.json`](docs/Documentation/Permanent-Links.md#the-symbol-index) --- under -`/tB/` because the file is part of the same contract as the URLs in it. 5,536 entries at -4,086 distinct URLs; 782 KB, 52 KB gzipped, under 100 ms of the build. Permanent Links -documents the format for any reader; [Building](docs/Documentation/Building.md#the-symbol-index) -and [WIP.Build.md](WIP.Build.md#the-symbol-index-and-the-drift-guard-on-its-urls) the build side. - -**The entries come from the pages, and the packages annotate them.** Joining the other way -round was tried first, and the packages declare far more than the pages give an anchor to: -of the 2,368 public members of VB's documented classes, 173 have no page or heading, and -of WinNativeCommonCtls' 738, 294 --- inherited members that pages name in a sentence -(`DTPicker` "inherits ... Anchors, Dock, Font ...") or not at all. An index built from the -packages would carry those as page-less names, and would put the pages' own layout in the -wrong place. So a page is read for what it documents, by these -rules, in [builder/symbols.mjs](builder/symbols.mjs)'s header: its title and its first -heading's comma list name a type or member; a page under a type's page in the nav is one -of its members; a heading on a type's page whose every part is an identifier and one a -member is that member's, as is any heading under Properties, Methods or Events; an -inherited member documented on its declaring type's page has that URL from every type that -inherits it (`CodeEditor.Close` is `Editor#close`); a page filed under one module and -declared in another belongs to the declarer (VBA's `Array` is under Information and -declared in `_HiddenModule`); a `$` form shares its base's URL; and a Core page's title -gives its statement and keywords (`Do...Loop`: `Do`, and `Loop` as a keyword). **Every -package page gives at least one entry**; a build names any that stops. - -**What the packages add, from [builder/package-api.json](builder/package-api.json)**, which -[scripts/build_package_api.mjs](scripts/build_package_api.mjs) writes from the packages of -an install, through the export `census_attributes.mjs` already made (now -[scripts/lib/tb-packages.mjs](scripts/lib/tb-packages.mjs), shared, and the census's output -checked identical) and a declaration scanner, -[scripts/lib/twin-api.mjs](scripts/lib/twin-api.mjs). It is committed, like -`inter-metrics.json`, because CI has no install. 1,379 types in 16 exports, 255 KB. Four -facts about the packages shaped it: - -- **A package is known by its project name, not its folder's.** TwinBasicAssertions is - `Assert`, all three CEF builds are `cefPackage` (their declared APIs are identical, and - the generator checks that they stay so), and AppGlobalClassObject is - `AppGlobalClassProject`. That is what code writes and hover says, so the index's - `package` is the project name, and its `packages` map gives each project's name on the - pages. -- **Visibility cannot be read off one declaration.** CEF's `CefLogSeverity` is a `Public - Enum` inside a `Private Module`, documented and used; CustomControls' `Borders` is a - `Private Class` a control's `Borders` property returns; and `CheckBox` declares almost - nothing itself, its members being on `Private Class CheckBoxBaseCtl` and four classes above - it; and VB's `Clipboard` is a public CoClass whose members are on `Private Interface - _Clipboard`. So every declared type is kept and marked, a non-public one keeping its - members only when a public type exposes them, by inheritance or as the interface a public - CoClass is built on. -- **`[Hidden]` is not "absent".** `Module [_HiddenModule]` carries it, and its members are - globals. -- **The default-interface map comes from CoClasses:** `interfaces` maps `VBA._Collection` - to `VBA.Collection` and `tbIDE.IToolWindowsV1` to `tbIDE.ToolWindows`, 37 in all, which - is P5's hover answer turned into a lookup. - -**Two pages needed `symbols:`**, a new optional frontmatter key naming what a page documents -when its title cannot: the (Default) module is `_HiddenModule`, and the comparison -operators page documents `=`, `<>` and four more. One page was wrong: IntegerDivide's -first heading, `# \ and \= operators`, rendered "\ and = operators" because `\=` is a -markdown escape. It is `\\=` now; its id did not change. - -**The gates.** `builder/symbol-baseline.json` lists every published URL, and a build that -loses one fails --- the case it exists for is a reworded member heading, which moves an -anchor an installed add-in still holds. `scripts/check_symbol_index.mjs`, a seventh -`test.bat` gate, asserts each derivation rule, each scanner trap and the guard's refusals -on fixtures. Every URL resolves by construction, and `tB/symbols.json` is in the online -tree's index, so `--check-audit-index` sees it written. The build's summary gives the -count of public symbols no page documents, 602 today, and `--symbol-gaps ` lists -them: 173 VB and 294 WinNativeCommonCtls members inherited and named only in prose, 48 -WinNativeCommonCtls types --- enumerations and structures declared in its controls' base -classes and its public `...Consts` modules --- 39 members of VBA's `_HiddenModule` and -`Interaction`, and a tail of a few each. - -**The URL is settled** (2026-09-25): `/tB/symbols.json` is part of the Permanent Links -contract from here on. - **Not done, and why:** -- **The embedded mode** above waits until the add-in works, at least in part (decided - 2026-09-25): it changes what the published pages do. +- **An embedded mode for pages: P3 says the theme needs one.** The pages work in the pane + unmodified, but they follow Windows' app mode, not the IDE's theme, and the add-in cannot + reach into the frame to change that: the live site is cross-site, so neither its document + nor its storage is the IDE page's. The site can take the theme from its URL instead: a + `theme=dark|light` query parameter, read by the no-flash snippet in `renderHead` + ([builder/template.mjs](builder/template.mjs)) and set as `data-theme` without being + stored, so a reader's own choice on the site is left as it is. Hiding the header and + navigation is a separate, optional question, untested. **Deferred until the add-in works, + at least in part**: it changes what the published pages do, and it needs a test that the + parameter keeps working. It serves the live site only: the IDE's own server answers any URL + with a query string with a 404 (P13), so the offline route would need the theme some other + way. - **Keywords with no page of their own** --- `ElseIf`, `Until`, `Step`, `To`, `In`, `ByVal`, `ByRef`, `Optional`, `As` --- are in the index only where a page's title gives them. Adding one is a `symbols:` line on the page that explains it, which is a content - decision per keyword. Deferred with the next item (2026-09-25). + decision per keyword. Deferred. - **Data types** --- `Long`, `String`, `LongPtr` --- have their page at `/Reference/Data-Types`, outside `/tB/`, so the index cannot carry them without breaking its own rule. Giving that page a `/tB/` permalink, with the old one in `redirect_from:`, - would. Deferred (2026-09-25). + would. Deferred. - **The offline tree has no copy of the index**, since the offline route is deferred. - **Hover over a VB control's member is unmeasured.** P5 measured CoClasses; for a `Class` such as `CheckBox`, whose members are inherited from private classes, hover may @@ -907,7 +791,7 @@ harness about a second against opening a new IDE. open the page in the browser or the pane. A miss says `No help for ''` through `ShowNotification`. - **The key is F1**, as planned: P1 and P2 do not rule it out. It fires wherever the focus + **The key is F1**: P1 and P2 do not rule it out. It fires wherever the focus is in the IDE's window. The one overlap is signature help: while it shows, F1 also expands or collapses it, and the cursor is then inside a call's parentheses, often on an argument rather than the procedure. If that proves a nuisance, Shift+F1 has no binding of its own. @@ -947,13 +831,13 @@ harness about a second against opening a new IDE. adding and removing widgets is not measured); offering only the packages the project references; the `[Description]` connection; offline use. -**Lookup**, from June: +**Lookup:** 1. A selection is looked up exactly as selected. 2. Otherwise take the dotted name under the cursor, and try it as `container.name` first. 3. With several matches, a language keyword wins a bare name; otherwise offer the choice. -**Word extraction**, from June. Given the line and the cursor's column: +**Word extraction.** Given the line and the cursor's column: 1. If the character at the cursor is not `A-Z`, `a-z`, `0-9` or `_`, there is no word. A `.` at the cursor sits between two segments, so it also gives no word. @@ -969,10 +853,10 @@ lines joined with `_`, and numbers (they miss the index). Whether `GetSelectionI columns from 1 is untested. **Settings:** per project through `Project.SaveMetaData` / `LoadMetaData`, add-in-wide -through `SaveSetting` / `GetSetting` --- which is the shared registry tree, so Stage 1's +through `SaveSetting` / `GetSetting` --- which is the shared registry tree, so the runner's registry restore has to include it. -**The add-in's own parser**, the June draft's Phase 4, kept as the fallback for increment 3: +**The add-in's own parser**, the fallback for increment 3: - On `Host_OnProjectLoaded`, which runs again after every compiler restart, in a new instance of the add-in (P9), go through the virtual file system with `For Each` --- never @@ -987,9 +871,11 @@ registry restore has to include it. traps a line scanner meets in this language: attributes that span lines, comma-separated attribute lists, escaped identifiers such as `[_HiddenModule]`, comments between attribute groups, and `Type As Long` fields. -- June proposed scanning on a background thread. Measure the scan first; a second thread +- Scanning on a background thread is possible. Measure the scan first; a second thread inside the IDE's process is the riskiest part of the whole design. +The add-in's code skeletons are written in Stage 4 against the compiler, with tests. + ### Stage 5: shipping - Build both bitnesses, which `buildAddin` does (P7). Add a documentation page under @@ -998,8 +884,7 @@ registry restore has to include it. - Take to upstream, with the probe results as evidence: the shortcut bug; a call to open a URL; a way to ask the compiler about the symbol at a position, whose answer hover already has (P5); tool windows given no id sharing one window. The shortcut bug and the windows - are in [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md). What `_v2` and `_v3` are for no longer - needs asking: P14 answered it. + are in [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md). ## Open decisions @@ -1014,33 +899,11 @@ Recommended, and not yet confirmed: P5 raises what the rule costs: through the page's socket the add-in would know the package and interface of any name under the cursor today (Stage 4, increment 3). - **The site reads a `theme` query parameter**, so that a page in the help pane can match - the IDE's theme (Stage 3, after P3). **Deferred** until the add-in works, at least in part - (2026-09-25); decided then, not before. + the IDE's theme (Stage 3, after P3). **Deferred** until the add-in works, at least in part. - **Isolation starts with restoring the registry** (Stage 1, item 3), and a private `APPDATA` for every lane IDE (P6). A separate Windows account for test runs comes only if that proves not to be enough. -## What changed from the June draft - -- **It said add-ins could only be tested by hand.** Stage 1 exists to change that. -- **It hand-wrote the symbol index.** Its URL-scheme section named paths that do not exist - (`/tB/Reference/Statements/`, `/tB/Reference/Procedures-and-Functions/`), - although its examples used the real `/tB/Core/` and - `/tB/Modules//`. **Of its five "VBRUN classes" only `PropertyBag` is in - VBRUN**: `Collection` is `/tB/Modules/Collection`, `Err` is `/tB/Modules/ErrObject`, `App` - is `/tB/Packages/VB/App/`, and the reference has no `Dictionary` page at all. Its - name-collision table has the same package errors, and two more: it calls `Timer` a - statement (it is the VB control and a VBA function), and it counts the IDE's `listview` - widget tag, which is a tag name, not a symbol. The collisions themselves are real, and - [Stage 3](#stage-3-the-symbol-index-generated-by-the-docs-build) lists them. -- **It asked whether F1 was free.** It is not; see [Keyboard - shortcuts](#keyboard-shortcuts). It also took the key strings on the published page on - trust, and `{ctrl}` and `{alt}` do not work for add-ins. -- **It said a tool window could not show a web page.** Nothing refuses an `iframe`, and P3 - showed one working, so the order of its three tiers is reversed. -- **Its code skeletons are not kept.** They were never compiled; Stage 4 writes the modules - against the compiler, with tests. - ## Rules Stage 1 is built, so the rules for testing add-ins bind every session and are in From 1cfeb34ad9bfa908ed8f744fbac6b708d9589348 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 4 Oct 2026 18:51:56 +0200 Subject: [PATCH 06/34] WIP.md, WIP.Authoring: drop incident history, state reasons in the present --- WIP.Authoring.md | 59 +++++++-------- WIP.md | 193 +++++++++++++++++++++-------------------------- 2 files changed, 113 insertions(+), 139 deletions(-) diff --git a/WIP.Authoring.md b/WIP.Authoring.md index 5c2f15732..ba18cc303 100644 --- a/WIP.Authoring.md +++ b/WIP.Authoring.md @@ -1,7 +1,7 @@ # twinBASIC Documentation --- Authoring a Page -The page-writing contract, split out of [WIP.md](WIP.md) so a session that is -not writing documentation does not carry it. **Read this before writing or +The page-writing contract, kept apart from [WIP.md](WIP.md) so a session that is +not writing documentation need not load it. **Read this before writing or editing any page under `docs/`.** WIP.md keeps the placement rules, the plain-English prose rules and the Don'ts; everything about the shape of a page is here. @@ -70,7 +70,7 @@ This example... Formatting conventions: -- Heading levels: the page title is `#` (a *chapter* -- a page may legitimately have more than one). Top-level sections (Example, See Also, ...) are `##`; subsections `###`. Do **not** skip `##` with the old `# Title` -> `### Example` "house style". That pattern (h1 straight to h3) exists on many older pages only to keep GitHub's raw-markdown view at a modest heading size; it is a heading-order defect on the built site. `headingLevelNormalizePlugin` in [builder/render.mjs](builder/render.mjs) repairs those legacy pages at build time (it raises `h3`->`h2` on any page that uses h1 and h3 but no h2), so existing pages are not being churned all at once -- but new content must use correct levels, and pages that already mix `##` and `###` are left untouched. A mixed page that skips a level is a real defect the normalizer will not save you from: five were found and fixed this way, and axe's `heading-order` rule now guards the sample against more. +- Heading levels: the page title is `#` (a *chapter* -- a page may legitimately have more than one). Top-level sections (Example, See Also, ...) are `##`; subsections `###`. Do **not** skip `##` with the old `# Title` -> `### Example` "house style". That pattern (h1 straight to h3) exists on many older pages only to keep GitHub's raw-markdown view at a modest heading size; it is a heading-order defect on the built site. `headingLevelNormalizePlugin` in [builder/render.mjs](builder/render.mjs) repairs those legacy pages at build time (it raises `h3`->`h2` on any page that uses h1 and h3 but no h2), so existing pages need no mass edit -- but new content must use correct levels, and pages that already mix `##` and `###` are left untouched. A mixed page that skips a level is a real defect the normalizer does not repair; axe's `heading-order` rule guards the sample against it. - `**...**` for keywords/literal tokens; `*...*` for placeholders/arguments. - Code blocks use ` ```tb ` (highlighted via Shiki using the vendored `builder/twinbasic.tmLanguage.json` grammar). - Parameter lists use the deflist `term` + `: definition` indentation pattern (NOT the MS-style markdown table). @@ -86,8 +86,8 @@ Three keys on a package's `index.md` are **provenance for the authoring pass**. them reaches the HTML --- `grep -r indexed_from docs/_site` returns nothing, because nothing in `template.mjs` iterates frontmatter generically; every consumer reads a named field. The build reads one of them, `exclude_from_docs`, for one purpose: the symbol index's count of -public symbols no page documents leaves those names out (`reportableGaps` in -[builder/symbols.mjs](builder/symbols.mjs), and `--symbol-gaps` writes the list). Leave all +public symbols that no page documents leaves those names out (`reportableGaps` in +[builder/symbols.mjs](builder/symbols.mjs); `--symbol-gaps` writes the list). Leave all three in place. | Key | Means | @@ -102,8 +102,7 @@ a documentation gap.** Without them a re-index cannot tell "not yet written" fro "deliberately omitted", which is the whole point of recording them. The six packages carrying `indexed_from` are `AppGlobalClassObject`, `CustomControls`, -`TwinBasicAssertions`, `WinEventLogLib`, `WinNamedPipesLib` and `WinServicesLib`, all set by -the 2026-06-04 pass: `9f406eb`, `05ed96b`, `77c6a03`, `4cd4f4b`, `3429b97`, `6a68815`. +`TwinBasicAssertions`, `WinEventLogLib`, `WinNamedPipesLib` and `WinServicesLib`. `exclude_from_docs` is on three of them (`InternalStuff`; `EventLogHelperPrivate` + `EventLogAPIs`; `ServicesConstantsPublic`). @@ -115,9 +114,9 @@ check was run against a build that no longer matches the package. > yet**, covering packages beyond the six above. If you are about to document a package > from scratch, ask first --- it may already be written and waiting to merge. -One other inert key exists, and is a different thing: `has_children` on four pages is a -just-the-docs leftover. tbdocs derives the nav tree itself and never reads it. Harmless, but -it is theme residue rather than something anyone is meant to maintain. +One other inert key is a different thing: `has_children` on four pages is a +just-the-docs leftover. tbdocs derives the nav tree itself and never reads it. Nobody +needs to maintain it. ### `symbols:` --- what a page documents, for the symbol index @@ -302,10 +301,10 @@ claim that a permalink is referenced from source, not an obligation to update on other changes. A reviewer looking at a `Description` string and at a reference page for the same symbol will reach for that connection; there isn't one yet. -Related: **`[Documentation(...)]` is not a twinBASIC attribute.** It does not exist. It was -asserted in eight places, including a `tb` code sample teaching it, as the in-source consumer -of the `/tB/` URL contract. The real attribute is `[Description(...)]`, which is not that. -The IDE help system remains a genuine consumer of the permalinks. +Related: **`[Documentation(...)]` is not a twinBASIC attribute.** It does not exist. Never +present it, in prose or in a `tb` sample, as the in-source consumer of the `/tB/` URL +contract. The real attribute is `[Description(...)]`, which is not that. The IDE help +system remains a genuine consumer of the permalinks. ## Per-symbol workflow @@ -322,7 +321,7 @@ The IDE help system remains a genuine consumer of the permalinks. - WinNativeCommonCtls → single-file (`DTPicker`, `MonthView`, `ProgressBar`, `Slider`, `UpDown`) or folder-style (`ImageList/`, `ListView/`, `TreeView/`) when the control has sub-object companions. Module-level enums under `Enumerations/`; per-control nested enums fold onto the declaring control's page. - Pick `` from VBA's grouping (Information, Interaction, Strings, FileSystem, DateTime, Math, Financial, Conversion, ...) and the existing folders under `Reference//`. 2. **Flag tB deviations** with a `> [!NOTE]` callout (see next section). -3. **Update the parent index** — turn an unlinked bullet into a link with a short blurb. Match the existing style of the page. If a new package is being added, also add a bullet to `docs/Reference/Built-In.md` (or `Default.md`) and bump the count on `docs/Reference/Packages.md` and `docs/Reference/index.md`. `Packages.md` itself only links to those two landing pages -- it does not enumerate packages, which is how `AppGlobalClassObject` went unlisted for months. +3. **Update the parent index** — turn an unlinked bullet into a link with a short blurb. Match the existing style of the page. If a new package is being added, also add a bullet to `docs/Reference/Built-In.md` (or `Default.md`) and bump the count on `docs/Reference/Packages.md` and `docs/Reference/index.md`. `Packages.md` itself only links to those two landing pages -- it does not enumerate packages. 4. **Add the page** to `Reference/Statements.md` or `Reference/Procedures and Functions.md` if it's a statement or callable and not already listed there. 5. **Run the [site integrity check](WIP.md#site-integrity-check)** after the batch and before committing. @@ -342,12 +341,11 @@ When in doubt about a tB-specific behavior, check `docs/Features/` and `docs/Ref **twinBASIC is in a long beta, so a superseded name is not history --- it is noise.** Document the current API and nothing else. A reader cannot use a name that no longer -exists, cannot search for it, and has to carry two names in their head to read one snippet. +exists, cannot search for it, and would have to keep two names in mind to read one snippet. -Two CustomControls tutorials showed `Public Sub New(Serializer As SerializationInfo)` and -`Deserialize()`, each with a NOTE underneath explaining that the framework actually names -them `SerializeInfo` and `RuntimeUISrzDeserialize()`. Both pages now show the current names -and carry no note; the snippets are shorter and the reader has one name per thing. +Show the current name and no note about the old one. The CustomControls tutorials show +`SerializeInfo` and `RuntimeUISrzDeserialize()`, not the superseded `SerializationInfo` +and `Deserialize()` with a NOTE explaining the difference. Version-skew notes earn their place when a reader may be *on* the older version --- a behaviour that changed in a shipped BETA, where the page says which build changed it (see @@ -368,22 +366,21 @@ reader's query `check_page_baseline` can never prefix-match. Measured: | `check_page_baseline` | **0** | | `check-page-baseline` | 420, as noise --- hyphens split, so it becomes three common words | -`build_dot_metrics` was 0 as well. An evaluator issued seven queries including the gate's -own filename and verbatim prose from its section, and never once saw it --- **the string a -refused developer actually has in hand is the one that found nothing.** +**The string a developer actually has in hand --- the bare script name --- is the one +that finds nothing.** -**The fix was to drop the directory prefix from the headings**, so the token becomes +**So write each script's heading without the directory prefix**, so the token is `check_page_baseline.mjs` and the trailing wildcard reaches it. Nothing is lost from the -page: each entry's anchor is pinned with `{: #... }` so no URL moved, and the synopsis +page: each entry's anchor is pinned with `{: #... }` so no URL moves, and the synopsis block on the next line still shows the full `node scripts/.mjs`. Every script name -returns hits now, and the index grew by nothing. +returns hits this way, and the index does not grow. -**Two other fixes were measured and rejected, and both are worth not re-proposing.** -Raising `search.heading_level` to 3 --- so each `###` becomes its own entry, which is what -would give a script section its own title boost and its own anchor in the results --- +**Two other fixes were measured and rejected; do not re-propose them.** +Raising `search.heading_level` to 3 --- so each `###` becomes its own entry, which would +give a script section its own title boost and its own anchor in the results --- **doubles the index, 3,742 entries to 7,524, and does not fix it**: the titles still carry -the `scripts/` prefix, so the bare query still misses. That is a 3.4 MB payload every page -already downloads, doubled, for nothing. Widening the tokeniser's separator to include +the `scripts/` prefix, so the bare query still misses. That doubles a 3.4 MB payload every +page already downloads, for nothing. Widening the tokeniser's separator to include `/`, `_` and `.` is the other obvious proposal; the `check-page-baseline` row above is what it would do to *every* path token on a site whose subject matter is `Debug.Print` and `_App`. diff --git a/WIP.md b/WIP.md index e24af1a5f..193e109d1 100644 --- a/WIP.md +++ b/WIP.md @@ -4,18 +4,17 @@ Static site (`just-the-docs` theme look-and-feel) deploying to `docs.twinbasic.c ## Which file to open -This file is the contract: what a session needs before touching anything. The -engineering casebook --- why a tool is built the way it is, what shipped broken, -and the measurements behind each decision --- lives in siblings, and **none of -them is loaded automatically.** Open the one covering what you are about to -change. +This file is the contract: what a session needs before touching anything. Its +siblings say why a tool is built the way it is: the reason, and any measurement +that still decides something. **None of them is loaded automatically.** Open the +one covering what you are about to change. | About to | Read first | |---|---| | write or edit any page under `docs/` | [WIP.Authoring.md](WIP.Authoring.md) --- page template, frontmatter, cross-section linking tables, per-symbol workflow | | document a specific package | that package's own file, listed under [Package API notes](#package-api-notes) | | run or change the twinBASIC compiler harness | [WIP.Harness.md](WIP.Harness.md) --- `export`, the attribute census, `tbbuild`, `tbrun`, the add-in test runner | -| change `builder/`, `scripts/`, or any gate | [WIP.Build.md](WIP.Build.md) --- the pipeline and every gate's failure history | +| change `builder/`, `scripts/`, or any gate | [WIP.Build.md](WIP.Build.md) --- the pipeline and why each gate exists | | touch fonts, diagrams, or the PDF's type | [WIP.Typography.md](WIP.Typography.md), then [WIP.Fonts.md](WIP.Fonts.md) for the generator | | change the accessibility scan | [WIP.A11y.md](WIP.A11y.md) --- the axe scan, the sample, the fingerprint gate | | work on the sample-compiling harness | [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md) | @@ -24,49 +23,33 @@ change. | 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 | The rule that decides where a new note belongs: **this file says what to do, a -sibling says why it is done that way.** A measurement, a war story, or a "this -shipped broken and nobody noticed" goes in a sibling. A rule you must follow -goes here. +sibling says why it is done that way.** A rule you must follow goes here. The +reason for a rule, and a measurement that still decides something, go in a +sibling. History goes nowhere: git keeps it. ## Status -Reference documentation is **complete** for all thirteen packages, adapted from primary sources (Microsoft VBA-Docs CC-BY-4.0 for the runtime library, `.twin` source for the twinBASIC-specific packages). The CEF and WebView2 packages also carry a tutorial set. - -| Package | Reference | Tutorials | -|--------------------------------------|-------------|-----------| -| VBA package | done | — | -| VBRUN package | done | — | -| VB package | done | — | -| WebView2Package | done | done | -| Assert package | done | — | -| CustomControls / CustomControlsPackage | done | — | -| cefPackage (CEF) | done | done | -| WinEventLogLib | done | — | -| WinNamedPipesLib | done | — | -| WinServicesLib | done | — | -| tbIDE | done | — | -| WinNativeCommonCtls | done | — | -| AppGlobalClassObject | done | — | - -The rest of this file is the maintenance guide for updating existing pages or adding new ones — high-level package surface notes, page templates, cross-section linking conventions, and the integrity check. +Reference documentation is **complete** for all thirteen packages: VBA, VBRUN, VB, WebView2, Assert, CustomControls, CEF, WinEventLogLib, WinNamedPipesLib, WinServicesLib, tbIDE, WinNativeCommonCtls and AppGlobalClassObject. It is adapted from primary sources: Microsoft VBA-Docs (CC-BY-4.0) for the runtime library, `.twin` source for the twinBASIC-specific packages. The CEF and WebView2 packages also carry a tutorial set. + +The rest of this file is the maintenance guide for updating existing pages or adding new ones: package surface notes, page conventions, and the integrity check. ## Where things live > **Packages are nested one level deeper than a bare `docs/Reference//`.** -> `58a5e1c` split them into `docs/Reference/Default/` --- the three packages every -> project references (`VB`, `VBA`, `VBRUN`) --- and `docs/Reference/Built-In/` --- the ten +> They sit in `docs/Reference/Default/` --- the three packages every +> project references (`VB`, `VBA`, `VBRUN`) --- or in `docs/Reference/Built-In/` --- the ten > that ship with the IDE but are referenced on demand (`AppGlobalClassObject`, `CEF`, > `CustomControls`, `TwinBasicAssertions`, `WebView2`, `WinEventLogLib`, -> `WinNamedPipesLib`, `WinNativeCommonCtls`, `WinServicesLib`, `tbIDE`). `Core/` did -> not move. +> `WinNamedPipesLib`, `WinNativeCommonCtls`, `WinServicesLib`, `tbIDE`). `Core/` is not +> nested. > -> **The reorg moved files, not URLs.** Every `permalink:` is unchanged, so the -> [Cross-section linking](WIP.Authoring.md#cross-section-linking) tables below are unaffected --- they +> **The folder layout does not affect URLs.** Every `permalink:` is independent of it, so the +> [Cross-section linking](WIP.Authoring.md#cross-section-linking) tables are unaffected --- they > resolve against the rendered URL, never the file path. Only the on-disk paths in this > section and in [Per-symbol workflow](WIP.Authoring.md#per-symbol-workflow) carry the prefix. > -> Note also that the `Assert` package's folder is `Built-In/TwinBasicAssertions/`, -> though its title, nav parent and permalinks all still say `Assert`. +> The `Assert` package's folder is `Built-In/TwinBasicAssertions/`, +> though its title, nav parent and permalinks all say `Assert`. - `docs/Reference/Core/` — language statements/keywords (`Dim`, `For-Next`, `Sub`, ...). - `docs/Reference/Default///` — runtime library (VBA, VBRUN), grouped by modules. @@ -82,23 +65,21 @@ The rest of this file is the maintenance guide for updating existing pages or ad - `docs/Reference/Built-In/WinNativeCommonCtls/` — Windows Native Common Controls compatibility package: a VB6-compatible Microsoft Common Controls 6.0 (`MSCOMCTL.OCX`) replacement, written on top of the Win32 ComCtl32 controls. Eight controls (**DTPicker**, **ImageList**, **ListView**, **MonthView**, **ProgressBar**, **Slider**, **TreeView**, **UpDown**), plus eight sub-object classes (**ListImages** / **ListImage**, **ListItems** / **ListItem**, **ColumnHeaders** / **ColumnHeader**, **Nodes** / **Node**) reached through container properties on the three collection-bearing controls, plus ~16 user-facing enumerations. Each control is a `BaseCtl` (`[COMCreatable(False)]`) plus a thin `` leaf tagged `[WindowsControl(...)]` — the same split VB-package and CEF use. - `docs/Reference/Built-In/AppGlobalClassObject/` — the `App` global object available in every twinBASIC project: the `_App` interface plus its property pages under `_App/` (`Build`, `Comments`, `CompanyName`, `EXEName`, …). 38 files. - > **This package published nothing at all until two causes were fixed**, and both - > leave a live rule. A blanket `**/_*/**` rule in `_config.yml`'s `exclude:`, there - > to drop `_Images`, swallowed all 37 pages under `_App/` --- the twinBASIC interface - > really is named `_App`, after the COM hidden-interface convention. It is scoped to - > `**/_Images/**` (plus `**/*.af`) now, so **never widen an exclude to a bare - > underscore prefix.** Separately `index.md` carried a UTF-8 BOM in front of the `---`, - > which hid the frontmatter, so `discover` filed it as a *static file* and served the raw - > markdown verbatim; `lib/frontmatter.mjs` strips a leading BOM before parsing now, and - > editors on Windows add one without being asked. + > **Two rules come from this package.** First, **never widen an `exclude:` entry in + > `_config.yml` to a bare underscore prefix.** The twinBASIC interface is named `_App` + > (the COM hidden-interface convention), so a blanket `**/_*/**` would drop all 37 + > pages under `_App/`. The exclude is scoped to `**/_Images/**` (plus `**/*.af`). + > Second, a UTF-8 BOM in front of a page's `---` hides its frontmatter: `discover` + > would file the page as a *static file* and serve the raw markdown. Editors on + > Windows add a BOM unasked. `lib/frontmatter.mjs` strips a leading BOM before parsing. - `docs/Reference/Built-In/tbIDE/` — IDE Extensibility package (this is the **addin SDK**). The package is type-only — it ships **public interfaces + CoClasses** that an addin DLL binds to; every implementation behind them lives in the twinBASIC IDE itself. The user-facing surface is one entry-point factory (`tbCreateCompilerAddin`) plus 23 CoClasses grouped by role: the addin contract (`AddIn`), the root API (`Host`), the loaded `Project`, the editors collection (`Editor` / `CodeEditor` / `Editors`), the virtual file system (`FileSystem` / `FileSystemItem` / `Folder` / `File`), the in-IDE UI surface (`Toolbar` / `Toolbars` / `Button` / `ToolWindow` / `ToolWindows`), the HTML DOM inside a tool window (`HtmlElement` / `HtmlElements` / `HtmlElementProperty` / `HtmlElementProperties` / `HtmlEventProperty` / `HtmlEventProperties`), the `DebugConsole`, `KeyboardShortcuts`, `Themes`, and the single concrete user-instantiable helper class `AddinTimer`. Flat layout — one page per CoClass / Class plus the index landing. - `docs/Reference/Statements.md` — alphabetical index of language statements. - `docs/Reference/Procedures and Functions.md` — alphabetical index of procedures/functions. -- `docs/LLVM/` — the LLVM section: compiling with the LLVM back end. A top-level section between Features and Reference Section in the nav, at `nav_order: 6` (Features moved to 5 to make room). `index.md` is the landing page and `Getting-Started.md` the only page so far, with its screenshots under `Images/`. Everything it describes arrived in **BETA 984**. Its language claims are measured on BETA 995 with `tbrun --llvm` and `--exe`: the comma, space and comma-space spellings of `[CompilerOptions]` all turn LLVM and `+optimize` on, `[CompilerOptions("")]` turns it off under project-wide LLVM, the long CPU-flag sample runs, and an error is lost between procedures when either the callee or the caller is LLVM-compiled. Its samples are marked `check_build`, which compiles them; `examples.bat --llvm` also builds them with LLVM. +- `docs/LLVM/` — the LLVM section: compiling with the LLVM back end. A top-level section between Features and Reference Section in the nav, at `nav_order: 6` (Features is 5). `index.md` is the landing page and `Getting-Started.md` the only page so far, with its screenshots under `Images/`. Everything it describes arrived in **BETA 984**. Its language claims are measured on BETA 995 with `tbrun --llvm` and `--exe`: the comma, space and comma-space spellings of `[CompilerOptions]` all turn LLVM and `+optimize` on, `[CompilerOptions("")]` turns it off under project-wide LLVM, the long CPU-flag sample runs, and an error is lost between procedures when either the callee or the caller is LLVM-compiled. Its samples are marked `check_build`, which compiles them; `examples.bat --llvm` also builds them with LLVM. **A new top-level section needs four things besides its folder**, and nothing checks the first and the third: a `nav_order` between its neighbours; an entry in `docs/_book.yml` for every page, in a part or in `left_out:` with a reason, which the build warns about under its `pdf:` summary when one is missing; a line in *Where content lives* in `docs/Documentation/Authoring.md`; and the page-count rise that `build.bat` writes to `builder/page-baseline.json`, committed with the pages. The Challenges and Videos sections are in `left_out:`, and so are the IDE pages that are still screenshots and labels; the IDE part names its pages one by one, so a new IDE page warns until it goes into the part or into `left_out:`. A link from the book to a left-out page opens the website, and the pass over `book.html` lists it as `OUT OF BOOK`. A section index lists its topics by hand and sets `has_toc: false`, or the template appends a second, automatic list of its children. - Footer rendering — [builder/template.mjs](builder/template.mjs)'s `renderFooterCustom()` renders the copyright line and, when `vba_attribution: true` is set in a page's frontmatter, an additional CC-BY-4.0 attribution line beneath it. -- Contributor authoring guide — [docs/Documentation/Authoring.md](docs/Documentation/Authoring.md) is the public "start here" page that distils this file's authoring conventions (page template, heading levels, formatting, plain-English prose, attribution policy, cross-section linking) for a new contributor. This file remains the exhaustive maintainer source of truth; keep the two in sync when a convention changes. +- Contributor authoring guide --- see [Authoring a page](#authoring-a-page). ## Package API notes @@ -120,12 +101,12 @@ The three "winlibs" packages — [WinServicesLib](WIP.WinServicesLib.md), [WinEv The package sources are not in this repository --- they are inside the `.twinproj` files an IDE install ships. Exported sources are the strongest -available evidence for anything the documentation asserts about legal syntax, -and for a question no shipped source demonstrates, something has to put the -construct in front of the compiler. Four tools do that, and each finds the IDE -itself: the newest `twinBASIC_IDE_BETA_` on the Desktop, with `--ide` or -`TB_IDE` overriding. **No install path is hardcoded anywhere in the tooling**, -because an install path contains a username. +evidence for anything the documentation asserts about legal syntax. For a +question no shipped source answers, the construct has to go to the compiler. +Four tools do that. Each finds the IDE itself: the newest +`twinBASIC_IDE_BETA_` on the Desktop, with `--ide` or `TB_IDE` overriding. +**No install path is hardcoded anywhere in the tooling**, because an install +path contains a username. ```sh "$TB/bin/twinBASIC_win32.exe" export ".twinproj" "C:\out\dir\" --overwrite @@ -135,15 +116,15 @@ node scripts/tbbuild.mjs C:/probe/Thing.twinproj # does it compile node scripts/tbrun.mjs # what does it print ``` -- **Give the executable backslashed paths, and `export` a full path to the project.** `export` prefixes `\\?\` to its project path, so a relative one, or one with forward slashes, reports `input twinproj file does not exist`; and a folder named with forward slashes cannot be created or even found, even when it exists. With backslashes `export` creates every missing level of its output folder. Redirect stdin (`\`), so the given `.twinproj` is untouched, and 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 mid-sentence. Set `TBBUILD_SHOW=1` while working interactively and leave it unset for unattended runs --- a wedged IDE nobody can see is the failure that costs an afternoon. -- **One project per IDE**, 6--8 seconds each and flat in project size. Reusing a live IDE for a second project wedges it, so the cold start is the unit of work, not overhead to optimise away. Concurrency is how to go faster: distinct `--port` values give distinct DevTools ports, user-data folders, temp folders (`%TEMP%\tbbuild-tmp-`; IDEs building in one temp folder fail now and then to write the type library) and desktops. -- **Keep a probe that might crash the compiler in a project of its own.** twinBASIC runs the compiler in the same process as user code, so one bad probe can take the run down and cost the other thirty their answer. -- **`tbrun` takes an exported tree, not a `.twinproj`**, because it has to pin `project.buildPath` in its own staged copy --- a project still on the default template opens a native Save dialog that is invisible on the private desktop, and the build simply never happens while every health check says the IDE is fine. The probe is a module with a `[RunAfterBuild]` Sub, and must start with `Debug.Cls`. Its exit codes: 0 the probe ran and its output was captured, 1 the project has compile errors, 2 the harness failed or the build did after a clean compile, 3 no output, 4 the compiler crashed, as `tbbuild` reports it, 5 the probe ended before it returned (`End`, or an error raised with no handler in LLVM-compiled code, which ends the run silently), 6 with `--exe`, the exe exited with a code other than 0 or was still running at `--timeout`. +- **Give the executable backslashed paths, and `export` a full path to the project.** `export` prefixes `\\?\` to its project path, so a relative path, or one with forward slashes, reports `input twinproj file does not exist`. A folder named with forward slashes cannot be created or found, even when it exists. With backslashes, `export` creates every missing level of its output folder. Redirect stdin (`\`), 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. +- **One project per IDE**, 6--8 seconds each and flat in project size. Reusing a live IDE for a second project wedges it, so the cold start is the unit of work, not overhead to optimise away. Run concurrently to go faster: distinct `--port` values give distinct DevTools ports, user-data folders, temp folders (`%TEMP%\tbbuild-tmp-`; IDEs building in one temp folder fail now and then to write the type library) and desktops. +- **Keep a probe that might crash the compiler in a project of its own.** twinBASIC runs the compiler in the same process as user code, so one bad probe can take the run down and cost the other probes their answer. +- **`tbrun` takes an exported tree, not a `.twinproj`**, because it has to pin `project.buildPath` in its own staged copy. A project still on the default template opens a native Save dialog that is invisible on the private desktop: the build never happens, while every health check says the IDE is fine. The probe is a module with a `[RunAfterBuild]` Sub, and must start with `Debug.Cls`. Its exit codes: 0 the probe ran and its output was captured, 1 the project has compile errors, 2 the harness failed or the build did after a clean compile, 3 no output, 4 the compiler crashed, as `tbbuild` reports it, 5 the probe ended before it returned (`End`, or an error raised with no handler in LLVM-compiled code, which ends the run silently), 6 with `--exe`, the exe exited with a code other than 0 or was still running at `--timeout`. - **Measuring LLVM: `tbrun --llvm`** (or `--compiler-options ""`) compiles the whole probe with LLVM, by setting `compiler.debugOptions` --- what a `[RunAfterBuild]` run is compiled with; `compiler.buildOptions` alone does not reach it (BETA 995) --- and `compiler.buildOptions`, for the exe. It refuses a Community or Personal licence. `--exe` also runs the built exe on a private desktop; the exe runs `Sub Main` and prints with `TbRun.Out`, a module `tbrun` adds, because `Debug.Print` writes nothing in an exe. - **A census is evidence, not applicability.** The corpus not using an attribute somewhere does not mean the compiler refuses it there, and the reverse also holds. Only a probe settles that. -- **The sweep is the probe for a whole attribute.** Before writing or changing an attribute's `Applicable to:` line, run `sweep_attributes.mjs --names ` (a minute or a few; the once-per-project ones, `RunAfterBuild` and `RunBeforeStartupObject`, take several); it tries every declaration site, not only the ones already claimed. **Always pass `--out` and `--dump-results`**: a run piped through `tail` keeps one line of a report that took ten minutes. A clean build there means the compiler accepts the attribute, not that it does anything, and an Enum member cannot be tested at all (see [WIP.Harness.md](WIP.Harness.md#sweeping-every-attribute-at-every-site)). +- **The sweep is the probe for a whole attribute.** Before writing or changing an attribute's `Applicable to:` line, run `sweep_attributes.mjs --names ` (a minute or a few; the once-per-project ones, `RunAfterBuild` and `RunBeforeStartupObject`, take several); it tries every declaration site, not only the ones already claimed. **Always pass `--out` and `--dump-results`**: a run piped through `tail` keeps one line of a report that takes ten minutes. A clean build there means the compiler accepts the attribute, not that it does anything, and an Enum member cannot be tested at all (see [WIP.Harness.md](WIP.Harness.md#sweeping-every-attribute-at-every-site)). - **End an IDE by its pid, never by image name.** `taskkill /IM twinBASIC.exe` ends every other run's IDE, another session's included, and the user's own. `tbbuild --keep` prints the pid for this reason. Why each of those is true, what the WebView/CDP route costs, why the compiler's @@ -161,7 +142,7 @@ addin-test.bat # every lane addin-test.bat --only sample15 # one lane; --port N moves the lanes' ports ``` -- **Never build or copy a test add-in into the real install's `addins\`, or into `%APPDATA%\twinBASIC\addins\`.** Either way it loads into the user's own IDE (P6 measured the second). A test add-in goes only into a lane's own folders: its copy of the install, where `addAddin` refuses anywhere else, or the `APPDATA` the lane gives every IDE it starts. That private `APPDATA` is also what keeps the user's own add-ins out of the test IDEs; never start a lane IDE without it. +- **Never build or copy a test add-in into the real install's `addins\`, or into `%APPDATA%\twinBASIC\addins\`.** Either way it loads into the user's own IDE (measured for the second). A test add-in goes only into a lane's own folders: its copy of the install, where `addAddin` refuses anywhere else, or the `APPDATA` the lane gives every IDE it starts. That private `APPDATA` is also what keeps the user's own add-ins out of the test IDEs; never start a lane IDE without it. - **A test never opens a real browser.** Every IDE the harness starts has `TB_ADDIN_TEST=1`, and an add-in under test prints `open ` to the DEBUG CONSOLE instead. Never start a test IDE with the variable removed unless its add-in opens nothing either way. - **Name in `lanes.mjs` every application an add-in under test passes to `SaveSetting`**, or its settings stay changed after the run: `SaveSetting` writes the key the user's own copy of the add-in reads. @@ -179,11 +160,11 @@ frontmatter keys, the attribution policy, the per-symbol workflow, the twinBASIC-vs-VBA deviations to flag, and the cross-section linking tables. Those tables are not optional guidance. Relative links resolve against the -**rendered URL** --- the page's `permalink:` --- rather than the file path, and -the URL prefixes are not uniform across packages: VBA pages sit one segment -shallower than VBRUN pages, and folder-style classes one deeper than -single-file ones. A cross-section link written by analogy with a neighbouring -page is usually wrong, and the build's link check is what will tell you. +**rendered URL** --- the page's `permalink:` --- not the file path. The URL +prefixes are not uniform across packages: VBA pages sit one segment shallower +than VBRUN pages, and folder-style classes one deeper than single-file ones. A +cross-section link written by analogy with a neighbouring page is usually +wrong, and the build's link check reports it. Three things are short enough to state here, because they decide whether a page is in the right place at all: @@ -192,8 +173,9 @@ is in the right place at all: - **Attribution is per page, decided by content provenance, not by package membership.** Set `vba_attribution: true` only on a page actually derived from a specific VBA-Docs source page. A symbol merely existing in VBA is not sufficient. - **Link to the canonical location**, meaning the page's own `permalink:`, never to one of its `redirect_from` aliases. -The public, contributor-facing distillation of the same conventions is -[docs/Documentation/Authoring.md](docs/Documentation/Authoring.md). Keep the two +The public "start here" page for a new contributor, [docs/Documentation/Authoring.md](docs/Documentation/Authoring.md), +distils the same conventions (page template, heading levels, formatting, +plain-English prose, attribution policy, cross-section linking). Keep the two in step when a convention changes; this file and its authoring sibling remain the exhaustive maintainer source of truth. @@ -230,7 +212,7 @@ The vocabulary tables further down cover word choice. The rules in this subsecti 8. **Name the fault directly; never build up to it.** Setting up a contrast and then withholding the point is coy, and it makes the reader parse the sentence twice to extract one fact. *Avoid:* "double-clicking it is the obvious shortcut, and it is the one that misleads"; "the specificity trap --- which is the one thing that reliably catches people out". *Use:* "double-clicking it is obvious, and wrong"; "the specificity trap: a rule that loses it applies in light mode and silently does not in dark". Say what the thing is and what it does, in that order, in one clause. The same applies to "and that is the one that…", "which is precisely the…" and "therein lies the…". -9. **State what twinBASIC does; do not report having measured, probed or observed it.** The documentation is the authority on what twinBASIC does and speaks from inside the project, so its prose states behaviour as fact, not as the outcome of an experiment on someone else's product. The same goes for *probed*, *observed*, *noticed*, *turns out* and *appears to*, when they report what twinBASIC does. *Avoid:* "measured on BETA 995, the comma form also turns LLVM on"; "as measured, the crash needs a variable declared `As Form`"; "probed against the compiler, the attribute is accepted on..."; "the value observed is 0". *Use:* "the comma form also turns LLVM on"; "the crash needs a variable declared `As Form`" --- and where the build matters, "in BETA 995" or "since BETA 984". When a sentence really must say that a claim was checked, write *checked*, *tested*, *confirmed* or *verified*, never *measured*. *Measure* keeps its literal sense: a size, a duration, a unit of measurement, **TextWidth** measuring a string, a benchmark's timings. This applies to every page under `docs/`, the developer pages under `docs/Documentation/` included; the `WIP.*.md` files are an internal casebook and keep their own register. +9. **State what twinBASIC does; do not report having measured, probed or observed it.** The documentation is the authority on what twinBASIC does and speaks from inside the project, so its prose states behaviour as fact, not as the outcome of an experiment on someone else's product. The same goes for *probed*, *observed*, *noticed*, *turns out* and *appears to*, when they report what twinBASIC does. *Avoid:* "measured on BETA 995, the comma form also turns LLVM on"; "as measured, the crash needs a variable declared `As Form`"; "probed against the compiler, the attribute is accepted on..."; "the value observed is 0". *Use:* "the comma form also turns LLVM on"; "the crash needs a variable declared `As Form`" --- and where the build matters, "in BETA 995" or "since BETA 984". When a sentence really must say that a claim was checked, write *checked*, *tested*, *confirmed* or *verified*, never *measured*. *Measure* keeps its literal sense: a size, a duration, a unit of measurement, **TextWidth** measuring a string, a benchmark's timings. This applies to every page under `docs/`, the developer pages under `docs/Documentation/` included; the `WIP.*.md` files are internal working notes and keep their own register. ### Replace @@ -320,9 +302,9 @@ converts the dash like any other text. Measured: **zero literal `--` survives in anywhere in the built site**, in all three trees. > **Verify through `builder/`, never a bare markdown-it.** A plain `markdown-it` with -> `typographer: true` leaves `--` in alt text untouched, so testing against the npm -> dependency reproduces a wrong claim perfectly and tells you nothing about this site. -> Render through the pipeline, or read the built HTML. +> `typographer: true` leaves `--` in alt text untouched, so a test against the npm +> dependency tells you nothing about this site. Render through the pipeline, or read +> the built HTML. The source uses the ASCII forms; the rendered HTML uses the typographic characters. Literal `–` or `—` in `docs/` markdown source is forbidden — see the Don'ts at the end of this file. `scripts/convert_em_dash_separators.mjs` is the canonical normaliser if any literals slip back in. @@ -340,8 +322,7 @@ alongside their licence files. | **Cascadia Mono** | all code, inline and block, web and PDF | `wght 200-700` | 67 KB + 77 KB italic | | **Source Serif 4** | PDF body text only | `wght 200-900` | 138 KB + 109 KB italic | -Inter is the brand face: twinbasic.com has always named it first in its own -stack, it just never shipped a `@font-face` to deliver it. Cascadia Mono is the +Inter is the brand face: twinbasic.com names it first in its own stack. Cascadia Mono is the **ligature-free** cut of Microsoft's terminal font --- in a *language* reference the literal characters are the subject matter, so a face that draws `->` as one mark is working against the text, and any replacement must be ligature-free too. @@ -368,11 +349,11 @@ Every diagram is Graphviz DOT --- sources under `docs/assets/images/dot/` for shared diagrams, `Images/` beside a page for one that belongs to it. The `.svg` is a build artifact. -- **Edit the `.dot`, never the `.svg`**, and never set a diagram's `font-family` anywhere else. Graphviz sizes every box to the text *it* measured, and its WASM build has no font machinery at all --- it falls back to Times for any face it does not know. A face the layout never saw leaves labels painted outside their boxes, which is how 27 labels across three diagrams shipped that way, unreported, for months. +- **Edit the `.dot`, never the `.svg`**, and never set a diagram's `font-family` anywhere else. Graphviz sizes every box to the text *it* measured, and its WASM build has no font machinery at all --- it falls back to Times for any face it does not know. A face the layout never saw leaves labels painted outside their boxes. - `node scripts/check_dot_fit.mjs` is the gate on exactly that, and runs inside `check.bat`. Run it after touching any diagram. -- **Scale a diagram whole, or not at all.** Stretching an SVG up inflates its labels; shrinking the text alone unmoors the labels from their boxes. Both stylesheets learned this, from opposite directions. +- **Scale a diagram whole, or not at all.** Stretching an SVG up inflates its labels; shrinking the text alone unmoors the labels from their boxes. - **A `@font-face` added to [docs/_sass/custom/_fonts.scss](docs/_sass/custom/_fonts.scss) needs its stack added to [docs/_sass/modules-dark.scss](docs/_sass/modules-dark.scss) as well**, and must stay inside the `emit-font-faces` mixin. The dark compilation re-emits its whole payload under two selectors at raised specificity: a face declared there would be invalid, and a stack left out there applies in light mode and silently does not in dark. -- **Run the full a11y sweep after any change that moves type metrics.** An inline element's measured height is its font's content area, so `target-size` results move with the face. The thirteen-page sample stayed clean through the entire font migration and would have shipped a regression. +- **Run the full a11y sweep after any change that moves type metrics.** An inline element's measured height is its font's content area, so `target-size` results move with the face. The thirteen-page sample can stay clean while the full sweep regresses. Why Graphviz needs Inter's widths patched into its Times table and what breaks that patch, why the web rule is `width: auto` and the PDF's is `zoom: 0.875`, @@ -414,17 +395,15 @@ relocating it. The reasoning, and everything about running the tool, is in The site builds via [builder/](builder/), a custom Node.js static site generator (`tbdocs`). See [builder/PLAN.md](builder/PLAN.md) for the architecture overview, [builder/README.md](builder/README.md) for the quickstart, and the [tbdocs Internals](docs/Documentation/Builder.md) site page for the high-level tour. -A task-graph scheduler / parallelisation pass is designed in [builder/PLAN-scheduler.md](builder/PLAN-scheduler.md) and has been implemented (Phases 0--4). +The task-graph scheduler and parallelisation are described in [builder/PLAN-scheduler.md](builder/PLAN-scheduler.md). ### Compiling the reference's own code samples -Two reference samples had shipped that do not compile --- one passing an icon key in a slot -the same package's prose says is validated, one a `Sub` with no name. Every gate was green -over them, because a `tb` fence is something `check_code_regions.mjs` protects the -*contents* of and never evaluates. +No build gate evaluates a `tb` fence: `check_code_regions.mjs` only protects its +*contents* from rewrites. A sample that does not compile therefore passes every gate. -`examples.bat` over [scripts/check_examples.mjs](scripts/check_examples.mjs) is what asks -the compiler now. A sample opts in by carrying `check_build` in its fence info string; the +`examples.bat` over [scripts/check_examples.mjs](scripts/check_examples.mjs) asks +the compiler. A sample opts in by carrying `check_build` in its fence info string. The tool works out what to generate around it, packs many samples into one project, builds them through `tbbuild` on concurrent lanes, and reports each diagnostic against the line in the page it came from. **1,197 samples are marked, and the run takes about 110 seconds.** @@ -440,18 +419,18 @@ line into its description** --- the contributor-facing statement is **[WIP.ExamplesBuild.md](WIP.ExamplesBuild.md) is the file for this** --- the markup, the slots, the template projects and their stage sets, the batching and bisect-on-crash rules, what actually collides inside one project, and what the first full run found. Two results -from it belong here because they are about the harness rather than about the samples: -`[RunAfterBuild]` is **one per project** (TB5114), which is what `check_run` has to be -designed around; and **`tbbuild` used to report a clean build on a project that crashed the -compiler**, which is fixed and is the reason to distrust any "it stopped changing, so it -must be done" heuristic against this compiler. +from it concern the harness rather than the samples: +`[RunAfterBuild]` is **one per project** (TB5114), which `check_run` has to be +designed around; and **`tbbuild` once reported a clean build on a project that crashed the +compiler** (fixed). Distrust any "it stopped changing, so it must be done" heuristic +against this compiler. ### A hung build times out A task a worker claims and never finishes wedges the whole graph in silence: its successors' dependency counts never drop, the scheduler's promise never settles, and the process sits there with its last log line on screen. `Scheduler` watches -for it --- no task completing for `--stall-timeout` seconds (default **120**, +for it. No task completing for `--stall-timeout` seconds (default **120**, `0` disables) prints what was outstanding, including the source pages behind a wedged `render:i` chunk, and fails the build. Readers get this at [When a build stops instead of failing](docs/Documentation/Building.md). @@ -464,7 +443,7 @@ Why the report separates the wedged task from the merely blocked ones, and why - `build.bat` — runs `node builder\tbdocs.mjs --src docs --check-audit-index` (which implies `--check`) and produces three trees in one pass: the online copy at `_site/`, a `file://`-browsable copy at `_site-offline/`, and the sparse pagedjs source at `_site-pdf/`. The offline pass adds ~700 ms and the PDF pass adds ~150 ms on top of the ~2 s online build. Toggle `also_build_offline` / `also_build_pdf` in `_config.yml` (or pass `--no-offline` / `--no-pdf`) to skip a sibling output. `--check` adds ~1.7 s and runs the link + integrity check over the HTML while it is still in worker memory; `build.bat --no-check` gets a plain build. - `serve.bat` — runs `tbdocs --serve`: initial build, then a long-lived process with watcher, debounced rebuilds, and SSE-driven browser auto-reload. Writes to `docs/_serve/` (disjoint from `build.bat`'s `_site*/`) and skips the offline + PDF passes — so a one-off `build.bat` for the PDF or offline mirror doesn't disturb the live preview. Ctrl+C to stop. -- `check.bat` — the gates that read the built site: a freshness check that refuses a stale tree (`scripts/check_tree_fresh.mjs`), the DOT diagram fit check (`scripts/check_dot_fit.mjs`), the a11y sample-coverage check (`scripts/pick_a11y_sample.mjs --check`), then the accessibility check (`scripts/check_a11y.mjs`). The link + integrity check moved into `build.bat`. ~37 s. +- `check.bat` — the gates that read the built site: a freshness check that refuses a stale tree (`scripts/check_tree_fresh.mjs`), the DOT diagram fit check (`scripts/check_dot_fit.mjs`), the a11y sample-coverage check (`scripts/pick_a11y_sample.mjs --check`), then the accessibility check (`scripts/check_a11y.mjs`). The link + integrity check runs in `build.bat`. ~37 s. - `test.bat` — the tests the *toolchain* has to pass: the publish-allowlist self-test (`scripts/check_publish_policy.mjs`), the gate-list check (`scripts/check_gate_lists.mjs`), the CI-workflow roster check (`scripts/check_ci_workflows.mjs`), the lint gate (`scripts/check_lint.mjs`), the site-search unit tests (`node --test test/search.test.mjs`), the markdown-plugin unit tests (`node --test test/render.test.mjs`), the date-formatter unit tests (`node --test test/strftime.test.mjs`), the bug-reproducer picture tests (`node --test test/png.test.mjs`), `check_examples.mjs`'s probes (`node --test test/example-batches.test.mjs`), the regex-safety gate (`scripts/check_regex_safety.mjs`), the code-region gate (`scripts/check_code_regions.mjs`), the page-count drift-guard probes (`scripts/check_page_baseline.mjs`), the book-coverage probes (`scripts/check_book_coverage.mjs`), the symbol-index probes (`scripts/check_symbol_index.mjs`), the twinBASIC-scanner probes (`scripts/check_twin_parsers.mjs`), the attribute-sweep probes (`scripts/check_attribute_sweep.mjs`), the command-line probes and cases (`scripts/check_cli.mjs`), the pdf-lib shim comparison (`scripts/check_pdf_shims_equiv.mjs`), the impexp parity check (`scripts/check_impexp_parity.mjs`), and the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`). ~23 s, of which the regex-safety gate is ~10 s and the impexp check ~4 s. See [What belongs in test.bat rather than check.bat](WIP.Build.md#what-belongs-in-testbat-rather-than-checkbat). - `book.bat` — renders the PDF from `docs\_site-pdf\book.html` via `node book\render-book.mjs` into `docs\_pdf\twinBASIC Book.pdf`. Run `build.bat` first to populate `_site-pdf/`; `book.bat` refuses a tree older than its sources rather than rendering the previous book (see [The book refuses a stale source tree](WIP.Build.md#the-book-refuses-a-stale-source-tree)). @@ -484,9 +463,9 @@ After a batch of changes, verify the site builds clean and all links resolve: 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 got folded into the build's task graph, what it cost and what it saved; the axe follow-ons are designed there but not implemented. +On the dev box that is ~4 s of build against ~37 s of check, of which the axe scan is ~20 s. [builder/PLAN-checks.md](builder/PLAN-checks.md) records how the link checker sits in the build's task graph and its cost; the axe follow-ons are designed there but not implemented. -**If the change touched `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `test/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~23 s. Seventeen of its twenty gates cannot be affected by an edit under `docs/` at all. **Three can.** `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `check_gate_lists.mjs` is the easy one to predict: it 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` is the one worth understanding**, and which half of it a content edit reaches is worth keeping straight. 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 structurally cannot see that one, because text the rewrite skipped is masked and restored unchanged and every region still matches. So run `test.bat` after adding an unusual code construct --- a fence whose contents include a fence marker, a 4-space indented block, an admonition wrapping a fence --- and read the built page as well, because for the mirror fault the gate is asserting that the mask and the admonition scan still work rather than checking your page: +**If the change touched `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `test/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~23 s. Seventeen of its twenty gates cannot be affected by an edit under `docs/` at all. **Three can.** `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `check_gate_lists.mjs` reads `README.md` and every page under `docs/Documentation/`, so an edit to any developer page that states a gate count can fail it. **`check_code_regions.mjs` has two halves, and a content edit reaches only one.** Its corpus sweep reads `DOCS_DIR`, which is `/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 @@ -520,7 +499,7 @@ wrapper: | `test.bat` | `check_code_regions` | no source or HTML rewrite altered a code region, and the rewrites over rendered HTML leave a raw `
` or `` as written; a rewrite still fires on prose beside a fence (the mirror fault); `lib/markdown.mjs` and `lib/frontmatter.mjs` pass their probes, and the block parse finds what the full parse finds; the count-name check skips code and names the file's line; `discover` warns about an unquoted frontmatter value that ends in `#`; the dash normaliser converts only prose |
 | `test.bat` | `check_regex_safety` | no regex in the tree can backtrack exponentially |
 | `test.bat` | `check_symbol_index` | the symbol index still places each kind of symbol, from fixtures |
-| `test.bat` | `check_twin_parsers` | every word of the shared modifier list reaches all three scanners of twinBASIC source; the census's declaration kinds and `parseTargets`' targets hold for the shapes each once misread |
+| `test.bat` | `check_twin_parsers` | every word of the shared modifier list reaches all three scanners of twinBASIC source; the census's declaration kinds and `parseTargets`' targets hold for the shapes each is known to misread |
 | `test.bat` | `check_attribute_sweep` | the sweep's site skeletons render and are each in a family of `Applicable to:` targets or listed as in none; its reading of a probe's diagnostics puts the refusals, the control's fold and a skeleton's own errors in the right states; every real `Applicable to:` line reads to its pinned targets; probes batch once each and never two of a once-per-project attribute; a cell missing a form is not reported refused; and the isolating runner, against a scripted fake in place of the IDE, finds a crash, a hang, a disturbed canary or a stray row and stops at each cap |
 | `test.bat` | `check_cli` | `lib/cli.mjs` parses as a strict `parseArgs` does, and also refuses an empty value unless the option allows one; every tool refuses an unknown flag, and every tool with a value option an empty value; each tool's recorded command-line errors still exit and print as recorded, run with an IDE and a browser that do not exist; every tool's `--help` and `-h` print its usage to stdout with exit 0 and start nothing, and each usage text ends in one `Exit codes:` block |
 | `test.bat` | `check_ci_workflows` | both CI workflows run every wrapper gate, with the same arguments and order, and build with `build.bat`'s flags |
@@ -549,11 +528,10 @@ The nav integrity check ([builder/nav.mjs](builder/nav.mjs)) runs during COMPUTE
 - **Ambiguity** — multiple pages share the title declared in `parent:` and `grand_parent:` is either absent or insufficient to disambiguate. The page would silently appear under every matching parent.
 - **Orphan** — no page has the title declared in `parent:`. The page would silently disappear from the navigation sidebar.
 
-Each gate's failure history --- what it caught, what shipped green past it, and
-the rule that came out of it --- is [WIP.Build.md](WIP.Build.md). Two of those
-rules bind every session and are repeated under [Don'ts](#donts): never rewrite
-markdown source or rendered HTML without a code guard, and whitespace inside
-inline `` is content.
+[WIP.Build.md](WIP.Build.md) says why each gate exists and what it must catch.
+Two of its rules bind every session and are repeated under [Don'ts](#donts):
+never rewrite markdown source or rendered HTML without a code guard, and
+whitespace inside inline `` is content.
 
 ## Repository Use
 
@@ -582,7 +560,7 @@ they are.
 - Don't commit `.claude/` or `CLAUDE.md` — both gitignored. (`WIP.md` is committed; `CLAUDE.md` is just a local `@WIP.md` import shim.)
 - Don't touch `_site/` or `_site-offline/` (build outputs, gitignored).
 - **Don't walk `docs/` for its markdown with a private `readdir`.** Call `markdownFiles` from [lib/markdown-files.mjs](lib/markdown-files.mjs), which never enters the build's output trees. A walk that does enter them crashes whenever a running `serve.bat` rewrites `_serve`; see [The code-region gate](WIP.Build.md#the-code-region-gate). Any other walk of `docs/` decides what is an output tree with the same module's `isOutputTree`, as `check_tree_fresh.mjs` does, rather than a list of its own.
-- **Don't judge rendered styling by opening a built page as a `file://` URL in the in-app browser pane.** It does not apply the page's stylesheets, so everything renders unstyled and any conclusion about colour, spacing, layout or contrast drawn from it is worthless. Use `serve.bat`, which serves over HTTP at localhost and renders for real. The confusing part is that `file://` is fine *through puppeteer* -- `scripts/check_a11y.mjs`, `scripts/sweep_a11y.mjs` and the `perf/` rigs all load `_site-offline/` over `file://` and get correct computed styles, which is the entire reason the offline tree exists (see [Site integrity check](#site-integrity-check)). So: puppeteer for measuring, `serve.bat` for looking. Never the preview pane on a `file://` path.
+- **Don't judge rendered styling by opening a built page as a `file://` URL in the in-app browser pane.** It does not apply the page's stylesheets, so everything renders unstyled and any conclusion about colour, spacing, layout or contrast drawn from it is worthless. Use `serve.bat`, which serves over HTTP at localhost and renders for real. `file://` does work *through puppeteer* -- `scripts/check_a11y.mjs`, `scripts/sweep_a11y.mjs` and the `perf/` rigs all load `_site-offline/` over `file://` and get correct computed styles, which is the reason the offline tree exists (see [Site integrity check](#site-integrity-check)). So: puppeteer for measuring, `serve.bat` for looking. Never the preview pane on a `file://` path.
 - Don't write literal en-dash `–` or em-dash `—` in `docs/` markdown source. Use `--` (renders as en-dash) or `---` (renders as em-dash) — markdown-it's typographer does the conversion at build time. `scripts/convert_em_dash_separators.mjs` normalises any strays.
 - **Never write or edit a file with a shell heredoc.** No `cat > file <<'EOF'`, no
   `printf` into a file, no `sed -i` for a content edit. Use the file-writing and
@@ -591,22 +569,21 @@ they are.
   on the shell's own metacharacters, and fails late and partially, which is worse than
   not writing the file at all. The shell is for running things, not for authoring them.
 
-  **It fails silently, which is the part worth fearing.** A scratch classifier written
-  through `<<'EOF'` had every `"\\s+"` delivered as `"\s+"`, matched nothing, and reported
-  **444 unclassifiable fences against a true 32** --- a number that reads as a finding
-  about the corpus and was a finding about the quoting.
+  **It fails silently.** Through `<<'EOF'` a regex written `"\\s+"` is delivered as
+  `"\s+"`, matches nothing, and the tool reports a wrong count: a number that reads as
+  a finding about the corpus and is a finding about the quoting.
 
   **The file tools decode a `\u` escape of four hex digits** into the character it
-  names: a BOM, an em-dash and a replacement character all arrived raw, and only NUL was
+  names: a BOM, an em-dash and a replacement character arrive raw, and only NUL is
   left as written. A doubled backslash arrives doubled, which is a different string.
   Write the brace form, `\u{FEFF}`, which arrives intact (a regex needs the `u` flag
   for it), and check a file you have written for raw non-ASCII.
 - Don't push or force-push without explicit user request.
-- Don't leave a remote image URL in a finished page. A pasted `https://github.com/user-attachments/assets/...` link is fine to write --- [builder/vendor-assets.mjs](builder/vendor-assets.mjs) downloads it to `docs/assets/attachments/gh-.` on the next local build and rewrites the render to point there; commit the downloaded file with the edit. Any other remote host has no such handling: download it yourself and commit it under the section's `Images/` folder. Remote images cost a network round trip per page view, break the `file://` offline mirror, and **abort the PDF book render** -- the forked paged.js in `book/lib/` dropped async image loading, so an image still in flight when the page-breaking pass runs raises instead of degrading. The build enforces this unconditionally (see [Site integrity check](#site-integrity-check)); `--check-remote-assets` is the standalone checker's flag, not a `tbdocs` one. The check is scoped to ``; `