Sunday, August 30, 2009

Simple Groovy mistake had me wondering what the heck was going on!

While 'scatching my programming itch' late one night last week, I made a small mistake which didn't present itself until runtime.   When it did occur, it made we wonder what the heck was happening.  After fixing my mistake,  I just had to figure out what was really happening.

Minor background for readers new to Groovy:

Groovy provides language support to make the routine tasks on collection types easier than Java programmers are used to.  For example, creating a map is as easy as:
def map = [key1:valueX, key2:valueY]
which creates a map with two entries: key1 maps to valueX and key2 maps to valueY. The fun starts with the many ways of accessing the maps data. The following are all valid ways of retrieving valueX from the map:
def value = map.get('key1')
def value = map['key1']
def value = map.key1

The first example looks just like Java. The second and third examples are provided by the Groovy language. Now, on to the mistake.

Can you guess what happens?
I have created a very small subset of the code to show the bug.  The actual code resides in a Grails controller method that gets invoked on a form submission.   In the submitted form, I use DOM scripting to dynamically create multiple line items for a transaction.

Take a look at the excerpt below and see if you can tell what happens when it executes.

def params = [itemCount:'3']  // this was really form data submitted
for (int i=0; i<params.itemCount; i++)
    def lineItem = new LineItem()
    bindData(lineItem, params["lineItem["+i+"]"])
    transactionInstance.lineItem[i] = lineItem

Anyone see the error?
The for loop looped about 10-12 times and then stop even though there were only 3 lineItems submitted on the form.  Other times, the loop ran forever.  The problem: the value of the itemCount entry is a String and not a Integer, which resulted in a strange comparison (more on that later).   The fix was easy, convert the String to an Integer.

def count = params.itemCount.toInteger()
for (int i=0; i<count; i++)

But why did the loop behave the way it did?

Operator overload for < (less than)
The next step is to figure out what Groovy's doing.   The operator <  is basically doing a comparison, so what happens when you try either of the following:

The answer is, both throw a ClassCastException indicating that an Integer can not be cast to a String and vice versa.  So how is Groovy coercing this comparison?  Time to decompile the code to get to the bottom of this. 

Decompile the code!
Below is a snippet of the decompiled code where you can see it calls the ScriptBytecodeAdapter compareLessThan method with both of the arguments as Objects, so we get an identity comparison!

  for(Integer i = $const$0; ScriptBytecodeAdapter.compareLessThan(i, acallsite[1].callGetProperty(params));)


  1. A mistake:

    def value = map.get(key1)
    def value = map[key1]
    def value = map.key1

    are not equivalent. These are:

    def value = map.get('key1')
    def value = map['key1']
    def value = map.key1


    def value = map.get(key1)
    def value = map[key1]
    def value = map."$key1"

  2. Thanks Chris, I have fixed the entriies. I added that section as 'background' without checking it closely. Mostly added to provide supporting evidence for the error I made.