Simple Things Pt.1: Values

Sometimes, simple things are just very effective abstractions that are very good at hiding the giant rabbit hole underneath.

Like an innocent little literal value for example:

Debug.Print 42

This 42 is just that, a plain integer value – isn’t it?

One could reasonably set out and write a parser that produces an abstract syntax tree (AST) here that would include some LiteralExpression node, that necessarily evaluates to 42.

But the node itself isn’t the value. In fact – perhaps surprisingly – it doesn’t even know what or where that value is. It knows where it is defined in the source code, but that’s not what I meant with where that value is.

So where does the 42 actually live? Where is it stored? And why does it even matter?

The answer is… not in the expression node: the node evaluates to a value, but is not that value.

For starters, the specifications dictate that 42 must be an Integer literal. Not a Long, not a Double – although a different value might yield either semantic data type. The grammar however, only distinguishes integer numbers from floating-point numbers: it’s token semantics specified in MS-VBAL that ultimately determines the semantic data type.

It also states very plainly that an integer value should be a 16-bit signed integer, and also defines such internal representations for each intrinsic data type: clearly we can’t just make everything a double and run with it, or we’ll almost certainly break something downstream.

Why is that?

More Than Values

If we stop and think about the type system in VBA, we quickly realize that we don’t just need values for literals, and that many other things evaluate to, or represent a value – like functions, parameters, and if we squint a little even a class instance (object) could be coerced into a value.

And that’s a problem if we model a semantic type system that embeds values directly, because then the assignment of a ByRef parameter can only behave as a value, not as a reference. Besides we also want to be able to represent values that exist and must be usable but aren’t defined anywhere in source code.

This is why RDCore introduced binding handles as an indirection layer between a VBTypedValue and the underlying binary value it represents.

Back to the literal 42 integer value: we have a value binding that yields the 42, and so the VBIntegerValue type knows it has a binding that can get the value, and it doesn’t need to know where that value actually is.

An integer parameter passed by reference would also be a VBIntegerValue, but its binding would be a reference binding rather than a value one, likely pointing to a symbol defined somewhere in the call stack: changing the value of this reference binding would necessarily affect the value everywhere it’s referenced, because its runtime value is associated to its declaration symbol within the program memory.

The RDCore semantic type system explicitly differentiates types and values, such that a VBIntegerValue represents a value of type VBInteger. This is slightly different from other models, where a value might be an instance of a given data type. Beyond that, a type might also be described by a value that represents a data type: VBTypeDescValue. This special meta-value is useful in a number of scenarios (notably TypeOf operators), and enables reflection-like capabilities and semantics that may or may not end up exposed at the language level, possibly through platform extensions.

This differentiation is necessary to correctly model arrays, UDT, and custom class types – that’s a whole other rabbit hole that deserves its own post, but for now this is what matters:

VBInteger:VBType
👇describes
VBIntegerValue:VBTypedValue
👇bound to
42:System.Int16

If an instance of a given VBType represented a value of that type, then we would need to generate the complex types on the fly, and that would be way more complicated than things need to be: we don’t need dynamic types when everything is statically defined – what we need is just a model that makes it work, and conceptually separating data types from typed values does exactly that.

Runtime Values

The RDCore SDK defines an entire type system that supports everything (and more!) VBA can do, using .net types to represent all underlying runtime values.

While several data types work pretty well as-is, others like Boolean, Currency, and Decimal don’t really map 1:1 and need a special type that correctly represents the value. For example an Integer value easily maps to a .net Int16, but mapping a .net Boolean to a VBA Boolean would introduce bit alignment issues (notably in UDT types), because it’s a 16-bit integer under the hood but a .net Boolean is only a single byte! Decimal is another, perhaps less consequential since it’s not declarable, but MS-VBAL specifies it as fitted across 14 bytes (3x Int32 +Int16), yet the corresponding .net type is 16 bytes wide (4x Int32); Variant is another data type that needs its own interop value, a 24-byte struct that can hold anything, value or reference.

