diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index cd0d659ed..b949bea2f 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -1150,17 +1150,17 @@ Severity: crash. The crash is in the read, so no `On Error` handler helps, and a ## File statements and functions raise *Unspecified error* (-2147467259) in place of VB6's error numbers **Describe the bug** -When a file statement or function fails, twinBASIC raises run-time error -2147467259 (`&H80004005`, *Unspecified error*) where VB6 raises a numbered error: 52 *Bad file name or number*, 53 *File not found*, 55 *File already open*, 58 *File already exists*, 59 *Bad record length*, 63 *Bad record number*, 75 *Path/File access error* or 76 *Path not found*. Of 42 failing file operations, 37 raise -2147467259, and the other 5 match VB6. An `On Error` handler written for VB6, which tests `Err.Number` for one of those numbers, does not recognise the error. Observed in a run of the reproducer project, in the IDE and in a built exe alike. +When a file statement or function fails, twinBASIC raises run-time error -2147467259 (`&H80004005`, *Unspecified error*) where VB6 and 64-bit VBA raise a numbered error: 5 *Invalid procedure call or argument*, 52 *Bad file name or number*, 53 *File not found*, 54 *Bad file mode*, 55 *File already open*, 58 *File already exists*, 59 *Bad record length*, 62 *Input past end of file*, 63 *Bad record number*, 67 *Too many files*, 68 *Device unavailable*, 75 *Path/File access error* or 76 *Path not found*. Of 72 failing file operations, 67 raise -2147467259, and the other 5 match VB6. An `On Error` handler written for VB6, which tests `Err.Number` for one of those numbers, does not recognise the error. Observed in a run of the reproducer project, in the IDE and in a built exe alike. **To Reproduce** Steps to reproduce the behavior: -1. Open `file-errors-unspecified.twinproj` (attached as `file-errors-unspecified.zip`) and run it (F5). Its `Sub Main` makes a folder in `%TEMP%`, makes 42 file operations fail one at a time under `On Error Resume Next`, and prints `: ` after each. The smallest of them: +1. Open `file-errors-unspecified.twinproj` (attached as `file-errors-unspecified.zip`) and run it (F5). Its `Sub Main` makes a folder in `%TEMP%`, makes 72 file operations fail one at a time under `On Error Resume Next`, and prints `: ` after each. The smallest of them: ``` On Error Resume Next Kill Environ$("TEMP") & "\no-such-file.txt" Debug.Print Err.Number, Err.Description ``` -2. See `-2147467259 Unspecified error` in each of these 37 cases, where the same program built in VB6 (attached as `file-errors-unspecified-vb6.zip`) prints the number shown: +2. See `-2147467259 Unspecified error` in each of these 67 cases, where the same program built in VB6 (attached as `file-errors-unspecified-vb6.zip`) prints the number shown: | operation that fails | VB6 | |---|---| @@ -1186,26 +1186,51 @@ Steps to reproduce the behavior: | `EOF`, `LOF`, `Loc`, `Seek`, `FileAttr`, `Get #`, `Put #`, `Print #` and `Line Input #` with file number 99, which is not open (9 cases) | 52 | | `Seek #f, 0` | 63 | | `Put` of a 22-character string to a `Random` file opened with `Len = 4` | 59 | + | `ChDir` to a file, to `""` and to a wildcard (3 cases) | 76 | + | `ChDrive` and `CurDir` of a drive letter no drive has (2 cases) | 68 | + | `ChDrive` and `CurDir` of `"1"`, which is not a letter (2 cases) | 5 | + | `Dir()` after `Dir` has returned `""` | 5 | + | `Dir` of a file name with a backslash at its end | 52 | + | `Dir` of a name of 256 characters | 53 | + | `FileAttr` with a *returntype* of 2 | 5 | + | `FileCopy` from `""` | 75 | + | `FileCopy` from a wildcard | 52 | + | `FileLen` of a file in a folder that does not exist | 76 | + | `FileLen` of a wildcard that matches nothing | 52 | + | `FreeFile` with every file number from 1 to 255 in use | 67 | + | `GetAttr` of a wildcard | 52 | + | `GetAttr` of a name with `<` in it | 5 | + | `Input` and `InputB` of more characters than are left in a file open `For Input` (2 cases) | 62 | + | `Input` from a file open `For Output` | 54 | + | `Input` of -1 characters | 5 | + | `Input` from file number 99, which is not open | 52 | + | `Kill` of a folder | 53 | + | `Kill` of a wildcard that matches a read-only file | 75 | + | `MkDir ""` | 76 | + | `RmDir` of a file | 76 | + | `RmDir` of the current directory | 75 | + | `SetAttr` of a wildcard | 52 | + | `Width #99`, which is not open | 52 | 3. See the 5 cases that match VB6: `FileCopy` of a file that does not exist (53) and into a folder that does not exist (76), `Line Input #` past the end of the file (62), and `Close` of a file number that is not open and `SetAttr` of a file open `For Input`, which raise no error in either. **Expected behavior** -The numbers in the table, with VB6's descriptions, as for the 5 cases that already match. A VB6 program that handles a missing file by testing `Err.Number = 53` takes its other branch in twinBASIC. +The numbers in the table, with VB6's descriptions, as for the 5 cases that already match; 64-bit VBA raises the same numbers. A VB6 program that handles a missing file by testing `Err.Number = 53` takes its other branch in twinBASIC. **Desktop:** - OS: Windows 10 Pro 22H2 (build 19045) - twinBASIC compiler version: BETA 1010 **Additional context** -This extends #2345, which reports the first row of the table, and is part of #56. `FileCopy` raises VB6's numbers, since #2451 fixed it. +This extends #2345, which reports the first row of the table, and is part of #56. `FileCopy` raises VB6's numbers for a source or a folder that does not exist, since #2451 fixed it; the numbers it raises for the other failures are a separate report. -The same in a built exe as in the IDE, and the same with the project setting *Use Unicode Standard Library* set to No. In code compiled with LLVM the number is the same, and the description is *Application-defined or object-defined error*. +The same in a built exe as in the IDE, and the same with the project setting *Use Unicode Standard Library* set to No. In code compiled with LLVM the number is the same, and the description is *Application-defined or object-defined error*. A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. What does not reproduce it: the `DirListBox` and `FileListBox` controls trap the error themselves, and raise 76 for a `Path` that does not exist, as VB6 does. Severity: moderate. An error is raised wherever VB6 raises one, so no failure goes unreported. But a handler that tests for the number does not recognise the error, a handler that shows `Err.Description` shows *Unspecified error*, and code that decides a file exists from `Err.Number <> 53` decides wrongly. - + --- @@ -1237,7 +1262,7 @@ Error 55 for a file open `For Output`, `Append`, `Binary` or `Random`, with its **Additional context** Severity: low, but silent. No error is raised, and the file's attributes change where VB6 leaves them alone: the file stays read-only until its attributes are set back. - + --- @@ -7259,13 +7284,14 @@ Given an argument they refuse, several runtime procedures raise -2147467259 (`&H - `Shell` with a zero-length path, spaces, a folder, a file that is not a program, or a path longer than 32,767 characters: 5; with a program or folder that does not exist, or an unclosed quotation mark: 53, *File not found*; with a *windowstyle* less than 0 or greater than 10: 5. - `MsgBox` and `InputBox` with a *helpfile* and no *context*: 5. - `SendKeys` raises -2147352567 (`&H80020009`), *Exception occurred.*, instead: for an unknown key code (`{INS}` and `{F17}` included), an unclosed brace, a count in braces that is not a whole number from 0 to 8,175, unbalanced parentheses or `++`, VB raises 5; for a *wait* of `Null`, 94; for a *wait* string that is not a `Boolean`, 13. +- The Financial functions, where VB raises 5: `DDB`, `SLN` and `SYD` with a *life* of 0 or less (`SLN`: of 0), `DDB` and `SYD` with a *period* of 0 or less or greater than *life*, or a *salvage* less than 0, and `DDB` with a *factor* of 0 or less; `IPmt` with an *nper* of 0 or less and `PPmt` with an *nper* less than 0; `Pmt` with a *rate* and an *nper* of 0, and `NPer` with a *rate* and a *pmt* of 0; `Rate` with an *nper* of 0 or less, or no result in 20 tries from *guess*; `IRR` with no payment or no receipt in *values*, or no result in 20 tries. `Rate` with an *nper* below 1 whose result is beyond the range of a `Double`: VB raises 6. It is the pattern of #2544 (`ReDim` with inverted bounds raises -2147467259 where VB6 raises 9). Observed in the compiler's test mode and in built exes, 32-bit and 64-bit. **To Reproduce** Steps to reproduce the behavior: 1. Open `argument-errors-unspecified-error.twinproj` (attached as `argument-errors-unspecified-error.zip`). -2. Run its test cases, the module below. On BETA 1010 the three controls pass and the other twenty-five fail, each with `Err.Number` -2147467259, or -2147352567 for the two `SendKeys` cases. +2. Run its test cases, the module below. On BETA 1010 the three controls pass and the other thirty-five fail, each with `Err.Number` -2147467259, or -2147352567 for the two `SendKeys` cases. The module is meant to be adapted as a regression test. Each case asserts VB6's number, which the attached VB6 project (`argument-errors-unspecified-error-vb6.zip`) prints; 64-bit VBA raises the same numbers. No case writes to the registry: the registry procedures are given names that do not exist. ```vb @@ -7507,6 +7533,88 @@ Module RegressionTests Assert.Strict.AreEqual 94&, Err.Number End Sub + ' The Financial functions (batch 9). + [TestCase] + Public Sub DDB_LifeZero_Raises5() + Dim R As Double + On Error Resume Next + R = DDB(1000, 100, 0, 1) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub SLN_LifeZero_Raises5() + Dim R As Double + On Error Resume Next + R = SLN(1000, 100, 0) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub SYD_PeriodAboveLife_Raises5() + Dim R As Double + On Error Resume Next + R = SYD(1000, 100, 5, 6) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub IPmt_NperZero_Raises5() + Dim R As Double + On Error Resume Next + R = IPmt(0.01, 1, 0, 1000) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub PPmt_NperNegative_Raises5() + Dim R As Double + On Error Resume Next + R = PPmt(0.01, 1, -10, 1000) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Pmt_RateAndNperZero_Raises5() + Dim R As Double + On Error Resume Next + R = Pmt(0, 0, 1000) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub NPer_RateAndPmtZero_Raises5() + Dim R As Double + On Error Resume Next + R = NPer(0, 0, 1000) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Rate_NperZero_Raises5() + Dim R As Double + On Error Resume Next + R = Rate(0, -100, 1000) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Rate_NperBelowOneOverflow_Raises6() + Dim R As Double + On Error Resume Next + R = Rate(0.5, -100, 1000) + Assert.Strict.AreEqual 6&, Err.Number + End Sub + + [TestCase] + Public Sub IRR_NoReceipt_Raises5() + Dim R As Double, F(1) As Double + F(0) = -100: F(1) = -110 + On Error Resume Next + R = IRR(F) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + End Module ``` @@ -7524,7 +7632,7 @@ A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01 What does not reproduce it: the errors these procedures already raise with VB's numbers, such as `LBound` of an array not yet sized (9), `QBColor` outside 0 to 15 (5), `RGB` with a negative component (5), and `Null` for any of the arguments above (94). - + --- @@ -8493,3 +8601,2534 @@ Severity: low. Programs on the user's own desktop are not affected. When the window is created hidden and shown with `SW_SHOWNA`, the 2 s moves from `CreateWindowEx` into `AppActivate`; so the wait belongs to the activation of a new window, whichever call makes it. The program never calls `GetForegroundWindow` during it. On a private desktop no window becomes the foreground window; something in the twinBASIC runtime apparently waits for that, with a 2 s limit. + +--- + +## `Kill` deletes hidden and system files, where VB6 and VBA raise error 53 and leave them + +**Describe the bug** +`Kill` deletes a file that has the hidden or the system attribute, and raises no error. VB6 and 64-bit VBA never delete such a file: `Kill` of a hidden or system file raises 53, *File not found*, and leaves it, and a wildcard skips such files, deleting the others it matches (and raising 53 when it matches nothing else). So `Kill "*.*"` in twinBASIC also deletes the hidden and system files of the folder, which a VB6 program relies on it to keep. Observed by running the reproducer's test cases. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `kill-hidden-system-files.twinproj` (attached as `kill-hidden-system-files.zip`). +2. Run its test cases, the module below. On BETA 1010 the two controls pass and the other five fail. + +The module is meant to be adapted as a regression test. Each case asserts VB6's number, which the attached VB6 project (`kill-hidden-system-files-vb6.zip`) prints; 64-bit VBA raises the same numbers. Every case runs under a copy of a files harness, which makes a folder of the case's own under `%TEMP%` the current directory, fills it with the files the case names, and deletes it after the case, so no case touches any other file. The harness is in the attached project, left out below. +```vb +' Regression tests: Kill refuses a hidden or a system file with error 53, as VB6 and 64-bit VBA +' do, and a wildcard skips such files, where twinBASIC deletes them. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + ' ... the copy of the files harness: in the attached project. files_Begin makes a folder of + ' the case's own the current directory, with a.txt (10 bytes), empty.txt, ro.txt (read-only), + ' hid.txt (hidden), the empty folder sub and the folder full holding f.txt; files_End closes + ' every file, deletes the folder and keeps the case's error; files_Attr is GetFileAttributesW ... + + ' A control: twinBASIC already does this. + [TestCase] + Public Sub Kill_NormalFile_Deletes() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + Kill "a.txt" + Holds = files_Attr("a.txt") = -1 + files_End + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.IsTrue Holds + End Sub + + ' A control: twinBASIC already does this. + [TestCase] + Public Sub Kill_ReadOnlyFile_RaisesAndKeepsIt() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + Kill "ro.txt" + Holds = files_Attr("ro.txt") <> -1 + files_End + Assert.Strict.AreNotEqual 0&, Err.Number + Assert.Strict.IsTrue Holds + End Sub + + [TestCase] + Public Sub Kill_HiddenFile_Raises53() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + Kill "hid.txt" + Holds = files_Attr("hid.txt") <> -1 + files_End + Assert.Strict.AreEqual 53&, Err.Number + Assert.Strict.IsTrue Holds + End Sub + + [TestCase] + Public Sub Kill_SystemFile_Raises53() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + SetAttr "a.txt", vbSystem + Kill "a.txt" + Holds = files_Attr("a.txt") <> -1 + files_End + Assert.Strict.AreEqual 53&, Err.Number + Assert.Strict.IsTrue Holds + End Sub + + [TestCase] + Public Sub Kill_HiddenSystemFile_Raises53() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + SetAttr "a.txt", vbHidden + vbSystem + Kill "a.txt" + Holds = files_Attr("a.txt") <> -1 + files_End + Assert.Strict.AreEqual 53&, Err.Number + Assert.Strict.IsTrue Holds + End Sub + + [TestCase] + Public Sub Kill_WildcardMatchingOnlyAHiddenFile_Raises53() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + Kill "h*" + Holds = files_Attr("hid.txt") <> -1 + files_End + Assert.Strict.AreEqual 53&, Err.Number + Assert.Strict.IsTrue Holds + End Sub + + [TestCase] + Public Sub Kill_Wildcard_LeavesHiddenFile() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + SetAttr "ro.txt", vbNormal + Kill "*.txt" + Holds = files_Attr("hid.txt") <> -1 + files_End + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.IsTrue Holds + End Sub + +End Module +``` + +**Expected behavior** +Error 53 for `Kill` of a hidden or a system file, the file left in place, and a wildcard that skips hidden and system files, as VB6 and 64-bit VBA do. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045), system locale 1033 + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: high. Files are deleted that a VB6 or VBA program would keep, with no error: `desktop.ini`, `Thumbs.db` and other hidden or system files in a folder a program empties with `Kill "*.*"`. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +What does not reproduce it: a file with the read-only attribute (75 in VB, -2147467259 in twinBASIC, the entry on *Unspecified error*, and the file kept in all three); a file with only the archive attribute, which all of them delete. + + + +--- + +## `FileCopy` raises 70 where VB6 and VBA raise 75, and 55 where they raise 70 + +**Describe the bug** +When `FileCopy` fails for one of these reasons, it raises another number than VB6 and 64-bit VBA: +- *source* is a folder, or *destination* is a folder, a read-only file or a hidden file: twinBASIC raises 70, *Permission denied*; VB raises 75, *Path/File access error*. +- *destination* is *source* itself, by any spelling (`"a.txt"`, `"A.TXT"`, `".\a.txt"`), or *source* is open `For Input Lock Read`: twinBASIC raises 55, *File already open*; VB raises 70. + +An error is raised in each case, and nothing is copied, but a handler written for VB6 or VBA does not recognise it. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `filecopy-error-numbers.twinproj` (attached as `filecopy-error-numbers.zip`). +2. Run its test cases, the module below. On BETA 1010 the two controls pass and the other seven fail. + +The module is meant to be adapted as a regression test. Each case asserts VB6's number, which the attached VB6 project (`filecopy-error-numbers-vb6.zip`) prints; 64-bit VBA raises the same numbers. Every case runs under a copy of a files harness, which makes a folder of the case's own under `%TEMP%` the current directory, fills it with the files the case names, and deletes it after the case, so no case touches any other file. The harness is in the attached project, left out below. +```vb +' Regression tests: FileCopy raises VB6's numbers, 70 for a copy onto its own source or of a +' source locked against reading, and 75 for a folder or a read-only or hidden destination. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + ' ... the copy of the files harness: in the attached project. files_Begin makes a folder of + ' the case's own the current directory, with a.txt (10 bytes), empty.txt, ro.txt (read-only), + ' hid.txt (hidden), the empty folder sub and the folder full holding f.txt; files_End closes + ' every file, deletes the folder and keeps the case's error; files_Attr is GetFileAttributesW ... + + ' A control: twinBASIC already does this. + [TestCase] + Public Sub FileCopy_MissingSource_Raises53() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + FileCopy "nosuch.txt", "b.txt" + files_End + Assert.Strict.AreEqual 53&, Err.Number + End Sub + + ' A control: twinBASIC already does this. + [TestCase] + Public Sub FileCopy_SourceOpenForAppend_Raises55() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + Open "a.txt" For Append As #1 + FileCopy "a.txt", "b.txt" + files_End + Assert.Strict.AreEqual 55&, Err.Number + End Sub + + [TestCase] + Public Sub FileCopy_OntoItself_Raises70() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + FileCopy "a.txt", "a.txt" + files_End + Assert.Strict.AreEqual 70&, Err.Number + End Sub + + [TestCase] + Public Sub FileCopy_OntoItselfOtherCase_Raises70() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + FileCopy "a.txt", "A.TXT" + files_End + Assert.Strict.AreEqual 70&, Err.Number + End Sub + + [TestCase] + Public Sub FileCopy_SourceOpenLockRead_Raises70() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + Open "a.txt" For Input Lock Read As #1 + FileCopy "a.txt", "b.txt" + files_End + Assert.Strict.AreEqual 70&, Err.Number + End Sub + + [TestCase] + Public Sub FileCopy_SourceFolder_Raises75() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + FileCopy "sub", "b.txt" + files_End + Assert.Strict.AreEqual 75&, Err.Number + End Sub + + [TestCase] + Public Sub FileCopy_DestinationFolder_Raises75() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + FileCopy "a.txt", "sub" + files_End + Assert.Strict.AreEqual 75&, Err.Number + End Sub + + [TestCase] + Public Sub FileCopy_DestinationReadOnly_Raises75() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + FileCopy "a.txt", "ro.txt" + Holds = FileLen("ro.txt") = 1 + files_End + Assert.Strict.AreEqual 75&, Err.Number + Assert.Strict.IsTrue Holds + End Sub + + [TestCase] + Public Sub FileCopy_DestinationHidden_Raises75() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + FileCopy "a.txt", "hid.txt" + Holds = FileLen("hid.txt") = 1 + files_End + Assert.Strict.AreEqual 75&, Err.Number + Assert.Strict.IsTrue Holds + End Sub + +End Module +``` + +**Expected behavior** +75 for a folder or a read-only or hidden destination, and 70 for a copy onto the source or of a source locked against reading, as VB6 and 64-bit VBA raise. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045), system locale 1033 + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. Each failure raises an error, but not the number a VB6 or VBA handler tests for. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +What does not reproduce it: a source that does not exist (53) or a folder that does not exist (76), which #2451 fixed; a source open with write access (55 in all). The other failures of `FileCopy` that raise -2147467259 (a zero-length name, a wildcard) are in the entry on *Unspecified error*. twinBASIC also copies a source open with `Access Read` in any mode, where VB raises 55; the owner takes that as an extension. + + + +--- + +## `CurDir`, `Dir`, `FileDateTime`, `FileLen`, `Input` and `Width` check their arguments differently from VB6 and VBA + +**Describe the bug** +Given these arguments, the procedures do something other than VB6 and 64-bit VBA: +- `CurDir(Null)` and `Dir(Null)` raise 94, *Invalid use of Null*; VB raises 13, *Type mismatch*. +- `FileLen` and `FileDateTime` of a name with a wildcard that matches something (`"*.txt"`, `"e*"`, `"a.tx?"`) return the length or the date of the first match, which may be a hidden file or a folder, and raise nothing; VB raises 52, *Bad file name or number*. (A wildcard that matches nothing raises -2147467259, the entry on *Unspecified error*; so does `"a?txt"`.) +- `Width #f, n` with a negative *n* raises nothing, and `Print #f` then starts a new line before each item after the first; VB raises 5, *Invalid procedure call or argument*. +- `Input(0, #f)` raises -2147467259, *Unspecified error*; VB returns a zero-length string. (`InputB(0, #f)` returns one in both.) + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `filesystem-argument-checks.twinproj` (attached as `filesystem-argument-checks.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass and the other six fail. + +The module is meant to be adapted as a regression test. Each case asserts VB6's number, which the attached VB6 project (`filesystem-argument-checks-vb6.zip`) prints; 64-bit VBA raises the same numbers. Every case runs under a copy of a files harness, which makes a folder of the case's own under `%TEMP%` the current directory, fills it with the files the case names, and deletes it after the case, so no case touches any other file. The harness is in the attached project, left out below. +```vb +' Regression tests: CurDir, Dir, FileDateTime, FileLen, Input and Width check their arguments +' as VB6 and 64-bit VBA do: 13 for a Null drive or path, 52 for a wildcard where a file name is +' expected, 5 for a negative width, and no error for Input of 0 characters. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + ' ... the copy of the files harness: in the attached project. files_Begin makes a folder of + ' the case's own the current directory, with a.txt (10 bytes), empty.txt, ro.txt (read-only), + ' hid.txt (hidden), the empty folder sub and the folder full holding f.txt; files_End closes + ' every file, deletes the folder and keeps the case's error; files_Attr is GetFileAttributesW ... + + ' A control: twinBASIC already does this. + [TestCase] + Public Sub CurDir_ZeroLength_ReturnsCurrent() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + V = CurDir("") + files_End + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + ' A control: twinBASIC already does this. + [TestCase] + Public Sub FileLen_File_Returns10() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + V = FileLen("a.txt") + Holds = V = 10 + files_End + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.IsTrue Holds + End Sub + + ' A control: twinBASIC already does this. + [TestCase] + Public Sub Width_255_Accepted() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + Open "b.txt" For Output As #1 + Width #1, 255 + files_End + Assert.Strict.AreEqual 0&, Err.Number + End Sub + + [TestCase] + Public Sub CurDir_Null_Raises13() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + V = CurDir(Null) + files_End + Assert.Strict.AreEqual 13&, Err.Number + End Sub + + [TestCase] + Public Sub Dir_Null_Raises13() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + V = Dir(Null) + files_End + Assert.Strict.AreEqual 13&, Err.Number + End Sub + + [TestCase] + Public Sub FileLen_Wildcard_Raises52() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + V = FileLen("*.txt") + files_End + Assert.Strict.AreEqual 52&, Err.Number + End Sub + + [TestCase] + Public Sub FileDateTime_Wildcard_Raises52() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + V = FileDateTime("e*") + files_End + Assert.Strict.AreEqual 52&, Err.Number + End Sub + + [TestCase] + Public Sub Width_Negative_Raises5() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + Open "b.txt" For Output As #1 + Width #1, -1 + files_End + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Input_ZeroCharacters_ReturnsEmpty() + Dim V As Variant, Holds As Boolean + On Error Resume Next + files_Begin + Open "a.txt" For Input As #1 + V = Input(0, #1) + Holds = V = "" + files_End + Assert.Strict.AreEqual 0&, Err.Number + Assert.Strict.IsTrue Holds + End Sub + +End Module +``` + +**Expected behavior** +13 for a `Null` drive or path, 52 for a wildcard in `FileLen` and `FileDateTime`, 5 for a negative width, and a zero-length string from `Input(0, #f)`, as VB6 and 64-bit VBA give. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045), system locale 1033 + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: medium for `FileLen` and `FileDateTime`, which return a value for a file the program did not name, silently, and for a negative `Width`, which changes what `Print #` writes, silently. Low for the rest. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +What does not reproduce it: a *Width* from 0 to 255, or from 256 to 32,767, which twinBASIC takes as a line width where VB raises 5 (the owner takes that as an extension); `FileLen` and `FileDateTime` of a file or a folder named without a wildcard; `Input` of 1 or more characters. + + + +--- + +## Assigning `Err.HelpFile` or `Err.HelpContext`, or `Err.ReturnHResult` in a module, compiles but fails the build with a codegen error + +**Describe the bug** +A statement that assigns `Err.HelpFile` or `Err.HelpContext` compiles with no diagnostic, but the build fails with a code-generation error in the procedure that holds it: +``` +[LINKER] compilation (codegen) error detected in 'Startup.Mainrootmain' at line #3 +[LINKER] FAILED due to compilation errors '...\ErrHelpfileHelpcontextCodegen_win32.exe' +[BUILD] failed +``` +The same happens through `With Err`, `VBA.Err` and a variable declared `As ErrObject`, for a constant or a variable value, and for `Err.ReturnHResult` assigned in a procedure of a module. In the compiler's test mode, which builds nothing, each such procedure ends at once as *aborted*. + +**To Reproduce** +Steps to reproduce the behavior: +1. Create a project whose only module is: + ```vb + Module Startup + Public Sub Main() + Err.HelpFile = "x" + Debug.Print Err.HelpFile + End Sub + End Module + ``` +2. Compile it: no diagnostic. Build it: the build fails with the error above, naming the line of the assignment. +3. Or open `err-helpfile-helpcontext-codegen.twinproj` (attached as `err-helpfile-helpcontext-codegen.zip`) and run its test cases, the module below, with the class `Thing` it calls. On BETA 1010 the five controls pass and the other four end as *aborted*. + +The module is meant to be adapted as a regression test. Each case assigns a property of `Err` and reads it back; the attached VB6 project (`err-helpfile-helpcontext-codegen-vb6.zip`) makes the same assignments and prints `HelpFile: [x]`, `HelpContext: 5` and `HelpFile through ErrObject: [y]`. +```vb +' The issue's regression tests. Each assigns one property of Err and reads it back. +' The controls (Description, Source, Number, HelpFile and HelpContext through an +' Object variable, and ReturnHResult in a class method) pass; HelpFile and +' HelpContext assigned through Err itself, or through a variable declared As +' ErrObject, and ReturnHResult in a module procedure, do not build. +[TestFixture] +Module RegressionTests + + [TestCase] + Public Sub Control_SetDescription() + Err.Description = "x" + Assert.Strict.AreEqual "x", Err.Description + End Sub + + [TestCase] + Public Sub Control_SetSource() + Err.Source = "x" + Assert.Strict.AreEqual "x", Err.Source + End Sub + + [TestCase] + Public Sub Control_SetNumber() + Err.Number = 5 + Assert.Strict.AreEqual 5&, Err.Number + Err.Clear + End Sub + + [TestCase] + Public Sub Control_LateBoundHelpFileAndContext() + Dim o As Object + Set o = Err + o.HelpFile = "x" + o.HelpContext = 5 + Assert.Strict.AreEqual "x", Err.HelpFile + Assert.Strict.AreEqual 5&, Err.HelpContext + End Sub + + [TestCase] + Public Sub SetHelpFile() + Err.HelpFile = "x" + Assert.Strict.AreEqual "x", Err.HelpFile + End Sub + + [TestCase] + Public Sub SetHelpContext() + Err.HelpContext = 5 + Assert.Strict.AreEqual 5&, Err.HelpContext + End Sub + + [TestCase] + Public Sub SetHelpFileThroughErrObjectVariable() + Dim e As ErrObject + Set e = Err + e.HelpFile = "x" + Assert.Strict.AreEqual "x", Err.HelpFile + End Sub + + [TestCase] + Public Sub Control_SetReturnHResultInClassMethod() + Dim t As New Thing + t.Status + Assert.Strict.AreEqual 1&, Err.LastHresult + End Sub + + [TestCase] + Public Sub SetReturnHResultInModule() + Err.ReturnHResult = 1 + Assert.Strict.AreEqual 0&, Err.Number + End Sub + +End Module +``` +```vb +' A class whose method sets the HRESULT it returns, as the ReturnHResult page shows. +Class Thing + + Public Function Status() As Long + Err.ReturnHResult = 1 + End Function + +End Class +``` + +**Expected behavior** +The assignments build and set the property, as in VB6 and 64-bit VBA, which assign `HelpFile` and `HelpContext` like any other property of `Err`. If assigning `ReturnHResult` outside a method of a class is meant to be refused, a diagnostic that says so at compile time, rather than a failed build. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. The build fails, so nothing is lost, and there are ways round it: `Err.Raise` sets both properties through its *helpfile* and *helpcontext* arguments, and an assignment through a variable declared `As Object` that holds `Err` works. + +A 64-bit build fails the same way. + +What does not reproduce it: assigning `Err.Description`, `Err.Source` or `Err.Number`; reading `Err.HelpFile` or `Err.HelpContext`; assigning them late-bound, through an `Object` variable; assigning `Err.ReturnHResult` in a method of a class, which builds and gives the caller its value in `Err.LastHresult`. + + + +--- + +## `Err.Raise` and `Err.Number` keep an `HRESULT` that VB6 and VBA turn into the VB error of the same meaning + +**Describe the bug** +`Err.Raise` of an `HRESULT` raises that `HRESULT`, and assigning one to `Err.Number` stores it. VB6 and 64-bit VBA raise, and store, the VB error of the same meaning for many of them: `Err.Raise &H80070057` (`E_INVALIDARG`) raises 5, *Invalid procedure call or argument*, there, and twinBASIC raises -2147024809. A handler that tests `Err.Number` for the VB number does not recognise the error. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `err-raise-hresult-mapping.twinproj` (attached as `err-raise-hresult-mapping.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass and the other nine fail. + +The module is meant to be adapted as a regression test. Each case asserts VB6's number, which the attached VB6 project (`err-raise-hresult-mapping-vb6.zip`) prints; 64-bit VBA raises the same numbers. +```vb +' Regression tests: Err.Raise of an HRESULT that has a VB error of the same meaning raises +' that VB error, as VB6 and 64-bit VBA do, and assigning one to Err.Number stores the VB +' error. The controls raise a VB number and an HRESULT that VB keeps as it is. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + Private Function Raised(ByVal Number As Long) As Long + On Error Resume Next + Err.Raise Number + Raised = Err.Number + End Function + + [TestCase] + Public Sub Control_Raise5_Raises5() + Assert.Strict.AreEqual 5&, Raised(5) + End Sub + + [TestCase] + Public Sub Control_RaiseEFail_RaisesEFail() + Assert.Strict.AreEqual &H80004005, Raised(&H80004005) + End Sub + + [TestCase] + Public Sub Control_RaiseDispException_RaisesDispException() + Assert.Strict.AreEqual &H80020009, Raised(&H80020009) + End Sub + + [TestCase] + Public Sub Raise_EInvalidArg_Raises5() + Assert.Strict.AreEqual 5&, Raised(&H80070057) + End Sub + + [TestCase] + Public Sub Raise_EOutOfMemory_Raises7() + Assert.Strict.AreEqual 7&, Raised(&H8007000E) + End Sub + + [TestCase] + Public Sub Raise_ENotImpl_Raises445() + Assert.Strict.AreEqual 445&, Raised(&H80004001) + End Sub + + [TestCase] + Public Sub Raise_ENoInterface_Raises430() + Assert.Strict.AreEqual 430&, Raised(&H80004002) + End Sub + + [TestCase] + Public Sub Raise_DispUnknownName_Raises438() + Assert.Strict.AreEqual 438&, Raised(&H80020006) + End Sub + + [TestCase] + Public Sub Raise_DispTypeMismatch_Raises13() + Assert.Strict.AreEqual 13&, Raised(&H80020005) + End Sub + + [TestCase] + Public Sub Raise_ClassNotRegistered_Raises429() + Assert.Strict.AreEqual 429&, Raised(&H80040154) + End Sub + + [TestCase] + Public Sub Raise_VbErrorAsHResult_Raises53() + Assert.Strict.AreEqual 53&, Raised(&H800A0035) + End Sub + + [TestCase] + Public Sub Number_EInvalidArg_Is5() + Err.Number = &H80070057 + Dim N As Long = Err.Number + Err.Clear + Assert.Strict.AreEqual 5&, N + End Sub + +End Module +``` + +Every `HRESULT` tried, with what `Err.Raise` gives (twinBASIC raises each one as it is): + +| `HRESULT` | VB6 and 64-bit VBA | +|---|---| +| `&H80070057` `E_INVALIDARG` | 5 | +| `&H8007000E` `E_OUTOFMEMORY` | 7 | +| `&H80070005` `E_ACCESSDENIED` | 70 | +| `&H80004001` `E_NOTIMPL` | 445 | +| `&H80004002` `E_NOINTERFACE` | 430 | +| `&H80004004` `E_ABORT` | 287 | +| `&H80020003` `DISP_E_MEMBERNOTFOUND` | 438 | +| `&H80020005` `DISP_E_TYPEMISMATCH` | 13 | +| `&H80020006` `DISP_E_UNKNOWNNAME` | 438 | +| `&H80020008` `DISP_E_BADVARTYPE` | 458 | +| `&H8002000A` `DISP_E_OVERFLOW` | 6 | +| `&H8002000B` `DISP_E_BADINDEX` | 9 | +| `&H8002000E` `DISP_E_BADPARAMCOUNT` | 450 | +| `&H80020011` `DISP_E_NOTACOLLECTION` | 451 | +| `&H80030002` `STG_E_FILENOTFOUND` | 432 | +| `&H80030003` `STG_E_PATHNOTFOUND` | 76 | +| `&H80030005` `STG_E_ACCESSDENIED` | 70 | +| `&H80030070` `STG_E_MEDIUMFULL` | 61 | +| `&H80040112` `CLASS_E_NOTLICENSED` | 429 | +| `&H80040154` `REGDB_E_CLASSNOTREG` | 429 | +| `&H800401E3` `MK_E_UNAVAILABLE` | 429 | +| `&H800401F3` `CO_E_CLASSSTRING` | 429 | +| `&H800A0005`, `&H800A0035`, `&H800A01A8`, `&H800AFFFF` (a VB error as an `HRESULT`) | 5, 53, 424, 65535 (the low 16 bits) | +| `&H80004005` `E_FAIL`, `&H80020009` `DISP_E_EXCEPTION`, `&H80070002`, `&H80070006`, `&H8007000B`, `&H80070020`, `&H80070070`, `&H8007007E`, `&H8000FFFF` `E_UNEXPECTED`, `&H80004003` `E_POINTER`, `&H80040000` (`vbObjectError`) | the same `HRESULT` | + +`Err.Number = &H80070057`, `&H80004001` and `&H800A0035` leave `Err.Number` 5, 445 and 53 in VB6 and 64-bit VBA, without raising an error. + +**Expected behavior** +The VB number, as VB6 and 64-bit VBA give, for `Err.Raise` and for an assignment to `Err.Number`, so that a handler written for VB sees the number it tests for. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low to medium. An error is raised in every case, but code that raises an `HRESULT` it got from an API, or a class that reports `E_INVALIDARG` the way COM code does, reaches a handler that tests for 5 with a number it does not recognise. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +Related: the queued entry on late-bound calls (`latebound-hresult-mapping`) is about the `HRESULT` a late-bound call returns, which VB maps the same way; this one is about `Err.Raise` and `Err.Number` themselves. + +What does not reproduce it: an `HRESULT` that VB keeps as it is (the last row of the table); a VB number from 1 to 65535. twinBASIC also raises a positive number above 65535, where VB raises 5; the owner takes that as an extension. + + + +--- + +## `Collection`'s `Item`, `Remove` and `Add` check an index or a position differently from VB6 and VBA + +**Describe the bug** +Given these indexes and positions, a `Collection` does something other than VB6 and 64-bit VBA: +- A `Currency`, `Decimal` or `Date` number is refused: `Item` and `Remove` raise 9, *Subscript out of range*, and `Add`'s *before* and *after* raise 13, *Type mismatch*, where VB takes the number as it takes a `Long` or a `Double` (`C.Item(CCur(2))` is the second item). +- `Item` of a number on an empty collection raises 9; VB raises 5, *Invalid procedure call or argument*, as twinBASIC's own `Remove` does. +- `Item` and `Remove` of `Null`, an object, `Nothing`, an array, an error value or a number outside the `Long` range raise 9; VB raises 13. So does `Add`'s *before* of a number outside the `Long` range. +- `Add`'s *before* or *after* of -1, `True` or `Empty` raises 13; VB raises 9, as for any other position that is not an item's. +- `Add` with a *key* of `Empty` adds the item without a key and raises nothing; VB raises 13, as for any other key that is not a `String`. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `collection-index-checks.twinproj` (attached as `collection-index-checks.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass and the other fourteen fail. + +The module is meant to be adapted as a regression test. Each case asserts VB6's number, which the attached VB6 project (`collection-index-checks-vb6.zip`) prints; 64-bit VBA raises the same numbers. +```vb +' Regression tests: Collection's Item, Remove and Add take an index or a position as VB6 and +' 64-bit VBA do: a Currency, Decimal or Date number like any other number; 5 for a number on +' an empty collection (Item); 13 for Null, an object or a number outside the Long range +' (Item, Remove); 9 for a position of -1, True or Empty, and 13 for a key of Empty (Add). The controls use a Long, a +' Double and a Byte, and a string key. tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' A collection holding "a" and "b", without keys. + Private Function Two() As Collection + Set Two = New Collection + Two.Add "a" + Two.Add "b" + End Function + + ' The error Item(Index) raises on C, or 0 and the item in Got. + Private Function ItemError(ByVal C As Collection, ByVal Index As Variant, Optional ByRef Got As Variant) As Long + On Error Resume Next + Got = C.Item(Index) + ItemError = Err.Number + End Function + + Private Function RemoveError(ByVal C As Collection, ByVal Index As Variant) As Long + On Error Resume Next + C.Remove Index + RemoveError = Err.Number + End Function + + Private Function AddBeforeError(ByVal C As Collection, ByVal Before As Variant) As Long + On Error Resume Next + C.Add "x", , Before + AddBeforeError = Err.Number + End Function + + [TestCase] + Public Sub Control_ItemOfLongDoubleByte() + Dim Got As Variant + Assert.Strict.AreEqual 0&, ItemError(Two(), 2&, Got) + Assert.Strict.AreEqual "b", Got + Assert.Strict.AreEqual 0&, ItemError(Two(), 2#, Got) + Assert.Strict.AreEqual 0&, ItemError(Two(), CByte(2), Got) + End Sub + + [TestCase] + Public Sub Control_ItemOutOfRange_Raises9() + Assert.Strict.AreEqual 9&, ItemError(Two(), 3) + End Sub + + [TestCase] + Public Sub Control_ItemOfMissingKey_Raises5() + Assert.Strict.AreEqual 5&, ItemError(Two(), "z") + End Sub + + [TestCase] + Public Sub Item_Currency_ReturnsItem() + Dim Got As Variant + Assert.Strict.AreEqual 0&, ItemError(Two(), CCur(2), Got) + End Sub + + [TestCase] + Public Sub Item_Decimal_ReturnsItem() + Assert.Strict.AreEqual 0&, ItemError(Two(), CDec(2)) + End Sub + + [TestCase] + Public Sub Item_Date_ReturnsItem() + Assert.Strict.AreEqual 0&, ItemError(Two(), CDate(2)) + End Sub + + [TestCase] + Public Sub Remove_Currency_Removes() + Assert.Strict.AreEqual 0&, RemoveError(Two(), CCur(2)) + End Sub + + [TestCase] + Public Sub AddBefore_Currency_Adds() + Assert.Strict.AreEqual 0&, AddBeforeError(Two(), CCur(2)) + End Sub + + [TestCase] + Public Sub Item_EmptyCollection_Raises5() + Assert.Strict.AreEqual 5&, ItemError(New Collection, 1) + End Sub + + [TestCase] + Public Sub Item_Null_Raises13() + Assert.Strict.AreEqual 13&, ItemError(Two(), Null) + End Sub + + [TestCase] + Public Sub Item_Object_Raises13() + Assert.Strict.AreEqual 13&, ItemError(Two(), New Collection) + End Sub + + [TestCase] + Public Sub Item_BeyondLong_Raises13() + Assert.Strict.AreEqual 13&, ItemError(Two(), 2147483648#) + End Sub + + [TestCase] + Public Sub Remove_Null_Raises13() + Assert.Strict.AreEqual 13&, RemoveError(Two(), Null) + End Sub + + [TestCase] + Public Sub AddBefore_MinusOne_Raises9() + Assert.Strict.AreEqual 9&, AddBeforeError(Two(), -1) + End Sub + + [TestCase] + Public Sub AddBefore_Empty_Raises9() + Assert.Strict.AreEqual 9&, AddBeforeError(Two(), Empty) + End Sub + + [TestCase] + Public Sub AddBefore_BeyondLong_Raises13() + Assert.Strict.AreEqual 13&, AddBeforeError(Two(), 2147483648#) + End Sub + + [TestCase] + Public Sub Add_EmptyKey_Raises13() + Dim C As New Collection + On Error Resume Next + C.Add "x", Empty + Assert.Strict.AreEqual 13&, Err.Number + End Sub + +End Module +``` + +**Expected behavior** +The numbers VB6 and 64-bit VBA give: a `Currency`, `Decimal` or `Date` index or position taken as a number; 5 for `Item` of a number on an empty collection; 13 for `Null`, an object, an array or a number outside the `Long` range; 9 for a position of -1, `True` or `Empty`; 13 for a key of `Empty`. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: medium for the `Currency`, `Decimal` and `Date` indexes, which VB code passes without thinking (a `Currency` loop counter, a value read from a database field), and which twinBASIC refuses. Low for the key of `Empty`, whose item can then be reached only by its position. Low for the rest: an error is raised where VB raises one, with another number. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +What does not reproduce it: an index or a position of `Byte`, `Integer`, `Long`, `LongLong`, `Single` or `Double`, rounded as VB rounds it; a string key; a number out of range on a collection that has items (9 in all); `Remove` on an empty collection (5 in all); `Add`'s *before* or *after* given on an empty collection (5 in all). twinBASIC also takes a *before* of one more than `Count` as the end and an *after* of 0 as the start, where VB raises 9; the owner takes those as extensions. + + + +--- + +## `Rate` raises -2147467259 for almost every annuity, and returns -2.1 for a lump sum where VB6 returns 0.1 + +**Describe the bug** +`Rate` raises -2147467259 (`&H80004005`), *Unspecified error*, for almost every annuity that VB6 and 64-bit VBA solve: a loan (`Rate(10, -100, 800)` is 0.0427749780351145 in VB6), a mortgage (`Rate(360, -600, 100000)`), a savings plan (`Rate(10, -100, 0, 1200)`), payments due at the start of the period, the signs reversed, a *guess* given, and a lump sum over ten periods (`Rate(10, 0, 1000, -2000)`). Of the annuities tried, it answers only some lump sums (a *pmt* of 0), such as those over one to four periods, and for an even number of periods it returns the wrong root: `Rate(2, 0, 100, -121)` and `Rate(4, 0, 100, -146.41)` are -2.1, where VB6 returns 0.1. (-2.1 also solves the equation, but no annuity has a rate below -1.) + +Also: +- With an *nper* below 1 and a *pmt* of 0 it raises 5, *Invalid procedure call or argument*; VB6 returns the rate (`Rate(0.5, 0, 100, -110)` is 0.21). +- It does not check *guess*: a *guess* of `Null`, a string that is not a number, or an error value raises nothing, where VB raises 94, *Invalid use of Null*, and 13, *Type mismatch*. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `rate-fails.twinproj` (attached as `rate-fails.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass and the other twelve fail. + +The module is meant to be adapted as a regression test. Each case asserts what VB6 returns or raises, which the attached VB6 project (`rate-fails-vb6.zip`) prints; 64-bit VBA raises the same numbers. +```vb +' Regression tests: Rate returns the rate of an annuity, as VB6 and 64-bit VBA do, for a loan, +' a mortgage, a savings plan, payments due at the start, a guess and an nper below 1, and +' the positive rate (0.1, not -2.1) of a lump sum over an even number of periods; and it raises +' 94 for a guess of Null and 13 for a guess that is not a number. The controls are two +' lump sums Rate answers rightly (one and three periods) and a Null nper. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + [TestCase] + Public Sub Control_PmtZeroOnePeriod() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(1, 0, 100, -110) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.1) < 0.000001 + End Sub + + [TestCase] + Public Sub Control_PmtZeroThreePeriods() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(3, 0, 100, -133.1) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.1) < 0.000001 + End Sub + + [TestCase] + Public Sub Control_NperNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(Null, -100, 800) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + + [TestCase] + Public Sub PmtZeroTwoPeriods_ReturnsPositiveRate() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(2, 0, 100, -121) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.1) < 0.000001 + End Sub + + [TestCase] + Public Sub PmtZeroFourPeriods_ReturnsPositiveRate() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(4, 0, 100, -146.41) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.1) < 0.000001 + End Sub + + [TestCase] + Public Sub Loan_ReturnsRate() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(10, -100, 800) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.0427749780351) < 0.000001 + End Sub + + [TestCase] + Public Sub Mortgage_ReturnsRate() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(360, -600, 100000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.0050058250068) < 0.000001 + End Sub + + [TestCase] + Public Sub DueAtStart_ReturnsRate() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(10, -100, 800, 0, 1) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.053446167382) < 0.000001 + End Sub + + [TestCase] + Public Sub SavingsPlan_ReturnsRate() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(10, -100, 0, 1200) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.0398902762) < 0.000001 + End Sub + + [TestCase] + Public Sub SignsReversed_ReturnsRate() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(10, 100, -800) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.0427749780351) < 0.000001 + End Sub + + [TestCase] + Public Sub PmtZeroTenPeriods_ReturnsRate() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(10, 0, 1000, -2000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.0717734625363) < 0.000001 + End Sub + + [TestCase] + Public Sub WithGuess_ReturnsRate() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(10, -100, 800, 0, 0, 0.04) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.0427749780351) < 0.000001 + End Sub + + [TestCase] + Public Sub NperBelowOne_ReturnsRate() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(0.5, 0, 100, -110) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.21) < 0.000001 + End Sub + + [TestCase] + Public Sub GuessNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(1, 0, 100, -110, 0, Null) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + + [TestCase] + Public Sub GuessString_Raises13() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(1, 0, 100, -110, 0, "abc") + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 13&, E + End Sub + +End Module +``` + +**Expected behavior** +The rate VB6 and 64-bit VBA return, found from the default *guess* of 0.1 or from the one given: 0.0427749780351145 for `Rate(10, -100, 800)`, 0.1 for `Rate(2, 0, 100, -121)`, 0.21 for `Rate(0.5, 0, 100, -110)`; and 94 or 13 for a *guess* of `Null` or one that is not a number. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: high. `Rate` cannot be used for a loan or a savings plan, its main use, and where it does answer it can return a rate below -1 with no error. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +Related: the queued entry on refused arguments (`argument-errors-unspecified-error`) covers `Rate`'s -2147467259 where VB raises 5 or 6 (no result in 20 tries, an *nper* of 0 or less); this one is about the annuities that have a rate. + +What does not reproduce it: a lump sum over one or three periods (`Rate(1, 0, 100, -110)` and `Rate(3, 0, 100, -133.1)` are 0.1); `Null` in *nper*, *pmt* or *pv* (94 in all). + + + +--- + +## `NPer` raises error 6 for a loan or a savings plan, where VB6 returns the number of periods + +**Describe the bug** +Given a *rate* and a *pmt* of opposite signs, as in every loan or savings plan at a positive rate, `NPer` raises 6, *Overflow*, where VB6 and 64-bit VBA return the number of periods. `NPer(0.01, -100, 1000)`, a loan of 1000 repaid at 100 a period at 1% a period, is 10.5886444594232 in VB6; so are a car loan (`NPer(0.1 / 12, -500, 20000)`, 48.86), a loan with a balance left (*fv*), one with payments due at the start, a savings plan (`NPer(0.01, -100, 0, 1000)`, 9.58) and a negative rate with a positive payment (`NPer(-0.5, 100, -1000)`, 2.58). + +Where no number of periods exists, it raises 6 (a payment that does not cover the interest, `NPer(0.01, -1, 1000)`) or 11, *Division by zero* (a *pmt* of 0, a *rate* of -1), where VB raises 5, *Invalid procedure call or argument*. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `nper-opposite-signs-overflow.twinproj` (attached as `nper-opposite-signs-overflow.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass and the other nine fail. + +The module is meant to be adapted as a regression test. Each case asserts what VB6 returns or raises, which the attached VB6 project (`nper-opposite-signs-overflow-vb6.zip`) prints; 64-bit VBA raises the same numbers. +```vb +' Regression tests: NPer returns the number of periods of a loan or a savings plan, as VB6 and +' 64-bit VBA do, whatever the signs of rate and pmt, and raises 5 where no number of periods +' exists. The controls have rate and pmt of the same sign, or a rate of 0. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + [TestCase] + Public Sub Control_PositivePmt() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(0.01, 100, -1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 10.5886444594232) < 0.000001 + End Sub + + [TestCase] + Public Sub Control_RateZero() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(0, -100, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 10) < 0.000001 + End Sub + + [TestCase] + Public Sub Control_SameSigns() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(0.01, 100, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - -9.57859403981317) < 0.000001 + End Sub + + [TestCase] + Public Sub Loan_ReturnsPeriods() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(0.01, -100, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 10.5886444594232) < 0.000001 + End Sub + + [TestCase] + Public Sub CarLoan_ReturnsPeriods() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(0.1 / 12, -500, 20000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 48.8582651211073) < 0.000001 + End Sub + + [TestCase] + Public Sub LoanWithBalance_ReturnsPeriods() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(0.01, -100, 1000, 100) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 11.5886444594232) < 0.000001 + End Sub + + [TestCase] + Public Sub LoanDueAtStart_ReturnsPeriods() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(0.01, -100, 1000, 0, 1) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 10.4781450851168) < 0.000001 + End Sub + + [TestCase] + Public Sub SavingsPlan_ReturnsPeriods() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(0.01, -100, 0, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 9.57859403981317) < 0.000001 + End Sub + + [TestCase] + Public Sub NegativeRate_ReturnsPeriods() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(-0.5, 100, -1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 2.58496250072116) < 0.000001 + End Sub + + [TestCase] + Public Sub PmtBelowInterest_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(0.01, -1, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub PmtZero_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(0.01, 0, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub RateMinusOne_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(-1, -100, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + +End Module +``` + +**Expected behavior** +The number of periods VB6 and 64-bit VBA return, whatever the signs of *rate* and *pmt*, and 5 where no number of periods exists. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: high. `NPer` fails for a loan, its main use. An error is raised, so no wrong value reaches the program. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +Related: the queued entry on refused arguments (`argument-errors-unspecified-error`) covers `NPer` with a *rate* and a *pmt* of 0 (-2147467259; VB 5). + +What does not reproduce it: a *rate* and a *pmt* of the same sign (`NPer(0.01, 100, -1000)` is 10.5886444594232, and `NPer(0.01, 100, 1000)` is -9.57859403981317, as in VB6); a *rate* of 0 (`NPer(0, -100, 1000)` is 10). + + + +--- + +## `Null` in an optional argument of a Financial function raises 13, where VB6 raises 94 + +**Describe the bug** +Given `Null` as an optional argument --- *pv*, *fv* or *type* of `FV`, `PV`, `Pmt`, `NPer`, `IPmt`, `PPmt` and `Rate`, or *factor* of `DDB` --- these functions raise 13, *Type mismatch*. VB6 and 64-bit VBA raise 94, *Invalid use of Null*, as twinBASIC itself does for `Null` in a required argument. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `financial-null-optional-argument.twinproj` (attached as `financial-null-optional-argument.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass and the other nine fail, each with `Err.Number` 13. + +The module is meant to be adapted as a regression test. Each case asserts VB6's number, which the attached VB6 project (`financial-null-optional-argument-vb6.zip`) prints; 64-bit VBA raises the same numbers. +```vb +' Regression tests: a Null optional argument of a Financial function (pv, fv, type, factor) +' raises 94, Invalid use of Null, as VB6 and 64-bit VBA do. The controls are a Null required +' argument (94), a string that is not a number (13) and a number in the optional argument. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + [TestCase] + Public Sub Control_RequiredNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = FV(Null, 10, -100) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + + [TestCase] + Public Sub Control_OptionalString_Raises13() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = FV(0.01, 10, -100, "abc") + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 13&, E + End Sub + + [TestCase] + Public Sub Control_OptionalNumber() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = FV(0.01, 10, -100, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + End Sub + + [TestCase] + Public Sub FV_PvNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = FV(0.01, 10, -100, Null) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + + [TestCase] + Public Sub FV_TypeNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = FV(0.01, 10, -100, 0, Null) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + + [TestCase] + Public Sub PV_FvNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = PV(0.01, 10, -100, Null) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + + [TestCase] + Public Sub Pmt_FvNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Pmt(0.01, 10, 1000, Null) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + + [TestCase] + Public Sub NPer_TypeNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPer(0.01, 100, -1000, 0, Null) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + + [TestCase] + Public Sub IPmt_FvNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = IPmt(0.01, 1, 10, 1000, Null) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + + [TestCase] + Public Sub PPmt_TypeNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = PPmt(0.01, 1, 10, 1000, 0, Null) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + + [TestCase] + Public Sub Rate_TypeNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Rate(1, 0, 100, -110, Null) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + + [TestCase] + Public Sub DDB_FactorNull_Raises94() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = DDB(1000, 100, 5, 1, Null) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 94&, E + End Sub + +End Module +``` + +**Expected behavior** +94, *Invalid use of Null*, as VB6 and 64-bit VBA raise. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low. An error is raised where VB raises one, with another number. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +What does not reproduce it: `Null` in a required argument (94 in all); a string that is not a number in an optional argument (13 in all). + + + +--- + +## `IRR`, `MIRR` and `NPV` end the program for an array that has never been sized, and raise &H8000FFFF for one of two dimensions + +**Describe the bug** +Given a `Double` array that has never been sized (`Dim A() As Double` with no `ReDim`), or one emptied by `Erase`, `IRR`, `MIRR` and `NPV` end the program with an access violation, in a built exe and in the IDE, 32-bit and 64-bit. In the compiler's test mode the case reports `NATIVE EXCEPTION: ACCESS_VIOLATION`. VB6 and 64-bit VBA raise 5, *Invalid procedure call or argument*. + +Given an array of two dimensions, they raise -2147418113 (`&H8000FFFF`), *Catastrophic failure*, where VB raises 5. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `financial-array-arguments.twinproj` (attached as `financial-array-arguments.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass, the four unsized cases end with an access violation, and the three two-dimensional ones fail with `Err.Number` -2147418113. + +The module is meant to be adapted as a regression test. Each case asserts VB6's number, which the attached VB6 project (`financial-array-arguments-vb6.zip`) prints; 64-bit VBA raises the same numbers. +```vb +' Regression tests: IRR, MIRR and NPV raise 5, Invalid procedure call or argument, for an +' array that has never been sized or has been erased, and for one of two dimensions, as VB6 +' and 64-bit VBA do. On BETA 1010 the unsized cases end with an access violation. The +' controls are each function of a sized one-dimensional array. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + [TestCase] + Public Sub Control_IRR() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + ReDim A(1) + A(0) = -100 + A(1) = 110 + R = IRR(A) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.1) < 0.000001 + End Sub + + [TestCase] + Public Sub Control_MIRR() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + ReDim A(2) + A(0) = -100 + A(1) = 50 + A(2) = 60 + R = MIRR(A, 0.1, 0.1) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0.0723805294763609) < 0.000001 + End Sub + + [TestCase] + Public Sub Control_NPV() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + ReDim A(2) + A(0) = -100 + A(1) = 50 + A(2) = 60 + R = NPV(0.1, A) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - -4.50788880540947) < 0.000001 + End Sub + + [TestCase] + Public Sub IRR_Unsized_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = IRR(A) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub IRR_Erased_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + ReDim A(1) + A(0) = -100 + A(1) = 110 + Erase A + R = IRR(A) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub MIRR_Unsized_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = MIRR(A, 0.1, 0.1) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub NPV_Unsized_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = NPV(0.1, A) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub IRR_TwoDimensions_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + B(0, 0) = -100 + B(1, 0) = 110 + B(0, 1) = -100 + B(1, 1) = 110 + R = IRR(B) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub MIRR_TwoDimensions_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + B(0, 0) = -100 + B(1, 0) = 110 + B(0, 1) = -100 + B(1, 1) = 110 + R = MIRR(B, 0.1, 0.1) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub NPV_TwoDimensions_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + B(0, 0) = -100 + B(1, 0) = 110 + B(0, 1) = -100 + B(1, 1) = 110 + R = NPV(0.1, B) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + +End Module +``` + +**Expected behavior** +5, *Invalid procedure call or argument*, as VB6 and 64-bit VBA raise, for an array that has never been sized, has been erased, or has more than one dimension. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: high for the access violation: the program ends without a chance to save its data, and an array that is filled later, or emptied by `Erase`, is an ordinary state for one passed to these functions. Low for the two dimensions. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +What does not reproduce it: a sized one-dimensional array, whatever its lower bound (`Dim A(1 To 3) As Double` works). + + + +--- + +## `IPmt`, `PPmt`, `MIRR`, `NPV`, `DDB`, `Pmt`, `FV`, `PV` and `SLN` check their arguments differently from 64-bit VBA + +**Describe the bug** +Given these arguments, the Financial functions do something other than 64-bit VBA: +- `IPmt` and `PPmt` take any *per*. For a *per* of 0 or less, or of *nper* + 1 or more, they return a value computed for a period outside the annuity (`IPmt(0.01, 0, 10, 1000)` is -10.946357193576) and raise nothing; VB raises 5, *Invalid procedure call or argument*. +- `MIRR` returns -1 for *values* of one element and a rate for a *finance_rate* or *reinvest_rate* of -1, where VB raises 5; and infinity or NaN for *values* with no element less than 0, where VB raises 11, *Division by zero*. +- `NPV` returns NaN for a *rate* of -1; VB raises 5. +- `DDB` raises -2147467259 (`&H80004005`), *Unspecified error*, for a *life* greater than 0 and less than 1; VB returns the depreciation (`DDB(1000, 100, 0.5, 0.5)` is 900). +- `Pmt` and `PPmt` raise 11 for an *nper* of 0; VB raises 5. +- With a *rate* of -1, `FV` raises 5 for a negative *nper*, where 64-bit VBA raises 6, *Overflow* (VB6 11), and `PV` raises -2147467259, where 64-bit VBA returns 0 for an *nper* of 0 and raises 11 for a positive *nper* and 6 for a negative one (VB6 raises nothing, and returns 0, infinity or NaN). +- `SLN` returns infinity for a result beyond the range of a `Double` (`SLN(1000, 100, 1E-308)`), where 64-bit VBA raises 6 (VB6 returns infinity). + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `financial-argument-checks.twinproj` (attached as `financial-argument-checks.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass and the other sixteen fail. + +The module is meant to be adapted as a regression test. Each case asserts 64-bit VBA's number. The attached VB6 project (`financial-argument-checks-vb6.zip`) prints VB6's, which are the same but for the four cases of `FV`, `PV` and `SLN` that the list above gives. +```vb +' Regression tests: the Financial functions check their arguments as 64-bit VBA does: 5 for a +' period outside 1 to nper (IPmt, PPmt), for an nper of 0 (Pmt, PPmt), for MIRR of one element +' or a rate of -1, and for NPV at a rate of -1; 11 for MIRR with no payment; a DDB of a life +' below 1; and with a rate of -1, FV's 6 and PV's 0, 11 and 6; SLN's 6 for an infinite result. +' VB6 agrees but for FV (11), PV and SLN (no error, infinity). The controls pass in all. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + [TestCase] + Public Sub Control_IPmtLastPeriod() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = IPmt(0.01, 10.9999999, 10, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + End Sub + + [TestCase] + Public Sub Control_PmtRateMinusTwo_Raises11() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Pmt(-2, 10, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 11&, E + End Sub + + [TestCase] + Public Sub Control_DDBLifeOne() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = DDB(1000, 100, 1, 0.5) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 900) < 0.000001 + End Sub + + [TestCase] + Public Sub IPmt_PerZero_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = IPmt(0.01, 0, 10, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub IPmt_PerNperPlusOne_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = IPmt(0.01, 11, 10, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub PPmt_PerZero_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = PPmt(0.01, 0, 10, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub PPmt_PerNperPlusOne_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = PPmt(0.01, 11, 10, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub Pmt_NperZero_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = Pmt(0.01, 0, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub PPmt_NperZero_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = PPmt(0.01, 1, 0, 1000) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub MIRR_OneElement_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + ReDim A(0) + A(0) = -100 + R = MIRR(A, 0.1, 0.1) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub MIRR_ReinvestRateMinusOne_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + ReDim A(2) + A(0) = -100 + A(1) = 50 + A(2) = 60 + R = MIRR(A, 0.1, -1) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub MIRR_NoPayment_Raises11() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + ReDim A(1) + A(0) = 100 + A(1) = 110 + R = MIRR(A, 0.1, 0.1) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 11&, E + End Sub + + [TestCase] + Public Sub NPV_RateMinusOne_Raises5() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + ReDim A(2) + A(0) = -100 + A(1) = 50 + A(2) = 60 + R = NPV(-1, A) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 5&, E + End Sub + + [TestCase] + Public Sub DDB_LifeBelowOne_Returns900() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = DDB(1000, 100, 0.5, 0.5) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 900) < 0.000001 + End Sub + + [TestCase] + Public Sub FV_RateMinusOneNegativeNper_Raises6() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = FV(-1, -1, -100) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 6&, E + End Sub + + [TestCase] + Public Sub PV_RateMinusOneNperZero_Returns0() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = PV(-1, 0, -100) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 0&, E + Assert.Strict.IsTrue Abs(R - 0) < 0.000001 + End Sub + + [TestCase] + Public Sub PV_RateMinusOne_Raises11() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = PV(-1, 10, -100) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 11&, E + End Sub + + [TestCase] + Public Sub PV_RateMinusOneNegativeNper_Raises6() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = PV(-1, -1, -100) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 6&, E + End Sub + + [TestCase] + Public Sub SLN_InfiniteResult_Raises6() + Dim A() As Double, B(1, 1) As Double + Dim E As Long, R As Variant + On Error Resume Next + R = SLN(1000, 100, 1E-308) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 6&, E + End Sub + +End Module +``` + +**Expected behavior** +The numbers 64-bit VBA gives, as listed above. Where VB6 and 64-bit VBA differ (`FV`, `PV`, `SLN`), twinBASIC is to do as 64-bit VBA does. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: medium for `IPmt`, `PPmt`, `MIRR` and `NPV`, which return a value with no error for arguments VB refuses, so a mistake in a program goes unnoticed. Low for the rest: an error is raised where VB raises one, with another number, or (`DDB`) a value VB computes is refused. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +Related: the queued entry on refused arguments (`argument-errors-unspecified-error`) covers these functions' -2147467259 where VB raises 5. + +What does not reproduce it: a *per* just below *nper* + 1 (`IPmt(0.01, 10.9999999, 10, 1000)`); `Pmt` with a *rate* of -2 and an even *nper* (11 in all); `DDB` with a *life* of 1. `PV` already raises 11 for a result beyond the range of a `Double`, as 64-bit VBA does (VB6 returns infinity). + + + +--- + +## `ConvertIconToBitmap`, `GetInheritedOwner`, `AddCustomBinder` and `AddCustomBinderObject` end the program when given `Nothing` + +**Describe the bug** +Five calls end the program with an access violation, in a built exe and in the IDE, 32-bit and 64-bit; in the compiler's test mode the case reports `NATIVE EXCEPTION: ACCESS_VIOLATION`: + +- `ConvertIconToBitmap(Nothing)`. Given any other object that is not an icon (a `Collection`, a bitmap picture) it raises -2147467259, as it should. +- `GetInheritedOwner(Nothing)`, and `GetInheritedOwner` of an object that is not a control: a `Collection` in both builds, a `Scripting.Dictionary` in a 64-bit build (in a 32-bit one it returns). +- `TbExpressionService.AddCustomBinder Nothing`. +- `TbExpressionService.AddCustomBinderObject "D", Nothing`. +- `V = RuntimeCreateGetMessageHook()`, a `Let` of the hook object into a `Variant`. `Set V = RuntimeCreateGetMessageHook()` works, and so does a `Let` of the same object held in a variable declared `As IGetMessageHook`; a `Let` of an object of a twinBASIC class behind an interface without `IDispatch` stores it with no error, so the fault is in the hook object. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `nothing-argument-crashes.twinproj` (attached as `nothing-argument-crashes.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass and the other seven end with an access violation; the `Scripting.Dictionary` case passes in a 32-bit build. + +The module is meant to be adapted as a regression test. A case asserts only that an error is raised (or, for `GetInheritedOwner`, that it returns `Nothing`), not which: VB6 and VBA have none of these procedures. +```vb +' Regression tests: given Nothing, or an object they cannot use, ConvertIconToBitmap, +' GetInheritedOwner, TbExpressionService.AddCustomBinder and AddCustomBinderObject raise a +' run-time error (or, for GetInheritedOwner, return Nothing) instead of ending the program +' with an access violation; and the object RuntimeCreateGetMessageHook returns can be +' assigned to a Variant without Set, as any other object without IDispatch can. On BETA 1010 +' each case but the controls ends with an access violation, 32-bit and 64-bit. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + Private Declare PtrSafe Function LoadIconW Lib "user32" (ByVal Inst As LongPtr, ByVal Name As LongPtr) As LongPtr + + ' The controls: each procedure working, or refusing an argument with an error. + [TestCase] + Public Sub Control_ConvertIconToBitmap_Icon() + Dim Icon As StdPicture = CreateStdPictureFromHandle(LoadIconW(0, 32512), vbPicTypeIcon, False) + Assert.Strict.AreEqual CLng(vbPicTypeBitmap), CLng(ConvertIconToBitmap(Icon).Type) + End Sub + + [TestCase] + Public Sub Control_ConvertIconToBitmap_NotAPicture_Raises() + Dim E As Long, R As Object + On Error Resume Next + Set R = ConvertIconToBitmap(New Collection) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual -2147467259, E + End Sub + + [TestCase] + Public Sub Control_AddCustomBinderObject_Object() + Dim S As New TbExpressionService + S.AddCustomBinderObject "D", CreateObject("Scripting.Dictionary") + Assert.Strict.AreEqual 0, CInt(S.Compile("D.Count").Evaluate()) + End Sub + + [TestCase] + Public Sub ConvertIconToBitmap_Nothing_RaisesAnError() + Dim E As Long, R As Object + On Error Resume Next + Set R = ConvertIconToBitmap(Nothing) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreNotEqual 0&, E + End Sub + + [TestCase] + Public Sub GetInheritedOwner_Nothing_DoesNotCrash() + Dim E As Long, R As Object + On Error Resume Next + Set R = GetInheritedOwner(Nothing) + E = Err.Number + On Error GoTo 0 + Assert.Strict.IsTrue E <> 0 Or R Is Nothing + End Sub + + [TestCase] + Public Sub GetInheritedOwner_Collection_DoesNotCrash() + Dim E As Long, R As Object + On Error Resume Next + Set R = GetInheritedOwner(New Collection) + E = Err.Number + On Error GoTo 0 + Assert.Strict.IsTrue E <> 0 Or R Is Nothing + End Sub + + [TestCase] + Public Sub GetInheritedOwner_Dictionary_DoesNotCrash() + Dim E As Long, R As Object + On Error Resume Next + Set R = GetInheritedOwner(CreateObject("Scripting.Dictionary")) + E = Err.Number + On Error GoTo 0 + Assert.Strict.IsTrue E <> 0 Or R Is Nothing + End Sub + + [TestCase] + Public Sub AddCustomBinder_Nothing_RaisesAnError() + Dim S As New TbExpressionService + Dim E As Long + On Error Resume Next + S.AddCustomBinder Nothing + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreNotEqual 0&, E + End Sub + + [TestCase] + Public Sub AddCustomBinderObject_Nothing_RaisesAnError() + Dim S As New TbExpressionService + Dim E As Long + On Error Resume Next + S.AddCustomBinderObject "D", Nothing + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreNotEqual 0&, E + End Sub + + [TestCase] + Public Sub RuntimeCreateGetMessageHook_LetIntoVariant_StoresTheObject() + Dim V As Variant + V = RuntimeCreateGetMessageHook() + Assert.Strict.AreEqual CInt(vbDataObject), VarType(V) + End Sub + +End Module +``` + +**Expected behavior** +A run-time error for **Nothing**, as `GetNameFromDispID(Nothing, 0)` raises -2147467261, *Invalid pointer*, and as VB's `CallByName` raises 5 for an object of **Nothing**; for `GetInheritedOwner`, an error or **Nothing**. A `Let` of the hook object into a `Variant` stores it, as a `Let` of any other object without `IDispatch` does. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: high: the program ends without a chance to save its data, and an object variable that is still **Nothing** is an ordinary mistake that an error handler should be able to catch. + +A 64-bit build gives the same results, but for `GetInheritedOwner` of a `Scripting.Dictionary` (above). + +What does not reproduce it: any of these procedures given the object it is meant for (the controls), or an object of the wrong kind other than those named (`ConvertIconToBitmap` of a bitmap or a `Collection` raises -2147467259). + + + +--- + +## `Eval` and `TbExpressionService.Compile` accept an expression that ends in an operator or has an unmatched parenthesis + +**Describe the bug** +`Eval` and `TbExpressionService.Compile` accept some malformed expressions without an error: the part they cannot read is left out, and the value is that of what comes before it. `Eval("2 +")`, `Eval("2 Mod")`, `Eval("(2")` and `Eval("2)")` each return 2, and `Compile` of each gives an expression whose value is 2. Other malformed expressions are refused with -2147467259: `"1 2"`, `"2 * (3 + 4"`, `"Sqr(4"`, `")"`. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `expression-service-dangling-tokens.twinproj` (attached as `expression-service-dangling-tokens.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass and the eight other cases fail: no error is raised. + +The module is meant to be adapted as a regression test. A case asserts only that an error is raised, not which. +```vb +' Regression tests: Eval and TbExpressionService.Compile refuse a malformed expression with a +' run-time error, as they refuse "1 2", instead of leaving out the part they cannot read. On +' BETA 1010 an expression that ends in an operator, or has a parenthesis with no partner at +' either end, is accepted, and its value is that of what comes before the fault: each case +' below gives 2. VBScript's Eval refuses each with 1002, Syntax error, or 1006, Expected ')'; +' Access's Eval with 2431, 2435 or 2436. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + Private Function EvalError(ByVal Source As String) As Long + Dim V As Variant + On Error Resume Next + V = Eval(Source) + EvalError = Err.Number + End Function + + Private Function CompileError(ByVal Source As String) As Long + Dim S As New TbExpressionService + Dim E As ITbExpression + S.AddStdLibraryBinder + On Error Resume Next + Set E = S.Compile(Source) + CompileError = Err.Number + End Function + + ' The controls: a well-formed expression, and malformed ones that are refused. + [TestCase] + Public Sub Control_Eval_WellFormed() + Assert.Strict.AreEqual 2, CInt(Eval("(2)")) + End Sub + + [TestCase] + Public Sub Control_Eval_TwoNumbers_Raises() + Assert.Strict.AreNotEqual 0&, EvalError("1 2") + End Sub + + [TestCase] + Public Sub Control_Compile_UnclosedAfterOperator_Raises() + Assert.Strict.AreNotEqual 0&, CompileError("2 * (3 + 4") + End Sub + + [TestCase] + Public Sub Eval_TrailingPlus_Raises() + Assert.Strict.AreNotEqual 0&, EvalError("2 +") + End Sub + + [TestCase] + Public Sub Eval_TrailingMod_Raises() + Assert.Strict.AreNotEqual 0&, EvalError("2 Mod") + End Sub + + [TestCase] + Public Sub Eval_UnclosedParenthesis_Raises() + Assert.Strict.AreNotEqual 0&, EvalError("(2") + End Sub + + [TestCase] + Public Sub Eval_UnopenedParenthesis_Raises() + Assert.Strict.AreNotEqual 0&, EvalError("2)") + End Sub + + [TestCase] + Public Sub Compile_TrailingPlus_Raises() + Assert.Strict.AreNotEqual 0&, CompileError("2 +") + End Sub + + [TestCase] + Public Sub Compile_TrailingMod_Raises() + Assert.Strict.AreNotEqual 0&, CompileError("2 Mod") + End Sub + + [TestCase] + Public Sub Compile_UnclosedParenthesis_Raises() + Assert.Strict.AreNotEqual 0&, CompileError("(2") + End Sub + + [TestCase] + Public Sub Compile_UnopenedParenthesis_Raises() + Assert.Strict.AreNotEqual 0&, CompileError("2)") + End Sub + +End Module +``` + +**Expected behavior** +A run-time error for each, as for `"1 2"`. VBScript's `Eval` (through `MSScriptControl.ScriptControl` in VB6) raises 1002, *Syntax error*, for `"2 +"`, `"2 Mod"` and `"2)"`, and 1006, *Expected ')'*, for `"(2"`; Access's `Eval` in 64-bit VBA raises 2431, *The expression you entered contains invalid syntax.*, 2436 (a missing closing parenthesis) and 2435 (too many closing parentheses). + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: medium: a wrong value with no error, for text a user typed, the very input an expression service is given. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +What does not reproduce it: two operands with no operator (`"1 2"`), an unclosed parenthesis after an operator (`"2 * (3 + 4"`, `"2 + (3"`), an unclosed argument list (`"Sqr(4"`), a lone `")"`, `"+"` or `"-"`. `"2 *"` is accepted, `"2 * "` with a trailing space is refused. + +Related: the queued entry `expression-service-error-numbers`, on the number these functions raise. + + + +--- + +## `Eval` and `ITbExpression.Evaluate` raise -2147467259 for every error inside the expression, instead of passing it on unchanged + +**Describe the bug** +An error that a function, an operator or a bound object's member raises while `Eval` or `ITbExpression.Evaluate` evaluates an expression does not reach the caller as it was raised. Whatever it was, the caller gets -2147467259 (`&H80004005`): `Eval("1 / 0")` raises -2147467259, not 11, *Division by zero*; `CInt("abc")` in an expression not 13, `CInt(40000)` not 6, `Sqr(-1)` not 5; and a bound object's error, a `Scripting.Dictionary`'s `Add` of a key it holds, not 457. Each of these raises its own number when the same code is written in a procedure. + +The description is lost with the number: in a built exe the first such error of the process has the description *Automation error* and every later one *Unspecified error*; in the IDE every one has *Automation error*. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `expression-service-error-numbers.twinproj` (attached as `expression-service-error-numbers.zip`). +2. Run its test cases, the module below. On BETA 1010 the two controls pass and the seven other cases fail, each with -2147467259 (and the description case with *Automation error*). + +The module is meant to be adapted as a regression test. Each case asserts the number, or the description, that the same code raises written in a procedure. +```vb +' Regression tests: an error raised by a function, an operator or a bound object's member +' while Eval or ITbExpression.Evaluate evaluates an expression reaches the caller unchanged, +' with the number and description the same code raises written in a procedure, as VBScript's +' Eval and Access's Eval pass it on. On BETA 1010 each of these raises -2147467259, whose +' description is "Automation error" in the IDE and in an exe's first such error, and +' "Unspecified error" after it. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + Private Function EvalError(ByVal Source As String) As Long + Dim V As Variant + On Error Resume Next + V = Eval(Source) + EvalError = Err.Number + End Function + + ' The controls: an expression that raises nothing, and a bound object's error raised by + ' the same call written in a procedure. + [TestCase] + Public Sub Control_Eval_Sum() + Assert.Strict.AreEqual 4, CInt(Eval("2 + 2")) + End Sub + + [TestCase] + Public Sub Control_DictionaryAddOfAKeyItHolds_Raises457() + Dim D As Object = CreateObject("Scripting.Dictionary") + Dim E As Long + D.Add 1, 1 + On Error Resume Next + D.Add 1, 2 + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 457&, E + End Sub + + [TestCase] + Public Sub Eval_DivisionByZero_Raises11() + Assert.Strict.AreEqual 11&, EvalError("1 / 0") + End Sub + + [TestCase] + Public Sub Eval_DivisionByZero_DescriptionIsDivisionByZero() + Dim V As Variant, D As String + On Error Resume Next + V = Eval("1 / 0") + D = Err.Description + On Error GoTo 0 + Assert.Strict.AreEqual "Division by zero", D + End Sub + + [TestCase] + Public Sub Eval_CIntOfText_Raises13() + Assert.Strict.AreEqual 13&, EvalError("CInt(""abc"")") + End Sub + + [TestCase] + Public Sub Eval_CIntOverflow_Raises6() + Assert.Strict.AreEqual 6&, EvalError("CInt(40000)") + End Sub + + [TestCase] + Public Sub Eval_SqrOfNegative_Raises5() + Assert.Strict.AreEqual 5&, EvalError("Sqr(-1)") + End Sub + + [TestCase] + Public Sub Evaluate_DivisionByZero_Raises11() + Dim S As New TbExpressionService + Dim X As ITbExpression, V As Variant, E As Long + S.AddStdLibraryBinder + Set X = S.Compile("1 / 0") + On Error Resume Next + V = X.Evaluate() + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 11&, E + End Sub + + [TestCase] + Public Sub Evaluate_BoundObjectsError_Raises457() + Dim S As New TbExpressionService + Dim D As Object = CreateObject("Scripting.Dictionary") + Dim X As ITbExpression, V As Variant, E As Long + D.Add 1, 1 + S.AddCustomBinderObject "D", D + Set X = S.Compile("D.Add(1, 2)") + On Error Resume Next + V = X.Evaluate() + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 457&, E + End Sub + +End Module +``` + +**Expected behavior** +The error reaches the caller unchanged, with the number and description it has when the same code is written in a procedure: `Eval` and `Evaluate` add nothing of their own. That is what the VB family's expression evaluators do: Access's `Eval` in 64-bit VBA raises 11, 13, 6 and 5 for the four `Eval` cases, with the same descriptions as direct code, and VBScript's `Eval` (through `MSScriptControl.ScriptControl` in VB6) the same numbers. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: medium: an error handler cannot tell a division by zero from a type mismatch or a bound object's own error, and a handler written for one number never runs. The class's own documentation says an error inside the expression propagates out of `Evaluate` like any other run-time error. + +A 64-bit build gives the same results. 64-bit VBA here is VBA 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +What does not reproduce it: an expression that raises nothing. + +Related: the queued entry `expression-service-dangling-tokens`, on malformed expressions that are accepted. + + + +--- + +## `vbaCastObj` returns `Nothing` for an interface the object does not have, where VB6's `__vbaCastObj` raises 13 + +**Describe the bug** +`vbaCastObj(Obj, IID)` asks *Obj* for the interface whose ID is at the address *IID*. When *Obj* does not have it, `vbaCastObj` returns `Nothing` and raises no error. VB6's own `__vbaCastObj`, which msvbvm60 exports and which `vbaCastObj` mirrors, raises 13, *Type mismatch*, there: it is the function VB6's compiled code calls for a `Set` to a variable of an interface type, and the 13 is the error that `Set` raises. A `String` given as *IID* is the same case: its text is read as the interface ID, which no object has, so VB6 raises 13 and twinBASIC returns `Nothing`. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `vbacastobj-no-type-mismatch.twinproj` (attached as `vbacastobj-no-type-mismatch.zip`). +2. Run its test cases, the module below. On BETA 1010 the three controls pass and the two other cases fail: `Err.Number` is 0. +3. Build and run the VB6 project (attached as `vbacastobj-no-type-mismatch-vb6.zip`), which calls `__vbaCastObj` through a `Declare` with the same arguments. It prints: + ``` + Control_IUnknown_ReturnsTheInterface: 0 an interface + Control_IDispatch_ReturnsTheInterface: 0 an interface + Control_Nothing_ReturnsNothing: 0 Nothing + InterfaceItLacks_Raises13: 13 Nothing + StringGivenAsTheInterfaceId_Raises13: 13 Nothing + ``` + +The module is meant to be adapted as a regression test. Each case asserts what VB6's `__vbaCastObj` does. +```vb +' Regression tests: vbaCastObj raises 13, Type mismatch, when the object does not have the +' interface asked for, as VB6's own __vbaCastObj (msvbvm60) does, and still returns the +' interface when it does, and Nothing for Nothing. On BETA 1010 it returns Nothing with no +' error, for an interface the object lacks and for a String given as the interface ID (whose +' text is not an interface ID). The VB6 project beside this one calls __vbaCastObj with the +' same arguments. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + Private Type InterfaceGuid + Data1 As Long + Data2 As Integer + Data3 As Integer + Data4(0 To 7) As Byte + End Type + + Private Iid As InterfaceGuid + + ' The address of the interface ID {Data1-0000-0000-C000-000000000046}: 0 is IUnknown, + ' &H20400 IDispatch, &H12345 an interface nothing has. + Private Function StdIid(ByVal Data1 As Long) As LongPtr + Iid.Data1 = Data1 + Iid.Data4(0) = &HC0 + Iid.Data4(7) = &H46 + StdIid = VarPtr(Iid) + End Function + + [TestCase] + Public Sub Control_IUnknown_ReturnsTheInterface() + Assert.Strict.IsNotNothing vbaCastObj(New Collection, StdIid(0)) + End Sub + + [TestCase] + Public Sub Control_IDispatch_ReturnsTheInterface() + Assert.Strict.IsNotNothing vbaCastObj(New Collection, StdIid(&H20400)) + End Sub + + [TestCase] + Public Sub Control_Nothing_ReturnsNothing() + Assert.Strict.IsNothing vbaCastObj(Nothing, StdIid(0)) + End Sub + + [TestCase] + Public Sub InterfaceItLacks_Raises13() + Dim E As Long, U As IUnknown + On Error Resume Next + Set U = vbaCastObj(New Collection, StdIid(&H12345)) + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 13&, E + End Sub + + [TestCase] + Public Sub StringGivenAsTheInterfaceId_Raises13() + Dim E As Long, U As IUnknown + On Error Resume Next + Set U = vbaCastObj(New Collection, "{00000000-0000-0000-C000-000000000046}") + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 13&, E + End Sub + +End Module +``` + +**Expected behavior** +13, *Type mismatch*, as VB6's `__vbaCastObj` raises, when the object does not have the interface; the interface when it does; `Nothing` for an object of `Nothing`, as now. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low to medium: code ported from VB6 that relies on the 13 from `__vbaCastObj` goes on with `Nothing`, and code that passes the IID as text, as the function's documentation once said it could, always gets `Nothing` and never learns why. + +A 64-bit build gives the same results. 64-bit VBA cannot be compared: VBE7.DLL exports no `__vba` function. + +What does not reproduce it: an interface the object has (`IUnknown`, `IDispatch` of a `Collection`), and an object of `Nothing`, for which both return `Nothing` with no error. + + + +--- + +## Assigning `Nothing` without `Set` to a variable that is not an object, or comparing with `Nothing` by `=`, compiles, where VB6 and VBA refuse it + +**Describe the bug** +`Nothing` is an object reference: it is assigned with `Set` and compared with `Is`. VB6 and 64-bit VBA refuse, when they compile, a Let of `Nothing` to a variable that is neither an object nor a `Variant` (`Count = Nothing` for a `Long`, a `String` or a `Boolean`), and `Nothing` as an operand of `=` or `<>` (`If Obj = Nothing`, `Count <> Nothing`), each with *Invalid use of object*. twinBASIC compiles every one of them with no diagnostic, and each raises 91, *Object variable or With block variable not set*, when it runs. An error the compiler could report is put off until the line runs, so a mistake on a path that rarely runs is found late or not at all. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `nothing-let-compiles.twinproj` (attached as `nothing-let-compiles.zip`). +2. Compile it. On BETA 1010 it compiles with no error. The module's five lines marked `error` are each one of the forms above; the three marked `none` are the forms VB6 and VBA compile too (`Set Items = Nothing`, `Items Is Nothing`, `Set Value = Nothing`). +3. The same lines in VB6 6.0 and in 64-bit VBA each stop the compile with *Compile error: Invalid use of object*. + +The module is meant to be adapted as a regression test, as compile cases. +```vb +Module NothingLetTests + + Private Sub Uses() + Dim Count As Long, Text As String, Flag As Boolean, Items As Collection, Value As Variant, Obj As Object + Count = Nothing ' CASE LetLong_Nothing: error + Text = Nothing ' CASE LetString_Nothing: error + Flag = Nothing ' CASE LetBoolean_Nothing: error + If Obj = Nothing Then Count = 1 ' CASE EqualsComparison_Nothing: error + Flag = (Count <> Nothing) ' CASE NotEqualsComparison_Nothing: error + Set Items = Nothing ' CASE Control_SetObject: none + If Items Is Nothing Then Count = 1 ' CASE Control_IsNothing: none + Set Value = Nothing ' CASE Control_SetVariant: none + End Sub + +End Module +``` + +**Expected behavior** +A compile error on each of the five lines, as VB6 and 64-bit VBA give (*Invalid use of object*); no change to the three controls. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low: the code is wrong in every runtime, and twinBASIC does report it, but only when the line runs, as error 91. + +A 64-bit build gives the same results. VBA was 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access. + +What does not reproduce it: a Let of `Nothing` to a variable declared `As Object` (`Obj = Nothing`), which VB6 and VBA compile too and which raises 91 in all three, and to a `Variant`, which VB6 and VBA also compile (the queued entry `nothing-let-variant-no-error` is about what twinBASIC does with it). `Items = Nothing` for a `Collection` is refused by twinBASIC as well, with TB5092, *Missing argument 'Index'*, from the collection's default member. + + + +--- + +## A `Variant` assigned `Nothing` without `Set` holds `Nothing`, where VB6 and VBA raise 91 and leave it `Empty` + +**Describe the bug** +`Value = Nothing`, with `Value` a `Variant` and no `Set`, makes `Value` hold `Nothing`, as `Set Value = Nothing` does, and raises no error. VB6 and 64-bit VBA read it as a Let of the default value of an object that is not there: they raise 91, *Object variable or With block variable not set*, and leave the `Variant` `Empty`. Code that leaves out `Set` by mistake therefore runs on in twinBASIC, where in VB6 and VBA it stops. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `nothing-let-variant-no-error.twinproj` (attached as `nothing-let-variant-no-error.zip`). +2. Run its test cases, the module below. On BETA 1010 the two controls pass and the two other cases fail: `Err.Number` is 0, and the `Variant` holds `Nothing`. +3. Build and run the VB6 project (attached as `nothing-let-variant-no-error-vb6.zip`), which runs the same statements. It prints: + ``` + LetVariant: error 91, Value Empty + Control_SetVariant: Value Is Nothing True + Control_LetObject: error 91 + ``` + +The module is meant to be adapted as a regression test. Each case asserts what VB6 and 64-bit VBA do. +```vb +' Regression tests: assigning Nothing to a Variant without Set raises 91, Object variable or With +' block variable not set, and leaves the Variant Empty, as VB6 does: it reads the assignment as +' a Let of the default value of an object that is not there. On BETA 1010 the Variant holds +' Nothing and no error is raised. The controls: Set stores Nothing, and a Let of Nothing to an +' Object variable raises 91 in both. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + [TestCase] + Public Sub LetVariant_Raises91() + Dim Value As Variant + Value = 3 + On Error Resume Next + Value = Nothing + Assert.Strict.AreEqual 91, Err.Number + End Sub + + [TestCase] + Public Sub LetVariant_LeavesItEmpty() + Dim Value As Variant + Value = 3 + On Error Resume Next + Value = Nothing + On Error GoTo 0 + Assert.Strict.IsTrue IsEmpty(Value) + End Sub + + [TestCase] + Public Sub Control_SetVariant_HoldsNothing() + Dim Value As Variant + Value = 3 + Set Value = Nothing + Assert.Strict.IsTrue Value Is Nothing + End Sub + + [TestCase] + Public Sub Control_LetObject_Raises91() + Dim Obj As Object + On Error Resume Next + Obj = Nothing + Assert.Strict.AreEqual 91, Err.Number + End Sub + +End Module +``` + +**Expected behavior** +Error 91, with the `Variant` left `Empty`, as VB6 and 64-bit VBA do. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: low: the missing `Set` goes unreported, and the `Variant` holds what `Set` would have stored. + +A 64-bit build gives the same results. 64-bit VBA, 7.1 (VBE7.DLL 7.01.1158) in 64-bit Microsoft 365 Access, gives what VB6 gives. + +What does not reproduce it: `Set Value = Nothing`, which holds `Nothing` in all three, and a Let of `Nothing` to an `Object` variable, which raises 91 in all three. + +Related: the queued entry `nothing-let-compiles`, on the forms VB6 and VBA refuse when they compile. + + + diff --git a/WIP.Authoring.md b/WIP.Authoring.md index 26636360e..aa5bd8051 100644 --- a/WIP.Authoring.md +++ b/WIP.Authoring.md @@ -127,7 +127,17 @@ both. A page whose title cannot name its subject says so with `symbols:`, which those names. Two pages need it: the (Default) module (`[_HiddenModule]`) and the comparison operators (`["=", "<>", "<", "<=", ">", ">="]`, quoted, since a bare `>` is YAML). On a Core statement page the first name is the statement and the rest its keywords; on an -operator page each is an operator. +operator page each is an operator. **Quote `True`, `False` and `Null` too**: YAML reads them +bare as a boolean and as null, and `symbolPages` fails the build on any entry that is not a +string, rather than index it as `true`. + +Two summary pages outside `/tB/` are read as well (`SUMMARY_PAGES` in +[builder/symbols.mjs](builder/symbols.mjs)): each name of their `symbols:` is at the `##` +heading of that name, a `type` on `Reference/Data-Types.md` and a `keyword` on +`Reference/Literal-Keywords.md` (owner, 2026-10-29: one page for **Nothing**, **Null**, +**Empty**, **True** and **False**, a section each). `keyword` rather than `constant`, because +the search ranks a keyword with no package first and a constant not; it is also VBA's word +for them. A name with no such heading fails the build. The build names any page in a package folder that gives the index no entry, after its summary line. That is the cue for a third. **Do not add `symbols:` to change how a page diff --git a/WIP.Build.md b/WIP.Build.md index cfa27f50e..6b9830693 100644 --- a/WIP.Build.md +++ b/WIP.Build.md @@ -190,7 +190,7 @@ opens in the IDE and builds (`bundle_extra`, `docs/_config.yml`). It is laid out for byte; nothing checks that they agree, so a change to `src/` is packed again by hand (`node scripts/impexp.mjs import scripts/impexp/impexp.twinproj scripts/impexp/src --overwrite`). It has the same command line, output, exit codes and `--self-test` (the same -20 tests) as `impexp.mjs` and `impexp.py`, and writes byte-identical project files. Its tests +21 tests) as `impexp.mjs` and `impexp.py`, and writes byte-identical project files. Its tests are that `--self-test` and the commands of `check_impexp_parity.mjs`, which compares it when `IMPEXP_EXE` names its built exe, because no gate can build it; CI never sets the variable. The check runs the exe through `launchOnDesktop` with `cwd` (`TBBUILD_CWD` in diff --git a/WIP.Errors.md b/WIP.Errors.md index 30fd8e945..aba6e02dd 100644 --- a/WIP.Errors.md +++ b/WIP.Errors.md @@ -70,7 +70,38 @@ harnesses (`dialog`, `keys`, `window`): 164 cases, all agreeing in all four runt Shell; DoEvents quiet; Beep nothing (D3). `argument-errors-unspecified-error` extended, three entries queued (*Batch 6b findings*). No row is `bug: pending`, and no question is open. -**Next: batch 7**, VBA `FileSystem`. +**Batch 7 is done** (2026-10-27), VBA `FileSystem`, under a new harness (`files`): 448 cases in +`errors/VBA.FileSystem.yml`, all agreeing in all four runtimes; sections on 21 of the module's 22 +pages (Reset raises nothing; the index documents nothing). The owner's rulings applied (six +extensions, three bugs); `file-errors-unspecified` extended by 30 cases, three entries queued +(*Batch 7 findings*). No row is `bug: pending`, and no question is open. + +**Batch 8 is done** (2026-10-28), VBA `Collection` and `ErrObject`: 259 cases in +`errors/VBA.Collection.yml` and `errors/VBA.ErrObject.yml`, all agreeing in all four runtimes; +sections on 10 of the 20 member pages (Count, both Clears, Items, Keys, LastDllError and +LastHresult raise nothing; HelpFile, HelpContext and ReturnHResult cannot be probed, their +assignment failing twinBASIC's build; the indexes document nothing). The owner's rulings +applied (two extensions, one bug); three entries queued (*Batch 8 findings*). No row is `bug: +pending`, and no question is open. + +**Batch 9 is done** (2026-10-28), VBA `Financial`: 349 cases in `errors/VBA.Financial.yml`, +all agreeing in all four runtimes; sections on all 13 member pages (the index documents +nothing); `-2147418113` (`E_UNEXPECTED`) added to `messages.yml` and measured. The owner's +rulings applied: five entries queued, `argument-errors-unspecified-error` extended, and PV's 11 +for an infinite result an extension (*Batch 9 findings*). No row is `bug: pending`, and no +question is open. + +**Batch 10 is done** (2026-10-28), VBA `HiddenModule`, +`Compilation` and `TbExpressionService`: 280 cases in `errors/VBA.HiddenModule.yml`, +`VBA.Compilation.yml` and `VBA.TbExpressionService.yml`, every one `only: tb` (VB6 and VBA have +none of these procedures), all agreeing in both twinBASIC builds; sections on 35 pages; two new +harnesses, `native` and `expression`; four HRESULTs added to `messages.yml` and measured. The +owner's rulings applied: four entries queued (`nothing-argument-crashes`, +`expression-service-dangling-tokens`, `expression-service-error-numbers`, +`vbacastobj-no-type-mismatch`), and `CreateGUID`'s braceless form and `AllocMem(0)`'s error are +the spec (*Batch 10 findings*). No question is open. + +**Next: batch 11**, Core file I/O statements. ## Owner's decisions @@ -84,7 +115,10 @@ entries queued (*Batch 6b findings*). No row is `bug: pending`, and no question VB6, and twinBASIC 64-bit, compared with **64-bit VBA, whose behaviour must be respected** (owner): a difference between twinBASIC 64-bit and 64-bit VBA is a bug, exactly as one from VB6 is, with a NOTE and a queued reproducer. (Before: twinBASIC 64-bit for operators and - `Math` only, and no VBA.) + `Math` only, and no VBA.) **Where VB6 and 64-bit VBA differ, twinBASIC does as 64-bit VBA + does**, in both builds (owner, 2026-10-28): matching 64-bit VBA is an extension against VB6 + (`only: [tb, vba64]` cases, a NOTE in the prose), and matching neither is a bug whose + reproducer asserts 64-bit VBA's number. - **Scope.** `docs/Reference/Default/` (VBA, VB, VBRUN), `docs/Reference/Core/` (statements and operators) and `docs/Reference/Built-In/`. Not `Contributed/`. - **Existing error prose moves into the tables.** A page that states an error in a sentence @@ -177,19 +211,14 @@ What `errors.mjs messages` found (BETA 1010): these the two messages differ, and the file says `{ raised, error }`. The sections show `Err.Description` (owner: settled); showing `Error$(n)` instead would be a one-line change in `renderSection` (`.raised` to `.error`). -- **Differences from VB6 that batch 8 (`ErrObject`) should take up**, none queued yet (batch 3 - queued `Error$` above 65535, `error-function-above-65535`): - `Err.Raise n` of a number from 1 to 65535 without a message of its own leaves - `Err.Description` empty in twinBASIC below 747, and from 747 up gives the Windows system - message of that number (*Page fault was a transition fault.* for 747); VB6 gives - *Application-defined or object-defined error* (the central page says so in a NOTE). - VB6's `Err.Raise` of a common HRESULT raises its own number instead (`&H80070057` raises 5, - `&H80004001` 445, `&H80004002` 430, `&H80020003` 438, `&H80040154` 429, ...) and describes - the rest as *Automation error* with the HRESULT's text on a second line; twinBASIC keeps the - HRESULT and its text. `Err.Raise 65536` raises 5 in VB6, and `Error$(65536)` raises 6; - twinBASIC raises 65536 as *Automation error*, and `Error$(65536)` is generic. twinBASIC's - `Error$` of a few HRESULTs is VB's message (`Error$(&H80070057)` is *Invalid procedure call - or argument*), VB6's is generic. +- **Differences from VB6 found here, and where each went** (batch 8 took them up): + `Err.Raise n` without a description, *Application-defined or object-defined error* in VB6 and + empty or a system message in twinBASIC, is filed (`err-raise-defaults`, #2504; the central + page's NOTE). VB's `Err.Raise` of a common HRESULT as its own number is queued + (`err-raise-hresult-mapping`). `Err.Raise 65536`, 5 in VB6, is an extension (owner, + 2026-10-28; the Raise page's NOTE). `Error$` above 65535 is filed (`error-function-above-65535`). Left alone: + twinBASIC's `Error$` of a few HRESULTs is VB's message (`Error$(&H80070057)` is *Invalid + procedure call or argument*), VB6's is generic. ## Tooling @@ -376,6 +405,31 @@ Windows refuses). `check_errors.mjs` reports a harness the data names and has no becomes the foreground one, and AppActivate raises nothing for that; in twinBASIC, activating a new window there takes 2 s from the third on (not on the user's desktop), so `errors.mjs` gives a twinBASIC probe 120 s. +- **`files`** (batch 7) --- every procedure that names or opens a file. `files_Begin` makes + `files_Dir`, `tbdocsErrorsProbe` and the process id under the temporary folder, makes it the + current directory (Windows' own calls, so no statement under test sets it up) and fills it: + `a.txt` (`abc`, CR LF, `def`, CR LF: 10 bytes), `empty.txt`, `ro.txt` (read-only), `hid.txt` + (hidden), the empty folder `sub` and `full`, which holds `f.txt`. A case names its files + relatively. `files_End` closes every file, puts the current directory back and deletes the + folder whatever its files' attributes, keeping the case's error; it deletes nothing whose path + does not end in the folder's own name. `files_Begin` raises 1003 when it cannot make or enter + the folder, so a case never runs in another folder. `files_Attr(name)` is + `GetFileAttributesW` (-1: no such file), `files_Expect Cond` raises 1001, `files_NoDrive` is a + letter no drive has. **A probe line that starts `Reset:` is a label**, not the statement: put + `Reset` on a line of its own (`run: "Reset\n..."`). Check after a run that no + `tbdocsErrorsProbe*` folder is left in `%TEMP%`. +- **`native`** (batch 10) --- **twinBASIC only**: every case that uses it is `only: tb`, so it uses + twinBASIC syntax. What the (Default) module's procedures take: `native_Icon` (the shared + `IDI_APPLICATION` icon), `native_Bitmap` (a 2 x 2 bitmap that `native_End` deletes unless + `native_Keep` says a picture owns it), `native_Window` (a hidden `STATIC` window, made by + `native_Begin`), `native_Handler` (a `GetMessageHookHelper.GetMessageHandler` that does + nothing), `native_StdIid(Data1)` (the address of the interface ID + `{Data1-0000-0000-C000-000000000046}`, for `vbaCastObj`) and the enumeration `native_Kind` + (-3 to 7). +- **`expression`** (batch 10) --- twinBASIC only. In a built exe the first error the expression + service (`Eval`, `TbExpressionService`) raises in a process has the description *Automation + error*, every later one *Unspecified error*; `expression_Begin` raises the first once and + clears it, so a case's description does not depend on which case runs first (`--only`). **YAML pitfalls**, each met in batch 2: @@ -443,6 +497,14 @@ What the probes must respect (BETA 1010; `errors.mjs` already does all of it): builds a failing project again without the case named in the error. - **A case that allocates near the string limit raises 14** (`String(1073741823, "a")` in 32-bit): use it to show that the 5 above the limit is a check of its own, never as a row. +- **A case that crashes stops the probe, and every later case reports `none`** (`the probe + stopped before it ended: exception 0xc0000005`), as a hang does. Explore one page per probe + (a data file holding only that page), then one case per probe for a page that stops; a case + that crashes is a WARNING by hand, never a case. +- **The probe is one module**: a case cannot declare a class, an interface or a `Type`. A + harness can hold a `Type`, an `Enum`, a `Declare` and procedures; a fact that needs a class + (`GetNameFromDispID` on a twinBASIC class) is checked with `tbrun` on a tree of its own and + stated in the page's prose. ## Batch 2 findings (VBA `Strings`, BETA 1010) @@ -864,6 +926,243 @@ twinBASIC's InputBox fault (a string it never set); a reproducer's VB6 project m InputBox when it carries the dialog harness (`reproAnswersBoxes`). The broker's startup race (four clients, all ENOENT) is fixed on both sides (WIP.Broker.md). +## Batch 7 findings (VBA `FileSystem`, BETA 1010, 2026-10-27) + +twinBASIC 64-bit agrees with 32-bit on every case, and 64-bit VBA with VB6, so every difference +is twinBASIC against both. + +- **-2147467259 where VB raises a number, almost everywhere** (`file-errors-unspecified`, queued + since BETA 1005 and extended by 30 cases): 5, 52, 53, 54, 62, 63, 67, 68, 75, 76. The page's old + hand NOTEs went; the rows' generated NOTEs replace them. The Core pages (Open, Name, Get, Put, + Print, Line Input) keep their hand NOTEs until batch 11. What VB raises for a bad name differs + by procedure: GetAttr 5 for `<` or a file name ending in a backslash, FileLen and FileDateTime + 53 for those, Dir 52 for the backslash; a wildcard is 52 everywhere but ChDir, MkDir and RmDir + (76) and Kill (53 when nothing matches). +- **Null raises 94 everywhere**, but `CurDir(Null)` and `Dir(Null)` raise 13 in VB + (`filesystem-argument-checks`). +- **Owner's rulings (2026-10-27)**. Extensions, with a NOTE saying what VB does and + twinBASIC-only quiet cases: Dir's *attributes* outside 0 to 63 (unknown bits ignored; VB 5); + a non-String argument to Dir and CurDir, converted as ChDir and ChDrive convert theirs in VB + (`Dir(5)`; VB 13); FileCopy of a source open for reading only, not locked against reading + (VB 55 for any mode but Input); SetAttr's *attributes* wherever Windows accepts them ("tB + extension where the attributes are valid for the underlying WINAPI; where the WINAPI fails, + SetAttr must correctly raise an error"; VB 5 for anything but sums of 1, 2, 4, 32, and 16 on a + folder); Width from 256 to 32,767 (VB 5). Bugs (`filesystem-argument-checks`): a negative Width + (no error, a new line before each item), FileLen and FileDateTime of a wildcard that matches (the + first match's value; VB 52), and with them `Input(0)` (-2147467259; VB returns `""`) and the + Null 13. +- **Kill deletes hidden and system files** (`kill-hidden-system-files`, a WARNING): VB raises 53 + for one named alone and skips it in a wildcard. +- **FileCopy's numbers** (`filecopy-error-numbers`): 70 for a folder or a read-only or hidden + destination (VB 75), 55 for a copy onto itself or of a source open `Lock Read` (VB 70). +- **SetAttr of a file open for writing**: no error (VB 55), `setattr-open-file-no-error`, queued + since BETA 1005; its WARNING stays beside the generated NOTE. +- **Agree in all four**: FileCopy's 53, 55 and 76; Binary-mode `Input` past the end returns + what is left, with no error; `InputB(0)` returns `""`; `FreeFile` takes any *rangenumber* + (nonzero after rounding is 256 to 511), and with 256 to 511 all in use `FreeFile(1)` returns + 512, which `Open` accepts; Reset closes files 256 to 511 too; Width does not wrap inside one + item; Kill stops at an open file a wildcard matches, after the files before it. +- **Page edits**: the error sentences of Dir, FileCopy, Kill, RmDir and Seek went into the + tables; Width's parameter is an **Integer** from 0 to 32,767 (32,768 is the conversion's 6); + CurDir's section moved below both functions, and ChDrive's, Dir's, MkDir's and RmDir's above + their See Also, which those pages put before the Example (`spliceSection` places a new + section before the first Example, so a page with See Also first needs moving by hand once). + +## Batch 8 findings (VBA `Collection` and `ErrObject`, BETA 1010, 2026-10-27) + +twinBASIC 64-bit agrees with 32-bit on every case, and 64-bit VBA with VB6. + +- **Assigning `Err.HelpFile`, `Err.HelpContext`, or `Err.ReturnHResult` in a module procedure, + compiles and fails the build** with a codegen error (`err-helpfile-helpcontext-codegen`); in + test mode the procedure aborts. So the data has no case for those three pages (a case would + fail the whole probe): each page has a hand NOTE, and Raise's *helpfile* and *helpcontext* + are covered on the Raise page. When it is fixed, add `Err.HelpFile = Null` and + `Err.HelpContext = Null` (94 in VB6 and VBA) as rows. ReturnHResult's example now sits in a + class (its build failed as a module function; `examples.bat --build` is clean). +- **VB turns HRESULTs into VB numbers; twinBASIC does not** (`err-raise-hresult-mapping`): + `Err.Raise` of 22 HRESULTs tried raises a VB number in VB6 and 64-bit VBA (`E_INVALIDARG` + 5, `E_NOTIMPL` 445, the `DISP_E_` ones, `&H800A0000 + n` as *n*, ...), and `Err.Number =` of + one stores the VB number without raising. Data rows cannot hold one `vb6:` per HRESULT, so + the Raise page has a hand NOTE with the table, and twinBASIC's side is `only: tb` quiet cases + (wrapped, below). `Err.Raise &H800A0000` raises nothing at all in VB6 and VBA; not written. +- **How a case says "raised what it was asked"**: `Err.Raise n` (and `Err.Number = n`) then + `If Err.Number = n Then Err.Clear`, so the case reports 0, and a runtime that raises or keeps + another number reports that. `Err.Number = 65536` raises 5 in VB (an `On Error GoTo` handler + is entered); `Err.Number = &H80070057` only stores 5. +- **Collection** (`collection-index-checks`): a `Currency`, `Decimal` or `Date` index is + refused (Item and Remove 9, Add's position 13; VB takes it as a number); Item of a number on + an empty collection 9 (VB 5; Remove is 5 in both); Item and Remove of `Null`, an object, an + array, an error value or a number beyond `Long` 9 (VB 13); Add's position of -1, `True` or + `Empty` 13 (VB 9). Agree in all four: duplicate key 457 (`""` is a key); a non-String key 13 + (a number too: VB does not convert it); both positions 5; a position on an empty collection + 5, `Null` there too; a string position or index that is no key 5. +- **Owner's rulings (2026-10-28)**. Extensions, each with a NOTE in the page's prose and + `only: tb` quiet cases: `Err.Raise` and `Err.Number =` of a number above 65535 (twinBASIC + raises or sets it; VB raises 5), and `Collection.Add` with *before* one more than **Count** + (adds at the end) or *after* 0 (adds at the start; VB raises 9 for both). A bug, in + `collection-index-checks`: `Collection.Add` with a *key* of **Empty** adds the item without a + key (VB 13). +- **Page edits**: the error sentences of Add, Item, Remove and KeyCompareMode went into the + tables; the central page has a NOTE on `Err.Raise` of an HRESULT. + +## Batch 9 findings (VBA `Financial`, BETA 1010, 2026-10-28) + +twinBASIC 64-bit agrees with 32-bit on every case. Unlike the earlier batches, **VB6 and 64-bit +VBA differ** where a result is infinite: VB6 returns infinity from SLN and PV, 64-bit VBA raises +6 or 11 (the rows carry `vba64:`). The values behind the defects below were read with a value +probe (twinBASIC through `tbrun`, VB6 through `vb6run`), not only the error numbers. + +- **Every parameter is a Double** in twinBASIC's declarations and in VBA's; FV, PV, Pmt and + PPmt said **Integer** for *nper* (and PPmt for *per*), from VBA-Docs. Fixed on the pages. +- **The -2147467259 family** (`argument-errors-unspecified-error`): every argument check where + VB raises 5 (and Rate's 6), in DDB, SLN, SYD, IPmt, PPmt, Pmt, NPer, Rate and IRR. +- **Rate fails for almost every annuity**: -2147467259 for every case tried with *pmt* other + than 0, and for `Rate(10, 0, 1000, -2000)`; it answers `Rate(1, 0, 100, -110)` and + `Rate(2, 0, 100, -121)`. It also raises 5 for an *nper* below 1 with *pmt* 0 (VB answers), and + never checks *guess*: **Null**, `"abc"` and an error value pass (VB 94, 13, 13). +- **NPer raises 6 when *rate* and *pmt* have opposite signs**, which is every loan and savings + plan at a positive rate (`NPer(0.01, -100, 1000)` is 10.59 in VB6); where it answers, the value + is right. Where no number of periods exists it raises 6 or 11, VB 5. +- **Null in an optional Variant argument raises 13**, VB 94: *pv*/*fv* and *type* of FV, PV, Pmt, + NPer, IPmt, PPmt and Rate, and DDB's *factor*. +- **Arrays**: IRR, MIRR and NPV of an array never sized **crash** (access violation, 32-bit and + 64-bit, IDE and exe; VB 5), a hand WARNING on each page and no case; a 2-D array raises + -2147418113, *Catastrophic failure* (VB 5). +- **Missing checks that return a value** (hand WARNINGs beside the generated NOTEs): IPmt and + PPmt take any *per* (VB 5 when *per* is 0 or less, or *nper* + 1 or more); MIRR returns -1 for + one element, a rate for a *finance_rate* or *reinvest_rate* of -1 (VB 5), and infinity or NaN + for *values* with no negative element (VB 11); NPV returns NaN for a *rate* of -1 (VB 5). +- **Other differences**: DDB refuses a *life* between 0 and 1 (VB answers); Pmt and PPmt raise 11 + for an *nper* of 0 (VB 5); FV raises 5 for *rate* -1 and a negative *nper* (VB6 11, 64-bit VBA + 6); PV raises -2147467259 for a *rate* of -1 (VB6 returns infinity or 0; 64-bit VBA 11, 6, or 0 + for *nper* 0). +- **Infinite results**: PV raises 11 (as 64-bit VBA does; VB6 returns infinity), SLN returns + infinity (as VB6 does; 64-bit VBA raises 6). FV raises 6 in all four. +- **Agree in all four**: **Null** in a required argument 94; a string that is not a number in an + optional argument 13; Pmt's 6 and 11 for a *rate* of -1 or -2; IRR's guess 94 and 13; FV's 6; + any *type* is taken (only 0 means the end of the period). +- **Page edits**: SLN's, SYD's and DDB's "All arguments must be positive numbers" and NPV's (and + MIRR's half of) "must contain at least one negative value and one positive value" are + `` comments (all four take what they forbid); IRR's and Rate's "If ... can't find a + result after 20 tries, it fails" is reworded, the table carrying the error; the module index no + longer says MIRR takes a *guess* or that every array needs both signs. + +- **Rate's wrong root** (found writing its reproducer): for a lump sum (*pmt* 0) over an even + number of periods it returns -2.1 where VB6 returns 0.1 (`Rate(2, 0, 100, -121)`, + `Rate(4, 0, 100, -146.41)`; one and three periods are right). Silent, so a WARNING by hand on + Rate, in `rate-fails`. Not in the data: a value, not an error. + +**Owner's rulings (2026-10-28)**: + +1. **The five entries accepted**, as proposed, and queued with `tests` reproducers and VB6 + sides: `rate-fails` (3 PASS, 12 FAIL), `nper-opposite-signs-overflow` (3, 9), + `financial-null-optional-argument` (3, 9), `financial-array-arguments` (3 PASS, 4 + ACCESS_VIOLATION, 3 FAIL), `financial-argument-checks` (3, 16). `argument-errors-unspecified-error` + extended by one case per function (DDB, SLN, SYD, IPmt, PPmt, Pmt, NPer, Rate twice, IRR: 3 + PASS, 35 FAIL now). +2. **Where VB6 and 64-bit VBA differ, twinBASIC does as 64-bit VBA does.** So PV's 11 for an + infinite result is an extension against VB6 (cases `only: [tb, vba64]`, a NOTE in PV's + prose), and SLN's infinity is a bug (`financial-argument-checks`, asserting 6). +3. **A *rate* of -1 in FV and PV, by the same rule**: FV is to raise 6, and PV 0, 11 or 6 by + the sign of *nper*, as 64-bit VBA; in `financial-argument-checks`, whose cases assert 64-bit + VBA's numbers (its VB6 side prints VB6's, which differ in those four cases). + +A reproducer's case asserts 64-bit VBA's number where the two VB runtimes differ; say so in the +entry, since the VB6 project then disagrees with the module. + +## Batch 10 findings (VBA `HiddenModule`, `Compilation`, `TbExpressionService`, BETA 1010, 2026-10-28) + +Every procedure is twinBASIC's own, so every case is `only: tb` and no row has a `vb6:`: a +defect here is one against the page or against what the procedure is for, not against VB. +twinBASIC 64-bit agrees with 32-bit on every case but `AllocMem(2000000000)` (a `win64: 0` +row). The pages with no section raise nothing of their own: `Compilation`'s eight (compile-time +intrinsics), `CreateGUID`, the `GetDeclared...` six, `StackArgsSize`, `StackOffset`, +`InterlockedIncrement32`/`Decrement32`, `Start`, `Stop`, `RuntimeCreateGetMessageHook`, +`UnprotectedAccess`, `vbaAryMove`, `vbaRefVarAry`, `AddStdLibraryBinder`; and `Emit`, `EmitAny`, +`InternalGetReturnAddress` (fails a 64-bit build), `AddCustomBinder` and `Bind` have no case. + +- **Null raises 94** for every numeric or string argument, **424** for an object one + (`AddClassInstanceFlag`, `ConvertIconToBitmap`, `GetInheritedOwner`, `GetNameFromDispID`, + `PictureToByteArray`, `vbaCastObj`). +- **Raw HRESULTs** where the procedure fails: `AllocMem` of 0 or less -2147024882 + (`E_OUTOFMEMORY`, as for a block too big); `CreateStdPictureFromHandle` -2147467259 for a + handle of the wrong kind and -2147418113 for a *Type* outside 0 to 4 (a bitmap handle is never + checked); `ConvertIconToBitmap` -2147467259 for anything but an icon, 5 and 13 for its + *BackColor* as `TranslateColor`; `PictureToByteArray` -2147467262 for a non-picture; + `GetNameFromDispID`'s four (the page's hand table, now data), and `ShowPropertyPageEditor`'s + -2147467261 and -2147467262. On an object of a twinBASIC class `GetNameFromDispID` raises 5 + (-4: -2147319765), checked with `tbrun` on a tree with a class: a sentence on the page. +- **The expression service** (`Eval`, `Compile`, `Evaluate`): every failure is -2147467259, a + name it cannot resolve and a syntax error alike, and so is **any error inside the expression** + (11, 13, 5, a bound object's 457): its number is lost (NOTEs on Eval and Evaluate). + `Eval("CVErr(5)")` and `Eval("Len(Null)")` raise it too, though `Len(Null)` is **Null** + outside an expression. **Some malformed expressions are accepted silently**: `"2 +"`, + `"2 Mod"`, `"(2"`, `"2)"` give 2 (WARNINGs on Eval and Compile; quiet cases). The first error of + a process says *Automation error*, later ones *Unspecified error*; in the IDE, always + *Automation error* (NOTEs; the `expression` harness). +- **Crashes** (access violation, 32-bit and 64-bit; a WARNING by hand on each page, no case): + `ConvertIconToBitmap(Nothing)`; `GetInheritedOwner` of **Nothing** or of an object that is not + a control (a `Collection` in both builds, a `Scripting.Dictionary` in 64-bit); + `AddCustomBinder Nothing`; `AddCustomBinderObject name, Nothing`; and `V = + RuntimeCreateGetMessageHook()` into a Variant without `Set` (`Set V =` is fine, and a Let of + another non-dispatch object, a twinBASIC class behind an `IUnknown` interface, stores it with + no error, so the fault is the hook object's). Also, with no WARNING (misuse of an internal or + of a raw address): `AddClassInstanceFlag` of -1, an address of 0 to `GetMem*`, `PutMem*` and + `vbaCopyBytes` (`FreeMem 0` and `vbaObjAddref 0` are quiet), and `vbaCastObj` given a number or + (32-bit) **Null** as *IID*. +- **Pages corrected from what twinBASIC does**: `CreateGUID` returns 36 characters without + braces (the page said `{...}`; the index pages too); `vbaCastObj` reads *IID* as the address of a + GUID, so a String gives **Nothing** silently (the page said it took a String; its example and + `GetDeclaredTypeIid`'s now use `VarPtr` of a GUID and print the IID); `PictureToByteArray(Nothing)` + is a `Byte` array with no bounds; `FreeMem 0` does nothing; `Compile` without a binder takes + `"1 + 1"`; `AllocMem`'s and `Compile`'s error sentences went into the tables. Added: + `RegisterMessage` checks no argument, `InterlockedIncrement32`/`Decrement32` wrap with no error. +- **Out of this batch**: `Set X = obj` where *X* is declared as an interface *obj* does not + have raises -2147467262 (`E_NOINTERFACE`); VB6 raises 13. Not checked in VB6 here, not queued: + for a Core batch. + +**Owner's rulings (2026-10-28)**, and what was done: + +1. **The crashes: one entry**, GetInheritedOwner of a non-control in it: queued as + `nothing-argument-crashes` (3 PASS, 7 FAIL with ACCESS_VIOLATION; the `Scripting.Dictionary` + case passes in 32-bit), the five WARNINGs tagged. +2. **Malformed expressions: an entry**, `expression-service-dangling-tokens` (3 PASS, 8 FAIL), the + two WARNINGs and the quiet cases' comments tagged. +3. **The lost error number: "investigate what is most appropriate from a tB user's perspective, + and proceed accordingly"**. Both VB-family evaluators keep the error: VBScript's `Eval` + (`MSScriptControl.ScriptControl` from VB6) raises 11, 13 (*Type mismatch: 'CInt'*), 6, 5 and + 450 for `1 / 0`, `CInt("abc")`, `CInt(40000)`, `Sqr(-1)`, `Sqr(1, 2)`, and 1002 *Syntax error* + or 1006 *Expected ')'* for every malformed expression; Access's `Eval` (64-bit VBA, VbaSession) + raises 11, 13, 6, 5, returns **Null** for `Len(Null)`, and raises 2431, 2434, 2435, 2436, 2439 + for syntax errors and argument counts. So a bug: queued as `expression-service-error-numbers`. + **Owner, then: "Eval should not be special casing errors"** unless VB does, which it does + not, and **the entry asks only for errors to be passed through unchanged** (2 PASS, 7 FAIL: + 11, 13, 6, 5 and a bound object's 457, and the description *Division by zero*). Left out of it, + and in no entry: a compile failure's number and description (no direct code to compare: + `Sqr(1, 2)` does not compile), `Eval("Len(Null)")` and `Eval("CVErr(5)")` (Eval fails where + direct code returns a value), and the unstable description (the description NOTEs, untagged, + and the `expression` harness). **Owner: they stay NOTEs, with no entry** (untagged, so no + retired entry removes them: check them against each new BETA, as every labelled callout is). + The Evaluate page's prose states the propagation, the tagged NOTEs say what BETA 1010 does. +4. **"What does VBx do?" for `vbaCastObj`**: VB6's own `__vbaCastObj` (msvbvm60, called through a + `Declare`) returns the interface for a GUID's address, 0 for **Nothing**, and raises **13, + Type mismatch** for an interface the object lacks, a String's text read as an IID included; + twinBASIC returns **Nothing** for both. VBE7.DLL exports no `__vba` procedure, so 64-bit VBA + cannot be asked. **Owner: a bug.** Queued as `vbacastobj-no-type-mismatch` (3 PASS, 2 FAIL; + VB6 side calling `__vbaCastObj`); the data's row is `error: 0`, `vb6: 13` (its cases `only: + tb`, so the NOTE names VB6 alone); the page states the 13 and has a WARNING. +5. **"What does VBx do?" for `CreateGUID`**: nothing to compare. VB6 and VBA have no CreateGUID, + nor does Access (`Application.CreateGUID` 438, `Eval("CreateGUID()")` 2425), and no runtime + exports one. VB code makes one with `CreateObject("Scriptlet.TypeLib").Guid`, which is braced + (`{...}`, and two trailing NULs: 40 characters), or `StringFromGUID2`, braced. twinBASIC's + `GetDeclaredTypeIid` and its kin return the braced form; `CreateGUID` the 36-character form the + `ClassId` and `InterfaceId` attributes take. **Owner: the existing behaviour is the spec**, + as the page now states it. +6. **"What does VBx do?" for `AllocMem(0)`**: nothing to compare: no AllocMem in VB6, VBA, msvbvm60 + or VBE7 (their GetMem/PutMem family has no allocator). Windows' allocators (`HeapAlloc`, + `CoTaskMemAlloc`, `GlobalAlloc`) return a block for 0 bytes. **Owner: the existing behaviour + is the spec**: the page's row of -2147024882 for 0 or less stands. + ## The 64-bit VBA re-check (BETA 1010, Office 365 x64, VBA 7.1 VBE7.DLL 7.01.1158, 2026-10-22) Batches 2 to 5 and `Core.yml` in all four runtimes. Every runtime agreed with the data as it @@ -945,10 +1244,10 @@ each can be run when there is budget. Status is kept here. | 5 | VBA `DateTime` | 20 | done: 17 sections, 328 cases; owner's questions answered 2026-10-22 | | 6a | VBA `Information`, and `Interaction`'s non-interactive procedures: `CallByName`, `CallByDispId`, `Choose`, `Command`, `CreateObject`, `GetObject`, `Environ`, `IIf`, `If`, `Partition`, `Switch`, `RaiseEventByName`, `RaiseEventByName2`, `SaveSetting`, `GetSetting`, `GetAllSettings`, `DeleteSetting` (owner split batch 6, 2026-10-22) | 45 | done: 27 sections, 302 cases; four entries queued 2026-10-22 | | 6b | `Interaction`'s interactive procedures: `AppActivate`, `Beep`, `DoEvents`, `InputBox`, `MsgBox`, `SendKeys`, `Shell`, under the `dialog`, `keys` and `window` harnesses (`Shell` only with programs that do not exist or `cmd.exe /c exit`) | 7 | done: 5 sections, 164 cases; three entries queued 2026-10-27 | -| 7 | VBA `FileSystem` | 23 | not started | -| 8 | VBA `Collection` and `ErrObject` (and the `Err.Raise` differences under *The central page*) | 22 | not started | -| 9 | VBA `Financial` | 14 | not started | -| 10 | VBA `HiddenModule`, then `Compilation` and `TbExpressionService` (`Constants` has nothing to raise) | 68 | not started | +| 7 | VBA `FileSystem`, under the `files` harness | 23 | done: 21 sections, 448 cases; three entries queued, one extended, 2026-10-27 | +| 8 | VBA `Collection` and `ErrObject` (and the `Err.Raise` differences under *The central page*) | 22 | done: 10 sections, 259 cases; three entries queued 2026-10-27; owner's rulings 2026-10-28 | +| 9 | VBA `Financial` | 14 | done: 13 sections, 349 cases; five entries queued, one extended; owner's rulings 2026-10-28 | +| 10 | VBA `HiddenModule`, then `Compilation` and `TbExpressionService` (`Constants` has nothing to raise) | 68 | 35 sections, 280 cases, harnesses `native` and `expression`; four entries queued; owner's rulings 2026-10-28 | | 11 | Core file I/O statements (`Open`, `Get`, `Put`, `Input`, ...: errors 52--75) | 15 | not started | | 12 | Core arrays and control flow (`ReDim`, `Erase`, `Resume`, `Return`, ...); ReDim has data already, in `errors/Core.yml` | 15 | not started | | 13 | Operators: the matrix generator and its pages | 25 | not started | diff --git a/add-in/Resources/SYMBOLS/symbols.json b/add-in/Resources/SYMBOLS/symbols.json index d8e114075..4c7f00bb7 100644 --- a/add-in/Resources/SYMBOLS/symbols.json +++ b/add-in/Resources/SYMBOLS/symbols.json @@ -18,6 +18,11 @@ {"name":"Single","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#single"}, {"name":"String","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#string"}, {"name":"Variant","package":null,"container":null,"kind":"type","url":"/Reference/Data-Types#variant"}, + {"name":"Empty","package":null,"container":null,"kind":"keyword","url":"/Reference/Literal-Keywords#empty"}, + {"name":"False","package":null,"container":null,"kind":"keyword","url":"/Reference/Literal-Keywords#false"}, + {"name":"Nothing","package":null,"container":null,"kind":"keyword","url":"/Reference/Literal-Keywords#nothing"}, + {"name":"Null","package":null,"container":null,"kind":"keyword","url":"/Reference/Literal-Keywords#null"}, + {"name":"True","package":null,"container":null,"kind":"keyword","url":"/Reference/Literal-Keywords#true"}, {"name":"AddressOf","package":null,"container":null,"kind":"operator","url":"/tB/Core/AddressOf"}, {"name":"Alias","package":null,"container":null,"kind":"statement","url":"/tB/Core/Alias"}, {"name":"And","package":null,"container":null,"kind":"operator","url":"/tB/Core/And"}, diff --git a/bugs/argument-errors-unspecified-error/argument-errors-unspecified-error.twinproj b/bugs/argument-errors-unspecified-error/argument-errors-unspecified-error.twinproj index aa20aba3f..19807f567 100644 Binary files a/bugs/argument-errors-unspecified-error/argument-errors-unspecified-error.twinproj and b/bugs/argument-errors-unspecified-error/argument-errors-unspecified-error.twinproj differ diff --git a/bugs/argument-errors-unspecified-error/repro.json b/bugs/argument-errors-unspecified-error/repro.json index 9617c6156..870a96a27 100644 --- a/bugs/argument-errors-unspecified-error/repro.json +++ b/bugs/argument-errors-unspecified-error/repro.json @@ -30,7 +30,17 @@ "^FAIL RegressionTests.MsgBox_HelpFileWithoutContext_Raises5: .*Actual: -2147467259 ", "^FAIL RegressionTests.InputBox_HelpFileWithoutContext_Raises5: .*Actual: -2147467259 ", "^FAIL RegressionTests.SendKeys_UnknownKeyCode_Raises5: .*Actual: -2147352567 ", - "^FAIL RegressionTests.SendKeys_WaitNull_Raises94: .*Actual: -2147352567 " + "^FAIL RegressionTests.SendKeys_WaitNull_Raises94: .*Actual: -2147352567 ", + "^FAIL RegressionTests.DDB_LifeZero_Raises5: .*Actual: -2147467259 ", + "^FAIL RegressionTests.SLN_LifeZero_Raises5: .*Actual: -2147467259 ", + "^FAIL RegressionTests.SYD_PeriodAboveLife_Raises5: .*Actual: -2147467259 ", + "^FAIL RegressionTests.IPmt_NperZero_Raises5: .*Actual: -2147467259 ", + "^FAIL RegressionTests.PPmt_NperNegative_Raises5: .*Actual: -2147467259 ", + "^FAIL RegressionTests.Pmt_RateAndNperZero_Raises5: .*Actual: -2147467259 ", + "^FAIL RegressionTests.NPer_RateAndPmtZero_Raises5: .*Actual: -2147467259 ", + "^FAIL RegressionTests.Rate_NperZero_Raises5: .*Actual: -2147467259 ", + "^FAIL RegressionTests.Rate_NperBelowOneOverflow_Raises6: .*Actual: -2147467259 ", + "^FAIL RegressionTests.IRR_NoReceipt_Raises5: .*Actual: -2147467259 " ] } } diff --git a/bugs/argument-errors-unspecified-error/src/Sources/RegressionTests.twin b/bugs/argument-errors-unspecified-error/src/Sources/RegressionTests.twin index e266f8e40..6f5bd9d38 100644 --- a/bugs/argument-errors-unspecified-error/src/Sources/RegressionTests.twin +++ b/bugs/argument-errors-unspecified-error/src/Sources/RegressionTests.twin @@ -236,4 +236,86 @@ Module RegressionTests Assert.Strict.AreEqual 94&, Err.Number End Sub + ' The Financial functions (batch 9). + [TestCase] + Public Sub DDB_LifeZero_Raises5() + Dim R As Double + On Error Resume Next + R = DDB(1000, 100, 0, 1) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub SLN_LifeZero_Raises5() + Dim R As Double + On Error Resume Next + R = SLN(1000, 100, 0) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub SYD_PeriodAboveLife_Raises5() + Dim R As Double + On Error Resume Next + R = SYD(1000, 100, 5, 6) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub IPmt_NperZero_Raises5() + Dim R As Double + On Error Resume Next + R = IPmt(0.01, 1, 0, 1000) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub PPmt_NperNegative_Raises5() + Dim R As Double + On Error Resume Next + R = PPmt(0.01, 1, -10, 1000) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Pmt_RateAndNperZero_Raises5() + Dim R As Double + On Error Resume Next + R = Pmt(0, 0, 1000) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub NPer_RateAndPmtZero_Raises5() + Dim R As Double + On Error Resume Next + R = NPer(0, 0, 1000) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Rate_NperZero_Raises5() + Dim R As Double + On Error Resume Next + R = Rate(0, -100, 1000) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + + [TestCase] + Public Sub Rate_NperBelowOneOverflow_Raises6() + Dim R As Double + On Error Resume Next + R = Rate(0.5, -100, 1000) + Assert.Strict.AreEqual 6&, Err.Number + End Sub + + [TestCase] + Public Sub IRR_NoReceipt_Raises5() + Dim R As Double, F(1) As Double + F(0) = -100: F(1) = -110 + On Error Resume Next + R = IRR(F) + Assert.Strict.AreEqual 5&, Err.Number + End Sub + End Module diff --git a/bugs/argument-errors-unspecified-error/vb6/Module1.bas b/bugs/argument-errors-unspecified-error/vb6/Module1.bas index c488ae46e..7c035b03b 100644 --- a/bugs/argument-errors-unspecified-error/vb6/Module1.bas +++ b/bugs/argument-errors-unspecified-error/vb6/Module1.bas @@ -379,5 +379,17 @@ Sub Main() dialog_End SendKeys "{BAD}": Report "SendKeys_UnknownKeyCode" SendKeys "", Null: Report "SendKeys_WaitNull" + Dim R As Double, F(1) As Double + R = DDB(1000, 100, 0, 1): Report "DDB_LifeZero" + R = SLN(1000, 100, 0): Report "SLN_LifeZero" + R = SYD(1000, 100, 5, 6): Report "SYD_PeriodAboveLife" + R = IPmt(0.01, 1, 0, 1000): Report "IPmt_NperZero" + R = PPmt(0.01, 1, -10, 1000): Report "PPmt_NperNegative" + R = Pmt(0, 0, 1000): Report "Pmt_RateAndNperZero" + R = NPer(0, 0, 1000): Report "NPer_RateAndPmtZero" + R = Rate(0, -100, 1000): Report "Rate_NperZero" + R = Rate(0.5, -100, 1000): Report "Rate_NperBelowOneOverflow" + F(0) = -100: F(1) = -110 + R = IRR(F): Report "IRR_NoReceipt" Close #9 End Sub diff --git a/bugs/collection-index-checks/collection-index-checks.twinproj b/bugs/collection-index-checks/collection-index-checks.twinproj new file mode 100644 index 000000000..683245a79 Binary files /dev/null and b/bugs/collection-index-checks/collection-index-checks.twinproj differ diff --git a/bugs/collection-index-checks/repro.json b/bugs/collection-index-checks/repro.json new file mode 100644 index 000000000..997ca6d2b --- /dev/null +++ b/bugs/collection-index-checks/repro.json @@ -0,0 +1,25 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests\\.Control_ItemOfLongDoubleByte$", + "^PASS RegressionTests\\.Control_ItemOutOfRange_Raises9$", + "^PASS RegressionTests\\.Control_ItemOfMissingKey_Raises5$", + "^FAIL RegressionTests\\.Item_Currency_ReturnsItem: .*Actual: 9 ", + "^FAIL RegressionTests\\.Item_Decimal_ReturnsItem: .*Actual: 9 ", + "^FAIL RegressionTests\\.Item_Date_ReturnsItem: .*Actual: 9 ", + "^FAIL RegressionTests\\.Remove_Currency_Removes: .*Actual: 9 ", + "^FAIL RegressionTests\\.AddBefore_Currency_Adds: .*Actual: 13 ", + "^FAIL RegressionTests\\.Item_EmptyCollection_Raises5: .*Actual: 9 ", + "^FAIL RegressionTests\\.Item_Null_Raises13: .*Actual: 9 ", + "^FAIL RegressionTests\\.Item_Object_Raises13: .*Actual: 9 ", + "^FAIL RegressionTests\\.Item_BeyondLong_Raises13: .*Actual: 9 ", + "^FAIL RegressionTests\\.Remove_Null_Raises13: .*Actual: 9 ", + "^FAIL RegressionTests\\.AddBefore_MinusOne_Raises9: .*Actual: 13 ", + "^FAIL RegressionTests\\.AddBefore_Empty_Raises9: .*Actual: 13 ", + "^FAIL RegressionTests\\.AddBefore_BeyondLong_Raises13: .*Actual: 9 ", + "^FAIL RegressionTests\\.Add_EmptyKey_Raises13: .*Actual: 0 " + ] + } +} diff --git a/bugs/collection-index-checks/src/Settings b/bugs/collection-index-checks/src/Settings new file mode 100644 index 000000000..e887e7c49 --- /dev/null +++ b/bugs/collection-index-checks/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "CollectionIndexChecks", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Collection's Item, Remove and Add check an index or a position differently from VB", + "project.exportPathIsV2": true, + "project.id": "{4A7DA874-1C47-458C-A7CB-31B020EF778B}", + "project.name": "CollectionIndexChecks", + "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/collection-index-checks/src/Sources/RegressionTests.twin b/bugs/collection-index-checks/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..08e5b44f8 --- /dev/null +++ b/bugs/collection-index-checks/src/Sources/RegressionTests.twin @@ -0,0 +1,128 @@ +' Regression tests: Collection's Item, Remove and Add take an index or a position as VB6 and +' 64-bit VBA do: a Currency, Decimal or Date number like any other number; 5 for a number on +' an empty collection (Item); 13 for Null, an object or a number outside the Long range +' (Item, Remove); 9 for a position of -1, True or Empty, and 13 for a key of Empty (Add). The controls use a Long, a +' Double and a Byte, and a string key. tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + ' A collection holding "a" and "b", without keys. + Private Function Two() As Collection + Set Two = New Collection + Two.Add "a" + Two.Add "b" + End Function + + ' The error Item(Index) raises on C, or 0 and the item in Got. + Private Function ItemError(ByVal C As Collection, ByVal Index As Variant, Optional ByRef Got As Variant) As Long + On Error Resume Next + Got = C.Item(Index) + ItemError = Err.Number + End Function + + Private Function RemoveError(ByVal C As Collection, ByVal Index As Variant) As Long + On Error Resume Next + C.Remove Index + RemoveError = Err.Number + End Function + + Private Function AddBeforeError(ByVal C As Collection, ByVal Before As Variant) As Long + On Error Resume Next + C.Add "x", , Before + AddBeforeError = Err.Number + End Function + + [TestCase] + Public Sub Control_ItemOfLongDoubleByte() + Dim Got As Variant + Assert.Strict.AreEqual 0&, ItemError(Two(), 2&, Got) + Assert.Strict.AreEqual "b", Got + Assert.Strict.AreEqual 0&, ItemError(Two(), 2#, Got) + Assert.Strict.AreEqual 0&, ItemError(Two(), CByte(2), Got) + End Sub + + [TestCase] + Public Sub Control_ItemOutOfRange_Raises9() + Assert.Strict.AreEqual 9&, ItemError(Two(), 3) + End Sub + + [TestCase] + Public Sub Control_ItemOfMissingKey_Raises5() + Assert.Strict.AreEqual 5&, ItemError(Two(), "z") + End Sub + + [TestCase] + Public Sub Item_Currency_ReturnsItem() + Dim Got As Variant + Assert.Strict.AreEqual 0&, ItemError(Two(), CCur(2), Got) + End Sub + + [TestCase] + Public Sub Item_Decimal_ReturnsItem() + Assert.Strict.AreEqual 0&, ItemError(Two(), CDec(2)) + End Sub + + [TestCase] + Public Sub Item_Date_ReturnsItem() + Assert.Strict.AreEqual 0&, ItemError(Two(), CDate(2)) + End Sub + + [TestCase] + Public Sub Remove_Currency_Removes() + Assert.Strict.AreEqual 0&, RemoveError(Two(), CCur(2)) + End Sub + + [TestCase] + Public Sub AddBefore_Currency_Adds() + Assert.Strict.AreEqual 0&, AddBeforeError(Two(), CCur(2)) + End Sub + + [TestCase] + Public Sub Item_EmptyCollection_Raises5() + Assert.Strict.AreEqual 5&, ItemError(New Collection, 1) + End Sub + + [TestCase] + Public Sub Item_Null_Raises13() + Assert.Strict.AreEqual 13&, ItemError(Two(), Null) + End Sub + + [TestCase] + Public Sub Item_Object_Raises13() + Assert.Strict.AreEqual 13&, ItemError(Two(), New Collection) + End Sub + + [TestCase] + Public Sub Item_BeyondLong_Raises13() + Assert.Strict.AreEqual 13&, ItemError(Two(), 2147483648#) + End Sub + + [TestCase] + Public Sub Remove_Null_Raises13() + Assert.Strict.AreEqual 13&, RemoveError(Two(), Null) + End Sub + + [TestCase] + Public Sub AddBefore_MinusOne_Raises9() + Assert.Strict.AreEqual 9&, AddBeforeError(Two(), -1) + End Sub + + [TestCase] + Public Sub AddBefore_Empty_Raises9() + Assert.Strict.AreEqual 9&, AddBeforeError(Two(), Empty) + End Sub + + [TestCase] + Public Sub AddBefore_BeyondLong_Raises13() + Assert.Strict.AreEqual 13&, AddBeforeError(Two(), 2147483648#) + End Sub + + [TestCase] + Public Sub Add_EmptyKey_Raises13() + Dim C As New Collection + On Error Resume Next + C.Add "x", Empty + Assert.Strict.AreEqual 13&, Err.Number + End Sub + +End Module diff --git a/bugs/collection-index-checks/src/Sources/Startup.twin b/bugs/collection-index-checks/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/collection-index-checks/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/collection-index-checks/vb6/Module1.bas b/bugs/collection-index-checks/vb6/Module1.bas new file mode 100644 index 000000000..497ab605b --- /dev/null +++ b/bugs/collection-index-checks/vb6/Module1.bas @@ -0,0 +1,67 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The VB6 side of bugs/collection-index-checks: the cases of src/Sources/RegressionTests.twin, +' in the same order, each printing the error VB6 raises (0: none). Build and run it with: +' node scripts/bug_repro.mjs vb6 collection-index-checks + +Private Function Two() As Collection + Set Two = New Collection + Two.Add "a" + Two.Add "b" +End Function + +Private Function ItemError(ByVal C As Collection, ByVal Index As Variant) As Long + Dim Got As Variant + On Error Resume Next + Got = C.Item(Index) + ItemError = Err.Number +End Function + +Private Function RemoveError(ByVal C As Collection, ByVal Index As Variant) As Long + On Error Resume Next + C.Remove Index + RemoveError = Err.Number +End Function + +Private Function AddBeforeError(ByVal C As Collection, ByVal Before As Variant) As Long + On Error Resume Next + C.Add "x", , Before + AddBeforeError = Err.Number +End Function + +Private Sub Report(ByVal Name As String, ByVal Number As Long) + Print #9, Name & ": " & Number +End Sub + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Report "Control_ItemOfLongDoubleByte", ItemError(Two(), 2&) + ItemError(Two(), 2#) + ItemError(Two(), CByte(2)) + Report "Control_ItemOutOfRange_Raises9", ItemError(Two(), 3) + Report "Control_ItemOfMissingKey_Raises5", ItemError(Two(), "z") + Report "Item_Currency_ReturnsItem", ItemError(Two(), CCur(2)) + Report "Item_Decimal_ReturnsItem", ItemError(Two(), CDec(2)) + Report "Item_Date_ReturnsItem", ItemError(Two(), CDate(2)) + Report "Remove_Currency_Removes", RemoveError(Two(), CCur(2)) + Report "AddBefore_Currency_Adds", AddBeforeError(Two(), CCur(2)) + Report "Item_EmptyCollection_Raises5", ItemError(New Collection, 1) + Report "Item_Null_Raises13", ItemError(Two(), Null) + Report "Item_Object_Raises13", ItemError(Two(), New Collection) + Report "Item_BeyondLong_Raises13", ItemError(Two(), 2147483648#) + Report "Remove_Null_Raises13", RemoveError(Two(), Null) + Report "AddBefore_MinusOne_Raises9", AddBeforeError(Two(), -1) + Report "AddBefore_Empty_Raises9", AddBeforeError(Two(), Empty) + Report "AddBefore_BeyondLong_Raises13", AddBeforeError(Two(), 2147483648#) + Dim C As New Collection + On Error Resume Next + C.Add "x", Empty + Report "Add_EmptyKey_Raises13", Err.Number + On Error GoTo Fail + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub diff --git a/bugs/collection-index-checks/vb6/Probe.vbp b/bugs/collection-index-checks/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/collection-index-checks/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/err-helpfile-helpcontext-codegen/err-helpfile-helpcontext-codegen.twinproj b/bugs/err-helpfile-helpcontext-codegen/err-helpfile-helpcontext-codegen.twinproj new file mode 100644 index 000000000..eaf7a4397 Binary files /dev/null and b/bugs/err-helpfile-helpcontext-codegen/err-helpfile-helpcontext-codegen.twinproj differ diff --git a/bugs/err-helpfile-helpcontext-codegen/repro.json b/bugs/err-helpfile-helpcontext-codegen/repro.json new file mode 100644 index 000000000..8c42fb175 --- /dev/null +++ b/bugs/err-helpfile-helpcontext-codegen/repro.json @@ -0,0 +1,17 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.Control_SetDescription$", + "^PASS RegressionTests.Control_SetSource$", + "^PASS RegressionTests.Control_SetNumber$", + "^PASS RegressionTests.Control_LateBoundHelpFileAndContext$", + "^PASS RegressionTests.Control_SetReturnHResultInClassMethod$", + "^FAIL RegressionTests.SetHelpFile: aborted", + "^FAIL RegressionTests.SetHelpContext: aborted", + "^FAIL RegressionTests.SetHelpFileThroughErrObjectVariable: aborted", + "^FAIL RegressionTests.SetReturnHResultInModule: aborted" + ] + } +} diff --git a/bugs/err-helpfile-helpcontext-codegen/src/Settings b/bugs/err-helpfile-helpcontext-codegen/src/Settings new file mode 100644 index 000000000..6b9ff2575 --- /dev/null +++ b/bugs/err-helpfile-helpcontext-codegen/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ErrHelpfileHelpcontextCodegen", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Assigning Err.HelpFile or Err.HelpContext fails the build with a codegen error", + "project.exportPathIsV2": true, + "project.id": "{214C222C-5F1B-40EE-BADD-FD3A99F370A4}", + "project.name": "ErrHelpfileHelpcontextCodegen", + "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/err-helpfile-helpcontext-codegen/src/Sources/RegressionTests.twin b/bugs/err-helpfile-helpcontext-codegen/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..0492347d7 --- /dev/null +++ b/bugs/err-helpfile-helpcontext-codegen/src/Sources/RegressionTests.twin @@ -0,0 +1,71 @@ +' The issue's regression tests. Each assigns one property of Err and reads it back. +' The controls (Description, Source, Number, HelpFile and HelpContext through an +' Object variable, and ReturnHResult in a class method) pass; HelpFile and +' HelpContext assigned through Err itself, or through a variable declared As +' ErrObject, and ReturnHResult in a module procedure, do not build. +[TestFixture] +Module RegressionTests + + [TestCase] + Public Sub Control_SetDescription() + Err.Description = "x" + Assert.Strict.AreEqual "x", Err.Description + End Sub + + [TestCase] + Public Sub Control_SetSource() + Err.Source = "x" + Assert.Strict.AreEqual "x", Err.Source + End Sub + + [TestCase] + Public Sub Control_SetNumber() + Err.Number = 5 + Assert.Strict.AreEqual 5&, Err.Number + Err.Clear + End Sub + + [TestCase] + Public Sub Control_LateBoundHelpFileAndContext() + Dim o As Object + Set o = Err + o.HelpFile = "x" + o.HelpContext = 5 + Assert.Strict.AreEqual "x", Err.HelpFile + Assert.Strict.AreEqual 5&, Err.HelpContext + End Sub + + [TestCase] + Public Sub SetHelpFile() + Err.HelpFile = "x" + Assert.Strict.AreEqual "x", Err.HelpFile + End Sub + + [TestCase] + Public Sub SetHelpContext() + Err.HelpContext = 5 + Assert.Strict.AreEqual 5&, Err.HelpContext + End Sub + + [TestCase] + Public Sub SetHelpFileThroughErrObjectVariable() + Dim e As ErrObject + Set e = Err + e.HelpFile = "x" + Assert.Strict.AreEqual "x", Err.HelpFile + End Sub + + [TestCase] + Public Sub Control_SetReturnHResultInClassMethod() + Dim t As New Thing + t.Status + Assert.Strict.AreEqual 1&, Err.LastHresult + End Sub + + [TestCase] + Public Sub SetReturnHResultInModule() + Err.ReturnHResult = 1 + Assert.Strict.AreEqual 0&, Err.Number + End Sub + +End Module diff --git a/bugs/err-helpfile-helpcontext-codegen/src/Sources/Startup.twin b/bugs/err-helpfile-helpcontext-codegen/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/err-helpfile-helpcontext-codegen/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/err-helpfile-helpcontext-codegen/src/Sources/Thing.twin b/bugs/err-helpfile-helpcontext-codegen/src/Sources/Thing.twin new file mode 100644 index 000000000..f826c80c1 --- /dev/null +++ b/bugs/err-helpfile-helpcontext-codegen/src/Sources/Thing.twin @@ -0,0 +1,8 @@ +' A class whose method sets the HRESULT it returns, as the ReturnHResult page shows. +Class Thing + + Public Function Status() As Long + Err.ReturnHResult = 1 + End Function + +End Class diff --git a/bugs/err-helpfile-helpcontext-codegen/vb6/Module1.bas b/bugs/err-helpfile-helpcontext-codegen/vb6/Module1.bas new file mode 100644 index 000000000..390320ccb --- /dev/null +++ b/bugs/err-helpfile-helpcontext-codegen/vb6/Module1.bas @@ -0,0 +1,25 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The VB6 side of err-helpfile-helpcontext-codegen: the same assignments as the +' twinBASIC tests, each read back. Build and run with: +' node scripts/bug_repro.mjs vb6 err-helpfile-helpcontext-codegen + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Err.HelpFile = "x" + Print #9, "HelpFile: [" & Err.HelpFile & "]" + Err.HelpContext = 5 + Print #9, "HelpContext: " & Err.HelpContext + Dim e As ErrObject + Set e = Err + e.HelpFile = "y" + Print #9, "HelpFile through ErrObject: [" & Err.HelpFile & "]" + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub diff --git a/bugs/err-helpfile-helpcontext-codegen/vb6/Probe.vbp b/bugs/err-helpfile-helpcontext-codegen/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/err-helpfile-helpcontext-codegen/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/err-raise-hresult-mapping/err-raise-hresult-mapping.twinproj b/bugs/err-raise-hresult-mapping/err-raise-hresult-mapping.twinproj new file mode 100644 index 000000000..fa084a0a6 Binary files /dev/null and b/bugs/err-raise-hresult-mapping/err-raise-hresult-mapping.twinproj differ diff --git a/bugs/err-raise-hresult-mapping/repro.json b/bugs/err-raise-hresult-mapping/repro.json new file mode 100644 index 000000000..0d6f9649f --- /dev/null +++ b/bugs/err-raise-hresult-mapping/repro.json @@ -0,0 +1,20 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests\\.Control_Raise5_Raises5$", + "^PASS RegressionTests\\.Control_RaiseEFail_RaisesEFail$", + "^PASS RegressionTests\\.Control_RaiseDispException_RaisesDispException$", + "^FAIL RegressionTests\\.Raise_EInvalidArg_Raises5: .*Actual: -2147024809 ", + "^FAIL RegressionTests\\.Raise_EOutOfMemory_Raises7: .*Actual: -2147024882 ", + "^FAIL RegressionTests\\.Raise_ENotImpl_Raises445: .*Actual: -2147467263 ", + "^FAIL RegressionTests\\.Raise_ENoInterface_Raises430: .*Actual: -2147467262 ", + "^FAIL RegressionTests\\.Raise_DispUnknownName_Raises438: .*Actual: -2147352570 ", + "^FAIL RegressionTests\\.Raise_DispTypeMismatch_Raises13: .*Actual: -2147352571 ", + "^FAIL RegressionTests\\.Raise_ClassNotRegistered_Raises429: .*Actual: -2147221164 ", + "^FAIL RegressionTests\\.Raise_VbErrorAsHResult_Raises53: .*Actual: -2146828235 ", + "^FAIL RegressionTests\\.Number_EInvalidArg_Is5: .*Actual: -2147024809 " + ] + } +} diff --git a/bugs/err-raise-hresult-mapping/src/Settings b/bugs/err-raise-hresult-mapping/src/Settings new file mode 100644 index 000000000..759972e5d --- /dev/null +++ b/bugs/err-raise-hresult-mapping/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ErrRaiseHresultMapping", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Err.Raise and Err.Number keep an HRESULT that VB turns into a VB error number", + "project.exportPathIsV2": true, + "project.id": "{F3098B1E-1A6C-44D4-B74D-2CC4A070AE85}", + "project.name": "ErrRaiseHresultMapping", + "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/err-raise-hresult-mapping/src/Sources/RegressionTests.twin b/bugs/err-raise-hresult-mapping/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..9d9f68414 --- /dev/null +++ b/bugs/err-raise-hresult-mapping/src/Sources/RegressionTests.twin @@ -0,0 +1,77 @@ +' Regression tests: Err.Raise of an HRESULT that has a VB error of the same meaning raises +' that VB error, as VB6 and 64-bit VBA do, and assigning one to Err.Number stores the VB +' error. The controls raise a VB number and an HRESULT that VB keeps as it is. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + Private Function Raised(ByVal Number As Long) As Long + On Error Resume Next + Err.Raise Number + Raised = Err.Number + End Function + + [TestCase] + Public Sub Control_Raise5_Raises5() + Assert.Strict.AreEqual 5&, Raised(5) + End Sub + + [TestCase] + Public Sub Control_RaiseEFail_RaisesEFail() + Assert.Strict.AreEqual &H80004005, Raised(&H80004005) + End Sub + + [TestCase] + Public Sub Control_RaiseDispException_RaisesDispException() + Assert.Strict.AreEqual &H80020009, Raised(&H80020009) + End Sub + + [TestCase] + Public Sub Raise_EInvalidArg_Raises5() + Assert.Strict.AreEqual 5&, Raised(&H80070057) + End Sub + + [TestCase] + Public Sub Raise_EOutOfMemory_Raises7() + Assert.Strict.AreEqual 7&, Raised(&H8007000E) + End Sub + + [TestCase] + Public Sub Raise_ENotImpl_Raises445() + Assert.Strict.AreEqual 445&, Raised(&H80004001) + End Sub + + [TestCase] + Public Sub Raise_ENoInterface_Raises430() + Assert.Strict.AreEqual 430&, Raised(&H80004002) + End Sub + + [TestCase] + Public Sub Raise_DispUnknownName_Raises438() + Assert.Strict.AreEqual 438&, Raised(&H80020006) + End Sub + + [TestCase] + Public Sub Raise_DispTypeMismatch_Raises13() + Assert.Strict.AreEqual 13&, Raised(&H80020005) + End Sub + + [TestCase] + Public Sub Raise_ClassNotRegistered_Raises429() + Assert.Strict.AreEqual 429&, Raised(&H80040154) + End Sub + + [TestCase] + Public Sub Raise_VbErrorAsHResult_Raises53() + Assert.Strict.AreEqual 53&, Raised(&H800A0035) + End Sub + + [TestCase] + Public Sub Number_EInvalidArg_Is5() + Err.Number = &H80070057 + Dim N As Long = Err.Number + Err.Clear + Assert.Strict.AreEqual 5&, N + End Sub + +End Module diff --git a/bugs/err-raise-hresult-mapping/src/Sources/Startup.twin b/bugs/err-raise-hresult-mapping/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/err-raise-hresult-mapping/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/err-raise-hresult-mapping/vb6/Module1.bas b/bugs/err-raise-hresult-mapping/vb6/Module1.bas new file mode 100644 index 000000000..ac80235d2 --- /dev/null +++ b/bugs/err-raise-hresult-mapping/vb6/Module1.bas @@ -0,0 +1,45 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The VB6 side of bugs/err-raise-hresult-mapping: the cases of src/Sources/RegressionTests.twin, +' in the same order, each printing the number Err holds after it. Build and run it with: +' node scripts/bug_repro.mjs vb6 err-raise-hresult-mapping + +Private Function Raised(ByVal Number As Long) As Long + On Error Resume Next + Err.Raise Number + Raised = Err.Number +End Function + +Private Sub Report(ByVal Name As String, ByVal Number As Long) + Print #9, Name & ": " & Number +End Sub + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 + Report "Control_Raise5_Raises5", Raised(5) + Report "Control_RaiseEFail_RaisesEFail", Raised(&H80004005) + Report "Control_RaiseDispException_RaisesDispException", Raised(&H80020009) + Report "Raise_EInvalidArg_Raises5", Raised(&H80070057) + Report "Raise_EOutOfMemory_Raises7", Raised(&H8007000E) + Report "Raise_ENotImpl_Raises445", Raised(&H80004001) + Report "Raise_ENoInterface_Raises430", Raised(&H80004002) + Report "Raise_DispUnknownName_Raises438", Raised(&H80020006) + Report "Raise_DispTypeMismatch_Raises13", Raised(&H80020005) + Report "Raise_ClassNotRegistered_Raises429", Raised(&H80040154) + Report "Raise_VbErrorAsHResult_Raises53", Raised(&H800A0035) + Dim N As Long + On Error Resume Next + Err.Number = &H80070057 + N = Err.Number + Err.Clear + On Error GoTo Fail + Report "Number_EInvalidArg_Is5", N + Close #9 + Exit Sub +Fail: + On Error Resume Next + Print #9, "unhandled error " & Err.Number & ": " & Err.Description + Close #9 +End Sub diff --git a/bugs/err-raise-hresult-mapping/vb6/Probe.vbp b/bugs/err-raise-hresult-mapping/vb6/Probe.vbp new file mode 100644 index 000000000..4d210b0e8 --- /dev/null +++ b/bugs/err-raise-hresult-mapping/vb6/Probe.vbp @@ -0,0 +1,7 @@ +Type=Exe +Module=Module1; Module1.bas +Startup="Sub Main" +ExeName32="Probe.exe" +Name="Probe" +CompilationType=0 +OptimizationType=0 diff --git a/bugs/expression-service-dangling-tokens/expression-service-dangling-tokens.twinproj b/bugs/expression-service-dangling-tokens/expression-service-dangling-tokens.twinproj new file mode 100644 index 000000000..ffcdd4e2f Binary files /dev/null and b/bugs/expression-service-dangling-tokens/expression-service-dangling-tokens.twinproj differ diff --git a/bugs/expression-service-dangling-tokens/repro.json b/bugs/expression-service-dangling-tokens/repro.json new file mode 100644 index 000000000..28947be0b --- /dev/null +++ b/bugs/expression-service-dangling-tokens/repro.json @@ -0,0 +1,19 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.Control_Eval_WellFormed$", + "^PASS RegressionTests.Control_Eval_TwoNumbers_Raises$", + "^PASS RegressionTests.Control_Compile_UnclosedAfterOperator_Raises$", + "^FAIL RegressionTests.Eval_TrailingPlus_Raises: .*Actual: 0 ", + "^FAIL RegressionTests.Eval_TrailingMod_Raises: .*Actual: 0 ", + "^FAIL RegressionTests.Eval_UnclosedParenthesis_Raises: .*Actual: 0 ", + "^FAIL RegressionTests.Eval_UnopenedParenthesis_Raises: .*Actual: 0 ", + "^FAIL RegressionTests.Compile_TrailingPlus_Raises: .*Actual: 0 ", + "^FAIL RegressionTests.Compile_TrailingMod_Raises: .*Actual: 0 ", + "^FAIL RegressionTests.Compile_UnclosedParenthesis_Raises: .*Actual: 0 ", + "^FAIL RegressionTests.Compile_UnopenedParenthesis_Raises: .*Actual: 0 " + ] + } +} diff --git a/bugs/expression-service-dangling-tokens/src/Settings b/bugs/expression-service-dangling-tokens/src/Settings new file mode 100644 index 000000000..24f386f05 --- /dev/null +++ b/bugs/expression-service-dangling-tokens/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ExpressionServiceDanglingTokens", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Eval and TbExpressionService.Compile accept an expression that ends in an operator or has an unmatched parenthesis", + "project.exportPathIsV2": true, + "project.id": "{31980062-F599-41C1-87EA-9FD72E3D3E1E}", + "project.name": "ExpressionServiceDanglingTokens", + "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/expression-service-dangling-tokens/src/Sources/RegressionTests.twin b/bugs/expression-service-dangling-tokens/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..6774b7eec --- /dev/null +++ b/bugs/expression-service-dangling-tokens/src/Sources/RegressionTests.twin @@ -0,0 +1,83 @@ +' Regression tests: Eval and TbExpressionService.Compile refuse a malformed expression with a +' run-time error, as they refuse "1 2", instead of leaving out the part they cannot read. On +' BETA 1010 an expression that ends in an operator, or has a parenthesis with no partner at +' either end, is accepted, and its value is that of what comes before the fault: each case +' below gives 2. VBScript's Eval refuses each with 1002, Syntax error, or 1006, Expected ')'; +' Access's Eval with 2431, 2435 or 2436. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + Private Function EvalError(ByVal Source As String) As Long + Dim V As Variant + On Error Resume Next + V = Eval(Source) + EvalError = Err.Number + End Function + + Private Function CompileError(ByVal Source As String) As Long + Dim S As New TbExpressionService + Dim E As ITbExpression + S.AddStdLibraryBinder + On Error Resume Next + Set E = S.Compile(Source) + CompileError = Err.Number + End Function + + ' The controls: a well-formed expression, and malformed ones that are refused. + [TestCase] + Public Sub Control_Eval_WellFormed() + Assert.Strict.AreEqual 2, CInt(Eval("(2)")) + End Sub + + [TestCase] + Public Sub Control_Eval_TwoNumbers_Raises() + Assert.Strict.AreNotEqual 0&, EvalError("1 2") + End Sub + + [TestCase] + Public Sub Control_Compile_UnclosedAfterOperator_Raises() + Assert.Strict.AreNotEqual 0&, CompileError("2 * (3 + 4") + End Sub + + [TestCase] + Public Sub Eval_TrailingPlus_Raises() + Assert.Strict.AreNotEqual 0&, EvalError("2 +") + End Sub + + [TestCase] + Public Sub Eval_TrailingMod_Raises() + Assert.Strict.AreNotEqual 0&, EvalError("2 Mod") + End Sub + + [TestCase] + Public Sub Eval_UnclosedParenthesis_Raises() + Assert.Strict.AreNotEqual 0&, EvalError("(2") + End Sub + + [TestCase] + Public Sub Eval_UnopenedParenthesis_Raises() + Assert.Strict.AreNotEqual 0&, EvalError("2)") + End Sub + + [TestCase] + Public Sub Compile_TrailingPlus_Raises() + Assert.Strict.AreNotEqual 0&, CompileError("2 +") + End Sub + + [TestCase] + Public Sub Compile_TrailingMod_Raises() + Assert.Strict.AreNotEqual 0&, CompileError("2 Mod") + End Sub + + [TestCase] + Public Sub Compile_UnclosedParenthesis_Raises() + Assert.Strict.AreNotEqual 0&, CompileError("(2") + End Sub + + [TestCase] + Public Sub Compile_UnopenedParenthesis_Raises() + Assert.Strict.AreNotEqual 0&, CompileError("2)") + End Sub + +End Module diff --git a/bugs/expression-service-dangling-tokens/src/Sources/Startup.twin b/bugs/expression-service-dangling-tokens/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/expression-service-dangling-tokens/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/expression-service-error-numbers/expression-service-error-numbers.twinproj b/bugs/expression-service-error-numbers/expression-service-error-numbers.twinproj new file mode 100644 index 000000000..24d187d9b Binary files /dev/null and b/bugs/expression-service-error-numbers/expression-service-error-numbers.twinproj differ diff --git a/bugs/expression-service-error-numbers/repro.json b/bugs/expression-service-error-numbers/repro.json new file mode 100644 index 000000000..b67871fb7 --- /dev/null +++ b/bugs/expression-service-error-numbers/repro.json @@ -0,0 +1,17 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.Control_Eval_Sum$", + "^PASS RegressionTests.Control_DictionaryAddOfAKeyItHolds_Raises457$", + "^FAIL RegressionTests.Eval_DivisionByZero_Raises11: .*Actual: -2147467259 ", + "^FAIL RegressionTests.Eval_DivisionByZero_DescriptionIsDivisionByZero: .*Actual: \"Automation error\"", + "^FAIL RegressionTests.Eval_CIntOfText_Raises13: .*Actual: -2147467259 ", + "^FAIL RegressionTests.Eval_CIntOverflow_Raises6: .*Actual: -2147467259 ", + "^FAIL RegressionTests.Eval_SqrOfNegative_Raises5: .*Actual: -2147467259 ", + "^FAIL RegressionTests.Evaluate_DivisionByZero_Raises11: .*Actual: -2147467259 ", + "^FAIL RegressionTests.Evaluate_BoundObjectsError_Raises457: .*Actual: -2147467259 " + ] + } +} diff --git a/bugs/expression-service-error-numbers/src/Settings b/bugs/expression-service-error-numbers/src/Settings new file mode 100644 index 000000000..063a95fc0 --- /dev/null +++ b/bugs/expression-service-error-numbers/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "ExpressionServiceErrorNumbers", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: Eval and ITbExpression.Evaluate raise -2147467259 for every error inside the expression", + "project.exportPathIsV2": true, + "project.id": "{7F94F222-CF31-426C-963D-719182EFF8B8}", + "project.name": "ExpressionServiceErrorNumbers", + "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/expression-service-error-numbers/src/Sources/RegressionTests.twin b/bugs/expression-service-error-numbers/src/Sources/RegressionTests.twin new file mode 100644 index 000000000..dbbde0a7b --- /dev/null +++ b/bugs/expression-service-error-numbers/src/Sources/RegressionTests.twin @@ -0,0 +1,95 @@ +' Regression tests: an error raised by a function, an operator or a bound object's member +' while Eval or ITbExpression.Evaluate evaluates an expression reaches the caller unchanged, +' with the number and description the same code raises written in a procedure, as VBScript's +' Eval and Access's Eval pass it on. On BETA 1010 each of these raises -2147467259, whose +' description is "Automation error" in the IDE and in an exe's first such error, and +' "Unspecified error" after it. +' tbrun --tests runs each case and prints PASS or FAIL. +[TestFixture] +Module RegressionTests + + Private Function EvalError(ByVal Source As String) As Long + Dim V As Variant + On Error Resume Next + V = Eval(Source) + EvalError = Err.Number + End Function + + ' The controls: an expression that raises nothing, and a bound object's error raised by + ' the same call written in a procedure. + [TestCase] + Public Sub Control_Eval_Sum() + Assert.Strict.AreEqual 4, CInt(Eval("2 + 2")) + End Sub + + [TestCase] + Public Sub Control_DictionaryAddOfAKeyItHolds_Raises457() + Dim D As Object = CreateObject("Scripting.Dictionary") + Dim E As Long + D.Add 1, 1 + On Error Resume Next + D.Add 1, 2 + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 457&, E + End Sub + + [TestCase] + Public Sub Eval_DivisionByZero_Raises11() + Assert.Strict.AreEqual 11&, EvalError("1 / 0") + End Sub + + [TestCase] + Public Sub Eval_DivisionByZero_DescriptionIsDivisionByZero() + Dim V As Variant, D As String + On Error Resume Next + V = Eval("1 / 0") + D = Err.Description + On Error GoTo 0 + Assert.Strict.AreEqual "Division by zero", D + End Sub + + [TestCase] + Public Sub Eval_CIntOfText_Raises13() + Assert.Strict.AreEqual 13&, EvalError("CInt(""abc"")") + End Sub + + [TestCase] + Public Sub Eval_CIntOverflow_Raises6() + Assert.Strict.AreEqual 6&, EvalError("CInt(40000)") + End Sub + + [TestCase] + Public Sub Eval_SqrOfNegative_Raises5() + Assert.Strict.AreEqual 5&, EvalError("Sqr(-1)") + End Sub + + [TestCase] + Public Sub Evaluate_DivisionByZero_Raises11() + Dim S As New TbExpressionService + Dim X As ITbExpression, V As Variant, E As Long + S.AddStdLibraryBinder + Set X = S.Compile("1 / 0") + On Error Resume Next + V = X.Evaluate() + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 11&, E + End Sub + + [TestCase] + Public Sub Evaluate_BoundObjectsError_Raises457() + Dim S As New TbExpressionService + Dim D As Object = CreateObject("Scripting.Dictionary") + Dim X As ITbExpression, V As Variant, E As Long + D.Add 1, 1 + S.AddCustomBinderObject "D", D + Set X = S.Compile("D.Add(1, 2)") + On Error Resume Next + V = X.Evaluate() + E = Err.Number + On Error GoTo 0 + Assert.Strict.AreEqual 457&, E + End Sub + +End Module diff --git a/bugs/expression-service-error-numbers/src/Sources/Startup.twin b/bugs/expression-service-error-numbers/src/Sources/Startup.twin new file mode 100644 index 000000000..be8f5c8bc --- /dev/null +++ b/bugs/expression-service-error-numbers/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/file-errors-unspecified/file-errors-unspecified.twinproj b/bugs/file-errors-unspecified/file-errors-unspecified.twinproj index bd08df406..c11437274 100644 Binary files a/bugs/file-errors-unspecified/file-errors-unspecified.twinproj and b/bugs/file-errors-unspecified/file-errors-unspecified.twinproj differ diff --git a/bugs/file-errors-unspecified/repro.json b/bugs/file-errors-unspecified/repro.json index 8a98b7f3a..68c9ac9ec 100644 --- a/bugs/file-errors-unspecified/repro.json +++ b/bugs/file-errors-unspecified/repro.json @@ -39,7 +39,37 @@ "^Print to file number 99, not open: -2147467259 ", "^Line Input from file number 99, not open: -2147467259 ", "^Seek to position 0: -2147467259 ", - "^Put a record longer than Len: -2147467259 " + "^Put a record longer than Len: -2147467259 ", + "^ChDir to a file: -2147467259 ", + "^ChDir to a zero-length string: -2147467259 ", + "^ChDir to a wildcard: -2147467259 ", + "^ChDrive to a drive that does not exist: -2147467259 ", + "^ChDrive to a character that is not a letter: -2147467259 ", + "^CurDir of a drive that does not exist: -2147467259 ", + "^CurDir of a character that is not a letter: -2147467259 ", + "^Dir without a pathname after it returned a zero-length string: -2147467259 ", + "^Dir of a file name with a backslash at its end: -2147467259 ", + "^Dir of a name longer than 255 characters: -2147467259 ", + "^FileAttr with a returntype of 2: -2147467259 ", + "^FileCopy from a zero-length string: -2147467259 ", + "^FileCopy from a wildcard: -2147467259 ", + "^FileLen in missing folder: -2147467259 ", + "^FileLen of a wildcard that matches nothing: -2147467259 ", + "^FreeFile with every file number from 1 to 255 in use: -2147467259 ", + "^GetAttr of a wildcard: -2147467259 ", + "^GetAttr of a name with a character a name cannot hold: -2147467259 ", + "^Input past the end of the file: -2147467259 ", + "^Input from a file open For Output: -2147467259 ", + "^Input of a negative number of characters: -2147467259 ", + "^Input from file number 99, not open: -2147467259 ", + "^InputB past the end of the file: -2147467259 ", + "^Kill of a folder: -2147467259 ", + "^Kill of a pattern that matches a read-only file: -2147467259 ", + "^MkDir of a zero-length string: -2147467259 ", + "^RmDir of a file: -2147467259 ", + "^RmDir of the current directory: -2147467259 ", + "^SetAttr of a wildcard: -2147467259 ", + "^Width of file number 99, not open: -2147467259 " ] } } diff --git a/bugs/file-errors-unspecified/src/Sources/Startup.twin b/bugs/file-errors-unspecified/src/Sources/Startup.twin index 956ec0df1..611cf382c 100644 --- a/bugs/file-errors-unspecified/src/Sources/Startup.twin +++ b/bugs/file-errors-unspecified/src/Sources/Startup.twin @@ -1,6 +1,17 @@ ' Each case makes one file statement or function fail and prints the error it raises. Module Startup + Private Declare PtrSafe Function GetLogicalDrives Lib "kernel32" () As Long + + ' A drive letter that no drive has, from Z down. + Private Function NoDrive() As String + Dim m As Long, i As Long + m = GetLogicalDrives() + For i = 25 To 3 Step -1 + If (m And (2 ^ i)) = 0 Then NoDrive = Chr$(65 + i): Exit Function + Next + End Function + Private Sub Show(ByVal Label As String) Debug.Print Label & ": " & Err.Number & " " & Err.Description End Sub @@ -231,6 +242,161 @@ Module Startup Show "Put a record longer than Len" Close #f + ' ---- added by batch 7 of the run-time errors work (WIP.Errors.md) + Dim i As Long, here As String + + Err.Clear + ChDir d & "\exists.txt" + Show "ChDir to a file" + + Err.Clear + ChDir "" + Show "ChDir to a zero-length string" + + Err.Clear + ChDir d & "\*" + Show "ChDir to a wildcard" + + Err.Clear + ChDrive NoDrive() + Show "ChDrive to a drive that does not exist" + + Err.Clear + ChDrive "1" + Show "ChDrive to a character that is not a letter" + + Err.Clear + s = CurDir(NoDrive()) + Show "CurDir of a drive that does not exist" + + Err.Clear + s = CurDir("1") + Show "CurDir of a character that is not a letter" + + Err.Clear + s = Dir(d & "\missing.txt") + Err.Clear + s = Dir() + Show "Dir without a pathname after it returned a zero-length string" + + Err.Clear + s = Dir(d & "\exists.txt\") + Show "Dir of a file name with a backslash at its end" + + Err.Clear + s = Dir(d & "\" & String(256, "a")) + Show "Dir of a name longer than 255 characters" + + Err.Clear + f = FreeFile + Open d & "\exists.txt" For Input As #f + n = FileAttr(f, 2) + Show "FileAttr with a returntype of 2" + Close #f + + Err.Clear + FileCopy "", d & "\copy.txt" + Show "FileCopy from a zero-length string" + + Err.Clear + FileCopy d & "\*.txt", d & "\copy.txt" + Show "FileCopy from a wildcard" + + Err.Clear + n = FileLen(d & "\nofolder\x.txt") + Show "FileLen in missing folder" + + Err.Clear + n = FileLen(d & "\z*") + Show "FileLen of a wildcard that matches nothing" + + Err.Clear + For i = 1 To 255 + Open d & "\n" & i & ".txt" For Output As #i + Next + Err.Clear + f = FreeFile + Show "FreeFile with every file number from 1 to 255 in use" + For i = 1 To 255 + Close #i + Next + + Err.Clear + n = GetAttr(d & "\*.txt") + Show "GetAttr of a wildcard" + + Err.Clear + n = GetAttr(d & "\a 9 Then Open d & "\n" & i & ".txt" For Output As #i + Next + Err.Clear + f = FreeFile + Show "FreeFile with every file number from 1 to 255 in use" + For i = 1 To 255 + If i <> 9 Then Close #i + Next + + Err.Clear + n = GetAttr(d & "\*.txt") + Show "GetAttr of a wildcard" + + Err.Clear + n = GetAttr(d & "\a