33

Please take a look at the latest updates at the end of the post.

In Particular, see Update 4: the Variant comparison Curse


I’ve already seen mates banging their head against the wall to understand how a variant works, but never imagined that I will have my own bad moment with it.

I have successfully used the following VBA construction:

For i = 1 to i

This works perfectly when i is an Integer or any numeric type, iterating from 1 to the original value of i. I do this on occasions where i is a ByVal parameter - you might say lazy - to spare myself the declaration of a new variable.

Then I had a bug when this construct “stopped” working as expected. After some hard debugging, I found that it doesn’t work the same way when i is not declared as explicit numeric type, but a Variant. The question is twofold:

1- What are the exact semantics of the For and the For Each loops? I mean what is the sequence of actions that the compiler undertakes and in which order? For example, does the evaluation of the limit precede the initialization of the counter? Is this limit copied and “fixed” somewhere before the loop starts? Etc. The same question applies to For Each.

2- How to explain the different outcomes on variants and on explicit numeric types? Some say a variant is an (immutable) reference type, can this definition explain the observed behavior?

I have prepared an MCVE for different (independent) scenarios involving the For and the For Each statements, combined with integers, variants and objects. The surprising results urge for defining unambiguously the semantics or, for the least, check if those results do conform to the defined semantics.

All insights are welcome, including partial ones that explain some of the surprising results or their contradictions.

Thanks.

Sub testForLoops()
    Dim i As Integer, v As Variant, vv As Variant, obj As Object, rng As Range

    Debug.Print vbCrLf & "Case1 i --> i    ",
    i = 4
    For i = 1 To i
        Debug.Print i,      ' 1, 2, 3, 4
    Next

    Debug.Print vbCrLf & "Case2 i --> v    ",
    v = 4
    For i = 1 To v  ' (same if you use a variant counter: For vv = 1 to v)
        v = i - 1   ' <-- doesn't affect the loop's outcome
        Debug.Print i,          ' 1, 2, 3, 4
    Next

    Debug.Print vbCrLf & "Case3 v-3 <-- v ",
    v = 4
    For v = v To v - 3 Step -1
       Debug.Print v,           ' 4, 3, 2, 1
    Next

    Debug.Print vbCrLf & "Case4 v --> v-0 ",
    v = 4
    For v = 1 To v - 0
        Debug.Print v,          ' 1, 2, 3, 4
    Next

    '  So far so good? now the serious business

    Debug.Print vbCrLf & "Case5 v --> v    ",
    v = 4
    For v = 1 To v
        Debug.Print v,          ' 1      (yes, just 1)
    Next

    Debug.Print vbCrLf & "Testing For-Each"

    Debug.Print vbCrLf & "Case6 v in v[]",
    v = Array(1, 1, 1, 1)
    i = 1
    ' Any of the Commented lines below generates the same RT error:
    'For Each v In v  ' "This array is fixed or temporarily locked"
    For Each vv In v
        'v = 4
        'ReDim Preserve v(LBound(v) To UBound(v))
        If i < UBound(v) Then v(i + 1) = i + 1 ' so we can alter the entries in the array, but not the array itself
        i = i + 1
         Debug.Print vv,            ' 1, 2, 3, 4
    Next

    Debug.Print vbCrLf & "Case7 obj in col",
    Set obj = New Collection: For i = 1 To 4: obj.Add Cells(i, i): Next
    For Each obj In obj
        Debug.Print obj.Column,    ' 1 only ?
    Next

    Debug.Print vbCrLf & "Case8 var in col",
    Set v = New Collection: For i = 1 To 4: v.Add Cells(i, i): Next
    For Each v In v
        Debug.Print v.column,      ' nothing!
    Next

    ' Excel Range
    Debug.Print vbCrLf & "Case9 range as var",
    ' Same with collection? let's see
    Set v = Sheet1.Range("A1:D1") ' .Cells ok but not .Value => RT err array locked
    For Each v In v ' (implicit .Cells?)
        Debug.Print v.Column,       ' 1, 2, 3, 4
    Next

    ' Amazing for Excel, no need to declare two vars to iterate over a range
    Debug.Print vbCrLf & "Case10 range in range",
    Set rng = Range("A1:D1") '.Cells.Cells add as many as you want
    For Each rng In rng ' (another implicit .Cells here?)
        Debug.Print rng.Column,     ' 1, 2, 3, 4
    Next
