diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 5afd4d9f..e9cc9724 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -7911,3 +7911,137 @@ Severity: high for the crash: a program loses its unsaved data, and an error han A 64-bit build gives the same results. + +--- + +## A function call in an `#If`, `#ElseIf` or `#Const` expression is always 0, so `#If CBool(1)` is false + +**Describe the bug** +Every function call in a conditional compilation expression evaluates as 0, and the IDE reports nothing. `#If CBool(1) Then`, `#If CLng(1) = 1 Then`, `#If Abs(-1) = 1 Then`, `#If Len("abc") = 3 Then` and `#If Len(TWINBASIC_BUILD_TYPE) = 12 Then` (in a Standard EXE) all take the `#Else` branch, and `#Const X = CBool(1)` makes `X` 0. As a result `#If Not CBool(Win64) Then`, the usual way to select 32-bit-only code from a constant that is 1 rather than `True`, is true in a 64-bit build too. Observed in the compiler's test mode, in a `[RunAfterBuild]` run and in the DIAGNOSTICS list (a branch that declares a variable of a type that does not exist reports an error only when it is active), 32-bit and 64-bit. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open `ifdef-function-call-is-zero.twinproj` (attached as `ifdef-function-call-is-zero.zip`). +2. Run its test cases, the module below. On BETA 1010 the two controls pass and the seven cases with a function call fail with `Actual: False`; in a 64-bit build `If_NotCBoolOfWin64_AgreesWithWin64EqualsZero` fails too. + +The module is meant to be adapted as a regression test. Each case records which branch an `#If` takes and asserts the branch VB6 takes, which the attached VB6 project (`ifdef-function-call-is-zero-vb6.zip`) prints. +```vb +[TestFixture] +Module RegressionTests + + ' Controls: literals, constants and operators alone are evaluated. + [TestCase] + Public Sub If_LiteralComparison_IsTrue() + Dim taken As Boolean +#If 1 = 1 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_Win32EqualsOne_IsTrue() + Dim taken As Boolean +#If Win32 = 1 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + ' The defect: each call is 0. + [TestCase] + Public Sub If_CBoolOfOne_IsTrue() + Dim taken As Boolean +#If CBool(1) Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_CBoolOfWin32_IsTrue() + Dim taken As Boolean +#If CBool(Win32) Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_NotCBoolOfWin64_AgreesWithWin64EqualsZero() + Dim notCBool As Boolean, equalsZero As Boolean +#If Not CBool(Win64) Then + notCBool = True +#End If +#If Win64 = 0 Then + equalsZero = True +#End If + Assert.Strict.AreEqual equalsZero, notCBool + End Sub + + [TestCase] + Public Sub If_CLngOfOneEqualsOne_IsTrue() + Dim taken As Boolean +#If CLng(1) = 1 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_AbsOfMinusOneEqualsOne_IsTrue() + Dim taken As Boolean +#If Abs(-1) = 1 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_LenOfLiteralEqualsThree_IsTrue() + Dim taken As Boolean +#If Len("abc") = 3 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_LenOfBuildTypeEqualsTwelve_IsTrue() + ' The project is a Standard EXE: "Standard EXE" has 12 characters. + Dim taken As Boolean +#If Len(TWINBASIC_BUILD_TYPE) = 12 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + +#Const FROM_CALL = CBool(1) + + [TestCase] + Public Sub Const_FromCBoolOfOne_IsTrue() + Dim taken As Boolean +#If FROM_CALL Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + +End Module +``` + +**Expected behavior** +Each call is evaluated, as VB6 evaluates it: `CBool(1)`, `CBool(Win32)`, `CLng(1) = 1`, `Abs(-1) = 1`, `Len("abc") = 3` and `#Const FROM_CALL = CBool(1)` are all true there. If twinBASIC is not meant to support them, the call should be a compile error rather than a silent 0. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1010 + +**Additional context** +Severity: medium. The wrong branch is compiled with no error or warning, so a program can call a 32-bit declaration in a 64-bit build, or leave out code it meant to keep. `#If Not CBool(Win64)` was the documentation's own advice until now. + +A 64-bit build gives the same results. Reported independently on BETA 1024 (docs issue twinbasic/documentation#250), with `VarType(...)` as well. + +What does not reproduce it: an expression of constants, literals and operators alone, such as `#If Win64 = 0`, `#If Win64 <> 1` or `#If Not (Win64 = 1)`, each true in a 32-bit build only, as it should be. + + diff --git a/bugs/ifdef-function-call-is-zero/ifdef-function-call-is-zero.twinproj b/bugs/ifdef-function-call-is-zero/ifdef-function-call-is-zero.twinproj new file mode 100644 index 00000000..543ea4a3 Binary files /dev/null and b/bugs/ifdef-function-call-is-zero/ifdef-function-call-is-zero.twinproj differ diff --git a/bugs/ifdef-function-call-is-zero/repro.json b/bugs/ifdef-function-call-is-zero/repro.json new file mode 100644 index 00000000..02bfc088 --- /dev/null +++ b/bugs/ifdef-function-call-is-zero/repro.json @@ -0,0 +1,17 @@ +{ + "mode": "test", + "expect": { + "exit": 7, + "output": [ + "^PASS RegressionTests.If_LiteralComparison_IsTrue$", + "^PASS RegressionTests.If_Win32EqualsOne_IsTrue$", + "^FAIL RegressionTests.If_CBoolOfOne_IsTrue: ", + "^FAIL RegressionTests.If_CBoolOfWin32_IsTrue: ", + "^FAIL RegressionTests.If_CLngOfOneEqualsOne_IsTrue: ", + "^FAIL RegressionTests.If_AbsOfMinusOneEqualsOne_IsTrue: ", + "^FAIL RegressionTests.If_LenOfLiteralEqualsThree_IsTrue: ", + "^FAIL RegressionTests.If_LenOfBuildTypeEqualsTwelve_IsTrue: ", + "^FAIL RegressionTests.Const_FromCBoolOfOne_IsTrue: " + ] + } +} diff --git a/bugs/ifdef-function-call-is-zero/src/Settings b/bugs/ifdef-function-call-is-zero/src/Settings new file mode 100644 index 00000000..ac4aa2d2 --- /dev/null +++ b/bugs/ifdef-function-call-is-zero/src/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "IfdefFunctionCallIsZero", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "Reproduces: #If treats every function call as 0", + "project.exportPathIsV2": true, + "project.id": "{95BFA9FC-6D6D-4000-B200-D5DB2BC0E770}", + "project.name": "IfdefFunctionCallIsZero", + "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/ifdef-function-call-is-zero/src/Sources/RegressionTests.twin b/bugs/ifdef-function-call-is-zero/src/Sources/RegressionTests.twin new file mode 100644 index 00000000..08e5744b --- /dev/null +++ b/bugs/ifdef-function-call-is-zero/src/Sources/RegressionTests.twin @@ -0,0 +1,106 @@ +' Regression tests: a function call in an #If expression must be evaluated, as VB6 and +' VBA evaluate it. On BETA 1010 every call is 0, with no diagnostic, so each #If below +' that calls a function takes its #Else branch. Each case records the branch taken and +' asserts the branch VB6 takes. +[TestFixture] +Module RegressionTests + + ' Controls: literals, constants and operators alone are evaluated. + [TestCase] + Public Sub If_LiteralComparison_IsTrue() + Dim taken As Boolean +#If 1 = 1 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_Win32EqualsOne_IsTrue() + Dim taken As Boolean +#If Win32 = 1 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + ' The defect: each call is 0. + [TestCase] + Public Sub If_CBoolOfOne_IsTrue() + Dim taken As Boolean +#If CBool(1) Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_CBoolOfWin32_IsTrue() + Dim taken As Boolean +#If CBool(Win32) Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_NotCBoolOfWin64_AgreesWithWin64EqualsZero() + Dim notCBool As Boolean, equalsZero As Boolean +#If Not CBool(Win64) Then + notCBool = True +#End If +#If Win64 = 0 Then + equalsZero = True +#End If + Assert.Strict.AreEqual equalsZero, notCBool + End Sub + + [TestCase] + Public Sub If_CLngOfOneEqualsOne_IsTrue() + Dim taken As Boolean +#If CLng(1) = 1 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_AbsOfMinusOneEqualsOne_IsTrue() + Dim taken As Boolean +#If Abs(-1) = 1 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_LenOfLiteralEqualsThree_IsTrue() + Dim taken As Boolean +#If Len("abc") = 3 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + + [TestCase] + Public Sub If_LenOfBuildTypeEqualsTwelve_IsTrue() + ' The project is a Standard EXE: "Standard EXE" has 12 characters. + Dim taken As Boolean +#If Len(TWINBASIC_BUILD_TYPE) = 12 Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + +#Const FROM_CALL = CBool(1) + + [TestCase] + Public Sub Const_FromCBoolOfOne_IsTrue() + Dim taken As Boolean +#If FROM_CALL Then + taken = True +#End If + Assert.Strict.AreEqual True, taken + End Sub + +End Module diff --git a/bugs/ifdef-function-call-is-zero/src/Sources/Startup.twin b/bugs/ifdef-function-call-is-zero/src/Sources/Startup.twin new file mode 100644 index 00000000..20233b3a --- /dev/null +++ b/bugs/ifdef-function-call-is-zero/src/Sources/Startup.twin @@ -0,0 +1,6 @@ +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/bugs/ifdef-function-call-is-zero/vb6/Module1.bas b/bugs/ifdef-function-call-is-zero/vb6/Module1.bas new file mode 100644 index 00000000..6757e510 --- /dev/null +++ b/bugs/ifdef-function-call-is-zero/vb6/Module1.bas @@ -0,0 +1,59 @@ +Attribute VB_Name = "Module1" +Option Explicit + +' The VB6 side of ifdef-function-call-is-zero: which branch each #If takes when its +' expression calls a function. Writes one line per test to out.txt beside the exe. +' VB6 has no Win64 and no TWINBASIC_BUILD_TYPE, so those tests are left out. + +#Const FROM_CALL = CBool(1) + +Sub Main() + On Error GoTo Fail + Open App.Path & "\out.txt" For Output As #9 +#If 1 = 1 Then + Print #9, "1 = 1: True" +#Else + Print #9, "1 = 1: False" +#End If +#If Win32 = 1 Then + Print #9, "Win32 = 1: True" +#Else + Print #9, "Win32 = 1: False" +#End If +#If CBool(1) Then + Print #9, "CBool(1): True" +#Else + Print #9, "CBool(1): False" +#End If +#If CBool(Win32) Then + Print #9, "CBool(Win32): True" +#Else + Print #9, "CBool(Win32): False" +#End If +#If CLng(1) = 1 Then + Print #9, "CLng(1) = 1: True" +#Else + Print #9, "CLng(1) = 1: False" +#End If +#If Abs(-1) = 1 Then + Print #9, "Abs(-1) = 1: True" +#Else + Print #9, "Abs(-1) = 1: False" +#End If +#If Len("abc") = 3 Then + Print #9, "Len(""abc"") = 3: True" +#Else + Print #9, "Len(""abc"") = 3: False" +#End If +#If FROM_CALL Then + Print #9, "#Const FROM_CALL = CBool(1): True" +#Else + Print #9, "#Const FROM_CALL = CBool(1): False" +#End If + 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/ifdef-function-call-is-zero/vb6/Probe.vbp b/bugs/ifdef-function-call-is-zero/vb6/Probe.vbp new file mode 100644 index 00000000..4d210b0e --- /dev/null +++ b/bugs/ifdef-function-call-is-zero/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/docs/Reference/Compiler Constants.md b/docs/Reference/Compiler Constants.md index ca00ccb2..8660f21b 100644 --- a/docs/Reference/Compiler Constants.md +++ b/docs/Reference/Compiler Constants.md @@ -15,7 +15,7 @@ The list includes the constants that VBA documents, even those twinBASIC does no ## Predefined constants -Each value below is the one a module sees unless it redefines the constant. `#Const` can redefine any of them, and the new value applies only in the module that declares it: after `#Const Win32 = 0`, `#If Win32` is false in that module and still true everywhere else. See [Predefined compiler constants](../tB/Core/Topic-Preprocessor#predefined-compiler-constants). +Each value below is the one code sees unless a `#Const` redefines the constant. `#Const` can redefine any of them. Like C's `#define`, the new value applies from the `#Const` line to the end of the `.twin` file that holds it, including any modules after it in that file. After `#Const Win32 = 0`, `#If Win32` is false below that line, and still true above it and in every other file. See [Predefined compiler constants](../tB/Core/Topic-Preprocessor#predefined-compiler-constants). ### `Win16` @@ -107,7 +107,11 @@ Or more simply, to decide whether to use `PtrSafe`, and then `DeclareWide` or ot ``` > [!IMPORTANT] -> Compiler constants are not `Boolean` values, so a test such as `#If Not Win64 Then` does not do what it appears to. It is `True` in both 32-bit and 64-bit mode, where the intent is usually `False` under 64-bit, to select 32-bit-only code. To treat a constant as a `Boolean`, convert it with `CBool()`, as in `#If Not CBool(Win64) Then`. +> Compiler constants are numbers, not `Boolean` values. `Win64` is 1 in a 64-bit build, and `Not 1` is -2, which counts as true, so `#If Not Win64 Then` is true in both 32-bit and 64-bit builds. To select 32-bit-only code, compare the constant with 0, as in `#If Win64 = 0 Then`, or put the code in the `#Else` branch of `#If Win64 Then`. + + +> [!WARNING] BETA 1010 +> A function call in an `#If`, `#ElseIf` or `#Const` expression is always 0, with no error or warning. `#If CBool(1) Then` and `#If Len("abc") = 3 Then` are both false, and `#If Not CBool(Win64) Then` is true in a 64-bit build. VB6 evaluates `CBool`, `CLng`, `Abs` and `Len` there. > [!NOTE] > **`vbPtrSize` is not a compiler constant.** [**vbPtrSize**](../tB/Modules/Constants/#vbPtrSize) is a constant of the VBA package, **4** or **8** in code, but `#If` does not see it: like any name that is not a compiler constant it is 0 there, so `#If vbPtrSize = 8 Then` is false in a 64-bit build too. To choose code by pointer size, test [`Win64`](#win64). diff --git a/docs/Reference/Core/Topic-Preprocessor.md b/docs/Reference/Core/Topic-Preprocessor.md index 2f8943aa..7efd0027 100644 --- a/docs/Reference/Core/Topic-Preprocessor.md +++ b/docs/Reference/Core/Topic-Preprocessor.md @@ -37,6 +37,10 @@ The directive's behaviour mirrors the runtime [**If...Then...Else**](If-Then-Els > [!NOTE] > The [**Option Compare**](Option) statement does not affect expressions in `#If`/`#ElseIf`. They are always evaluated as if **Option Compare Text** were in effect. + +> [!WARNING] BETA 1010 +> A function call in an `#If`, `#ElseIf` or `#Const` expression is always 0, with no error or warning. `#If CBool(1) Then` and `#If Len("abc") = 3 Then` are both false, and `#If Not CBool(Win64) Then` is true in a 64-bit build. VB6 evaluates `CBool`, `CLng`, `Abs` and `Len` there. To test whether a constant is 0, compare it with 0, as in `#If Win64 = 0 Then`. + ## #Const directive Syntax: @@ -49,7 +53,7 @@ Syntax: *expression* : A literal, another conditional compiler constant, or any combination using arithmetic or logical operators (except [**Is**](Is)). Standard runtime constants (declared with [**Const**](Const)) are *not* allowed here. -Conditional compiler constants declared with `#Const` are private to the module in which they appear. Project-wide conditional constants must be defined in the project's compilation settings --- `#Const` cannot create them. +A conditional compiler constant declared with `#Const` works like C's `#define`: it applies from the `#Const` line to the end of the `.twin` file that holds it, including any modules after it in that file. Lines above it, and other files, do not see it. In VB6 and VBA, where each file holds one module, that is the rest of the module. Project-wide conditional constants must be defined in the project's compilation settings --- `#Const` cannot create them. Conditional compiler constants are always evaluated at the module level regardless of where they appear in code; they can only be used in `#If`/`#ElseIf` expressions. @@ -57,7 +61,7 @@ Conditional compiler constants are always evaluated at the module level regardle twinBASIC provides a set of built-in compiler constants --- `Win64`, `Win32`, `TWINBASIC`, `TWINBASIC_BUILD`, `VBA7`, etc. See the dedicated [Compiler Constants](../../Reference/Compiler-Constants) page for the full list and what each one means. -`#Const` can redefine a built-in compiler constant. As with any `#Const`, the new value applies only in the module that declares it: after `#Const Win32 = 0`, `#If Win32` is false in that module and still true everywhere else. +`#Const` can redefine a built-in compiler constant. As with any `#Const`, the new value applies from the `#Const` line to the end of its file: after `#Const Win32 = 0`, `#If Win32` is false below that line, in that module and any after it in the same file, and still true above it and in every other file. ### Example