The Debug.Print 42 instruction parses the 42 as a literal expression that evaluates to a VBIntegerValue that has a ValueBindingHandle that holds the managed Int16 value 42.

If it were Debug.Print “Hello, world!”, we would’ve had a VBStringValue with a ReferenceBindingHandle instead, holding a reference to a managed string holding the literal value. Concretely, this means a reference binding is really just a wrapper around a managed object; this makes it possible to box value types and use them as references in ByRef scenarios.

But it’s just a literal!

And it is. But to be useful, the semantic model must be able to tell a Variant from a Long from a Double, and a unified semantic type system that doesn’t care whether a value came from source code or is the result of an operation, and isn’t concerned about the internal memory representation of that value: that’s what the semantic model abstraction level does, and that’s where the AST belongs.

Constantly wrapping and unwrapping runtime values into semantic ones would be terribly inefficient, for run-time though. If it doesn’t sound so bad, try looping a million times into an array and adding one to each element, and the overhead should become obvious: it’ll interpret nodes all right, but something else must be able to actually run things without caring for the semantic layer.


Progress Status

As of this writing, the parser produces an AST containing all the expected nodes in an initial declaration pass: module directives and attributes, as well as everything in the declarations section, and procedure member signatures (including parameters) – but no executable statement nodes yet, although many of these have already been defined.

These nodes will be introduced in the AST alongside their respective evaluation semantics in an evaluation engine that’ll become the interpreter that will eventually evaluate nodes in break mode, while a separate execution engine will operate at a lower level, working directly with the underlying runtime types.

This will all be tested and benchmarked (and adjusted as needed) in due time, of course.

For now the development focus is turning to the LSP client/server SDK, which is what underpins cross-process communications across the entire RDCore platform. Once that’s in place, the language server can start implementing LSP handlers that an IDE can use, for everything a declaration pass already unlocks – but that work will be ticketed and not completed right away (not by me anyway), because then focus will have to shift to the semantic evaluation engine and the statement nodes, because GoTo and GoSub/Return, not to mention error states, are an interesting twist on what would otherwise be a rather straightforward AST traversal.

What’s Cooking for Rubberduck 2.5.x

If you’ve been following the project all along, this isn’t going to be news, but we kind of missed the v2.4.2 milestone we were slated to release back in April, and here we are with our [next] branch (“pre-release” builds) being a whopping 580+ commits ahead of [master] (“green-release” builds). These commits change a lot of things… so much that v2.4.1 will end up being the only “green-release” of the 2.4.x release cycle, and we’ve decided next release will have to be 2.5.0 – but what is it specifically that warrants such delays and the +1 on the minor version number?

ITypeLib

Perhaps the most important set of changes since v1.2 where we introduced an ANTLR-generated parser, this internal API was actually introduced last year, but until relatively recently it was only used to make the unit testing feature fully host-agnostic (i.e. unit testing works in every host application since) and to retrieve project-level precompiler constants, which closed an otherwise desperate gaping hole in Rubberduck’s understanding of the code that’s in the editor. We are also using it to retrieve and manipulate project references, and possibly in other places I don’t recall at the moment.

But this internal API unlocks much more power than that, and until very recently we hadn’t really started tapping into it. During the v2.5.x cycle, we’ll be using it to instantly populate the Code Explorer toolwindow with tree nodes that still drill down to member level – of course Rubberduck won’t know where a procedure is referenced or be able to refactor anything until parsing has actually occurred, but the project should be instantly navigatable regardless.

We have already begun leveraging this ITypeLib API to augment resolver capabilities, notably with regards to member and module attributes: we can now read most of their values without needing to export anything to any temp file.

