diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 7493e337..ac0eb062 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -21,7 +21,7 @@ What an entry owes a reader: ## The recent-projects list fills its empty slots with copies of its last entry -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** cosmetic, but it shows on a new installation, which is exactly when the list has empty slots --- the same project repeated down the Recent tab. @@ -62,7 +62,7 @@ opens then trips this. ## Compiler crashes on an `Interface` named by an angle-bracket placeholder that has an `Extends` clause -**Build:** BETA 983 (`twinBASIC_win32.dll+00141F7A`) +**Build:** BETA 995; BETA 983 at `twinBASIC_win32.dll+00141F7A` **Severity:** crash --- takes the compiler down, three restarts, then the IDE gives up. This two-line file is the whole reproduction: @@ -101,7 +101,7 @@ costs the whole batch its result, which is why that tool isolates the sample on ## An `Interface` that extends itself compiles without a diagnostic -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** invalid code accepted --- the same cycle through a class is refused. This two-line file compiles with no error, warning, hint or info: @@ -134,7 +134,7 @@ candidate, and the interface cycle compiled instead of crashing. ## `--buildAndExit32` writes nothing, exits 0 on a project with errors, and hangs on a failing build -**Build:** BETA 983 +**Build:** BETA 995; the silence on stdout and stderr was measured on BETA 983 **Severity:** makes the documented unattended-build switch unusable. The IDE executable accepts `--buildAndExit32` and `--buildAndExit64`; `parseCommandLine()` @@ -154,7 +154,7 @@ records the same measurements. ## Public members are typed with Private components, so a default project cannot use them -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** documented APIs need the consumer to expose the package's internals; one event asks for a type it then refuses. @@ -250,7 +250,7 @@ compiled in a default project. ## `Err.Raise` rejects `HelpContext` as a named argument, while its three siblings work -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** VBA-compatible code that names the fifth argument does not compile, and the diagnostic does not say which name was wrong. @@ -293,7 +293,7 @@ whose sample was written in the named form. The page uses the positional form no ## An interface member marked `[PreserveSig]` cannot be implemented by a class -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** an interface the language lets you declare cannot be implemented at all, and the diagnostic asks for the signature that is already written. @@ -313,9 +313,11 @@ End Class ``` TB5004 unable to match this implementation to its interface member. The expected signature was: Private Function IProbeD_F() As Long -TB5000 Missing implementation of member Function F() As Long +TB65535 Missing implementation of member Function F() As Long ``` +BETA 983 gave the second message as TB5000. + **The "expected" signature is character for character the one on the line the error is reported against.** Whatever the compiler is comparing, it is not what it prints. @@ -341,7 +343,7 @@ member it implements, and its description of the attribute says why. ## `import` stops with exit code 999 on any folder inside `Packages`, so a project that embeds a package cannot be packed -**Build:** BETA 983 --- `twinBASIC_win32.exe` and `twinBASIC_win64.exe` alike +**Build:** BETA 995 --- `twinBASIC_win32.exe`; on BETA 983 `twinBASIC_win64.exe` as well **Severity:** the command line cannot pack any project that embeds a package, and the failure prints neither `... DONE` nor `... FAILED`. @@ -399,7 +401,7 @@ included, which `export` does not write. ## `export` and `import` stop at the 260-character path limit, apart from the one path they prefix with `\\?\` -**Build:** BETA 983 --- `twinBASIC_win32.exe`, on a machine with `LongPathsEnabled` set to 1 +**Build:** BETA 995 --- `twinBASIC_win32.exe`, on a machine with `LongPathsEnabled` set to 1 **Severity:** an `export` to a deep folder writes part of the tree and exits 0, and the errors it prints blame permissions and storage space. @@ -437,7 +439,8 @@ trees as complete. ## A damaged project file opens a message box, and the command waits until it is closed -**Build:** BETA 983 --- `twinBASIC_win32.exe` +**Build:** BETA 983 --- `twinBASIC_win32.exe`; not re-run on BETA 995, since the box opens +on the desktop of whoever runs the command **Severity:** an unattended `export`, `settings` or `readme` never finishes; once the box is closed, `export` reports success. @@ -469,7 +472,7 @@ known. ## `export` refused for lack of `--overwrite` still writes part of the tree -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** a refused export leaves the folder a mixture of the old tree and the project. Export the HelloWorld sample into a folder, delete the exported `Settings`, edit @@ -494,7 +497,7 @@ writes one copy and then refuses the other because of the file it has just writt ## The IDE has written the same name twice into project files it ships -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** a folder can hold only one of them, so unpacking keeps one copy; which copy the IDE itself uses is not known. @@ -518,7 +521,7 @@ the repeated entries in a warning. ## `export` needs a full, backslashed project path, and no folder path may use forward slashes -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** ordinary relative and forward-slashed paths fail, with messages that say the file or folder does not exist. @@ -531,7 +534,8 @@ file or folder does not exist. The echo line explains the first two: `exporting from "\\?\hello.twinproj"`. The project path is prefixed with `\\?\`, which turns off Windows' path normalisation, so only a full path with -backslashes survives it. **What does not reproduce it:** `import`'s project path and the +backslashes survives it. The forward-slashed folder ends `... FAILED`, creates nothing, and +exits 0 (BETA 995), so a script sees success unless it reads the output. **What does not reproduce it:** `import`'s project path and the printing commands' take relative and forward-slashed paths, and with backslashes `export` creates every missing level of its output folder. @@ -539,7 +543,7 @@ creates every missing level of its output folder. ## `import` of a folder with no `Settings` file fails without saying why -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** minor --- the refusal is right, and the silence is not. Given a folder with no `Settings` file at its top, `import` lists the files it read, ends @@ -548,44 +552,9 @@ cause. --- -## `\` and `Mod` on the most negative `Integer` or `Long` by -1 raise a native exception that `On Error` cannot handle - -**Build:** BETA 983, 32-bit target -**Severity:** a division that should raise the trappable error 6 stops the procedure instead; the -`LongLong` form returns a wrong value with no error at all. - -``` -Dim a As Integer = -32768 -Dim b As Integer = -1 -On Error Resume Next -Debug.Print a \ b -``` - -The DEBUG CONSOLE shows `NATIVE EXCEPTION: NT_OVERFLOW /; . LINE -[CONTINUABLE]` for the division's line, then `[IDE] auto-activated TRACE-MODE in this session`, -and nothing after that line runs, `On Error Resume Next` notwithstanding. - -| operands | `\` | `Mod` | -|---|---|---| -| `Integer` -32768 and -1 | native exception | native exception | -| `Long` -2147483648 and -1 | native exception | native exception | -| `LongLong` -9223372036854775808 and -1 | **-9223372036854775808**, no error | 0 (right) | -| a `Variant` holding the `Integer` -32768, and -1 | `Long` 32768 (right) | --- | -| a `Variant` holding the `Long` -2147483648, and -1 | error 6 (right) | --- | - -**What does not reproduce it:** every other overflow measured raises error 6 as it should --- -`32767 + 1`, `32767 * 32767` and `-(-32768)` on typed `Integer` values, and the same kind of -overflow on `Long`, `LongLong`, `Single`, `Double`, `Currency` and `Decimal` --- and division by -zero raises error 11. Only the one quotient that does not fit its type fails this way. - -**Found by** probing the arithmetic operators' result types for `Reference/Operators.md`. The -probe's first run lost every case after this one. - ---- - ## Shifting a `Single`, `Double`, `Date`, `Boolean` or `String` compiles clean, then fails code generation -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** the compiler accepts the expression with no diagnostic, and the procedure that contains it never runs. @@ -616,26 +585,15 @@ floating-point operands are truncated before shifting. --- -## `>>` gives three different results for the same value, and a `Variant` shift can return `Empty` +## A `Variant` shift multiplies a fractional value, and can return `Empty` -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** wrong values, with no diagnostic. -``` -Dim n As Long = -8 -Dim v As Variant = CLng(-8) -Debug.Print -8& >> 1 ' -4 constants: an arithmetic shift -Debug.Print n >> 1 ' 2147483644 a Long variable: a logical shift -Debug.Print v >> 1 ' -4 a Variant: a division, truncated toward zero -``` - -The logical shift is what a typed variable gets and what the documentation describes. The -constant folder disagrees with the code generator: `-1 >> 1` and `-1& >> 1` are both -1, where an -`Integer` variable holding -1 gives 32767 and a `Long` variable gives 2147483647. - -A `Variant` operand is not shifted but multiplied or divided --- a `Variant` holding the `Double` -7.9, shifted left by 1, is 15.8 --- and a count as large as the width of the type it holds gives -`Empty` rather than 0: +A `Variant` holding the `Double` 7.9, shifted left by 1, is 15.8: the value is multiplied, not +shifted, and so is a `Currency` or `Decimal` holding 7.9. Shifted right by 1, the `Variant` and +the `Decimal` give 3 but the `Currency` gives 3.95. A count as large as the width of the type the `Variant` holds gives `Empty` rather +than 0: | expression | result | |---|---| @@ -644,14 +602,15 @@ A `Variant` operand is not shifted but multiplied or divided --- a `Variant` hol | a `Variant` holding `CLng(1)`, `<< 31` | `Long` -2147483648 | | a `Long` variable holding 1, `<< 32` | 0 | -**Found by** probing the operators for `Reference/Core/LeftShift.md` and `RightShift.md`, whose -examples said `-1 >> 1` returns `&H7FFFFFFF`. It returns -1. +**Found by** probing the operators for `Reference/Core/LeftShift.md` and `RightShift.md`. The +same probe found `>>` logical on a typed variable and arithmetic on a constant, up to BETA 983; +BETA 984 made both arithmetic. --- ## Overloads on `Date` and `Double` resolve by declaration order, not by the argument's type -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** the wrong overload runs, with no diagnostic. ``` @@ -684,7 +643,7 @@ measuring the operators for `Reference/Operators.md`. ## `Boolean \ String` and `Boolean Mod String` convert the `String` to `Boolean` -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** a wrong value and a wrong type, with no diagnostic. ``` @@ -711,7 +670,7 @@ the other way round: `"2" \ b` is the `Long` -2. ## Export Project follows a directory junction in its folder and deletes what it points to -**Build:** BETA 983 +**Build:** BETA 995 (`ide-test.bat`'s `export` lane asserts it) **Severity:** data loss outside the folder the user chose. Export Project empties its folder before writing, as the *Export Path* setting warns; it does not stop at a junction. @@ -734,7 +693,7 @@ before writing, as the *Export Path* setting warns; it does not stop at a juncti ## Export Project stops at a read-only file after deleting everything before it, and the IDE reports nothing -**Build:** BETA 983 +**Build:** BETA 995 (`ide-test.bat`'s `export` lane asserts it) **Severity:** a partly emptied folder, with the only record in the Debug Console. On a Git working copy it breaks the repository, because Git makes its object files read-only. @@ -762,7 +721,7 @@ completely --- `.git` included, with no prompt. ## Export Path refuses `${SourcePath}` alone, but not the same folder written as a path -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** the project file is deleted when the export folder is the folder that holds it. The Settings editor's check on `project.exportPath` in `ide/main.js` compares the text with @@ -773,14 +732,17 @@ logged `[EXPORT] DELETED: \\?\\.twinproj` and completed. A **S wrote the file back; closing without saving loses it. **Found by** the same probe. The compiler's side was measured, by calling `exportProjectTo()` -with the folder; that the editor accepts the same folder typed as a path is read from the -check's code, not tried. +with the folder, and `ide-test.bat`'s `export` lane asserts it; that the editor accepts the +same folder typed as a path is read from the check's code, not tried. The Save that writes +the file back was seen on BETA 983 only. --- ## Export Project writes the compiler packages, which the project does not hold, and the command line cannot pack the result -**Build:** BETA 983 +**Build:** BETA 995 for the export and the command line's `import` of it, which +`ide-test.bat`'s `export` lane asserts; the IDE's own import and the dead copy were measured +on BETA 983 **Severity:** the IDE's export of a project cannot be packed back into a project by the supported tool, so it cannot serve for version control; and a two-file project exports as 477 files. @@ -835,7 +797,7 @@ dialog calls after its folder picker --- and `root.saveProjectAs()` over DevTool ## An out-of-range index raises `&H8002000B` or `&H80004005`, not VBA's error 9 -**Build:** BETA 983 --- the IDE and a compiled EXE alike +**Build:** BETA 995 in the IDE; BETA 983 in the IDE and a compiled EXE alike **Severity:** VBA code that handles `Err.Number = 9` does not recognise the error, with no diagnostic. @@ -867,7 +829,7 @@ varies between runs --- *Unspecified error* in one, *Automation error* in anothe ## Reading `Forms` by index returns a broken reference, and the process then crashes -**Build:** BETA 983 --- the IDE and a compiled EXE alike +**Build:** BETA 995 as a compiled EXE; BETA 983 in the IDE and a compiled EXE alike **Severity:** crash (`0xC0000005`), from a form of access the documentation shows. With one form loaded (`Load Form1`): @@ -877,9 +839,12 @@ Dim s As String s = Forms(0).Name ' s is "", and the process later dies with 0xC0000005 ``` -`Set f = Forms(0)` followed by `f.Name` does the same, and so does `s = Forms(n).Name` with -`n` a variable. Inside `For k = 0 To Forms.Count - 1`, `Set f = Forms(k)` corrupts the loop -variable: `k` read 0, 0, 0, then 8195702. +`Set f = Forms(0)` followed by `f.Name` does the same when `f` is declared `As Form`, and so +does `s = Forms(n).Name` with `n` a variable. Inside `For k = 0 To Forms.Count - 1`, +`Set f = Forms(k)` with `f` declared `As Form` corrupts the loop variable: `k` read 0, 0, 0, +then 8195702. With three forms loaded, BETA 995 and 983 alike log `k=0` twice, then a +corrupted string, and exit `0xC0000005`. With `f` declared `As Form1` or `As Object`, the same +code returns the form and exits 0. **What does not reproduce it:** `n = 0: Set f = Forms(n)` outside a loop returns the form (`f.Name` is `Form1`) and the program exits 0; `For Each f In Forms` and `Unload Forms(i)` work; @@ -896,7 +861,7 @@ and as the built EXE, with identical results. ## A step key pressed on the line that raised an error leaves a step pending -**Build:** BETA 983 +**Build:** BETA 995 (`ide-test.bat`'s `debugger` lane asserts it) **Severity:** the debugger stops where it was not asked to, and one command no longer means one thing. @@ -907,8 +872,9 @@ thing. Moving past the line with **Set Next Statement** (CTRL+F9) instead, each F5 then advances one line. -Seen in three runs. In the one followed to the end, it lasted until the procedure returned; in -another, a fix-then-F5 stopped once. Which of the two applies was not isolated. +In `ide-test.bat`'s `debugger` lane, on BETA 983 and 995 alike, the waiting step is used up by +that one stop: the next F5 runs on to the loop's next error. An earlier run by hand, followed to +the end, saw it last until the procedure returned; what differed there is not known. **What does not reproduce it:** choosing **Ignore** without pressing a step key first, which runs on from the next line as the panel says. @@ -919,7 +885,7 @@ runs on from the next line as the panel says. ## Stop at a run-time error ends only the procedure that raised it -**Build:** BETA 983 +**Build:** BETA 995 (`ide-test.bat`'s `debugger` and `assert` lanes assert it, and pass on BETA 983 as well) **Severity:** the program goes on running after the user asked it to stop. A `Sub Main` that calls a procedure which raises an untrapped error, and prints a line after @@ -933,9 +899,10 @@ prints `aborted` and ends the whole run. **Its worst consequence is a false pass.** At a failed `Assert` --- whose error is raised by the assertion's own procedure --- **Stop**, and **Run → End** too, end only that procedure: the test carries on past the failed check, and a runner in the shape `Testing-with-Assert.md` teaches then -prints `All PadLeft tests passed.` Three trials, one per button (**Ignore (Resume Next)** does the -same, as it should). Moving execution to the test's `End Sub` with **Set Next Statement** and then -choosing **Run → End** makes it an ordinary break, and the run is aborted (two trials). +prints `All PadLeft tests passed.` The `assert` lane checks the panel's **Stop** and the Stop +command that the toolbar and **Run → End** run. By hand on BETA 983, **Ignore (Resume Next)** did +the same, as it should, and moving execution to the test's `End Sub` with **Set Next Statement** +and then choosing **Run → End** made it an ordinary break, and the run was aborted (two trials). **Found by** the same probe; the assertion case by the fix pass for the Assert tutorial. @@ -943,7 +910,10 @@ choosing **Run → End** makes it an ordinary break, and the run is aborted (two ## A `Static` declaration cannot initialise with a constructor that takes arguments -**Build:** BETA 983 +**Build:** BETA 983 --- **not reproduced on a re-check**: a `Static s As Dog = New Dog("Rex")`, +in a Module procedure, a Function, a Class method and a Property Get, with `Dog` a Private or a +`[COMCreatable(False)]` class with a one-parameter `Sub New`, compiled and ran on BETA 983 and +995 alike. What else the original case held is not recorded; find it before filing. **Severity:** a valid declaration does not compile; the workaround is a `Static` without an initialiser and a `Set` on first use. @@ -973,7 +943,7 @@ no arguments (`Static c As Collection = New Collection`); and a `Static` of a va ## *Import from file...* leaves the imported package unticked -**Build:** BETA 983 +**Build:** BETA 995 (`ide-test.bat`'s `packages` lane asserts it); BETA 983 by hand **Severity:** the package is imported but not referenced, and the documentation says it is. Settings → References → Available Packages → *Import from file...*, and choose a `.twinpack`. The @@ -981,7 +951,8 @@ compiler answers the IDE's `importPackage` request with `success: true, body: { packageSymbol: "DocProbePkg" }`, and the package appears in the list unticked, so nothing in the project can use it until it is ticked by hand. `packageLoadFromFile` in `ide/main.js` reads `packageSymbol` from the response itself rather -than from its `body`, which is consistent with what is seen; that part is read, not traced. +than from its `body`, while the online import path beside it, `importPackage`, reads +`t.body.packageSymbol`. **Found by** the package probe for round 8's UC-60, which drove the import over DevTools with the file's path in place of the native picker. @@ -990,7 +961,8 @@ the file's path in place of the native picker. ## Replacing an embedded package under one Apply keeps running the old copy -**Build:** BETA 983 +**Build:** BETA 995, asserted by `ide-test.bat`'s `packages` lane with the two-Apply path as +its control; the linked-copy rows below were measured on BETA 983 **Severity:** the project builds and runs the old package after the user has replaced it. 1. A project embeds a package built locally, `DocProbePkg` v1. @@ -1010,9 +982,78 @@ and v2 runs at once. --- +## Embedding a package with no `Packages` folder puts the compiler in a crash loop + +**Build:** BETA 995 +**Severity:** low. The input is invalid, and nothing in a normal workflow makes it: every package +the IDE writes has the folder, and `scripts/impexp.mjs` and `impexp.py` add it when a tree +lacks it. A crash is still a poor answer to it. + +1. Pack a package tree that has no `Packages` folder into a `.twinpack`. The tB executable's + `import` packs such a tree as it is; the repository's scripts no longer do. The tree may lack + `ImportedTypeLibraries` and `Miscellaneous` as well; neither matters. +2. In a project, Settings → References → Available Packages → *Import from file...* the + `.twinpack`, tick it, and apply. +3. The console shows `[PROJECT] twinBASIC project saving to disk [DONE]`, then `restarting from + FILE` four times about two seconds apart, and the IDE reports "Compiler crash loop detected. + Restarting in SAFE mode." + +An empty `Packages` folder in the package tree is enough to prevent it: the same steps with that +folder alone, or with all three, restart the compiler once and run the package. + +**What does not reproduce it:** a project with the same package already embedded under +`Packages\DocProbePkg`, without the folder, and opened cold compiles clean (also on BETA 983). So the loop needs the package to be embedded by the IDE. +BETA 983 has not been tried through the IDE: in a lane its References page never finishes loading. + +**Found by** `ide-test.bat`'s `packages` lane, with the package's folders varied one at a time, +and one run watched with `--show`. + +--- + +## A call through a `FastCall` or `ThisCall` delegate is made as stdcall on win32 + +**Build:** BETA 995 +**Severity:** the delegate is unusable on win32; every call through it raises an error. + +```tb +Public Delegate Function FastDel FastCall (ByVal a As Long, ByVal b As Long) As Long + +Public Function GF FastCall(ByVal a As Long, ByVal b As Long) As Long + Return a * 100 + b +End Function + +Public Function GS(ByVal a As Long, ByVal b As Long) As Long + Return a * 100 + b +End Function + +Dim d As FastDel = AddressOf GF +Debug.Print d(9, 1) ' error: "Bad DLL definition. Stack corruption detected." +Dim e As FastDel = AddressOf GS +Debug.Print e(9, 1) ' 901: a stdcall target works, after warning TB0026 +``` + +The same with `ThisCall` in place of `FastCall`, for the delegate and the function, raises the +same error. So the call through the delegate passes the arguments as stdcall does, whatever +convention the delegate declares. + +**What does not reproduce it:** + +- calling `GF` directly: 901. The callee side is right: a `FastCall Naked` function that + returns `ECX + EDX`, and a `ThisCall Naked` one that returns `ECX + [ESP+4]` and ends + `ret 4`, return the right sums when called directly; +- delegates declared stdcall (no keyword) or `CDecl`, each pointed at a function of its own + convention: 901; +- a win64 build: every case above returns 901 (x64 has one calling convention). + +**Observed** with a `[RunAfterBuild]` probe through `tbrun`, and in the compiled EXE `tbrun` +left, run from its `Sub Main` and writing to a file: the same five results both ways. Both +keywords are new in BETA 990 and 992; BETA 987 refuses them (TB5182). + +--- + ## An error in the body of a generic procedure names neither the type nor the call that caused it -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** a diagnostic that points at correct code. In a project with many calls to a generic procedure, nothing says which call to fix. @@ -1043,7 +1084,7 @@ nothing to say which types it accepts. ## Text that continues a `Debug.Print` line is escaped twice in the DEBUG CONSOLE -**Build:** BETA 983 +**Build:** BETA 995 **Severity:** cosmetic, but it changes what a program appears to print: `&`, `<` and `>` in the continued part of a line show as `&`, `<` and `>`. @@ -1081,7 +1122,7 @@ that the IDE appends to an open console line. ## An add-in's keyboard shortcut does not fire if it includes `{CTRL}` or `{ALT}` -**Build:** BETA 983 +**Build:** BETA 995 (`addin-test.bat`'s `keys` lane asserts it) **Severity:** the SDK's own example, `{CTRL}{SHIFT}d` in `KeyboardShortcuts.Add`'s description, cannot be used, and nothing says why. @@ -1120,7 +1161,8 @@ key events and checks each result. Every case in the table is a test in that lan ## F1 and the fold icon toggle the signature help, then fail -**Build:** BETA 983 +**Build:** BETA 995 for F1 (`addin-test.bat`'s `keys` lane asserts it); the fold icon was +clicked on BETA 983, and `toggleSigHelp` and both callers are unchanged in BETA 995's `ide/main.js` **Severity:** cosmetic --- the toggle works, but every F1 adds `command failed: "tbHelp_ToggleExpandSignatureHelp"` to the DEBUG CONSOLE, and every click on the icon throws in the page. @@ -1145,7 +1187,8 @@ line, and the click in a harness IDE with `Runtime.exceptionThrown` recorded ove ## Typing just after a file opens at a position puts the text at that position, in reverse -**Build:** BETA 983 +**Build:** BETA 983; `parseDocumentDecorations` and `revealLineInEditor` are unchanged in BETA +995's `ide/main.js` **Severity:** typed text goes to the wrong place and in the wrong order, and nothing shows that it happened. @@ -1180,7 +1223,7 @@ opening a file (`afterReveal` in `scripts/lib/tb-operate.mjs`). ## Hover says a `ByVal` parameter was auto-generated because `Option Explicit` is off -**Build:** BETA 983 +**Build:** BETA 995 (`addin-test.bat`'s `symbols` lane asserts it) **Severity:** cosmetic, but it tells the user to turn on an option that is already on, over a parameter they declared. @@ -1221,7 +1264,7 @@ checks every row of the table). The text is the markdown the IDE's hover shows. ## Every tool window given no id is the same window -**Build:** BETA 983 +**Build:** BETA 995 (`addin-test.bat`'s `panes` lane asserts it) **Severity:** an add-in's windows overwrite each other, or another add-in's, and nothing says so. The id is declared `Optional`, so leaving it out looks correct. @@ -1256,7 +1299,7 @@ IDE's add-in samples leaves the id out. ## `[PopulateFrom]` with no arguments crashes the compiler -**Build:** BETA 987 +**Build:** BETA 995; first seen on BETA 987 **Severity:** the compiler process dies while the project is being parsed, which `tbbuild` reports as a crash (its exit code 4), so a person who forgets the arguments is not told what is missing. @@ -1295,3 +1338,66 @@ a project holding only it and a two-line `Sub Main`, with no resources: `tbbuild The same project with `[PopulateFrom("probe")]` builds and reports the one TB5083 row. The rows for `(True)`, `(False)` and `(1)` come from the sweep's batches, not from that project. + +## `As New` refuses a class whose only constructor has all-`Optional` arguments + +**Build:** BETA 995; BETA 983 accepts it and runs it +**Severity:** code that compiled before BETA 993 stops compiling, and the two checks for "can +this class be created without arguments" disagree. + +``` +Class COpt + Public V As Long + Public Sub New(Optional ByVal n As Long = 3) + V = n + End Sub +End Class + +Module Probe + Public Sub T() + Dim x As New COpt + Debug.Print x.V + End Sub +End Module +``` + +fails on the `Dim` with TB5121 `can't use this type with As-New syntax as it doesn't have a +parameterless constructor`. The same class satisfies TB5135, the check for COM exposure: it +compiles as a public class without `[COMCreatable(False)]`, so that check counts the +constructor as one that takes no arguments. On BETA 983 the reproduction compiles, and `x.V` +prints `3`. TB5121 is the diagnostic BETA 993's notes describe ("classes with +[COMCreatable(False)] set on them cannot be used as an As-New datatype"), corrected in 995. + +**What does not reproduce it:** a class with a `Class_Initialize` beside a `Sub New` that takes +a required argument, or with a second `Sub New` with no parameters, is accepted. A class whose +only `Sub New` takes a required argument is refused, `Private` or `[COMCreatable(False)]` +alike, which is the diagnostic working as intended. + +**Observed** on 2026-10-01 with compile probes through `tbbuild`, each case a project of its +own, on BETA 995 and BETA 983; the run on 983 was a compiled EXE through `tbrun`. + +## `FileCopy` of an open file raises `&H80004005`, where VB6 raises 55 or copies it + +**Build:** BETA 995; BETA 983 copied an open file with no error +**Severity:** code that handles VB6's error 55 does not recognise the error, and a copy that +VB6 makes is refused. + +``` +Dim f As String = Environ$("TEMP") & "\probe.txt" +Open f For Output As #1: Print #1, "one": Close #1 +On Error Resume Next +Open f For Append As #2 +FileCopy f, f & ".copy" +Debug.Print Err.Number, Err.Description +``` + +prints `-2147467259 Unspecified error`. VB6 prints `55 File already open`. With the file +open `For Input` instead, twinBASIC raises the same `-2147467259`, and VB6 copies the file +without an error. BETA 984's notes list the change ("FileSystem.FileCopy function would +previously allow copying of an already open file without error"); only the error number and +the `Input` case differ from VB6. + +**What does not reproduce it:** the file closed. + +**Observed** on 2026-10-01: the twinBASIC lines through `tbrun` on BETA 995 and BETA 983, the +VB6 lines from the same statements compiled by `VB6.EXE /make` and run. diff --git a/README.md b/README.md index 2db71111..8cd0773f 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,10 @@ test.bat # the gates that test the toolchain itself book.bat # renders the PDF book; run build.bat first examples.bat # compiles the twinBASIC code samples in the pages (Windows + a twinBASIC install) addin-test.bat # tests IDE add-ins by operating an IDE (Windows + a twinBASIC install) +ide-test.bat # tests the IDE itself by operating it (Windows + a twinBASIC install) ``` -A clean `build.bat && check.bat` is the bar for "ready to commit"; add `test.bat` when the change touched anything outside `docs/`. Each wrapper names the gates it runs, in order, on [Tools and Scripts](https://docs.twinbasic.com/Documentation/Development/Tools). On Linux or macOS, run the `node` command inside each batch file directly --- they are thin wrappers. `examples.bat` and `addin-test.bat` are the exceptions to both: they drive the twinBASIC IDE, so they are Windows-only and deliberately outside every gate and outside CI. +A clean `build.bat && check.bat` is the bar for "ready to commit"; add `test.bat` when the change touched anything outside `docs/`. Each wrapper names the gates it runs, in order, on [Tools and Scripts](https://docs.twinbasic.com/Documentation/Development/Tools). On Linux or macOS, run the `node` command inside each batch file directly --- they are thin wrappers. `examples.bat`, `addin-test.bat` and `ide-test.bat` are the exceptions to both: they drive the twinBASIC IDE, so they are Windows-only and deliberately outside every gate and outside CI. Where to read more: diff --git a/WIP.Harness.md b/WIP.Harness.md index ee052367..e44c3b9c 100644 --- a/WIP.Harness.md +++ b/WIP.Harness.md @@ -26,16 +26,16 @@ compiler's `export` verb unpacks any of them without opening the IDE: "$TB/bin/twinBASIC_win32.exe" export ".twinproj" "C:\out\dir\" --overwrite ``` -Against BETA 983 that yields **820 `.twin` files** --- 661 from the sixteen packages under -`packages/`, 159 from the thirty-two sample and template projects under `projects/` and -`addins/`. All of it is code the compiler accepts, which makes it the strongest available -evidence for anything the documentation asserts about legal syntax. +Against BETA 983 and 995 alike that yields **820 `.twin` files** --- 661 from the sixteen +packages under `packages/`, 159 from the thirty-two sample and template projects under +`projects/` (`addins/` holds no `.twinproj`). All of it is code the compiler accepts, which +makes it the strongest available evidence for anything the documentation asserts about legal +syntax. Two operational notes, both learned the annoying way. **Use backslashes.** A folder named with forward slashes fails with `output folder does not exist and could not be created` -whether it exists or not. This note used to say that the folder must already exist and that -only one level of it is created; measured against BETA 983, `export` given backslashes -creates every missing level, three deep in the test. Its project path must also be a full +whether it exists or not, creates nothing, and still exits 0. Given backslashes, `export` +creates every missing level, three deep in the test (BETA 983 and 995). Its project path must also be a full one, because it is prefixed with `\\?\`. And **redirect stdin** when looping (`` as well (P6), and the `APPDATA` each lane gives its IDEs keeps the user's out ([The add-in test runner](#the-add-in-test-runner)). -**Measured, against BETA 983:** +**Measured, against BETA 983 (and 995 where stated):** - **An IDE session writes nothing into its install.** A compile, a compiler crash and a - `tbrun` build-and-run each left all 233 files byte-identical, down to the mtimes. A + `tbrun` build-and-run each left all 233 files byte-identical, down to the mtimes; on + BETA 995 the install (235 files) was byte-identical after exports, a `tbbuild` and two + `tbrun` runs. A compile also left the per-user `%APPDATA%\twinBASIC` unchanged; that folder holds the user's downloaded packages and empty `addins`, `locale` and `themes` folders, and is shared by every install. So a compile's whole footprint outside its temp folders is the diff --git a/WIP.HelpAddin.md b/WIP.HelpAddin.md index 20de292f..87ff1b78 100644 --- a/WIP.HelpAddin.md +++ b/WIP.HelpAddin.md @@ -17,7 +17,7 @@ the detached HEAD of an old worktree keeps it --- so everything in it worth keep 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**. An offset like `main.js@611152` is a byte offset +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: @@ -70,7 +70,7 @@ the compiler what a symbol is.** Each of those gaps shapes a stage below. own environment, creates `packages`, `themes`, `locale`, `addins\win32` and `addins\win64` in, and returns. The page keeps the path as `commonFolderRootPath` and sends it with `RequestStartCompiler` and `RequestLoadAddins` (`main.js@1047705` and - `@1048667`). Measured on BETA 983 by + `@1048667`). Measured on BETA 983 and 995 by [test/addin/appdata.test.mjs](test/addin/appdata.test.mjs), with a probe add-in that prints the file it was loaded from: an IDE started with `APPDATA` naming a folder of the lane's own loaded the probe from `\twinBASIC\addins\win32`, and not a second @@ -107,7 +107,7 @@ 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 by + 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 @@ -124,13 +124,13 @@ the compiler what a symbol is.** Each of those gaps shapes a stage below. `_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 983, has the same + 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).** - Measured on BETA 983 by [test/addin/reload.test.mjs](test/addin/reload.test.mjs). A + Measured on BETA 983 and 995 by [test/addin/reload.test.mjs](test/addin/reload.test.mjs). A restart is the toolbar's restart button, every switch of the build target (below), and the IDE's own restart after a compiler crash. The page's `restartCompiler` (`main.js@153451`) first takes away what every add-in added: `removeAddinAlterations` removes their toolbar @@ -162,7 +162,7 @@ the compiler what a symbol is.** Each of those gaps shapes a stage below. `addins\win64` alone. **Switching the target of an open project (Ctrl+F1 / Ctrl+F2) restarts the compiler in the other bitness, and the new one loads the other folders.** `changedActiveBuildConfig` in `ide/main2.js` records the new target and kills the - compiler. Measured on BETA 983 by [test/addin/arch.test.mjs](test/addin/arch.test.mjs), + compiler. Measured on BETA 983 and 995 by [test/addin/arch.test.mjs](test/addin/arch.test.mjs), with a 32-bit and a 64-bit build of a probe in both folders of their bitness, the install's and `%APPDATA%`'s: the project opened in win32, whose compiler loaded the two 32-bit copies alone; a switch to win64 started `twinBASIC_win64_noDEP.exe`, which loaded the two 64-bit @@ -173,7 +173,7 @@ the compiler what a symbol is.** Each of those gaps shapes a stage below. ### Keyboard shortcuts -Read at `main.js@608242`, `@610953` and `@611152`, and measured on BETA 983 by P1 and P2, +Read at `main.js@608242`, `@610953` and `@611152`, and measured on BETA 983 and 995 by P1 and P2, whose lane is [test/addin/keys.test.mjs](test/addin/keys.test.mjs). - `KeyboardShortcuts.Add` lowercases the key string, deletes its whitespace and stores it @@ -216,7 +216,7 @@ whose lane is [test/addin/keys.test.mjs](test/addin/keys.test.mjs). ### Tool windows Read at `main.js@1002292` (`toolWindowElementAddChild`) and `@1005960` -(`toolWindowElementSetProperty`), and measured on BETA 983 by P3, P4 and P12, whose lane is +(`toolWindowElementSetProperty`), and measured on BETA 983 and 995 by P3, P4 and P12, whose lane is [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. @@ -231,7 +231,7 @@ Read at `main.js@1002292` (`toolWindowElementAddChild`) and `@1005960` `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: P9's + 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 @@ -295,7 +295,7 @@ Read at `main.js@1002292` (`toolWindowElementAddChild`) and `@1005960` 1. **The external browser.** `ShellExecuteW` from the add-in DLL. Certain to work; it leaves the IDE. The IDE itself opens links with `hostAppObject.Shell('cmd.exe /c start "link" "'+url+'"',1)` *(reported)* --- a host object that only page script can reach. -2. **An `iframe` in a tool window --- it works (P3, BETA 983).** Nothing refuses the tag. No +2. **An `iframe` in a tool window --- it works (P3, BETA 983 and 995).** Nothing refuses the tag. No file under `ide\` sets a Content-Security-Policy --- there is no `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, @@ -341,7 +341,7 @@ Read at `main.js@1002292` (`toolWindowElementAddChild`) and `@1005960` **Offline** is harder. `_site-offline/` works over `file://`, and a page served from `http://localhost` cannot frame a `file://` URL. The options are `srcdoc` with rewritten links, or files under the IDE's `ide\` folder. **The IDE serves any file placed there (P13, -BETA 983)**, measured by [test/addin/ideserver.test.mjs](test/addin/ideserver.test.mjs) +BETA 983 and 995)**, measured by [test/addin/ideserver.test.mjs](test/addin/ideserver.test.mjs) with files put in a lane's copy of the install: - The server is the page server, `bin\twinBASIC_win32.exe --ide=`, not the compiler: @@ -372,7 +372,7 @@ have: writing into the install, which every new build replaces. Still deferred. 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 - on BETA 983 by [test/addin/symbols.test.mjs](test/addin/symbols.test.mjs), which puts each + on BETA 983 and 995 by [test/addin/symbols.test.mjs](test/addin/symbols.test.mjs), which puts each question the way the IDE's own code does. Hover (`textDocument/hover`, `main.js@842393`) returns markdown: for a procedure, its declaration, then a heading naming where it is declared, then its `[Description]` text, which for a VBA function is several paragraphs: @@ -391,7 +391,7 @@ have: writing into the install, which every new build replaces. Still deferred. says `*class* **Collection** ... in package VBA` and lists `*[default]* VBA._Collection`. A variable gives its declaration, `*local variable* Dim c As Collection`. `Debug`, `Debug.Print` and a statement such as `Dim` give nothing, and a type such as `Long` a line - about it. BETA 983 gives no hover for those three, and BETA 987 a hover whose text is + about it. BETA 983 gives no hover for those three, and BETA 987 and 995 a hover whose text is empty, so the add-in must treat both as nothing. Over a procedure's name in its own declaration, hover gives a debug block instead, `TB-DEBUG CODEGEN SIZE: [NOT-READY]`; over a `ByVal` parameter of a class, `String`, `Variant` or `Object` it adds a wrong note about `Option Explicit` @@ -449,7 +449,7 @@ have: writing into the install, which every new build replaces. Still deferred. 1](#stage-1-testing-add-ins-by-machine) fixes this for every harness tool, not only for add-in tests. - **The `.twinproj` association** is `HKCU\Software\Classes\.twinproj` → - `twinBASIC.ProjectFile`, and it currently points at the BETA 983 `twinBASIC.exe`. A + `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 @@ -703,20 +703,20 @@ WebView2 preferring light. | # | 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: 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: 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: 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: 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:** 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: 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: 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 | -| P8 | Is a loaded add-in DLL locked against being overwritten? **Answered, BETA 983: 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: 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 | +| 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 | +| 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 | +| 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: 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, and a compiler started by the restart button returned it too; left out, both said it was 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: no.** A compile, a compiler crash and a `tbrun` build-and-run left all 233 files byte-identical, mtimes included. | hardlinks or copies --- copies, for safety, at 380 ms | -| P12 | Does `raiseEvent` from plain tool-window HTML throw? **Answered, BETA 983: 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: yes**, and it is the page server, `twinBASIC_win32.exe --ide=`, not the compiler. Any file, byte for byte, below the page's passkey 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: 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 | +| 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 passkey 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 | ### Stage 3: the symbol index, generated by the docs build diff --git a/WIP.Typography.md b/WIP.Typography.md index 038502a7..c50a550f 100644 --- a/WIP.Typography.md +++ b/WIP.Typography.md @@ -65,8 +65,9 @@ once, and any of the three can move. the later `@font-face` fetch is not reused, so the file downloads twice. - [docs/assets/css/print.css](docs/assets/css/print.css) --- the book's own `@font-face` block and three stacks. -- [builder/pdf.mjs](builder/pdf.mjs) --- `REQUIRED_FONTS`, copied into the - sparse `_site-pdf/` tree. Keep it in step with print.css's `@font-face` block. +- [builder/pdf.mjs](builder/pdf.mjs) --- copies every face print.css names + as `url("../fonts/...")` into the sparse `_site-pdf/` tree, reading the + list from print.css itself. Two rules that are easy to get wrong: diff --git a/WIP.md b/WIP.md index ac02e83f..9b896209 100644 --- a/WIP.md +++ b/WIP.md @@ -164,6 +164,12 @@ addin-test.bat --only sample15 # one lane; --port N moves the lanes' ports - **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. +**Testing the IDE itself** (the debugger, Export Project, the Packages dialog) is +`ide-test.bat`, the same runner (`scripts/lib/lane-runner.mjs`) over `test/ide/lanes.mjs`, +with base port 9660 against the add-in runner's 9560. Every rule above applies to it +unchanged: the private `APPDATA`, `TB_ADDIN_TEST=1`, and ending an IDE by its pid. Its +scenario files import `scenario` from `test/addin/scenario.mjs`. + ## Authoring a page **[WIP.Authoring.md](WIP.Authoring.md) is required reading before writing or @@ -459,6 +465,7 @@ Why the report separates the wedged task from the merely blocked ones, and why - `examples.bat` — compiles the documentation's own twinBASIC code samples, every `tb` fence marked `check_build`, and reports the ones the compiler refuses against the line in the page they came from. Needs a twinBASIC install and Windows, so it is outside every gate and outside CI; ~120 s over the 1,136 samples marked. Two modes need no compiler at all: `--census` classifies every fence and says how many classifiable ones are still unmarked, and `--report ` groups a saved `--propose --json` survey by diagnostic, section and unresolved name. `--propose` itself does compile. See [Compiling the reference's own code samples](#compiling-the-references-own-code-samples) and [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md). - `addin-test.bat` — tests IDE add-ins by operating an IDE: every lane in `test/addin/lanes.mjs` builds the add-ins it tests into a private copy of the install, opens a project and checks what the add-in does. Outside every gate and outside CI for the same reasons as `examples.bat`; ~140 s for the ten lanes today: Samples 10 and 15, and the eight probe lanes behind Stage 2's answers in [WIP.HelpAddin.md](WIP.HelpAddin.md). Exit 0 every lane passed and the registry is as it was found, 1 a lane failed, 2 the harness failed, 3 the registry or a work folder was not put back. See [Driving the twinBASIC compiler](#driving-the-twinbasic-compiler) for its rules. +- `ide-test.bat` --- the same runner for scenarios that operate the IDE itself rather than an add-in: every lane in `test/ide/lanes.mjs`, base port 9660. Same exit codes, same standing outside every gate and outside CI, same rules. Three generators sit outside that loop and produce committed artifacts rather than build output — none runs during a build, and none is needed for one. `python scripts/build_fonts.py` rebuilds the subset webfaces under `docs/assets/fonts/` and needs a network connection; `node scripts/build_dot_metrics.mjs` regenerates `builder/inter-metrics.json` from those webfaces and needs only a browser. See [Typography](#typography). `node scripts/build_package_api.mjs` regenerates `builder/package-api.json`, the packages' declared API that the build's symbol index (`tB/symbols.json`, for the IDE help add-in) is annotated from; it needs a twinBASIC install, so **run it when the reference is re-indexed against a newer build** and commit it with the pages. See [WIP.HelpAddin.md, Stage 3](WIP.HelpAddin.md#stage-3-the-symbol-index-generated-by-the-docs-build). diff --git a/WIP.tbIDE.md b/WIP.tbIDE.md index 14900422..29eeaa54 100644 --- a/WIP.tbIDE.md +++ b/WIP.tbIDE.md @@ -27,7 +27,7 @@ End Module The returned object must implement the [`AddIn`](#public-user-facing-surface) interface (a single read-only `Name` property, declared in `Addin.twin`). Every sample uses this exact `tbCreateCompilerAddin` skeleton — surface it on the index landing as the canonical entry point. -Two things about loading were measured on BETA 983 (P9 and P14 in [WIP.HelpAddin.md](WIP.HelpAddin.md)), and the index page says both: +Two things about loading were measured on BETA 983 and 995 (P9 and P14 in [WIP.HelpAddin.md](WIP.HelpAddin.md)), and the index page says both: - **The linker exports `tbCreateCompilerAddin` as `tbCreateCompilerAddin_v3`**, and under no other name. The IDE's loader accepts `tbCreateCompilerAddin`, `_v2` and `_v3`, calls each the same way, and refuses a DLL with none of them as *compiled for a newer version of the twinBASIC IDE*. The suffix is a version stamp, not a different signature. - **A compiler restart does not release the object; it kills it.** The addin lives in the compiler's process, and the page ends that process with `taskkill /F` (`forceTerminate` in `main.js`) on every compiler restart --- the restart button, a switch of build target, a crash --- so `Class_Terminate` does not run, and a new instance is loaded from the DLL on disk. Closing the project calls `forceTerminate` as well *(read, not measured)*. This note used to say the object is released "when the addin is disabled or the IDE shuts down"; the Add-Ins menu's items cannot disable one *(reported)*, and the index page repeated the claim until the P9 lane showed `Class_Terminate` not running. @@ -112,7 +112,7 @@ Three things make this surface unusual and have to be surfaced on the docs: 2. **The custom-element tags.** `HtmlElements.Add(id, tagName)` accepts standard HTML tags (`"div"`, `"input"`, `"span"`, `"h1"`, …) AND four IDE-specific widget tags: `"chartjs"` (Chart.js wrapper — surfaces a `.chart` property), `"monaco"` (the Monaco editor — surfaces a `.editor` property), `"listview"` (the IDE's listview widget — surfaces a `.listview` property with `addItem` / `removeItem` / etc.), and `"virtuallistview"` (the same with `setItemCount` + the `onAsyncGetItemHTML` event). All four are demonstrated in samples 11–14. Surface as *"the tag name is forwarded to the IDE's tool-window renderer, which understands the standard HTML tags plus the custom widget tags … see sample 11 / 12 / 13 / 14"*. 3. **`AddEventListener(DomEventName As String, CallbackFunc As LongPtr, Optional Data As Variant)`.** The callback is passed as `AddressOf SomeSub`, and `SomeSub` must have the signature `Sub(ByVal eventInfo As HtmlEventProperties)`. The `eventInfo` parameter is the IDE-marshalled equivalent of the JavaScript `Event` object — `eventInfo.key` / `eventInfo.target.value` / `eventInfo.target.id` are the usual fields, but again the property names are resolved against the *actual* event object at run time, not declared statically. Sample 13 also demonstrates **custom event names raised from inline HTML** via the IDE-side `raiseEvent("name", event, stopPropagation, …customData)` JavaScript helper; the custom-data values become `eventInfo.customData0`, `eventInfo.customData1`, … and are demonstrated in sample 15's `ClickedMatch` handler. Sample 14 demonstrates **async events** via `eventInfo.setAsyncResult("")` (the listener returns the requested HTML asynchronously back into the virtual list view's render cycle). -**`raiseEvent` is narrower than item 3 suggests** (P12 in [WIP.HelpAddin.md](WIP.HelpAddin.md), measured on BETA 983): it works only inside a `listview` / `virtuallistview` or an `AddMonacoWidget` widget, and from plain tool-window HTML it throws a `TypeError` in the IDE's page. P4 measured the rest of this surface: `innerHTML`'s inline handlers run as the IDE page's own script, and a property whose name starts with `on` is dropped without an error. The HtmlElement and HtmlElementProperties pages say all three; the ToolWindow page says that showing a window resets its root's `display`. +**`raiseEvent` is narrower than item 3 suggests** (P12 in [WIP.HelpAddin.md](WIP.HelpAddin.md), measured on BETA 983, the `TypeError` on 995 as well): it works only inside a `listview` / `virtuallistview` or an `AddMonacoWidget` widget, and from plain tool-window HTML it throws a `TypeError` in the IDE's page. P4 measured the rest of this surface: `innerHTML`'s inline handlers run as the IDE page's own script, and a property whose name starts with `on` is dropped without an error. The HtmlElement and HtmlElementProperties pages say all three; the ToolWindow page says that showing a window resets its root's `display`. Document the four `Html*` classes as the *contract* (`Item` default member, the `Value` accessor, the `Properties` chaining) and use the samples to illustrate the dynamic-resolution mechanism. Do not try to enumerate the resolved property surface — it's open-ended. @@ -209,9 +209,9 @@ Surface as the canonical safe-cast pattern on `CodeEditor.md`. ## `KeyboardShortcuts.Add` callback signature -The `keyString` argument is a literal key with optional `{CTRL}` / `{SHIFT}` / `{ALT}` prefixes, e.g. `"{CTRL}{SHIFT}d"` (the source-side `[Description]`'s example). The `Callback` is `AddressOf` an addin function; the function takes no arguments and returns nothing --- a `Private Sub` with no parameters, in the add-in's class, works (measured, BETA 983). +The `keyString` argument is a literal key with optional `{CTRL}` / `{SHIFT}` / `{ALT}` prefixes, e.g. `"{CTRL}{SHIFT}d"` (the source-side `[Description]`'s example). The `Callback` is `AddressOf` an addin function; the function takes no arguments and returns nothing --- a `Private Sub` with no parameters, in the add-in's class, works (measured, BETA 983 and 995). -**The source's own example does not fire.** No sample uses `KeyboardShortcuts.Add`, so it was measured instead: P1 and P2 in [WIP.HelpAddin.md](WIP.HelpAddin.md), with the lane `test/addin/keys.test.mjs`. In BETA 983 a shortcut with `{CTRL}` or `{ALT}` never fires when pressed, the prefixes must come in the order `{ctrl}{shift}{alt}`, a shortcut fires on key-up wherever the focus is in the IDE's window, and a key that types fires each time it is typed. The published page says all of that, with an example on Shift+F12; keep it in step with the lane. +**The source's own example does not fire.** No sample uses `KeyboardShortcuts.Add`, so it was measured instead: P1 and P2 in [WIP.HelpAddin.md](WIP.HelpAddin.md), with the lane `test/addin/keys.test.mjs`. In BETA 983 and 995 a shortcut with `{CTRL}` or `{ALT}` never fires when pressed, the prefixes must come in the order `{ctrl}{shift}{alt}`, a shortcut fires on key-up wherever the focus is in the IDE's window, and a key that types fires each time it is typed. The published page says all of that, with an example on Shift+F12; keep it in step with the lane. **Do not describe callbacks as running "on the IDE's UI thread".** This file used to say to, and it is unmeasured and probably misleading: an add-in runs inside the compiler's process (P10), not in the IDE's page, which is a WebView2 process of its own. The KeyboardShortcuts page no longer says it. **The AddinTimer page still does**, three times, with "long-running work ... will block the UI thread" --- unverified; which thread runs an add-in's callbacks, and what a slow one holds up, is not known. Fix it when something measures it. diff --git a/biome.jsonc b/biome.jsonc index 899798cc..999d6075 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -51,7 +51,10 @@ "enabled": true, "rules": { "preset": "none", - "correctness": { "preset": "recommended" }, + // A name a module uses but never declares or imports. Off in the + // recommended set; a function moved to a module of its own without its + // imports passed every gate until the script ran. + "correctness": { "preset": "recommended", "noUndeclaredVariables": "error" }, "suspicious": { "preset": "recommended", // `while ((m = re.exec(s)))` is how this tree walks a regex's matches. @@ -63,6 +66,17 @@ } }, "overrides": [ + { + // Functions these hand to puppeteer's page.evaluate run in the page, + // after axe-core has been injected into it as the global `axe`. + "includes": ["scripts/lib/axe-scan.mjs", "scripts/check_axe_patch_equiv.mjs"], + "javascript": { "globals": ["axe"] } + }, + { + // Loaded into the book's page by pagedjs, which defines `Paged`. + "includes": ["book/lib/progress-handler.js"], + "javascript": { "globals": ["Paged"] } + }, { // ES5-style scripts that ship to readers as written. // A trailing comma after a call's last argument is ES2017. diff --git a/builder/PLAN-TOOLING-REVIEW.md b/builder/PLAN-TOOLING-REVIEW.md index be8454cf..b4412fa8 100644 --- a/builder/PLAN-TOOLING-REVIEW.md +++ b/builder/PLAN-TOOLING-REVIEW.md @@ -877,7 +877,7 @@ expectations, the usage texts and Tools.md together. stdout and exiting 0. Each declares `help` with `short: "h"` and `stopAt: ["help"]`, answers it straight after the parse, before any number, project or install check, and has a `USAGE` constant (the usage line, one sentence, the options, and the `Exit codes:` block). A new tool -is added to `HELP_TOOLS` in `scripts/check_cli.mjs`, which checks that its help exits 0 and +is added to `HELP_TOOLS` in `scripts/lib/cli-cases.mjs`, from which `scripts/check_cli.mjs` checks that its help exits 0 and leaves its scratch folder empty. ### C72 — `scripts, book, eval, wisdom: an unknown flag or a bad value exits 2` @@ -888,7 +888,7 @@ boolean given a value, a value option with none, an empty value unless the optio `empty: true` (only `tbdocs`' `--baseurl`), and a positional beyond the tool's count are each a `CliError`, reported on stderr with exit 2 through `withUsageError`. A term that starts with a dash goes after `--`. A new tool needs an unknown-flag case in `REFUSALS`, and an empty-value -case if it has a value option, in `scripts/check_cli.mjs`, which checks `REFUSALS` against +case if it has a value option, in `scripts/lib/cli-cases.mjs`, which checks `REFUSALS` against `HELP_TOOLS`. ### C72a — `scripts, book, eval, wisdom: a bad value exits 2` @@ -899,7 +899,7 @@ file, and refuses a bad one on stderr with exit 2, in its usage-error form. It d `lib/cli.mjs`'s `numberOption` (read as `Number()` reads, so `0x10` passes and `12abc` does not), `choiceOption`, `regexOption`, `urlOption`, `dateOption` and `refuseTogether`, and a new option that takes a number, regex, URL, date or fixed set needs a case in `BAD_VALUES` in -`scripts/check_cli.mjs`. +`scripts/lib/cli-cases.mjs`. ### C72b — `builder, scripts: tbdocs and check_links exit 0, 1 or 2 like every tool` diff --git a/builder/pdf.mjs b/builder/pdf.mjs index ed7dd3cf..98386b9b 100644 --- a/builder/pdf.mjs +++ b/builder/pdf.mjs @@ -25,18 +25,13 @@ import { WRITE_LIMIT, mkdirRec, runLimited, safeWrite, writeFileMkdirp } from ". const PDF_SUFFIX = "-pdf"; const REQUIRED_CSS = ["assets/css/print.css", "assets/css/tb-highlight.css"]; -// The six faces print.css declares, copied into the sparse tree so its +// The faces print.css declares, copied into the sparse tree so its // `url("../fonts/...")` resolves under the file:// URL render-book.mjs loads. -// This list has to stay in step with the @font-face block at the top of -// print.css. -const REQUIRED_FONTS = [ - "assets/fonts/source-serif-4-variable.woff2", - "assets/fonts/source-serif-4-variable-italic.woff2", - "assets/fonts/inter-variable.woff2", - "assets/fonts/inter-variable-italic.woff2", - "assets/fonts/cascadia-mono-variable.woff2", - "assets/fonts/cascadia-mono-variable-italic.woff2", -]; +// They are read from print.css itself, so a face added to its @font-face +// block is copied without a second list to keep in step. +const PRINT_CSS = "assets/css/print.css"; +const FONT_URL = /url\(\s*["']?\.\.\/fonts\/([^"')\s]+)["']?\s*\)/g; +const CSS_COMMENT = /\/\*[\s\S]*?\*\//g; const LIMIT = WRITE_LIMIT; // --------------------------------------------------------------------------- @@ -62,11 +57,12 @@ export async function writePdf( const staticByDestRel = new Map(staticFiles.map((s) => [s.destRel.replaceAll("\\", "/"), s])); const counters = { bookBytes: 0, html: 0, css: 0, fonts: 0, images: 0, missing: 0 }; const missingPaths = []; + const requiredFonts = await printCssFonts(staticByDestRel); await Promise.all([ writePdfBook(bookHtml, pdfRoot, counters), copyPdfCss(staticByDestRel, highlightCss, pdfRoot, counters), - copyPdfFonts(staticByDestRel, pdfRoot, counters), + copyPdfFonts(requiredFonts, staticByDestRel, pdfRoot, counters), copyPdfImages(imagePaths, staticByDestRel, pdfRoot, counters, missingPaths), ]); @@ -86,7 +82,7 @@ export async function writePdf( const missing = new Set(missingPaths); counters.checkBook = { html: bookHtml, - rels: ["book.html", ...REQUIRED_CSS, ...REQUIRED_FONTS, ...imagePaths.filter((r) => !missing.has(r))], + rels: ["book.html", ...REQUIRED_CSS, ...requiredFonts, ...imagePaths.filter((r) => !missing.has(r))], }; } return counters; @@ -161,6 +157,16 @@ async function copyPdfCss(staticByDestRel, highlightCss, pdfRoot, counters) { for (const w of warnings) console.warn(`pdf: ${w}`); } +// The tree-relative path of every face print.css points at, in its order. +// Comments are dropped first: print.css's own header names the url() form. +// A missing print.css gives none: copyPdfCss already warns about it. +async function printCssFonts(staticByDestRel) { + const sf = staticByDestRel.get(PRINT_CSS); + if (!sf) return []; + const css = (await fs.readFile(sf.srcPath, "utf8")).replace(CSS_COMMENT, ""); + return [...new Set(Array.from(css.matchAll(FONT_URL), (m) => `assets/fonts/${m[1]}`))]; +} + // Copy the webfaces print.css declares into /assets/fonts/. // // This throws where copyPdfCss warns and copyPdfImages collects, because a @@ -171,8 +177,8 @@ async function copyPdfCss(staticByDestRel, highlightCss, pdfRoot, counters) { // rejects a face whose fetch errored, but a declared face the layout never // exercises is never fetched at all and passes silently. Failing here names // the path instead. -async function copyPdfFonts(staticByDestRel, pdfRoot, counters) { - await runLimited(REQUIRED_FONTS, LIMIT, async (rel) => { +async function copyPdfFonts(requiredFonts, staticByDestRel, pdfRoot, counters) { + await runLimited(requiredFonts, LIMIT, async (rel) => { const sf = staticByDestRel.get(rel); if (!sf) { throw new Error( diff --git a/docs/Documentation/Builder.md b/docs/Documentation/Builder.md index 8726d3a8..cc584349 100644 --- a/docs/Documentation/Builder.md +++ b/docs/Documentation/Builder.md @@ -573,7 +573,7 @@ This follows on from [Project styling](#project-styling). The site uses three fa 1. **The font files** (all three faces). [`scripts/build_fonts.py`](Tools#build-fonts) downloads each face's pinned release, verifies its SHA-256, subsets it, and writes the `.woff2` files and their licences into `docs/assets/fonts/`. A new face is an entry in its `SOURCES` table --- the release URL, the hash and the licence --- and one entry per file in `FACES`: the file inside the archive, the pinned axes, the Unicode ranges and the output name. Keep the output `.woff2`, which is the only font format the [publish allowlist](Building#what-the-build-refuses-to-publish) accepts. 2. **The web stylesheet** (Inter and Cascadia Mono). `docs/_sass/custom/_fonts.scss` holds the `@font-face` rules, inside the `emit-font-faces` mixin, and the `$tb-body-font-family` and `$tb-mono-font-family` stacks, which `docs/assets/css/just-the-docs-combined.scss` passes into the theme. `docs/_sass/modules-dark.scss` passes them again for the dark compilation, and `docs/assets/css/just-the-docs-dark.scss` uses the mono stack once more for `pre`, `kbd` and `samp`. All of these read the two variables, so replacing a face inside an existing stack changes nothing in them. A new stack has to be passed in both compilations, or the dark theme keeps the system fonts --- the [specificity trap](#the-specificity-trap) again. 3. **The preloads** (the two roman web faces). `fontPreloads()` in `builder/template.mjs` preloads `inter-variable.woff2` and `cascadia-mono-variable.woff2` by name, from its `PRELOAD_FONTS` list. It sets `crossorigin`, which a font preload needs even from the same origin: without it the browser downloads the file twice. A preload of a file that does not exist is a broken link on every page, and the build's link check reports it. The link-check fixture under `test/fixtures/check-src/assets/fonts/` holds stub files under the same two names, so renaming either file means renaming its stub too, or [`check_links_diff.mjs`](Tools#check-links-diff) fails on every pull request. -4. **The book** (all three faces). `docs/assets/css/print.css` is the whole design of the PDF and loads nothing from the Sass build, so it has its own `@font-face` block and its own stacks. `REQUIRED_FONTS` in `builder/pdf.mjs` names the same six files and copies them into the sparse `_site-pdf/` tree. Keep the two in step: the PDF pass aborts on a listed file that does not exist, and a face `print.css` uses that was not copied fails to load, which aborts the book render. +4. **The book** (all three faces). `docs/assets/css/print.css` is the whole design of the PDF and loads nothing from the Sass build, so it has its own `@font-face` block and its own stacks. `builder/pdf.mjs` reads that block for every file named as `url("../fonts/...")` and copies each into the sparse `_site-pdf/` tree, so there is no second list to keep in step. The PDF pass aborts on a named file that does not exist. 5. **Diagram exports** (Inter and Cascadia Mono). `FONT_FILES` in `docs/assets/js/svg-inline.js` maps each family name to its weight range and file names, and the Download and Copy buttons embed those files in an exported SVG or PNG. A family it does not list is exported without its face and without a message; a listed file that no longer exists costs one console warning. 6. **The Gantt chart** (Inter). `builder/gantt.mjs` writes a `