End Sub

UPDATE 1

An interesting observation that can help understanding some of this. Concerning cases 7 and 8: if we hold another reference on the collection being iterated, the behavior changes completely:

    Debug.Print vbCrLf & "Case7 modified",
    Set obj = New Collection: For i = 1 To 4: obj.Add Cells(i, i): Next
    Dim obj2: set obj2 = obj  ' <-- This changes the whole thing !!!
    For Each obj In obj
        Debug.Print obj.Column,    ' 1, 2, 3, 4 Now !!!
    Next

This means that in the initial case7 the collection being iterated was garbage-collected (due to reference counting) just after the variable obj was assigned to the first element of the collection. But this is still weird though. The compiler should have held some hidden reference on the object being iterated!? Compare this to case 6 where the array being iterated was "locked"...

UPDATE 2

The semantics of the For statement as defined by MSDN can be found on this page. You can see that it is explicitly stated that the end-value should be evaluated only once and before the execution of the loop proceeds. Should we consider this odd behavior as a compiler bug?

UPDATE 3

The intriguing case 7 again. The counter-intuitive behavior of case7 is not restricted to the (say unusual) iteration of a variable on itself. It may happen in a seemingly "innocent" code that, by mistake removes the only reference on the collection being iterated, leading to its garbage collection.

Debug.Print vbCrLf & "Case7 Innocent"
Dim col As New Collection, member As Object, i As Long
For i = 1 To 4: col.Add Cells(i, i): Next
Dim someCondition As Boolean ' say some business rule that says change the col
For Each member In col
    someCondition = True
    If someCondition Then Set col = Nothing ' or New Collection
    ' now GC has killed the initial collection while being iterated
    ' If you had maintained another reference on it somewhere, the behavior would've been "normal"
    Debug.Print member.Column, ' 1 only
Next

