Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

VBA takes wrong branch at If-statement - severe compiler bug?

The question mark in the title is only there because I'm very reluctant to call anything a compiler bug, but in this case, I'd be surprised if anyone could explain this behavior in any other way.

The code to reproduce the problem is very, very simple. In a standard module we have the following:

Sub CompilerBug()
    Dim oClass As cClass
    Set oClass = New cClass

    If False Then
        Debug.Print "This doesn't print, as it shouldn't."
    End If

    If Falsee(oClass.Clone) Then
        Debug.Print "This does print, although it shouldn't!"
    End If
End Sub

Public Function Falsee(oClass As cClass) As Boolean
    Falsee = False
End Function

And we have a class (cClass) defined in a class module named cClass, containing the following code:

Public Function Clone() As cClass
    Dim oClass As cClass
    Set oClass = New cClass
    Set Clone = oClass
End Function

Private Sub Class_Terminate()
End Sub

The code is pretty self-explanatory. The second if statement gets entered in spite of the aptly named function Falsee returning False, no matter the input! The same result can be observed when the function in the class is replaced by a similar Public Property Get.

For the purpose of reproduction, I'm getting this behavior in my Office 365 Excel, 64bit, Version 2011 (Build 13426.20274) which is the current up-to-date version of excel. I also tested this exact code in my Word VBA IDE with exactly the same results.

"Proof":

"Proof"

I have no idea what causes this behavior, but here are a few clues:

If we rewrite the code in our sub to:

Sub CompilerBug()
    Dim oClass As cClass
    Set oClass = New cClass

    Dim bFalse As Boolean
    bFalse = Falsee(oClass.Clone)

    If bFalse Then
        Debug.Print "This doesn't print, as it shouldn't."
    End If
End Sub

(The first if statement is omitted for the sake of brevity.) The code gets executed as expected, so it is crucial that the function is called directly in the condition for the if statement (not something that should usually make a difference).

And the next interesting clue is the following (assume we use the buggy sub code again with If Falsee(oClass.Clone) Then): If we remove the following from our class module:

Private Sub Class_Terminate()
End Sub

The if statement works as expected and nothing gets printed! So somehow the Terminate event being executed during evaluation of the If-statement messes things up, but the Class_Terminate() sub doesn't even contain any code! That's the next thing that shouldn't make a difference, yet does!

This idea is further supported by the fact, that when we declare a public variable in the module by adding Public poClass As cClass at the top and rewrite the function code to:

Public Function Falsee(oClass As cClass) As Boolean
    Set poClass = oClass
    Falsee = False
End Function

Now the Terminate event doesn't get called during execution of the If-statement, because the instance of the class doesn't go out of scope during the execution of the If-statement and as a result, the If-statement evaluates properly - the Line doesn't get printed.

Obviously, the Terminate-event being executed during evaluation of the If-statement can't be the whole story, because this happens all the time. It also seems to have something to do with the scope of the object that gets terminated and the way the parameter is passed to the function. For instance, the following does not produce the same behavior:

Module code:

Sub CompilerBug()
    Dim oClass As cClass
    Set oClass = New cClass

    If Falsee(oClass.CreateAndDestroyObject) Then
        Debug.Print "This doesn't print, as it shouldn't."
    End If
End Sub

Public Function Falsee(lng As Variant) As Boolean
    Falsee = False
End Function

And in the class module:

Public Function CreateAndDestroyObject() As Long
    Dim oClass2 As cClass
    Set oClass2 = New cClass
    Set oClass2 = Nothing
End Function

Private Sub Class_Terminate()
End Sub

To sum everything up, the behavior occurs when:

A method of a class returns an instance of the same class, and this method is called inside the condition of an If-statement, as an argument for a function, and this instance of the class (that was created by the method) then goes out of scope inside that function and the terminate event of the class gets called (and exists as code). In this case, the if statement gets entered, regardless of the return value of the function.

To me, many questions remain... Why does the Terminate event make a difference in this case? Is any of my code supposed to produce undefined behavior or is this actually a bug? Are there other cases where If-statements don't work the expected way? What exactly causes this bug?

The problem doesn't seem to exist in the 32-bit version of Excel.

like image 366
GWD Avatar asked Nov 27 '20 17:11

GWD


2 Answers

This bug is not present on 32-bit, but it seems to be present in 64-bit VBA-capable applications (I've tried Excel, Word, and AutoCAD).

Since the question already covers what happens if the Object does not get terminated or if there is no Class_Terminate event, the following examples all use an Object that will surely go out of scope and we also assume there is a Class_Terminate that gets called:

Option Explicit

#If Win64 Then
Sub Bug()
    ' We don't really need a Clone method to reproduce the bug
    If Falsee(New cClass) Then
        Debug.Print "This does print, although it shouldn't!"
    End If

    ' If we add a logical operator and a second method call then the bug disappears:
    If Falsee(New cClass) Or Falsee(New cClass) Then
        Debug.Print "This doesn't print, as it shouldn't."
    End If

    ' It could be any other method. The order of the methods also doesn't matter
    If Falsee(New cClass) Or Sin(0) Then
        Debug.Print "This doesn't print, as it shouldn't."
    End If

    ' The above workaround does not work if we simply use a boolean value after the method call
    If Falsee(New cClass) Or False Then
        Debug.Print "This does print, although it shouldn't!"
    End If

    ' But it does work if we add the boolean before the method call:
    If False Or Falsee(New cClass) Then
        Debug.Print "This doesn't print, as it shouldn't."
    End If
    If True And Falsee(New cClass) Then
        Debug.Print "This doesn't print, as it shouldn't."
    End If
End Sub

Function Falsee(oClass As cClass) As Boolean
    Falsee = False
End Function

#End If
like image 165
Cristian Buse Avatar answered Oct 23 '22 10:10

Cristian Buse


It is indeed a bug of the 64-bit version of VBA7. Here's a smaller example:

SomeClass.cls:

Private Sub Class_Terminate()
End Sub

Main.bas:

Function ReturnFalse(o As Object) As Boolean
 ReturnFalse = False
End Function
Sub test()    
 Debug.Print ReturnFalse(New SomeClass)
 If ReturnFalse(New SomeClass) Then
  Debug.Print "True"
 Else
  Debug.Print "False"
 End If     
End Sub

This prints

False
False

on 32-Bit VBA and

False
True

on 64-Bit VBA

I could find a report on this error in an almost three year old uservoice post (https://excel.uservoice.com/forums/304921-excel-for-windows-desktop-application/suggestions/35735881-fix-inlined-member-calls-on-user-objects-on-64-bi), but there hasn't been a reaction since then. Also, there is apparently no user accessible "real" way to report VBA bugs to Microsoft. This is all very frustrating, because we have a few VBA projects that direly need porting to 64-bit because of their memory requirements.

like image 27
Nordic Mainframe Avatar answered Oct 23 '22 08:10

Nordic Mainframe