So what this API does, is that it taps into VBA/VB6’s internal storage: you may not realize, but compiling your VBA code, internally, creates a COM type library. With this API we can safely query this type library and model user code modules and their members just like any other COM type library, e.g. project references. This means Rubberduck is be able to know what interfaces a document module implements – in other words, when we fully leverage this API we will be able to tell that Sheet1 is a Worksheet and that ThisWorkbook is a Workbook… which means a library-specific inspection like “sheet accessed using string” can now work exactly as intended. We already correctly identify event handler procedures in document modules thanks to these new capabilities; it might seem simple on the surface, but knowing that Sheet1 is a Worksheet and that this Worksheet_Change procedure is handling the Change event of that Worksheet interface, requires looking well beyond the code… and a side-effect of this, is that “procedure not used” no longer fires inspection results for them (the inspection already ignored event handler procedures… all it needed was for the resolver to recognize event handlers in document modules as such).

Default Member Resolution

Once again, a tremendous amount of effort went into augmenting resolver capabilities. This piece of the puzzle is the cornerstone that makes everything else fall into place: if we’re able to issue an inspection result when a variable is never referenced, it’s because the resolver processed all the parse trees and located no references to that variable. But it’s much more than just unused variables: the resolver is the literal central nervous system of Rubberduck – if the resolver doesn’t work well, everything else falls apart.

Except, resolving VBA code correctly is hard. We have an inspection that means to flag missing Set keywords, and until recently it would fire false positives whenever implicit default member calls were involved. What’s that? Picture this code:

Range("A1") = Range("B1")

What’s really happening is this:

Global.Range("A1").[_Default] = Global.Range("A1").[_Default]

But in order to know that, Rubberduck needs to know much more about the code than just what the code is saying: it needs to know that Range is an implicitly-qualified member call on Global (or is it? what if that very same code is in the code-behind of the Sheet3 module?), and that it has a default member that’s the target of this assignment on the left-hand side, and the provider of a value on the right-hand side; it needs to know that this default member yields a Variant (and not another object, which may have its own default member, which might yield an object, which may have a default member, which… so yeah, recursive resolution). And once it knows all that, it can warn you about implicit default member assignments, and soon about any implicit default member call – and help you make them explicit!

Bang notation now also resolves correctly. You write this:

Dim rs As ADODB.Recordset
Set rs = conn.Execute(procName)
Debug.Print rs!Field1

Rubberduck sees this:

Dim rs As ADODB.Recordset
Set rs = conn.Execute(procName)
Debug.Print rs.Fields.Item("Field1").Value

…and this means we’ll soon be able to offer quickfixes/refactorings that turn one notation into the other, and vice-versa.

This is where Rubberduck’s resolver is at, and I need to pinch myself to believe just how crazy wicked awesome it’s becoming – it’s not perfect, but I’m positive, and I’ll repeat this even though it’s been the case for a very long while, but no other VBIDE add-in understands VBA as deeply as Rubberduck.

Moq.Mock<T>

Rubberduck uses the Moq framework for its thousands of unit tests. With it, we’re able to inject “mock” implementations of any abstract dependency: unit testing isn’t complete without a mocking framework, and there’s none for VBA, …for now.

The amount of work involved is astounding, but the important and hard parts are working and we’re just a few road-bumps away from having a COM-visible Moq wrapper API that VBA code can consume to mock any class – your Class1 module or ISomething interface, a ListObject Excel table, any Word.Range, ADODB.Connection, or Scripting.FileSystemObject. This is a massive and complete game-changer that takes unit testing VBA code to a whole new level of credibility.


Timeline

To be honest, there isn’t really any timeline on the table: the 2.5.0 green-release will happen when it does. In the meantime you’ll want to keep an eye on pre-release builds: in the next couple of weeks we’ll be polishing the new features, reviewing what few inspection false positives remain, address a number of prioritized bugs (the all-or-nothing collapsing/expanding of grouping grids, for one), and then we’ll be ready.

There’s plenty of work for all levels and skills, you’re welcome to help us!