vendredi 27 novembre 2020

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

(First if statement 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 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?

Aucun commentaire:

Enregistrer un commentaire