diff --git a/.gitattributes b/.gitattributes index e118aa03..182d9a63 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,3 +16,8 @@ # Wisdom extract script is passed to it by path, so a CRLF checkout of it # cannot be run. wisdom/extract/workflow.mjs text eol=lf + +# A bug's reproducer is checked out byte for byte: its .twinproj is packed +# from its source tree, and the two must agree in every checkout. +bugs/** -text +*.twinproj binary diff --git a/.gitignore b/.gitignore index 6f3687b4..62cb1f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,7 @@ indexer/.packages/ # (~190 MB). The subset .woff2 it produces ARE committed, under # docs/assets/fonts/ -- only the archives are throwaway. /.font-cache/ + +# A bug reproducer's zip is only for attaching to its GitHub issue (see +# BUGS-TO-REPORT.md); the .twinproj beside it is committed. +/bugs/**/*.zip diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index ac0eb062..f179d743 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -1,9 +1,13 @@ # twinBASIC bugs to report A queue, not a record. Each entry is a **product** bug --- something twinBASIC itself does -wrong --- observed while working on this documentation. **Delete an entry once it has been -filed upstream**; nothing here is meant to accumulate, and an entry that stays after it is -filed turns this file into a second, worse issue tracker. +wrong --- observed while working on this documentation. **Mark an entry once it has been +filed upstream**, and `node scripts/bug_repro.mjs file --marked` moves it to +`bugs/filed//REPORT.md`; nothing here is meant to accumulate, and an entry that stays +after it is filed turns this file into a second, worse issue tracker. A filed bug is not a +fixed one, so its reproducer stays until a fix is released and `verify` says it no longer +reproduces: then the docs its HTML comment names are updated, and its folder under +`bugs/filed/` is deleted. Documentation defects do not belong here. They are fixed in `docs/`, and the ones that are not yet fixed are recorded in the relevant `WIP.*.md`. @@ -17,1387 +21,806 @@ What an entry owes a reader: actionable; - how it was **observed**, so somebody else can see the same thing. ---- +## The report template -## The recent-projects list fills its empty slots with copies of its last entry +**Each entry is the report**, written to twinBASIC's GitHub bug-report template, so it is +copied into a new issue as it stands: the entry's `##` title is the issue's title, and +everything under it, down to the next `---`, is the issue's body. The template: -**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. +``` +**Describe the bug** +A clear and concise description of what the bug is. -The list is 21 values, `"0"` to `"20"`, under -`HKCU\Software\VB and VBA Program Settings\twinBASIC_IDE\RecentlyOpened`. Reproduction: +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error -1. Leave two entries: `"0"` = `A.twinproj`, `"1"` = `B.twinproj`. Either delete `"2"` to - `"20"` or set them to empty strings; both reproduce it. -2. Open a third project, `C.twinproj` --- on the command line is enough. -3. The IDE writes `"0"` = `C`, `"1"` = `A`, and `"2"` to `"20"` = `B`: **nineteen copies of - `B`**. +**Expected behavior** +A clear and concise description of what you expected to happen. -What does **not** reproduce it: +**Screenshots** +If applicable, add screenshots to help explain your problem. -| starting list | result | -|---|---| -| no `RecentlyOpened` key at all | `C` and 20 empty strings --- correct | -| 21 distinct entries | 21 distinct entries, the oldest dropped --- correct, read at the end of 41 successive opens | -| `C` already at the top | unchanged --- the opened project itself is never duplicated | - -So it takes at least one existing entry and at least one slot with nothing in it, which -looks like each slot being read with the previous slot's value as its default. Once a -duplicate is there, the next opened project keeps it: 18 copies of one project became 20 -after one more open, with the new project on top. - -**Observed** on 2026-09-23 by reading the registry values after `tbbuild --keep` opened a -fixture project on a private desktop and the IDE was killed: once for each variant of step -1, and in two successive sessions on one project for the third row and the growth from 18 -copies to 20. The first row was seen when the key had been deleted and the next harness run -recreated it, the second at the end of a `check_examples` run. The -harness records it because its own runs trip it: every IDE a run starts opens a project, and -a run that began on a list holding one entry ended with seventeen copies of it. The registry -tidy (`scripts/lib/tb-registry.mjs`) now puts the list back as it found it, without the -copies; a list that was short to begin with is left short, and the next project the user -opens then trips this. +**Desktop:** + - OS: [e.g. Windows 10] + - twinBASIC compiler version [e.g. 0.9.1111] ---- +**Additional context** +Add any other context about the problem here. +``` -## Compiler crashes on an `Interface` named by an angle-bracket placeholder that has an `Extends` clause +How the four duties above fit it: -**Build:** BETA 995; BETA 983 at `twinBASIC_win32.dll+00141F7A` -**Severity:** crash --- takes the compiler down, three restarts, then the IDE gives up. +- **Describe the bug** says what goes wrong, in a sentence or two, and how it was + observed. +- **To Reproduce** starts by opening the entry's reproducer project (see below), then gives + the narrowed steps. +- **Expected behavior** says what should happen instead, and why: what VBA or VB6 does, + what the documentation says, or what the IDE does in the neighbouring case. +- **Screenshots** is left out when there are none. +- **Desktop** gives the build as `BETA `, the build it was last reproduced on. +- **Additional context** gives what did not reproduce it, the severity, and the builds it + was checked on besides the one above. -This two-line file is the whole reproduction: +**An entry's paragraphs and list items are not wrapped**, one line each, unlike the rest of +this file. A GitHub issue renders a single newline as a line break, so a wrapped paragraph +pastes with a break in mid-sentence. Entries are separated by a `---` line. -``` -Interface Extends -End Interface -``` +Anything meant for this repository alone (which test asserts the behaviour, which page +states it, what to update when it is fixed) goes in an HTML comment at the end of the entry. +GitHub does not render a comment, so it is harmless if pasted along with the rest. -The IDE's DEBUG CONSOLE reports `NATIVE EXCEPTION: ACCESS_VIOLATION {no-basic-code}` with -`>>> thread 0004: ParsingFileStart, `, then `restarting from MEMORY`, three -times over. +**Entry titles do not change.** Comments in `scripts/` and `test/` refer to entries by +title. -**It takes a placeholder name and an `Extends` clause**, and what the clause names does not -matter: +## Reproducer projects -| source | result | -|---|---| -| `Interface ` + `End Interface` | TB5182 Syntax error, no crash | -| `Interface IFoo Extends ` + `End Interface` | TB5182 + TB5079 + TB5127, no crash | -| `Interface Extends ` + `End Interface` | **crash** | -| `Interface Extends IBase` + `End Interface`, no `IBase` anywhere | **crash** | -| the same, with `Interface IBase` or `Class IBase` declared in another file | **crash** | - -This entry used to say that it takes a placeholder in *both* positions; the last two rows, -measured on 2026-09-24 with a project of its own each, say otherwise. The input is not real -code --- it is a syntax skeleton, the shape `docs/Reference/Attributes.md` uses to show -where an attribute goes --- but a parser meeting nonsense should diagnose it, and this one -dereferences something instead. - -**Found by** pointing `scripts/check_examples.mjs` at the documentation's own code samples; -the skeleton is one of the 1,124 `tb` fences under `docs/`. A crash in a batch of samples -costs the whole batch its result, which is why that tool isolates the sample on exit code 4. +**Every entry has a reproducer project** under `bugs//`, where `` is a short +kebab-case name for the bug, and its **To Reproduce** names the project file: ---- +| path | what it is | in git | +|---|---|---| +| `bugs//src/` | the project's exported source tree: `Settings`, `Sources/` and the rest | yes, byte for byte | +| `bugs//.twinproj` | the project file, packed from `src/` | yes | +| `bugs//.zip` | the `.twinproj` zipped, because a GitHub issue does not accept a `.twinproj` attachment, with any file `repro.json`'s `attach` names, such as a `.twinpack` | no | -## An `Interface` that extends itself compiles without a diagnostic +`scripts/bug_repro.mjs` makes and checks them (the tool's page is +[Tools and Scripts](docs/Documentation/Tools.md#bug-repro)): -**Build:** BETA 995 -**Severity:** invalid code accepted --- the same cycle through a class is refused. +```sh +node scripts/bug_repro.mjs new "" # bugs//src/ and repro.json +node scripts/bug_repro.mjs pack # src/ -> .twinproj -> .zip +node scripts/bug_repro.mjs compile # compile it in the IDE, print the diagnostics +node scripts/bug_repro.mjs build # and build it +node scripts/bug_repro.mjs run # run Sub Main, print the DEBUG CONSOLE +node scripts/bug_repro.mjs verify [ ...] # does each entry still reproduce? +node scripts/bug_repro.mjs file # move a filed entry out of the queue +node scripts/bug_repro.mjs file --marked # the same for every marked entry +``` -This two-line file compiles with no error, warning, hint or info: +`new` starts the project from the console template, with an empty `Sub Main` in a `Startup` +module, or with `--template ` from a folder of `test/repro-templates/`, such as +`webview2-form`, a form holding one WebView2 control; edit `src/Sources/`, then `pack`. What `pack` does is the importer, then a zip of the +file it writes and of the files `attach` names, which the tool does itself: -``` -Interface IA Extends IA -End Interface +```sh +node scripts/impexp.mjs import bugs//.twinproj bugs//src --overwrite ``` -A cycle through two interfaces is accepted the same way, in one file or split across two: -`Interface IA Extends IB` and `Interface IB Extends IA`. The other kinds of cycle are -diagnosed: +`bugs//repro.json` says how to ask the compiler about the entry, and is committed with +the reproducer: a `mode` of `compile`, `build`, `run`, `cli` (the compiler executable's own +command line) or `manual`, and what a reproduction looks like in `expect`, such as the exit +code of `tbbuild`, the diagnostic codes, or a regular expression the output must match. +`verify` reads it for every entry and reports `reproduces`, `NO LONGER REPRODUCES` (the bug +may be fixed on this build) or `manual`, which prints the `steps` it holds. It needs a +twinBASIC install, and is run by a person, never by a gate or by CI. -| source | result | -|---|---| -| `Class CA` + `Inherits CA` + `End Class` | TB5127 circular reference | -| `Class CA` inheriting `CB` and `Class CB` inheriting `CA`, two files | TB5127 circular reference, TB5022 failed to import inherited members | -| `Type TA` holding a `TB` and `Type TB` holding a `TA`, two modules | TB5101 unable to finalize User Defined Type, possible circular reference | +Attach the `.zip` to the issue. When the entry is filed, its folder moves to +`bugs/filed//`; see [Filed bugs](#filed-bugs). -So a cycle is checked for classes and UDTs, and not for interfaces. What happens when such a -project is built --- its type library has to describe the cycle --- was not tried. +## Filed bugs -**Observed** on 2026-09-24 with `tbbuild`, a project of its own for each source: exit 0 and -`0 error(s), 0 warning(s), 0 hint(s), 0 info` for the three interface cases, and the -diagnostics above for the rest. **Found by** looking for a compiler crash that needs two -files, to test `check_examples`' handling of one: a cycle between two files was the likeliest -candidate, and the interface cycle compiled instead of crashing. +The owner marks an entry while filing it, with a line directly under its `##` title: ---- +- `*FILED #2453*` --- filed as issue 2453; +- `*CAPTURED IN EXISTING #841*` or `*CAPTURED IN \#841*` --- an existing issue covers it. -## `--buildAndExit32` writes nothing, exits 0 on a project with errors, and hangs on a failing build +Issues are at `https://github.com/twinbasic/twinbasic/issues/`. `file --marked` takes the +issue from each mark and the reproducer from the `.twinproj` the entry names (an entry +whose reproducer is not an attachment names `bugs//` in its closing comment). It moves +the entry out of this file, with one `---` still between its neighbours, into +`bugs/filed//REPORT.md`, whose first line links the issue and which holds no mark line. +It moves `bugs//` beside it and records `issue` in its `repro.json`, and `existing` when +an existing issue covered the bug. If any marked entry cannot be filed, none is. +`file [--existing]` does the same for one entry without a mark. -**Build:** BETA 995; the silence on stdout and stderr was measured on BETA 983 -**Severity:** makes the documented unattended-build switch unusable. +`verify` checks the filed reproducers as well as the queued ones, labels each filed one with +its issue, such as `(filed #2453)`, and counts them on a line of their own. For a filed bug, +`NO LONGER REPRODUCES` is the signal that it was fixed. -The IDE executable accepts `--buildAndExit32` and `--buildAndExit64`; `parseCommandLine()` -reads them, and Personal Edition is refused by name, so they are real. Measured three ways: +An entry marked `*DEFERRED until after v1*` is held on purpose, and is not forgotten. It stays +in this file with its reproducer, `verify` still checks it, and `file --marked` skips it. It is +filed when the owner decides. -- **nothing is written to stdout or stderr, ever** --- no diagnostics, no summary; -- it **exits 0 on a project the IDE flags with errors**; -- when the build genuinely fails it **does not exit at all**, sitting on a *"Please wait…"* - dialog at 100% indefinitely. +--- -Silent, falsely green, and hanging on the one case worth catching. This is why -`scripts/tbbuild.mjs` drives the IDE's WebView over CDP instead --- see -[WIP.Harness.md](WIP.Harness.md#compiling-a-twinbasic-project-without-the-ide-in-front-of-you), which -records the same measurements. +## The recent-projects list fills its empty slots with copies of its last entry ---- +*DEFERRED until after v1* -## Public members are typed with Private components, so a default project cannot use them +**Describe the bug** +When the recent-projects list holds fewer entries than it has slots and a project is opened, the IDE writes a copy of the list's last entry into every empty slot, so the same project is repeated down the Recent tab. The list is 21 values, `"0"` to `"20"`, under `HKCU\Software\VB and VBA Program Settings\twinBASIC_IDE\RecentlyOpened`. A new installation has empty slots, so this is where it shows first. -**Build:** BETA 995 -**Severity:** documented APIs need the consumer to expose the package's internals; one -event asks for a type it then refuses. +**To Reproduce** +Steps to reproduce the behavior: +1. Unzip `recent-projects-copies.zip` (it holds `recent-projects-copies.twinproj`, an ordinary project; any three projects will do) and copy the project to `A.twinproj`, `B.twinproj` and `C.twinproj` in one folder. +2. Export the key with `reg export` first if the list matters to you. Under `RecentlyOpened`, set `"0"` to the full path of `A.twinproj` and `"1"` to that of `B.twinproj`. Delete `"2"` to `"20"`, or set them to empty strings; both reproduce it. +3. Open `C.twinproj`: `twinBASIC.exe C:\path\C.twinproj` is enough. +4. Read the values of the key, with the IDE still running or after it has closed. +5. See `"0"` = `C`, `"1"` = `A`, and `"2"` to `"20"` all = `B`: nineteen copies of `B`. -`VB.Report` shows it most sharply, because the compiler names the type in one diagnostic -and rejects it in the next. In a project referencing the VB package the ordinary way: +**Expected behavior** +The slots that held nothing stay empty, as they do when the key does not exist at all: `"0"` = `C`, `"1"` = `A`, `"2"` = `B`, and the rest empty. The Recent tab then shows three projects. -``` -Class ProbeReport - Private WithEvents rpt As VB.Report - Private Sub rpt_BeforePaintSection(ByVal Section As VB.ControlsSection) - End Sub -End Class -``` +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -``` -TB5018 unable to match this handler to its event member. The expected signature was: - Private Sub rpt_BeforePaintSection(ByVal Section As ControlsSection) -TB5079 Unrecognized datatype symbol 'ControlsSection' -``` +**Additional context** +Severity: cosmetic, but it shows on a new installation, which is exactly when the list has empty slots. -**The remedy is the asterisk**, and it works: with the library symbol set to `*VB`, the -handler above compiles exactly as written. So this is not an unreachable type --- it is a -*public* event whose parameter type is `Private Class ControlsSection` -(`VB/Sources/SUPPORT/ControlsSection.twin:4`), which means handling a documented event -requires opting into the package's private half. The other spellings are worse than useless: -`ByVal Section As Object` binds and compiles, while `Variant`, the bare name, and omitting -the parameter all fail. - -**The CustomControls package has the same shape across its whole style surface.** Every -class in `CustomControlsPackage/Sources/zTemporarySupport.twin` is `Private` --- `Corner`, -`Corners`, `Padding`, `FillColorPoint`, `FillColorPoints`, `Border`, `Borders`, `Line`, -`Fill`, `TextRendering`, `Anchors`, `WindowsFormOptions` and the per-control `…State` -classes --- while the members that hand them out are public: - -| member | declared | what a default project can do | -|---|---|---| -| `Borders.Elements` | `Public WithEvents Elements() As Border` | read it; assigning needs a `Border()` it cannot declare | -| `FillColorPoints.Values` | `Public WithEvents Values() As FillColorPoint` | the same | -| `TextRendering.Outlines` | an array of `Border` | the same, and with no `SetSimpleBorder` equivalent to fall back on | -| `Canvas.RuntimeUICCCanvasAddElement` | `(ByVal Me As Canvas, ByRef ElementDescriptor As Any)` | pass a record it declares itself, with the style members left `Nothing` | - -With `*CustomControlsPackage`, all of it works under the package qualifier: -`New CustomControlsPackage.Border`, `Dim elems(0 To 2) As CustomControlsPackage.Border`, -`Dim descriptor As CustomControlsPackage.ElementDescriptor`, and a complete -`ICustomControl` implementation that sets `BackgroundFill` and `TextRenderingOptions` from -`New CustomControlsPackage.Fill` / `.TextRendering`. Both `CustomControlsPackage.ElementDescriptor` -and `CustomControlsPackage.UDTs.ElementDescriptor` resolve, although the UDT is declared -`Public Type` inside `Private Class UDTs`. - -**Why this reads as an oversight rather than a policy.** The file is called -`zTemporarySupport.twin`, and the package's own changelog dates the narrowing: -*"v0.0.5.0, 15th September 2022 --- improved: made changes to ensure nothing within the -package is being exposed unnecessarily."* Reducing the surface is reasonable; what it missed -is that these particular classes are not internal --- they appear in the signatures of -members that stayed public, so every consumer of those members is now required to import the -package with an asterisk and qualify names that the package's own controls write bare. - -**Two more packages have the same shape, found the same way.** In both, a `Public Enum` sits -inside a `Private Module`, so a consumer can name neither the enum nor its members --- and -both enums are the argument type of a documented, public member: - -| package | declaration | what a default project cannot write | -|---|---|---| -| WinNativeCommonCtls | `Private Module ImageListConsts` → `Public Enum ImlDrawConstants` | `ImageList1.ListImages(1).Draw hDC, x, y, ImlDrawTransparent Or ImlDrawFocus` | -| cefPackage | `Private Module _cef_log_severity_t` → `Enum CefLogSeverity` | `CefBrowser1.EnvironmentOptions.LogSeverity = CefLogWarning` | - -Neither the bare member (`ImlDrawTransparent`), the enum name (`ImlDrawConstants.…`), nor the -owning control as a qualifier (`ImageList.ImlDrawConstants.…`) resolves --- all three are -`TB5079 Unrecognized symbol`. **The asterisk works**, as it does for the VB and -CustomControls cases: with the library symbol set to `*WinNativeCommonCtls`, -`WinNativeCommonCtls.ImlDrawTransparent` compiles, and the documentation now shows that form -with a note. That is a workaround, not a fix --- a documented, user-facing enum should not -require a project to expose the package's private half. **The VB package has it too**, from a different direction: the -enums nested inside a control class, `MultiFrameDirectionConstants` in `MultiFrame` and -`QRCodegenEccConstants` in `QRCode`, are equally unreachable, so -`mfPanels.Direction = vbDirectionHorizontal` does not compile although it is what the -property's own documentation says to write. - -**Why it went unnoticed:** none of the 32 sample projects the IDE ships exercises any of -this from code. Exported and grepped, all of them: `ControlsSection` appears only as a -designer `_className` in the two `.tbreport` files and no sample handles -`BeforePaintSection`; `ElementDescriptor`, `ICustomControl`, `ICustomForm` and -`RuntimeUICC*` appear nowhere at all; `Border` appears 971 times, every one a designer -`_className` in a `.tbform`. No sample line sets `NormalState`, `HoverState` or any -`.Fill.` from code. - -**Found by** `scripts/check_examples.mjs`, which compiles the documentation's own code -samples. The pages documenting `New Border`, `Dim descriptor As ElementDescriptor` and a -`ControlsSection` handler were all written from the packages' sources, and none of them -compiled in a default project. +What does not reproduce it: + +| starting list | result | +|---|---| +| no `RecentlyOpened` key at all | `C` and 20 empty strings, which is correct | +| 21 distinct entries | 21 distinct entries, the oldest dropped, which is correct; read at the end of 41 successive opens | +| `C` already at the top | unchanged: the opened project itself is never duplicated | + +So it takes at least one existing entry and at least one slot with nothing in it, which looks like each slot being read with the previous slot's value as its default. Once a duplicate is there, the next opened project keeps it: 18 copies of one project became 20 after one more open, with the new project on top. Both variants of step 2 (deleted slots, empty strings) were measured on BETA 995, ending with 21 values and 3 distinct projects. + + --- -## `Err.Raise` rejects `HelpContext` as a named argument, while its three siblings work +## `--buildAndExit32` writes nothing, exits 0 on a project with errors, and hangs on a failing build -**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. +*DEFERRED until after v1* -``` -Dim myHelpFile As String, myHelpContext As Long -Err.Raise vbObjectError + 894, Source:="MyApp.MyClass", _ - Description:="Was not able to complete your task", _ - HelpFile:=myHelpFile, HelpContext:=myHelpContext -``` +**Describe the bug** +`twinBASIC.exe --buildAndExit32 ` (and `--buildAndExit64`) cannot be used for an unattended build. It writes nothing about the build to stdout or stderr, it exits 0 on a project the IDE flags with an error when the error is in code nothing calls, and when the build really fails it does not exit at all. Measured on a private desktop with the process's standard output and error redirected to a file, and the process ended by its pid after 30 seconds. -``` -TB5090 unrecognized named argument -``` +**To Reproduce** +Steps to reproduce the behavior: +1. Open `build-and-exit-silent.twinproj` (attached as `build-and-exit-silent.zip`). Its only source file is `Sources\Startup.twin`: + ``` + Module Startup + + Public Sub Main() + End Sub + + Private Sub Unused() + NoSuchProcedure + End Sub + + End Module + ``` +2. See the IDE report 1 error, TB5079 `Unrecognized symbol 'NoSuchProcedure'`. +3. Close the IDE and run `twinBASIC.exe --buildAndExit32 C:\path\build-and-exit-silent.twinproj`. +4. See the command exit 0 after a few seconds. `Build\BuildAndExitSilent_win32.exe` has been written, although the project has an error. Nothing about the build was written to stdout or stderr: with both redirected to a file, the only line is `[...:ERROR:ui\gfx\win\window_impl.cc:172] Failed to unregister class Chrome_WidgetWin_0. Error = 1412`, which is Chromium shutting down. +5. Change `Main` to call `Unused`, so that the build really fails, and run the same command again. +6. See the command never exit. It writes nothing to stdout or stderr and no `.exe`, and was still running when it was ended after 30 and 40 seconds. On the desktop it sat on a *Please wait...* dialog at 100%, as seen on BETA 983. -**What does work**, each verified on its own: the same call with `Source:=`, -`Description:=` and `HelpFile:=` named and the fifth argument dropped compiles, and so does -the fully positional form `Err.Raise vbObjectError + 894, myObjectID, "...", myHelpFile, -myHelpContext`. So the parameter exists and accepts a **Long**; only its *name* is -unrecognised. `HelpContextID:=` is rejected as well, so this is not simply a different -spelling to discover --- and the compiler binary's only `HelpContextID` strings belong to -project settings, not to a signature. +**Expected behavior** +A build switch for unattended use should report the diagnostics on stdout or stderr, exit with a non-zero code when the project has errors, and exit when the build fails instead of waiting. At the least it should not exit 0 and write an `.exe` for a project the IDE flags with an error. -**Why it matters:** `HelpContext` is what VBA itself names that parameter. Read out of the -VBA type library on the machine this was found on --- `VBE7.DLL` 7.01.1158, VBA7.1, via -`LoadTypeLibEx` and `ITypeInfo::GetNames` on `_ErrObject`: +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -``` -Raise(Number, Source, Description, HelpFile, HelpContext) -``` +**Additional context** +Severity: it makes the switch unusable for the one job it exists for, because it is silent, falsely green and hangs on the case worth catching. + +Which errors hang and which exit 0, on BETA 995. All of these hang, with no `.exe` written: `NoSuchProcedure` in `Main`, `NoSuchProcedure` in a procedure that `Main` calls, a bare `Dim`, `Dim x As NoSuchType`, `y = 1` under `Option Explicit`, and a project with no `Sub Main`. The same `NoSuchProcedure` in a procedure nothing calls exits 0 with the `.exe` written. Assigning `"abc"` to a `Long` and an unused `Dim x As Long` compile without an error and exit 0, as they should. On BETA 983 the unused-procedure case exits 0, and `NoSuchProcedure` in `Main` and the missing `Sub Main` hang. -All five names are exactly the ones the failing call uses, and the call is Microsoft's own -`Err.Source` example, named arguments and all --- so the code twinBASIC rejects is the code -a VBA developer is most likely to have copied. Four of the five names are accepted here; -only the fifth is not. +So exit 0 on a project with errors happens only for an error in code nothing calls; an error in code the build reaches never exits 0. -**Found by** `scripts/check_examples.mjs` over `Reference/Default/VBA/ErrObject/Source.md`, -whose sample was written in the named form. The page uses the positional form now. +Silence on stdout and stderr: measured on BETA 983 first, and on BETA 995 by redirecting the standard handles of the process to a file. The same redirection captures the compiler executable's own output (`twinBASIC_win32.exe settings ` wrote the whole `Settings` file), so the capture works. + + --- ## An interface member marked `[PreserveSig]` cannot be implemented by a class -**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. +*DEFERRED until after v1* -``` -[InterfaceId("11111111-0000-4000-8000-000000000004")] -Interface IProbeD Extends IUnknown - [PreserveSig] - Function F() As Long -End Interface - -Class ImplD - Implements IProbeD - Private Function IProbeD_F() As Long Implements IProbeD.F - End Function -End Class -``` +**Describe the bug** +A class cannot implement an interface member that is marked `[PreserveSig]`. The compiler reports that it cannot match the implementation to the member, and prints as the expected signature exactly the one on the line the error is reported against, so whatever it compares is not what it prints. -``` -TB5004 unable to match this implementation to its interface member. The expected signature was: Private Function IProbeD_F() As Long -TB65535 Missing implementation of member Function F() As Long -``` +**To Reproduce** +Steps to reproduce the behavior: +1. Open `preservesig-implements.twinproj` (attached as `preservesig-implements.zip`). Its one source file, `Probe.twin`, holds the whole bug: + ``` + [InterfaceId("11111111-0000-4000-8000-000000000004")] + Interface IProbeD Extends IUnknown + [PreserveSig] + Function F() As Long + End Interface + + Class ImplD + Implements IProbeD + Private Function IProbeD_F() As Long Implements IProbeD.F + End Function + End Class + ``` +2. See two errors: + ``` + TB5004 unable to match this implementation to its interface member. The expected signature was: Private Function IProbeD_F() As Long + TB65535 Missing implementation of member Function F() As Long + ``` -BETA 983 gave the second message as TB5000. +What was tried and does not help, each on its own: +- the VB6-style name without the method-level `Implements IProbeD.F` clause gives `TB5018 unable to match this handler to its event member`, an event-handler message for an interface member, with the same expected signature; +- `[PreserveSig]` on the implementing method as well gives `TB5155 This attribute is not supported in this context`. -**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. +What does not matter: a parameter, or a `Boolean` return (the case it was found in was `Function MyFunc(Arg1 As Variant) As Boolean`), or a `[TypeHint(...)]` on that parameter, which implements cleanly without `[PreserveSig]`. The same interface with the attribute removed implements cleanly. `[PreserveSig]` alone is the trigger. -**What was tried and does not help**, each on its own: +**Expected behavior** +The implementation is accepted, as it is without the attribute. If `[PreserveSig]` members are not meant to be implemented in twinBASIC, a diagnostic saying so at the declaration. -- the VB6-style name without the method-level `Implements IProbeD.F` clause --- `TB5018 - unable to match this handler to its event member`, an *event-handler* message for an - interface member, with the same expected signature; -- `[PreserveSig]` on the implementing method as well --- `TB5155 This attribute is not - supported in this context`. +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -**What does not matter:** a parameter, or a **Boolean** return --- the case it was found in -was `Function MyFunc(Arg1 As Variant) As Boolean` --- or a `[TypeHint(...)]` on that -parameter, which implements cleanly without `[PreserveSig]`. The same interface with the -attribute removed implements cleanly. `[PreserveSig]` alone is the trigger. +**Additional context** +BETA 983 gives the second message as TB5000 instead of TB65535 (the reproducer, checked there, does not report TB65535). Severity: an interface the language lets you declare cannot be implemented at all, and the diagnostic asks for the signature that is already written. -**Found by** `scripts/check_examples.mjs` over `Reference/Core/Interface.md`, whose example -declared `IFoo` with a `[PreserveSig]` member and then showed a class implementing it. The -implementation had never compiled. The page's example no longer puts `[PreserveSig]` on a -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 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`. - -`import` is the compiler executable's verb for packing a folder tree into a project file. -Given a tree whose top-level `Packages` folder contains a folder, it ends partway through: - -``` -twinBASIC_win32.exe import out.twinproj tree\ --overwrite -``` +## `export` and `import` stop at the 260-character path limit, apart from the one path they prefix with `\\?\` -- the exit code is **999**, where every other failure observed exits 0; -- no project file is written, and one already at the output path is left untouched; -- the last line printed is ` IMPORTED FOLDER: \\Packages\`, and nothing reaches - stderr. +*DEFERRED until after v1* -**The smallest reproduction is one empty folder.** Export any project, add an empty -`Packages\Nested\` to the tree, and import it. Every case below starts from a fresh `export` -of the HelloWorld sample: +**Describe the bug** +`export` names its input with a `\\?\` prefix (*exporting from "\\?\C:\...\package.twinproj"*), and a 301-character input path exports normally. Every other path `export` and `import` touch is held to the ordinary Win32 limits, on a machine with `LongPathsEnabled` set to 1. A file or folder that would pass 259 or 247 characters is not created, the error blames permissions and storage space, the run ends `... FAILED` with exit code 0, and `export` carries on, so what it leaves is a partial tree. -| added to the exported tree | result | -|---|---| -| nothing | exit 0, `... DONE` | -| an empty `Packages\Nested\` | **exit 999, no project** | -| `Packages\Nested\x.txt` | **exit 999, no project** | -| `Packages\Nested\Settings`, a copy of the root `Settings` | **exit 999, no project** | -| `Packages\A\B\` | **exit 999, no project** | -| `packages\Nested\`, in lower case | **exit 999, no project** | -| `Packages\x.txt` --- a file, no folder | exit 0, `... DONE` | -| an empty `Packages\` on its own | exit 0, `... DONE` | -| `Miscellaneous\Nested\x.txt` | exit 0, `... DONE` | -| `Sources\Packages\Nested\` --- a `Packages` below the top level | exit 0, `... DONE` | - -So the trigger is a folder inside the top-level `Packages`, whatever it holds: an empty one -does it, and so does one with a `Settings` file of its own, which is what a real package -has. Leaving out `--overwrite` makes no difference: with a project already at the output -path, `import` still stops with 999 rather than refusing to overwrite it. - -**This is not malformed input.** A package a project uses is embedded in it by default, as -a folder of its own under `Packages`, and `export` writes that folder out with the rest of -the tree. Five of the 48 project and package files the IDE ships have one --- -`WinNativeCommonCtls` (which embeds `VBComDlg`), samples 8, 17 and 23, and the *Standard -EXE (plus VBCCR v1.8)* project template --- and each was measured: `export` succeeds, and -`import` of the tree it has just written stops as above. None of them round-trips through -the command line, and neither does any project created from that template. Nor does any -export written by the IDE's **Export Project**, which always adds the compiler packages under -`Packages` (see *Export Project writes the compiler packages*, below). - -**Found by** checking `scripts/impexp.mjs` against the compiler's `import` for line-ending -handling: a probe tree with a made-up `Packages\Nested\` folder never produced a project to -compare. The standalone scripts pack all five exported trees with every file byte-identical -to the original; the only files missing are `.meta` files, the embedded packages' own -included, which `export` does not write. +**To Reproduce** +Steps to reproduce the behavior: +1. Unzip `path-limit-260.zip` (it holds `path-limit-260.twinproj`, an ordinary project) and export it: `twinBASIC_win32.exe export C:\path\path-limit-260.twinproj C:\path\src\`. +2. In PowerShell, make a project file name whose path is longer than 259 characters, and import the tree into it: `$file = "$env:TEMP\" + ("f" * 230) + ".twinproj"`, then `twinBASIC_win32.exe import $file C:\path\src\`. See `ERROR: failed to create output file: (check permissions and storage space)`, then `... FAILED`, exit code 0, and no project file. +3. For the `export` side, in PowerShell: `$dir = "C:\p"; while ($dir.Length -lt 251) { $dir += "\" + ("d" * [Math]::Min(100, 250 - $dir.Length)) }`, then `New-Item -ItemType Directory $dir -Force`, then `twinBASIC_win32.exe export C:\path\path-limit-260.twinproj "$dir\"`. +4. See `ERROR: failed to create output file: \Settings (check permissions and storage space)`, because `Settings` would be 260 characters long, and `[EXPORT] ERROR: folder does not exist and could not be created:` for each of `Sources\`, `Resources\`, `Packages\`, `Miscellaneous\` and `ImportedTypeLibraries\`. The run ends `... FAILED` and exits 0. With `$dir` 250 characters long, `Settings` (259) is written and the folders still fail. ---- +**Expected behavior** +Every file and folder is written, however long its path, as `export` already reads a 301-character input path. If a path cannot be used, the error should say so, and `export` should not write part of a tree and exit 0. -## `export` and `import` stop at the 260-character path limit, apart from the one path they prefix with `\\?\` +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -**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. +**Additional context** +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. `LongPathsEnabled` is 1 on the machine this was measured on, so the Windows setting does not rescue it. -`export` names its input with a `\\?\` prefix --- *exporting from -"\\?\C:\...\package.twinproj"* --- and a 301-character input path exports normally. Every -other path the two verbs touch is held to the ordinary Win32 limits: +What was observed, by path: | path | observed | what it prints | |---|---|---| | a file `export` writes | 259 characters written, 260 fails | `ERROR: failed to create output file: (check permissions and storage space)` | | a folder `export` creates | 247 characters with its trailing `\` created, 248 fails | `[EXPORT] ERROR: folder does not exist and could not be created: \` | -| the project file `import` writes | a 271-character path fails | `ERROR: failed to create output file: (check permissions and storage space)` | +| the project file `import` writes | a 271-character path fails (259 works, 262 fails) | `ERROR: failed to create output file: (check permissions and storage space)` | | the tree `import` reads | a tree at a 250-character path fails | `ERROR: unable to read from folder: \\ImportedTypeLibraries\*` | -Every one of those runs ends `... FAILED` and exits 0. `export` carries on past each error, -so what it leaves is a partial tree: exporting `WebView2Package` to a 198-character folder -wrote 45 of its 72 files. `LongPathsEnabled` is 1 on the machine this was measured on, so -the Windows setting does not rescue it. - -**Reproduction.** Export the HelloWorld sample into an existing folder whose own path is 231 -characters long. `Settings` and `Sources\HelloWorld.twin` are written, at 240 and 255 -characters; `Resources\ICON\twinBASIC.ico` would be 260 and is not. At 220 characters every -file is written and the run ends `... DONE`. +Every one of those runs ends `... FAILED` and exits 0. Exporting `WebView2Package` to a 198-character folder wrote 45 of its 72 files. With the HelloWorld sample into an existing 231-character folder, `Settings` and `Sources\HelloWorld.twin` are written, at 240 and 255 characters, and `Resources\ICON\twinBASIC.ico` (260) is not; at 220 characters every file is written and the run ends `... DONE`. With the reproducer's project the first failure is a folder: into an existing 225-character folder `Settings` and `Sources\Startup.twin` are written and `ImportedTypeLibraries\` (248 with its trailing `\`) is not. -**What does not reproduce it:** a long *input* path to `export`, and any output folder short -enough that no file path reaches 260 characters and no folder path 248. +What does not reproduce it: a long input path to `export`, and any output folder short enough that no file path reaches 260 characters and no folder path 248. -**Found by** the attribute census, `scripts/census_attributes.mjs`, pointed at a cache folder -inside a deep working directory: `WebView2Package` and the three `cefPackage` versions came -back `... FAILED` while the other twelve packages exported. The census used to trust -`export`'s exit code, so until it tested for `... DONE` it would have scanned those partial -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`; 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. - -``` -twinBASIC_win32.exe export C:\probe\garbage.twinproj C:\probe\out\ -``` +*DEFERRED until after v1* -`garbage.twinproj` can be any file that is not a project: a 20-byte text file, an empty file, -a real project with its first byte changed, or one cut off halfway. All four were tried. +**Describe the bug** +Given a file that is not a valid project, the compiler executable opens a modal message box (*invalid header* and *invalid file format* were both seen) and prints nothing more until it is closed. `export`, `settings` and `readme` each waited on the box indefinitely. Once the box is closed, `export` prints `WARNING: failed to parse project file, file may be corrupt`, then `... DONE`, and exits 0. -- The executable opens a modal message box --- *invalid header* and *invalid file format* - were both seen across those inputs --- and prints nothing more until it is closed. Left - alone, each of `export`, `settings` and `readme` was still waiting when the harness killed - it at 25 seconds. -- Once the box is closed, `export` prints `WARNING: failed to parse project file, file may be - corrupt`, then `... DONE`, and exits 0. For the project cut off halfway it also writes the - one file it could read, `Settings`. +**To Reproduce** +Steps to reproduce the behavior: +1. Unzip `damaged-project-modal-box.zip` (it holds `damaged-project-modal-box.twinproj`, an ordinary valid project) and make damaged copies of it, in PowerShell: `$b = [IO.File]::ReadAllBytes("C:\p\damaged-project-modal-box.twinproj")`, then `$b[0] = $b[0] -bxor 0xFF; [IO.File]::WriteAllBytes("C:\p\firstbyte.twinproj", $b)` for one with its first byte changed, and `[IO.File]::WriteAllBytes("C:\p\cut.twinproj", $b[0..([int]($b.Length / 2))])` for one cut off halfway. A 20-byte text file named `garbage.twinproj`, or an empty file, is damaged enough too. +2. Run `twinBASIC_win32.exe export C:\p\garbage.twinproj C:\p\out\`, on a desktop someone is watching. +3. See a message box and no further output. The command was still waiting when it was ended after 25 seconds. +4. Close the box. See `WARNING: failed to parse project file, file may be corrupt`, then `... DONE`, and exit code 0. For the project cut off halfway it also writes the one file it could read, `Settings`. +5. Run `settings` or `readme` on the same file: each waits on a box the same way. -So the `... DONE` test that the exit code forces on every script is fooled as well. **What -does not reproduce it:** a folder given where the project should be. `settings` then prints -`ERROR: failed to parse project file, file may be corrupt or inaccessible` and exits, with no -box. +**Expected behavior** +No window opens from a command-line verb. The command prints the problem (`ERROR: failed to parse project file, file may be corrupt or inaccessible`, which it already prints when a folder is given where the project should be), ends `... FAILED`, and does not report `... DONE` for a file it could not read. -**Found by** probing the command line for the rewrite of the Import/Export Tool page. The -boxes appeared on the desktop of the person at the machine, which is how their wording is -known. +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 983 (the 20-byte text file was also seen on BETA 995) ---- +**Additional context** +Severity: an unattended `export`, `settings` or `readme` never finishes, and once the box is closed `export` reports success, so a script that tests for `... DONE` is fooled as well. -## `export` refused for lack of `--overwrite` still writes part of the tree +All four kinds of damaged input were tried on BETA 983: a 20-byte text file, an empty file, a real project with its first byte changed, and one cut off halfway. On BETA 995 a run with a 20-second limit found `export` and `settings` on the 20-byte text file still waiting at the limit; the other inputs returned inside it, and whether each opened a box was not checked. -**Build:** BETA 995 -**Severity:** a refused export leaves the folder a mixture of the old tree and the project. +What does not reproduce it: a folder given where the project should be. `settings` then prints `ERROR: failed to parse project file, file may be corrupt or inaccessible` and exits, with no box. -Export the HelloWorld sample into a folder, delete the exported `Settings`, edit -`Sources\HelloWorld.twin`, and export again without `--overwrite`: + -``` -[EXPORT] ERROR: output file already exists and --overwrite not set: \Resources\ICON\twinBASIC.ico -[EXPORT] ERROR: output file already exists and --overwrite not set: \Sources\HelloWorld.twin -[EXPORT] DONE: \Settings -... FAILED -``` +--- -`Settings` comes back from the project while the edited `HelloWorld.twin` stays, so the folder -now matches neither the old tree nor the project. `import` checks before it writes, so a -refused `import` leaves the project file as it was; `export` should do the same. +## `export` refused for lack of `--overwrite` still writes part of the tree -The same behaviour makes **the VB package impossible to export without `--overwrite`**, even -into an empty folder: it holds `Resources\MANIFEST\#1.xml` twice (next entry), and `export` -writes one copy and then refuses the other because of the file it has just written. +*DEFERRED until after v1* ---- +**Describe the bug** +When `export` is run without `--overwrite` into a folder that already holds some of the project's files, it refuses those files with an error but still writes every file that is not there yet. The folder is left a mixture of the old tree and the project, matching neither. `import` checks before it writes, so a refused `import` leaves the project file as it was. -## The IDE has written the same name twice into project files it ships +**To Reproduce** +Steps to reproduce the behavior: +1. Unzip `export-refused-partial-write.zip` (it holds `export-refused-partial-write.twinproj`) and run `twinBASIC_win32.exe export C:\path\export-refused-partial-write.twinproj C:\path\out\`. See `... DONE`. +2. Delete `C:\path\out\Settings`, and edit `C:\path\out\Sources\Startup.twin` (add a line at the end). +3. Run the same `export` again, without `--overwrite`. +4. See: + ``` + [EXPORT] ERROR: output file already exists and --overwrite not set: C:\path\out\export-refused-partial-write.twinproj + [EXPORT] DONE: C:\path\out\Settings + [EXPORT] ERROR: output file already exists and --overwrite not set: C:\path\out\Sources\Startup.twin + ... FAILED + ``` +5. See `Settings` come back from the project while the edited `Startup.twin` stays, so the folder now matches neither the old tree nor the project. The exit code is 0. -**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. +**Expected behavior** +The files that would be overwritten are found first, as `import` does, and a refused `export` writes nothing. -Two of the 48 project and package files an installation ships hold one name more than once, -with different contents: +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -| file | name | copies | -|---|---|---| -| the VB package | `Resources/MANIFEST/#1.xml` | 2 --- 703 and 682 bytes | -| Sample 16, *twinBASIC IDE Addin (TODO Widgets demo)* | `.addins/WaynesTodoItemsData` | 8, no two alike | +**Additional context** +Severity: a refused export leaves the folder a mixture of the old tree and the project. -`export` writes entries in reverse order, so with `--overwrite` the first copy in the file is -the one left on disk. The eight Sample 16 copies suggest that each save of the add-in's data -added an entry instead of replacing the old one --- a guess, not a measurement. +The same behaviour makes the VB package impossible to export without `--overwrite`, even into an empty folder: it holds `Resources\MANIFEST\#1.xml` twice (see the report on names written twice into shipped project files), and `export` writes one copy and then refuses the other because of the file it has just written. Measured again on BETA 995: `export` of `packages\{F50B82D0-DCAB-43FE-9631-11959D4A4728}_VB\package.twinproj` into an empty folder prints one `output file already exists` error for that file and ends `... FAILED`. -**Found by** comparing the standalone scripts' `export` with the executable's over every -shipped project file. The scripts now keep the first copy, as the executable does, and name -the repeated entries in a warning. + --- ## `export` needs a full, backslashed project path, and no folder path may use forward slashes -**Build:** BETA 995 -**Severity:** ordinary relative and forward-slashed paths fail, with messages that say the -file or folder does not exist. +*DEFERRED until after v1* + +**Describe the bug** +`export` fails on an ordinary relative project path, on a project path with forward slashes, and on an output folder given with forward slashes, with messages that say the file or folder does not exist when it does. `import` fails the same way on an input folder with forward slashes. The forward-slashed case ends `... FAILED`, creates nothing and exits 0, so a script sees success unless it reads the output. + +**To Reproduce** +Steps to reproduce the behavior: +1. Unzip `export-needs-backslashes.zip` (it holds `export-needs-backslashes.twinproj`, an ordinary project) into `C:\p\`, and create the folders `C:\p\out\` and `C:\p\tree\` (any existing folders will do). +2. Run `twinBASIC_win32.exe export C:\p\export-needs-backslashes.twinproj C:/p/out/`. See `ERROR: output folder does not exist and could not be created`, although `C:\p\out\` exists, then `... FAILED`, and exit code 0. +3. Run the other forms in the table below. | argument | example | result | |---|---|---| -| `export`'s project path, relative | `export hello.twinproj out\` | `ERROR: input twinproj file does not exist` | -| `export`'s project path, forward slashes | `export C:/p/hello.twinproj C:\p\out\` | the same | -| `export`'s folder, forward slashes | `export C:\p\hello.twinproj C:/p/out/` | `ERROR: output folder does not exist and could not be created`, although it exists | +| `export`'s project path, relative | `export export-needs-backslashes.twinproj C:\p\out\` | `ERROR: input twinproj file does not exist` | +| `export`'s project path, forward slashes | `export C:/p/export-needs-backslashes.twinproj C:\p\out\` | the same | +| `export`'s folder, forward slashes | `export C:\p\export-needs-backslashes.twinproj C:/p/out/` | `ERROR: output folder does not exist and could not be created`, although it exists | | `import`'s folder, forward slashes | `import C:\p\x.twinproj C:/p/tree/` | `ERROR: input folder does not exist`, although it exists | -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. 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. - ---- - -## `import` of a folder with no `Settings` file fails without saying why - -**Build:** BETA 995 -**Severity:** minor --- the refusal is right, and the silence is not. +**Expected behavior** +Relative paths and paths with forward slashes work for `export` and for the output folder, as they do for the project path of `import` and for the printing commands, or the error says that the form of the path is the problem. A failure should not exit with 0. -Given a folder with no `Settings` file at its top, `import` lists the files it read, ends -`... FAILED` with no `ERROR:` line, and writes nothing. Every other failure measured names its -cause. - ---- - -## Shifting a `Single`, `Double`, `Date`, `Boolean` or `String` compiles clean, then fails code generation - -**Build:** BETA 995 -**Severity:** the compiler accepts the expression with no diagnostic, and the procedure that -contains it never runs. - -``` -Dim a As Single = 7.9 -Dim c As Integer = 1 -Debug.Print a << c -``` +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -The problems panel shows no errors and the build reports `[LINKER] SUCCESS created output file`. -When the procedure is called, the DEBUG CONSOLE shows `[LINKER] compilation (codegen) error -detected in '.' at line #`, naming the shift's line, and nothing in the -procedure runs --- not even the statements before the shift. `On Error Resume Next` in the caller -does not see it; the caller stops too. +**Additional context** +Severity: ordinary relative and forward-slashed paths fail, with messages that say the file or folder does not exist. -| left operand | `<<` and `>>` | -|---|---| -| `Single`, `Double`, `Date`, `Boolean`, `String` | codegen error | -| `Byte`, `Integer`, `Long`, `LongLong`, `LongPtr` | shifts | -| `Currency`, `Decimal` | builds, but works on the value: a `Currency` holding 7.9, shifted left by 1, is 15.8 | -| a `Variant` holding any of the types above | builds, and multiplies or divides the value | +The echo line explains the first two: `exporting from "\\?\export-needs-backslashes.twinproj"`. The project path is prefixed with `\\?\`, which turns off Windows' path normalisation, so only a full path with backslashes survives it. A folder given partly with forward slashes (an existing `C:\p\out/`) fails the same way. -Precedence reaches it too: `"x" & n << 2` parses as `("x" & n) << 2`, a `String` shift, and fails -the same way. Either a diagnostic or a working shift is expected; the documentation had said -floating-point operands are truncated before shifting. +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 (`C:\p\a\b\c\` was created in one run). -**Found by** probing the operators' result types for `Reference/Operators.md`. + --- -## A `Variant` shift multiplies a fractional value, and can return `Empty` - -**Build:** BETA 995 -**Severity:** wrong values, with no diagnostic. - -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 | -|---|---| -| a `Variant` holding `CInt(1)`, `<< 20` | `Empty` | -| a `Variant` holding `CLng(1)`, `<< 32` | `Empty` | -| a `Variant` holding `CLng(1)`, `<< 31` | `Long` -2147483648 | -| a `Long` variable holding 1, `<< 32` | 0 | +## `import` of a folder with no `Settings` file fails without saying why -**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. +*DEFERRED until after v1* ---- +**Describe the bug** +Given a folder with no `Settings` file at its top, `import` lists the files it read, ends `... FAILED` with no `ERROR:` line, and writes nothing. Every other failure measured names its cause. The refusal is right, and the silence is not. -## Overloads on `Date` and `Double` resolve by declaration order, not by the argument's type +**To Reproduce** +Steps to reproduce the behavior: +1. Unzip `import-no-settings-silent.zip` (it holds `import-no-settings-silent.twinproj`, an ordinary project) and export it: `twinBASIC_win32.exe export C:\p\import-no-settings-silent.twinproj C:\p\src\`. +2. Import only the folder `Sources`, which has no `Settings` file at its top: `twinBASIC_win32.exe import C:\p\x.twinproj C:\p\src\Sources\`. +3. See: + ``` + importing into "C:\p\x.twinproj" from "C:\p\src\Sources\"... + IMPORTED: C:\p\src\Sources\\Startup.twin + ... FAILED + ``` +4. See that there is no `ERROR:` line, the exit code is 0, and `C:\p\x.twinproj` was not written. -**Build:** BETA 995 -**Severity:** the wrong overload runs, with no diagnostic. +**Expected behavior** +An `ERROR:` line that names the cause, such as `ERROR: no Settings file in the input folder`, before `... FAILED`, as the other failures print. A failure should also not exit with 0. -``` -Private Function F(ByVal x As Date) As String - F = "Date" -End Function -Private Function F(ByVal x As Double) As String - F = "Double" -End Function - -Dim x As Double = 1.5 -Debug.Print F(x) ' Date -``` +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -Whichever of the two is declared first receives every call. With `Date` first, a `Double` -argument reaches the `Date` overload; with `Double` first, a `Date` variable, `#1/2/2026#` and -`CDate(1)` all reach the `Double` overload. +**Additional context** +Severity: minor, because the refusal is right. -**What does not reproduce it:** a `Date` overload beside a `String` one resolves correctly, and an -overload set on `Byte`, `Integer`, `Long`, `LongLong`, `Single`, `Double`, `Currency`, `Decimal`, -`Boolean`, `String` and `Variant` sends arguments of each of those types to their own overload. -The compiler does tell the two types apart elsewhere: `TypeName` of a `Date` expression is -`Date`, and a `Long` overload beside a `LongPtr` one is refused as a duplicate definition in a -32-bit build, as it should be. +Also on BETA 983, where a copy of the HelloWorld sample's tree with `Settings` deleted behaves the same: it lists `Resources\ICON\twinBASIC.ico` and `Sources\HelloWorld.twin`, ends `... FAILED` and writes no project. -**Found by** the overload set used to detect the static type of arithmetic results while -measuring the operators for `Reference/Operators.md`. + --- -## `Boolean \ String` and `Boolean Mod String` convert the `String` to `Boolean` - -**Build:** BETA 995 -**Severity:** a wrong value and a wrong type, with no diagnostic. - -``` -Dim b As Boolean = True -Debug.Print TypeName(b \ "2") -``` - -| expression | result | expected | -|---|---|---| -| `b \ "2"` | `Boolean` True | `Long` 0, as `b \ 2.0` gives | -| `b Mod "2"` | `Boolean` False | `Long` -1, as `b Mod 2.0` gives | - -The results are consistent with converting `"2"` to `Boolean` (`True`, -1) first: -1 \ -1 is 1, -stored as `True`, and -1 Mod -1 is 0, stored as `False`. A `String` literal and a `String` -variable on the right both reproduce it. - -**What does not reproduce it:** every other operator converts the `String` to a number --- `b + -"2"` is the `Double` 1, `b / "2"` the `Double` -0.5 --- and so do `\` and `Mod` with the operands -the other way round: `"2" \ b` is the `Long` -2. - -**Found by** the result-type probe for `Reference/Operators.md`. +## Export Project stops at a read-only file after deleting everything before it, and the IDE reports nothing ---- +*DEFERRED until after v1* -## Export Project follows a directory junction in its folder and deletes what it points to +**Describe the bug** +When the export folder holds a read-only file, **File → Export Project** deletes the files and folders that sort before it, fails to delete the read-only one, and stops. Nothing is exported. The only record is three lines in the Debug Console: no dialog opens, and the compiler's response to the IDE is code 0, so the user is left with a half-emptied folder and no sign that the export failed. -**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. +**To Reproduce** +Do it in a scratch folder made for this, `` below, for example `C:\Scratch\readonly-test`. **File → Export Project** empties `\export`. -1. In the export folder, make a junction to another folder that holds a file: - `mklink /J \linked `, with `\precious.txt`. -2. Run **File → Export Project** into ``, with *Export Verbose* on. -3. The Debug Console shows: +Steps to reproduce the behavior: +1. Open `export-stops-at-readonly-file.twinproj` (attached as `export-stops-at-readonly-file.zip`), after unzipping it into ``. Its *Export Path* is `${SourcePath}\export` and *Export Verbose* is on (Project Settings, Export). +2. In a Command Prompt, make three files in the export folder and mark the middle one read-only: ``` - [EXPORT] DELETED: \\?\\linked\precious.txt - [EXPORT] DELETED: \\?\\linked + mkdir \export + echo x> \export\a-first.txt + echo x> \export\m-readonly.txt + echo x> \export\z-last.txt + attrib +R \export\m-readonly.txt ``` - and `` is empty afterwards. - -**What does not reproduce it:** the command-line `export` verb, which deletes nothing. - -**Found by** the Export Project probe for round 8's UC-55, which drove the IDE's own -`exportProjectTo()` over DevTools on a scratch folder. - ---- - -## Export Project stops at a read-only file after deleting everything before it, and the IDE reports nothing - -**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. - -1. Put a read-only file in the export folder among other files. -2. Run **File → Export Project** into it. -3. The Debug Console shows: +3. Choose **File → Export Project**. +4. The last three lines of the Debug Console are: ``` - [EXPORT] DELETE FAILED: \\?\\readonly.txt + [EXPORT] DELETE FAILED: \\?\\export\m-readonly.txt [EXPORT] ERROR: unable to clean the output folder [EXPORT] export failed. ``` - The files and folders that sort before the read-only one are already deleted, nothing is - exported, and no dialog appears: the compiler's response to the IDE is code 0. - -On a `git init` working copy with a commit, it deletes `.git\config`, `HEAD`, `index`, `hooks` -and `info`, then stops at the first object file. `git status` there reports -`fatal: not a git repository`. - -**What does not reproduce it:** a folder with no read-only file, which is emptied and exported -completely --- `.git` included, with no prompt. - -**Found by** the same probe. - ---- - -## Export Path refuses `${SourcePath}` alone, but not the same folder written as a path - -**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 -`${sourcepath}` and `${sourcepath}\`, with the message "This would DELETE the project file, as -the `Export Project` command empties the output folder before exporting". It does not resolve -the path. The compiler applies no check of its own: an export into the project's own folder -logged `[EXPORT] DELETED: \\?\\.twinproj` and completed. A **Save** afterwards -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, 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 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. - -**File → Export Project** writes a `Packages` folder holding the full source of the compiler -packages the project uses: `VB`, `VBA`, `VBRUN` and `AppGlobalClassProject` for a project -with the default references --- 475 of the 477 files an export of a two-file project wrote. -The project file does not hold them. A `.twinproj` the IDE saved holds only the packages the -project embeds, and `twinBASIC_win32.exe export` of it writes only those: for a project -embedding WinDevLib, `Packages\WinDevLib` and no other package. - -Then, on that 477-file export: - -- `twinBASIC_win32.exe import x.twinproj \` stops with exit code 999 and writes - nothing (the `import` entry above), as it does for any folder under `Packages`; -- the standalone script packs it, into a 4,220,723-byte project, against 2,055 bytes for the - same export with `Packages` removed. The project now embeds its own copy of the four - compiler packages. It compiles with no errors; -- the IDE's own **New Project → Import from folder...** does the same, into a - 4,222,833-byte project, against 4,207 bytes from the export with `Packages` removed. - -**The embedded copy is dead, and every later export writes it back.** Measured on the -IDE's import (round 10): - -1. Export a project with the default references into an empty folder `E`. -2. In `E\Packages\VBA\Sources\Math.twin`, add `Public Function ProbeEmbeddedMarker() As - Long` before `End Module`, and a call to it in one of the project's own modules. -3. **Import from folder...** on `E`: TB5079, *Unrecognized symbol 'ProbeEmbeddedMarker'*. - The compiler uses its own VBA package, not the copy the project now holds. -4. Save the project, and export it again: the Debug Console reports - `[EXPORT] COMPLETED (139 folders, 954 files)`, against `(72 folders, 479 files)` before, - and the exported `Math.twin` holds the marker. The export writes both copies to the same - paths, the embedded one last. - -So a project kept in Git through *Export After Save* and rebuilt from a clone keeps committing -the package source of the IDE that first exported it, while it compiles against the current -IDE's. Expected: a `.twinproj` the IDE saves holds no compiler packages, so **Import from -folder** could skip them, or the export could write the IDE's own copy rather than the -project's. - -**What does not reproduce it:** the command line's own `export`, which writes what the -project file holds; and **Import from folder** on the export with the compiler packages' -folders removed, which gives the 4,207-byte project, compiles, and exports 479 files. - -**Found by** checking round 9's UC-62 answer, which sets up *Export After Save* into a Git -repository and rebuilds the project from a fresh clone with the tB executable. The export was -round 8's, written by the IDE's `exportProjectTo()` over DevTools. The dead copy was measured -following round 10's UC-66, the fresh clone, with `root.loadProjectFromFolder()` --- what the -dialog calls after its folder picker --- and `root.saveProjectAs()` over DevTools. - ---- + `\export` now holds `m-readonly.txt` and `z-last.txt`: `a-first.txt`, which sorts before the read-only file, is deleted, and nothing was exported. +5. Afterwards: `attrib -R \export\m-readonly.txt`. -## An out-of-range index raises `&H8002000B` or `&H80004005`, not VBA's error 9 +On a `git init` working copy with a commit, the same export deletes `.git\config`, `HEAD`, `index`, `hooks` and `info`, then stops at the first object file. `git status` there reports `fatal: not a git repository`. -**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. - -``` -Dim a(5) As Long -On Error Resume Next -a(7) = 1 -Debug.Print Err.Number, Hex$(Err.Number), Err.Description -``` - -prints `-2147352565 8002000B Invalid index.`. Every case measured, reading `Err.Number` in -the program: - -| access | twinBASIC | VBA, per VBA-Docs' *Subscript out of range (Error 9)* | -|---|---|---| -| past a fixed or dynamic array's bound, a `Variant` array's, or `Split("x y")(5)` | -2147352565 (`8002000B`) *Invalid index.* | 9 | -| an element of an array never dimensioned: `Dim u() As Integer: u(8) = 234`, VBA-Docs' own example | -2147467259 (`80004005`) *Unspecified error* | 9 | -| a `Collection` member by a missing index or key | -2147467259 *Unspecified error* | 9 for a missing member | -| `Forms(99)`, `Forms.Item(-1)` | -2147467259 *Unspecified error* | --- | - -**What does not reproduce it:** `UBound` of an erased array, `Printers(99)` and `Err.Raise 9` -all give 9, and division by zero gives 11. The IDE's run-time error panel shows the same number -`Err.Number` holds, for the array case. An erased array behaves as one never dimensioned: -`-2147467259` for an element, 9 from `LBound` and `UBound`. `Printers` raises 9 past its end but -`-2147467259` for a negative index and for an unknown name. The description of `-2147467259` -varies between runs --- *Unspecified error* in one, *Automation error* in another. - ---- - -## Reading `Forms` by index returns a broken reference, and the process then crashes - -**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`): - -``` -Dim s As String -s = Forms(0).Name ' s is "", and the process later dies with 0xC0000005 -``` +**Expected behavior** +Either the export deletes nothing when it cannot empty the whole folder, or it reports the failure to the user in a dialog, as for any other error, and says what it left behind. A failed clean-up should not leave the folder half emptied without a visible message. -`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. +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -**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; -`Printers(0)` with a literal index works. +**Additional context** +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. -**Found by** the fix pass for round 8's error-number findings: an EXE that logs a line before -each statement, run once per case with crash dialogs suppressed. The crash itself was -reproduced by the orchestrator; the loop-variable corruption was measured by the fix agent only. +A folder with no read-only file does not reproduce it: it is emptied and exported completely, `.git` included, with no prompt. -**Found by** the IDE debugging probe for round 8's UC-61, then a probe of its own run in the IDE -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 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. +*DEFERRED until after v1* -1. Run a procedure that raises an untrapped error inside a loop, and let the error panel open. -2. Press F8 (or F10, F11, SHIFT+F8) on the failing line. The line re-runs, the error recurs, - and the mark does not move --- as expected. -3. Now choose **Ignore (Resume Next)**. It stops at the next line instead of running on. - Moving past the line with **Set Next Statement** (CTRL+F9) instead, each F5 then advances - one line. +**Describe the bug** +At a run-time error the debugger shows the error panel. Pressing a step key (F8, F10, F11 or Shift+F8) on the failing line re-runs the line, the error recurs and the mark does not move, as expected. But the step is then still pending: choosing **Ignore (Resume Next)** afterwards stops at the next line instead of running on, and the next F5 is used up as well. One command no longer means one thing, and the debugger stops where it was not asked to. -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. +**To Reproduce** +Steps to reproduce the behavior: +1. Open `step-key-leaves-step-pending.twinproj` (attached as `step-key-leaves-step-pending.zip`) and press F5. `FillTable` raises an error inside a loop, at the line marked `FAILS`: + ``` + Private Sub FillTable() + Dim a(3) As Long, i As Long, idx As Long = 5 + For i = 0 To 2 + Debug.Print "loop " & i + a(idx) = i ' FAILS + Debug.Print "after fail " & i + Next + Debug.Print "fill end" + End Sub + ``` + The error panel opens. The Debug Console shows `main start` and `loop 0`. +2. Press F8 on that line. The line runs again, the error recurs and the mark does not move. +3. In the error panel choose **Ignore (Resume Next)**. +4. It stops at the next line, `Debug.Print "after fail " & i`, which has not run (`after fail 0` is not in the Debug Console). +5. Press F5. Instead of running on from there, it runs to the loop's next error: the Debug Console now ends with `after fail 0` and `loop 1`, and the error panel is open again. The step F8 asked for was used up by that one stop. -**What does not reproduce it:** choosing **Ignore** without pressing a step key first, which -runs on from the next line as the panel says. +**Expected behavior** +**Ignore (Resume Next)** runs on from the next line, whether or not a step key was pressed on the failing line first, as the panel says. -**Found by** the IDE debugging probe for round 8's UC-61, driving real keys over DevTools. +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 ---- +**Additional context** +Also on BETA 983, identically. Moving past the failing line with **Set Next Statement** (Ctrl+F9) instead, each F5 then advances one line. An earlier run by hand, followed to the end, saw the pending step last until the procedure returned; what differed there is not known, and in the lane the step is used up by the one stop, as above. -## Stop at a run-time error ends only the procedure that raised it +What does not reproduce it: choosing **Ignore** without pressing a step key first, which runs on from the next line, to the next pass's error, as the panel says. Severity: the debugger stops where it was not asked to. -**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 -the call. At the error panel, choose **Stop** --- the panel's button or the toolbar's. The -failing procedure ends, and `Main`'s following `Debug.Print` still runs. Three runs, the same -each time. +--- -**What does not reproduce it:** **Stop** at an ordinary break (a breakpoint or a step), which -prints `aborted` and ends the whole run. +## A `Static` in a Module procedure cannot be initialised with an argument constructor of a class declared later in the project -**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.` 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). +*DEFERRED until after v1* -**Found by** the same probe; the assertion case by the fix pass for the Assert tutorial. +**Describe the bug** +A `Static` variable in a Module procedure, initialised with `New` and a constructor that takes arguments, fails with TB5074 when the class is declared later in the project than the Module, in another file or further down the same file. With the class declared first, the same line compiles. `Dim` with the same initialiser compiles in both orders, so the declaration's position in the project changes whether a valid `Static` compiles. ---- +**To Reproduce** +Steps to reproduce the behavior: +1. Open `static-ctor-args.twinproj` (attached as `static-ctor-args.zip`). The project lists `Probe.twin`, which holds the Module, before `ProbeDog.twin`, which holds the class: + ``` + ' Probe.twin + Module Probe + Public Sub T() + Static s As Dog = New Dog("Rex") + Debug.Print s.GetName() + End Sub + End Module + + ' ProbeDog.twin + Private Class Dog + Private m_Name As String + Public Sub New(ByVal Name As String) + m_Name = Name + End Sub + Public Function GetName() As String + Return m_Name + End Function + End Class + ``` +2. See the project fail to compile with `TB5074 Could not bind to parameterized constructor of class 'Dog'. No compatible Sub New() method found`, at the `New`. +3. Move the `Dog` class into `Probe.twin`, above the Module: the project compiles with no error. -## A `Static` declaration cannot initialise with a constructor that takes arguments +What does not reproduce it, each measured with the class declared later than the Module unless it says otherwise: `Dim s As Dog = New Dog("Rex")` compiles, and so does `Static s As Dog` with `Set s = New Dog("Rex")`; a `Static` with a constructor that takes no arguments compiles; the same `Static` in a method of a Class compiles; and the same `Static` with the class declared first compiles, in a Module procedure, a Function, a Class method and a Property Get, in one file or two, with the class `Private` or `[COMCreatable(False)]`. The failure holds for a `Private` class and a public `[COMCreatable(False)]` one, and with a second constructor beside the one that takes arguments. -**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. +**Expected behavior** +The declaration compiles whichever of the Module and the class comes first, as `Dim` does and as `Static` does when the class is first. -``` -Private Class Dog - Private m_Name As String - Public Sub New(ByVal Name As String) - m_Name = Name - End Sub -End Class - -' in a procedure: -Static s As Dog = New Dog("Rex") -``` - -fails with TB5074, *Could not bind to parameterized constructor of class 'Dog'. No compatible -Sub New() method found*, at the `New`. +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -**What does not reproduce it:** the same initialiser on `Dim` (`Dim d As Dog = New Dog("Rex")`), -and on a module-level `Private` or `Public`; a `Static` initialised with a constructor that takes -no arguments (`Static c As Collection = New Collection`); and a `Static` of a value type -(`Static n As Long = 5`). All compile and run. +**Additional context** +Also on BETA 983. Severity: a valid declaration does not compile, and which projects it affects depends on file order, so it appears and disappears as files are added. The workaround is a `Static` without an initialiser and a `Set` on first use, or declaring the class first. The order is the order of the files inside the `.twinproj`; the reproducer is packed with `Probe.twin` before `ProbeDog.twin`. -**Found by** the fix pass for round 8's UC-59, measuring the forms `New.md` documents. + --- ## *Import from file...* leaves the imported package unticked -**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. +*DEFERRED until after v1* -Settings → References → Available Packages → *Import from file...*, and choose a `.twinpack`. The -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`, while the online import path beside it, `importPackage`, reads -`t.body.packageSymbol`. +**Describe the bug** +In Settings → References → Available Packages, **Import from file...** imports a `.twinpack` but leaves the package unticked. The compiler answers the IDE's `importPackage` request with success and the package's symbol, and the package appears in the Available Packages list, but nothing in the project can use it until the user ticks it by hand. The online import beside it ticks the package it imports. -**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. +**To Reproduce** +Steps to reproduce the behavior: +1. Open `import-package-unticked.twinproj` (attached as `import-package-unticked.zip`). The zip also holds the package to import, `DocProbePkg.twinpack`, a package of one function, `DocProbeVersion`. +2. Open Project Settings → References → Available Packages, press **Import from file...**, and choose `DocProbePkg.twinpack`. +3. `DocProbePkg` appears in the Available Packages list, unticked. (In the page, the compiler's answer to `importPackage` is `success: true, body: { packageSymbol: "DocProbePkg" }`.) ---- +**Expected behavior** +The imported package is ticked, as after the online import, so that **Apply** is all that is left to do. -## Replacing an embedded package under one Apply keeps running the old copy +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -**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. +**Additional context** +Also on BETA 983, by hand. Severity: the package is imported but not referenced, and nothing says so. -1. A project embeds a package built locally, `DocProbePkg` v1. -2. In Settings → References, untick it; *Import from file...* its v2; tick v2; apply once. -3. The console shows only `[COMPILER] Project settings updated` --- no restart and no save. Builds - keep running v1. Save All and then a compiler restart give v2; a restart *without* saving - brings v1 back, under a reference numbered 1.1.0.0. +The cause looks like this. `packageLoadFromFile` in `ide/main.js` reads `packageSymbol` from the response itself (`t.packageSymbol`) rather than from its `body`, so it passes `"/Packages/undefined"` to `getAvailableTypeLibrariesData` and ticks nothing; `importPackage`, the online import beside it, reads `t.body.packageSymbol`. This was read again in BETA 995's `ide/main.js`. -Two runs of two, on a machine with no linked copy of the package. - -**What does not reproduce it:** the same steps with a linked copy of the package present in -`%APPDATA%\twinBASIC\packages` (six runs of six), and an apply after the untick followed by -another after the import and tick (every run): each of those restarts the compiler and saves, -and v2 runs at once. - -**Found by** the same probe. + --- -## 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`. - ---- +## Replacing an embedded package under one Apply keeps running the old copy -## A call through a `FastCall` or `ThisCall` delegate is made as stdcall on win32 +*DEFERRED until after v1* -**Build:** BETA 995 -**Severity:** the delegate is unusable on win32; every call through it raises an error. +**Describe the bug** +A project embeds a package built locally. In Settings → References, replacing it by another build of the same package (untick the old one, import the new `.twinpack`, tick the new one) and pressing **Apply** once leaves the compiler running the old copy. The console shows only `[COMPILER] Project settings updated`: no compiler restart and no save. Builds and runs keep using the old package. -```tb -Public Delegate Function FastDel FastCall (ByVal a As Long, ByVal b As Long) As Long +**To Reproduce** +Use a machine with no copy of the package in `%APPDATA%\twinBASIC\packages`: a linked copy there makes the bug go away (see below). -Public Function GF FastCall(ByVal a As Long, ByVal b As Long) As Long - Return a * 100 + b -End Function +Steps to reproduce the behavior: +1. Open `replace-embedded-package-one-apply.twinproj` (attached as `replace-embedded-package-one-apply.zip`). The zip also holds the two package builds, `DocProbePkg-v1.twinpack` and `DocProbePkg-v2.twinpack`, whose function `DocProbeVersion` returns 1 and 2. +2. Embed v1: Project Settings → References → Available Packages → **Import from file...** `DocProbePkg-v1.twinpack`, tick it, **Apply**. Replace the body of `Main` with + ``` + Debug.Print "DocProbeVersion=" & DocProbeVersion() + ``` + and press F5: the Debug Console shows `DocProbeVersion=1`. +3. Replace it with v2 under one Apply: in References untick `DocProbePkg`, then Available Packages → **Import from file...** `DocProbePkg-v2.twinpack`, tick it, and press **Apply** once. +4. The Debug Console shows only `[COMPILER] Project settings updated`. Press F5: it prints `DocProbeVersion=1`. -Public Function GS(ByVal a As Long, ByVal b As Long) As Long - Return a * 100 + b -End Function +Save All and then a compiler restart give v2. A restart without saving brings v1 back, under a reference numbered 1.1.0.0. Two runs of two, on a machine with no linked copy of the package. -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 -``` +**Expected behavior** +After the **Apply**, builds and runs use v2, as they do when the same replacement is applied in two steps (below). Or the Apply restarts the compiler and saves, as the first embedding does. -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. +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -**What does not reproduce it:** +**Additional context** +Severity: the project builds and runs the old package after the user has replaced 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). +What does not reproduce it: an **Apply** after the untick, followed by another after the import and tick (every run; this is the control in the lane on BETA 995): each of those restarts the compiler and saves, and v2 runs at once. Also the same steps as above with a linked copy of the package present in `%APPDATA%\twinBASIC\packages` (six runs of six, on BETA 983): the same restart and save, and v2 runs. -**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 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. - -``` -Module GenMax -Public Function Max(Of T)(a As T, b As T) As T - If a > b Then - Return a - Else - Return b - End If -End Function -End Module -``` - -With one call, `Set m = Max(Of Collection)(c1, c2)`, the project fails to compile with -`TB5092 Missing argument 'Index'`, reported twice, both times at `[3,14]` of the module that -holds `Max` --- the line with `>`. The message comes from `Collection`'s default member, -`Item`. Neither error names `Collection`, and neither names the line of the call. +*DEFERRED until after v1* -**What does not reproduce it:** calls with `Long`, `Double` and `String`, deduced or given -with `(Of ...)`, which compile and return the larger value. +**Describe the bug** +When a call to a generic procedure makes the procedure's body invalid for the type given, the error is reported in the body, twice, and names neither the type nor the line of the call. The diagnostic points at correct code. In a project with many calls to a generic procedure, nothing says which call to fix. -**Found by** probing round 9's UC-65 answer, whose `Max` uses `>` on a type parameter with -nothing to say which types it accepts. - ---- +**To Reproduce** +Steps to reproduce the behavior: +1. Open `generic-body-error.twinproj` (attached as `generic-body-error.zip`). Its one source file, `GenMax.twin`, holds the whole bug: + ``` + Module GenMax + Public Function Max(Of T)(a As T, b As T) As T + If a > b Then + Return a + Else + Return b + End If + End Function + + Public Sub Use() + Dim c1 As New Collection, c2 As New Collection + Dim m As Collection + Set m = Max(Of Collection)(c1, c2) + End Sub + End Module + ``` +2. See the project fail to compile with `TB5092 Missing argument 'Index'`, reported twice, both times at the line of `If a > b` in `Max`. The message comes from `Collection`'s default member, `Item`. Neither error names `Collection`, and neither names the line of the call. -## Text that continues a `Debug.Print` line is escaped twice in the DEBUG CONSOLE +What does not reproduce it: calls with `Long`, `Double` and `String`, deduced or given with `(Of ...)`, which compile and return the larger value. -**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 `>`. +**Expected behavior** +A diagnostic that names the type argument and the call that supplied it. The `>` on a `Collection` is a legitimate error for this call, but it is the call that has to change, so that is where a person needs to be pointed. -Two statements in a `[RunAfterBuild]` Sub are the whole reproduction: +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -``` -Debug.Print "A"; -Debug.Print "&" -``` +**Additional context** +Also on BETA 983, with the same TB5092. Severity: a diagnostic that points at correct code. `Max` uses `>` on a type parameter with nothing to say which types it accepts, which is how the problem arises. -The DEBUG CONSOLE shows `A&`. The text that opens the line comes out right --- -`Debug.Print "a < b";` shows `a < b` --- and everything printed after it until the line -ends is escaped twice: after `Debug.Print "C";`, `Debug.Print "D";` and -`Debug.Print "<&>"`, the line reads `CD<&>`. - -**What does not reproduce it:** a whole line (`Debug.Print "a < b & c"` shows exactly that), -and the same text in one statement (`Debug.Print "B"; "&"` shows `B&`). - -`debugOutputPartial` in `ide/main.js`, which takes all of a program's output, and an -add-in's `PrintText` too, and adds to a line that is still open, passes the new text through -`TEXTtoHTML` twice: once as it builds the text and again as it stores it. When the new -text's colour differs from the line's, the `` it puts in to change -colour goes through the second pass too, so the tags themselves show as text. The colour -comes from the output: a program's plain output is `debugConsoleOutputText`, and a -`PrintText` is `debugConsoleOutputTextYELLOW`. With a line left open in the first, made by -calling `debugOutputPartial` from the page, a `PrintText` from the IDE's own Sample 10 add-in -showed as `Hello there from -WaynesWorldAddIn!`. A program's own open line followed by a `PrintText` was not tried. - -**Observed** on 2026-09-24 with `scripts/tbrun.mjs`, which decodes the console's stored -entries once, as the pane renders them. Found while making the add-in harness read text -that the IDE appends to an open console line. + --- -## An add-in's keyboard shortcut does not fire if it includes `{CTRL}` or `{ALT}` +## `End` in an LLVM-compiled procedure restarts the IDE's compiler -**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. +*DEFERRED until after v1* -An add-in that registers +**Describe the bug** +Running a project in the IDE (F5), an `End` statement in a procedure compiled with LLVM ends the run, and the DEBUG CONSOLE then shows `restarting from MEMORY []`: the compiler restarts and compiles the project again. The IDE recovers. -``` -Host.KeyboardShortcuts.Add "{CTRL}{SHIFT}d", AddressOf OnCtrlShiftD -Host.KeyboardShortcuts.Add "{SHIFT}d", AddressOf OnShiftD -``` - -gets `OnShiftD` for Shift+D, and nothing at all for Ctrl+Shift+D. - -| registered | pressed | fires | -|---|---|---| -| `d`, `{SHIFT}D`, `F1`, `{shift}f1` | D, Shift+D, F1, Shift+F1 | yes | -| `{CTRL}{SHIFT}d`, `{ctrl}d`, `{ALT}f` | Ctrl+Shift+D, Ctrl+D, Alt+F, each more than 0.5 s after any other press of D or F | **no** | -| the same three | D alone, then Ctrl+D and Ctrl+Shift+D; F alone, then Alt+F, all inside 0.5 s | yes, all three | - -The last row shows the cause. `globalKeyUp` in `ide/main.js` matches an add-in's shortcut when -the key is released, and only if `realKeyPresses` holds a press of the same key from less than -500 ms before. `globalKeyDown` records a press only -`if((!e.ctrlKey||e.key==="Control")&&(!e.altKey||e.key==="Alt"))`, so a key pressed with Ctrl -or Alt held is never recorded. Its release finds either no press, or an earlier one of the same -key made without the modifier. The IDE's own bindings are unaffected, because they are matched -on the key-down. - -A smaller point for the same fix: `KeyboardShortcuts.Add` stores the string as given, lowercased -and without spaces, and the key-up builds the string it looks up as `{ctrl}`, `{shift}`, `{alt}` -and the key, in that order. So `{SHIFT}{CTRL}d` could never match even with the recording fixed. - -**Observed** on 2026-09-24 with a probe add-in whose shortcuts print to the DEBUG CONSOLE -(`test/addin/probes/keys`), operated by `test/addin/keys.test.mjs`, which presses keys as CDP -key events and checks each result. Every case in the table is a test in that lane. - ---- - -## F1 and the fold icon toggle the signature help, then fail +**To Reproduce** +Steps to reproduce the behavior: +1. Open `llvm-end-restarts-compiler.twinproj` (attached as `llvm-end-restarts-compiler.zip`). It needs an LLVM licence (Ultimate): the procedure below is compiled with LLVM by its attribute. + ``` + [CompilerOptions("+llvm")] + Private Sub Stopper() + Debug.Print "before End" + End + End Sub + ``` + `Sub Main` calls `Stopper`. +2. Run the project in the IDE (F5). +3. See `before End` in the DEBUG CONSOLE, and then `restarting from MEMORY []`. -**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. +**Expected behavior** +The run ends, as it does without the attribute, with no restart of the compiler. -1. In the code editor, put the cursor inside a call's parentheses and press Ctrl+Space. The - signature help shows, with a fold icon whose tooltip reads *Fold/Collapse (F1)*. -2. Press F1. The signature help expands, and the DEBUG CONSOLE shows - `command failed: "tbHelp_ToggleExpandSignatureHelp"`. F1 again collapses it, with a second - such line. -3. Click the fold icon instead. The signature help toggles, and the page throws - `TypeError: Cannot read properties of undefined (reading 'stopPropagation') at toggleSigHelp`. +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -`toggleSigHelp(e)` in `ide/main.js` ends with `e.stopPropagation();e.preventDefault()`, and -neither caller passes an event: the command is `internalAction:()=>{toggleSigHelp()}`, and the -icon is `onclick='toggleSigHelp()'`. The toggle comes first, so the error is the only symptom. -`executeKeyboardShortcuts` catches the command's error and writes the DEBUG CONSOLE line. +**Additional context** +Severity: low; the IDE recovers, but the project is compiled again, and LLVM-compiled code run in the IDE is not recommended anyway. Without the attribute the run ends with no restart. In a built exe, with or without LLVM, `End` ends the program with exit code 0 (this project run with `tbrun --exe` on 995 exits 0). BETA 983 ignored the `End` and went on (fixed in 985 for built programs); this project's LLVM procedure is refused by 983 ("Unable to compile due to use of datatype that is not yet supported for LLVM compilation"), so that was not re-measured with it. -**Observed** on 2026-09-24: the F1 case in `test/addin/keys.test.mjs`, which checks for the -line, and the click in a harness IDE with `Runtime.exceptionThrown` recorded over CDP. + --- -## Typing just after a file opens at a position puts the text at that position, in reverse - -**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. - -For 700 ms after the code editor opens a file at a line and column --- Go To Definition, a -Find in Files result, an add-in's `Editors.Open` --- the IDE puts the cursor back at that -place whenever the compiler's decorations for the document arrive. Every edit brings new -decorations, and each time the cursor goes back the 700 ms start again. So typing that -starts inside the window, and goes on without a 0.7 s pause, puts each character at the -opened position, in front of the one before it. - -1. Open `Haystack.twin`, not yet open, at line 4, column 9, through - `openEditors.openFile(node, false, false, false, 4, 9)` --- the call Find in Files makes. -2. 0.3 s later, move the cursor to line 3, column 1, and type `xyz`, one key every 150 ms. -3. Line 3 starts with `x`, and line 4 reads ` zyDim needleCount As Long`. - -`parseDocumentDecorations` in `ide/main.js` ends with -`if(performance.now()-revealedLineTime<700){revealLineInEditor(revealedLine,revealedLineColumn,revealedLineViewPortTop)}`, -and `revealLineInEditor` sets the cursor's position and `revealedLineTime` again. Logged in the -run above: `revealLineInEditor(4,9)` from `gotFileData`, then from `parseDocumentDecorations` -9 ms later, and again after each key. The same happens for a file that is already open, whose -`onReveal` calls `revealLineInEditor` too. - -**What does not reproduce it:** the same typing started more than 0.7 s after the file opened, -which puts `xyz` at 3:1 in order. Keeping the view where the reveal left it may be what the -repeat is for; setting the cursor again is what does the damage. - -**Found by** the add-in harness: `MsgBox(`, typed into the code editor just after opening a -file at line 5, came out as `gBox(s` at the start of that line, with the `M` on the line below. The harness now waits for the 700 ms to pass after -opening a file (`afterReveal` in `scripts/lib/tb-operate.mjs`). +## Builds running at the same time in one TEMP folder sometimes fail to write the type library ---- +*DEFERRED until after v1* -## Hover says a `ByVal` parameter was auto-generated because `Option Explicit` is off +**Describe the bug** +When several twinBASIC IDEs build at the same time, and their `TEMP` is the same folder, which is the default for one user, a build now and then fails while writing the type library, with nothing wrong in the project. The same build passes when repeated, or when it runs alone. With a different `TEMP` folder for each IDE, the failure did not occur. -**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. +**To Reproduce** +Steps to reproduce the behavior: +1. Unzip `concurrent-builds-shared-temp.zip` (it holds `concurrent-builds-shared-temp.twinproj`, an ordinary console project with an empty `Sub Main`) and copy the project into eight folders, one copy each. +2. Open each copy in its own instance of the IDE, all eight running at once, with the default environment, so that every instance has the same `TEMP`. +3. Press **Build** in all eight at about the same time, and repeat. About one build in 24 fails; the DEBUG CONSOLE of the one that failed shows: + ``` + [BUILD] Starting... + [TYPELIB] failed to finalize typelibrary. Disk error? + [LINKER] FAILED to create type library + [BUILD] failed + ``` +4. Control: start each instance with `TEMP` and `TMP` set to a folder of its own (for example from a Command Prompt, `set TEMP=C:\t\1` and `set TMP=C:\t\1`, then start the IDE from it). The failure does not occur. -In a project with `project.optionExplicit` set to true: +Measured with the documentation's build harness, which starts each IDE on its own port and presses Build through it, eight IDEs at once, over 192 builds each way: 8 failed with one `TEMP` for all eight, and 0 failed with a `TEMP` folder for each. With one or two IDEs at once, 0 of 120 failed. Distinct project names and project ids in each copy did not prevent it. The failing build is a different one each time. -``` -Public Sub Probe2(ByVal h As Host, ByVal count As Long, ByVal col As Collection, _ - ByVal o As Object, ByVal v As Variant, ByRef r As Host, ByVal s As String) - Dim d As Host - Debug.Print h Is Nothing, count, col Is Nothing, o Is Nothing, IsEmpty(v), r Is Nothing, s, d Is Nothing -End Sub -``` - -Hover over `s` where it is used shows +**Expected behavior** +Each build writes its type library and succeeds, however many IDEs share the `TEMP` folder. -> *parameter* ByVal s As String -> -> ***note:*** *this variable was auto-generated due to* ***Option Explicit*** *being Off* -> -> ***recommendation:*** *use Option Explicit and declare variables explicitly* +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -| hovered | note | -|---|---| -| `ByVal` of `String`, `Variant`, `Object`, `Collection` or tbIDE's `Host` | **yes** | -| `ByVal` of `Long` | no | -| `ByRef r As Host` | no | -| a local, `Dim d As Host` or `Dim c As New Collection` | no | +**Additional context** +Severity: low; the build passes when repeated, and an IDE run by a person rarely builds at the same moment as another. It affects tools that build several projects at once. The compiler imports `GetTempFileNameW`, and its messages show that it writes the type library to a file and reads it back (`[TYPELIB] failed to read in generated type library file` is the message beside this one). A temporary file name that two processes both use would explain the counts; that is an inference, not observed. The temp folder is empty after the builds, so whatever is written there is deleted. -So it takes `ByVal` and a type that is not a plain number. That looks like a hidden local copy -that the compiler makes for such a parameter, which the hover then describes as a variable it -generated for an undeclared name. - -**Observed** on 2026-09-24 by sending `textDocument/hover` over the compiler's language socket -with the parameters the IDE's own hover provider sends (`test/addin/symbols.test.mjs`, which -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 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. - -``` -Set w1 = Host.ToolWindows.Add("First") -w1.Title = "First" -w1.RootDomElement.ChildDomElements.Add("one", "div").Properties.innerText = "first" -w1.Visible = True -Set w2 = Host.ToolWindows.Add("Second") -w2.Title = "Second" -w2.RootDomElement.ChildDomElements.Add("two", "div").Properties.innerText = "second" -w2.Visible = True -``` - -shows one window, titled `Second` and holding `second` alone. What is then added through -`w1` goes into that same window. - -`createToolWindow` in `ide/main.js` files each window under `e.guid`, which is the -`UniqueIdForPositionPersistance` argument, and `""` when it is left out. -`createToolWindowById(e)` returns the window it already has under that id, after -`n.bodyElement.innerHTML=""`, instead of making another, and the page answers the compiler -with that window's number, so both `ToolWindow` objects are bound to it. The same reuse is -what hands an add-in its own window back after a compiler restart, when it asks again for -the id it used before (`test/addin/reload.test.mjs`), so a fix would give each window without -an id one of its own rather than change the reuse. - -**What does not reproduce it:** a window given an id, or one window given none. None of the -IDE's add-in samples leaves the id out. - -**Observed** on 2026-09-25 with the panes probe's third button, operated by -`test/addin/panes.test.mjs`, which reads `toolWindowsById` over CDP. - -## `[PopulateFrom]` with no arguments crashes the compiler - -**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. - -The whole reproduction is one Enum: - -``` -Public Module CrashProbe - [PopulateFrom] - Public Enum E - End Enum -End Module -``` - -The Enum's body does not matter: the same crash comes with a member in it, and with the -Enum inside a Class instead of a Module. The documented shape is five strings, -`("json", "/Resources/PROBE/Strings.json", "events", "name", "id")`, and the other wrong -shapes tried are handled: - -| argument list | result | -|---|---| -| none, `[PopulateFrom]` | **the compiler crashes** | -| `(True)`, `(False)`, `(1)` | TB5155 `This attribute is not supported in this context` | -| `("probe")`, on the reproduction above | TB5083 `unsupported data source` | -| the documented five strings, with a resource that exists | compiles | - -So a missing argument list is the one wrong shape that is not checked. - -**Observed** on 2026-09-30 in two ways. `scripts/sweep_attributes.mjs` builds every attribute -at every declaration site in batches of 400 and halves a batch the compiler crashes on; each -of the three Enum sites (an Enum with a member, an empty Enum, an Enum in a Class) was -narrowed to one probe beside the three canaries the tool adds to every batch, which build -clean without it. The four-line reproduction above was then built **exactly as written**, in -a project holding only it and a two-line `Sub Main`, with no resources: `tbbuild` exits 4, -`the compiler crashed 2x -- this project takes it down`, `last parsing: CrashProbe.twin`. -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. +## For Each over WebView2 request or response headers crashes in WebView2HeadersCollection.Next -**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`. +**Describe the bug** +`For Each` over the `WebView2RequestHeaders` that `NavigationStarting` receives crashes with an access violation in `WebView2HeadersCollection.Next`. `WebView2ResponseHeaders` returns the same enumerator from its `_NewEnum`, so `For Each` over response headers reaches the same code (not run). `For Each` calls `IEnumVARIANT::Next` with `pCeltFetched` set to a null pointer, which the interface allows, and the package's `Next` assigns to `pCeltFetched` without testing it. The DEBUG CONSOLE shows `NATIVE EXCEPTION: ACCESS_VIOLATION /WebView2HeadersCollection.twin; WebView2HeadersCollection.Next`. -## `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. +**To Reproduce** +Steps to reproduce the behavior: +1. Open `wv2-headers-foreach-crash.twinproj` (attached as `wv2-headers-foreach-crash.zip`). It references the WebView2 package and has one form, `Form1`, with one WebView2 control, `WebView21`. `Sub Main` shows the form modally. When the control is ready it navigates to `about:blank`, and its `NavigationStarting` handler goes through the request headers: + ``` + Private Sub WebView21_NavigationStarting(ByVal Uri As String, ByVal IsUserInitiated As Boolean, _ + ByVal IsRedirected As Boolean, ByVal RequestHeaders As WebView2RequestHeaders, _ + Cancel As Boolean) Handles WebView21.NavigationStarting + Debug.Print "NavigationStarting " & Uri + Dim h As WebView2Header + For Each h In RequestHeaders + Debug.Print h.Name & ": " & h.Value + Next + Debug.Print "after For Each" + End Sub + ``` +2. Run the project (F5). +3. See `NavigationStarting about:blank` in the DEBUG CONSOLE, and then `NATIVE EXCEPTION: ACCESS_VIOLATION /WebView2HeadersCollection.twin; WebView2HeadersCollection.Next`. `after For Each` is never printed. -``` -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 -``` +**Expected behavior** +`For Each` yields each header, and the loop ends. The package's documentation shows this loop in a `NavigationStarting` handler. `Next` should assign to `pCeltFetched` only when its address is not zero, for example `If VarPtr(pCeltFetched) <> 0 Then pCeltFetched = 1`, in both places it assigns it. -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. +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 -**What does not reproduce it:** the file closed. +**Additional context** +Severity: medium; `For Each` is the documented way to read the headers, and it ends the program. Without the `For Each`, the same project navigates, closes the form and returns. The same project crashes the same way on BETA 983. Calling `Next` directly, through a copy of `IEnumVARIANT` with a variable for `pCeltFetched`, returns the headers and then the end; the crash needs the null pointer that `For Each` passes. The same null `pCeltFetched` from `For Each` was measured with an enumerator written in a project: an unguarded assignment fails with an access violation there too. `Reset`, which `For Each` calls first, returns `E_NOTIMPL` here, and `For Each` goes on to call `Next` regardless. -**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/WIP.ExamplesBuild.md b/WIP.ExamplesBuild.md index 71c50c52..6bb076b4 100644 --- a/WIP.ExamplesBuild.md +++ b/WIP.ExamplesBuild.md @@ -135,7 +135,7 @@ a fence at all. | token | meaning | default | |---|---|---| | `check_build` | compile this sample | --- | -| `check_run` | compile it *and* run it, capturing Debug output. **Not implemented**; such a fence is compiled only, and the run says so | --- | +| `check_run` | compile it, build it and run it, and compare what it prints with what the fence says it prints (a trailing comment on a `Debug.Print` line, or the comment lines under `' Output:`). Statement samples (`slot=sub`) only; `MsgBox`, `InputBox` and `End` are refused. See the dispatcher design below | --- | | `hidden` | context for the page's samples, compiled with them and rendered to nothing. Implies `check_build` | --- | | `slot=` | `file`, `module`, `sub`, `class` or `method` --- what to generate around it | inferred | | `inherits=` | the class the sample is code-behind *of*; forces the Class row | --- | @@ -387,10 +387,20 @@ force two samples apart, and a rule that also tracked procedures would split bat costing a whole IDE startup --- for nothing. **`[RunAfterBuild]` is one per project.** `TB5114 encountered too many [RunAfterBuild] -attributes. Only allowed one per-project.` So `check_run` cannot batch the naive way: either -a project per sample, or one generated dispatcher that calls each sample's Sub in turn, with -a marker line printed around each call so the output can be attributed and a sample that -throws does not silently swallow the rest. +attributes. Only allowed one per-project.` So `check_run` cannot batch the naive way. It +uses one generated dispatcher per run batch (`Module tbxRun`, `lib/example-run.mjs`'s +`dispatcherText`) that calls each sample's `tbxBody` in turn, with a marker line printed +around each call so the output can be attributed and a sample that throws does not silently +swallow the rest. **Each call has an `On Error GoTo` handler of its own, not `On Error +Resume Next` and a test of `Err.Number` after the call.** A procedure that handles an error +with `On Error Resume Next` and returns, by `End Sub` or `Exit Sub`, leaves `Err` set for its +caller, in VB6 and in twinBASIC alike; only a `Resume` from an `On Error GoTo` handler, or +any `On Error` statement, clears it (measured, VB6 against BETA 995, identical; kit +`s69/errpersist/`). So the first dispatcher reported InStr.md's sample that demonstrates +error 5 under its own `On Error Resume Next` as raising it. The caller's handler is reached +only by an error the callee did not handle (measured: `InStr(0, "abc", "a")` unhandled in a +sample's body printed `[tbx-run] error 5 Invalid procedure call or argument`, and the next +sample ran), and `Resume tbxNext` clears it before the next call. Two things a batch runner must do that a single-fence runner need not: @@ -537,6 +547,75 @@ quiet about it deliberately, on the grounds that the filter is the caller's own says so as an advisory finding. **A narrowed run's results are not a full run's, and the tool has to be the thing that says which.** +### `--build` and `--llvm`: what a compile does not ask + +**A compile asks the front end; code generation runs in a build.** A sample can compile +clean and still fail the build, and under LLVM the IDE reports "a feature used in your code +is not yet supported with the LLVM compiler". `--build` presses Build on each project whose +compile has no errors, through `compileProject`'s `build` option, and `--llvm` (which implies +`--build`) writes `+llvm` into each batch's `compiler.buildOptions` and +`compiler.debugOptions`. A plain `--build` run is the control for an `--llvm` one: a sample +that fails only the second is one LLVM cannot generate code for. + +- **A failed build is a crash, for isolation.** `compileProject` returns code 5 and + `buildStaged` turns it into `{ crashed: true, buildFailed: true, named: }`. + Nothing in a build log names a sample, so `runBatch` halves the batch, and `together` + finds a set that fails only in combination. The notes and findings are worded by kind: + "fails the build", and a second line naming "the LLVM build" under `--llvm` and "the + build" otherwise, with the advice to record it in `BUGS-TO-REPORT.md` if it is the + compiler's fault. The lane's `llvm` field chooses the wording. +- **The canary does not stop a build.** `[EnforceWarnings(TB0005)]` keeps its `#Warning` a + warning whatever the project's settings say, and a warning does not stop the IDE building: + a probe with the canary module beside it builds and runs (tbrun, exit 0, BETA 995). +- **A project with compile errors is compiled and not built.** The run ends by counting + the samples that only ever sat in such a project: "N sample(s) in batches with errors were + compiled but not built". A sample counts as built if any project that held it, a smaller + one from isolating a larger included, was built. +- **`--llvm` needs a Professional or Ultimate licence.** `llvmLicence` in `tb-ide.mjs`, + shared with `tbrun`, reads the status bar's licence once the compile has settled, and + `compileProject` returns code 2 for a Community or Personal one. +- **A build that ends the compiler writes no failure line.** A native exception during an + LLVM build (`NATIVE EXCEPTION: ACCESS_VIOLATION`, then "restarting from MEMORY") leaves + the log without a success or a failure line, so `buildProject` treats the exception line + as a failure; before that it waited out its whole timeout. +- **A `[RunAfterBuild]` that calls `Debug.Cls` would erase the build log.** `buildProject` + wraps the page's `clearDebugConsole` (`keepClears`, as `tbrun` does) and reads what each + clear after its mark erased in front of the console, so Tools.md's own tbrun sample builds. +- **A failed build is believed when it repeats.** `buildTwiceOnFailure` builds a failed + batch again before halving, and every part the halving builds the same way: a build can + fail once and pass when repeated, as one that reported nothing for 120 s did in a full + `--llvm` run (BETA 995). +- **Each IDE has a temp folder of its own**, `%TEMP%\tbbuild-tmp-`, set as its `TEMP` + and `TMP` by `launchIde`. IDEs building at once in one temp folder fail now and then with + `[TYPELIB] failed to finalize typelibrary. Disk error?` and `[LINKER] FAILED to create + type library`, on samples that build clean alone: 8 of 192 builds, eight at once, against + 0 of 192 with a folder each (BETA 995). With the folders, a full `--build` run and a full + `--llvm` run retried no type-library failure. The compiler imports `GetTempFileNameW`, and + writes the type library to a file it then reads back; a name two processes both take fits + the counts, but is not proved. +- **A project with errors says which.** `not built: b.twinproj has errors, the first ...` + prints when a compile has errors, and the run ends counting the samples never built. +- **A build adds three collision rules a compile does not have**, each measured on BETA 995 + as a false finding in the first full `--build` run: + - **`expect-error` samples are batched apart.** One such sample (Option.md, TB5079) left + the 129 samples beside it unbuilt, since a project with an error is not built. + - **A unit declaring its own `Sub Main` gets a project of its own, without `tbxMain`.** + Two Mains compile, but binding the startup object fails the build ("'Main' is + ambiguous"), as `tbxMain.twin`'s header says. `makeBatches`' `alone` picks the unit, + the batch carries `noMain`, and `stageBatch` leaves out `tbxMain.twin`. HelpFile, + PrevInstance, Project-Types and the two WinServicesLib groups failed this way. + - **A `[DllExport]` name counts among a sample's names.** Two samples exporting + `MyExportedFunction` (API-Declarations, Classes-and-Modules) compile together, and the + linker refuses them ("duplicate [DLLExport] functions detected"); as names, the batcher + keeps them apart. +- **A marked `Sub Main` in a class slot is a finding**, in a compile as in a build. It is a + method of the class, never the startup object, so the sample is no program: usually a + class and the module that starts it, written as one fence. A compile passes it and a + build fails with no startup object, so `select` refuses it before batching. Split it, with + the startup Main in a fence of its own. The class slot may come from `inherits=` or from + the inference: a top-level `WithEvents` field is TB5182 in a module, whether `Private`, + `Dim` or `Public` (measured on BETA 995). + ## Traps already paid for Each cost a run, either during the probing that produced this file or during the @@ -895,11 +974,10 @@ by grep. - Where does compile time stop being flat in fence count? 120 per project is the current default and 275 worked; the measured evidence does not say where the knee is. -- `check_run` needs the dispatcher design above, plus the `MsgBox` screen, plus a decision - about what a sample's *output* is compared against. A sample that prints is a sample whose - printed value the page probably states, and that is the check worth having. Deliberately - not started --- see [What is left](#what-is-left-179-samples-and-no-lever) for why the - editorial pass comes first. +- `check_run` is implemented (`lib/example-run.mjs`, `lib/tb-run.mjs`) but no fence in `docs/` + carries the marker yet. A sample states its output in its own text, as a trailing comment on a + `Debug.Print` line or as the comment lines under `' Output:`; which pages should carry one is + the editorial pass's question --- see [What is left](#what-is-left-179-samples-and-no-lever). - A `projname` is global, so two pages choosing `demo` would merge without saying so. Scoping it to the page would prevent that and would also prevent a group spanning pages, which a multi-page tutorial wants. Left global and documented; revisit if a collision happens. @@ -1014,11 +1092,9 @@ because grouping puts the two definitions in *one* project rather than keeping t and `COLLIDES` does not track procedure names. The page also calls `SaveLongData` while declaring `SaveData`, which is a defect in the page rather than in the harness. -**`check_run` waits for this work rather than the other way round.** No fence in `docs/` -carries the marker, so it gates nothing today; and its open question --- what a sample's -printed output is compared against --- is answered by pages that state a printed value, which -is what the editorial pass produces. Building the dispatcher first would be building for -candidates that do not exist yet. +**`check_run` gates nothing yet.** No fence in `docs/` carries the marker. Its output +comparison is only as useful as the pages that state a printed value, which is what the +editorial pass produces. ## The excerpts: 25 to none diff --git a/WIP.Harness.md b/WIP.Harness.md index e44c3b9c..2caaaada 100644 --- a/WIP.Harness.md +++ b/WIP.Harness.md @@ -457,7 +457,7 @@ compilation. That number was once guessed at "roughly 40 seconds" and is out by of four: time it before quoting it. **Concurrency works and is the route to a fast probe suite.** Distinct `--port` values give -distinct DevTools ports, WebView2 user-data folders and private desktops, so instances do +distinct DevTools ports, WebView2 user-data folders, temp folders and private desktops, so instances do not collide. Three projects: **26 s sequentially, 10 s in parallel**, with each run reporting its own diagnostics and no bleed between them. @@ -522,7 +522,8 @@ Four smaller things it knows, each of which cost a run: library`, `[BUILD] failed`. `tbrun` returned exactly that as the probe's output, with exit 0, twice in round 8's fix pass --- five runs going at once on ports 9740--9744, and both passed when repeated. It now exits 2 on a `[BUILD] failed` or `[LINKER] FAILED` line, which the - probe's own `Debug.Cls` would have erased. What made the type library fail was not isolated. + probe's own `Debug.Cls` would have erased. The type library failed because the IDEs shared + one temp folder; `launchIde` now gives each its own (see WIP.ExamplesBuild.md). Since the tooling review's C16 it exits 2 on any line `buildProject`'s `BUILD_FAILED` matches, which adds `[BUILD] ERROR` and `[LINKER] compilation (codegen) error`. The second was measured: a `[RunAfterBuild]` Sub that shifts a `Single` (BUGS-TO-REPORT.md) builds with @@ -548,6 +549,20 @@ Four smaller things it knows, each of which cost a run: remedies (rerun the second, isolate the probe for the first), and `check_examples` already isolated a sample on `tbbuild`'s 4. `tbrun` exits 3 for no output at all, and 2 for a compile that never settled, which `tbbuild` reports as 3. +- **`tbrun` exits 5 when the probe ended before it returned.** The quiet period cannot see + it: on BETA 995, `Err.Raise` with no handler in a `+llvm` procedure ends the run with + nothing in the console, and `tbrun` exited 0 with the output up to there. `End` does the + same, and so does an unhandled error in plain code, in the same time as a probe that + returns (20.7-21.9 s over three runs on BETA 995, against 18.6-20.4 s; one earlier 30 s + run came with a 25 s clean run beside it). `lib/tb-probe.mjs`'s `wrapProbe` moves the + attribute, blanked to spaces so diagnostics keep their positions, to a Sub appended to the + same module, so a Private probe Sub can still be called; that Sub prints a sentinel after + the call. A probe that leaves a form loaded returned too: the IDE then prints `[DEBUGGER] + Waiting for remaining forms to close...` after the sentinel, which `sentinelIndex` allows + and `tbrun` prints (measured on BETA 983 and 995). `check_twin_parsers` has fixtures for both. +- **`tbrun --exe` exits 6 when the exe exited with a code other than 0**, or was still + running at `--timeout` and was ended. A run that would exit 5 exits 5 first, since the + IDE's run is the one `--exe` follows. A reader of the console that is not `tbrun` should **compare the whole console before and after, not read on from an index**: new text can be appended to an entry that is still open. @@ -570,8 +585,8 @@ IDE escapes that continued text twice** ([BUGS-TO-REPORT.md](BUGS-TO-REPORT.md)) printing `&`, `<` or `>` after a `Debug.Print ...;` reads them back as `&`, `<` and `>`, which is also what the console shows. -It settles on a quiet period rather than a sentinel, so no probe has to print a marker the -script knows about. Distinct `--port` values let probes run concurrently, exactly as +It settles on the wrapper's sentinel, or else on a quiet period, so no probe has to print a +marker of its own. Distinct `--port` values let probes run concurrently, exactly as `tbbuild`'s do. **Two things make that safe, and both had to be built.** The workspace and `project.id` @@ -589,6 +604,32 @@ on an image allowlist, *and* windowless --- a new one that has a window is repor alone, since that cannot be told from a copy the user opened. `--no-reap` turns it off, and concurrent runs driving the same server should use it and sweep once at the end. +### Measuring LLVM + +`tbrun --llvm` sets `compiler.debugOptions` and `compiler.buildOptions` to `+llvm` in the +staged Settings; `--compiler-options` sets any other string. Measured on BETA 995 with no +per-procedure attributes, one probe in four variants of Settings: **`debugOptions` is what +the `[RunAfterBuild]` run is compiled with.** With `+llvm` there, `Debug.Assert`'s condition +was evaluated 0 times and a 200-million-step loop took 516 ms; with it only in +`buildOptions`, 1 time and 672 ms, as with neither. (`+llvm` alone is not +`+llvm +optimize`, which took the same loop to 62 ms as a procedure attribute.) + +**The licence is in the status bar's `compilerLicence`:** one of `COMMUNITY EDITION`, +`PERSONAL EDITION`, `PROFESSIONAL EDITION`, `ULTIMATE EDITION`, and `tB Licence: ...` +until the IDE knows (`ide/main.js`, BETA 995). The licence key is in HKCU, so a lane IDE +sees the user's. `tbrun` refuses an LLVM run on the first two, which compile no user code +with LLVM; it cannot be tested with a real Community licence here, only through a fault. + +**`--exe` runs the exe on a private desktop**, through `launchOnDesktop`, the part of +`launchIde` that calls `tb-launch.ps1`, which reports the program's exit code on a +second line (`exit `) once it ends. Nothing reaches the exe's standard output: the +launcher creates it with no inherited handles. So `TbRun.Out` writes UTF-8 to the file +`TBRUN_OUT` names, and to standard output only when there is none. Measured on BETA 995: +`App.IsInIDE` is True in a `[RunAfterBuild]` run and False in the exe, the exit code from +`ExitProcess 7` comes back as 7, a hung exe is ended at `--timeout` with nothing left +running, and **the exe never evaluates `Debug.Assert`, with or without LLVM**, as VB6 drops +`Debug` statements from a compiled program. + ### Building for win64 **`tbrun` and `tbbuild` take `--arch win32|win64`, and set it on every run, win32 diff --git a/WIP.md b/WIP.md index 9b896209..880f5ca5 100644 --- a/WIP.md +++ b/WIP.md @@ -94,7 +94,7 @@ The rest of this file is the maintenance guide for updating existing pages or ad - `docs/Reference/Built-In/tbIDE/` — IDE Extensibility package (this is the **addin SDK**). The package is type-only — it ships **public interfaces + CoClasses** that an addin DLL binds to; every implementation behind them lives in the twinBASIC IDE itself. The user-facing surface is one entry-point factory (`tbCreateCompilerAddin`) plus 23 CoClasses grouped by role: the addin contract (`AddIn`), the root API (`Host`), the loaded `Project`, the editors collection (`Editor` / `CodeEditor` / `Editors`), the virtual file system (`FileSystem` / `FileSystemItem` / `Folder` / `File`), the in-IDE UI surface (`Toolbar` / `Toolbars` / `Button` / `ToolWindow` / `ToolWindows`), the HTML DOM inside a tool window (`HtmlElement` / `HtmlElements` / `HtmlElementProperty` / `HtmlElementProperties` / `HtmlEventProperty` / `HtmlEventProperties`), the `DebugConsole`, `KeyboardShortcuts`, `Themes`, and the single concrete user-instantiable helper class `AddinTimer`. Flat layout — one page per CoClass / Class plus the index landing. - `docs/Reference/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**; the local BETA 983 compiler restricts `+llvm` to standard-module procedures and to Professional/Ultimate, and has no LLVM project settings at all. Both of the page's samples are marked `check_build` and compile clean on 983 --- but the harness asks only the front end, which accepts `[CompilerOptions]` with every CPU flag in it; nothing runs LLVM code generation, so a clean run says nothing about the 984 behaviour the page describes. +- `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. **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. @@ -136,11 +136,12 @@ 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**, 8--11 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 and desktops. +- **One project per IDE**, 8--11 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. +- **`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`. +- **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)). - **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. @@ -227,6 +228,8 @@ 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. + ### Replace | Term | Use instead | @@ -463,9 +466,10 @@ Why the report separates the wedged task from the merely blocked ones, and why - `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`), `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)). -- `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). +- `examples.bat` — compiles the documentation's own twinBASIC code samples, every `tb` fence marked `check_build`, and reports the ones the compiler refuses against the line in the page they came from; a statement sample also marked `check_run` is built and run, and what it prints is checked against the page. Needs a twinBASIC install and Windows, so it is outside every gate and outside CI; ~120 s over the 1,136 samples marked. `--build` also builds each project that compiles clean, and `--llvm` builds it with LLVM (see [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md)). Two modes need no compiler at all: `--census` classifies every fence and says how many classifiable ones are still unmarked, and `--report ` groups a saved `--propose --json` survey by diagnostic, section and unresolved name. `--propose` itself does compile. See [Compiling the reference's own code samples](#compiling-the-references-own-code-samples) and [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md). - `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. +- `node scripts/bug_repro.mjs` --- the reproducer projects under `bugs//` for the entries of [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md): `new`, `pack` (impexp, then the zip, written in Node), `compile`, `build` and `run` through `tbbuild` and `tbrun`, `file` (moves a filed entry and its reproducer to `bugs/filed//`; `file --marked` does every marked entry), and `verify`, which reads each `bugs/*/repro.json` and `bugs/filed/*/repro.json` and says whether the bug still reproduces on the newest beta. It has no wrapper. **`verify` is run by a person only**, never by a gate or CI: it needs a twinBASIC install, like `examples.bat`. Default port 9440, and `--jobs N` takes N ports from there. Its command-line cases are in `scripts/lib/cli-cases.mjs`, and need no IDE. 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). @@ -521,7 +525,7 @@ wrapper: | `test.bat` | `test/search.test.mjs` | the search entries `builder/search.mjs` writes hold what they should, and the copies of the search client still agree. Run by `node --test`; the gate roster reads such a line as a gate, named by its path | | `test.bat` | `test/render.test.mjs` | `builder/render.mjs`'s markdown-it plugins, one at a time, on inputs no page holds --- today the ellipsis plugin's dot counting. Run by `node --test` | | `test.bat` | `test/strftime.test.mjs` | `builder/strftime.mjs`'s `formatDate`, which no build calls because no page sets `last_modified_date`. Run by `node --test` | -| `test.bat` | `test/example-batches.test.mjs` | `check_examples.mjs`'s probes, from `scripts/lib/example-batches.mjs`: batching, crash isolation through a fake lane, the canaries and the fence classifier, with no IDE. Run by `node --test` | +| `test.bat` | `test/example-batches.test.mjs` | `check_examples.mjs`'s probes, from `scripts/lib/example-batches.mjs`: batching, crash isolation through a fake lane, the canaries, the fence classifier and `check_run`'s refusals, expected output, dispatcher and marker reading, with no IDE. Run by `node --test` | | `test.bat` | `check_pdf_shims_equiv` | the book's pdf-lib shims write what stock pdf-lib writes: one document written by the gate and one built with `PDFDocument.create` are each saved both ways in child processes, and each pair of files is compared object by object with streams inflated and their cross-reference entries checked; every shim must run, and the members of pdf-lib the shims patch must be those its `PATCHES` lists, each run in one document or the other unless marked there as not reached | | `test.bat` | `check_impexp_parity` | `impexp.mjs` and `impexp.py` pass the same built-in tests, and one sequence of commands through each gives the exit code each command is there for, the same output and the same files. Without Python it reports itself skipped and passes, but fails when `CI=true` | | `test.bat` | `check_publish_policy`, `check_gate_lists`, `check_page_baseline`, `check_book_coverage`, `check_axe_patch_equiv` | the gates on the gates | @@ -564,7 +568,7 @@ The pre-commit hook in `.githooks/` runs it on the staged scripts and nothing el in a clone with `git config core.hooksPath .githooks`. **A bug in twinBASIC itself goes in [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md)**, which is a -queue rather than a record: an entry is deleted once it has been filed upstream. Each one +queue rather than a record: an entry that has been filed upstream is marked and moved to `bugs/filed//` by `bug_repro.mjs file --marked`, and deleted only once a fix is released and `verify` says it no longer reproduces. Each one carries the build it was seen on and a *narrowed* reproduction --- the compiler crash recorded there is two lines, and neither line reproduces it alone. Documentation defects do not go there; they are fixed in `docs/`, or recorded in the relevant `WIP.*.md` until diff --git a/bugs/build-and-exit-silent/build-and-exit-silent.twinproj b/bugs/build-and-exit-silent/build-and-exit-silent.twinproj new file mode 100644 index 00000000..1f91b33e Binary files /dev/null and b/bugs/build-and-exit-silent/build-and-exit-silent.twinproj differ diff --git a/bugs/build-and-exit-silent/repro.json b/bugs/build-and-exit-silent/repro.json new file mode 100644 index 00000000..e2887e78 --- /dev/null +++ b/bugs/build-and-exit-silent/repro.json @@ -0,0 +1,4 @@ +{ + "mode": "manual", + "steps": "Not automatable: --buildAndExit32 is a switch of the IDE executable twinBASIC.exe, which opens a window. 1. Pack the project (node scripts/bug_repro.mjs pack build-and-exit-silent) or unzip build-and-exit-silent.zip. 2. Open it in the IDE: 1 error, TB5079 at Startup.twin line 8. 3. Run: twinBASIC.exe --buildAndExit32 \\build-and-exit-silent.twinproj. 4. See it exit 0 after a few seconds with Build\\BuildAndExitSilent_win32.exe written, and nothing about the build on stdout or stderr. 5. Edit Main to call Unused, pack again and repeat: the command never exits and writes no .exe. End the hung IDE by its pid, never by image name." +} diff --git a/bugs/build-and-exit-silent/src/Settings b/bugs/build-and-exit-silent/src/Settings new file mode 100644 index 00000000..40b317da --- /dev/null +++ b/bugs/build-and-exit-silent/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "BuildAndExitSilent", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `--buildAndExit32` writes nothing, exits 0 on a project with errors, and hangs on a failing build", + "project.exportPathIsV2": true, + "project.id": "{A4E66768-51AD-4F94-AD4F-8B54999E31B4}", + "project.name": "BuildAndExitSilent", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/build-and-exit-silent/src/Sources/Startup.twin b/bugs/build-and-exit-silent/src/Sources/Startup.twin new file mode 100644 index 00000000..38ccb5e4 --- /dev/null +++ b/bugs/build-and-exit-silent/src/Sources/Startup.twin @@ -0,0 +1,11 @@ +' Unused has an error (TB5079). twinBASIC.exe --buildAndExit32 exits 0 on this project; make Main call Unused and it never exits. +Module Startup + + Public Sub Main() + End Sub + + Private Sub Unused() + NoSuchProcedure + End Sub + +End Module diff --git a/bugs/concurrent-builds-shared-temp/concurrent-builds-shared-temp.twinproj b/bugs/concurrent-builds-shared-temp/concurrent-builds-shared-temp.twinproj new file mode 100644 index 00000000..7ee685cb Binary files /dev/null and b/bugs/concurrent-builds-shared-temp/concurrent-builds-shared-temp.twinproj differ diff --git a/bugs/concurrent-builds-shared-temp/repro.json b/bugs/concurrent-builds-shared-temp/repro.json new file mode 100644 index 00000000..3d06ed65 --- /dev/null +++ b/bugs/concurrent-builds-shared-temp/repro.json @@ -0,0 +1,4 @@ +{ + "mode": "manual", + "steps": "Needs several IDEs building at once, which bug_repro cannot run.\n1. Unzip concurrent-builds-shared-temp.zip and copy the project into eight folders.\n2. Open each copy in its own IDE, all eight at once, with the default environment, so that all share one TEMP.\n3. Press Build in all eight at about the same time, and repeat. About one build in 24 fails with [TYPELIB] failed to finalize typelibrary. Disk error? then [LINKER] FAILED to create type library and [BUILD] failed.\n4. Control: give each IDE a TEMP and TMP of its own; no build fails.\nMeasured on BETA 995 through the documentation's harness with TEMP shared (8 of 192 failed) and per IDE (0 of 192). launchIde gives each IDE its own TEMP today, so pass TEMP and TMP in launchIde's env to measure it again." +} diff --git a/bugs/concurrent-builds-shared-temp/src/Settings b/bugs/concurrent-builds-shared-temp/src/Settings new file mode 100644 index 00000000..4fffbb12 --- /dev/null +++ b/bugs/concurrent-builds-shared-temp/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ConcurrentBuildsSharedTemp", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Builds running at the same time in one TEMP folder sometimes fail to write the type library", + "project.exportPathIsV2": true, + "project.id": "{F3F2C6A4-A713-4F53-9B49-830560B557F9}", + "project.name": "ConcurrentBuildsSharedTemp", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/concurrent-builds-shared-temp/src/Sources/Startup.twin b/bugs/concurrent-builds-shared-temp/src/Sources/Startup.twin new file mode 100644 index 00000000..20233b3a --- /dev/null +++ b/bugs/concurrent-builds-shared-temp/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/damaged-project-modal-box/damaged-project-modal-box.twinproj b/bugs/damaged-project-modal-box/damaged-project-modal-box.twinproj new file mode 100644 index 00000000..fd13e0e9 Binary files /dev/null and b/bugs/damaged-project-modal-box/damaged-project-modal-box.twinproj differ diff --git a/bugs/damaged-project-modal-box/repro.json b/bugs/damaged-project-modal-box/repro.json new file mode 100644 index 00000000..96eb76ad --- /dev/null +++ b/bugs/damaged-project-modal-box/repro.json @@ -0,0 +1,4 @@ +{ + "mode": "manual", + "steps": "Not automatable, and not run unattended: the compiler executable opens a modal message box on the desktop of whoever runs it, and waits until it is closed. 1. Pack the project (node scripts/bug_repro.mjs pack damaged-project-modal-box). 2. Make a damaged copy: change the first byte of the .twinproj, or cut it off halfway, or write any short text file with the name garbage.twinproj. 3. Run: bin\\twinBASIC_win32.exe export \\garbage.twinproj \\out\\ and see a message box (invalid header, or invalid file format) and no further output until it is closed. 4. Close the box: export prints WARNING: failed to parse project file, file may be corrupt, then ... DONE, and exits 0. settings and readme wait on the box the same way." +} diff --git a/bugs/damaged-project-modal-box/src/Settings b/bugs/damaged-project-modal-box/src/Settings new file mode 100644 index 00000000..0c563041 --- /dev/null +++ b/bugs/damaged-project-modal-box/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "DamagedProjectModalBox", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: A damaged project file opens a message box, and the command waits until it is closed", + "project.exportPathIsV2": true, + "project.id": "{2862891C-B9AA-41DB-AF65-18C3C0261364}", + "project.name": "DamagedProjectModalBox", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/damaged-project-modal-box/src/Sources/Startup.twin b/bugs/damaged-project-modal-box/src/Sources/Startup.twin new file mode 100644 index 00000000..bb163686 --- /dev/null +++ b/bugs/damaged-project-modal-box/src/Sources/Startup.twin @@ -0,0 +1,7 @@ +' An ordinary, valid project. Nothing to run: a copy of its .twinproj with the first byte changed, or cut off halfway, is the damaged file. +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/export-needs-backslashes/export-needs-backslashes.twinproj b/bugs/export-needs-backslashes/export-needs-backslashes.twinproj new file mode 100644 index 00000000..4f110788 Binary files /dev/null and b/bugs/export-needs-backslashes/export-needs-backslashes.twinproj differ diff --git a/bugs/export-needs-backslashes/repro.json b/bugs/export-needs-backslashes/repro.json new file mode 100644 index 00000000..e81070f4 --- /dev/null +++ b/bugs/export-needs-backslashes/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "cli", + "cli": ["export", "{project}", "{tmp}/"], + "expect": { + "exit": 0, + "output": ["ERROR: output folder does not exist and could not be created", "^\\.\\.\\. FAILED"] + } +} diff --git a/bugs/export-needs-backslashes/src/Settings b/bugs/export-needs-backslashes/src/Settings new file mode 100644 index 00000000..3db39cc5 --- /dev/null +++ b/bugs/export-needs-backslashes/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ExportNeedsBackslashes", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `export` needs a full, backslashed project path, and no folder path may use forward slashes", + "project.exportPathIsV2": true, + "project.id": "{2B5FF908-D321-4B02-9501-159483C29DBA}", + "project.name": "ExportNeedsBackslashes", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/export-needs-backslashes/src/Sources/Startup.twin b/bugs/export-needs-backslashes/src/Sources/Startup.twin new file mode 100644 index 00000000..cefaf83e --- /dev/null +++ b/bugs/export-needs-backslashes/src/Sources/Startup.twin @@ -0,0 +1,7 @@ +' An ordinary project. Nothing to run: export of this file fails when a path is relative or uses forward slashes. +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/export-refused-partial-write/export-refused-partial-write.twinproj b/bugs/export-refused-partial-write/export-refused-partial-write.twinproj new file mode 100644 index 00000000..e3e6d75d Binary files /dev/null and b/bugs/export-refused-partial-write/export-refused-partial-write.twinproj differ diff --git a/bugs/export-refused-partial-write/repro.json b/bugs/export-refused-partial-write/repro.json new file mode 100644 index 00000000..b6cfa887 --- /dev/null +++ b/bugs/export-refused-partial-write/repro.json @@ -0,0 +1,12 @@ +{ + "mode": "cli", + "cli": ["export", "{project}", "{tmp}\\"], + "expect": { + "exit": 0, + "output": [ + "ERROR: output file already exists and --overwrite not set: .*export-refused-partial-write\\.twinproj", + "DONE: .*\\\\Settings$", + "^\\.\\.\\. FAILED" + ] + } +} diff --git a/bugs/export-refused-partial-write/src/Settings b/bugs/export-refused-partial-write/src/Settings new file mode 100644 index 00000000..004b01f9 --- /dev/null +++ b/bugs/export-refused-partial-write/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ExportRefusedPartialWrite", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `export` refused for lack of `--overwrite` still writes part of the tree", + "project.exportPathIsV2": true, + "project.id": "{7859DCBC-B904-435C-8224-FF1870516258}", + "project.name": "ExportRefusedPartialWrite", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/export-refused-partial-write/src/Sources/Startup.twin b/bugs/export-refused-partial-write/src/Sources/Startup.twin new file mode 100644 index 00000000..b6c90730 --- /dev/null +++ b/bugs/export-refused-partial-write/src/Sources/Startup.twin @@ -0,0 +1,7 @@ +' An ordinary project. Nothing to run: export of it into a folder that already holds part of the tree is refused, and still writes the rest. +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/export-refused-partial-write/src/export-refused-partial-write.twinproj b/bugs/export-refused-partial-write/src/export-refused-partial-write.twinproj new file mode 100644 index 00000000..20f7b4c4 --- /dev/null +++ b/bugs/export-refused-partial-write/src/export-refused-partial-write.twinproj @@ -0,0 +1 @@ +This file stands for one that is already in the folder the project is exported to. diff --git a/bugs/export-stops-at-readonly-file/export-stops-at-readonly-file.twinproj b/bugs/export-stops-at-readonly-file/export-stops-at-readonly-file.twinproj new file mode 100644 index 00000000..723bc5c7 Binary files /dev/null and b/bugs/export-stops-at-readonly-file/export-stops-at-readonly-file.twinproj differ diff --git a/bugs/export-stops-at-readonly-file/repro.json b/bugs/export-stops-at-readonly-file/repro.json new file mode 100644 index 00000000..9160f848 --- /dev/null +++ b/bugs/export-stops-at-readonly-file/repro.json @@ -0,0 +1,4 @@ +{ + "mode": "manual", + "steps": "Do it in a scratch folder made for this, below (for example C:\\Scratch\\readonly-test). File > Export Project empties \\export.\n1. Unzip export-stops-at-readonly-file.zip into , so that \\export-stops-at-readonly-file.twinproj exists.\n2. In a Command Prompt: mkdir \\export, then make three files in it, a-first.txt, m-readonly.txt and z-last.txt (echo x> \\export\\a-first.txt, and so on), and attrib +R \\export\\m-readonly.txt.\n3. Open the project in the twinBASIC IDE. Its Export Path is ${SourcePath}\\export and Export Verbose is on (Project Settings, Export).\n4. Choose File > Export Project.\n5. See, as the last three lines of the DEBUG CONSOLE: [EXPORT] DELETE FAILED: \\\\?\\\\export\\m-readonly.txt, [EXPORT] ERROR: unable to clean the output folder and [EXPORT] export failed. No dialog opens. \\export holds m-readonly.txt and z-last.txt: a-first.txt, which sorts before the read-only file, is deleted, and nothing was exported.\n6. Afterwards: attrib -R \\export\\m-readonly.txt.\nRe-measured on BETA 995 by `ide-test.bat --only export`." +} diff --git a/bugs/export-stops-at-readonly-file/src/Settings b/bugs/export-stops-at-readonly-file/src/Settings new file mode 100644 index 00000000..9155c1d1 --- /dev/null +++ b/bugs/export-stops-at-readonly-file/src/Settings @@ -0,0 +1,63 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ExportStopsAtReadonlyFile", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Export Project stops at a read-only file after deleting everything before it, and the IDE reports nothing", + "project.exportPath": "${SourcePath}\\export", + "project.exportPathIsV2": true, + "project.exportVerbose": true, + "project.id": "{378AAC1E-FD9C-4E87-A739-94995FCA08C7}", + "project.name": "ExportStopsAtReadonlyFile", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/export-stops-at-readonly-file/src/Sources/Startup.twin b/bugs/export-stops-at-readonly-file/src/Sources/Startup.twin new file mode 100644 index 00000000..1cd8339b --- /dev/null +++ b/bugs/export-stops-at-readonly-file/src/Sources/Startup.twin @@ -0,0 +1,5 @@ +' Nothing to run. The bug is in File > Export Project, which the steps in the report set up. +Module Startup + Public Sub Main() + End Sub +End Module diff --git a/bugs/filed/addin-shortcut-ctrl-alt/REPORT.md b/bugs/filed/addin-shortcut-ctrl-alt/REPORT.md new file mode 100644 index 00000000..c2851bcf --- /dev/null +++ b/bugs/filed/addin-shortcut-ctrl-alt/REPORT.md @@ -0,0 +1,55 @@ +Filed as [twinbasic/twinbasic#2445](https://github.com/twinbasic/twinbasic/issues/2445). + +## An add-in's keyboard shortcut does not fire if it includes `{CTRL}` or `{ALT}` + +**Describe the bug** +A shortcut an add-in registers with `Host.KeyboardShortcuts.Add` does not fire when its key string includes `{CTRL}` or `{ALT}`. The SDK's own example, `{CTRL}{SHIFT}d` in the description of `KeyboardShortcuts.Add`, cannot be used, and nothing says why. An add-in that registers + +``` +Host.KeyboardShortcuts.Add "{CTRL}{SHIFT}d", AddressOf OnCtrlShiftD +Host.KeyboardShortcuts.Add "{SHIFT}d", AddressOf OnShiftD +``` + +gets `OnShiftD` for Shift+D, and nothing at all for Ctrl+Shift+D. + +**To Reproduce** +`addin-shortcut-ctrl-alt` is an add-in project: building it makes a DLL that the IDE's compiler loads on every start until the DLL is removed. Install it knowingly, in an IDE you can restart, and remove it afterwards. + +Steps to reproduce the behavior: +1. Open `addin-shortcut-ctrl-alt.twinproj` (attached as `addin-shortcut-ctrl-alt.zip`) and build it for Win32. The DLL is written to `Build\AddinShortcutCtrlAlt_win32.dll` beside the project file. Close the IDE, copy the DLL into the IDE's `addins\win32` folder (beside `twinBASIC.exe`), and start the IDE again. Open any project: the Debug Console shows `[KeysProbe] registered`. The add-in registers eight key strings and prints `[KeysProbe] fired ` when one fires: + ``` + .Add "{CTRL}{SHIFT}d", AddressOf CtrlShiftD + .Add "{ctrl}d", AddressOf CtrlD + .Add "{ALT}f", AddressOf AltF + .Add "{SHIFT}D", AddressOf ShiftD + .Add "d", AddressOf PlainD + .Add "F1", AddressOf F1 + .Add "{shift}f1", AddressOf ShiftF1 + .Add "q", AddressOf PlainQ + ``` +2. With no text box focused (click an empty part of the toolbar), press D, Shift+D, F1 and Shift+F1, a second apart. Each prints a `fired` line: `d`, `{shift}d`, `f1`, `{shift}f1`. +3. Press Ctrl+Shift+D, Ctrl+D and Alt+F, a second apart. Nothing is printed for any of them. +4. Press D, then within half a second Ctrl+D and Ctrl+Shift+D; then F, and within half a second Alt+F. Now all of them print. +5. Remove the add-in: close the IDE and delete `addins\win32\AddinShortcutCtrlAlt_win32.dll`. + +| registered | pressed | fires | +|---|---|---| +| `d`, `{SHIFT}D`, `F1`, `{shift}f1` | D, Shift+D, F1, Shift+F1 | yes | +| `{CTRL}{SHIFT}d`, `{ctrl}d`, `{ALT}f` | Ctrl+Shift+D, Ctrl+D, Alt+F, each more than 0.5 s after any other press of D or F | **no** | +| the same three | D alone, then Ctrl+D and Ctrl+Shift+D; F alone, then Alt+F, all inside 0.5 s | yes, all three | + +**Expected behavior** +Each registered shortcut fires when its keys are pressed, whatever modifiers it names. The SDK's example, `{CTRL}{SHIFT}d`, works. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Severity: the SDK's own example cannot be used, and nothing says why. + +The last row of the table shows the cause. `globalKeyUp` in `ide/main.js` matches an add-in's shortcut when the key is released, and only if `realKeyPresses` holds a press of the same key from less than 500 ms before. `globalKeyDown` records a press only `if((!e.ctrlKey||e.key==="Control")&&(!e.altKey||e.key==="Alt"))`, so a key pressed with Ctrl or Alt held is never recorded. Its release finds either no press, or an earlier one of the same key made without the modifier. The IDE's own bindings are unaffected, because they are matched on the key-down. + +A smaller point for the same fix: `KeyboardShortcuts.Add` stores the string as given, lowercased and without spaces, and the key-up builds the string it looks up as `{ctrl}`, `{shift}`, `{alt}` and the key, in that order. So `{SHIFT}{CTRL}d` could never match even with the recording fixed. + + diff --git a/bugs/filed/addin-shortcut-ctrl-alt/addin-shortcut-ctrl-alt.twinproj b/bugs/filed/addin-shortcut-ctrl-alt/addin-shortcut-ctrl-alt.twinproj new file mode 100644 index 00000000..bf2082c9 Binary files /dev/null and b/bugs/filed/addin-shortcut-ctrl-alt/addin-shortcut-ctrl-alt.twinproj differ diff --git a/bugs/filed/addin-shortcut-ctrl-alt/repro.json b/bugs/filed/addin-shortcut-ctrl-alt/repro.json new file mode 100644 index 00000000..5f704479 --- /dev/null +++ b/bugs/filed/addin-shortcut-ctrl-alt/repro.json @@ -0,0 +1,5 @@ +{ + "mode": "manual", + "steps": "This is an add-in project: building it makes a DLL that the IDE's compiler loads on every start until the DLL is removed. Install it knowingly, in an IDE you can restart, and remove it afterwards.\n1. Open addin-shortcut-ctrl-alt.twinproj and build it for Win32. The DLL is written to Build\\AddinShortcutCtrlAlt_win32.dll beside the project file. Close the IDE, copy the DLL into the IDE's addins\\win32 folder (beside twinBASIC.exe), and start the IDE again. Open any project: the DEBUG CONSOLE shows [KeysProbe] registered.\n2. With no text box focused (click an empty part of the toolbar), press, one at a time and a second apart: D, Shift+D, F1, Shift+F1. Each prints [KeysProbe] fired , as d, {shift}d, f1 and {shift}f1.\n3. Press Ctrl+Shift+D, Ctrl+D and Alt+F, a second apart. Nothing is printed for any of them, although the add-in registered {CTRL}{SHIFT}d, {ctrl}d and {ALT}f.\n4. Press D, then within half a second Ctrl+D, Ctrl+Shift+D, then F and Alt+F: now all of them print.\n5. Remove the add-in: close the IDE and delete addins\\win32\\AddinShortcutCtrlAlt_win32.dll.\nRe-measured on BETA 995 by `addin-test.bat --only keys` (the project is test/addin/probes/keys; the lane builds it into a private copy of the install and presses the keys as CDP key events).", + "issue": 2445 +} diff --git a/bugs/filed/addin-shortcut-ctrl-alt/src/Settings b/bugs/filed/addin-shortcut-ctrl-alt/src/Settings new file mode 100644 index 00000000..85ca859f --- /dev/null +++ b/bugs/filed/addin-shortcut-ctrl-alt/src/Settings @@ -0,0 +1,42 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "AddinShortcutCtrlAlt", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard DLL", + "project.description": "Reproduces: An add-in's keyboard shortcut does not fire if it includes `{CTRL}` or `{ALT}`", + "project.exportPathIsV2": true, + "project.id": "{68070481-D097-46F3-B6F9-3EEA2470F03D}", + "project.name": "AddinShortcutCtrlAlt", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{99DEC38C-75F6-4488-8EE7-2D52D83881D2}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC IDE Extensibility Package", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "tbIDE", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 1 + } + ], + "project.settingsVersion": 1, + "project.warnings": { + "errors": [], + "ignored": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/addin-shortcut-ctrl-alt/src/Sources/MainModule.twin b/bugs/filed/addin-shortcut-ctrl-alt/src/Sources/MainModule.twin new file mode 100644 index 00000000..0faa2882 --- /dev/null +++ b/bugs/filed/addin-shortcut-ctrl-alt/src/Sources/MainModule.twin @@ -0,0 +1,74 @@ +' An add-in that registers eight keyboard shortcuts and prints "[KeysProbe] fired " to the DEBUG CONSOLE when one fires. +Module MainModule + [DllExport] + Public Function tbCreateCompilerAddin(ByVal Host As Host) As AddIn + Return New KeysProbeAddin(Host) + End Function +End Module + +' Not "KeysProbe": that is the project's name, and a class named after its own +' project is TB5139, "a project or library, not a datatype". +Private Class KeysProbeAddin + Implements AddIn + Private WithEvents Host As Host + + Public Sub New(ByVal Host As Host) + Set Me.Host = Host + End Sub + + Private Property Get AddIn_Name() As String + Return "KeysProbe AddIn" + End Property + + ' Upper case in some strings on purpose: KeyboardShortcuts.Add lowercases + ' what it is given, and the scenario checks that it did. + Private Sub Host_OnProjectLoaded() + With Host.KeyboardShortcuts + .Add "{CTRL}{SHIFT}d", AddressOf CtrlShiftD + .Add "{ctrl}d", AddressOf CtrlD + .Add "{ALT}f", AddressOf AltF + .Add "{SHIFT}D", AddressOf ShiftD + .Add "d", AddressOf PlainD + .Add "F1", AddressOf F1 + .Add "{shift}f1", AddressOf ShiftF1 + .Add "q", AddressOf PlainQ + End With + Host.DebugConsole.PrintText "[KeysProbe] registered" + End Sub + + Private Sub Fired(ByVal keyString As String) + Host.DebugConsole.PrintText "[KeysProbe] fired " & keyString + End Sub + + Private Sub CtrlShiftD() + Fired "{ctrl}{shift}d" + End Sub + + Private Sub CtrlD() + Fired "{ctrl}d" + End Sub + + Private Sub AltF() + Fired "{alt}f" + End Sub + + Private Sub ShiftD() + Fired "{shift}d" + End Sub + + Private Sub PlainD() + Fired "d" + End Sub + + Private Sub F1() + Fired "f1" + End Sub + + Private Sub ShiftF1() + Fired "{shift}f1" + End Sub + + Private Sub PlainQ() + Fired "q" + End Sub +End Class diff --git a/bugs/filed/asnew-optional-ctor/REPORT.md b/bugs/filed/asnew-optional-ctor/REPORT.md new file mode 100644 index 00000000..639f9bd0 --- /dev/null +++ b/bugs/filed/asnew-optional-ctor/REPORT.md @@ -0,0 +1,40 @@ +Filed as [twinbasic/twinbasic#2459](https://github.com/twinbasic/twinbasic/issues/2459). + +## `As New` refuses a class whose only constructor has all-`Optional` arguments + +**Describe the bug** +`Dim x As New C` fails with TB5121 when the only constructor of `C` has nothing but `Optional` arguments, so it can be called with none. The same class passes 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. The two checks for "can this class be created without arguments" disagree. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `asnew-optional-ctor.twinproj` (attached as `asnew-optional-ctor.zip`). Its one source file, `Probe.twin`, holds the whole bug: + ``` + 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 + ``` +2. See the project fail to compile, on the `Dim`, with `TB5121 can't use this type with As-New syntax as it doesn't have a parameterless constructor`. + +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. + +**Expected behavior** +The project compiles, and `x.V` prints `3`, as it does on BETA 983. A constructor whose arguments are all `Optional` can be called without arguments, which is what `As New` needs. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +BETA 983 accepts the reproducer and runs it. 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. Severity: code that compiled before BETA 993 stops compiling. + + diff --git a/bugs/filed/asnew-optional-ctor/asnew-optional-ctor.twinproj b/bugs/filed/asnew-optional-ctor/asnew-optional-ctor.twinproj new file mode 100644 index 00000000..96098ce6 Binary files /dev/null and b/bugs/filed/asnew-optional-ctor/asnew-optional-ctor.twinproj differ diff --git a/bugs/filed/asnew-optional-ctor/repro.json b/bugs/filed/asnew-optional-ctor/repro.json new file mode 100644 index 00000000..8d3ed22b --- /dev/null +++ b/bugs/filed/asnew-optional-ctor/repro.json @@ -0,0 +1,9 @@ +{ + "mode": "compile", + "expect": { + "diagnostics": [ + "TB5121" + ] + }, + "issue": 2459 +} diff --git a/bugs/filed/asnew-optional-ctor/src/Settings b/bugs/filed/asnew-optional-ctor/src/Settings new file mode 100644 index 00000000..d1f654b7 --- /dev/null +++ b/bugs/filed/asnew-optional-ctor/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "AsnewOptionalCtor", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `As New` refuses a class whose only constructor has all-`Optional` arguments", + "project.exportPathIsV2": true, + "project.id": "{3E2698F7-6F1E-4F69-B66D-7135C879453E}", + "project.name": "AsnewOptionalCtor", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/asnew-optional-ctor/src/Sources/Probe.twin b/bugs/filed/asnew-optional-ctor/src/Sources/Probe.twin new file mode 100644 index 00000000..16a1482a --- /dev/null +++ b/bugs/filed/asnew-optional-ctor/src/Sources/Probe.twin @@ -0,0 +1,14 @@ +' As New refuses a class whose only constructor has all-Optional arguments (TB5121), though the same class passes the COM-exposure check. +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 diff --git a/bugs/filed/asnew-optional-ctor/src/Sources/Startup.twin b/bugs/filed/asnew-optional-ctor/src/Sources/Startup.twin new file mode 100644 index 00000000..20233b3a --- /dev/null +++ b/bugs/filed/asnew-optional-ctor/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/filed/boolean-intdiv-string/REPORT.md b/bugs/filed/boolean-intdiv-string/REPORT.md new file mode 100644 index 00000000..3003404b --- /dev/null +++ b/bugs/filed/boolean-intdiv-string/REPORT.md @@ -0,0 +1,34 @@ +Filed as [twinbasic/twinbasic#2439](https://github.com/twinbasic/twinbasic/issues/2439). + +## `Boolean \ String` and `Boolean Mod String` convert the `String` to `Boolean` + +**Describe the bug** +With a `Boolean` on the left and a `String` on the right, `\` and `Mod` convert the `String` to `Boolean` instead of to a number, so the result has the wrong value and the wrong type. There is no diagnostic. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `boolean-intdiv-string.twinproj` (attached as `boolean-intdiv-string.zip`) and run it (F5). Its `Sub Main` holds the whole bug: + ``` + Dim b As Boolean = True + Debug.Print TypeName(b \ "2") + ``` +2. See `Boolean`, where `Long` is expected. The project prints these: + + | expression | result | expected | + |---|---|---| + | `b \ "2"` | `Boolean` True | `Long` 0, as `b \ 2.0` gives | + | `b Mod "2"` | `Boolean` False | `Long` -1, as `b Mod 2.0` gives | + +The results are consistent with converting `"2"` to `Boolean` (`True`, -1) first: -1 \ -1 is 1, stored as `True`, and -1 Mod -1 is 0, stored as `False`. A `String` literal and a `String` variable on the right both reproduce it. + +**Expected behavior** +The `String` should be converted to a number, as every other operator does: `b + "2"` is the `Double` 1 and `b / "2"` the `Double` -0.5. `b \ "2"` should be the `Long` 0, and `b Mod "2"` the `Long` -1, the same as with the number 2.0. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +What does not reproduce it: every other operator converts the `String` to a number, as `b + "2"` and `b / "2"` above show, and so do `\` and `Mod` with the operands the other way round: `"2" \ b` is the `Long` -2. Also on BETA 983, with the same results. Severity: a wrong value and a wrong type, with no diagnostic. + + diff --git a/bugs/filed/boolean-intdiv-string/boolean-intdiv-string.twinproj b/bugs/filed/boolean-intdiv-string/boolean-intdiv-string.twinproj new file mode 100644 index 00000000..37a58735 Binary files /dev/null and b/bugs/filed/boolean-intdiv-string/boolean-intdiv-string.twinproj differ diff --git a/bugs/filed/boolean-intdiv-string/repro.json b/bugs/filed/boolean-intdiv-string/repro.json new file mode 100644 index 00000000..29a29855 --- /dev/null +++ b/bugs/filed/boolean-intdiv-string/repro.json @@ -0,0 +1,13 @@ +{ + "mode": "run", + "expect": { + "exit": 0, + "output": [ + "^b \\\\ \"2\"\\s+= True Boolean$", + "^b Mod \"2\" = False Boolean$", + "^b \\\\ s\\s+= True Boolean$", + "^b Mod s\\s+= False Boolean$" + ] + }, + "issue": 2439 +} diff --git a/bugs/filed/boolean-intdiv-string/src/Settings b/bugs/filed/boolean-intdiv-string/src/Settings new file mode 100644 index 00000000..fa1b6194 --- /dev/null +++ b/bugs/filed/boolean-intdiv-string/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "BooleanIntdivString", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `Boolean \\ String` and `Boolean Mod String` convert the `String` to `Boolean`", + "project.exportPathIsV2": true, + "project.id": "{084C6717-68EE-47C6-A59C-477702219315}", + "project.name": "BooleanIntdivString", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/boolean-intdiv-string/src/Sources/Startup.twin b/bugs/filed/boolean-intdiv-string/src/Sources/Startup.twin new file mode 100644 index 00000000..15665163 --- /dev/null +++ b/bugs/filed/boolean-intdiv-string/src/Sources/Startup.twin @@ -0,0 +1,20 @@ +' Boolean \ String and Boolean Mod String convert the String to Boolean, not to a number. +Module Startup + + Public Sub Main() + Dim b As Boolean = True + Dim s As String = "2" + + Debug.Print "b \ ""2"" = "; b \ "2"; " "; TypeName(b \ "2") ' expected Long 0, got Boolean True + Debug.Print "b Mod ""2"" = "; b Mod "2"; " "; TypeName(b Mod "2") ' expected Long -1, got Boolean False + Debug.Print "b \ s = "; b \ s; " "; TypeName(b \ s) + Debug.Print "b Mod s = "; b Mod s; " "; TypeName(b Mod s) + + ' The same operators with the number 2.0, and with the operands the other way round. + Debug.Print "b \ 2.0 = "; b \ 2#; " "; TypeName(b \ 2#) + Debug.Print "b Mod 2.0 = "; b Mod 2#; " "; TypeName(b Mod 2#) + Debug.Print """2"" \ b = "; "2" \ b; " "; TypeName("2" \ b) + Debug.Print "b + ""2"" = "; b + "2"; " "; TypeName(b + "2") + End Sub + +End Module diff --git a/bugs/filed/crash-placeholder-interface/REPORT.md b/bugs/filed/crash-placeholder-interface/REPORT.md new file mode 100644 index 00000000..aa7620ce --- /dev/null +++ b/bugs/filed/crash-placeholder-interface/REPORT.md @@ -0,0 +1,37 @@ +Filed as [twinbasic/twinbasic#2453](https://github.com/twinbasic/twinbasic/issues/2453). + +## Compiler crashes on an `Interface` named by an angle-bracket placeholder that has an `Extends` clause + +**Describe the bug** +A source file holding an `Interface` whose name is an angle-bracket placeholder and that has an `Extends` clause crashes the compiler while it parses the file. The DEBUG CONSOLE shows `NATIVE EXCEPTION: ACCESS_VIOLATION {no-basic-code}` with `>>> thread 0004: ParsingFileStart, `, then `restarting from MEMORY`, three times over, and then the IDE gives up. The input is not real code (it is a syntax skeleton, the shape the documentation uses to show where an attribute goes), but a parser given nonsense should report a diagnostic, and this one dereferences something instead. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `crash-placeholder-interface.twinproj` (attached as `crash-placeholder-interface.zip`). Its one source file, `Placeholder.twin`, is the whole reproduction: + ``` + Interface Extends + End Interface + ``` +2. See the compiler crash with `NATIVE EXCEPTION: ACCESS_VIOLATION {no-basic-code}` in the DEBUG CONSOLE, restart from memory three times, and then stop restarting. + +It takes a placeholder name and an `Extends` clause, and what the clause names does not matter: + +| source | result | +|---|---| +| `Interface ` + `End Interface` | TB5182 Syntax error, no crash | +| `Interface IFoo Extends ` + `End Interface` | TB5182 + TB5079 + TB5127, no crash | +| `Interface Extends ` + `End Interface` | **crash** | +| `Interface Extends IBase` + `End Interface`, no `IBase` anywhere | **crash** | +| the same, with `Interface IBase` or `Class IBase` declared in another file | **crash** | + +**Expected behavior** +A syntax error (TB5182, as for the same line without the `Extends` clause), not a crash. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Also on BETA 983, at `twinBASIC_win32.dll+00141F7A`. Severity: crash. It takes the compiler down, three restarts, then the IDE gives up. A crash in a batch of projects costs the whole batch its result, so a tool that compiles many files at once has to isolate the file that does it. An earlier version of this report said the crash needs a placeholder in both positions, the name and the base; the last two rows of the table, each measured with a project of its own, show the base can be a real name or missing entirely. + + diff --git a/bugs/filed/crash-placeholder-interface/crash-placeholder-interface.twinproj b/bugs/filed/crash-placeholder-interface/crash-placeholder-interface.twinproj new file mode 100644 index 00000000..9ac459e8 Binary files /dev/null and b/bugs/filed/crash-placeholder-interface/crash-placeholder-interface.twinproj differ diff --git a/bugs/filed/crash-placeholder-interface/repro.json b/bugs/filed/crash-placeholder-interface/repro.json new file mode 100644 index 00000000..facbed9b --- /dev/null +++ b/bugs/filed/crash-placeholder-interface/repro.json @@ -0,0 +1,7 @@ +{ + "mode": "compile", + "expect": { + "exit": 4 + }, + "issue": 2453 +} diff --git a/bugs/filed/crash-placeholder-interface/src/Settings b/bugs/filed/crash-placeholder-interface/src/Settings new file mode 100644 index 00000000..75407d39 --- /dev/null +++ b/bugs/filed/crash-placeholder-interface/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "CrashPlaceholderInterface", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Compiler crashes on an `Interface` named by an angle-bracket placeholder that has an `Extends` clause", + "project.exportPathIsV2": true, + "project.id": "{51505D43-8F7F-448C-B5F9-370DF2631F41}", + "project.name": "CrashPlaceholderInterface", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/crash-placeholder-interface/src/Sources/Placeholder.twin b/bugs/filed/crash-placeholder-interface/src/Sources/Placeholder.twin new file mode 100644 index 00000000..f224906c --- /dev/null +++ b/bugs/filed/crash-placeholder-interface/src/Sources/Placeholder.twin @@ -0,0 +1,3 @@ +' An Interface named by an angle-bracket placeholder, with an Extends clause: the compiler crashes while parsing this file. +Interface Extends +End Interface diff --git a/bugs/filed/crash-placeholder-interface/src/Sources/Startup.twin b/bugs/filed/crash-placeholder-interface/src/Sources/Startup.twin new file mode 100644 index 00000000..20233b3a --- /dev/null +++ b/bugs/filed/crash-placeholder-interface/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/filed/date-double-overload/REPORT.md b/bugs/filed/date-double-overload/REPORT.md new file mode 100644 index 00000000..8d29ddc1 --- /dev/null +++ b/bugs/filed/date-double-overload/REPORT.md @@ -0,0 +1,39 @@ +Filed as [twinbasic/twinbasic#2438](https://github.com/twinbasic/twinbasic/issues/2438). + +## Overloads on `Date` and `Double` resolve by declaration order, not by the argument's type + +**Describe the bug** +When a procedure is overloaded on `Date` and on `Double`, whichever of the two is declared first receives every call, whatever the type of the argument. No diagnostic is shown. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `date-double-overload.twinproj` (attached as `date-double-overload.zip`). Its source file `Probe.twin` declares the two overloads, `Date` first, and `Sub Main` calls `Probe.Show`: + ``` + Private Function F(ByVal x As Date) As String + F = "Date" + End Function + Private Function F(ByVal x As Double) As String + F = "Double" + End Function + + Dim x As Double = 1.5 + Debug.Print F(x) ' Date + ``` +2. Press F5 and read the DEBUG CONSOLE: `Show` calls `F` with a `Double` variable, a `Date` variable, a `Date` literal and `CDate(1)`, and all four print `Date`. +3. Swap the two declarations and run again: all four print `Double`. + +Whichever of the two is declared first receives every call. With `Date` first, a `Double` argument reaches the `Date` overload; with `Double` first, a `Date` variable, `#1/2/2026#` and `CDate(1)` all reach the `Double` overload. + +What does not reproduce it: a `Date` overload beside a `String` one resolves correctly, and an overload set on `Byte`, `Integer`, `Long`, `LongLong`, `Single`, `Double`, `Currency`, `Decimal`, `Boolean`, `String` and `Variant` sends arguments of each of those types to their own overload. The compiler does tell the two types apart elsewhere: `TypeName` of a `Date` expression is `Date`, and a `Long` overload beside a `LongPtr` one is refused as a duplicate definition in a 32-bit build, as it should be. + +**Expected behavior** +Each call reaches the overload for its argument's type: `Double` for the `Double` variable and `Date` for the three `Date` arguments, whichever overload is declared first. `Date` and `Double` are stored alike but are different types, and the other types in the overload set above are told apart. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Also on BETA 983, with the same result (the reproducer, `Date` first, prints `Date` four times). Severity: the wrong overload runs, with no diagnostic. + + diff --git a/bugs/filed/date-double-overload/date-double-overload.twinproj b/bugs/filed/date-double-overload/date-double-overload.twinproj new file mode 100644 index 00000000..6010902c Binary files /dev/null and b/bugs/filed/date-double-overload/date-double-overload.twinproj differ diff --git a/bugs/filed/date-double-overload/repro.json b/bugs/filed/date-double-overload/repro.json new file mode 100644 index 00000000..3652d689 --- /dev/null +++ b/bugs/filed/date-double-overload/repro.json @@ -0,0 +1,9 @@ +{ + "mode": "run", + "expect": { + "output": [ + "^Double variable: Date$" + ] + }, + "issue": 2438 +} diff --git a/bugs/filed/date-double-overload/src/Settings b/bugs/filed/date-double-overload/src/Settings new file mode 100644 index 00000000..c7c6ff05 --- /dev/null +++ b/bugs/filed/date-double-overload/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "DateDoubleOverload", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Overloads on `Date` and `Double` resolve by declaration order, not by the argument's type", + "project.exportPathIsV2": true, + "project.id": "{8E1C7755-5F43-494D-8885-CB28980701D4}", + "project.name": "DateDoubleOverload", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/date-double-overload/src/Sources/Probe.twin b/bugs/filed/date-double-overload/src/Sources/Probe.twin new file mode 100644 index 00000000..defae709 --- /dev/null +++ b/bugs/filed/date-double-overload/src/Sources/Probe.twin @@ -0,0 +1,18 @@ +' Overloads on Date and Double: the one declared first receives every call, whatever the argument's type. +Module Probe + Private Function F(ByVal x As Date) As String + F = "Date" + End Function + Private Function F(ByVal x As Double) As String + F = "Double" + End Function + + Public Sub Show() + Dim x As Double = 1.5 + Dim d As Date = #1/2/2026# + Debug.Print "Double variable: " & F(x) + Debug.Print "Date variable: " & F(d) + Debug.Print "Date literal: " & F(#1/2/2026#) + Debug.Print "CDate(1): " & F(CDate(1)) + End Sub +End Module diff --git a/bugs/filed/date-double-overload/src/Sources/Startup.twin b/bugs/filed/date-double-overload/src/Sources/Startup.twin new file mode 100644 index 00000000..6a21ebe9 --- /dev/null +++ b/bugs/filed/date-double-overload/src/Sources/Startup.twin @@ -0,0 +1,7 @@ +Module Startup + + Public Sub Main() + Probe.Show + End Sub + +End Module diff --git a/bugs/filed/debug-print-escaped-twice/REPORT.md b/bugs/filed/debug-print-escaped-twice/REPORT.md new file mode 100644 index 00000000..9046027e --- /dev/null +++ b/bugs/filed/debug-print-escaped-twice/REPORT.md @@ -0,0 +1,27 @@ +Filed as [twinbasic/twinbasic#2444](https://github.com/twinbasic/twinbasic/issues/2444). + +## Text that continues a `Debug.Print` line is escaped twice in the DEBUG CONSOLE + +**Describe the bug** +When a `Debug.Print` statement ends with `;`, the next `Debug.Print` continues the same line, and the DEBUG CONSOLE escapes that continued text twice: `&`, `<` and `>` show as `&`, `<` and `>`. The text that opens the line comes out right. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `debug-print-escaped-twice.twinproj` (attached as `debug-print-escaped-twice.zip`) and run it (F5). Its `Sub Main` holds the whole bug: + ``` + Debug.Print "A"; + Debug.Print "&" + ``` +2. See the DEBUG CONSOLE show `A&`. The project's other lines show the rest: `Debug.Print "a < b";` followed by `Debug.Print " and c > d"` shows `a < b and c > d`. After three statements, `Debug.Print "C";`, `Debug.Print "D";` and `Debug.Print "<&>"`, the line reads `CD<&>`. + +**Expected behavior** +`A&`, `a < b and c > d` and `CD<&>`: the console shows what the program printed, whether it is one statement or several. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Severity: cosmetic, but it changes what a program appears to print. Also on BETA 983, with the same output. What does not reproduce it: a whole line (`Debug.Print "a < b & c"` shows exactly that), and the same text in one statement (`Debug.Print "B"; "&"` shows `B&`). The cause looks to be `debugOutputPartial` in `ide/main.js`, which takes all of a program's output, and an add-in's `PrintText` too, and adds to a line that is still open: it passes the new text through `TEXTtoHTML` twice, once as it builds the text and again as it stores it. When the new text's colour differs from the line's, the `` it puts in to change colour goes through the second pass too, so the tags themselves show as text. The colour comes from the output: a program's plain output is `debugConsoleOutputText`, and a `PrintText` is `debugConsoleOutputTextYELLOW`. With a line left open in the first, made by calling `debugOutputPartial` from the page, a `PrintText` from the IDE's own Sample 10 add-in showed as `Hello there from WaynesWorldAddIn!`. A program's own open line followed by a `PrintText` was not tried. + + diff --git a/bugs/filed/debug-print-escaped-twice/debug-print-escaped-twice.twinproj b/bugs/filed/debug-print-escaped-twice/debug-print-escaped-twice.twinproj new file mode 100644 index 00000000..08e30c9d Binary files /dev/null and b/bugs/filed/debug-print-escaped-twice/debug-print-escaped-twice.twinproj differ diff --git a/bugs/filed/debug-print-escaped-twice/repro.json b/bugs/filed/debug-print-escaped-twice/repro.json new file mode 100644 index 00000000..5d7bae17 --- /dev/null +++ b/bugs/filed/debug-print-escaped-twice/repro.json @@ -0,0 +1,12 @@ +{ + "mode": "run", + "expect": { + "exit": 0, + "output": [ + "^A&$", + "^a < b and c > d$", + "^B&$" + ] + }, + "issue": 2444 +} diff --git a/bugs/filed/debug-print-escaped-twice/src/Settings b/bugs/filed/debug-print-escaped-twice/src/Settings new file mode 100644 index 00000000..14ea3324 --- /dev/null +++ b/bugs/filed/debug-print-escaped-twice/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "DebugPrintEscapedTwice", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Text that continues a `Debug.Print` line is escaped twice in the DEBUG CONSOLE", + "project.exportPathIsV2": true, + "project.id": "{7D848A28-61FD-4058-AEC0-7136C282F2D7}", + "project.name": "DebugPrintEscapedTwice", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/debug-print-escaped-twice/src/Sources/Startup.twin b/bugs/filed/debug-print-escaped-twice/src/Sources/Startup.twin new file mode 100644 index 00000000..d26146e8 --- /dev/null +++ b/bugs/filed/debug-print-escaped-twice/src/Sources/Startup.twin @@ -0,0 +1,15 @@ +' Text that continues a Debug.Print line is escaped twice in the DEBUG CONSOLE. +Module Startup + + Public Sub Main() + Debug.Print "A"; + Debug.Print "&" ' the console shows A& instead of A& + + Debug.Print "a < b"; ' the text that opens the line is right + Debug.Print " and c > d" ' the console shows and c > d + + Debug.Print "B"; "&" ' the same text in one statement is right: B& + Debug.Print "a < b & c" ' a whole line is right + End Sub + +End Module diff --git a/bugs/filed/duplicate-names-shipped/REPORT.md b/bugs/filed/duplicate-names-shipped/REPORT.md new file mode 100644 index 00000000..57e55b3b --- /dev/null +++ b/bugs/filed/duplicate-names-shipped/REPORT.md @@ -0,0 +1,33 @@ +Filed as [twinbasic/twinbasic#2435](https://github.com/twinbasic/twinbasic/issues/2435). + +## The IDE has written the same name twice into project files it ships + +**Describe the bug** +Two of the 48 project and package files an installation ships hold one name more than once, with different contents: the VB package holds `Resources/MANIFEST/#1.xml` twice, and Sample 16, *twinBASIC IDE Addin (TODO Widgets demo)*, holds `.addins/WaynesTodoItemsData` eight times. A folder can hold only one file of a name, so unpacking keeps one copy. The eight copies in Sample 16 suggest that each save of the add-in's data added an entry instead of replacing the old one, which is a guess and not a measurement. + +**To Reproduce** +Steps to reproduce the behavior: +1. The reproducer is not an attachment: it is two files in the installation, so there is no `.zip` for this report. They are `packages\{F50B82D0-DCAB-43FE-9631-11959D4A4728}_VB\package.twinproj` and `projects\Sample 16. twinBASIC IDE Addin (TODO Widgets demo)\projectName.twinproj`, under the IDE's folder. +2. Run `twinBASIC_win32.exe export "\packages\{F50B82D0-DCAB-43FE-9631-11959D4A4728}_VB\package.twinproj" C:\out1\`, into a folder that does not exist yet, without `--overwrite`. +3. See one `[EXPORT] ERROR: output file already exists and --overwrite not set: C:\out1\Resources\MANIFEST\#1.xml`, then `... FAILED`. The folder is empty before the run, so the only file that already exists is the one just written for the first copy of the name. +4. Run `twinBASIC_win32.exe export "\projects\Sample 16. twinBASIC IDE Addin (TODO Widgets demo)\projectName.twinproj" C:\out2\` the same way. +5. See seven of `[EXPORT] ERROR: output file already exists and --overwrite not set: C:\out2\.addins\WaynesTodoItemsData`, then `... FAILED`: one for each copy of the name after the first. + +**Expected behavior** +A project file holds each name once. Where the IDE rewrites an entry, it replaces it. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Severity: a folder can hold only one of them, so unpacking keeps one copy; which copy the IDE itself uses is not known. + +| file | name | copies | +|---|---|---| +| the VB package | `Resources/MANIFEST/#1.xml` | 2, of 703 and 682 bytes | +| Sample 16, *twinBASIC IDE Addin (TODO Widgets demo)* | `.addins/WaynesTodoItemsData` | 8, no two alike | + +`export` writes entries in reverse order, so with `--overwrite` the first copy in the file is the one left on disk. The same two files, and no others of the 48, hold a repeated name on BETA 983 as well as on BETA 995. + + diff --git a/bugs/filed/duplicate-names-shipped/repro.json b/bugs/filed/duplicate-names-shipped/repro.json new file mode 100644 index 00000000..aa2e3638 --- /dev/null +++ b/bugs/filed/duplicate-names-shipped/repro.json @@ -0,0 +1,15 @@ +{ + "mode": "cli", + "cli": [ + ["export", "{ide}\\packages\\{F50B82D0-DCAB-43FE-9631-11959D4A4728}_VB\\package.twinproj", "{tmp}\\out1\\"], + ["export", "{ide}\\projects\\Sample 16. twinBASIC IDE Addin (TODO Widgets demo)\\projectName.twinproj", "{tmp}\\out2\\"] + ], + "expect": { + "exit": 0, + "output": [ + "ERROR: output file already exists and --overwrite not set: .*\\\\out1\\\\Resources\\\\MANIFEST\\\\#1\\.xml", + "(?:^.*ERROR: output file already exists and --overwrite not set: .*\\\\out2\\\\\\.addins\\\\WaynesTodoItemsData\\r?\\n){7}" + ] + }, + "issue": 2435 +} diff --git a/bugs/filed/embed-package-without-packages-folder/DocProbePkg-control.twinpack b/bugs/filed/embed-package-without-packages-folder/DocProbePkg-control.twinpack new file mode 100644 index 00000000..cf3aa1cb Binary files /dev/null and b/bugs/filed/embed-package-without-packages-folder/DocProbePkg-control.twinpack differ diff --git a/bugs/filed/embed-package-without-packages-folder/DocProbePkg-nopackages.twinpack b/bugs/filed/embed-package-without-packages-folder/DocProbePkg-nopackages.twinpack new file mode 100644 index 00000000..ba77179b Binary files /dev/null and b/bugs/filed/embed-package-without-packages-folder/DocProbePkg-nopackages.twinpack differ diff --git a/bugs/filed/embed-package-without-packages-folder/REPORT.md b/bugs/filed/embed-package-without-packages-folder/REPORT.md new file mode 100644 index 00000000..228d0216 --- /dev/null +++ b/bugs/filed/embed-package-without-packages-folder/REPORT.md @@ -0,0 +1,30 @@ +Filed as [twinbasic/twinbasic#2442](https://github.com/twinbasic/twinbasic/issues/2442). + +## Embedding a package with no `Packages` folder puts the compiler in a crash loop + +**Describe the bug** +A `.twinpack` whose tree has no `Packages` folder, imported through Settings → References → Available Packages → **Import from file...**, ticked and applied, makes the compiler crash on each restart: `restarting from FILE` four times about two seconds apart, and then the IDE reports "Compiler crash loop detected. Restarting in SAFE mode." + +**To Reproduce** +The package is invalid input, and it crashes the compiler four times: use an IDE you can restart. + +Steps to reproduce the behavior: +1. Open `embed-package-without-packages-folder.twinproj` (attached as `embed-package-without-packages-folder.zip`). The package is in the second attachment, `DocProbePkg-nopackages.zip`: `DocProbePkg-nopackages.twinpack`, a package of one function, packed by `twinBASIC_win32.exe import` from a tree with a `Settings` file and `Sources\DocProbe.twin` and no other folder (the tree is `package\DocProbePkg` in the reproducer). The `import` verb writes `.twinproj`; the file was renamed to `.twinpack`. The tree may lack `ImportedTypeLibraries` and `Miscellaneous` as well; neither matters. +2. In the project, Settings → References → Available Packages → **Import from file...** `DocProbePkg-nopackages.twinpack`, tick it, and **Apply**. +3. The Debug 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." + +**Expected behavior** +The compiler embeds the package, or refuses it with a diagnostic, and does not crash. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +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. + +An empty `Packages` folder in the package tree is enough to prevent it: the same steps with that folder alone, or with all three (`DocProbePkg-control.twinpack` in the same attachment, which `impexp.mjs` made), 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. + + diff --git a/bugs/filed/embed-package-without-packages-folder/embed-package-without-packages-folder.twinproj b/bugs/filed/embed-package-without-packages-folder/embed-package-without-packages-folder.twinproj new file mode 100644 index 00000000..9dd8eb2e Binary files /dev/null and b/bugs/filed/embed-package-without-packages-folder/embed-package-without-packages-folder.twinproj differ diff --git a/bugs/filed/embed-package-without-packages-folder/package/DocProbePkg/Settings b/bugs/filed/embed-package-without-packages-folder/package/DocProbePkg/Settings new file mode 100644 index 00000000..5811b67c --- /dev/null +++ b/bugs/filed/embed-package-without-packages-folder/package/DocProbePkg/Settings @@ -0,0 +1,21 @@ +{ + "configuration.inherits": "Defaults", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Package TWINPACK", + "project.description": "A package of one function, DocProbeVersion, for a bug report.", + "project.id": "{7B247500-0000-4000-9000-7B2475000004}", + "project.licence": "MIT", + "project.name": "DocProbePkg", + "project.optionExplicit": true, + "project.packageVisibility": "PUBLIC", + "project.references": [], + "project.versionBuild": 0, + "project.versionMajor": 1, + "project.versionMinor": 0, + "project.versionRevision": 0, + "project.warnings": { + "errors": [], + "ignored": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/embed-package-without-packages-folder/package/DocProbePkg/Sources/DocProbe.twin b/bugs/filed/embed-package-without-packages-folder/package/DocProbePkg/Sources/DocProbe.twin new file mode 100644 index 00000000..2f81c56c --- /dev/null +++ b/bugs/filed/embed-package-without-packages-folder/package/DocProbePkg/Sources/DocProbe.twin @@ -0,0 +1,6 @@ +' The package of the report: one function, DocProbeVersion, which returns 1. +Module DocProbe + Public Function DocProbeVersion() As Long + Return 1 + End Function +End Module diff --git a/bugs/filed/embed-package-without-packages-folder/repro.json b/bugs/filed/embed-package-without-packages-folder/repro.json new file mode 100644 index 00000000..67a16259 --- /dev/null +++ b/bugs/filed/embed-package-without-packages-folder/repro.json @@ -0,0 +1,6 @@ +{ + "mode": "manual", + "attach": ["DocProbePkg-nopackages.twinpack", "DocProbePkg-control.twinpack"], + "steps": "Invalid input, and it crashes the compiler four times: use an IDE you can restart.\n1. Unzip embed-package-without-packages-folder.zip (pack writes it; the issue has the project and the packages as two attachments), which holds embed-package-without-packages-folder.twinproj, DocProbePkg-nopackages.twinpack, made by \"\\bin\\twinBASIC_win32.exe\" import from package/DocProbePkg, a tree that has no Packages folder, and DocProbePkg-control.twinpack. Open the project.\n2. Project Settings > References > Available Packages > Import from file... DocProbePkg-nopackages.twinpack; tick the package; Apply.\n3. See, in the DEBUG CONSOLE, [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.\"\nControl: the same steps with DocProbePkg-control.twinpack (made by scripts/impexp.mjs, which adds the empty Packages, ImportedTypeLibraries and Miscellaneous folders): the compiler restarts once and runs the package.\nMeasured on BETA 995 earlier this session with the packages lane's host and package, with the package's three empty folders varied one at a time; `ide-test.bat` now packs the package with impexp.mjs, which adds the folders, so no lane reproduces the crash today.", + "issue": 2442 +} diff --git a/bugs/filed/embed-package-without-packages-folder/src/Settings b/bugs/filed/embed-package-without-packages-folder/src/Settings new file mode 100644 index 00000000..815a8b85 --- /dev/null +++ b/bugs/filed/embed-package-without-packages-folder/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "EmbedPackageWithoutPackagesFolder", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Embedding a package with no `Packages` folder puts the compiler in a crash loop", + "project.exportPathIsV2": true, + "project.id": "{40E080EA-E880-41FF-A7F2-C2D9A63B5696}", + "project.name": "EmbedPackageWithoutPackagesFolder", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/embed-package-without-packages-folder/src/Sources/Startup.twin b/bugs/filed/embed-package-without-packages-folder/src/Sources/Startup.twin new file mode 100644 index 00000000..20eabc89 --- /dev/null +++ b/bugs/filed/embed-package-without-packages-folder/src/Sources/Startup.twin @@ -0,0 +1,5 @@ +' The project the report imports DocProbePkg into. Main stays empty until the package is referenced. +Module Startup + Public Sub Main() + End Sub +End Module diff --git a/bugs/filed/err-raise-helpcontext/REPORT.md b/bugs/filed/err-raise-helpcontext/REPORT.md new file mode 100644 index 00000000..8da9ef9d --- /dev/null +++ b/bugs/filed/err-raise-helpcontext/REPORT.md @@ -0,0 +1,31 @@ +Filed as [twinbasic/twinbasic#2455](https://github.com/twinbasic/twinbasic/issues/2455). + +## `Err.Raise` rejects `HelpContext` as a named argument, while its three siblings work + +**Describe the bug** +`Err.Raise` accepts `Source:=`, `Description:=` and `HelpFile:=` as named arguments, but not `HelpContext:=`. The error is `TB5090 unrecognized named argument`, and it does not say which name was wrong. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `err-raise-helpcontext.twinproj` (attached as `err-raise-helpcontext.zip`). Its one source file, `Probe.twin`, holds the whole bug: + ``` + Dim myHelpFile As String, myHelpContext As Long + Err.Raise vbObjectError + 894, Source:="MyApp.MyClass", _ + Description:="Was not able to complete your task", _ + HelpFile:=myHelpFile, HelpContext:=myHelpContext + ``` +2. See the project fail to compile with `TB5090 unrecognized named argument`. + +What does work, each verified on its own: the same call with `Source:=`, `Description:=` and `HelpFile:=` named and the fifth argument dropped compiles, and so does the fully positional form `Err.Raise vbObjectError + 894, myObjectID, "...", myHelpFile, myHelpContext`. So the parameter exists and accepts a `Long`; only its name is unrecognised. `HelpContextID:=` is rejected as well, so this is not simply a different spelling to discover, and the compiler binary's only `HelpContextID` strings belong to project settings, not to a signature. + +**Expected behavior** +The call compiles. `HelpContext` is what VBA itself names that parameter. Read out of the VBA type library on the machine this was found on (`VBE7.DLL` 7.01.1158, VBA7.1, via `LoadTypeLibEx` and `ITypeInfo::GetNames` on `_ErrObject`), the method is `Raise(Number, Source, Description, HelpFile, HelpContext)`. All five names are exactly the ones the failing call uses, and the call is Microsoft's own `Err.Source` example, named arguments and all, so the code twinBASIC rejects is the code a VBA developer is most likely to have copied. Four of the five names are accepted here; only the fifth is not. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Also on BETA 983, with the same TB5090. Severity: VBA-compatible code that names the fifth argument does not compile, and the diagnostic does not say which name was wrong. + + diff --git a/bugs/filed/err-raise-helpcontext/err-raise-helpcontext.twinproj b/bugs/filed/err-raise-helpcontext/err-raise-helpcontext.twinproj new file mode 100644 index 00000000..dffb5535 Binary files /dev/null and b/bugs/filed/err-raise-helpcontext/err-raise-helpcontext.twinproj differ diff --git a/bugs/filed/err-raise-helpcontext/repro.json b/bugs/filed/err-raise-helpcontext/repro.json new file mode 100644 index 00000000..0c79bb72 --- /dev/null +++ b/bugs/filed/err-raise-helpcontext/repro.json @@ -0,0 +1,9 @@ +{ + "mode": "compile", + "expect": { + "diagnostics": [ + "TB5090" + ] + }, + "issue": 2455 +} diff --git a/bugs/filed/err-raise-helpcontext/src/Settings b/bugs/filed/err-raise-helpcontext/src/Settings new file mode 100644 index 00000000..b16ac6b8 --- /dev/null +++ b/bugs/filed/err-raise-helpcontext/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ErrRaiseHelpcontext", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `Err.Raise` rejects `HelpContext` as a named argument, while its three siblings work", + "project.exportPathIsV2": true, + "project.id": "{EF8E8496-D541-4FDA-8AD6-D2BB86A3F1D1}", + "project.name": "ErrRaiseHelpcontext", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/err-raise-helpcontext/src/Sources/Probe.twin b/bugs/filed/err-raise-helpcontext/src/Sources/Probe.twin new file mode 100644 index 00000000..85184d2a --- /dev/null +++ b/bugs/filed/err-raise-helpcontext/src/Sources/Probe.twin @@ -0,0 +1,9 @@ +' Err.Raise with its fifth argument named: HelpContext is rejected, the other three names are not. +Module Probe + Public Sub T() + Dim myHelpFile As String, myHelpContext As Long + Err.Raise vbObjectError + 894, Source:="MyApp.MyClass", _ + Description:="Was not able to complete your task", _ + HelpFile:=myHelpFile, HelpContext:=myHelpContext + End Sub +End Module diff --git a/bugs/filed/err-raise-helpcontext/src/Sources/Startup.twin b/bugs/filed/err-raise-helpcontext/src/Sources/Startup.twin new file mode 100644 index 00000000..20233b3a --- /dev/null +++ b/bugs/filed/err-raise-helpcontext/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/filed/export-follows-junction/REPORT.md b/bugs/filed/export-follows-junction/REPORT.md new file mode 100644 index 00000000..823ae6e5 --- /dev/null +++ b/bugs/filed/export-follows-junction/REPORT.md @@ -0,0 +1,38 @@ +Filed as [twinbasic/twinbasic#2456](https://github.com/twinbasic/twinbasic/issues/2456). + +## Export Project follows a directory junction in its folder and deletes what it points to + +**Describe the bug** +**File → Export Project** empties its export folder before it writes, as the *Export Path* setting warns. It does not stop at a directory junction in that folder: it goes through the junction, deletes the files in the folder the junction points to, and then deletes the junction. Data outside the export folder is lost, and the only record is the Debug Console, with *Export Verbose* on. + +**To Reproduce** +**This deletes files.** Do it only in a scratch folder made for this, `` below, for example `C:\Scratch\junction-test`. Everything in `\outside` is deleted. + +Steps to reproduce the behavior: +1. Open `export-follows-junction.twinproj` (attached as `export-follows-junction.zip`), after unzipping it into ``. Its *Export Path* is `${SourcePath}\export` and *Export Verbose* is on (Project Settings, Export). +2. In a Command Prompt, make the export folder, a folder outside it that holds a file, and a junction in the export folder to that folder: + ``` + mkdir \export + mkdir \outside + echo keep me> \outside\precious.txt + mklink /J \export\linked \outside + ``` +3. Choose **File → Export Project**. +4. The Debug Console shows: + ``` + [EXPORT] DELETED: \\?\\export\linked\precious.txt + [EXPORT] DELETED: \\?\\export\linked + ``` + and `\outside` is empty afterwards. + +**Expected behavior** +The export deletes the junction itself, or leaves it alone, and never what it points to. A junction (or symbolic link) is not part of the export folder's contents. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Severity: data loss outside the folder the user chose. The command-line `export` verb does not reproduce it: it deletes nothing. + + diff --git a/bugs/filed/export-follows-junction/export-follows-junction.twinproj b/bugs/filed/export-follows-junction/export-follows-junction.twinproj new file mode 100644 index 00000000..102b4c7a Binary files /dev/null and b/bugs/filed/export-follows-junction/export-follows-junction.twinproj differ diff --git a/bugs/filed/export-follows-junction/repro.json b/bugs/filed/export-follows-junction/repro.json new file mode 100644 index 00000000..dced202b --- /dev/null +++ b/bugs/filed/export-follows-junction/repro.json @@ -0,0 +1,5 @@ +{ + "mode": "manual", + "steps": "WARNING: File > Export Project empties its export folder before it writes, and this bug makes it empty the folder a junction in that folder points to as well. Do it only in a scratch folder made for this, below (for example C:\\Scratch\\junction-test). Anything in \\outside is deleted.\n1. Unzip export-follows-junction.zip into , so that \\export-follows-junction.twinproj exists.\n2. In a Command Prompt: mkdir \\export, mkdir \\outside, echo keep me> \\outside\\precious.txt, then mklink /J \\export\\linked \\outside.\n3. Open the project in the twinBASIC IDE. Its Export Path is ${SourcePath}\\export and Export Verbose is on (Project Settings, Export).\n4. Choose File > Export Project.\n5. See, in the DEBUG CONSOLE, [EXPORT] DELETED: \\\\?\\\\export\\linked\\precious.txt and then [EXPORT] DELETED: \\\\?\\\\export\\linked. \\outside is empty: precious.txt is gone.\nRe-measured on BETA 995 by `ide-test.bat --only export` (it calls exportProjectTo() on a scratch folder under its work folder).", + "issue": 2456 +} diff --git a/bugs/filed/export-follows-junction/src/Settings b/bugs/filed/export-follows-junction/src/Settings new file mode 100644 index 00000000..609cc3e8 --- /dev/null +++ b/bugs/filed/export-follows-junction/src/Settings @@ -0,0 +1,63 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ExportFollowsJunction", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Export Project follows a directory junction in its folder and deletes what it points to", + "project.exportPath": "${SourcePath}\\export", + "project.exportPathIsV2": true, + "project.exportVerbose": true, + "project.id": "{CB4E9670-139D-4170-99CD-7A828375DA9D}", + "project.name": "ExportFollowsJunction", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/export-follows-junction/src/Sources/Startup.twin b/bugs/filed/export-follows-junction/src/Sources/Startup.twin new file mode 100644 index 00000000..1cd8339b --- /dev/null +++ b/bugs/filed/export-follows-junction/src/Sources/Startup.twin @@ -0,0 +1,5 @@ +' Nothing to run. The bug is in File > Export Project, which the steps in the report set up. +Module Startup + Public Sub Main() + End Sub +End Module diff --git a/bugs/filed/export-path-sourcepath-as-path/REPORT.md b/bugs/filed/export-path-sourcepath-as-path/REPORT.md new file mode 100644 index 00000000..e96a6332 --- /dev/null +++ b/bugs/filed/export-path-sourcepath-as-path/REPORT.md @@ -0,0 +1,30 @@ +Filed as [twinbasic/twinbasic#2457](https://github.com/twinbasic/twinbasic/issues/2457). + +## Export Path refuses `${SourcePath}` alone, but not the same folder written as a path + +**Describe the bug** +The Settings editor refuses `${SourcePath}` as the *Export Path*, because **File → Export Project** empties its folder and `${SourcePath}` is the folder that holds the project file. The check compares the text only: the same folder typed in full is accepted, and the compiler applies no check of its own. An export into the project's own folder deletes the project file. + +**To Reproduce** +**This deletes the project file.** Do it in a scratch folder made for this, `` below, for example `C:\Scratch\sourcepath-test`, which holds nothing but the unzipped project. + +Steps to reproduce the behavior: +1. Open `export-path-sourcepath-as-path.twinproj` (attached as `export-path-sourcepath-as-path.zip`), after unzipping it into ``, so that it is the only file there. *Export Verbose* is on. +2. In Project Settings, Export, set *Export Path* to `${SourcePath}`. The editor refuses it: "${SourcePath} on it's own is not allowed. This would DELETE the project file, as the `Export Project` command empties the output folder before exporting." +3. Set it to `` written out in full instead (`C:\Scratch\sourcepath-test`). The editor accepts it. Apply. +4. Choose **File → Export Project**. +5. The Debug Console shows `[EXPORT] DELETED: \\?\\export-path-sourcepath-as-path.twinproj`, and then `[EXPORT] COMPLETED`. The project file is no longer on disk. The project is still open, and **Save Project** writes the file back; closing without saving loses it. + +**Expected behavior** +The check resolves the path, so that any spelling of the project's own folder (or a folder above it that the export would empty, down to the project file) is refused alike. Failing that, the compiler refuses to delete the project file it was exporting. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Severity: the project file is deleted when the export folder is the folder that holds it. + +The check is the `validate` function of `project.exportPath` in `ide/main.js`. It compares the lower-cased text with `${sourcepath}` and `${sourcepath}\`, and does not resolve the path. The compiler's side was measured by calling `exportProjectTo()` with the project's own folder: it logged `[EXPORT] DELETED: \\?\\.twinproj` and completed. That the editor accepts the same folder typed as a path (step 3) is read from the check's code, not tried by hand. The **Save** that writes the file back (step 5) was seen on BETA 983 only. + + diff --git a/bugs/filed/export-path-sourcepath-as-path/export-path-sourcepath-as-path.twinproj b/bugs/filed/export-path-sourcepath-as-path/export-path-sourcepath-as-path.twinproj new file mode 100644 index 00000000..170f69b1 Binary files /dev/null and b/bugs/filed/export-path-sourcepath-as-path/export-path-sourcepath-as-path.twinproj differ diff --git a/bugs/filed/export-path-sourcepath-as-path/repro.json b/bugs/filed/export-path-sourcepath-as-path/repro.json new file mode 100644 index 00000000..bd9dc59c --- /dev/null +++ b/bugs/filed/export-path-sourcepath-as-path/repro.json @@ -0,0 +1,5 @@ +{ + "mode": "manual", + "steps": "WARNING: this deletes the project file. Do it in a scratch folder made for this, below (for example C:\\Scratch\\sourcepath-test), holding nothing but the unzipped project.\n1. Unzip export-path-sourcepath-as-path.zip into , so that \\export-path-sourcepath-as-path.twinproj is the only file there. Open it in the twinBASIC IDE. Export Verbose is on.\n2. Project Settings > Export > Export Path: type ${SourcePath}. The editor refuses it: \"${SourcePath} on it's own is not allowed. This would DELETE the project file ...\".\n3. Type in full instead (C:\\Scratch\\sourcepath-test). The editor accepts it. Apply.\n4. Choose File > Export Project.\n5. See, in the DEBUG CONSOLE, [EXPORT] DELETED: \\\\?\\\\export-path-sourcepath-as-path.twinproj, and then [EXPORT] COMPLETED. The project file is no longer on disk; the project is still open, and Save Project would write it back.\nRe-measured on BETA 995 by `ide-test.bat --only export`, which calls exportProjectTo() on the project's own folder (the compiler's side: step 4 and 5). Step 3, that the Settings editor accepts the full path, is read from the validate() function of project.exportPath in ide/main.js, which compares the text with ${sourcepath} and ${sourcepath}\\ and does not resolve the path; it has not been tried by hand.", + "issue": 2457 +} diff --git a/bugs/filed/export-path-sourcepath-as-path/src/Settings b/bugs/filed/export-path-sourcepath-as-path/src/Settings new file mode 100644 index 00000000..21fad79e --- /dev/null +++ b/bugs/filed/export-path-sourcepath-as-path/src/Settings @@ -0,0 +1,62 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ExportPathSourcepathAsPath", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Export Path refuses `${SourcePath}` alone, but not the same folder written as a path", + "project.exportPathIsV2": true, + "project.exportVerbose": true, + "project.id": "{D612CA20-6672-44F8-AECB-61A7C54E7692}", + "project.name": "ExportPathSourcepathAsPath", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/export-path-sourcepath-as-path/src/Sources/Startup.twin b/bugs/filed/export-path-sourcepath-as-path/src/Sources/Startup.twin new file mode 100644 index 00000000..77488dcf --- /dev/null +++ b/bugs/filed/export-path-sourcepath-as-path/src/Sources/Startup.twin @@ -0,0 +1,5 @@ +' Nothing to run. The bug is in the Export Path setting and File > Export Project, which the steps in the report set up. +Module Startup + Public Sub Main() + End Sub +End Module diff --git a/bugs/filed/export-writes-compiler-packages/REPORT.md b/bugs/filed/export-writes-compiler-packages/REPORT.md new file mode 100644 index 00000000..3d2c5379 --- /dev/null +++ b/bugs/filed/export-writes-compiler-packages/REPORT.md @@ -0,0 +1,50 @@ +Covered by the existing issue [twinbasic/twinbasic#841](https://github.com/twinbasic/twinbasic/issues/841). + +## Export Project writes the compiler packages, which the project does not hold, and the command line cannot pack the result + +**Describe the bug** +**File → Export Project** writes a `Packages` folder holding the full source of the compiler packages the project uses: `VB`, `VBA`, `VBRUN` and `AppGlobalClassProject` for a project with the default references. That is 475 of the 477 files an export of a two-file project wrote. The project file does not hold them: a `.twinproj` the IDE saved holds only the packages the project embeds, and `twinBASIC_win32.exe export` of it writes only those. + +The result is an export that 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. `twinBASIC_win32.exe import` stops with exit code 999 and writes nothing on any folder under `Packages`. + +**To Reproduce** +Do it in a scratch folder made for this, `` below, for example `C:\Scratch\packages-test`. **File → Export Project** empties `\export`. `` is the folder that holds `twinBASIC.exe`. + +Steps to reproduce the behavior: +1. Open `export-writes-compiler-packages.twinproj` (attached as `export-writes-compiler-packages.zip`), after unzipping it into ``. It has two files and the default references. Its *Export Path* is `${SourcePath}\export`. In a Command Prompt: `mkdir \export`. +2. Choose **File → Export Project**. +3. `\export\Packages` holds the source of `VB`, `VBA`, `VBRUN` and `AppGlobalClassProject`. +4. In a Command Prompt: + ``` + "\bin\twinBASIC_win32.exe" import \repacked.twinproj \export\ --overwrite + ``` + It stops with exit code 999 and writes nothing. +5. For comparison, `"\bin\twinBASIC_win32.exe" export \export-writes-compiler-packages.twinproj \fromfile\ --overwrite` writes three files (`Settings` and the two sources) and an empty `Packages` folder, because the project file holds no package. (For a project embedding WinDevLib it writes `Packages\WinDevLib` and no other package.) + +Then, on the 477-file export: +- the standalone script `scripts/impexp.mjs` packs it into a 4,220,723-byte project, against 2,055 bytes for the same export with `Packages` removed. The project now embeds its own copy of the four compiler packages. It compiles with no errors; +- the IDE's own **New Project → Import from folder...** does the same, into a 4,222,833-byte project, against 4,207 bytes from the export with `Packages` removed. + +**The embedded copy is dead, and every later export writes it back.** Measured on the IDE's import: +1. Export a project with the default references into an empty folder `E`. +2. In `E\Packages\VBA\Sources\Math.twin`, add `Public Function ProbeEmbeddedMarker() As Long` before `End Module`, and a call to it in one of the project's own modules. +3. **Import from folder...** on `E`: TB5079, *Unrecognized symbol 'ProbeEmbeddedMarker'*. The compiler uses its own VBA package, not the copy the project now holds. +4. Save the project, and export it again: the Debug Console reports `[EXPORT] COMPLETED (139 folders, 954 files)`, against `(72 folders, 479 files)` before, and the exported `Math.twin` holds the marker. The export writes both copies to the same paths, the embedded one last. + +So a project kept in Git through *Export After Save* and rebuilt from a clone keeps committing the package source of the IDE that first exported it, while it compiles against the current IDE's. + +**Expected behavior** +A `.twinproj` the IDE saves holds no compiler packages, and so the export of it should not write them either; or **Import from folder** (and `import`) should skip the compiler packages' folders, or the export should write the IDE's own copy rather than the project's. The IDE's export of a project should pack back into the same project with the supported tool. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +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. + +Measured on BETA 995: the export writing `Packages\VBA` and the others, and the command line's `import` of it stopping with 999. The IDE's own import, the 4,222,833-byte project and the dead copy were measured on BETA 983. The two byte counts for the standalone script come from the earlier export, not from the reproducer project, whose two files differ a little. + +What does not reproduce it: the command line's own `export`, which writes what the project file holds; and **Import from folder** on the export with the compiler packages' folders removed, which gives the 4,207-byte project, compiles, and exports 479 files. + + diff --git a/bugs/filed/export-writes-compiler-packages/export-writes-compiler-packages.twinproj b/bugs/filed/export-writes-compiler-packages/export-writes-compiler-packages.twinproj new file mode 100644 index 00000000..1b368e9d Binary files /dev/null and b/bugs/filed/export-writes-compiler-packages/export-writes-compiler-packages.twinproj differ diff --git a/bugs/filed/export-writes-compiler-packages/repro.json b/bugs/filed/export-writes-compiler-packages/repro.json new file mode 100644 index 00000000..651a6d98 --- /dev/null +++ b/bugs/filed/export-writes-compiler-packages/repro.json @@ -0,0 +1,6 @@ +{ + "mode": "manual", + "steps": "Do it in a scratch folder made for this, below (for example C:\\Scratch\\packages-test). File > Export Project empties \\export.\n1. Unzip export-writes-compiler-packages.zip into , so that \\export-writes-compiler-packages.twinproj exists. In a Command Prompt: mkdir \\export.\n2. Open the project in the twinBASIC IDE. Its Export Path is ${SourcePath}\\export. Choose File > Export Project.\n3. See \\export\\Packages: it holds the source of the compiler packages the project uses, VB, VBA, VBRUN and AppGlobalClassProject (475 of the 477 files an export of a two-file project wrote). The project file holds none of them.\n4. In a Command Prompt, with the folder that holds twinBASIC.exe: \"\\bin\\twinBASIC_win32.exe\" import \\repacked.twinproj \\export\\ --overwrite. It stops with exit code 999 and writes nothing.\n5. Control: \"\\bin\\twinBASIC_win32.exe\" export \\export-writes-compiler-packages.twinproj \\fromfile\\ --overwrite writes 3 files, Settings and the two sources, and an empty Packages folder, because the project file holds no package.\nRe-measured on BETA 995 by `ide-test.bat --only export` (steps 2 to 4).", + "issue": 841, + "existing": true +} diff --git a/bugs/filed/export-writes-compiler-packages/src/Settings b/bugs/filed/export-writes-compiler-packages/src/Settings new file mode 100644 index 00000000..ecd4db81 --- /dev/null +++ b/bugs/filed/export-writes-compiler-packages/src/Settings @@ -0,0 +1,62 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ExportWritesCompilerPackages", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Export Project writes the compiler packages, which the project does not hold, and the command line cannot pack the result", + "project.exportPath": "${SourcePath}\\export", + "project.exportPathIsV2": true, + "project.id": "{291151F0-EF69-47AF-B151-97A00747A457}", + "project.name": "ExportWritesCompilerPackages", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/export-writes-compiler-packages/src/Sources/Helpers.twin b/bugs/filed/export-writes-compiler-packages/src/Sources/Helpers.twin new file mode 100644 index 00000000..5d63d910 --- /dev/null +++ b/bugs/filed/export-writes-compiler-packages/src/Sources/Helpers.twin @@ -0,0 +1,6 @@ +' The project's second file. +Module Helpers + Public Function Greeting() As String + Return "hello" + End Function +End Module diff --git a/bugs/filed/export-writes-compiler-packages/src/Sources/Startup.twin b/bugs/filed/export-writes-compiler-packages/src/Sources/Startup.twin new file mode 100644 index 00000000..8bd8b3a3 --- /dev/null +++ b/bugs/filed/export-writes-compiler-packages/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +' A two-file project with the default references: File > Export Project writes the source of the compiler packages as well. +Module Startup + Public Sub Main() + Debug.Print Greeting() + End Sub +End Module diff --git a/bugs/filed/f1-fold-signature-help/REPORT.md b/bugs/filed/f1-fold-signature-help/REPORT.md new file mode 100644 index 00000000..c874f733 --- /dev/null +++ b/bugs/filed/f1-fold-signature-help/REPORT.md @@ -0,0 +1,28 @@ +Filed as [twinbasic/twinbasic#2446](https://github.com/twinbasic/twinbasic/issues/2446). + +## F1 and the fold icon toggle the signature help, then fail + +**Describe the bug** +In the code editor's signature help, F1 and a click on the fold icon both toggle the help between expanded and collapsed, and then fail. Every F1 adds `command failed: "tbHelp_ToggleExpandSignatureHelp"` to the Debug Console, and every click on the icon throws an error in the page. The toggle works, so the error is the only symptom. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `f1-fold-signature-help.twinproj` (attached as `f1-fold-signature-help.zip`) and open `Main.twin`. Put the cursor inside the parentheses of the call `FindTheNeedle(3)` and press Ctrl+Space. The signature help shows, with a fold icon whose tooltip reads *Fold/Collapse (F1)*. +2. Press F1. The signature help expands, and the Debug Console shows `command failed: "tbHelp_ToggleExpandSignatureHelp"`. F1 again collapses it, with a second such line. +3. Click the fold icon instead. The signature help toggles, and the page throws `TypeError: Cannot read properties of undefined (reading 'stopPropagation') at toggleSigHelp`. + +**Expected behavior** +The toggle, and no error. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Severity: cosmetic. The toggle works, but every F1 adds the failure line to the Debug Console, and every click on the icon throws in the page. + +`toggleSigHelp(e)` in `ide/main.js` ends with `e.stopPropagation();e.preventDefault()`, and neither caller passes an event: the command is `internalAction:()=>{toggleSigHelp()}`, and the icon is `onclick='toggleSigHelp()'`. The toggle comes first, so the error is the only symptom. `executeKeyboardShortcuts` catches the command's error and writes the Debug Console line. + +F1 was measured on BETA 995, in a run where an add-in that registers F1 was loaded (it only shows that the shortcut fires; the IDE's own command is the one that fails). The click on the icon was measured on BETA 983, in a harness IDE with the page's exceptions recorded over CDP; `toggleSigHelp` and both callers are unchanged in BETA 995's `ide/main.js`. + + diff --git a/bugs/filed/f1-fold-signature-help/f1-fold-signature-help.twinproj b/bugs/filed/f1-fold-signature-help/f1-fold-signature-help.twinproj new file mode 100644 index 00000000..acdf3db8 Binary files /dev/null and b/bugs/filed/f1-fold-signature-help/f1-fold-signature-help.twinproj differ diff --git a/bugs/filed/f1-fold-signature-help/repro.json b/bugs/filed/f1-fold-signature-help/repro.json new file mode 100644 index 00000000..aa196caa --- /dev/null +++ b/bugs/filed/f1-fold-signature-help/repro.json @@ -0,0 +1,5 @@ +{ + "mode": "manual", + "steps": "1. Open f1-fold-signature-help.twinproj in the twinBASIC IDE and open Main.twin. Put the cursor inside the parentheses of FindTheNeedle( in the call, and press Ctrl+Space. The signature help shows, with a fold icon whose tooltip reads Fold/Collapse (F1).\n2. Press F1. The signature help expands, and the DEBUG CONSOLE shows command failed: \"tbHelp_ToggleExpandSignatureHelp\". F1 again collapses it, with a second such line.\n3. Click the fold icon instead. The signature help toggles, and the IDE page throws TypeError: Cannot read properties of undefined (reading 'stopPropagation') at toggleSigHelp.\nExpected: the toggle, and no error. Measured on BETA 995 by `addin-test.bat --only keys` (the last test; an add-in that registers F1 was loaded in that run, to see the shortcut fire, and the IDE's own command fails the same way). The click was measured on BETA 983 only, in a harness IDE with the page's exceptions recorded over CDP; toggleSigHelp and both callers are unchanged in BETA 995's ide/main.js.", + "issue": 2446 +} diff --git a/bugs/filed/f1-fold-signature-help/src/Settings b/bugs/filed/f1-fold-signature-help/src/Settings new file mode 100644 index 00000000..50517dba --- /dev/null +++ b/bugs/filed/f1-fold-signature-help/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "F1FoldSignatureHelp", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: F1 and the fold icon toggle the signature help, then fail", + "project.exportPathIsV2": true, + "project.id": "{E749543A-96A4-414C-83F4-CAE38BBA3E60}", + "project.name": "F1FoldSignatureHelp", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/f1-fold-signature-help/src/Sources/Haystack.twin b/bugs/filed/f1-fold-signature-help/src/Sources/Haystack.twin new file mode 100644 index 00000000..9da7815c --- /dev/null +++ b/bugs/filed/f1-fold-signature-help/src/Sources/Haystack.twin @@ -0,0 +1,8 @@ +' A haystack with a needle in it. +Module Haystack + Public Function FindTheNeedle(ByVal n As Long) As Long + Dim needleCount As Long + needleCount = n * 2 + Return needleCount + End Function +End Module diff --git a/bugs/filed/f1-fold-signature-help/src/Sources/Main.twin b/bugs/filed/f1-fold-signature-help/src/Sources/Main.twin new file mode 100644 index 00000000..73585bb7 --- /dev/null +++ b/bugs/filed/f1-fold-signature-help/src/Sources/Main.twin @@ -0,0 +1,8 @@ +' Put the cursor inside the parentheses of FindTheNeedle( and press Ctrl+Space to show the signature help. +Module Main + Public Sub Main() + Dim needle As Long + needle = FindTheNeedle(3) + Debug.Print needle + End Sub +End Module diff --git a/bugs/filed/fastcall-delegate-stdcall/REPORT.md b/bugs/filed/fastcall-delegate-stdcall/REPORT.md new file mode 100644 index 00000000..a017c7f5 --- /dev/null +++ b/bugs/filed/fastcall-delegate-stdcall/REPORT.md @@ -0,0 +1,44 @@ +Filed as [twinbasic/twinbasic#2443](https://github.com/twinbasic/twinbasic/issues/2443). + +## A call through a `FastCall` or `ThisCall` delegate is made as stdcall on win32 + +**Describe the bug** +On win32, a call through a delegate declared `FastCall` or `ThisCall` passes its arguments as stdcall does, whatever convention the delegate declares, and raises *Bad DLL definition. Stack corruption detected.* The delegate is unusable on win32: every call through it raises an error. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `fastcall-delegate-stdcall.twinproj` (attached as `fastcall-delegate-stdcall.zip`) and run it (F5) with the win32 target. Its declarations and the calls in its `Sub Main` hold the whole bug: + ``` + 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 + ``` +2. See the error for the call through `d`, and 901 for the call through `e`. The project declares a `ThisCall` delegate and function as well, and the same call through it raises the same error. + +**Expected behavior** +901 from each call through `d`, as from calling `GF` directly. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Severity: the delegate is unusable on win32. 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; this project run with the win64 target prints 901 for all of them). + +Both keywords are new in BETA 990 and 992; BETA 987 refuses them (TB5182), and BETA 983 reports errors for this project at compile time. Measured in the IDE's `[RunAfterBuild]` run, and in the compiled EXE that `tbrun` left, run from its `Sub Main` and writing to a file: the same five results both ways. + + diff --git a/bugs/filed/fastcall-delegate-stdcall/fastcall-delegate-stdcall.twinproj b/bugs/filed/fastcall-delegate-stdcall/fastcall-delegate-stdcall.twinproj new file mode 100644 index 00000000..6ab4c1ca Binary files /dev/null and b/bugs/filed/fastcall-delegate-stdcall/fastcall-delegate-stdcall.twinproj differ diff --git a/bugs/filed/fastcall-delegate-stdcall/repro.json b/bugs/filed/fastcall-delegate-stdcall/repro.json new file mode 100644 index 00000000..9c69d9ba --- /dev/null +++ b/bugs/filed/fastcall-delegate-stdcall/repro.json @@ -0,0 +1,14 @@ +{ + "mode": "run", + "arch": "win32", + "expect": { + "exit": 0, + "output": [ + "^GF directly:\\s+901", + "^FastCall delegate:\\s+-353703419 Bad DLL definition", + "^ThisCall delegate:\\s+-353703419 Bad DLL definition", + "^FastCall delegate to a stdcall function:\\s+901" + ] + }, + "issue": 2443 +} diff --git a/bugs/filed/fastcall-delegate-stdcall/src/Settings b/bugs/filed/fastcall-delegate-stdcall/src/Settings new file mode 100644 index 00000000..95251090 --- /dev/null +++ b/bugs/filed/fastcall-delegate-stdcall/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "FastcallDelegateStdcall", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: A call through a `FastCall` or `ThisCall` delegate is made as stdcall on win32", + "project.exportPathIsV2": true, + "project.id": "{59B08673-59DD-4518-88FE-E8577E5B2F09}", + "project.name": "FastcallDelegateStdcall", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/fastcall-delegate-stdcall/src/Sources/Startup.twin b/bugs/filed/fastcall-delegate-stdcall/src/Sources/Startup.twin new file mode 100644 index 00000000..4cd5c335 --- /dev/null +++ b/bugs/filed/fastcall-delegate-stdcall/src/Sources/Startup.twin @@ -0,0 +1,39 @@ +' A call through a FastCall or ThisCall delegate is made as stdcall on win32. +' Build for win32: on win64 every case below returns 901. +Module Startup + + Public Delegate Function FastDel FastCall (ByVal a As Long, ByVal b As Long) As Long + Public Delegate Function ThisDel ThisCall (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 GT ThisCall(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 + + Public Sub Main() + Debug.Print "GF directly: "; GF(9, 1) ' 901 + Debug.Print "GT directly: "; GT(9, 1) ' 901 + + On Error Resume Next + Dim f As FastDel = AddressOf GF + Debug.Print "FastCall delegate: "; f(9, 1) ' expected 901 + Debug.Print Err.Number; Err.Description ' got: Stack corruption detected + Err.Clear + + Dim t As ThisDel = AddressOf GT + Debug.Print "ThisCall delegate: "; t(9, 1) ' expected 901 + Debug.Print Err.Number; Err.Description + Err.Clear + + Dim e As FastDel = AddressOf GS ' warning TB0026: a stdcall target works + Debug.Print "FastCall delegate to a stdcall function: "; e(9, 1) ' 901 + End Sub + +End Module diff --git a/bugs/filed/filecopy-open-file/REPORT.md b/bugs/filed/filecopy-open-file/REPORT.md new file mode 100644 index 00000000..ae9c539f --- /dev/null +++ b/bugs/filed/filecopy-open-file/REPORT.md @@ -0,0 +1,30 @@ +Filed as [twinbasic/twinbasic#2451](https://github.com/twinbasic/twinbasic/issues/2451). + +## `FileCopy` of an open file raises `&H80004005`, where VB6 raises 55 or copies it + +**Describe the bug** +`FileCopy` of a file that is open raises -2147467259 (`&H80004005`, *Unspecified error*), whatever mode the file is open in. VB6 raises error 55, *File already open*, for a file open `For Append`, and copies a file open `For Input` without an error. Code that handles VB6's error 55 does not recognise the error, and a copy that VB6 makes is refused. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `filecopy-open-file.twinproj` (attached as `filecopy-open-file.zip`) and run it (F5). Its `Sub Main` makes a file in `%TEMP%` and then does this: + ``` + On Error Resume Next + Open f For Append As #2 + FileCopy f, f & ".copy" + Debug.Print Err.Number, Err.Description + ``` +2. See `-2147467259 Unspecified error`. VB6 prints `55 File already open`. +3. See the second line the project prints: with the file open `For Input` instead, twinBASIC raises the same `-2147467259`, and VB6 copies the file without an error. + +**Expected behavior** +Error 55 for the file open `For Append`, and a successful copy for the file open `For Input`, as in VB6. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +BETA 983 copied an open file with no error, in both modes (the project prints 0 for all three lines); BETA 984's notes list the change ("FileSystem.FileCopy function would previously allow copying of an already open file without error"), and only the error number and the `Input` case differ from VB6. What does not reproduce it: the file closed (the third line the project prints is 0). Severity: code that handles VB6's error 55 does not recognise the error, and a copy that VB6 makes is refused. + + diff --git a/bugs/filed/filecopy-open-file/filecopy-open-file.twinproj b/bugs/filed/filecopy-open-file/filecopy-open-file.twinproj new file mode 100644 index 00000000..74d9aaa4 Binary files /dev/null and b/bugs/filed/filecopy-open-file/filecopy-open-file.twinproj differ diff --git a/bugs/filed/filecopy-open-file/repro.json b/bugs/filed/filecopy-open-file/repro.json new file mode 100644 index 00000000..4c2b6be1 --- /dev/null +++ b/bugs/filed/filecopy-open-file/repro.json @@ -0,0 +1,12 @@ +{ + "mode": "run", + "expect": { + "exit": 0, + "output": [ + "^open For Append: -2147467259 ", + "^open For Input: -2147467259 ", + "^closed:\\s+0\\s*$" + ] + }, + "issue": 2451 +} diff --git a/bugs/filed/filecopy-open-file/src/Settings b/bugs/filed/filecopy-open-file/src/Settings new file mode 100644 index 00000000..4f72a85e --- /dev/null +++ b/bugs/filed/filecopy-open-file/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "FilecopyOpenFile", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: FileCopy of an open file raises `&H80004005`, where VB6 raises 55 or copies it", + "project.exportPathIsV2": true, + "project.id": "{C8DBD6E9-219B-4F1A-A73E-EA267F50EA49}", + "project.name": "FilecopyOpenFile", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/filecopy-open-file/src/Sources/Startup.twin b/bugs/filed/filecopy-open-file/src/Sources/Startup.twin new file mode 100644 index 00000000..6774cb54 --- /dev/null +++ b/bugs/filed/filecopy-open-file/src/Sources/Startup.twin @@ -0,0 +1,30 @@ +' FileCopy of an open file raises &H80004005, where VB6 raises error 55 or copies it. +Module Startup + + Public Sub Main() + Dim f As String = Environ$("TEMP") & "\filecopy-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 "open For Append: "; Err.Number; Err.Description ' VB6: 55 File already open + Err.Clear + Close #2 + + Open f For Input As #2 + FileCopy f, f & ".copy" + Debug.Print "open For Input: "; Err.Number; Err.Description ' VB6: copies, no error + Err.Clear + Close #2 + + FileCopy f, f & ".copy" + Debug.Print "closed: "; Err.Number; Err.Description ' no error + Err.Clear + + Kill f + Kill f & ".copy" + End Sub + +End Module diff --git a/bugs/filed/forms-by-index-crash/REPORT.md b/bugs/filed/forms-by-index-crash/REPORT.md new file mode 100644 index 00000000..875fa1e8 --- /dev/null +++ b/bugs/filed/forms-by-index-crash/REPORT.md @@ -0,0 +1,37 @@ +Filed as [twinbasic/twinbasic#2458](https://github.com/twinbasic/twinbasic/issues/2458). + +## Reading `Forms` by index returns a broken reference, and the process then crashes + +**Describe the bug** +In a loop over `Forms.Count`, `Set f = Forms(k)` with `f` declared `As Form`, followed by a read of `f.Name`, ends in an access violation (`0xC0000005`). As a compiled EXE the process exits with that code. Run in the IDE, the DEBUG CONSOLE reports `NATIVE EXCEPTION: ACCESS_VIOLATION` at the line that reads `f.Name`, and the run ends without returning. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `forms-by-index-crash.twinproj` (attached as `forms-by-index-crash.zip`). It is a Standard EXE project with one empty form, `Form1`, and a `Sub Main` that loads it: + ``` + Dim f As Form + Dim k As Long + Load Form1 + For k = 0 To Forms.Count - 1 + Set f = Forms(k) + Debug.Print "k = "; k; " name = "; f.Name + Next k + ``` +2. Build the project and run the EXE, or run it in the IDE (F5). +3. See the process end with `0xC0000005` (exit code -1073741819), or the access violation in the DEBUG CONSOLE. + +Declaring `f` As `Form1` or As `Object` instead, with everything else the same, returns the form and the program exits 0. With three forms loaded, the loop crashes the same way, and an earlier probe saw the loop variable corrupted: `k` read 0, 0, 0, then 8195702. + +**Expected behavior** +`Forms(k)` returns the loaded form, `f.Name` is `Form1`, and the program exits 0, as it does with `f` declared `As Object`. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Also on BETA 983, as a compiled EXE (reproduced again with this project). Severity: crash (`0xC0000005`), from a form of access the documentation shows. What does not reproduce it, measured on BETA 995 as a compiled EXE: `Set f = Forms(0)` with the literal index 0 and no loop, then `f.Name`, returns `Form1` and the program exits 0; so does `s = Forms(0).Name`, and `Debug.Print Forms(0).Name`. An earlier entry said `s = Forms(0).Name` returns an empty string and the process later dies; this was not seen again here, and the loop is the smallest form found. From earlier probes: `n = 0: Set f = Forms(n)` outside a loop returns the form and the program exits 0; `For Each f In Forms` and `Unload Forms(i)` work; `Printers(0)` with a literal index works. A variant of the loop that wrote the name with a helper `Sub` instead of `Debug.Print` did not crash in one run; the crash was seen with `Debug.Print`, and with `Dim t As String = f.Name` and no printing. + + + + diff --git a/bugs/filed/forms-by-index-crash/forms-by-index-crash.twinproj b/bugs/filed/forms-by-index-crash/forms-by-index-crash.twinproj new file mode 100644 index 00000000..bcadc311 Binary files /dev/null and b/bugs/filed/forms-by-index-crash/forms-by-index-crash.twinproj differ diff --git a/bugs/filed/forms-by-index-crash/repro.json b/bugs/filed/forms-by-index-crash/repro.json new file mode 100644 index 00000000..cbf52636 --- /dev/null +++ b/bugs/filed/forms-by-index-crash/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "run", + "exe": true, + "expect": { + "exit": 6 + }, + "issue": 2458 +} diff --git a/bugs/filed/forms-by-index-crash/src/Settings b/bugs/filed/forms-by-index-crash/src/Settings new file mode 100644 index 00000000..f99f10a5 --- /dev/null +++ b/bugs/filed/forms-by-index-crash/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "FormsByIndexCrash", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Reading `Forms` by index returns a broken reference, and the process then crashes", + "project.exportPathIsV2": true, + "project.id": "{E4316C37-27D2-4045-A50A-369871A26E4A}", + "project.name": "FormsByIndexCrash", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/forms-by-index-crash/src/Sources/Form1.tbform b/bugs/filed/forms-by-index-crash/src/Sources/Form1.tbform new file mode 100644 index 00000000..a7f451a7 --- /dev/null +++ b/bugs/filed/forms-by-index-crash/src/Sources/Form1.tbform @@ -0,0 +1,83 @@ +[ + { + "AlwaysShowKeyboardCues": false, + "Appearance": "vbAppear3d", + "AutoRedraw": false, + "BackColor": -2147483633, + "BorderStyle": "vbSizable", + "Caption": "Form1", + "ClipControls": true, + "ControlBox": true, + "DrawMode": "vbCopyPen", + "DrawStyle": "vbSolid", + "DrawWidth": 1, + "Enabled": true, + "FillColor": 0, + "FillStyle": "vbFSTransparent", + "FontBold": false, + "FontItalic": false, + "FontName": "Segoe UI", + "FontSize": 8, + "FontStrikethru": false, + "FontTransparent": true, + "FontUnderline": false, + "ForeColor": -2147483630, + "FormDesignerId": "{EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA01}", + "HasDC": true, + "Height": 300, + "HelpContextID": 0, + "Icon": [], + "KeyPreview": false, + "Left": 0, + "LinkMode": "vbLinkNone", + "LinkTopic": null, + "MDIChild": false, + "MaxButton": true, + "MaxHeight": 0, + "MaxWidth": 0, + "MinButton": true, + "MinHeight": 0, + "MinWidth": 0, + "MouseIcon": [], + "MousePointer": "vbDefault", + "Moveable": true, + "Name": "Form1", + "NegotiateMenus": false, + "OLEDropMode": "vbOLEDropNone", + "Opacity": 100, + "Palette": [], + "PaletteMode": "vbPaletteModeHalftone", + "Picture": [], + "PictureDpiScaling": false, + "RightToLeft": false, + "ScaleHeight": 4500, + "ScaleLeft": 0, + "ScaleMode": "vbTwips", + "ScaleTop": 0, + "ScaleWidth": 4500, + "ShowInTaskbar": true, + "StartUpPosition": "vbStartUpScreen", + "TabFocusAutoSelect": false, + "Tag": null, + "Top": 0, + "TopMost": false, + "TransparencyKey": -1, + "Visible": true, + "WhatsThisButton": false, + "WhatsThisHelp": false, + "Width": 300, + "WindowState": "vbNormal", + "__IDEOptions": { + "alignToGrid": false, + "gridHeight": 10, + "gridWidth": 10, + "lockedControls": [], + "multiColoredGrabbers": false, + "showGrid": true, + "showOutlines": false + }, + "__lastUpdateMarker": 2061908507, + "_className": "Form", + "_clsid": "{33AD4F38-6699-11CF-B70C-00AA0060D393}" + } +] \ No newline at end of file diff --git a/bugs/filed/forms-by-index-crash/src/Sources/Form1.twin b/bugs/filed/forms-by-index-crash/src/Sources/Form1.twin new file mode 100644 index 00000000..cc6a437d --- /dev/null +++ b/bugs/filed/forms-by-index-crash/src/Sources/Form1.twin @@ -0,0 +1,10 @@ +' An empty form, loaded by Startup.Main. Its design is in Form1.tbform. +[Description("")] +[FormDesignerId("EAEAEAEA-EAEA-EAEA-EAEA-EAEAEAEAEA01")] +[PredeclaredId] +Class Form1 + + Sub New() + End Sub + +End Class diff --git a/bugs/filed/forms-by-index-crash/src/Sources/Startup.twin b/bugs/filed/forms-by-index-crash/src/Sources/Startup.twin new file mode 100644 index 00000000..5dfd00b0 --- /dev/null +++ b/bugs/filed/forms-by-index-crash/src/Sources/Startup.twin @@ -0,0 +1,17 @@ +' Reading Forms(k) through a variable declared As Form returns a broken reference, and the EXE crashes. +' Build the project and run the EXE (Debug.Print writes nothing there): it ends with 0xC0000005. +Module Startup + + Public Sub Main() + Dim f As Form ' the crash needs As Form; As Form1 or As Object works + Dim k As Long + + Load Form1 + For k = 0 To Forms.Count - 1 + Set f = Forms(k) + Debug.Print "k = "; k; " name = "; f.Name + Next k + End + End Sub + +End Module diff --git a/bugs/filed/hover-byval-option-explicit/REPORT.md b/bugs/filed/hover-byval-option-explicit/REPORT.md new file mode 100644 index 00000000..ce03019f --- /dev/null +++ b/bugs/filed/hover-byval-option-explicit/REPORT.md @@ -0,0 +1,48 @@ +Filed as [twinbasic/twinbasic#2448](https://github.com/twinbasic/twinbasic/issues/2448). + +## Hover says a `ByVal` parameter was auto-generated because `Option Explicit` is off + +**Describe the bug** +In a project with `Option Explicit` on, hovering over a `ByVal` parameter of type `String`, `Variant`, `Object`, a class or tbIDE's `Host` shows a note that the variable was auto-generated because `Option Explicit` is off, and recommends turning it on. The option is on, and the parameter was declared by the user. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `hover-byval-option-explicit.twinproj` (attached as `hover-byval-option-explicit.zip`) and open `Symbols.twin`. `project.optionExplicit` is true in its `Settings`. The file holds: + ``` + Public Sub Probe2(ByVal h As Host, ByVal count As Long, ByVal col As Collection, _ + ByVal o As Object, ByVal v As Variant, ByRef r As Host, ByVal s As String) + Dim d As Host + Debug.Print h Is Nothing, count, col Is Nothing, o Is Nothing, IsEmpty(v), r Is Nothing, s, d Is Nothing + End Sub + ``` +2. Hover over `s` where `Debug.Print` uses it. +3. The hover shows: + + > *parameter* ByVal s As String + > + > ***note:*** *this variable was auto-generated due to* ***Option Explicit*** *being Off* + > + > ***recommendation:*** *use Option Explicit and declare variables explicitly* + +| hovered | note | +|---|---| +| `ByVal` of `String`, `Variant`, `Object`, `Collection` or tbIDE's `Host` | **yes** | +| `ByVal` of `Long` | no | +| `ByRef r As Host` | no | +| a local, `Dim d As Host` or `Dim c As New Collection` | no | + +**Expected behavior** +No note on any of them: `Option Explicit` is on and every one of these names is declared. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Severity: cosmetic, but it tells the user to turn on an option that is already on, over a parameter they declared. + +So it takes `ByVal` and a type that is not a plain number. That looks like a hidden local copy that the compiler makes for such a parameter, which the hover then describes as a variable it generated for an undeclared name. + +This was measured by sending `textDocument/hover` over the compiler's language socket, with the parameters the IDE's own hover provider sends; the text above is the markdown that request returns, which is what the IDE's hover shows. The tooltip itself was not looked at in this run. + + diff --git a/bugs/filed/hover-byval-option-explicit/hover-byval-option-explicit.twinproj b/bugs/filed/hover-byval-option-explicit/hover-byval-option-explicit.twinproj new file mode 100644 index 00000000..0e1c2b7d Binary files /dev/null and b/bugs/filed/hover-byval-option-explicit/hover-byval-option-explicit.twinproj differ diff --git a/bugs/filed/hover-byval-option-explicit/repro.json b/bugs/filed/hover-byval-option-explicit/repro.json new file mode 100644 index 00000000..bec73993 --- /dev/null +++ b/bugs/filed/hover-byval-option-explicit/repro.json @@ -0,0 +1,5 @@ +{ + "mode": "manual", + "steps": "1. Open hover-byval-option-explicit.twinproj in the twinBASIC IDE (project.optionExplicit is true in its Settings) and open Symbols.twin.\n2. Hover the mouse over the name s, where Debug.Print uses it.\n3. The hover shows: *parameter* ByVal s As String, then \"note: this variable was auto-generated due to Option Explicit being Off\" and \"recommendation: use Option Explicit and declare variables explicitly\".\n4. Hover over count (ByVal Long), r (ByRef r As Host) and d (a Dim): no such note. Over h (Host), col (Collection), o (Object) and v (Variant): the note.\nExpected: no note on any of them, since Option Explicit is on and the parameters are declared.\nRe-measured on BETA 995 by `addin-test.bat --only symbols` (a script sends textDocument/hover over the compiler's language socket, as the IDE's own hover provider does; the project is test/addin/probes/symbols). The text in step 3 is what that request returns; the hover tooltip itself was not looked at.", + "issue": 2448 +} diff --git a/bugs/filed/hover-byval-option-explicit/src/Settings b/bugs/filed/hover-byval-option-explicit/src/Settings new file mode 100644 index 00000000..d8efc19c --- /dev/null +++ b/bugs/filed/hover-byval-option-explicit/src/Settings @@ -0,0 +1,42 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "HoverByvalOptionExplicit", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard DLL", + "project.description": "Reproduces: Hover says a `ByVal` parameter was auto-generated because `Option Explicit` is off", + "project.exportPathIsV2": true, + "project.id": "{0A327BDD-05D5-4672-AE72-41AF6C684CD8}", + "project.name": "HoverByvalOptionExplicit", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{99DEC38C-75F6-4488-8EE7-2D52D83881D2}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC IDE Extensibility Package", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "tbIDE", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 1 + } + ], + "project.settingsVersion": 1, + "project.warnings": { + "errors": [], + "ignored": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/hover-byval-option-explicit/src/Sources/Symbols.twin b/bugs/filed/hover-byval-option-explicit/src/Sources/Symbols.twin new file mode 100644 index 00000000..b1beae21 --- /dev/null +++ b/bugs/filed/hover-byval-option-explicit/src/Sources/Symbols.twin @@ -0,0 +1,8 @@ +' Hover over the parameters where Probe2 uses them. project.optionExplicit is true. +Module Symbols + Public Sub Probe2(ByVal h As Host, ByVal count As Long, ByVal col As Collection, _ + ByVal o As Object, ByVal v As Variant, ByRef r As Host, ByVal s As String) + Dim d As Host + Debug.Print h Is Nothing, count, col Is Nothing, o Is Nothing, IsEmpty(v), r Is Nothing, s, d Is Nothing + End Sub +End Module diff --git a/bugs/filed/import-packages-folder/REPORT.md b/bugs/filed/import-packages-folder/REPORT.md new file mode 100644 index 00000000..1205f847 --- /dev/null +++ b/bugs/filed/import-packages-folder/REPORT.md @@ -0,0 +1,44 @@ +Covered by the existing issue [twinbasic/twinbasic#841](https://github.com/twinbasic/twinbasic/issues/841). + +## `import` stops with exit code 999 on any folder inside `Packages`, so a project that embeds a package cannot be packed + +**Describe the bug** +`import`, the compiler executable's verb for packing a folder tree into a project file, stops partway through when the tree's top-level `Packages` folder contains a folder. It exits with code 999, writes no project file and leaves one already at the output path untouched. The last line printed is `IMPORTED FOLDER: \\Packages\`, with no `... DONE` and no `... FAILED`, and nothing reaches stderr. A package a project uses is embedded in it by default, as a folder of its own under `Packages`, so a project that embeds a package cannot be packed from its exported tree. + +**To Reproduce** +Steps to reproduce the behavior: +1. Unzip `import-packages-folder.zip` (it holds `import-packages-folder.twinproj`) and run `twinBASIC_win32.exe export C:\path\import-packages-folder.twinproj C:\path\tree\`. The only unusual thing in the tree is the file `Packages\Nested\x.txt`, so `Packages` holds a folder. +2. Run `twinBASIC_win32.exe import C:\path\out.twinproj C:\path\tree\ --overwrite`. +3. See the output end at `IMPORTED FOLDER: C:\path\tree\\Packages\`, and the exit code be 999. +4. See that `out.twinproj` was not written, and that one already there is unchanged. + +**Expected behavior** +The tree is packed, as it is when `Packages` holds no folder, and the run ends `... DONE` with exit code 0. If a folder in `Packages` is something the importer cannot accept, it should say so with an `ERROR:` line and `... FAILED`. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Severity: the command line cannot pack any project that embeds a package, and the failure prints neither `... DONE` nor `... FAILED`. The exit code is 999, where every other failure observed exits 0. Also on BETA 983, and there with `twinBASIC_win64.exe` as well. + +The smallest reproduction is one empty folder: export any project, add an empty `Packages\Nested\` to the tree, and import it. Every case below starts from a fresh `export` of the HelloWorld sample: + +| added to the exported tree | result | +|---|---| +| nothing | exit 0, `... DONE` | +| an empty `Packages\Nested\` | **exit 999, no project** | +| `Packages\Nested\x.txt` | **exit 999, no project** | +| `Packages\Nested\Settings`, a copy of the root `Settings` | **exit 999, no project** | +| `Packages\A\B\` | **exit 999, no project** | +| `packages\Nested\`, in lower case | **exit 999, no project** | +| `Packages\x.txt` (a file, no folder) | exit 0, `... DONE` | +| an empty `Packages\` on its own | exit 0, `... DONE` | +| `Miscellaneous\Nested\x.txt` | exit 0, `... DONE` | +| `Sources\Packages\Nested\` (a `Packages` below the top level) | exit 0, `... DONE` | + +So the trigger is a folder inside the top-level `Packages`, whatever it holds: an empty one does it, and so does one with a `Settings` file of its own, which is what a real package has. Leaving out `--overwrite` makes no difference: with a project already at the output path, `import` still stops with 999 rather than refusing to overwrite it. + +This is not malformed input. `export` writes the embedded package out as a folder under `Packages`, and `import` of that tree then stops. Five of the 48 project and package files the IDE ships have such a folder: `WinNativeCommonCtls` (which embeds `VBComDlg`), samples 8, 17 and 23, and the *Standard EXE (plus VBCCR v1.8)* project template. Each was measured: `export` succeeds, and `import` of the tree it has just written stops as above. None of them round-trips through the command line, and neither does any project created from that template, nor any export written by the IDE's **Export Project**, which always adds the compiler packages under `Packages`. + + diff --git a/bugs/filed/import-packages-folder/import-packages-folder.twinproj b/bugs/filed/import-packages-folder/import-packages-folder.twinproj new file mode 100644 index 00000000..e82900a8 Binary files /dev/null and b/bugs/filed/import-packages-folder/import-packages-folder.twinproj differ diff --git a/bugs/filed/import-packages-folder/repro.json b/bugs/filed/import-packages-folder/repro.json new file mode 100644 index 00000000..e6ec95c9 --- /dev/null +++ b/bugs/filed/import-packages-folder/repro.json @@ -0,0 +1,16 @@ +{ + "mode": "cli", + "cli": [ + "import", + "{tmp}\\packed.twinproj", + "{src}\\" + ], + "expect": { + "exit": 999, + "output": [ + "IMPORTED FOLDER: .*Packages" + ] + }, + "issue": 841, + "existing": true +} diff --git a/bugs/filed/import-packages-folder/src/Packages/Nested/x.txt b/bugs/filed/import-packages-folder/src/Packages/Nested/x.txt new file mode 100644 index 00000000..53ee3b4b --- /dev/null +++ b/bugs/filed/import-packages-folder/src/Packages/Nested/x.txt @@ -0,0 +1 @@ +A folder inside Packages. import stops with exit code 999 on it, whatever it holds. diff --git a/bugs/filed/import-packages-folder/src/Settings b/bugs/filed/import-packages-folder/src/Settings new file mode 100644 index 00000000..244c8a29 --- /dev/null +++ b/bugs/filed/import-packages-folder/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ImportPackagesFolder", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `import` stops with exit code 999 on any folder inside `Packages`, so a project that embeds a package cannot be packed", + "project.exportPathIsV2": true, + "project.id": "{E3DF8196-4455-43C9-8AEA-CF274E7333FB}", + "project.name": "ImportPackagesFolder", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/import-packages-folder/src/Sources/Startup.twin b/bugs/filed/import-packages-folder/src/Sources/Startup.twin new file mode 100644 index 00000000..7a80e6b2 --- /dev/null +++ b/bugs/filed/import-packages-folder/src/Sources/Startup.twin @@ -0,0 +1,7 @@ +' A project with a folder inside Packages. Nothing to run: import of this tree stops with exit code 999. +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/filed/index-error-numbers/REPORT.md b/bugs/filed/index-error-numbers/REPORT.md new file mode 100644 index 00000000..74ef83ae --- /dev/null +++ b/bugs/filed/index-error-numbers/REPORT.md @@ -0,0 +1,36 @@ +Filed as [twinbasic/twinbasic#2440](https://github.com/twinbasic/twinbasic/issues/2440). + +## An out-of-range index raises `&H8002000B` or `&H80004005`, not VBA's error 9 + +**Describe the bug** +An array or `Collection` index that is out of range raises -2147352565 (`&H8002000B`, *Invalid index.*) or -2147467259 (`&H80004005`, *Unspecified error*). VBA raises error 9, *Subscript out of range*, so code that handles `Err.Number = 9` does not recognise the error. There is no diagnostic. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `index-error-numbers.twinproj` (attached as `index-error-numbers.zip`) and run it (F5). Its `Sub Main` holds the whole bug: + ``` + Dim a(5) As Long + On Error Resume Next + a(7) = 1 + Debug.Print Err.Number, Hex$(Err.Number), Err.Description + ``` +2. See `-2147352565 8002000B Invalid index.` for this case. The project goes on to try the other cases, one line each: + + | access | twinBASIC | VBA, per VBA-Docs' *Subscript out of range (Error 9)* | + |---|---|---| + | past a fixed or dynamic array's bound, a `Variant` array's, or `Split("x y")(5)` | -2147352565 (`8002000B`) *Invalid index.* | 9 | + | an element of an array never dimensioned: `Dim u() As Integer: u(8) = 234`, VBA-Docs' own example | -2147467259 (`80004005`) *Unspecified error* | 9 | + | a `Collection` member by a missing index or key | -2147467259 *Unspecified error* | 9 for a missing member | + | `Forms(99)`, `Forms.Item(-1)` | -2147467259 *Unspecified error* | --- | + +**Expected behavior** +Error 9, as in VBA, for each of these. `Err.Raise 9` already gives 9 with the description *Subscript out of range*. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Also on BETA 983, in the IDE and in a compiled EXE alike. Severity: VBA code that handles `Err.Number = 9` does not recognise the error, with no diagnostic. What does not reproduce it: `UBound` of an erased array, `Printers(99)` and `Err.Raise 9` all give 9, and division by zero gives 11. The IDE's run-time error panel shows the same number `Err.Number` holds, for the array case. An erased array behaves as one never dimensioned: `-2147467259` for an element, 9 from `LBound` and `UBound`. `Printers` raises 9 past its end but `-2147467259` for a negative index and for an unknown name. The description of `-2147467259` varies between runs: *Unspecified error* in one, *Automation error* in another. The project's output on BETA 995 matches the table's first three rows; the `Forms` row and the other facts in this paragraph are from earlier runs of the same kind, not re-run for this report. + + diff --git a/bugs/filed/index-error-numbers/index-error-numbers.twinproj b/bugs/filed/index-error-numbers/index-error-numbers.twinproj new file mode 100644 index 00000000..cd76c7e2 Binary files /dev/null and b/bugs/filed/index-error-numbers/index-error-numbers.twinproj differ diff --git a/bugs/filed/index-error-numbers/repro.json b/bugs/filed/index-error-numbers/repro.json new file mode 100644 index 00000000..d06f67c3 --- /dev/null +++ b/bugs/filed/index-error-numbers/repro.json @@ -0,0 +1,12 @@ +{ + "mode": "run", + "expect": { + "exit": 0, + "output": [ + "^a\\(7\\) = 1, past a fixed array: -2147352565 +8002000B ", + "^u\\(8\\) = 234, an array never dimensioned: -2147467259 +80004005 ", + "^c\\(5\\), a missing Collection index: -2147467259 +80004005 " + ] + }, + "issue": 2440 +} diff --git a/bugs/filed/index-error-numbers/src/Settings b/bugs/filed/index-error-numbers/src/Settings new file mode 100644 index 00000000..f41aba41 --- /dev/null +++ b/bugs/filed/index-error-numbers/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "IndexErrorNumbers", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: An out-of-range index raises `&H8002000B` or `&H80004005`, not VBA's error 9", + "project.exportPathIsV2": true, + "project.id": "{D19A71F3-2CAA-4DC8-89B9-102978B7BB28}", + "project.name": "IndexErrorNumbers", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/index-error-numbers/src/Sources/Startup.twin b/bugs/filed/index-error-numbers/src/Sources/Startup.twin new file mode 100644 index 00000000..63313204 --- /dev/null +++ b/bugs/filed/index-error-numbers/src/Sources/Startup.twin @@ -0,0 +1,38 @@ +' An out-of-range index raises &H8002000B or &H80004005, where VBA raises error 9. +Module Startup + + Private Sub Show(ByVal what As String) + Debug.Print what; ": "; Err.Number; " "; Hex$(Err.Number); " "; Err.Description + Err.Clear + End Sub + + Public Sub Main() + On Error Resume Next + + Dim a(5) As Long + a(7) = 1 + Show "a(7) = 1, past a fixed array" ' VBA: 9 + + Dim v As Variant = Array(1, 2, 3) + v(9) = 1 + Show "v(9) = 1, past a Variant array" ' VBA: 9 + + Dim s As String = Split("x y")(5) + Show "Split(""x y"")(5)" ' VBA: 9 + + Dim u() As Integer + u(8) = 234 + Show "u(8) = 234, an array never dimensioned" ' VBA: 9 + + Dim c As New Collection + c.Add 1, "one" + v = c(5) + Show "c(5), a missing Collection index" ' VBA: 9 + v = c("nokey") + Show "c(""nokey""), a missing Collection key" + + Err.Raise 9 + Show "Err.Raise 9" ' 9 + End Sub + +End Module diff --git a/bugs/filed/interface-extends-itself/REPORT.md b/bugs/filed/interface-extends-itself/REPORT.md new file mode 100644 index 00000000..e0265530 --- /dev/null +++ b/bugs/filed/interface-extends-itself/REPORT.md @@ -0,0 +1,48 @@ +Filed as [twinbasic/twinbasic#2434](https://github.com/twinbasic/twinbasic/issues/2434). + +## An `Interface` that extends itself compiles without a diagnostic, and Build then does nothing + +**Describe the bug** +An interface that extends itself, directly or through another interface, compiles with no error, warning, hint or info. Building the project then does nothing at all: clicking **Build** writes nothing to the DEBUG CONSOLE, opens no dialog and creates no file, and the IDE stays responsive. A cycle through classes or UDTs is refused at compile time instead. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `interface-extends-itself.twinproj` (attached as `interface-extends-itself.zip`). Its three source files hold the whole bug: + ``` + ' SelfCycle.twin + Interface IA Extends IA + End Interface + + ' PairCycleB.twin + Interface IB Extends IC + End Interface + + ' PairCycleC.twin + Interface IC Extends IB + End Interface + ``` +2. See the project compile with 0 errors, 0 warnings, 0 hints and 0 infos. +3. Click **Build**. +4. See nothing happen: no `[BUILD] Starting...` line in the DEBUG CONSOLE, no message, no output file. + +Any one of the three files is enough on its own; so is `IB` and `IC` in one file. + +**Expected behavior** +A compile error, as the other kinds of cycle get: + +| source | result | +|---|---| +| `Class CA` + `Inherits CA` + `End Class` | TB5127 circular reference | +| `Class CA` inheriting `CB` and `Class CB` inheriting `CA`, two files | TB5127 circular reference, TB5022 and TB5135 failed to import inherited members | +| `Type TA` holding a `TB` and `Type TB` holding a `TA`, two modules | TB5101 unable to finalize User Defined Type, possible circular reference | + +Failing that, a build that reports why it stopped. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Also on BETA 983, identically. The same project without the cyclic interfaces builds in about 16 seconds on both, and so does one with an ordinary chain, `Interface IQ Extends IP`. Severity: invalid code is accepted, and the only symptom is a Build button that silently does nothing. + + diff --git a/bugs/filed/interface-extends-itself/interface-extends-itself.twinproj b/bugs/filed/interface-extends-itself/interface-extends-itself.twinproj new file mode 100644 index 00000000..d1e4f913 Binary files /dev/null and b/bugs/filed/interface-extends-itself/interface-extends-itself.twinproj differ diff --git a/bugs/filed/interface-extends-itself/repro.json b/bugs/filed/interface-extends-itself/repro.json new file mode 100644 index 00000000..4e666a80 --- /dev/null +++ b/bugs/filed/interface-extends-itself/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "build", + "expect": { + "exit": 5, + "message": "did not start" + }, + "issue": 2434 +} diff --git a/bugs/filed/interface-extends-itself/src/Settings b/bugs/filed/interface-extends-itself/src/Settings new file mode 100644 index 00000000..ba8f5282 --- /dev/null +++ b/bugs/filed/interface-extends-itself/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "InterfaceExtendsItself", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: An `Interface` that extends itself compiles without a diagnostic", + "project.exportPathIsV2": true, + "project.id": "{9FBD3E0D-484A-42F6-85B2-F3E531CB3E3F}", + "project.name": "InterfaceExtendsItself", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/interface-extends-itself/src/Sources/PairCycleB.twin b/bugs/filed/interface-extends-itself/src/Sources/PairCycleB.twin new file mode 100644 index 00000000..8fde9b7e --- /dev/null +++ b/bugs/filed/interface-extends-itself/src/Sources/PairCycleB.twin @@ -0,0 +1,3 @@ +' With PairCycleC.twin: two interfaces that extend each other, across two files. +Interface IB Extends IC +End Interface diff --git a/bugs/filed/interface-extends-itself/src/Sources/PairCycleC.twin b/bugs/filed/interface-extends-itself/src/Sources/PairCycleC.twin new file mode 100644 index 00000000..f0304020 --- /dev/null +++ b/bugs/filed/interface-extends-itself/src/Sources/PairCycleC.twin @@ -0,0 +1,3 @@ +' With PairCycleB.twin: two interfaces that extend each other, across two files. +Interface IC Extends IB +End Interface diff --git a/bugs/filed/interface-extends-itself/src/Sources/SelfCycle.twin b/bugs/filed/interface-extends-itself/src/Sources/SelfCycle.twin new file mode 100644 index 00000000..7d77df05 --- /dev/null +++ b/bugs/filed/interface-extends-itself/src/Sources/SelfCycle.twin @@ -0,0 +1,3 @@ +' An interface that extends itself. Expected: a circular-reference error, as for a class. +Interface IA Extends IA +End Interface diff --git a/bugs/filed/interface-extends-itself/src/Sources/Startup.twin b/bugs/filed/interface-extends-itself/src/Sources/Startup.twin new file mode 100644 index 00000000..20233b3a --- /dev/null +++ b/bugs/filed/interface-extends-itself/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/filed/llvm-err-after-raise/REPORT.md b/bugs/filed/llvm-err-after-raise/REPORT.md new file mode 100644 index 00000000..18eb2ba4 --- /dev/null +++ b/bugs/filed/llvm-err-after-raise/REPORT.md @@ -0,0 +1,31 @@ +Filed as [twinbasic/twinbasic#2452](https://github.com/twinbasic/twinbasic/issues/2452). + +## `Err` after a handled `Err.Raise` in an LLVM-compiled procedure holds `&HEAEAEA01` and no text + +**Describe the bug** +In a procedure compiled with LLVM, an `Err.Raise` that the procedure handles leaves `Err.Number` at -353703423 (`&HEAEAEA01`), with an empty `Err.Source` and the generic description *Application-defined or object-defined error*. The handler cannot tell which error it caught. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `llvm-err-after-raise.twinproj` (attached as `llvm-err-after-raise.zip`). It needs an LLVM licence (Ultimate): the procedure below is compiled with LLVM by its attribute. Run it (F5). + ``` + [CompilerOptions("+llvm")] + Private Sub LlvmRaise() + On Error Resume Next + Err.Raise 5, "MySrc", "my text" + Debug.Print Err.Number & " / " & Err.Source & " / " & Err.Description + End Sub + ``` +2. See `-353703423 / / Application-defined or object-defined error`. The project also calls the same procedure without the attribute, and that one prints `5 / MySrc / my text`. + +**Expected behavior** +`5 / MySrc / my text`, as without the attribute. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Severity: an error handler in LLVM-compiled code cannot tell which error it caught. The same happens with `On Error GoTo` and a handler, with `Err.Raise 11` and with `Err.Raise 1000`, on win32 and win64. What does not reproduce it: a run-time error the procedure causes itself, `1 \ 0` on a `Long`, reads correctly as 11, and `Err.Number = 7` assigned directly reads 7. Measured in the IDE's run only (a `[RunAfterBuild]` probe through `tbrun`); not run in a built exe. BETA 983 refuses the project's LLVM procedure ("Unable to compile due to use of datatype that is not yet supported for LLVM compilation"), so it was not compared there. + + diff --git a/bugs/filed/llvm-err-after-raise/llvm-err-after-raise.twinproj b/bugs/filed/llvm-err-after-raise/llvm-err-after-raise.twinproj new file mode 100644 index 00000000..c90cafe3 Binary files /dev/null and b/bugs/filed/llvm-err-after-raise/llvm-err-after-raise.twinproj differ diff --git a/bugs/filed/llvm-err-after-raise/repro.json b/bugs/filed/llvm-err-after-raise/repro.json new file mode 100644 index 00000000..84109ac6 --- /dev/null +++ b/bugs/filed/llvm-err-after-raise/repro.json @@ -0,0 +1,11 @@ +{ + "mode": "run", + "expect": { + "exit": 0, + "output": [ + "^plain:\\s+5\\s+/\\s+MySrc\\s+/\\s+my text$", + "^LLVM:\\s+-353703423\\s+/\\s+/\\s+Application-defined or object-defined error$" + ] + }, + "issue": 2452 +} diff --git a/bugs/filed/llvm-err-after-raise/src/Settings b/bugs/filed/llvm-err-after-raise/src/Settings new file mode 100644 index 00000000..689fbc49 --- /dev/null +++ b/bugs/filed/llvm-err-after-raise/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "LlvmErrAfterRaise", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Err after a handled Err.Raise in an LLVM-compiled procedure holds `&HEAEAEA01` and no text", + "project.exportPathIsV2": true, + "project.id": "{E1AE854F-2975-49DC-9886-6A541C84316C}", + "project.name": "LlvmErrAfterRaise", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/llvm-err-after-raise/src/Sources/Startup.twin b/bugs/filed/llvm-err-after-raise/src/Sources/Startup.twin new file mode 100644 index 00000000..a5238e1a --- /dev/null +++ b/bugs/filed/llvm-err-after-raise/src/Sources/Startup.twin @@ -0,0 +1,23 @@ +' Err after a handled Err.Raise in an LLVM-compiled procedure holds &HEAEAEA01 and no text. +' Needs an LLVM licence (Ultimate): LlvmRaise is compiled with LLVM by its [CompilerOptions("+llvm")]. +Module Startup + + [CompilerOptions("+llvm")] + Private Sub LlvmRaise() + On Error Resume Next + Err.Raise 5, "MySrc", "my text" + Debug.Print "LLVM: "; Err.Number; " / "; Err.Source; " / "; Err.Description ' expected 5 / MySrc / my text + End Sub + + Private Sub PlainRaise() + On Error Resume Next + Err.Raise 5, "MySrc", "my text" + Debug.Print "plain: "; Err.Number; " / "; Err.Source; " / "; Err.Description + End Sub + + Public Sub Main() + PlainRaise + LlvmRaise + End Sub + +End Module diff --git a/bugs/filed/populatefrom-no-args/REPORT.md b/bugs/filed/populatefrom-no-args/REPORT.md new file mode 100644 index 00000000..dd02a849 --- /dev/null +++ b/bugs/filed/populatefrom-no-args/REPORT.md @@ -0,0 +1,39 @@ +Filed as [twinbasic/twinbasic#2450](https://github.com/twinbasic/twinbasic/issues/2450). + +## `[PopulateFrom]` with no arguments crashes the compiler + +**Describe the bug** +An `Enum` marked `[PopulateFrom]` with no argument list crashes the compiler while the project is parsed. `tbbuild` reports it as a crash, `the compiler crashed 2x -- this project takes it down`, `last parsing: CrashProbe.twin`. A person who forgets the arguments is not told what is missing. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `populatefrom-no-args.twinproj` (attached as `populatefrom-no-args.zip`). Its one source file, `Probe.twin`, holds the whole bug: + ``` + Public Module CrashProbe + [PopulateFrom] + Public Enum E + End Enum + End Module + ``` +2. See the compiler crash and restart while the project is parsed. + +The body of the Enum does not matter: the same crash comes with a member in it, and with the Enum inside a Class instead of a Module. The documented shape is five strings, `("json", "/Resources/PROBE/Strings.json", "events", "name", "id")`, and the other wrong shapes tried are handled: + +| argument list | result | +|---|---| +| none, `[PopulateFrom]` | **the compiler crashes** | +| `(True)`, `(False)`, `(1)` | TB5155 `This attribute is not supported in this context` | +| `("probe")`, on the reproduction above | TB5083 `unsupported data source` | +| the documented five strings, with a resource that exists | compiles | + +**Expected behavior** +A diagnostic naming the missing arguments, as the other wrong argument lists get (TB5155 or TB5083), not a crash. A missing argument list is the one wrong shape that is not checked. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +First seen on BETA 987; the reproducer crashes BETA 983 as well. Severity: the compiler process dies while the project is being parsed. The same project with `[PopulateFrom("probe")]` builds and reports the one TB5083 row. The rows for `(True)`, `(False)` and `(1)` come from a batch of probes, not from the reproducer. + + diff --git a/bugs/filed/populatefrom-no-args/populatefrom-no-args.twinproj b/bugs/filed/populatefrom-no-args/populatefrom-no-args.twinproj new file mode 100644 index 00000000..9d301c33 Binary files /dev/null and b/bugs/filed/populatefrom-no-args/populatefrom-no-args.twinproj differ diff --git a/bugs/filed/populatefrom-no-args/repro.json b/bugs/filed/populatefrom-no-args/repro.json new file mode 100644 index 00000000..92e88432 --- /dev/null +++ b/bugs/filed/populatefrom-no-args/repro.json @@ -0,0 +1,7 @@ +{ + "mode": "compile", + "expect": { + "exit": 4 + }, + "issue": 2450 +} diff --git a/bugs/filed/populatefrom-no-args/src/Settings b/bugs/filed/populatefrom-no-args/src/Settings new file mode 100644 index 00000000..610bb318 --- /dev/null +++ b/bugs/filed/populatefrom-no-args/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "PopulatefromNoArgs", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `[PopulateFrom]` with no arguments crashes the compiler", + "project.exportPathIsV2": true, + "project.id": "{283FE68E-EE87-4A38-97FD-E228E8D2CB5F}", + "project.name": "PopulatefromNoArgs", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/populatefrom-no-args/src/Sources/Probe.twin b/bugs/filed/populatefrom-no-args/src/Sources/Probe.twin new file mode 100644 index 00000000..3674040a --- /dev/null +++ b/bugs/filed/populatefrom-no-args/src/Sources/Probe.twin @@ -0,0 +1,6 @@ +' [PopulateFrom] with no argument list on an Enum: the compiler crashes while parsing this file. +Public Module CrashProbe + [PopulateFrom] + Public Enum E + End Enum +End Module diff --git a/bugs/filed/populatefrom-no-args/src/Sources/Startup.twin b/bugs/filed/populatefrom-no-args/src/Sources/Startup.twin new file mode 100644 index 00000000..20233b3a --- /dev/null +++ b/bugs/filed/populatefrom-no-args/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/filed/private-types-in-public-members/REPORT.md b/bugs/filed/private-types-in-public-members/REPORT.md new file mode 100644 index 00000000..16cec217 --- /dev/null +++ b/bugs/filed/private-types-in-public-members/REPORT.md @@ -0,0 +1,62 @@ +Filed as [twinbasic/twinbasic#2454](https://github.com/twinbasic/twinbasic/issues/2454). + +## Public members are typed with Private components, so a default project cannot use them + +**Describe the bug** +Public members of the compiler packages are typed with `Private` classes, and `Public Enum`s sit inside `Private Module`s, so a project that references a package the ordinary way cannot name the types its documented members take. `VB.Report` shows it most sharply, because the compiler names the type in one diagnostic and rejects it in the next. Handling a documented public event requires opting into the package's private half by setting the library symbol to `*VB`. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `private-types-in-public-members.twinproj` (attached as `private-types-in-public-members.zip`). It references the VB package the ordinary way, and its one source file, `ProbeReport.twin`, holds the whole bug: + ``` + Class ProbeReport + Private WithEvents rpt As VB.Report + Private Sub rpt_BeforePaintSection(ByVal Section As VB.ControlsSection) + End Sub + End Class + ``` +2. See two errors: + ``` + TB5018 unable to match this handler to its event member. The expected signature was: + Private Sub rpt_BeforePaintSection(ByVal Section As ControlsSection) + TB5079 Unrecognized datatype symbol 'ControlsSection' + ``` +3. Set the VB reference's library symbol to `*VB` and compile again: the handler compiles exactly as written. + +`BeforePaintSection` is a public event whose parameter type is `Private Class ControlsSection` (`VB/Sources/SUPPORT/ControlsSection.twin:4`). The other spellings are worse than useless: `ByVal Section As Object` binds and compiles, while `Variant`, the bare name, and omitting the parameter all fail. + +**The CustomControls package has the same shape across its whole style surface.** Every class in `CustomControlsPackage/Sources/zTemporarySupport.twin` is `Private` (`Corner`, `Corners`, `Padding`, `FillColorPoint`, `FillColorPoints`, `Border`, `Borders`, `Line`, `Fill`, `TextRendering`, `Anchors`, `WindowsFormOptions` and the per-control `...State` classes), while the members that hand them out are public: + +| member | declared | what a default project can do | +|---|---|---| +| `Borders.Elements` | `Public WithEvents Elements() As Border` | read it; assigning needs a `Border()` it cannot declare | +| `FillColorPoints.Values` | `Public WithEvents Values() As FillColorPoint` | the same | +| `TextRendering.Outlines` | an array of `Border` | the same, and with no `SetSimpleBorder` equivalent to fall back on | +| `Canvas.RuntimeUICCCanvasAddElement` | `(ByVal Me As Canvas, ByRef ElementDescriptor As Any)` | pass a record it declares itself, with the style members left `Nothing` | + +With `*CustomControlsPackage`, all of it works under the package qualifier: `New CustomControlsPackage.Border`, `Dim elems(0 To 2) As CustomControlsPackage.Border`, `Dim descriptor As CustomControlsPackage.ElementDescriptor`, and a complete `ICustomControl` implementation that sets `BackgroundFill` and `TextRenderingOptions` from `New CustomControlsPackage.Fill` / `.TextRendering`. Both `CustomControlsPackage.ElementDescriptor` and `CustomControlsPackage.UDTs.ElementDescriptor` resolve, although the UDT is declared `Public Type` inside `Private Class UDTs`. + +**Two more packages have the same shape.** In both, a `Public Enum` sits inside a `Private Module`, so a consumer can name neither the enum nor its members, and both enums are the argument type of a documented, public member: + +| package | declaration | what a default project cannot write | +|---|---|---| +| WinNativeCommonCtls | `Private Module ImageListConsts` then `Public Enum ImlDrawConstants` | `ImageList1.ListImages(1).Draw hDC, x, y, ImlDrawTransparent Or ImlDrawFocus` | +| cefPackage | `Private Module _cef_log_severity_t` then `Enum CefLogSeverity` | `CefBrowser1.EnvironmentOptions.LogSeverity = CefLogWarning` | + +Neither the bare member (`ImlDrawTransparent`), the enum name (`ImlDrawConstants.…`), nor the owning control as a qualifier (`ImageList.ImlDrawConstants.…`) resolves: all three are `TB5079 Unrecognized symbol`. The asterisk works here too: with the library symbol set to `*WinNativeCommonCtls`, `WinNativeCommonCtls.ImlDrawTransparent` compiles. The VB package has it from a different direction as well: the enums nested inside a control class, `MultiFrameDirectionConstants` in `MultiFrame` and `QRCodegenEccConstants` in `QRCode`, are equally unreachable, so `mfPanels.Direction = vbDirectionHorizontal` does not compile although it is what the property's own documentation says to write. + +**Expected behavior** +A type that appears in the signature of a public member is itself public, so a default project can write the handler, declare the `Border()`, and pass the enum, without importing the package's private half. If the types are meant to be internal, the members that hand them out should not be public. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +The reproducer holds the `VB.Report` case; the CustomControls, WinNativeCommonCtls, cefPackage and MultiFrame / QRCode cases above need packages the reproducer does not reference, and were each measured on BETA 995. The VB.Report case reproduces identically on BETA 983. Severity: documented APIs need the consumer to expose the package's internals, and one event asks for a type it then refuses. The asterisk is a workaround, not a fix. + +This reads as an oversight rather than a policy. The file is called `zTemporarySupport.twin`, and the package's own changelog dates the narrowing: "v0.0.5.0, 15th September 2022 --- improved: made changes to ensure nothing within the package is being exposed unnecessarily." Reducing the surface is reasonable; what it missed is that these particular classes are not internal. They appear in the signatures of members that stayed public, so every consumer of those members is now required to import the package with an asterisk and qualify names that the package's own controls write bare. + +It went unnoticed because none of the 32 sample projects the IDE ships exercises any of this from code. Exported and grepped, all of them: `ControlsSection` appears only as a designer `_className` in the two `.tbreport` files and no sample handles `BeforePaintSection`; `ElementDescriptor`, `ICustomControl`, `ICustomForm` and `RuntimeUICC*` appear nowhere at all; `Border` appears 971 times, every one a designer `_className` in a `.tbform`. No sample line sets `NormalState`, `HoverState` or any `.Fill.` from code. + + diff --git a/bugs/filed/private-types-in-public-members/private-types-in-public-members.twinproj b/bugs/filed/private-types-in-public-members/private-types-in-public-members.twinproj new file mode 100644 index 00000000..98f780ad Binary files /dev/null and b/bugs/filed/private-types-in-public-members/private-types-in-public-members.twinproj differ diff --git a/bugs/filed/private-types-in-public-members/repro.json b/bugs/filed/private-types-in-public-members/repro.json new file mode 100644 index 00000000..69fb4f48 --- /dev/null +++ b/bugs/filed/private-types-in-public-members/repro.json @@ -0,0 +1,10 @@ +{ + "mode": "compile", + "expect": { + "diagnostics": [ + "TB5018", + "TB5079" + ] + }, + "issue": 2454 +} diff --git a/bugs/filed/private-types-in-public-members/src/Settings b/bugs/filed/private-types-in-public-members/src/Settings new file mode 100644 index 00000000..81b6fa29 --- /dev/null +++ b/bugs/filed/private-types-in-public-members/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "PrivateTypesInPublicMembers", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Public members are typed with Private components, so a default project cannot use them", + "project.exportPathIsV2": true, + "project.id": "{D5784AB4-D1A4-4EA2-A6F5-80272AD52107}", + "project.name": "PrivateTypesInPublicMembers", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/private-types-in-public-members/src/Sources/ProbeReport.twin b/bugs/filed/private-types-in-public-members/src/Sources/ProbeReport.twin new file mode 100644 index 00000000..b65821bf --- /dev/null +++ b/bugs/filed/private-types-in-public-members/src/Sources/ProbeReport.twin @@ -0,0 +1,6 @@ +' A public VB.Report event whose parameter type is a Private class: the handler cannot be written with the type the compiler names. +Class ProbeReport + Private WithEvents rpt As VB.Report + Private Sub rpt_BeforePaintSection(ByVal Section As VB.ControlsSection) + End Sub +End Class diff --git a/bugs/filed/private-types-in-public-members/src/Sources/Startup.twin b/bugs/filed/private-types-in-public-members/src/Sources/Startup.twin new file mode 100644 index 00000000..20233b3a --- /dev/null +++ b/bugs/filed/private-types-in-public-members/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/filed/shift-nonintegral/REPORT.md b/bugs/filed/shift-nonintegral/REPORT.md new file mode 100644 index 00000000..f2caaa1e --- /dev/null +++ b/bugs/filed/shift-nonintegral/REPORT.md @@ -0,0 +1,42 @@ +Filed as [twinbasic/twinbasic#2436](https://github.com/twinbasic/twinbasic/issues/2436). + +## Shifting a `Single`, `Double`, `Date`, `Boolean` or `String` compiles clean, then fails code generation + +**Describe the bug** +`<<` and `>>` on a `Single`, `Double`, `Date`, `Boolean` or `String` compile with no diagnostic, and code generation then fails for the procedure that holds the shift. The failure is reported only in the build log, as `[LINKER] compilation (codegen) error detected in '.' at line #`, naming the shift's line. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `shift-nonintegral.twinproj` (attached as `shift-nonintegral.zip`). Its source file `Probe.twin` holds the shift, and `Sub Main` calls it: + ``` + Dim a As Single = 7.9 + Dim c As Integer = 1 + Debug.Print "before the shift" + Debug.Print a << c + ``` +2. See the problems panel show 0 errors, 0 warnings, 0 hints and 0 infos. +3. Click **Build**. +4. See the build fail: `[LINKER] compilation (codegen) error detected in 'Probe.Show' at line #7`, then `[LINKER] FAILED due to compilation errors`, and no output file. + +The same shift in a procedure that nothing calls builds clean. In a `[RunAfterBuild]` Sub the build reports `[LINKER] SUCCESS created output file`, and the error appears only when the procedure is called: `[LINKER] compilation (codegen) error detected in 'Probe.Go' at line #8`. Nothing in the procedure runs, not even the statements before the shift, and `On Error Resume Next` in the caller does not see it; the caller stops too. + +| left operand | `<<` and `>>` | +|---|---| +| `Single`, `Double`, `Date`, `Boolean`, `String` | codegen error | +| `Byte`, `Integer`, `Long`, `LongLong`, `LongPtr` | shifts | +| `Currency`, `Decimal` | builds, but works on the value: a `Currency` holding 7.9, shifted left by 1, is 15.8 | +| a `Variant` holding any of the types above | builds, and multiplies or divides the value | + +Precedence reaches it too: `"x" & n << 2` parses as `("x" & n) << 2`, a `String` shift, and fails the same way. + +**Expected behavior** +Either a compile-time diagnostic at the shift, or a working shift. The documentation had said floating-point operands are truncated before shifting. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +BETA 983 gives the same result for the reproducer: the build fails with the same codegen error. Severity: the compiler accepts the expression with no diagnostic, and the procedure that contains it never runs. + + diff --git a/bugs/filed/shift-nonintegral/repro.json b/bugs/filed/shift-nonintegral/repro.json new file mode 100644 index 00000000..181a4502 --- /dev/null +++ b/bugs/filed/shift-nonintegral/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "build", + "expect": { + "exit": 5, + "message": "codegen" + }, + "issue": 2436 +} diff --git a/bugs/filed/shift-nonintegral/shift-nonintegral.twinproj b/bugs/filed/shift-nonintegral/shift-nonintegral.twinproj new file mode 100644 index 00000000..dc78acb8 Binary files /dev/null and b/bugs/filed/shift-nonintegral/shift-nonintegral.twinproj differ diff --git a/bugs/filed/shift-nonintegral/src/Settings b/bugs/filed/shift-nonintegral/src/Settings new file mode 100644 index 00000000..590e22ea --- /dev/null +++ b/bugs/filed/shift-nonintegral/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ShiftNonintegral", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Shifting a `Single`, `Double`, `Date`, `Boolean` or `String` compiles clean, then fails code generation", + "project.exportPathIsV2": true, + "project.id": "{B4999489-5B3A-4F1E-8E38-125E630D67CF}", + "project.name": "ShiftNonintegral", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/shift-nonintegral/src/Sources/Probe.twin b/bugs/filed/shift-nonintegral/src/Sources/Probe.twin new file mode 100644 index 00000000..3262ccb4 --- /dev/null +++ b/bugs/filed/shift-nonintegral/src/Sources/Probe.twin @@ -0,0 +1,9 @@ +' Shifting a Single compiles clean; code generation then fails for this procedure, which never runs. +Module Probe + Public Sub Show() + Dim a As Single = 7.9 + Dim c As Integer = 1 + Debug.Print "before the shift" + Debug.Print a << c + End Sub +End Module diff --git a/bugs/filed/shift-nonintegral/src/Sources/Startup.twin b/bugs/filed/shift-nonintegral/src/Sources/Startup.twin new file mode 100644 index 00000000..6a21ebe9 --- /dev/null +++ b/bugs/filed/shift-nonintegral/src/Sources/Startup.twin @@ -0,0 +1,7 @@ +Module Startup + + Public Sub Main() + Probe.Show + End Sub + +End Module diff --git a/bugs/filed/stop-at-error-ends-procedure/REPORT.md b/bugs/filed/stop-at-error-ends-procedure/REPORT.md new file mode 100644 index 00000000..e8e44004 --- /dev/null +++ b/bugs/filed/stop-at-error-ends-procedure/REPORT.md @@ -0,0 +1,47 @@ +Filed as [twinbasic/twinbasic#2441](https://github.com/twinbasic/twinbasic/issues/2441). + +## Stop at a run-time error ends only the procedure that raised it + +**Describe the bug** +When a program raises an untrapped error in the debugger and the error panel opens, **Stop** (the panel's button, the toolbar's Stop, or **Run → End**) ends only the procedure that raised the error. The caller carries on running: statements after the call still execute. The program goes on running after the user asked it to stop. Three runs, the same each time. + +Its worst consequence is a false pass. At a failed `Assert`, whose error is raised by the assertion's own procedure, **Stop** ends only that procedure: the test carries on past the failed check, and a runner in the shape of the Assert tutorial (`Testing-with-Assert.md`) then prints `All PadLeft tests passed.` + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `stop-at-error-ends-procedure.twinproj` (attached as `stop-at-error-ends-procedure.zip`) and press F5. `Main` calls `FillTable`, which raises an error at the line marked `FAILS`, and prints a line after the call: + ``` + Public Sub Main() + Debug.Print "main start" + FillTable + Debug.Print "main after call" + End Sub + + Private Sub FillTable() + Dim a(3) As Long, i As Long, idx As Long = 5 + For i = 0 To 2 + Debug.Print "loop " & i + a(idx) = i ' FAILS + Debug.Print "after fail " & i + Next + Debug.Print "fill end" + End Sub + ``` + The error panel opens. The Debug Console shows `main start` and `loop 0`. +2. In the error panel choose **Stop**. (Or press the toolbar's Stop, or **Run → End**: the same.) +3. `FillTable` ends, and `Main` goes on: the Debug Console shows `main after call`. +4. A failed assertion: in `Main` change the call `FillTable` to `TestPadLeft`, which holds `Assert.Exact.AreEqual " hi", "hi"` between two `Debug.Print` lines, and press F5. At the error panel choose **Stop**, or **Run → End**. `TestPadLeft` carries on past the failed assertion (`test after assert`) and `Main` prints `main after call`. + +**Expected behavior** +**Stop** ends the whole run, as it does at an ordinary break: nothing more is printed. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Also on BETA 983: both lanes pass there as well. Severity: the program goes on running after the user asked it to stop. + +What does not reproduce it: **Stop** at an ordinary break (a breakpoint or a step) prints `aborted` and ends the whole run. By hand on BETA 983, **Ignore (Resume Next)** at the failed assertion did the same as **Stop**, 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). + + diff --git a/bugs/filed/stop-at-error-ends-procedure/repro.json b/bugs/filed/stop-at-error-ends-procedure/repro.json new file mode 100644 index 00000000..c74db8c9 --- /dev/null +++ b/bugs/filed/stop-at-error-ends-procedure/repro.json @@ -0,0 +1,5 @@ +{ + "mode": "manual", + "steps": "1. Open stop-at-error-ends-procedure.twinproj in the twinBASIC IDE and press F5. FillTable raises an error at the line marked FAILS and the error panel opens. The DEBUG CONSOLE shows main start and loop 0.\n2. In the error panel choose Stop (or the toolbar's Stop, or Run > End).\n3. Expected: the run ends, and nothing more is printed. Actual: FillTable ends, and Main goes on: the DEBUG CONSOLE shows main after call.\n4. A failed assertion: in Main, change the call FillTable to TestPadLeft, press F5, and at the error panel (Assert.Exact.AreEqual raised it) choose Stop, or Run > End. TestPadLeft carries on past the failed assertion (test after assert) and Main prints main after call.\nControl: Stop at an ordinary break, such as a breakpoint or a step, ends the whole run and prints aborted.\nRe-measured on BETA 995 by `ide-test.bat --only debugger` and `--only assert` (the projects are test/ide/probes/debugger and assert); both pass on BETA 983 too.", + "issue": 2441 +} diff --git a/bugs/filed/stop-at-error-ends-procedure/src/Settings b/bugs/filed/stop-at-error-ends-procedure/src/Settings new file mode 100644 index 00000000..f1636493 --- /dev/null +++ b/bugs/filed/stop-at-error-ends-procedure/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "StopAtErrorEndsProcedure", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Stop at a run-time error ends only the procedure that raised it", + "project.exportPathIsV2": true, + "project.id": "{4D2A5FB0-AFB9-482C-BCBD-5F1D555BBE38}", + "project.name": "StopAtErrorEndsProcedure", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Assert", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "Assert", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 24 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/stop-at-error-ends-procedure/src/Sources/Startup.twin b/bugs/filed/stop-at-error-ends-procedure/src/Sources/Startup.twin new file mode 100644 index 00000000..a6d43049 --- /dev/null +++ b/bugs/filed/stop-at-error-ends-procedure/src/Sources/Startup.twin @@ -0,0 +1,25 @@ +' Run it with F5. FillTable fails, at the line marked FAILS, and Main prints a line after calling it. +' To try a failed assertion instead, change the call in Main from FillTable to TestPadLeft. +Module Startup + Public Sub Main() + Debug.Print "main start" + FillTable + Debug.Print "main after call" + End Sub + + Private Sub FillTable() + Dim a(3) As Long, i As Long, idx As Long = 5 + For i = 0 To 2 + Debug.Print "loop " & i + a(idx) = i ' FAILS + Debug.Print "after fail " & i + Next + Debug.Print "fill end" + End Sub + + Private Sub TestPadLeft() + Debug.Print "test start" + Assert.Exact.AreEqual " hi", "hi" ' FAILS + Debug.Print "test after assert" + End Sub +End Module diff --git a/bugs/filed/stop-at-error-ends-procedure/stop-at-error-ends-procedure.twinproj b/bugs/filed/stop-at-error-ends-procedure/stop-at-error-ends-procedure.twinproj new file mode 100644 index 00000000..a914bbb1 Binary files /dev/null and b/bugs/filed/stop-at-error-ends-procedure/stop-at-error-ends-procedure.twinproj differ diff --git a/bugs/filed/toolwindow-no-id/REPORT.md b/bugs/filed/toolwindow-no-id/REPORT.md new file mode 100644 index 00000000..4d822d8b --- /dev/null +++ b/bugs/filed/toolwindow-no-id/REPORT.md @@ -0,0 +1,42 @@ +Filed as [twinbasic/twinbasic#2449](https://github.com/twinbasic/twinbasic/issues/2449). + +## Every tool window given no id is the same window + +**Describe the bug** +`Host.ToolWindows.Add` declares its id (`UniqueIdForPositionPersistance`) `Optional`, so leaving it out looks correct. But every tool window added without an id is the same window: the second `Add` returns the window the first one made, after emptying it, and both `ToolWindow` objects are bound to it. An add-in's windows overwrite each other, or another add-in's, and nothing says so. + +**To Reproduce** +`toolwindow-no-id` is an add-in project: building it makes a DLL that the IDE's compiler loads on every start until the DLL is removed. Install it knowingly, in an IDE you can restart, and remove it afterwards. + +Steps to reproduce the behavior: +1. Open `toolwindow-no-id.twinproj` (attached as `toolwindow-no-id.zip`) and build it for Win32. The DLL is written to `Build\ToolwindowNoId_win32.dll` beside the project file. Close the IDE, copy the DLL into the IDE's `addins\win32` folder (beside `twinBASIC.exe`), and start the IDE again. Open any project: the toolbar has a button, *No id windows*. +2. Click the button. The add-in runs: + ``` + Set NoId1 = Host.ToolWindows.Add("Window one") + NoId1.Title = "NO ID 1" + NoId1.RootDomElement.ChildDomElements.Add("noid1", "div").Properties.innerText = "first" + NoId1.Visible = True + Set NoId2 = Host.ToolWindows.Add("Window two") + NoId2.Title = "NO ID 2" + NoId2.RootDomElement.ChildDomElements.Add("noid2", "div").Properties.innerText = "second" + NoId2.Visible = True + NoId1.RootDomElement.ChildDomElements.Add("noid1again", "div").Properties.innerText = "first again" + ``` +3. One tool window shows, titled `NO ID 2`, holding `noid2` (`second`) and `noid1again` (`first again`). The first window is gone, and what the add-in added through `NoId1` went into the second window. +4. Remove the add-in: close the IDE and delete `addins\win32\ToolwindowNoId_win32.dll`. + +**Expected behavior** +Two windows, `NO ID 1` holding `first` and `first again`, and `NO ID 2` holding `second`: each window given no id is a window of its own. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +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. None of the IDE's add-in samples leaves the id out. + +What does not reproduce it: a window given an id, or one window given none. + +`createToolWindow` in `ide/main.js` files each window under `e.guid`, which is the `UniqueIdForPositionPersistance` argument, and `""` when it is left out. `createToolWindowById(e)` returns the window it already has under that id, after `n.bodyElement.innerHTML=""`, instead of making another, and the page answers the compiler with that window's number, so both `ToolWindow` objects are bound to it. The same reuse is what hands an add-in its own window back after a compiler restart, when it asks again for the id it used before (`test/addin/reload.test.mjs` in this repository's harness), so a fix would give each window without an id one of its own rather than change the reuse. + + diff --git a/bugs/filed/toolwindow-no-id/repro.json b/bugs/filed/toolwindow-no-id/repro.json new file mode 100644 index 00000000..0d7cbc4b --- /dev/null +++ b/bugs/filed/toolwindow-no-id/repro.json @@ -0,0 +1,5 @@ +{ + "mode": "manual", + "steps": "This is an add-in project: building it makes a DLL that the IDE's compiler loads on every start until the DLL is removed. Install it knowingly, in an IDE you can restart, and remove it afterwards.\n1. Open toolwindow-no-id.twinproj and build it for Win32. The DLL is written to Build\\ToolwindowNoId_win32.dll beside the project file. Close the IDE, copy the DLL into the IDE's addins\\win32 folder (beside twinBASIC.exe), and start the IDE again. Open any project: the toolbar has a button, No id windows.\n2. Click the button. The add-in adds two tool windows, \"Window one\" and \"Window two\", with no id, and then adds a third element to the first.\n3. See one tool window, titled NO ID 2, holding the elements noid2 (\"second\") and noid1again (\"first again\"). The first window is gone, and what was added through its ToolWindow object went into the second window.\n4. Remove the add-in: close the IDE and delete addins\\win32\\ToolwindowNoId_win32.dll.\nRe-measured on BETA 995 by `addin-test.bat --only panes` (the last test; its add-in is test/addin/probes/panes, whose third button runs the same code).", + "issue": 2449 +} diff --git a/bugs/filed/toolwindow-no-id/src/Settings b/bugs/filed/toolwindow-no-id/src/Settings new file mode 100644 index 00000000..cc92a81a --- /dev/null +++ b/bugs/filed/toolwindow-no-id/src/Settings @@ -0,0 +1,42 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ToolwindowNoId", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard DLL", + "project.description": "Reproduces: Every tool window given no id is the same window", + "project.exportPathIsV2": true, + "project.id": "{48A74330-858B-4FFF-81B2-9BC0C7F8A8A6}", + "project.name": "ToolwindowNoId", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{99DEC38C-75F6-4488-8EE7-2D52D83881D2}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC IDE Extensibility Package", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "tbIDE", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 1 + } + ], + "project.settingsVersion": 1, + "project.warnings": { + "errors": [], + "ignored": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/toolwindow-no-id/src/Sources/MainModule.twin b/bugs/filed/toolwindow-no-id/src/Sources/MainModule.twin new file mode 100644 index 00000000..ba64eeed --- /dev/null +++ b/bugs/filed/toolwindow-no-id/src/Sources/MainModule.twin @@ -0,0 +1,42 @@ +' An add-in with one toolbar button. Clicking it adds two tool windows without an id, and then writes to the first one again. +Module MainModule + [DllExport] + Public Function tbCreateCompilerAddin(ByVal Host As Host) As AddIn + Return New NoIdAddin(Host) + End Function +End Module + +Private Class NoIdAddin + Implements AddIn + Private WithEvents Host As Host + Private WithEvents NoIdButton As Button + Private NoId1 As ToolWindow + Private NoId2 As ToolWindow + + Public Sub New(ByVal Host As Host) + Set Me.Host = Host + End Sub + + Private Property Get AddIn_Name() As String + Return "ToolWindow NoId AddIn" + End Property + + Private Sub Host_OnProjectLoaded() + Set NoIdButton = Host.Toolbars(0).AddButton("NoIdButton", "No id windows") + Host.DebugConsole.PrintText "[NoId] loaded" + End Sub + + ' Two windows given no id, then the first one written to again. + Private Sub NoIdButton_OnClick() + Set NoId1 = Host.ToolWindows.Add("Window one") + NoId1.Title = "NO ID 1" + NoId1.RootDomElement.ChildDomElements.Add("noid1", "div").Properties.innerText = "first" + NoId1.Visible = True + Set NoId2 = Host.ToolWindows.Add("Window two") + NoId2.Title = "NO ID 2" + NoId2.RootDomElement.ChildDomElements.Add("noid2", "div").Properties.innerText = "second" + NoId2.Visible = True + NoId1.RootDomElement.ChildDomElements.Add("noid1again", "div").Properties.innerText = "first again" + Host.DebugConsole.PrintText "[NoId] two windows added" + End Sub +End Class diff --git a/bugs/filed/toolwindow-no-id/toolwindow-no-id.twinproj b/bugs/filed/toolwindow-no-id/toolwindow-no-id.twinproj new file mode 100644 index 00000000..fc390831 Binary files /dev/null and b/bugs/filed/toolwindow-no-id/toolwindow-no-id.twinproj differ diff --git a/bugs/filed/typing-after-open-reversed/REPORT.md b/bugs/filed/typing-after-open-reversed/REPORT.md new file mode 100644 index 00000000..1fd08407 --- /dev/null +++ b/bugs/filed/typing-after-open-reversed/REPORT.md @@ -0,0 +1,35 @@ +Filed as [twinbasic/twinbasic#2447](https://github.com/twinbasic/twinbasic/issues/2447). + +## Typing just after a file opens at a position puts the text at that position, in reverse + +**Describe the bug** +For 700 ms after the code editor opens a file at a line and column (Go To Definition, a Find in Files result, an add-in's `Editors.Open`), the IDE puts the cursor back at that place whenever the compiler's decorations for the document arrive. Every edit brings new decorations, and each time the cursor goes back the 700 ms start again. So typing that starts inside the window, and goes on without a 0.7 s pause, puts each character at the opened position, in front of the one before it. Typed text goes to the wrong place and in the wrong order, and nothing shows that it happened. + +**To Reproduce** +This was measured by a script. By hand it needs typing without a pause of 0.7 s, which is hard to time. + +Steps to reproduce the behavior: +1. Open `typing-after-open-reversed.twinproj` (attached as `typing-after-open-reversed.zip`), with `Haystack.twin` not open in an editor tab. +2. Open `Haystack.twin` at line 4, column 9 through `openEditors.openFile(node, false, false, false, 4, 9)`, the call Find in Files makes (`node` is the file's entry in the project tree). By hand: Find in Files for `Dim needleCount` (one match, at line 4, column 9), and double-click the result. +3. 0.3 s later, move the cursor to line 3, column 1, and type `xyz`, one key every 150 ms. +4. Line 3 starts with `x`, and line 4 reads ` zyDim needleCount As Long`. + +The cause is in `ide/main.js`: `parseDocumentDecorations` ends with `if(performance.now()-revealedLineTime<700){revealLineInEditor(revealedLine,revealedLineColumn,revealedLineViewPortTop)}`, and `revealLineInEditor` sets the cursor's position and `revealedLineTime` again. Logged in the run above: `revealLineInEditor(4,9)` from `gotFileData`, then from `parseDocumentDecorations` 9 ms later, and again after each key. The same happens for a file that is already open, whose `onReveal` calls `revealLineInEditor` too. + +**Expected behavior** +Typing after the file opens puts `xyz` at 3:1, in order. Keeping the view where the reveal left it may be what the repeat is for; setting the cursor again is what does the damage. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 983 (not measured on BETA 995, see below) + +**Additional context** +Severity: typed text goes to the wrong place and in the wrong order, and nothing shows that it happened. + +What does not reproduce it: the same typing started more than 0.7 s after the file opened, which puts `xyz` at 3:1 in order. + +Found by the add-in test harness: `MsgBox(`, typed into the code editor just after opening a file at line 5, came out as `gBox(s` at the start of that line, with the `M` on the line below. + +BETA 995 was not measured: no lane exercises it. `parseDocumentDecorations` and `revealLineInEditor` are unchanged in BETA 995's `ide/main.js` (the `revealedLineTime<700` test was read again), and the test harness, which runs on BETA 995, still has to wait out the 700 ms before it places a cursor. + + diff --git a/bugs/filed/typing-after-open-reversed/repro.json b/bugs/filed/typing-after-open-reversed/repro.json new file mode 100644 index 00000000..6d52d4b2 --- /dev/null +++ b/bugs/filed/typing-after-open-reversed/repro.json @@ -0,0 +1,5 @@ +{ + "mode": "manual", + "steps": "Measured by a script, not by hand: a person has to type without a pause of 0.7 s, which is hard to time.\n1. Open typing-after-open-reversed.twinproj in the twinBASIC IDE, with Haystack.twin not open in an editor tab.\n2. Open Haystack.twin at line 4, column 9 the way Find in Files does: in the page, openEditors.openFile(node, false, false, false, 4, 9), with node the file's entry in the project tree. By hand: Find in Files for \"Dim needleCount\" (one match, at line 4, column 9), and double-click the result.\n3. 0.3 s later move the cursor to line 3, column 1, and type xyz, one key every 150 ms.\n4. Expected: line 3 starts with xyz. Actual: line 3 starts with x, and line 4 reads \" zyDim needleCount As Long\".\nControl: the same typing started more than 0.7 s after the file opened puts xyz at 3:1 in order.\nBETA 983 only: no lane measures it on BETA 995. In BETA 995's ide/main.js, parseDocumentDecorations still ends with if(performance.now()-revealedLineTime<700){revealLineInEditor(...)}, and the test harness (afterReveal in scripts/lib/tb-operate.mjs, used by the keys and symbols lanes on 995) still has to wait out that time before it places a cursor.", + "issue": 2447 +} diff --git a/bugs/filed/typing-after-open-reversed/src/Settings b/bugs/filed/typing-after-open-reversed/src/Settings new file mode 100644 index 00000000..eca4618b --- /dev/null +++ b/bugs/filed/typing-after-open-reversed/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "TypingAfterOpenReversed", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Typing just after a file opens at a position puts the text at that position, in reverse", + "project.exportPathIsV2": true, + "project.id": "{96B90D34-3517-4C6C-8502-9949C7E19A30}", + "project.name": "TypingAfterOpenReversed", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/typing-after-open-reversed/src/Sources/Haystack.twin b/bugs/filed/typing-after-open-reversed/src/Sources/Haystack.twin new file mode 100644 index 00000000..9da7815c --- /dev/null +++ b/bugs/filed/typing-after-open-reversed/src/Sources/Haystack.twin @@ -0,0 +1,8 @@ +' A haystack with a needle in it. +Module Haystack + Public Function FindTheNeedle(ByVal n As Long) As Long + Dim needleCount As Long + needleCount = n * 2 + Return needleCount + End Function +End Module diff --git a/bugs/filed/typing-after-open-reversed/src/Sources/Main.twin b/bugs/filed/typing-after-open-reversed/src/Sources/Main.twin new file mode 100644 index 00000000..42bbfed8 --- /dev/null +++ b/bugs/filed/typing-after-open-reversed/src/Sources/Main.twin @@ -0,0 +1,8 @@ +' Find in Files for "Dim needleCount" and open the result: Haystack.twin opens at line 4, column 9. +Module Main + Public Sub Main() + Dim needle As Long + needle = FindTheNeedle(3) + Debug.Print needle + End Sub +End Module diff --git a/bugs/filed/typing-after-open-reversed/typing-after-open-reversed.twinproj b/bugs/filed/typing-after-open-reversed/typing-after-open-reversed.twinproj new file mode 100644 index 00000000..0f1d26b7 Binary files /dev/null and b/bugs/filed/typing-after-open-reversed/typing-after-open-reversed.twinproj differ diff --git a/bugs/filed/variant-shift/REPORT.md b/bugs/filed/variant-shift/REPORT.md new file mode 100644 index 00000000..f7cac16e --- /dev/null +++ b/bugs/filed/variant-shift/REPORT.md @@ -0,0 +1,41 @@ +Filed as [twinbasic/twinbasic#2437](https://github.com/twinbasic/twinbasic/issues/2437). + +## A `Variant` shift multiplies a fractional value, and can return `Empty` + +**Describe the bug** +A `Variant` holding a `Double`, `Currency` or `Decimal` of 7.9, shifted left by 1, gives 15.8: the value is multiplied by 2, not shifted. Shifted right by 1, the `Double` and the `Decimal` give 3, but the `Currency` gives 3.95. A `Variant` holding an `Integer` or a `Long` shifted left by a count as large as the type's width gives `Empty` rather than 0. There is no diagnostic. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `variant-shift.twinproj` (attached as `variant-shift.zip`) and run it (F5). Its `Sub Main` holds the whole bug: + ``` + Dim d As Variant = CDbl(7.9) + Dim c As Variant = CCur(7.9) + Debug.Print d << 1 ' 15.8 + Debug.Print c >> 1 ' 3.95 + + Dim i As Variant = CInt(1) + Dim l As Variant = CLng(1) + Debug.Print TypeName(i << 20) ' Empty + Debug.Print TypeName(l << 32) ' Empty + ``` +2. See the DEBUG CONSOLE show 15.8, 3.95, `Empty` and `Empty`. The project prints the same for a `Decimal` too. + +| expression | result | +|---|---| +| a `Variant` holding `CInt(1)`, `<< 20` | `Empty` | +| a `Variant` holding `CLng(1)`, `<< 32` | `Empty` | +| a `Variant` holding `CLng(1)`, `<< 31` | `Long` -2147483648 | +| a `Long` variable holding 1, `<< 32` | 0 | + +**Expected behavior** +A shift moves bits, so a fractional value should be truncated first, as the right shift of the `Double` and the `Decimal` already does: `7.9 << 1` is 14 and `7.9 >> 1` is 3. A count as large as the width of the type should give 0 and keep the type, as the `Long` variable's shift does, not `Empty`. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 995 + +**Additional context** +Severity: wrong values, with no diagnostic. Also on BETA 983, with the same results, except that the right shift of a `Variant` holding 7.9 gave 4 there for all three types (BETA 984 made `>>` arithmetic). A `Long` variable's shift by 32 gives 0 on both builds. + + diff --git a/bugs/filed/variant-shift/repro.json b/bugs/filed/variant-shift/repro.json new file mode 100644 index 00000000..0e3cfe5b --- /dev/null +++ b/bugs/filed/variant-shift/repro.json @@ -0,0 +1,13 @@ +{ + "mode": "run", + "expect": { + "exit": 0, + "output": [ + "^Double\\s+7\\.9 << 1 =\\s+15\\.8", + "^Currency 7\\.9 >> 1 =\\s+3\\.95", + "^Integer 1 << 20 is Empty$", + "^Long\\s+1 << 32 is Empty$" + ] + }, + "issue": 2437 +} diff --git a/bugs/filed/variant-shift/src/Settings b/bugs/filed/variant-shift/src/Settings new file mode 100644 index 00000000..35f50a22 --- /dev/null +++ b/bugs/filed/variant-shift/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "VariantShift", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: A `Variant` shift multiplies a fractional value, and can return `Empty`", + "project.exportPathIsV2": true, + "project.id": "{6F429156-FE69-4C9A-930A-7303C8643923}", + "project.name": "VariantShift", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/filed/variant-shift/src/Sources/Startup.twin b/bugs/filed/variant-shift/src/Sources/Startup.twin new file mode 100644 index 00000000..f21eb9b0 --- /dev/null +++ b/bugs/filed/variant-shift/src/Sources/Startup.twin @@ -0,0 +1,25 @@ +' A Variant shift multiplies a fractional value, and a large count gives Empty. +Module Startup + + Public Sub Main() + Dim d As Variant = CDbl(7.9) + Dim c As Variant = CCur(7.9) + Dim m As Variant = CDec(7.9) + Debug.Print "Double 7.9 << 1 = "; d << 1 ' expected 14, got 15.8 + Debug.Print "Currency 7.9 << 1 = "; c << 1 ' expected 14, got 15.8 + Debug.Print "Decimal 7.9 << 1 = "; m << 1 ' expected 14, got 15.8 + Debug.Print "Double 7.9 >> 1 = "; d >> 1 ' 3 + Debug.Print "Currency 7.9 >> 1 = "; c >> 1 ' expected 3, got 3.95 + Debug.Print "Decimal 7.9 >> 1 = "; m >> 1 ' 3 + + Dim i As Variant = CInt(1) + Dim l As Variant = CLng(1) + Debug.Print "Integer 1 << 20 is "; TypeName(i << 20) ' expected Integer 0, got Empty + Debug.Print "Long 1 << 32 is "; TypeName(l << 32) ' expected Long 0, got Empty + Debug.Print "Long 1 << 31 is "; TypeName(l << 31); " "; l << 31 + + Dim n As Long = 1 + Debug.Print "Long variable 1 << 32 = "; n << 32 ' 0 + End Sub + +End Module diff --git a/bugs/filed/variant-shift/variant-shift.twinproj b/bugs/filed/variant-shift/variant-shift.twinproj new file mode 100644 index 00000000..0153644c Binary files /dev/null and b/bugs/filed/variant-shift/variant-shift.twinproj differ diff --git a/bugs/generic-body-error/generic-body-error.twinproj b/bugs/generic-body-error/generic-body-error.twinproj new file mode 100644 index 00000000..f72622f5 Binary files /dev/null and b/bugs/generic-body-error/generic-body-error.twinproj differ diff --git a/bugs/generic-body-error/repro.json b/bugs/generic-body-error/repro.json new file mode 100644 index 00000000..de94b82b --- /dev/null +++ b/bugs/generic-body-error/repro.json @@ -0,0 +1,6 @@ +{ + "mode": "compile", + "expect": { + "diagnostics": ["TB5092"] + } +} diff --git a/bugs/generic-body-error/src/Settings b/bugs/generic-body-error/src/Settings new file mode 100644 index 00000000..ef7bdbe6 --- /dev/null +++ b/bugs/generic-body-error/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "GenericBodyError", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: An error in the body of a generic procedure names neither the type nor the call that caused it", + "project.exportPathIsV2": true, + "project.id": "{A5955179-132F-4565-80DD-75F5E26972D4}", + "project.name": "GenericBodyError", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/generic-body-error/src/Sources/GenMax.twin b/bugs/generic-body-error/src/Sources/GenMax.twin new file mode 100644 index 00000000..317bf376 --- /dev/null +++ b/bugs/generic-body-error/src/Sources/GenMax.twin @@ -0,0 +1,16 @@ +' The error is reported at line 4, "If a > b", for the call in Use; neither the type nor the call is named. +Module GenMax +Public Function Max(Of T)(a As T, b As T) As T + If a > b Then + Return a + Else + Return b + End If +End Function + +Public Sub Use() + Dim c1 As New Collection, c2 As New Collection + Dim m As Collection + Set m = Max(Of Collection)(c1, c2) +End Sub +End Module diff --git a/bugs/generic-body-error/src/Sources/Startup.twin b/bugs/generic-body-error/src/Sources/Startup.twin new file mode 100644 index 00000000..20233b3a --- /dev/null +++ b/bugs/generic-body-error/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/import-no-settings-silent/import-no-settings-silent.twinproj b/bugs/import-no-settings-silent/import-no-settings-silent.twinproj new file mode 100644 index 00000000..f4b8fa49 Binary files /dev/null and b/bugs/import-no-settings-silent/import-no-settings-silent.twinproj differ diff --git a/bugs/import-no-settings-silent/repro.json b/bugs/import-no-settings-silent/repro.json new file mode 100644 index 00000000..bc0a5d4b --- /dev/null +++ b/bugs/import-no-settings-silent/repro.json @@ -0,0 +1,9 @@ +{ + "mode": "cli", + "cli": ["import", "{tmp}\\packed.twinproj", "{src}\\Sources\\"], + "expect": { + "exit": 0, + "output": ["IMPORTED: .*Startup\\.twin", "^\\.\\.\\. FAILED"], + "absent": ["ERROR"] + } +} diff --git a/bugs/import-no-settings-silent/src/Settings b/bugs/import-no-settings-silent/src/Settings new file mode 100644 index 00000000..224f710b --- /dev/null +++ b/bugs/import-no-settings-silent/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ImportNoSettingsSilent", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `import` of a folder with no `Settings` file fails without saying why", + "project.exportPathIsV2": true, + "project.id": "{6ADA554D-C646-459B-BE03-F459BC2393EB}", + "project.name": "ImportNoSettingsSilent", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/import-no-settings-silent/src/Sources/Startup.twin b/bugs/import-no-settings-silent/src/Sources/Startup.twin new file mode 100644 index 00000000..48b35cb8 --- /dev/null +++ b/bugs/import-no-settings-silent/src/Sources/Startup.twin @@ -0,0 +1,7 @@ +' An ordinary project. Nothing to run: import of its Sources folder alone, which has no Settings file, fails without a reason. +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/import-package-unticked/DocProbePkg.twinpack b/bugs/import-package-unticked/DocProbePkg.twinpack new file mode 100644 index 00000000..cf3aa1cb Binary files /dev/null and b/bugs/import-package-unticked/DocProbePkg.twinpack differ diff --git a/bugs/import-package-unticked/import-package-unticked.twinproj b/bugs/import-package-unticked/import-package-unticked.twinproj new file mode 100644 index 00000000..e0039194 Binary files /dev/null and b/bugs/import-package-unticked/import-package-unticked.twinproj differ diff --git a/bugs/import-package-unticked/package/DocProbePkg/Settings b/bugs/import-package-unticked/package/DocProbePkg/Settings new file mode 100644 index 00000000..5811b67c --- /dev/null +++ b/bugs/import-package-unticked/package/DocProbePkg/Settings @@ -0,0 +1,21 @@ +{ + "configuration.inherits": "Defaults", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Package TWINPACK", + "project.description": "A package of one function, DocProbeVersion, for a bug report.", + "project.id": "{7B247500-0000-4000-9000-7B2475000004}", + "project.licence": "MIT", + "project.name": "DocProbePkg", + "project.optionExplicit": true, + "project.packageVisibility": "PUBLIC", + "project.references": [], + "project.versionBuild": 0, + "project.versionMajor": 1, + "project.versionMinor": 0, + "project.versionRevision": 0, + "project.warnings": { + "errors": [], + "ignored": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/import-package-unticked/package/DocProbePkg/Sources/DocProbe.twin b/bugs/import-package-unticked/package/DocProbePkg/Sources/DocProbe.twin new file mode 100644 index 00000000..2f81c56c --- /dev/null +++ b/bugs/import-package-unticked/package/DocProbePkg/Sources/DocProbe.twin @@ -0,0 +1,6 @@ +' The package of the report: one function, DocProbeVersion, which returns 1. +Module DocProbe + Public Function DocProbeVersion() As Long + Return 1 + End Function +End Module diff --git a/bugs/import-package-unticked/repro.json b/bugs/import-package-unticked/repro.json new file mode 100644 index 00000000..2d924c7b --- /dev/null +++ b/bugs/import-package-unticked/repro.json @@ -0,0 +1,5 @@ +{ + "mode": "manual", + "attach": ["DocProbePkg.twinpack"], + "steps": "1. Unzip import-package-unticked.zip (attached to the report; pack writes it), which holds import-package-unticked.twinproj and DocProbePkg.twinpack. Open the project in the twinBASIC IDE.\n2. Open Project Settings > References > Available Packages and press Import from file...; choose DocProbePkg.twinpack.\n3. DocProbePkg appears in the Available Packages list, unticked. The compiler has answered the importPackage request with success: true and body: { packageSymbol: \"DocProbePkg\" }, so the import worked. Expected: the package is ticked, as after the online import. Actual: nothing in the project can use it until it is ticked by hand.\nRe-measured on BETA 995 by `ide-test.bat --only packages` (first test; its probe is test/ide/probes/packages); BETA 983 by hand. The lane has no native file picker, so it answers the dialog's OpenPackage call itself; the click on the button is real." +} diff --git a/bugs/import-package-unticked/src/Settings b/bugs/import-package-unticked/src/Settings new file mode 100644 index 00000000..5b4a5ed0 --- /dev/null +++ b/bugs/import-package-unticked/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ImportPackageUnticked", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: *Import from file...* leaves the imported package unticked", + "project.exportPathIsV2": true, + "project.id": "{3EEF20DA-3B7D-40D9-ABC0-45D5BF65A499}", + "project.name": "ImportPackageUnticked", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/import-package-unticked/src/Sources/Startup.twin b/bugs/import-package-unticked/src/Sources/Startup.twin new file mode 100644 index 00000000..20eabc89 --- /dev/null +++ b/bugs/import-package-unticked/src/Sources/Startup.twin @@ -0,0 +1,5 @@ +' The project the report imports DocProbePkg into. Main stays empty until the package is referenced. +Module Startup + Public Sub Main() + End Sub +End Module diff --git a/bugs/llvm-end-restarts-compiler/llvm-end-restarts-compiler.twinproj b/bugs/llvm-end-restarts-compiler/llvm-end-restarts-compiler.twinproj new file mode 100644 index 00000000..87b83588 Binary files /dev/null and b/bugs/llvm-end-restarts-compiler/llvm-end-restarts-compiler.twinproj differ diff --git a/bugs/llvm-end-restarts-compiler/repro.json b/bugs/llvm-end-restarts-compiler/repro.json new file mode 100644 index 00000000..09d82f27 --- /dev/null +++ b/bugs/llvm-end-restarts-compiler/repro.json @@ -0,0 +1,10 @@ +{ + "mode": "run", + "expect": { + "exit": 5, + "output": [ + "^before End$", + "^restarting from MEMORY" + ] + } +} diff --git a/bugs/llvm-end-restarts-compiler/src/Settings b/bugs/llvm-end-restarts-compiler/src/Settings new file mode 100644 index 00000000..473c6794 --- /dev/null +++ b/bugs/llvm-end-restarts-compiler/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "LlvmEndRestartsCompiler", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: End in an LLVM-compiled procedure restarts the IDE's compiler", + "project.exportPathIsV2": true, + "project.id": "{759C51F2-25BD-4183-AC0A-2F2EC6206A70}", + "project.name": "LlvmEndRestartsCompiler", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/llvm-end-restarts-compiler/src/Sources/Startup.twin b/bugs/llvm-end-restarts-compiler/src/Sources/Startup.twin new file mode 100644 index 00000000..37eddc81 --- /dev/null +++ b/bugs/llvm-end-restarts-compiler/src/Sources/Startup.twin @@ -0,0 +1,15 @@ +' End in an LLVM-compiled procedure, run in the IDE, restarts the IDE's compiler. +Module Startup + + [CompilerOptions("+llvm")] + Private Sub Stopper() + Debug.Print "before End" + End + End Sub + + Public Sub Main() + Stopper + Debug.Print "after End: not reached" + End Sub + +End Module diff --git a/bugs/path-limit-260/path-limit-260.twinproj b/bugs/path-limit-260/path-limit-260.twinproj new file mode 100644 index 00000000..83bd358b Binary files /dev/null and b/bugs/path-limit-260/path-limit-260.twinproj differ diff --git a/bugs/path-limit-260/repro.json b/bugs/path-limit-260/repro.json new file mode 100644 index 00000000..0593b2de --- /dev/null +++ b/bugs/path-limit-260/repro.json @@ -0,0 +1,15 @@ +{ + "mode": "cli", + "cli": [ + "import", + "{tmp}\\ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.twinproj", + "{src}\\" + ], + "expect": { + "exit": 0, + "output": [ + "ERROR: failed to create output file: .*(check permissions and storage space)", + "^... FAILED" + ] + } +} diff --git a/bugs/path-limit-260/src/Settings b/bugs/path-limit-260/src/Settings new file mode 100644 index 00000000..3f6c8acf --- /dev/null +++ b/bugs/path-limit-260/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "PathLimit260", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: `export` and `import` stop at the 260-character path limit, apart from the one path they prefix with `\\?\\`", + "project.exportPathIsV2": true, + "project.id": "{F550D4C1-8D8E-4835-A0BA-66BA8747A27A}", + "project.name": "PathLimit260", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/path-limit-260/src/Sources/Startup.twin b/bugs/path-limit-260/src/Sources/Startup.twin new file mode 100644 index 00000000..b804d325 --- /dev/null +++ b/bugs/path-limit-260/src/Sources/Startup.twin @@ -0,0 +1,7 @@ +' An ordinary project. Nothing to run: export and import of it fail when a path they write is long. +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/preservesig-implements/preservesig-implements.twinproj b/bugs/preservesig-implements/preservesig-implements.twinproj new file mode 100644 index 00000000..c5ecfa47 Binary files /dev/null and b/bugs/preservesig-implements/preservesig-implements.twinproj differ diff --git a/bugs/preservesig-implements/repro.json b/bugs/preservesig-implements/repro.json new file mode 100644 index 00000000..8a3de7f3 --- /dev/null +++ b/bugs/preservesig-implements/repro.json @@ -0,0 +1,6 @@ +{ + "mode": "compile", + "expect": { + "diagnostics": ["TB5004", "TB65535"] + } +} diff --git a/bugs/preservesig-implements/src/Settings b/bugs/preservesig-implements/src/Settings new file mode 100644 index 00000000..5982596b --- /dev/null +++ b/bugs/preservesig-implements/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "PreservesigImplements", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: An interface member marked `[PreserveSig]` cannot be implemented by a class", + "project.exportPathIsV2": true, + "project.id": "{C2FB8648-9156-4192-8DA3-60ED79F7CED1}", + "project.name": "PreservesigImplements", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/preservesig-implements/src/Sources/Probe.twin b/bugs/preservesig-implements/src/Sources/Probe.twin new file mode 100644 index 00000000..4c345386 --- /dev/null +++ b/bugs/preservesig-implements/src/Sources/Probe.twin @@ -0,0 +1,12 @@ +' An interface member marked [PreserveSig] cannot be implemented: the "expected" signature is the one already written. +[InterfaceId("11111111-0000-4000-8000-000000000004")] +Interface IProbeD Extends IUnknown + [PreserveSig] + Function F() As Long +End Interface + +Class ImplD + Implements IProbeD + Private Function IProbeD_F() As Long Implements IProbeD.F + End Function +End Class diff --git a/bugs/preservesig-implements/src/Sources/Startup.twin b/bugs/preservesig-implements/src/Sources/Startup.twin new file mode 100644 index 00000000..20233b3a --- /dev/null +++ b/bugs/preservesig-implements/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/recent-projects-copies/recent-projects-copies.twinproj b/bugs/recent-projects-copies/recent-projects-copies.twinproj new file mode 100644 index 00000000..27655477 Binary files /dev/null and b/bugs/recent-projects-copies/recent-projects-copies.twinproj differ diff --git a/bugs/recent-projects-copies/repro.json b/bugs/recent-projects-copies/repro.json new file mode 100644 index 00000000..8859c042 --- /dev/null +++ b/bugs/recent-projects-copies/repro.json @@ -0,0 +1,4 @@ +{ + "mode": "manual", + "steps": "Not automatable: it changes HKCU\\Software\\VB and VBA Program Settings\\twinBASIC_IDE\\RecentlyOpened, the person's own list. Export that key first with reg export, and import it back afterwards. 1. Copy recent-projects-copies.twinproj to A.twinproj, B.twinproj and C.twinproj. 2. In RecentlyOpened leave \"0\" = A.twinproj and \"1\" = B.twinproj, and delete \"2\" to \"20\" or set them to empty strings. 3. Open C.twinproj (twinBASIC.exe C.twinproj is enough). 4. Read the values: \"0\" is C, \"1\" is A and \"2\" to \"20\" are all B.twinproj, nineteen copies." +} diff --git a/bugs/recent-projects-copies/src/Settings b/bugs/recent-projects-copies/src/Settings new file mode 100644 index 00000000..5967fa46 --- /dev/null +++ b/bugs/recent-projects-copies/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "RecentProjectsCopies", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: The recent-projects list fills its empty slots with copies of its last entry", + "project.exportPathIsV2": true, + "project.id": "{E0313902-BEA5-4E74-BFB5-0377E7EFF9F0}", + "project.name": "RecentProjectsCopies", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/recent-projects-copies/src/Sources/Startup.twin b/bugs/recent-projects-copies/src/Sources/Startup.twin new file mode 100644 index 00000000..81f81918 --- /dev/null +++ b/bugs/recent-projects-copies/src/Sources/Startup.twin @@ -0,0 +1,7 @@ +' An ordinary project. Nothing to run: copy it to A, B and C .twinproj and open them in turn to fill the recent-projects list. +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/replace-embedded-package-one-apply/DocProbePkg-v1.twinpack b/bugs/replace-embedded-package-one-apply/DocProbePkg-v1.twinpack new file mode 100644 index 00000000..cf3aa1cb Binary files /dev/null and b/bugs/replace-embedded-package-one-apply/DocProbePkg-v1.twinpack differ diff --git a/bugs/replace-embedded-package-one-apply/DocProbePkg-v2.twinpack b/bugs/replace-embedded-package-one-apply/DocProbePkg-v2.twinpack new file mode 100644 index 00000000..c81c3684 Binary files /dev/null and b/bugs/replace-embedded-package-one-apply/DocProbePkg-v2.twinpack differ diff --git a/bugs/replace-embedded-package-one-apply/package-v1/DocProbePkg/Settings b/bugs/replace-embedded-package-one-apply/package-v1/DocProbePkg/Settings new file mode 100644 index 00000000..5811b67c --- /dev/null +++ b/bugs/replace-embedded-package-one-apply/package-v1/DocProbePkg/Settings @@ -0,0 +1,21 @@ +{ + "configuration.inherits": "Defaults", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Package TWINPACK", + "project.description": "A package of one function, DocProbeVersion, for a bug report.", + "project.id": "{7B247500-0000-4000-9000-7B2475000004}", + "project.licence": "MIT", + "project.name": "DocProbePkg", + "project.optionExplicit": true, + "project.packageVisibility": "PUBLIC", + "project.references": [], + "project.versionBuild": 0, + "project.versionMajor": 1, + "project.versionMinor": 0, + "project.versionRevision": 0, + "project.warnings": { + "errors": [], + "ignored": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/replace-embedded-package-one-apply/package-v1/DocProbePkg/Sources/DocProbe.twin b/bugs/replace-embedded-package-one-apply/package-v1/DocProbePkg/Sources/DocProbe.twin new file mode 100644 index 00000000..2f81c56c --- /dev/null +++ b/bugs/replace-embedded-package-one-apply/package-v1/DocProbePkg/Sources/DocProbe.twin @@ -0,0 +1,6 @@ +' The package of the report: one function, DocProbeVersion, which returns 1. +Module DocProbe + Public Function DocProbeVersion() As Long + Return 1 + End Function +End Module diff --git a/bugs/replace-embedded-package-one-apply/package-v2/DocProbePkg/Settings b/bugs/replace-embedded-package-one-apply/package-v2/DocProbePkg/Settings new file mode 100644 index 00000000..a0ed5524 --- /dev/null +++ b/bugs/replace-embedded-package-one-apply/package-v2/DocProbePkg/Settings @@ -0,0 +1,21 @@ +{ + "configuration.inherits": "Defaults", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Package TWINPACK", + "project.description": "A package of one function, DocProbeVersion, for a bug report.", + "project.id": "{7B247500-0000-4000-9000-7B2475000004}", + "project.licence": "MIT", + "project.name": "DocProbePkg", + "project.optionExplicit": true, + "project.packageVisibility": "PUBLIC", + "project.references": [], + "project.versionBuild": 0, + "project.versionMajor": 2, + "project.versionMinor": 0, + "project.versionRevision": 0, + "project.warnings": { + "errors": [], + "ignored": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/replace-embedded-package-one-apply/package-v2/DocProbePkg/Sources/DocProbe.twin b/bugs/replace-embedded-package-one-apply/package-v2/DocProbePkg/Sources/DocProbe.twin new file mode 100644 index 00000000..b04cdceb --- /dev/null +++ b/bugs/replace-embedded-package-one-apply/package-v2/DocProbePkg/Sources/DocProbe.twin @@ -0,0 +1,6 @@ +' The package of the report: one function, DocProbeVersion, which returns 2. +Module DocProbe + Public Function DocProbeVersion() As Long + Return 2 + End Function +End Module diff --git a/bugs/replace-embedded-package-one-apply/replace-embedded-package-one-apply.twinproj b/bugs/replace-embedded-package-one-apply/replace-embedded-package-one-apply.twinproj new file mode 100644 index 00000000..d0577a71 Binary files /dev/null and b/bugs/replace-embedded-package-one-apply/replace-embedded-package-one-apply.twinproj differ diff --git a/bugs/replace-embedded-package-one-apply/repro.json b/bugs/replace-embedded-package-one-apply/repro.json new file mode 100644 index 00000000..41bc1133 --- /dev/null +++ b/bugs/replace-embedded-package-one-apply/repro.json @@ -0,0 +1,5 @@ +{ + "mode": "manual", + "attach": ["DocProbePkg-v1.twinpack", "DocProbePkg-v2.twinpack"], + "steps": "Use a machine with no copy of DocProbePkg in %APPDATA%\\twinBASIC\\packages (a linked copy makes the bug go away).\n1. Unzip replace-embedded-package-one-apply.zip (attached; pack writes it), which holds replace-embedded-package-one-apply.twinproj, DocProbePkg-v1.twinpack and DocProbePkg-v2.twinpack, whose function DocProbeVersion returns 1 and 2. Open the project.\n2. Embed v1: Project Settings > References > Available Packages > Import from file... DocProbePkg-v1.twinpack, tick it, Apply. Replace the body of Main with Debug.Print \"DocProbeVersion=\" & DocProbeVersion(), and press F5: the DEBUG CONSOLE shows DocProbeVersion=1.\n3. Replace it with v2 under ONE Apply: in References untick DocProbePkg, then Available Packages > Import from file... DocProbePkg-v2.twinpack, tick it, and press Apply once.\n4. See, in the DEBUG CONSOLE, only [COMPILER] Project settings updated: no compiler restart and no save. Press F5: it prints DocProbeVersion=1. Expected: 2.\nControl: Apply after the untick, and again after the import and tick: each Apply restarts the compiler and saves, and F5 prints DocProbeVersion=2.\nRe-measured on BETA 995 by `ide-test.bat --only packages` (the second and third tests, with the control; probes in test/ide/probes/packages)." +} diff --git a/bugs/replace-embedded-package-one-apply/src/Settings b/bugs/replace-embedded-package-one-apply/src/Settings new file mode 100644 index 00000000..f94bef15 --- /dev/null +++ b/bugs/replace-embedded-package-one-apply/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ReplaceEmbeddedPackageOneApply", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Replacing an embedded package under one Apply keeps running the old copy", + "project.exportPathIsV2": true, + "project.id": "{671668B3-8C7F-4E3E-A726-9616F0E563B5}", + "project.name": "ReplaceEmbeddedPackageOneApply", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/replace-embedded-package-one-apply/src/Sources/Startup.twin b/bugs/replace-embedded-package-one-apply/src/Sources/Startup.twin new file mode 100644 index 00000000..20eabc89 --- /dev/null +++ b/bugs/replace-embedded-package-one-apply/src/Sources/Startup.twin @@ -0,0 +1,5 @@ +' The project the report imports DocProbePkg into. Main stays empty until the package is referenced. +Module Startup + Public Sub Main() + End Sub +End Module diff --git a/bugs/static-ctor-args/repro.json b/bugs/static-ctor-args/repro.json new file mode 100644 index 00000000..ac758e91 --- /dev/null +++ b/bugs/static-ctor-args/repro.json @@ -0,0 +1,6 @@ +{ + "mode": "compile", + "expect": { + "diagnostics": ["TB5074"] + } +} diff --git a/bugs/static-ctor-args/src/Settings b/bugs/static-ctor-args/src/Settings new file mode 100644 index 00000000..f6329086 --- /dev/null +++ b/bugs/static-ctor-args/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "StaticCtorArgs", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: A `Static` declaration cannot initialise with a constructor that takes arguments", + "project.exportPathIsV2": true, + "project.id": "{23D9FD5A-CCDC-4DD9-BBE7-DB23BA777AD6}", + "project.name": "StaticCtorArgs", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/static-ctor-args/src/Sources/Probe.twin b/bugs/static-ctor-args/src/Sources/Probe.twin new file mode 100644 index 00000000..899e1bb4 --- /dev/null +++ b/bugs/static-ctor-args/src/Sources/Probe.twin @@ -0,0 +1,7 @@ +' A Static in a Module procedure, initialised with a constructor that takes an argument, of a class declared later in the project: TB5074 at the New. +Module Probe + Public Sub T() + Static s As Dog = New Dog("Rex") + Debug.Print s.GetName() + End Sub +End Module diff --git a/bugs/static-ctor-args/src/Sources/ProbeDog.twin b/bugs/static-ctor-args/src/Sources/ProbeDog.twin new file mode 100644 index 00000000..71931289 --- /dev/null +++ b/bugs/static-ctor-args/src/Sources/ProbeDog.twin @@ -0,0 +1,10 @@ +' A class whose only constructor takes an argument. It comes later in the project than the Module that uses it (Probe.twin). +Private Class Dog + Private m_Name As String + Public Sub New(ByVal Name As String) + m_Name = Name + End Sub + Public Function GetName() As String + Return m_Name + End Function +End Class diff --git a/bugs/static-ctor-args/src/Sources/Startup.twin b/bugs/static-ctor-args/src/Sources/Startup.twin new file mode 100644 index 00000000..cbea39c1 --- /dev/null +++ b/bugs/static-ctor-args/src/Sources/Startup.twin @@ -0,0 +1,7 @@ +Module Startup + + Public Sub Main() + Probe.T + End Sub + +End Module diff --git a/bugs/static-ctor-args/static-ctor-args.twinproj b/bugs/static-ctor-args/static-ctor-args.twinproj new file mode 100644 index 00000000..1e1d0b53 Binary files /dev/null and b/bugs/static-ctor-args/static-ctor-args.twinproj differ diff --git a/bugs/step-key-leaves-step-pending/repro.json b/bugs/step-key-leaves-step-pending/repro.json new file mode 100644 index 00000000..dcd9c128 --- /dev/null +++ b/bugs/step-key-leaves-step-pending/repro.json @@ -0,0 +1,4 @@ +{ + "mode": "manual", + "steps": "1. Open step-key-leaves-step-pending.twinproj in the twinBASIC IDE and press F5. FillTable raises an error at the line marked FAILS (subscript out of range) and the error panel opens. The DEBUG CONSOLE shows main start and loop 0.\n2. Press F8 on that line. The line runs again, the error recurs and the mark does not move: as expected.\n3. In the error panel choose Ignore (Resume Next).\n4. Expected: the program runs on from the next line. Actual: it stops at the next line, Debug.Print \"after fail \" & i, and has not printed after fail 0.\n5. Press F5. It does not run on from there: it runs to the loop's next error, so the DEBUG CONSOLE now ends with after fail 0 and loop 1, and the error panel is open again. The step F8 asked for was used up by that one stop.\nControl: restart, and choose Ignore (Resume Next) without pressing F8 first: it runs on from the next line, to the next pass's error.\nRe-measured on BETA 995 by `ide-test.bat --only debugger` (the project is test/ide/probes/debugger). The same on BETA 983." +} diff --git a/bugs/step-key-leaves-step-pending/src/Settings b/bugs/step-key-leaves-step-pending/src/Settings new file mode 100644 index 00000000..11d24def --- /dev/null +++ b/bugs/step-key-leaves-step-pending/src/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "StepKeyLeavesStepPending", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: A step key pressed on the line that raised an error leaves a step pending", + "project.exportPathIsV2": true, + "project.id": "{A62C0B30-2F2C-423E-91B8-9506C132C177}", + "project.name": "StepKeyLeavesStepPending", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/step-key-leaves-step-pending/src/Sources/Startup.twin b/bugs/step-key-leaves-step-pending/src/Sources/Startup.twin new file mode 100644 index 00000000..c2a3bac8 --- /dev/null +++ b/bugs/step-key-leaves-step-pending/src/Sources/Startup.twin @@ -0,0 +1,18 @@ +' Run it with F5. FillTable fails on every pass of its loop, at the line marked FAILS; each Debug.Print shows how far it got. +Module Startup + Public Sub Main() + Debug.Print "main start" + FillTable + Debug.Print "main after call" + End Sub + + Private Sub FillTable() + Dim a(3) As Long, i As Long, idx As Long = 5 + For i = 0 To 2 + Debug.Print "loop " & i + a(idx) = i ' FAILS + Debug.Print "after fail " & i + Next + Debug.Print "fill end" + End Sub +End Module diff --git a/bugs/step-key-leaves-step-pending/step-key-leaves-step-pending.twinproj b/bugs/step-key-leaves-step-pending/step-key-leaves-step-pending.twinproj new file mode 100644 index 00000000..c92d3c4d Binary files /dev/null and b/bugs/step-key-leaves-step-pending/step-key-leaves-step-pending.twinproj differ diff --git a/bugs/wv2-headers-foreach-crash/repro.json b/bugs/wv2-headers-foreach-crash/repro.json new file mode 100644 index 00000000..5afabb0f --- /dev/null +++ b/bugs/wv2-headers-foreach-crash/repro.json @@ -0,0 +1,13 @@ +{ + "mode": "run", + "expect": { + "exit": 5, + "output": [ + "^NavigationStarting about:blank$", + "ACCESS_VIOLATION .*WebView2HeadersCollection\\.Next" + ], + "absent": [ + "^after For Each$" + ] + } +} diff --git a/bugs/wv2-headers-foreach-crash/src/Settings b/bugs/wv2-headers-foreach-crash/src/Settings new file mode 100644 index 00000000..85222816 --- /dev/null +++ b/bugs/wv2-headers-foreach-crash/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "Wv2HeadersForeachCrash", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: For Each over WebView2 request or response headers crashes in WebView2HeadersCollection.Next", + "project.exportPathIsV2": true, + "project.id": "{6BD10F28-52EF-443F-AAE0-C288FDBC31CB}", + "project.name": "Wv2HeadersForeachCrash", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{5D19653F-F49E-41F2-ABF8-16E473A89A7C}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] WebView2 Package", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "WebView2Package", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 3 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/bugs/wv2-headers-foreach-crash/src/Sources/Form1.tbform b/bugs/wv2-headers-foreach-crash/src/Sources/Form1.tbform new file mode 100644 index 00000000..5610cc55 --- /dev/null +++ b/bugs/wv2-headers-foreach-crash/src/Sources/Form1.tbform @@ -0,0 +1,146 @@ +[ + { + "AlwaysShowKeyboardCues": false, + "Appearance": "vbAppear3d", + "AutoRedraw": false, + "BackColor": 13223074, + "BorderStyle": "vbSizable", + "Caption": "WebView2 form", + "ClipControls": true, + "ControlBox": true, + "DrawMode": "vbCopyPen", + "DrawStyle": "vbSolid", + "DrawWidth": 1, + "Enabled": true, + "FillColor": 0, + "FillStyle": "vbFSTransparent", + "FontBold": false, + "FontItalic": false, + "FontName": "Segoe UI", + "FontSize": 8, + "FontStrikethru": false, + "FontTransparent": true, + "FontUnderline": false, + "ForeColor": -2147483630, + "FormDesignerId": "{7B247400-0000-4000-9000-7B2474000F01}", + "HasDC": true, + "Height": 480, + "HelpContextID": 0, + "Icon": "", + "KeyPreview": false, + "Left": 0, + "LinkMode": "vbLinkNone", + "LinkTopic": null, + "MDIChild": false, + "MaxButton": true, + "MaxHeight": 0, + "MaxWidth": 0, + "MinButton": true, + "MinHeight": 0, + "MinWidth": 0, + "MouseIcon": "", + "MousePointer": "vbDefault", + "Moveable": true, + "Name": "Form1", + "NegotiateMenus": false, + "OLEDropMode": "vbOLEDropNone", + "Opacity": 100, + "Palette": "", + "PaletteMode": "vbPaletteModeHalftone", + "Picture": "", + "PictureDpiScaling": false, + "RightToLeft": false, + "ScaleHeight": 8560, + "ScaleLeft": 0, + "ScaleMode": "vbTwips", + "ScaleTop": 0, + "ScaleWidth": 11760, + "ShowInTaskbar": true, + "StartUpPosition": "vbStartUpScreen", + "TabFocusAutoSelect": false, + "Tag": null, + "Top": 0, + "TopMost": false, + "TransparencyKey": -1, + "Visible": true, + "WhatsThisButton": false, + "WhatsThisHelp": false, + "Width": 640, + "WindowState": "vbNormal", + "__IDEOptions": { + "alignToGrid": false, + "gridHeight": 10, + "gridWidth": 10, + "lockedControls": [], + "multiColoredGrabbers": true, + "showGrid": true, + "showOutlines": false + }, + "__lastUpdateMarker": 3573907440, + "_children": [ + { + "AdditionalAllowedFrameAncestors": null, + "Anchors": { + "Bottom": true, + "Left": true, + "Right": true, + "Top": true, + "_clsid": "{8524D4B5-72A9-40A9-A189-29E7905C40CA}" + }, + "AreBrowserAcceleratorKeysEnabled": true, + "AreDefaultContextMenusEnabled": true, + "AreDefaultScriptDialogsEnabled": true, + "AreDevToolsEnabled": true, + "AreHostObjectsAllowed": true, + "BackColor": 10534293, + "CausesValidation": true, + "Dock": "vbDockNone", + "DocumentURL": "about:blank", + "DragIcon": "", + "DragMode": "vbManual", + "Enabled": true, + "EnvironmentOptions": { + "AdditionalBrowserArguments": null, + "AllowSingleSignOnUsingOSPrimaryAccount": false, + "BrowserExecutableFolder": null, + "EnableTrackingPrevention": true, + "ExclusiveUserDataFolderAccess": false, + "Language": null, + "TargetCompatibleBrowserVersion": "86.0.616.0", + "UserDataFolder": "", + "_className": "WebView2EnvironmentOptions", + "_clsid": "{185E63C0-E641-47ED-B6A9-8565168A7FCF}" + }, + "Height": 440, + "Index": -1, + "IsBuiltInErrorPageEnabled": true, + "IsGeneralAutoFillEnabled": true, + "IsMuted": false, + "IsPasswordAutoSaveEnabled": false, + "IsPinchZoomEnabled": true, + "IsScriptEnabled": true, + "IsStatusBarEnabled": false, + "IsSwipeNavigationEnabled": true, + "IsWebMessageEnabled": true, + "IsZoomControlEnabled": true, + "Left": 0, + "MouseIcon": "", + "MousePointer": "vbDefault", + "Name": "WebView21", + "TabIndex": 7, + "TabStop": true, + "Tag": null, + "Top": 0, + "UseDeferredEvents": true, + "UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0", + "Visible": true, + "Width": 620, + "ZoomFactor": 1, + "_className": "WebView2", + "_clsid": "{93B7E07E-6C7F-4758-8E51-4B1B17EC0A62}" + } + ], + "_className": "Form", + "_clsid": "{33AD4F38-6699-11CF-B70C-00AA0060D393}" + } +] diff --git a/bugs/wv2-headers-foreach-crash/src/Sources/Form1.twin b/bugs/wv2-headers-foreach-crash/src/Sources/Form1.twin new file mode 100644 index 00000000..b8dbc789 --- /dev/null +++ b/bugs/wv2-headers-foreach-crash/src/Sources/Form1.twin @@ -0,0 +1,32 @@ +' For Each over the request headers that NavigationStarting receives crashes +' with an access violation in WebView2HeadersCollection.Next: For Each passes +' Next a null pCeltFetched, and Next assigns to it without testing it. +' Without the For Each, the form opens about:blank and closes, and Main returns. + +[FormDesignerId("7B247400-0000-4000-9000-7B2474000F01")] +[PredeclaredId] +Class Form1 + + Private Sub WebView21_Ready() Handles WebView21.Ready + Debug.Print "WebView21 ready" + WebView21.Navigate "about:blank" + End Sub + + Private Sub WebView21_NavigationStarting(ByVal Uri As String, ByVal IsUserInitiated As Boolean, _ + ByVal IsRedirected As Boolean, ByVal RequestHeaders As WebView2RequestHeaders, _ + Cancel As Boolean) Handles WebView21.NavigationStarting + Debug.Print "NavigationStarting " & Uri + Dim h As WebView2Header + For Each h In RequestHeaders + Debug.Print h.Name & ": " & h.Value + Next + Debug.Print "after For Each" + End Sub + + Private Sub WebView21_NavigationComplete(ByVal IsSuccess As Boolean, ByVal WebErrorStatus As Long) _ + Handles WebView21.NavigationComplete + Debug.Print "NavigationComplete " & IsSuccess + Me.Close + End Sub + +End Class diff --git a/bugs/wv2-headers-foreach-crash/src/Sources/Startup.twin b/bugs/wv2-headers-foreach-crash/src/Sources/Startup.twin new file mode 100644 index 00000000..9d70f179 --- /dev/null +++ b/bugs/wv2-headers-foreach-crash/src/Sources/Startup.twin @@ -0,0 +1,8 @@ +Module Startup + + Public Sub Main() + Form1.Show vbModal + Debug.Print "Form1 closed" + End Sub + +End Module diff --git a/bugs/wv2-headers-foreach-crash/wv2-headers-foreach-crash.twinproj b/bugs/wv2-headers-foreach-crash/wv2-headers-foreach-crash.twinproj new file mode 100644 index 00000000..c0506abb Binary files /dev/null and b/bugs/wv2-headers-foreach-crash/wv2-headers-foreach-crash.twinproj differ diff --git a/builder/page-baseline.json b/builder/page-baseline.json index 243f2cfd..dc5ba9e9 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 914, + "pages": 917, "staticFiles": 250 } diff --git a/docs/Documentation/Authoring.md b/docs/Documentation/Authoring.md index e2d777a6..283595d0 100644 --- a/docs/Documentation/Authoring.md +++ b/docs/Documentation/Authoring.md @@ -547,7 +547,7 @@ generated `Sub`. Three keys override it when it guesses wrong, and one flag asks | In the fence | Means | |---|---| | `check_build` | Compile this sample. | -| `check_run` | Compile it and run it, capturing what it prints. *Not implemented yet --- such a fence is compiled only, and the run says so.* | +| `check_run` | Compile it, build it and run it, then compare what it prints with what the page says it prints, as described below. Only a statement sample (`slot=sub`) can be run. A sample that raises an error, does not return, or prints something else is a finding. A sample that calls `MsgBox` or `InputBox`, or contains an `End` statement, is refused without being built. | | `hidden` | Context for the page's other samples that the reader never sees. Implies `check_build`. See below. | | `slot=file` / `slot=module` / `slot=sub` / `slot=class` / `slot=method` | What to generate around it, when the inference is wrong. `class` and `method` are the same two shapes inside a `Class` rather than a `Module`, for code-behind. The report always names the slot it used, so a wrong guess reads as a wrong guess. | | `inherits=` | The sample is code-behind *of* something --- `inherits=Form`, `inherits=MDIForm`. The wrapper becomes a `Class` that inherits it, so `Me.Caption` resolves against the real type. | @@ -558,6 +558,37 @@ generated `Sub`. Three keys override it when it guesses wrong, and one flag asks | `resource=` | **On a fence in any language**, not just ` ```tb `. The fence's contents are written into the generated project at that project-relative path, so the page's samples can be compiled against it. For the compile-time attributes that read a project file --- see below. | | `inert=` | This fence is **not a program**, and saying so settles it: it is never compiled, never proposed, and counted under its reason instead of sitting in the backlog. See below. | +A `check_run` sample states what it prints in one of two ways, and the checker reads +the sample's own text, so the page needs nothing extra. A trailing comment on a +`Debug.Print` line gives that line's output. A comment line that is exactly `' Output:` +gives the whole output, one line per comment line below it. A sample that states +nothing is still run, and must return without raising an error. An error the sample +handles itself is not raised, so a sample can show an error with `On Error Resume Next` +and print `Err.Number`. When a sample states +anything, it must print exactly as many lines as it states. Each line is compared with +the spaces at both ends removed, because twinBASIC prints a space before a positive +number. + +````markdown +```tb check_run +Debug.Print InStr(1, "abc", "b") ' 2 +Debug.Print UCase("tb") ' TB +``` +```` + +````markdown +```tb check_run +Dim i As Long +For i = 1 To 3 + Debug.Print i +Next +' Output: +' 1 +' 2 +' 3 +``` +```` + ### Saying that a sample is not a program Plenty of good fences are not programs. A syntax skeleton written with placeholder names @@ -875,6 +906,8 @@ Beyond word choice: prefer the active voice and the present tense (`returns`, no **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 get one fact out of it. Write *double-clicking it is obvious, and wrong* --- not *double-clicking it is the obvious shortcut, and it is the one that misleads*. 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 the one thing that...*, *which is precisely the...* and *therein lies the...*. +**State what twinBASIC does; do not report having measured, probed or observed it.** The documentation is the authority on what twinBASIC does and speaks for the project, so it states behaviour as fact rather than as the result of an experiment. The same goes for *probed*, *observed*, *noticed*, *turns out* and *appears to*. Write *the comma form also turns LLVM on* --- not *measured on BETA 995, the comma form also turns LLVM on*. Where the build matters, say *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. + ## Attribution Some reference pages are adapted from Microsoft's VBA-Docs, which is licensed CC-BY-4.0. Those pages set `vba_attribution: true` in their frontmatter, which renders an extra attribution line in the site footer. diff --git a/docs/Documentation/Building.md b/docs/Documentation/Building.md index d267f05c..71f10123 100644 --- a/docs/Documentation/Building.md +++ b/docs/Documentation/Building.md @@ -551,7 +551,7 @@ Four changes move a label back inside its box, roughly in order of preference. T 3. **Widen the box without touching the text.** Raising the x component of the node's `margin` to `0.30` took the same box from 150pt to 176pt. Check what yours starts from rather than assuming: the three shared diagrams under `assets/images/dot/` set `0.12,0.06` and both tutorial diagrams set `0.16,0.09`. `width=` is the other form and is a *minimum*: the box still grows past it when the label needs more room. 4. **Reduce `fontsize`.** This moves both sides of the comparison at once --- Graphviz measures smaller and the browser paints smaller --- so it always works, which is why it is last: a diagram at natural size paints 12pt labels, and 12pt is exactly the site's 16px body size, so shrinking the whole diagram's type makes it read as smaller than the prose around it. Use it per-line instead, as `scheduler-dag.dot` does with `` for its `[M]` and `[W]` annotations. -**Do not use `fixedsize=true`.** It reinterprets `width` and `height` as exact rather than minimum, so the label stops being what sizes the box. Measured, `width=1 fixedsize=true` produced a 72pt box for a label needing about 150pt --- which is not a fix for the overflow, it is the mechanism that manufactures one. No diagram here uses it. +**Do not use `fixedsize=true`.** It reinterprets `width` and `height` as exact rather than minimum, so the label stops being what sizes the box. `width=1 fixedsize=true` produces a 72pt box for a label needing about 150pt --- which is not a fix for the overflow, it is the mechanism that manufactures one. No diagram here uses it. Whichever of them you choose, the edit goes in the `.dot`, then `build.bat` regenerates the `.svg`, and both files are committed. The prohibitions above still hold: the `.svg` is a build artifact, and a `font-family` set anywhere but the `.dot` is a face the layout never measured. diff --git a/docs/Documentation/Extending.md b/docs/Documentation/Extending.md index 253d3d66..cc757073 100644 --- a/docs/Documentation/Extending.md +++ b/docs/Documentation/Extending.md @@ -669,7 +669,7 @@ The split exists so that an edit confined to `docs/` usually has to pay for `che Separating 1 from 2 is what stops a broken gate reading as a clean site, and it has to hold at the top level too. Node's own exit for an uncaught exception is 1, which reads as a finding. So a new tool calls `exitOnCrash()` from `lib/cli.mjs` before it does anything else; it makes an uncaught exception, or a rejection nothing awaits, print the error and exit 2, and it also catches a rejected top-level await. It installs when called, never on import, so a module that can also be imported calls it only when it is the entry point. `exitOnCrash(cleanup)` runs `cleanup(err)` first for a tool that holds something the exit would lose. A script with a `main()` can instead end with `main().catch((err) => { console.error(err); process.exit(2); })`, as `check_a11y.mjs` does; either way a crash must not leave Node's own exit 1. -The rule is not limited to gates. Every tool under `builder/`, `scripts/`, `book/`, `eval/` and `wisdom/` reports a finding as 1 where it has findings, and everything that stops it doing its job as 2. A tool with more outcomes to tell apart adds codes above 2 --- `tbbuild` and `tbrun` use 3 and 4, `addin_test` and `wisdom` use 3 --- and `impexp` keeps a table of its own, shared with its Python edition. **Each tool ends its `--help` text with an `Exit codes:` block, one line per code, and that block is the source of truth**: [Tools and Scripts](Tools) states the same codes once in each tool's entry, and a change to one goes into the other. +The rule is not limited to gates. Every tool under `builder/`, `scripts/`, `book/`, `eval/` and `wisdom/` reports a finding as 1 where it has findings, and everything that stops it doing its job as 2. A tool with more outcomes to tell apart adds codes above 2 --- `tbbuild` uses 3 and 4, `tbrun` 3 to 6, `addin_test` and `wisdom` use 3 --- and `impexp` keeps a table of its own, shared with its Python edition. **Each tool ends its `--help` text with an `Exit codes:` block, one line per code, and that block is the source of truth**: [Tools and Scripts](Tools) states the same codes once in each tool's entry, and a change to one goes into the other. **Say what a pass covered.** Nearly every gate in both wrappers does: `check_dot_fit.mjs` gives the diagram count, `pick_a11y_sample.mjs --check` the sample size and the number of construct families in use, `check_publish_policy.mjs` the probe counts on both sides, `check_code_regions.mjs` the number of files swept, the number whose code regions moved and the number of fences found, `check_a11y.mjs` the page × theme × viewport product it audited. A gate silent on success says nothing about whether it examined anything, which is the state a gate that has quietly stopped working also reports. diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md index f5a20789..1b7a35b2 100644 --- a/docs/Documentation/Tools.md +++ b/docs/Documentation/Tools.md @@ -182,11 +182,11 @@ One invocation of [`check_examples.mjs`](#check-examples), with every flag passe node scripts/check_examples.mjs [flags] -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. [Authoring Pages](Authoring#checking-that-a-sample-compiles) is the page for marking a sample and for what a pull request that changes one shows; this entry is about running the tool. +Compiles the documentation's own twinBASIC code samples --- every ` ```tb ` fence marked `check_build` --- and reports the ones the compiler refuses, against the line in the page they came from. A sample also marked `check_run` is run, and what it prints is checked against what the page says it prints. [Authoring Pages](Authoring#checking-that-a-sample-compiles) is the page for marking a sample and for what a pull request that changes one shows; this entry is about running the tool. **It is not one of the gates, and it must not become one.** It is absent from `build.bat`, `check.bat`, `test.bat` and both CI workflows, for three reasons that are not going to change: it needs a twinBASIC install, where `npm install` has to remain sufficient to build the docs; it needs Windows, a private desktop and a CDP-reachable WebView2, none of which exists on the CI box; and an IDE cold start is 8 to 11 seconds against a whole site build's four. It is run by a person, deliberately, which is the same arrangement [`sweep_a11y.mjs`](#sweep-a11y) already has. -Exit codes: those of [`check_examples.mjs`](#check-examples), returned as they are: **0** clean, **1** a sample does not compile, **2** the harness failed. +Exit codes: those of [`check_examples.mjs`](#check-examples), returned as they are: **0** clean, **1** a sample does not compile, or does not run as its page says, **2** the harness failed. ### addin-test.bat {: #addin-testbat } @@ -467,7 +467,7 @@ The cause is one shape, every time: **two parts of the pattern can match the sam **The rewrite that works is to stop describing the structure between the delimiters.** Both regexes are `<(br|hr|...)\b([^>]*)>`, and the attribute handling happens afterwards in ordinary JavaScript, where it is easier to read and cannot backtrack at all. `[^>]*` and the `>` after it share no character, so there is no division to try. **Do not reintroduce a per-attribute sub-pattern in either one**: a per-attribute sub-pattern is what made them exponential. -It gates on **exponential only**. recheck also reports polynomial blowup, and about a fifth of the patterns here are polynomial --- nearly all the ordinary `]*>` shape on bounded input. Failing those would mean fifty findings on day one, and a gate that fails on day one gets switched off. The `degN` a census prints is worth even less than that: measured on one pattern over three runs each, the native backend calls it degree 2 and the pure-JavaScript fallback calls it degree 3. Both agree on exponential-or-not, which is the only thing the gate rests on. +It gates on **exponential only**. recheck also reports polynomial blowup, and about a fifth of the patterns here are polynomial --- nearly all the ordinary `]*>` shape on bounded input. Failing those would mean fifty findings on day one, and a gate that fails on day one gets switched off. The `degN` a census prints is worth even less than that: on one pattern, over three runs each, the native backend calls it degree 2 and the pure-JavaScript fallback calls it degree 3. Both agree on exponential-or-not, which is the only thing the gate rests on. Two sets of probes run inside the normal pass rather than behind `--self-test`, because a green line saying *no exponential regex* is otherwise indistinguishable from a gate that has stopped detecting them. Eight are regexes with known answers in both directions, including the three this repository actually shipped. Fourteen more cover the folding: eight constructions that must resolve to an exact pattern, and six that must be refused with a reason --- a folder that quietly resolves nothing moves every construction into the unresolved list and the run still passes. @@ -588,7 +588,7 @@ Exit codes: **0** every test passed, **1** a test failed. node --test test/example-batches.test.mjs -Runs the probes of [`check_examples.mjs`](#check-examples) under Node's own test runner. They live in `scripts/lib/example-batches.mjs`, beside what they test: how samples are packed into projects, how a batch whose build crashed the compiler is cut down to the samples that crash it, the canary every batch carries, and the fence classifier. `check_examples.mjs` runs them before every run too, but it needs a twinBASIC install, so it runs only by hand and never in CI. The probes need no IDE: crash isolation is driven through a fake lane whose builds crash on the samples a probe chooses. No browser, no built tree, well under a second. +Runs the probes of [`check_examples.mjs`](#check-examples) under Node's own test runner. They live in `scripts/lib/example-batches.mjs`, beside what they test: how samples are packed into projects, how a batch whose build crashed the compiler is cut down to the samples that crash it, the canary every batch carries, the fence classifier, and how a `check_run` sample is refused, read for what it says it prints, called and judged. `check_examples.mjs` runs them before every run too, but it needs a twinBASIC install, so it runs only by hand and never in CI. The probes need no IDE: crash isolation is driven through a fake lane whose builds crash on the samples a probe chooses. No browser, no built tree, well under a second. Exit codes: **0** every test passed, **1** a test failed. @@ -810,8 +810,8 @@ Exit codes: **0** the trees match; **1** the trees differ; **2** a refused comma {: #tbbuild } node scripts/tbbuild.mjs [--ide ] [--port N] - [--arch win32|win64] [--timeout S] [--json] [--keep] - [--show|--hide] + [--arch win32|win64] [--timeout S] [--build | --llvm] + [--json] [--keep] [--show|--hide] Compiles a `.twinproj` and prints its diagnostics, with no IDE window to click through. This is how a claim the documentation makes about the language gets checked against the compiler rather than against memory: write a one-module project that uses the construct in the position you are asking about, run this, and read what comes back. Windows only, and no part of the site build. @@ -820,13 +820,17 @@ twinBASIC has no command-line build. The compiler executable's whole surface is | Flag | Effect | |---|---| | `--ide ` | Path to `twinBASIC.exe`. Default: `$TB_IDE`, else the newest `twinBASIC_IDE_BETA_` folder on `%USERPROFILE%\Desktop`, which is where the IDE's own zip says to unpack it. **No install path is hardcoded anywhere in this tooling** --- an install path contains a username --- so an install kept elsewhere needs one of those two. | -| `--port ` | DevTools port. Default 9333. It also names the WebView2 user-data folder and the private desktop, which is what makes concurrent instances possible. A port another IDE already holds --- another run's, or another session's --- is refused after ten seconds, rather than attached to. | +| `--port ` | DevTools port. Default 9333. It also names the WebView2 user-data folder, the IDE's temp folder (`%TEMP%\tbbuild-tmp-`, its `TEMP` and `TMP`) and the private desktop, which is what makes concurrent instances possible: IDEs building at once in one temp folder fail now and then to write the type library. A port another IDE already holds --- another run's, or another session's --- is refused after ten seconds, rather than attached to. | | `--arch ` | The target to compile for, `win32` or `win64`. Default `win32`. The diagnostics can differ between the two, because `#If Win64` and the size of `LongPtr` change what compiles. The target is set on every run, because the IDE opens a project in whatever target it last used for that project. Switching restarts the compiler, which then compiles the project again, so a switch adds a few seconds. When the target is not `win32`, or the IDE remembered another one for the project, the report starts with a `target:` line. | | `--timeout ` | Give up waiting for the compile to settle. Default 180. | -| `--json` | Emit one JSON object --- the target, counts, diagnostic rows, and the text of any alert the IDE opened, which is dismissed so the compile can go on --- instead of lines of text. | -| `--keep` | Leave the IDE running afterwards. The IDE's registry entries for the project are then left as they are, because the IDE is still writing them. | +| `--build` | After a compile with no errors, build the project, as the toolbar's Build button does, and print `built: ` after the summary line. The project is exported and packed again first (see below), so the given file is never changed. | +| `--llvm` | Build with LLVM: `--build`, with the project's compiler options set to `+llvm`. It is refused, with exit 2, on a Community or Personal licence, which would build with the default compiler and say nothing. Without it, `--build` is the control for an `--llvm` run. | +| `--json` | Emit one JSON object --- the target, counts, diagnostic rows, the text of any alert the IDE opened, which is dismissed so the compile can go on, and `built` (the file a build wrote, or null) and `buildLog` --- instead of lines of text. | +| `--keep` | Leave the IDE running afterwards, and print its pid as `ide-pid: `, followed by the `taskkill` command that ends it and every process it started, as cmd and PowerShell spell it and as Git Bash does (`//PID`). The IDE's registry entries for the project are then left as they are, because the IDE is still writing them. | | `--show` / `--hide` | Put the IDE on your own desktop where you can watch it, or on a private one where it cannot take focus. Hidden is the default unless `TBBUILD_SHOW` is set to something other than `0`, `false` or `no`; the two flags override that for one invocation. | +**A compile does not generate code, and a build does.** The compiler's front end accepts a construct that its LLVM code generation refuses ("a feature used in your code is not yet supported with the LLVM compiler"), so a clean compile says nothing about an LLVM build. `--build` and `--llvm` press Build after the compile and read the build log. The default build path of a packed project opens a *Save* dialog that a private desktop hides, so either flag first exports the project into `%TEMP%\tbbuild\\src`, packs a copy with an explicit build path under `%TEMP%\tbbuild\\out` and a project id of its own, and opens that copy. A project with compile errors is not built: the report is the compile's, with exit 1. A build that fails, or that ends the compiler with a native exception, prints the build log on stdout and the failing line on stderr, and exits 5. A `[RunAfterBuild]` procedure runs during the build, and the log it would erase with `Debug.Cls` is kept and read all the same. + **It runs the IDE on a private Windows desktop, and that is not decoration.** The IDE calls `HostForceFocus()` from its own `window.onload`, so it takes the keyboard whatever window style it starts with --- `start /min` was tried and the window still came to the front. A process on another desktop has no foreground to take, and the compile does not care whether anything is on screen. Hidden by default has one real cost. A wedged IDE on a private desktop is invisible to the person debugging it, and the only way to see anything is to run it again visible. Export `TBBUILD_SHOW=1` for a session you are working through interactively, and leave it unset for unattended runs. **One IDE handles one project.** Loading a second project into a running IDE wedges it, so a fresh IDE per project is the design rather than a convenience. It costs roughly 8 to 11 seconds each on a development box and is flat in project size, because what is being paid for is IDE startup and not compilation. Concurrency is the way to make a batch of probes fast: distinct `--port` values give distinct DevTools ports, user-data folders and desktops, so instances do not collide. Keep a question that might crash the compiler in a project of its own, so the answer is attributable and one bad probe cannot cost the rest of the batch its run. @@ -835,9 +839,9 @@ twinBASIC has no command-line build. The compiler executable's whole surface is **It leaves the IDE's own settings as it found them.** Every IDE it starts writes to the same registry keys as your own IDE: a saved state for the project (open tabs, watch expressions, Debug Console history), a place at the top of the recent-projects list, and, when the run switches the target, the target the IDE remembers for the project. Once the IDE has exited, `tbbuild` puts all three back. An entry the run created is deleted, and a project that already had one --- one of your own --- gets its old state, its old place in the list and its old target back. The `.twinproj` file association is restored too, if the IDE changed it. When [`check_examples.mjs`](#check-examples) or [`sweep_attributes.mjs`](#sweep-attributes) builds many projects, it does this once for all its lanes instead. -Six files under `scripts/lib/` belong to it and are never run directly. `tb-build.mjs` is `tbbuild` without its command line: `compileProject` opens a project in the IDE and returns its diagnostics as an array, which is how `check_examples.mjs` and `sweep_attributes.mjs` build many projects without starting a process for each. It never exits the process and never tidies the registry, so its caller owns both. `tb-ide.mjs` holds the mechanics `tb-build.mjs` and `tbrun.mjs` share: starting the IDE, attaching to it, waiting for the compile, and reading the diagnostics. `tb-ide-console.mjs` reads the IDE's DEBUG CONSOLE, which is where `tbrun` finds what a probe printed. `tb-registry.mjs` records and restores the registry entries described above, through .NET's registry API by way of PowerShell, because `reg.exe` mangles any path holding a character outside the console code page; [`check_tb_registry.mjs`](#check-tb-registry) is its self-test. `tb-cdp.mjs` is a minimal CDP client over Node's global `WebSocket`, raw rather than puppeteer because a pending `alert()` blocks the renderer and puppeteer's `connect()` handshake talks to the renderer --- so it hangs on precisely the state you need to recover from. Every call it makes has a time limit, so a blocked page ends a run with a message rather than holding it forever. `tb-launch.ps1` holds the Win32 calls Node cannot make without a native FFI addon: `CreateDesktop` and `CreateProcess` with `STARTUPINFO.lpDesktop` for the private desktop, and the job object described above. It is the only PowerShell file under `scripts/`, and it is not executed as a file: `tb-ide.mjs` reads the text and passes it through `-EncodedCommand`, so the execution policy never comes into it and nobody has to be told to bypass one. +Seven files under `scripts/lib/` belong to it and are never run directly. `tb-build.mjs` is `tbbuild` without its command line: `compileProject` opens a project in the IDE and returns its diagnostics as an array, which is how `check_examples.mjs` and `sweep_attributes.mjs` build many projects without starting a process for each. It never exits the process and never tidies the registry, so its caller owns both. `tb-ide.mjs` holds the mechanics `tb-build.mjs` and `tbrun.mjs` share: starting the IDE, attaching to it, waiting for the compile, and reading the diagnostics. `tb-ide-console.mjs` reads the IDE's DEBUG CONSOLE, which is where `tbrun` finds what a probe printed. `tb-run.mjs` presses Build and reads that console until a `[RunAfterBuild]` Sub has finished, and checks what was erased for a failed build; `tbrun` and `check_examples.mjs`'s `check_run` capture a run with it, so they capture it the same way. `tb-registry.mjs` records and restores the registry entries described above, through .NET's registry API by way of PowerShell, because `reg.exe` mangles any path holding a character outside the console code page; [`check_tb_registry.mjs`](#check-tb-registry) is its self-test. `tb-cdp.mjs` is a minimal CDP client over Node's global `WebSocket`, raw rather than puppeteer because a pending `alert()` blocks the renderer and puppeteer's `connect()` handshake talks to the renderer --- so it hangs on precisely the state you need to recover from. Every call it makes has a time limit, so a blocked page ends a run with a message rather than holding it forever. `tb-launch.ps1` holds the Win32 calls Node cannot make without a native FFI addon: `CreateDesktop` and `CreateProcess` with `STARTUPINFO.lpDesktop` for the private desktop, and the job object described above. It is the only PowerShell file under `scripts/`, and it is not executed as a file: `tb-ide.mjs` reads the text and passes it through `-EncodedCommand`, so the execution policy never comes into it and nobody has to be told to bypass one. -Exit codes: **0** the project compiled without errors; **1** the project has errors; **2** a refused command line (a path that is not a `.twinproj` included), no IDE, an IDE that did not start or expose a debug port, or a crash; **3** the compile never settled: the IDE did not report the project open, or its diagnostics did not match its status bar; **4** the project crashes the compiler. +Exit codes: **0** the project compiled without errors; **1** the project has errors; **2** a refused command line (a path that is not a `.twinproj` included), no IDE, an IDE that did not start or expose a debug port, a project that could not be exported or packed, an `--llvm` run on a Community or Personal licence, or a crash; **3** the compile never settled: the IDE did not report the project open, or its diagnostics did not match its status bar; **4** the project crashes the compiler; **5** the build failed after a clean compile. ### tbrun.mjs {: #tbrun } @@ -845,6 +849,7 @@ Exit codes: **0** the project compiled without errors; **1** the project has err node scripts/tbrun.mjs [--port N] [--arch win32|win64] [--timeout S] [--quiet MS] [--json] [--raw] [--keep] [--no-reap] [--reap-images a,b] [--show|--hide] + [--llvm | --compiler-options S] [--exe] Builds a probe project and captures what it writes to the IDE's [Debug Console](../../tB/IDE/Project/DebugConsole). Where [`tbbuild.mjs`](#tbbuild) answers @@ -886,6 +891,20 @@ code generation is a failed run as well. Its error line is written before the pr statement, so the probe's `Debug.Cls` erases it, and the probe stops at the call. `tbrun` keeps what each clear erases, so it names that line and prints the output up to the call. +**A probe that ends before it returns is a failed run, exit 5**, with what it printed +printed all the same. `End` ends a probe that way, and so does an error raised with no +handler in a procedure compiled with LLVM, which ends the run without a report. In the +staged copy, `tbrun` moves the `[RunAfterBuild]` attribute to a Sub it adds to the same +module, which calls the probe's Sub and then prints a line of its own. That line is missing +when the probe did not return, and it is never printed. A probe that leaves a form loaded +has returned too: the IDE prints `[DEBUGGER] Waiting for remaining forms to close...` after +that line, and `tbrun` prints it. The attribute is replaced with +spaces, so the line and column numbers in a diagnostic are still the ones in your file. +`tbrun` warns when it cannot add the wrapper --- the Sub is in a class, takes parameters, or +is one of several marked --- and the check is then off. A probe that stays silent for longer +than `--quiet` while it works also ends the wait without that line, so raise `--quiet` for a +slow one. + **The capture is complete however much a probe prints**, so there is no reason to keep one short. `tbrun` reads the console's backing array rather than the pane, which is a virtualised list view holding only the rows that fit --- reading that instead returns the last ten or so @@ -905,15 +924,36 @@ they are 4, **vbArchWin32** and `x86`. console shows. The IDE does this, not the probe; `Debug.Print "A"; "&"`, in one statement, comes back as `A&`. +**`--llvm` compiles the whole probe with [LLVM](../../LLVM/)**, with no +`[CompilerOptions("+llvm")]` on each procedure. It sets the project's compiler options in the +staged copy: `compiler.debugOptions`, which the `[RunAfterBuild]` run is compiled with, and +`compiler.buildOptions`, which the exe is. `--compiler-options` sets both to any other +string, such as `"+llvm +optimize"`. A run that uses LLVM --- through either option, the +tree's own settings or a procedure's `[CompilerOptions]` --- is refused when the IDE shows a +Community or Personal licence. Neither of those compiles your code with LLVM, so the run would +test the default compiler. + +**`--exe` also runs the exe the build wrote**, after the probe has run in the IDE. It +starts the exe on a private desktop, as it starts the IDE, so a message box the exe opens +appears on no desktop you use. It also ends the exe at `--timeout`. The exe runs its +`Sub Main`, not the `[RunAfterBuild]` Sub, so a probe for both gives the tree a `Main` that +calls the probe, and leaves out the template's own module with an empty `Main`. A built exe +writes nothing with `Debug.Print`, so the probe prints with `TbRun.Out`, from a module `tbrun` +adds to the staged copy. `TbRun.Out` writes to the Debug Console in the IDE, and to a file +`tbrun` reads in the exe. The exe's lines and its exit code follow the probe's output. + | Flag | Effect | |---|---| | `--port ` | DevTools port for the IDE. Default 9346. Distinct ports let probes run concurrently --- the staging directory and the project id are keyed to it, so two runs never share a workspace. A port another IDE holds is refused, as for `tbbuild`. | | `--arch ` | The target to build for, `win32` or `win64`. Default `win32`, set on every run, as for `tbbuild`. A `win64` probe runs as a 64-bit process. | | `--timeout ` | Give up waiting for console output. Default 120. | -| `--quiet ` | How long the console must stop changing before the output counts as complete. Default 2500. There is no sentinel string to match, so any probe works without telling the script anything. Raise it well above the default for a probe that drives an out-of-process server, which can take longer than that to start. | +| `--quiet ` | How long the console must stop changing before the output counts as complete, when the probe has not returned. Default 2500. Raise it well above the default for a probe that drives an out-of-process server, which can take longer than that to start. | +| `--llvm` | Compile the whole probe, and the exe, with LLVM. The same as `--compiler-options +llvm`. | +| `--compiler-options ` | The project's compiler options, for the run and the exe. | +| `--exe` | Also run the built exe, and print what it writes with `TbRun.Out` and its exit code. | | `--raw` | Keep the console's timestamp column, which is otherwise stripped. | -| `--json` | One object with the path of the built file, the target, the captured lines, the IDE pid and anything reaped. | -| `--keep` | Leave the IDE running. Implies `--no-reap`, and leaves the IDE's registry entries for the probe as they are. | +| `--json` | One object with the path of the built file, the target, the captured lines, whether the probe returned, the licence an LLVM run checked, the exe's run, the IDE pid and anything reaped. | +| `--keep` | Leave the IDE running, and print its pid as `ide-pid: ` (with `--json`, `idePid`), followed by the `taskkill` command that ends it and every process it started, as cmd and PowerShell spell it and as Git Bash does (`//PID`). Implies `--no-reap`, and leaves the IDE's registry entries for the probe as they are. | | `--no-reap` | Do not harvest automation servers the probe left behind. | | `--reap-images ` | Replace the harvested image list. Default is the Office suite. | | `--show` / `--hide` | As for [`tbbuild.mjs`](#tbbuild): your own desktop or a private one, with `TBBUILD_SHOW` setting the default. | @@ -947,7 +987,77 @@ behind. That includes the target the IDE remembers for each project, which a `wi writes. **A probe builds for the target `--arch` names**, whatever the IDE remembers, so a kept IDE switched to `win64` does not make later runs on the same port build 64-bit. -Exit codes: **0** the probe ran and its output was captured; **1** the project has compile errors (the diagnostics are printed); **2** a refused command line (a source folder that is missing or has no `Settings` file included), no IDE or compiler, an IDE that did not start, a compile that never settled, a build that failed after a clean compile, a probe that never ran or stopped at a procedure that failed code generation, or a crash; **3** no output: the console held none before the timeout, or the probe printed none after its last `Debug.Cls`; **4** the compiler crashed, or restarted twice, while compiling the project. +Exit codes: **0** the probe ran and its output was captured; **1** the project has compile errors (the diagnostics are printed); **2** a refused command line (a source folder that is missing or has no `Settings` file included), no IDE or compiler, an IDE that did not start, a compile that never settled, a build that failed after a clean compile, a probe that never ran or stopped at a procedure that failed code generation, an LLVM run on a Community or Personal licence, an `--exe` run with no exe built, or a crash; **3** no output: the console held none before the timeout, or the probe printed none after its last `Debug.Cls`; **4** the compiler crashed, or restarted twice, while compiling the project; **5** the probe ended before it returned, its output printed all the same; **6** under `--exe`, the exe exited with a code other than 0, or was still running after `--timeout` and was ended, its output and exit code printed all the same. A run that would exit 5 exits 5 whatever the exe did. + +### bug_repro.mjs +{: #bug-repro } + + node scripts/bug_repro.mjs new "" [--template ] + node scripts/bug_repro.mjs pack + node scripts/bug_repro.mjs compile|build|run [--ide ] [--port N] + [--arch win32|win64] [--timeout S] [--llvm] [--exe] [--keep] [--show|--hide] + node scripts/bug_repro.mjs verify [slug ...] [--ide ] [--port N] [--timeout S] + [--jobs N] [--show|--hide] + node scripts/bug_repro.mjs file [--existing] + node scripts/bug_repro.mjs file --marked + +Keeps the reproducer projects of `BUGS-TO-REPORT.md` --- one folder, `bugs//`, for each +entry, or `bugs/filed//` once the entry has been filed upstream --- and puts them in +front of the compiler. A slug is kebab-case, lowercase letters and digits joined by single +hyphens, and anything else is refused, as is `filed`. A slug that exists in both places is an +error, exit 2. Like [`tbbuild.mjs`](#tbbuild) +and [`tbrun.mjs`](#tbrun), which it runs, it needs a twinBASIC install and Windows. It is +outside every gate and outside CI, and `verify` is run by a person only. + +| Command | Effect | +|---|---| +| `new ""` | Creates `bugs/<slug>/src/` from the console template under `test/example-projects/`, with the project named after the slug in PascalCase, a fresh project id, the description `Reproduces: <title>` and a `Startup` module holding an empty `Sub Main`. Also writes `bugs/<slug>/repro.json` with `"mode": "manual"`. Refused, with exit 3, when `bugs/<slug>` exists. `--template <name>` starts from a folder of `test/repro-templates/` instead: its `Settings`, with the same four keys rewritten, and its `Sources/` as they are. `webview2-form` is a form holding one WebView2 control, which opens `about:blank` when the control is ready and closes when that navigation completes, shown modally by `Sub Main`. | +| `pack <slug>` | Runs `node scripts/impexp.mjs import` on `src/`, to `<slug>.twinproj`, and then writes `<slug>.zip` holding that file and the files `repro.json`'s `attach` names. The zip is written by the script itself, so neither PowerShell nor 7-Zip is needed. An `impexp` exit of 0 or 6 counts as a pack, and its output is printed. | +| `compile <slug>` | Packs, then compiles the project with `tbbuild --json` and prints its diagnostics. | +| `build <slug>` | Packs, then compiles and builds it with `tbbuild --build`, or `--llvm` when that is given. A build that fails prints the build log and the failing line. | +| `run <slug>` | Copies `src/` to `%TEMP%\bugrepro\<port>\<slug>`, adds a `TbRunProbe` module whose `[RunAfterBuild]` Sub calls `Debug.Cls` and then `Main`, runs `tbrun` on the copy and prints what it captured. The Sub clears `WEBVIEW2_USER_DATA_FOLDER` and `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS` while `Main` runs: the harness starts the IDE with both, and WebView2 lets them override what a WebView2 control in the project asks for, so the control would fail to start inside the IDE's process. The tree under `bugs/` is not changed. With `--exe` no probe module is added: `tbrun` runs `Sub Main` in the built exe. | +| `verify [slug ...]` | Reads `repro.json` for each named reproducer, or every one under `bugs/` and `bugs/filed/`, runs what it says and reports one line each. A filed reproducer's line is labelled with its issue, such as `(filed #2453)`, and the summary counts the filed ones on a line of their own. | +| `file <slug> <issue>` | Moves the entry that names `` `<slug>.twinproj` `` out of `BUGS-TO-REPORT.md`, together with one `---` beside it, into `bugs/filed/<slug>/REPORT.md`, whose first line links the issue (`--existing`: "Covered by the existing issue ..." when an existing issue already covered the bug) and which holds no mark line. Then moves `bugs/<slug>/` to `bugs/filed/<slug>/` and records `issue` (and `existing`) in its `repro.json`. An entry whose reproducer is not an attachment names `bugs/<slug>/` in its closing comment instead. Refused, with exit 2 and nothing changed, when no entry or more than one names the slug, `bugs/<slug>` is missing, or `bugs/filed/<slug>` exists. | +| `file --marked` | Does that for every entry with a mark line directly under its title: `*FILED #<n>*`, `*CAPTURED IN EXISTING #<n>*` or `*CAPTURED IN \#<n>*`, the issue and the slug taken from the entry. A line such as `*DEFERRED until after v1*` is not a mark, and the entry is skipped. If a mark cannot be read, or a slug cannot be settled, the entries concerned are printed and nothing at all is filed, exit 2. One line is printed for each entry filed. Takes neither a slug nor an issue. | + +The options of `compile`, `build` and `run` are those of `tbbuild` and `tbrun` of the same +name, and are passed to them: `--ide`, `--port` (default 9440), `--arch`, `--timeout`, +`--keep` and `--show` / `--hide`, with `--llvm` for `build` and `run`, and `--exe` for `run`. +An option that does not apply to a command is refused, not ignored. + +**`repro.json` says how to ask the compiler about a reproducer.** It is committed with the +reproducer and is not part of the `.twinproj`. A key it does not have, or a value of the +wrong type, is refused with exit 2, naming the file and the key, before anything runs. + +| Key | Meaning | +|---|---| +| `mode` | `compile`, `build`, `run`, `cli` or `manual`. `manual` is a reproducer that cannot be automated, such as one that needs a click in the IDE. A `cli` or `manual` reproducer may have no `src/`, when the bug is in files the installation ships; `verify` then runs it without packing, and `{project}` and `{src}` are refused. | +| `arch` | Optional. `win32` (default) or `win64`. | +| `llvm` | Optional, `build` and `run`. `true` builds with LLVM. | +| `exe` | Optional, `run` only. `true` also runs the built exe, as `run --exe` does: no probe module is added, `Sub Main` runs in the exe, and an exe that exits with a code other than 0 is `tbrun`'s exit 6. `Debug.Print` writes nothing in an exe, so what `expect.output` can match is only what `TbRun.Out` wrote; a bug that crashes the exe is expected as `"exit": 6`. | +| `expect.exit` | The exit code of `tbbuild` or `tbrun` as they print it, not this tool's mapped code; for `cli`, the compiler executable's. | +| `expect.diagnostics` | `compile`. Diagnostic codes, such as `TB5182`, that must all be reported. | +| `expect.noDiagnostics` | `compile`. `true` expects no error, warning, hint or information. | +| `expect.message` | `build`. A regular expression the message `tbbuild` prints on standard error must match. | +| `expect.output` | `run` and `cli`. Regular expressions, each of which must match the output. They are matched line by line, so `^` and `$` hold at each line. | +| `expect.absent` | `run` and `cli`. Regular expressions, none of which may match the output, such as an `ERROR` line the bug's fix would print. | +| `cli` | `cli` mode. The arguments for the compiler executable, `bin\twinBASIC_win32.exe`, or a list of such lists, run in turn; their output is joined, and their exit code is the one they all gave, or the codes joined by commas, such as `0,999`. `{tmp}` stands for a new temp folder, deleted afterwards; `{project}` for a copy of the packed `.twinproj` in it, and `{src}` for a copy of `src/`, so a command that writes either never touches the committed reproducer; `{ide}` for the folder of the IDE that `--ide` names or that is found, so a file the installation ships can be named without a user name. Give an output folder with backslashes and a trailing one, as `export` requires. | +| `steps` | `manual`. What a person does to see the bug. `verify` prints it. | +| `attach` | Optional. Files besides the project that the issue needs, such as a `.twinpack`: paths relative to the reproducer's folder, with forward slashes. `pack` adds each to `<slug>.zip`. | +| `issue` | Optional. A positive whole number, the number of the `twinbasic/twinbasic` issue the bug was filed as. `file` writes it. | +| `existing` | Optional, with `issue` only. `true` when the issue was not filed for this bug but already covered it. `file --existing` writes it. | + +A `verify` line is one of four things. **reproduces**: everything `expect` names is as +expected. **NO LONGER REPRODUCES**: it ran, and something expected is not so; the bug may +be fixed in this build, and the entry may be ready to retire; for a filed bug it is the signal +that a fix has been released. **manual**: not automatable, +and `steps` is printed. **harness failed**: the tool could not do its job, as for a +`tbbuild` or `tbrun` exit of 2, or a compile that never settled; that says nothing about +the bug unless `expect.exit` names it. Reproducers run one at a time. `--jobs N` runs N at +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 `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 `verify`, a lane's harness failed; for `file`, an entry that is missing, ambiguous or marked unreadably, or a `bugs/filed/<slug>` already there, with nothing changed; **3** `new` found `bugs/<slug>` or `bugs/filed/<slug>` already there; **4** the compile never settled; **5** the project crashes the compiler; **6** `run`: the probe printed nothing; **7** `run`: the probe ended before it returned; **8** `run --exe`: the exe exited with a code other than 0, or was still running after `--timeout`. ### addin_test.mjs {: #addin-test } @@ -1011,7 +1121,7 @@ and keep running after the run. compiler loads the add-ins there as well as those in the install's own `addins` folders, but it takes that folder from the IDE, which builds its path from the `APPDATA` environment variable. Every IDE a lane starts has an `APPDATA` inside the lane's work folder, and a lane -fails if its IDE's add-in folder turns out to be anywhere else. +fails if its IDE's add-in folder is anywhere else. Exit codes: **0** every lane passed, and the registry is as it was found; **1** a lane failed, or the run was interrupted; **2** the harness could not run: a refused command line, no IDE, no matching lane, a registry it could not record, or a crash after which the registry was put back; **3** the registry or a work folder was not put back (see the lines above), at the end of a run or after a crash, which wins over a 1 because the registry is what to repair. @@ -1068,7 +1178,8 @@ Exit codes: **0** every assertion held, **1** an assertion failed, **2** the tes node scripts/check_examples.mjs [--only <regex>] [--census] [--propose [--apply]] [--report <file>] [--jobs N] [--port N] [--batch N] - [--ide <path>] [--keep] [--verbose] [--json] + [--ide <path>] [--build | --llvm] [--keep] [--verbose] + [--json] Compiles the documentation's own code samples. A ` ```tb ` fence is something [`check_code_regions.mjs`](#check-code-regions) protects the *contents* of and nothing ever @@ -1111,6 +1222,8 @@ reports. | `--port <n>` | Base DevTools port. Default 9480; lane *n* uses base + *n*. | | `--batch <n>` | Upper bound on samples per generated project. Default 120. The batcher packs fewer than this when there are lanes to fill. | | `--ide <path>` | `twinBASIC.exe`. Default: `$TB_IDE`, else the newest `twinBASIC_IDE_BETA_<n>` on the Desktop. | +| `--build` | Also build each project that compiles without errors. A project whose build fails is cut down, as a crash is, to the samples that fail it. A project whose compile has errors is not built: the run names its first error, and ends by counting its samples as "compiled but not built". | +| `--llvm` | Build with LLVM (implies `--build`): each project's compiler options are set to `+llvm`. It needs a Professional or Ultimate licence and exits 2 without one. A plain `--build` run is its control: a sample that fails only under `--llvm` is one LLVM cannot generate code for. | | `--keep` | Leave the generated projects on disk and print where. | | `--verbose` | Report warnings as well as errors. Only errors ever fail the run. | | `--json` | One object on stdout; every report line moves to stderr. | @@ -1142,6 +1255,13 @@ the batch crashes by itself. The samples it needs are then searched for as a set of them are reported. The cost is paid only on failure. The finding names the sample, or the set, and points at `BUGS-TO-REPORT.md`. +**A sample can compile and still fail the build.** The front end accepts constructs that code generation refuses, so `--build` presses Build on each project that compiled without errors, and `--llvm` does the same with LLVM switched on. A build can fail once and pass when repeated, so a project whose build fails is built once more, and only a second failure counts. `tbbuild` reports a failed build as exit 5 and names no sample, so the project is cut by halving, with the crash machinery and its costs, until one sample is left or the samples a failure needs together are found. The finding says "fails the build", and its second line says "the LLVM build" under `--llvm` and "the build" otherwise. The canary described below is a warning, so it does not stop a project from building. A build also refuses three things a compile accepts, so a run that builds batches around them: an `expect-error` sample is batched apart from the samples that should build; a sample, or a group, that declares its own `Sub Main` gets a project of its own without the template's `Main`, because two make the startup object ambiguous; and two samples that export one `[DllExport]` name are kept in different projects. + +**A sample can be run.** A statement sample marked `check_run` is built and run in every mode, and what it prints is compared with what the page says it prints: a trailing comment on a `Debug.Print` line, or the comment lines under an `' Output:` line ([Authoring Pages](Authoring#checking-that-a-sample-compiles) has the markup). A project may have only one `[RunAfterBuild]` Sub (TB5114), so run samples are batched apart from the rest, and each run batch gets one generated `Module tbxRun` whose `[RunAfterBuild]` Sub calls each sample's body in turn. It prints a marker line before and after each call, and an `On Error GoTo` handler around each call keeps one sample's error from ending the rest. The handler is reached only by an error the sample does not handle itself, so a sample can demonstrate an error under its own `On Error Resume Next` and print `Err.Number`. A check of `Err.Number` after the call could not tell the two apart, because a procedure that handles an error with `On Error Resume Next` returns with `Err` still set, in VB6 and in twinBASIC alike. The run's console output is captured as [`tbrun.mjs`](#tbrun) captures it and split by those markers, so each line is charged to the sample that printed it. A sample that raises an error, does not return, or prints something other than the page says is a finding at the page line that states the value. A run sample that is not `slot=sub`, is marked `expect-error`, or calls `MsgBox` or `InputBox` or contains an `End` statement is refused without being built: a message box waits for a click on a desktop nobody sees, and `End` ends every sample after it. + + FAIL docs/Reference/Default/VBA/Strings/InStr.md:77 (Reference/Default/VBA/Strings/InStr.md#3) + prints "3", the page says "7" + **A batch can report nothing when it should report something.** `tbbuild` does not wait for a build: it reads the IDE's own window, the status bar and the Problems panel for the project the IDE has open, once the compiler's status reads OPERATIONAL and has stopped changing. An IDE under load can be OPERATIONAL with an empty panel before it has published its diagnostics, and a batch read then reports every sample as compiling, which looks exactly like a batch with nothing wrong. So every batch carries a canary: a module holding a `#Warning` directive, whose warning (`TB0005`) is known. A read with no errors in it must report the canary, or it is not believed. The module carries `[EnforceWarnings(TB0005)]`, so a project setting that ignores the warning, or turns it into an error, does not change it. The warning is reported whatever else the batch holds: unterminated blocks, stray `End` statements, broken classes and many undefined names in other files do not hide it. It is a warning rather than an error so that a batch with nothing wrong still builds clean. A batch that crashes the compiler reports nothing at all and is isolated as a crash; its canary is never read. The canary proves only that the IDE published something, not that it published everything: a read that includes the canary but not a sample's later diagnostics would still pass that sample. So a read that holds real errors needs no canary --- the IDE was plainly not silent --- and is taken as read, whatever the canary did. Real errors here are errors in the batch's samples, and errors outside every sample that the template does not draw by itself; a template's own errors do not count, or a template that always draws one would switch the canary off for every batch built from it. A canary missing beside real errors has never been seen, and is printed as a note if it happens. A read with no errors and no canary is built once more, because a read that came too early says nothing about the batch. If it is silent again, the batch is split in half repeatedly, as for a crash, until each part reports the canary or errors of its own. A single unit --- one sample, or a group compiled as one program --- that is still silent stops the run with exit code 2 and its name, because its clean result cannot be trusted and it is not blamed for errors nobody saw. The template built with no samples, which is how the tool learns the errors a template draws by itself, follows the same rule: it needs its canary only if it has no errors, is read again when it is silent, and stops the run when it is silent twice. @@ -1164,15 +1284,17 @@ isolates it, after one build of the template with nothing in it decides whether the template's own rather than any sample's. A split never cuts a `projname` group in half, and never separates a page's `hidden` context from the samples that need it. -Three files under `scripts/lib/` belong to it. `tb-fences.mjs` is the half that needs no +Four files under `scripts/lib/` belong to it. `tb-fences.mjs` is the half that needs no compiler --- fence extraction, the markup, and the classifier --- and is where a new key or a new slot goes. `example-batches.mjs` packs samples into batches and cuts a crashed batch down, and holds the probes, which run before every run and in -[`example-batches.test.mjs`](#example-batches-test). `tb-install.mjs` finds the IDE and the +[`example-batches.test.mjs`](#example-batches-test). `example-run.mjs` is what `check_run` +needs without an IDE: which samples may be run, what a sample says it prints, the generated +dispatcher, and the reading of the run's markers. `tb-install.mjs` finds the IDE and the compiler beside it, and is shared with the two IDE-driving tools so the three cannot come to disagree about where an install is. -Exit codes: **0** every marked sample compiles, or none is marked (`--report` always, and `--propose` when it found only unmarked samples that fail, which is advisory); **1** a marked sample does not compile, a marker is misused, a template does not compile, or the compiler crashed on a project (the report names each); **2** the harness could not run: a refused command line, a failed self-test probe, no IDE or compiler, an unreadable `--report` file, a work folder it could not clear, or a crash. +Exit codes: **0** every marked sample compiles, or none is marked (`--report` always, and `--propose` when it found only unmarked samples that fail, which is advisory); **1** a marked sample does not compile, a marker is misused, a template does not compile, the compiler crashed on a project, `--build` or `--llvm` found a sample that fails the build, or a `check_run` sample raised an error, did not return or printed something other than the page says (the report names each); **2** the harness could not run: a refused command line, a failed self-test probe, no IDE or compiler, an unreadable `--report` file, a work folder it could not clear, an `--llvm` run on a Community or Personal licence, or a crash. ### gen_attribute_probes.mjs {: #gen-attribute-probes } diff --git a/docs/Features/Language/Custom-Enumerators.md b/docs/Features/Language/Custom-Enumerators.md new file mode 100644 index 00000000..406b078a --- /dev/null +++ b/docs/Features/Language/Custom-Enumerators.md @@ -0,0 +1,160 @@ +--- +title: Custom Enumerators +parent: Language Syntax +nav_order: 18 +permalink: /Features/Language/Custom-Enumerators +--- + +# Custom Enumerators + +A class becomes usable with [**For Each**](../../tB/Core/For-Each-Next) when one of its members supplies an enumerator: an object that implements the COM interface [**IEnumVARIANT**](../../Reference/COM-Interfaces/IEnumVARIANT). The member is marked with the [**[Enumerator]**](../../tB/Core/Attributes#enumerator) attribute. It can return the enumerator of an object that already has one, such as a [**Collection**](../../tB/Modules/Collection/), or an enumerator the class implements itself. [Writing an enumerator](#writing-an-enumerator) shows a class that produces its own items, with no collection behind it. + +## Returning a Collection's enumerator + +A class that keeps its items in a **Collection** returns the collection's own enumerator, which is its hidden `_NewEnum` member. The square brackets let the name start with an underscore. + +```tb check_build projname=enum-bag slot=file +Class Bag + Private Items As New Collection + + Public Sub Add(ByVal Item As Variant) + Items.Add Item + End Sub + + [Enumerator] + Public Function _NewEnum() As stdole.IUnknown + Return Items.[_NewEnum] + End Function +End Class +``` + +```tb check_run projname=enum-bag +Dim b As New Bag +b.Add "a" +b.Add "b" +b.Add "c" + +Dim x As Variant +For Each x In b + Debug.Print x +Next +' Output: +' a +' b +' c +``` + +Each **For Each** calls the member again and gets an enumerator of its own, so two loops over the same object --- one nested in the other --- do not disturb each other. + +## The enumerator member + +- **The attribute is what counts.** Mark the member with **[Enumerator]**, or with **[DispId(-4)]**, the dispatch identifier COM reserves for it. A member named `_NewEnum` without either attribute is not used: **For Each** raises error 438, *Object doesn't support this property or method*, as it does for a class with no enumerator at all. +- **The name and the visibility are free.** `_NewEnum` is the convention, but a member called `Items` works too, and so does a **Private** or **Friend** one. +- **It can be a Function or a Property Get.** It returns **stdole.IUnknown**, a **Variant** holding the enumerator, or the type of a class that implements **IEnumVARIANT**. + +> [!NOTE] +> twinBASIC also accepts a member that returns the **Collection** itself rather than its `[_NewEnum]`, and **For Each** then uses the collection's enumerator. VB6 does not: **For Each** raises error 451, *Property let procedure not defined and property get procedure did not return an object*. Return `[_NewEnum]` in code that must also run in VB6. + +## Writing an enumerator + +When the items are not in a collection --- a range of numbers, records read on demand --- the class implements **IEnumVARIANT** itself. The interface in **stdole** cannot be implemented from twinBASIC: its counts are unsigned 32-bit integers, which have no twinBASIC type, so the compiler reports `{unsupported VT_UI4}` for them. Declare the interface in the project instead, with the same interface identifier and **Long** counts: + +```tb check_build projname=enum-range slot=file +[InterfaceId("00020404-0000-0000-C000-000000000046")] +Private Interface IEnumVARIANT Extends stdole.IUnknown + Sub Next(ByVal celt As Long, ByRef rgVar As Variant, ByRef pCeltFetched As Long) + Sub Skip(ByVal celt As Long) + Sub Reset() + Function Clone() As IEnumVARIANT +End Interface +``` + +**For Each** uses the enumerator in a fixed way, and an implementation has to allow for it: + +- It calls **Reset** first, then **Next** once per item, always with *celt* = 1. **Skip** and **Clone** are never called, so they can return `E_NOTIMPL`. +- It passes *pCeltFetched* as a null pointer. Assigning to it then fails with an access violation, so test its address with [**VarPtr**](../../tB/Modules/Information/VarPtr) first. +- **Next** ends the loop by returning `S_FALSE` (1) through [**Err.ReturnHResult**](../../tB/Modules/ErrObject/ReturnHResult). Without it the loop never ends: **For Each** cannot see the count of 0, and calls **Next** again for as long as it returns `S_OK`. An item written together with `S_FALSE` is dropped, so the call that writes the last item returns `S_OK`, and the call after it `S_FALSE`. +- An error raised in **Next** ends the loop and reaches the error handler of the procedure running the **For Each**, with its number and description. + +```tb check_build projname=enum-range slot=file +Class RangeEnum + Implements IEnumVARIANT + + Private Const S_FALSE As Long = 1 + Private Const E_NOTIMPL As Long = &H80004001 + Private First As Long, Last As Long, Current As Long + + Public Sub Init(ByVal FromValue As Long, ByVal ToValue As Long) + First = FromValue + Last = ToValue + Current = FromValue + End Sub + + Private Sub Next(ByVal celt As Long, ByRef rgVar As Variant, ByRef pCeltFetched As Long) _ + Implements IEnumVARIANT.Next + If Current > Last Then + If VarPtr(pCeltFetched) <> 0 Then pCeltFetched = 0 + Err.ReturnHResult = S_FALSE + Exit Sub + End If + rgVar = Current + Current += 1 + If VarPtr(pCeltFetched) <> 0 Then pCeltFetched = 1 + End Sub + + Private Sub Skip(ByVal celt As Long) Implements IEnumVARIANT.Skip + Err.ReturnHResult = E_NOTIMPL + End Sub + + Private Sub Reset() Implements IEnumVARIANT.Reset + Current = First + End Sub + + Private Function Clone() As IEnumVARIANT Implements IEnumVARIANT.Clone + Err.ReturnHResult = E_NOTIMPL + End Function +End Class +``` + +```tb check_build projname=enum-range slot=file +Class NumberRange + Private First As Long, Last As Long + + Public Sub Init(ByVal FromValue As Long, ByVal ToValue As Long) + First = FromValue + Last = ToValue + End Sub + + [Enumerator] + Public Function _NewEnum() As stdole.IUnknown + Dim e As New RangeEnum + e.Init First, Last + Return e + End Function +End Class +``` + +```tb check_run projname=enum-range +Dim r As New NumberRange +r.Init 3, 5 + +Dim n As Variant +For Each n In r + Debug.Print n +Next +' Output: +' 3 +' 4 +' 5 +``` + +This **Next** returns one item per call, which is all **For Each** asks for. Native code that calls the enumerator directly may ask for several items at once, and an enumerator meant for such callers must handle a *celt* above 1. [**IEnumVARIANT**](../../Reference/COM-Interfaces/IEnumVARIANT) describes the whole contract, the declaration that allows it, and an enumerator that implements it. + +## See Also + +- [For Each...Next](../../tB/Core/For-Each-Next) statement +- [Enumerator](../../tB/Core/Attributes#enumerator) attribute +- [IEnumVARIANT](../../Reference/COM-Interfaces/IEnumVARIANT) interface +- [Interfaces and CoClasses](Interfaces-CoClasses) -- declaring an interface in twinBASIC +- [Implements](../../tB/Core/Implements) statement +- [Collection](../../tB/Modules/Collection/) class diff --git a/docs/Features/Language/index.md b/docs/Features/Language/index.md index dd4645a5..25552c82 100644 --- a/docs/Features/Language/index.md +++ b/docs/Features/Language/index.md @@ -30,3 +30,4 @@ twinBASIC introduces numerous enhancements to VBx language syntax, including new - [Handler Methods](Handlers) - **Handles** and **Implements** syntax - [Module Organization](Module-Organization) - Code placement flexibility - [Comments](Comments) - New code comment syntax +- [Custom Enumerators](Custom-Enumerators) - **For Each** over a class, with **[Enumerator]** and **IEnumVARIANT** diff --git a/docs/Features/Packages/TWINPACK file format.md b/docs/Features/Packages/TWINPACK file format.md index 47c5682c..6e60723a 100644 --- a/docs/Features/Packages/TWINPACK file format.md +++ b/docs/Features/Packages/TWINPACK file format.md @@ -79,7 +79,7 @@ Follows the common header for file entries: | +var | 4 | uint32 | `revisionCount` | Number of trailing revision entries. | | +var+4 | `revisionCount`×4 | uint32[] | `revisions` | Revision entries. Absent when `revisionCount` is 0. | -The `revisionCount` field is 0 for the vast majority of files, making the file body effectively just the contents followed by 4 zero bytes. Non-zero counts have been observed in packages that embed other packages. +The `revisionCount` field is 0 for the vast majority of files, making the file body effectively just the contents followed by 4 zero bytes. Non-zero counts occur in packages that embed other packages. ## Field details @@ -93,11 +93,11 @@ For files, `revision` is a 64-bit counter that starts at a low value and increme | Heavily edited file | `0x17D5`, `0x1AA0` | | Root and directories | `0x0000` | -Only the low 16 bits have been observed to vary in real-world files; the upper 48 bits are always zero in practice. +In practice only the low 16 bits vary; the upper 48 bits are always zero. ### flags -A 32-bit bitmask describing file-system-level properties of the entry. Every entry observed so far has `flags == 0`, but the IDE recognises the following bits: +A 32-bit bitmask describing file-system-level properties of the entry. In practice every entry has `flags == 0`, but the IDE recognises the following bits: | Bit value | Name | Meaning | |--------------|---------------|---------| diff --git a/docs/LLVM/Getting-Started.md b/docs/LLVM/Getting-Started.md index 9406f241..45c25a4d 100644 --- a/docs/LLVM/Getting-Started.md +++ b/docs/LLVM/Getting-Started.md @@ -55,7 +55,10 @@ Building with LLVM does not currently work on Windows 7; Windows 10 or 11 is rec ### Language support -The main feature not yet supported is passing an error up to the calling procedure. If an error occurs in a procedure that has no error handler, its caller does not receive the error; instead, the program crashes with an unhandled error. A fix is planned. +The main feature not yet supported is passing an error up to the calling procedure. If an error occurs in a procedure that has no error handler, its caller does not receive the error when either of the two procedures is compiled with LLVM: the caller's own `On Error` statement does not catch it. Instead, a built program stops as it does for an unhandled error, and a run in the IDE ends without a message. A fix is planned. + +> [!NOTE] +> In twinBASIC BETA 995, an error handler in a procedure compiled with LLVM cannot read an error raised with [**Err.Raise**](../tB/Modules/ErrObject/Raise) in that procedure: **Err.Number** returns -353703423 (`&HEAEAEA01`), **Err.Source** is empty, and **Err.Description** is the generic "Application-defined or object-defined error". An error that a statement causes, such as a division by zero, is read correctly. Code compiled with LLVM ignores [**Debug.Assert**](../tB/Modules/Debug#assert): the statement does not stop, and its condition is not evaluated. diff --git a/docs/Reference/Attributes.md b/docs/Reference/Attributes.md index 9f02067f..5b7778dc 100644 --- a/docs/Reference/Attributes.md +++ b/docs/Reference/Attributes.md @@ -509,11 +509,11 @@ Private InternalCollection As New Collection [Enumerator] Public Property Get _NewEnum() As Variant - Return InternalCollection + Return InternalCollection.[_NewEnum] End Property ``` -This replaces VB6's hidden `VB_UserMemId = -4` procedure attribute, which twinBASIC still accepts for compatibility. +This replaces VB6's hidden `VB_UserMemId = -4` procedure attribute, which twinBASIC still accepts for compatibility. [Custom Enumerators](../../Features/Language/Custom-Enumerators) describes the member's rules, and how a class can implement the enumerator itself. <!-- Applicability from the package census: ~10 uses in VB, VBRUN, WebView2, WinNativeCommonCtls and WinServicesLib, on a Function or a Property Get. @@ -678,13 +678,13 @@ For an overview of the `Implements ... Via` mechanism itself, see [Implements Vi <!-- Applicability by probe, BETA 983, and by the attribute sweep, BETA 995, which gives the same answers; no package or sample uses this - attribute. The control and the effect below were measured on 983. Accepted on `Implements <Class> Via <field> = <expr>` and on + attribute. The control and the effect below were tested on 983. Accepted on `Implements <Class> Via <field> = <expr>` and on `Implements <Interface> Via <Class>`; rejected on a plain Implements statement (TB5155), on the Class (TB5182), on the Interface (TB5182), on an Inherits statement (TB5155) and on an Interface line in a CoClass (TB5182). The control is [WithDispatchForwarding] on the same Via statement, which draws TB5155 -- so the Via form is not simply accepting - any attribute. Effect measured as an A/B on one source with and without + any attribute. Effect tested as an A/B on one source with and without the attribute: a Friend member of the delegate called from a Module draws TB5027 with it and compiles without it, while the same member called from inside the class compiles either way, qualified with Me or diff --git a/docs/Reference/Built-In/CustomControls/Enumerations/Customtate.md b/docs/Reference/Built-In/CustomControls/Enumerations/Customtate.md index 9bf64f29..27e6deed 100644 --- a/docs/Reference/Built-In/CustomControls/Enumerations/Customtate.md +++ b/docs/Reference/Built-In/CustomControls/Enumerations/Customtate.md @@ -8,7 +8,7 @@ permalink: /tB/Packages/CustomControls/Enumerations/Customtate {: .no_toc } > [!NOTE] -> The name **Customtate** appears to be a typo for "CustomState" preserved from an early draft of the package. The enum is not referenced by any of the eight concrete `Waynes…` controls; the actual minimized / normal / maximized state of a [**WaynesForm**](../WaynesForm/) is controlled by the parallel [**WindowState**](WindowState) enum, which has identical members. Treat **Customtate** as reserved. +> The name **Customtate** is a typo for "CustomState". The enum is not referenced by any of the eight concrete `Waynes…` controls; the actual minimized / normal / maximized state of a [**WaynesForm**](../WaynesForm/) is controlled by the parallel [**WindowState**](WindowState) enum, which has identical members. Treat **Customtate** as reserved. A reserved enumeration with the same three members as [**WindowState**](WindowState). Defined in `Module Constants` of the **CustomControls DESIGNER** library, exported as **Public**, but otherwise unused inside the package. diff --git a/docs/Reference/Built-In/WebView2/WebView2Header.md b/docs/Reference/Built-In/WebView2/WebView2Header.md index 27bf15c9..376af010 100644 --- a/docs/Reference/Built-In/WebView2/WebView2Header.md +++ b/docs/Reference/Built-In/WebView2/WebView2Header.md @@ -10,6 +10,9 @@ has_toc: false A single HTTP header --- a name / value pair. **WebView2Header** is the element type produced by iterating a [**WebView2HeadersCollection**](WebView2HeadersCollection), which in turn comes from a [**WebView2RequestHeaders**](WebView2RequestHeaders) or [**WebView2ResponseHeaders**](WebView2ResponseHeaders) collection. +> [!WARNING] +> In BETA 995, the `For Each` loop below ends the program with an access violation. See [**WebView2HeadersCollection**](WebView2HeadersCollection) for the cause and what to use instead. + ```tb check_build Private Sub WebView21_WebResourceRequested( _ ByVal Request As WebView2Request, _ diff --git a/docs/Reference/Built-In/WebView2/WebView2HeadersCollection.md b/docs/Reference/Built-In/WebView2/WebView2HeadersCollection.md index 861ec2bd..bd6ded93 100644 --- a/docs/Reference/Built-In/WebView2/WebView2HeadersCollection.md +++ b/docs/Reference/Built-In/WebView2/WebView2HeadersCollection.md @@ -12,6 +12,9 @@ An enumerator that yields [**WebView2Header**](WebView2Header) values one by one The collection is forward-only: once iterated it is exhausted. It does not implement **Reset**, **Skip**, or **Clone**, and calling those raises run-time error 80004001 (*Not implemented*). +> [!WARNING] +> In BETA 995, a `For Each` loop over the collection, such as the one below, ends the program with an access violation in the collection's **Next**. `For Each` passes **Next** a null pointer for the count of items it returned, as [**IEnumVARIANT**](../../../Reference/COM-Interfaces/IEnumVARIANT) allows, and **Next** writes to it without checking. Until this is fixed, read the headers needed by name with **GetHeader** or **Contains** on [**WebView2RequestHeaders**](WebView2RequestHeaders) or [**WebView2ResponseHeaders**](WebView2ResponseHeaders). + ```tb check_build Private Sub WebView21_NavigationStarting( _ ByVal Uri As String, _ diff --git a/docs/Reference/Built-In/WebView2/WebView2RequestHeaders.md b/docs/Reference/Built-In/WebView2/WebView2RequestHeaders.md index d031a148..ba61ca0d 100644 --- a/docs/Reference/Built-In/WebView2/WebView2RequestHeaders.md +++ b/docs/Reference/Built-In/WebView2/WebView2RequestHeaders.md @@ -15,6 +15,9 @@ The mutable HTTP-request-header collection for a navigation or a web-resource re The collection is enumerable: a `For Each` loop yields one [**WebView2Header**](WebView2Header) per entry. +> [!WARNING] +> In BETA 995, `For Each` over the collection ends the program with an access violation. See [**WebView2HeadersCollection**](WebView2HeadersCollection) for the cause and what to use instead. + ```tb check_build Private Sub WebView21_NavigationStarting( _ ByVal Uri As String, _ diff --git a/docs/Reference/Built-In/WebView2/WebView2ResponseHeaders.md b/docs/Reference/Built-In/WebView2/WebView2ResponseHeaders.md index 4f279816..1ecd0949 100644 --- a/docs/Reference/Built-In/WebView2/WebView2ResponseHeaders.md +++ b/docs/Reference/Built-In/WebView2/WebView2ResponseHeaders.md @@ -12,6 +12,9 @@ The HTTP-response-header collection for a web-resource response. Reached through The collection is enumerable --- `For Each` yields one [**WebView2Header**](WebView2Header) per entry. Unlike the request side, **AppendHeader** appends additional values rather than overwriting the existing one, matching HTTP's allowance of repeated response headers (e.g. multiple `Set-Cookie`). +> [!WARNING] +> In BETA 995, `For Each` over the collection ends the program with an access violation. See [**WebView2HeadersCollection**](WebView2HeadersCollection) for the cause and what to use instead. + ```tb check_build Private Sub WebView21_WebResourceRequested( _ ByVal Request As WebView2Request, _ diff --git a/docs/Reference/Built-In/WinNamedPipesLib/NamedPipeServer.md b/docs/Reference/Built-In/WinNamedPipesLib/NamedPipeServer.md index 2ebbf3e4..8ac23fbd 100644 --- a/docs/Reference/Built-In/WinNamedPipesLib/NamedPipeServer.md +++ b/docs/Reference/Built-In/WinNamedPipesLib/NamedPipeServer.md @@ -116,30 +116,39 @@ When **True**, each event fires directly on the IOCP worker thread that received #### Example -This example sets **FreeThreadingEvents** to **True** to host the server in a console application without requiring a message loop. +This example sets **FreeThreadingEvents** to **True** to host the server in a console application without requiring a message loop. A **WithEvents** field must be in a class, so the server is held by a class, which the startup module creates. ```tb check_build -Private WithEvents server As NamedPipeServer - -Sub Main() - Set server = New NamedPipeServer - server.PipeName = "MyService" - server.FreeThreadingEvents = True ' events fire on the IOCP worker thread - server.Start - - ' No ManualMessageLoopEnter needed; block here however is appropriate. - Sleep 10000 - server.Stop -End Sub - -Private Sub server_ClientMessageReceived( _ - Connection As NamedPipeServerConnection, _ - ByRef Cookie As Variant, _ - ByRef Data() As Byte) - ' This handler executes on the IOCP worker thread. - ' Shared state accessed here requires external synchronisation. - Connection.AsyncWrite Data -End Sub +Class PipeHost + Private WithEvents server As NamedPipeServer + + Public Sub Run() + Set server = New NamedPipeServer + server.PipeName = "MyService" + server.FreeThreadingEvents = True ' events fire on the IOCP worker thread + server.Start + + ' No ManualMessageLoopEnter needed; block here however is appropriate. + Sleep 10000 + server.Stop + End Sub + + Private Sub server_ClientMessageReceived( _ + Connection As NamedPipeServerConnection, _ + ByRef Cookie As Variant, _ + ByRef Data() As Byte) + ' This handler executes on the IOCP worker thread. + ' Shared state accessed here requires external synchronisation. + Connection.AsyncWrite Data + End Sub +End Class + +Module Startup + Sub Main() + Dim host As New PipeHost + host.Run + End Sub +End Module ``` ### MessageBufferSize diff --git a/docs/Reference/COM-Interfaces/IEnumVARIANT.md b/docs/Reference/COM-Interfaces/IEnumVARIANT.md new file mode 100644 index 00000000..e4abed37 --- /dev/null +++ b/docs/Reference/COM-Interfaces/IEnumVARIANT.md @@ -0,0 +1,268 @@ +--- +title: IEnumVARIANT +parent: COM Interfaces +permalink: /Reference/COM-Interfaces/IEnumVARIANT +--- + +# IEnumVARIANT interface +{: .no_toc } + +Enumerates a sequence of **Variant** values, a fixed number at a time. It is the interface [**For Each**](../../tB/Core/For-Each-Next) uses to go through an object, and the one an object's [**[Enumerator]**](../../tB/Core/Attributes#enumerator) member returns. + +* TOC +{:toc} + +## Declaration + +**IEnumVARIANT** derives from **IUnknown** and has the interface identifier `00020404-0000-0000-C000-000000000046`. Its four methods return an **HRESULT**, which twinBASIC hides as it does for any interface method: a failure code raises a run-time error, and a success code other than `S_OK` is read with [**Err.LastHresult**](../../tB/Modules/ErrObject/LastHresult). + +The declaration in **stdole** cannot be implemented from twinBASIC. Its counts are unsigned 32-bit integers, which have no twinBASIC type, and the compiler reports `{unsupported VT_UI4}` for them. A project declares its own copy instead, with the same interface identifier and **Long** counts. Either of two forms works. + +This form is enough for an enumerator that only **For Each** uses, since **For Each** asks for one item at a time: + +```tb check_build slot=file +[InterfaceId("00020404-0000-0000-C000-000000000046")] +Private Interface IEnumVARIANT Extends stdole.IUnknown + Sub Next(ByVal celt As Long, ByRef rgVar As Variant, ByRef pCeltFetched As Long) + Sub Skip(ByVal celt As Long) + Sub Reset() + Function Clone() As IEnumVARIANT +End Interface +``` + +Here *rgVar* names the first element of the caller's array and nothing after it, and *pCeltFetched* can be tested for a null pointer only through [**VarPtr**](../../tB/Modules/Information/VarPtr). The [Custom Enumerators](../../Features/Language/Custom-Enumerators) page uses this form. + +This form passes both as addresses, so an implementation can fill any number of elements and test *pCeltFetched* directly: + +```tb check_build slot=file +[InterfaceId("00020404-0000-0000-C000-000000000046")] +Private Interface IEnumVARIANT Extends stdole.IUnknown + Sub Next(ByVal celt As Long, ByVal rgVar As LongPtr, ByVal pCeltFetched As LongPtr) + Sub Skip(ByVal celt As Long) + Sub Reset() + Function Clone() As IEnumVARIANT +End Interface +``` + +A class that implements a member marked [**[PreserveSig]**](../../tB/Core/Attributes#preservesig) does not compile (BETA 995), so an implementation cannot return its **HRESULT** as a function result. It sets [**Err.ReturnHResult**](../../tB/Modules/ErrObject/ReturnHResult) instead: `S_FALSE` is 1, `E_NOTIMPL` is `&H80004001`. + +## Methods + +### Next +{: .no_toc } + +Returns the next *celt* items of the sequence, or as many as are left. + +Syntax: *object*.**Next** *celt*, *rgVar*, *pCeltFetched* + +*celt* +: *required* A **Long**: how many items the caller asks for. + +*rgVar* +: *required* The first element of an array of at least *celt* **Variant** values, which receives the items in order. The caller owns what is written there; the enumerator does not free it, and does not read what the elements held before. + +*pCeltFetched* +: *required* A **Long** that receives the number of items written to *rgVar*. The caller may pass a null pointer when it asks for one item, and **For Each** does, so an implementation tests the address before writing to it. + +Returns `S_OK` when it wrote *celt* items, and `S_FALSE` when it wrote fewer because the sequence ended. A call at the end of the sequence writes nothing, sets *pCeltFetched* to 0 and returns `S_FALSE`. A *celt* of 0 writes nothing and returns `S_OK`. + +The position moves past every item written, so the next call starts after the last of them. + +### Skip +{: .no_toc } + +Moves past the next *celt* items without returning them. + +Syntax: *object*.**Skip** *celt* + +*celt* +: *required* A **Long**: how many items to move past. + +Returns `S_OK` when it moved past *celt* items, and `S_FALSE` when the sequence ended first. The position is then at the end. + +### Reset +{: .no_toc } + +Moves back to the start of the sequence. + +Syntax: *object*.**Reset** + +Returns `S_OK`. + +### Clone +{: .no_toc } + +Returns a new enumerator over the same sequence, at the same position. + +Syntax: **Set** *copy* **=** *object*.**Clone** + +The copy and the original then move independently: a **Next** or **Skip** on one does not move the other. A caller uses it to record a position and come back to it later. + +## How For Each uses it + +**For Each** over an object calls the object's enumerator member once, then uses the enumerator it returns in a fixed way, the same in BETA 983 and BETA 995: + +- It calls **Reset** first. A **Reset** that returns `E_NOTIMPL` is ignored, and the loop goes on. +- It calls **Next** once per item, always with *celt* = 1 and a null *pCeltFetched*. +- It ends when **Next** returns `S_FALSE`. It cannot see the count, so a **Next** that returns `S_OK` with nothing written makes the loop go on forever. +- An item that **Next** writes and returns with `S_FALSE` is dropped. When **Next** writes the last item it must return `S_OK`, and `S_FALSE` on the call after it. +- It never calls **Skip** or **Clone**. +- An error raised in **Next** ends the loop and reaches the error handler of the procedure running the **For Each**. + +Each **For Each** gets an enumerator of its own, so two loops over one object, one nested in the other, do not disturb each other. + +## Reading S_FALSE + +The methods are declared as **Sub**, so a caller does not get the **HRESULT** as a value. A success code such as `S_FALSE` raises no error; [**Err.LastHresult**](../../tB/Modules/ErrObject/LastHresult) holds it until the next call. Read it in the statement straight after the method, before calling anything else --- even a procedure of the caller's own project replaces it. + +## Example + +An enumerator over a range of numbers that implements the whole contract, with the second form of the declaration. It writes each item with **VariantInit** and **VariantCopy**, from oleaut32.dll, at the address of its element; a **Variant** is 16 bytes in 32-bit code and 24 bytes in 64-bit code. + +```tb check_build projname=com-ienumvariant slot=file +[InterfaceId("00020404-0000-0000-C000-000000000046")] +Private Interface IEnumVARIANT Extends stdole.IUnknown + Sub Next(ByVal celt As Long, ByVal rgVar As LongPtr, ByVal pCeltFetched As LongPtr) + Sub Skip(ByVal celt As Long) + Sub Reset() + Function Clone() As IEnumVARIANT +End Interface + +Private Module EnumSupport + Public Declare PtrSafe Sub VariantInit Lib "oleaut32" (ByVal pvarg As LongPtr) + Public Declare PtrSafe Function VariantCopy Lib "oleaut32" ( _ + ByVal pvargDest As LongPtr, ByRef pvargSrc As Variant) As Long + Public Declare PtrSafe Sub PutLong Lib "kernel32" Alias "RtlMoveMemory" ( _ + ByVal Destination As LongPtr, ByRef Source As Long, ByVal Length As LongPtr) +#If Win64 Then + Public Const VARIANT_SIZE As Long = 24 +#Else + Public Const VARIANT_SIZE As Long = 16 +#End If + Public Const S_FALSE As Long = 1 +End Module +``` + +```tb check_build projname=com-ienumvariant slot=file +Class RangeEnum + Implements IEnumVARIANT + + Private First As Long, Last As Long, Current As Long + + Public Sub Init(ByVal FromValue As Long, ByVal ToValue As Long, _ + Optional ByVal Position As Long = 0) + First = FromValue + Last = ToValue + Current = IIf(Position = 0, FromValue, Position) + End Sub + + Private Sub Next(ByVal celt As Long, ByVal rgVar As LongPtr, ByVal pCeltFetched As LongPtr) _ + Implements IEnumVARIANT.Next + Dim n As Long + Do While n < celt And Current <= Last + Dim item As Variant = Current + Dim element As LongPtr = rgVar + n * VARIANT_SIZE + VariantInit element + VariantCopy element, item + Current += 1 + n += 1 + Loop + If pCeltFetched <> 0 Then PutLong pCeltFetched, n, 4 + If n < celt Then Err.ReturnHResult = S_FALSE + End Sub + + Private Sub Skip(ByVal celt As Long) Implements IEnumVARIANT.Skip + Dim n As Long + Do While n < celt And Current <= Last + Current += 1 + n += 1 + Loop + If n < celt Then Err.ReturnHResult = S_FALSE + End Sub + + Private Sub Reset() Implements IEnumVARIANT.Reset + Current = First + End Sub + + Private Function Clone() As IEnumVARIANT Implements IEnumVARIANT.Clone + Dim copy As New RangeEnum + copy.Init First, Last, Current + Return copy + End Function +End Class +``` + +```tb check_build projname=com-ienumvariant slot=file +Class NumberRange + Private First As Long, Last As Long + + Public Sub Init(ByVal FromValue As Long, ByVal ToValue As Long) + First = FromValue + Last = ToValue + End Sub + + [Enumerator] + Public Function _NewEnum() As stdole.IUnknown + Dim e As New RangeEnum + e.Init First, Last + Return e + End Function +End Class +``` + +**For Each** over a **NumberRange** asks for one item at a time: + +```tb check_run projname=com-ienumvariant +Dim r As New NumberRange +r.Init 1, 3 + +Dim x As Variant +For Each x In r + Debug.Print x +Next +' Output: +' 1 +' 2 +' 3 +``` + +Called directly, as native code would call it, the enumerator returns several items at once, and the other three methods do what the contract says: + +```tb check_run projname=com-ienumvariant +Dim re As New RangeEnum +re.Init 1, 5 +Dim e As IEnumVARIANT +Set e = re + +Dim items(0 To 2) As Variant, fetched As Long, hr As Long + +' Three items asked for, three written: S_OK (0). +e.Next 3, VarPtr(items(0)), VarPtr(fetched) +hr = Err.LastHresult +Debug.Print fetched & " " & hr & " " & items(0) & items(1) & items(2) ' 3 0 123 + +' Three asked for, two left: S_FALSE (1). +e.Next 3, VarPtr(items(0)), VarPtr(fetched) +hr = Err.LastHresult +Debug.Print fetched & " " & hr & " " & items(0) & items(1) ' 2 1 45 + +' Back to the start, past two items, and a copy of that position. +e.Reset +e.Skip 2 +Dim c As IEnumVARIANT +Set c = e.Clone() +e.Next 1, VarPtr(items(0)), VarPtr(fetched) +Debug.Print items(0) ' 3 +e.Next 1, VarPtr(items(0)), VarPtr(fetched) +Debug.Print items(0) ' 4 +c.Next 1, VarPtr(items(0)), VarPtr(fetched) +Debug.Print items(0) ' 3 +``` + +## See Also + +- [Custom Enumerators](../../Features/Language/Custom-Enumerators) -- making a class usable with **For Each** +- [For Each...Next](../../tB/Core/For-Each-Next) statement +- [Enumerator](../../tB/Core/Attributes#enumerator) attribute +- [Implements](../../tB/Core/Implements) statement +- [Interfaces and CoClasses](../../Features/Language/Interfaces-CoClasses) -- declaring an interface in twinBASIC diff --git a/docs/Reference/COM-Interfaces/index.md b/docs/Reference/COM-Interfaces/index.md new file mode 100644 index 00000000..379eca1e --- /dev/null +++ b/docs/Reference/COM-Interfaces/index.md @@ -0,0 +1,17 @@ +--- +title: COM Interfaces +parent: Reference Section +nav_order: 8 +has_toc: false +permalink: /Reference/COM-Interfaces/ +--- + +# COM Interfaces + +The standard COM interfaces that twinBASIC code implements or calls, described with what each method must do and what twinBASIC adds or requires. + +A twinBASIC class implements a COM interface with [**Implements**](../../tB/Core/Implements), and calls one through a variable of the interface's type. Each page gives the interface's declaration as a twinBASIC project writes it, the contract of each method --- what a caller may pass and what the method must return --- and the behaviour of the language features that use the interface. + +## Interfaces + +- [**IEnumVARIANT**](IEnumVARIANT) -- enumerates a sequence of **Variant** values; what [**For Each**](../../tB/Core/For-Each-Next) uses to go through an object diff --git a/docs/Reference/Core/For-Each-Next.md b/docs/Reference/Core/For-Each-Next.md index 543f29b0..f9122038 100644 --- a/docs/Reference/Core/For-Each-Next.md +++ b/docs/Reference/Core/For-Each-Next.md @@ -48,6 +48,8 @@ The **For…Each** block is entered if there is at least one element in *group*. The **For...Each...Next** statement cannot be used with an array of user-defined types because a **Variant** can't contain a user-defined type. +An object of a class can be the *group* when the class has a member marked [**[Enumerator]**](Attributes#enumerator) that supplies an enumerator. See [Custom Enumerators](../../Features/Language/Custom-Enumerators) for how to write one. + ### Example This example uses the **For Each...Next** statement to search the **Text** property of all elements in a collection for the existence of the string "Hello". In the example, *MyObject* is a text-related object and is an element of the collection *MyCollection*. Both are generic names used for illustration purposes only. diff --git a/docs/Reference/Core/New.md b/docs/Reference/Core/New.md index cd70f728..d20608d5 100644 --- a/docs/Reference/Core/New.md +++ b/docs/Reference/Core/New.md @@ -28,7 +28,7 @@ Syntax: : A class name or other creatable object type. **New** can't be used to create new instances of any intrinsic data type (such as **Long** or **String**), and can't be used to create dependent objects. *arglist* -: *optional* (twinBASIC) The arguments passed to the class's **Sub New** constructor, separated by commas --- `New Dog("Rex")`. The class must declare a **Sub New** whose parameters match; see [Parameterized Class Constructors](../../Features/Advanced/Classes-and-Modules#parameterized-class-constructors). The **As New** form takes no arguments: `Dim d As New Dog("Rex")` is a syntax error. So **As New** needs a class that can be created without arguments: one with a **Sub New** that takes none, or a `Class_Initialize`. For a class whose only **Sub New** takes arguments --- even if every argument is **Optional** --- **As New** fails with TB5121, *can't use this type with As-New syntax as it doesn't have a parameterless constructor*. **Static** does not accept them in an initializer either: `Static d As Dog = New Dog("Rex")` fails with TB5074. Declare the **Static** variable, then assign it with **Set**. +: *optional* (twinBASIC) The arguments passed to the class's **Sub New** constructor, separated by commas --- `New Dog("Rex")`. The class must declare a **Sub New** whose parameters match; see [Parameterized Class Constructors](../../Features/Advanced/Classes-and-Modules#parameterized-class-constructors). The **As New** form takes no arguments: `Dim d As New Dog("Rex")` is a syntax error. So **As New** needs a class that can be created without arguments: one with a **Sub New** that takes none, or a `Class_Initialize`. For a class whose only **Sub New** takes arguments --- even if every argument is **Optional** --- **As New** fails with TB5121, *can't use this type with As-New syntax as it doesn't have a parameterless constructor*. In BETA 995, a **Static** variable in a module procedure can take them in an initializer only when the class is declared earlier in the project: `Static d As Dog = New Dog("Rex")` fails with TB5074 when **Dog** is declared later --- in a file that comes later in the project, or lower in the same file. **Dim** with the same initializer, and a **Static** in a class method, compile in either order. So that the code does not depend on file order, declare the **Static** variable, then assign it with **Set**. When **New** is used in a declaration, no instance is created at the point of declaration. Instead, an instance is created automatically the first time the variable is referenced after declaration. Each time the variable is set to **Nothing** and then referenced again, a new instance is created. diff --git a/docs/Reference/Data-Types.md b/docs/Reference/Data-Types.md index 4c8f3ce0..b1a22266 100644 --- a/docs/Reference/Data-Types.md +++ b/docs/Reference/Data-Types.md @@ -114,7 +114,7 @@ Dim Meeting As Date = #3/17/2026 13:45:30# ' March 17, 2026, 1:45:30 PM The date can be written month first with `/` or `-` between the parts (`#3/17/2026#`, `#3-17-2026#`), year first (`#2026-03-17#`, `#2026/3/17#`), or with an English month name (`#Mar 17, 2026#`, `#17 March 2026#`). The time can be 24-hour (`#13:45#`, `#13:45:30#`) or 12-hour with `AM` or `PM` in either case (`#1:45 PM#`, `#1:45 pm#`). A literal with both puts a space between the date and the time. A time on its own has the date part December 30, 1899. -The compiler reads the parts in the same order whatever the regional format of the machine that compiles the project. These results were measured under English (United States) and under English (United Kingdom), whose short dates put the month and the day in opposite orders: +The compiler reads the parts in the same order whatever the regional format of the machine that compiles the project. These results were checked under English (United States) and under English (United Kingdom), whose short dates put the month and the day in opposite orders: - `#1/2/2026#` is January 2, 2026, under both: the first number is always the month. - When the first number cannot be a month, the first two numbers are swapped: `#13/1/2026#` is January 13, 2026. So a day-first literal compiles without a diagnostic, and is read correctly only when its day is 13 or more. diff --git a/docs/Reference/Default/VB/Form/index.md b/docs/Reference/Default/VB/Form/index.md index 46db03c2..215d9532 100644 --- a/docs/Reference/Default/VB/Form/index.md +++ b/docs/Reference/Default/VB/Form/index.md @@ -34,7 +34,9 @@ End Sub Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer) If MsgBox("Quit?", vbYesNo) = vbNo Then Cancel = 1 End Sub +``` +```tb check_build ' In a startup module: Sub Main() Form1.Show vbModal @@ -236,7 +238,7 @@ Determines whether the form accepts user input. A disabled form ignores keyboard ### FillColor {: .no_toc } -The fill colour for closed shapes drawn by [**Circle**](#circle) and the rectangle form of [**Line**](#line). **OLE_COLOR**, default `0` (black). Used only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. +The fill colour for closed shapes drawn by [**Circle**](#circle) and the rectangle form of [**Line**](#line) without **F**. **OLE_COLOR**, default `0` (black). Used only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. ### FillStyle {: .no_toc } @@ -561,10 +563,10 @@ Syntax: *object*.**Line** [ [ **Step** ] ( *X1*, *Y1* ) ] -[ **Step** ] ( *X2*, : *optional* An **OLE_COLOR** for the line; defaults to [**ForeColor**](#forecolor). **B** -: *optional* Draw a rectangle whose opposite corners are (*X1*, *Y1*) and (*X2*, *Y2*) instead of a line. +: *optional* Draw a rectangle whose opposite corners are (*X1*, *Y1*) and (*X2*, *Y2*) instead of a line. Without **F**, the interior is filled with [**FillColor**](#fillcolor) in the current [**FillStyle**](#fillstyle); **vbFSTransparent** leaves it unfilled. **F** -: *optional* When combined with **B**, fill the rectangle with [**ForeColor**](#forecolor) instead of [**FillColor**](#fillcolor)/[**FillStyle**](#fillstyle). +: *optional* Used with **B**. Fills the rectangle with its outline colour --- *Color*, or [**ForeColor**](#forecolor) when *Color* is omitted --- instead of [**FillColor**](#fillcolor)/[**FillStyle**](#fillstyle). ### Move {: .no_toc } diff --git a/docs/Reference/Default/VB/Global/index.md b/docs/Reference/Default/VB/Global/index.md index bbd425f1..3a99acec 100644 --- a/docs/Reference/Default/VB/Global/index.md +++ b/docs/Reference/Default/VB/Global/index.md @@ -44,7 +44,7 @@ These properties are cached references --- repeated reads return the same object - **Forms.Add(** *Name* **)** --- creates a new instance of the form class named *Name*, adds it to the collection, and returns the new [**Form**](../Form/). The form is loaded but not shown. > [!WARNING] -> In BETA 995, taking a form out of **Forms** by index can crash the program. `Forms(0).Name` returns an empty string, and so does the **Name** of a form taken with `Set f = Forms(0)` when `f` is declared `As Form`; the program then stops with an access violation (`0xC0000005`), in the IDE and as a compiled program alike. Inside a `For` loop, `Set f = Forms(k)` also changes the loop variable. `For Each` over **Forms**, and `Unload Forms(i)`, both work, and so does `Set f = Forms(k)` when `f` is declared `As Object` or as the form's own class: reach a loaded form that way, with `For Each`, or by its class name. +> In BETA 995, a form taken out of **Forms** by index is not reliably the form. `Forms(i)` can return a reference to another object, at an address just past the form's own, whose **Name** is an empty string, and the program can then stop with an access violation (`0xC0000005`). Which code does this depends on the expression around it, not on the index: `Set f = Forms(0)` with `f` declared `As Form`, followed by `f.Name` in a string expression, gives an empty **Name** in the IDE and as a compiled program alike, while `Forms(0).Name` is empty only in the IDE. `For Each` over **Forms** works, and so does `Set f = Forms(k)` when `f` is declared `As Object` or as the form's own class: reach a loaded form that way, with `For Each`, or by its class name. The collection also supports `For Each` enumeration: diff --git a/docs/Reference/Default/VB/MDIForm/index.md b/docs/Reference/Default/VB/MDIForm/index.md index 1a76f546..d9ff6c25 100644 --- a/docs/Reference/Default/VB/MDIForm/index.md +++ b/docs/Reference/Default/VB/MDIForm/index.md @@ -22,12 +22,16 @@ End Sub Private Sub mnuWindowCascade_Click() Arrange vbCascade End Sub +``` +```tb check_build inherits=Form ' In a child form (Form1) marked MDIChild = True: Private Sub Form_Load() Caption = "Untitled" End Sub +``` +```tb check_build ' In a startup module: Sub Main() MDIForm1.Show diff --git a/docs/Reference/Default/VB/PictureBox/index.md b/docs/Reference/Default/VB/PictureBox/index.md index 8257f2fd..8dbdd762 100644 --- a/docs/Reference/Default/VB/PictureBox/index.md +++ b/docs/Reference/Default/VB/PictureBox/index.md @@ -201,7 +201,7 @@ Determines whether the control accepts user input. **Boolean**, default **True** ### FillColor {: .no_toc } -The colour used to fill closed shapes drawn by [**Line**](#line) (with the `F` flag) and [**Circle**](#circle), as an **OLE_COLOR**. Default **0** (black). Honoured only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. +The colour used to fill closed shapes drawn by [**Line**](#line) with **B** and without **F**, and by [**Circle**](#circle), as an **OLE_COLOR**. Default **0** (black). Honoured only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. ### FillStyle {: .no_toc } @@ -504,10 +504,10 @@ Syntax: *object*.**Line** [ [ **Step** ] ( *X1*, *Y1* ) ] **-** [ **Step** ] ( * : *optional* An **OLE_COLOR** for the line. Defaults to [**ForeColor**](#forecolor). *B* -: *optional* When present, draws a rectangle whose opposite corners are *(X1, Y1)* and *(X2, Y2)* instead of a line. +: *optional* When present, draws a rectangle whose opposite corners are *(X1, Y1)* and *(X2, Y2)* instead of a line. Without **F**, the interior is filled with [**FillColor**](#fillcolor) in the current [**FillStyle**](#fillstyle); **vbFSTransparent** leaves it unfilled. *F* -: *optional* Only valid with **B**. Fills the rectangle with [**FillColor**](#fillcolor) at the current [**FillStyle**](#fillstyle). +: *optional* Only valid with **B**. Fills the rectangle with its outline colour --- *Color*, or [**ForeColor**](#forecolor) when *Color* is omitted --- instead of [**FillColor**](#fillcolor) at the current [**FillStyle**](#fillstyle). [**CurrentX**](#currentx) / [**CurrentY**](#currenty) are left at *(X2, Y2)*. diff --git a/docs/Reference/Default/VB/Printer/index.md b/docs/Reference/Default/VB/Printer/index.md index 996eaf30..098a1a3a 100644 --- a/docs/Reference/Default/VB/Printer/index.md +++ b/docs/Reference/Default/VB/Printer/index.md @@ -145,7 +145,7 @@ Syntax: *object*.**Duplex** [ = *value* ] ### FillColor {: .no_toc } -The colour used to fill closed shapes drawn by [**Line**](#line) (with the `F` flag) and [**Circle**](#circle), as an **OLE_COLOR**. Default **0** (black). Honoured only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. +The colour used to fill closed shapes drawn by [**Line**](#line) with **B** and without **F**, and by [**Circle**](#circle), as an **OLE_COLOR**. Default **0** (black). Honoured only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. ### FillStyle {: .no_toc } @@ -396,10 +396,10 @@ Syntax: *object*.**Line** [ [ **Step** ] ( *X1*, *Y1* ) ] **-** [ **Step** ] ( * : *optional* An **OLE_COLOR** for the line. Defaults to [**ForeColor**](#forecolor). *B* -: *optional* Draws a rectangle whose opposite corners are *(X1, Y1)* and *(X2, Y2)* instead of a line. +: *optional* Draws a rectangle whose opposite corners are *(X1, Y1)* and *(X2, Y2)* instead of a line. Without **F**, the interior is filled with [**FillColor**](#fillcolor) in the current [**FillStyle**](#fillstyle); **vbFSTransparent** leaves it unfilled. *F* -: *optional* Only valid with **B**. Fills the rectangle with [**FillColor**](#fillcolor) at the current [**FillStyle**](#fillstyle). +: *optional* Only valid with **B**. Fills the rectangle with its outline colour --- *Color*, or [**ForeColor**](#forecolor) when *Color* is omitted --- instead of [**FillColor**](#fillcolor) at the current [**FillStyle**](#fillstyle). If no job is in progress, **Line** implicitly starts one. diff --git a/docs/Reference/Default/VB/PropertyPage/index.md b/docs/Reference/Default/VB/PropertyPage/index.md index 80914f2e..100a2e84 100644 --- a/docs/Reference/Default/VB/PropertyPage/index.md +++ b/docs/Reference/Default/VB/PropertyPage/index.md @@ -177,7 +177,7 @@ The pen width in pixels for drawing primitives. **Long**, default `1`. Widths gr ### FillColor {: .no_toc } -The fill colour for closed shapes drawn by [**Circle**](#circle) and the rectangle form of [**Line**](#line). **OLE_COLOR**, default `0` (black). Used only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. +The fill colour for closed shapes drawn by [**Circle**](#circle) and the rectangle form of [**Line**](#line) without **F**. **OLE_COLOR**, default `0` (black). Used only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. ### FillStyle {: .no_toc } @@ -429,10 +429,10 @@ Syntax: *object*.**Line** [ [ **Step** ] ( *X1*, *Y1* ) ] -[ **Step** ] ( *X2*, : *optional* An **OLE_COLOR** for the line; defaults to [**ForeColor**](#forecolor). **B** -: *optional* Draw a rectangle whose opposite corners are (*X1*, *Y1*) and (*X2*, *Y2*) instead of a line. +: *optional* Draw a rectangle whose opposite corners are (*X1*, *Y1*) and (*X2*, *Y2*) instead of a line. Without **F**, the interior is filled with [**FillColor**](#fillcolor) in the current [**FillStyle**](#fillstyle); **vbFSTransparent** leaves it unfilled. **F** -: *optional* When combined with **B**, fill the rectangle with [**ForeColor**](#forecolor) instead of [**FillColor**](#fillcolor)/[**FillStyle**](#fillstyle). +: *optional* Used with **B**. Fills the rectangle with its outline colour --- *Color*, or [**ForeColor**](#forecolor) when *Color* is omitted --- instead of [**FillColor**](#fillcolor)/[**FillStyle**](#fillstyle). ### OLEDrag {: .no_toc } diff --git a/docs/Reference/Default/VB/Report/index.md b/docs/Reference/Default/VB/Report/index.md index 2dd2d9a9..1c6c3c96 100644 --- a/docs/Reference/Default/VB/Report/index.md +++ b/docs/Reference/Default/VB/Report/index.md @@ -250,7 +250,7 @@ Determines whether the report window accepts user input. A disabled report ignor ### FillColor {: .no_toc } -The fill colour for closed shapes drawn by [**Circle**](#circle) and the rectangle form of [**Line**](#line). **OLE_COLOR**, default `0` (black). Used only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. +The fill colour for closed shapes drawn by [**Circle**](#circle) and the rectangle form of [**Line**](#line) without **F**. **OLE_COLOR**, default `0` (black). Used only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. ### FillStyle {: .no_toc } @@ -605,10 +605,10 @@ Syntax: *object*.**Line** [ [ **Step** ] ( *X1*, *Y1* ) ] -[ **Step** ] ( *X2*, : *optional* An **OLE_COLOR** for the line; defaults to [**ForeColor**](#forecolor). **B** -: *optional* Draw a rectangle whose opposite corners are (*X1*, *Y1*) and (*X2*, *Y2*). +: *optional* Draw a rectangle whose opposite corners are (*X1*, *Y1*) and (*X2*, *Y2*) instead of a line. Without **F**, the interior is filled with [**FillColor**](#fillcolor) in the current [**FillStyle**](#fillstyle); **vbFSTransparent** leaves it unfilled. **F** -: *optional* When combined with **B**, fill the rectangle with [**ForeColor**](#forecolor) instead of [**FillColor**](#fillcolor)/[**FillStyle**](#fillstyle). +: *optional* Used with **B**. Fills the rectangle with its outline colour --- *Color*, or [**ForeColor**](#forecolor) when *Color* is omitted --- instead of [**FillColor**](#fillcolor)/[**FillStyle**](#fillstyle). ### Move {: .no_toc } diff --git a/docs/Reference/Default/VB/UserControl/index.md b/docs/Reference/Default/VB/UserControl/index.md index e8ad55d4..24a08ad1 100644 --- a/docs/Reference/Default/VB/UserControl/index.md +++ b/docs/Reference/Default/VB/UserControl/index.md @@ -276,7 +276,7 @@ The host's extender wrapper around this **UserControl**, as an **Object**. Read- ### FillColor {: .no_toc } -The fill colour for closed shapes drawn by [**Circle**](#circle) and the rectangle form of [**Line**](#line). **OLE_COLOR**, default `0` (black). Used only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. +The fill colour for closed shapes drawn by [**Circle**](#circle) and the rectangle form of [**Line**](#line) without **F**. **OLE_COLOR**, default `0` (black). Used only when [**FillStyle**](#fillstyle) is not **vbFSTransparent**. ### FillStyle {: .no_toc } @@ -649,10 +649,10 @@ Syntax: *object*.**Line** [ [ **Step** ] ( *X1*, *Y1* ) ] -[ **Step** ] ( *X2*, : *optional* An **OLE_COLOR** for the line; defaults to [**ForeColor**](#forecolor). **B** -: *optional* Draw a rectangle whose opposite corners are (*X1*, *Y1*) and (*X2*, *Y2*) instead of a line. +: *optional* Draw a rectangle whose opposite corners are (*X1*, *Y1*) and (*X2*, *Y2*) instead of a line. Without **F**, the interior is filled with [**FillColor**](#fillcolor) in the current [**FillStyle**](#fillstyle); **vbFSTransparent** leaves it unfilled. **F** -: *optional* When combined with **B**, fill the rectangle with [**ForeColor**](#forecolor) instead of [**FillColor**](#fillcolor)/[**FillStyle**](#fillstyle). +: *optional* Used with **B**. Fills the rectangle with its outline colour --- *Color*, or [**ForeColor**](#forecolor) when *Color* is omitted --- instead of [**FillColor**](#fillcolor)/[**FillStyle**](#fillstyle). ### OLEDrag {: .no_toc } diff --git a/docs/Reference/Default/VBA/Strings/InStr.md b/docs/Reference/Default/VBA/Strings/InStr.md index 7d0e8779..5654466a 100644 --- a/docs/Reference/Default/VBA/Strings/InStr.md +++ b/docs/Reference/Default/VBA/Strings/InStr.md @@ -15,7 +15,7 @@ Syntax: - **InStrB(** [ *start* **,** ] *string1*, *string2* [ **,** *compare* ] **)** *start* -: *optional* Numeric expression that sets the starting position for each search. If omitted, search begins at the first character position. If *start* contains **Null**, an error occurs. The *start* argument is required if *compare* is specified. +: *optional* Numeric expression that sets the starting position for each search. If omitted, search begins at the first character position. *start* must be 1 or greater: 0 or a negative value raises error 5 (Invalid procedure call or argument), and **Null** raises error 94 (Invalid use of Null). The *start* argument is required if *compare* is specified. *string1* : *required* String expression being searched. @@ -34,17 +34,17 @@ The *compare* argument settings are: | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | -**Return values:** +**Return values:** the first row that applies gives the result. | If | **InStr** returns | |-------------------------------------------------|----------------------------------| -| *string1* is zero-length | 0 | | *string1* is **Null** | **Null** | -| *string2* is zero-length | *start* | | *string2* is **Null** | **Null** | +| *string1* is zero-length | 0 | +| *string2* is zero-length | *start*, even when *start* is greater than **Len**(*string1*) | +| *start* > **Len**(*string1*) | 0 | | *string2* is not found | 0 | | *string2* is found within *string1* | Position at which match is found | -| *start* > **Len**(*string2*) | 0 | The **InStrB** function is used with byte data contained in a string. Instead of returning the character position of the first occurrence of one string within another, **InStrB** returns the byte position. @@ -52,21 +52,35 @@ The **InStrB** function is used with byte data contained in a string. Instead of This example uses the **InStr** function to return the position of the first occurrence of one string within another. -```tb check_build -Dim SearchString, SearchChar, MyPos +```tb check_run +Dim SearchString As String, SearchChar As String SearchString = "XXpXXpXXPXXP" ' String to search in. SearchChar = "P" ' Search for "P". -' A textual comparison starting at position 4. Returns 6. -MyPos = InStr(4, SearchString, SearchChar, 1) +' A textual comparison starting at position 4. +Debug.Print InStr(4, SearchString, SearchChar, 1) ' 6 -' A binary comparison starting at position 1. Returns 9. -MyPos = InStr(1, SearchString, SearchChar, 0) +' A binary comparison starting at position 1. +Debug.Print InStr(1, SearchString, SearchChar, 0) ' 9 ' Comparison is binary by default (last argument is omitted). -MyPos = InStr(SearchString, SearchChar) ' Returns 9. +Debug.Print InStr(SearchString, SearchChar) ' 9 + +Debug.Print InStr(1, SearchString, "W") ' 0 +``` + +This example shows the rows of the return-value table that are easy to get wrong. + +```tb check_run +Debug.Print InStr(3, "abcdef", "cd") ' 3 +Debug.Print InStr(4, "abc", "c") ' 0 +Debug.Print InStr(5, "abc", "") ' 5 +Debug.Print InStr(1, "", "") ' 0 +Debug.Print IsNull(InStr(1, Null, "a")) ' True -MyPos = InStr(1, SearchString, "W") ' Returns 0. +On Error Resume Next +Debug.Print InStr(0, "abc", "a") +Debug.Print Err.Number ' 5 ``` ### See Also diff --git a/docs/Reference/Default/VBA/Strings/InStrRev.md b/docs/Reference/Default/VBA/Strings/InStrRev.md index 55930a43..3aee7a6c 100644 --- a/docs/Reference/Default/VBA/Strings/InStrRev.md +++ b/docs/Reference/Default/VBA/Strings/InStrRev.md @@ -18,7 +18,7 @@ Syntax: **InStrRev(** *stringcheck*, *stringmatch* [ **,** *start* [ **,** *comp : *required* String expression being searched for. *start* -: *optional* Numeric expression that sets the starting position for each search. If omitted, -1 is used, which means that the search begins at the last character position. If *start* contains **Null**, an error occurs. +: *optional* Numeric expression that sets the starting position for each search. If omitted, -1 is used, which means that the search begins at the last character position. *start* must be -1, or 1 or greater: 0 or any other negative value raises error 5 (Invalid procedure call or argument). If *start* contains **Null**, an error occurs. *compare* : *optional* Numeric value indicating the kind of comparison to use when evaluating substrings. If omitted, a binary comparison is performed. See settings below. @@ -31,17 +31,17 @@ The *compare* argument can have the following values: | **vbBinaryCompare** | 0 | Performs a binary comparison. | | **vbTextCompare** | 1 | Performs a textual comparison. | -**Return values:** +**Return values:** the first row that applies gives the result. | If | **InStrRev** returns | |-----------------------------------------------------|----------------------------------| | *stringcheck* is zero-length | 0 | -| *stringcheck* is **Null** | **Null** | -| *stringmatch* is zero-length | *start* | -| *stringmatch* is **Null** | **Null** | +| *start* > **Len**(*stringcheck*) | 0 | +| *stringmatch* is zero-length | *start*, or **Len**(*stringcheck*) when *start* is -1 | | *stringmatch* is not found | 0 | | *stringmatch* is found within *stringcheck* | Position at which match is found | -| *start* > **Len**(*stringcheck*) | 0 | + +If *stringcheck* or *stringmatch* is **Null**, error 94 (Invalid use of Null) occurs. Unlike **InStr**, **InStrRev** does not return **Null**. > [!NOTE] > The syntax for the **InStrRev** function is not the same as the syntax for the [**InStr**](InStr) function --- note the swapped order of the search arguments. @@ -52,10 +52,28 @@ The *compare* argument can have the following values: This example uses **InStrRev** to find the last occurrence of a substring. -```tb check_build -Debug.Print InStrRev("a.b.c", ".") ' 4 — last dot -Debug.Print InStrRev("a.b.c", ".", 3) ' 2 — last dot at or before position 3 -Debug.Print InStrRev("a.b.c", "x") ' 0 — not found +```tb check_run +' The last dot. +Debug.Print InStrRev("a.b.c", ".") ' 4 +' The last dot at or before position 3. +Debug.Print InStrRev("a.b.c", ".", 3) ' 2 +' Not found. +Debug.Print InStrRev("a.b.c", "x") ' 0 +``` + +This example shows the end-character rule and the rows of the return-value table that are easy to get wrong. + +```tb check_run +' "bc" at position 5 ends at 6, after start, so the match at 2 is found. +Debug.Print InStrRev("abcabc", "bc", 5) ' 2 +Debug.Print InStrRev("abcabc", "bc", 6) ' 5 +Debug.Print InStrRev("abc", "c", 4) ' 0 +Debug.Print InStrRev("abc", "") ' 3 +Debug.Print InStrRev("abc", "", 4) ' 0 + +On Error Resume Next +Debug.Print InStrRev(Null, "a") +Debug.Print Err.Number ' 94 ``` ### See Also diff --git a/docs/Reference/index.md b/docs/Reference/index.md index 15aa0e7c..a5473cdb 100644 --- a/docs/Reference/index.md +++ b/docs/Reference/index.md @@ -18,6 +18,7 @@ The reference is organized into three layers: the language constructs the compil - [**Data Types**](Reference/Data-Types) -- storage size, range, and suffix for every intrinsic type (**Boolean** through **Variant**) - [**Compiler Constants**](Reference/Compiler-Constants) -- the `#If` symbols the compiler recognises - [**Attributes**](../tB/Core/Attributes) -- `[Description(...)]`, `[COMCreatable(...)]`, and the rest of the attribute syntax +- [**COM Interfaces**](Reference/COM-Interfaces/) -- the standard COM interfaces twinBASIC code implements or calls, such as **IEnumVARIANT** - [**twinBASIC Additions**](Reference/twinBASIC-Additions) -- curated list of language and runtime additions beyond standard VBA **Controls and glossary:** diff --git a/docs/_book.yml b/docs/_book.yml index d71f9245..111a234e 100644 --- a/docs/_book.yml +++ b/docs/_book.yml @@ -343,7 +343,7 @@ parts: - title: Reference Section - subtitle: Controls, enumerations, twinBASIC's additions, and the glossary + subtitle: Controls, enumerations, COM interfaces, twinBASIC's additions, and the glossary outline_closed: true chapters: - title: Controls @@ -352,6 +352,10 @@ parts: - title: Enumerations landing_page: /Reference/Enumerations outline_closed: true + - title: COM Interfaces + landing_page: /Reference/COM-Interfaces/ + page: /Reference/COM-Interfaces/ + outline_closed: true - title: twinBASIC Additions landing_page: /Reference/twinBASIC-Additions outline_closed: true diff --git a/scripts/bug_repro.mjs b/scripts/bug_repro.mjs new file mode 100644 index 00000000..fdcd7f03 --- /dev/null +++ b/scripts/bug_repro.mjs @@ -0,0 +1,1274 @@ +#!/usr/bin/env node +// Reproducer projects for the entries of BUGS-TO-REPORT.md, under bugs/<slug>/. +// +// node scripts/bug_repro.mjs new <slug> "<entry title>" +// node scripts/bug_repro.mjs pack <slug> +// node scripts/bug_repro.mjs compile|build|run <slug> [options] +// node scripts/bug_repro.mjs verify [slug ...] [options] +// node scripts/bug_repro.mjs file <slug> <issue> [--existing] +// node scripts/bug_repro.mjs file --marked +// +// What a reproducer is, and where it lives, is the preamble of BUGS-TO-REPORT.md +// ("Reproducer projects"). A reproducer is bugs/<slug>/ while its entry is queued +// and bugs/filed/<slug>/ once the entry has been filed upstream: a filed bug is not +// a fixed one, and its reproducer is what says when it is. This tool keeps the +// three files in step -- the source +// tree, the packed .twinproj and the zip for the GitHub issue -- and puts a +// reproducer in front of the compiler, through scripts/tbbuild.mjs and +// scripts/tbrun.mjs, so that an entry can say what it saw and a later beta can be +// asked whether it still does. +// +// bugs/<slug>/repro.json says how to ask. `verify` reads it for every reproducer: +// the mode (compile, build, run, cli or manual), the target, and what a +// reproduction looks like (`expect`). Its keys are listed by loadRepro() below, +// and the contributor-facing description is docs/Documentation/Tools.md. +// +// Exit codes: 0 done, 1 a finding, 2 the tool could not do its job, 3 and up its +// own -- see USAGE. tbbuild's and tbrun's exit codes are mapped into that table by +// TBBUILD_EXIT and TBRUN_EXIT, and are what `expect.exit` in repro.json is +// compared with, unmapped. +// +// ---------------------------------------------------------------- why this +// +// An entry in BUGS-TO-REPORT.md owes a reader a project that shows the bug. By +// hand, that is an impexp import and a zip, and then a build to see that the bug +// is still there; this does the first two the same way every time and the third +// without a person at the IDE. `verify` is the reason the file exists: an entry +// that no longer reproduces on the newest beta is one to retire or to re-check. +// +// `verify` is never run by a wrapper, a gate or CI. It needs a twinBASIC install, +// and Windows with a private desktop, as examples.bat does. For a filed bug, +// `NO LONGER REPRODUCES` is the signal that a fix has been released. +// +// ------------------------------------------------------------- what it relies on +// +// 1. THE ZIP IS WRITTEN HERE, in Node. Compress-Archive is PowerShell and 7-Zip +// is not on PATH; Git Bash's `tar -a` writes a tar archive under the .zip +// name and exits 0. A single-entry zip is a local header, the deflated +// bytes, a central directory entry and the end record, and zlib.crc32 is the +// checksum, so no dependency is needed. The entry's time is the .twinproj's +// own, so packing an unchanged source tree twice writes the same zip. +// 2. impexp EXITS 6 WHEN IT WARNS. `import` that finished with a warning is +// still a pack, so 0 and 6 are both success, and its output is printed. +// 3. AN IDE THAT RUNS MANY REPRODUCERS OWNS THE REGISTRY TIDY ONCE. Under +// `verify` this process calls startTidy for every lane, so the tbbuild and +// tbrun children (which find TB_REGISTRY_OWNER set) leave the registry alone, +// and nothing a lane that finishes first puts back is another's entry. The +// lanes use ports base, base+1, ..., which key the work folders tbbuild and +// tbrun use, as in check_examples. +// 4. `run` STAGES A COPY, never the tree: it adds Sources/TbRunProbe.twin, a +// module whose [RunAfterBuild] Sub clears the console and calls Main. With +// --exe that module is left out, because tbrun runs Sub Main in the built exe +// itself. +// 5. `cli` MODE RUNS THE COMPILER EXECUTABLE (bin\twinBASIC_win32.exe) with +// stdin from the null device and a time limit, on the packed project, and +// compares its exit code and output. Paths are given backslashed and in full: +// `export` prefixes \\?\ to its project path, and cannot find a folder named +// with forward slashes (WIP.md, "Driving the twinBASIC compiler"). +import { spawn, spawnSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { + copyFileSync, + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + renameSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import zlib from "node:zlib"; +import { + CliError, + choiceOption, + die, + exitOnCrash, + numberOption, + parseCli, + printHelpAndExit, + withUsageError, +} from "../lib/cli.mjs"; +import { REPO_ROOT } from "../lib/repo-paths.mjs"; +import { keptIdeLines, summaryLine, TARGETS } from "./lib/tb-ide.mjs"; +import { compilerExe, findIde } from "./lib/tb-install.mjs"; +import { finishTidy, startTidy } from "./lib/tb-registry.mjs"; + +let tidy = null; +exitOnCrash(() => finishTidy(tidy)); + +const USAGE = `usage: node scripts/bug_repro.mjs <command> [slug ...] [--ide <twinBASIC.exe>] [--port N] [--arch win32|win64] [--timeout S] [--llvm] [--exe] [--jobs N] [--keep] [--show|--hide] [--template <name>] [--existing] [--marked] [-h, --help] + +Reproducer projects for the entries of BUGS-TO-REPORT.md, under bugs/<slug>/, or +under bugs/filed/<slug>/ once the entry has been filed upstream. +<slug> is kebab-case: lowercase letters and digits joined by single hyphens, and +is never "filed". + +Commands: + new <slug> "<title>" create bugs/<slug>/src/ from the console template, with a + project of the slug's name and a Startup module, and + bugs/<slug>/repro.json with "mode": "manual"; with + --template, from that template's Settings and Sources + pack <slug> pack src/ into <slug>.twinproj with scripts/impexp.mjs, and + write <slug>.zip, the file a GitHub issue accepts, with the + files repro.json's "attach" names + compile <slug> pack, then compile the project in the IDE (tbbuild) and + print its diagnostics + build <slug> pack, then compile and build it (tbbuild --build) + run <slug> run a copy of src/ whose [RunAfterBuild] probe calls Sub + Main, and print what it writes to the DEBUG CONSOLE (tbrun) + verify [slug ...] run what each repro.json says, for the named reproducers or + all of bugs/* and bugs/filed/*, and report per reproducer + whether it reproduces; a filed one is labelled with its + issue, and one that no longer reproduces is probably fixed. + Needs a twinBASIC install; run by a person only + file <slug> <issue> move the entry whose reproducer is <slug> out of + BUGS-TO-REPORT.md into bugs/filed/<slug>/REPORT.md, move + bugs/<slug>/ to bugs/filed/<slug>/, and record the issue in + its repro.json + file --marked do that for every entry that carries a mark line under its + title: *FILED #<n>*, *CAPTURED IN EXISTING #<n>* or + *CAPTURED IN \\#<n>*. If any entry cannot be filed, none is + +Options: + --ide <path> twinBASIC.exe (default: $TB_IDE, else the newest + twinBASIC_IDE_BETA_* on the Desktop) + --port <n> DevTools port to start the IDE on (default 9440); with verify + the first of the lanes' ports + --arch <target> win32 or win64 (default win32); compile, build and run + --timeout <secs> as tbbuild's and tbrun's; with a cli reproducer, the time + limit on the compiler executable + --llvm build with LLVM; build and run + --exe run also runs the built exe and prints what it writes; run + --jobs <n> reproducers to run at once, on ports base, base+1, ... (default + 1); verify + --keep leave the IDE running; its pid is printed; compile, build, run + --show, --hide show the IDE on the desktop, or keep it on a private one + (default: hidden, unless TBBUILD_SHOW is set) + --template <name> new: the project to start from, console (the default) or a + folder of test/repro-templates/, such as webview2-form + --existing file: the issue was already open, and covers this bug; recorded + as "existing" in repro.json + --marked file: take the issue and the slug from the entries' marks + -h, --help print this text and exit + +Exit codes: + 0 done: a project that compiled, built or ran as it should; with verify, every + reproducer that can be run on its own still reproduces; with file, filed + 1 a finding: the project has errors, or its build failed after a clean compile; + with verify, at least one reproducer no longer reproduces + 2 a refused command line (a slug that is not kebab-case or is "filed", a + reproducer that does not exist or is in both bugs/ and bugs/filed/, an option + that does not apply to the command), a repro.json that is not valid, no IDE, a + project that could not be packed, a harness that failed, or a crash; with + verify, a lane's harness failed; with file, an entry that is missing, + ambiguous or marked unreadably, or a bugs/filed/<slug> that is already there + (nothing is changed) + 3 new: bugs/<slug> or bugs/filed/<slug> already exists + 4 the compile never settled + 5 the project crashes the compiler + 6 run: the probe printed nothing + 7 run: the probe ended before it returned + 8 run --exe: the exe exited with a code other than 0, or was still running after + --timeout`; + +const usageError = { format: (err) => `${err.message}\n${USAGE}` }; + +// What each command takes besides -h; anything else given is refused. +const APPLIES = { + new: ["template"], + pack: [], + compile: ["ide", "port", "arch", "timeout", "keep", "show", "hide"], + build: ["ide", "port", "arch", "timeout", "keep", "show", "hide", "llvm"], + run: ["ide", "port", "arch", "timeout", "keep", "show", "hide", "llvm", "exe"], + verify: ["ide", "port", "timeout", "jobs", "show", "hide"], + file: ["existing", "marked"], +}; +const FLAG = (key) => `--${key}`; + +const SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/; +// The folder under bugs/ that holds the reproducers of filed entries, so not a slug. +const FILED = "filed"; + +const { values, positionals } = withUsageError( + () => + parseCli(process.argv.slice(2), { + options: { + ide: { type: "string" }, + port: { type: "string" }, + arch: { type: "string" }, + timeout: { type: "string" }, + jobs: { type: "string" }, + llvm: { type: "boolean", default: false }, + exe: { type: "boolean", default: false }, + keep: { type: "boolean", default: false }, + show: { type: "boolean", default: false }, + hide: { type: "boolean", default: false }, + template: { type: "string" }, + existing: { type: "boolean", default: false }, + marked: { type: "boolean", default: false }, + help: { type: "boolean", short: "h", default: false }, + }, + positionals: { min: 0, max: Infinity }, + stopAt: ["help"], + }), + usageError, +); +if (values.help) printHelpAndExit(USAGE); + +const [command, ...args] = positionals; +if (!command) die(2, USAGE); + +// Everything on the command line is read before anything starts. +const { port, arch, timeout, jobs, template, slugs, title, issue } = withUsageError(() => { + if (!(command in APPLIES)) throw new CliError("unknown-command", `unknown command: ${command}`); + for (const key of Object.keys(values)) { + if (key === "help" || !values[key] || APPLIES[command].includes(key)) continue; + throw new CliError("inapplicable", `${FLAG(key)} does not apply to ${command}`, { option: FLAG(key) }); + } + if (values.show && values.hide) { + throw new CliError("conflict", "--show and --hide cannot be given together", { options: ["--show", "--hide"] }); + } + const read = { + port: numberOption(values.port ?? "9440", { option: "--port", integer: true, min: 1, max: 65535 }), + arch: values.arch === undefined ? undefined : choiceOption(values.arch, { option: "--arch", choices: TARGETS }), + timeout: values.timeout === undefined ? undefined : numberOption(values.timeout, { option: "--timeout", above: 0 }), + jobs: numberOption(values.jobs ?? "1", { option: "--jobs", integer: true, min: 1 }), + template: + values.template === undefined || values.template === "console" + ? null + : choiceOption(values.template, { option: "--template", choices: ["console", ...reproTemplates()] }), + }; + if (read.port + read.jobs - 1 > 65535) { + throw new CliError("bad-number", `--port ${read.port} with --jobs ${read.jobs} runs past port 65535`); + } + const named = command === "verify" ? args : args.slice(0, 1); + let issue; + if (command === "file") { + // file <slug> <issue>, or file --marked, which takes both from the entries. + if (values.marked) { + if (args.length) { + throw new CliError("unexpected-positional", "--marked takes no slug or issue", { arg: args[0] }); + } + if (values.existing) { + throw new CliError("conflict", "--existing does not apply with --marked: an entry's mark says it", { + options: ["--existing", "--marked"], + }); + } + named.length = 0; + } else { + if (args.length < 2) { + throw new CliError("missing-positional", "file needs a slug and an issue number: file <slug> <issue>"); + } + if (args.length > 2) + throw new CliError("unexpected-positional", `unexpected argument: ${args[2]}`, { arg: args[2] }); + if (!/^[1-9]\d*$/.test(args[1])) { + throw new CliError("bad-number", `the issue number expects a whole number of at least 1, got: ${args[1]}`, { + value: args[1], + }); + } + issue = Number(args[1]); + } + } else if (command !== "verify") { + // verify takes any number of slugs, none meaning every reproducer under bugs/. + if (!args.length) { + throw new CliError("missing-positional", `${command} needs a slug`); + } else if (command === "new" && args.length !== 2) { + throw new CliError("missing-positional", `new needs a slug and the entry's title, quoted: new <slug> "<title>"`); + } else if (command !== "new" && args.length > 1) { + throw new CliError("unexpected-positional", `unexpected argument: ${args[1]}`, { arg: args[1] }); + } + } + for (const slug of named) { + if (!SLUG.test(slug)) { + throw new CliError( + "bad-slug", + `not a valid slug: ${slug} (lowercase letters and digits joined by single hyphens, such as my-bug)`, + ); + } + if (slug === FILED) { + throw new CliError("bad-slug", `not a valid slug: ${slug} (it is the folder the filed reproducers are moved to)`); + } + } + if (command === "new" && !args[1].trim()) throw new CliError("empty-value", "the entry's title is empty"); + return { ...read, slugs: named, title: command === "new" ? args[1] : undefined, issue }; +}, usageError); + +// ------------------------------------------------------------------ the files + +const BUGS = path.join(REPO_ROOT, "bugs"); +const TEMPLATE = path.join(REPO_ROOT, "test", "example-projects", "console", "Settings"); + +// The templates new --template takes besides console: each folder of +// test/repro-templates/ that holds a Settings file and a Sources/ folder. A +// function rather than a constant, since the command line is read above. +function reproTemplatesDir() { + return path.join(REPO_ROOT, "test", "repro-templates"); +} +function reproTemplates() { + const dir = reproTemplatesDir(); + if (!existsSync(dir)) return []; + return readdirSync(dir, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .filter((n) => existsSync(path.join(dir, n, "Settings")) && existsSync(path.join(dir, n, "Sources"))) + .sort(); +} +const IMPEXP = path.join(REPO_ROOT, "scripts", "impexp.mjs"); +const TBBUILD = path.join(REPO_ROOT, "scripts", "tbbuild.mjs"); +const TBRUN = path.join(REPO_ROOT, "scripts", "tbrun.mjs"); + +const FILED_DIR = path.join(BUGS, FILED); +const QUEUE = path.join(REPO_ROOT, "BUGS-TO-REPORT.md"); + +// Where a reproducer is: bugs/<slug>/ while its entry is queued, bugs/filed/<slug>/ +// once it is filed. A slug in neither is taken to be a queued one, which is where +// `new` makes it and what a refusal names; a slug in both is an error. +const where = (slug) => { + const queued = path.join(BUGS, slug); + const filed = path.join(FILED_DIR, slug); + const isFiled = existsSync(filed); + if (isFiled && existsSync(queued)) { + throw new Fail(`${rel(queued)} and ${rel(filed)} both exist: a reproducer is in one or the other`); + } + const dir = isFiled ? filed : queued; + return { + filed: isFiled, + dir, + src: path.join(dir, "src"), + repro: path.join(dir, "repro.json"), + twinproj: path.join(dir, `${slug}.twinproj`), + zip: path.join(dir, `${slug}.zip`), + }; +}; +const rel = (file) => path.relative(REPO_ROOT, file).replaceAll("\\", "/"); + +/** A failure the command (or one verify lane) reports and ends on, with the exit code it means. */ +class Fail extends Error { + constructor(message, code = 2) { + super(message); + this.code = code; + } +} + +const crlf = (lines) => lines.join("\r\n"); +const STARTUP = crlf(["Module Startup", "", " Public Sub Main()", " End Sub", "", "End Module", ""]); +// The probe that runs Main inside the IDE. The harness starts the IDE with +// WEBVIEW2_USER_DATA_FOLDER and WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS +// (tb-ide.mjs), so that it can be driven over DevTools, and code run in the IDE +// inherits them. WebView2 lets them override what a WebView2 control asks for, +// so a reproducer's control would join the IDE's own browser with other options +// and fail to start (8007139F, ERROR_INVALID_STATE; measured, BETA 995). The +// probe clears both for the length of Main, and an IDE started by hand has +// neither. +const PROBE = crlf([ + "Module TbRunProbe", + ' Private Declare PtrSafe Function SetEnvironmentVariableW Lib "kernel32" ( _', + " ByVal lpName As LongPtr, ByVal lpValue As LongPtr) As Long", + " [RunAfterBuild]", + " Public Sub RunRepro()", + " Debug.Cls", + ' Dim folder As String = Environ$("WEBVIEW2_USER_DATA_FOLDER")', + ' Dim args As String = Environ$("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS")', + ' SetEnvironmentVariableW StrPtr("WEBVIEW2_USER_DATA_FOLDER"), 0', + ' SetEnvironmentVariableW StrPtr("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS"), 0', + " Main", + ' If Len(folder) Then SetEnvironmentVariableW StrPtr("WEBVIEW2_USER_DATA_FOLDER"), StrPtr(folder)', + ' If Len(args) Then SetEnvironmentVariableW StrPtr("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS"), StrPtr(args)', + " End Sub", + "End Module", + "", +]); + +// kebab-case to PascalCase, for the project's name. A name that begins with a +// digit is not an identifier, so it is given a word to start with. +function pascal(slug) { + const name = slug + .split("-") + .map((w) => w[0].toUpperCase() + w.slice(1)) + .join(""); + return (/^\d/.test(name) ? `Repro${name}` : name).slice(0, 60); +} + +// Rewrites the string value of one key of the template's Settings. A key the +// template does not have is an error, because it means the template changed. +function setKey(text, key, value, file = TEMPLATE) { + const re = new RegExp(`("${key.replaceAll(".", "\\.")}"\\s*:\\s*)"(?:[^"\\\\]|\\\\.)*"`); + if (!re.test(text)) throw new Fail(`${rel(file)} has no "${key}" to set`); + return text.replace(re, (_, head) => `${head}${JSON.stringify(value)}`); +} + +// `template` is null for the console template, else a folder of +// test/repro-templates/, whose Sources/ are copied as they are. +function newReproducer(slug, entryTitle, template = null) { + for (const dir of [path.join(BUGS, slug), path.join(FILED_DIR, slug)]) { + if (existsSync(dir)) throw new Fail(`${rel(dir)} already exists`, 3); + } + const p = where(slug); + const from = template ? path.join(reproTemplatesDir(), template, "Settings") : TEMPLATE; + if (!existsSync(from)) throw new Fail(`no template project at ${rel(from)}`); + const name = pascal(slug); + let settings = readFileSync(from, "utf8"); + settings = setKey(settings, "project.name", name, from); + settings = setKey(settings, "project.appTitle", name, from); + settings = setKey(settings, "project.description", `Reproduces: ${entryTitle}`, from); + settings = setKey(settings, "project.id", `{${randomUUID().toUpperCase()}}`, from); + if (template) { + mkdirSync(p.src, { recursive: true }); + writeFileSync(path.join(p.src, "Settings"), settings); + cpSync(path.join(reproTemplatesDir(), template, "Sources"), path.join(p.src, "Sources"), { recursive: true }); + const steps = "Describe what a person does to see the bug, or set mode to compile, build, run or cli."; + writeFileSync(p.repro, `${JSON.stringify({ mode: "manual", steps }, null, 2)}\n`); + console.log( + `created ${rel(p.dir)}/ (project ${name}, from template ${template}): edit src/Sources, then pack; ` + + "repro.json is manual until set", + ); + return; + } + mkdirSync(path.join(p.src, "Sources"), { recursive: true }); + writeFileSync(path.join(p.src, "Settings"), settings); + writeFileSync(path.join(p.src, "Sources", "Startup.twin"), STARTUP); + const steps = "Describe what a person does to see the bug, or set mode to compile, build, run or cli."; + writeFileSync(p.repro, `${JSON.stringify({ mode: "manual", steps }, null, 2)}\n`); + console.log(`created ${rel(p.dir)}/ (project ${name}): edit src/Sources, then pack; repro.json is manual until set`); +} + +// ------------------------------------------------------------------------ zip + +/** + * A zip file, as the bytes: for each file a local header and the deflated data, + * then a central directory entry for each and the end record. Each file is + * `{ name, data, mtime }`, and keeps its own modified time. + */ +function zipFiles(files) { + const UTF8 = 0x0800; + const locals = []; + const centrals = []; + let offset = 0; + for (const { name, data, mtime } of files) { + const compressed = zlib.deflateRawSync(data, { level: 9 }); + const crc = zlib.crc32(data); + const nameBytes = Buffer.from(name, "utf8"); + const time = (mtime.getHours() << 11) | (mtime.getMinutes() << 5) | (mtime.getSeconds() >> 1); + const date = ((Math.max(mtime.getFullYear(), 1980) - 1980) << 9) | ((mtime.getMonth() + 1) << 5) | mtime.getDate(); + + const local = Buffer.alloc(30); + local.writeUInt32LE(0x04034b50, 0); + local.writeUInt16LE(20, 4); // version needed: deflate + local.writeUInt16LE(UTF8, 6); + local.writeUInt16LE(8, 8); // method: deflate + local.writeUInt16LE(time, 10); + local.writeUInt16LE(date, 12); + local.writeUInt32LE(crc, 14); + local.writeUInt32LE(compressed.length, 18); + local.writeUInt32LE(data.length, 22); + local.writeUInt16LE(nameBytes.length, 26); + // extra field length (28) stays 0 + + const central = Buffer.alloc(46); + central.writeUInt32LE(0x02014b50, 0); + central.writeUInt16LE(20, 4); // version made by + central.writeUInt16LE(20, 6); // version needed + central.writeUInt16LE(UTF8, 8); + central.writeUInt16LE(8, 10); + central.writeUInt16LE(time, 12); + central.writeUInt16LE(date, 14); + central.writeUInt32LE(crc, 16); + central.writeUInt32LE(compressed.length, 20); + central.writeUInt32LE(data.length, 24); + central.writeUInt16LE(nameBytes.length, 28); + // extra, comment, disk, internal and external attributes (30-41) stay 0 + central.writeUInt32LE(offset, 42); // offset of the local header + + locals.push(local, nameBytes, compressed); + centrals.push(central, nameBytes); + offset += local.length + nameBytes.length + compressed.length; + } + const directory = Buffer.concat(centrals); + const end = Buffer.alloc(22); + end.writeUInt32LE(0x06054b50, 0); + end.writeUInt16LE(files.length, 8); // entries on this disk + end.writeUInt16LE(files.length, 10); // entries in all + end.writeUInt32LE(directory.length, 12); + end.writeUInt32LE(offset, 16); + + return Buffer.concat([...locals, directory, end]); +} + +const fileEntry = (name, file) => ({ name, data: readFileSync(file), mtime: statSync(file).mtime }); + +// Packs src/ into the .twinproj with impexp, then zips it with the files +// repro.json's `attach` names. Returns what impexp printed, which the caller +// prints or not. +function pack(slug) { + const p = where(slug); + if (!existsSync(path.join(p.src, "Settings"))) throw new Fail(`no ${rel(p.src)}/Settings: nothing to pack`); + const attach = existsSync(p.repro) ? (loadRepro(slug).attach ?? []) : []; + const r = spawnSync(process.execPath, [IMPEXP, "import", p.twinproj, p.src, "--overwrite"], { + encoding: "utf8", + cwd: REPO_ROOT, + windowsHide: true, + }); + const said = `${r.stdout ?? ""}${r.stderr ?? ""}`; + if (r.error || (r.status !== 0 && r.status !== 6)) { + throw new Fail( + `impexp import ${r.error ? `failed (${r.error.message})` : `exited ${r.status}`}:\n${said.trimEnd()}`, + ); + } + const files = [fileEntry(`${slug}.twinproj`, p.twinproj), ...attach.map((a) => fileEntry(a, path.join(p.dir, a)))]; + writeFileSync(p.zip, zipFiles(files)); + const also = attach.length ? `, with ${attach.join(", ")}` : ""; + return `${said}packed ${rel(p.twinproj)} and ${rel(p.zip)}${also}`; +} + +// ------------------------------------------------------------ repro.json + +const MODES = ["compile", "build", "run", "cli", "manual"]; +// What `expect` may hold in each mode. +const EXPECT_KEYS = { + compile: ["exit", "diagnostics", "noDiagnostics"], + build: ["exit", "message"], + run: ["exit", "output", "absent"], + cli: ["exit", "output", "absent"], +}; + +/** + * Reads and strictly checks bugs/<slug>/repro.json. A fault is a Fail that names + * the file and the key. Returns the file's content with each regular expression + * compiled (`expect.output` as RegExp objects, `expect.message` as one). + */ +function loadRepro(slug) { + const file = where(slug).repro; + const name = rel(file); + const bad = (key, why) => { + throw new Fail(`${name}: ${key ? `"${key}" ` : ""}${why}`); + }; + if (!existsSync(file)) throw new Fail(`${name}: no such file`); + let json; + try { + json = JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, "")); + } catch (e) { + return bad("", `is not valid JSON: ${e.message}`); + } + const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v); + if (!isObject(json)) return bad("", "must be a JSON object"); + const keys = ["mode", "arch", "llvm", "exe", "expect", "cli", "steps", "attach", "issue", "existing"]; + for (const key of Object.keys(json)) if (!keys.includes(key)) bad(key, "is not a key repro.json has"); + if (!MODES.includes(json.mode)) bad("mode", `must be one of ${MODES.join(", ")}`); + const { mode } = json; + // A cli or manual reproducer may have no project of its own: a cli one can + // name the files an installation ships by {ide}. + const p = where(slug); + const hasSrc = existsSync(path.join(p.src, "Settings")); + if (!hasSrc && !["cli", "manual"].includes(mode)) + bad("mode", `${mode} needs a project, and there is no ${rel(p.src)}/Settings`); + if ("attach" in json) { + if (!hasSrc) bad("attach", `goes into ${slug}.zip, which pack writes only from ${rel(p.src)}/`); + const own = [`${slug}.twinproj`, `${slug}.zip`, "repro.json", "REPORT.md"]; + if (!Array.isArray(json.attach) || !json.attach.length) { + bad("attach", "must be a list of paths, relative to the reproducer's folder"); + } + json.attach.forEach((a, i) => { + const key = `attach[${i}]`; + if (typeof a !== "string" || !a) bad(key, "must be a path, relative to the reproducer's folder"); + if (a.includes("\\") || path.posix.isAbsolute(a) || a.split("/").some((s) => s === ".." || s === "." || !s)) { + bad(key, "must be a relative path with forward slashes, inside the reproducer's folder"); + } + if (own.includes(a)) bad(key, `names ${a}, which is not an attachment`); + if (json.attach.indexOf(a) !== i) bad(key, `names ${a} twice`); + const file = path.join(p.dir, a); + if (!existsSync(file) || !statSync(file).isFile()) bad(key, `names ${a}, which is not a file in ${rel(p.dir)}/`); + }); + } + if ("issue" in json && !(Number.isInteger(json.issue) && json.issue > 0)) { + bad("issue", "must be a positive whole number, the number of a twinbasic/twinbasic issue"); + } + if ("existing" in json) { + if (typeof json.existing !== "boolean") bad("existing", "must be true or false"); + if (!("issue" in json)) bad("existing", "applies with issue only"); + } + if ("arch" in json && !TARGETS.includes(json.arch)) bad("arch", `must be ${TARGETS.join(" or ")}`); + if ("llvm" in json && typeof json.llvm !== "boolean") bad("llvm", "must be true or false"); + if (json.llvm && !["build", "run"].includes(mode)) bad("llvm", "applies to the build and run modes only"); + if ("exe" in json && typeof json.exe !== "boolean") bad("exe", "must be true or false"); + if (json.exe && mode !== "run") bad("exe", "applies to the run mode only"); + if ("steps" in json && typeof json.steps !== "string") bad("steps", "must be a string"); + if (mode === "cli") { + // One command, a list of arguments, or several, a list of such lists. + const isArgs = (v) => Array.isArray(v) && v.length && v.every((a) => typeof a === "string"); + const commands = Array.isArray(json.cli) && json.cli.every(Array.isArray) ? json.cli : [json.cli]; + if (!commands.length || !commands.every(isArgs)) { + bad("cli", "must be a list of strings, the compiler executable's arguments, or a list of such lists"); + } + if (!hasSrc) { + for (const arg of commands.flat()) { + const name = /\{(project|src)\}/.exec(arg)?.[0]; + if (name) bad("cli", `names ${name}, and there is no ${rel(p.src)}/Settings to make it from`); + } + } + } else if ("cli" in json) bad("cli", "applies to the cli mode only"); + + const out = { ...json, expect: undefined }; + if (mode === "manual") { + if ("expect" in json) bad("expect", "does not apply to the manual mode"); + return out; + } + if (!isObject(json.expect)) bad("expect", "is required, an object, for every mode but manual"); + const e = json.expect; + for (const key of Object.keys(e)) { + if (!["exit", "diagnostics", "noDiagnostics", "output", "absent", "message"].includes(key)) { + bad(`expect.${key}`, "is not a key expect has"); + } + if (!EXPECT_KEYS[mode].includes(key)) bad(`expect.${key}`, `does not apply to the ${mode} mode`); + } + if ("exit" in e && !Number.isInteger(e.exit)) bad("expect.exit", "must be a whole number"); + if ("diagnostics" in e && !(Array.isArray(e.diagnostics) && e.diagnostics.every((d) => typeof d === "string"))) { + bad("expect.diagnostics", "must be a list of strings, diagnostic codes"); + } + if ("noDiagnostics" in e && typeof e.noDiagnostics !== "boolean") + bad("expect.noDiagnostics", "must be true or false"); + const regex = (key, text) => { + if (typeof text !== "string") bad(key, "must be a string, a regular expression"); + try { + return new RegExp(text, "m"); + } catch (err) { + return bad(key, `is not a regular expression: ${err.message}`); + } + }; + const expect = { ...e }; + for (const key of ["output", "absent"]) { + if (!(key in e)) continue; + if (!Array.isArray(e[key])) bad(`expect.${key}`, "must be a list of regular expressions"); + expect[key] = e[key].map((text, i) => regex(`expect.${key}[${i}]`, text)); + } + if ("message" in e) expect.message = regex("expect.message", e.message); + if (!Object.keys(e).length) bad("expect", "is empty: say what a reproduction looks like"); + out.expect = expect; + return out; +} + +// --------------------------------------------------------- running the tools + +function findTools(ide) { + const exe = findIde(ide); + if (!exe || !existsSync(exe)) { + throw new Fail( + (exe ? `no twinBASIC IDE at ${exe}: ` : "no twinBASIC IDE found: ") + + "pass --ide <twinBASIC.exe>, set TB_IDE, or unpack a twinBASIC_IDE_BETA_<n> folder on your Desktop", + ); + } + return exe; +} + +// A tool run as a child, with its output collected. A tool that outlives its +// time limit is ended with its process tree, by pid. +function runNode(scriptArgs, limitMs) { + return new Promise((resolve) => { + const child = spawn(process.execPath, scriptArgs, { + cwd: REPO_ROOT, + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + let stdout = ""; + let stderr = ""; + let timedOut = false; + child.stdout.setEncoding("utf8").on("data", (d) => { + stdout += d; + }); + child.stderr.setEncoding("utf8").on("data", (d) => { + stderr += d; + }); + const timer = setTimeout(() => { + timedOut = true; + spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }); + }, limitMs); + child.on("error", (e) => { + clearTimeout(timer); + resolve({ code: null, stdout, stderr: `${stderr}${e.message}`, timedOut }); + }); + child.on("close", (code) => { + clearTimeout(timer); + resolve({ code, stdout, stderr, timedOut }); + }); + }); +} + +// The limit on a child: its own timeout, and then time for it to end the IDE. +const limitFor = (secs, own) => ((secs ?? own) + 240) * 1000; + +const ideFlags = (o) => [ + ...(o.ide ? ["--ide", o.ide] : []), + "--port", + String(o.port), + ...(o.arch ? ["--arch", o.arch] : []), + ...(o.timeout !== undefined ? ["--timeout", String(o.timeout)] : []), + ...(o.show ? ["--show"] : []), + ...(o.hide ? ["--hide"] : []), + ...(o.keep ? ["--keep"] : []), +]; + +/** tbbuild on the packed project: `build` adds --build, or --llvm, which implies it. */ +async function compileOrBuild(slug, o) { + const p = where(slug); + const flags = [...ideFlags(o), ...(o.llvm ? ["--llvm"] : o.build ? ["--build"] : [])]; + const r = await runNode([TBBUILD, p.twinproj, "--json", ...flags], limitFor(o.timeout, 180)); + let json = null; + try { + json = JSON.parse(r.stdout); + } catch { + // a failure before the report: its words are on stderr + } + return { ...r, json, message: r.stderr.trim(), output: r.stdout }; +} + +/** tbrun on a staged copy of src/, with the probe module added unless --exe runs Sub Main in the exe. */ +async function runProbe(slug, o) { + const p = where(slug); + const stage = path.join(tmpdir(), "bugrepro", String(o.port), slug); + rmSync(stage, { recursive: true, force: true }); + try { + cpSync(p.src, stage, { recursive: true }); + mkdirSync(path.join(stage, "Sources"), { recursive: true }); + if (!o.exe) writeFileSync(path.join(stage, "Sources", "TbRunProbe.twin"), PROBE); + const flags = [...ideFlags(o), ...(o.llvm ? ["--llvm"] : []), ...(o.exe ? ["--exe"] : [])]; + const r = await runNode([TBRUN, stage, ...flags], limitFor(o.timeout, 120)); + return { ...r, json: null, message: r.stderr.trim(), output: r.stdout }; + } finally { + rmSync(stage, { recursive: true, force: true }); + } +} + +/** + * The compiler executable, as a cli repro.json says: one command, or several in + * turn in the one temp folder. Their output is joined, and their exit code is + * the one they all gave, or the codes joined by commas when they differ. + */ +function runCli(slug, repro, o) { + const ide = findTools(o.ide); + const exe = compilerExe(ide); + if (!existsSync(exe)) throw new Fail(`no compiler beside the IDE at ${exe}`); + const scratch = mkdtempSync(path.join(tmpdir(), "bugrepro-cli-")); + try { + // Copies, so a command that writes its project or its folder (an import, an + // export --overwrite) never touches the committed reproducer. A reproducer + // with no src/ has neither, and loadRepro refused {project} and {src} for it. + const p = where(slug); + const project = path.join(scratch, path.basename(p.twinproj)); + const src = path.join(scratch, "src"); + if (existsSync(path.join(p.src, "Settings"))) { + copyFileSync(p.twinproj, project); + cpSync(p.src, src, { recursive: true }); + } + const commands = repro.cli.every(Array.isArray) ? repro.cli : [repro.cli]; + const limit = (o.timeout ?? 120) * 1000; + const codes = []; + let stdout = ""; + let stderr = ""; + let output = ""; + for (const command of commands) { + const argv = command.map((a) => + a + .replaceAll("{project}", project) + .replaceAll("{src}", src) + .replaceAll("{tmp}", scratch) + .replaceAll("{ide}", path.resolve(path.dirname(ide))), + ); + const r = spawnSync(exe, argv, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + timeout: limit, + windowsHide: true, + }); + if (r.error) { + throw new Fail( + r.error.code === "ETIMEDOUT" ? `the compiler executable ran past ${limit / 1000} s` : r.error.message, + ); + } + codes.push(r.status); + stdout += r.stdout ?? ""; + stderr += r.stderr ?? ""; + output += `${r.stdout ?? ""}${r.stderr ?? ""}`; + } + const code = codes.every((c) => c === codes[0]) ? codes[0] : codes.join(","); + return { code, stdout, stderr, json: null, message: "", output }; + } finally { + rmSync(scratch, { recursive: true, force: true }); + } +} + +// tbbuild's and tbrun's exit codes in this tool's table. +const TBBUILD_EXIT = { 0: 0, 1: 1, 2: 2, 3: 4, 4: 5, 5: 1 }; +const TBRUN_EXIT = { 0: 0, 1: 1, 2: 2, 3: 6, 4: 5, 5: 7, 6: 8 }; +const mapped = (table, code) => table[code] ?? 2; + +// ------------------------------------------------------------------- printing + +function printBuild(r) { + const j = r.json; + if (j) { + if (j.arch && j.arch !== TARGETS[0]) console.log(`target: ${j.arch}`); + for (const row of j.diagnostics ?? []) console.log(row); + console.log(summaryLine([j.errors, j.warnings, j.hints, j.infos])); + // The build log is the finding when the build failed; on success it is noise. + if (r.code === 5) for (const line of j.buildLog ?? []) console.log(line); + if (j.built) console.log(`built: ${j.built}`); + if (j.dialogs?.length) console.log("dialogs:", JSON.stringify(j.dialogs)); + if (j.kept && j.idePid) for (const l of keptIdeLines(j.idePid)) console.log(l); + } else if (r.stdout.trim()) process.stdout.write(r.stdout); + if (r.stderr.trim()) process.stderr.write(r.stderr.endsWith("\n") ? r.stderr : `${r.stderr}\n`); +} + +function printRun(r) { + if (r.stdout) process.stdout.write(r.stdout.endsWith("\n") ? r.stdout : `${r.stdout}\n`); + if (r.stderr.trim()) process.stderr.write(r.stderr.endsWith("\n") ? r.stderr : `${r.stderr}\n`); +} + +// A reproducer is a folder with src/Settings; for verify, one with a repro.json +// will do, since a cli reproducer may name only files an installation ships. +const requireReproducer = (slug, { project = true } = {}) => { + const p = where(slug); + if (existsSync(path.join(p.src, "Settings"))) return; + if (!project && existsSync(p.repro)) return; + throw new Fail(`no such reproducer: ${rel(p.dir)} (no src/Settings${project ? "" : " and no repro.json"})`); +}; + +// --------------------------------------------------------------------- verify + +// Whether a run reproduces what repro.json expects: the problems, none if it does. +function mismatches(repro, r) { + const e = repro.expect; + const problems = []; + if ("exit" in e && r.code !== e.exit) problems.push(`expected exit ${e.exit}, got ${r.code}`); + if (e.diagnostics) { + const rows = (r.json?.diagnostics ?? []).join("\n"); + for (const code of e.diagnostics) if (!rows.includes(code)) problems.push(`${code} was not reported`); + } + if (e.noDiagnostics) { + const j = r.json; + const total = j ? j.errors + j.warnings + j.hints + j.infos : null; + if (total === null) problems.push("there is no report to read"); + else if (total) problems.push(`expected no diagnostics, got ${total}`); + } + for (const re of e.output ?? []) if (!re.test(r.output)) problems.push(`the output does not match /${re.source}/`); + for (const re of e.absent ?? []) if (re.test(r.output)) problems.push(`the output matches /${re.source}/`); + if (e.message && !e.message.test(r.message)) problems.push(`the message does not match /${e.message.source}/`); + return problems; +} + +// One reproducer: pack it, run what repro.json says, judge the result. +async function verifyOne(slug, repro, lane) { + if (repro.mode === "manual") return { slug, status: "manual", detail: repro.steps ?? "(no steps recorded)" }; + const o = { ide: values.ide, port: lane, arch: repro.arch, timeout, show: values.show, hide: values.hide }; + try { + if (existsSync(path.join(where(slug).src, "Settings"))) pack(slug); + let r; + let table; + if (repro.mode === "cli") r = runCli(slug, repro, o); + else if (repro.mode === "run") { + r = await runProbe(slug, { ...o, llvm: repro.llvm, exe: repro.exe }); + table = TBRUN_EXIT; + } else { + r = await compileOrBuild(slug, { ...o, build: repro.mode === "build", llvm: repro.llvm }); + table = TBBUILD_EXIT; + } + const wanted = repro.expect.exit; + if (r.timedOut) return { slug, status: "harness", detail: "the tool outlived its time limit and was ended" }; + // Exit 2 is the tool's own failure, and tbbuild's 3 a compile that never + // settled: neither says anything about the bug unless the file expects it. + const failed = table && (r.code === 2 || (table === TBBUILD_EXIT && r.code === 3)) && r.code !== wanted; + if (failed || r.code === null) { + const said = (r.message || r.stdout || "no output").trim().split("\n").slice(-3).join(" | "); + return { slug, status: "harness", detail: `exit ${r.code}: ${said}` }; + } + const problems = mismatches(repro, r); + return problems.length + ? { slug, status: "no-longer", detail: problems.join("; ") } + : { slug, status: "reproduces", detail: `${repro.mode} exit ${r.code}` }; + } catch (e) { + if (!(e instanceof Fail)) throw e; + return { slug, status: "harness", detail: e.message.split("\n").join(" | ") }; + } +} + +const LABEL = { + reproduces: "reproduces", + "no-longer": "NO LONGER REPRODUCES", + manual: "manual", + harness: "harness failed", +}; + +async function verify() { + let all = slugs; + if (!all.length) { + // bugs/<slug>/ and bugs/filed/<slug>/. A slug in both is listed twice, and + // requireReproducer refuses it below. + const under = (dir) => + existsSync(dir) + ? readdirSyncDirs(dir).filter((s) => SLUG.test(s) && existsSync(path.join(dir, s, "repro.json"))) + : []; + all = [...under(BUGS).filter((s) => s !== FILED), ...under(FILED_DIR)]; + } + if (!all.length) { + console.log("no reproducers: nothing under bugs/ has a repro.json"); + return 0; + } + // Every file is read before anything starts: a fault in one is the person's to + // fix, and not a reason to have run the others. + const repros = new Map(); + for (const slug of all) { + requireReproducer(slug, { project: false }); + repros.set(slug, loadRepro(slug)); + } + const automatic = all.filter((s) => ["compile", "build", "run"].includes(repros.get(s).mode)); + const lanes = Math.min(jobs, Math.max(all.length, 1)); + if (automatic.length || all.some((s) => repros.get(s).mode === "cli")) findTools(values.ide); + if (automatic.length) { + const ports = Array.from({ length: lanes }, (_, i) => port + i); + tidy = startTidy({ + paths: automatic.map((s) => where(s).twinproj), + prefixes: ports.flatMap((n) => [ + path.join(tmpdir(), "tbbuild", String(n)), + path.join(tmpdir(), "tbrun", String(n)), + ]), + }); + } + const results = []; + const queue = [...all]; + const lane = async (index) => { + while (queue.length) { + const slug = queue.shift(); + const repro = repros.get(slug); + const res = { ...(await verifyOne(slug, repro, port + index)), filed: where(slug).filed }; + results.push(res); + console.log( + `${slug}${filedLabel(res.filed, repro)}: ${LABEL[res.status]}${res.detail ? ` -- ${res.detail}` : ""}`, + ); + } + }; + try { + await Promise.all(Array.from({ length: lanes }, (_, i) => lane(i))); + } finally { + finishTidy(tidy); + tidy = null; + } + const count = (status, filed = false) => results.filter((r) => r.status === status && r.filed === filed).length; + const summary = (filed) => + `${count("reproduces", filed)} reproduce, ${count("no-longer", filed)} no longer reproduce, ` + + `${count("manual", filed)} manual, ${count("harness", filed)} harness failed`; + console.log(summary(false)); + const anyFiled = results.some((r) => r.filed); + if (anyFiled) { + console.log(`filed: ${summary(true)}`); + if (count("no-longer", true)) { + console.log("filed bugs that no longer reproduce are probably fixed"); + } + } + const total = (status) => count(status) + count(status, true); + return total("harness") ? 2 : total("no-longer") ? 1 : 0; +} + +// A filed reproducer's result line says so, and names its issue. +function filedLabel(filed, repro) { + if (!filed) return ""; + if (!repro.issue) return " (filed)"; + return ` (filed #${repro.issue}${repro.existing ? ", existing" : ""})`; +} + +function readdirSyncDirs(dir) { + return readdirSync(dir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort(); +} + +// ----------------------------------------------------------------------- file + +// BUGS-TO-REPORT.md is a preamble, a `---` line, and then entries separated by +// `---` lines. An entry runs from its `## ` title to the line before the next +// `---` (or to the end of the file). The file is handled as lines that keep their +// own endings, so what is not changed is written back byte for byte. +const bare = (line) => line.replace(/\r?\n$/, ""); +const FILED_MARK = /^\*FILED #([1-9]\d*)\*$/; +const CAPTURED_MARK = /^\*CAPTURED IN (?:EXISTING )?\\?#([1-9]\d*)\*$/; +const TWINPROJ_NAME = /`([a-z0-9]+(?:-[a-z0-9]+)*)\.twinproj`/g; +const BUGS_FOLDER = /\bbugs\/([a-z0-9]+(?:-[a-z0-9]+)*)\//g; +const issueUrl = (n) => `https://github.com/twinbasic/twinbasic/issues/${n}`; + +/** The queue file's lines and entries: each entry's index range and title. */ +function parseQueue(text) { + const lines = text.split(/(?<=\n)/); + const seps = []; + lines.forEach((line, i) => { + if (bare(line) === "---") seps.push(i); + }); + if (!seps.length) throw new Fail(`${rel(QUEUE)} has no "---" line after its preamble`); + const entries = seps.map((sep, k) => { + const end = k + 1 < seps.length ? seps[k + 1] : lines.length; + let from = sep + 1; + while (from < end && bare(lines[from]) === "") from++; + let to = end; + while (to > from && bare(lines[to - 1]) === "") to--; + if (from >= end || !bare(lines[from]).startsWith("## ")) { + throw new Fail(`${rel(QUEUE)}: the entry after line ${sep + 1} does not begin with a "## " title`); + } + return { k, sep, end, from, to, title: bare(lines[from]) }; + }); + const seen = new Set(); + for (const e of entries) { + if (seen.has(e.title)) throw new Fail(`${rel(QUEUE)}: two entries have the title ${e.title}`); + seen.add(e.title); + } + return { lines, entries, eol: text.includes("\r\n") ? "\r\n" : "\n" }; +} + +/** The index of the first line under an entry's title that is not blank, or -1. */ +function lineUnderTitle(lines, entry) { + let i = entry.from + 1; + while (i < entry.to && bare(lines[i]) === "") i++; + return i < entry.to ? i : -1; +} + +/** + * The mark line under an entry's title: null if it has none, `{ bad }` if it cannot + * be read, else its index, issue and whether it is "existing". A `*DEFERRED ...*` + * line is the owner's note that the entry is held on purpose: it is not a mark + * `file --marked` acts on, and is skipped without a word. + */ +function markOf(lines, entry) { + const i = lineUnderTitle(lines, entry); + if (i < 0) return null; + const line = bare(lines[i]).trimEnd(); + if (!/^\*(FILED|CAPTURED)/.test(line)) return null; + const filed = FILED_MARK.exec(line); + const captured = CAPTURED_MARK.exec(line); + const m = filed ?? captured; + return m ? { index: i, issue: Number(m[1]), existing: !filed } : { bad: line }; +} + +/** + * The entries whose body names `<slug>.twinproj`. An entry whose reproducer is not + * an attachment (it names two files in the installation instead) has no such name, + * and says which reproducer is its own only in its closing comment, as bugs/<slug>/: + * when no entry names the project, the entries that name that folder are the answer. + */ +function entriesNaming(queue, slug) { + const naming = (needle) => + queue.entries.filter((e) => queue.lines.slice(e.from, e.to).some((line) => line.includes(needle))); + const byProject = naming(`\`${slug}.twinproj\``); + return byProject.length ? byProject : naming(`bugs/${slug}/`); +} + +/** The reproducers (folders under bugs/) an entry names: by `<slug>.twinproj`, else by bugs/<slug>/ in its comment. */ +function reproducersNamed(queue, entry) { + const found = (re) => { + const names = new Set(); + for (const line of queue.lines.slice(entry.from, entry.to)) { + for (const m of line.matchAll(re)) if (m[1] !== FILED && existsSync(path.join(BUGS, m[1]))) names.add(m[1]); + } + return names; + }; + const byProject = found(TWINPROJ_NAME); + return byProject.size ? byProject : found(BUGS_FOLDER); +} + +/** The lines of an entry as the report holds them: its title down, without its mark line and the blank after it. */ +function reportLines(queue, entry) { + const body = queue.lines.slice(entry.from, entry.to).map(bare); + const mark = markOf(queue.lines, entry); + // A filing mark goes, and so does a DEFERRED line when an entry is filed by hand. + const at = mark && !mark.bad ? mark.index : lineUnderTitle(queue.lines, entry); + const drop = at >= 0 && (mark ? !mark.bad : /^\*DEFERRED\b/.test(bare(queue.lines[at]))); + if (drop) body.splice(at - entry.from, body[at - entry.from + 1] === "" ? 2 : 1); + return body; +} + +/** The file's lines without the entry and one `---` beside it, so one `---` still divides the entries either side. */ +function withoutEntry(queue, entry) { + const out = queue.lines.slice(); + if (entry.end < out.length) { + // the blank line, the entry, the blank line and the `---` after it + out.splice(entry.sep + 1, entry.end - entry.sep); + } else { + // the last entry: its own `---` goes too, unless it is the preamble's + out.splice(entry.k === 0 ? entry.sep + 1 : entry.sep); + while (out.length > 1 && bare(out[out.length - 1]) === "") out.pop(); + if (out.length && !/\n$/.test(out[out.length - 1])) out[out.length - 1] += queue.eol; + } + return out.join(""); +} + +/** + * Everything filing `slug` as `issue` needs, checked, and nothing written: the + * report, the file's text without the entry, and the paths. `original` is the + * file as read, which says whether exactly one entry names the slug; `text` is + * what is left of it after the entries filed before this one. + */ +function prepareFiling(slug, issue, existing, original, text) { + const queued = path.join(BUGS, slug); + const filed = path.join(FILED_DIR, slug); + if (existsSync(filed)) throw new Fail(`${rel(filed)} already exists`); + if (!existsSync(queued)) throw new Fail(`no such reproducer: ${rel(queued)}`); + loadRepro(slug); + const named = entriesNaming(original, slug); + if (named.length !== 1) { + const what = named.length ? `${named.length} entries name` : "no entry names"; + throw new Fail(`${what} \`${slug}.twinproj\` in ${rel(QUEUE)}${named.map((e) => `\n ${e.title}`).join("")}`); + } + const current = parseQueue(text); + const entry = current.entries.find((e) => e.title === named[0].title); + const head = existing + ? `Covered by the existing issue [twinbasic/twinbasic#${issue}](${issueUrl(issue)}).` + : `Filed as [twinbasic/twinbasic#${issue}](${issueUrl(issue)}).`; + const report = `${[head, "", ...reportLines(current, entry)].join(current.eol)}${current.eol}`; + return { slug, issue, existing, queued, filed, report, text: withoutEntry(current, entry), title: entry.title }; +} + +/** Moves the reproducer, writes REPORT.md and repro.json's issue, and rewrites the queue file. */ +function carryOutFiling(plan) { + mkdirSync(FILED_DIR, { recursive: true }); + renameSync(plan.queued, plan.filed); + writeFileSync(path.join(plan.filed, "REPORT.md"), plan.report); + const file = path.join(plan.filed, "repro.json"); + const raw = readFileSync(file, "utf8"); + const json = JSON.parse(raw.replace(/^/, "")); + json.issue = plan.issue; + if (plan.existing) json.existing = true; + else delete json.existing; + const out = `${JSON.stringify(json, null, 2)}\n`; + writeFileSync(file, raw.includes("\r\n") ? out.replaceAll("\n", "\r\n") : out); + writeFileSync(QUEUE, plan.text); +} + +const filingLine = (plan) => + `filed ${plan.slug} as ${plan.existing ? "existing issue " : "issue "}#${plan.issue}: ${rel(plan.queued)} -> ${rel(plan.filed)}/ with REPORT.md`; + +function readQueue() { + if (!existsSync(QUEUE)) throw new Fail(`no ${rel(QUEUE)}`); + const text = readFileSync(QUEUE, "utf8"); + return { text, queue: parseQueue(text) }; +} + +/** `file <slug> <issue> [--existing]` */ +function fileOne(slug, issueNumber, existing) { + const queued = path.join(BUGS, slug); + const filed = path.join(FILED_DIR, slug); + if (existsSync(filed)) throw new Fail(`${rel(filed)} already exists`); + if (!existsSync(queued)) throw new Fail(`no such reproducer: ${rel(queued)}`); + const { text, queue } = readQueue(); + const plan = prepareFiling(slug, issueNumber, existing, queue, text); + carryOutFiling(plan); + console.log(filingLine(plan)); +} + +/** `file --marked`: every marked entry, or none of them. */ +function fileMarked() { + const { text, queue } = readQueue(); + const problems = []; + const wanted = []; + for (const entry of queue.entries) { + const mark = markOf(queue.lines, entry); + if (!mark) continue; + if (mark.bad) { + problems.push(`${entry.title}\n the mark cannot be read: ${mark.bad}`); + continue; + } + const names = reproducersNamed(queue, entry); + if (names.size !== 1) { + const what = names.size + ? `names ${names.size} reproducers: ${[...names].join(", ")}` + : "names no reproducer under bugs/"; + problems.push(`${entry.title}\n the entry ${what}, as \`<slug>.twinproj\` or bugs/<slug>/`); + continue; + } + wanted.push({ slug: [...names][0], ...mark }); + } + const plans = []; + let left = text; + for (const w of wanted) { + try { + const plan = prepareFiling(w.slug, w.issue, w.existing, queue, left); + left = plan.text; + plans.push(plan); + } catch (e) { + if (!(e instanceof Fail)) throw e; + problems.push(`${w.slug}\n ${e.message.split("\n").join("\n ")}`); + } + } + if (problems.length) { + throw new Fail( + `nothing was filed: ${problems.length} marked ${problems.length === 1 ? "entry" : "entries"} cannot be filed\n${problems.join("\n")}`, + ); + } + if (!plans.length) { + console.log(`no entry in ${rel(QUEUE)} carries a mark line`); + return; + } + for (const plan of plans) { + carryOutFiling(plan); + console.log(filingLine(plan)); + } + console.log(`${plans.length} filed; ${parseQueue(left).entries.length} left in ${rel(QUEUE)}`); +} + +// ----------------------------------------------------------------------- main + +async function main() { + const [slug] = slugs; + const o = { + ide: values.ide, + port, + arch, + timeout, + show: values.show, + hide: values.hide, + keep: values.keep, + llvm: values.llvm, + exe: values.exe, + }; + switch (command) { + case "new": + newReproducer(slug, title, template); + return 0; + case "pack": + requireReproducer(slug); + console.log(pack(slug)); + return 0; + case "file": + if (values.marked) fileMarked(); + else fileOne(slug, issue, values.existing); + return 0; + case "compile": + case "build": { + requireReproducer(slug); + findTools(o.ide); + console.log(pack(slug)); + const r = await compileOrBuild(slug, { ...o, build: command === "build" }); + printBuild(r); + if (r.timedOut) throw new Fail("tbbuild outlived its time limit and was ended"); + return mapped(TBBUILD_EXIT, r.code); + } + case "run": { + requireReproducer(slug); + findTools(o.ide); + const r = await runProbe(slug, o); + printRun(r); + if (r.timedOut) throw new Fail("tbrun outlived its time limit and was ended"); + return mapped(TBRUN_EXIT, r.code); + } + default: + return verify(); + } +} + +try { + process.exit(await main()); +} catch (e) { + if (!(e instanceof Fail)) throw e; + finishTidy(tidy); + die(e.code, e.message); +} diff --git a/scripts/check_examples.mjs b/scripts/check_examples.mjs index 9e8a3b67..bc1e1c06 100644 --- a/scripts/check_examples.mjs +++ b/scripts/check_examples.mjs @@ -8,8 +8,14 @@ // node scripts/check_examples.mjs --propose --apply # ...and mark the ones that pass // node scripts/check_examples.mjs --report survey.json # group a saved survey // -// Exit: 0 clean, 1 a sample does not compile, 2 the harness could not run (a refused -// command line, no IDE, or a crash). +// Exit: 0 clean, 1 a sample does not compile (or, for a `check_run` sample, does not +// run as the page says), 2 the harness could not run (a refused command line, no IDE, +// or a crash). +// +// A `check_run` sample is a statement sample that is built and run: its batch gets one +// generated [RunAfterBuild] Sub that calls each sample in turn between marker lines, +// and the lines the run writes to the DEBUG CONSOLE are attributed to samples by +// those markers (lib/example-run.mjs; the capture is lib/tb-run.mjs). // // ------------------------------------------------------------------ why // @@ -56,11 +62,13 @@ // `Sub Main` is not one of them, though it was once listed as one. The template // brings a Main, and a sample may bring its own beside it: two `Public Sub // Main`s in different modules compile (measured, BETA 983), which is how the -// WinServicesLib `Module Startup` samples build as written. +// WinServicesLib `Module Startup` samples build as written. That holds for a +// compile; a build refuses two Mains, and two [DllExport]s of one name, and a +// project with an error at all, so --build and --llvm batch around those three. // // And one that does not: a sample can take the compiler down. twinBASIC runs it // in-process with user code, and a two-line syntax skeleton in Attributes.md -// crashes it outright (BUGS-TO-REPORT.md). In a batch that costs every other +// crashes it outright (twinbasic/twinbasic#2453). In a batch that costs every other // sample its result, so a crash is isolated, paid for only on failure: the // sample tbbuild names as the one the compiler died parsing is built on its own // and the rest without it, a crash that names none bisects, O(log n) builds, and @@ -90,6 +98,7 @@ import { } from "../lib/cli.mjs"; import { mapLines } from "../lib/markdown.mjs"; import { + CLASS_SLOTS, CONCAT_KEY, HIDDEN_MARKER, MARKER, @@ -117,6 +126,18 @@ import { sectionOf, unresolvedName, } from "./lib/example-batches.mjs"; +import { + RUN_DONE, + RUN_FILE, + RUN_TAG, + dispatcherText, + expectedOutput, + isRunFence, + judgeOutput, + parseRun, + partitionRun, + runRefusal, +} from "./lib/example-run.mjs"; import { compileProject } from "./lib/tb-build.mjs"; import { wantShow } from "./lib/tb-ide.mjs"; import { buildNumber, compilerExe, findIde, runCompiler } from "./lib/tb-install.mjs"; @@ -143,6 +164,8 @@ const { values } = withUsageError( verbose: { type: "boolean", default: false }, json: { type: "boolean", default: false }, keep: { type: "boolean", default: false }, + build: { type: "boolean", default: false }, + llvm: { type: "boolean", default: false }, show: { type: "boolean", default: false }, hide: { type: "boolean", default: false }, help: { type: "boolean", short: "h", default: false }, @@ -155,7 +178,12 @@ const { values } = withUsageError( const USAGE = `usage: node scripts/check_examples.mjs [options] Compiles the documentation's own twinBASIC code samples, every tb fence marked -\`${MARKER}\`, and reports the ones the compiler refuses. +\`${MARKER}\`, and reports the ones the compiler refuses. A statement sample +(slot=sub) marked \`${RUN_MARKER}\` is also built and run, in every mode, and what it prints +is compared with what the page says it prints: the trailing comment on a Debug.Print +line, or the comment lines under a \`' Output:\` line. A sample that raises an error, +does not return, or prints something else is a finding; one that calls MsgBox or +InputBox, or contains End, or is not slot=sub, is refused without being built. --only <regex> restrict to pages whose path matches --census classify every tb fence and print the table; no compiler @@ -167,6 +195,10 @@ Compiles the documentation's own twinBASIC code samples, every tb fence marked --port <n> base DevTools port (default 9480) --batch <n> samples per generated project (default 120) --ide <path> twinBASIC.exe (default: $TB_IDE, else the newest on the Desktop) + --build also build each project that compiles without errors; a project + whose build fails is cut down to the samples that fail it + --llvm build with LLVM (implies --build); needs a Professional or + Ultimate licence, and --build alone is its control --keep leave the generated projects on disk and say where --show, --hide as tbbuild's --verbose also print warnings, not only errors @@ -177,10 +209,12 @@ Exit codes: 0 every marked sample compiles, or none is marked; --report always, and --propose when it found only unmarked samples that fail (advisory) 1 a marked sample does not compile, a marker is misused, a template does not - compile, or the compiler crashed on a project; the report names each + compile, the compiler crashed on a project, --build or --llvm found a sample + that fails the build, or a \`${RUN_MARKER}\` sample raised an error, did not return or + printed something other than the page says; the report names each 2 the harness could not run: a refused command line, a failed self-test probe, no IDE or compiler, an unreadable --report file, a work folder it could not clear, - or a crash`; + an --llvm run on a Community or Personal licence, or a crash`; if (values.help) printHelpAndExit(USAGE); @@ -208,6 +242,21 @@ const MODE_REPORT = values.report ?? null; const APPLY = values.apply; const VERBOSE = values.verbose; const AS_JSON = values.json; +const LLVM = values.llvm; +// --llvm is a build, so the one switch the rest of the file reads is BUILD. +const BUILD = values.build || LLVM; +// The procedure a [DllExport] attribute is on, for a run that builds. +const DLL_EXPORT_NAME = /\[\s*DllExport\b[^\]]*\][\s_]*(?:(?:Public|Private|Friend)\s+)?(?:Function|Sub)\s+(\w+)/gi; +// A sample's own `Sub Main`, which a class body may not hold and a build has to +// keep apart from the template's. +const DECLARES_MAIN = /^[ \t]*(?:(?:Public|Private|Friend)[ \t]+)?Sub[ \t]+Main[ \t]*\(/im; + +// What a run that builds could say about each sample. A project whose compile has +// errors is not built (the IDE refuses one), so its samples are compiled and +// nothing more; a sample is counted as unbuilt only if no build of any project +// that held it, a smaller one from isolating a larger included, ever ran. +const builtIds = new Set(); +const unbuiltIds = new Set(); // A page's template, when its fence does not name one. Inferred from the path // because the package a sample needs is what the page is ABOUT -- stating @@ -362,7 +411,12 @@ function select(fences) { // fact written twice, and the pair could then disagree. fence.base = fence.keys.get("inherits") ?? null; if (fence.base && slot) slot = PROMOTE_TO_CLASS[slot] ?? slot; - fence.inferred = inferred; + // A [DllExport] name is the exe's, whatever module declares it, and a build + // refuses two ("[LINKER] FAILED duplicate [DLLExport] functions detected"), + // which a compile accepts. Counted among the names, it keeps two samples + // exporting one name out of one project, as a clash of names does. + const exported = BUILD ? [...fence.content.matchAll(DLL_EXPORT_NAME)].map((m) => `[DllExport] ${m[1]}`) : []; + fence.inferred = exported.length ? { ...inferred, names: [...(inferred.names ?? []), ...exported] } : inferred; fence.slot = slot; fence.slotStated = Boolean(stated); fence.project = fence.keys.get("project") ?? defaultProject(fence.rel); @@ -402,6 +456,26 @@ function select(fences) { ); continue; } + // A fence that asks to be RUN has to be one that can be: a statement + // sample, with nothing in it that waits for a person or ends the run. + const refusal = runRefusal(fence); + if (refusal) { + addFinding(fence, refusal.message, refusal.detail); + continue; + } + // A class body with a `Sub Main` in it is no program: that Main is a method + // of the class, never the startup object. It is usually two files written + // as one fence, a class and the module that starts it. + if (CLASS_SLOTS.has(slot) && DECLARES_MAIN.test(fence.content)) { + if (marked) { + addFinding( + fence, + `declares \`Sub Main\` in a class (${slot}${fence.base ? `, inherits=${fence.base}` : stated ? "" : ", inferred"}), where it is never the startup object`, + "put the startup Main in a fence of its own, which a module slot holds", + ); + } + continue; + } chosen.push(fence); } return chosen; @@ -480,6 +554,8 @@ function stageBatch(batch, work) { for (const name of templateChain(batch.project)) { cpSync(path.join(TEMPLATES, name), dir, { recursive: true }); } + // The sample brings the project's Main (see makeBatches' `alone`). + if (batch.noMain) rmSync(path.join(dir, "Sources", "tbxMain.twin"), { force: true }); const settingsPath = path.join(dir, "Settings"); const settings = JSON.parse(readFileSync(settingsPath, "utf8")); @@ -492,6 +568,11 @@ function stageBatch(batch, work) { // invisible and unreachable -- so the build never happens while the WebView2 // renderer stays responsive and every health check says the IDE is fine. settings["project.buildPath"] = path.join(dir, `${name}.exe`).split("/").join("\\"); + // The run's compiler options and the exe's alike, as tbrun's --llvm sets them. + if (LLVM) { + settings["compiler.debugOptions"] = "+llvm"; + settings["compiler.buildOptions"] = "+llvm"; + } writeFileSync(settingsPath, JSON.stringify(settings, null, "\t") + "\n", "utf8"); const map = new Map(); @@ -521,6 +602,17 @@ function stageBatch(batch, work) { for (const c of CANARIES) { writeFileSync(path.join(dir, "Sources", c.file), c.text.replace(/\n/g, "\r\n"), "utf8"); } + // A run batch gets the one [RunAfterBuild] Sub a project may have, calling + // each run sample's body in turn. The members of a group that are not run + // samples are staged above and get no call. + const runs = batch.run ? batch.fences.filter(isRunFence) : []; + if (runs.length) { + writeFileSync( + path.join(dir, "Sources", RUN_FILE), + dispatcherText(runs.map((f) => moduleName(f.id))).replace(/\n/g, "\r\n"), + "utf8", + ); + } const proj = path.join(work, `b${index}.twinproj`); // Pure Windows paths: the compiler prefixes \\?\, which does not accept @@ -531,7 +623,7 @@ function stageBatch(batch, work) { // reports, 999 on a tree holding an embedded package, which a resource= fence // staged under Packages/ would make -- so runCompiler reads the output. if (!pack.done) throw new Error(`packing failed${pack.why}:\n${pack.tail}`); - return { proj, dir, map }; + return { proj, dir, map, runs }; } // ------------------------------------------------------------------- building @@ -545,19 +637,86 @@ const COMPILER = IDE ? compilerExe(IDE) : null; // between. let tidy = null; +// How a run batch's wait ends (see buildStaged), in milliseconds. +const RUN_QUIET_MS = 8000; +const RUN_TIMEOUT_MS = 180000; + +// What each run sample did, by fence id, from the last build of a project that +// held it: its share of the lines the dispatcher wrote, and the sample that +// began and did not return, if there was one. A later build of a smaller +// project replaces an earlier one's entry, as it does for `builtIds`. +const runById = new Map(); + +/** + * What is wrong with a run's capture as a whole, or null: the dispatcher wrote + * no marker, or stopped with every sample that began returned and some never + * reached. (A sample that began and did not return is the finding of that + * sample, not a fault of the capture.) + */ +function runProblem(staged, cap) { + const parsed = parseRun(cap.captured, staged.runs.length); + if (parsed.done) return null; + if (!parsed.began) { + return ( + `the run wrote no ${RUN_TAG} line${cap.timedOut ? " before the timeout" : ""}, so the dispatcher never ran` + + (cap.captured.length ? `; the console held: ${cap.captured.slice(0, 3).join(" | ")}` : "") + ); + } + if (!parsed.items.some((i) => i.began && !i.ended)) { + return "the run stopped before the dispatcher finished, with no sample left unreturned"; + } + return null; +} + +function recordRun(staged, cap) { + const parsed = parseRun(cap.captured, staged.runs.length); + const culprit = staged.runs[parsed.items.findIndex((i) => i.began && !i.ended)] ?? null; + for (const [i, fence] of staged.runs.entries()) { + runById.set(fence.id, { ...parsed.items[i], culprit, done: parsed.done }); + } +} + /** Build one staged batch; returns per-fence errors, or a crash marker. */ async function buildStaged(staged, port) { - const r = await compileProject({ - project: staged.proj, - ide: IDE, - port, - show: wantShow({ show: values.show, hide: values.hide }), - }); + // A run batch is built and its [RunAfterBuild] Sub is run, in every mode. The + // run is over when the dispatcher says it is done, or when the console has + // been quiet for RUN_QUIET_MS: long enough to outlast a pause in an LLVM + // build's log, since a quiet period that ends inside one reads as a run that + // never started. + const run = staged.runs.length + ? { done: (lines) => lines.some((l) => l.trim() === RUN_DONE), quietMs: RUN_QUIET_MS, timeoutMs: RUN_TIMEOUT_MS } + : null; + const compile = () => + compileProject({ + project: staged.proj, + ide: IDE, + port, + show: wantShow({ show: values.show, hide: values.hide }), + build: BUILD || !!run, + llvm: LLVM, + run, + }); + let r = await compile(); + // A run that wrote no marker at all, or stopped without finishing and without + // a sample that began and did not return, is read again once: nothing says + // which sample is to blame, and the IDE can read the console early. + const lost = (res) => (res.code === 0 && run ? runProblem(staged, res.run) : null); + if (lost(r)) r = await compile(); + if (lost(r)) throw new Error(`${lost(r)} (${staged.proj})`); if (r.code === 4) return { crashed: true, detail: r.message, named: crashedIn(r.crashFiles, staged.map) }; + // A build that failed after a clean compile is isolated as a crash is: nothing + // names a sample, so it is cut down by halving. `named` is empty for that. + if (r.code === 5) return { crashed: true, buildFailed: true, named: new Set(), detail: r.message }; if (r.code !== 0 && r.code !== 1) { throw new Error(`tbbuild exited ${r.code} on ${staged.proj}\n${r.message}`); } + if (run && r.code === 0) recordRun(staged, r.run); + if (BUILD || run) { + for (const entry of staged.map.values()) (r.code === 0 ? builtIds : unbuiltIds).add(entry.fence.id); + const first = r.rows.find((row) => row.startsWith("{ERROR}")); + if (r.code === 1) say(` not built: ${path.basename(staged.proj)} has errors, the first ${first}`); + } const result = { diagnostics: r.rows }; const perFence = new Map(); @@ -625,6 +784,7 @@ function laneOf(port, work) { }, finding: addFinding, note: say, + llvm: LLVM, }; } @@ -887,7 +1047,42 @@ async function main() { process.exit(findings.some((f) => !f.advisory) ? 1 : 0); } - const batches = makeBatches(selected, { batchSize, jobs }); + // A project with an error is not built, and an `expect-error` sample is one on + // purpose; batched with the rest it would leave every sample beside it unbuilt. + // So a run that builds batches those samples apart, each with its page's + // hidden blocks, which the rest of the page keeps as well. + const expectsError = (f) => f.keys.has("expect-error"); + // A build binds the startup object, and fails on a template Main beside a + // sample's own ("'Main' is ambiguous"), which a compile accepts; so a run that + // builds gives such a unit a project of its own without the template's. + const alone = BUILD ? (members) => members.some((f) => DECLARES_MAIN.test(f.content)) : null; + // A `check_run` sample is built apart from everything else, whatever the mode, + // since only a build runs [RunAfterBuild]: its batches get a dispatcher and + // are built and run. A `projname` group holding one goes there whole. + const { run: runList, rest: restList } = partitionRun(selected); + const errorPages = new Set(restList.filter(expectsError).map((f) => f.rel)); + const hiddenOfErrorPage = (f) => f.flags.has(HIDDEN_MARKER) && errorPages.has(f.rel); + const restBatches = BUILD + ? [ + ...makeBatches( + restList.filter((f) => !expectsError(f)), + { batchSize, jobs, alone }, + ), + ...(errorPages.size + ? makeBatches( + restList.filter((f) => expectsError(f) || hiddenOfErrorPage(f)), + { batchSize, jobs, alone }, + ) + : []), + ] + : makeBatches(restList, { batchSize, jobs }); + // A run that builds always keeps a unit with its own `Sub Main` apart, as above. + const runBatches = makeBatches(runList, { + batchSize, + jobs, + alone: (members) => members.some((f) => DECLARES_MAIN.test(f.content)), + }).map((b) => ({ ...b, run: true })); + const batches = [...restBatches, ...runBatches]; const work = path.join(tmpdir(), "tbexamples", String(basePort)); try { rmSync(work, { recursive: true, force: true }); @@ -918,15 +1113,9 @@ async function main() { (staged ? `, ${staged} staged file(s)` : ""), ); - // Said out loud rather than passed over in silence: a sample asking to be RUN - // is only being compiled today, and a reader of this output would otherwise - // have no way to tell which of the two happened. - const wantRun = selected.filter((f) => f.flags.has(RUN_MARKER)).length; + const wantRun = selected.filter(isRunFence).length; if (wantRun) { - say( - ` note: ${wantRun} sample(s) ask for \`${RUN_MARKER}\`; execution is not ` + - `implemented yet, so they were compiled only`, - ); + say(` ${wantRun} sample(s) marked \`${RUN_MARKER}\` are built and run, in ${runBatches.length} project(s)`); } // Every lane's IDE records its projects in the user's recent list and saved @@ -1007,7 +1196,10 @@ async function main() { // compiler down together with, carries no rows of its own -- one finding // names the rest -- but it is still not a pass. if (!diags.length) { - if (!blamed.has(fence.id)) passed.push(fence); + if (!blamed.has(fence.id)) { + passed.push(fence); + if (isRunFence(fence)) judgeRun(fence); + } continue; } findings.push({ @@ -1068,6 +1260,18 @@ async function main() { `${real} finding(s), ${secs}s` + (real ? "" : " -- clean"), ); + if (wantRun) { + say( + `${runStats.ran} run sample(s) ran, ${runStats.matched} without a finding` + + (runStats.unrun ? `, ${runStats.unrun} compiled but not run` : ""), + ); + for (const note of runNotes) say(note); + } + // A project with errors is never built, so the build asked nothing of these. + if (BUILD) { + const unbuilt = samples.filter((f) => unbuiltIds.has(f.id) && !builtIds.has(f.id)).length; + if (unbuilt) say(`${unbuilt} sample(s) in batches with errors were compiled but not built`); + } } if (values.keep) say(`generated projects kept in ${work}`); else rmSync(work, { recursive: true, force: true }); @@ -1075,6 +1279,49 @@ async function main() { process.exit(real ? 1 : 0); } +// What the run samples did, for the summary line. +const runStats = { ran: 0, matched: 0, unrun: 0 }; +const runNotes = []; + +/** + * Compare one run sample's output with what its text says it prints, and report + * what it raised or did not return. A sample that raised an error is reported + * for that alone: what it printed before it stopped is not its output. + */ +function judgeRun(fence) { + const out = runById.get(fence.id); + const where = (f) => `docs/${f.rel}:${f.line}`; + if (!out) { + // Compiled clean, but its project had errors elsewhere, so nothing in it was built. + runStats.unrun++; + return; + } + if (!out.began) { + runStats.unrun++; + runNotes.push( + ` note: ${where(fence)} (${fence.id}) was not run: ` + + (out.culprit ? `${where(out.culprit)} did not return` : "the run ended before it"), + ); + return; + } + runStats.ran++; + const before = findings.length; + if (!out.ended) { + addFinding( + fence, + "did not return", + "the run saw this sample begin and never saw it end: a loop that does not finish, or a call that never comes back", + ); + } else if (out.error) { + addFinding(fence, `raised error ${out.error.number}: ${out.error.description}`); + } else { + for (const p of judgeOutput(expectedOutput(fence), out.output)) { + addFinding(fence, p.message, p.detail, p.line ? { line: p.line } : {}); + } + } + if (findings.length === before) runStats.matched++; +} + function reportFindings() { if (!findings.length) return; say(""); diff --git a/scripts/check_twin_parsers.mjs b/scripts/check_twin_parsers.mjs index 87c96ade..1e805581 100644 --- a/scripts/check_twin_parsers.mjs +++ b/scripts/check_twin_parsers.mjs @@ -21,11 +21,17 @@ // under. // - parseTargets (scripts/lib/attributes-doc.mjs), which turns an // `Applicable to:` line into gen_attribute_probes.mjs's targets. +// - wrapProbe (scripts/lib/tb-probe.mjs), which moves a tbrun probe's +// [RunAfterBuild] to a wrapper; a Sub it wraps wrongly runs the wrong code, +// and one it misses loses tbrun's check that the probe returned. +// - sentinelIndex (the same module), which reads that check from the console; +// a line the IDE prints after a return must not read as the probe ending. import { exitOnCrash, parseCli, printHelpAndExit, withUsageError } from "../lib/cli.mjs"; import { parseTargets } from "./lib/attributes-doc.mjs"; import { createProbes } from "./lib/gate-probes.mjs"; import { classify } from "./lib/tb-fences.mjs"; +import { SENTINEL, sentinelIndex, TBRUN_FILE, WRAPPER_SUB, wrapProbe } from "./lib/tb-probe.mjs"; import { parseTwin } from "./lib/twin-api.mjs"; import { MODIFIERS, declarationKind } from "./lib/twin-declarations.mjs"; @@ -129,4 +135,99 @@ for (const [app, want] of [ check(`parseTargets: ${app}`, show(got) === show(want), `got ${show(got)}`); } +// ---------------------------------------------------------------- wrapProbe + +// Each fixture is one file, Probe.twin; `want` is the Sub wrapped, or null for +// none. A wrapped file must keep every line where it was, and add exactly one +// [RunAfterBuild] -- the wrapper's -- inside the probe's module. +const probe = (body) => `Module Probe\n${body}\nEnd Module\n`; +for (const [what, text, want] of [ + ["a Public Sub", probe(" [RunAfterBuild]\n Public Sub Run()\n Debug.Cls\n End Sub"), "Run"], + [ + "a Private Sub, CRLF", + probe(" [RunAfterBuild]\n Private Sub Go()\n End Sub").replaceAll("\n", "\r\n"), + "Go", + ], + ["the Sub on the attribute's line", probe(" [RunAfterBuild] Sub Go\n End Sub"), "Go"], + [ + "a comment and another attribute between", + probe(' [RunAfterBuild]\n \' why\n [Description("x")]\n Sub Go()\n End Sub'), + "Go", + ], + ["a commented-out attribute", probe(" ' [RunAfterBuild]\n Sub Go()\n End Sub"), null], + ["a Sub with parameters", probe(" [RunAfterBuild]\n Sub Go(ByVal n As Long)\n End Sub"), null], + ["a Function", probe(" [RunAfterBuild]\n Function Go() As Long\n End Function"), null], + ["in a Class", "Class Probe\n [RunAfterBuild]\n Sub Go()\n End Sub\nEnd Class\n", null], + // The End Module that follows is another module's, where Go cannot be called. + [ + "in a Class before a Module", + "Class Probe\n [RunAfterBuild]\n Sub Go()\n End Sub\nEnd Class\nModule M\nEnd Module\n", + null, + ], + // The container is the last one before the attribute, not the last in the file. + [ + "in a Module before a Class", + `${probe(" [RunAfterBuild]\n Sub Go()\n End Sub")}Class C\nEnd Class\n`, + "Go", + ], + [ + "two of them", + probe(" [RunAfterBuild]\n Sub A()\n End Sub\n [RunAfterBuild]\n Sub B()\n End Sub"), + null, + ], + [ + "the second of two modules", + `${probe(" Sub A()\n End Sub")}Module Second\n [RunAfterBuild]\n Sub B()\n End Sub\nEnd Module\n`, + "B", + ], +]) { + const r = wrapProbe([{ name: "Probe.twin", text }]); + const out = r.files.find((f) => f.name === "Probe.twin")?.text; + const tbrun = r.files.some((f) => f.name === TBRUN_FILE); + let ok = (r.wrapped?.sub ?? null) === want && tbrun; + if (ok && want) { + const eol = text.includes("\r\n") ? "\r\n" : "\n"; + const [before, after] = [text.split(eol), out.split(eol)]; + const attrs = after.filter((l) => /\[RunAfterBuild\]/.test(l)).length; + const call = after.findIndex((l) => l.trim() === `Public Sub ${WRAPPER_SUB}()`); + // The wrapper is six lines, from its attribute to a blank line; without + // them, the file is the probe's with its attribute blanked. + const rest = after.filter((_, i) => i < call - 1 || i >= call + 5); + const kept = + rest.length === before.length && + before.every((l, i) => rest[i] === l || rest[i] === l.replace("[RunAfterBuild]", " ".repeat(15))); + const endModule = after.findIndex((l, i) => i > call && /^End Module/.test(l)); + ok = + kept && + attrs === 1 && + after[call + 1].trim() === want && + after[call + 2].trim() === `Debug.Print "${SENTINEL}"` && + endModule > call && + !after.slice(0, call).some((l) => l.includes(WRAPPER_SUB)); + } + check(`wrapProbe: ${what}`, ok, show({ wrapped: r.wrapped, why: r.why, tbrun, out })); +} +check( + "wrapProbe: a tree with its own Module TbRun gets no second one", + !wrapProbe([{ name: "Mine.twin", text: "Module TbRun\nEnd Module\n" }]).files.some((f) => f.name === TBRUN_FILE), + "", +); + +// ---------------------------------------------------------------- sentinelIndex +// What tbrun reads to tell a probe that returned from one that ended first. +const WAITING = "[DEBUGGER] Waiting for remaining forms to close..."; +for (const [what, lines, want] of [ + ["the sentinel last", ["a", SENTINEL], 1], + ["a form left loaded", ["a", SENTINEL, WAITING], 1], + ["nothing printed before it", [SENTINEL, WAITING], 0], + ["no sentinel", ["a", WAITING], -1], + ["the probe's own line after it", ["a", SENTINEL, "b"], -1], + ["an IDE line other than the wait", [SENTINEL, "[DEBUGGER] closed file #1"], -1], + ["the wait line, prefixed", [SENTINEL, `x${WAITING}`], -1], + ["an empty console", [], -1], +]) { + const got = sentinelIndex(lines); + check(`sentinelIndex: ${what}`, got === want, `got ${got}, want ${want}`); +} + process.exit(report()); diff --git a/scripts/lib/cli-cases.mjs b/scripts/lib/cli-cases.mjs index b3ab1608..206264bd 100644 --- a/scripts/lib/cli-cases.mjs +++ b/scripts/lib/cli-cases.mjs @@ -103,6 +103,8 @@ const CASES = [ { tool: "scripts/tbrun.mjs", args: ["no-such-dir", "--arch", ""], exit: 2, stderr: /^--arch needs a non-empty value\nusage: node scripts\/tbrun\.mjs / }, { tool: "scripts/tbrun.mjs", args: ["--bogus", "no-such-dir"], exit: 2, stderr: /^unknown option: --bogus\nusage: node scripts\/tbrun\.mjs / }, { tool: "scripts/tbrun.mjs", args: ["no-such-dir", "--arch", "win64"], exit: 2, stderr: /^not a directory: .*no-such-dir\ntbrun takes an exported source tree / }, + { tool: "scripts/bug_repro.mjs", args: [], exit: 2, stderr: /^usage: node scripts\/bug_repro\.mjs / }, + { tool: "scripts/bug_repro.mjs", args: ["compile", "--help", "--bogus"], exit: 0, stdout: /^usage: node scripts\/bug_repro\.mjs / }, { tool: "scripts/addin_test.mjs", args: ["--help"], exit: 0, stdout: /^usage: node scripts\/addin_test\.mjs / }, { tool: "scripts/addin_test.mjs", args: ["--ide"], exit: 2, stderr: "--ide needs a value\n" }, { tool: "scripts/addin_test.mjs", args: ["--ide", ""], exit: 2, stderr: "--ide needs a non-empty value\n" }, @@ -323,6 +325,7 @@ const HELP_TOOLS = { "eval/transcript.mjs": "Usage: node eval/transcript.mjs ", "scripts/addin_test.mjs": null, "scripts/ide_test.mjs": null, + "scripts/bug_repro.mjs": null, "scripts/build_dot_metrics.mjs": null, "scripts/build_package_api.mjs": null, "scripts/census_attributes.mjs": null, @@ -394,6 +397,7 @@ const REFUSALS = { "eval/transcript.mjs": [null], "scripts/addin_test.mjs": ["ide"], "scripts/ide_test.mjs": ["ide"], + "scripts/bug_repro.mjs": ["ide"], "scripts/build_dot_metrics.mjs": [null], "scripts/build_package_api.mjs": ["out"], "scripts/census_attributes.mjs": ["out"], @@ -510,6 +514,16 @@ for (const [tool, first] of [ bad(tool, [first, "--show", "--hide"], thenUsage("--show and --hide cannot be given together", tool)); } bad("scripts/tbrun.mjs", ["no-such-dir", "--quiet=-1"], thenUsage(NOT_WHOLE("--quiet", -1), "scripts/tbrun.mjs")); +bad( + "scripts/tbrun.mjs", + ["no-such-dir", "--llvm", "--compiler-options", "+llvm"], + thenUsage("--llvm and --compiler-options cannot be given together", "scripts/tbrun.mjs"), +); +bad( + "scripts/tbrun.mjs", + ["no-such-dir", "--compiler-options="], + thenUsage("--compiler-options needs a non-empty value", "scripts/tbrun.mjs"), +); // sweep_attributes reads its values before it looks for an IDE, and follows the // message with its usage. @@ -531,6 +545,86 @@ bad("scripts/tbrun.mjs", ["no-such-dir", "--quiet=-1"], thenUsage(NOT_WHOLE("--q } bad("scripts/tbrun.mjs", ["no-such-dir", "--quiet", "1.5"], thenUsage(NOT_WHOLE("--quiet", 1.5), "scripts/tbrun.mjs")); +// bug_repro follows the message with its usage. It reads and checks its command +// line before it looks for a reproducer or an IDE, and the reproducers it is +// asked for here do not exist. +{ + const tool = "scripts/bug_repro.mjs"; + const slugMessage = (slug) => + `not a valid slug: ${slug} (lowercase letters and digits joined by single hyphens, such as my-bug)`; + const FILED_MESSAGE = "not a valid slug: filed (it is the folder the filed reproducers are moved to)"; + bad(tool, ["frobnicate"], thenUsage("unknown command: frobnicate", tool)); + bad(tool, ["new"], thenUsage("new needs a slug", tool)); + bad( + tool, + ["new", "no-such-bug"], + thenUsage(`new needs a slug and the entry's title, quoted: new <slug> "<title>"`, tool), + ); + bad(tool, ["new", "Bad_Slug", "title"], thenUsage(slugMessage("Bad_Slug"), tool)); + bad(tool, ["new", "no-such-bug", " "], thenUsage("the entry's title is empty", tool)); + bad( + tool, + ["new", "no-such-bug", "title", "--template", "no-such-template"], + thenUsage("--template expects console or webview2-form, got: no-such-template", tool), + ); + bad(tool, ["pack", "no-such-bug", "--template", "console"], thenUsage("--template does not apply to pack", tool)); + bad(tool, ["pack", "-x"], thenUsage("unknown option: -x", tool)); + bad(tool, ["pack", "bad--slug"], thenUsage(slugMessage("bad--slug"), tool)); + bad(tool, ["compile"], thenUsage("compile needs a slug", tool)); + bad(tool, ["compile", "no-such-bug", "other"], thenUsage("unexpected argument: other", tool)); + bad(tool, ["compile", "no-such-bug", "--llvm"], thenUsage("--llvm does not apply to compile", tool)); + bad(tool, ["build", "no-such-bug", "--exe"], thenUsage("--exe does not apply to build", tool)); + bad(tool, ["pack", "no-such-bug", "--port", "9440"], thenUsage("--port does not apply to pack", tool)); + bad(tool, ["verify", "--keep"], thenUsage("--keep does not apply to verify", tool)); + bad(tool, ["verify", "--arch", "win32"], thenUsage("--arch does not apply to verify", tool)); + bad(tool, ["compile", "no-such-bug", "--port", "0"], thenUsage(NOT_PORT(0), tool)); + bad(tool, ["run", "no-such-bug", "--timeout", "0"], thenUsage(NOT_ABOVE_ZERO("--timeout", 0), tool)); + bad(tool, ["run", "no-such-bug", "--arch", "WIN32"], thenUsage("--arch expects win32 or win64, got: WIN32", tool)); + bad(tool, ["verify", "--jobs", "0"], thenUsage(NOT_COUNT("--jobs", 0), tool)); + bad( + tool, + ["verify", "--port", "65535", "--jobs", "2"], + thenUsage("--port 65535 with --jobs 2 runs past port 65535", tool), + ); + bad( + tool, + ["compile", "no-such-bug", "--show", "--hide"], + thenUsage("--show and --hide cannot be given together", tool), + ); + bad(tool, ["run", "no-such-bug"], "no such reproducer: bugs/no-such-bug (no src/Settings)\n"); + bad(tool, ["verify", "no-such-bug"], "no such reproducer: bugs/no-such-bug (no src/Settings and no repro.json)\n"); + // `file` moves things, so these are the refusals only: every one is decided from the + // command line or from a reproducer that does not exist, before anything is read or written. + const fileNeeds = "file needs a slug and an issue number: file <slug> <issue>"; + bad(tool, ["file"], thenUsage(fileNeeds, tool)); + bad(tool, ["file", "no-such-bug"], thenUsage(fileNeeds, tool)); + bad( + tool, + ["file", "no-such-bug", "x"], + thenUsage("the issue number expects a whole number of at least 1, got: x", tool), + ); + bad( + tool, + ["file", "no-such-bug", "0"], + thenUsage("the issue number expects a whole number of at least 1, got: 0", tool), + ); + bad(tool, ["file", "no-such-bug", "12", "13"], thenUsage("unexpected argument: 13", tool)); + bad(tool, ["file", "Bad_Slug", "12"], thenUsage(slugMessage("Bad_Slug"), tool)); + bad(tool, ["file", "filed", "12"], thenUsage(FILED_MESSAGE, tool)); + bad(tool, ["new", "filed", "title"], thenUsage(FILED_MESSAGE, tool)); + bad(tool, ["verify", "filed"], thenUsage(FILED_MESSAGE, tool)); + bad(tool, ["file", "--marked", "no-such-bug"], thenUsage("--marked takes no slug or issue", tool)); + bad( + tool, + ["file", "--marked", "--existing"], + thenUsage("--existing does not apply with --marked: an entry's mark says it", tool), + ); + bad(tool, ["file", "no-such-bug", "12", "--port", "9440"], thenUsage("--port does not apply to file", tool)); + bad(tool, ["compile", "no-such-bug", "--marked"], thenUsage("--marked does not apply to compile", tool)); + bad(tool, ["verify", "--existing"], thenUsage("--existing does not apply to verify", tool)); + bad(tool, ["file", "no-such-bug", "12"], "no such reproducer: bugs/no-such-bug\n"); +} + bad("scripts/addin_test.mjs", ["--only", "("], REGEX_REASON("--only", "(")); bad("scripts/addin_test.mjs", ["--port", "0"], NOT_PORT(0) + "\n"); bad("scripts/addin_test.mjs", ["--port=1.5"], NOT_PORT(1.5) + "\n"); diff --git a/scripts/lib/example-batches.mjs b/scripts/lib/example-batches.mjs index 3cbe2539..5e820af0 100644 --- a/scripts/lib/example-batches.mjs +++ b/scripts/lib/example-batches.mjs @@ -22,6 +22,16 @@ import { resourcePath, wrapFence, } from "./tb-fences.mjs"; +import { + RUN_DONE, + dispatcherText, + expectedOutput, + isRunFence, + judgeOutput, + parseRun, + partitionRun, + runRefusal, +} from "./example-run.mjs"; /** check_examples.mjs's defaults for `--jobs` and `--batch`, which the probes batch with. */ export const DEFAULT_JOBS = 4; @@ -83,7 +93,13 @@ function unitKey(fence) { return fence.keys.get("projname") ? `@${fence.keys.get("projname")}` : `#${fence.id}`; } -export function makeBatches(fences, { batchSize = DEFAULT_BATCH, jobs = DEFAULT_JOBS } = {}) { +/** + * `alone`, given a unit's fences, says whether the unit is to be built in a + * project of its own with no template Main (`noMain` on the batch): a run that + * builds passes one for a unit declaring its own `Sub Main`, because a build, + * unlike a compile, fails on two. + */ +export function makeBatches(fences, { batchSize = DEFAULT_BATCH, jobs = DEFAULT_JOBS, alone = null } = {}) { // A `hidden` fence is not a unit of its own: it is the PAGE's context, and // it joins every project that holds a sample from that page. So a page can // carry the declarations its samples assume -- the class its prose describes @@ -156,7 +172,9 @@ export function makeBatches(fences, { batchSize = DEFAULT_BATCH, jobs = DEFAULT_ // author can actually reason about: what compiles is what they grouped, // plus the template. It costs one project per group, and groups are // written by hand, so there are never many. - if (key.startsWith("@")) { + // A unit `alone` picks gets one too, built without the template's Main. + const solo = !!alone?.(members); + if (key.startsWith("@") || solo) { const own = new Set(names); for (const rel of pages) for (const n of hiddenNamesFor(rel)) own.add(n); batches.push({ @@ -164,7 +182,8 @@ export function makeBatches(fences, { batchSize = DEFAULT_BATCH, jobs = DEFAULT_ fences: [...members], names: own, pages, - group: key.slice(1), + ...(key.startsWith("@") ? { group: key.slice(1) } : {}), + ...(solo ? { noMain: true } : {}), }); continue; } @@ -382,7 +401,11 @@ function ownRowsOf(project, lane) { ); } const rows = result.crashed - ? [`the ${project} template crashes the compiler with no samples in it`] + ? [ + result.buildFailed + ? `the ${project} template fails the build with no samples in it` + : `the ${project} template crashes the compiler with no samples in it`, + ] : [...(result.unattributed ?? []), ...(result.unreadable ?? [])]; if (rows.length) { lane.note( @@ -460,18 +483,26 @@ async function split(batch, lane, why) { * with nothing in `crashed` -- its crash needed several samples too -- and * whether a part crashed is what decides the next step. */ -async function isolateCrash(batch, named, lane) { - const where = `a compiler crash in ${batch.fences.length} sample(s) [${batch.project}]`; +async function isolateCrash(batch, named, lane, failed = false, detail = "") { + const where = `${failed ? "a failed build" : "a compiler crash"} in ${batch.fences.length} sample(s) [${batch.project}]`; let parts = takeOut(batch, named); if (parts) lane.note(` ${where}: building the sample it died parsing on its own`); else if ((parts = splitBatch(batch))) lane.note(` ${where}: splitting to find it`); else { const { rep, ids, rest } = leafOf(batch); - lane.finding( - rep, - "crashes the twinBASIC compiler" + rest, - "the compiler dies parsing this sample; record it in BUGS-TO-REPORT.md", - ); + if (failed) { + lane.finding( + rep, + "fails the build" + rest, + `${buildName(lane)} fails on this sample${detail ? ` (${detail})` : ""}; record it in BUGS-TO-REPORT.md if it is the compiler's fault`, + ); + } else { + lane.finding( + rep, + "crashes the twinBASIC compiler" + rest, + "the compiler dies parsing this sample; record it in BUGS-TO-REPORT.md", + ); + } // Named back to the caller, because a crashed sample produced no // diagnostics and would otherwise be counted as one that compiled -- the // same false-clean shape tbbuild's own crash check exists to close. @@ -480,10 +511,37 @@ async function isolateCrash(batch, named, lane) { const subs = []; for (const part of parts) subs.push(await runBatch(part, lane)); if (subs.some((s) => s.fromCrash)) return { ...merge(subs), fromCrash: true }; - lane.note(" neither part crashes on its own: looking for the samples it needs together"); - return { ...merge([...subs, await together(batch, ...parts, lane)]), fromCrash: true }; + lane.note( + ` neither part ${failed ? "fails the build" : "crashes"} on its own: looking for the samples it needs together`, + ); + return { ...merge([...subs, await together(batch, ...parts, lane, failed)]), fromCrash: true }; } +/** + * Build a batch, and build it once more if the build failed after a clean + * compile. + * + * A build can fail once and pass when repeated: one that reported nothing for + * 120 s, once in a full --llvm run (BETA 995). Lanes whose IDEs shared a temp + * folder failed with "[LINKER] FAILED to create type library" too, which + * launchIde's folder per IDE prevents. A failure is believed only if the same + * batch fails twice running, and so is each part the halving that follows builds. + */ +async function buildTwiceOnFailure(batch, lane) { + const first = await lane.build(batch); + if (!first.crashed || !first.buildFailed) return first; + lane.note( + ` a failed build in ${batch.fences.length} sample(s) [${batch.project}] (${first.detail}): building it again`, + ); + return lane.build(batch); +} + +/** + * What the lane's build is called in a finding: LLVM is the point of an --llvm + * run, and the plain build is its control. + */ +const buildName = (lane) => (lane.llvm ? "the LLVM build" : "the build"); + /** * The samples a crash needs when it needs several: `a` and `b` each built * clean, and together they crash. @@ -498,7 +556,7 @@ async function isolateCrash(batch, named, lane) { * Every member built clean in `a` or `b`, so each has its own result. They are * blamed rather than passed, because together they take the compiler down. */ -async function together(batch, a, b, lane) { +async function together(batch, a, b, lane, failed = false) { const { hidden } = unitsOf(batch); const crashes = async (units) => !!(await lane.build(batchOf(batch, units, hidden))).crashed; const partners = async (fixed, pool) => { @@ -519,14 +577,24 @@ async function together(batch, a, b, lane) { .flat() .filter((f) => !f.flags.has(HIDDEN_MARKER) && !f.isResource); const [rep, ...others] = members; - lane.finding( - rep, - `crashes the twinBASIC compiler when built with ${others.length} other ` + - `sample(s), though none of the ${members.length} does on its own`, - `the others: ${others.map((f) => `${f.rel}:${f.line}`).join(", ")}` + - (found ? "" : "; no smaller set that crashes was found") + - " -- record it in BUGS-TO-REPORT.md", - ); + const theOthers = `the others: ${others.map((f) => `${f.rel}:${f.line}`).join(", ")}`; + if (failed) { + lane.finding( + rep, + `fails the build when built with ${others.length} other ` + + `sample(s), though none of the ${members.length} does on its own`, + theOthers + + (found ? "" : "; no smaller set that fails was found") + + `; ${buildName(lane)} fails on them together -- record it in BUGS-TO-REPORT.md if it is the compiler's fault`, + ); + } else { + lane.finding( + rep, + `crashes the twinBASIC compiler when built with ${others.length} other ` + + `sample(s), though none of the ${members.length} does on its own`, + theOthers + (found ? "" : "; no smaller set that crashes was found") + " -- record it in BUGS-TO-REPORT.md", + ); + } return { ...blank(), blamed: members.map((f) => f.id) }; } @@ -543,8 +611,8 @@ async function together(batch, a, b, lane) { * compiling while the run fails with a row naming no page. */ export async function runBatch(batch, lane) { - let result = await lane.build(batch); - if (result.crashed) return isolateCrash(batch, result.named, lane); + let result = await buildTwiceOnFailure(batch, lane); + if (result.crashed) return isolateCrash(batch, result.named, lane, !!result.buildFailed, result.detail); // The canary is needed only by a read that holds no error: that is the read // an IDE that published nothing returns, and it looks exactly like a clean @@ -558,8 +626,8 @@ export async function runBatch(batch, lane) { ` the canary did not report in ${batch.fences.length} sample(s) [${batch.project}] ` + `(${result.canaryProblem}), and nothing else did: building it again`, ); - result = await lane.build(batch); - if (result.crashed) return isolateCrash(batch, result.named, lane); + result = await buildTwiceOnFailure(batch, lane); + if (result.crashed) return isolateCrash(batch, result.named, lane, !!result.buildFailed, result.detail); if (result.canaryProblem && !(await heard(result, batch.project, lane))) { // Twice silent: either the IDE keeps reading early under this load, or a // sample in the batch hides the diagnostics of the rest. Halving finds @@ -868,6 +936,13 @@ export async function runProbes(say) { // definitions its tests need, and a page's hidden context left in the other // half takes away the declarations it exists to supply -- and the hidden // fences sit at the END of batch.fences, where a plain slice drops them. + // A unit `alone` picks is a project of its own, marked to leave out the + // template's Main; the rest batch as before. + const solo = makeBatches([fake("s1"), fake("s2"), fake("s3")], { alone: (m) => m.some((f) => f.id === "s2") }); + const soloShape = solo.map((b) => `${b.fences.map((f) => f.id)}${b.noMain ? "!" : ""}`).join(" "); + if (soloShape !== "s2! s1,s3" && soloShape !== "s1,s3 s2!") { + failures.push(`batching: a unit alone picks -> ${soloShape}, want s2 alone and marked`); + } const soleGroup = makeBatches([fake("g1", "g"), fake("g2", "g")])[0]; if (splitBatch(soleGroup) !== null) failures.push("split: a projname group was cut in half"); const mixed = makeBatches([ @@ -935,10 +1010,19 @@ export async function runProbes(say) { lane.builds++; const c = crash(new Set(b.fences.map((f) => f.id))); return c - ? { crashed: true, named: new Set(c.named ?? []) } + ? { + crashed: true, + buildFailed: !!c.failed, + detail: c.failed ? "[BUILD] FAILED boom" : undefined, + named: new Set(c.named ?? []), + } : { perFence: new Map(), unreadable: [], unattributed: [] }; }, - finding: (fence) => lane.found.push(fence.id), + said: [], + finding: (fence, message, detail) => { + lane.found.push(fence.id); + lane.said.push([message, detail]); + }, note: () => {}, }; return lane; @@ -967,6 +1051,48 @@ export async function runProbes(say) { const cheap = fakeLane((ids) => when("c")(ids) && naming("c")(ids)); await runBatch(five, cheap); if (cheap.builds !== 3) failures.push(`isolation: a named crash took ${cheap.builds} builds, want 3`); + // A build that fails after a clean compile names no sample, so it is halved to + // the one that fails it, and reported in the build's words -- the LLVM build + // under --llvm, the build without it -- never as a compiler crash. + const failsBuild = (...need) => fakeLane((ids) => when(...need)(ids) && { failed: true }); + const lone = failsBuild("d"); + lone.llvm = true; + const loneResult = await runBatch(five, lone); + const loneSaid = lone.said[0] ?? []; + if (`${loneResult.crashed} / ${lone.found}` !== "d / d") { + failures.push(`failed build: a sample that fails alone -> ${loneResult.crashed} / ${lone.found}, want d / d`); + } + if (loneSaid[0] !== "fails the build" || !loneSaid[1]?.includes("the LLVM build fails on this sample")) { + failures.push(`failed build: an --llvm finding was worded ${JSON.stringify(loneSaid)}`); + } + if (!loneSaid[1]?.includes("([BUILD] FAILED boom)")) { + failures.push("failed build: the finding does not quote the build's failing line"); + } + if (!loneSaid[1]?.includes("BUGS-TO-REPORT.md if it is the compiler's fault")) { + failures.push("failed build: the finding does not say when to record it"); + } + const control = failsBuild("d"); + await runBatch(five, control); + if (!control.said[0]?.[1]?.startsWith("the build fails on this sample")) { + failures.push(`failed build: a plain build's finding was worded ${JSON.stringify(control.said[0])}`); + } + // A failure that does not repeat is no finding: the batch is built again. + let calls = 0; + const flaky = fakeLane(() => ++calls === 1 && { failed: true }); + const flakyResult = await runBatch(five, flaky); + if (flaky.found.length || flakyResult.crashed.length || flaky.builds !== 2) { + failures.push( + `failed build: one that passed when built again -> ${flaky.found} / ${flakyResult.crashed} in ${flaky.builds} builds`, + ); + } + const pair = failsBuild("b", "d"); + const pairResult = await runBatch(five, pair); + if ( + `${[...pairResult.blamed].sort()}` !== "b,d" || + !pair.said[0]?.[0].startsWith("fails the build when built with 1 other sample(s)") + ) { + failures.push(`failed build: two samples that fail only together -> ${JSON.stringify(pair.said[0])}`); + } // The canaries: what a batch's rows say about them, and what runBatch does when // they are not there. A batch with no rows at all is the one that matters, since // it is what an IDE that read its diagnostics early returns. @@ -1207,6 +1333,241 @@ export async function runProbes(say) { failures.push("report: a diagnostic naming nothing produced a name anyway"); } + // `check_run`: which fences may be run, what a fence says it prints, the + // dispatcher that calls each run sample, and the reading of what the run wrote. + let runProbeCount = 0; + const expect = (ok, what) => { + runProbeCount++; + if (!ok) failures.push(what); + }; + const rfence = (content, o = {}) => ({ + rel: "R.md", + line: 10, + id: o.id ?? "R.md#1", + content, + slot: o.slot ?? "sub", + project: "console", + keys: new Map(o.keys ?? []), + flags: new Set(o.flags ?? [MARKER, RUN_MARKER]), + inferred: { names: [] }, + }); + // Refused: a fence that cannot be run, whatever it prints. The two that must + // NOT be refused name MsgBox in a comment and in a string, which are not calls. + for (const [name, f, refused] of [ + ["a class", rfence("Debug.Print 1\n", { slot: "class" }), true], + ["a module", rfence("Public Sub S()\nEnd Sub\n", { slot: "module" }), true], + ["a MsgBox", rfence('MsgBox "hi"\n'), true], + ["a MsgBox with a result", rfence('Dim r As Long\nr = msgbox("hi", 4)\n'), true], + ["an InputBox", rfence('Dim s As String\ns = InputBox("name")\n'), true], + ["an End", rfence("Debug.Print 1\nEnd\n"), true], + ["an End with a comment", rfence("Debug.Print 1\n End ' stop\n"), true], + ["an End after Then", rfence("If x Then End\n"), true], + ["an expect-error", rfence("Debug.Print 1\n", { keys: [["expect-error", ""]] }), true], + ["a hidden fence", rfence("Dim x As Long\n", { flags: [MARKER, RUN_MARKER, HIDDEN_MARKER] }), true], + ["a MsgBox in a comment", rfence("' MsgBox is not called\nDebug.Print 1 ' not MsgBox\n"), false], + ["a MsgBox in a string", rfence('Debug.Print "MsgBox"\nDebug.Print "End"\n'), false], + [ + "End If and End Select", + rfence("If True Then\n Debug.Print 1\nEnd If\nSelect Case 1\nCase 1\nEnd Select\n"), + false, + ], + ["a plain statement sample", rfence('Debug.Print InStr(1, "abc", "b") \' 2\n'), false], + ["a fence that is not run", rfence('MsgBox "hi"\n', { flags: [MARKER] }), false], + ]) { + expect( + !!runRefusal(f) === refused, + `run refusal: ${name} -> ${runRefusal(f)?.message ?? "accepted"}, want ${refused ? "refused" : "accepted"}`, + ); + } + expect( + runRefusal(rfence("Debug.Print 1\n", { slot: "class" }))?.message === + "check_run runs statement samples (slot=sub) only", + "run refusal: a non-sub slot does not say what check_run runs", + ); + + // What a fence says it prints, each line with its place on the page: the + // fence opens at page line 10, so its first line is page line 11. + const said = (content) => expectedOutput(rfence(content)); + const shape = (e) => `${e.stated}:${e.lines.map((l) => `${l.line}=${l.text}`).join("|")}`; + for (const [name, content, want] of [ + ["a trailing comment", 'Debug.Print InStr(1, "abc", "b") \' 2\n', "true:11=2"], + ["two of three lines", "Debug.Print 1 ' one\nDebug.Print 2\nDebug.Print 3 ' three \n", "true:11=one|13=three"], + ["a quote inside a string", "Debug.Print \"it's\" ' 5\n", "true:11=5"], + ["a quote inside a string, no comment", 'Debug.Print "it\'s"\nDebug.Print """\'"""\n', "false:"], + ["an escaped quote then a comment", 'Debug.Print "say ""hi\'""" \' ok\n', "true:11=ok"], + ["a comment that is not on a Debug.Print", "x = 1 ' not output\n' also not\nDebug.Print x\n", "false:"], + ["an Output block", "Debug.Print 1\nDebug.Print 2\n' Output:\n' 1\n' 2\n", "true:14=1|15= 2"], + ["an Output block, any case and indent", " ' output:\n '1\n '\n", "true:12=1|13="], + ["an Output block ends at a line that is not a comment", "' Output:\n' a\n\n' not this\n", "true:12=a"], + ["an Output block beats trailing comments", "Debug.Print 1 ' no\n' Output:\n' yes\n", "true:13=yes"], + ["an empty Output block", "Debug.Print 1\n' Output:\n", "true:"], + ["no expectations", "Dim x As Long\nx = 1\nDebug.Print x\n", "false:"], + ["a comment that says Output but is not a header", "' Output: 5\nDebug.Print 5\n", "false:"], + ]) { + const got = shape(said(content)); + expect(got === want, `expected output: ${name} -> ${got}, want ${want}`); + } + // ...and compared with what was printed, trimmed at both ends. + const sayOf = (content, output) => judgeOutput(said(content), output).map((p) => `${p.line}:${p.message}`); + for (const [name, content, output, want] of [ + ["a match, sign space ignored", "Debug.Print 2 ' 2\n", [" 2"], ""], + [ + "a mismatch at the page line", + "Debug.Print 1 ' 1\nDebug.Print 2 ' 3\n", + [" 1", " 2"], + '12:prints "2", the page says "3"', + ], + ["too few lines", "Debug.Print 1 ' 1\nDebug.Print 2 ' 2\n", [" 1"], "null:prints 1 line(s), the page states 2"], + ["too many lines", "Debug.Print 1 ' 1\n", [" 1", " 2"], "null:prints 2 line(s), the page states 1"], + ["nothing stated, anything printed", "Debug.Print 1\n", [" 1", " 2"], ""], + ["an Output block that is not met", "Debug.Print 7\n' Output:\n' 8\n", [" 7"], '13:prints "7", the page says "8"'], + ]) { + const got = sayOf(content, output).join(";"); + expect(got === want, `judged output: ${name} -> ${got}, want ${want}`); + } + + // The dispatcher: one block per run sample, numbered in order, calling the + // generated module's body between the markers. + const dispatch = dispatcherText(["tbx_aaa", "tbx_bbb"]); + const dLines = dispatch.split("\n").map((l) => l.trim()); + expect( + dLines[0] === "Module tbxRun" && dLines[1] === "[RunAfterBuild]" && dLines[3] === "Debug.Cls", + "dispatcher: the header is not a RunAfterBuild Sub that starts with Debug.Cls", + ); + expect( + ["begin 0", "begin 1", "end 0", "end 1"].every((m) => dLines.includes(`Debug.Print "[tbx-run] ${m}"`)) && + dLines.includes("tbx_aaa.tbxBody") && + dLines.includes("tbx_bbb.tbxBody") && + dLines.indexOf("tbx_aaa.tbxBody") < dLines.indexOf("tbx_bbb.tbxBody"), + "dispatcher: two samples are not called in order between their markers", + ); + expect( + [0, 1].every( + (i) => + dLines.indexOf(`On Error GoTo tbxError${i}`) < dLines.indexOf(`tbx_${i ? "bbb" : "aaa"}.tbxBody`) && + dLines.indexOf(`On Error GoTo tbxError${i}`) >= 0 && + dLines.includes(`tbxError${i}:`) && + dLines.includes(`Resume tbxNext${i}`) && + dLines.includes(`tbxNext${i}:`), + ) && + !dLines.includes("On Error Resume Next") && + dLines.at(-4) === `Debug.Print "${RUN_DONE}"`, + "dispatcher: each call does not have a handler of its own (On Error GoTo, label, Resume), or the done line is missing", + ); + // The markers are what parseRun reads, so the dispatcher's own text is run + // through it: what it prints for a sample that returns is a match. + const marker = (s) => `[tbx-run] ${s}`; + const run3 = (lines) => parseRun(lines, 3); + const full = run3([ + marker("begin 0"), + " 2", + marker("end 0"), + marker("begin 1"), + "a", + "b", + marker("error 5 Invalid procedure call or argument"), + marker("end 1"), + marker("begin 2"), + marker("end 2"), + marker("done"), + ]); + expect( + full.done && full.began === 3 && full.items[0].output.join() === " 2" && full.items[0].error === null, + "run output: a sample that returned was not read", + ); + expect( + full.items[1].output.join() === "a,b" && + full.items[1].error?.number === 5 && + full.items[1].error.description === "Invalid procedure call or argument" && + full.items[1].ended, + "run output: an error line was not attributed to the sample it follows", + ); + expect(full.items[2].began && full.items[2].ended && !full.items[2].output.length, "run output: a silent sample"); + const hung = run3([marker("begin 0"), "x", marker("end 0"), marker("begin 1"), "y"]); + expect( + !hung.done && + hung.items[1].began && + !hung.items[1].ended && + !hung.items[2].began && + hung.items[1].output.join() === "y", + "run output: a sample that began and did not return was not found, or the one after it was said to have begun", + ); + const none = run3(["[BUILD] Starting...", "[LINKER] SUCCESS"]); + expect(!none.done && none.began === 0, "run output: a console with no marker said the run began"); + expect( + run3([marker("begin 0"), marker("end 0"), "[DEBUGGER] Waiting for remaining forms to close..."]).items[0].output + .length === 0, + "run output: a line after a sample's end was attributed to it", + ); + + // A run fence's body is Public, to be called from the dispatcher, and that + // changes no line: the line map is as for a Private one. + const runFence = { ...fence, flags: new Set([MARKER, RUN_MARKER]) }; + for (const base of [null, "Form"]) { + const { text, offset } = wrapFence(runFence, "sub", "tbx_probe", base); + const genLine = text.split("\n").findIndex((l) => l.trim() === "Dim b") + 1; + expect( + text.includes(" Public Sub tbxBody()") && fence.line + genLine - offset === 12, + `line map: a run fence's body is not Public with the same offset (${fence.line + genLine - offset})`, + ); + } + expect( + wrapFence({ ...fence, flags: new Set([MARKER]) }, "sub", "tbx_probe").text.includes(" Private Sub tbxBody()"), + "wrapper: a sample that is not run lost its Private body", + ); + + // Batching: a run fence is built apart from every other sample, with its + // page's hidden context; a projname group holding one goes whole to the run + // batches, its class beside it, and only the run fence is called. + const sample = (id, o = {}) => ({ + ...rfence("Debug.Print 1\n", { + id, + flags: o.run ? [MARKER, RUN_MARKER] : [MARKER], + keys: o.group ? [["projname", o.group]] : [], + slot: o.slot, + }), + rel: o.rel ?? "R.md", + ...(o.hidden ? { flags: new Set([HIDDEN_MARKER, MARKER]), slot: "module" } : {}), + }); + const plainOne = sample("plain"), + runner = sample("runner", { run: true }), + ctx = sample("ctx", { hidden: true }), + cls = sample("cls", { group: "g", slot: "class" }), + grunner = sample("grunner", { group: "g", run: true }), + otherPage = sample("other", { rel: "O.md" }); + const part = partitionRun([plainOne, runner, ctx, cls, grunner, otherPage]); + const ids = (list) => list.map((f) => f.id).join(); + expect(ids(part.rest) === "plain,ctx,other", `run batching: the rest is ${ids(part.rest)}, want plain,ctx,other`); + expect( + ids(part.run) === "runner,ctx,cls,grunner", + `run batching: the run list is ${ids(part.run)}, want runner,ctx,cls,grunner`, + ); + const runBatches = makeBatches(part.run); + const groupRun = runBatches.find((b) => b.fences.some((f) => f.id === "cls")); + expect( + runBatches.length === 2 && + groupRun?.group === "g" && + groupRun.fences.some((f) => f.id === "grunner") && + !groupRun.fences.some((f) => f.id === "runner"), + "run batching: a group of a class and a run sample was not one run batch of its own", + ); + expect( + ids(groupRun?.fences.filter(isRunFence) ?? []) === "grunner", + "run batching: the dispatcher would call the group's class", + ); + expect( + !runBatches.some((b) => b.fences.some((f) => f.id === "plain" || f.id === "other")), + "run batching: a sample that is not run was built into a run batch", + ); + expect( + runBatches.find((b) => b.fences.some((f) => f.id === "runner"))?.fences.some((f) => f.id === "ctx"), + "run batching: a page's hidden context did not travel with its run sample", + ); + expect( + partitionRun([plainOne, otherPage]).run.length === 0, + "run batching: a selection with no run sample made run batches", + ); + // The markup must be invisible to the site. Verified against the REAL // pipeline -- createMarkdownIt plus the highlighter -- because a bare // markdown-it is a different renderer, which is the mistake WIP.md's @@ -1236,15 +1597,17 @@ export async function runProbes(say) { for (const f of failures) say(`FAIL probe: ${f}`); return false; } - // 10 line-map (5 slots x 2 bases) + 4 wrapper container + 7 batching - // + 5 splitting + 5 taking out + 3 crash report + 7 isolation + 9 concat + // 10 line-map (5 slots x 2 bases) + 4 wrapper container + 8 batching + // (1 of a unit built alone) + 5 splitting + 5 taking out + 3 crash report + 7 isolation + 9 concat // + 10 resource + 8 report + 6 markup + 15 canaries (5 of what a batch's rows say, // 7 of what runBatch does about a batch whose canary did not report, 3 of the - // template built with no samples). + // template built with no samples) + 7 of a failed build (wording, when to + // record it, one that passes when built again, a pair that fails only together) + // + the check_run probes, which count themselves. say( - `ok ${CLASSIFIER_PROBES.length + INFO_PROBES.length + 89} probes: ` + - "classifier, markup, line mapping, batching, splitting, crash isolation, canaries, concat, " + - "resources and the report", + `ok ${CLASSIFIER_PROBES.length + INFO_PROBES.length + 97 + runProbeCount} probes: ` + + "classifier, markup, line mapping, batching, splitting, crash isolation, failed builds, canaries, concat, " + + "resources, the report and check_run", ); return true; } diff --git a/scripts/lib/example-run.mjs b/scripts/lib/example-run.mjs new file mode 100644 index 00000000..dd4cbb3b --- /dev/null +++ b/scripts/lib/example-run.mjs @@ -0,0 +1,273 @@ +// What check_examples.mjs needs to RUN a sample, and none of it needs an IDE: +// which fences may be run, what a fence says it prints, the generated +// dispatcher that calls each one, and the reading of what the run wrote. +// +// A fence marked `check_run` is a statement sample (slot=sub). It is built into a +// project with a generated `[RunAfterBuild]` Sub that calls each run sample's +// body in turn and prints a marker line around the call (`dispatcherText`), +// because `[RunAfterBuild]` is one per project (TB5114). The lines the run +// writes are split by those markers (`parseRun`) and each sample's share is +// compared with what its own text says it prints (`expectedOutput`, +// `judgeOutput`). +// +// The probes for all of it are in example-batches.mjs's `runProbes`. + +import { HIDDEN_MARKER, RUN_MARKER, partOf } from "./tb-fences.mjs"; +import { logicalLines } from "./twin-api.mjs"; + +/** The generated module, Sub and file that call every run sample. */ +export const RUN_MODULE = "tbxRun"; +export const RUN_SUB = "tbxRunAll"; +export const RUN_FILE = "tbxRun.twin"; + +/** What a marker line starts with, and the line that says the dispatcher finished. */ +export const RUN_TAG = "[tbx-run]"; +export const RUN_DONE = `${RUN_TAG} done`; + +/** A fence that is run: not context, not a resource file. */ +export const isRunFence = (f) => !!f.flags?.has(RUN_MARKER) && !f.flags.has(HIDDEN_MARKER) && !f.isResource; + +// ------------------------------------------------------------------ selection + +// A bare `End` statement ends the whole run, on its own line or after `Then`, +// `Else` or a colon. Read from logicalLines, whose comments and string contents +// are already gone, so `' End` and `"End"` are not matches. +const ENDS_RUN = /(?:^|:|\bThen|\bElse)\s*End$/i; +const PROMPTS = /\b(MsgBox|InputBox)\b/i; + +/** + * Why a run fence's own text cannot be run, or null. + * + * `MsgBox` and `InputBox` wait for a click on a private desktop nobody can see, + * so the run would hang to its timeout; `End` ends the run, and every sample + * after it. Comments and string literals are not code and are not read. + */ +export function runFenceProblem(content) { + for (const { text } of logicalLines(content)) { + const code = text.trim(); + const prompt = PROMPTS.exec(code); + if (prompt) { + return `calls ${prompt[1]}, which waits for a click on a desktop nobody sees, so the run would hang`; + } + if (ENDS_RUN.test(code)) return "contains an `End` statement, which ends the run and every sample after it"; + } + return null; +} + +/** + * Whether a selected fence may be run, as a finding's message and detail, or + * null when it may (or does not ask to). The fence has its slot already. + */ +export function runRefusal(fence) { + if (!fence.flags?.has(RUN_MARKER)) return null; + if (fence.flags.has(HIDDEN_MARKER)) { + return { + message: `\`${HIDDEN_MARKER}\` and \`${RUN_MARKER}\` contradict each other`, + detail: "context is compiled and never run", + }; + } + if (fence.keys?.has("expect-error")) { + return { + message: `\`${RUN_MARKER}\` cannot be combined with expect-error`, + detail: "a sample cannot both fail to compile and run", + }; + } + if (fence.slot !== "sub") { + return { + message: `${RUN_MARKER} runs statement samples (slot=sub) only`, + detail: `this sample is slot=${fence.slot}${fence.base ? ` (inherits=${fence.base})` : ""}`, + }; + } + const problem = runFenceProblem(fence.content); + return problem ? { message: `a \`${RUN_MARKER}\` sample ${problem}` } : null; +} + +/** + * Split the selection into what is built and run, and the rest. + * + * A run fence is a unit. A `projname` group holding one is run as a whole: every + * visible member is staged in the run project, so the members that define what + * the run fence uses are there, and only the run fences get a dispatcher call. + * Hidden fences and resource files travel with their page, so the pages of the + * run units' samples bring theirs to both lists. + * + * @returns {{run: object[], rest: object[]}} + */ +export function partitionRun(selected) { + const groups = new Set( + selected + .filter(isRunFence) + .map((f) => f.keys?.get("projname")) + .filter(Boolean), + ); + const travels = (f) => f.isResource || f.flags?.has(HIDDEN_MARKER); + const inUnit = (f) => !travels(f) && (isRunFence(f) || groups.has(f.keys?.get("projname"))); + const pages = new Set(selected.filter(inUnit).map((f) => f.rel)); + return { + run: selected.filter((f) => inUnit(f) || (travels(f) && pages.has(f.rel))), + rest: selected.filter((f) => !inUnit(f)), + }; +} + +// --------------------------------------------------------------------- output + +/** The `'` that starts a comment: the first one outside a string literal (`""` escapes). */ +function splitComment(line) { + let inString = false; + for (let k = 0; k < line.length; k++) { + const c = line[k]; + if (c === '"') inString = !inString; + else if (c === "'" && !inString) return { code: line.slice(0, k), comment: line.slice(k + 1) }; + } + return { code: line, comment: null }; +} + +/** + * What a fence says it prints. + * + * Either a `' Output:` comment line, whose following comment lines (up to the + * first line that is not one) are the whole output, or a trailing comment on a + * `Debug.Print` line, each of which states one line. A `Debug.Print` with no + * trailing comment states nothing. When anything is stated, the output has + * exactly that many lines. + * + * @returns {{stated: boolean, lines: {text: string, line: number}[]}} + * `line` is where the expected line is on the page + */ +export function expectedOutput(fence) { + const rows = fence.content + .replace(/\n+$/, "") + .split("\n") + .map((l) => l.replace(/\r$/, "")); + const pageLine = (i) => (fence.concatParts ? partOf(fence.concatParts, i + 1)?.pageLine : null) ?? fence.line + 1 + i; + + const header = rows.findIndex((r) => /^\s*'\s*Output:\s*$/i.test(r)); + if (header >= 0) { + const lines = []; + for (let i = header + 1; i < rows.length && /^\s*'/.test(rows[i]); i++) { + lines.push({ text: rows[i].replace(/^\s*' ?/, ""), line: pageLine(i) }); + } + return { stated: true, lines }; + } + + const lines = []; + rows.forEach((row, i) => { + const { code, comment } = splitComment(row); + if (comment === null || !/^\s*Debug\s*\.\s*Print\b/i.test(code)) return; + if (comment.trim()) lines.push({ text: comment.trim(), line: pageLine(i) }); + }); + return { stated: lines.length > 0, lines }; +} + +/** + * Compare what a sample printed with what it states, each line trimmed at both + * ends (twinBASIC prints a number's sign space). + * + * @param {{stated: boolean, lines: {text: string, line: number}[]}} expected + * @param {string[]} output the lines the sample printed + * @returns {{line: number | null, message: string, detail?: string}[]} + * one entry per mismatch; `line` is the expectation's page line, or null for + * a problem with the fence as a whole + */ +export function judgeOutput(expected, output) { + if (!expected.stated) return []; + const got = output.map((l) => l.trim()); + const want = expected.lines.map((l) => l.text.trim()); + if (got.length !== want.length) { + return [ + { + line: null, + message: `prints ${got.length} line(s), the page states ${want.length}`, + detail: `prints ${JSON.stringify(got)}, the page states ${JSON.stringify(want)}`, + }, + ]; + } + const problems = []; + got.forEach((text, i) => { + if (text !== want[i]) { + problems.push({ + line: expected.lines[i].line, + message: `prints ${JSON.stringify(text)}, the page says ${JSON.stringify(want[i])}`, + }); + } + }); + return problems; +} + +// ----------------------------------------------------------------- dispatcher + +/** + * The generated `[RunAfterBuild]` Sub, calling each module's `tbxBody` in turn. + * + * Each call has an `On Error GoTo` handler of its own, so one sample's error + * does not end the rest, and the error line is the way the run says which one + * raised it. The handler is reached only by an error the sample did not handle. + * Testing `Err.Number` after the call would not do: a procedure that handles an + * error with `On Error Resume Next` returns with `Err` still set, in VB6 and in + * twinBASIC alike (measured, BETA 995), so a sample that demonstrates an error + * would be reported as raising it. The first statement is `Debug.Cls`, which + * erases the IDE's own build log from the console, so what is left is the + * dispatcher's. + * + * @param {string[]} modules the generated module names, in the order the + * samples are numbered + */ +export function dispatcherText(modules) { + const out = [`Module ${RUN_MODULE}`, " [RunAfterBuild]", ` Public Sub ${RUN_SUB}()`, " Debug.Cls"]; + modules.forEach((module, i) => { + out.push( + ` Debug.Print "${RUN_TAG} begin ${i}"`, + " Err.Clear", + ` On Error GoTo tbxError${i}`, + ` ${module}.tbxBody`, + ` GoTo tbxNext${i}`, + `tbxError${i}:`, + ` Debug.Print "${RUN_TAG} error " & Err.Number & " " & Err.Description`, + ` Resume tbxNext${i}`, + `tbxNext${i}:`, + " On Error GoTo 0", + ` Debug.Print "${RUN_TAG} end ${i}"`, + ); + }); + out.push(` Debug.Print "${RUN_DONE}"`, " End Sub", "End Module", ""); + return out.join("\n"); +} + +const MARKER_RE = /^\[tbx-run\] (?:(begin|end) (\d+)|error (-?\d+)(?: (.*))?|(done))$/; + +/** + * Split the lines a run wrote by the dispatcher's markers. + * + * @param {string[]} lines the captured console + * @param {number} count how many samples the dispatcher calls + * @returns {{ + * items: {began: boolean, ended: boolean, output: string[], error: {number: number, description: string} | null}[], + * began: number, done: boolean, + * }} + */ +export function parseRun(lines, count) { + const items = Array.from({ length: count }, () => ({ began: false, ended: false, output: [], error: null })); + let current = -1, + began = 0, + done = false; + for (const raw of lines) { + const m = MARKER_RE.exec(raw.trim()); + if (!m) { + if (current >= 0) items[current].output.push(raw); + continue; + } + if (m[5]) { + done = true; + } else if (m[1] === "begin" && Number(m[2]) < count) { + current = Number(m[2]); + items[current].began = true; + began++; + } else if (m[1] === "end" && Number(m[2]) < count) { + items[Number(m[2])].ended = true; + current = -1; + } else if (m[3] !== undefined && current >= 0) { + items[current].error = { number: Number(m[3]), description: m[4] ?? "" }; + } + } + return { items, began, done }; +} diff --git a/scripts/lib/tb-build.mjs b/scripts/lib/tb-build.mjs index 8b1ce8b6..a8bfc387 100644 --- a/scripts/lib/tb-build.mjs +++ b/scripts/lib/tb-build.mjs @@ -34,12 +34,15 @@ import { COMPILE_TIMEOUT, TARGETS, attachIde, + buildProject, compileOutcome, launchIde, + llvmLicence, setBuildTarget, shutdownIdeAsync, waitForCompile, } from "./tb-ide.mjs"; +import { captureRun, checkCapture } from "./tb-run.mjs"; /** * @param {object} o @@ -50,14 +53,35 @@ import { * @param {number} [o.timeout] ms to wait for the compile (default COMPILE_TIMEOUT) * @param {boolean} [o.show] on the user's desktop instead of a private one * @param {boolean} [o.keep] leave the IDE running, and report its pid + * @param {boolean} [o.build] after a compile with no errors, build the project + * as the toolbar's Build button does. The project's settings decide what is + * built and how: the caller stages them (lib/tb-project.mjs), because a + * project opened in place has the template's build path, whose Save dialog + * a private desktop hides + * @param {boolean} [o.llvm] refuse a licence that does not compile user code + * with LLVM. It checks the licence and changes no setting; the project's + * own `compiler.buildOptions` is what asks for LLVM + * @param {number} [o.buildTimeout] ms to wait for the build (default buildProject's) + * @param {{done?: (lines: string[]) => boolean, quietMs: number, timeoutMs: number} | null} [o.run] + * after a clean compile, build the project and run its [RunAfterBuild] Sub + * and capture what it writes (captureRun in lib/tb-run.mjs), in place of + * `build`'s own wait, which ends before the Sub's output has arrived. The + * Sub is expected to start with Debug.Cls. The capture is returned as `run` * @returns {Promise<{ * code: number, message: string, rows: string[], counts: number[], dialogs: string[], * openedIn: string | null, arch: string, idePid: number | null, kept: boolean, crashFiles: string[], + * built: string | null, buildLog: string[], + * run?: {captured: string[], last: string, erased: string[], timedOut: boolean}, * }>} `code` is tbbuild's exit code: 0 clean, 1 the project has errors, 2 the IDE - * could not be started or attached, 3 the compile never settled, 4 the project - * crashes the compiler. `message` is what tbbuild prints on stderr for 2, 3 - * and 4 and is empty otherwise. `counts` is errors, warnings, hints, infos. - * `crashFiles` names, for 4, the files the compiler died parsing. + * could not be started or attached or the licence refuses LLVM, 3 the compile + * never settled, 4 the project crashes the compiler, 5 the build failed after + * a clean compile (with `run`, also a Sub that failed code generation, or a + * failure a Debug.Cls erased after the run started). `message` is what tbbuild + * prints on stderr for 2 to 5 and is empty otherwise: for 5 the failing line + * of the log. `counts` is errors, warnings, hints, infos. `crashFiles` names, + * for 4, the files the compiler died parsing. `built` is the file a + * successful build wrote, and `buildLog` the console from the build's first + * line on. */ export async function compileProject({ project, @@ -67,6 +91,10 @@ export async function compileProject({ timeout = COMPILE_TIMEOUT, show = false, keep = false, + build = false, + llvm = false, + buildTimeout, + run = null, }) { let handle = null; let c = null; @@ -81,6 +109,8 @@ export async function compileProject({ idePid: handle?.pid ?? null, kept: keep, crashFiles: [], + built: null, + buildLog: [], ...fields, }); try { @@ -110,7 +140,40 @@ export async function compileProject({ } catch (e) { return result(2, { message: e.message }); } - return result(outcome.counts[0] > 0 ? 1 : 0, { rows: outcome.rows, counts: outcome.counts, openedIn }); + const found = { rows: outcome.rows, counts: outcome.counts, openedIn }; + + // The licence is read once the compile has settled, which is when the IDE + // knows it. A project that is to be built with LLVM on a licence that does + // not compile user code with LLVM would build with the default compiler and + // say nothing, so it is refused whatever the compile found. + if (llvm) { + const { refusal } = await llvmLicence(c); + if (refusal) return result(2, { ...found, message: refusal }); + } + if (outcome.counts[0] > 0) return result(1, found); + + // The IDE does not build a project it flags with errors, so a build is asked + // of a clean compile only. A warning does not stop one. + if (!build && !run) return result(0, found); + // A run builds, and reads the console after the project's [RunAfterBuild] + // Sub has run, which buildProject returns too early to see. + if (run) { + let capture; + try { + capture = await captureRun(c, run); + } catch (e) { + return result(2, { ...found, message: e.message }); + } + const { buildFailed, lost } = checkCapture(capture.captured, capture.erased); + const failure = buildFailed ?? lost; + if (failure !== undefined) { + return result(5, { ...found, message: failure, buildLog: capture.captured, run: capture }); + } + return result(0, { ...found, run: capture }); + } + const built = await buildProject(c, { timeout: buildTimeout }); + if (!built.ok) return result(5, { ...found, message: built.message, buildLog: built.log }); + return result(0, { ...found, built: built.file, buildLog: built.log }); } finally { // A close that threw must not skip ending the IDE, or mask what was thrown. try { diff --git a/scripts/lib/tb-fences.mjs b/scripts/lib/tb-fences.mjs index 91c25915..eb5c8178 100644 --- a/scripts/lib/tb-fences.mjs +++ b/scripts/lib/tb-fences.mjs @@ -642,7 +642,8 @@ export function moduleName(id) { * pageLine = fence.line + generatedLine - offset * * Everything generated is Private, because two samples in one project must not - * see each other's names: eleven pages declare a `MyString`. + * see each other's names: eleven pages declare a `MyString`. The one exception + * is the body Sub of a `check_run` sample. */ export function wrapFence(fence, slot, name, base = null) { const header = `' ${fence.rel}:${fence.line} (${fence.id})`; @@ -670,8 +671,12 @@ export function wrapFence(fence, slot, name, base = null) { if (slot === "module" || slot === "class") { return { text: `${header}\n${open}\n${body}\nEnd ${container}\n`, offset: 2 + extra }; } + // A `check_run` sample's body is called from the generated dispatcher, in + // another module, so it is Public. The line count is the same as Private's, + // so the offset does not change. + const access = slot === "sub" && fence.flags?.has(RUN_MARKER) ? "Public" : "Private"; return { - text: `${header}\n${open}\n Private Sub tbxBody()\n${body}\n End Sub\nEnd ${container}\n`, + text: `${header}\n${open}\n ${access} Sub tbxBody()\n${body}\n End Sub\nEnd ${container}\n`, offset: 3 + extra, }; } diff --git a/scripts/lib/tb-ide-console.mjs b/scripts/lib/tb-ide-console.mjs index 69c0a24c..260da112 100644 --- a/scripts/lib/tb-ide-console.mjs +++ b/scripts/lib/tb-ide-console.mjs @@ -35,7 +35,7 @@ // An entry's text is stored escaped: an add-in's PrintText of "<b>©=1" // is stored as "<b>&copy=1" (measured, BETA 983), so decoding gives // back exactly what was printed, markup and ampersands included. Except for -// the IDE's own bug (BUGS-TO-REPORT.md): text that continues a line left open +// the IDE's own bug (twinbasic/twinbasic#2444): text that continues a line left open // by `Debug.Print ...;` is escaped twice, so the console shows "&" for // "&", and so does this reader. It returns what the console shows. // diff --git a/scripts/lib/tb-ide.mjs b/scripts/lib/tb-ide.mjs index e831ada2..f833ea63 100644 --- a/scripts/lib/tb-ide.mjs +++ b/scripts/lib/tb-ide.mjs @@ -9,13 +9,13 @@ // twinBASIC project without the IDE in front of you", for the history. import { execFile, execFileSync, spawn } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { mkdirSync, readFileSync, rmSync } from "node:fs"; import net from "node:net"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { attach } from "./tb-cdp.mjs"; import { click } from "./tb-click.mjs"; -import { consoleMark, linesSince } from "./tb-ide-console.mjs"; +import { consoleMark, keepClears, keptClears, linesSince, readConsole } from "./tb-ide-console.mjs"; export const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); @@ -136,10 +136,23 @@ export async function launchIde({ exe, project, port, show = false, keep = false } const exeWin = exe.split("/").join("\\"); const target = path.resolve(project).split("/").join("\\"); + // Each IDE has a temp folder of its own, emptied at each launch on its port. + // Builds sharing one fail now and then with `[TYPELIB] failed to finalize + // typelibrary. Disk error?`: 0 in 192 with a folder per IDE, 8 in 192 with + // one for all eight at once (BETA 995). + const ideTemp = path.join(process.env.TEMP, `tbbuild-tmp-${port}`); + try { + rmSync(ideTemp, { recursive: true, force: true }); + } catch { + // a file still held open: the IDE gets the folder as it is + } + mkdirSync(ideTemp, { recursive: true }); // Node leaves out of a child's environment any variable whose value is // undefined, so `env` can remove one as well as set it. const fullEnv = { ...process.env, + TEMP: ideTemp, + TMP: ideTemp, WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: `--remote-debugging-port=${port} --remote-allow-origins=*`, WEBVIEW2_USER_DATA_FOLDER: `${process.env.TEMP}/tbbuild-wv2-${port}`, [ADDIN_TEST_ENV]: "1", @@ -180,6 +193,40 @@ export async function launchIde({ exe, project, port, show = false, keep = false // anything could attach to it, because the launcher died with tbbuild and // took the job with it. With the launcher detached from Node instead, // PowerShell exited at once, without a pid and without a word on stderr. + try { + const { pid, launcher } = await launchOnDesktop({ + exe: exeWin, + arg: target, + desktop: `tbbuild-${port}`, + job: !keep, + env: fullEnv, + }); + return { pid, launcher }; + } catch (e) { + throw new Error( + `could not start the IDE on a private desktop:\n${e.message}\n(--show runs it on your own desktop instead)`, + ); + } +} + +/** + * Start a program on a private desktop, inside a kill-on-close job, through + * lib/tb-launch.ps1. launchIde starts the IDE this way, and tbrun's --exe the + * probe's exe, so that neither can show a window on the user's desktop. + * + * @param {object} o + * @param {string} o.exe the program, a Windows path + * @param {string} [o.arg] its one argument; none when empty + * @param {string} o.desktop the private desktop's name + * @param {boolean} [o.job] false for no job, for a program that is to outlive + * this Node process + * @param {object} o.env its whole environment + * @returns {Promise<{pid: number, launcher: import("node:child_process").ChildProcess, exited: Promise<number | null>}>} + * `exited` settles with the program's exit code when it ends, or null when + * the launcher ended without one -- killed, or failed. Throws, with the + * launcher's error, when the program did not start. + */ +export async function launchOnDesktop({ exe, arg = "", desktop, job = true, env }) { const script = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "tb-launch.ps1"), "utf8"); const ps = spawn( "powershell", @@ -187,34 +234,46 @@ export async function launchIde({ exe, project, port, show = false, keep = false { stdio: ["ignore", "pipe", "pipe"], windowsHide: true, - env: { - ...fullEnv, - TBBUILD_EXE: exeWin, - TBBUILD_ARG: target, - TBBUILD_DESKTOP: `tbbuild-${port}`, - TBBUILD_JOB: keep ? "0" : "1", - }, + env: { ...env, TBBUILD_EXE: exe, TBBUILD_ARG: arg, TBBUILD_DESKTOP: desktop, TBBUILD_JOB: job ? "1" : "0" }, }, ); - let err = ""; + let err = "", + out = ""; ps.stderr.on("data", (d) => { err += d; }); + ps.stdout.on("data", (d) => { + out += d; + }); + // "close" rather than "exit": the exit line can still be in the pipe at "exit". + const ended = new Promise((res) => ps.on("close", res)); const pid = await new Promise((res) => { - let buf = ""; - ps.stdout.on("data", (d) => { - buf += d; - const m = /^\s*(\d+)\s*$/m.exec(buf); + ps.stdout.on("data", () => { + const m = /^\s*(\d+)\s*$/m.exec(out); if (m) res(Number(m[1])); }); - ps.on("exit", () => res(null)); + ended.then(() => res(null)); }); - if (!pid) { - throw new Error( - `could not start the IDE on a private desktop:\n${err.trim()}\n` + "(--show runs it on your own desktop instead)", - ); - } - return { pid, launcher: ps }; + if (!pid) throw new Error(err.trim()); + const exited = ended.then(() => { + const m = /^exit (-?\d+)\s*$/m.exec(out); + return m ? Number(m[1]) : null; + }); + return { pid, launcher: ps, exited }; +} + +/** + * What a tool prints under --keep: the kept IDE's pid, and the commands that end + * it with every process it started (/T takes the compiler and the rest of its + * tree), in each shell's spelling; Git Bash would read /PID as a path. + */ +export function keptIdeLines(pid) { + return [ + `ide-pid: ${pid}`, + "to end the IDE and every process it started:", + ` cmd or PowerShell: taskkill /PID ${pid} /T /F`, + ` Git Bash: taskkill //PID ${pid} //T //F`, + ]; } /** @@ -774,7 +833,68 @@ export async function awaitNewCompiler(c, before, { why = "restarting it", timeo // ...", "[BUILD] failed" and "[LINKER] compilation (codegen) error ...". const BUILD_START = "[BUILD] Starting..."; const BUILD_OK = /^\[LINKER\] SUCCESS created output file '(.+)'$/; -export const BUILD_FAILED = /^\[(?:LINKER|BUILD)\] (?:FAILED|ERROR|failed)\b|^\[LINKER\] compilation \(codegen\) error/; +// The last is LLVM's refusal of a procedure, which has no bracketed prefix: +// "LLVM compilation error in 'Probe.Stopper': Unable to compile due to use of +// datatype that is not yet supported for LLVM compilation" (BETA 983). +export const BUILD_FAILED = + /^\[(?:LINKER|BUILD)\] (?:FAILED|ERROR|failed)\b|^\[LINKER\] compilation \(codegen\) error|^LLVM compilation error in /; +// A compiler that crashes while building -- measured under LLVM, BETA 995 -- +// writes "<n> <time> NATIVE EXCEPTION: ..." and then restarts, and the build +// never reports again. The line carries the console's own prefix, so it is not +// anchored. +const BUILD_CRASHED = /\bNATIVE EXCEPTION:/; + +/** + * Whether the IDE's licence lets a project compile user code with LLVM. + * + * A Community licence ignores the LLVM settings and a Personal one applies them + * only to the built-in packages (docs/LLVM/Getting-Started.md), so on either an + * LLVM run measures the default compiler and says nothing. The status bar's + * compilerLicence holds one of four "<NAME> EDITION" strings once the IDE knows + * (ide/main.js, BETA 995), and "tB Licence: ..." until then, so it is polled. + * + * @param {object} c a tb-cdp connection + * @param {object} [o] + * @param {number} [o.timeout] milliseconds to wait for the licence (default 15000) + * @returns {Promise<{licence: string | null, refusal: string | null}>} `refusal` + * is null when LLVM may be used, and otherwise the sentence saying why not + */ +export async function llvmLicence(c, { timeout = 15 * 1000 } = {}) { + let licence = null; + const until = Date.now() + timeout; + while (Date.now() < until) { + licence = await c + .evaluate("typeof compilerLicence === 'undefined' || !compilerLicence ? null : compilerLicence.innerText") + .catch(() => null); + if (/ EDITION$/.test(licence ?? "")) break; + licence = null; + await sleep(250); + } + if (!licence) + return { licence, refusal: "the IDE never showed its licence, so an LLVM run could not be checked for one." }; + if (/^(?:COMMUNITY|PERSONAL) /.test(licence)) { + return { + licence, + refusal: + `this IDE has a ${licence.toLowerCase()} licence, which does not compile user code with LLVM, ` + + "so the run would measure the default compiler. An LLVM run needs a Professional or Ultimate licence.", + }; + } + return { licence, refusal: null }; +} + +/** + * The console's lines since `mark`, with what any clear since then erased put + * back in front: the first clear's record from the mark's entry count on, each + * later record whole, then the console as it is. `clearsBefore` is how many + * clears were on record at the mark, or null when no record is kept. + */ +async function buildLines(c, mark, clearsBefore) { + const kept = clearsBefore === null ? [] : ((await keptClears(c)) ?? []).slice(clearsBefore); + if (!kept.length) return linesSince(c, mark); + const split = (text) => text.split("\n").map((l) => l.trim()); + return [...split(kept[0]).slice(mark.n), ...kept.slice(1).flatMap(split), ...split((await readConsole(c)) ?? "")]; +} /** * Build the open project, as the toolbar's Build button does, and wait for the @@ -800,6 +920,11 @@ export async function buildProject(c, { timeout = 120 * 1000 } = {}) { message: "no debugConsoleContent.dataNodes in this IDE, " + "so the build log cannot be read", }; } + // A [RunAfterBuild] procedure runs during the build, and its Debug.Cls would + // erase the log being read; what each clear erases is kept, and the clears + // already on record before this build are not this build's. + const keeping = await keepClears(c).catch(() => false); + const clearsBefore = keeping ? ((await keptClears(c)) ?? []).length : 0; try { await click(c, "buildIcon"); } catch (e) { @@ -810,7 +935,7 @@ export async function buildProject(c, { timeout = 120 * 1000 } = {}) { failedAt = 0; while (Date.now() - t0 < timeout) { await sleep(250); - const lines = await linesSince(c, mark); + const lines = await buildLines(c, mark, keeping ? clearsBefore : null); const start = lines.indexOf(BUILD_START); if (start < 0) continue; log = lines.slice(start); @@ -820,9 +945,10 @@ export async function buildProject(c, { timeout = 120 * 1000 } = {}) { // failed to use project.iconForm setting", and whether a build goes on // after that one has not been seen -- so one decides only after two // seconds with no success line after it. - if (!failedAt && log.some((l) => BUILD_FAILED.test(l))) failedAt = Date.now(); + const failed = (l) => BUILD_FAILED.test(l) || BUILD_CRASHED.test(l); + if (!failedAt && log.some(failed)) failedAt = Date.now(); if (failedAt && Date.now() - failedAt > 2000) { - return { ok: false, message: log.find((l) => BUILD_FAILED.test(l)), log }; + return { ok: false, message: log.find(failed), log }; } } return { diff --git a/scripts/lib/tb-install.mjs b/scripts/lib/tb-install.mjs index 31581095..a5618230 100644 --- a/scripts/lib/tb-install.mjs +++ b/scripts/lib/tb-install.mjs @@ -57,7 +57,7 @@ export function compilerExe(ide) { * The exit code cannot say so. `import` and `export` exit 0 on every failure * they report themselves, and `import` exits 999 with no result line at all * when the tree has a folder inside its top-level `Packages` -- any project - * that embeds a package (BUGS-TO-REPORT.md). A last line of `... DONE` is the + * that embeds a package (twinbasic/twinbasic#841). A last line of `... DONE` is the * only success, so that is the test, whatever the exit status. Not * execFileSync: it throws on a non-zero exit, so a 999 escaped as an exception * before the output was ever read, and tbrun reported it as compile errors. diff --git a/scripts/lib/tb-launch.ps1 b/scripts/lib/tb-launch.ps1 index c6a794a9..2e636f74 100644 --- a/scripts/lib/tb-launch.ps1 +++ b/scripts/lib/tb-launch.ps1 @@ -7,7 +7,7 @@ # for the same reason: there is no argument quoting to get wrong. # # TBBUILD_EXE the executable -# TBBUILD_ARG its single argument +# TBBUILD_ARG its single argument, or empty for none # TBBUILD_DESKTOP desktop name to create # TBBUILD_JOB "0" for no job -- a --keep IDE, which must outlive the run # @@ -120,6 +120,12 @@ public static class TbLaunch { [DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateProcess(IntPtr process, uint exitCode); + [DllImport("kernel32.dll", SetLastError = true)] + static extern uint WaitForSingleObject(IntPtr handle, uint ms); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool GetExitCodeProcess(IntPtr process, out int exitCode); + // The error of the call just made, with its text. Nothing may come between // that call and this one. static Exception Failed(string what) { @@ -171,6 +177,15 @@ public static class TbLaunch { public static void Resume(PROCESS_INFORMATION pi) { ResumeThread(pi.hThread); } + + // Waits for the process to end, through the handle CreateProcess returned, + // which stays valid however soon it ends; returns its exit code. + public static int Wait(PROCESS_INFORMATION pi) { + WaitForSingleObject(pi.hProcess, 0xFFFFFFFF); + int code; + if (!GetExitCodeProcess(pi.hProcess, out code)) throw Failed("GetExitCodeProcess"); + return code; + } } '@ @@ -188,7 +203,8 @@ if ($useJob) { $job = [TbLaunch]::KillOnCloseJob() } # appends a trailing space -- parseCommandLine() in ide/main2.js reads that as an # empty second file argument and refuses the launch with "Bad command line # syntax." -$cmd = '"' + $exe + '" "' + $arg + '"' +$cmd = '"' + $exe + '"' +if ($arg) { $cmd += ' "' + $arg + '"' } # CREATE_SUSPENDED (0x4): the IDE goes into the job before it runs a single # instruction, so there is no moment in which it could start a child outside. @@ -204,4 +220,6 @@ if ($useJob) { Write-Output $pi.dwProcessId -try { (Get-Process -Id $pi.dwProcessId).WaitForExit() } catch { } +# The process's exit code, on a line of its own once it ends: tbrun's --exe +# reads it for the probe's exe. +Write-Output ("exit " + [TbLaunch]::Wait($pi)) diff --git a/scripts/lib/tb-probe.mjs b/scripts/lib/tb-probe.mjs new file mode 100644 index 00000000..bdb1568c --- /dev/null +++ b/scripts/lib/tb-probe.mjs @@ -0,0 +1,154 @@ +// What tbrun adds to a probe's sources before it packs them. +// +// A probe is a module with a [RunAfterBuild] Sub. Two things are added to the +// staged copy, never to the caller's tree: +// +// * A wrapper. The attribute moves from the probe's Sub to a Sub appended to +// the same module, which calls the probe's Sub and then prints SENTINEL. A +// probe that stops before it returns -- End, or an error that ends the run +// without a word, as an unhandled one raised in LLVM-compiled code does on +// BETA 995 -- leaves no SENTINEL, which is how tbrun tells it from one that +// finished. Appended to the same module, so a Private probe Sub can be +// called. The attribute is blanked to spaces rather than deleted, so every +// line and column a diagnostic names is still the caller's. +// * The TbRun module, whose Out writes a line where the run can read it: the +// DEBUG CONSOLE in the IDE, and standard output in a built exe, where +// Debug.Print writes nothing. + +import { MODIFIERS } from "./twin-declarations.mjs"; + +export const SENTINEL = "[tbrun] the probe returned"; +export const WRAPPER_SUB = "tbrun_RunProbe"; +export const TBRUN_FILE = "TbRun.twin"; + +// Lines the IDE prints after the probe's own run has returned. A probe that +// leaves a form loaded gets this one after SENTINEL (measured, BETA 983 and 995). +const AFTER_RETURN_RE = /^\[DEBUGGER\] Waiting for remaining forms to close\.\.\.$/; + +/** + * Where SENTINEL is in a captured console, if the probe returned. + * + * @param {string[]} lines the console's lines, blank ends trimmed + * @returns {number} SENTINEL's index when only lines the IDE prints after a + * return follow it, else -1 + */ +export function sentinelIndex(lines) { + const at = lines.lastIndexOf(SENTINEL); + return at >= 0 && lines.slice(at + 1).every((l) => AFTER_RETURN_RE.test(l)) ? at : -1; +} + +const ATTRIBUTE_RE = /^([ \t]*)\[RunAfterBuild\]/gim; +const SUB_RE = new RegExp( + `^[ \\t]*(?:(?:${MODIFIERS})[ \\t]+)*Sub[ \\t]+(\\w+)[ \\t]*(?:\\([ \\t]*\\))?[ \\t]*(?:'.*)?$`, + "i", +); +const SKIPPED_RE = /^[ \t]*(?:'.*|\[[^\]]*\][ \t]*)?$/; +const CONTAINER_RE = new RegExp( + `^[ \\t]*(?:(?:${MODIFIERS})[ \\t]+)*(Module|Class|CoClass|Interface)[ \\t]+(\\w+)`, + "gim", +); +const END_MODULE_RE = /^[ \t]*End[ \t]+Module\b/im; +const TBRUN_MODULE_RE = /^[ \t]*(?:(?:Public|Private)[ \t]+)?Module[ \t]+TbRun\b/im; + +/** + * Wrap a probe's [RunAfterBuild] Sub, and add the TbRun module. + * + * @param {{name: string, text: string}[]} files the tree's Sources/*.twin + * @returns {{files: {name: string, text: string}[], wrapped: {file: string, module: string, sub: string} | null, why?: string}} + * the files to write (changed or new), what was wrapped, and why nothing was + */ +export function wrapProbe(files) { + const out = []; + if (!files.some((f) => TBRUN_MODULE_RE.test(f.text))) out.push({ name: TBRUN_FILE, text: TBRUN_MODULE }); + const none = (why) => ({ files: out, wrapped: null, why }); + + const hits = files.flatMap((f) => + [...f.text.matchAll(ATTRIBUTE_RE)].map((m) => ({ f, at: m.index + m[1].length, indent: m[1] })), + ); + if (hits.length !== 1) { + return none( + hits.length ? `${hits.length} [RunAfterBuild] attributes` : "no [RunAfterBuild] attribute at the start of a line", + ); + } + const { f, at, indent } = hits[0]; + const end = at + "[RunAfterBuild]".length; + + // The Sub it marks: the rest of the attribute's line, or else the first line + // after it that is not blank, a comment or another attribute. + const lines = f.text.slice(end).split("\n"); + let sub = null; + for (const [i, raw] of lines.entries()) { + const line = raw.replace(/\r$/, ""); + if (SKIPPED_RE.test(line)) continue; + sub = SUB_RE.exec(line)?.[1] ?? null; + if (!sub) return none(`the line after [RunAfterBuild] is not a Sub without parameters: ${line.trim()}`); + lines.length = i + 1; + break; + } + if (!sub) return none("no Sub after [RunAfterBuild]"); + + const containers = [...f.text.slice(0, at).matchAll(CONTAINER_RE)]; + const container = containers.at(-1); + if (!container || container[1].toLowerCase() !== "module") { + return none( + `the [RunAfterBuild] Sub is not in a Module${container ? ` but in ${container[1]} ${container[2]}` : ""}`, + ); + } + const afterSub = end + lines.join("\n").length; + const closing = END_MODULE_RE.exec(f.text.slice(afterSub)); + if (!closing) return none(`no End Module after ${sub}`); + const insertAt = afterSub + closing.index; + + const eol = f.text.includes("\r\n") ? "\r\n" : "\n"; + const wrapper = [ + `${indent}[RunAfterBuild]`, + `${indent}Public Sub ${WRAPPER_SUB}()`, + `${indent} ${sub}`, + `${indent} Debug.Print "${SENTINEL}"`, + `${indent}End Sub`, + "", + "", + ].join(eol); + const text = + f.text.slice(0, at) + " ".repeat(end - at) + f.text.slice(end, insertAt) + wrapper + f.text.slice(insertAt); + out.push({ name: f.name, text }); + return { files: out, wrapped: { file: f.name, module: container[2], sub } }; +} + +// Debug.Print writes nothing in a built exe (docs/Features/Project-Configuration/ +// Project-Types.md), so there Out writes UTF-8: to the file TBRUN_OUT names, or +// else to standard output. tbrun's --exe names a file, because it starts the +// exe on a private desktop with no handles inherited, so no pipe reaches it. +const TBRUN_MODULE = `' Added by tbrun to the staged copy of a probe. TbRun.Out writes a line to the +' DEBUG CONSOLE in the IDE; in a built exe, as UTF-8, to the file the TBRUN_OUT +' environment variable names, or else to standard output. +Module TbRun + + Private Declare PtrSafe Function GetStdHandle Lib "kernel32" (ByVal nStdHandle As Long) As LongPtr + Private Declare PtrSafe Function WriteFile Lib "kernel32" (ByVal hFile As LongPtr, ByVal lpBuffer As LongPtr, ByVal nNumberOfBytesToWrite As Long, ByRef lpNumberOfBytesWritten As Long, ByVal lpOverlapped As LongPtr) As Long + Private Declare PtrSafe Function WideCharToMultiByte Lib "kernel32" (ByVal CodePage As Long, ByVal dwFlags As Long, ByVal lpWideCharStr As LongPtr, ByVal cchWideChar As Long, ByVal lpMultiByteStr As LongPtr, ByVal cbMultiByte As Long, ByVal lpDefaultChar As LongPtr, ByVal lpUsedDefaultChar As LongPtr) As Long + + Public Sub Out(ByVal Text As String) + If App.IsInIDE Then + Debug.Print Text + Exit Sub + End If + Text = Text & vbCrLf + Dim n As Long = WideCharToMultiByte(65001, 0, StrPtr(Text), Len(Text), 0, 0, 0, 0) + Dim bytes() As Byte + ReDim bytes(n - 1) + WideCharToMultiByte 65001, 0, StrPtr(Text), Len(Text), VarPtr(bytes(0)), n, 0, 0 + Dim file As String = Environ$("TBRUN_OUT") + If Len(file) = 0 Then + Dim written As Long + WriteFile GetStdHandle(-11), VarPtr(bytes(0)), n, written, 0 + Exit Sub + End If + Dim f As Integer = FreeFile + Open file For Binary Access Write As #f + Put #f, LOF(f) + 1, bytes + Close #f + End Sub + +End Module +`; diff --git a/scripts/lib/tb-project.mjs b/scripts/lib/tb-project.mjs index e9f193d8..d0917c42 100644 --- a/scripts/lib/tb-project.mjs +++ b/scripts/lib/tb-project.mjs @@ -30,15 +30,17 @@ import { runCompiler } from "./tb-install.mjs"; * @param {string} o.compiler the compiler executable (tb-install's compilerExe) * @param {object | ((original: object) => object)} [o.settings] settings to * set in the copy, or a function from the tree's own settings to them + * @param {(stage: string) => void} [o.prepare] changes the copy before it is packed * @returns {{original: object, settings: object}} the tree's settings, and the copy's */ -export function stageProject({ src, stage, project, compiler, settings = {} }) { +export function stageProject({ src, stage, project, compiler, settings = {}, prepare }) { rmSync(stage, { recursive: true, force: true }); cpSync(src, stage, { recursive: true }); const file = path.join(stage, "Settings"); const original = JSON.parse(readFileSync(file, "utf8")); const staged = { ...original, ...(typeof settings === "function" ? settings(original) : settings) }; writeFileSync(file, JSON.stringify(staged, null, "\t"), "utf8"); + prepare?.(stage); // import's exit code does not say whether it worked -- 0 on the failures it // reports, 999 on a tree holding an embedded package -- so runCompiler reads @@ -53,9 +55,10 @@ export function stageProject({ src, stage, project, compiler, settings = {} }) { * * Every harness id starts 7B247, and the digit after that is the role: 0 is * tbrun's probe, 1 an add-in being built, 2 the project an add-in test opens, - * and 4 and 5 are check_examples' template and batches, which it numbers - * itself. The last six hex digits are the lane's DevTools port, which is what - * already has to differ between runs going on at once. + * 3 the project tbbuild stages for --build or --llvm, and 4 and 5 are + * check_examples' template and batches, which it numbers itself. The last six + * hex digits are the lane's DevTools port, which is what already has to differ + * between runs going on at once. */ export const laneProjectId = (role, port) => `{7B247${role}00-0000-4000-9000-7B247${role}${port.toString(16).padStart(6, "0")}}`; diff --git a/scripts/lib/tb-run.mjs b/scripts/lib/tb-run.mjs new file mode 100644 index 00000000..313550d7 --- /dev/null +++ b/scripts/lib/tb-run.mjs @@ -0,0 +1,141 @@ +// Run the open project's [RunAfterBuild] Sub in an IDE and capture what it +// wrote to the DEBUG CONSOLE: the capture tbrun.mjs makes, as a function, so +// that check_examples' `check_run` batches make it the same way. +// +// The caller has attached a connection to an IDE whose project compiled clean. +// This presses the Build button, which runs the [RunAfterBuild] Sub once the exe +// is built, and reads the console until the run is over. tbrun.mjs's comment +// "seven things it gets right" says why each step is the way it is: the build +// button needs real pointer events, the console is read from its backing array +// and not the pane, and the Sub is expected to start with Debug.Cls, whose clear +// erases the IDE's build log and could erase a failure with it -- so each clear's +// erasure is kept (keepClears) and read back. + +import { click } from "./tb-click.mjs"; +import { BUILD_FAILED } from "./tb-ide.mjs"; +import { keepClears, keptClears, readConsole } from "./tb-ide-console.mjs"; + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +/** + * The console's text as lines: blank lines trimmed off both ends, and nothing + * else done. Reading the backing array rather than the pane means the header, + * the `>` input prompt and the timestamp column never arrive in the first place. + * Given `raw`, the same console read with its timestamps, it returns the same + * entries from that instead, since a line holding a timestamp is never blank. + */ +export function strip(text, raw = null) { + if (!text) return []; + const lines = (s) => s.split("\n").map((l) => l.replace(/\r$/, "")); + const out = lines(text); + let from = 0, + to = out.length; + while (from < to && !out[from].trim()) from++; + while (to > from && !out[to - 1].trim()) to--; + return (raw === null ? out : lines(raw)).slice(from, to); +} + +/** + * Press Build and read the console until the run is over. + * + * The run is over when `done(lines)` says so, when the console has stopped + * changing for `quietMs` after the first line, or at `timeoutMs`. A caller whose + * Sub ends with a marker line passes a `done` that looks for it; one that cannot + * says `() => false` and relies on the quiet period. + * + * It throws, with a message that says why, when the IDE has no + * `clearDebugConsole()` to wrap or no console to read, or the Build button + * cannot be pressed, or the page no longer holds what the clears erased. + * + * @param {object} c a tb-cdp connection, the project compiled clean + * @param {object} o + * @param {(lines: string[]) => boolean} [o.done] + * @param {number} o.quietMs + * @param {number} o.timeoutMs + * @returns {Promise<{captured: string[], last: string, erased: string[], timedOut: boolean}>} + * `captured` is the console after the run; `last` the same, unsplit; `erased` + * every line a clear removed, oldest first; `timedOut` is true when the wait + * ended at `timeoutMs` + */ +export async function captureRun(c, { done = () => false, quietMs, timeoutMs }) { + // Keep what each clear erases, for the check after the run. + if (!(await keepClears(c))) { + throw new Error( + "no clearDebugConsole() in this IDE -- a probe's Debug.Cls could erase a " + + "failure unseen. Refusing rather than returning what it left as complete.", + ); + } + // A real press/release pair; element.click() is ignored. + await click(c, "buildIcon"); + + const started = Date.now(); + let last = "", + lastChange = Date.now(), + seen = false, + timedOut = true; + while (Date.now() - started < timeoutMs) { + await sleep(400); + const now = await readConsole(c); + if (now === null) { + throw new Error( + "no debugConsoleContent.dataNodes in this IDE -- the DEBUG CONSOLE " + + "was never created, or this build moved it. Refusing rather than " + + "falling back to scraping the pane, which silently truncates.", + ); + } + if (now !== last) { + last = now; + lastChange = Date.now(); + const lines = strip(now); + if (lines.length) seen = true; + if (done(lines)) { + timedOut = false; + break; + } + } else if (seen && Date.now() - lastChange > quietMs) { + timedOut = false; + break; + } + } + const captured = strip(last); + const kept = await keptClears(c); + if (!kept) { + throw new Error( + "the IDE page no longer holds what the DEBUG CONSOLE's clears erased, so " + + "a failure they erased cannot be ruled out", + ); + } + return { captured, last, erased: kept.flatMap((text) => text.split("\n")), timedOut }; +} + +/** + * What a capture says about the build and the start of the run, apart from the + * Sub's own output. + * + * A build that fails after a clean compile never runs the Sub, and leaves the + * IDE's own build log in the console: the Sub's first statement is Debug.Cls, + * which would have erased it, so its survival means the capture is not the + * Sub's output. A [RunAfterBuild] Sub that fails code generation leaves the log + * too: the build succeeds, and then nothing in the Sub runs. A procedure the Sub + * calls that fails code generation is reported straight after the "[BUILD] + * Executing '<project>.<module>.<Sub>'..." line, before the Sub's first + * statement, so its Debug.Cls erases the report and the Sub stops where it calls + * that procedure (measured, BETA 983). A failure line among what the clears + * erased counts only after the last Executing line: before it is the build's own + * log, which ended in success or the Sub would not have run. + * + * @param {string[]} captured from captureRun + * @param {string[]} erased from captureRun + * @returns {{started: number, buildFailed: string | undefined, lost: string | undefined}} + * `started` is the index in `erased` of the last Executing line, or -1; + * `buildFailed` the first failure line left in the console; `lost` the first + * failure line a clear erased after the run started + */ +export function checkCapture(captured, erased) { + const started = erased.findLastIndex((l) => /^\[BUILD\] Executing '/.test(l)); + return { + started, + buildFailed: captured.find((l) => BUILD_FAILED.test(l)), + lost: started < 0 ? undefined : erased.slice(started + 1).find((l) => BUILD_FAILED.test(l)), + }; +} diff --git a/scripts/tbbuild.mjs b/scripts/tbbuild.mjs index 85d0c2af..884aff72 100644 --- a/scripts/tbbuild.mjs +++ b/scripts/tbbuild.mjs @@ -12,6 +12,15 @@ // target the IDE remembers for its path, so the // target is set on every run, win32 included. // --timeout <secs> give up waiting for the compile (default 180) +// --build after a compile with no errors, build the project, as +// the toolbar's Build button does, and report the +// build log. The project is exported and packed again +// first, with an explicit build path in a private +// folder, so the given .twinproj is never changed +// --llvm build with LLVM: --build, with the project's compiler +// options set to +llvm. Refused on a Community or +// Personal licence, which would build with the default +// compiler // --json emit one JSON object instead of text // --keep leave the IDE running afterwards. The IDE's pid is // then printed as `ide-pid: N` (and is always in --json @@ -25,7 +34,7 @@ // // Exit codes: 0 clean, 1 the project has errors, 2 the harness could not run (a // refused command line included) or crashed, 3 the compile never settled, -// 4 the project crashes the compiler. +// 4 the project crashes the compiler, 5 the build failed after a clean compile. // // ---------------------------------------------------------------- why this // @@ -53,10 +62,12 @@ // scripts/gen_attribute_probes.mjs -- but it does not know anything about // them. See WIP.Harness.md, "Compiling a twinBASIC project without the IDE in // front of you". -import { existsSync, statSync } from "node:fs"; +import { existsSync, mkdirSync, rmSync, statSync } from "node:fs"; +import { tmpdir } from "node:os"; import path from "node:path"; import { choiceOption, + die, exitOnCrash, numberOption, parseCli, @@ -64,24 +75,34 @@ import { refuseTogether, withUsageError, } from "../lib/cli.mjs"; -import { findIde } from "./lib/tb-install.mjs"; +import { compilerExe, findIde, runCompiler } from "./lib/tb-install.mjs"; import { compileProject } from "./lib/tb-build.mjs"; -import { COMPILE_TIMEOUT, TARGETS, summaryLine, wantShow } from "./lib/tb-ide.mjs"; +import { COMPILE_TIMEOUT, keptIdeLines, TARGETS, summaryLine, wantShow } from "./lib/tb-ide.mjs"; +import { laneProjectId, stageProject } from "./lib/tb-project.mjs"; import { finishTidy, startTidy } from "./lib/tb-registry.mjs"; exitOnCrash(); -const USAGE = `usage: node scripts/tbbuild.mjs <project.twinproj> [--ide <twinBASIC.exe>] [--port N] [--arch win32|win64] [--timeout S] [--json] [--keep] [--show|--hide] [-h, --help] +const USAGE = `usage: node scripts/tbbuild.mjs <project.twinproj> [--ide <twinBASIC.exe>] [--port N] [--arch win32|win64] [--timeout S] [--build | --llvm] [--json] [--keep] [--show|--hide] [-h, --help] Compiles a packed .twinproj in the twinBASIC IDE and prints its diagnostics. +With --build or --llvm it then builds the project. --ide <path> twinBASIC.exe (default: $TB_IDE, else the newest twinBASIC_IDE_BETA_* on the Desktop) --port <n> DevTools port to start the IDE on (default 9333) --arch <target> win32 or win64 (default win32) --timeout <secs> give up waiting for the compile (default 180) + --build after a compile with no errors, build the project and print + \`built: <file>\`; the project is exported and packed again + with a build path in a private folder, and the given file is + not changed + --llvm build with LLVM, as --build with the project's compiler + options set to +llvm; refused on a Community or Personal + licence. Without it, --build is the control for an --llvm run --json emit one JSON object instead of text - --keep leave the IDE running; its pid is printed as \`ide-pid: N\` + --keep leave the IDE running; its pid is printed as \`ide-pid: N\`, + with the taskkill command that ends it --show, --hide show the IDE on the desktop, or keep it on a private one (default: hidden, unless TBBUILD_SHOW is set) -h, --help print this text and exit @@ -90,10 +111,13 @@ Exit codes: 0 the project compiled without errors 1 the project has errors 2 a refused command line (a path that is not a .twinproj included), no IDE, an IDE - that did not start or expose a debug port, or a crash + that did not start or expose a debug port, a project that could not be exported + or packed, an --llvm run on a Community or Personal licence, or a crash 3 the compile never settled: the IDE did not report the project open, or its diagnostics did not match its status bar - 4 the project crashes the compiler`; + 4 the project crashes the compiler + 5 the build failed after a clean compile; the build log is printed on stdout and + the failing line on stderr`; function usage() { console.error(USAGE); @@ -110,6 +134,8 @@ const { values, positionals } = withUsageError( port: { type: "string" }, arch: { type: "string" }, timeout: { type: "string" }, + build: { type: "boolean", default: false }, + llvm: { type: "boolean", default: false }, json: { type: "boolean", default: false }, keep: { type: "boolean", default: false }, show: { type: "boolean", default: false }, @@ -141,6 +167,9 @@ const { port, arch, timeout } = withUsageError(() => { const IDE = findIde(values.ide); const asJson = values.json; const keep = values.keep; +// --llvm is a build, so the one switch the rest of the file reads is `build`. +const llvm = values.llvm; +const build = values.build || llvm; const show = wantShow({ show: values.show, hide: values.hide }); const proj = positionals[0]; @@ -189,7 +218,48 @@ let tidy = null; // back. Not under --keep, because a kept IDE is still writing; and not when // another process owns the tidy (startTidy then returns null), as a tool that // runs many builds does for every lane. -if (!keep) tidy = startTidy({ paths: [path.resolve(proj)] }); +// A build stages the project first, so what the IDE opens, and so what the tidy +// sweeps, is the folder under the temp directory and not the given path. +let target = path.resolve(proj); +let work = null; +if (build) { + // Keyed to --port, as tbrun's is: the port is what already differs between + // runs going on at once. + work = path.join(tmpdir(), "tbbuild", String(port)); + rmSync(work, { recursive: true, force: true }); + const out = path.join(work, "out"); + mkdirSync(out, { recursive: true }); + const compiler = compilerExe(IDE); + if (!existsSync(compiler)) die(2, `no compiler beside the IDE at ${compiler}`); + target = path.join(work, "tbbuild-probe.twinproj"); + // The IDE opens a project in place, and a build writes where the project's + // buildPath says. The packed project's is the template's, whose Save dialog a + // private desktop hides, so the project is exported, given an explicit file and + // a project id of its own, and packed again (lib/tb-project.mjs). + // Backslashes throughout and a trailing one on the folder: `export` prefixes + // \\?\ to what it is given and cannot create or find a folder named with + // forward slashes. Its exit code does not say whether it worked: runCompiler. + const src = path.join(work, "src"); + const exported = runCompiler(compiler, ["export", path.resolve(proj), src + path.sep, "--overwrite"]); + if (!exported.done) die(2, `exporting ${proj} failed${exported.why}:\n${exported.tail}`); + // Under --llvm the options are the run's and the exe's alike, as tbrun's are. + try { + stageProject({ + src, + stage: path.join(work, "stage"), + project: target, + compiler, + settings: { + "project.buildPath": path.join(out, "${ProjectName}_${Architecture}.${FileExtension}"), + "project.id": laneProjectId(3, port), + ...(llvm ? { "compiler.debugOptions": "+llvm", "compiler.buildOptions": "+llvm" } : {}), + }, + }); + } catch (e) { + die(2, e.message); + } +} +if (!keep) tidy = startTidy(work ? { prefixes: [work] } : { paths: [target] }); // compileProject ends its IDE before it returns, unless --keep, so the tidy comes // after the IDE has gone. Tidies whether or not an IDE was started, as tbrun @@ -198,12 +268,17 @@ if (!keep) tidy = startTidy({ paths: [path.resolve(proj)] }); // tidy does not rely on that. let r; try { - r = await compileProject({ project: proj, ide: IDE, port, arch, timeout, show, keep }); + r = await compileProject({ project: work ? target : proj, ide: IDE, port, arch, timeout, show, keep, build, llvm }); } finally { finishTidy(tidy); } if (r.code >= 2) { + // A failed build reports the log it read, on stdout, as the diagnostics are. + if (r.code === 5) { + if (asJson) console.log(JSON.stringify(jsonReport(r), null, 2)); + else for (const line of r.buildLog) console.log(line); + } if (r.message) console.error(r.message); process.exit(r.code); } @@ -213,26 +288,25 @@ const { rows, counts, dialogs, openedIn } = r; // under --keep, where this process leaves the IDE running and something else has // to end it: killing by image name instead takes out every concurrent run's IDE, // and the user's own open IDE with it. +function jsonReport(res) { + return { + project: proj, + arch, + openedIn: res.openedIn, + errors: res.counts[0], + warnings: res.counts[1], + hints: res.counts[2], + infos: res.counts[3], + idePid: res.idePid, + kept: keep, + diagnostics: res.rows, + dialogs: res.dialogs, + built: res.built, + buildLog: res.buildLog, + }; +} if (asJson) { - console.log( - JSON.stringify( - { - project: proj, - arch, - openedIn, - errors: counts[0], - warnings: counts[1], - hints: counts[2], - infos: counts[3], - idePid: r.idePid, - kept: keep, - diagnostics: rows, - dialogs, - }, - null, - 2, - ), - ); + console.log(JSON.stringify(jsonReport(r), null, 2)); } else { // Said only when either target is not the default, so the usual report is // unchanged, and the summary stays the last line. A project opens in win32 @@ -244,9 +318,10 @@ if (asJson) { } for (const row of rows) console.log(row); console.log(summaryLine(counts)); + if (r.built) console.log(`built: ${r.built}`); if (dialogs.length) console.log("dialogs:", JSON.stringify(dialogs)); // Only under --keep, where the pid is still alive and therefore actionable. - if (keep && r.idePid) console.log(`ide-pid: ${r.idePid}`); + if (keep && r.idePid) for (const l of keptIdeLines(r.idePid)) console.log(l); } process.exit(r.code); diff --git a/scripts/tbrun.mjs b/scripts/tbrun.mjs index ef8edf92..01cb96fa 100644 --- a/scripts/tbrun.mjs +++ b/scripts/tbrun.mjs @@ -11,11 +11,20 @@ // (default 2500) // --json emit one JSON object instead of text // --raw do not strip the console's timestamp column -// --keep leave the IDE running afterwards (implies --no-reap) +// --keep leave the IDE running afterwards, and print its pid +// (implies --no-reap) // --no-reap do not harvest automation servers the probe left behind // --reap-images comma-separated image names to harvest // (default: the Office suite -- see REAP_IMAGES) // --show / --hide as tbbuild's +// --llvm compile the whole project with LLVM: the same as +// --compiler-options +llvm +// --compiler-options <s> the project's compiler options, for the run +// and for the exe (compiler.debugOptions and +// compiler.buildOptions); an option string with +llvm +// is refused on a Community or Personal licence +// --exe run the built exe as well, on a private desktop, and +// capture what it writes with TbRun.Out and its exit code // // Exit: 0 captured output, 1 the project has compile errors, 2 the harness // could not run (a refused command line included), a compile never settled, or @@ -24,7 +33,10 @@ // and a procedure the probe calls that fails it, since the probe stops at the // call -- 3 no output: the build produced none in the console before the // timeout, or the probe ran and printed none after its last Debug.Cls -- 4 the -// compiler crashed, or restarted twice, while compiling the project. +// compiler crashed, or restarted twice, while compiling the project -- 5 the +// probe ended before it returned, its output printed all the same -- 6 with +// --exe, the exe exited with a code other than 0, or was still running after +// --timeout; its output and exit code printed all the same. // // ---------------------------------------------------------------- why // @@ -80,9 +92,15 @@ // generation is reported before the probe's first statement runs. So the // script wraps the page's clearDebugConsole() before the build, keeps // what each clear erases, and looks there too (tb-ide's keepClears). -// 5. QUIET-PERIOD, NOT A MARKER. Waiting for a sentinel string means every -// probe has to print one and the script has to know it. Waiting for the -// console to stop changing works for any probe. +// 5. QUIET-PERIOD, AND A MARKER THE PROBE NEVER WRITES. The run is over when +// the console stops changing, which works for any probe. But a probe that +// stops early also stops changing the console: End does, and so does an +// error raised in LLVM-compiled code with no handler, which ends the run +// without a word (measured, BETA 995). So the staged copy calls the +// probe's Sub from a wrapper that prints a sentinel once it returns +// (lib/tb-probe.mjs). Its absence is exit 5; its arrival ends the wait +// without the quiet period. Only the IDE's line about forms still loaded +// may follow it (sentinelIndex). // 6. EVERY RUN OWNS ITS OWN WORKSPACE AND KILLS ONLY ITS OWN IDE. Both were // shared, and both broke concurrency in ways that looked like something // else. The staging directory was a fixed %TEMP%/tbrun/src, so a second @@ -100,7 +118,7 @@ // a blunt enough instrument to need the guard rails in reapOrphans(). import { execFileSync } from "node:child_process"; -import { existsSync, readFileSync, mkdirSync, statSync, readdirSync, rmSync } from "node:fs"; +import { existsSync, readFileSync, mkdirSync, statSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { @@ -113,29 +131,32 @@ import { refuseTogether, withUsageError, } from "../lib/cli.mjs"; -import { click } from "./lib/tb-click.mjs"; import { compilerExe, findIde } from "./lib/tb-install.mjs"; import { - BUILD_FAILED, COMPILE_TIMEOUT, TARGETS, attachIde, compileOutcome, + keptIdeLines, killTree, launchIde, + launchOnDesktop, + llvmLicence, setBuildTarget, shutdownIde, summaryLine, waitForCompile, wantShow, } from "./lib/tb-ide.mjs"; -import { keepClears, keptClears, readConsole } from "./lib/tb-ide-console.mjs"; +import { readConsole } from "./lib/tb-ide-console.mjs"; +import { sentinelIndex, wrapProbe } from "./lib/tb-probe.mjs"; import { laneProjectId, stageProject } from "./lib/tb-project.mjs"; import { finishTidy, startTidy } from "./lib/tb-registry.mjs"; +import { captureRun, checkCapture, strip } from "./lib/tb-run.mjs"; exitOnCrash(); -const USAGE = `usage: node scripts/tbrun.mjs <source-dir> [--ide <twinBASIC.exe>] [--port N] [--arch win32|win64] [--timeout S] [--quiet MS] [--json] [--raw] [--keep] [--no-reap] [--reap-images a,b] [--show|--hide] [-h, --help] +const USAGE = `usage: node scripts/tbrun.mjs <source-dir> [--ide <twinBASIC.exe>] [--port N] [--arch win32|win64] [--timeout S] [--quiet MS] [--json] [--raw] [--keep] [--no-reap] [--reap-images a,b] [--show|--hide] [--llvm | --compiler-options S] [--exe] [-h, --help] Builds an exported twinBASIC source tree in the IDE, runs it, and prints what it writes to the DEBUG CONSOLE. @@ -148,11 +169,19 @@ writes to the DEBUG CONSOLE. (default 2500) --json emit one JSON object instead of text --raw do not strip the console's timestamp column - --keep leave the IDE running afterwards (implies --no-reap) + --keep leave the IDE running afterwards, and print its pid + (implies --no-reap) --no-reap do not harvest automation servers the probe left behind --reap-images a,b comma-separated image names to harvest (default: the Office suite) --show, --hide as tbbuild's + --llvm compile the whole project with LLVM, as + --compiler-options +llvm + --compiler-options <s> + the project's compiler options, for the run and the exe; + +llvm is refused on a Community or Personal licence + --exe also run the built exe on a private desktop, and print + what it writes with TbRun.Out and its exit code -h, --help print this text and exit Exit codes: @@ -161,10 +190,15 @@ Exit codes: 2 a refused command line (a source folder that is missing or has no Settings file included), no IDE or compiler, an IDE that did not start, a compile that never settled, a build that failed after a clean compile, a probe that never ran or - stopped at a procedure that failed code generation, or a crash + stopped at a procedure that failed code generation, an --llvm run on a + Community or Personal licence, an --exe run with no exe built, or a crash 3 no output: the console held none before the timeout, or the probe printed none after its last Debug.Cls - 4 the compiler crashed, or restarted twice, while compiling the project`; + 4 the compiler crashed, or restarted twice, while compiling the project + 5 the probe ended before it returned (End, or an error that ended the run); what + it printed is printed all the same + 6 --exe: the exe exited with a code other than 0, or was still running after + --timeout and was ended; its output and exit code are printed all the same`; const { values, positionals } = withUsageError( () => @@ -176,6 +210,9 @@ const { values, positionals } = withUsageError( quiet: { type: "string" }, ide: { type: "string" }, "reap-images": { type: "string" }, + "compiler-options": { type: "string" }, + llvm: { type: "boolean", default: false }, + exe: { type: "boolean", default: false }, json: { type: "boolean", default: false }, raw: { type: "boolean", default: false }, keep: { type: "boolean", default: false }, @@ -195,6 +232,7 @@ if (values.help) printHelpAndExit(USAGE); const { port, arch, timeoutMs, quietMs } = withUsageError( () => { refuseTogether(values, ["show", "hide"]); + refuseTogether(values, ["llvm", "compiler-options"]); return { port: numberOption(values.port ?? "9346", { option: "--port", integer: true, min: 1, max: 65535 }), arch: choiceOption(values.arch ?? TARGETS[0], { option: "--arch", choices: TARGETS }), @@ -288,21 +326,54 @@ if (!hasHook) { // ------------------------------------------------------------------- pack // A packing failure is the harness's, exit 2. +// The project's compiler options: compiler.debugOptions are what the +// [RunAfterBuild] run is compiled with, and compiler.buildOptions what the exe +// is -- measured on BETA 995, where +llvm in the build options alone left the +// run's Debug.Assert evaluated and its loop at the speed of the default. +const compilerOptions = values.llvm ? "+llvm" : values["compiler-options"]; let wasTemplate = false, - projectName = ""; + projectName = "", + usesLlvm = false, + wrap = null; try { const staged = stageProject({ src: srcDir, stage, project: projPath, compiler: COMPILER, - settings: { "project.buildPath": buildPath, "project.id": laneProjectId(0, port) }, + settings: { + "project.buildPath": buildPath, + "project.id": laneProjectId(0, port), + ...(compilerOptions === undefined + ? {} + : { "compiler.debugOptions": compilerOptions, "compiler.buildOptions": compilerOptions }), + }, + prepare: (dir) => { + const sources = path.join(dir, "Sources"); + const files = existsSync(sources) + ? readdirSync(sources) + .filter((f) => f.endsWith(".twin")) + .map((name) => ({ name, text: readFileSync(path.join(sources, name), "utf8") })) + : []; + wrap = wrapProbe(files); + for (const f of wrap.files) writeFileSync(path.join(sources, f.name), f.text, "utf8"); + }, }); wasTemplate = /\$\{/.test(staged.original["project.buildPath"] ?? ""); projectName = String(staged.settings["project.name"] ?? ""); + // Any LLVM in the run: the project's options, or a procedure's own. + usesLlvm = + /\+llvm\b/i.test( + `${staged.settings["compiler.debugOptions"] ?? ""} ${staged.settings["compiler.buildOptions"] ?? ""}`, + ) || /\[\s*CompilerOptions\s*\(\s*"[^"]*\+llvm/i.test(sourceText); } catch (e) { die(2, e.message); } +if (hasHook && !wrap.wrapped) { + console.error( + `warning: ${wrap.why} -- so a probe that ends before it returns cannot be told from one that finished.`, + ); +} // What the build wrote: the IDE expands the template, so the name is looked for // rather than assumed -- ${FileExtension} follows the build type -- and the @@ -386,6 +457,15 @@ if (outcome.counts[0] > 0) { failBuild(1, [...outcome.rows, summaryLine(outcome.counts)].join("\n")); } +// The licence, for a run with LLVM in it: llvmLicence says why a Community or +// Personal one measures nothing. +let licence = null; +if (usesLlvm) { + const found = await llvmLicence(cdp); + licence = found.licence; + if (found.refusal) failBuild(2, `tbrun: ${found.refusal}`); +} + // --------------------------------------------- build the exe, read the console let captured = null, @@ -393,51 +473,20 @@ let captured = null, erased = null, failure = null; try { - // (4) Keep what each clear erases, for the check after the run. - if (!(await keepClears(cdp))) { - throw new Error( - "no clearDebugConsole() in this IDE -- a probe's Debug.Cls could erase a " + - "failure unseen. Refusing rather than returning what it left as complete.", - ); - } - // (2) a real press/release pair; element.click() is ignored. - await click(cdp, "buildIcon"); - - // (5) settle on a quiet period rather than a sentinel. - const started = Date.now(); - let last = "", - lastChange = Date.now(), - seen = false; - while (Date.now() - started < timeoutMs) { - await new Promise((r) => setTimeout(r, 400)); - const now = await readConsole(cdp); - if (now === null) { - throw new Error( - "no debugConsoleContent.dataNodes in this IDE -- the DEBUG CONSOLE " + - "was never created, or this build moved it. Refusing rather than " + - "falling back to scraping the pane, which silently truncates.", - ); - } - if (now !== last) { - last = now; - lastChange = Date.now(); - if (strip(now).length) seen = true; - } else if (seen && Date.now() - lastChange > quietMs) break; - } - captured = strip(last); + // (4) the clears are kept, (2) the build button is pressed, (5) and the wait + // settles on the sentinel, or else a quiet period: lib/tb-run.mjs. + const run = await captureRun(cdp, { + done: (lines) => wrap.wrapped && sentinelIndex(lines) >= 0, + quietMs, + timeoutMs, + }); + captured = run.captured; + erased = run.erased; // --raw changes what is printed, never what is checked. BUILD_FAILED needs a // line that starts where the console's text does, and a line holding only a // timestamp is never blank, so every check reads without the column; under // --raw the lines printed are the same entries, read again with it. - shown = values.raw ? strip(last, await readConsole(cdp, { timestamps: true })) : captured; - const kept = await keptClears(cdp); - if (!kept) { - throw new Error( - "the IDE page no longer holds what the DEBUG CONSOLE's clears erased, so " + - "a failure they erased cannot be ruled out", - ); - } - erased = kept.flatMap((text) => text.split("\n")); + shown = values.raw ? strip(run.last, await readConsole(cdp, { timestamps: true })) : captured; cdp.close(); } catch (e) { failure = e.message; @@ -454,7 +503,8 @@ if (failure) die(2, `tbrun: ${failure}`); // that fails code generation leaves the log too: the build succeeds, and then // nothing in the Sub runs, Debug.Cls included. BUILD_FAILED is buildProject's // list of failure lines, the code-generation one among them. -if (captured.some((l) => BUILD_FAILED.test(l))) { +const { started, buildFailed, lost } = checkCapture(captured, erased); +if (buildFailed !== undefined) { die( 2, "tbrun: the build or the probe's code generation failed, so the probe never ran. " + @@ -469,9 +519,7 @@ if (captured.some((l) => BUILD_FAILED.test(l))) { // before then -- which tbrun returned as the whole output, exit 0 (measured, // BETA 983). A failure line among what the clears erased counts only after the // last Executing line: before it is the build's own log, which ended in success -// or the probe would not have run. -const started = erased.findLastIndex((l) => /^\[BUILD\] Executing '/.test(l)); -const lost = started < 0 ? undefined : erased.slice(started + 1).find((l) => BUILD_FAILED.test(l)); +// or the probe would not have run (checkCapture). if (lost) { die( 2, @@ -481,13 +529,24 @@ if (lost) { (shown.length ? shown.map((l) => ` ${l}`).join("\n") : " (nothing)"), ); } +// (5) The wrapper's sentinel says the probe returned. It is the wrapper's line, +// not the probe's, so it is never printed. The IDE's own lines after it stay: +// one says the probe left a form loaded. +const at = wrap.wrapped ? sentinelIndex(captured) : -1; +const returned = wrap.wrapped ? at >= 0 : null; +if (returned) { + // Without --raw, shown is captured itself. + if (shown !== captured) shown.splice(at, 1); + captured.splice(at, 1); +} // A probe that ran leaves its Executing line among what its Debug.Cls erased, // so an empty console then means it printed nothing after its last clear, not // that it never ran. -if (!captured.length && started >= 0) { +const printed = returned ? at : captured.length; +if (!printed && started >= 0 && returned !== false) { die(3, `tbrun: the probe ran (${erased[started]}) but printed nothing after its last Debug.Cls.`); } -if (!captured.length) { +if (!captured.length && started < 0) { die( 3, "tbrun: the build produced no console output before the timeout.\n" + @@ -498,28 +557,78 @@ if (!captured.length) { ); } +const exeRun = values.exe ? await runExe() : null; + if (values.json) { - console.log(JSON.stringify({ exe: builtFile(), arch, lines: shown, idePid: ideRun?.pid ?? null, reaped }, null, 2)); + console.log( + JSON.stringify( + { exe: builtFile(), arch, lines: shown, returned, licence, exeRun, idePid: ideRun?.pid ?? null, reaped }, + null, + 2, + ), + ); } else { for (const l of shown) console.log(l); + if (exeRun) { + console.log(`--- exe: ${exeRun.timedOut ? "still running after --timeout, ended" : `exit ${exeRun.exitCode}`}`); + for (const l of exeRun.lines) console.log(l); + } + // Only under --keep, where the pid is still alive and therefore actionable. + if (values.keep && ideRun?.pid) for (const l of keptIdeLines(ideRun.pid)) console.log(l); +} +if (returned === false) { + die( + 5, + `tbrun: the probe ended before it returned${started >= 0 ? "" : ", and the run's start was not seen"}: ` + + "End, or an error that ended the run without a report -- as one raised with no handler in " + + "LLVM-compiled code does. Or it was still running, silent, after --quiet ms; raise --quiet for a slow probe.", + ); } +if (exeRun?.timedOut) die(6, `tbrun: the exe was still running after --timeout, and was ended.`); +if (exeRun && exeRun.exitCode !== 0) die(6, `tbrun: the exe exited with code ${exeRun.exitCode}.`); +// Explicitly: under --keep the launcher, and the pipes to it, live as long as the +// IDE, and would keep this process waiting for it. +process.exit(0); // ------------------------------------------------------------------ helpers -// Trim blank lines off both ends. That is all this has to do: reading -// dataNodes rather than the pane means the header, the ">" input prompt and -// the timestamp column never arrive in the first place. Given `raw`, -// the same console read with its timestamps, it returns the same entries from -// that instead, since a line holding a timestamp is never blank. -function strip(text, raw = null) { - if (!text) return []; - const lines = (s) => s.split("\n").map((l) => l.replace(/\r$/, "")); - const out = lines(text); - let from = 0, - to = out.length; - while (from < to && !out[from].trim()) from++; - while (to > from && !out[to - 1].trim()) to--; - return (raw === null ? out : lines(raw)).slice(from, to); +// --exe: the built exe, started as the IDE is, on a private desktop and inside +// a kill-on-close job, so a window it opens -- a MsgBox -- is on no desktop +// anyone uses, and nothing it starts outlives it. It inherits no handles, and +// Debug.Print writes nothing in an exe, so what it printed is what TbRun.Out +// appended to the file TBRUN_OUT names. +async function runExe() { + const file = builtFile(); + if (!file || !/\.exe$/i.test(file)) { + die(2, `tbrun: --exe, but the build wrote no exe to ${outDir}${file ? ` (it wrote ${path.basename(file)})` : ""}`); + } + const outFile = path.join(work, "exe-out.txt"); + rmSync(outFile, { force: true }); + let run; + try { + run = await launchOnDesktop({ + exe: file, + desktop: `tbrun-exe-${port}`, + env: { ...process.env, TBRUN_OUT: outFile }, + }); + } catch (e) { + die(2, `tbrun: could not start the exe on a private desktop: ${e.message}`); + } + let timer; + const timedOut = await Promise.race([ + run.exited.then(() => false), + new Promise((r) => { + timer = setTimeout(() => r(true), timeoutMs); + }), + ]); + clearTimeout(timer); + if (timedOut) { + killTree(run.pid); + run.launcher.kill(); + } + const exitCode = await run.exited; + const text = existsSync(outFile) ? readFileSync(outFile, "utf8") : ""; + return { file, exitCode: timedOut ? null : exitCode, timedOut, lines: strip(text) }; } // (6) End OUR IDE by pid, never by image name. The tree kill takes the probe exe diff --git a/test/addin/keys.test.mjs b/test/addin/keys.test.mjs index ca96b308..87a2aff4 100644 --- a/test/addin/keys.test.mjs +++ b/test/addin/keys.test.mjs @@ -5,8 +5,8 @@ // // Each test states what BETA 983 and 995 do. If one fails after an IDE update, the // IDE has changed: update P1 and P2 in WIP.HelpAddin.md, the NOTE on -// docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md and the entry in -// BUGS-TO-REPORT.md, and then this file. +// docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md and the filed reports in +// bugs/filed/ (twinbasic/twinbasic#2445, #2446), and then this file. // // Run it with addin-test.bat, which gives it a lane; on its own it is skipped. @@ -164,7 +164,7 @@ scenario("P1 and P2: add-in keyboard shortcuts", (lane) => { const lines = (await linesSince(c, mark)).filter(Boolean); assert.deepEqual(fired, ["f1"]); assert.equal(await c.evaluate("sigHelpIsExpanded"), !expanded, "F1 did not toggle signature help"); - // toggleSigHelp() dereferences the event it is not given (BUGS-TO-REPORT.md). + // toggleSigHelp() dereferences the event it is not given (twinbasic/twinbasic#2446). assert.ok( lines.includes('command failed: "tbHelp_ToggleExpandSignatureHelp"'), `the IDE no longer reports the toggle failing: ${JSON.stringify(lines)}`, diff --git a/test/addin/symbols.test.mjs b/test/addin/symbols.test.mjs index 42253022..27632bf0 100644 --- a/test/addin/symbols.test.mjs +++ b/test/addin/symbols.test.mjs @@ -7,8 +7,8 @@ // Only page script can ask any of them; the add-in API has no such call. // // Each test states what BETA 983, 987 and 995 do. If one fails after an IDE update, the -// IDE has changed: update P5 in WIP.HelpAddin.md, and the entry in -// BUGS-TO-REPORT.md that the last hover test rests on, and then this file. +// IDE has changed: update P5 in WIP.HelpAddin.md, and the filed report +// that the last hover test rests on (bugs/filed/, twinbasic/twinbasic#2448), and then this file. // // Run it with addin-test.bat, which gives it a lane; on its own it is skipped. @@ -139,7 +139,7 @@ scenario("P5: what the compiler says about the name under the cursor", (lane) => }); test("hover says a ByVal parameter of String, Variant, Object or a class was made because Option Explicit is off, which it is not", async () => { - // BUGS-TO-REPORT.md. The project has project.optionExplicit true. + // twinbasic/twinbasic#2448. The project has project.optionExplicit true. const NOTE = "***note:*** *this variable was auto-generated due to* ***Option Explicit*** *being Off*"; const noted = { "Debug.Print h Is": "h", diff --git a/test/ide/assert.test.mjs b/test/ide/assert.test.mjs index c86193e1..2b424e77 100644 --- a/test/ide/assert.test.mjs +++ b/test/ide/assert.test.mjs @@ -6,8 +6,8 @@ // // Each test states what BETA 983 and 995 do. If one fails after an IDE update, the // IDE has changed: update the NOTE under "Running the tests" in -// Testing-with-Assert.md and the "Stop at a run-time error" entry in -// BUGS-TO-REPORT.md, and then this file. +// Testing-with-Assert.md and the filed report of +// twinbasic/twinbasic#2441 in bugs/filed/, and then this file. // // Run it with ide-test.bat, which gives it a lane; on its own it is skipped. diff --git a/test/ide/debugger.test.mjs b/test/ide/debugger.test.mjs index b806bdcf..107819c3 100644 --- a/test/ide/debugger.test.mjs +++ b/test/ide/debugger.test.mjs @@ -6,8 +6,9 @@ // shows how far it got. // // Each test states what BETA 983 and 995 do. If one fails after an IDE update, -// the IDE has changed: update the Traps on docs/IDE/Menu/Debug.md and the two -// debugger entries in BUGS-TO-REPORT.md, and then this file. +// the IDE has changed: update the Traps on docs/IDE/Menu/Debug.md and the +// debugger entry in BUGS-TO-REPORT.md and twinbasic/twinbasic#2441's report in +// bugs/filed/, and then this file. // // Run it with ide-test.bat, which gives it a lane; on its own it is skipped. diff --git a/test/ide/export.test.mjs b/test/ide/export.test.mjs index 9d88c4ff..9f2e226d 100644 --- a/test/ide/export.test.mjs +++ b/test/ide/export.test.mjs @@ -5,8 +5,8 @@ // lane's work folder: one of the behaviours tested deletes through a junction. // // Each test states what BETA 983 and 995 do. If one fails after an IDE update, -// the IDE has changed: update the matching entry in BUGS-TO-REPORT.md, and -// then this file. +// the IDE has changed: update the matching entry in BUGS-TO-REPORT.md or +// bugs/filed/, and then this file. // // Run it with ide-test.bat, which gives it a lane; on its own it is skipped. diff --git a/test/ide/packages.test.mjs b/test/ide/packages.test.mjs index 64fbdb31..e3590339 100644 --- a/test/ide/packages.test.mjs +++ b/test/ide/packages.test.mjs @@ -15,7 +15,7 @@ // Each test states what BETA 995 does, which is what BETA 983 did by hand. In // a lane, BETA 983's References page never finishes loading, so this file // cannot run against it. If a test fails after an IDE update, -// the IDE has changed: update the matching entry in BUGS-TO-REPORT.md and the +// the IDE has changed: update the matching entry in BUGS-TO-REPORT.md or bugs/filed/ and the // two Packages pages under docs/Features/Packages, and then this file. // // Run it with ide-test.bat, which gives it a lane; on its own it is skipped. @@ -124,8 +124,7 @@ scenario("Import from file... in the Packages dialog", (lane) => { // impexp.mjs adds the empty folders every exported package has; without its // Packages folder, BETA 995's compiler crashes on each restart after the // embedding Apply, and the IDE drops into Safe Mode, where nothing runs -// (BUGS-TO-REPORT.md, "Embedding a package with no Packages folder puts the -// compiler in a crash loop"). Each test works on a copy of the host project in its own folder, +// (twinbasic/twinbasic#2442). Each test works on a copy of the host project in its own folder, // because applying writes the project's settings. The copy's Main is empty // until v1 is embedded, since the host cannot compile before the package is // referenced; the test then writes Main in the editor. diff --git a/test/repro-templates/webview2-form/Settings b/test/repro-templates/webview2-form/Settings new file mode 100644 index 00000000..ca70e4a6 --- /dev/null +++ b/test/repro-templates/webview2-form/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "WebView2Form", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Template for scripts/bug_repro.mjs new --template webview2-form: a form holding one WebView2 control. project.name, project.appTitle, project.description and project.id are rewritten in each reproducer.", + "project.exportPathIsV2": true, + "project.id": "{7B247400-0000-4000-9000-7B2474000F00}", + "project.name": "WebView2Form", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{5D19653F-F49E-41F2-ABF8-16E473A89A7C}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] WebView2 Package", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "WebView2Package", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 3 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/test/repro-templates/webview2-form/Sources/Form1.tbform b/test/repro-templates/webview2-form/Sources/Form1.tbform new file mode 100644 index 00000000..a4762c7a --- /dev/null +++ b/test/repro-templates/webview2-form/Sources/Form1.tbform @@ -0,0 +1,146 @@ +[ + { + "AlwaysShowKeyboardCues": false, + "Appearance": "vbAppear3d", + "AutoRedraw": false, + "BackColor": 13223074, + "BorderStyle": "vbSizable", + "Caption": "WebView2 form", + "ClipControls": true, + "ControlBox": true, + "DrawMode": "vbCopyPen", + "DrawStyle": "vbSolid", + "DrawWidth": 1, + "Enabled": true, + "FillColor": 0, + "FillStyle": "vbFSTransparent", + "FontBold": false, + "FontItalic": false, + "FontName": "Segoe UI", + "FontSize": 8, + "FontStrikethru": false, + "FontTransparent": true, + "FontUnderline": false, + "ForeColor": -2147483630, + "FormDesignerId": "{7B247400-0000-4000-9000-7B2474000F01}", + "HasDC": true, + "Height": 480, + "HelpContextID": 0, + "Icon": "", + "KeyPreview": false, + "Left": 0, + "LinkMode": "vbLinkNone", + "LinkTopic": null, + "MDIChild": false, + "MaxButton": true, + "MaxHeight": 0, + "MaxWidth": 0, + "MinButton": true, + "MinHeight": 0, + "MinWidth": 0, + "MouseIcon": "", + "MousePointer": "vbDefault", + "Moveable": true, + "Name": "Form1", + "NegotiateMenus": false, + "OLEDropMode": "vbOLEDropNone", + "Opacity": 100, + "Palette": "", + "PaletteMode": "vbPaletteModeHalftone", + "Picture": "", + "PictureDpiScaling": false, + "RightToLeft": false, + "ScaleHeight": 8560, + "ScaleLeft": 0, + "ScaleMode": "vbTwips", + "ScaleTop": 0, + "ScaleWidth": 11760, + "ShowInTaskbar": true, + "StartUpPosition": "vbStartUpScreen", + "TabFocusAutoSelect": false, + "Tag": null, + "Top": 0, + "TopMost": false, + "TransparencyKey": -1, + "Visible": true, + "WhatsThisButton": false, + "WhatsThisHelp": false, + "Width": 640, + "WindowState": "vbNormal", + "__IDEOptions": { + "alignToGrid": false, + "gridHeight": 10, + "gridWidth": 10, + "lockedControls": [], + "multiColoredGrabbers": true, + "showGrid": true, + "showOutlines": false + }, + "__lastUpdateMarker": 3573907440, + "_children": [ + { + "AdditionalAllowedFrameAncestors": null, + "Anchors": { + "Bottom": true, + "Left": true, + "Right": true, + "Top": true, + "_clsid": "{8524D4B5-72A9-40A9-A189-29E7905C40CA}" + }, + "AreBrowserAcceleratorKeysEnabled": true, + "AreDefaultContextMenusEnabled": true, + "AreDefaultScriptDialogsEnabled": true, + "AreDevToolsEnabled": true, + "AreHostObjectsAllowed": true, + "BackColor": 10534293, + "CausesValidation": true, + "Dock": "vbDockNone", + "DocumentURL": "about:blank", + "DragIcon": "", + "DragMode": "vbManual", + "Enabled": true, + "EnvironmentOptions": { + "AdditionalBrowserArguments": null, + "AllowSingleSignOnUsingOSPrimaryAccount": false, + "BrowserExecutableFolder": null, + "EnableTrackingPrevention": true, + "ExclusiveUserDataFolderAccess": false, + "Language": null, + "TargetCompatibleBrowserVersion": "86.0.616.0", + "UserDataFolder": "", + "_className": "WebView2EnvironmentOptions", + "_clsid": "{185E63C0-E641-47ED-B6A9-8565168A7FCF}" + }, + "Height": 440, + "Index": -1, + "IsBuiltInErrorPageEnabled": true, + "IsGeneralAutoFillEnabled": true, + "IsMuted": false, + "IsPasswordAutoSaveEnabled": false, + "IsPinchZoomEnabled": true, + "IsScriptEnabled": true, + "IsStatusBarEnabled": false, + "IsSwipeNavigationEnabled": true, + "IsWebMessageEnabled": true, + "IsZoomControlEnabled": true, + "Left": 0, + "MouseIcon": "", + "MousePointer": "vbDefault", + "Name": "WebView21", + "TabIndex": 7, + "TabStop": true, + "Tag": null, + "Top": 0, + "UseDeferredEvents": true, + "UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0", + "Visible": true, + "Width": 620, + "ZoomFactor": 1, + "_className": "WebView2", + "_clsid": "{93B7E07E-6C7F-4758-8E51-4B1B17EC0A62}" + } + ], + "_className": "Form", + "_clsid": "{33AD4F38-6699-11CF-B70C-00AA0060D393}" + } +] diff --git a/test/repro-templates/webview2-form/Sources/Form1.twin b/test/repro-templates/webview2-form/Sources/Form1.twin new file mode 100644 index 00000000..c9468e2b --- /dev/null +++ b/test/repro-templates/webview2-form/Sources/Form1.twin @@ -0,0 +1,27 @@ +' A form holding one WebView2 control. Once the control is ready it opens +' about:blank, and when that navigation completes the form closes, so a run of +' the project ends by itself. Put the code that reproduces a bug in the +' handlers, and keep the close, or the run waits for a person to close the form. + +[FormDesignerId("7B247400-0000-4000-9000-7B2474000F01")] +[PredeclaredId] +Class Form1 + + Private Sub WebView21_Ready() Handles WebView21.Ready + Debug.Print "WebView21 ready" + WebView21.Navigate "about:blank" + End Sub + + Private Sub WebView21_NavigationStarting(ByVal Uri As String, ByVal IsUserInitiated As Boolean, _ + ByVal IsRedirected As Boolean, ByVal RequestHeaders As WebView2RequestHeaders, _ + Cancel As Boolean) Handles WebView21.NavigationStarting + Debug.Print "NavigationStarting " & Uri + End Sub + + Private Sub WebView21_NavigationComplete(ByVal IsSuccess As Boolean, ByVal WebErrorStatus As Long) _ + Handles WebView21.NavigationComplete + Debug.Print "NavigationComplete " & IsSuccess + Me.Close + End Sub + +End Class diff --git a/test/repro-templates/webview2-form/Sources/Startup.twin b/test/repro-templates/webview2-form/Sources/Startup.twin new file mode 100644 index 00000000..ce122c95 --- /dev/null +++ b/test/repro-templates/webview2-form/Sources/Startup.twin @@ -0,0 +1,8 @@ +Module Startup + + Public Sub Main() + Form1.Show vbModal + Debug.Print "Form1 closed" + End Sub + +End Module