By intuition one expects that some hidden reference is held on the collection to stay alive during iteration. Not only it doesn't, but the program runs smoothly with no run-time error, leading probably to hard bugs. While the spec does not state any rule about manipulating objects under iteration, the implementation happens to protect and lock iterated Arrays (case 6) but neglects - doesn't even hold a dummy reference - on a collection (neither on a Dictionary, I've tested that also).

It's the responsibility of the programmer to care about the reference counting, which is not the "spirit" of VBA/VB6 and the architectural motivations behind reference counting.

UPDATE 4: The Variant Comparison Curse

Variants exhibit weird behaviors in many situations. In particular, comparing two Variants of different sub-types yields undefined results. Consider these simple examples:

Sub Test1()
  Dim x, y: x = 30: y = "20"
  Debug.Print x > y               ' False !!
End Sub

Sub Test2()
  Dim x As Long, y: x = 30: y = "20"
  '     ^^^^^^^^
  Debug.Print x > y             ' True
End Sub

Sub Test3()
  Dim x, y As String:  x = 30: y = "20"
  '        ^^^^^^^^^
  Debug.Print x > y             ' True
End Sub

As you can see, when both variables, the number and the string, were declared variants, the comparison is undefined. When at least one of them is explicitly typed, the comparison succeeds.

The same occurs when comparing for equality! For instance, ?2="2" returns True, but if you define two Variant variables, assign them those values and compare them, the comparison fails!

Sub Test4()
  Debug.Print 2 = "2"           ' True

  Dim x, y:  x = 2:  y = "2"
  Debug.Print x = y             ' False !

End Sub
A.S.H
  • 29,101
  • 5
  • 23
  • 50
  • 2
    The description in [Documentation](http://stackoverflow.com/documentation/vba/3418/data-types-and-limits/11786/variant) is pretty close to the "formal" definition of a `Variant`. If you want more formal than that, follow the links to MSDN. I'm not entirely clear what the data type, parameter type, and loop exit condition have to do with each other in the question though. – Comintern Feb 20 '17 at 21:15
  • @Comintern Thanks for the link. To be honest I had seen that page and I know the structure of a variant.. My question is focused on its behavior. – A.S.H Feb 20 '17 at 21:22
  • A `Variant` is a data type, so by definition it doesn't have "behavior". Maybe that's why I'm confused by that part of your question. Are you just asking what a disassembled VBA loop looks like? – Comintern Feb 20 '17 at 21:24
  • @Mat'sMugThanks for the comment. I made sure that each test is completely independent of the others and strived to shorten the MCVE. But if you notice anything wrong please dont hesitate to notify me. You may also edit the post as you please :) – A.S.H Feb 20 '17 at 21:25
  • @Comintern Let's say I am most interested in the *semantics* of the `For` and `ForEach`, and an explanation of their different behavior when a variant is involved, compared to basic data types and object types. – A.S.H Feb 20 '17 at 21:27
  • 3
    Hint: it has something to do with using your loop counter in your exit condition. I'd mentally prepare for a metal-level answer if I were you =) – Mathieu Guindon Feb 20 '17 at 21:40
  • @Mat'sMug, the enigma is how VBA treats this exit condition... is it copied somewhere? Cases 1 and 2 suggest so, but case 5 doesn't... – A.S.H Feb 20 '17 at 23:24
  • I changed slightly case 4 to better highlight the issue. `For v = 1 to v` is different from `For v = 1 to v-0`.... – A.S.H Feb 21 '17 at 00:43
  • 2
    Somewhat odd: if you define a function `Echo(v)` whose sole line of code is `Echo = v` and you replace `For v = 1 To v` in your case 5 by `For v = 1 To Echo(v)` then it prints 1,2,3,4 after all. – John Coleman Feb 21 '17 at 01:05
  • @Mat'sMug can you be more specific about the assert? Sorry I wanted to add that but did not get it completely. Any example? – A.S.H Feb 21 '17 at 01:08
  • @JohnColeman yes, and so did case 4 actually: `For v = 1 to v-0`. Odd enough that it yields a different result than `For v = 1 to v`. – A.S.H Feb 21 '17 at 01:10
  • 1
    This looks promising: https://github.com/bontchev/pcodedmp . You can make educated guesses, but would probably need to look at and understand the generated p-code to really understand what is happening (and even then, there will be some guess work since the p-code interpreter isn't open-source). – John Coleman Feb 21 '17 at 01:19
  • Oh, I meant isolated test methods, like [these](https://i.stack.imgur.com/sFIsW.png). The `Assert` is really just a testing framework API artifact, e.g. `Assert.IsTrue expected, actual` ;-) – Mathieu Guindon Feb 21 '17 at 01:21
  • @JohnColeman in both cases we forced the compiler to create a copy of `v`. The question is does it do that automatically (as in case 1) or not (as in case 5). Is this a bug, or is there a concise semantic that defines this? Besides thanks for the link I will take a look. – A.S.H Feb 21 '17 at 01:21
  • The `To v-0` case *has* to get allocated elsewhere than in `v`'s spot, because it's an expression that needs to get evaluated (as would be a function call result). – Mathieu Guindon Feb 21 '17 at 01:32
  • Can you define MCVE? – StayOnTarget Feb 21 '17 at 12:35
  • 1
    @DaveInCaz I adhered to [this definition](http://stackoverflow.com/help/mcve) as far as I could :) – A.S.H Feb 21 '17 at 12:50
  • 2
    Outside of the existing answer, mine would only verify that the actual code in the runtime is adhering to the specification. I disassembled the runtime last night, but haven't had the time to dig through it yet. It basically boils down to whether the pointer to the `Variant` *structure* is pushed to the stack or the pointer to the `Variant` *data area* is pushed to the stack. Relevant is the fact that loops are excuted via function calls in the msvbvm6 library, so a `Variant` would get boxed *twice*. – Comintern Mar 01 '17 at 20:52
  • @Comintern what particularly intrigates me in `case7` is how a collection could be garbage-collected while being iterated, and yet, we didn't even have any runtime issue, just that the iteration stopped after the garbage collection! I could imagine this resulting in some crash, not just stopping the iteration... – A.S.H Mar 01 '17 at 21:16
  • That's true... but they're related. But you know, case7 (compared to case7 modified) is **indeed** the one that intrigues me the most most. It has become somehow " the whole thing" for me. I just cant understand how this GC happened so smoothly without any trouble, and no crash. – A.S.H Mar 01 '17 at 21:31
  • 1
    Another take on `case5`: I think it's related to how a Variant is stored in memory - for numeric sub-types 16 bytes are allocated to store the data sub-type and memory location of the data. When the For loop is initialised a copy of `v` is pushed to the stack (or wherever it's stored) which is the sub-type + memory location. When `v` is iterated the value at the memory location is updated - which the loop terminal value copy of `v` is pointing at, so the terminal value has changed, and the loop terminates! – chris neilsen Mar 02 '17 at 03:54
  • Thanks @chrisneilsen for your comment. The problem is that [this link](https://msdn.microsoft.com/en-us/library/windows/desktop/ms221627%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396) suggests that when the embedded value is numeric, the data is stored in the variant structure itself, not by a pointer to that data. What do you think? – A.S.H Mar 02 '17 at 04:10
  • But your analysis seems perfect for case8 by the way. I mean precisely its difference with case 7. – A.S.H Mar 02 '17 at 04:15
  • 2
    @ASH thanks. My reading of the link you provided is that a variant can store numeric data as values _or_ pointers - question is which one does VBA use? (I don't have a definitive answer to that by the way) – chris neilsen Mar 02 '17 at 04:30
  • Ah, indeed there are two alternatives to store a numeric... i.e. `LONGLONG llVal` or `LONGLONG *pllVal` (pointer). hmm VBA's imlpementation chose the latter !!? @chrisneilsen – A.S.H Mar 02 '17 at 04:32
  • VBA uses both [in different situations](https://stackoverflow.com/a/31637346/11683). It seems plausible that VB caches the `for` boundary in a hidden Variant-byref, and thus consulting this cached variable yields different results each time because it is a vt-byref. The `v` loop variable itself is not a vt-byref. (This `VT_BYREF` flag seems to cause problems in other places too, such as https://stackoverflow.com/a/52686311/11683). @chrisneilsen (Yes I know this is old but it comes up from time to time.) – GSerg Jan 04 '21 at 21:54

1 Answers1

16

Please see edits below!

For Each edits also added below under Edit2

More edits about ForEach and Collections at Edit3

One last edit about ForEach and Collections at Edit4

A final note about iteration behavior at Edit5

Part of the subtlety of this odd behavior in the semantics of variant evaluation when used as a loop control variable or terminating condition.

In a nutshell, when a variant is the terminating value, or the control variable, the terminating value is naturally re-evaluated by the runtime with each iteration. A value type, however, such as an Integer, is pushed directly, and thus not re-evaluated (and its value doesn't change). If the control variable is an Integer, but the terminating value is a Variant, the Variant is coerced to an Integer on the first iteration, and pushed similarly. The same situation arises when the terminating condition is an expression involving a Variant and an Integer - it's coerced to an Integer.

In this example:

Dim v as Variant
v=4
for v= 1 to v
  Debug.print v,
next

The variant v is assigned an integer value of 1, and the loop termination condition is re-evaluated because terminating variable is a variant - the runtime recognizes the presence of the Variant reference and forces re-evaluation with each iteration. As a result, the loop completes because of the in-loop reassignment. Because the variant now has a value of 1, the loop termination condition is satisfied.

Consider this next example:

Dim v as variant
v=4
for v=1 to v-0
   Debug.Print v,
next 

When the terminating condition is an expression, such as "v - 0", the expression is evaluated and coerced to a regular integer, not a variant, and thus its hard value is pushed to the stack at runtime. As a result, the value is not re-evaluated upon each loop iteration.

The other interesting example:

Dim i as Integer
Dim v as variant
v=4
For i = 1 to v
   v=i-1
   Debug.print i,
next

behaves as it does because the control variable is an Integer, and thus the terminating variable is coerced to an integer as well, then pushed to the stack for iteration.

I cannot swear these are the semantics, but I believe the terminating condition or value is simply pushed onto a stack, thus the integer value is pushed, or the Variant's object reference is pushed, thus triggering the re-evaluation when the compiler realizes a variant holds the terminating value. When the variant gets reassigned within the loop, and the value is re-queried as the loop completes, the new value is returned, and the loop terminates.

Sorry if that's a little muddy, but it's kinda late, but I saw this and couldn't help but take a shot at an answer. Hope it makes some sense. Ah, good ol' VBA :)

EDIT:

Found some actual info from the VBA language spec at MS:

The expressions [start-value], [end-value], and [step-increment] are evaluated once, in order, and prior to any of the following computations. If the value of [start-value], [end-value], and [step-increment] are not Let-coercible to Double, error 13 (Type mismatch) is raised immediately. Otherwise, proceed with the following algorithm using the original, uncoerced values.

Execution of the [for-statement] proceeds according to the following algorithm:

  1. If the data value of [step-increment] is zero or a positive number, and the value of [bound-variable-expression] is greater than the value of [end-value], then execution of the [forstatement] immediately completes; otherwise, advance to Step 2.

  2. If the data value of [step-increment] is a negative number, and the value of [bound-variable-expression] is less than the value of [end-value], execution of the [for-statement] immediately completes; otherwise, advance to Step 3.

  3. The [statement-block] is executed. If a [nested-for-statement] is present, it is then executed. Finally, the value of [bound-variable-expression] is added to the value of [step-increment] and Let-assigned back to [bound-variable-expression]. Execution then repeats at step 1.

What I gather from this is that the intent is for the terminating condition value to be evaluated once and once only. If we see evidence that changing that value changes the behavior of the loop from its initial condition, it is almost certainly due to what might be termed informally as accidental re-evaluation because it's a variant. If it's unintentional, we can probably only use anecodtal evidence to predict its behavior.

If as the runtime evaluates a loop's start/end/step values, and pushes the "value" of those expressions onto the stack, a Variant value throws a "byref wrench" into the process. If the runtime does not first recognize the variant, evaluate it, and push that value as the terminating condition, curious behavior (as you are showing) would almost certainly ensue. Exactly how VBA handles variants in this case would be a great task for pcode analysis, as others have suggested.

EDIT2: FOREACH

The VBA spec again provides insight into the evaluation of ForEach loops over collections and arrays:

The expression [collection] is evaluated once prior to any of the >following computations.

  1. If the data value of [collection] is an array:

    If the array has no elements, then execution of the [for-each-statement] immediately completes.

    If the declared type of the array is Object, then the [bound-variable-expression] is Set-assigned to the first element in the >array. Otherwise, the [bound-variable-expression] is Let-assigned to the >first element in the array.

    After [bound-variable-expression] has been set, the [statement-block] >is executed. If a [nested-for-statement] is present, it is then executed.

    Once the [statement-block] and, if present, the [nested-for-statement] >have completed execution, [bound-variable-expression] is Let-assigned to >the next element in the array (or Set-assigned if it is an array of >Object). If and only if there are no more elements in the array, then >execution of the [for-each-statement] immediately completes. Otherwise, >[statement-block] is executed again, followed by [nested-forstatement] if >present, and this step is repeated.

    When the [for-each-statement] has finished executing, the value of >[bound-variable-expression] is the data value of the last element of the >array.

  2. If the data value of [collection] is not an array:

    The data value of [collection] must be an object-reference to an >external object that supports an implementation-defined enumeration >interface. The [bound-variable-expression] is either Let-assigned or >Set-assigned to the first element in [collection] in an >implementation->defined manner.

    After [bound-variable-expression] has been set, the [statement-block] >is executed. If a [nested-for-statement] is present, it is then executed.

    Once the [statement-block] and, if present, the [nested-for-statement] >have completed execution, [bound-variable-expression] is Set-assigned to >the next element in [collection] in an implementation-defined manner. If >there are no more elements in [collection], then execution of the [for-each->statement] immediately completes. Otherwise, [statement-block] is >executed again, followed by [nested-for-statement] if present, and this >step is repeated.

    When the [for-each-statement] has finished executing, the value of >[bound-variable-expression] is the data value of the last element in >[collection].

Using this as a base, I think it becomes clear that a Variant assigned to a variable that then becomes the bound-variable-expression generates the "Array is locked" error in this example:

    Dim v As Variant, vv As Variant
v = Array(1, 1, 1, 1)
i = 1
' Any of the Commented lines below generates the same RT error:
For Each v In v  ' "This array is fixed or temporarily locked"
'For Each vv In v
    'v = 4
    'ReDim Preserve v(LBound(v) To UBound(v))
    If i < UBound(v) Then v(i + 1) = i + 1 ' so we can alter the entries in the array, but not the array itself
    i = i + 1
     Debug.Print vv,            ' 1, 2, 3, 4
Next

Using 'v' as the [bound-variable-expression] creates a Let-assignment back to V that is prevented by the runtime because it is the target of an enumeration underway to support the ForEach loop itself; that is, the runtime locks the variant, thus precluding the loop from assigning a different value to the variant as would necessarily have to occur.

This also applies to the 'Redim Preserve' - resizing or changing the array, thus changing the variant's assignment, is going to violate the lock placed on the enumeration target at the loop's initialization.

With regard to Range-based assignments/iteration, note the separate semantics for non-object elements kicks in; the "external objects" provide an implementation-specific enumeration behavior. An excel Range object has a _Default property that is being called when referenced by the object name only, as in this case, which does not take an implicit lock when used as the iteration target of the ForEach (and thus does not generate the locking error, as it has different semantics than the Variant variety):

Debug.Print vbCrLf & "Case10 range in range",
Set rng = Range("A1:D1") '.Cells.Cells add as many as you want
For Each rng In rng ' (another implicit .Cells here?)
    Debug.Print rng.Column,     ' 1, 2, 3, 4
Next

(The _Default property can be identified by examining the Excel object library within the VBA Object Browser via highlighting the Range object ,right-clicking, and selecting "Show Hidden Members").

EDIT3: Collections

The code involving collections gets interesting and a little hairy :)

Debug.Print vbCrLf & "Case7 obj in col",
Set obj = New Collection: For i = 1 To 4: obj.Add Cells(i, i): Next
For Each obj In obj
    Debug.Print obj.Column,    ' 1 only ?
Next

Debug.Print vbCrLf & "Case8 var in col",
Set v = New Collection: For i = 1 To 4: v.Add Cells(i, i): Next
For Each v In v
    Debug.Print v.column,      ' nothing!
Next

This is where nothing more than a genuine bug has to be considered at play. When I first ran these two samples in the VBA debugger, they ran precisely as the OP offered in the initial question. Then, after a restart of the routine following a few tests, but then restoring the code to its original form (as shown here), the latter behavior arbitrarily started matching that of the object-based predecessor above it! Only after I stopped Excel, and restarted it, did the original behavior of the latter loop (printing nothing), return. There's really no way to explain that other than a compiler bug.

EDIT4 Reproducible behavior with Variants

After noting that I'd done something within the debugger to force the variant-based iteration through a Collection to loop at least once (as it had with the Object version), I finally found a code-reproducible way of changing the behavior

Consider this original code:

Dim v As Variant, vv As Variant

Set v = New Collection: For x = 1 To 4: v.Add Cells(x, x): Next x
'Set vv = v
For Each v In v
   Debug.Print v.Column
Next

This is essentially the OP's original case, and the ForEach loop terminates without a single iteration. Now, uncomment the 'Set vv=v' line, and re-run: now the For Each will iterate one time. I think there's no question that we've found some very (very!) subtle bug in Variant evaluation mechanism in the VB runtime; the arbitrary setting of another 'Variant' equal to the loop variable forces an evaluation that does not take place in the For Each evaluation - and I suspect that's tied to the fact that the Collection is represented within the Variant as a Variant/Object/Collection. Adding this bogus 'set' seems to force the issue and make the loop operate as the Object-based version does.

EDIT5: A final thought about iterations and collections

This will probably be my last edit to this answer, but one thing I had to force myself to be sure I recognized during the observation of odd loop behavior when a variables was used as the 'bound-variable-expression' and the limit expression was that, particularly when it comes to 'Variants', sometimes the behavior is induced by virtue of the iteration changing the contents of the 'bound-variable-expresssion.' That is, if you have:

Dim v as Variant
Dim vv as Variant
Set v = new Collection(): for x = 1 to 4: v.Add Cells(x,x):next
Set vv = v ' placeholder to make the loop "kinda" work
for each v in v
   'do something
Next

it is vital to remember (at least it was for me) to keep in mind that within the For Each, the 'bound-variable-expression' held in 'v' gets changed by virtue of the iteration. That is, when we start the loop, v holds a Collection, and the enumeration begins. But when that enumeration starts, the contents of v are now the product of the enumeration - in this case, a Range object (from the Cell). This behavior can be seen in the debugger, as you can observe 'v' go from Collection to Range; meaning that the next kick in the iteration returns whatever the enumeration context of the Range object would provide, not the 'Collection.'

This has been a great study and I appreciate the feedback. It's helped me understand things even better than I thought. Unless there are more comments or questions on this, I suspect this will be my last edit to the answer.

David W
  • 10,062
  • 34
  • 60
  • Thanks, I appreciate your effort :). However this tentative explanation seems to fail at case2, doesn't it?.Please check it back . – A.S.H Feb 22 '17 at 07:03
  • No - I tried to address that situation. Case 2 is an instance of the *control* variable being an explicit Integer which, in turn, forces the limit variable to be *coerced* to an integer, thus causing the loop to behave "correctly" (unfettered by the control variable change within the loop). – David W Feb 22 '17 at 12:46
  • Try case2 with a variant counter (`vv`) instead of an integer counter (`i`), it will exhibit the same :D – A.S.H Feb 22 '17 at 13:23
  • 1
    `Dim v, vv: v = 5: For vv = 1 To v: v = 2: Debug.Print vv: Next` ----> `1, 2, 3, 4, 5`. See, it did the same even with a variant control variable. The enigma is why it didnt do the same when `For v = 1 to v` ??? – A.S.H Feb 22 '17 at 13:25
  • Can't yet explain the former, but be careful on the latter as a reference - that's not a purely apples-to-apples comparison! :) The 'For v=1 to v' behavior is critically different in that the control and limit variables are the same! When that first iteration occurs, and *then* the limit condition is rechecked, the loop executes only the first time. That leaves your former example as still an edge case, and I regret I have no explanation at the moment! I do, however, believe the basic rules of coercion are in play - I just am not seeing why this instance is (obviously) an exception. – David W Feb 22 '17 at 14:26
  • I'm going to have to think about my answer. I just looked at the VBA language spec and found this little gem about loop controls: "The expressions , , and are evaluated once, in order, and prior to any of the following computations." That tends to explain why changing the end value within the loop does not affect the number of iterations, but it also argues against the idea that variants as end conditions are re-evaluated (unless it's an *accidental* re-evaluation because the runtime pushes the value - whatever it might be (eg variant) - to a stack). – David W Feb 22 '17 at 14:44
  • 1
    I have to give credit for your effort again. I think that I have to add [a link to that page](https://msdn.microsoft.com/en-us/library/ee157012.aspx) in my Question to facilitate fiurther investigation. To me, it appears clearly enough that the compiler did **not conform** to that semantics in the `For v = 1 to v` statement. – A.S.H Feb 22 '17 at 16:23
  • 2
    Shall we consider this as a compiler bug? – A.S.H Feb 22 '17 at 16:31
  • 1
    @A.S.H. You're welcome - enjoyed the research! I think it's *at least* a bug in the runtime. It could be that the compiler is doing exactly what it should be as far as pcode is concerned, but the *runtime* assessment of variants in loops may be the issue. Either way, smells like a bug to me. – David W Feb 22 '17 at 16:45
  • 1
    This seems to be some backward compatibility for legacy code bases, because `VBScript` exhibits the same quirky behavior. My guess is this must be left on purpose. – wqw Feb 23 '17 at 17:55
  • @wqw do you concur that this behavior violates the semantics? If so, I dont understand any reason to leave it that way. You cant leave a bug tbecause it *eventually* corrected previous programming bugs. Add to it that this construct is rarely used, expectedly by people who know what they're doing and therefore should avoid counting on such a bug. What do you think? – A.S.H Feb 24 '17 at 07:21
  • 3
    Yes, it's clearly a bug that persist even when VB6 code is compiled to executable. You can post a thread in [VBForums](http://www.vbforums.com/forumdisplay.php?1-Visual-Basic-6-and-Earlier) and probably user `The trick` can explain what's wrong by examining the (wrongly) generated x86 assembly. – wqw Feb 24 '17 at 10:21
  • @DavidW your answer is of good quality, but so far it did not address the `For Each` cases. I am interested in addressing those issues as well if you don't mind :) – A.S.H Mar 01 '17 at 21:11
  • I'll try but I have limited time this afternoon :) – David W Mar 01 '17 at 21:36
  • Thanks @DavidW for the addition. You surely deserved the bounty for the effort you have put in. Now that the time limit imposed by the bounty is behind us, there's no time pressure anymore, but I think that there remains a little something in order to mark the answer. I am hopeful that you're interested in taking it to good end. A few comments will follow. – A.S.H Mar 02 '17 at 01:46
  • Yes, I will try to flush out the answer more either tonight or tomorrow afternoon. Thanks for the bounty. – David W Mar 02 '17 at 01:52
  • You deserved it. Please no time pressure at all, only when you *feel like* doing it. :) – A.S.H Mar 02 '17 at 02:41
  • For now the spec states no rule about objects under iteration, it seems that only the implementation protects iterated arrays but completely neglects collections. This trouble is not the result of iterating a variable on itself, but anytime one dereferences a collection being iterated without holding another reference on it. It's like it's the responsibility of the programmer to care about the reference countingm which is not the "spirit" of BBA/VB6. – A.S.H Mar 02 '17 at 02:43
  • Please take a look to "Update 3" for more details. – A.S.H Mar 02 '17 at 02:59
  • The case of the range is pretty simple IMO, the range forks an implicit `.cells` collection when it's requested an iterator. Sometimes it forks a collection of rows or columns according to how the range was defined. Only remaining issue is indeed case 7, we only need to "judge it". – A.S.H Mar 02 '17 at 03:09
  • What do you mean by *"after a restart of the routine following a few tests but then restoring the code to its original form"*? Is that a reproducible scenario? – A.S.H Mar 02 '17 at 04:20
  • I just meant that I had tinkered with some of the source - setting the Variant to a numeric value before setting it to the Collection (among others), trying to induce something that might suggest why the variant became Empty and fell through without one iteration that last loop. I had gotten to a point where stopping and restarting the VBA debugger didn't change anything, but restarting Excel and reopening the VBA environment did. – David W Mar 02 '17 at 04:29
  • 1
    I even installed the VBA pcode disassembler, but it showed that the object-based and Variant-based lines parsed identically; nothing indicated that the general interpretation of the code varied by virtue of the variant versus an object. We'd need to see what happens at run-time at a debugger level (not just VBA) to really find out what's going on. – David W Mar 02 '17 at 04:29
  • I did find something reproducible - The Variant iteration would mimic the Object iteration (printing one time) *WHEN* I had the variable in the VBA debug "Watch" window. With a breakpoint on the "For Each v in v" line, I think the Watch forces an evaluation of the Variant that, somehow (perhaps an extra unboxing?) causes the collection to be evaluated rather than immediately returning Empty and falling through!! Suspect that's tied to the Variant holding an Object that's then a Collection... – David W Mar 02 '17 at 05:00
  • 1
    Yes I reproduced it. That's funny. – A.S.H Mar 02 '17 at 05:09
  • @A.S.H I was finally able to convert the watch window behavior into something reproducible in code. Clearly an issue in Variant evaluation somehow. Thought I'd amend the answer to share. – David W Mar 02 '17 at 14:49
  • With the `Set vv = v` it prints `1 2 3 4` for me. I had mentioned that in in update1, and my own conclusion is that holding an addintional ref on the collection prevented its garbage-collection during the iteration, which is bigus. In update 4 I also show a similar odd case that does not even involve a variable iterating on itself... The bug is clear for me now: VB permits the GC to destroy collections while they're being iterated. It should hold some hidden reference on them that lives throughout the loop. – A.S.H Mar 02 '17 at 16:14
  • Anyway, unfortunately we're not allowed to continue this discussion in public... So we may leave it a that for the comments section, but I will be glad to follow your edits "in silence" :) .. and mods thanks for your tolerance so far :D – A.S.H Mar 02 '17 at 16:18